vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Bound and stream the anonymous read path
4581690d1ae9d87c272428d101a0fbe3538d7f44 · Keenan Nemetz · 13d ago · parent e05f1c48
Bound and stream the anonymous read path Every anonymous read path read a whole git object before deciding what to do with it: the code browser truncated after allocating, the overview rendered the README it had just read in full, and the site pool allocated the entire object for every request. `max_blob_bytes` allows a 100 MiB blob, so one pushed file made every concurrent request for it a 100 MiB allocation, with no account needed to ask for it. Read the header first instead. `core/git` gains `BlobSize`, `BlobSizes` (one `cat-file --batch-check` for a whole tree) and `BlobStream`, which parses the batch header before touching the payload and stops the process when a caller wants only a prefix. Then use it: `/raw` streams with the object's own `Content-Length`, the inline viewer reads a one-MiB prefix and reports the file's real size, the README is capped and named instead of rendered when it is too large, site blobs are capped at 25 MiB with a 413, and the commit and tree objects read to resolve a site path are capped separately. Metadata gets the same treatment on both sides: 4 MiB per file, 2 MiB per body, 20,000 files and 64 MiB for `issues/` and `requests/` together. The loader asks git for every size in one call and refuses the tree before reading it; the push boundary refuses the same tree, so the author learns why instead of finding that their issue list stopped loading. Counting is now per subtree, so the machine written `runs/` tree no longer eats the issue file budget.
server/pages/pages_test.go +27
| @@ -400,3 +400,30 @@ func BenchmarkPageCold(b *testing.B) { | |||
| 400 | 400 | } | |
| 401 | 401 | } | |
| 402 | 402 | } | |
| 403 | |||
| 404 | // TestOversizedPageIsRefused covers the size cap on a site blob. A site is | ||
| 405 | // served to anyone who can reach the hostname, so one pushed file must not be | ||
| 406 | // able to make every request for it allocate: the answer is a 413 that names the | ||
| 407 | // limit, and the pool keeps working afterwards. | ||
| 408 | func TestOversizedPageIsRefused(t *testing.T) { | ||
| 409 | t.Parallel() | ||
| 410 | f := newFixture(t) | ||
| 411 | f.publish(map[string][]byte{ | ||
| 412 | "index.html": []byte("<!doctype html><title>home</title>\n"), | ||
| 413 | "huge.bin": make([]byte, maxPageBytes+1), | ||
| 414 | }) | ||
| 415 | |||
| 416 | rec := f.get("site."+domain, "/huge.bin") | ||
| 417 | if rec.Code != http.StatusRequestEntityTooLarge { | ||
| 418 | t.Fatalf("oversized page = %d, want 413", rec.Code) | ||
| 419 | } | ||
| 420 | if !strings.Contains(rec.Body.String(), "larger than") { | ||
| 421 | t.Fatalf("413 did not explain itself: %q", rec.Body.String()) | ||
| 422 | } | ||
| 423 | |||
| 424 | // The refused object left a cat-file process killed mid-stream; the next read | ||
| 425 | // must start a fresh one rather than see a half-consumed stream. | ||
| 426 | if rec := f.get("site."+domain, "/"); rec.Code != http.StatusOK { | ||
| 427 | t.Fatalf("page after a refused one = %d", rec.Code) | ||
| 428 | } | ||
| 429 | } | ||