Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion aop/traffic/exchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func ExchangeFromHTTP(req *http.Request, resp *http.Response, requestBody, respo
Method: req.Method,
URL: urlString,
Protocol: req.Proto,
Headers: PairsFromHTTP(req.Header),
Headers: PairsFromHTTPWithHost(req.Header, req.Host),
Body: requestBody,
}
}
Expand Down Expand Up @@ -387,6 +387,31 @@ func PairsFromHTTP(headers http.Header) []Pair {
return out
}

// containsHeaderName reports whether pairs already carry a header with this
// name, compared case-insensitively.
func containsHeaderName(pairs []Pair, name string) bool {
for _, p := range pairs {
if strings.EqualFold(p.Name, name) {
return true
}
}
return false
}

// PairsFromHTTPWithHost is PairsFromHTTP plus the Host header net/http hides.
// The standard library parses the request-line authority into Request.Host and
// deletes "Host" from Request.Header, so a pair sequence built from the header
// map alone never carries it. When host is non-empty and no Host header is
// already present, it is prepended — Host conventionally leads the field block —
// so a request reconstructed from these pairs is complete and replayable.
func PairsFromHTTPWithHost(headers http.Header, host string) []Pair {
pairs := PairsFromHTTP(headers)
if host == "" || containsHeaderName(pairs, "Host") {
return pairs
}
return append([]Pair{{Name: "Host", Value: host}}, pairs...)
}

func pairsToProto(pairs []Pair) []*Header {
if len(pairs) == 0 {
return nil
Expand Down
29 changes: 29 additions & 0 deletions aop/traffic/exchange_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,35 @@ func TestExchangeFromHTTPUsesCanonicalPairs(t *testing.T) {
}
}

func TestPairsFromHTTPWithHost(t *testing.T) {
// net/http keeps Host out of the header map, so a pair sequence built from
// the map alone lacks it; the helper prepends it.
got := PairsFromHTTPWithHost(http.Header{"Accept": {"*/*"}}, "example.test:8090")
if len(got) != 2 || got[0] != (Pair{Name: "Host", Value: "example.test:8090"}) {
t.Fatalf("Host not prepended: %#v", got)
}

// Empty host: nothing to add, sequence is unchanged.
if got := PairsFromHTTPWithHost(http.Header{"Accept": {"*/*"}}, ""); len(got) != 1 {
t.Fatalf("empty host should not add a header: %#v", got)
}

// An existing Host header (any case) is never duplicated.
got = PairsFromHTTPWithHost(http.Header{"host": {"already.test"}}, "example.test")
if len(got) != 1 || !containsHeaderName(got, "Host") {
t.Fatalf("existing Host must not be duplicated: %#v", got)
}
}

func TestExchangeFromHTTPAddsHost(t *testing.T) {
u, _ := url.Parse("https://example.test:8443/a")
req := &http.Request{Method: "GET", URL: u, Host: "example.test:8443", Proto: "HTTP/1.1", Header: http.Header{"Accept": {"*/*"}}}
e := ExchangeFromHTTP(req, nil, nil, nil)
if len(e.Request.Headers) == 0 || e.Request.Headers[0] != (Pair{Name: "Host", Value: "example.test:8443"}) {
t.Fatalf("Host header not synthesized from req.Host: %#v", e.Request.Headers)
}
}

func TestFlowExchangeRoundTrip(t *testing.T) {
flow := &Flow{
Id: "flow-1",
Expand Down
36 changes: 36 additions & 0 deletions tools/proxy/hub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,42 @@ func TestCapturePostRequestBody(t *testing.T) {
}
}

// TestCaptureIncludesHostHeader guards the Host reconstruction: net/http parses
// the request-line authority into Request.Host and drops "Host" from the header
// map, so a flow whose headers came straight off that map would have no Host
// line and could not be replayed. The capture path must put it back.
func TestCaptureIncludesHostHeader(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}))
defer srv.Close()
want, err := url.Parse(srv.URL)
if err != nil {
t.Fatalf("parse server url: %v", err)
}
hub, _, client := newTestHub(t, true)

if body := get(t, client, srv.URL); body != "" {
_ = body // body is empty; the request headers are what we assert on
}

flows := hub.Store().Query(QueryOpts{})
var host string
for _, f := range flows {
for _, h := range f.Request.Headers {
if strings.EqualFold(h.Name, "Host") {
host = h.Value
}
}
}
if host == "" {
t.Fatal("captured request carried no Host header")
}
if host != want.Host {
t.Errorf("Host = %q, want %q", host, want.Host)
}
}

func TestCaptureFiltersAndVerbs(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/ok", func(w http.ResponseWriter, r *http.Request) {
Expand Down
19 changes: 18 additions & 1 deletion tools/proxy/mitm.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,13 +324,30 @@ func newCaptureState(hub *ProxyHub, f *mitmproxy.Flow) *captureState {
Method: f.Request.Method,
URL: f.Request.URL.String(),
Protocol: f.Request.Proto,
Headers: traffic.PairsFromHTTP(f.Request.Header),
Headers: traffic.PairsFromHTTPWithHost(f.Request.Header, requestHost(f.Request)),
}
flow.Host = f.Request.URL.Hostname()
}
return &captureState{hub: hub, owner: nil, proxy: f.Id.String(), start: f.StartTime, flow: flow}
}

// requestHost recovers the Host header value net/http strips from Request.Header
// into Request.Host, so the captured flow can reconstruct a complete request
// line block. It prefers the client-sent Host (which keeps a non-default port)
// and falls back to the URL authority.
func requestHost(req *mitmproxy.Request) string {
if req == nil {
return ""
}
if raw := req.Raw(); raw != nil && raw.Host != "" {
return raw.Host
}
if req.URL != nil {
return req.URL.Host
}
return ""
}

func (s *captureState) setRequestBody(body []byte) {
if len(body) == 0 {
return
Expand Down
Loading