2022-06-30 10:27:26 +00:00
|
|
|
// 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 (
|
2024-03-02 06:59:55 +00:00
|
|
|
"context"
|
2022-06-30 10:27:26 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"path"
|
|
|
|
"strings"
|
|
|
|
"time"
|
2022-08-03 06:26:59 +00:00
|
|
|
|
2023-09-06 08:32:51 +00:00
|
|
|
"github.com/alist-org/alist/v3/internal/stream"
|
|
|
|
|
2022-08-03 06:26:59 +00:00
|
|
|
"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"
|
2023-04-17 07:31:39 +00:00
|
|
|
log "github.com/sirupsen/logrus"
|
2022-06-30 10:27:26 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
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
|
2022-12-02 09:59:59 +00:00
|
|
|
brw := newBufferedResponseWriter()
|
|
|
|
useBufferedWriter := true
|
2022-06-30 14:41:55 +00:00
|
|
|
if h.LockSystem == nil {
|
2022-06-30 10:27:26 +00:00
|
|
|
status, err = http.StatusInternalServerError, errNoLockSystem
|
|
|
|
} else {
|
|
|
|
switch r.Method {
|
|
|
|
case "OPTIONS":
|
2022-12-02 09:59:59 +00:00
|
|
|
status, err = h.handleOptions(brw, r)
|
2022-06-30 10:27:26 +00:00
|
|
|
case "GET", "HEAD", "POST":
|
2022-12-02 09:59:59 +00:00
|
|
|
useBufferedWriter = false
|
2022-06-30 10:27:26 +00:00
|
|
|
status, err = h.handleGetHeadPost(w, r)
|
|
|
|
case "DELETE":
|
2022-12-02 09:59:59 +00:00
|
|
|
status, err = h.handleDelete(brw, r)
|
2022-06-30 10:27:26 +00:00
|
|
|
case "PUT":
|
2022-12-02 09:59:59 +00:00
|
|
|
status, err = h.handlePut(brw, r)
|
2022-06-30 10:27:26 +00:00
|
|
|
case "MKCOL":
|
2022-12-02 09:59:59 +00:00
|
|
|
status, err = h.handleMkcol(brw, r)
|
2022-06-30 10:27:26 +00:00
|
|
|
case "COPY", "MOVE":
|
2022-12-02 09:59:59 +00:00
|
|
|
status, err = h.handleCopyMove(brw, r)
|
2022-06-30 10:27:26 +00:00
|
|
|
case "LOCK":
|
2022-12-02 09:59:59 +00:00
|
|
|
status, err = h.handleLock(brw, r)
|
2022-06-30 10:27:26 +00:00
|
|
|
case "UNLOCK":
|
2022-12-02 09:59:59 +00:00
|
|
|
status, err = h.handleUnlock(brw, r)
|
2022-06-30 10:27:26 +00:00
|
|
|
case "PROPFIND":
|
2022-12-02 09:59:59 +00:00
|
|
|
status, err = h.handlePropfind(brw, r)
|
2023-07-05 05:52:21 +00:00
|
|
|
// if there is a error for PROPFIND, we should be as an empty folder to the client
|
|
|
|
if err != nil {
|
|
|
|
status = http.StatusNotFound
|
|
|
|
}
|
2022-06-30 10:27:26 +00:00
|
|
|
case "PROPPATCH":
|
2022-12-02 09:59:59 +00:00
|
|
|
status, err = h.handleProppatch(brw, r)
|
2022-06-30 10:27:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if status != 0 {
|
|
|
|
w.WriteHeader(status)
|
|
|
|
if status != http.StatusNoContent {
|
|
|
|
w.Write([]byte(StatusText(status)))
|
|
|
|
}
|
2022-12-02 09:59:59 +00:00
|
|
|
} else if useBufferedWriter {
|
|
|
|
brw.WriteToResponse(w)
|
2022-06-30 10:27:26 +00:00
|
|
|
}
|
2022-06-30 14:41:55 +00:00
|
|
|
if h.Logger != nil && err != nil {
|
2022-06-30 10:27:26 +00:00
|
|
|
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()
|
2022-06-30 14:41:55 +00:00
|
|
|
user := ctx.Value("user").(*model.User)
|
2022-11-30 13:38:00 +00:00
|
|
|
reqPath, err = user.JoinPath(reqPath)
|
|
|
|
if err != nil {
|
|
|
|
return 403, err
|
|
|
|
}
|
2022-06-30 10:27:26 +00:00
|
|
|
allow := "OPTIONS, LOCK, PUT, MKCOL"
|
2023-04-06 16:02:07 +00:00
|
|
|
if fi, err := fs.Get(ctx, reqPath, &fs.GetArgs{}); err == nil {
|
2022-06-30 10:27:26 +00:00
|
|
|
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()
|
2022-06-30 14:41:55 +00:00
|
|
|
user := ctx.Value("user").(*model.User)
|
2022-11-30 13:38:00 +00:00
|
|
|
reqPath, err = user.JoinPath(reqPath)
|
|
|
|
if err != nil {
|
2023-09-06 08:32:51 +00:00
|
|
|
return http.StatusForbidden, err
|
2022-11-30 13:38:00 +00:00
|
|
|
}
|
2023-04-06 16:02:07 +00:00
|
|
|
fi, err := fs.Get(ctx, reqPath, &fs.GetArgs{})
|
2022-06-30 10:27:26 +00:00
|
|
|
if err != nil {
|
|
|
|
return http.StatusNotFound, err
|
|
|
|
}
|
2022-06-30 14:41:55 +00:00
|
|
|
etag, err := findETag(ctx, h.LockSystem, reqPath, fi)
|
2022-06-30 10:27:26 +00:00
|
|
|
if err != nil {
|
|
|
|
return http.StatusInternalServerError, err
|
|
|
|
}
|
|
|
|
w.Header().Set("ETag", etag)
|
2023-09-06 08:32:51 +00:00
|
|
|
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
|
|
|
|
}
|
2022-06-30 10:27:26 +00:00
|
|
|
// Let ServeContent determine the Content-Type header.
|
2023-04-06 16:02:07 +00:00
|
|
|
storage, _ := fs.GetStorage(reqPath, &fs.GetStoragesArgs{})
|
2022-11-30 12:52:33 +00:00
|
|
|
downProxyUrl := storage.GetStorage().DownProxyUrl
|
|
|
|
if storage.GetStorage().WebdavNative() || (storage.GetStorage().WebdavProxy() && downProxyUrl == "") {
|
2023-05-18 16:12:17 +00:00
|
|
|
link, _, err := fs.Link(ctx, reqPath, model.LinkArgs{Header: r.Header, HttpReq: r})
|
2022-06-30 14:41:55 +00:00
|
|
|
if err != nil {
|
|
|
|
return http.StatusInternalServerError, err
|
|
|
|
}
|
2024-05-22 15:31:42 +00:00
|
|
|
if storage.GetStorage().ProxyRange {
|
|
|
|
common.ProxyRange(link, fi.GetSize())
|
|
|
|
}
|
2022-06-30 14:41:55 +00:00
|
|
|
err = common.Proxy(w, r, link, fi)
|
|
|
|
if err != nil {
|
2023-04-17 07:31:39 +00:00
|
|
|
log.Errorf("webdav proxy error: %+v", err)
|
2022-06-30 14:41:55 +00:00
|
|
|
return http.StatusInternalServerError, err
|
|
|
|
}
|
2022-11-30 12:52:33 +00:00
|
|
|
} else if storage.GetStorage().WebdavProxy() && downProxyUrl != "" {
|
|
|
|
u := fmt.Sprintf("%s%s?sign=%s",
|
|
|
|
strings.Split(downProxyUrl, "\n")[0],
|
2022-08-12 06:51:23 +00:00
|
|
|
utils.EncodePath(reqPath, true),
|
2022-11-11 08:24:25 +00:00
|
|
|
sign.Sign(reqPath))
|
2022-11-05 07:54:51 +00:00
|
|
|
w.Header().Set("Cache-Control", "max-age=0, no-cache, no-store, must-revalidate")
|
2022-11-30 12:52:33 +00:00
|
|
|
http.Redirect(w, r, u, http.StatusFound)
|
2022-06-30 14:41:55 +00:00
|
|
|
} else {
|
2023-05-26 13:54:57 +00:00
|
|
|
link, _, err := fs.Link(ctx, reqPath, model.LinkArgs{IP: utils.ClientIP(r), Header: r.Header, HttpReq: r})
|
2022-06-30 14:41:55 +00:00
|
|
|
if err != nil {
|
|
|
|
return http.StatusInternalServerError, err
|
|
|
|
}
|
2022-11-30 12:52:33 +00:00
|
|
|
http.Redirect(w, r, link.URL, http.StatusFound)
|
2022-06-30 14:41:55 +00:00
|
|
|
}
|
2022-06-30 10:27:26 +00:00
|
|
|
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()
|
2022-06-30 14:41:55 +00:00
|
|
|
user := ctx.Value("user").(*model.User)
|
2022-11-30 13:38:00 +00:00
|
|
|
reqPath, err = user.JoinPath(reqPath)
|
|
|
|
if err != nil {
|
|
|
|
return 403, err
|
|
|
|
}
|
2022-06-30 10:27:26 +00:00
|
|
|
// 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.
|
2023-04-06 16:02:07 +00:00
|
|
|
if _, err := fs.Get(ctx, reqPath, &fs.GetArgs{}); err != nil {
|
2022-06-30 14:41:55 +00:00
|
|
|
if errs.IsObjectNotFound(err) {
|
2022-06-30 10:27:26 +00:00
|
|
|
return http.StatusNotFound, err
|
|
|
|
}
|
|
|
|
return http.StatusMethodNotAllowed, err
|
|
|
|
}
|
2022-06-30 14:41:55 +00:00
|
|
|
if err := fs.Remove(ctx, reqPath); err != nil {
|
2022-06-30 10:27:26 +00:00
|
|
|
return http.StatusMethodNotAllowed, err
|
|
|
|
}
|
2022-09-14 12:28:52 +00:00
|
|
|
//fs.ClearCache(path.Dir(reqPath))
|
2022-06-30 10:27:26 +00:00
|
|
|
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
|
|
|
|
}
|
2023-02-23 13:19:50 +00:00
|
|
|
if reqPath == "" {
|
|
|
|
return http.StatusMethodNotAllowed, nil
|
|
|
|
}
|
2022-06-30 10:27:26 +00:00
|
|
|
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()
|
2022-06-30 14:41:55 +00:00
|
|
|
user := ctx.Value("user").(*model.User)
|
2022-11-30 13:38:00 +00:00
|
|
|
reqPath, err = user.JoinPath(reqPath)
|
|
|
|
if err != nil {
|
feat: Crypt driver, improve http/webdav handling (#4884)
this PR has several enhancements, fixes, and features:
- [x] Crypt: a transparent encryption driver. Anyone can easily, and safely store encrypted data on the remote storage provider. Consider your data is safely stored in the safe, and the storage provider can only see the safe, but not your data.
- [x] Optional: compatible with [Rclone Crypt](https://rclone.org/crypt/). More ways to manipulate the encrypted data.
- [x] directory and filename encryption
- [x] server-side encryption mode (server encrypts & decrypts all data, all data flows thru the server)
- [x] obfuscate sensitive information internally
- [x] introduced a server memory-cached multi-thread downloader.
- [x] Driver: **Quark** enabled this feature, faster load in any single thread scenario. e.g. media player directly playing from the link, now it's faster.
- [x] general improvement on HTTP/WebDAV stream processing & header handling & response handling
- [x] Driver: **Mega** driver support ranged http header
- [x] Driver: **Quark** fix bug of not closing HTTP request to Quark server while user end has closed connection to alist
## Crypt, a transparent Encrypt/Decrypt Driver. (Rclone Crypt compatible)
e.g.
Crypt mount path -> /vault
Crypt remote path -> /ali/encrypted
Aliyun mount paht -> /ali
when the user uploads a.jpg to /vault, the data will be encrypted and saved to /ali/encrypted/xxxxx. And when the user wants to access a.jpg, it's automatically decrypted, and the user can do anything with it.
Since it's Rclone Crypt compatible, users can download /ali/encrypted/xxxxx and decrypt it with rclone crypt tool. Or the user can mount this folder using rclone, then mount the decrypted folder in Linux...
NB. Some breaking changes is made to make it follow global standard, e.g. processing the HTTP header properly.
close #4679
close #4827
Co-authored-by: Sean He <866155+seanhe26@users.noreply.github.com>
Co-authored-by: Andy Hsu <i@nn.ci>
2023-08-02 06:40:36 +00:00
|
|
|
return http.StatusForbidden, err
|
2022-11-30 13:38:00 +00:00
|
|
|
}
|
2022-06-30 14:41:55 +00:00
|
|
|
obj := model.Object{
|
|
|
|
Name: path.Base(reqPath),
|
|
|
|
Size: r.ContentLength,
|
2023-08-27 13:14:23 +00:00
|
|
|
Modified: h.getModTime(r),
|
|
|
|
Ctime: h.getCreateTime(r),
|
2022-06-30 10:27:26 +00:00
|
|
|
}
|
2024-04-24 09:13:30 +00:00
|
|
|
fsStream := &stream.FileStream{
|
2023-08-27 13:14:23 +00:00
|
|
|
Obj: &obj,
|
|
|
|
Reader: r.Body,
|
|
|
|
Mimetype: r.Header.Get("Content-Type"),
|
2022-06-30 10:27:26 +00:00
|
|
|
}
|
2024-04-24 09:13:30 +00:00
|
|
|
if fsStream.Mimetype == "" {
|
|
|
|
fsStream.Mimetype = utils.GetMimeType(reqPath)
|
2022-10-09 11:29:55 +00:00
|
|
|
}
|
2024-04-24 09:13:30 +00:00
|
|
|
err = fs.PutDirectly(ctx, path.Dir(reqPath), fsStream)
|
feat: Crypt driver, improve http/webdav handling (#4884)
this PR has several enhancements, fixes, and features:
- [x] Crypt: a transparent encryption driver. Anyone can easily, and safely store encrypted data on the remote storage provider. Consider your data is safely stored in the safe, and the storage provider can only see the safe, but not your data.
- [x] Optional: compatible with [Rclone Crypt](https://rclone.org/crypt/). More ways to manipulate the encrypted data.
- [x] directory and filename encryption
- [x] server-side encryption mode (server encrypts & decrypts all data, all data flows thru the server)
- [x] obfuscate sensitive information internally
- [x] introduced a server memory-cached multi-thread downloader.
- [x] Driver: **Quark** enabled this feature, faster load in any single thread scenario. e.g. media player directly playing from the link, now it's faster.
- [x] general improvement on HTTP/WebDAV stream processing & header handling & response handling
- [x] Driver: **Mega** driver support ranged http header
- [x] Driver: **Quark** fix bug of not closing HTTP request to Quark server while user end has closed connection to alist
## Crypt, a transparent Encrypt/Decrypt Driver. (Rclone Crypt compatible)
e.g.
Crypt mount path -> /vault
Crypt remote path -> /ali/encrypted
Aliyun mount paht -> /ali
when the user uploads a.jpg to /vault, the data will be encrypted and saved to /ali/encrypted/xxxxx. And when the user wants to access a.jpg, it's automatically decrypted, and the user can do anything with it.
Since it's Rclone Crypt compatible, users can download /ali/encrypted/xxxxx and decrypt it with rclone crypt tool. Or the user can mount this folder using rclone, then mount the decrypted folder in Linux...
NB. Some breaking changes is made to make it follow global standard, e.g. processing the HTTP header properly.
close #4679
close #4827
Co-authored-by: Sean He <866155+seanhe26@users.noreply.github.com>
Co-authored-by: Andy Hsu <i@nn.ci>
2023-08-02 06:40:36 +00:00
|
|
|
if errs.IsNotFoundError(err) {
|
|
|
|
return http.StatusNotFound, err
|
|
|
|
}
|
2022-06-30 14:41:55 +00:00
|
|
|
|
2023-08-27 13:14:23 +00:00
|
|
|
_ = r.Body.Close()
|
2024-04-24 09:13:30 +00:00
|
|
|
_ = fsStream.Close()
|
2022-06-30 14:41:55 +00:00
|
|
|
// TODO(rost): Returning 405 Method Not Allowed might not be appropriate.
|
|
|
|
if err != nil {
|
|
|
|
return http.StatusMethodNotAllowed, err
|
2022-06-30 10:27:26 +00:00
|
|
|
}
|
2023-04-06 16:02:07 +00:00
|
|
|
fi, err := fs.Get(ctx, reqPath, &fs.GetArgs{})
|
2022-06-30 14:41:55 +00:00
|
|
|
if err != nil {
|
2022-09-02 10:24:14 +00:00
|
|
|
fi = &obj
|
2022-06-30 10:27:26 +00:00
|
|
|
}
|
2022-06-30 14:41:55 +00:00
|
|
|
etag, err := findETag(ctx, h.LockSystem, reqPath, fi)
|
2022-06-30 10:27:26 +00:00
|
|
|
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()
|
2022-06-30 14:41:55 +00:00
|
|
|
user := ctx.Value("user").(*model.User)
|
2022-11-30 13:38:00 +00:00
|
|
|
reqPath, err = user.JoinPath(reqPath)
|
|
|
|
if err != nil {
|
|
|
|
return 403, err
|
|
|
|
}
|
2022-06-30 10:27:26 +00:00
|
|
|
|
|
|
|
if r.ContentLength > 0 {
|
|
|
|
return http.StatusUnsupportedMediaType, nil
|
|
|
|
}
|
2023-11-27 10:53:52 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
2022-06-30 14:41:55 +00:00
|
|
|
if err := fs.MakeDir(ctx, reqPath); err != nil {
|
2022-06-30 10:27:26 +00:00
|
|
|
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()
|
2022-06-30 14:41:55 +00:00
|
|
|
user := ctx.Value("user").(*model.User)
|
2022-11-30 13:38:00 +00:00
|
|
|
src, err = user.JoinPath(src)
|
|
|
|
if err != nil {
|
|
|
|
return 403, err
|
|
|
|
}
|
|
|
|
dst, err = user.JoinPath(dst)
|
|
|
|
if err != nil {
|
|
|
|
return 403, err
|
|
|
|
}
|
2022-06-30 10:27:26 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2022-06-30 14:41:55 +00:00
|
|
|
return copyFiles(ctx, src, dst, r.Header.Get("Overwrite") != "F")
|
2022-06-30 10:27:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2022-06-30 14:41:55 +00:00
|
|
|
return moveFiles(ctx, src, dst, r.Header.Get("Overwrite") == "T")
|
2022-06-30 10:27:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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()
|
2022-06-30 14:41:55 +00:00
|
|
|
user := ctx.Value("user").(*model.User)
|
2022-06-30 10:27:26 +00:00
|
|
|
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)
|
2022-11-30 13:38:00 +00:00
|
|
|
if err != nil {
|
2023-11-27 10:53:52 +00:00
|
|
|
return status, err
|
2022-11-30 13:38:00 +00:00
|
|
|
}
|
2023-11-27 10:53:52 +00:00
|
|
|
reqPath, err = user.JoinPath(reqPath)
|
2022-06-30 10:27:26 +00:00
|
|
|
if err != nil {
|
2023-11-27 10:53:52 +00:00
|
|
|
return 403, err
|
2022-06-30 10:27:26 +00:00
|
|
|
}
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2022-06-30 14:41:55 +00:00
|
|
|
// ??? 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
|
|
|
|
//}
|
2022-06-30 10:27:26 +00:00
|
|
|
|
|
|
|
// 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()
|
2024-03-02 06:59:55 +00:00
|
|
|
userAgent := r.Header.Get("User-Agent")
|
|
|
|
ctx = context.WithValue(ctx, "userAgent", userAgent)
|
2022-06-30 14:41:55 +00:00
|
|
|
user := ctx.Value("user").(*model.User)
|
2022-11-30 13:38:00 +00:00
|
|
|
reqPath, err = user.JoinPath(reqPath)
|
|
|
|
if err != nil {
|
|
|
|
return 403, err
|
|
|
|
}
|
2023-04-06 16:02:07 +00:00
|
|
|
fi, err := fs.Get(ctx, reqPath, &fs.GetArgs{})
|
2022-06-30 10:27:26 +00:00
|
|
|
if err != nil {
|
feat: Crypt driver, improve http/webdav handling (#4884)
this PR has several enhancements, fixes, and features:
- [x] Crypt: a transparent encryption driver. Anyone can easily, and safely store encrypted data on the remote storage provider. Consider your data is safely stored in the safe, and the storage provider can only see the safe, but not your data.
- [x] Optional: compatible with [Rclone Crypt](https://rclone.org/crypt/). More ways to manipulate the encrypted data.
- [x] directory and filename encryption
- [x] server-side encryption mode (server encrypts & decrypts all data, all data flows thru the server)
- [x] obfuscate sensitive information internally
- [x] introduced a server memory-cached multi-thread downloader.
- [x] Driver: **Quark** enabled this feature, faster load in any single thread scenario. e.g. media player directly playing from the link, now it's faster.
- [x] general improvement on HTTP/WebDAV stream processing & header handling & response handling
- [x] Driver: **Mega** driver support ranged http header
- [x] Driver: **Quark** fix bug of not closing HTTP request to Quark server while user end has closed connection to alist
## Crypt, a transparent Encrypt/Decrypt Driver. (Rclone Crypt compatible)
e.g.
Crypt mount path -> /vault
Crypt remote path -> /ali/encrypted
Aliyun mount paht -> /ali
when the user uploads a.jpg to /vault, the data will be encrypted and saved to /ali/encrypted/xxxxx. And when the user wants to access a.jpg, it's automatically decrypted, and the user can do anything with it.
Since it's Rclone Crypt compatible, users can download /ali/encrypted/xxxxx and decrypt it with rclone crypt tool. Or the user can mount this folder using rclone, then mount the decrypted folder in Linux...
NB. Some breaking changes is made to make it follow global standard, e.g. processing the HTTP header properly.
close #4679
close #4827
Co-authored-by: Sean He <866155+seanhe26@users.noreply.github.com>
Co-authored-by: Andy Hsu <i@nn.ci>
2023-08-02 06:40:36 +00:00
|
|
|
if errs.IsNotFoundError(err) {
|
2022-06-30 10:27:26 +00:00
|
|
|
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}
|
|
|
|
|
2022-06-30 14:41:55 +00:00
|
|
|
walkFn := func(reqPath string, info model.Obj, err error) error {
|
2022-06-30 10:27:26 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
var pstats []Propstat
|
|
|
|
if pf.Propname != nil {
|
2022-06-30 14:41:55 +00:00
|
|
|
pnames, err := propnames(ctx, h.LockSystem, info)
|
2022-06-30 10:27:26 +00:00
|
|
|
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 {
|
2022-06-30 14:41:55 +00:00
|
|
|
pstats, err = allprop(ctx, h.LockSystem, info, pf.Prop)
|
2022-06-30 10:27:26 +00:00
|
|
|
} else {
|
2022-06-30 14:41:55 +00:00
|
|
|
pstats, err = props(ctx, h.LockSystem, info, pf.Prop)
|
2022-06-30 10:27:26 +00:00
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-09-10 11:27:34 +00:00
|
|
|
href := path.Join(h.Prefix, strings.TrimPrefix(reqPath, user.BasePath))
|
2022-06-30 10:27:26 +00:00
|
|
|
if href != "/" && info.IsDir() {
|
|
|
|
href += "/"
|
|
|
|
}
|
|
|
|
return mw.write(makePropstatResponse(href, pstats))
|
|
|
|
}
|
|
|
|
|
2022-06-30 14:41:55 +00:00
|
|
|
walkErr := walkFS(ctx, depth, reqPath, fi, walkFn)
|
2022-06-30 10:27:26 +00:00
|
|
|
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()
|
2022-06-30 14:41:55 +00:00
|
|
|
user := ctx.Value("user").(*model.User)
|
2022-11-30 13:38:00 +00:00
|
|
|
reqPath, err = user.JoinPath(reqPath)
|
|
|
|
if err != nil {
|
|
|
|
return 403, err
|
|
|
|
}
|
2023-04-06 16:02:07 +00:00
|
|
|
if _, err := fs.Get(ctx, reqPath, &fs.GetArgs{}); err != nil {
|
2022-06-30 14:41:55 +00:00
|
|
|
if errs.IsObjectNotFound(err) {
|
2022-06-30 10:27:26 +00:00
|
|
|
return http.StatusNotFound, err
|
|
|
|
}
|
|
|
|
return http.StatusMethodNotAllowed, err
|
|
|
|
}
|
|
|
|
patches, status, err := readProppatch(r.Body)
|
|
|
|
if err != nil {
|
|
|
|
return status, err
|
|
|
|
}
|
2022-06-30 14:41:55 +00:00
|
|
|
pstats, err := patch(ctx, h.LockSystem, reqPath, patches)
|
2022-06-30 10:27:26 +00:00
|
|
|
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")
|
|
|
|
)
|