package httpapi import ( "context" "net/http/httptest" "os" "path/filepath" "testing" "github.com/go-chi/chi/v5" ) // buildDownloadsRouter mounts DownloadsHandler on a real chi router (mirrors // how main.go mounts it under "/downloads/*") so the wildcard param behaves // exactly as it does in production. func buildDownloadsRouter(dir string) chi.Router { h := NewDownloadsHandler(dir) r := chi.NewRouter() r.Get("/downloads/*", h.Serve) return r } func TestDownloadsHandler_ServesExistingFile(t *testing.T) { dir := t.TempDir() content := []byte("hello pangolin apk bytes") if err := os.WriteFile(filepath.Join(dir, "pangolin-android.apk"), content, 0o644); err != nil { t.Fatalf("write fixture: %v", err) } r := buildDownloadsRouter(dir) req := httptest.NewRequest("GET", "/downloads/pangolin-android.apk", nil) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != 200 { t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String()) } if got := rec.Body.String(); got != string(content) { t.Errorf("body = %q, want %q", got, content) } } func TestDownloadsHandler_MissingFile404(t *testing.T) { dir := t.TempDir() r := buildDownloadsRouter(dir) req := httptest.NewRequest("GET", "/downloads/nope.exe", nil) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != 404 { t.Fatalf("status = %d, want 404", rec.Code) } } func TestDownloadsHandler_BareDirNotListed(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "pangolin-android.apk"), []byte("x"), 0o644); err != nil { t.Fatalf("write fixture: %v", err) } r := buildDownloadsRouter(dir) req := httptest.NewRequest("GET", "/downloads/", nil) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != 404 { t.Fatalf("bare dir status = %d, want 404 (no directory listing)", rec.Code) } } func TestDownloadsHandler_PathTraversalBlocked(t *testing.T) { outerDir := t.TempDir() secretPath := filepath.Join(outerDir, "secret.txt") if err := os.WriteFile(secretPath, []byte("top secret"), 0o600); err != nil { t.Fatalf("write secret: %v", err) } dir := filepath.Join(outerDir, "downloads") if err := os.Mkdir(dir, 0o755); err != nil { t.Fatalf("mkdir downloads: %v", err) } if err := os.WriteFile(filepath.Join(dir, "pangolin-android.apk"), []byte("apk"), 0o644); err != nil { t.Fatalf("write fixture: %v", err) } r := buildDownloadsRouter(dir) // net/http's ServeMux/chi normalize ".." segments in the URL path before // routing, so we exercise the handler directly with a raw URLParam to // simulate any escape attempt that might otherwise reach it, in addition // to the router-level request below. h := NewDownloadsHandler(dir) req := httptest.NewRequest("GET", "/downloads/../secret.txt", nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("*", "../secret.txt") ctx := context.WithValue(req.Context(), chi.RouteCtxKey, rctx) req = req.WithContext(ctx) rec := httptest.NewRecorder() h.Serve(rec, req) if rec.Code != 404 { t.Fatalf("direct traversal status = %d, want 404 (must not escape dir)", rec.Code) } if rec.Body.String() == "top secret" { t.Fatalf("traversal leaked secret file contents") } // Router-level request: most HTTP clients/servers collapse ".." during URL // normalization, but confirm the end-to-end path also can't reach the file // outside dir and doesn't 200 with the secret's contents. req2 := httptest.NewRequest("GET", "/downloads/../secret.txt", nil) rec2 := httptest.NewRecorder() r.ServeHTTP(rec2, req2) if rec2.Body.String() == "top secret" { t.Fatalf("router-level traversal leaked secret file contents (status=%d)", rec2.Code) } }