Skip to content

Commit f2ce697

Browse files
committed
bound dump
1 parent a272dab commit f2ce697

3 files changed

Lines changed: 104 additions & 7 deletions

File tree

‎internal/runner/options.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ func ParseOptions() *Options {
7474
flagSet.BoolVar(&options.Sandbox, "sandbox", false, "Enable sandbox mode"),
7575
flagSet.BoolVar(&options.HTTP1Only, "http1", false, "Enable only HTTP1"),
7676
flagSet.IntVar(&options.MaxFileSize, "max-file-size", 50, "Max Upload File Size"),
77-
flagSet.IntVar(&options.MaxDumpBodySize, "max-dump-body-size", -1, "Max Dump Body Size"),
77+
flagSet.IntVar(&options.MaxDumpBodySize, "max-dump-body-size", -1, "Max Dump Body Size in MB (request bodies are logged up to 1 MB when not set)"),
7878
flagSet.BoolVar(&options.Python, "py", false, "Emulate Python Style"),
7979
flagSet.BoolVar(&options.CORS, "cors", false, "Enable Cross-Origin Resource Sharing (CORS)"),
8080
flagSet.Var(&options.HTTPHeaders, "header", "Add HTTP Response Header (name: value), can be used multiple times"),

‎pkg/httpserver/loglayer.go‎

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package httpserver
22

33
import (
44
"bytes"
5+
"io"
56
"net/http"
67
"net/http/httputil"
78
"time"
@@ -16,17 +17,40 @@ var (
1617
EnableLogUA bool
1718
)
1819

19-
func (t *HTTPServer) shouldDumpBody(bodysize int64) bool {
20-
return t.options.MaxDumpBodySize > 0 && bodysize > t.options.MaxDumpBodySize
20+
// defaultMaxDumpBodySize bounds the request body kept for logging when
21+
// -max-dump-body-size is not set.
22+
const defaultMaxDumpBodySize = 1024 * 1024
23+
24+
func (t *HTTPServer) maxDumpBodySize() int64 {
25+
if t.options.MaxDumpBodySize > 0 {
26+
return t.options.MaxDumpBodySize
27+
}
28+
return defaultMaxDumpBodySize
29+
}
30+
31+
// dumpRequest dumps r with at most maxDumpBodySize bytes of its body. The
32+
// loglayer runs before authentication, so the size kept must not depend on
33+
// Content-Length (absent for chunked bodies). The bytes read are put back in
34+
// front of the body, so the next handlers still read all of it.
35+
func (t *HTTPServer) dumpRequest(r *http.Request) []byte {
36+
dump, _ := httputil.DumpRequest(r, false)
37+
if r.Body == nil || r.Body == http.NoBody {
38+
return dump
39+
}
40+
prefix, _ := io.ReadAll(io.LimitReader(r.Body, t.maxDumpBodySize()))
41+
r.Body = struct {
42+
io.Reader
43+
io.Closer
44+
}{io.MultiReader(bytes.NewReader(prefix), r.Body), r.Body}
45+
return append(dump, prefix...)
2146
}
2247

2348
func (t *HTTPServer) loglayer(handler http.Handler) http.Handler {
2449
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
50+
// the body is read only when a log needs it
2551
var fullRequest []byte
26-
if t.shouldDumpBody(r.ContentLength) {
27-
fullRequest, _ = httputil.DumpRequest(r, false)
28-
} else {
29-
fullRequest, _ = httputil.DumpRequest(r, true)
52+
if EnableVerbose || t.jsonLogger != nil {
53+
fullRequest = t.dumpRequest(r)
3054
}
3155
lrw := newLoggingResponseWriter(w, t.options.MaxDumpBodySize)
3256
handler.ServeHTTP(lrw, r)

‎pkg/httpserver/loglayer_test.go‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package httpserver
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
)
10+
11+
type countingBody struct {
12+
r io.Reader
13+
read int64
14+
}
15+
16+
func (c *countingBody) Read(p []byte) (int, error) {
17+
n, err := c.r.Read(p)
18+
c.read += int64(n)
19+
return n, err
20+
}
21+
22+
func (c *countingBody) Close() error { return nil }
23+
24+
// TestLogLayerBoundsChunkedBody checks that a body without Content-Length
25+
// (chunked) is not read past the dump limit before the next handler runs,
26+
// and that the next handler still gets the whole body.
27+
func TestLogLayerBoundsChunkedBody(t *testing.T) {
28+
EnableVerbose = true
29+
defer func() { EnableVerbose = false }()
30+
31+
const limit = 1024
32+
payload := bytes.Repeat([]byte("A"), 1<<20)
33+
body := &countingBody{r: bytes.NewReader(payload)}
34+
35+
server := &HTTPServer{options: &Options{MaxDumpBodySize: limit}}
36+
var readBeforeHandler int64
37+
var got []byte
38+
handler := server.loglayer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
39+
readBeforeHandler = body.read
40+
got, _ = io.ReadAll(r.Body)
41+
w.WriteHeader(http.StatusUnauthorized)
42+
}))
43+
44+
req := httptest.NewRequest(http.MethodPost, "http://example.com/", nil)
45+
req.Body = body
46+
req.ContentLength = -1
47+
req.TransferEncoding = []string{"chunked"}
48+
handler.ServeHTTP(httptest.NewRecorder(), req)
49+
50+
if readBeforeHandler > limit {
51+
t.Fatalf("loglayer read %d bytes before the next handler, limit %d", readBeforeHandler, limit)
52+
}
53+
if !bytes.Equal(got, payload) {
54+
t.Fatalf("next handler got %d bytes, want %d", len(got), len(payload))
55+
}
56+
}
57+
58+
// TestLogLayerSkipsBodyWithoutLog checks that the body is not read when no
59+
// log needs it.
60+
func TestLogLayerSkipsBodyWithoutLog(t *testing.T) {
61+
body := &countingBody{r: bytes.NewReader(bytes.Repeat([]byte("A"), 4096))}
62+
server := &HTTPServer{options: &Options{}}
63+
var readBeforeHandler int64 = -1
64+
handler := server.loglayer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
65+
readBeforeHandler = body.read
66+
}))
67+
req := httptest.NewRequest(http.MethodPost, "http://example.com/", nil)
68+
req.Body = body
69+
handler.ServeHTTP(httptest.NewRecorder(), req)
70+
if readBeforeHandler != 0 {
71+
t.Fatalf("loglayer read %d bytes with no log enabled", readBeforeHandler)
72+
}
73+
}

0 commit comments

Comments
 (0)