diff options
| author | srdusr <[email protected]> | 2024-08-29 22:03:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-08-29 22:03:00 +0200 |
| commit | 70f675e59ac4bb0e469ca0073f8cc0506b17677e (patch) | |
| tree | e3a525b26dfcf16ce4f9bde97b1408a373d87944 /internal/ipc/server.go | |
| parent | 8d15c2e0b326933f8fc912e3b13f37e78a9bc0b6 (diff) | |
| download | mitmux-70f675e59ac4bb0e469ca0073f8cc0506b17677e.tar.gz mitmux-70f675e59ac4bb0e469ca0073f8cc0506b17677e.zip | |
Repeater: raw-byte send/resend
Implements build-order step 4, the feature the plan calls out as used
daily. internal/proxy/repeat.go adds Server.Repeat(scheme, host, raw):
dials fresh (HTTP/1.1-only - raw edited text has no equivalent in
HTTP/2's binary framing), writes raw exactly as given with no framing
correction or header injection, and captures the exact response bytes.
This is deliberately separate from forward()'s parsed-*http.Request path
since Repeater's entire point is letting a malformed/edited request
reach the wire unmodified.
Repeater sends are recorded to the same history table as proxy traffic
(added a "source" column: "proxy" vs "repeater") so they show up in the
unified history view and the live subscribe stream, not a separate silo.
internal/ipc gains a "repeat" request/response pair; cmd/mitmuxd wires
proxy.Server into ipc.NewServer via a small Repeater interface so the
daemon keeps owning all network I/O and the TUI stays a thin client.
cmd/mitmux gains a repeater view (bubbles/textarea for the editable raw
request, a read-only viewport for the response), reachable with 'r' from
either the list or detail view, ctrl+r to send.
One real bug found via testing: bubbles/textarea only understands LF,
but HTTP/1.1 requires CRLF, so loading raw bytes straight into it split
each line in two on render. Fixed by normalizing CRLF<->LF at the editor
boundary only (load: strip \r; send: restore it) - documented as a
narrow, known trade-off for bodies with their own embedded LF line
breaks, which is the cost of being able to edit raw HTTP as text at all.
Verified live: edited and sent a plain-HTTP repeater request (confirmed
in SQLite that the edit - including an intentional extra blank line from
imprecise cursor navigation during testing - went out completely
unmodified, which is the correct behavior: mitmux must never "fix" what
the user typed), and sent an HTTPS repeater request against a freshly
captured entry, both getting real 200 responses with exact response
bytes back.
Diffstat (limited to 'internal/ipc/server.go')
| -rw-r--r-- | internal/ipc/server.go | 54 |
1 files changed, 39 insertions, 15 deletions
diff --git a/internal/ipc/server.go b/internal/ipc/server.go index 11ba033..e3b2a29 100644 --- a/internal/ipc/server.go +++ b/internal/ipc/server.go @@ -1,6 +1,7 @@ package ipc import ( + "context" "encoding/json" "log" "net" @@ -9,6 +10,12 @@ import ( "mitmux/internal/store" ) +// Repeater sends raw bytes to scheme://host exactly as given and +// records the exchange to history - implemented by *proxy.Server. +type Repeater interface { + Repeat(ctx context.Context, scheme, host string, raw []byte) (*store.Entry, error) +} + // Hub fans out newly captured history entries to subscribed clients. type Hub struct { mu sync.Mutex @@ -50,14 +57,15 @@ func (h *Hub) unsubscribe(ch chan store.Summary) { // Server serves the daemon side of the mitmux control protocol. type Server struct { - db *store.Store - hub *Hub + db *store.Store + hub *Hub + repeater Repeater } // NewServer creates a control-protocol Server backed by db, broadcasting -// through hub. -func NewServer(db *store.Store, hub *Hub) *Server { - return &Server{db: db, hub: hub} +// through hub and sending Repeater requests through rep. +func NewServer(db *store.Store, hub *Hub, rep Repeater) *Server { + return &Server{db: db, hub: hub, repeater: rep} } // Serve accepts connections on ln until it returns an error (e.g. the @@ -98,16 +106,19 @@ func (s *Server) handleConn(conn net.Conn) { enc.Encode(Response{Type: "error", Error: err.Error()}) continue } - enc.Encode(Response{Type: "get", Detail: &EntryDetail{ - Summary: store.Summary{ - ID: e.ID, StartedAt: e.StartedAt, Duration: e.Duration, - Method: e.Method, Scheme: e.Scheme, Host: e.Host, Path: e.Path, - StatusCode: e.StatusCode, ReqSize: len(e.RequestRaw), RespSize: len(e.ResponseRaw), - Error: e.Error, - }, - RequestRaw: e.RequestRaw, ResponseRaw: e.ResponseRaw, - RequestExact: e.RequestExact, ResponseExact: e.ResponseExact, - }}) + enc.Encode(Response{Type: "get", Detail: detailFromEntry(e)}) + + case "repeat": + if s.repeater == nil { + enc.Encode(Response{Type: "error", Error: "repeater not available"}) + continue + } + e, err := s.repeater.Repeat(context.Background(), req.Scheme, req.Host, req.Raw) + if err != nil { + enc.Encode(Response{Type: "error", Error: err.Error()}) + continue + } + enc.Encode(Response{Type: "repeat", Detail: detailFromEntry(e)}) case "subscribe": sub := s.hub.subscribe() @@ -126,6 +137,19 @@ func (s *Server) handleConn(conn net.Conn) { } } +func detailFromEntry(e *store.Entry) *EntryDetail { + return &EntryDetail{ + Summary: store.Summary{ + ID: e.ID, StartedAt: e.StartedAt, Duration: e.Duration, + Method: e.Method, Scheme: e.Scheme, Host: e.Host, Path: e.Path, + StatusCode: e.StatusCode, ReqSize: len(e.RequestRaw), RespSize: len(e.ResponseRaw), + Error: e.Error, Source: e.Source, + }, + RequestRaw: e.RequestRaw, ResponseRaw: e.ResponseRaw, + RequestExact: e.RequestExact, ResponseExact: e.ResponseExact, + } +} + // LogAndBroadcast is a convenience OnEntry callback: logs the entry and // broadcasts it through hub. func LogAndBroadcast(hub *Hub) func(store.Summary) { |