package xfer
import (
"bytes"
"compress/zlib"
"encoding/binary"
"net/http"
"net/http/httptest"
"runtime"
"testing"
"time"
)
// bomb is a container claiming claim bytes whose zlib stream inflates to
// n zeros. The zeros are streamed into the compressor, so building one
// costs the test almost nothing whatever n is.
func bomb(t *testing.T, claim uint32, n int64) []byte {
t.Helper()
var buf bytes.Buffer
var lenb [4]byte
binary.BigEndian.PutUint32(lenb[:], claim)
buf.Write(lenb[:])
zw, err := zlib.NewWriterLevel(&buf, zlib.BestCompression)
if err != nil {
t.Fatal(err)
}
zeros := make([]byte, 1<<20)
for left := n; left > 0; left -= int64(len(zeros)) {
if left < int64(len(zeros)) {
zeros = zeros[:left]
}
zw.Write(zeros)
}
zw.Close()
return buf.Bytes()
}
// postRaw sends raw to a Fronter with the push gate on, so the old code
// path that decoded a request before any auth is the one exercised.
func postRaw(t *testing.T, raw []byte) (*httptest.ResponseRecorder, uint64, time.Duration) {
t.Helper()
f := New()
f.SetAuth(secrets{})
req := httptest.NewRequest(http.MethodPost, "/r/alpha", bytes.NewReader(raw))
req.SetPathValue("repo", "alpha")
req.Header.Set("Content-Type", "application/x-fossil")
w := httptest.NewRecorder()
runtime.GC()
var before, after runtime.MemStats
runtime.ReadMemStats(&before)
start := time.Now()
f.ServeHTTP(w, req)
took := time.Since(start)
runtime.ReadMemStats(&after)
return w, after.TotalAlloc - before.TotalAlloc, took
}
// An anonymous request claiming 4 GiB is refused on its claim, before a
// byte of it is inflated. Before this, the front inflated it in full,
// several times per request, ahead of any authentication.
func TestAContainerClaimingGigabytesIsRefusedWithoutInflating(t *testing.T) {
raw := bomb(t, 0xFFFFFFFF, 128<<20)
w, grew, took := postRaw(t, raw)
if w.Code != http.StatusRequestEntityTooLarge {
t.Errorf("FAIL: a 4 GiB claim answered %d, want 413", w.Code)
} else {
t.Log("ok: a 4 GiB claim is refused 413")
}
if grew > 32<<20 {
t.Errorf("FAIL: refusing it allocated %d MiB", grew>>20)
} else {
t.Logf("ok: refusing it allocated %d KiB in %v", grew>>10, took)
}
}
// A claim just past the decoded limit is refused, however small the
// stream behind it really is.
func TestAClaimJustPastTheDecodedLimitIsRefused(t *testing.T) {
w, _, _ := postRaw(t, bomb(t, 2*maxRequest+1, 16))
if w.Code != http.StatusRequestEntityTooLarge {
t.Errorf("FAIL: a claim one past the limit answered %d, want 413", w.Code)
}
}
// A container whose stream inflates past what it claimed is cut at the
// claim and refused, rather than handed to the child as plaintext for
// the child to inflate in full.
func TestAContainerInflatingPastItsClaimIsRefused(t *testing.T) {
w, grew, _ := postRaw(t, bomb(t, 1<<10, 128<<20))
if w.Code != http.StatusRequestEntityTooLarge {
t.Errorf("FAIL: a lying claim answered %d, want 413", w.Code)
}
if grew > 32<<20 {
t.Errorf("FAIL: refusing it allocated %d MiB", grew>>20)
}
}
// An ordinary container is decoded and goes on to the gates: here the
// repository is unknown, so the answer is 404, not a size refusal.
func TestAnOrdinaryContainerIsNotRefusedForSize(t *testing.T) {
w, _, _ := postRaw(t, encodeContainer([]byte("pragma client-version 1\npull SRV PRJ\n")))
if w.Code == http.StatusRequestEntityTooLarge || w.Code == http.StatusServiceUnavailable {
t.Errorf("FAIL: an ordinary container answered %d", w.Code)
}
}
// Decodes in flight share one process-wide budget. A request whose claim
// does not fit is told to come back, before anything is inflated, and the
// budget is whole again once the requests holding it finish.
func TestDecodesPastTheBudgetAreToldToRetry(t *testing.T) {
held := requestBudget.size - maxDecoded/2
if !requestBudget.take(held) {
t.Fatal("the budget was not free at the start")
}
raw := bomb(t, maxDecoded, 16)
w, _, _ := postRaw(t, raw)
if w.Code != http.StatusServiceUnavailable || w.Header().Get("Retry-After") == "" {
t.Errorf("FAIL: a decode past the budget answered %d (Retry-After %q), want 503 with one", w.Code, w.Header().Get("Retry-After"))
}
requestBudget.give(held)
w, _, _ = postRaw(t, raw)
if w.Code == http.StatusServiceUnavailable || w.Code == http.StatusRequestEntityTooLarge {
t.Errorf("FAIL: with the budget free the same request answered %d", w.Code)
}
if requestBudget.used != 0 {
t.Errorf("FAIL: %d bytes still reserved after the requests finished", requestBudget.used)
}
}