diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/ipc/ipc.go | 15 | ||||
| -rw-r--r-- | internal/ipc/server.go | 27 | ||||
| -rw-r--r-- | internal/proxy/capture.go | 14 | ||||
| -rw-r--r-- | internal/proxy/proxy.go | 14 | ||||
| -rw-r--r-- | internal/proxy/repeat.go | 5 |
5 files changed, 62 insertions, 13 deletions
diff --git a/internal/ipc/ipc.go b/internal/ipc/ipc.go index 691dadf..f890715 100644 --- a/internal/ipc/ipc.go +++ b/internal/ipc/ipc.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "net" + "sync" "mitmux/internal/store" ) @@ -51,7 +52,15 @@ type EntryDetail struct { // Client talks to a mitmuxd instance for request/response queries // (list, get). Use Subscribe separately for the live-update stream. +// +// One request/response round trip is in flight on the connection at a +// time, guarded by mu - a caller like the mitmux TUI dispatches each +// request as its own goroutine (a Bubble Tea tea.Cmd), and without this +// two overlapping calls (e.g. opening two entries in quick succession) +// would interleave their JSON on the wire or hand one call the other's +// response. type Client struct { + mu sync.Mutex conn net.Conn dec *json.Decoder enc *json.Encoder @@ -84,6 +93,8 @@ func (c *Client) Search(query string, limit int, beforeID int64) ([]store.Summar } func (c *Client) list(req Request) ([]store.Summary, error) { + c.mu.Lock() + defer c.mu.Unlock() if err := c.enc.Encode(req); err != nil { return nil, err } @@ -99,6 +110,8 @@ func (c *Client) list(req Request) ([]store.Summary, error) { // Get returns the full entry (raw bytes included) for id. func (c *Client) Get(id int64) (*EntryDetail, error) { + c.mu.Lock() + defer c.mu.Unlock() if err := c.enc.Encode(Request{Type: "get", ID: id}); err != nil { return nil, err } @@ -116,6 +129,8 @@ func (c *Client) Get(id int64) (*EntryDetail, error) { // no header injection) and returns the resulting entry, including the raw // response bytes. The exchange is also recorded to history. func (c *Client) Repeat(scheme, host string, raw []byte) (*EntryDetail, error) { + c.mu.Lock() + defer c.mu.Unlock() if err := c.enc.Encode(Request{Type: "repeat", Scheme: scheme, Host: host, Raw: raw}); err != nil { return nil, err } diff --git a/internal/ipc/server.go b/internal/ipc/server.go index 0e839fb..69044af 100644 --- a/internal/ipc/server.go +++ b/internal/ipc/server.go @@ -3,6 +3,7 @@ package ipc import ( "context" "encoding/json" + "io" "log" "net" "sync" @@ -129,13 +130,31 @@ func (s *Server) handleConn(conn net.Conn) { case "subscribe": sub := s.hub.subscribe() defer s.hub.unsubscribe(sub) - for e := range sub { - e := e - if err := enc.Encode(Response{Type: "new", New: &e}); err != nil { + + // The client never sends anything more on this connection, + // but reading it anyway is how we notice it went away when + // no new entry ever arrives to trigger a failed Encode - + // otherwise a subscriber that quits while the daemon is + // idle leaks its goroutine and channel indefinitely. + disconnected := make(chan struct{}) + go func() { + io.Copy(io.Discard, conn) + close(disconnected) + }() + + for { + select { + case e, ok := <-sub: + if !ok { + return + } + if err := enc.Encode(Response{Type: "new", New: &e}); err != nil { + return + } + case <-disconnected: return } } - return default: enc.Encode(Response{Type: "error", Error: "unknown request type: " + req.Type}) diff --git a/internal/proxy/capture.go b/internal/proxy/capture.go index ccc95e9..ec4dc8d 100644 --- a/internal/proxy/capture.go +++ b/internal/proxy/capture.go @@ -59,14 +59,12 @@ func captureRequest(r *http.Request, tee *teeConn, bodyCap *cappedTee) (raw []by return buf.Bytes(), false } -// captureResponse mirrors captureRequest for the upstream leg: exact -// wire bytes when tee is non-nil (upstream negotiated HTTP/1.1), -// otherwise a reconstruction. -func captureResponse(resp *http.Response, tee *teeConn, bodyCap *cappedTee) (raw []byte, exact bool) { - if tee != nil { - return tee.Take(), true - } - +// captureResponse reconstructs raw response bytes from the parsed +// response for the HTTP/2 upstream case - the exact-capture path +// (HTTP/1.1 upstream) is handled directly in forward() via the +// teeConn's own Take(), which is the only reason this one doesn't also +// need a *teeConn parameter. +func captureResponse(resp *http.Response, bodyCap *cappedTee) (raw []byte, exact bool) { dump := *resp if bodyCap != nil { dump.Body = io.NopCloser(bytes.NewReader(bodyCap.buf.Bytes())) diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index fcf8c14..014c601 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -38,6 +38,10 @@ import ( "mitmux/internal/store" ) +// upstreamTimeout bounds the write-request/read-response phase of an +// upstream exchange, once dialing has already succeeded. +const upstreamTimeout = 60 * time.Second + // hopByHopHeaders are stripped before forwarding a request or response, // per RFC 7230 6.1 - they are meaningful only between a client and its // immediate next hop, not end-to-end. @@ -259,6 +263,14 @@ func (s *Server) forward(dial dialer, scheme, hostname string, w http.ResponseWr return } defer conn.Close() + // The dial itself is bounded (net.Dialer.Timeout / HandshakeContext); + // without this, a server that accepts the connection and then never + // writes or never finishes writing would hang the request forever - + // there's no other timeout covering the write-request/read-response + // phase. Bounds the whole exchange, so a legitimately slow multi- + // minute transfer would also get cut off; a fixed default is enough + // for now, not worth a config surface yet. + conn.SetDeadline(time.Now().Add(upstreamTimeout)) var resp *http.Response var upstreamTee *teeConn @@ -299,7 +311,7 @@ func (s *Server) forward(dial dialer, scheme, hostname string, w http.ResponseWr if upstreamTee != nil { respRaw, respExact = upstreamTee.Take(), true } else { - respRaw, respExact = captureResponse(resp, nil, respBodyCap) + respRaw, respExact = captureResponse(resp, respBodyCap) } s.record(started, duration, scheme, hostname, r, reqRaw, reqExact, respRaw, respExact, resp.StatusCode, "") diff --git a/internal/proxy/repeat.go b/internal/proxy/repeat.go index 6373ee1..cecf481 100644 --- a/internal/proxy/repeat.go +++ b/internal/proxy/repeat.go @@ -33,6 +33,11 @@ func (s *Server) Repeat(ctx context.Context, scheme, host string, raw []byte) (* return s.recordRepeat(started, time.Since(started), scheme, host, method, path, raw, nil, 0, err.Error()) } defer conn.Close() + // See the matching comment in forward(): without this, a hung + // server - or a user-edited request malformed enough that nothing + // ever replies - blocks this Repeat call, and the IPC connection + // handling it, forever. + conn.SetDeadline(time.Now().Add(upstreamTimeout)) if _, err := conn.Write(raw); err != nil { return s.recordRepeat(started, time.Since(started), scheme, host, method, path, raw, nil, 0, err.Error()) |