srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/internal/proxy
diff options
context:
space:
mode:
Diffstat (limited to 'internal/proxy')
-rw-r--r--internal/proxy/capture.go21
-rw-r--r--internal/proxy/proxy.go43
-rw-r--r--internal/proxy/repeat.go38
-rw-r--r--internal/proxy/tee.go29
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