Files
Hadi 1a1c0cff30 refactor: centralize raw HTTP parsing and header serialization
- 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 <noreply@anthropic.com>
2026-05-19 13:38:30 +02:00

320 lines
7.3 KiB
Go

package intercept
import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"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
if parsed.Method != "" {
r.Method = parsed.Method
}
if parsed.Path != "" {
if u, err := url.ParseRequestURI(parsed.Path); err == nil {
r.URL.Path = u.Path
r.URL.RawQuery = u.RawQuery
}
}
if parsed.Proto != "" {
r.Proto = parsed.Proto
}
r.Header = make(http.Header)
for _, h := range parsed.Headers {
r.Header.Set(h.Key, h.Value)
}
if parsed.Body != "" {
r.Body = []byte(parsed.Body)
} else {
r.Body = nil
}
}
func parseRawResponse(content string, resp *intercept.PendingResponse) {
r := resp.Flow.Response
if r == nil {
return
}
lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
if len(lines) == 0 {
return
}
parts := strings.SplitN(lines[0], " ", 3)
if len(parts) >= 2 {
if code, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil {
r.StatusCode = code
}
}
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++
}
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
}
}
r.Header.Set("Content-Length", strconv.Itoa(len(r.Body)))
}
func (m *Model) currentLabel() string {
if m.captureResponse && m.focusedPanel == panelResponses {
if len(m.responseQueue) == 0 {
return ""
}
resp := m.responseQueue[m.responseCursor]
code := 0
if resp.Flow.Response != nil {
code = resp.Flow.Response.StatusCode
}
return fmt.Sprintf("%d %s %s", code, http.StatusText(code), resp.Flow.Request.URL.RequestURI())
}
if len(m.queue) == 0 {
return ""
}
req := m.queue[m.cursor]
return req.Flow.Request.Method + " " + req.Flow.Request.URL.RequestURI()
}
func (m *Model) removeFromQueue(index int) {
m.queue = append(m.queue[:index], m.queue[index+1:]...)
if m.cursor >= len(m.queue) && m.cursor > 0 {
m.cursor--
}
m.refreshListViewport()
m.refreshBody()
}
func (m *Model) removeFromResponseQueue(index int) {
m.responseQueue = append(m.responseQueue[:index], m.responseQueue[index+1:]...)
if m.responseCursor >= len(m.responseQueue) && m.responseCursor > 0 {
m.responseCursor--
}
m.refreshResponseListViewport()
m.refreshBody()
}
func (m *Model) applyAndDecide(d intercept.Decision) {
if len(m.queue) == 0 {
return
}
req := m.queue[m.cursor]
if d == intercept.Forward {
if edited, ok := m.pendingEdits[req]; ok {
parseRawRequest(edited, req)
}
}
delete(m.pendingEdits, req)
m.broker.Decide(req, d)
m.removeFromQueue(m.cursor)
}
func (m *Model) applyAndDecideResponse(d intercept.Decision) {
if len(m.responseQueue) == 0 {
return
}
resp := m.responseQueue[m.responseCursor]
if d == intercept.Forward {
if edited, ok := m.pendingResponseEdits[resp]; ok {
parseRawResponse(edited, resp)
}
}
delete(m.pendingResponseEdits, resp)
m.broker.DecideResponse(resp, d)
m.removeFromResponseQueue(m.responseCursor)
}
func (m *Model) listHalfWidths() (leftW, rightW int) {
leftW = m.width / 2
rightW = m.width - leftW
return
}
func (m *Model) recalcSizes() {
m.help.SetWidth(m.width - 2)
listH, bodyH := style.SplitH(m.height, m.renderStatusBar(), 0.35)
bodyInner := m.width - 2
if bodyInner < 0 {
bodyInner = 0
}
bodyVH := style.PanelContentH(bodyH)
m.textarea.SetWidth(bodyInner)
m.textarea.SetHeight(bodyVH)
m.bodyViewport.SetWidth(bodyInner)
m.bodyViewport.SetHeight(bodyVH)
listVH := style.PanelContentH(listH) - 1 // -1 for the pager dots row
if listVH < 0 {
listVH = 0
}
if m.captureResponse {
leftW, rightW := m.listHalfWidths()
leftInner := leftW - 2
rightInner := rightW - 2
if leftInner < 0 {
leftInner = 0
}
if rightInner < 0 {
rightInner = 0
}
m.listViewport.SetWidth(leftInner)
m.listViewport.SetHeight(listVH)
m.pager.PerPage = listVH
if m.pager.PerPage < 1 {
m.pager.PerPage = 1
}
m.responseViewport.SetWidth(rightInner)
m.responseViewport.SetHeight(listVH)
m.responsePager.PerPage = listVH
if m.responsePager.PerPage < 1 {
m.responsePager.PerPage = 1
}
} else {
listInner := m.width - 2
if listInner < 0 {
listInner = 0
}
m.listViewport.SetWidth(listInner)
m.listViewport.SetHeight(listVH)
m.pager.PerPage = listVH
if m.pager.PerPage < 1 {
m.pager.PerPage = 1
}
}
m.refreshListViewport()
m.refreshResponseListViewport()
m.refreshBody()
}
func (m *Model) refreshListViewport() {
if m.pager.PerPage > 0 {
if len(m.queue) == 0 {
m.pager.Page = 0
m.pager.TotalPages = 0
} else {
m.pager.Page = m.cursor / m.pager.PerPage
m.pager.SetTotalPages(len(m.queue))
}
}
m.listViewport.SetContent(m.renderList())
}
func (m *Model) refreshResponseListViewport() {
if m.responsePager.PerPage > 0 {
if len(m.responseQueue) == 0 {
m.responsePager.Page = 0
m.responsePager.TotalPages = 0
} else {
m.responsePager.Page = m.responseCursor / m.responsePager.PerPage
m.responsePager.SetTotalPages(len(m.responseQueue))
}
}
m.responseViewport.SetContent(m.renderResponseList())
}
// saveCurrentEdit must only be called when exiting edit mode.
func (m *Model) saveCurrentEdit() {
if m.captureResponse && m.focusedPanel == panelResponses {
if len(m.responseQueue) > 0 {
m.pendingResponseEdits[m.responseQueue[m.responseCursor]] = m.textarea.Value()
}
} else {
if len(m.queue) > 0 {
m.pendingEdits[m.queue[m.cursor]] = m.textarea.Value()
}
}
}
const maxInlineEditBytes = 32 * 1024
func (m *Model) loadIntoTextarea() {
if m.captureResponse && m.focusedPanel == panelResponses {
if len(m.responseQueue) == 0 {
return
}
resp := m.responseQueue[m.responseCursor]
if edited, ok := m.pendingResponseEdits[resp]; ok {
m.textarea.SetValue(edited)
} else {
m.textarea.SetValue(intercept.FormatRawResponse(resp.Flow))
}
} else {
if len(m.queue) == 0 {
return
}
req := m.queue[m.cursor]
if edited, ok := m.pendingEdits[req]; ok {
m.textarea.SetValue(edited)
} else {
m.textarea.SetValue(intercept.FormatRawRequest(req.Flow))
}
}
}
// refreshBody does not touch the textarea - it is only loaded when entering edit mode.
func (m *Model) refreshBody() {
var raw string
if m.captureResponse && m.focusedPanel == panelResponses {
if len(m.responseQueue) == 0 {
m.bodyViewport.SetContent("")
return
}
resp := m.responseQueue[m.responseCursor]
if edited, ok := m.pendingResponseEdits[resp]; ok {
raw = edited
} else {
raw = intercept.FormatRawResponse(resp.Flow)
}
} else {
if len(m.queue) == 0 {
m.bodyViewport.SetContent("")
return
}
req := m.queue[m.cursor]
if edited, ok := m.pendingEdits[req]; ok {
raw = edited
} else {
raw = intercept.FormatRawRequest(req.Flow)
}
}
m.bodyViewport.SetContent(style.HighlightHTTP(raw))
m.bodyViewport.SetYOffset(0)
m.bodyViewport.SetXOffset(0)
}
func (m *Model) refreshBodyViewport() {
m.bodyViewport.SetContent(style.HighlightHTTP(m.textarea.Value()))
}