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