aboutsummaryrefslogblamecommitdiff
path: root/main.go
blob: 6bf7f7a85fd78b637eabcb8474a77cdc0f7f7820 (plain) (tree)
1
2
3
4
5
6
7
8
9


            



                 
             
                  



                 


                                 
                                    





                                                      
 


                                          

                                        

 






                                             
             





                                                                                              

                               
                                       







                                                                    







                                                                                      
                                                             































































                                                                                                                               













                                                                                                                                
                                                      












                                   
                      






                                                                                    
                                                  


                                                                                                                
                                       


                                                                      
                                                      


                                                                               
                                                   


                                                                                
                                       


                        

                            

                    
                   


                                                                           



                                                     




                                       
                      


                                     

















                                                                                                  


                                                         







                                         

 

                                                          


                                                                     



                                         









                                                                                                                                                                                                                       







                                                                                 


                                                                     














                                                                  


                                                                                             


















                                                                                


                                                   
                                             




                        

                               


                                                                           











                                                             


                                                                                































                                                                                        


                                                                                                                 


                             
 


                                                             


                                     




                                            


                                    
                          


                                          




                                               


                                           
                                  


                                    
                                


                                         
                  
 
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 EnvOrDefault(key, def string) string {
	if val, ok := os.LookupEnv(key); ok {
		return val
	}
	return def
}

func main() {
	pathPrefix := EnvOrDefault("BAGAGE_WEBDAV_PREFIX", "/webdav")
	LdapServer := EnvOrDefault("BAGAGE_LDAP_ENDPOINT", "127.0.0.1:1389")
	UserBaseDN := EnvOrDefault("BAGAGE_LDAP_USER_BASE_DN", "ou=users,dc=deuxfleurs,dc=fr")
	UserNameAttr := EnvOrDefault("BAGAGE_LDAP_USERNAME_ATTR", "cn")
	Endpoint := EnvOrDefault("BAGAGE_S3_ENDPOINT", "garage.deuxfleurs.fr")
	UseSSL := EnvOrDefault("BAGAGE_S3_SSL", "true") == "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", LdapServer)
		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
  obj  *minio.Object
  stat *GarageStat
	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
  stat, err := NewGarageStat(ctx, path)
  if err != nil {
    return nil, err
  }
  gf.stat = stat
	return gf, nil
}

func (gf *GarageFile) Close() error {
  if gf.obj == nil {
    return nil
  }
  err := gf.obj.Close()
  gf.obj = nil
  return err
}

func (gf *GarageFile) loadObject() error {
  if gf.obj == nil {
    log.Println("Called GetObject on", gf.path)
    obj, err := gf.mc.GetObject(gf.ctx, gf.stat.bucket, gf.stat.obj.Key, minio.GetObjectOptions{})
    if err != nil {
      return err
    }
    gf.obj = obj
  }
  return nil
}

func (gf *GarageFile) Read(p []byte) (n int, err error) {
	if gf.stat.Mode().IsDir() {
		return 0, os.ErrInvalid
	}
  if err := gf.loadObject(); err != nil {
    return 0, err
  }

  return gf.obj.Read(p)
}

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) {
  if err := gf.loadObject(); err != nil {
    return 0, err
  }
  return gf.obj.Seek(offset, whence)
}

/*
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
}