github.com/divyam234/rclone@v1.64.1/cmd/serve/webdav/webdav.go (about) 1 // Package webdav implements a WebDAV server backed by rclone VFS 2 package webdav 3 4 import ( 5 "context" 6 "encoding/xml" 7 "errors" 8 "fmt" 9 "mime" 10 "net/http" 11 "os" 12 "path" 13 "strconv" 14 "strings" 15 "time" 16 17 chi "github.com/go-chi/chi/v5" 18 "github.com/go-chi/chi/v5/middleware" 19 "github.com/divyam234/rclone/cmd" 20 "github.com/divyam234/rclone/cmd/serve/proxy" 21 "github.com/divyam234/rclone/cmd/serve/proxy/proxyflags" 22 "github.com/divyam234/rclone/fs" 23 "github.com/divyam234/rclone/fs/config/flags" 24 "github.com/divyam234/rclone/fs/hash" 25 libhttp "github.com/divyam234/rclone/lib/http" 26 "github.com/divyam234/rclone/lib/http/serve" 27 "github.com/divyam234/rclone/vfs" 28 "github.com/divyam234/rclone/vfs/vfsflags" 29 "github.com/spf13/cobra" 30 "golang.org/x/net/webdav" 31 ) 32 33 // Options required for http server 34 type Options struct { 35 Auth libhttp.AuthConfig 36 HTTP libhttp.Config 37 Template libhttp.TemplateConfig 38 HashName string 39 HashType hash.Type 40 DisableGETDir bool 41 } 42 43 // DefaultOpt is the default values used for Options 44 var DefaultOpt = Options{ 45 Auth: libhttp.DefaultAuthCfg(), 46 HTTP: libhttp.DefaultCfg(), 47 Template: libhttp.DefaultTemplateCfg(), 48 HashType: hash.None, 49 DisableGETDir: false, 50 } 51 52 // Opt is options set by command line flags 53 var Opt = DefaultOpt 54 55 // flagPrefix is the prefix used to uniquely identify command line flags. 56 // It is intentionally empty for this package. 57 const flagPrefix = "" 58 59 func init() { 60 flagSet := Command.Flags() 61 libhttp.AddAuthFlagsPrefix(flagSet, flagPrefix, &Opt.Auth) 62 libhttp.AddHTTPFlagsPrefix(flagSet, flagPrefix, &Opt.HTTP) 63 libhttp.AddTemplateFlagsPrefix(flagSet, "", &Opt.Template) 64 vfsflags.AddFlags(flagSet) 65 proxyflags.AddFlags(flagSet) 66 flags.StringVarP(flagSet, &Opt.HashName, "etag-hash", "", "", "Which hash to use for the ETag, or auto or blank for off", "") 67 flags.BoolVarP(flagSet, &Opt.DisableGETDir, "disable-dir-list", "", false, "Disable HTML directory list on GET request for a directory", "") 68 } 69 70 // Command definition for cobra 71 var Command = &cobra.Command{ 72 Use: "webdav remote:path", 73 Short: `Serve remote:path over WebDAV.`, 74 Long: `Run a basic WebDAV server to serve a remote over HTTP via the 75 WebDAV protocol. This can be viewed with a WebDAV client, through a web 76 browser, or you can make a remote of type WebDAV to read and write it. 77 78 ### WebDAV options 79 80 #### --etag-hash 81 82 This controls the ETag header. Without this flag the ETag will be 83 based on the ModTime and Size of the object. 84 85 If this flag is set to "auto" then rclone will choose the first 86 supported hash on the backend or you can use a named hash such as 87 "MD5" or "SHA-1". Use the [hashsum](/commands/rclone_hashsum/) command 88 to see the full list. 89 90 ### Access WebDAV on Windows 91 WebDAV shared folder can be mapped as a drive on Windows, however the default settings prevent it. 92 Windows will fail to connect to the server using insecure Basic authentication. 93 It will not even display any login dialog. Windows requires SSL / HTTPS connection to be used with Basic. 94 If you try to connect via Add Network Location Wizard you will get the following error: 95 "The folder you entered does not appear to be valid. Please choose another". 96 However, you still can connect if you set the following registry key on a client machine: 97 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters\BasicAuthLevel to 2. 98 The BasicAuthLevel can be set to the following values: 99 0 - Basic authentication disabled 100 1 - Basic authentication enabled for SSL connections only 101 2 - Basic authentication enabled for SSL connections and for non-SSL connections 102 If required, increase the FileSizeLimitInBytes to a higher value. 103 Navigate to the Services interface, then restart the WebClient service. 104 105 ### Access Office applications on WebDAV 106 Navigate to following registry HKEY_CURRENT_USER\Software\Microsoft\Office\[14.0/15.0/16.0]\Common\Internet 107 Create a new DWORD BasicAuthLevel with value 2. 108 0 - Basic authentication disabled 109 1 - Basic authentication enabled for SSL connections only 110 2 - Basic authentication enabled for SSL and for non-SSL connections 111 112 https://learn.microsoft.com/en-us/office/troubleshoot/powerpoint/office-opens-blank-from-sharepoint 113 114 ` + libhttp.Help(flagPrefix) + libhttp.TemplateHelp(flagPrefix) + libhttp.AuthHelp(flagPrefix) + vfs.Help + proxy.Help, 115 Annotations: map[string]string{ 116 "versionIntroduced": "v1.39", 117 "groups": "Filter", 118 }, 119 RunE: func(command *cobra.Command, args []string) error { 120 var f fs.Fs 121 if proxyflags.Opt.AuthProxy == "" { 122 cmd.CheckArgs(1, 1, command, args) 123 f = cmd.NewFsSrc(args) 124 } else { 125 cmd.CheckArgs(0, 0, command, args) 126 } 127 Opt.HashType = hash.None 128 if Opt.HashName == "auto" { 129 Opt.HashType = f.Hashes().GetOne() 130 } else if Opt.HashName != "" { 131 err := Opt.HashType.Set(Opt.HashName) 132 if err != nil { 133 return err 134 } 135 } 136 if Opt.HashType != hash.None { 137 fs.Debugf(f, "Using hash %v for ETag", Opt.HashType) 138 } 139 cmd.Run(false, false, command, func() error { 140 s, err := newWebDAV(context.Background(), f, &Opt) 141 if err != nil { 142 return err 143 } 144 err = s.serve() 145 if err != nil { 146 return err 147 } 148 s.Wait() 149 return nil 150 }) 151 return nil 152 }, 153 } 154 155 // WebDAV is a webdav.FileSystem interface 156 // 157 // A FileSystem implements access to a collection of named files. The elements 158 // in a file path are separated by slash ('/', U+002F) characters, regardless 159 // of host operating system convention. 160 // 161 // Each method has the same semantics as the os package's function of the same 162 // name. 163 // 164 // Note that the os.Rename documentation says that "OS-specific restrictions 165 // might apply". In particular, whether or not renaming a file or directory 166 // overwriting another existing file or directory is an error is OS-dependent. 167 type WebDAV struct { 168 *libhttp.Server 169 opt Options 170 f fs.Fs 171 _vfs *vfs.VFS // don't use directly, use getVFS 172 webdavhandler *webdav.Handler 173 proxy *proxy.Proxy 174 ctx context.Context // for global config 175 } 176 177 // check interface 178 var _ webdav.FileSystem = (*WebDAV)(nil) 179 180 // Make a new WebDAV to serve the remote 181 func newWebDAV(ctx context.Context, f fs.Fs, opt *Options) (w *WebDAV, err error) { 182 w = &WebDAV{ 183 f: f, 184 ctx: ctx, 185 opt: *opt, 186 } 187 if proxyflags.Opt.AuthProxy != "" { 188 w.proxy = proxy.New(ctx, &proxyflags.Opt) 189 // override auth 190 w.opt.Auth.CustomAuthFn = w.auth 191 } else { 192 w._vfs = vfs.New(f, &vfsflags.Opt) 193 } 194 195 w.Server, err = libhttp.NewServer(ctx, 196 libhttp.WithConfig(w.opt.HTTP), 197 libhttp.WithAuth(w.opt.Auth), 198 libhttp.WithTemplate(w.opt.Template), 199 ) 200 if err != nil { 201 return nil, fmt.Errorf("failed to init server: %w", err) 202 } 203 204 webdavHandler := &webdav.Handler{ 205 Prefix: w.opt.HTTP.BaseURL, 206 FileSystem: w, 207 LockSystem: webdav.NewMemLS(), 208 Logger: w.logRequest, // FIXME 209 } 210 w.webdavhandler = webdavHandler 211 212 router := w.Server.Router() 213 router.Use( 214 middleware.SetHeader("Accept-Ranges", "bytes"), 215 middleware.SetHeader("Server", "rclone/"+fs.Version), 216 ) 217 218 router.Handle("/*", w) 219 220 // Webdav only methods not defined in chi 221 methods := []string{ 222 "COPY", // Copies the resource. 223 "LOCK", // Locks the resource. 224 "MKCOL", // Creates the collection specified. 225 "MOVE", // Moves the resource. 226 "PROPFIND", // Performs a property find on the server. 227 "PROPPATCH", // Sets or removes properties on the server. 228 "UNLOCK", // Unlocks the resource. 229 } 230 for _, method := range methods { 231 chi.RegisterMethod(method) 232 router.Method(method, "/*", w) 233 } 234 235 return w, nil 236 } 237 238 // Gets the VFS in use for this request 239 func (w *WebDAV) getVFS(ctx context.Context) (VFS *vfs.VFS, err error) { 240 if w._vfs != nil { 241 return w._vfs, nil 242 } 243 value := libhttp.CtxGetAuth(ctx) 244 if value == nil { 245 return nil, errors.New("no VFS found in context") 246 } 247 VFS, ok := value.(*vfs.VFS) 248 if !ok { 249 return nil, fmt.Errorf("context value is not VFS: %#v", value) 250 } 251 return VFS, nil 252 } 253 254 // auth does proxy authorization 255 func (w *WebDAV) auth(user, pass string) (value interface{}, err error) { 256 VFS, _, err := w.proxy.Call(user, pass, false) 257 if err != nil { 258 return nil, err 259 } 260 return VFS, err 261 } 262 263 type webdavRW struct { 264 http.ResponseWriter 265 status int 266 } 267 268 func (rw *webdavRW) WriteHeader(statusCode int) { 269 rw.status = statusCode 270 rw.ResponseWriter.WriteHeader(statusCode) 271 } 272 273 func (rw *webdavRW) isSuccessfull() bool { 274 return rw.status == 0 || (rw.status >= 200 && rw.status <= 299) 275 } 276 277 func (w *WebDAV) postprocess(r *http.Request, remote string) { 278 // set modtime from requests, don't write to client because status is already written 279 switch r.Method { 280 case "COPY", "MOVE", "PUT": 281 VFS, err := w.getVFS(r.Context()) 282 if err != nil { 283 fs.Errorf(nil, "Failed to get VFS: %v", err) 284 return 285 } 286 287 // Get the node 288 node, err := VFS.Stat(remote) 289 if err != nil { 290 fs.Errorf(nil, "Failed to stat node: %v", err) 291 return 292 } 293 294 mh := r.Header.Get("X-OC-Mtime") 295 if mh != "" { 296 modtimeUnix, err := strconv.ParseInt(mh, 10, 64) 297 if err == nil { 298 err = node.SetModTime(time.Unix(modtimeUnix, 0)) 299 if err != nil { 300 fs.Errorf(nil, "Failed to set modtime: %v", err) 301 } 302 } else { 303 fs.Errorf(nil, "Failed to parse modtime: %v", err) 304 } 305 } 306 } 307 } 308 309 func (w *WebDAV) ServeHTTP(rw http.ResponseWriter, r *http.Request) { 310 urlPath := r.URL.Path 311 isDir := strings.HasSuffix(urlPath, "/") 312 remote := strings.Trim(urlPath, "/") 313 if !w.opt.DisableGETDir && (r.Method == "GET" || r.Method == "HEAD") && isDir { 314 w.serveDir(rw, r, remote) 315 return 316 } 317 // Add URL Prefix back to path since webdavhandler needs to 318 // return absolute references. 319 r.URL.Path = w.opt.HTTP.BaseURL + r.URL.Path 320 wrw := &webdavRW{ResponseWriter: rw} 321 w.webdavhandler.ServeHTTP(wrw, r) 322 323 if wrw.isSuccessfull() { 324 w.postprocess(r, remote) 325 } 326 } 327 328 // serveDir serves a directory index at dirRemote 329 // This is similar to serveDir in serve http. 330 func (w *WebDAV) serveDir(rw http.ResponseWriter, r *http.Request, dirRemote string) { 331 VFS, err := w.getVFS(r.Context()) 332 if err != nil { 333 http.Error(rw, "Root directory not found", http.StatusNotFound) 334 fs.Errorf(nil, "Failed to serve directory: %v", err) 335 return 336 } 337 // List the directory 338 node, err := VFS.Stat(dirRemote) 339 if err == vfs.ENOENT { 340 http.Error(rw, "Directory not found", http.StatusNotFound) 341 return 342 } else if err != nil { 343 serve.Error(dirRemote, rw, "Failed to list directory", err) 344 return 345 } 346 if !node.IsDir() { 347 http.Error(rw, "Not a directory", http.StatusNotFound) 348 return 349 } 350 dir := node.(*vfs.Dir) 351 dirEntries, err := dir.ReadDirAll() 352 353 if err != nil { 354 serve.Error(dirRemote, rw, "Failed to list directory", err) 355 return 356 } 357 358 // Make the entries for display 359 directory := serve.NewDirectory(dirRemote, w.Server.HTMLTemplate()) 360 for _, node := range dirEntries { 361 if vfsflags.Opt.NoModTime { 362 directory.AddHTMLEntry(node.Path(), node.IsDir(), node.Size(), time.Time{}) 363 } else { 364 directory.AddHTMLEntry(node.Path(), node.IsDir(), node.Size(), node.ModTime().UTC()) 365 } 366 } 367 368 sortParm := r.URL.Query().Get("sort") 369 orderParm := r.URL.Query().Get("order") 370 directory.ProcessQueryParams(sortParm, orderParm) 371 372 directory.Serve(rw, r) 373 } 374 375 // serve runs the http server in the background. 376 // 377 // Use s.Close() and s.Wait() to shutdown server 378 func (w *WebDAV) serve() error { 379 w.Serve() 380 fs.Logf(w.f, "WebDav Server started on %s", w.URLs()) 381 return nil 382 } 383 384 // logRequest is called by the webdav module on every request 385 func (w *WebDAV) logRequest(r *http.Request, err error) { 386 fs.Infof(r.URL.Path, "%s from %s", r.Method, r.RemoteAddr) 387 } 388 389 // Mkdir creates a directory 390 func (w *WebDAV) Mkdir(ctx context.Context, name string, perm os.FileMode) (err error) { 391 // defer log.Trace(name, "perm=%v", perm)("err = %v", &err) 392 VFS, err := w.getVFS(ctx) 393 if err != nil { 394 return err 395 } 396 dir, leaf, err := VFS.StatParent(name) 397 if err != nil { 398 return err 399 } 400 _, err = dir.Mkdir(leaf) 401 return err 402 } 403 404 // OpenFile opens a file or a directory 405 func (w *WebDAV) OpenFile(ctx context.Context, name string, flags int, perm os.FileMode) (file webdav.File, err error) { 406 // defer log.Trace(name, "flags=%v, perm=%v", flags, perm)("err = %v", &err) 407 VFS, err := w.getVFS(ctx) 408 if err != nil { 409 return nil, err 410 } 411 f, err := VFS.OpenFile(name, flags, perm) 412 if err != nil { 413 return nil, err 414 } 415 return Handle{Handle: f, w: w, ctx: ctx}, nil 416 } 417 418 // RemoveAll removes a file or a directory and its contents 419 func (w *WebDAV) RemoveAll(ctx context.Context, name string) (err error) { 420 // defer log.Trace(name, "")("err = %v", &err) 421 VFS, err := w.getVFS(ctx) 422 if err != nil { 423 return err 424 } 425 node, err := VFS.Stat(name) 426 if err != nil { 427 return err 428 } 429 err = node.RemoveAll() 430 if err != nil { 431 return err 432 } 433 return nil 434 } 435 436 // Rename a file or a directory 437 func (w *WebDAV) Rename(ctx context.Context, oldName, newName string) (err error) { 438 // defer log.Trace(oldName, "newName=%q", newName)("err = %v", &err) 439 VFS, err := w.getVFS(ctx) 440 if err != nil { 441 return err 442 } 443 return VFS.Rename(oldName, newName) 444 } 445 446 // Stat returns info about the file or directory 447 func (w *WebDAV) Stat(ctx context.Context, name string) (fi os.FileInfo, err error) { 448 // defer log.Trace(name, "")("fi=%+v, err = %v", &fi, &err) 449 VFS, err := w.getVFS(ctx) 450 if err != nil { 451 return nil, err 452 } 453 fi, err = VFS.Stat(name) 454 if err != nil { 455 return nil, err 456 } 457 return FileInfo{FileInfo: fi, w: w}, nil 458 } 459 460 // Handle represents an open file 461 type Handle struct { 462 vfs.Handle 463 w *WebDAV 464 ctx context.Context 465 } 466 467 // Readdir reads directory entries from the handle 468 func (h Handle) Readdir(count int) (fis []os.FileInfo, err error) { 469 fis, err = h.Handle.Readdir(count) 470 if err != nil { 471 return nil, err 472 } 473 // Wrap each FileInfo 474 for i := range fis { 475 fis[i] = FileInfo{FileInfo: fis[i], w: h.w} 476 } 477 return fis, nil 478 } 479 480 // Stat the handle 481 func (h Handle) Stat() (fi os.FileInfo, err error) { 482 fi, err = h.Handle.Stat() 483 if err != nil { 484 return nil, err 485 } 486 return FileInfo{FileInfo: fi, w: h.w}, nil 487 } 488 489 // DeadProps returns extra properties about the handle 490 func (h Handle) DeadProps() (map[xml.Name]webdav.Property, error) { 491 var ( 492 xmlName xml.Name 493 property webdav.Property 494 properties = make(map[xml.Name]webdav.Property) 495 ) 496 if h.w.opt.HashType != hash.None { 497 entry := h.Handle.Node().DirEntry() 498 if o, ok := entry.(fs.Object); ok { 499 hash, err := o.Hash(h.ctx, h.w.opt.HashType) 500 if err == nil { 501 xmlName.Space = "http://owncloud.org/ns" 502 xmlName.Local = "checksums" 503 property.XMLName = xmlName 504 property.InnerXML = append(property.InnerXML, "<checksum xmlns=\"http://owncloud.org/ns\">"...) 505 property.InnerXML = append(property.InnerXML, strings.ToUpper(h.w.opt.HashType.String())...) 506 property.InnerXML = append(property.InnerXML, ':') 507 property.InnerXML = append(property.InnerXML, hash...) 508 property.InnerXML = append(property.InnerXML, "</checksum>"...) 509 properties[xmlName] = property 510 } else { 511 fs.Errorf(nil, "failed to calculate hash: %v", err) 512 } 513 } 514 } 515 516 xmlName.Space = "DAV:" 517 xmlName.Local = "lastmodified" 518 property.XMLName = xmlName 519 property.InnerXML = strconv.AppendInt(nil, h.Handle.Node().ModTime().Unix(), 10) 520 properties[xmlName] = property 521 522 return properties, nil 523 } 524 525 // Patch changes modtime of the underlying resources, it returns ok for all properties, the error is from setModtime if any 526 // FIXME does not check for invalid property and SetModTime error 527 func (h Handle) Patch(proppatches []webdav.Proppatch) ([]webdav.Propstat, error) { 528 var ( 529 stat webdav.Propstat 530 err error 531 ) 532 stat.Status = http.StatusOK 533 for _, patch := range proppatches { 534 for _, prop := range patch.Props { 535 stat.Props = append(stat.Props, webdav.Property{XMLName: prop.XMLName}) 536 if prop.XMLName.Space == "DAV:" && prop.XMLName.Local == "lastmodified" { 537 var modtimeUnix int64 538 modtimeUnix, err = strconv.ParseInt(string(prop.InnerXML), 10, 64) 539 if err == nil { 540 err = h.Handle.Node().SetModTime(time.Unix(modtimeUnix, 0)) 541 } 542 } 543 } 544 } 545 return []webdav.Propstat{stat}, err 546 } 547 548 // FileInfo represents info about a file satisfying os.FileInfo and 549 // also some additional interfaces for webdav for ETag and ContentType 550 type FileInfo struct { 551 os.FileInfo 552 w *WebDAV 553 } 554 555 // ETag returns an ETag for the FileInfo 556 func (fi FileInfo) ETag(ctx context.Context) (etag string, err error) { 557 // defer log.Trace(fi, "")("etag=%q, err=%v", &etag, &err) 558 if fi.w.opt.HashType == hash.None { 559 return "", webdav.ErrNotImplemented 560 } 561 node, ok := (fi.FileInfo).(vfs.Node) 562 if !ok { 563 fs.Errorf(fi, "Expecting vfs.Node, got %T", fi.FileInfo) 564 return "", webdav.ErrNotImplemented 565 } 566 entry := node.DirEntry() 567 o, ok := entry.(fs.Object) 568 if !ok { 569 return "", webdav.ErrNotImplemented 570 } 571 hash, err := o.Hash(ctx, fi.w.opt.HashType) 572 if err != nil || hash == "" { 573 return "", webdav.ErrNotImplemented 574 } 575 return `"` + hash + `"`, nil 576 } 577 578 // ContentType returns a content type for the FileInfo 579 func (fi FileInfo) ContentType(ctx context.Context) (contentType string, err error) { 580 // defer log.Trace(fi, "")("etag=%q, err=%v", &contentType, &err) 581 node, ok := (fi.FileInfo).(vfs.Node) 582 if !ok { 583 fs.Errorf(fi, "Expecting vfs.Node, got %T", fi.FileInfo) 584 return "application/octet-stream", nil 585 } 586 entry := node.DirEntry() // can be nil 587 switch x := entry.(type) { 588 case fs.Object: 589 return fs.MimeType(ctx, x), nil 590 case fs.Directory: 591 return "inode/directory", nil 592 case nil: 593 return mime.TypeByExtension(path.Ext(node.Name())), nil 594 } 595 fs.Errorf(fi, "Expecting fs.Object or fs.Directory, got %T", entry) 596 return "application/octet-stream", nil 597 }