From 0a477fd288d064d265395c88dbd0f261b723804e Mon Sep 17 00:00:00 2001 From: spiperac Date: Mon, 15 Jun 2026 06:42:13 +0200 Subject: Initial release --- internal/http/rawrequest_test.go | 254 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 internal/http/rawrequest_test.go (limited to 'internal/http/rawrequest_test.go') diff --git a/internal/http/rawrequest_test.go b/internal/http/rawrequest_test.go new file mode 100644 index 0000000..5dd411c --- /dev/null +++ b/internal/http/rawrequest_test.go @@ -0,0 +1,254 @@ +package http + +import ( + "fmt" + "net" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// ── ParseHostFromRaw ────────────────────────────────────────────────────────── + +func TestParseHostFromRaw_HostOnly(t *testing.T) { + host, port, useTLS := ParseHostFromRaw("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") + assert.Equal(t, "example.com", host) + assert.Equal(t, 80, port) + assert.False(t, useTLS) +} + +func TestParseHostFromRaw_HostWithPort443(t *testing.T) { + host, port, useTLS := ParseHostFromRaw("GET / HTTP/1.1\r\nHost: example.com:443\r\n\r\n") + assert.Equal(t, "example.com", host) + assert.Equal(t, 443, port) + assert.True(t, useTLS) +} + +func TestParseHostFromRaw_HostWithPort80(t *testing.T) { + host, port, useTLS := ParseHostFromRaw("GET / HTTP/1.1\r\nHost: example.com:80\r\n\r\n") + assert.Equal(t, "example.com", host) + assert.Equal(t, 80, port) + assert.False(t, useTLS) +} + +func TestParseHostFromRaw_CustomPort(t *testing.T) { + host, port, useTLS := ParseHostFromRaw("GET / HTTP/1.1\r\nHost: example.com:8080\r\n\r\n") + assert.Equal(t, "example.com", host) + assert.Equal(t, 8080, port) + assert.False(t, useTLS) +} + +func TestParseHostFromRaw_IP(t *testing.T) { + host, port, useTLS := ParseHostFromRaw("GET / HTTP/1.1\r\nHost: 1.2.3.4\r\n\r\n") + assert.Equal(t, "1.2.3.4", host) + assert.Equal(t, 80, port) + assert.False(t, useTLS) +} + +func TestParseHostFromRaw_NoHostHeader(t *testing.T) { + host, port, useTLS := ParseHostFromRaw("GET / HTTP/1.1\r\nContent-Type: text/plain\r\n\r\n") + assert.Empty(t, host) + assert.Equal(t, 0, port) + assert.False(t, useTLS) +} + +func TestParseHostFromRaw_CaseInsensitive(t *testing.T) { + host, _, _ := ParseHostFromRaw("GET / HTTP/1.1\r\nHOST: example.com\r\n\r\n") + assert.Equal(t, "example.com", host) +} + +func TestParseHostFromRaw_LFOnly(t *testing.T) { + host, port, _ := ParseHostFromRaw("GET / HTTP/1.1\nHost: example.com\n\n") + assert.Equal(t, "example.com", host) + assert.Equal(t, 80, port) +} + +// ── ParseRawHeaders ─────────────────────────────────────────────────────────── + +func TestParseRawHeaders_Single(t *testing.T) { + raw := "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n" + h := ParseRawHeaders(raw) + assert.Equal(t, "text/html", h.Get("Content-Type")) +} + +func TestParseRawHeaders_Multiple(t *testing.T) { + raw := "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nX-Custom: value\r\n\r\n" + h := ParseRawHeaders(raw) + assert.Equal(t, "text/html", h.Get("Content-Type")) + assert.Equal(t, "value", h.Get("X-Custom")) +} + +func TestParseRawHeaders_StopsAtBlankLine(t *testing.T) { + raw := "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\nbody here" + h := ParseRawHeaders(raw) + assert.Equal(t, "text/html", h.Get("Content-Type")) + assert.Empty(t, h.Get("body here")) +} + +func TestParseRawHeaders_Empty(t *testing.T) { + h := ParseRawHeaders("HTTP/1.1 200 OK\r\n\r\n") + assert.Equal(t, http.Header{}, h) +} + +// ── normalizeLineEndings ────────────────────────────────────────────────────── + +func TestNormalizeLineEndings_LFtoCRLF(t *testing.T) { + out := NormalizeLineEndings("GET / HTTP/1.1\nHost: example.com\n\n") + assert.Contains(t, out, "\r\n") + assert.NotContains(t, out, "\n\n") +} + +func TestNormalizeLineEndings_AlreadyCRLF(t *testing.T) { + in := "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" + out := NormalizeLineEndings(in) + assert.Equal(t, in, out) +} + +func TestNormalizeLineEndings_FoldsContinuation(t *testing.T) { + // Continuation line folding was removed to avoid corrupting body content. + // normalizeLineEndings only touches the header section and converts LF to CRLF. + // Continuation lines are preserved as-is. + in := "GET / HTTP/1.1\r\nHost: example.com\r\n continued\r\n\r\n" + out := NormalizeLineEndings(in) + assert.Equal(t, in, out) +} + +// ── rewriteHeaders ──────────────────────────────────────────────────────────── + +func TestRewriteHeaders_StripsAcceptEncoding(t *testing.T) { + raw := "GET / HTTP/1.1\r\nHost: example.com\r\nAccept-Encoding: gzip\r\n\r\n" + out := rewriteHeaders(raw, nil) + assert.NotContains(t, out, "Accept-Encoding") +} + +func TestRewriteHeaders_StripsContentLength(t *testing.T) { + raw := "POST / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 999\r\n\r\nbody" + out := rewriteHeaders(raw, nil) + assert.NotContains(t, out, "Content-Length: 999") + assert.Contains(t, out, fmt.Sprintf("Content-Length: %d", len("body"))) +} + +func TestRewriteHeaders_PreservesCookieWhenNoJar(t *testing.T) { + raw := "GET / HTTP/1.1\r\nHost: example.com\r\nCookie: session=abc\r\n\r\n" + out := rewriteHeaders(raw, nil) + assert.Contains(t, out, "Cookie: session=abc") +} + +func TestRewriteHeaders_ReplacesCookieFromJar(t *testing.T) { + raw := "GET / HTTP/1.1\r\nHost: example.com\r\nCookie: session=old\r\n\r\n" + out := rewriteHeaders(raw, map[string]string{"session": "new"}) + assert.NotContains(t, out, "session=old") + assert.Contains(t, out, "session=new") +} + +func TestRewriteHeaders_StripsDefaultPort443(t *testing.T) { + raw := "GET / HTTP/1.1\r\nHost: example.com:443\r\n\r\n" + out := rewriteHeaders(raw, nil) + assert.Contains(t, out, "Host: example.com\r\n") + assert.NotContains(t, out, "Host: example.com:443") +} + +func TestRewriteHeaders_StripsDefaultPort80(t *testing.T) { + raw := "GET / HTTP/1.1\r\nHost: example.com:80\r\n\r\n" + out := rewriteHeaders(raw, nil) + assert.Contains(t, out, "Host: example.com\r\n") + assert.NotContains(t, out, "Host: example.com:80") +} + +func TestRewriteHeaders_KeepsNonDefaultPort(t *testing.T) { + raw := "GET / HTTP/1.1\r\nHost: example.com:8080\r\n\r\n" + out := rewriteHeaders(raw, nil) + assert.Contains(t, out, "Host: example.com:8080") +} + +// ── SendRaw (mock TCP server) ───────────────────────────────────────────────── + +func startMockServer(t *testing.T, response string) (string, int) { + t.Helper() + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + go func() { + conn, err := ln.Accept() + if err != nil { + return + } + defer conn.Close() + buf := make([]byte, 4096) + conn.Read(buf) + fmt.Fprint(conn, response) + }() + t.Cleanup(func() { ln.Close() }) + addr := ln.Addr().(*net.TCPAddr) + return "127.0.0.1", addr.Port +} + +func TestSendRaw_200OK(t *testing.T) { + host, port := startMockServer(t, "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello") + raw := fmt.Sprintf("GET / HTTP/1.1\r\nHost: %s:%d\r\n\r\n", host, port) + result, err := SendRaw(RawRequestOptions{Host: host, Port: port, RawReq: raw}) + require.NoError(t, err) + assert.Equal(t, 200, result.StatusCode) + assert.Equal(t, "hello", string(result.Body)) +} + +func TestSendRaw_404(t *testing.T) { + host, port := startMockServer(t, "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n") + raw := fmt.Sprintf("GET /missing HTTP/1.1\r\nHost: %s:%d\r\n\r\n", host, port) + result, err := SendRaw(RawRequestOptions{Host: host, Port: port, RawReq: raw}) + require.NoError(t, err) + assert.Equal(t, 404, result.StatusCode) +} + +func TestSendRaw_ConnectionRefused(t *testing.T) { + _, err := SendRaw(RawRequestOptions{ + Host: "127.0.0.1", Port: 1, + RawReq: "GET / HTTP/1.1\r\nHost: 127.0.0.1:1\r\n\r\n", + }) + assert.Error(t, err) +} + +func TestSendRaw_CookieJarApplied(t *testing.T) { + received := make(chan string, 1) + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + t.Cleanup(func() { ln.Close() }) + go func() { + conn, _ := ln.Accept() + defer conn.Close() + buf := make([]byte, 4096) + n, _ := conn.Read(buf) + received <- string(buf[:n]) + fmt.Fprint(conn, "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + }() + addr := ln.Addr().(*net.TCPAddr) + raw := fmt.Sprintf("GET / HTTP/1.1\r\nHost: 127.0.0.1:%d\r\n\r\n", addr.Port) + SendRaw(RawRequestOptions{ + Host: "127.0.0.1", Port: addr.Port, + RawReq: raw, + CookieJar: map[string]string{"session": "abc123"}, + }) + req := <-received + assert.Contains(t, req, "Cookie: session=abc123") +} + +func TestSendRaw_AcceptEncodingStripped(t *testing.T) { + received := make(chan string, 1) + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + t.Cleanup(func() { ln.Close() }) + go func() { + conn, _ := ln.Accept() + defer conn.Close() + buf := make([]byte, 4096) + n, _ := conn.Read(buf) + received <- string(buf[:n]) + fmt.Fprint(conn, "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + }() + addr := ln.Addr().(*net.TCPAddr) + raw := fmt.Sprintf("GET / HTTP/1.1\r\nHost: 127.0.0.1:%d\r\nAccept-Encoding: gzip\r\n\r\n", addr.Port) + SendRaw(RawRequestOptions{Host: "127.0.0.1", Port: addr.Port, RawReq: raw}) + req := <-received + assert.NotContains(t, req, "Accept-Encoding") +} -- cgit v1.2.3