package main import ( "context" "errors" "fmt" "io/fs" "log" "net/http" "os" "path" "strings" "time" "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 }