Skip to content
quarry Artifact [abc284b2ac]
Login

Artifact [abc284b2ac]

Artifact abc284b2ac7b10e4a8c12cba0bdc77bb13af1fdfb8c2636ca31a8c0846747c15:


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()
}

// gated is a Fronter with the push gate on, so the request is decoded,
// drawing on a budget of its own rather than the process's.
func gated() *Fronter {
	f := New()
	f.budget = &decodeBudget{size: requestBudget.size}
	f.SetAuth(secrets{})
	return f
}

// postRaw sends raw to f and reports the answer, how much the handler
// allocated, and how long it took.
func postRaw(t *testing.T, f *Fronter, raw []byte) (*httptest.ResponseRecorder, uint64, time.Duration) {
	t.Helper()
	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, gated(), 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, gated(), 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, gated(), 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, gated(), 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 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) {
	f := gated()
	held := f.budget.size - maxDecoded/2
	if !f.budget.take(held) {
		t.Fatal("the budget was not free at the start")
	}
	raw := bomb(t, maxDecoded, 16)
	w, _, _ := postRaw(t, f, 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"))
	}
	f.budget.give(held)
	w, _, _ = postRaw(t, f, 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 f.budget.used != 0 {
		t.Errorf("FAIL: %d bytes still reserved after the requests finished", f.budget.used)
	}
}

// An over-claim costs the budget only what it really inflated to, so a
// few tiny containers each claiming the limit cannot pin it.
func TestOnlyDecodedBytesStayReserved(t *testing.T) {
	f := gated()
	var during int64 = -1
	f.SetReadGate(func(*http.Request, string, string) bool {
		during = f.budget.used
		return false
	})
	postRaw(t, f, bomb(t, maxDecoded, 16))
	if during != 16 {
		t.Errorf("FAIL: a 16-byte stream claiming %d held %d of the budget, want 16", maxDecoded, during)
	}
}

// The relay to the child lasts as long as the client takes to read the
// reply, so nothing is reserved while it runs.
func TestTheBudgetIsFreeWhileTheReplyIsRelayed(t *testing.T) {
	f := gated()
	defer f.Close()
	srv, _ := fakeChild(t, f, "alpha")
	var during int64 = -1
	srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		during = f.budget.used
		w.Header().Set("Content-Type", "application/x-fossil")
		w.Write([]byte("pragma ok\n"))
	})
	w, _, _ := postRaw(t, f, encodeContainer([]byte("pull SRV PRJ\n")))
	if w.Code != http.StatusOK {
		t.Fatalf("the pull answered %d", w.Code)
	}
	if during != 0 {
		t.Errorf("FAIL: %d bytes were reserved while the child answered, want 0", during)
	}
}

// With no gate nothing reads the body, so nothing inflates it: only the
// claim is checked.
func TestWithoutAGateTheBodyIsNotInflated(t *testing.T) {
	f := New()
	f.budget = &decodeBudget{size: requestBudget.size}
	w, grew, _ := postRaw(t, f, bomb(t, 1<<10, 128<<20))
	if w.Code == http.StatusRequestEntityTooLarge || grew > 32<<20 {
		t.Errorf("FAIL: an ungated request answered %d and allocated %d MiB", w.Code, grew>>20)
	}
	if w, _, _ := postRaw(t, f, bomb(t, 0xFFFFFFFF, 16)); w.Code != http.StatusRequestEntityTooLarge {
		t.Errorf("FAIL: an ungated 4 GiB claim answered %d, want 413", w.Code)
	}
}

// Nothing parsed from the body may keep it alive through the relay: a
// login card, verified or not, used to hold a slice of the decoded body
// until the tally ran, so a slow reader kept it while the budget said it
// was free.
func TestTheDecodedBodyIsUnreachableDuringTheRelay(t *testing.T) {
	f := gated()
	defer f.Close()
	f.SetCounted(func(string, Reader) {})
	srv, _ := fakeChild(t, f, "alpha")
	var heap uint64
	srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		runtime.GC()
		var m runtime.MemStats
		runtime.ReadMemStats(&m)
		heap = m.HeapAlloc
		w.Header().Set("Content-Type", "application/x-fossil")
		w.Write([]byte("pragma ok\n"))
	})
	const filler = 64 << 20
	body := append([]byte("login bob 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000\npull SRV PRJ\n"), make([]byte, filler)...)
	raw := encodeContainer(body)
	body = nil
	w, _, _ := postRaw(t, f, raw)
	if w.Code != http.StatusOK {
		t.Fatalf("the pull answered %d", w.Code)
	}
	if heap > filler/2 {
		t.Errorf("FAIL: %d MiB live while the child answered; the decoded body is %d MiB", heap>>20, filler>>20)
	}
}