aboutsummaryrefslogblamecommitdiff
path: root/main.go
blob: 89fc88a44478f4901c4dc749b650301a5adb97b5 (plain) (tree)












































































































































































































































































































































































                                                                                                                                                                                                                       
package main

import (
  "context"
	"log"
  "fmt"
  "os"
  "io/fs"
  "time"
  "errors"
	"net/http"
  "strings"
  "path"

	"golang.org/x/net/webdav"

  "github.com/go-ldap/ldap/v3"

	"github.com/minio/minio-go/v7"
	"github.com/minio/minio-go/v7/pkg/credentials"
)

type bagageCtxKey string
const garageEntry = bagageCtxKey("garage")

type garageCtx struct {
	MC *minio.Client
  StatCache map[string]*GarageStat
}

func main() {
  pathPrefix := "/webdav"
  UserBaseDN := "ou=users,dc=deuxfleurs,dc=fr"
  UserNameAttr := "cn"
  Endpoint := "garage.deuxfleurs.fr"
  UseSSL := true

	srv := &webdav.Handler{
    Prefix: pathPrefix,
		FileSystem: NewGarageFS(),
		LockSystem: webdav.NewMemLS(),
		Logger: func(r *http.Request, err error) {
			log.Printf("WEBDAV: %#s, ERROR: %v", r, err)
		},
	}

	//http.Handle("/", srv)
  http.HandleFunc(pathPrefix + "/", func(w http.ResponseWriter, r *http.Request) {
    username, password, ok := r.BasicAuth()

    if !ok {
      NotAuthorized(w,r)
      return
    }

    ldapSock, err := ldap.Dial("tcp", "127.0.0.1:1389")
    if err != nil {
	    log.Println(err)
      InternalError(w,r)
      return
    }
    defer ldapSock.Close()

    // Check credential
    userDn := fmt.Sprintf("%s=%s,%s", UserNameAttr, username, UserBaseDN)
    err = ldapSock.Bind(userDn, password)
    if err != nil {
      log.Println(err)
      NotAuthorized(w,r)
      return
    }

    // Get S3 creds garage_s3_access_key garage_s3_secret_key
    searchRequest := ldap.NewSearchRequest(
      userDn,
      ldap.ScopeBaseObject,
      ldap.NeverDerefAliases,
      0,
      0,
      false,
      "(objectClass=*)",
      []string{"garage_s3_access_key", "garage_s3_secret_key"},
      nil)

    sr, err := ldapSock.Search(searchRequest)
    if err != nil {
	    log.Println(err)
      InternalError(w,r)
      return
    }

    if len(sr.Entries) != 1 {
      log.Println("Wrong number of LDAP entries, expected 1, got", len(sr.Entries))
      InternalError(w,r)
      return
    }

    access_key := sr.Entries[0].GetAttributeValue("garage_s3_access_key")
    secret_key := sr.Entries[0].GetAttributeValue("garage_s3_secret_key")

    if access_key == "" || secret_key == "" {
      log.Println("Either access key or secret key is missing in LDAP for ", userDn)
      InternalError(w,r)
      return
    }

	  mc, err := minio.New(Endpoint, &minio.Options{
		  Creds:  credentials.NewStaticV4(access_key, secret_key, ""),
		  Secure: UseSSL,
    })
	  if err != nil {
      log.Println(err)
      InternalError(w,r)
      return
	  }

    nctx := context.WithValue(r.Context(), garageEntry, garageCtx{MC: mc, StatCache: make(map[string]*GarageStat)})
    srv.ServeHTTP(w, r.WithContext(nctx))
    return
  })

	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatalf("Error with WebDAV server: %v", err)
	}
}

func NotAuthorized(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("WWW-Authenticate", `Basic realm="Pour accéder à Bagage, veuillez entrer vos identifiants Deuxfleurs"`)
	w.WriteHeader(401)
	w.Write([]byte("401 Unauthorized\n"))
}

func InternalError(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(500)
  w.Write([]byte("500 Internal Server Error\n"))
}

/*
  /////// Select Action
  If no slash or one trailing slash
    return ListBuckets
  Else
    obj := ListObjects
    If obj.Length == 1
      return GetObject
    Else
      return obj
*/
type GarageFS struct {}

func NewGarageFS() *GarageFS {
	grg := new(GarageFS)
	return grg
}

func (s *GarageFS) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
  return errors.New("Not implemented Mkdir")
}

func (s *GarageFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
  return NewGarageFile(ctx, name)
}

func (s *GarageFS) RemoveAll(ctx context.Context, name string) error {
  return errors.New("Not implemented RemoveAll")
}

func (s *GarageFS) Rename(ctx context.Context, oldName, newName string) error {
  return errors.New("Not implemented Rename")
}

func (s *GarageFS) Stat(ctx context.Context, name string) (os.FileInfo, error) {
  return NewGarageStat(ctx, name)
}

type GarageFile struct {
  ctx context.Context
  mc *minio.Client
  path string
}

func NewGarageFile(ctx context.Context, path string) (webdav.File, error) {
  gf := new(GarageFile)
  gf.ctx = ctx
  gf.mc = ctx.Value(garageEntry).(garageCtx).MC
  gf.path = path
  return gf, nil
}

func (gf *GarageFile) Close() error {
  return errors.New("not implemented Close")
}

func (gf *GarageFile) Read(p []byte) (n int, err error) {
  return 0, errors.New("not implemented Read")
}

func (gf *GarageFile)	Write(p []byte) (n int, err error) {
  return 0, errors.New("not implemented Write")
}

func (gf *GarageFile) Seek(offset int64, whence int) (int64, error) {
  return 0, errors.New("not implemented Seek")
}

/*
ReadDir reads the contents of the directory associated with the file f and returns a slice of DirEntry values in directory order. Subsequent calls on the same file will yield later DirEntry records in the directory.

If n > 0, ReadDir returns at most n DirEntry records. In this case, if ReadDir returns an empty slice, it will return an error explaining why. At the end of a directory, the error is io.EOF.

If n <= 0, ReadDir returns all the DirEntry records remaining in the directory. When it succeeds, it returns a nil error (not io.EOF).
*/
func (gf *GarageFile) Readdir(count int) ([]fs.FileInfo, error) {
  log.Println("Call Readdir with count", count)

  if gf.path == "/" {
    return gf.readDirRoot(count)
  } else {
    exploded_path := strings.SplitN(gf.path, "/", 3)
    return gf.readDirChild(count, exploded_path[1], exploded_path[2])
  }
}

func (gf *GarageFile) readDirRoot(count int) ([]fs.FileInfo, error) {
  buckets, err := gf.mc.ListBuckets(gf.ctx)
  if err != nil {
    return nil, err
  }

  entries := make([]fs.FileInfo, 0, len(buckets))
  for _, bucket := range buckets {
    ngf, err := NewGarageStat(gf.ctx, "/"+bucket.Name)
    if err != nil {
      return nil, err
    }
    entries = append(entries, ngf)
  }

  return entries, nil
}

func (gf *GarageFile) readDirChild(count int, bucket, prefix string) ([]fs.FileInfo, error) {
  log.Println("call ListObjects with", bucket, prefix)
  objs_info := gf.mc.ListObjects(gf.ctx, bucket, minio.ListObjectsOptions{
    Prefix: prefix,
    Recursive: false,
  })

  entries := make([]fs.FileInfo,0)
  for object := range objs_info {
    if object.Err != nil {
        return nil, object.Err
    }
    ngf, err := NewGarageStatFromObjectInfo(gf.ctx, bucket, object)
    if err != nil {
      return nil, err
    }
    entries = append(entries, ngf)
  }

  return entries, nil
}

func (gf *GarageFile) Stat() (fs.FileInfo, error) {
  return NewGarageStat(gf.ctx, gf.path)
}

/* Implements */
// StatObject???
type GarageStat struct {
  obj minio.ObjectInfo
  bucket string
}

func NewGarageStat(ctx context.Context, path string) (*GarageStat, error) {
  cache := ctx.Value(garageEntry).(garageCtx).StatCache
  if entry, ok := cache[path]; ok {
    return entry, nil
  }

  gs, err := newGarageStatFresh(ctx, path)
  if err != nil {
    return nil, err
  }

  cache[path] = gs
  return gs, nil
}

func newGarageStatFresh(ctx context.Context, path string) (*GarageStat, error) {
  mc := ctx.Value(garageEntry).(garageCtx).MC
  gs := new(GarageStat)
  gs.bucket = "/"
  gs.obj = minio.ObjectInfo{}

  exploded_path := strings.SplitN(path, "/", 3)

  // Check if we can extract the bucket name
  if len(exploded_path) < 2 {
    return gs, nil
  }
  gs.bucket = exploded_path[1]

  // Check if we can extract the prefix
  if len(exploded_path) < 3 || exploded_path[2] == "" {
    return gs, nil
  }
  gs.obj.Key = exploded_path[2]

  // Check if this is a file or a folder
  log.Println("call StatObject with", gs.bucket, gs.obj.Key)
  obj, err := mc.StatObject(ctx, gs.bucket, gs.obj.Key, minio.StatObjectOptions{})
  if e, ok := err.(minio.ErrorResponse); ok && e.Code == "NoSuchKey" {
    return gs, nil
  }
  if err != nil {
    return nil, err
  }

  // If it is a file, assign its data
  gs.obj = obj
  return gs, nil
}

func NewGarageStatFromObjectInfo(ctx context.Context, bucket string, obj minio.ObjectInfo) (*GarageStat, error) {
  gs := new(GarageStat)
  gs.bucket = bucket
  gs.obj = obj

  cache := ctx.Value(garageEntry).(garageCtx).StatCache
  cache[path.Join("/", bucket, obj.Key)] = gs
  return gs, nil
}

func (gs *GarageStat) Name() string {
  if gs.obj.Key != "" {
    return path.Base(gs.obj.Key)
  } else {
    return gs.bucket
  }
}

func (gs *GarageStat) Size() int64 {
  return gs.obj.Size
}

func (gs *GarageStat) Mode() fs.FileMode {
  if gs.obj.ETag == "" {
    return fs.ModeDir | fs.ModePerm
  } else {
    return fs.ModePerm
  }
}

func (gs *GarageStat) ModTime() time.Time {
  return gs.obj.LastModified
}

func (gs *GarageStat) IsDir() bool {
  return gs.Mode().IsDir()
}

func (gs *GarageStat) Sys() interface{} {
  return nil
}