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


            


                 
            
               
             
              
                  



                 


                                 
                                    





                                                      
 


                                          

                                        

 






                                             
             

                                                                 





                                                                                              

                               
                                       


                                                          



                                                                                     


                  







                                                                                      
                                                             































































                                                                                                                               
 
                                                                    











                                                                                                                                
                                                      












                                   
                      






                                                                                    
                                                  


                                                                                                                
                                                          
                                        
                                       


                                                                      
                                                      


                                                                               
                                                   


                                                                                
                                                      
                                       


                        






                             


                                                                           

                             
                  
                                                     
                                 
                      


                                     

























                                                                                                                                  


                                          







                                                                                                          


                                                         




                                                                                                                      
         
 
                             

 
                                                          




















                                                                                                                                                             


                                                                     






                                               









                                                                                                                                                                                                                       


                                                                                                                   
 
                                  

                                            
                                             
         


                                                                     






                                                       
                                                                                  







                                                                  

 
                                                                      



                                                             
 
                                                                                        
                                  







                                              
                                                                                                              
                                                                                       






                                              


                                                   
                                                                
                                                          




                        


                             

 



                                                                                   
                                                             

                                                   



                                         



                                                                  


                                                                              






                                                                      




                               

 



                                                                                                                 
                             

                                               
 



                                                             
 



                                                                           


                                                             
         
 




                                            
         
 

                                                                                                         

         

                                
                      

 



                                                             
 
































                                                                                     


                                     



                                           
                
                                             
         


                                    
                          


                                          
                                    
                                  

                                               
         


                                           
                                  


                                    
                                      


                                         
                  
 
















































                                                                           
package main

import (
	"context"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"log"
	"mime"
	"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() {
	log.Println("=== Starting Bagage ===")
	HttpListen := EnvOrDefault("BAGAGE_HTTP_LISTEN", ":8080")
	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("INFO: %s %s %s\n", r.RemoteAddr, r.Method, r.URL)
			if err != nil {
				log.Printf("ERR: %v", err)
			}
		},
	}

	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(HttpListen, 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) {
	log.Println("Stat from GarageFS.OpenFile()", name)
	NewGarageStatFromFile(ctx, name)
	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) {
	log.Println("Stat from GarageFS.Stat()", name)
	return NewGarageStat(ctx, name)
}

type GarageFile struct {
	ctx   context.Context
	mc    *minio.Client
	obj   *minio.Object
	objw  *io.PipeWriter
	donew chan error
	pos   int64
	path  S3Path
}

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

func (gf *GarageFile) Close() error {
	err := make([]error, 0)

	if gf.obj != nil {
		err = append(err, gf.obj.Close())
		gf.obj = nil
	}

	if gf.objw != nil {
		// wait that minio completes its transfers in background
		err = append(err, gf.objw.Close())
		err = append(err, <-gf.donew)
		gf.donew = nil
		gf.objw = nil
	}

	count := 0
	for _, e := range err {
		if e != nil {
			count++
			log.Println(e)
		}
	}
	if count > 0 {
		return errors.New(fmt.Sprintf("%d errors when closing this WebDAV File. Read previous logs to know more.", count))
	}
	return nil
}

func (gf *GarageFile) loadObject() error {
	if gf.obj == nil {
		obj, err := gf.mc.GetObject(gf.ctx, gf.path.bucket, gf.path.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() & OBJECT == 0 { /* @FIXME Ideally we would check against OBJECT but we need a non OPAQUE_KEY */
	//	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) {
	/*if gf.path.class != OBJECT {
		return 0, os.ErrInvalid
	}*/

	if gf.objw == nil {
		if gf.pos != 0 {
			return 0, errors.New("writing with an offset is not implemented")
		}

		r, w := io.Pipe()
		gf.donew = make(chan error, 1)
		gf.objw = w

		contentType := mime.TypeByExtension(path.Ext(gf.path.key))
		go func() {
			_, err := gf.mc.PutObject(context.Background(), gf.path.bucket, gf.path.key, r, -1, minio.PutObjectOptions{ContentType: contentType})
			gf.donew <- err
		}()
	}

	return gf.objw.Write(p)
}

func (gf *GarageFile) Seek(offset int64, whence int) (int64, error) {
	if err := gf.loadObject(); err != nil {
		return 0, err
	}

	pos, err := gf.obj.Seek(offset, whence)
	gf.pos += pos
	return pos, err
}

/*
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) {
	if count > 0 {
		return nil, errors.New("returning a limited number of directory entry is not supported in readdir")
	}

	if gf.path.class == ROOT {
		return gf.readDirRoot(count)
	} else {
		return gf.readDirChild(count)
	}
}

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 {
		log.Println("Stat from GarageFile.readDirRoot()", "/"+bucket.Name)
		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) ([]fs.FileInfo, error) {
	prefix := gf.path.key
	if len(prefix) > 0 && prefix[len(prefix)-1:] != "/" {
		prefix = prefix + "/"
	}

	objs_info := gf.mc.ListObjects(gf.ctx, gf.path.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
		}
		log.Println("Stat from GarageFile.readDirChild()", path.Join("/", gf.path.bucket, object.Key))
		ngf, err := NewGarageStatFromObjectInfo(gf.ctx, gf.path.bucket, object)
		if err != nil {
			return nil, err
		}
		entries = append(entries, ngf)
	}

	return entries, nil
}

func (gf *GarageFile) Stat() (fs.FileInfo, error) {
	log.Println("Stat from GarageFile.Stat()", gf.path.path)
	return NewGarageStatFromFile(gf.ctx, gf.path.path)
}

/* Implements */
// StatObject???
type GarageStat struct {
	obj  minio.ObjectInfo
	ctx  context.Context
	path S3Path
}

/*
 * Stat a path
 */
func NewGarageStatFromFile(ctx context.Context, path string) (*GarageStat, error) {
	cache := ctx.Value(garageEntry).(garageCtx).StatCache

	// Maybe this file is already in our cache?
	if entry, ok := cache[path]; ok {
		return entry, nil
	}

	// Create a placeholder in case we are creating the object
	gs := new(GarageStat)
	gs.ctx = ctx
	gs.path = NewS3Path(path)
	if gs.path.class == OPAQUE_KEY {
		gs.path.class = OBJECT // known because called from GarageFile
	}
	gs.obj.Key = gs.path.key
	gs.obj.LastModified = time.Now()

	// Maybe this file exists in garage?
	err := gs.Refresh()
	if err != nil && !os.IsNotExist(err) {
		// There is an error and this is not a 404, report it.
		return nil, err
	}

	cache[path] = gs
	return gs, nil
}

/*
 * Stat a path knowing its ObjectInfo
 */
func NewGarageStatFromObjectInfo(ctx context.Context, bucket string, obj minio.ObjectInfo) (*GarageStat, error) {
	gs := new(GarageStat)
	gs.path = NewTrustedS3Path(bucket, obj)
	gs.obj = obj

	cache := ctx.Value(garageEntry).(garageCtx).StatCache
	cache[gs.path.path] = gs
	return gs, nil
}

/*
 * Stat a path without additional information
 */
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 := new(GarageStat)
	gs.ctx = ctx
	gs.path = NewS3Path(path)
	if err := gs.Refresh(); err != nil {
		return nil, err
	}

	if gs.path.class&OPAQUE_KEY != 0 {
		return nil, errors.New("Failed to precisely determine the key type, this a logic error.")
	}

	cache[path] = gs
	cache[gs.path.path] = gs
	return gs, nil
}

func (gs *GarageStat) Refresh() error {
	if gs.path.class == ROOT || gs.path.class == BUCKET {
		return nil
	}

	mc := gs.ctx.Value(garageEntry).(garageCtx).MC

	// Compute the prefix to have the desired behaviour for our stat logic
	prefix := gs.path.key
	if prefix[len(prefix)-1:] == "/" {
		prefix = prefix[:len(prefix)-1]
	}

	// Get info and check if the key exists
	objs_info := mc.ListObjects(gs.ctx, gs.path.bucket, minio.ListObjectsOptions{
		Prefix:    prefix,
		Recursive: false,
	})

	found := false
	for object := range objs_info {
		if object.Err != nil {
			return object.Err
		}

		if object.Key == prefix || object.Key == prefix+"/" {
			gs.obj = object
			gs.path = NewTrustedS3Path(gs.path.bucket, object)
			found = true
			break
		}
	}

	if !found {
		return fs.ErrNotExist
	}

	return nil
}

func (gs *GarageStat) Name() string {
	if gs.path.class == ROOT {
		return "/"
	} else if gs.path.class == BUCKET {
		return gs.path.bucket
	} else {
		return path.Base(gs.path.key)
	}
}

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

func (gs *GarageStat) Mode() fs.FileMode {
	if gs.path.class == OBJECT {
		return fs.ModePerm
	} else {
		return fs.ModeDir | fs.ModePerm
	}
}

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

func (gs *GarageStat) IsDir() bool {
	return gs.path.class != OBJECT
}

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

type S3Class int

const (
	ROOT S3Class = 1 << iota
	BUCKET
	COMMON_PREFIX
	OBJECT
	OPAQUE_KEY

	KEY = COMMON_PREFIX | OBJECT | OPAQUE_KEY
)

type S3Path struct {
	path   string
	class  S3Class
	bucket string
	key    string
}

func NewS3Path(path string) S3Path {
	exploded_path := strings.SplitN(path, "/", 3)

	// If there is no bucket name (eg. "/")
	if len(exploded_path) < 2 || exploded_path[1] == "" {
		return S3Path{path, ROOT, "", ""}
	}

	// If there is no key
	if len(exploded_path) < 3 || exploded_path[2] == "" {
		return S3Path{path, BUCKET, exploded_path[1], ""}
	}

	return S3Path{path, OPAQUE_KEY, exploded_path[1], exploded_path[2]}
}

func NewTrustedS3Path(bucket string, obj minio.ObjectInfo) S3Path {
	cl := OBJECT
	if obj.Key[len(obj.Key)-1:] == "/" {
		cl = COMMON_PREFIX
	}

	return S3Path{
		path:   path.Join("/", bucket, obj.Key),
		bucket: bucket,
		key:    obj.Key,
		class:  cl,
	}
}