mirror of
https://github.com/filebrowser/filebrowser.git
synced 2026-07-29 10:40:44 +00:00
A TUS PATCH rejected before its body was read left the client still streaming a chunk. net/http gives up draining an unread request body after 256KiB and then closes the connection, so the status we replied with was lost and the browser saw a bare transport error instead. The client could not tell a conflict from a network fault, retried blindly, and the upload stalled after the first chunk — while curl, which reads the response as it writes, still reported the status and appeared to work. Drain what is left of a rejected chunk so the answer arrives on a connection that stays usable. The explicit r.Body.Close() had to go with it: it ran before the drain, and the server closes the request body itself once the handler returns. Pressing Upload awaited a recursive listing of the whole destination subtree before a single byte was sent, with no sign that anything was happening. On a large destination that reads as a dead button, and the walk kept running after the client gave up, then reported the failed write as a 500. A flat upload can only collide with a direct child of the destination, so walk recursively only for folder uploads, stop the walk once the request is done, and show the action as busy while the destination is checked. Along the way: a failed upload was credited as fully sent, so the progress bar reported 100% when nothing had been written; the progress timer was recreated without clearing the previous one; and the chunk size field threw on a value with no unit and raced its own debounce on submit, so it appeared to refuse every change. Fixes #6006 Refs #5987
127 lines
4 KiB
Go
127 lines
4 KiB
Go
package fbhttp
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
gopath "path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
libErrors "github.com/filebrowser/filebrowser/v2/errors"
|
|
imgErrors "github.com/filebrowser/filebrowser/v2/img"
|
|
)
|
|
|
|
// slashClean canonicalizes a virtual path to the absolute, "/"-separated,
|
|
// lexically cleaned form that both the rule checker and the scoped filesystem
|
|
// expect.
|
|
//
|
|
// Paths reach us either verbatim from a request or built by the OS — afero.Walk
|
|
// and filepath.Join use "\" on Windows. There the filesystem resolves "\" as a
|
|
// separator while the rule checker treated it as an ordinary character, so
|
|
// "/allow\..\Secret.txt" evaded a rule for "/Secret.txt" and still opened it.
|
|
// Normalizing with the host's own separator makes both layers agree on which
|
|
// file a path names, and is a no-op on POSIX, where "\" is a legal character in
|
|
// a filename and must stay one.
|
|
func slashClean(name string) string {
|
|
return cleanSeparators(name, string(filepath.Separator))
|
|
}
|
|
|
|
// canonicalizeRequestPath rewrites the request path to its canonical virtual
|
|
// form, so that the filesystem operation, the hook, the stored share record and
|
|
// the thumbnail cache key all use the same string the rule check approved.
|
|
//
|
|
// A trailing separator is preserved: handlers distinguish "/dir/" from "/dir"
|
|
// to decide whether to create a directory, and gopath.Clean would drop it.
|
|
func canonicalizeRequestPath(r *http.Request) {
|
|
p := slashClean(r.URL.Path)
|
|
trailing := strings.HasSuffix(r.URL.Path, "/") || strings.HasSuffix(r.URL.Path, string(filepath.Separator))
|
|
if p != "/" && trailing {
|
|
p += "/"
|
|
}
|
|
r.URL.Path = p
|
|
}
|
|
|
|
// cleanSeparators is slashClean with the host separator passed in explicitly,
|
|
// so the Windows behaviour can be exercised on any platform.
|
|
func cleanSeparators(name, sep string) string {
|
|
if sep != "/" {
|
|
name = strings.ReplaceAll(name, sep, "/")
|
|
}
|
|
if name == "" || name[0] != '/' {
|
|
name = "/" + name
|
|
}
|
|
return gopath.Clean(name)
|
|
}
|
|
|
|
func renderJSON(w http.ResponseWriter, _ *http.Request, data interface{}) (int, error) {
|
|
marsh, err := json.Marshal(data)
|
|
|
|
if err != nil {
|
|
return http.StatusInternalServerError, err
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
if _, err := w.Write(marsh); err != nil {
|
|
// The status line is already on the wire, so there is no error status
|
|
// left to send: a failed write here means the client hung up. Report it
|
|
// for the log without asking the caller to write a second header.
|
|
return 0, err
|
|
}
|
|
|
|
return 0, nil
|
|
}
|
|
|
|
func errToStatus(err error) int {
|
|
switch {
|
|
case err == nil:
|
|
return http.StatusOK
|
|
case os.IsPermission(err):
|
|
return http.StatusForbidden
|
|
case os.IsNotExist(err), errors.Is(err, libErrors.ErrNotExist):
|
|
return http.StatusNotFound
|
|
case os.IsExist(err), errors.Is(err, libErrors.ErrExist):
|
|
return http.StatusConflict
|
|
case errors.Is(err, libErrors.ErrPermissionDenied):
|
|
return http.StatusForbidden
|
|
case errors.Is(err, libErrors.ErrInvalidRequestParams):
|
|
return http.StatusBadRequest
|
|
case errors.Is(err, libErrors.ErrRootUserDeletion):
|
|
return http.StatusForbidden
|
|
case errors.Is(err, imgErrors.ErrImageTooLarge):
|
|
return http.StatusRequestEntityTooLarge
|
|
default:
|
|
return http.StatusInternalServerError
|
|
}
|
|
}
|
|
|
|
// This is an adaptation if http.StripPrefix in which we don't
|
|
// return 404 if the page doesn't have the needed prefix.
|
|
func stripPrefix(prefix string, h http.Handler) http.Handler {
|
|
if prefix == "" || prefix == "/" {
|
|
return h
|
|
}
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
p := strings.TrimPrefix(r.URL.Path, prefix)
|
|
rp := strings.TrimPrefix(r.URL.RawPath, prefix)
|
|
|
|
// If the path is exactly the prefix (no trailing slash), redirect to
|
|
// the prefix with a trailing slash so the router receives "/" instead
|
|
// of "", which would otherwise cause a redirect to the site root.
|
|
if p == "" {
|
|
http.Redirect(w, r, prefix+"/", http.StatusMovedPermanently)
|
|
return
|
|
}
|
|
|
|
r2 := new(http.Request)
|
|
*r2 = *r
|
|
r2.URL = new(url.URL)
|
|
*r2.URL = *r.URL
|
|
r2.URL.Path = p
|
|
r2.URL.RawPath = rp
|
|
h.ServeHTTP(w, r2)
|
|
})
|
|
}
|