Skip to content
Draft
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
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ An easy-to-use and flexible MITM proxy library for Go. It can intercept and insp
## Features

- HTTP/1.1 keep-alive and end-to-end pipelining, HTTP/2 over TLS, and h2c support
- Wire-order preservation for HTTP/1 and HTTP/2 request/response headers and trailers
- HTTP/2 SETTINGS, connection WINDOW_UPDATE, standalone PRIORITY, HEADERS priority, and pseudo-header fingerprint mirroring
- HTTPS interception with custom CA certificates
- WebSocket and secure WebSocket interception
- Pre-relay observation for classified raw TCP tunnels without exposing payloads
Expand Down Expand Up @@ -55,7 +57,7 @@ package main
import (
"fmt"
"log"
"net/http"
"github.com/josexy/xhttp"

mitmproxy "github.com/josexy/mitmproxy-go"
)
Expand Down Expand Up @@ -83,7 +85,7 @@ package main
import (
"context"
"fmt"
"net/http"
"github.com/josexy/xhttp"

mitmproxy "github.com/josexy/mitmproxy-go"
)
Expand Down Expand Up @@ -117,7 +119,7 @@ package main
import (
"context"
"log"
"net/http"
"github.com/josexy/xhttp"

mitmproxy "github.com/josexy/mitmproxy-go"
)
Expand Down Expand Up @@ -250,6 +252,8 @@ mitmproxy.WithCertCachePool(2048, 30, 15)

HTTPS and WSS interception automatically captures the client's TLS ClientHello, fingerprints it with uTLS (`github.com/refraction-networking/utls`), patches SNI/ALPN for the target server, and uses that spec for the upstream TLS handshake. `WithDisableHTTP2` also removes `h2` from mirrored ALPN protocols.

For HTTP/2, the proxy captures the client's ordered SETTINGS values, initial connection WINDOW_UPDATE, pre-request standalone PRIORITY frames, priority carried by each request's initial HEADERS frame, and per-request pseudo-header order. It replays the connection-level fingerprint, pseudo-header order, and HEADERS priority that depends on the connection root (stream 0). A captured non-root HEADERS dependency remains available as structured request metadata but is not copied to the upstream hop, because HTTP/2 stream IDs are scoped to each connection and require explicit translation. The canonical four-part fingerprint string/hash contains only standalone PRIORITY frames. Connections with different connection-level fingerprints use separate upstream HTTP/2 pool entries; initial headers and trailers preserve their observed wire order for both HTTP/1 and HTTP/2.

`WithCertCachePool(capacity, intervalSecond, expireSecond)` configures the generated certificate cache. `capacity` must be a multiple of 256 when it is set; the interval and expiration values are seconds.

### Interceptors
Expand Down Expand Up @@ -386,7 +390,7 @@ import (
"context"
"crypto/tls"
"fmt"
"net/http"
"github.com/josexy/xhttp"
"time"

mitmproxy "github.com/josexy/mitmproxy-go"
Expand Down Expand Up @@ -470,6 +474,8 @@ go run ./examples/dumper/main.go -cacert certs/ca.crt -cakey certs/ca.key -mode
go run ./examples/dumper/main.go -cacert certs/ca.crt -cakey certs/ca.key -mode socks5 -port 10086
```

The dumper logs ordered HTTP/1 and HTTP/2 header blocks, late trailer blocks, canonical four-part HTTP/2 fingerprints and hashes, separate standalone and HEADERS priority metadata, TLS negotiation/certificate metadata, bodies, WebSocket frames, and raw TCP tunnel metadata.

### Modify Content

```bash
Expand Down
2 changes: 1 addition & 1 deletion bufconn.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import (
"bufio"
"errors"
"fmt"
"github.com/josexy/xhttp"
"io"
"net"
"net/http"

"github.com/josexy/mitmproxy-go/buf"
"github.com/josexy/websocket"
Expand Down
2 changes: 1 addition & 1 deletion domain_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"net/netip"
"strings"

"golang.org/x/net/idna"
"github.com/josexy/net/idna"
)

type trieNode struct {
Expand Down
2 changes: 1 addition & 1 deletion examples/chain-interceptors/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import (
"context"
"flag"
"fmt"
"github.com/josexy/xhttp"
"log/slog"
"net/http"
"os"

"github.com/josexy/mitmproxy-go"
Expand Down
178 changes: 173 additions & 5 deletions examples/dumper/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@ import (
"io"
"log/slog"
"net"
"net/http"
"net/http/httputil"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"time"

"github.com/josexy/mitmproxy-go"
"github.com/josexy/mitmproxy-go/metadata"
http "github.com/josexy/xhttp"
"github.com/josexy/xhttp/httputil"
)

const CHUNK_SIZE = 512
Expand All @@ -31,13 +32,151 @@ const (
CHUNK_TYPE_RSP
)

type headerBlockDump struct {
Kind string `json:"kind"`
ProtoMajor int `json:"proto_major,omitempty"`
StatusCode int `json:"status_code,omitempty"`
Truncated bool `json:"truncated,omitempty"`
Fields []http.HeaderField `json:"fields"`
}

type http2FingerprintDump struct {
Canonical string `json:"canonical"`
Hash string `json:"hash"`
Settings []http.Setting `json:"settings"`
WindowUpdate uint32 `json:"window_update"`
Priorities []http.FingerprintPriority `json:"priorities"`
HeaderPriority *http.FingerprintHeaderPriority `json:"header_priority,omitempty"`
PseudoHeaderOrder []string `json:"pseudo_header_order"`
}

type wireProfileDump struct {
HeaderOrder []string
TrailerOrder []string
HeaderBlocks []headerBlockDump
HTTP2Fingerprint *http2FingerprintDump
}

func requestWireProfileDump(req *http.Request) wireProfileDump {
order := mitmproxy.RequestWireHeaderOrder(req)
dump := wireProfileDump{
HeaderOrder: append([]string(nil), order.Headers...),
TrailerOrder: append([]string(nil), order.Trailers...),
HeaderBlocks: headerBlocksDump(mitmproxy.RequestWireHeaderBlocks(req)),
}
if fingerprint, ok := mitmproxy.RequestHTTP2Fingerprint(req); ok {
var headerPriority *http.FingerprintHeaderPriority
if fingerprint.HeaderPriority != nil {
cloned := *fingerprint.HeaderPriority
headerPriority = &cloned
}
dump.HTTP2Fingerprint = &http2FingerprintDump{
Canonical: fingerprint.String(),
Hash: fingerprint.Hash(),
Settings: append([]http.Setting(nil), fingerprint.Settings...),
WindowUpdate: fingerprint.WindowUpdate,
Priorities: append([]http.FingerprintPriority(nil), fingerprint.Priorities...),
HeaderPriority: headerPriority,
PseudoHeaderOrder: append([]string(nil), fingerprint.PseudoHeaderOrder...),
}
}
return dump
}

func responseWireProfileDump(response *http.Response) wireProfileDump {
order := mitmproxy.ResponseWireHeaderOrder(response)
return wireProfileDump{
HeaderOrder: append([]string(nil), order.Headers...),
TrailerOrder: append([]string(nil), order.Trailers...),
HeaderBlocks: headerBlocksDump(mitmproxy.ResponseWireHeaderBlocks(response)),
}
}

func headerBlocksDump(blocks []http.HeaderBlock) []headerBlockDump {
if len(blocks) == 0 {
return nil
}
dump := make([]headerBlockDump, len(blocks))
for i, block := range blocks {
dump[i] = headerBlockDump{
Kind: headerBlockKindName(block.Kind),
ProtoMajor: block.ProtoMajor,
StatusCode: block.StatusCode,
Truncated: block.Truncated,
Fields: append([]http.HeaderField(nil), block.Fields...),
}
}
return dump
}

func headerBlockKindName(kind http.HeaderBlockKind) string {
switch kind {
case http.HeaderBlockInitial:
return "initial"
case http.HeaderBlockInformational:
return "informational"
case http.HeaderBlockTrailer:
return "trailer"
default:
return fmt.Sprintf("unknown(%d)", kind)
}
}

func logWireProfile(ctx context.Context, message, phase string, dump wireProfileDump) {
attrs := []slog.Attr{
slog.String("phase", phase),
slog.Any("header_order", dump.HeaderOrder),
slog.Any("trailer_order", dump.TrailerOrder),
slog.Any("header_blocks", dump.HeaderBlocks),
}
if fingerprint := dump.HTTP2Fingerprint; fingerprint != nil {
attrs = append(attrs, slog.Group("http2_fingerprint",
slog.String("canonical", fingerprint.Canonical),
slog.String("hash", fingerprint.Hash),
slog.Any("settings", fingerprint.Settings),
slog.Uint64("window_update", uint64(fingerprint.WindowUpdate)),
slog.Any("priorities", fingerprint.Priorities),
slog.Any("header_priority", fingerprint.HeaderPriority),
slog.Any("pseudo_header_order", fingerprint.PseudoHeaderOrder),
))
}
slog.LogAttrs(ctx, slog.LevelDebug, message, attrs...)
}

type observedReadCloser struct {
io.ReadCloser
once sync.Once
onDone func()
}

func (r *observedReadCloser) complete() {
r.once.Do(func() {
if r.onDone != nil {
r.onDone()
}
})
}

func (r *observedReadCloser) Read(p []byte) (int, error) {
n, err := r.ReadCloser.Read(p)
if err != nil {
r.complete()
}
return n, err
}

func (r *observedReadCloser) Close() error {
r.complete()
return r.ReadCloser.Close()
}

type bodyDecoder struct {
reader io.ReadCloser
pw *io.PipeWriter
}

func newBodyDecoder(r io.ReadCloser, encoding string, chunkType int) (io.ReadCloser, error) {
if r == http.NoBody { // no body and no need to replace it
if r == nil || r == http.NoBody { // no body and no need to replace it
return r, nil
}
if encoding == "" {
Expand Down Expand Up @@ -251,7 +390,22 @@ func httpInterceptor(ctx context.Context, req *http.Request, invoker mitmproxy.H
)
}

req.Body, _ = newBodyDecoder(req.Body, req.Header.Get("Content-Encoding"), CHUNK_TYPE_REQ)
var err error
req.Body, err = newBodyDecoder(req.Body, req.Header.Get("Content-Encoding"), CHUNK_TYPE_REQ)
if err != nil {
return nil, err
}
logWireProfile(ctx, "request wire profile", "initial", requestWireProfileDump(req))
if req.Body == nil || req.Body == http.NoBody {
logWireProfile(ctx, "request wire profile", "complete", requestWireProfileDump(req))
} else {
req.Body = &observedReadCloser{
ReadCloser: req.Body,
onDone: func() {
logWireProfile(ctx, "request wire profile", "complete", requestWireProfileDump(req))
},
}
}

rsp, err := invoker.Invoke(req)
if err != nil {
Expand All @@ -277,8 +431,22 @@ func httpInterceptor(ctx context.Context, req *http.Request, invoker mitmproxy.H
)

rsp.Body, err = newBodyDecoder(rsp.Body, rsp.Header.Get("Content-Encoding"), CHUNK_TYPE_RSP)
if err != nil {
return rsp, err
}
logWireProfile(ctx, "response wire profile", "initial", responseWireProfileDump(rsp))
if rsp.Body == nil || rsp.Body == http.NoBody {
logWireProfile(ctx, "response wire profile", "complete", responseWireProfileDump(rsp))
} else {
rsp.Body = &observedReadCloser{
ReadCloser: rsp.Body,
onDone: func() {
logWireProfile(ctx, "response wire profile", "complete", responseWireProfileDump(rsp))
},
}
}

return rsp, err
return rsp, nil
}

func durationBetween(start, end time.Time) time.Duration {
Expand Down
2 changes: 1 addition & 1 deletion examples/dynamic-config/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import (
"crypto/tls"
"flag"
"fmt"
"github.com/josexy/xhttp"
"io"
"log/slog"
"net"
"net/http"
"net/url"
"os"
"os/signal"
Expand Down
2 changes: 1 addition & 1 deletion examples/helloworld/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import (
"context"
"flag"
"fmt"
"github.com/josexy/xhttp"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
Expand Down
2 changes: 1 addition & 1 deletion examples/modify-content/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import (
"context"
"flag"
"fmt"
"github.com/josexy/xhttp"
"io"
"log/slog"
"net/http"
"os"
"strings"

Expand Down
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ module github.com/josexy/mitmproxy-go
go 1.26.5

require (
github.com/josexy/websocket v0.0.0-20260219083038-11b2ba10886b
github.com/josexy/net v0.0.0-20260809092324-c0d6b15b8e7b
github.com/josexy/websocket v0.0.0-20260809103806-4bb5473da1cf
github.com/josexy/xhttp v0.0.0-20260809094234-b930f9aa588a
github.com/refraction-networking/utls v1.8.2
golang.org/x/net v0.57.0
)

require (
Expand Down
10 changes: 6 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM=
github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/josexy/websocket v0.0.0-20260219083038-11b2ba10886b h1:ahmk86ulmaZRnXZFfLWPahG0FCcBw6Bco8NML+4UCNU=
github.com/josexy/websocket v0.0.0-20260219083038-11b2ba10886b/go.mod h1:E1y5c7BPj8laYUGMExE3snEx4mLawJGeVztAKQQeNcg=
github.com/josexy/net v0.0.0-20260809092324-c0d6b15b8e7b h1:Wvj0rSQ+WISVjpMnx2Wci0nrRb9yAL444HaMbAWs5xk=
github.com/josexy/net v0.0.0-20260809092324-c0d6b15b8e7b/go.mod h1:U2/ek4YggQo15z4uwJcTmV/8S7EPplb9K4KQlAA0j2I=
github.com/josexy/websocket v0.0.0-20260809103806-4bb5473da1cf h1:8u1JbqscuDUvp/HDt2WVHkoHWvbHHdaGBYXjUecTmJ4=
github.com/josexy/websocket v0.0.0-20260809103806-4bb5473da1cf/go.mod h1:VqFW1+LXDzh717s0H3J/AoS4G4ns297H7QldoYe0qao=
github.com/josexy/xhttp v0.0.0-20260809094234-b930f9aa588a h1:fyRdc/V1gPf0bXQx34Vdz08Zu8wk6MjHcpSsSmCVoQU=
github.com/josexy/xhttp v0.0.0-20260809094234-b930f9aa588a/go.mod h1:IJG/ZONLw42obBvUwJM45WkY/bQsj7rscaHZrqLLINs=
github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
Expand All @@ -10,8 +14,6 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
Expand Down
Loading
Loading