Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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 }