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/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0: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/web/codebrowser.go +22−9

@@ -10,8 +10,10 @@ package web
10 10 import (
11 11 "fmt"
12 12 "html/template"
13 "io"
13 14 "net/http"
14 15 "sort"
16 "strconv"
15 17 "strings"
16 18 "unicode/utf8"
17 19
@@ -193,23 +195,34 @@ func (w *Web) blob(rw http.ResponseWriter, r *http.Request, raw bool) {
193 195 w.notFound(rw, r)
194 196 return
195 197 }
196 data, err := g.BlobAt(commit, p)
198 stream, err := g.OpenBlobAt(commit, p)
197 199 if err != nil {
198 200 w.notFound(rw, r)
199 201 return
200 202 }
203 defer stream.Close()
201 204 if raw {
202 rw.Header().Set("Content-Type", "text/plain; charset=utf-8")
203 rw.Write(data)
205 // The download is the one place that should carry every byte, so it is
206 // streamed rather than read and written: a 100 MiB asset costs one copy
207 // buffer per request, not 100 MiB of heap.
208 h := rw.Header()
209 h.Set("Content-Type", "text/plain; charset=utf-8")
210 h.Set("Content-Length", strconv.FormatInt(stream.Size, 10))
211 if _, err := io.Copy(rw, stream); err != nil {
212 // The client hung up mid-download; headers are already out.
213 w.Logger("raw %s %s: %v", m.Name, p, err)
214 }
215 return
216 }
217 // Everything the page shows is a preview: the tree, the line numbers and the
218 // highlighter are all bounded, so there is no reason to hold the whole file.
219 data, truncated, err := stream.ReadPrefix(maxInlineBytes)
220 if err != nil {
221 w.serverError(rw, r, "cannot read "+p+": "+err.Error())
204 222 return
205 223 }
206 224 content := string(data)
207 225 binary := !utf8.Valid(data)
208 truncated := false
209 if len(data) > maxInlineBytes {
210 content = string(data[:maxInlineBytes])
211 truncated = true
212 }
213 226 // A table row per line is pleasant to read and cheap for a source file, but a
214 227 // million blank lines would be neither. Past the cap the page falls back to a
215 228 // preformatted block.
@@ -249,7 +262,7 @@ func (w *Web) blob(rw http.ResponseWriter, r *http.Request, raw bool) {
249 262 view.LineCut = lineCut
250 263 view.Lines = lines
251 264 view.Highlight = highlighted
252 view.Size = int64(len(data))
265 view.Size = stream.Size
253 266 view.IsMarkdown = markdown
254 267 view.Preview = preview
255 268 if !full {