package proxy import ( "reflect" "testing" ) func TestParseMarkers(t *testing.T) { tests := []struct { name string template string wantPos []IntrudePosition wantOut string wantErr bool }{ { name: "single position", template: "GET /users/§123§ HTTP/1.1", wantPos: []IntrudePosition{{Index: 0, Base: "123"}}, wantOut: "GET /users/123 HTTP/1.1", }, { name: "two positions", template: "GET /a/§1§/b/§2§ HTTP/1.1", wantPos: []IntrudePosition{{Index: 0, Base: "1"}, {Index: 1, Base: "2"}}, wantOut: "GET /a/1/b/2 HTTP/1.1", }, { name: "no markers", template: "GET / HTTP/1.1", wantPos: nil, wantOut: "GET / HTTP/1.1", }, { name: "empty marker", template: "GET /§§ HTTP/1.1", wantPos: []IntrudePosition{{Index: 0, Base: ""}}, wantOut: "GET / HTTP/1.1", }, { name: "unterminated marker", template: "GET /§broken HTTP/1.1", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pos, out, err := ParseMarkers([]byte(tt.template)) if tt.wantErr { if err == nil { t.Fatalf("expected error, got nil") } return } if err != nil { t.Fatalf("unexpected error: %v", err) } if !reflect.DeepEqual(pos, tt.wantPos) { t.Errorf("positions = %+v, want %+v", pos, tt.wantPos) } if string(out) != tt.wantOut { t.Errorf("stripped = %q, want %q", out, tt.wantOut) } }) } } func TestBuildRequest(t *testing.T) { template := "GET /a/§1§/b/§2§/c/§3§ HTTP/1.1" tests := []struct { active int payload string want string }{ {0, "PAYLOAD", "GET /a/PAYLOAD/b/2/c/3 HTTP/1.1"}, {1, "PAYLOAD", "GET /a/1/b/PAYLOAD/c/3 HTTP/1.1"}, {2, "PAYLOAD", "GET /a/1/b/2/c/PAYLOAD HTTP/1.1"}, } for _, tt := range tests { got, err := buildRequest([]byte(template), tt.active, tt.payload) if err != nil { t.Fatalf("active=%d: unexpected error: %v", tt.active, err) } if string(got) != tt.want { t.Errorf("active=%d: got %q, want %q", tt.active, got, tt.want) } } } func TestBuildRequestPayloadContainingMarkerChar(t *testing.T) { // A payload that itself contains the marker character must not be // reinterpreted as a marker on a later buildRequest call - each call // re-splits the ORIGINAL template, not the previously built request. template := "GET /§1§/§2§ HTTP/1.1" got, err := buildRequest([]byte(template), 0, "§injected§") if err != nil { t.Fatalf("unexpected error: %v", err) } want := "GET /§injected§/2 HTTP/1.1" if string(got) != want { t.Errorf("got %q, want %q", got, want) } } func TestIntrudeRequestCount(t *testing.T) { positions, _, err := ParseMarkers([]byte("GET /§a§/§b§ HTTP/1.1")) if err != nil { t.Fatal(err) } if len(positions) != 2 { t.Fatalf("expected 2 positions, got %d", len(positions)) } } func TestFixContentLength(t *testing.T) { tests := []struct { name string raw string want string }{ { name: "recalculates a stale length", raw: "POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 3\r\n\r\nfuzzedvalue", want: "POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 11\r\n\r\nfuzzedvalue", }, { name: "case-insensitive header name", raw: "POST / HTTP/1.1\r\nHost: x\r\ncontent-length: 1\r\n\r\nabc", want: "POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 3\r\n\r\nabc", }, { name: "no content-length header - unchanged", raw: "GET /§1§ HTTP/1.1\r\nHost: x\r\n\r\n", want: "GET /§1§ HTTP/1.1\r\nHost: x\r\n\r\n", }, { name: "no body boundary - unchanged", raw: "GET / HTTP/1.1\r\nHost: x", want: "GET / HTTP/1.1\r\nHost: x", }, { name: "two content-length headers - left alone, ambiguous smuggling case", raw: "POST / HTTP/1.1\r\nContent-Length: 3\r\nContent-Length: 999\r\n\r\nabc", want: "POST / HTTP/1.1\r\nContent-Length: 3\r\nContent-Length: 999\r\n\r\nabc", }, { name: "empty body recalculates to zero", raw: "POST / HTTP/1.1\r\nContent-Length: 5\r\n\r\n", want: "POST / HTTP/1.1\r\nContent-Length: 0\r\n\r\n", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := fixContentLength([]byte(tt.raw)) if string(got) != tt.want { t.Errorf("fixContentLength(%q) = %q, want %q", tt.raw, got, tt.want) } }) } }