mirror of
https://github.com/soypat/lneto.git
synced 2026-08-22 07:29:04 +00:00
more httpraw bug fixes (#160)
* more httpraw bug fixes * add http-linux example and add httpraw.Header.SetInt * finish http-linux example * fix CI vet
This commit is contained in:
committed by
Patricio Whittingslow
parent
347cef9ba5
commit
1a474154cb
@@ -240,3 +240,186 @@ func TestHeaderSetBytesEmptyValue(t *testing.T) {
|
||||
t.Errorf("want empty value, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// buffer/offset past uint16 range silently corrupts or panics.
|
||||
// A header block > 64KiB pushes a field's offset past 65535. tokint truncation
|
||||
// makes Get return the wrong window (silent corruption) or panic on slice bounds.
|
||||
func TestHeader_LargeBufferOverflow(t *testing.T) {
|
||||
const wantVal = "the-canary-value"
|
||||
// Pad with a big header so the canary field lands past offset 65535.
|
||||
pad := strings.Repeat("x", 70000)
|
||||
raw := "GET / HTTP/1.1\r\n" +
|
||||
"X-Pad: " + pad + "\r\n" +
|
||||
"X-Canary: " + wantVal + "\r\n" +
|
||||
"\r\n"
|
||||
|
||||
var h Header
|
||||
err := h.ParseBytes(false, []byte(raw))
|
||||
if err != nil {
|
||||
// Clean rejection of the oversized header is the intended behavior:
|
||||
// no panic, no silent corruption.
|
||||
return
|
||||
}
|
||||
// If it did parse, the value must be correct (never a truncated-offset window).
|
||||
got := string(h.Get("X-Canary"))
|
||||
if got != wantVal {
|
||||
t.Fatalf("overflow corruption: want X-Canary %q, got %q", wantVal, got)
|
||||
}
|
||||
}
|
||||
|
||||
// a complete but malformed header line with no colon must be a hard error,
|
||||
// not errNeedMore (which makes a streaming parser wait forever).
|
||||
func TestHeader_ColonlessLineIsHardError(t *testing.T) {
|
||||
raw := "GET / HTTP/1.1\r\nBadHeaderNoColon\r\n\r\n"
|
||||
var h Header
|
||||
err := h.ParseBytes(false, []byte(raw))
|
||||
if err == nil {
|
||||
t.Fatal("want error on colonless header line, got nil")
|
||||
}
|
||||
if err == errNeedMore {
|
||||
t.Fatalf("colonless line reported as errNeedMore (parser would hang); want a hard error like errInvalidName")
|
||||
}
|
||||
}
|
||||
|
||||
// Regression for a TCP split landing BEFORE the colon (no newline yet) must
|
||||
// stay "need more data" and parse once the rest arrives. This is the case the
|
||||
// Colonless fix must NOT turn into a hard error.
|
||||
func TestHeader_SplitBeforeColonStillParses(t *testing.T) {
|
||||
const part1 = "GET / HTTP/1.1\r\nHost" // split mid-key, before colon+newline
|
||||
const part2 = ": example.com\r\n\r\n"
|
||||
|
||||
var h Header
|
||||
h.Reset(nil)
|
||||
if _, err := h.ReadFromBytes([]byte(part1)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
needMore, err := h.TryParse(false)
|
||||
if err != nil && err != errNeedMore {
|
||||
t.Fatalf("split before colon: want errNeedMore/nil, got %v", err)
|
||||
}
|
||||
if !needMore {
|
||||
t.Fatal("want needMoreData=true after partial input")
|
||||
}
|
||||
|
||||
if _, err := h.ReadFromBytes([]byte(part2)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
needMore, err = h.TryParse(false)
|
||||
if err != nil {
|
||||
t.Fatalf("after full input: %v", err)
|
||||
}
|
||||
if needMore {
|
||||
t.Fatal("want needMoreData=false after full input")
|
||||
}
|
||||
if got := string(h.Get("Host")); got != "example.com" {
|
||||
t.Fatalf("want Host %q, got %q", "example.com", got)
|
||||
}
|
||||
}
|
||||
|
||||
// appendHeader must reserve the +1 byte that mustAppendSlice consumes on an
|
||||
// empty buffer. Force cap == len(key)+len(value) to defeat allocator rounding.
|
||||
func TestHeader_AppendHeaderExactCapNoPanic(t *testing.T) {
|
||||
const key, value = "K", "V"
|
||||
buf := make([]byte, 0, len(key)+len(value)) // exact cap, no slack.
|
||||
var h Header
|
||||
h.Reset(buf)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("appendHeader panicked on exact-cap buffer: %v", r)
|
||||
}
|
||||
}()
|
||||
h.Add(key, value)
|
||||
if got := string(h.Get(key)); got != value {
|
||||
t.Fatalf("want %q, got %q", value, got)
|
||||
}
|
||||
}
|
||||
|
||||
// Add/Set on a full buffer with growth disabled must drop gracefully (flag OOM),
|
||||
// never panic. Panicking is unacceptable for this package.
|
||||
func TestHeader_AddFullBufferNoPanic(t *testing.T) {
|
||||
buf := make([]byte, 0, 40) // Small cap; enough for Reset (len 0) but not the field below.
|
||||
var h Header
|
||||
h.Reset(buf)
|
||||
h.EnableBufferGrowth(false)
|
||||
h.SetMethod("GET")
|
||||
h.SetRequestURI("/")
|
||||
h.SetProtocol("HTTP/1.1")
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("Add on full no-grow buffer panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
h.Add("X-Very-Long-Header-Key", "a-value-that-cannot-possibly-fit-in-the-buffer")
|
||||
|
||||
// Graceful drop: request marshalling reports OOM instead of emitting bad data.
|
||||
if _, err := h.AppendRequest(nil); err == nil {
|
||||
t.Fatal("want OOM error after dropped Add, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeader_SetInt(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
value int64
|
||||
base int
|
||||
want string
|
||||
}{
|
||||
{"decimal", 1234, 10, "1234"},
|
||||
{"zero", 0, 10, "0"},
|
||||
{"negative", -42, 10, "-42"},
|
||||
{"maxint64", 9223372036854775807, 10, "9223372036854775807"},
|
||||
{"minint64", -9223372036854775808, 10, "-9223372036854775808"},
|
||||
{"hex", 255, 16, "ff"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var h Header
|
||||
h.Reset(nil)
|
||||
h.SetInt("Content-Length", tc.value, tc.base)
|
||||
if got := string(h.Get("Content-Length")); got != tc.want {
|
||||
t.Fatalf("want %q, got %q", tc.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// SetInt on an existing key must reuse the slot in place (single field, latest value).
|
||||
func TestHeader_SetIntOverwrite(t *testing.T) {
|
||||
var h Header
|
||||
h.Reset(nil)
|
||||
h.SetMethod("GET")
|
||||
h.SetRequestURI("/")
|
||||
h.SetProtocol("HTTP/1.1")
|
||||
|
||||
h.SetInt("Content-Length", 100, 10)
|
||||
h.SetInt("Content-Length", 5, 10) // shorter, must fit in old slot.
|
||||
|
||||
if got := string(h.Get("Content-Length")); got != "5" {
|
||||
t.Fatalf("want Content-Length %q, got %q", "5", got)
|
||||
}
|
||||
req, err := h.AppendRequest(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n := strings.Count(string(req), "Content-Length:"); n != 1 {
|
||||
t.Errorf("want 1 Content-Length field, got %d:\n%s", n, req)
|
||||
}
|
||||
}
|
||||
|
||||
// SetInt must not heap-allocate: it must format directly into the header buffer.
|
||||
func TestHeader_SetIntNoAlloc(t *testing.T) {
|
||||
buf := make([]byte, 0, 256)
|
||||
var h Header
|
||||
h.Reset(buf)
|
||||
h.EnableBufferGrowth(false)
|
||||
h.Add("Content-Length", "0000000000000000000000") // pre-size a reusable slot.
|
||||
allocs := testing.AllocsPerRun(100, func() {
|
||||
h.SetInt("Content-Length", 1234567890, 10)
|
||||
})
|
||||
if allocs != 0 {
|
||||
t.Fatalf("SetInt allocated %v times, want 0", allocs)
|
||||
}
|
||||
if got := string(h.Get("Content-Length")); got != "1234567890" {
|
||||
t.Fatalf("want %q, got %q", "1234567890", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user