From 16055e8ae4359bd9d910b6358cad95b7933aed70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:12:50 +0000 Subject: [PATCH] Bump github.com/elazarl/goproxy from 1.8.5 to 1.9.0 Bumps [github.com/elazarl/goproxy](https://github.com/elazarl/goproxy) from 1.8.5 to 1.9.0. - [Release notes](https://github.com/elazarl/goproxy/releases) - [Commits](https://github.com/elazarl/goproxy/compare/v1.8.5...v1.9.0) --- updated-dependencies: - dependency-name: github.com/elazarl/goproxy dependency-version: 1.9.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 8 +- .../github.com/elazarl/goproxy/.golangci.yml | 4 +- vendor/github.com/elazarl/goproxy/h2.go | 208 --------- vendor/github.com/elazarl/goproxy/http2.go | 274 ++++++++++++ vendor/github.com/elazarl/goproxy/https.go | 406 ++++++++++++------ .../goproxy/internal/http1parser/request.go | 3 +- vendor/github.com/elazarl/goproxy/proxy.go | 14 +- vendor/modules.txt | 4 +- 9 files changed, 572 insertions(+), 351 deletions(-) delete mode 100644 vendor/github.com/elazarl/goproxy/h2.go create mode 100644 vendor/github.com/elazarl/goproxy/http2.go diff --git a/go.mod b/go.mod index 3db4761..e00379d 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25.0 require ( github.com/BurntSushi/toml v1.6.0 - github.com/elazarl/goproxy v1.8.5 + github.com/elazarl/goproxy v1.9.0 golang.org/x/net v0.57.0 ) diff --git a/go.sum b/go.sum index 6d6441f..3d471ed 100644 --- a/go.sum +++ b/go.sum @@ -4,18 +4,14 @@ github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9 github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/elazarl/goproxy v1.8.5 h1:33R3Q6geBd2PHmjEI82s3dQWSoBKjTktg4YAFXutIoY= -github.com/elazarl/goproxy v1.8.5/go.mod h1:b5xm6W48AUHNpRTCvlnd0YVh+JafCCtsLsJZvvNTz+E= +github.com/elazarl/goproxy v1.9.0 h1:2j3c13lD5v0QTjxphJSSIHS7w8/m/pzSHtLMPOpznC0= +github.com/elazarl/goproxy v1.9.0/go.mod h1:THdE5ix2clxX9lZzcICPpZ67d6CdrPZxdOYsNgU5e30= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= 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/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/vendor/github.com/elazarl/goproxy/.golangci.yml b/vendor/github.com/elazarl/goproxy/.golangci.yml index aaa111a..b6af05e 100644 --- a/vendor/github.com/elazarl/goproxy/.golangci.yml +++ b/vendor/github.com/elazarl/goproxy/.golangci.yml @@ -15,7 +15,6 @@ linters: - errchkjson - errname - errorlint - - exhaustive - fatcontext - forbidigo - forcetypeassert @@ -24,7 +23,7 @@ linters: - gocritic - godot - goheader - - gomodguard + - gomodguard_v2 - goprintffuncname - gosec - gosmopolitan @@ -67,6 +66,7 @@ linters: - dupl - dupword - err113 + - exhaustive - exhaustruct - funlen - ginkgolinter diff --git a/vendor/github.com/elazarl/goproxy/h2.go b/vendor/github.com/elazarl/goproxy/h2.go deleted file mode 100644 index 2f6342c..0000000 --- a/vendor/github.com/elazarl/goproxy/h2.go +++ /dev/null @@ -1,208 +0,0 @@ -package goproxy - -import ( - "bufio" - "context" - "crypto/tls" - "errors" - "io" - "net" - "net/http" - "strings" - - "golang.org/x/net/http2" -) - -var ErrInvalidH2Frame = errors.New("invalid H2 frame") - -// H2Transport is an implementation of RoundTripper that abstracts an entire -// HTTP/2 session, sending all client frames to the server and responses back -// to the client. -type H2Transport struct { - ClientReader io.Reader - ClientWriter io.Writer - TLSConfig *tls.Config - Host string -} - -// RoundTrip executes an HTTP/2 session (including all contained streams). -// The request and response are ignored but any error encountered during the -// proxying from the session is returned as a result of the invocation. -func (r *H2Transport) RoundTrip(_ *http.Request) (*http.Response, error) { - raddr := r.Host - if !strings.Contains(raddr, ":") { - raddr += ":443" - } - rawServerTLS, err := dial("tcp", raddr) - if err != nil { - return nil, err - } - defer rawServerTLS.Close() - // Ensure that we only advertise HTTP/2 as the accepted protocol. - r.TLSConfig.NextProtos = []string{http2.NextProtoTLS} - // Initiate TLS and check remote host name against certificate. - rawServerTLS = tls.Client(rawServerTLS, r.TLSConfig) - rawTLSConn, ok := rawServerTLS.(*tls.Conn) - if !ok { - return nil, errors.New("invalid TLS connection") - } - if err = rawTLSConn.HandshakeContext(context.Background()); err != nil { - return nil, err - } - if r.TLSConfig == nil || !r.TLSConfig.InsecureSkipVerify { - if err = rawTLSConn.VerifyHostname(raddr[:strings.LastIndex(raddr, ":")]); err != nil { - return nil, err - } - } - // Send new client preface to match the one parsed in req. - if _, err := io.WriteString(rawServerTLS, http2.ClientPreface); err != nil { - return nil, err - } - serverTLSReader := bufio.NewReader(rawServerTLS) - cToS := http2.NewFramer(rawServerTLS, r.ClientReader) - sToC := http2.NewFramer(r.ClientWriter, serverTLSReader) - errSToC := make(chan error) - errCToS := make(chan error) - go func() { - for { - if err := proxyFrame(sToC); err != nil { - errSToC <- err - break - } - } - }() - go func() { - for { - if err := proxyFrame(cToS); err != nil { - errCToS <- err - break - } - } - }() - for i := 0; i < 2; i++ { - select { - case err := <-errSToC: - if !errors.Is(err, io.EOF) { - return nil, err - } - case err := <-errCToS: - if !errors.Is(err, io.EOF) { - return nil, err - } - } - } - return nil, nil -} - -func dial(network, addr string) (c net.Conn, err error) { - addri, err := net.ResolveTCPAddr(network, addr) - if err != nil { - return - } - c, err = net.DialTCP(network, nil, addri) - return -} - -// proxyFrame reads a single frame from the Framer and, when successful, writes -// a ~identical one back to the Framer. -func proxyFrame(fr *http2.Framer) error { - f, err := fr.ReadFrame() - if err != nil { - return err - } - switch f.Header().Type { - case http2.FrameData: - tf, ok := f.(*http2.DataFrame) - if !ok { - return ErrInvalidH2Frame - } - terr := fr.WriteData(tf.StreamID, tf.StreamEnded(), tf.Data()) - if terr == nil && tf.StreamEnded() { - terr = io.EOF - } - return terr - case http2.FrameHeaders: - tf, ok := f.(*http2.HeadersFrame) - if !ok { - return ErrInvalidH2Frame - } - terr := fr.WriteHeaders(http2.HeadersFrameParam{ - StreamID: tf.StreamID, - BlockFragment: tf.HeaderBlockFragment(), - EndStream: tf.StreamEnded(), - EndHeaders: tf.HeadersEnded(), - PadLength: 0, - Priority: tf.Priority, - }) - if terr == nil && tf.StreamEnded() { - terr = io.EOF - } - return terr - case http2.FrameContinuation: - tf, ok := f.(*http2.ContinuationFrame) - if !ok { - return ErrInvalidH2Frame - } - return fr.WriteContinuation(tf.StreamID, tf.HeadersEnded(), tf.HeaderBlockFragment()) - case http2.FrameGoAway: - tf, ok := f.(*http2.GoAwayFrame) - if !ok { - return ErrInvalidH2Frame - } - return fr.WriteGoAway(tf.StreamID, tf.ErrCode, tf.DebugData()) - case http2.FramePing: - tf, ok := f.(*http2.PingFrame) - if !ok { - return ErrInvalidH2Frame - } - return fr.WritePing(tf.IsAck(), tf.Data) - case http2.FrameRSTStream: - tf, ok := f.(*http2.RSTStreamFrame) - if !ok { - return ErrInvalidH2Frame - } - return fr.WriteRSTStream(tf.StreamID, tf.ErrCode) - case http2.FrameSettings: - tf, ok := f.(*http2.SettingsFrame) - if !ok { - return ErrInvalidH2Frame - } - if tf.IsAck() { - return fr.WriteSettingsAck() - } - var settings []http2.Setting - // NOTE: If we want to parse headers, need to handle - // settings where s.ID == http2.SettingHeaderTableSize and - // accordingly update the Framer options. - for i := 0; i < tf.NumSettings(); i++ { - settings = append(settings, tf.Setting(i)) - } - return fr.WriteSettings(settings...) - case http2.FrameWindowUpdate: - tf, ok := f.(*http2.WindowUpdateFrame) - if !ok { - return ErrInvalidH2Frame - } - return fr.WriteWindowUpdate(tf.StreamID, tf.Increment) - case http2.FramePriority: - tf, ok := f.(*http2.PriorityFrame) - if !ok { - return ErrInvalidH2Frame - } - return fr.WritePriority(tf.StreamID, tf.PriorityParam) - case http2.FramePushPromise: - tf, ok := f.(*http2.PushPromiseFrame) - if !ok { - return ErrInvalidH2Frame - } - return fr.WritePushPromise(http2.PushPromiseParam{ - StreamID: tf.StreamID, - PromiseID: tf.PromiseID, - BlockFragment: tf.HeaderBlockFragment(), - EndHeaders: tf.HeadersEnded(), - PadLength: 0, - }) - default: - return errors.New("Unsupported frame: " + string(f.Header().Type)) - } -} diff --git a/vendor/github.com/elazarl/goproxy/http2.go b/vendor/github.com/elazarl/goproxy/http2.go new file mode 100644 index 0000000..4628696 --- /dev/null +++ b/vendor/github.com/elazarl/goproxy/http2.go @@ -0,0 +1,274 @@ +package goproxy + +import ( + "context" + "crypto/tls" + "errors" + "io" + "mime" + "net" + "net/http" + "strings" + "sync/atomic" + "time" + + "golang.org/x/net/http2" +) + +// h2StreamConn wraps an HTTP/2 CONNECT stream as a net.Conn. +// +// When a CONNECT request arrives over HTTP/2, hijacking is unavailable. +// We implement net.Conn directly on top of the H2 stream: +// - Read: r.Body (the request body carries client -> proxy data) +// - Write: w (the response body carries proxy -> client data) +type h2StreamConn struct { + r io.ReadCloser + w http.ResponseWriter + ctrl *http.ResponseController + local net.Addr + remote net.Addr +} + +// responseWriterProvider is implemented by h2StreamConn so that httpError +// can recover an http.ResponseWriter from an io.Writer in H2 mode. +type responseWriterProvider interface { + ResponseWriter() http.ResponseWriter +} + +func newH2StreamConn(w http.ResponseWriter, r *http.Request) *h2StreamConn { + return &h2StreamConn{ + r: r.Body, + w: w, + ctrl: http.NewResponseController(w), + local: h2streamAddr("h2-proxy"), + remote: h2streamAddr(r.RemoteAddr), + } +} + +func (c *h2StreamConn) ResponseWriter() http.ResponseWriter { + return c.w +} + +func (c *h2StreamConn) Read(b []byte) (int, error) { + return c.r.Read(b) +} + +func (c *h2StreamConn) Write(b []byte) (int, error) { + n, err := c.w.Write(b) + if err == nil { + _ = c.ctrl.Flush() + } + return n, err +} + +func (c *h2StreamConn) Close() error { + return c.r.Close() +} + +func (c *h2StreamConn) LocalAddr() net.Addr { + return c.local +} + +func (c *h2StreamConn) RemoteAddr() net.Addr { + return c.remote +} + +func (c *h2StreamConn) SetDeadline(t time.Time) error { + rerr := c.ctrl.SetReadDeadline(t) + werr := c.ctrl.SetWriteDeadline(t) + if rerr != nil { + return rerr + } + return werr +} + +func (c *h2StreamConn) SetReadDeadline(t time.Time) error { + return c.ctrl.SetReadDeadline(t) +} + +func (c *h2StreamConn) SetWriteDeadline(t time.Time) error { + return c.ctrl.SetWriteDeadline(t) +} + +type h2streamAddr string + +func (a h2streamAddr) Network() string { + return "h2" +} + +func (a h2streamAddr) String() string { + return string(a) +} + +// serveH2Mitm serves an HTTP/2 MITM connection via an embedded http2.Server. +// - client is the underlying connection (*tls.Conn for ALPN-h2, plain net.Conn for h2c). +// - host is the CONNECT target (e.g. "example.com:443"). +// - parentCtx carries the UserData / CertStore / RoundTripper from the CONNECT handler, +// propagated into every per-stream ProxyCtx. +func (proxy *ProxyHttpServer) serveH2Mitm(client net.Conn, host string, parentCtx *ProxyCtx) { + scheme := "https" + if _, isTLS := client.(*tls.Conn); !isTLS { + scheme = "http" + } + + proxy.h2Server.ServeConn(client, &http2.ServeConnOpts{ + Context: context.Background(), + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + proxy.handleH2MitmStream(w, r, host, scheme, parentCtx) + }), + }) +} + +// handleH2MitmStream handles a single HTTP/2 stream inside a MITM session, +// running request/response filters and forwarding to the upstream server. +func (proxy *ProxyHttpServer) handleH2MitmStream( + w http.ResponseWriter, + r *http.Request, + host, scheme string, + parentCtx *ProxyCtx, +) { + // r.Host contains the :authority pseudo-header: prefer it over the + // fallback CONNECT host so virtual-hosting works correctly. + if r.Host != "" { + r.URL.Host = r.Host + } else if r.URL.Host == "" { + r.URL.Host = host + } + r.URL.Scheme = scheme + + // Carry over the connecting client's address so that IP-matching + // filters (e.g. SrcIpIs) keep working. + r.RemoteAddr = parentCtx.Req.RemoteAddr + + reqCtx, finishRequest := context.WithCancel(r.Context()) + defer finishRequest() + r = r.WithContext(reqCtx) + + ctx := &ProxyCtx{ + Req: r, + Session: atomic.AddInt64(&proxy.sess, 1), + Proxy: proxy, + UserData: parentCtx.UserData, + RoundTripper: parentCtx.RoundTripper, + certStore: parentCtx.certStore, + } + + req, resp := proxy.filterRequest(r, ctx) + if resp == nil { + removeH2HopByHopHeaders(req) + if !proxy.KeepHeader { + RemoveProxyHeaders(ctx, req) + } + + // bodyless h2 requests arrive with a non-nil empty Body; forwarding as-is + // makes the upstream transport send a phantom body (-1). keep it bodyless. + if req.ContentLength == 0 { + req.Body = http.NoBody + } + + var err error + resp, err = ctx.RoundTrip(req) + if err != nil { + ctx.Warnf("HTTP/2 MITM: upstream RoundTrip failed: %v", err) + http.Error(w, err.Error(), http.StatusBadGateway) + return + } + ctx.Logf("resp %v", resp.Status) + } + + origBody := resp.Body + resp = proxy.filterResponse(resp, ctx) + defer resp.Body.Close() + + copyHeaders(w.Header(), resp.Header, proxy.KeepDestinationHeaders) + + // If the body was replaced by a filter, drop Content-Length so + // the http2 framer can stream it without a length mismatch. + if resp.Body != origBody { + w.Header().Del("Content-Length") + } + + // Announce pre-known trailers before WriteHeader (see handleHttp in http.go). + announcedTrailers := len(resp.Trailer) + if announcedTrailers > 0 { + trailerKeys := make([]string, 0, announcedTrailers) + for k := range resp.Trailer { + trailerKeys = append(trailerKeys, k) + } + w.Header().Add("Trailer", strings.Join(trailerKeys, ", ")) + } + + w.WriteHeader(resp.StatusCode) + + if resp.Body != nil { + if shouldFlushStreaming(resp) { + // Streaming (gRPC/Connect/SSE/chunked): flush each chunk so it reaches the + // client as it arrives. Mirrors net/http/httputil.ReverseProxy.flushInterval. + rc := http.NewResponseController(w) + buf := make([]byte, 32*1024) + for { + nr, er := resp.Body.Read(buf) + if nr > 0 { + if _, ew := w.Write(buf[:nr]); ew != nil { + ctx.Warnf("HTTP/2 MITM: error writing response body: %v", ew) + break + } + _ = rc.Flush() + } + if er != nil { + // Mirror net/http/httputil.ReverseProxy.copyBuffer: io.EOF is the + // normal end of stream and context.Canceled means the client went + // away or cancelled the request, so neither is worth logging. + if er != io.EOF && !errors.Is(er, context.Canceled) { + ctx.Warnf("HTTP/2 MITM: error reading response body: %v", er) + } + break + } + } + } else { + // Fixed-length response: let the h2 server batch writes for throughput. + if _, err := io.Copy(w, resp.Body); err != nil { + ctx.Warnf("HTTP/2 MITM: error writing response body: %v", err) + } + } + } + + // Forward response trailers after the body: pre-announced by name, the rest + // (h2/gRPC send them unannounced) via http.TrailerPrefix; Flush forces + // chunking. Mirrors handleHttp in http.go. + if len(resp.Trailer) > 0 { + if rc := http.NewResponseController(w); rc != nil { + _ = rc.Flush() + } + if len(resp.Trailer) == announcedTrailers { + copyHeaders(w.Header(), resp.Trailer, proxy.KeepDestinationHeaders) + } else { + for k, vs := range resp.Trailer { + k = http.TrailerPrefix + k + for _, v := range vs { + w.Header().Add(k, v) + } + } + } + } +} + +// shouldFlushStreaming reports whether resp should be forwarded with a flush after +// each chunk, mirroring net/http/httputil.ReverseProxy.flushInterval: Server-Sent +// Events, or any response with an unknown (-1) Content-Length (gRPC, Connect, chunked). +func shouldFlushStreaming(resp *http.Response) bool { + if baseCT, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type")); baseCT == "text/event-stream" { + return true + } + return resp.ContentLength == -1 +} + +// removeH2HopByHopHeaders deletes HTTP/1.x hop-by-hop headers that are +// illegal in HTTP/2 (RFC 9113 §8.2.2). +func removeH2HopByHopHeaders(r *http.Request) { + r.Header.Del("Connection") + r.Header.Del("Keep-Alive") + r.Header.Del("Proxy-Connection") + r.Header.Del("Transfer-Encoding") + r.Header.Del("Upgrade") +} diff --git a/vendor/github.com/elazarl/goproxy/https.go b/vendor/github.com/elazarl/goproxy/https.go index bd4137a..4f88051 100644 --- a/vendor/github.com/elazarl/goproxy/https.go +++ b/vendor/github.com/elazarl/goproxy/https.go @@ -2,6 +2,7 @@ package goproxy import ( "bufio" + "bytes" "context" "crypto/tls" "errors" @@ -11,14 +12,59 @@ import ( "net/http" "net/url" "os" + "slices" "strings" "sync" "sync/atomic" + "time" "github.com/elazarl/goproxy/internal/http1parser" "github.com/elazarl/goproxy/internal/signer" + "golang.org/x/net/http2" ) +var responseHeadTerminator = []byte("\r\n\r\n") + +type responseHeadWriter struct { + writer io.Writer + head bytes.Buffer + wroteHead bool +} + +func (w *responseHeadWriter) Write(p []byte) (int, error) { + if w.wroteHead { + return w.writer.Write(p) + } + + buffered := w.head.Len() + _, _ = w.head.Write(p) + headEnd := bytes.Index(w.head.Bytes(), responseHeadTerminator) + if headEnd < 0 { + return len(p), nil + } + headEnd += len(responseHeadTerminator) + + data := w.head.Bytes() + n, err := w.writer.Write(data[:headEnd]) + if err != nil || n != headEnd { + current := max(0, min(len(p), n-buffered)) + if err == nil { + err = io.ErrShortWrite + } + return current, err + } + + w.wroteHead = true + body := data[headEnd:] + w.head.Reset() + if len(body) == 0 { + return len(p), nil + } + + n, err = w.writer.Write(body) + return headEnd - buffered + n, err +} + // ConnectActionLiteral defines the action the proxy should take // when it receives an HTTP CONNECT request from a client. type ConnectActionLiteral int @@ -97,25 +143,11 @@ type ConnectAction struct { } func stripPort(s string) string { - var ix int - if strings.Contains(s, "[") && strings.Contains(s, "]") { - // ipv6 address example: [2606:4700:4700::1111]:443 - // strip '[' and ']' - s = strings.ReplaceAll(s, "[", "") - s = strings.ReplaceAll(s, "]", "") - - ix = strings.LastIndexAny(s, ":") - if ix == -1 { - return s - } - } else { - // ipv4 - ix = strings.IndexRune(s, ':') - if ix == -1 { - return s - } + host, _, err := net.SplitHostPort(s) + if err != nil { + return s } - return s[:ix] + return host } func (proxy *ProxyHttpServer) dial(ctx *ProxyCtx, network, addr string) (c net.Conn, err error) { @@ -156,21 +188,11 @@ var _ halfClosable = (*net.TCPConn)(nil) func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request) { ctx := &ProxyCtx{Req: r, Session: atomic.AddInt64(&proxy.sess, 1), Proxy: proxy, certStore: proxy.CertStore} - hij, ok := w.(http.Hijacker) - if !ok { - panic("httpserver does not support hijacking") - } - - proxyClient, _, e := hij.Hijack() - if e != nil { - panic("Cannot hijack connection " + e.Error()) - } - + // Run CONNECT handlers first, before any connection hijacking ctx.Logf("Running %d CONNECT handlers", len(proxy.httpsHandlers)) todo, host := OkConnect, r.URL.Host for i, h := range proxy.httpsHandlers { newtodo, newhost := h.HandleConnect(host, ctx) - // If found a result, break the loop immediately if newtodo != nil { todo, host = newtodo, newhost @@ -178,9 +200,90 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request break } } + + hij, canHijack := w.(http.Hijacker) + + // Handle actions that do NOT require a bidirectional tunnel + switch todo.Action { + case ConnectReject: + if ctx.Resp != nil { + if canHijack { + proxyClient, _, e := hij.Hijack() + if e != nil { + ctx.Warnf("Cannot hijack connection: %v", e) + return + } + defer proxyClient.Close() + if err := ctx.Resp.Write(proxyClient); err != nil { + ctx.Warnf("Cannot write response that reject http CONNECT: %v", err) + } + } else { + // HTTP/2: write the rejection as a proper HTTP response. + copyHeaders(w.Header(), ctx.Resp.Header, proxy.KeepDestinationHeaders) + w.WriteHeader(ctx.Resp.StatusCode) + if ctx.Resp.Body != nil { + _, _ = io.Copy(w, ctx.Resp.Body) + _ = ctx.Resp.Body.Close() + } + } + } else if canHijack { + proxyClient, _, _ := hij.Hijack() + _ = proxyClient.Close() + } else { + http.Error(w, "Connection rejected", http.StatusForbidden) + } + return + + case ConnectProxyAuthHijack: + if !canHijack { + // Extended-CONNECT over HTTP/2 does not support 407 hijack flow. + ctx.Warnf("ConnectProxyAuthHijack is not supported when the proxy is served over HTTP/2") + http.Error(w, "Proxy auth hijack not supported in HTTP/2 mode", http.StatusInternalServerError) + return + } + proxyClient, _, e := hij.Hijack() + if e != nil { + ctx.Warnf("Cannot hijack connection: %v", e) + return + } + _, _ = proxyClient.Write([]byte("HTTP/1.1 407 Proxy Authentication Required\r\n")) + todo.Hijack(r, proxyClient, ctx) + return + } + + // All remaining actions need a bidirectional tunnel (proxyClient) + // + // In HTTP/1.1 mode we hijack the connection. + // In HTTP/2 mode (r.ProtoMajor == 2), we take the H2 path as explained in RFC 8441 extended-CONNECT. + var proxyClient net.Conn + isH2Tunnel := false + + if canHijack { + var e error + proxyClient, _, e = hij.Hijack() + if e != nil { + ctx.Warnf("Cannot hijack connection: %v", e) + return + } + } else if r.ProtoMajor == 2 { + // The incoming CONNECT arrived over HTTP/2 (no hijacking available). + // Use h2StreamConn so reads/writes go directly against the H2 stream. + isH2Tunnel = true + // Wrap the H2 stream directly as a net.Conn — no intermediate pipe, + // no goroutines, no unnecessary copies. + proxyClient = newH2StreamConn(w, r) + } else { + // Hijacking is not supported and the request is not HTTP/2. + // This can happen if goproxy is wrapped by middleware that strips the + // Hijacker interface. There is no safe way to tunnel here. + ctx.Warnf("CONNECT: server does not support hijacking and request is not HTTP/2 (proto=%s)", r.Proto) + http.Error(w, "Proxy: cannot establish tunnel (no hijacking support)", http.StatusInternalServerError) + return + } + switch todo.Action { case ConnectAccept: - if !hasPort.MatchString(host) { + if !hasPort(host) { host += ":80" } targetSiteCon, err := proxy.connectDial(ctx, "tcp", host) @@ -190,64 +293,105 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request return } ctx.Logf("Accepting CONNECT to %s", host) - _, _ = proxyClient.Write([]byte("HTTP/1.0 200 Connection established\r\n\r\n")) + // In HTTP/1.1 mode the client is waiting for the 200 confirmation; + // In HTTP/2 mode we send it now, after we know the dial succeeded. + if isH2Tunnel { + w.WriteHeader(http.StatusOK) + if f, ok := w.(http.Flusher); ok { + f.Flush() + } - targetTCP, targetOK := targetSiteCon.(halfClosable) - proxyClientTCP, clientOK := proxyClient.(halfClosable) - if targetOK && clientOK { - go func() { - var wg sync.WaitGroup - wg.Add(2) - go copyAndClose(ctx, targetTCP, proxyClientTCP, &wg) - go copyAndClose(ctx, proxyClientTCP, targetTCP, &wg) - wg.Wait() - // Make sure to close the underlying TCP socket. - // CloseRead() and CloseWrite() keep it open until its timeout, - // causing error when there are thousands of requests. - proxyClientTCP.Close() - targetTCP.Close() - }() - } else { - // There is a race with the runtime here. In the case where the - // connection to the target site times out, we cannot control which - // io.Copy loop will receive the timeout signal first. This means - // that in some cases the error passed to the ConnErrorHandler will - // be the timeout error, and in other cases it will be an error raised - // by the use of a closed network connection. - // - // 2020/05/28 23:42:17 [001] WARN: Error copying to client: read tcp 127.0.0.1:33742->127.0.0.1:34763: i/o timeout - // 2020/05/28 23:42:17 [001] WARN: Error copying to client: read tcp 127.0.0.1:45145->127.0.0.1:60494: use of closed - // network connection - // - // It's also not possible to synchronize these connection closures due to - // TCP connections which are half-closed. When this happens, only the one - // side of the connection breaks out of its io.Copy loop. The other side - // of the connection remains open until it either times out or is reset by - // the client. + // H2 handler must block until the tunnel closes, the HTTP/2 server + // keeps the stream alive only while ServeHTTP is running. + var wg sync.WaitGroup + wg.Add(2) go func() { + defer wg.Done() err := copyOrWarn(ctx, targetSiteCon, proxyClient) if err != nil && proxy.ConnectionErrHandler != nil { proxy.ConnectionErrHandler(proxyClient, ctx, err) } _ = targetSiteCon.Close() + // Goroutine 2 may be blocked writing to the H2 stream (flow-control + // stall). Set a past deadline to unblock it immediately. + _ = proxyClient.SetWriteDeadline(time.Now()) }() - go func() { + defer wg.Done() _ = copyOrWarn(ctx, proxyClient, targetSiteCon) + // Close r.Body to unblock goroutine 1 if it is still reading from + // the H2 stream. _ = proxyClient.Close() }() + wg.Wait() + } else { + _, _ = proxyClient.Write([]byte("HTTP/1.0 200 Connection established\r\n\r\n")) + + targetTCP, targetOK := targetSiteCon.(halfClosable) + proxyClientTCP, clientOK := proxyClient.(halfClosable) + if targetOK && clientOK { + go func() { + var wg sync.WaitGroup + wg.Add(2) + go copyAndClose(ctx, targetTCP, proxyClientTCP, &wg) + go copyAndClose(ctx, proxyClientTCP, targetTCP, &wg) + wg.Wait() + // Make sure to close the underlying TCP socket. + // CloseRead() and CloseWrite() keep it open until its timeout, + // causing error when there are thousands of requests. + proxyClientTCP.Close() + targetTCP.Close() + }() + } else { + // There is a race with the runtime here. In the case where the + // connection to the target site times out, we cannot control which + // io.Copy loop will receive the timeout signal first. This means + // that in some cases the error passed to the ConnErrorHandler will + // be the timeout error, and in other cases it will be an error raised + // by the use of a closed network connection. + // + // 2020/05/28 23:42:17 [001] WARN: Error copying to client: read tcp 127.0.0.1:33742->127.0.0.1:34763: i/o timeout + // 2020/05/28 23:42:17 [001] WARN: Error copying to client: read tcp 127.0.0.1:45145->127.0.0.1:60494: use of closed + // network connection + // + // It's also not possible to synchronize these connection closures due to + // TCP connections which are half-closed. When this happens, only the one + // side of the connection breaks out of its io.Copy loop. The other side + // of the connection remains open until it either times out or is reset by + // the client. + go func() { + err := copyOrWarn(ctx, targetSiteCon, proxyClient) + if err != nil && proxy.ConnectionErrHandler != nil { + proxy.ConnectionErrHandler(proxyClient, ctx, err) + } + _ = targetSiteCon.Close() + }() + + go func() { + _ = copyOrWarn(ctx, proxyClient, targetSiteCon) + _ = proxyClient.Close() + }() + } } case ConnectHijack: todo.Hijack(r, proxyClient, ctx) + case ConnectHTTPMitm, ConnectMitm: - _, _ = proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) + if isH2Tunnel { + w.WriteHeader(http.StatusOK) + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + } else { + _, _ = proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) + } ctx.Logf("Received CONNECT request, mitm proxying it") - // this goes in a separate goroutine, so that the net/http server won't think we're - // still handling the request even after hijacking the connection. Those HTTP CONNECT - // request can take forever, and the server will be stuck when "closed". - // TODO: Allow Server.Close() mechanism to shut down this connection as nicely as possible - go func() { + // For HTTP/1.x (after Hijack), the MITM loop runs in a goroutine so the HTTP/1.x + // server is not blocked by the (potentially very long) tunnel and can shut down cleanly. + // For HTTP/2 (isH2Tunnel), the handler must block, the HTTP/2 server keeps the H2 + // stream alive only while ServeHTTP is running; returning early closes the stream. + mitmWork := func() { // Check if this is an HTTP or an HTTPS MITM request readBuffer := bufio.NewReader(proxyClient) peek, _ := readBuffer.Peek(1) @@ -258,11 +402,8 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request _ = client.Close() }() - var tlsConfig *tls.Config - scheme := "http" if isTLS { - scheme = "https" - tlsConfig = defaultTLSConfig + tlsConfig := defaultTLSConfig if todo.TLSConfig != nil { var err error tlsConfig, err = todo.TLSConfig(host, ctx) @@ -271,6 +412,17 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request return } } + tlsConfig = tlsConfig.Clone() + + if proxy.AllowHTTP2 { + if !slices.Contains(tlsConfig.NextProtos, "h2") { + tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2") + } + } + + if !slices.Contains(tlsConfig.NextProtos, "http/1.1") { + tlsConfig.NextProtos = append(tlsConfig.NextProtos, "http/1.1") + } // Create a TLS connection over the TCP connection rawClientTls := tls.Server(client, tlsConfig) @@ -279,6 +431,23 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request ctx.Warnf("Cannot handshake client %v %v", r.Host, err) return } + if proxy.AllowHTTP2 && rawClientTls.ConnectionState().NegotiatedProtocol == "h2" { + ctx.Logf("ALPN negotiated h2, starting http2.ServeConn") + proxy.serveH2Mitm(client, host, ctx) + return + } + } else if proxy.AllowHTTP2 { + // Handle cleartext HTTP/2 (h2c) by looking for the client preface. + preface, err := readBuffer.Peek(len(http2.ClientPreface)) + if err == nil && string(preface) == http2.ClientPreface { + proxy.serveH2Mitm(client, host, ctx) + return + } + } + + scheme := "http" + if isTLS { + scheme = "https" } clientReader := http1parser.NewRequestReader(proxy.PreventCanonicalization, client) @@ -303,8 +472,12 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request req.RemoteAddr = r.RemoteAddr ctx.Logf("req %v", r.Host) - if !strings.HasPrefix(req.URL.String(), scheme+"://") { + if !req.URL.IsAbs() { + // Origin-form request target (/path) req.URL, err = url.Parse(scheme + "://" + r.Host + req.URL.String()) + } else { + // Absolute-form request target + req.URL.Scheme = scheme } if continueLoop := func(req *http.Request) bool { @@ -325,31 +498,6 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request req, resp := proxy.filterRequest(req, ctx) if resp == nil { - if req.Method == "PRI" { - // Handle HTTP/2 connections. - - // NOTE: As of 1.22, golang's http module will not recognize or - // parse the HTTP Body for PRI requests. This leaves the body of - // the http2.ClientPreface ("SM\r\n\r\n") on the wire which we need - // to clear before setting up the connection. - reader := clientReader.Reader() - _, err := reader.Discard(6) - if err != nil { - ctx.Warnf("Failed to process HTTP2 client preface: %v", err) - return false - } - if !proxy.AllowHTTP2 { - ctx.Warnf("HTTP2 connection failed: disallowed") - return false - } - tr := H2Transport{reader, client, tlsConfig, host} - if _, err := tr.RoundTrip(req); err != nil { - ctx.Warnf("HTTP2 connection failed: %v", err) - } else { - ctx.Logf("Exiting on EOF") - } - return false - } if err != nil { if req.URL != nil { ctx.Warnf("Illegal URL %s", scheme+"://"+r.Host+req.URL.Path) @@ -422,15 +570,11 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request return false } - bw := bufio.NewWriter(client) - if err := resp.Write(bw); err != nil { + writer := &responseHeadWriter{writer: client} + if err := resp.Write(writer); err != nil { ctx.Warnf("Cannot write response from mitm'd client: %v", err) return false } - if err := bw.Flush(); err != nil { - ctx.Warnf("Cannot flush response from mitm'd client: %v", err) - return false - } return true }(req); !continueLoop { @@ -438,36 +582,44 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request } } ctx.Logf("Exiting on EOF") - }() - case ConnectProxyAuthHijack: - _, _ = proxyClient.Write([]byte("HTTP/1.1 407 Proxy Authentication Required\r\n")) - todo.Hijack(r, proxyClient, ctx) - case ConnectReject: - if ctx.Resp != nil { - if err := ctx.Resp.Write(proxyClient); err != nil { - ctx.Warnf("Cannot write response that reject http CONNECT: %v", err) - } } - _ = proxyClient.Close() + if isH2Tunnel { + mitmWork() + } else { + go mitmWork() + } } } -func httpError(w io.WriteCloser, ctx *ProxyCtx, err error) { +func httpError(w io.Writer, ctx *ProxyCtx, err error) { if ctx.Proxy.ConnectionErrHandler != nil { ctx.Proxy.ConnectionErrHandler(w, ctx, err) } else { - errorMessage := err.Error() - errStr := fmt.Sprintf( - "HTTP/1.1 502 Bad Gateway\r\nContent-Type: text/plain\r\nContent-Length: %d\r\n\r\n%s", - len(errorMessage), - errorMessage, - ) - if _, err := io.WriteString(w, errStr); err != nil { - ctx.Warnf("Error responding to client: %s", err) + var rw http.ResponseWriter + if r, ok := w.(http.ResponseWriter); ok { + rw = r + } else if h2, ok := w.(responseWriterProvider); ok { + rw = h2.ResponseWriter() + } + + if rw != nil { + http.Error(rw, err.Error(), http.StatusBadGateway) + } else { + errorMessage := err.Error() + errStr := fmt.Sprintf( + "HTTP/1.1 502 Bad Gateway\r\nContent-Type: text/plain\r\nContent-Length: %d\r\n\r\n%s", + len(errorMessage), + errorMessage, + ) + if _, err := io.WriteString(w, errStr); err != nil { + ctx.Warnf("Error responding to client: %s", err) + } } } - if err := w.Close(); err != nil { - ctx.Warnf("Error closing client connection: %s", err) + if c, ok := w.(io.Closer); ok { + if err := c.Close(); err != nil { + ctx.Warnf("Error closing client connection: %s", err) + } } } diff --git a/vendor/github.com/elazarl/goproxy/internal/http1parser/request.go b/vendor/github.com/elazarl/goproxy/internal/http1parser/request.go index 0e37bc2..1f4d76b 100644 --- a/vendor/github.com/elazarl/goproxy/internal/http1parser/request.go +++ b/vendor/github.com/elazarl/goproxy/internal/http1parser/request.go @@ -40,8 +40,7 @@ func (r *RequestReader) IsEOF() bool { return errors.Is(err, io.EOF) } -// Reader is used to take over the buffered connection data -// (e.g. with HTTP/2 data). +// Reader is used to take over the buffered connection data. // After calling this function, make sure to consume all the data related // to the current request. func (r *RequestReader) Reader() *bufio.Reader { diff --git a/vendor/github.com/elazarl/goproxy/proxy.go b/vendor/github.com/elazarl/goproxy/proxy.go index 1c65790..50fad0a 100644 --- a/vendor/github.com/elazarl/goproxy/proxy.go +++ b/vendor/github.com/elazarl/goproxy/proxy.go @@ -6,7 +6,8 @@ import ( "net" "net/http" "os" - "regexp" + + "golang.org/x/net/http2" ) // The basic proxy type. Implements http.Handler. @@ -68,9 +69,15 @@ type ProxyHttpServer struct { // Accept-Encoding header. To disable this behavior, set // Tr.DisableCompression to true. KeepAcceptEncoding bool + // h2Server is the HTTP/2 server instance used for MITM. + // It is shared across all connections. + h2Server *http2.Server } -var hasPort = regexp.MustCompile(`:\d+$`) +func hasPort(s string) bool { + _, _, err := net.SplitHostPort(s) + return err == nil +} func copyHeaders(dst, src http.Header, keepDestHeaders bool) { if !keepDestHeaders { @@ -166,7 +173,8 @@ func NewProxyHttpServer() *ProxyHttpServer { NonproxyHandler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { http.Error(w, "This is a proxy server. Does not respond to non-proxy requests.", http.StatusInternalServerError) }), - Tr: &http.Transport{TLSClientConfig: tlsClientSkipVerify, Proxy: http.ProxyFromEnvironment}, + Tr: &http.Transport{TLSClientConfig: tlsClientSkipVerify, Proxy: http.ProxyFromEnvironment}, + h2Server: &http2.Server{}, } proxy.ConnectDial = dialerFromEnv(&proxy) return &proxy diff --git a/vendor/modules.txt b/vendor/modules.txt index b2858e9..160303e 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -2,8 +2,8 @@ ## explicit; go 1.18 github.com/BurntSushi/toml github.com/BurntSushi/toml/internal -# github.com/elazarl/goproxy v1.8.5 -## explicit; go 1.23.0 +# github.com/elazarl/goproxy v1.9.0 +## explicit; go 1.24.0 github.com/elazarl/goproxy github.com/elazarl/goproxy/internal/http1parser github.com/elazarl/goproxy/internal/signer