mirror of
https://github.com/anotherhadi/spilltea.git
synced 2026-05-20 01:32:33 +02:00
[37mfix: use ParseRawRequest and cap response body in replay[0m
[37m- replay/update.go uses util.ParseRawRequest instead of inline parsing[0m [37m- Response body capped with io.LimitReader at MaxBodySizeMB[0m [37m- Uses util.SortedHeaderLines for deterministic header order[0m [37m- Adds navigation key handling (GotoTop/Bottom/PrevPage/NextPage)[0m [37mCo-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>[0m
This commit is contained in:
@@ -1,18 +1,17 @@
|
|||||||
package replay
|
package replay
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"charm.land/bubbles/v2/key"
|
"charm.land/bubbles/v2/key"
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
|
"github.com/anotherhadi/spilltea/internal/config"
|
||||||
"github.com/anotherhadi/spilltea/internal/db"
|
"github.com/anotherhadi/spilltea/internal/db"
|
||||||
"github.com/anotherhadi/spilltea/internal/keys"
|
"github.com/anotherhadi/spilltea/internal/keys"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
"github.com/anotherhadi/spilltea/internal/style"
|
||||||
@@ -213,6 +212,47 @@ func (m Model) updateNormalMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
|||||||
m.refreshListViewport()
|
m.refreshListViewport()
|
||||||
m.refreshBody()
|
m.refreshBody()
|
||||||
|
|
||||||
|
case key.Matches(msg, keys.Keys.Global.GotoTop):
|
||||||
|
m.cursor = 0
|
||||||
|
m.pager.Page = 0
|
||||||
|
m.refreshListViewport()
|
||||||
|
m.refreshBody()
|
||||||
|
|
||||||
|
case key.Matches(msg, keys.Keys.Global.GotoBottom):
|
||||||
|
if len(m.entries) > 0 {
|
||||||
|
m.cursor = len(m.entries) - 1
|
||||||
|
m.pager.Page = m.pager.TotalPages - 1
|
||||||
|
m.refreshListViewport()
|
||||||
|
m.refreshBody()
|
||||||
|
}
|
||||||
|
|
||||||
|
case key.Matches(msg, keys.Keys.Global.PrevPage):
|
||||||
|
step := m.pager.PerPage
|
||||||
|
if step < 1 {
|
||||||
|
step = 1
|
||||||
|
}
|
||||||
|
m.cursor -= step
|
||||||
|
if m.cursor < 0 {
|
||||||
|
m.cursor = 0
|
||||||
|
}
|
||||||
|
m.refreshListViewport()
|
||||||
|
m.refreshBody()
|
||||||
|
|
||||||
|
case key.Matches(msg, keys.Keys.Global.NextPage):
|
||||||
|
step := m.pager.PerPage
|
||||||
|
if step < 1 {
|
||||||
|
step = 1
|
||||||
|
}
|
||||||
|
m.cursor += step
|
||||||
|
if m.cursor >= len(m.entries) {
|
||||||
|
m.cursor = len(m.entries) - 1
|
||||||
|
if m.cursor < 0 {
|
||||||
|
m.cursor = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.refreshListViewport()
|
||||||
|
m.refreshBody()
|
||||||
|
|
||||||
case key.Matches(msg, g.Help):
|
case key.Matches(msg, g.Help):
|
||||||
m.help.ShowAll = !m.help.ShowAll
|
m.help.ShowAll = !m.help.ShowAll
|
||||||
m.recalcSizes()
|
m.recalcSizes()
|
||||||
@@ -276,58 +316,35 @@ func (m *Model) refreshBody() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func doSend(entry Entry) (responseRaw string, statusCode int, err error) {
|
func doSend(entry Entry) (responseRaw string, statusCode int, err error) {
|
||||||
lines := strings.Split(strings.ReplaceAll(entry.RequestRaw, "\r\n", "\n"), "\n")
|
parsed := util.ParseRawRequest(entry.RequestRaw)
|
||||||
if len(lines) == 0 {
|
if parsed.Method == "" {
|
||||||
return "", 0, fmt.Errorf("empty request")
|
return "", 0, fmt.Errorf("empty request")
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.SplitN(lines[0], " ", 3)
|
host := parsed.Host
|
||||||
if len(parts) < 2 {
|
if host == "" {
|
||||||
return "", 0, fmt.Errorf("invalid request line")
|
host = entry.Host
|
||||||
}
|
}
|
||||||
method := strings.TrimSpace(parts[0])
|
|
||||||
path := strings.TrimSpace(parts[1])
|
|
||||||
|
|
||||||
headers := make(http.Header)
|
headers := make(http.Header)
|
||||||
host := entry.Host
|
for _, h := range parsed.Headers {
|
||||||
i := 1
|
if strings.EqualFold(h.Key, "host") {
|
||||||
for i < len(lines) {
|
continue
|
||||||
line := strings.TrimRight(lines[i], "\r")
|
|
||||||
if line == "" {
|
|
||||||
i++
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
if kv := strings.SplitN(line, ": ", 2); len(kv) == 2 {
|
headers.Add(h.Key, h.Value)
|
||||||
k := strings.TrimSpace(kv[0])
|
|
||||||
v := strings.TrimSpace(kv[1])
|
|
||||||
if strings.ToLower(k) == "host" {
|
|
||||||
host = v
|
|
||||||
} else {
|
|
||||||
headers.Add(k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
var bodyBytes []byte
|
|
||||||
if i < len(lines) {
|
|
||||||
b := strings.Join(lines[i:], "\n")
|
|
||||||
b = strings.TrimRight(b, "\n")
|
|
||||||
bodyBytes = []byte(b)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scheme := entry.Scheme
|
scheme := entry.Scheme
|
||||||
if scheme == "" {
|
if scheme == "" {
|
||||||
scheme = "https"
|
scheme = "https"
|
||||||
}
|
}
|
||||||
urlStr := scheme + "://" + host + path
|
urlStr := scheme + "://" + host + parsed.Path
|
||||||
|
|
||||||
var bodyReader io.Reader
|
var bodyReader io.Reader
|
||||||
if len(bodyBytes) > 0 {
|
if parsed.Body != "" {
|
||||||
bodyReader = bytes.NewReader(bodyBytes)
|
bodyReader = strings.NewReader(parsed.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest(method, urlStr, bodyReader)
|
req, err := http.NewRequest(parsed.Method, urlStr, bodyReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, err
|
return "", 0, err
|
||||||
}
|
}
|
||||||
@@ -349,19 +366,13 @@ func doSend(entry Entry) (responseRaw string, statusCode int, err error) {
|
|||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
respBody, _ := io.ReadAll(resp.Body)
|
limit := int64(config.Global.App.MaxBodySizeMB) * 1024 * 1024
|
||||||
|
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, limit))
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
fmt.Fprintf(&sb, "%s %d %s\n", resp.Proto, resp.StatusCode, http.StatusText(resp.StatusCode))
|
fmt.Fprintf(&sb, "%s %d %s\n", resp.Proto, resp.StatusCode, http.StatusText(resp.StatusCode))
|
||||||
sortedKeys := make([]string, 0, len(resp.Header))
|
for _, line := range util.SortedHeaderLines(resp.Header) {
|
||||||
for k := range resp.Header {
|
sb.WriteString(line)
|
||||||
sortedKeys = append(sortedKeys, k)
|
|
||||||
}
|
|
||||||
sort.Strings(sortedKeys)
|
|
||||||
for _, k := range sortedKeys {
|
|
||||||
for _, v := range resp.Header[k] {
|
|
||||||
fmt.Fprintf(&sb, "%s: %s\n", k, v)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
sb.Write(respBody)
|
sb.Write(respBody)
|
||||||
@@ -390,7 +401,11 @@ func entryToDB(e Entry) db.ReplayEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func entryFromMsg(msg SendToReplayMsg) Entry {
|
func entryFromMsg(msg SendToReplayMsg) Entry {
|
||||||
method, host, path := parseFirstLine(msg.RequestRaw, msg.Host)
|
parsed := util.ParseRawRequest(msg.RequestRaw)
|
||||||
|
host := parsed.Host
|
||||||
|
if host == "" {
|
||||||
|
host = msg.Host
|
||||||
|
}
|
||||||
scheme := msg.Scheme
|
scheme := msg.Scheme
|
||||||
if scheme == "" {
|
if scheme == "" {
|
||||||
scheme = util.InferScheme(host)
|
scheme = util.InferScheme(host)
|
||||||
@@ -398,34 +413,9 @@ func entryFromMsg(msg SendToReplayMsg) Entry {
|
|||||||
return Entry{
|
return Entry{
|
||||||
Scheme: scheme,
|
Scheme: scheme,
|
||||||
Host: host,
|
Host: host,
|
||||||
Path: path,
|
Path: parsed.Path,
|
||||||
Method: method,
|
Method: parsed.Method,
|
||||||
OriginalRaw: msg.RequestRaw,
|
OriginalRaw: msg.RequestRaw,
|
||||||
RequestRaw: msg.RequestRaw,
|
RequestRaw: msg.RequestRaw,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseFirstLine(raw, fallbackHost string) (method, host, path string) {
|
|
||||||
host = fallbackHost
|
|
||||||
path = "/"
|
|
||||||
lines := strings.SplitN(raw, "\n", 2)
|
|
||||||
if len(lines) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
parts := strings.Fields(lines[0])
|
|
||||||
if len(parts) >= 1 {
|
|
||||||
method = parts[0]
|
|
||||||
}
|
|
||||||
if len(parts) >= 2 {
|
|
||||||
path = parts[1]
|
|
||||||
}
|
|
||||||
if len(lines) > 1 {
|
|
||||||
for _, line := range strings.Split(lines[1], "\n") {
|
|
||||||
if strings.HasPrefix(strings.ToLower(line), "host:") {
|
|
||||||
host = strings.TrimSpace(line[5:])
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user