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.
5 // Package webdav provides a WebDAV server implementation.
6 package webdav // import "golang.org/x/net/webdav"
21 // Prefix is the URL path prefix to strip from WebDAV resource paths.
23 // FileSystem is the virtual file system.
25 // LockSystem is the lock management system.
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)
32 func (h *Handler) stripPrefix(p string) (string, int, error) {
34 return p, http.StatusOK, nil
36 if r := strings.TrimPrefix(p, h.Prefix); len(r) < len(p) {
37 return r, http.StatusOK, nil
39 return p, http.StatusNotFound, errPrefixMismatch
42 func (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
51 status, err = h.handleOptions(w, r)
52 case "GET", "HEAD", "POST":
53 status, err = h.handleGetHeadPost(w, r)
55 status, err = h.handleDelete(w, r)
57 status, err = h.handlePut(w, r)
59 status, err = h.handleMkcol(w, r)
61 status, err = h.handleCopyMove(w, r)
63 status, err = h.handleLock(w, r)
65 status, err = h.handleUnlock(w, r)
67 status, err = h.handlePropfind(w, r)
69 status, err = h.handleProppatch(w, r)
75 if status != http.StatusNoContent {
76 w.Write([]byte(StatusText(status)))
84 func (h *Handler) lock(now time.Time, root string) (token string, status int, err error) {
85 token, err = h.LockSystem.Create(now, LockDetails{
87 Duration: infiniteTimeout,
92 return "", StatusLocked, err
94 return "", http.StatusInternalServerError, err
99 func (h *Handler) confirmLocks(r *http.Request, src, dst string) (release func(), status int, err error) {
100 hdr := r.Header.Get("If")
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(), "", ""
109 srcToken, status, err = h.lock(now, src)
111 return nil, status, err
115 dstToken, status, err = h.lock(now, dst)
118 h.LockSystem.Unlock(now, srcToken)
120 return nil, status, err
126 h.LockSystem.Unlock(now, dstToken)
129 h.LockSystem.Unlock(now, srcToken)
134 ih, ok := parseIfHeader(hdr)
136 return nil, http.StatusBadRequest, errInvalidIfHeader
138 // ih is a disjunction (OR) of ifLists, so any ifList will do.
139 for _, l := range ih.lists {
140 lsrc := l.resourceTag
144 u, err := url.Parse(lsrc)
148 if u.Host != r.Host {
151 lsrc, status, err = h.stripPrefix(u.Path)
153 return nil, status, err
156 release, err = h.LockSystem.Confirm(time.Now(), lsrc, dst, l.conditions...)
157 if err == ErrConfirmationFailed {
161 return nil, http.StatusInternalServerError, err
163 return release, 0, nil
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
172 func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request) (status int, err error) {
173 reqPath, status, err := h.stripPrefix(r.URL.Path)
178 allow := "OPTIONS, LOCK, PUT, MKCOL"
179 if fi, err := h.FileSystem.Stat(ctx, reqPath); err == nil {
181 allow = "OPTIONS, LOCK, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND"
183 allow = "OPTIONS, LOCK, GET, HEAD, POST, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND, PUT"
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")
194 func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (status int, err error) {
195 reqPath, status, err := h.stripPrefix(r.URL.Path)
199 // TODO: check locks for read-only access??
201 f, err := h.FileSystem.OpenFile(ctx, reqPath, os.O_RDONLY, 0)
203 return http.StatusNotFound, err
208 return http.StatusNotFound, err
211 return http.StatusMethodNotAllowed, nil
213 etag, err := findETag(ctx, h.FileSystem, h.LockSystem, reqPath, fi)
215 return http.StatusInternalServerError, err
217 w.Header().Set("ETag", etag)
218 // Let ServeContent determine the Content-Type header.
219 http.ServeContent(w, r, reqPath, fi.ModTime(), f)
223 func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request) (status int, err error) {
224 reqPath, status, err := h.stripPrefix(r.URL.Path)
228 release, status, err := h.confirmLocks(r, reqPath, "")
236 // TODO: return MultiStatus where appropriate.
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
245 return http.StatusMethodNotAllowed, err
247 if err := h.FileSystem.RemoveAll(ctx, reqPath); err != nil {
248 return http.StatusMethodNotAllowed, err
250 return http.StatusNoContent, nil
253 func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int, err error) {
254 reqPath, status, err := h.stripPrefix(r.URL.Path)
258 release, status, err := h.confirmLocks(r, reqPath, "")
263 // TODO(rost): Support the If-Match, If-None-Match headers? See bradfitz'
264 // comments in http.checkEtag.
267 f, err := h.FileSystem.OpenFile(ctx, reqPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
269 return http.StatusNotFound, err
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.
276 return http.StatusMethodNotAllowed, copyErr
279 return http.StatusMethodNotAllowed, statErr
282 return http.StatusMethodNotAllowed, closeErr
284 etag, err := findETag(ctx, h.FileSystem, h.LockSystem, reqPath, fi)
286 return http.StatusInternalServerError, err
288 w.Header().Set("ETag", etag)
289 return http.StatusCreated, nil
292 func (h *Handler) handleMkcol(w http.ResponseWriter, r *http.Request) (status int, err error) {
293 reqPath, status, err := h.stripPrefix(r.URL.Path)
297 release, status, err := h.confirmLocks(r, reqPath, "")
305 if r.ContentLength > 0 {
306 return http.StatusUnsupportedMediaType, nil
308 if err := h.FileSystem.Mkdir(ctx, reqPath, 0777); err != nil {
309 if os.IsNotExist(err) {
310 return http.StatusConflict, err
312 return http.StatusMethodNotAllowed, err
314 return http.StatusCreated, nil
317 func (h *Handler) handleCopyMove(w http.ResponseWriter, r *http.Request) (status int, err error) {
318 hdr := r.Header.Get("Destination")
320 return http.StatusBadRequest, errInvalidDestination
322 u, err := url.Parse(hdr)
324 return http.StatusBadRequest, errInvalidDestination
326 if u.Host != r.Host {
327 return http.StatusBadGateway, errInvalidDestination
330 src, status, err := h.stripPrefix(r.URL.Path)
335 dst, status, err := h.stripPrefix(u.Path)
341 return http.StatusBadGateway, errInvalidDestination
344 return http.StatusForbidden, errDestinationEqualsSource
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)
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
372 return copyFiles(ctx, h.FileSystem, src, dst, r.Header.Get("Overwrite") != "F", depth, 0)
375 release, status, err := h.confirmLocks(r, src, dst)
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
389 return moveFiles(ctx, h.FileSystem, src, dst, r.Header.Get("Overwrite") == "T")
392 func (h *Handler) handleLock(w http.ResponseWriter, r *http.Request) (retStatus int, retErr error) {
393 duration, err := parseTimeout(r.Header.Get("Timeout"))
395 return http.StatusBadRequest, err
397 li, status, err := readLockInfo(r.Body)
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"))
408 return http.StatusBadRequest, errInvalidIfHeader
410 if len(ih.lists) == 1 && len(ih.lists[0].conditions) == 1 {
411 token = ih.lists[0].conditions[0].Token
414 return http.StatusBadRequest, errInvalidLockToken
416 ld, err = h.LockSystem.Refresh(now, token, duration)
418 if err == ErrNoSuchLock {
419 return http.StatusPreconditionFailed, err
421 return http.StatusInternalServerError, err
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
436 reqPath, status, err := h.stripPrefix(r.URL.Path)
443 OwnerXML: li.Owner.InnerXML,
444 ZeroDepth: depth == 0,
446 token, err = h.LockSystem.Create(now, ld)
448 if err == ErrLocked {
449 return StatusLocked, err
451 return http.StatusInternalServerError, err
455 h.LockSystem.Unlock(now, token)
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)
463 // TODO: detect missing intermediate dirs and return http.StatusConflict?
464 return http.StatusInternalServerError, err
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+">")
475 w.Header().Set("Content-Type", "application/xml; charset=utf-8")
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)
482 writeLockInfo(w, token, ld)
486 func (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
495 switch err = h.LockSystem.Unlock(time.Now(), t); err {
497 return http.StatusNoContent, err
499 return http.StatusForbidden, err
501 return StatusLocked, err
503 return http.StatusConflict, err
505 return http.StatusInternalServerError, err
509 func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status int, err error) {
510 reqPath, status, err := h.stripPrefix(r.URL.Path)
515 fi, err := h.FileSystem.Stat(ctx, reqPath)
517 if os.IsNotExist(err) {
518 return http.StatusNotFound, err
520 return http.StatusMethodNotAllowed, err
522 depth := infiniteDepth
523 if hdr := r.Header.Get("Depth"); hdr != "" {
524 depth = parseDepth(hdr)
525 if depth == invalidDepth {
526 return http.StatusBadRequest, errInvalidDepth
529 pf, status, err := readPropfind(r.Body)
534 mw := multistatusWriter{w: w}
536 walkFn := func(reqPath string, info os.FileInfo, err error) error {
540 var pstats []Propstat
541 if pf.Propname != nil {
542 pnames, err := propnames(ctx, h.FileSystem, h.LockSystem, reqPath)
546 pstat := Propstat{Status: http.StatusOK}
547 for _, xmlname := range pnames {
548 pstat.Props = append(pstat.Props, Property{XMLName: xmlname})
550 pstats = append(pstats, pstat)
551 } else if pf.Allprop != nil {
552 pstats, err = allprop(ctx, h.FileSystem, h.LockSystem, reqPath, pf.Prop)
554 pstats, err = props(ctx, h.FileSystem, h.LockSystem, reqPath, pf.Prop)
559 return mw.write(makePropstatResponse(path.Join(h.Prefix, reqPath), pstats))
562 walkErr := walkFS(ctx, h.FileSystem, depth, reqPath, fi, walkFn)
563 closeErr := mw.close()
565 return http.StatusInternalServerError, walkErr
568 return http.StatusInternalServerError, closeErr
573 func (h *Handler) handleProppatch(w http.ResponseWriter, r *http.Request) (status int, err error) {
574 reqPath, status, err := h.stripPrefix(r.URL.Path)
578 release, status, err := h.confirmLocks(r, reqPath, "")
586 if _, err := h.FileSystem.Stat(ctx, reqPath); err != nil {
587 if os.IsNotExist(err) {
588 return http.StatusNotFound, err
590 return http.StatusMethodNotAllowed, err
592 patches, status, err := readProppatch(r.Body)
596 pstats, err := patch(ctx, h.FileSystem, h.LockSystem, reqPath, patches)
598 return http.StatusInternalServerError, err
600 mw := multistatusWriter{w: w}
601 writeErr := mw.write(makePropstatResponse(r.URL.Path, pstats))
602 closeErr := mw.close()
604 return http.StatusInternalServerError, writeErr
607 return http.StatusInternalServerError, closeErr
612 func makePropstatResponse(href string, pstats []Propstat) *response {
614 Href: []string{(&url.URL{Path: href}).EscapedPath()},
615 Propstat: make([]propstat, 0, len(pstats)),
617 for _, p := range pstats {
619 if p.XMLError != "" {
620 xmlErr = &xmlError{InnerXML: []byte(p.XMLError)}
622 resp.Propstat = append(resp.Propstat, propstat{
623 Status: fmt.Sprintf("HTTP/1.1 %d %s", p.Status, StatusText(p.Status)),
625 ResponseDescription: p.ResponseDescription,
637 // parseDepth maps the strings "0", "1" and "infinity" to 0, 1 and
638 // infiniteDepth. Parsing any other string returns invalidDepth.
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.
646 func parseDepth(s string) int {
658 // http://www.webdav.org/specs/rfc4918.html#status.code.extensions.to.http11
661 StatusUnprocessableEntity = 422
663 StatusFailedDependency = 424
664 StatusInsufficientStorage = 507
667 func StatusText(code int) string {
670 return "Multi-Status"
671 case StatusUnprocessableEntity:
672 return "Unprocessable Entity"
675 case StatusFailedDependency:
676 return "Failed Dependency"
677 case StatusInsufficientStorage:
678 return "Insufficient Storage"
680 return http.StatusText(code)
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")