summaryrefslogtreecommitdiff
path: root/internal/http/rawrequest_test.go
diff options
context:
space:
mode:
authorspiperac <[email protected]>2026-06-15 06:42:13 +0200
committerspiperac <[email protected]>2026-06-15 06:42:13 +0200
commit0a477fd288d064d265395c88dbd0f261b723804e (patch)
tree9b129f8f41c907285b89ed8385679934bb59c14f /internal/http/rawrequest_test.go
Initial release
Diffstat (limited to 'internal/http/rawrequest_test.go')
-rw-r--r--internal/http/rawrequest_test.go254
1 files changed, 254 insertions, 0 deletions
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")
+}