alist/server/webdav/webdav.go

809 lines
23 KiB
Go
Raw Normal View History

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 (
"errors"
"fmt"
"net/http"
"net/url"
"os"
"path"
"strings"
"time"
2022-08-03 06:26:59 +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"
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
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":
status, err = h.handleOptions(brw, r)
2022-06-30 10:27:26 +00:00
case "GET", "HEAD", "POST":
useBufferedWriter = false
2022-06-30 10:27:26 +00:00
status, err = h.handleGetHeadPost(w, r)
case "DELETE":
status, err = h.handleDelete(brw, r)
2022-06-30 10:27:26 +00:00
case "PUT":
status, err = h.handlePut(brw, r)
2022-06-30 10:27:26 +00:00
case "MKCOL":
status, err = h.handleMkcol(brw, r)
2022-06-30 10:27:26 +00:00
case "COPY", "MOVE":
status, err = h.handleCopyMove(brw, r)
2022-06-30 10:27:26 +00:00
case "LOCK":
status, err = h.handleLock(brw, r)
2022-06-30 10:27:26 +00:00
case "UNLOCK":
status, err = h.handleUnlock(brw, r)
2022-06-30 10:27:26 +00:00
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
}
2022-06-30 10:27:26 +00:00
case "PROPPATCH":
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)))
}
} 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)
reqPath, err = user.JoinPath(reqPath)
if err != nil {
return 403, err
}
2022-06-30 10:27:26 +00:00
allow := "OPTIONS, LOCK, PUT, MKCOL"
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)
reqPath, err = user.JoinPath(reqPath)
if err != nil {
return http.StatusForbidden, err
}
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)
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.
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})
2022-06-30 14:41:55 +00:00
if err != nil {
return http.StatusInternalServerError, err
}
err = common.Proxy(w, r, link, fi)
if err != nil {
log.Errorf("webdav proxy error: %+v", err)
2022-06-30 14:41:55 +00:00
return http.StatusInternalServerError, err
}
} 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),
sign.Sign(reqPath))
w.Header().Set("Cache-Control", "max-age=0, no-cache, no-store, must-revalidate")
http.Redirect(w, r, u, http.StatusFound)
2022-06-30 14:41:55 +00:00
} else {
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
}
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)
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.
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
}
//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
}
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)
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-06-30 14:41:55 +00:00
obj := model.Object{
Name: path.Base(reqPath),
Size: r.ContentLength,
Modified: h.getModTime(r),
Ctime: h.getCreateTime(r),
2022-06-30 10:27:26 +00:00
}
stream := &stream.FileStream{
Obj: &obj,
Reader: r.Body,
Mimetype: r.Header.Get("Content-Type"),
2022-06-30 10:27:26 +00:00
}
if stream.Mimetype == "" {
stream.Mimetype = utils.GetMimeType(reqPath)
}
2022-06-30 14:41:55 +00:00
err = fs.PutDirectly(ctx, path.Dir(reqPath), stream)
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
_ = r.Body.Close()
_ = stream.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
}
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)
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
}
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)
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)
reqPath, err = user.JoinPath(reqPath)
if err != nil {
return 403, err
}
2022-06-30 10:27:26 +00:00
if err != nil {
return status, 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)
}
}()
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()
2022-06-30 14:41:55 +00:00
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{})
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
}
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)
reqPath, err = user.JoinPath(reqPath)
if err != nil {
return 403, err
}
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")
)