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
}