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 := r.Context()
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 := r.Context()
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 := r.Context()
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 := r.Context()
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 := r.Context()
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 != "" && 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 := r.Context()
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 := r.Context()
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 := r.Context()
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		href := path.Join(h.Prefix, reqPath)
560		if href != "/" && info.IsDir() {
561			href += "/"
562		}
563		return mw.write(makePropstatResponse(href, pstats))
564	}
565
566	walkErr := walkFS(ctx, h.FileSystem, depth, reqPath, fi, walkFn)
567	closeErr := mw.close()
568	if walkErr != nil {
569		return http.StatusInternalServerError, walkErr
570	}
571	if closeErr != nil {
572		return http.StatusInternalServerError, closeErr
573	}
574	return 0, nil
575}
576
577func (h *Handler) handleProppatch(w http.ResponseWriter, r *http.Request) (status int, err error) {
578	reqPath, status, err := h.stripPrefix(r.URL.Path)
579	if err != nil {
580		return status, err
581	}
582	release, status, err := h.confirmLocks(r, reqPath, "")
583	if err != nil {
584		return status, err
585	}
586	defer release()
587
588	ctx := r.Context()
589
590	if _, err := h.FileSystem.Stat(ctx, reqPath); err != nil {
591		if os.IsNotExist(err) {
592			return http.StatusNotFound, err
593		}
594		return http.StatusMethodNotAllowed, err
595	}
596	patches, status, err := readProppatch(r.Body)
597	if err != nil {
598		return status, err
599	}
600	pstats, err := patch(ctx, h.FileSystem, h.LockSystem, reqPath, patches)
601	if err != nil {
602		return http.StatusInternalServerError, err
603	}
604	mw := multistatusWriter{w: w}
605	writeErr := mw.write(makePropstatResponse(r.URL.Path, pstats))
606	closeErr := mw.close()
607	if writeErr != nil {
608		return http.StatusInternalServerError, writeErr
609	}
610	if closeErr != nil {
611		return http.StatusInternalServerError, closeErr
612	}
613	return 0, nil
614}
615
616func makePropstatResponse(href string, pstats []Propstat) *response {
617	resp := response{
618		Href:     []string{(&url.URL{Path: href}).EscapedPath()},
619		Propstat: make([]propstat, 0, len(pstats)),
620	}
621	for _, p := range pstats {
622		var xmlErr *xmlError
623		if p.XMLError != "" {
624			xmlErr = &xmlError{InnerXML: []byte(p.XMLError)}
625		}
626		resp.Propstat = append(resp.Propstat, propstat{
627			Status:              fmt.Sprintf("HTTP/1.1 %d %s", p.Status, StatusText(p.Status)),
628			Prop:                p.Props,
629			ResponseDescription: p.ResponseDescription,
630			Error:               xmlErr,
631		})
632	}
633	return &resp
634}
635
636const (
637	infiniteDepth = -1
638	invalidDepth  = -2
639)
640
641// parseDepth maps the strings "0", "1" and "infinity" to 0, 1 and
642// infiniteDepth. Parsing any other string returns invalidDepth.
643//
644// Different WebDAV methods have further constraints on valid depths:
645//	- PROPFIND has no further restrictions, as per section 9.1.
646//	- COPY accepts only "0" or "infinity", as per section 9.8.3.
647//	- MOVE accepts only "infinity", as per section 9.9.2.
648//	- LOCK accepts only "0" or "infinity", as per section 9.10.3.
649// These constraints are enforced by the handleXxx methods.
650func parseDepth(s string) int {
651	switch s {
652	case "0":
653		return 0
654	case "1":
655		return 1
656	case "infinity":
657		return infiniteDepth
658	}
659	return invalidDepth
660}
661
662// http://www.webdav.org/specs/rfc4918.html#status.code.extensions.to.http11
663const (
664	StatusMulti               = 207
665	StatusUnprocessableEntity = 422
666	StatusLocked              = 423
667	StatusFailedDependency    = 424
668	StatusInsufficientStorage = 507
669)
670
671func StatusText(code int) string {
672	switch code {
673	case StatusMulti:
674		return "Multi-Status"
675	case StatusUnprocessableEntity:
676		return "Unprocessable Entity"
677	case StatusLocked:
678		return "Locked"
679	case StatusFailedDependency:
680		return "Failed Dependency"
681	case StatusInsufficientStorage:
682		return "Insufficient Storage"
683	}
684	return http.StatusText(code)
685}
686
687var (
688	errDestinationEqualsSource = errors.New("webdav: destination equals source")
689	errDirectoryNotEmpty       = errors.New("webdav: directory not empty")
690	errInvalidDepth            = errors.New("webdav: invalid depth")
691	errInvalidDestination      = errors.New("webdav: invalid destination")
692	errInvalidIfHeader         = errors.New("webdav: invalid If header")
693	errInvalidLockInfo         = errors.New("webdav: invalid lock info")
694	errInvalidLockToken        = errors.New("webdav: invalid lock token")
695	errInvalidPropfind         = errors.New("webdav: invalid propfind")
696	errInvalidProppatch        = errors.New("webdav: invalid proppatch")
697	errInvalidResponse         = errors.New("webdav: invalid response")
698	errInvalidTimeout          = errors.New("webdav: invalid timeout")
699	errNoFileSystem            = errors.New("webdav: no file system")
700	errNoLockSystem            = errors.New("webdav: no lock system")
701	errNotADirectory           = errors.New("webdav: not a directory")
702	errPrefixMismatch          = errors.New("webdav: prefix mismatch")
703	errRecursionTooDeep        = errors.New("webdav: recursion too deep")
704	errUnsupportedLockInfo     = errors.New("webdav: unsupported lock info")
705	errUnsupportedMethod       = errors.New("webdav: unsupported method")
706)
707