github.com/Andyfoo/golang/x/net@v0.0.0-20190901054642-57c1bf301704/webdav/webdav.go (about)

     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.
     6  package webdav // import "github.com/Andyfoo/golang/x/net/webdav"
     7  
     8  import (
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"net/http"
    13  	"net/url"
    14  	"os"
    15  	"path"
    16  	"strings"
    17  	"time"
    18  )
    19  
    20  type 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  
    32  func (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  
    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
    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  
    84  func (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  
    99  func (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  
   172  func (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  
   194  func (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  
   223  func (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  
   253  func (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  
   292  func (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  
   317  func (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 := 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  
   392  func (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  
   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
   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  
   509  func (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  
   577  func (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  
   616  func 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  
   636  const (
   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.
   650  func 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
   663  const (
   664  	StatusMulti               = 207
   665  	StatusUnprocessableEntity = 422
   666  	StatusLocked              = 423
   667  	StatusFailedDependency    = 424
   668  	StatusInsufficientStorage = 507
   669  )
   670  
   671  func 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  
   687  var (
   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  )