Spaces:
Running
Running
// Copyright 2014 The Go Authors. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
// Package webdav provides a WebDAV server implementation. | |
package webdav // import "golang.org/x/net/webdav" | |
import ( | |
"context" | |
"errors" | |
"fmt" | |
"net/http" | |
"net/url" | |
"os" | |
"path" | |
"strings" | |
"time" | |
"github.com/alist-org/alist/v3/internal/stream" | |
"github.com/alist-org/alist/v3/internal/errs" | |
"github.com/alist-org/alist/v3/internal/fs" | |
"github.com/alist-org/alist/v3/internal/model" | |
"github.com/alist-org/alist/v3/internal/sign" | |
"github.com/alist-org/alist/v3/pkg/utils" | |
"github.com/alist-org/alist/v3/server/common" | |
log "github.com/sirupsen/logrus" | |
) | |
type Handler struct { | |
// Prefix is the URL path prefix to strip from WebDAV resource paths. | |
Prefix string | |
// LockSystem is the lock management system. | |
LockSystem LockSystem | |
// Logger is an optional error logger. If non-nil, it will be called | |
// for all HTTP requests. | |
Logger func(*http.Request, error) | |
} | |
func (h *Handler) stripPrefix(p string) (string, int, error) { | |
if h.Prefix == "" { | |
return p, http.StatusOK, nil | |
} | |
if r := strings.TrimPrefix(p, h.Prefix); len(r) < len(p) { | |
return r, http.StatusOK, nil | |
} | |
return p, http.StatusNotFound, errPrefixMismatch | |
} | |
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |
status, err := http.StatusBadRequest, errUnsupportedMethod | |
brw := newBufferedResponseWriter() | |
useBufferedWriter := true | |
if h.LockSystem == nil { | |
status, err = http.StatusInternalServerError, errNoLockSystem | |
} else { | |
switch r.Method { | |
case "OPTIONS": | |
status, err = h.handleOptions(brw, r) | |
case "GET", "HEAD", "POST": | |
useBufferedWriter = false | |
status, err = h.handleGetHeadPost(w, r) | |
case "DELETE": | |
status, err = h.handleDelete(brw, r) | |
case "PUT": | |
status, err = h.handlePut(brw, r) | |
case "MKCOL": | |
status, err = h.handleMkcol(brw, r) | |
case "COPY", "MOVE": | |
status, err = h.handleCopyMove(brw, r) | |
case "LOCK": | |
status, err = h.handleLock(brw, r) | |
case "UNLOCK": | |
status, err = h.handleUnlock(brw, r) | |
case "PROPFIND": | |
status, err = h.handlePropfind(brw, r) | |
// if there is a error for PROPFIND, we should be as an empty folder to the client | |
if err != nil { | |
status = http.StatusNotFound | |
} | |
case "PROPPATCH": | |
status, err = h.handleProppatch(brw, r) | |
} | |
} | |
if status != 0 { | |
w.WriteHeader(status) | |
if status != http.StatusNoContent { | |
w.Write([]byte(StatusText(status))) | |
} | |
} else if useBufferedWriter { | |
brw.WriteToResponse(w) | |
} | |
if h.Logger != nil && err != nil { | |
h.Logger(r, err) | |
} | |
} | |
func (h *Handler) lock(now time.Time, root string) (token string, status int, err error) { | |
token, err = h.LockSystem.Create(now, LockDetails{ | |
Root: root, | |
Duration: infiniteTimeout, | |
ZeroDepth: true, | |
}) | |
if err != nil { | |
if err == ErrLocked { | |
return "", StatusLocked, err | |
} | |
return "", http.StatusInternalServerError, err | |
} | |
return token, 0, nil | |
} | |
func (h *Handler) confirmLocks(r *http.Request, src, dst string) (release func(), status int, err error) { | |
hdr := r.Header.Get("If") | |
if hdr == "" { | |
// An empty If header means that the client hasn't previously created locks. | |
// Even if this client doesn't care about locks, we still need to check that | |
// the resources aren't locked by another client, so we create temporary | |
// locks that would conflict with another client's locks. These temporary | |
// locks are unlocked at the end of the HTTP request. | |
now, srcToken, dstToken := time.Now(), "", "" | |
if src != "" { | |
srcToken, status, err = h.lock(now, src) | |
if err != nil { | |
return nil, status, err | |
} | |
} | |
if dst != "" { | |
dstToken, status, err = h.lock(now, dst) | |
if err != nil { | |
if srcToken != "" { | |
h.LockSystem.Unlock(now, srcToken) | |
} | |
return nil, status, err | |
} | |
} | |
return func() { | |
if dstToken != "" { | |
h.LockSystem.Unlock(now, dstToken) | |
} | |
if srcToken != "" { | |
h.LockSystem.Unlock(now, srcToken) | |
} | |
}, 0, nil | |
} | |
ih, ok := parseIfHeader(hdr) | |
if !ok { | |
return nil, http.StatusBadRequest, errInvalidIfHeader | |
} | |
// ih is a disjunction (OR) of ifLists, so any ifList will do. | |
for _, l := range ih.lists { | |
lsrc := l.resourceTag | |
if lsrc == "" { | |
lsrc = src | |
} else { | |
u, err := url.Parse(lsrc) | |
if err != nil { | |
continue | |
} | |
if u.Host != r.Host { | |
continue | |
} | |
lsrc, status, err = h.stripPrefix(u.Path) | |
if err != nil { | |
return nil, status, err | |
} | |
} | |
release, err = h.LockSystem.Confirm(time.Now(), lsrc, dst, l.conditions...) | |
if err == ErrConfirmationFailed { | |
continue | |
} | |
if err != nil { | |
return nil, http.StatusInternalServerError, err | |
} | |
return release, 0, nil | |
} | |
// Section 10.4.1 says that "If this header is evaluated and all state lists | |
// fail, then the request must fail with a 412 (Precondition Failed) status." | |
// We follow the spec even though the cond_put_corrupt_token test case from | |
// the litmus test warns on seeing a 412 instead of a 423 (Locked). | |
return nil, http.StatusPreconditionFailed, ErrLocked | |
} | |
func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request) (status int, err error) { | |
reqPath, status, err := h.stripPrefix(r.URL.Path) | |
if err != nil { | |
return status, err | |
} | |
ctx := r.Context() | |
user := ctx.Value("user").(*model.User) | |
reqPath, err = user.JoinPath(reqPath) | |
if err != nil { | |
return 403, err | |
} | |
allow := "OPTIONS, LOCK, PUT, MKCOL" | |
if fi, err := fs.Get(ctx, reqPath, &fs.GetArgs{}); err == nil { | |
if fi.IsDir() { | |
allow = "OPTIONS, LOCK, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND" | |
} else { | |
allow = "OPTIONS, LOCK, GET, HEAD, POST, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND, PUT" | |
} | |
} | |
w.Header().Set("Allow", allow) | |
// http://www.webdav.org/specs/rfc4918.html#dav.compliance.classes | |
w.Header().Set("DAV", "1, 2") | |
// http://msdn.microsoft.com/en-au/library/cc250217.aspx | |
w.Header().Set("MS-Author-Via", "DAV") | |
return 0, nil | |
} | |
func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (status int, err error) { | |
reqPath, status, err := h.stripPrefix(r.URL.Path) | |
if err != nil { | |
return status, err | |
} | |
// TODO: check locks for read-only access?? | |
ctx := r.Context() | |
user := ctx.Value("user").(*model.User) | |
reqPath, err = user.JoinPath(reqPath) | |
if err != nil { | |
return http.StatusForbidden, err | |
} | |
fi, err := fs.Get(ctx, reqPath, &fs.GetArgs{}) | |
if err != nil { | |
return http.StatusNotFound, err | |
} | |
etag, err := findETag(ctx, h.LockSystem, reqPath, fi) | |
if err != nil { | |
return http.StatusInternalServerError, err | |
} | |
w.Header().Set("ETag", etag) | |
if r.Method == http.MethodHead { | |
w.Header().Set("Content-Length", fmt.Sprintf("%d", fi.GetSize())) | |
return http.StatusOK, nil | |
} | |
if fi.IsDir() { | |
return http.StatusMethodNotAllowed, nil | |
} | |
// Let ServeContent determine the Content-Type header. | |
storage, _ := fs.GetStorage(reqPath, &fs.GetStoragesArgs{}) | |
downProxyUrl := storage.GetStorage().DownProxyUrl | |
if storage.GetStorage().WebdavNative() || (storage.GetStorage().WebdavProxy() && downProxyUrl == "") { | |
link, _, err := fs.Link(ctx, reqPath, model.LinkArgs{Header: r.Header, HttpReq: r}) | |
if err != nil { | |
return http.StatusInternalServerError, err | |
} | |
err = common.Proxy(w, r, link, fi) | |
if err != nil { | |
log.Errorf("webdav proxy error: %+v", err) | |
return http.StatusInternalServerError, err | |
} | |
} else if storage.GetStorage().WebdavProxy() && downProxyUrl != "" { | |
u := fmt.Sprintf("%s%s?sign=%s", | |
strings.Split(downProxyUrl, "\n")[0], | |
utils.EncodePath(reqPath, true), | |
sign.Sign(reqPath)) | |
w.Header().Set("Cache-Control", "max-age=0, no-cache, no-store, must-revalidate") | |
http.Redirect(w, r, u, http.StatusFound) | |
} else { | |
link, _, err := fs.Link(ctx, reqPath, model.LinkArgs{IP: utils.ClientIP(r), Header: r.Header, HttpReq: r}) | |
if err != nil { | |
return http.StatusInternalServerError, err | |
} | |
http.Redirect(w, r, link.URL, http.StatusFound) | |
} | |
return 0, nil | |
} | |
func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request) (status int, err error) { | |
reqPath, status, err := h.stripPrefix(r.URL.Path) | |
if err != nil { | |
return status, err | |
} | |
release, status, err := h.confirmLocks(r, reqPath, "") | |
if err != nil { | |
return status, err | |
} | |
defer release() | |
ctx := r.Context() | |
user := ctx.Value("user").(*model.User) | |
reqPath, err = user.JoinPath(reqPath) | |
if err != nil { | |
return 403, err | |
} | |
// TODO: return MultiStatus where appropriate. | |
// "godoc os RemoveAll" says that "If the path does not exist, RemoveAll | |
// returns nil (no error)." WebDAV semantics are that it should return a | |
// "404 Not Found". We therefore have to Stat before we RemoveAll. | |
if _, err := fs.Get(ctx, reqPath, &fs.GetArgs{}); err != nil { | |
if errs.IsObjectNotFound(err) { | |
return http.StatusNotFound, err | |
} | |
return http.StatusMethodNotAllowed, err | |
} | |
if err := fs.Remove(ctx, reqPath); err != nil { | |
return http.StatusMethodNotAllowed, err | |
} | |
//fs.ClearCache(path.Dir(reqPath)) | |
return http.StatusNoContent, nil | |
} | |
func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int, err error) { | |
reqPath, status, err := h.stripPrefix(r.URL.Path) | |
if err != nil { | |
return status, err | |
} | |
if reqPath == "" { | |
return http.StatusMethodNotAllowed, nil | |
} | |
release, status, err := h.confirmLocks(r, reqPath, "") | |
if err != nil { | |
return status, err | |
} | |
defer release() | |
// TODO(rost): Support the If-Match, If-None-Match headers? See bradfitz' | |
// comments in http.checkEtag. | |
ctx := r.Context() | |
user := ctx.Value("user").(*model.User) | |
reqPath, err = user.JoinPath(reqPath) | |
if err != nil { | |
return http.StatusForbidden, err | |
} | |
obj := model.Object{ | |
Name: path.Base(reqPath), | |
Size: r.ContentLength, | |
Modified: h.getModTime(r), | |
Ctime: h.getCreateTime(r), | |
} | |
fsStream := &stream.FileStream{ | |
Obj: &obj, | |
Reader: r.Body, | |
Mimetype: r.Header.Get("Content-Type"), | |
} | |
if fsStream.Mimetype == "" { | |
fsStream.Mimetype = utils.GetMimeType(reqPath) | |
} | |
err = fs.PutDirectly(ctx, path.Dir(reqPath), fsStream) | |
if errs.IsNotFoundError(err) { | |
return http.StatusNotFound, err | |
} | |
_ = r.Body.Close() | |
_ = fsStream.Close() | |
// TODO(rost): Returning 405 Method Not Allowed might not be appropriate. | |
if err != nil { | |
return http.StatusMethodNotAllowed, err | |
} | |
fi, err := fs.Get(ctx, reqPath, &fs.GetArgs{}) | |
if err != nil { | |
fi = &obj | |
} | |
etag, err := findETag(ctx, h.LockSystem, reqPath, fi) | |
if err != nil { | |
return http.StatusInternalServerError, err | |
} | |
w.Header().Set("ETag", etag) | |
return http.StatusCreated, nil | |
} | |
func (h *Handler) handleMkcol(w http.ResponseWriter, r *http.Request) (status int, err error) { | |
reqPath, status, err := h.stripPrefix(r.URL.Path) | |
if err != nil { | |
return status, err | |
} | |
release, status, err := h.confirmLocks(r, reqPath, "") | |
if err != nil { | |
return status, err | |
} | |
defer release() | |
ctx := r.Context() | |
user := ctx.Value("user").(*model.User) | |
reqPath, err = user.JoinPath(reqPath) | |
if err != nil { | |
return 403, err | |
} | |
if r.ContentLength > 0 { | |
return http.StatusUnsupportedMediaType, nil | |
} | |
// RFC 4918 9.3.1 | |
//405 (Method Not Allowed) - MKCOL can only be executed on an unmapped URL | |
if _, err := fs.Get(ctx, reqPath, &fs.GetArgs{}); err == nil { | |
return http.StatusMethodNotAllowed, err | |
} | |
// RFC 4918 9.3.1 | |
// 409 (Conflict) The server MUST NOT create those intermediate collections automatically. | |
reqDir := path.Dir(reqPath) | |
if _, err := fs.Get(ctx, reqDir, &fs.GetArgs{}); err != nil { | |
if errs.IsObjectNotFound(err) { | |
return http.StatusConflict, err | |
} | |
return http.StatusMethodNotAllowed, err | |
} | |
if err := fs.MakeDir(ctx, reqPath); err != nil { | |
if os.IsNotExist(err) { | |
return http.StatusConflict, err | |
} | |
return http.StatusMethodNotAllowed, err | |
} | |
return http.StatusCreated, nil | |
} | |
func (h *Handler) handleCopyMove(w http.ResponseWriter, r *http.Request) (status int, err error) { | |
hdr := r.Header.Get("Destination") | |
if hdr == "" { | |
return http.StatusBadRequest, errInvalidDestination | |
} | |
u, err := url.Parse(hdr) | |
if err != nil { | |
return http.StatusBadRequest, errInvalidDestination | |
} | |
if u.Host != "" && u.Host != r.Host { | |
return http.StatusBadGateway, errInvalidDestination | |
} | |
src, status, err := h.stripPrefix(r.URL.Path) | |
if err != nil { | |
return status, err | |
} | |
dst, status, err := h.stripPrefix(u.Path) | |
if err != nil { | |
return status, err | |
} | |
if dst == "" { | |
return http.StatusBadGateway, errInvalidDestination | |
} | |
if dst == src { | |
return http.StatusForbidden, errDestinationEqualsSource | |
} | |
ctx := r.Context() | |
user := ctx.Value("user").(*model.User) | |
src, err = user.JoinPath(src) | |
if err != nil { | |
return 403, err | |
} | |
dst, err = user.JoinPath(dst) | |
if err != nil { | |
return 403, err | |
} | |
if r.Method == "COPY" { | |
// Section 7.5.1 says that a COPY only needs to lock the destination, | |
// not both destination and source. Strictly speaking, this is racy, | |
// even though a COPY doesn't modify the source, if a concurrent | |
// operation modifies the source. However, the litmus test explicitly | |
// checks that COPYing a locked-by-another source is OK. | |
release, status, err := h.confirmLocks(r, "", dst) | |
if err != nil { | |
return status, err | |
} | |
defer release() | |
// Section 9.8.3 says that "The COPY method on a collection without a Depth | |
// header must act as if a Depth header with value "infinity" was included". | |
depth := infiniteDepth | |
if hdr := r.Header.Get("Depth"); hdr != "" { | |
depth = parseDepth(hdr) | |
if depth != 0 && depth != infiniteDepth { | |
// Section 9.8.3 says that "A client may submit a Depth header on a | |
// COPY on a collection with a value of "0" or "infinity"." | |
return http.StatusBadRequest, errInvalidDepth | |
} | |
} | |
return copyFiles(ctx, src, dst, r.Header.Get("Overwrite") != "F") | |
} | |
release, status, err := h.confirmLocks(r, src, dst) | |
if err != nil { | |
return status, err | |
} | |
defer release() | |
// Section 9.9.2 says that "The MOVE method on a collection must act as if | |
// a "Depth: infinity" header was used on it. A client must not submit a | |
// Depth header on a MOVE on a collection with any value but "infinity"." | |
if hdr := r.Header.Get("Depth"); hdr != "" { | |
if parseDepth(hdr) != infiniteDepth { | |
return http.StatusBadRequest, errInvalidDepth | |
} | |
} | |
return moveFiles(ctx, src, dst, r.Header.Get("Overwrite") == "T") | |
} | |
func (h *Handler) handleLock(w http.ResponseWriter, r *http.Request) (retStatus int, retErr error) { | |
duration, err := parseTimeout(r.Header.Get("Timeout")) | |
if err != nil { | |
return http.StatusBadRequest, err | |
} | |
li, status, err := readLockInfo(r.Body) | |
if err != nil { | |
return status, err | |
} | |
ctx := r.Context() | |
user := ctx.Value("user").(*model.User) | |
token, ld, now, created := "", LockDetails{}, time.Now(), false | |
if li == (lockInfo{}) { | |
// An empty lockInfo means to refresh the lock. | |
ih, ok := parseIfHeader(r.Header.Get("If")) | |
if !ok { | |
return http.StatusBadRequest, errInvalidIfHeader | |
} | |
if len(ih.lists) == 1 && len(ih.lists[0].conditions) == 1 { | |
token = ih.lists[0].conditions[0].Token | |
} | |
if token == "" { | |
return http.StatusBadRequest, errInvalidLockToken | |
} | |
ld, err = h.LockSystem.Refresh(now, token, duration) | |
if err != nil { | |
if err == ErrNoSuchLock { | |
return http.StatusPreconditionFailed, err | |
} | |
return http.StatusInternalServerError, err | |
} | |
} else { | |
// Section 9.10.3 says that "If no Depth header is submitted on a LOCK request, | |
// then the request MUST act as if a "Depth:infinity" had been submitted." | |
depth := infiniteDepth | |
if hdr := r.Header.Get("Depth"); hdr != "" { | |
depth = parseDepth(hdr) | |
if depth != 0 && depth != infiniteDepth { | |
// Section 9.10.3 says that "Values other than 0 or infinity must not be | |
// used with the Depth header on a LOCK method". | |
return http.StatusBadRequest, errInvalidDepth | |
} | |
} | |
reqPath, status, err := h.stripPrefix(r.URL.Path) | |
if err != nil { | |
return status, err | |
} | |
reqPath, err = user.JoinPath(reqPath) | |
if err != nil { | |
return 403, err | |
} | |
ld = LockDetails{ | |
Root: reqPath, | |
Duration: duration, | |
OwnerXML: li.Owner.InnerXML, | |
ZeroDepth: depth == 0, | |
} | |
token, err = h.LockSystem.Create(now, ld) | |
if err != nil { | |
if err == ErrLocked { | |
return StatusLocked, err | |
} | |
return http.StatusInternalServerError, err | |
} | |
defer func() { | |
if retErr != nil { | |
h.LockSystem.Unlock(now, token) | |
} | |
}() | |
// ??? Why create resource here? | |
//// Create the resource if it didn't previously exist. | |
//if _, err := h.FileSystem.Stat(ctx, reqPath); err != nil { | |
// f, err := h.FileSystem.OpenFile(ctx, reqPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) | |
// if err != nil { | |
// // TODO: detect missing intermediate dirs and return http.StatusConflict? | |
// return http.StatusInternalServerError, err | |
// } | |
// f.Close() | |
// created = true | |
//} | |
// http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the | |
// Lock-Token value is a Coded-URL. We add angle brackets. | |
w.Header().Set("Lock-Token", "<"+token+">") | |
} | |
w.Header().Set("Content-Type", "application/xml; charset=utf-8") | |
if created { | |
// This is "w.WriteHeader(http.StatusCreated)" and not "return | |
// http.StatusCreated, nil" because we write our own (XML) response to w | |
// and Handler.ServeHTTP would otherwise write "Created". | |
w.WriteHeader(http.StatusCreated) | |
} | |
writeLockInfo(w, token, ld) | |
return 0, nil | |
} | |
func (h *Handler) handleUnlock(w http.ResponseWriter, r *http.Request) (status int, err error) { | |
// http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the | |
// Lock-Token value is a Coded-URL. We strip its angle brackets. | |
t := r.Header.Get("Lock-Token") | |
if len(t) < 2 || t[0] != '<' || t[len(t)-1] != '>' { | |
return http.StatusBadRequest, errInvalidLockToken | |
} | |
t = t[1 : len(t)-1] | |
switch err = h.LockSystem.Unlock(time.Now(), t); err { | |
case nil: | |
return http.StatusNoContent, err | |
case ErrForbidden: | |
return http.StatusForbidden, err | |
case ErrLocked: | |
return StatusLocked, err | |
case ErrNoSuchLock: | |
return http.StatusConflict, err | |
default: | |
return http.StatusInternalServerError, err | |
} | |
} | |
func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status int, err error) { | |
reqPath, status, err := h.stripPrefix(r.URL.Path) | |
if err != nil { | |
return status, err | |
} | |
ctx := r.Context() | |
userAgent := r.Header.Get("User-Agent") | |
ctx = context.WithValue(ctx, "userAgent", userAgent) | |
user := ctx.Value("user").(*model.User) | |
reqPath, err = user.JoinPath(reqPath) | |
if err != nil { | |
return 403, err | |
} | |
fi, err := fs.Get(ctx, reqPath, &fs.GetArgs{}) | |
if err != nil { | |
if errs.IsNotFoundError(err) { | |
return http.StatusNotFound, err | |
} | |
return http.StatusMethodNotAllowed, err | |
} | |
depth := infiniteDepth | |
if hdr := r.Header.Get("Depth"); hdr != "" { | |
depth = parseDepth(hdr) | |
if depth == invalidDepth { | |
return http.StatusBadRequest, errInvalidDepth | |
} | |
} | |
pf, status, err := readPropfind(r.Body) | |
if err != nil { | |
return status, err | |
} | |
mw := multistatusWriter{w: w} | |
walkFn := func(reqPath string, info model.Obj, err error) error { | |
if err != nil { | |
return err | |
} | |
var pstats []Propstat | |
if pf.Propname != nil { | |
pnames, err := propnames(ctx, h.LockSystem, info) | |
if err != nil { | |
return err | |
} | |
pstat := Propstat{Status: http.StatusOK} | |
for _, xmlname := range pnames { | |
pstat.Props = append(pstat.Props, Property{XMLName: xmlname}) | |
} | |
pstats = append(pstats, pstat) | |
} else if pf.Allprop != nil { | |
pstats, err = allprop(ctx, h.LockSystem, info, pf.Prop) | |
} else { | |
pstats, err = props(ctx, h.LockSystem, info, pf.Prop) | |
} | |
if err != nil { | |
return err | |
} | |
href := path.Join(h.Prefix, strings.TrimPrefix(reqPath, user.BasePath)) | |
if href != "/" && info.IsDir() { | |
href += "/" | |
} | |
return mw.write(makePropstatResponse(href, pstats)) | |
} | |
walkErr := walkFS(ctx, depth, reqPath, fi, walkFn) | |
closeErr := mw.close() | |
if walkErr != nil { | |
return http.StatusInternalServerError, walkErr | |
} | |
if closeErr != nil { | |
return http.StatusInternalServerError, closeErr | |
} | |
return 0, nil | |
} | |
func (h *Handler) handleProppatch(w http.ResponseWriter, r *http.Request) (status int, err error) { | |
reqPath, status, err := h.stripPrefix(r.URL.Path) | |
if err != nil { | |
return status, err | |
} | |
release, status, err := h.confirmLocks(r, reqPath, "") | |
if err != nil { | |
return status, err | |
} | |
defer release() | |
ctx := r.Context() | |
user := ctx.Value("user").(*model.User) | |
reqPath, err = user.JoinPath(reqPath) | |
if err != nil { | |
return 403, err | |
} | |
if _, err := fs.Get(ctx, reqPath, &fs.GetArgs{}); err != nil { | |
if errs.IsObjectNotFound(err) { | |
return http.StatusNotFound, err | |
} | |
return http.StatusMethodNotAllowed, err | |
} | |
patches, status, err := readProppatch(r.Body) | |
if err != nil { | |
return status, err | |
} | |
pstats, err := patch(ctx, h.LockSystem, reqPath, patches) | |
if err != nil { | |
return http.StatusInternalServerError, err | |
} | |
mw := multistatusWriter{w: w} | |
writeErr := mw.write(makePropstatResponse(r.URL.Path, pstats)) | |
closeErr := mw.close() | |
if writeErr != nil { | |
return http.StatusInternalServerError, writeErr | |
} | |
if closeErr != nil { | |
return http.StatusInternalServerError, closeErr | |
} | |
return 0, nil | |
} | |
func makePropstatResponse(href string, pstats []Propstat) *response { | |
resp := response{ | |
Href: []string{(&url.URL{Path: href}).EscapedPath()}, | |
Propstat: make([]propstat, 0, len(pstats)), | |
} | |
for _, p := range pstats { | |
var xmlErr *xmlError | |
if p.XMLError != "" { | |
xmlErr = &xmlError{InnerXML: []byte(p.XMLError)} | |
} | |
resp.Propstat = append(resp.Propstat, propstat{ | |
Status: fmt.Sprintf("HTTP/1.1 %d %s", p.Status, StatusText(p.Status)), | |
Prop: p.Props, | |
ResponseDescription: p.ResponseDescription, | |
Error: xmlErr, | |
}) | |
} | |
return &resp | |
} | |
const ( | |
infiniteDepth = -1 | |
invalidDepth = -2 | |
) | |
// parseDepth maps the strings "0", "1" and "infinity" to 0, 1 and | |
// infiniteDepth. Parsing any other string returns invalidDepth. | |
// | |
// Different WebDAV methods have further constraints on valid depths: | |
// - PROPFIND has no further restrictions, as per section 9.1. | |
// - COPY accepts only "0" or "infinity", as per section 9.8.3. | |
// - MOVE accepts only "infinity", as per section 9.9.2. | |
// - LOCK accepts only "0" or "infinity", as per section 9.10.3. | |
// | |
// These constraints are enforced by the handleXxx methods. | |
func parseDepth(s string) int { | |
switch s { | |
case "0": | |
return 0 | |
case "1": | |
return 1 | |
case "infinity": | |
return infiniteDepth | |
} | |
return invalidDepth | |
} | |
// http://www.webdav.org/specs/rfc4918.html#status.code.extensions.to.http11 | |
const ( | |
StatusMulti = 207 | |
StatusUnprocessableEntity = 422 | |
StatusLocked = 423 | |
StatusFailedDependency = 424 | |
StatusInsufficientStorage = 507 | |
) | |
func StatusText(code int) string { | |
switch code { | |
case StatusMulti: | |
return "Multi-Status" | |
case StatusUnprocessableEntity: | |
return "Unprocessable Entity" | |
case StatusLocked: | |
return "Locked" | |
case StatusFailedDependency: | |
return "Failed Dependency" | |
case StatusInsufficientStorage: | |
return "Insufficient Storage" | |
} | |
return http.StatusText(code) | |
} | |
var ( | |
errDestinationEqualsSource = errors.New("webdav: destination equals source") | |
errDirectoryNotEmpty = errors.New("webdav: directory not empty") | |
errInvalidDepth = errors.New("webdav: invalid depth") | |
errInvalidDestination = errors.New("webdav: invalid destination") | |
errInvalidIfHeader = errors.New("webdav: invalid If header") | |
errInvalidLockInfo = errors.New("webdav: invalid lock info") | |
errInvalidLockToken = errors.New("webdav: invalid lock token") | |
errInvalidPropfind = errors.New("webdav: invalid propfind") | |
errInvalidProppatch = errors.New("webdav: invalid proppatch") | |
errInvalidResponse = errors.New("webdav: invalid response") | |
errInvalidTimeout = errors.New("webdav: invalid timeout") | |
errNoFileSystem = errors.New("webdav: no file system") | |
errNoLockSystem = errors.New("webdav: no lock system") | |
errNotADirectory = errors.New("webdav: not a directory") | |
errPrefixMismatch = errors.New("webdav: prefix mismatch") | |
errRecursionTooDeep = errors.New("webdav: recursion too deep") | |
errUnsupportedLockInfo = errors.New("webdav: unsupported lock info") | |
errUnsupportedMethod = errors.New("webdav: unsupported method") | |
) | |