srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/ipc/ipc.go15
-rw-r--r--internal/ipc/server.go27
-rw-r--r--internal/proxy/capture.go14
-rw-r--r--internal/proxy/proxy.go14
-rw-r--r--internal/proxy/repeat.go5
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())