diff options
Diffstat (limited to 'internal/proxy')
| -rw-r--r-- | internal/proxy/capture.go | 21 | ||||
| -rw-r--r-- | internal/proxy/proxy.go | 43 | ||||
| -rw-r--r-- | internal/proxy/repeat.go | 38 | ||||
| -rw-r--r-- | internal/proxy/tee.go | 29 |
4 files changed, 78 insertions, 53 deletions
diff --git a/internal/proxy/capture.go b/internal/proxy/capture.go index ec4dc8d..3902e5c 100644 --- a/internal/proxy/capture.go +++ b/internal/proxy/capture.go @@ -36,12 +36,19 @@ func (c *cappedTee) Read(p []byte) (int, error) { // captureRequest returns the raw bytes of r for storage. When tee is // non-nil (an HTTP/1.1 client connection), the bytes are exactly what -// was read off the wire. Otherwise (HTTP/2, which has no single "raw -// bytes" representation - it's multiplexed, HPACK-compressed framing) -// it's a reconstruction from the parsed request, exact=false. -func captureRequest(r *http.Request, tee *teeConn, bodyCap *cappedTee) (raw []byte, exact bool) { +// was read off the wire, unless truncated hits maxCaptureBytes and has +// to drop bytes off the end - still real wire bytes, just incomplete, +// which is a different (and less severe) kind of "not exact" than the +// HTTP/2 case below and worth telling apart in the UI (see +// store.Entry's *Truncated fields). Otherwise (HTTP/2, which has no +// single "raw bytes" representation - it's multiplexed, HPACK- +// compressed framing) it's a reconstruction from the parsed request, +// exact=false, truncated=false (truncated only applies to a would-be- +// exact capture). +func captureRequest(r *http.Request, tee *teeConn, bodyCap *cappedTee) (raw []byte, exact, truncated bool) { if tee != nil { - return tee.Take(), true + data, truncated := tee.Take() + return data, !truncated, truncated } dump := r.Clone(r.Context()) @@ -54,9 +61,9 @@ func captureRequest(r *http.Request, tee *teeConn, bodyCap *cappedTee) (raw []by } var buf bytes.Buffer if err := dump.Write(&buf); err != nil { - return nil, false + return nil, false, false } - return buf.Bytes(), false + return buf.Bytes(), false, false } // captureResponse reconstructs raw response bytes from the parsed diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index 82328db..08b8551 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -383,8 +383,8 @@ func (s *Server) forward(dial dialer, scheme, hostname string, w http.ResponseWr started := time.Now() conn, negotiated, dialErr := dial(r.Context()) if dialErr != nil { - reqRaw, reqExact := captureRequest(r, clientTee, reqBodyCap) - s.record(started, time.Since(started), scheme, hostname, r, reqRaw, reqExact, nil, false, 0, dialErr.Error()) + reqRaw, reqExact, reqTrunc := captureRequest(r, clientTee, reqBodyCap) + s.record(started, time.Since(started), scheme, hostname, r, reqRaw, reqExact, reqTrunc, nil, false, false, 0, dialErr.Error()) http.Error(w, dialErr.Error(), http.StatusBadGateway) return } @@ -412,10 +412,10 @@ func (s *Server) forward(dial dialer, scheme, hostname string, w http.ResponseWr } duration := time.Since(started) - reqRaw, reqExact := captureRequest(r, clientTee, reqBodyCap) + reqRaw, reqExact, reqTrunc := captureRequest(r, clientTee, reqBodyCap) if err != nil { - s.record(started, duration, scheme, hostname, r, reqRaw, reqExact, nil, false, 0, err.Error()) + s.record(started, duration, scheme, hostname, r, reqRaw, reqExact, reqTrunc, nil, false, false, 0, err.Error()) http.Error(w, err.Error(), http.StatusBadGateway) return } @@ -448,14 +448,15 @@ func (s *Server) forward(dial dialer, scheme, hostname string, w http.ResponseWr io.Copy(w, resp.Body) var respRaw []byte - var respExact bool + var respExact, respTrunc bool if upstreamTee != nil { - respRaw, respExact = upstreamTee.Take(), true + respRaw, respTrunc = upstreamTee.Take() + respExact = !respTrunc } else { respRaw, respExact = captureResponse(resp, respBodyCap) } - s.record(started, duration, scheme, hostname, r, reqRaw, reqExact, respRaw, respExact, resp.StatusCode, "") + s.record(started, duration, scheme, hostname, r, reqRaw, reqExact, reqTrunc, respRaw, respExact, respTrunc, resp.StatusCode, "") } // enabledRules fetches the current enabled match-and-replace rules for @@ -471,24 +472,26 @@ func (s *Server) enabledRules(scope string) ([]rules.Rule, error) { // record stores one history entry and notifies OnEntry. func (s *Server) record(started time.Time, duration time.Duration, scheme, host string, r *http.Request, - reqRaw []byte, reqExact bool, respRaw []byte, respExact bool, status int, errMsg string) { + reqRaw []byte, reqExact, reqTruncated bool, respRaw []byte, respExact, respTruncated bool, status int, errMsg string) { if s.store == nil { return } e := &store.Entry{ - StartedAt: started, - Duration: duration, - Method: r.Method, - Scheme: scheme, - Host: host, - Path: r.URL.Path, - StatusCode: status, - RequestRaw: reqRaw, - ResponseRaw: respRaw, - RequestExact: reqExact, - ResponseExact: respExact, - Error: errMsg, + StartedAt: started, + Duration: duration, + Method: r.Method, + Scheme: scheme, + Host: host, + Path: r.URL.Path, + StatusCode: status, + RequestRaw: reqRaw, + ResponseRaw: respRaw, + RequestExact: reqExact, + ResponseExact: respExact, + RequestTruncated: reqTruncated, + ResponseTruncated: respTruncated, + Error: errMsg, } id, err := s.store.Insert(e) if err != nil { diff --git a/internal/proxy/repeat.go b/internal/proxy/repeat.go index 3ee7cea..f682b99 100644 --- a/internal/proxy/repeat.go +++ b/internal/proxy/repeat.go @@ -38,7 +38,7 @@ func (s *Server) sendRaw(ctx context.Context, scheme, host string, raw []byte, s conn, err := dialForRepeat(ctx, scheme, host) if err != nil { - return s.recordRaw(started, time.Since(started), scheme, host, method, path, raw, nil, 0, err.Error(), source) + return s.recordRaw(started, time.Since(started), scheme, host, method, path, raw, nil, false, false, 0, err.Error(), source) } defer conn.Close() // See the matching comment in forward(): without this, a hung @@ -48,37 +48,39 @@ func (s *Server) sendRaw(ctx context.Context, scheme, host string, raw []byte, s conn.SetDeadline(time.Now().Add(upstreamTimeout)) if _, err := conn.Write(raw); err != nil { - return s.recordRaw(started, time.Since(started), scheme, host, method, path, raw, nil, 0, err.Error(), source) + return s.recordRaw(started, time.Since(started), scheme, host, method, path, raw, nil, false, false, 0, err.Error(), source) } tee := newTeeConn(conn) resp, err := http.ReadResponse(bufio.NewReader(tee), &http.Request{Method: method}) duration := time.Since(started) if err != nil { - return s.recordRaw(started, duration, scheme, host, method, path, raw, nil, 0, err.Error(), source) + return s.recordRaw(started, duration, scheme, host, method, path, raw, nil, false, false, 0, err.Error(), source) } defer resp.Body.Close() io.Copy(io.Discard, resp.Body) - return s.recordRaw(started, duration, scheme, host, method, path, raw, tee.Take(), resp.StatusCode, "", source) + respRaw, truncated := tee.Take() + return s.recordRaw(started, duration, scheme, host, method, path, raw, respRaw, !truncated, truncated, resp.StatusCode, "", source) } func (s *Server) recordRaw(started time.Time, duration time.Duration, scheme, host, method, path string, - reqRaw, respRaw []byte, status int, errMsg, source string) (*store.Entry, error) { + reqRaw, respRaw []byte, respExact, respTruncated bool, status int, errMsg, source string) (*store.Entry, error) { e := &store.Entry{ - StartedAt: started, - Duration: duration, - Method: method, - Scheme: scheme, - Host: host, - Path: path, - StatusCode: status, - RequestRaw: reqRaw, - ResponseRaw: respRaw, - RequestExact: true, - ResponseExact: respRaw != nil, - Error: errMsg, - Source: source, + StartedAt: started, + Duration: duration, + Method: method, + Scheme: scheme, + Host: host, + Path: path, + StatusCode: status, + RequestRaw: reqRaw, + ResponseRaw: respRaw, + RequestExact: true, + ResponseExact: respRaw != nil && respExact, + ResponseTruncated: respRaw != nil && respTruncated, + Error: errMsg, + Source: source, } if s.store != nil { id, err := s.store.Insert(e) diff --git a/internal/proxy/tee.go b/internal/proxy/tee.go index f6a385a..bd34d19 100644 --- a/internal/proxy/tee.go +++ b/internal/proxy/tee.go @@ -22,8 +22,9 @@ const maxCaptureBytes = 10 << 20 // 10 MiB // here (call sites synchronize on the request/response boundary itself). type teeConn struct { net.Conn - mu sync.Mutex - buf []byte + mu sync.Mutex + buf []byte + truncated bool } func newTeeConn(c net.Conn) *teeConn { @@ -34,12 +35,18 @@ func (c *teeConn) Read(p []byte) (int, error) { n, err := c.Conn.Read(p) if n > 0 { c.mu.Lock() - if room := maxCaptureBytes - len(c.buf); room > 0 { + room := maxCaptureBytes - len(c.buf) + if room > 0 { end := n if end > room { end = room } c.buf = append(c.buf, p[:end]...) + if end < n { + c.truncated = true + } + } else { + c.truncated = true } c.mu.Unlock() } @@ -47,13 +54,19 @@ func (c *teeConn) Read(p []byte) (int, error) { } // Take returns the bytes read since the last Take call (or since the -// connection was created) and resets the buffer. -func (c *teeConn) Take() []byte { +// connection was created), whether that capture hit maxCaptureBytes and +// had to drop bytes off the end, and resets both. A caller storing this +// as an "exact" capture must fold truncated into that decision - bytes +// silently missing from the tail is exactly the kind of gap "exact" +// promises doesn't exist, and callers finding it separately or not at +// all is how a request smuggling investigation loses the one thing it +// was looking for. +func (c *teeConn) Take() (data []byte, truncated bool) { c.mu.Lock() defer c.mu.Unlock() - out := c.buf - c.buf = nil - return out + out, trunc := c.buf, c.truncated + c.buf, c.truncated = nil, false + return out, trunc } // teeListener wraps a net.Listener so every accepted connection is |