diff --git a/internal/common/path.go b/internal/common/path.go new file mode 100644 index 0000000..6abb0a5 --- /dev/null +++ b/internal/common/path.go @@ -0,0 +1,19 @@ +package common + +import ( + "os" +) + +// GetProjectRoot returns the root directory of the project, e.g. "/root/project/GoWebDAV/" +func GetProjectRoot() string { + wd, _ := os.Getwd() + // look for parents until we find a directory with a .git folder + for { + if _, err := os.Stat(wd + "/.git"); err == nil { + break + } + wd = wd[:len(wd)-1] + } + + return wd +} \ No newline at end of file diff --git a/internal/server/config.go b/internal/server/config.go deleted file mode 100644 index a86bc12..0000000 --- a/internal/server/config.go +++ /dev/null @@ -1,2 +0,0 @@ -package server - diff --git a/internal/server/handler.go b/internal/server/handler.go new file mode 100644 index 0000000..8f01092 --- /dev/null +++ b/internal/server/handler.go @@ -0,0 +1,125 @@ +package server + +import ( + "errors" + "net/http" + "os" + "strings" + + "golang.org/x/net/webdav" +) + +type HandlerConfig struct { + Prefix string + PathDir string + Username string + Password string + ReadOnly bool +} + +type handler struct { + handler *webdav.Handler + + prefix string // URL prefix + dirPath string // File system directory + + username string // HTTP Basic Auth Username. if empty, no auth + password string // HTTP Basic Auth Password + + readOnly bool // if true, only allow GET, OPTIONS, PROPFIND, HEAD +} + +func NewHandler(cfg *HandlerConfig) *handler { + return &handler{ + handler: &webdav.Handler{ + FileSystem: webdav.Dir(cfg.PathDir), + LockSystem: webdav.NewMemLS(), + Prefix: cfg.Prefix, + }, + prefix: cfg.Prefix, + dirPath: cfg.PathDir, + username: cfg.Username, + password: cfg.Password, + readOnly: cfg.ReadOnly, + } +} + +func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + enableBasicAuth := h.username != "" + if enableBasicAuth { + username, password, ok := req.BasicAuth() + // log.Debug().Str("username", username).Str("password", password).Bool("ok", ok).Msg("BasicAuth Request") + if !ok { + w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) + w.WriteHeader(http.StatusUnauthorized) + return + } + + if username != h.username || password != h.password { + w.WriteHeader(http.StatusUnauthorized) + return + } + } + + if h.readOnly { + allowedMethods := map[string]bool{ + "GET": true, + "OPTIONS": true, + "PROPFIND": true, + "HEAD": true, + } + if !allowedMethods[req.Method] { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + } + + h.handler.ServeHTTP(w, req) +} + +func checkHandlerConfig(cfg *HandlerConfig) error { + // prefix must start with "/", contains only one "/" + if cfg.Prefix == "" || cfg.Prefix[0] != '/' { + return errors.New("prefix must start with /") + } + + // prefix must contain only one "/" + if strings.Count(cfg.Prefix, "/") != 1 { + return errors.New("prefix must contain only one /") + } + + // prefix must not contain not allowed characters + notAllowedChars := []string{"?", "%", "#", "&"} + for _, char := range notAllowedChars { + if strings.Contains(cfg.Prefix, char) { + return errors.New("prefix must not contain " + char) + } + } + + // pathDir must be a valid directory + if fileinfo, err := os.Stat(cfg.PathDir); err != nil { + return err + } else if !fileinfo.IsDir() { + return errors.New("pathDir must be a directory") + } + + return nil +} + +func checkHandlerConfigs(cfgs []*HandlerConfig) error { + for _, cfg := range cfgs { + if err := checkHandlerConfig(cfg); err != nil { + return err + } + } + + prefixs := make(map[string]bool) + for _, cfg := range cfgs { + if _, ok := prefixs[cfg.Prefix]; ok { + return errors.New("prefix " + cfg.Prefix + " is duplicated") + } + prefixs[cfg.Prefix] = true + } + + return nil +} diff --git a/internal/server/handler_test.go b/internal/server/handler_test.go new file mode 100644 index 0000000..8a607cb --- /dev/null +++ b/internal/server/handler_test.go @@ -0,0 +1,112 @@ +package server + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHandlerConfig(t *testing.T) { + assert := assert.New(t) + + wd, err := os.Getwd() + assert.Nil(err) + + cases := []struct { + cfg *HandlerConfig + valid bool + }{ + { + &HandlerConfig{ + Prefix: "/data", + PathDir: wd, + }, + true, + }, { + &HandlerConfig{ + Prefix: "/", + PathDir: wd, + }, + true, + }, { + &HandlerConfig{ + Prefix: "data", + PathDir: wd, + }, + false, + }, { + &HandlerConfig{ + Prefix: "/data/", + PathDir: wd, + }, + false, + }, { + &HandlerConfig{ + Prefix: "/114?514", + PathDir: wd, + }, + false, + }, { + &HandlerConfig{ + Prefix: "/data", + PathDir: "/114514", + }, + false, + }, + } + + for _, c := range cases { + if c.valid { + assert.Nil(checkHandlerConfig(c.cfg), "cfg: %+v should be valid", c.cfg) + } else { + assert.NotNil(checkHandlerConfig(c.cfg), "cfg: %+v should be invalid", c.cfg) + } + } +} + +func TestHandlerConfigs(t *testing.T) { + assert := assert.New(t) + + wd, err := os.Getwd() + assert.Nil(err) + + cases := []struct { + cfgs []*HandlerConfig + valid bool + }{ + { + []*HandlerConfig{ + { + Prefix: "/data1", + PathDir: wd, + }, + { + Prefix: "/data2", + PathDir: wd, + }, + }, + true, + }, { + []*HandlerConfig{ + { + Prefix: "/data1", + PathDir: wd, + }, + { + Prefix: "/data1", + PathDir: wd, + }, + }, + false, + }, + } + + for _, c := range cases { + if c.valid { + assert.Nil(checkHandlerConfigs(c.cfgs), "cfgs: %+v should be valid", c.cfgs) + } else { + assert.NotNil(checkHandlerConfigs(c.cfgs), "cfgs: %+v should be invalid", c.cfgs) + } + } +} diff --git a/internal/server/server.go b/internal/server/server.go index fe09120..3a43b77 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -10,106 +10,23 @@ import ( "golang.org/x/net/webdav" ) -type HandlerConfig struct { - Prefix string - PathDir string - Username string - Password string - ReadOnly bool -} - -type Handler struct { - handler *webdav.Handler - - prefix string // URL prefix - dirPath string // File system directory - - username string // HTTP Basic Auth Username. if empty, no auth - password string // HTTP Basic Auth Password - - readOnly bool // if true, only allow GET, OPTIONS, PROPFIND, HEAD -} - -// func NewHandler(prefix, dirPath, username, password string, readOnly bool) *Handler { -// return &Handler{ -// handler: &webdav.Handler{ -// FileSystem: webdav.Dir(dirPath), -// LockSystem: webdav.NewMemLS(), -// Prefix: prefix, -// }, -// } -// } - -func NewHandler(cfg *HandlerConfig) *Handler { - return &Handler{ - handler: &webdav.Handler{ - FileSystem: webdav.Dir(cfg.PathDir), - LockSystem: webdav.NewMemLS(), - Prefix: cfg.Prefix, - }, - prefix: cfg.Prefix, - dirPath: cfg.PathDir, - username: cfg.Username, - password: cfg.Password, - readOnly: cfg.ReadOnly, - } -} - -func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - enableBasicAuth := h.username != "" - if enableBasicAuth { - username, password, ok := req.BasicAuth() - // log.Debug().Str("username", username).Str("password", password).Bool("ok", ok).Msg("BasicAuth Request") - if !ok { - w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) - w.WriteHeader(http.StatusUnauthorized) - return - } - - if username != h.username || password != h.password { - w.WriteHeader(http.StatusUnauthorized) - return - } - } - - if h.readOnly { - allowedMethods := map[string]bool{ - "GET": true, - "OPTIONS": true, - "PROPFIND": true, - "HEAD": true, - } - if !allowedMethods[req.Method] { - w.WriteHeader(http.StatusMethodNotAllowed) - return - } - } - - h.handler.ServeHTTP(w, req) -} - type WebDAVServer struct { addr string // Address to listen on, e.g. "0.0.0.0:8080" smux *http.ServeMux } -func (s *WebDAVServer) Run() { - if err := http.ListenAndServe(s.addr, s.smux); err != nil { - panic(err) - } -} - func NewWebDAVServer(addr string, handlerConfigs []*HandlerConfig) *WebDAVServer { sMux := http.NewServeMux() - handlers := make(map[string]*Handler) // URL prefix -> Handler + handlers := make(map[string]*handler) // URL prefix -> Handler for _, cfg := range handlerConfigs { h := NewHandler(cfg) handlers[cfg.Prefix] = h } + // single dav mode: if there is only one handler and its prefix is "/", route all requests to it enableSingleDavMode := false - var singleHandler *Handler + var singleHandler *handler if len(handlers) == 1 { for _, h := range handlers { if h.prefix == "/" { @@ -159,3 +76,9 @@ func NewWebDAVServer(addr string, handlerConfigs []*HandlerConfig) *WebDAVServer smux: sMux, } } + +func (s *WebDAVServer) Run() { + if err := http.ListenAndServe(s.addr, s.smux); err != nil { + panic(err) + } +}