diff --git a/cmd/serve/webdav/webdav.go b/cmd/serve/webdav/webdav.go index 5ab881019..00af1f033 100644 --- a/cmd/serve/webdav/webdav.go +++ b/cmd/serve/webdav/webdav.go @@ -10,14 +10,15 @@ import ( "strings" "time" + chi "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" "github.com/rclone/rclone/cmd" - "github.com/rclone/rclone/cmd/serve/httplib" - "github.com/rclone/rclone/cmd/serve/httplib/httpflags" "github.com/rclone/rclone/cmd/serve/proxy" "github.com/rclone/rclone/cmd/serve/proxy/proxyflags" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/config/flags" "github.com/rclone/rclone/fs/hash" + libhttp "github.com/rclone/rclone/lib/http" "github.com/rclone/rclone/lib/http/serve" "github.com/rclone/rclone/vfs" "github.com/rclone/rclone/vfs/vfsflags" @@ -25,19 +26,37 @@ import ( "golang.org/x/net/webdav" ) -var ( - hashName string - hashType = hash.None - disableGETDir = false -) +// Options required for http server +type Options struct { + Auth libhttp.AuthConfig + HTTP libhttp.Config + Template libhttp.TemplateConfig + HashName string + HashType hash.Type + DisableGETDir bool +} + +// DefaultOpt is the default values used for Options +var DefaultOpt = Options{ + Auth: libhttp.DefaultAuthCfg(), + HTTP: libhttp.DefaultCfg(), + Template: libhttp.DefaultTemplateCfg(), + HashType: hash.None, + DisableGETDir: false, +} + +// Opt is options set by command line flags +var Opt = DefaultOpt func init() { flagSet := Command.Flags() - httpflags.AddFlags(flagSet) + libhttp.AddAuthFlagsPrefix(flagSet, "", &Opt.Auth) + libhttp.AddHTTPFlagsPrefix(flagSet, "", &Opt.HTTP) + libhttp.AddTemplateFlagsPrefix(flagSet, "", &Opt.Template) vfsflags.AddFlags(flagSet) proxyflags.AddFlags(flagSet) - flags.StringVarP(flagSet, &hashName, "etag-hash", "", "", "Which hash to use for the ETag, or auto or blank for off") - flags.BoolVarP(flagSet, &disableGETDir, "disable-dir-list", "", false, "Disable HTML directory list on GET request for a directory") + flags.StringVarP(flagSet, &Opt.HashName, "etag-hash", "", "", "Which hash to use for the ETag, or auto or blank for off") + flags.BoolVarP(flagSet, &Opt.DisableGETDir, "disable-dir-list", "", false, "Disable HTML directory list on GET request for a directory") } // Command definition for cobra @@ -60,7 +79,7 @@ supported hash on the backend or you can use a named hash such as "MD5" or "SHA-1". Use the [hashsum](/commands/rclone_hashsum/) command to see the full list. -` + httplib.Help + vfs.Help + proxy.Help, +` + libhttp.Help + libhttp.TemplateHelp + libhttp.AuthHelp + vfs.Help + proxy.Help, Annotations: map[string]string{ "versionIntroduced": "v1.39", }, @@ -72,21 +91,24 @@ to see the full list. } else { cmd.CheckArgs(0, 0, command, args) } - hashType = hash.None - if hashName == "auto" { - hashType = f.Hashes().GetOne() - } else if hashName != "" { - err := hashType.Set(hashName) + Opt.HashType = hash.None + if Opt.HashName == "auto" { + Opt.HashType = f.Hashes().GetOne() + } else if Opt.HashName != "" { + err := Opt.HashType.Set(Opt.HashName) if err != nil { return err } } - if hashType != hash.None { - fs.Debugf(f, "Using hash %v for ETag", hashType) + if Opt.HashType != hash.None { + fs.Debugf(f, "Using hash %v for ETag", Opt.HashType) } cmd.Run(false, false, command, func() error { - s := newWebDAV(context.Background(), f, &httpflags.Opt) - err := s.serve() + s, err := newWebDAV(context.Background(), f, &Opt) + if err != nil { + return err + } + err = s.serve() if err != nil { return err } @@ -110,7 +132,8 @@ to see the full list. // might apply". In particular, whether or not renaming a file or directory // overwriting another existing file or directory is an error is OS-dependent. type WebDAV struct { - *httplib.Server + *libhttp.Server + opt Options f fs.Fs _vfs *vfs.VFS // don't use directly, use getVFS webdavhandler *webdav.Handler @@ -122,29 +145,63 @@ type WebDAV struct { var _ webdav.FileSystem = (*WebDAV)(nil) // Make a new WebDAV to serve the remote -func newWebDAV(ctx context.Context, f fs.Fs, opt *httplib.Options) *WebDAV { - w := &WebDAV{ +func newWebDAV(ctx context.Context, f fs.Fs, opt *Options) (w *WebDAV, err error) { + w = &WebDAV{ f: f, ctx: ctx, + opt: *opt, } if proxyflags.Opt.AuthProxy != "" { w.proxy = proxy.New(ctx, &proxyflags.Opt) // override auth - copyOpt := *opt - copyOpt.Auth = w.auth - opt = ©Opt + w.opt.Auth.CustomAuthFn = w.auth } else { w._vfs = vfs.New(f, &vfsflags.Opt) } - w.Server = httplib.NewServer(http.HandlerFunc(w.handler), opt) + + w.Server, err = libhttp.NewServer(ctx, + libhttp.WithConfig(w.opt.HTTP), + libhttp.WithAuth(w.opt.Auth), + libhttp.WithTemplate(w.opt.Template), + ) + if err != nil { + return nil, fmt.Errorf("failed to init server: %w", err) + } + webdavHandler := &webdav.Handler{ - Prefix: w.Server.Opt.BaseURL, + Prefix: w.opt.HTTP.BaseURL, FileSystem: w, LockSystem: webdav.NewMemLS(), Logger: w.logRequest, // FIXME } w.webdavhandler = webdavHandler - return w + + router := w.Server.Router() + router.Use( + middleware.SetHeader("Accept-Ranges", "bytes"), + middleware.SetHeader("Server", "rclone/"+fs.Version), + ) + + router.Handle("/*", w) + + // Webdav only methods not defined in chi + methods := []string{ + "COPY", // Copies the resource. + "LOCK", // Locks the resource. + "MKCOL", // Creates the collection specified. + "MOVE", // Moves the resource. + "PROPFIND", // Performs a property find on the server. + "PROPPATCH", // Sets or removes properties on the server. + "UNLOCK", // Unlocks the resource. + } + for _, method := range methods { + chi.RegisterMethod(method) + router.Method(method, "/*", w) + } + + w.Server.Serve() + + return w, nil } // Gets the VFS in use for this request @@ -152,7 +209,7 @@ func (w *WebDAV) getVFS(ctx context.Context) (VFS *vfs.VFS, err error) { if w._vfs != nil { return w._vfs, nil } - value := ctx.Value(httplib.ContextAuthKey) + value := libhttp.CtxGetAuth(ctx) if value == nil { return nil, errors.New("no VFS found in context") } @@ -172,14 +229,11 @@ func (w *WebDAV) auth(user, pass string) (value interface{}, err error) { return VFS, err } -func (w *WebDAV) handler(rw http.ResponseWriter, r *http.Request) { - urlPath, ok := w.Path(rw, r) - if !ok { - return - } +func (w *WebDAV) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + urlPath := r.URL.Path isDir := strings.HasSuffix(urlPath, "/") remote := strings.Trim(urlPath, "/") - if !disableGETDir && (r.Method == "GET" || r.Method == "HEAD") && isDir { + if !w.opt.DisableGETDir && (r.Method == "GET" || r.Method == "HEAD") && isDir { w.serveDir(rw, r, remote) return } @@ -217,7 +271,7 @@ func (w *WebDAV) serveDir(rw http.ResponseWriter, r *http.Request, dirRemote str } // Make the entries for display - directory := serve.NewDirectory(dirRemote, w.HTMLTemplate) + directory := serve.NewDirectory(dirRemote, w.Server.HTMLTemplate()) for _, node := range dirEntries { if vfsflags.Opt.NoModTime { directory.AddHTMLEntry(node.Path(), node.IsDir(), node.Size(), time.Time{}) @@ -237,11 +291,8 @@ func (w *WebDAV) serveDir(rw http.ResponseWriter, r *http.Request, dirRemote str // // Use s.Close() and s.Wait() to shutdown server func (w *WebDAV) serve() error { - err := w.Serve() - if err != nil { - return err - } - fs.Logf(w.f, "WebDav Server started on %s", w.URL()) + w.Serve() + fs.Logf(w.f, "WebDav Server started on %s", w.URLs()) return nil } @@ -276,7 +327,7 @@ func (w *WebDAV) OpenFile(ctx context.Context, name string, flags int, perm os.F if err != nil { return nil, err } - return Handle{f}, nil + return Handle{Handle: f, w: w}, nil } // RemoveAll removes a file or a directory and its contents @@ -318,12 +369,13 @@ func (w *WebDAV) Stat(ctx context.Context, name string) (fi os.FileInfo, err err if err != nil { return nil, err } - return FileInfo{fi}, nil + return FileInfo{FileInfo: fi, w: w}, nil } // Handle represents an open file type Handle struct { vfs.Handle + w *WebDAV } // Readdir reads directory entries from the handle @@ -334,7 +386,7 @@ func (h Handle) Readdir(count int) (fis []os.FileInfo, err error) { } // Wrap each FileInfo for i := range fis { - fis[i] = FileInfo{fis[i]} + fis[i] = FileInfo{FileInfo: fis[i], w: h.w} } return fis, nil } @@ -345,19 +397,20 @@ func (h Handle) Stat() (fi os.FileInfo, err error) { if err != nil { return nil, err } - return FileInfo{fi}, nil + return FileInfo{FileInfo: fi, w: h.w}, nil } // FileInfo represents info about a file satisfying os.FileInfo and // also some additional interfaces for webdav for ETag and ContentType type FileInfo struct { os.FileInfo + w *WebDAV } // ETag returns an ETag for the FileInfo func (fi FileInfo) ETag(ctx context.Context) (etag string, err error) { // defer log.Trace(fi, "")("etag=%q, err=%v", &etag, &err) - if hashType == hash.None { + if fi.w.opt.HashType == hash.None { return "", webdav.ErrNotImplemented } node, ok := (fi.FileInfo).(vfs.Node) @@ -370,7 +423,7 @@ func (fi FileInfo) ETag(ctx context.Context) (etag string, err error) { if !ok { return "", webdav.ErrNotImplemented } - hash, err := o.Hash(ctx, hashType) + hash, err := o.Hash(ctx, fi.w.opt.HashType) if err != nil || hash == "" { return "", webdav.ErrNotImplemented } diff --git a/cmd/serve/webdav/webdav_test.go b/cmd/serve/webdav/webdav_test.go index bb95b0793..fa0626075 100644 --- a/cmd/serve/webdav/webdav_test.go +++ b/cmd/serve/webdav/webdav_test.go @@ -19,7 +19,6 @@ import ( "time" _ "github.com/rclone/rclone/backend/local" - "github.com/rclone/rclone/cmd/serve/httplib" "github.com/rclone/rclone/cmd/serve/servetest" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/config/configmap" @@ -40,9 +39,9 @@ const ( // check interfaces var ( - _ os.FileInfo = FileInfo{nil} - _ webdav.ETager = FileInfo{nil} - _ webdav.ContentTyper = FileInfo{nil} + _ os.FileInfo = FileInfo{nil, nil} + _ webdav.ETager = FileInfo{nil, nil} + _ webdav.ContentTyper = FileInfo{nil, nil} ) // TestWebDav runs the webdav server then runs the unit tests for the @@ -50,28 +49,29 @@ var ( func TestWebDav(t *testing.T) { // Configure and start the server start := func(f fs.Fs) (configmap.Simple, func()) { - opt := httplib.DefaultOpt - opt.ListenAddr = testBindAddress - opt.BasicUser = testUser - opt.BasicPass = testPass - opt.Template = testTemplate - hashType = hash.MD5 + opt := DefaultOpt + opt.HTTP.ListenAddr = []string{testBindAddress} + opt.Auth.BasicUser = testUser + opt.Auth.BasicPass = testPass + opt.Template.Path = testTemplate + opt.HashType = hash.MD5 // Start the server - w := newWebDAV(context.Background(), f, &opt) - assert.NoError(t, w.serve()) + w, err := newWebDAV(context.Background(), f, &opt) + require.NoError(t, err) + require.NoError(t, w.serve()) // Config for the backend we'll use to connect to the server config := configmap.Simple{ "type": "webdav", "vendor": "other", - "url": w.Server.URL(), + "url": w.Server.URLs()[0], "user": testUser, "pass": obscure.MustObscure(testPass), } return config, func() { - w.Close() + assert.NoError(t, w.Shutdown()) w.Wait() } } @@ -98,18 +98,19 @@ func TestHTTPFunction(t *testing.T) { f, err := fs.NewFs(context.Background(), "../http/testdata/files") assert.NoError(t, err) - opt := httplib.DefaultOpt - opt.ListenAddr = testBindAddress - opt.Template = testTemplate + opt := DefaultOpt + opt.HTTP.ListenAddr = []string{testBindAddress} + opt.Template.Path = testTemplate // Start the server - w := newWebDAV(context.Background(), f, &opt) - assert.NoError(t, w.serve()) + w, err := newWebDAV(context.Background(), f, &opt) + assert.NoError(t, err) + require.NoError(t, w.serve()) defer func() { - w.Close() + assert.NoError(t, w.Shutdown()) w.Wait() }() - testURL := w.Server.URL() + testURL := w.Server.URLs()[0] pause := time.Millisecond i := 0 for ; i < 10; i++ {