package app import ( "bufio" "bytes" "crypto/sha256" "encoding/json" "fmt" "hash" "io" "net" "net/http" "net/http/httptest" "os" "path/filepath" "runtime" "strconv" "strings" "sync" "testing" "time" "golang.org/x/time/rate" ) type testClient struct { a *App h http.Handler cookies map[string]*http.Cookie ip string } func fixture(t *testing.T, encrypted bool) *testClient { t.Helper() c := DefaultConfig() c.DataDir = t.TempDir() c.Encryption = encrypted c.MinFreeBytes = 0 c.DataBudget = 128 << 20 c.FileBudget = 64 << 20 a, e := Open(c) if e != nil { t.Fatal(e) } t.Cleanup(func() { a.Close() }) if e = a.SetAdmin("admin", "test-password"); e != nil { t.Fatal(e) } return &testClient{a: a, h: a.Handler(http.NotFoundHandler()), cookies: map[string]*http.Cookie{}, ip: "203.0.113.10"} } func (c *testClient) request(t *testing.T, method, path string, body []byte) *httptest.ResponseRecorder { t.Helper() r := httptest.NewRequest(method, "http://localhost:8080/api/v1"+path, bytes.NewReader(body)) r.RemoteAddr = c.ip + ":1234" if method != "GET" && method != "HEAD" { r.Header.Set("Origin", "http://localhost:8080") } for _, cookie := range c.cookies { r.AddCookie(cookie) } w := httptest.NewRecorder() c.h.ServeHTTP(w, r) for _, cookie := range w.Result().Cookies() { if cookie.MaxAge < 0 { delete(c.cookies, cookie.Name) } else { c.cookies[cookie.Name] = cookie } } return w } func (c *testClient) json(t *testing.T, method, path string, v any) *httptest.ResponseRecorder { b, e := json.Marshal(v) if e != nil { t.Fatal(e) } return c.request(t, method, path, b) } func expect(t *testing.T, w *httptest.ResponseRecorder, status int) { t.Helper() if w.Code != status { t.Fatalf("status=%d want=%d body=%s", w.Code, status, w.Body.String()) } } func (c *testClient) create(t *testing.T, names []string, data [][]byte, downloads int, renew bool) (string, []string) { t.Helper() fs := []ManifestFile{} for i, d := range data { fs = append(fs, ManifestFile{Name: names[i], Size: int64(len(d))}) } w := c.json(t, "POST", "/uploads", Manifest{Files: fs, Days: 7, Downloads: downloads, Renew: renew}) expect(t, w, 201) var result struct { ID string `json:"id"` Files []struct { ID string `json:"id"` } `json:"files"` } if e := json.Unmarshal(w.Body.Bytes(), &result); e != nil { t.Fatal(e) } ids := []string{} for _, f := range result.Files { ids = append(ids, f.ID) } return result.ID, ids } func (c *testClient) publish(t *testing.T, id string) string { w := c.json(t, "POST", "/uploads/"+id+"/publish", struct{}{}) expect(t, w, 200) var out struct { Code string `json:"code"` } json.Unmarshal(w.Body.Bytes(), &out) if len(out.Code) != 10 { t.Fatal("missing code") } return out.Code } func (c *testClient) resolve(t *testing.T, code string) { w := c.json(t, "POST", "/pickup/resolve", map[string]string{"code": code}) expect(t, w, 200) } func (c *testClient) upload(t *testing.T, id, fid string, b []byte) { expect(t, c.request(t, "PUT", "/uploads/"+id+"/files/"+fid, b), 200) } func scalar(t *testing.T, a *App, q string, args ...any) int64 { t.Helper() var n int64 if e := a.db.QueryRow(q, args...).Scan(&n); e != nil { t.Fatal(e) } return n } func TestDedupIndependentPermissionsAndCleanup(t *testing.T) { for _, encrypted := range []bool{false, true} { t.Run(map[bool]string{false: "plain", true: "age"}[encrypted], func(t *testing.T) { c := fixture(t, encrypted) data := []byte(strings.Repeat("confidential-content", 1000)) id, files := c.create(t, []string{"a.txt", "b.txt"}, [][]byte{data, data}, 3, true) for _, f := range files { c.upload(t, id, f, data) } code := c.publish(t, id) if n := scalar(t, c.a, "SELECT count(*) FROM objects"); n != 1 { t.Fatalf("dedup objects=%d", n) } var oid string c.a.db.QueryRow("SELECT id FROM objects").Scan(&oid) payload, _ := c.a.root.ReadFile(objectPath(oid)) if encrypted && bytes.Contains(payload, data) { t.Fatal("plaintext at rest") } c.resolve(t, code) w := c.request(t, "GET", "/pickup/files/"+files[0]+"/download", nil) expect(t, w, 200) if !bytes.Equal(w.Body.Bytes(), data) { t.Fatal("roundtrip mismatch") } if scalar(t, c.a, "SELECT remaining FROM files WHERE id=?", files[0]) != 2 || scalar(t, c.a, "SELECT remaining FROM files WHERE id=?", files[1]) != 3 { t.Fatal("download counters not independent") } aExpiry := scalar(t, c.a, "SELECT expires FROM files WHERE id=?", files[0]) bExpiry := scalar(t, c.a, "SELECT expires FROM files WHERE id=?", files[1]) if aExpiry-bExpiry != 3*86400 { t.Fatalf("renewal delta=%d", aExpiry-bExpiry) } _, e := c.a.db.Exec("UPDATE files SET status='deleting' WHERE id=?", files[0]) if e != nil { t.Fatal(e) } if e = c.a.Cleanup(); e != nil { t.Fatal(e) } if scalar(t, c.a, "SELECT count(*) FROM objects") != 1 { t.Fatal("deleted shared payload prematurely") } _, e = c.a.db.Exec("UPDATE files SET status='deleting' WHERE id=?", files[1]) if e != nil { t.Fatal(e) } if e = c.a.Cleanup(); e != nil { t.Fatal(e) } if scalar(t, c.a, "SELECT count(*) FROM objects") != 0 { t.Fatal("unreferenced object leaked") } if _, e = c.a.root.Stat(objectPath(oid)); !os.IsNotExist(e) { t.Fatal("payload not removed") } }) } } func TestLimitsAndOversizedBody(t *testing.T) { c := fixture(t, false) p := c.a.cfg.Policy p.MaxFiles = 1 p.MaxFileBytes = 4 p.MaxBatchBytes = 4 b, _ := json.Marshal(p) c.a.db.Exec("UPDATE settings SET json=?", string(b)) expect(t, c.json(t, "POST", "/uploads", Manifest{Files: []ManifestFile{{"a", 3}, {"b", 1}}}), 400) expect(t, c.json(t, "POST", "/uploads", Manifest{Files: []ManifestFile{{"a", 5}}}), 400) id, files := c.create(t, []string{"a"}, [][]byte{[]byte("1234")}, 2, false) expect(t, c.request(t, "PUT", "/uploads/"+id+"/files/"+files[0], []byte("12345")), 400) if scalar(t, c.a, "SELECT count(*) FROM objects") != 0 { t.Fatal("oversize object published") } expect(t, c.json(t, "POST", "/uploads/"+id+"/publish", struct{}{}), 409) c.upload(t, id, files[0], []byte("1234")) c.publish(t, id) var billed int64 c.a.db.QueryRow("SELECT bytes FROM usage WHERE subject LIKE 'ip:%'").Scan(&billed) if billed != 8 { t.Fatalf("retry traffic billed=%d want=8", billed) } } func TestConcurrentLastDownload(t *testing.T) { c := fixture(t, false) id, files := c.create(t, []string{"a"}, [][]byte{[]byte("one")}, 1, true) c.upload(t, id, files[0], []byte("one")) c.resolve(t, c.publish(t, id)) cookie := *c.cookies["bb_pickup"] var wg sync.WaitGroup codes := make(chan int, 12) for i := 0; i < 12; i++ { wg.Add(1) go func() { defer wg.Done() r := httptest.NewRequest("GET", "http://localhost:8080/api/v1/pickup/files/"+files[0]+"/download", nil) r.RemoteAddr = "203.0.113.11:1234" r.AddCookie(&cookie) w := httptest.NewRecorder() c.h.ServeHTTP(w, r) codes <- w.Code }() } wg.Wait() close(codes) success := 0 for n := range codes { if n == 200 { success++ } } if success != 1 { t.Fatalf("accepted %d downloads", success) } if scalar(t, c.a, "SELECT remaining FROM files WHERE id=?", files[0]) != 0 { t.Fatal("counter below zero or unconsumed") } if scalar(t, c.a, "SELECT expires FROM files WHERE id=?", files[0]) != scalar(t, c.a, "SELECT expires FROM parcels WHERE id=?", id) { t.Fatal("exhausted file renewed") } } func TestConcurrentCapacityReservation(t *testing.T) { c := fixture(t, false) c.a.cfg.FileBudget = reserveSize(1024, false) * 2 testRuntimeBudgets(t, c.a, c.a.cfg.FileBudget, c.a.cfg.DataBudget) c.a.cfg.Policy.IPDailyUploads = 100 c.a.rateMu.Lock() c.a.rates["upload:"+c.ip] = &rateEntry{limiter: rate.NewLimiter(rate.Inf, 100), touched: time.Now()} c.a.rateMu.Unlock() var wg sync.WaitGroup statuses := make(chan int, 10) for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() b, _ := json.Marshal(Manifest{Files: []ManifestFile{{"a", 1024}}}) r := httptest.NewRequest("POST", "http://localhost:8080/api/v1/uploads", bytes.NewReader(b)) r.Header.Set("Origin", "http://localhost:8080") r.RemoteAddr = c.ip + ":12" w := httptest.NewRecorder() c.h.ServeHTTP(w, r) statuses <- w.Code }() } wg.Wait() close(statuses) success := 0 for n := range statuses { if n == 201 { success++ } else if n != 507 { t.Fatalf("unexpected %d", n) } } if success != 2 { t.Fatalf("reserved %d sessions want2", success) } _, reserved, _, e := c.a.capacity() if e != nil || reserved != c.a.cfg.FileBudget { t.Fatal("reservation ledger inconsistent") } } func TestPasswordOwnershipAndOrigin(t *testing.T) { c := fixture(t, false) id, files := c.create(t, []string{"../../evil.html"}, [][]byte{[]byte("")}, 2, false) other := &testClient{a: c.a, h: c.h, cookies: map[string]*http.Cookie{}, ip: "203.0.113.20"} expect(t, other.request(t, "PUT", "/uploads/"+id+"/files/"+files[0], []byte("abc")), 404) c.upload(t, id, files[0], []byte("")) code := c.publish(t, id) hash, _ := hashPassword("pickup-password") c.a.db.Exec("UPDATE parcels SET password_hash=? WHERE id=?", hash, id) expect(t, c.json(t, "POST", "/pickup/resolve", map[string]string{"code": code}), 401) expect(t, c.json(t, "POST", "/pickup/resolve", map[string]string{"code": code, "password": "wrong"}), 404) expect(t, c.json(t, "POST", "/pickup/resolve", map[string]string{"code": code, "password": "pickup-password"}), 200) w := c.request(t, "GET", "/pickup/files/"+files[0]+"/download", nil) expect(t, w, 200) if !strings.HasPrefix(w.Header().Get("Content-Disposition"), "attachment") || w.Header().Get("Content-Type") != "application/octet-stream" { t.Fatal("active content served inline") } r := httptest.NewRequest("POST", "http://localhost:8080/api/v1/parcels/"+id+"/revoke", nil) r.Header.Set("Origin", "https://evil.example") r.AddCookie(c.cookies["bb_visitor"]) w = httptest.NewRecorder() c.h.ServeHTTP(w, r) expect(t, w, 403) } func TestRenewalModesAndLifetimeCeiling(t *testing.T) { for _, mode := range []string{"add", "sliding"} { t.Run(mode, func(t *testing.T) { c := fixture(t, false) id, files := c.create(t, []string{"a"}, [][]byte{[]byte("content")}, 4, true) c.upload(t, id, files[0], []byte("content")) c.resolve(t, c.publish(t, id)) expiry := scalar(t, c.a, "SELECT expires FROM files WHERE id=?", files[0]) hard := expiry + 86400 c.a.db.Exec("UPDATE parcels SET renewal_mode=? WHERE id=?", mode, id) c.a.db.Exec("UPDATE files SET hard_expires=? WHERE id=?", hard, files[0]) expect(t, c.request(t, "GET", "/pickup/files/"+files[0]+"/download", nil), 200) actual := scalar(t, c.a, "SELECT expires FROM files WHERE id=?", files[0]) expected := hard if mode == "sliding" { expected = expiry } if actual != expected { t.Fatalf("expiry=%d want%d", actual, expected) } if scalar(t, c.a, "SELECT remaining FROM files WHERE id=?", files[0]) != 3 { t.Fatal("renewal reset downloads") } }) } } type failWriter struct { header http.Header bytes int64 } func (w *failWriter) Header() http.Header { return w.header } func (w *failWriter) WriteHeader(int) {} func (w *failWriter) Write(p []byte) (int, error) { return 0, io.ErrClosedPipe } func TestInterruptedDownloadDoesNotRenew(t *testing.T) { c := fixture(t, false) id, files := c.create(t, []string{"a"}, [][]byte{[]byte("content")}, 3, true) c.upload(t, id, files[0], []byte("content")) c.resolve(t, c.publish(t, id)) expiry := scalar(t, c.a, "SELECT expires FROM files WHERE id=?", files[0]) r := httptest.NewRequest("GET", "http://localhost:8080/api/v1/pickup/files/"+files[0]+"/download", nil) r.RemoteAddr = c.ip + ":12" r.AddCookie(c.cookies["bb_pickup"]) c.h.ServeHTTP(&failWriter{header: make(http.Header)}, r) if scalar(t, c.a, "SELECT expires FROM files WHERE id=?", files[0]) != expiry { t.Fatal("failed download renewed") } if scalar(t, c.a, "SELECT remaining FROM files WHERE id=?", files[0]) != 2 { t.Fatal("failed begun download not charged") } } func TestBackupRestoreAndMissingKey(t *testing.T) { c := fixture(t, true) id, files := c.create(t, []string{"a"}, [][]byte{[]byte("content")}, 3, true) c.upload(t, id, files[0], []byte("content")) code := c.publish(t, id) config := filepath.Join(t.TempDir(), "config.toml") os.WriteFile(config, []byte("listen='127.0.0.1:8080'\n"), 0600) archive := filepath.Join(t.TempDir(), "backup.tar.gz") if e := c.a.Backup(config, archive); e != nil { t.Fatal(e) } cfg := c.a.cfg cfg.DataDir = t.TempDir() if e := Restore(cfg, archive); e != nil { t.Fatal(e) } restored, e := Open(cfg) if e != nil { t.Fatal(e) } if scalar(t, restored, "SELECT count(*) FROM objects") != 1 { t.Fatal("restore lost payload") } if scalar(t, restored, "SELECT count(*) FROM parcels WHERE state='restored' AND code_hash IS NULL") != 1 { t.Fatal("old share remains authorized") } if scalar(t, restored, "SELECT count(*) FROM parcels WHERE code_hash=?", restored.keyed(code)) != 0 { t.Fatal("old code restored") } restored.Close() os.Remove(filepath.Join(cfg.DataDir, "secrets", "age.key")) if a, e := Open(cfg); e == nil { a.Close() t.Fatal("missing encrypted identity silently regenerated") } } func TestRecoveryAndIPPersistence(t *testing.T) { c := fixture(t, false) id, files := c.create(t, []string{"a"}, [][]byte{[]byte("content")}, 3, false) c.upload(t, id, files[0], []byte("content")) cfg := c.a.cfg c.a.Close() c.a.db = nil c.a.root = nil c.a.lock = nil a, e := Open(cfg) if e != nil { t.Fatal(e) } defer a.Close() if scalar(t, a, "SELECT count(*) FROM parcels WHERE state='revoked'") != 1 || scalar(t, a, "SELECT count(*) FROM objects") != 0 { t.Fatal("partial upload became accessible or leaked") } if scalar(t, a, "SELECT uploads FROM usage WHERE subject LIKE 'ip:%'") != 1 { t.Fatal("IP daily counter reset") } r := httptest.NewRequest("GET", "http://localhost", nil) r.RemoteAddr = "203.0.113.21:80" r.Header.Set("X-Forwarded-For", "8.8.8.8") if a.remoteIP(r) != "203.0.113.21" { t.Fatal("untrusted forwarded IP accepted") } } type gatedReader struct { r *bytes.Reader once sync.Once started chan<- struct{} release <-chan struct{} } func (r *gatedReader) Read(p []byte) (int, error) { r.once.Do(func() { r.started <- struct{}{}; <-r.release }) return r.r.Read(p) } type sinkWriter struct { header http.Header status int hash hash.Hash count int64 once sync.Once started chan<- struct{} release <-chan struct{} } func (w *sinkWriter) Header() http.Header { return w.header } func (w *sinkWriter) WriteHeader(n int) { w.status = n } func (w *sinkWriter) Write(p []byte) (int, error) { w.once.Do(func() { w.started <- struct{}{}; <-w.release }) if w.status == 0 { w.status = 200 } n, e := w.hash.Write(p) w.count += int64(n) return n, e } func TestConcurrentStreamingTransfers(t *testing.T) { c := fixture(t, true) mib := 8 if v, e := strconv.Atoi(os.Getenv("BLUEBIRD_STRESS_MIB")); e == nil && v >= 1 && v <= 100 { mib = v } c.a.cfg.FileBudget = int64(mib*8+64) << 20 c.a.cfg.DataBudget = c.a.cfg.FileBudget + (64 << 20) testRuntimeBudgets(t, c.a, c.a.cfg.FileBudget, c.a.cfg.DataBudget) payload := bytes.Repeat([]byte("transfer"), mib*(1<<17)) clients := make([]*testClient, 8) ids := make([]string, 8) files := make([]string, 8) for i := range clients { clients[i] = &testClient{a: c.a, h: c.h, cookies: map[string]*http.Cookie{}, ip: fmt.Sprintf("203.0.113.%d", 40+i)} id, f := clients[i].create(t, []string{"big.bin"}, [][]byte{payload}, 20, true) ids[i], files[i] = id, f[0] } runtime.GC() var before runtime.MemStats runtime.ReadMemStats(&before) started := make(chan struct{}, 8) release := make(chan struct{}) var releaseOnce sync.Once defer releaseOnce.Do(func() { close(release) }) var wg sync.WaitGroup out := make(chan int, 8) for i := range clients { wg.Add(1) go func(i int) { defer wg.Done() r := httptest.NewRequest("PUT", "http://localhost:8080/api/v1/uploads/"+ids[i]+"/files/"+files[i], &gatedReader{r: bytes.NewReader(payload), started: started, release: release}) r.Header.Set("Origin", "http://localhost:8080") r.RemoteAddr = clients[i].ip + ":1234" for _, cookie := range clients[i].cookies { r.AddCookie(cookie) } w := httptest.NewRecorder() c.h.ServeHTTP(w, r) out <- w.Code }(i) } for range 8 { select { case <-started: case <-time.After(10 * time.Second): t.Fatal("8 upload streams did not start together") } } var duringUploads runtime.MemStats runtime.ReadMemStats(&duringUploads) uploadStart := time.Now() releaseOnce.Do(func() { close(release) }) wg.Wait() uploadDuration := time.Since(uploadStart) close(out) for status := range out { if status != 200 { t.Fatalf("upload status=%d", status) } } if scalar(t, c.a, "SELECT count(*) FROM objects") != 1 { t.Fatal("simultaneous identical content not deduplicated") } for i := range clients { clients[i].publish(t, ids[i]) } var codeHash string c.a.db.QueryRow("SELECT code_hash FROM parcels WHERE id=?", ids[0]).Scan(&codeHash) _ = codeHash code := pickupCode() c.a.db.Exec("UPDATE parcels SET code_hash=? WHERE id=?", c.a.keyed(code), ids[0]) clients[0].resolve(t, code) cookie := *clients[0].cookies["bb_pickup"] downloadStarted := make(chan struct{}, 16) downloadRelease := make(chan struct{}) var downOnce sync.Once defer downOnce.Do(func() { close(downloadRelease) }) sinks := make([]*sinkWriter, 16) for i := range sinks { sinks[i] = &sinkWriter{header: make(http.Header), hash: sha256.New(), started: downloadStarted, release: downloadRelease} wg.Add(1) go func(i int) { defer wg.Done() r := httptest.NewRequest("GET", "http://localhost:8080/api/v1/pickup/files/"+files[0]+"/download", nil) r.RemoteAddr = fmt.Sprintf("203.0.113.%d:1234", 80+i) r.AddCookie(&cookie) c.h.ServeHTTP(sinks[i], r) }(i) } for range 16 { select { case <-downloadStarted: case <-time.After(10 * time.Second): t.Fatal("16 download streams did not start together") } } var duringDownloads runtime.MemStats runtime.ReadMemStats(&duringDownloads) downloadStart := time.Now() downOnce.Do(func() { close(downloadRelease) }) wg.Wait() downloadDuration := time.Since(downloadStart) want := sha256.Sum256(payload) for _, s := range sinks { if s.status != 200 || s.count != int64(len(payload)) || !bytes.Equal(s.hash.Sum(nil), want[:]) { t.Fatal("stream output checksum/count mismatch") } } t.Logf("age enabled; 8 simultaneous ×%dMiB uploads: %s (%.1f MiB/s); 16 simultaneous ×%dMiB downloads: %s (%.1f MiB/s); heap delta at barrier: uploads=%.2fMiB downloads=%.2fMiB; in-process HTTP handlers, excludes network/TLS", mib, uploadDuration, float64(mib*8)/uploadDuration.Seconds(), mib, downloadDuration, float64(mib*16)/downloadDuration.Seconds(), float64(int64(duringUploads.Alloc)-int64(before.Alloc))/(1<<20), float64(int64(duringDownloads.Alloc)-int64(before.Alloc))/(1<<20)) } func TestInjectedCommitFailureAndDeletionFailure(t *testing.T) { c := fixture(t, true) payload := []byte("test fault recovery") id, files := c.create(t, []string{"a"}, [][]byte{payload}, 3, true) if _, e := c.a.db.Exec("CREATE TRIGGER reject_object_attach BEFORE UPDATE OF object_id ON files WHEN NEW.object_id IS NOT NULL BEGIN SELECT RAISE(ABORT,'injected commit failure'); END"); e != nil { t.Fatal(e) } expect(t, c.request(t, "PUT", "/uploads/"+id+"/files/"+files[0], payload), 500) if scalar(t, c.a, "SELECT count(*) FROM objects") != 0 { t.Fatal("rolled-back object still registered") } entries, e := os.ReadDir(filepath.Join(c.a.cfg.DataDir, "objects")) if e != nil { t.Fatal(e) } for _, d := range entries { sub, _ := os.ReadDir(filepath.Join(c.a.cfg.DataDir, "objects", d.Name())) if len(sub) > 0 { t.Fatal("failed commit left uncharged payload") } } c.a.db.Exec("DROP TRIGGER reject_object_attach") c.upload(t, id, files[0], payload) c.publish(t, id) var oid string c.a.db.QueryRow("SELECT id FROM objects").Scan(&oid) path := objectPath(oid) if e = c.a.root.Remove(path); e != nil { t.Fatal(e) } if e = c.a.root.Mkdir(path, 0700); e != nil { t.Fatal(e) } if e = c.a.root.WriteFile(path+"/blocker", []byte("x"), 0600); e != nil { t.Fatal(e) } c.a.db.Exec("UPDATE files SET status='deleting' WHERE id=?", files[0]) if e = c.a.Cleanup(); e == nil { t.Fatal("expected physical deletion failure") } if scalar(t, c.a, "SELECT count(*) FROM objects") != 1 || scalar(t, c.a, "SELECT sum(stored_bytes) FROM objects") <= 0 { t.Fatal("failed deletion incorrectly released capacity") } c.a.root.Remove(path + "/blocker") if e = c.a.Cleanup(); e != nil { t.Fatal(e) } if scalar(t, c.a, "SELECT count(*) FROM objects") != 0 { t.Fatal("retry deletion failed") } } func TestDeleteDuringUploadDoesNotRepublish(t *testing.T) { c := fixture(t, false) payload := []byte("delete while uploading") id, files := c.create(t, []string{"a"}, [][]byte{payload}, 3, false) started := make(chan struct{}, 1) release := make(chan struct{}) defer func() { select { case <-release: default: close(release) } }() r := httptest.NewRequest("PUT", "http://localhost:8080/api/v1/uploads/"+id+"/files/"+files[0], &gatedReader{r: bytes.NewReader(payload), started: started, release: release}) r.Header.Set("Origin", "http://localhost:8080") r.RemoteAddr = c.ip + ":12" r.AddCookie(c.cookies["bb_visitor"]) done := make(chan *httptest.ResponseRecorder, 1) go func() { w := httptest.NewRecorder(); c.h.ServeHTTP(w, r); done <- w }() select { case <-started: case <-time.After(5 * time.Second): t.Fatal("stream did not start") } expect(t, c.json(t, "POST", "/admin/login", map[string]string{"username": "admin", "password": "test-password"}), 200) expect(t, c.json(t, "POST", "/admin/files/"+files[0]+"/delete", struct{}{}), 200) _, reserved, _, e := c.a.capacity() if e != nil || reserved != reserveSize(int64(len(payload)), false) { t.Fatal("active deletion released upload reservation too early") } close(release) expect(t, <-done, 500) if scalar(t, c.a, "SELECT count(*) FROM files WHERE status='ready'") != 0 { t.Fatal("deleted file became ready") } if scalar(t, c.a, "SELECT count(*) FROM objects") != 0 { t.Fatal("canceled payload leaked") } if e := c.a.Cleanup(); e != nil { t.Fatal(e) } } func TestAnonymousCookieResetDoesNotResetIPQuota(t *testing.T) { c := fixture(t, false) p := c.a.cfg.Policy p.IPDailyUploads = 1 b, _ := json.Marshal(p) c.a.db.Exec("UPDATE settings SET json=?", string(b)) c.create(t, []string{"a"}, [][]byte{[]byte("x")}, 1, false) fresh := &testClient{a: c.a, h: c.h, cookies: map[string]*http.Cookie{}, ip: c.ip} expect(t, fresh.json(t, "POST", "/uploads", Manifest{Files: []ManifestFile{{Name: "a", Size: 1}}}), 429) if scalar(t, c.a, "SELECT uploads FROM usage WHERE subject LIKE 'ip:%'") != 1 { t.Fatal("daily quota mutated on rejection") } } func TestFailedRestoreCannotStart(t *testing.T) { cfg := DefaultConfig() cfg.DataDir = t.TempDir() cfg.MinFreeBytes = 0 archive := filepath.Join(t.TempDir(), "broken.tar.gz") os.WriteFile(archive, []byte("not a backup"), 0600) if e := Restore(cfg, archive); e == nil { t.Fatal("invalid archive accepted") } if a, e := Open(cfg); e == nil { a.Close() t.Fatal("partially restored service can start") } } func TestExclusiveLockAndZeroByteIntegrity(t *testing.T) { c := fixture(t, true) if another, e := Open(c.a.cfg); e == nil { another.Close() t.Fatal("second process lock succeeded") } id, files := c.create(t, []string{"empty"}, [][]byte{{}}, 2, true) c.upload(t, id, files[0], nil) c.resolve(t, c.publish(t, id)) expect(t, c.request(t, "HEAD", "/pickup/files/"+files[0]+"/download", nil), 200) if scalar(t, c.a, "SELECT remaining FROM files WHERE id=?", files[0]) != 2 { t.Fatal("HEAD consumed count") } var oid string c.a.db.QueryRow("SELECT id FROM objects").Scan(&oid) path := objectPath(oid) raw, _ := c.a.root.ReadFile(path) raw[len(raw)-1] ^= 1 c.a.root.WriteFile(path, raw, 0600) expect(t, c.request(t, "GET", "/pickup/files/"+files[0]+"/download", nil), 500) if scalar(t, c.a, "SELECT remaining FROM files WHERE id=?", files[0]) != 2 { t.Fatal("failed empty-content authentication consumed count") } } func TestSlowUploadIdleDeadline(t *testing.T) { c := fixture(t, false) id, files := c.create(t, []string{"slow"}, [][]byte{[]byte("0123456789")}, 2, false) c.a.cfg.TransferIdleSeconds = 1 c.a.cfg.MaxTransferSeconds = 10 srv := httptest.NewServer(c.h) defer srv.Close() c.a.cfg.PublicURL = srv.URL conn, e := net.Dial("tcp", strings.TrimPrefix(srv.URL, "http://")) if e != nil { t.Fatal(e) } defer conn.Close() conn.SetDeadline(time.Now().Add(5 * time.Second)) _, e = fmt.Fprintf(conn, "PUT /api/v1/uploads/%s/files/%s HTTP/1.1\r\nHost: %s\r\nOrigin: %s\r\nCookie: bb_visitor=%s\r\nContent-Length: 10\r\nConnection: close\r\n\r\nx", id, files[0], strings.TrimPrefix(srv.URL, "http://"), srv.URL, c.cookies["bb_visitor"].Value) if e != nil { t.Fatal(e) } start := time.Now() response, e := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "PUT"}) if e != nil { t.Fatal(e) } defer response.Body.Close() io.Copy(io.Discard, response.Body) if response.StatusCode != 400 || time.Since(start) > 4*time.Second { t.Fatal("idle upload was not rejected promptly") } if scalar(t, c.a, "SELECT count(*) FROM objects") != 0 { t.Fatal("slow partial content became available") } } func TestTrustedProxyChainAndIPv6(t *testing.T) { c := fixture(t, false) _, network, _ := net.ParseCIDR("10.0.0.0/8") c.a.proxies = []*net.IPNet{network} r := httptest.NewRequest("GET", "http://localhost", nil) r.RemoteAddr = "10.0.0.1:80" r.Header.Set("X-Forwarded-For", "8.8.8.8, 203.0.113.9, 10.0.0.2") if c.a.remoteIP(r) != "203.0.113.9" { t.Fatal("trusted proxy chain accepted leftmost forged IP") } r.RemoteAddr = "[2001:db8:1:2::1]:80" r.Header.Del("X-Forwarded-For") first := c.a.remoteIP(r) r.RemoteAddr = "[2001:db8:1:2::ffff]:80" if c.a.remoteIP(r) != first { t.Fatal("IPv6 rotation evaded prefix grouping") } } func testRuntimeBudgets(t *testing.T, a *App, file, data int64) { t.Helper() s, e := a.runtimeSettings() if e != nil { t.Fatal(e) } s.FileBudget = file s.DataBudget = data b, _ := json.Marshal(s) if _, e = a.db.Exec("UPDATE runtime_settings SET json=? WHERE id=1", string(b)); e != nil { t.Fatal(e) } }