1// Copyright 2014 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5// Package webdav provides a WebDAV server implementation. 6package webdav // import "golang.org/x/net/webdav" 7 8import ( 9 "errors" 10 "fmt" 11 "io" 12 "net/http" 13 "net/url" 14 "os" 15 "path" 16 "strings" 17 "time" 18) 19 20type Handler struct { 21 // Prefix is the URL path prefix to strip from WebDAV resource paths. 22 Prefix string 23 // FileSystem is the virtual file system. 24 FileSystem FileSystem 25 // LockSystem is the lock management system. 26 LockSystem LockSystem 27 // Logger is an optional error logger. If non-nil, it will be called 28 // for all HTTP requests. 29 Logger func(*http.Request, error) 30} 31 32func (h *Handler) stripPrefix(p string) (string, int, error) { 33 if h.Prefix == "" { 34 return p, http.StatusOK, nil 35 } 36 if r := strings.TrimPrefix(p, h.Prefix); len(r) < len(p) { 37 return r, http.StatusOK, nil 38 } 39 return p, http.StatusNotFound, errPrefixMismatch 40} 41 42func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 43 status, err := http.StatusBadRequest, errUnsupportedMethod 44 if h.FileSystem == nil { 45 status, err = http.StatusInternalServerError, errNoFileSystem 46 } else if h.LockSystem == nil { 47 status, err = http.StatusInternalServerError, errNoLockSystem 48 } else { 49 switch r.Method { 50 case "OPTIONS": 51 status, err = h.handleOptions(w, r) 52 case "GET", "HEAD", "POST": 53 status, err = h.handleGetHeadPost(w, r) 54 case "DELETE": 55 status, err = h.handleDelete(w, r) 56 case "PUT": 57 status, err = h.handlePut(w, r) 58 case "MKCOL": 59 status, err = h.handleMkcol(w, r) 60 case "COPY", "MOVE": 61 status, err = h.handleCopyMove(w, r) 62 case "LOCK": 63 status, err = h.handleLock(w, r) 64 case "UNLOCK": 65 status, err = h.handleUnlock(w, r) 66 case "PROPFIND": 67 status, err = h.handlePropfind(w, r) 68 case "PROPPATCH": 69 status, err = h.handleProppatch(w, r) 70 } 71 } 72 73 if status != 0 { 74 w.WriteHeader(status) 75 if status != http.StatusNoContent { 76 w.Write([]byte(StatusText(status))) 77 } 78 } 79 if h.Logger != nil { 80 h.Logger(r, err) 81 } 82} 83 84func (h *Handler) lock(now time.Time, root string) (token string, status int, err error) { 85 token, err = h.LockSystem.Create(now, LockDetails{ 86 Root: root, 87 Duration: infiniteTimeout, 88 ZeroDepth: true, 89 }) 90 if err != nil { 91 if err == ErrLocked { 92 return "", StatusLocked, err 93 } 94 return "", http.StatusInternalServerError, err 95 } 96 return token, 0, nil 97} 98 99func (h *Handler) confirmLocks(r *http.Request, src, dst string) (release func(), status int, err error) { 100 hdr := r.Header.Get("If") 101 if hdr == "" { 102 // An empty If header means that the client hasn't previously created locks. 103 // Even if this client doesn't care about locks, we still need to check that 104 // the resources aren't locked by another client, so we create temporary 105 // locks that would conflict with another client's locks. These temporary 106 // locks are unlocked at the end of the HTTP request. 107 now, srcToken, dstToken := time.Now(), "", "" 108 if src != "" { 109 srcToken, status, err = h.lock(now, src) 110 if err != nil { 111 return nil, status, err 112 } 113 } 114 if dst != "" { 115 dstToken, status, err = h.lock(now, dst) 116 if err != nil { 117 if srcToken != "" { 118 h.LockSystem.Unlock(now, srcToken) 119 } 120 return nil, status, err 121 } 122 } 123 124 return func() { 125 if dstToken != "" { 126 h.LockSystem.Unlock(now, dstToken) 127 } 128 if srcToken != "" { 129 h.LockSystem.Unlock(now, srcToken) 130 } 131 }, 0, nil 132 } 133 134 ih, ok := parseIfHeader(hdr) 135 if !ok { 136 return nil, http.StatusBadRequest, errInvalidIfHeader 137 } 138 // ih is a disjunction (OR) of ifLists, so any ifList will do. 139 for _, l := range ih.lists { 140 lsrc := l.resourceTag 141 if lsrc == "" { 142 lsrc = src 143 } else { 144 u, err := url.Parse(lsrc) 145 if err != nil { 146 continue 147 } 148 if u.Host != r.Host { 149 continue 150 } 151 lsrc, status, err = h.stripPrefix(u.Path) 152 if err != nil { 153 return nil, status, err 154 } 155 } 156 release, err = h.LockSystem.Confirm(time.Now(), lsrc, dst, l.conditions...) 157 if err == ErrConfirmationFailed { 158 continue 159 } 160 if err != nil { 161 return nil, http.StatusInternalServerError, err 162 } 163 return release, 0, nil 164 } 165 // Section 10.4.1 says that "If this header is evaluated and all state lists 166 // fail, then the request must fail with a 412 (Precondition Failed) status." 167 // We follow the spec even though the cond_put_corrupt_token test case from 168 // the litmus test warns on seeing a 412 instead of a 423 (Locked). 169 return nil, http.StatusPreconditionFailed, ErrLocked 170} 171 172func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request) (status int, err error) { 173 reqPath, status, err := h.stripPrefix(r.URL.Path) 174 if err != nil { 175 return status, err 176 } 177 ctx := getContext(r) 178 allow := "OPTIONS, LOCK, PUT, MKCOL" 179 if fi, err := h.FileSystem.Stat(ctx, reqPath); err == nil { 180 if fi.IsDir() { 181 allow = "OPTIONS, LOCK, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND" 182 } else { 183 allow = "OPTIONS, LOCK, GET, HEAD, POST, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND, PUT" 184 } 185 } 186 w.Header().Set("Allow", allow) 187 // http://www.webdav.org/specs/rfc4918.html#dav.compliance.classes 188 w.Header().Set("DAV", "1, 2") 189 // http://msdn.microsoft.com/en-au/library/cc250217.aspx 190 w.Header().Set("MS-Author-Via", "DAV") 191 return 0, nil 192} 193 194func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (status int, err error) { 195 reqPath, status, err := h.stripPrefix(r.URL.Path) 196 if err != nil { 197 return status, err 198 } 199 // TODO: check locks for read-only access?? 200 ctx := getContext(r) 201 f, err := h.FileSystem.OpenFile(ctx, reqPath, os.O_RDONLY, 0) 202 if err != nil { 203 return http.StatusNotFound, err 204 } 205 defer f.Close() 206 fi, err := f.Stat() 207 if err != nil { 208 return http.StatusNotFound, err 209 } 210 if fi.IsDir() { 211 return http.StatusMethodNotAllowed, nil 212 } 213 etag, err := findETag(ctx, h.FileSystem, h.LockSystem, reqPath, fi) 214 if err != nil { 215 return http.StatusInternalServerError, err 216 } 217 w.Header().Set("ETag", etag) 218 // Let ServeContent determine the Content-Type header. 219 http.ServeContent(w, r, reqPath, fi.ModTime(), f) 220 return 0, nil 221} 222 223func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request) (status int, err error) { 224 reqPath, status, err := h.stripPrefix(r.URL.Path) 225 if err != nil { 226 return status, err 227 } 228 release, status, err := h.confirmLocks(r, reqPath, "") 229 if err != nil { 230 return status, err 231 } 232 defer release() 233 234 ctx := getContext(r) 235 236 // TODO: return MultiStatus where appropriate. 237 238 // "godoc os RemoveAll" says that "If the path does not exist, RemoveAll 239 // returns nil (no error)." WebDAV semantics are that it should return a 240 // "404 Not Found". We therefore have to Stat before we RemoveAll. 241 if _, err := h.FileSystem.Stat(ctx, reqPath); err != nil { 242 if os.IsNotExist(err) { 243 return http.StatusNotFound, err 244 } 245 return http.StatusMethodNotAllowed, err 246 } 247 if err := h.FileSystem.RemoveAll(ctx, reqPath); err != nil { 248 return http.StatusMethodNotAllowed, err 249 } 250 return http.StatusNoContent, nil 251} 252 253func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int, err error) { 254 reqPath, status, err := h.stripPrefix(r.URL.Path) 255 if err != nil { 256 return status, err 257 } 258 release, status, err := h.confirmLocks(r, reqPath, "") 259 if err != nil { 260 return status, err 261 } 262 defer release() 263 // TODO(rost): Support the If-Match, If-None-Match headers? See bradfitz' 264 // comments in http.checkEtag. 265 ctx := getContext(r) 266 267 f, err := h.FileSystem.OpenFile(ctx, reqPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) 268 if err != nil { 269 return http.StatusNotFound, err 270 } 271 _, copyErr := io.Copy(f, r.Body) 272 fi, statErr := f.Stat() 273 closeErr := f.Close() 274 // TODO(rost): Returning 405 Method Not Allowed might not be appropriate. 275 if copyErr != nil { 276 return http.StatusMethodNotAllowed, copyErr 277 } 278 if statErr != nil { 279 return http.StatusMethodNotAllowed, statErr 280 } 281 if closeErr != nil { 282 return http.StatusMethodNotAllowed, closeErr 283 } 284 etag, err := findETag(ctx, h.FileSystem, h.LockSystem, reqPath, fi) 285 if err != nil { 286 return http.StatusInternalServerError, err 287 } 288 w.Header().Set("ETag", etag) 289 return http.StatusCreated, nil 290} 291 292func (h *Handler) handleMkcol(w http.ResponseWriter, r *http.Request) (status int, err error) { 293 reqPath, status, err := h.stripPrefix(r.URL.Path) 294 if err != nil { 295 return status, err 296 } 297 release, status, err := h.confirmLocks(r, reqPath, "") 298 if err != nil { 299 return status, err 300 } 301 defer release() 302 303 ctx := getContext(r) 304 305 if r.ContentLength > 0 { 306 return http.StatusUnsupportedMediaType, nil 307 } 308 if err := h.FileSystem.Mkdir(ctx, reqPath, 0777); err != nil { 309 if os.IsNotExist(err) { 310 return http.StatusConflict, err 311 } 312 return http.StatusMethodNotAllowed, err 313 } 314 return http.StatusCreated, nil 315} 316 317func (h *Handler) handleCopyMove(w http.ResponseWriter, r *http.Request) (status int, err error) { 318 hdr := r.Header.Get("Destination") 319 if hdr == "" { 320 return http.StatusBadRequest, errInvalidDestination 321 } 322 u, err := url.Parse(hdr) 323 if err != nil { 324 return http.StatusBadRequest, errInvalidDestination 325 } 326 if u.Host != r.Host { 327 return http.StatusBadGateway, errInvalidDestination 328 } 329 330 src, status, err := h.stripPrefix(r.URL.Path) 331 if err != nil { 332 return status, err 333 } 334 335 dst, status, err := h.stripPrefix(u.Path) 336 if err != nil { 337 return status, err 338 } 339 340 if dst == "" { 341 return http.StatusBadGateway, errInvalidDestination 342 } 343 if dst == src { 344 return http.StatusForbidden, errDestinationEqualsSource 345 } 346 347 ctx := getContext(r) 348 349 if r.Method == "COPY" { 350 // Section 7.5.1 says that a COPY only needs to lock the destination, 351 // not both destination and source. Strictly speaking, this is racy, 352 // even though a COPY doesn't modify the source, if a concurrent 353 // operation modifies the source. However, the litmus test explicitly 354 // checks that COPYing a locked-by-another source is OK. 355 release, status, err := h.confirmLocks(r, "", dst) 356 if err != nil { 357 return status, err 358 } 359 defer release() 360 361 // Section 9.8.3 says that "The COPY method on a collection without a Depth 362 // header must act as if a Depth header with value "infinity" was included". 363 depth := infiniteDepth 364 if hdr := r.Header.Get("Depth"); hdr != "" { 365 depth = parseDepth(hdr) 366 if depth != 0 && depth != infiniteDepth { 367 // Section 9.8.3 says that "A client may submit a Depth header on a 368 // COPY on a collection with a value of "0" or "infinity"." 369 return http.StatusBadRequest, errInvalidDepth 370 } 371 } 372 return copyFiles(ctx, h.FileSystem, src, dst, r.Header.Get("Overwrite") != "F", depth, 0) 373 } 374 375 release, status, err := h.confirmLocks(r, src, dst) 376 if err != nil { 377 return status, err 378 } 379 defer release() 380 381 // Section 9.9.2 says that "The MOVE method on a collection must act as if 382 // a "Depth: infinity" header was used on it. A client must not submit a 383 // Depth header on a MOVE on a collection with any value but "infinity"." 384 if hdr := r.Header.Get("Depth"); hdr != "" { 385 if parseDepth(hdr) != infiniteDepth { 386 return http.StatusBadRequest, errInvalidDepth 387 } 388 } 389 return moveFiles(ctx, h.FileSystem, src, dst, r.Header.Get("Overwrite") == "T") 390} 391 392func (h *Handler) handleLock(w http.ResponseWriter, r *http.Request) (retStatus int, retErr error) { 393 duration, err := parseTimeout(r.Header.Get("Timeout")) 394 if err != nil { 395 return http.StatusBadRequest, err 396 } 397 li, status, err := readLockInfo(r.Body) 398 if err != nil { 399 return status, err 400 } 401 402 ctx := getContext(r) 403 token, ld, now, created := "", LockDetails{}, time.Now(), false 404 if li == (lockInfo{}) { 405 // An empty lockInfo means to refresh the lock. 406 ih, ok := parseIfHeader(r.Header.Get("If")) 407 if !ok { 408 return http.StatusBadRequest, errInvalidIfHeader 409 } 410 if len(ih.lists) == 1 && len(ih.lists[0].conditions) == 1 { 411 token = ih.lists[0].conditions[0].Token 412 } 413 if token == "" { 414 return http.StatusBadRequest, errInvalidLockToken 415 } 416 ld, err = h.LockSystem.Refresh(now, token, duration) 417 if err != nil { 418 if err == ErrNoSuchLock { 419 return http.StatusPreconditionFailed, err 420 } 421 return http.StatusInternalServerError, err 422 } 423 424 } else { 425 // Section 9.10.3 says that "If no Depth header is submitted on a LOCK request, 426 // then the request MUST act as if a "Depth:infinity" had been submitted." 427 depth := infiniteDepth 428 if hdr := r.Header.Get("Depth"); hdr != "" { 429 depth = parseDepth(hdr) 430 if depth != 0 && depth != infiniteDepth { 431 // Section 9.10.3 says that "Values other than 0 or infinity must not be 432 // used with the Depth header on a LOCK method". 433 return http.StatusBadRequest, errInvalidDepth 434 } 435 } 436 reqPath, status, err := h.stripPrefix(r.URL.Path) 437 if err != nil { 438 return status, err 439 } 440 ld = LockDetails{ 441 Root: reqPath, 442 Duration: duration, 443 OwnerXML: li.Owner.InnerXML, 444 ZeroDepth: depth == 0, 445 } 446 token, err = h.LockSystem.Create(now, ld) 447 if err != nil { 448 if err == ErrLocked { 449 return StatusLocked, err 450 } 451 return http.StatusInternalServerError, err 452 } 453 defer func() { 454 if retErr != nil { 455 h.LockSystem.Unlock(now, token) 456 } 457 }() 458 459 // Create the resource if it didn't previously exist. 460 if _, err := h.FileSystem.Stat(ctx, reqPath); err != nil { 461 f, err := h.FileSystem.OpenFile(ctx, reqPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) 462 if err != nil { 463 // TODO: detect missing intermediate dirs and return http.StatusConflict? 464 return http.StatusInternalServerError, err 465 } 466 f.Close() 467 created = true 468 } 469 470 // http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the 471 // Lock-Token value is a Coded-URL. We add angle brackets. 472 w.Header().Set("Lock-Token", "<"+token+">") 473 } 474 475 w.Header().Set("Content-Type", "application/xml; charset=utf-8") 476 if created { 477 // This is "w.WriteHeader(http.StatusCreated)" and not "return 478 // http.StatusCreated, nil" because we write our own (XML) response to w 479 // and Handler.ServeHTTP would otherwise write "Created". 480 w.WriteHeader(http.StatusCreated) 481 } 482 writeLockInfo(w, token, ld) 483 return 0, nil 484} 485 486func (h *Handler) handleUnlock(w http.ResponseWriter, r *http.Request) (status int, err error) { 487 // http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the 488 // Lock-Token value is a Coded-URL. We strip its angle brackets. 489 t := r.Header.Get("Lock-Token") 490 if len(t) < 2 || t[0] != '<' || t[len(t)-1] != '>' { 491 return http.StatusBadRequest, errInvalidLockToken 492 } 493 t = t[1 : len(t)-1] 494 495 switch err = h.LockSystem.Unlock(time.Now(), t); err { 496 case nil: 497 return http.StatusNoContent, err 498 case ErrForbidden: 499 return http.StatusForbidden, err 500 case ErrLocked: 501 return StatusLocked, err 502 case ErrNoSuchLock: 503 return http.StatusConflict, err 504 default: 505 return http.StatusInternalServerError, err 506 } 507} 508 509func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status int, err error) { 510 reqPath, status, err := h.stripPrefix(r.URL.Path) 511 if err != nil { 512 return status, err 513 } 514 ctx := getContext(r) 515 fi, err := h.FileSystem.Stat(ctx, reqPath) 516 if err != nil { 517 if os.IsNotExist(err) { 518 return http.StatusNotFound, err 519 } 520 return http.StatusMethodNotAllowed, err 521 } 522 depth := infiniteDepth 523 if hdr := r.Header.Get("Depth"); hdr != "" { 524 depth = parseDepth(hdr) 525 if depth == invalidDepth { 526 return http.StatusBadRequest, errInvalidDepth 527 } 528 } 529 pf, status, err := readPropfind(r.Body) 530 if err != nil { 531 return status, err 532 } 533 534 mw := multistatusWriter{w: w} 535 536 walkFn := func(reqPath string, info os.FileInfo, err error) error { 537 if err != nil { 538 return err 539 } 540 var pstats []Propstat 541 if pf.Propname != nil { 542 pnames, err := propnames(ctx, h.FileSystem, h.LockSystem, reqPath) 543 if err != nil { 544 return err 545 } 546 pstat := Propstat{Status: http.StatusOK} 547 for _, xmlname := range pnames { 548 pstat.Props = append(pstat.Props, Property{XMLName: xmlname}) 549 } 550 pstats = append(pstats, pstat) 551 } else if pf.Allprop != nil { 552 pstats, err = allprop(ctx, h.FileSystem, h.LockSystem, reqPath, pf.Prop) 553 } else { 554 pstats, err = props(ctx, h.FileSystem, h.LockSystem, reqPath, pf.Prop) 555 } 556 if err != nil { 557 return err 558 } 559 return mw.write(makePropstatResponse(path.Join(h.Prefix, reqPath), pstats)) 560 } 561 562 walkErr := walkFS(ctx, h.FileSystem, depth, reqPath, fi, walkFn) 563 closeErr := mw.close() 564 if walkErr != nil { 565 return http.StatusInternalServerError, walkErr 566 } 567 if closeErr != nil { 568 return http.StatusInternalServerError, closeErr 569 } 570 return 0, nil 571} 572 573func (h *Handler) handleProppatch(w http.ResponseWriter, r *http.Request) (status int, err error) { 574 reqPath, status, err := h.stripPrefix(r.URL.Path) 575 if err != nil { 576 return status, err 577 } 578 release, status, err := h.confirmLocks(r, reqPath, "") 579 if err != nil { 580 return status, err 581 } 582 defer release() 583 584 ctx := getContext(r) 585 586 if _, err := h.FileSystem.Stat(ctx, reqPath); err != nil { 587 if os.IsNotExist(err) { 588 return http.StatusNotFound, err 589 } 590 return http.StatusMethodNotAllowed, err 591 } 592 patches, status, err := readProppatch(r.Body) 593 if err != nil { 594 return status, err 595 } 596 pstats, err := patch(ctx, h.FileSystem, h.LockSystem, reqPath, patches) 597 if err != nil { 598 return http.StatusInternalServerError, err 599 } 600 mw := multistatusWriter{w: w} 601 writeErr := mw.write(makePropstatResponse(r.URL.Path, pstats)) 602 closeErr := mw.close() 603 if writeErr != nil { 604 return http.StatusInternalServerError, writeErr 605 } 606 if closeErr != nil { 607 return http.StatusInternalServerError, closeErr 608 } 609 return 0, nil 610} 611 612func makePropstatResponse(href string, pstats []Propstat) *response { 613 resp := response{ 614 Href: []string{(&url.URL{Path: href}).EscapedPath()}, 615 Propstat: make([]propstat, 0, len(pstats)), 616 } 617 for _, p := range pstats { 618 var xmlErr *xmlError 619 if p.XMLError != "" { 620 xmlErr = &xmlError{InnerXML: []byte(p.XMLError)} 621 } 622 resp.Propstat = append(resp.Propstat, propstat{ 623 Status: fmt.Sprintf("HTTP/1.1 %d %s", p.Status, StatusText(p.Status)), 624 Prop: p.Props, 625 ResponseDescription: p.ResponseDescription, 626 Error: xmlErr, 627 }) 628 } 629 return &resp 630} 631 632const ( 633 infiniteDepth = -1 634 invalidDepth = -2 635) 636 637// parseDepth maps the strings "0", "1" and "infinity" to 0, 1 and 638// infiniteDepth. Parsing any other string returns invalidDepth. 639// 640// Different WebDAV methods have further constraints on valid depths: 641// - PROPFIND has no further restrictions, as per section 9.1. 642// - COPY accepts only "0" or "infinity", as per section 9.8.3. 643// - MOVE accepts only "infinity", as per section 9.9.2. 644// - LOCK accepts only "0" or "infinity", as per section 9.10.3. 645// These constraints are enforced by the handleXxx methods. 646func parseDepth(s string) int { 647 switch s { 648 case "0": 649 return 0 650 case "1": 651 return 1 652 case "infinity": 653 return infiniteDepth 654 } 655 return invalidDepth 656} 657 658// http://www.webdav.org/specs/rfc4918.html#status.code.extensions.to.http11 659const ( 660 StatusMulti = 207 661 StatusUnprocessableEntity = 422 662 StatusLocked = 423 663 StatusFailedDependency = 424 664 StatusInsufficientStorage = 507 665) 666 667func StatusText(code int) string { 668 switch code { 669 case StatusMulti: 670 return "Multi-Status" 671 case StatusUnprocessableEntity: 672 return "Unprocessable Entity" 673 case StatusLocked: 674 return "Locked" 675 case StatusFailedDependency: 676 return "Failed Dependency" 677 case StatusInsufficientStorage: 678 return "Insufficient Storage" 679 } 680 return http.StatusText(code) 681} 682 683var ( 684 errDestinationEqualsSource = errors.New("webdav: destination equals source") 685 errDirectoryNotEmpty = errors.New("webdav: directory not empty") 686 errInvalidDepth = errors.New("webdav: invalid depth") 687 errInvalidDestination = errors.New("webdav: invalid destination") 688 errInvalidIfHeader = errors.New("webdav: invalid If header") 689 errInvalidLockInfo = errors.New("webdav: invalid lock info") 690 errInvalidLockToken = errors.New("webdav: invalid lock token") 691 errInvalidPropfind = errors.New("webdav: invalid propfind") 692 errInvalidProppatch = errors.New("webdav: invalid proppatch") 693 errInvalidResponse = errors.New("webdav: invalid response") 694 errInvalidTimeout = errors.New("webdav: invalid timeout") 695 errNoFileSystem = errors.New("webdav: no file system") 696 errNoLockSystem = errors.New("webdav: no lock system") 697 errNotADirectory = errors.New("webdav: not a directory") 698 errPrefixMismatch = errors.New("webdav: prefix mismatch") 699 errRecursionTooDeep = errors.New("webdav: recursion too deep") 700 errUnsupportedLockInfo = errors.New("webdav: unsupported lock info") 701 errUnsupportedMethod = errors.New("webdav: unsupported method") 702) 703