From 1a1c0cff30d5e4fcb37d29a8547123f7b89042de Mon Sep 17 00:00:00 2001 From: Hadi <112569860+anotherhadi@users.noreply.github.com> Date: Tue, 19 May 2026 13:38:30 +0200 Subject: [PATCH] =?UTF-8?q?=1B[37mrefactor:=20centralize=20raw=20HTTP=20pa?= =?UTF-8?q?rsing=20and=20header=20serialization=1B[0m?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add internal/util/rawhttp.go with ParseRawRequest and SortedHeaderLines - Refactor intercept/format.go and ui/intercept/helpers.go to use them - Eliminates duplicated bufio.Reader + textproto parsing spread across 3+ files Co-Authored-By: Claude Sonnet 4.6  --- internal/intercept/format.go | 24 ++------- internal/ui/intercept/helpers.go | 47 ++++++----------- internal/util/rawhttp.go | 86 ++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 52 deletions(-) create mode 100644 internal/util/rawhttp.go diff --git a/internal/intercept/format.go b/internal/intercept/format.go index d1cb8bb..68cc8d9 100644 --- a/internal/intercept/format.go +++ b/internal/intercept/format.go @@ -3,9 +3,9 @@ package intercept import ( "fmt" "net/http" - "sort" "strings" + "github.com/anotherhadi/spilltea/internal/util" "github.com/lqqyt2423/go-mitmproxy/proxy" ) @@ -14,15 +14,8 @@ func FormatRawRequest(f *proxy.Flow) string { r := f.Request var sb strings.Builder fmt.Fprintf(&sb, "%s %s %s\n", r.Method, r.URL.RequestURI(), r.Proto) - keys := make([]string, 0, len(r.Header)) - for k := range r.Header { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - for _, v := range r.Header[k] { - fmt.Fprintf(&sb, "%s: %s\n", k, v) - } + for _, line := range util.SortedHeaderLines(r.Header) { + sb.WriteString(line) } sb.WriteString("\n") if len(r.Body) > 0 { @@ -43,15 +36,8 @@ func FormatRawResponse(f *proxy.Flow) string { proto = "HTTP/1.1" } fmt.Fprintf(&sb, "%s %d %s\n", proto, r.StatusCode, http.StatusText(r.StatusCode)) - keys := make([]string, 0, len(r.Header)) - for k := range r.Header { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - for _, v := range r.Header[k] { - fmt.Fprintf(&sb, "%s: %s\n", k, v) - } + for _, line := range util.SortedHeaderLines(r.Header) { + sb.WriteString(line) } sb.WriteString("\n") if len(r.Body) > 0 { diff --git a/internal/ui/intercept/helpers.go b/internal/ui/intercept/helpers.go index 7a60c88..108f3d0 100644 --- a/internal/ui/intercept/helpers.go +++ b/internal/ui/intercept/helpers.go @@ -9,51 +9,32 @@ import ( "github.com/anotherhadi/spilltea/internal/intercept" "github.com/anotherhadi/spilltea/internal/style" + "github.com/anotherhadi/spilltea/internal/util" ) func parseRawRequest(content string, req *intercept.PendingRequest) { + parsed := util.ParseRawRequest(content) r := req.Flow.Request - lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n") - if len(lines) == 0 { - return + if parsed.Method != "" { + r.Method = parsed.Method } - - parts := strings.SplitN(lines[0], " ", 3) - if len(parts) >= 1 { - r.Method = strings.TrimSpace(parts[0]) - } - if len(parts) >= 2 { - if u, err := url.ParseRequestURI(strings.TrimSpace(parts[1])); err == nil { + if parsed.Path != "" { + if u, err := url.ParseRequestURI(parsed.Path); err == nil { r.URL.Path = u.Path r.URL.RawQuery = u.RawQuery } } - if len(parts) >= 3 { - r.Proto = strings.TrimSpace(parts[2]) + if parsed.Proto != "" { + r.Proto = parsed.Proto } - r.Header = make(http.Header) - i := 1 - for i < len(lines) { - line := strings.TrimRight(lines[i], "\r") - if line == "" { - i++ - break - } - if kv := strings.SplitN(line, ": ", 2); len(kv) == 2 { - r.Header.Set(strings.TrimSpace(kv[0]), strings.TrimSpace(kv[1])) - } - i++ + for _, h := range parsed.Headers { + r.Header.Set(h.Key, h.Value) } - - if i < len(lines) { - body := strings.Join(lines[i:], "\n") - body = strings.TrimRight(body, "\n") - if body != "" { - r.Body = []byte(body) - } else { - r.Body = nil - } + if parsed.Body != "" { + r.Body = []byte(parsed.Body) + } else { + r.Body = nil } } diff --git a/internal/util/rawhttp.go b/internal/util/rawhttp.go new file mode 100644 index 0000000..27544cf --- /dev/null +++ b/internal/util/rawhttp.go @@ -0,0 +1,86 @@ +package util + +import ( + "fmt" + "net/http" + "sort" + "strings" +) + +// RawRequest holds a parsed raw HTTP request string. +type RawRequest struct { + Method string + Path string + Proto string + Host string + Headers []RawHeader + Body string +} + +// RawHeader is a single header key/value pair preserving insertion order. +type RawHeader struct { + Key string + Value string +} + +// ParseRawRequest parses a raw HTTP request string (as produced by +// FormatRawRequest). The Host header, if present, is extracted into Host +// but also kept in Headers. +func ParseRawRequest(raw string) RawRequest { + lines := strings.Split(strings.ReplaceAll(raw, "\r\n", "\n"), "\n") + var r RawRequest + if len(lines) == 0 { + return r + } + + parts := strings.SplitN(lines[0], " ", 3) + if len(parts) >= 1 { + r.Method = strings.TrimSpace(parts[0]) + } + if len(parts) >= 2 { + r.Path = strings.TrimSpace(parts[1]) + } + if len(parts) >= 3 { + r.Proto = strings.TrimSpace(parts[2]) + } + + i := 1 + for i < len(lines) { + line := strings.TrimRight(lines[i], "\r") + if line == "" { + i++ + break + } + if kv := strings.SplitN(line, ": ", 2); len(kv) == 2 { + k := strings.TrimSpace(kv[0]) + v := strings.TrimSpace(kv[1]) + r.Headers = append(r.Headers, RawHeader{k, v}) + if strings.EqualFold(k, "host") { + r.Host = v + } + } + i++ + } + + if i < len(lines) { + r.Body = strings.TrimRight(strings.Join(lines[i:], "\n"), "\n") + } + return r +} + +// SortedHeaderLines returns header lines sorted by key name, formatted as +// "Key: Value\n" strings. Useful for deterministic serialisation. +func SortedHeaderLines(h http.Header) []string { + keys := make([]string, 0, len(h)) + for k := range h { + keys = append(keys, k) + } + sort.Strings(keys) + var out []string + for _, k := range keys { + for _, v := range h[k] { + out = append(out, fmt.Sprintf("%s: %s\n", k, v)) + } + } + return out +}