Spaces:
Paused
Paused
| // 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), | |
| } | |
| stream := &stream.FileStream{ | |
| Obj: &obj, | |
| Reader: r.Body, | |
| Mimetype: r.Header.Get("Content-Type"), | |
| } | |
| if stream.Mimetype == "" { | |
| stream.Mimetype = utils.GetMimeType(reqPath) | |
| } | |
| err = fs.PutDirectly(ctx, path.Dir(reqPath), stream) | |
| if errs.IsNotFoundError(err) { | |
| return http.StatusNotFound, err | |
| } | |
| _ = r.Body.Close() | |
| _ = stream.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") | |
| ) | |