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