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/preview", s.preview)
|
||||
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 {
|
||||
c, err := r.Cookie("JWT")
|
||||
@@ -659,7 +669,9 @@ func (s *Server) shared(w http.ResponseWriter, r *http.Request) {
|
||||
if 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
|
||||
}
|
||||
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"
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/zip")
|
||||
// CHFS v4 does not emit Content-Disposition for directory archives.
|
||||
zw := zip.NewWriter(w)
|
||||
defer zw.Close()
|
||||
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