fix: preserve CHFS shared redirect compatibility
This commit is contained in:
@@ -76,7 +76,17 @@ func (s *Server) Handler() http.Handler {
|
|||||||
mux.HandleFunc("/chfs/shared/", s.shared)
|
mux.HandleFunc("/chfs/shared/", s.shared)
|
||||||
mux.HandleFunc("/chfs/preview", s.preview)
|
mux.HandleFunc("/chfs/preview", s.preview)
|
||||||
mux.HandleFunc("/", s.fileHandler)
|
mux.HandleFunc("/", s.fileHandler)
|
||||||
return s.ipFilter(mux)
|
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Go's ServeMux canonicalizes a subtree route before dispatching.
|
||||||
|
// CHFS v4 deliberately returns a relative 301 from /chfs/shared, so
|
||||||
|
// intercept the exact path before ServeMux gets a chance to rewrite it.
|
||||||
|
if r.URL.Path == "/chfs/shared" {
|
||||||
|
s.shared(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mux.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
return s.ipFilter(h)
|
||||||
}
|
}
|
||||||
func account(r *http.Request, s *Server) *auth.Account {
|
func account(r *http.Request, s *Server) *auth.Account {
|
||||||
c, err := r.Cookie("JWT")
|
c, err := r.Cookie("JWT")
|
||||||
@@ -659,7 +669,9 @@ func (s *Server) shared(w http.ResponseWriter, r *http.Request) {
|
|||||||
if r.URL.RawQuery != "" {
|
if r.URL.RawQuery != "" {
|
||||||
location += "?" + r.URL.RawQuery
|
location += "?" + r.URL.RawQuery
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, location, http.StatusMovedPermanently)
|
w.Header().Set("Content-Disposition", `attachment;filename="shared"`)
|
||||||
|
w.Header().Set("Location", location)
|
||||||
|
w.WriteHeader(http.StatusMovedPermanently)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
p := strings.Trim(strings.TrimPrefix(r.URL.Path, "/chfs/shared/"), "/")
|
p := strings.Trim(strings.TrimPrefix(r.URL.Path, "/chfs/shared/"), "/")
|
||||||
@@ -808,6 +820,7 @@ func (s *Server) downloadDir(w http.ResponseWriter, r *http.Request) {
|
|||||||
name = "download"
|
name = "download"
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/zip")
|
w.Header().Set("Content-Type", "application/zip")
|
||||||
|
// CHFS v4 does not emit Content-Disposition for directory archives.
|
||||||
zw := zip.NewWriter(w)
|
zw := zip.NewWriter(w)
|
||||||
defer zw.Close()
|
defer zw.Close()
|
||||||
var walk func(string, string) error
|
var walk func(string, string) error
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"chfs-ng/internal/auth"
|
||||||
|
"chfs-ng/internal/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSharedRoutesMatchCHFS(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
if err := os.WriteFile(filepath.Join(root, "hello.txt"), []byte("hello"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
s := &Server{Store: storage.Local{Root: root}, Auth: &auth.Manager{Accounts: map[string]*auth.Account{}, Guest: &auth.Account{Mask: auth.Read | auth.Write | auth.Delete}}}
|
||||||
|
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/chfs/shared?file=hello.txt", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.Handler().ServeHTTP(w, r)
|
||||||
|
if w.Code != http.StatusMovedPermanently || w.Header().Get("Location") != "shared/?file=hello.txt" {
|
||||||
|
t.Fatalf("shared redirect: status=%d location=%q", w.Code, w.Header().Get("Location"))
|
||||||
|
}
|
||||||
|
|
||||||
|
r = httptest.NewRequest(http.MethodGet, "/chfs/shared/hello.txt", nil)
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
s.Handler().ServeHTTP(w, r)
|
||||||
|
if w.Code != http.StatusOK || w.Header().Get("Content-Disposition") != `attachment;filename="hello.txt"` {
|
||||||
|
t.Fatalf("shared file: status=%d disposition=%q", w.Code, w.Header().Get("Content-Disposition"))
|
||||||
|
}
|
||||||
|
body, _ := io.ReadAll(w.Result().Body)
|
||||||
|
if string(body) != "hello" {
|
||||||
|
t.Fatalf("shared body=%q", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloadDirArchiveHeadersAndEntries(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
dir := filepath.Join(root, "pack")
|
||||||
|
if err := os.Mkdir(dir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "a.txt"), []byte("abc"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
s := &Server{Store: storage.Local{Root: root}, Auth: &auth.Manager{Accounts: map[string]*auth.Account{}, Guest: &auth.Account{Mask: auth.Read | auth.Write | auth.Delete}}}
|
||||||
|
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/chfs/downloaddir/pack", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.Handler().ServeHTTP(w, r)
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("archive status=%d body=%s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
if got := w.Header().Get("Content-Type"); got != "application/zip" {
|
||||||
|
t.Fatalf("archive content type=%q", got)
|
||||||
|
}
|
||||||
|
if got := w.Header().Get("Content-Disposition"); got != "" {
|
||||||
|
t.Fatalf("CHFS archive must not emit Content-Disposition, got %q", got)
|
||||||
|
}
|
||||||
|
zr, err := zip.NewReader(bytesReaderAt{r: w.Body.Bytes()}, int64(w.Body.Len()))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open archive: %v", err)
|
||||||
|
}
|
||||||
|
if len(zr.File) != 1 || zr.File[0].Name != "a.txt" {
|
||||||
|
t.Fatalf("archive entries=%v", archiveNames(zr.File))
|
||||||
|
}
|
||||||
|
if zr.File[0].UncompressedSize64 != 3 {
|
||||||
|
t.Fatalf("archive size=%d", zr.File[0].UncompressedSize64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type bytesReaderAt struct{ r []byte }
|
||||||
|
|
||||||
|
func (b bytesReaderAt) ReadAt(p []byte, off int64) (int, error) {
|
||||||
|
if off >= int64(len(b.r)) {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
n := copy(p, b.r[off:])
|
||||||
|
if n < len(p) {
|
||||||
|
return n, io.EOF
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
func archiveNames(files []*zip.File) []string {
|
||||||
|
n := make([]string, 0, len(files))
|
||||||
|
for _, f := range files {
|
||||||
|
n = append(n, f.Name)
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user