diff --git a/go.mod b/go.mod index a40c87f..8d16da6 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.0 require ( github.com/BurntSushi/toml v1.6.0 - github.com/elazarl/goproxy v1.7.2 + github.com/elazarl/goproxy v1.8.1 ) require ( diff --git a/go.sum b/go.sum index 0066927..bcb429c 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,17 @@ -github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= -github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= +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.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= -github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/elazarl/goproxy v1.8.1 h1:/qGpPJGgIPOTZ7IoIQvjavocp//qYSe9LQnIGCgRY5k= +github.com/elazarl/goproxy v1.8.1/go.mod h1:b5xm6W48AUHNpRTCvlnd0YVh+JafCCtsLsJZvvNTz+E= 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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +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/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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 69e36d9..aaa111a 100644 --- a/vendor/github.com/elazarl/goproxy/.golangci.yml +++ b/vendor/github.com/elazarl/goproxy/.golangci.yml @@ -1,18 +1,10 @@ +version: "2" run: - timeout: 5m modules-download-mode: readonly # List from https://golangci-lint.run/usage/linters/ linters: enable: - # Default linters - - errcheck - - gosimple - - govet - - ineffassign - - staticcheck - - unused - # Other linters - asasalint - asciicheck - bidichk @@ -27,13 +19,10 @@ linters: - fatcontext - forbidigo - forcetypeassert - - gci - gocheckcompilerdirectives - gochecksumtype - gocritic - godot - - gofmt - - gofumpt - goheader - gomodguard - goprintffuncname @@ -57,9 +46,8 @@ linters: - predeclared - reassign - revive - - stylecheck + - staticcheck - tagalign - - tenv - testableexamples - testifylint - testpackage @@ -69,7 +57,6 @@ linters: - usestdlibvars - wastedassign - whitespace - disable: - bodyclose - canonicalheader @@ -89,7 +76,6 @@ linters: - goconst - gocyclo - godox - - goimports - gomoddirectives - inamedparam - intrange @@ -115,46 +101,65 @@ linters: - wrapcheck - wsl - zerologlint - -linters-settings: - gci: - sections: - - standard - - default - skip-generated: false - custom-order: true - gosec: - excludes: - - G402 # InsecureSkipVerify - - G102 # Binds to all network interfaces - - G403 # RSA keys should be at least 2048 bits - - G115 # Integer overflow conversion (uint64 -> int64) - - G404 # Use of weak random number generator (math/rand) - - G204 # Subprocess launched with a potential tainted input or cmd arguments - -issues: - exclude-rules: - - linters: - - gocritic - text: "ifElseChain" - - linters: - - lll - source: "^// " - - linters: - - revive - text: "add-constant: " - - linters: - - revive - text: "unused-parameter: " - - linters: - - revive - text: "empty-block: " - - linters: - - revive - text: "var-naming: " # TODO: Re-enable in V2 - - linters: - - stylecheck - text: " should be " # TODO: Re-enable in V2 - - linters: - - stylecheck - text: "ST1003: should not use ALL_CAPS in Go names; use CamelCase instead" # TODO: Re-enable in V2 + settings: + gosec: + excludes: + - G402 # InsecureSkipVerify + - G102 # Binds to all network interfaces + - G403 # RSA keys should be at least 2048 bits + - G115 # Integer overflow conversion (uint64 -> int64) + - G404 # Use of weak random number generator (math/rand) + - G204 # Subprocess launched with a potential tainted input or cmd arguments + - G602 # Slice index out of range + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - gocritic + text: ifElseChain + - linters: + - lll + source: '^// ' + - linters: + - revive + text: 'add-constant: ' + - linters: + - revive + text: 'unused-parameter: ' + - linters: + - revive + text: 'empty-block: ' + - linters: + - revive + text: 'var-naming: ' # TODO: Re-enable in V2 + - linters: + - staticcheck + text: ' should be ' # TODO: Re-enable in V2 + - linters: + - staticcheck + text: 'ST1003: should not use ALL_CAPS in Go names; use CamelCase instead' + paths: + - examples$ + - transport +formatters: + enable: + - gci + - gofmt + - gofumpt + settings: + gci: + sections: + - standard + - default + custom-order: true + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/vendor/github.com/elazarl/goproxy/README.md b/vendor/github.com/elazarl/goproxy/README.md index 435eded..b6c7208 100644 --- a/vendor/github.com/elazarl/goproxy/README.md +++ b/vendor/github.com/elazarl/goproxy/README.md @@ -26,6 +26,8 @@ You also have to [trust](https://github.com/elazarl/goproxy/blob/master/examples the proxy CA certificate, to avoid any certificate issue in the clients. > [✈️ Telegram Group](https://telegram.me/goproxygroup) +> +> [🎁 Become a Sponsor](https://opencollective.com/goproxy) ## Features - Perform certain actions only on `specific hosts`, with a single equality comparison or with regex evaluation @@ -43,19 +45,14 @@ the proxy CA certificate, to avoid any certificate issue in the clients. 4. "Hijacked" proxy connection, where the configured handler can access the raw net.Conn data ## Sponsors -Does your company use GoProxy? Ask your manager or marketing team -if your company would be interested in supporting our project. -Supporting this project will allow the maintainers to dedicate more time -for maintenance and new features for everyone. -This will also benefit you, because maintainers will fix problems that will occur -and keep GoProxy up to date for your projects. -Moreover, your company logo will be shown on GitHub, in this README section. -> [Apply Here](https://opencollective.com/goproxy) - -[![GoProxy Sponsor](https://opencollective.com/goproxy/tiers/sponsor/0/avatar)](https://opencollective.com/goproxy/tiers/sponsor/0/website) -[![GoProxy Sponsor](https://opencollective.com/goproxy/tiers/sponsor/1/avatar)](https://opencollective.com/goproxy/tiers/sponsor/1/website) -[![GoProxy Sponsor](https://opencollective.com/goproxy/tiers/sponsor/2/avatar)](https://opencollective.com/goproxy/tiers/sponsor/2/website) -[![GoProxy Sponsor](https://opencollective.com/goproxy/tiers/sponsor/3/avatar)](https://opencollective.com/goproxy/tiers/sponsor/3/website) +Does your company use GoProxy? Help us keep the project maintained and healthy! +Supporting GoProxy allows us to dedicate more time to bug fixes and new features. +In exchange, if you choose a Gold Supporter or Enterprise plan, we'll proudly display your company logo here. + +> [Become a Sponsor](https://opencollective.com/goproxy) + +[![Gold Supporters](https://opencollective.com/goproxy/tiers/gold-sponsor.svg?width=890)](https://opencollective.com/goproxy) +[![Enterprise Supporters](https://opencollective.com/goproxy/tiers/enterprise.svg?width=890)](https://opencollective.com/goproxy) ## Maintainers - [Elazar Leibovich](https://github.com/elazarl): Creator of the project, Software Engineer @@ -169,10 +166,10 @@ local timezone: proxy.OnRequest(goproxy.DstHostIs("www.reddit.com")).DoFunc( func(req *http.Request,ctx *goproxy.ProxyCtx)(*http.Request,*http.Response) { if h,_,_ := time.Now().Clock(); h >= 8 && h <= 17 { - resp := goproxy.NewResponse(r, goproxy.ContentTypeText, http.StatusForbidden, "Don't waste your time!") + resp := goproxy.NewResponse(req, goproxy.ContentTypeText, http.StatusForbidden, "Don't waste your time!") return req, resp } - return req,nil + return req, nil }) ``` diff --git a/vendor/github.com/elazarl/goproxy/chunked.go b/vendor/github.com/elazarl/goproxy/chunked.go deleted file mode 100644 index 8b34b68..0000000 --- a/vendor/github.com/elazarl/goproxy/chunked.go +++ /dev/null @@ -1,57 +0,0 @@ -// Taken from $GOROOT/src/pkg/net/http/chunked -// needed to write https responses to client. -package goproxy - -import ( - "io" - "strconv" -) - -// newChunkedWriter returns a new chunkedWriter that translates writes into HTTP -// "chunked" format before writing them to w. Closing the returned chunkedWriter -// sends the final 0-length chunk that marks the end of the stream. -// -// newChunkedWriter is not needed by normal applications. The http -// package adds chunking automatically if handlers don't set a -// Content-Length header. Using newChunkedWriter inside a handler -// would result in double chunking or chunking with a Content-Length -// length, both of which are wrong. -func newChunkedWriter(w io.Writer) io.WriteCloser { - return &chunkedWriter{w} -} - -// Writing to chunkedWriter translates to writing in HTTP chunked Transfer -// Encoding wire format to the underlying Wire chunkedWriter. -type chunkedWriter struct { - Wire io.Writer -} - -// Write the contents of data as one chunk to Wire. -// NOTE: Note that the corresponding chunk-writing procedure in Conn.Write has -// a bug since it does not check for success of io.WriteString. -func (cw *chunkedWriter) Write(data []byte) (n int, err error) { - // Don't send 0-length data. It looks like EOF for chunked encoding. - if len(data) == 0 { - return 0, nil - } - - head := strconv.FormatInt(int64(len(data)), 16) + "\r\n" - - if _, err = io.WriteString(cw.Wire, head); err != nil { - return 0, err - } - if n, err = cw.Wire.Write(data); err != nil { - return n, err - } - if n != len(data) { - err = io.ErrShortWrite - return n, err - } - _, err = io.WriteString(cw.Wire, "\r\n") - return n, err -} - -func (cw *chunkedWriter) Close() error { - _, err := io.WriteString(cw.Wire, "0\r\n") - return err -} diff --git a/vendor/github.com/elazarl/goproxy/h2.go b/vendor/github.com/elazarl/goproxy/h2.go index 6d50948..2f6342c 100644 --- a/vendor/github.com/elazarl/goproxy/h2.go +++ b/vendor/github.com/elazarl/goproxy/h2.go @@ -2,6 +2,7 @@ package goproxy import ( "bufio" + "context" "crypto/tls" "errors" "io" @@ -45,7 +46,7 @@ func (r *H2Transport) RoundTrip(_ *http.Request) (*http.Response, error) { if !ok { return nil, errors.New("invalid TLS connection") } - if err = rawTLSConn.Handshake(); err != nil { + if err = rawTLSConn.HandshakeContext(context.Background()); err != nil { return nil, err } if r.TLSConfig == nil || !r.TLSConfig.InsecureSkipVerify { diff --git a/vendor/github.com/elazarl/goproxy/https.go b/vendor/github.com/elazarl/goproxy/https.go index 016e513..7fe5a81 100644 --- a/vendor/github.com/elazarl/goproxy/https.go +++ b/vendor/github.com/elazarl/goproxy/https.go @@ -11,7 +11,6 @@ import ( "net/http" "net/url" "os" - "strconv" "strings" "sync" "sync/atomic" @@ -27,19 +26,32 @@ const ( ConnectReject ConnectMitm ConnectHijack + // Deprecated: use ConnectMitm. ConnectHTTPMitm ConnectProxyAuthHijack ) var ( - OkConnect = &ConnectAction{Action: ConnectAccept, TLSConfig: TLSConfigFromCA(&GoproxyCa)} - MitmConnect = &ConnectAction{Action: ConnectMitm, TLSConfig: TLSConfigFromCA(&GoproxyCa)} + OkConnect = &ConnectAction{Action: ConnectAccept, TLSConfig: TLSConfigFromCA(&GoproxyCa)} + MitmConnect = &ConnectAction{Action: ConnectMitm, TLSConfig: TLSConfigFromCA(&GoproxyCa)} + // Deprecated: use MitmConnect. HTTPMitmConnect = &ConnectAction{Action: ConnectHTTPMitm, TLSConfig: TLSConfigFromCA(&GoproxyCa)} RejectConnect = &ConnectAction{Action: ConnectReject, TLSConfig: TLSConfigFromCA(&GoproxyCa)} ) var _errorRespMaxLength int64 = 500 +const _tlsRecordTypeHandshake = byte(22) + +type readBufferedConn struct { + net.Conn + r io.Reader +} + +func (c *readBufferedConn) Read(p []byte) (int, error) { + return c.r.Read(p) +} + // ConnectAction enables the caller to override the standard connect flow. // When Action is ConnectHijack, it is up to the implementer to send the // HTTP 200, or any other valid http response back to the client from within the @@ -83,7 +95,8 @@ func (proxy *ProxyHttpServer) dial(ctx *ProxyCtx, network, addr string) (c net.C // if the user didn't specify any dialer, we just use the default one, // provided by net package - return net.Dial(network, addr) + var d net.Dialer + return d.DialContext(ctx.Req.Context(), network, addr) } func (proxy *ProxyHttpServer) connectDial(ctx *ProxyCtx, network, addr string) (c net.Conn, err error) { @@ -193,105 +206,50 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request case ConnectHijack: todo.Hijack(r, proxyClient, ctx) - case ConnectHTTPMitm: + case ConnectHTTPMitm, ConnectMitm: _, _ = proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) - ctx.Logf("Assuming CONNECT is plain HTTP tunneling, mitm proxying it") - - var targetSiteCon net.Conn - var remote *bufio.Reader - - client := http1parser.NewRequestReader(proxy.PreventCanonicalization, proxyClient) - for !client.IsEOF() { - req, err := client.ReadRequest() - if err != nil && !errors.Is(err, io.EOF) { - ctx.Warnf("cannot read request of MITM HTTP client: %+#v", err) - } - if err != nil { - return - } - - if requestOk := func(req *http.Request) bool { - // Since we handled the request parsing by our own, we manually - // need to set a cancellable context when we finished the request - // processing (same behaviour of the stdlib) - requestContext, finishRequest := context.WithCancel(req.Context()) - req = req.WithContext(requestContext) - defer finishRequest() - - // since we're converting the request, need to carry over the - // original connecting IP as well - req.RemoteAddr = r.RemoteAddr - ctx.Logf("req %v", r.Host) - ctx.Req = req - - req, resp := proxy.filterRequest(req, ctx) - if resp == nil { - // Establish a connection with the remote server only if the proxy - // doesn't produce a response - if targetSiteCon == nil { - targetSiteCon, err = proxy.connectDial(ctx, "tcp", host) - if err != nil { - ctx.Warnf("Error dialing to %s: %s", host, err.Error()) - return false - } - remote = bufio.NewReader(targetSiteCon) - } + 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() { + // Check if this is an HTTP or an HTTPS MITM request + readBuffer := bufio.NewReader(proxyClient) + peek, _ := readBuffer.Peek(1) + isTLS := len(peek) > 0 && peek[0] == _tlsRecordTypeHandshake + + var client net.Conn = &readBufferedConn{Conn: proxyClient, r: readBuffer} + defer func() { + _ = client.Close() + }() - if err := req.Write(targetSiteCon); err != nil { - httpError(proxyClient, ctx, err) - return false - } - resp, err = func() (*http.Response, error) { - defer req.Body.Close() - return http.ReadResponse(remote, req) - }() + var tlsConfig *tls.Config + scheme := "http" + if isTLS { + scheme = "https" + tlsConfig = defaultTLSConfig + if todo.TLSConfig != nil { + var err error + tlsConfig, err = todo.TLSConfig(host, ctx) if err != nil { httpError(proxyClient, ctx, err) - return false + return } } - resp = proxy.filterResponse(resp, ctx) - defer resp.Body.Close() - err = resp.Write(proxyClient) - if err != nil { - httpError(proxyClient, ctx, err) - return false + // Create a TLS connection over the TCP connection + rawClientTls := tls.Server(client, tlsConfig) + client = rawClientTls + if err := rawClientTls.HandshakeContext(context.Background()); err != nil { + ctx.Warnf("Cannot handshake client %v %v", r.Host, err) + return } - - return true - }(req); !requestOk { - break - } - } - case ConnectMitm: - _, _ = proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) - ctx.Logf("Assuming CONNECT is TLS, 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 - tlsConfig := defaultTLSConfig - if todo.TLSConfig != nil { - var err error - tlsConfig, err = todo.TLSConfig(host, ctx) - if err != nil { - httpError(proxyClient, ctx, err) - return - } - } - go func() { - // TODO: cache connections to the remote website - rawClientTls := tls.Server(proxyClient, tlsConfig) - defer rawClientTls.Close() - if err := rawClientTls.Handshake(); err != nil { - ctx.Warnf("Cannot handshake client %v %v", r.Host, err) - return } - clientTlsReader := http1parser.NewRequestReader(proxy.PreventCanonicalization, rawClientTls) - for !clientTlsReader.IsEOF() { - req, err := clientTlsReader.ReadRequest() + clientReader := http1parser.NewRequestReader(proxy.PreventCanonicalization, client) + for !clientReader.IsEOF() { + req, err := clientReader.ReadRequest() ctx := &ProxyCtx{ Req: req, Session: atomic.AddInt64(&proxy.sess, 1), @@ -300,7 +258,7 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request RoundTripper: ctx.RoundTripper, } if err != nil && !errors.Is(err, io.EOF) { - ctx.Warnf("Cannot read TLS request from mitm'd client %v %v", r.Host, err) + ctx.Warnf("Cannot read request from mitm'd client %v %v", r.Host, err) } if err != nil { return @@ -311,8 +269,8 @@ 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(), "https://") { - req.URL, err = url.Parse("https://" + r.Host + req.URL.String()) + if !strings.HasPrefix(req.URL.String(), scheme+"://") { + req.URL, err = url.Parse(scheme + "://" + r.Host + req.URL.String()) } if continueLoop := func(req *http.Request) bool { @@ -323,6 +281,10 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request req = req.WithContext(requestContext) defer finishRequest() + // explicitly discard request body to avoid data races in certain RoundTripper implementations + // see https://github.com/golang/go/issues/61596#issuecomment-1652345131 + defer req.Body.Close() + // Bug fix which goproxy fails to provide request // information URL in the context when does HTTPS MITM ctx.Req = req @@ -336,7 +298,7 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request // 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 := clientTlsReader.Reader() + reader := clientReader.Reader() _, err := reader.Discard(6) if err != nil { ctx.Warnf("Failed to process HTTP2 client preface: %v", err) @@ -346,7 +308,7 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request ctx.Warnf("HTTP2 connection failed: disallowed") return false } - tr := H2Transport{reader, rawClientTls, tlsConfig.Clone(), host} + tr := H2Transport{reader, client, tlsConfig, host} if _, err := tr.RoundTrip(req); err != nil { ctx.Warnf("HTTP2 connection failed: %v", err) } else { @@ -356,67 +318,38 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request } if err != nil { if req.URL != nil { - ctx.Warnf("Illegal URL %s", "https://"+r.Host+req.URL.Path) + ctx.Warnf("Illegal URL %s", scheme+"://"+r.Host+req.URL.Path) } else { - ctx.Warnf("Illegal URL %s", "https://"+r.Host) + ctx.Warnf("Illegal URL %s", scheme+"://"+r.Host) } return false } if !proxy.KeepHeader { RemoveProxyHeaders(ctx, req) } - resp, err = func() (*http.Response, error) { - // explicitly discard request body to avoid data races in certain RoundTripper implementations - // see https://github.com/golang/go/issues/61596#issuecomment-1652345131 - defer req.Body.Close() - return ctx.RoundTrip(req) - }() + resp, err = ctx.RoundTrip(req) if err != nil { - ctx.Warnf("Cannot read TLS response from mitm'd server %v", err) + ctx.Warnf("Cannot read response from mitm'd server %v", err) return false } ctx.Logf("resp %v", resp.Status) } + origBody := resp.Body resp = proxy.filterResponse(resp, ctx) + bodyModified := resp.Body != origBody defer resp.Body.Close() - - text := resp.Status - statusCode := strconv.Itoa(resp.StatusCode) + " " - text = strings.TrimPrefix(text, statusCode) - // always use 1.1 to support chunked encoding - if _, err := io.WriteString(rawClientTls, "HTTP/1.1"+" "+statusCode+text+"\r\n"); err != nil { - ctx.Warnf("Cannot write TLS response HTTP status from mitm'd client: %v", err) - return false - } - - isWebsocket := isWebSocketHandshake(resp.Header) - if isWebsocket || resp.Request.Method == http.MethodHead { - // don't change Content-Length for HEAD request - } else if (resp.StatusCode >= 100 && resp.StatusCode < 200) || - resp.StatusCode == http.StatusNoContent { - // RFC7230: A server MUST NOT send a Content-Length header field in any response - // with a status code of 1xx (Informational) or 204 (No Content) - resp.Header.Del("Content-Length") - } else { - // Since we don't know the length of resp, return chunked encoded response - // TODO: use a more reasonable scheme + if bodyModified || (resp.ContentLength <= 0 && resp.Header.Get("Content-Length") == "") { + // Return chunked encoded response when we don't know the length of the resp, if the body + // has been modified by the response handler or if there is no content length in the response. + // We include 0 in resp.ContentLength <= 0 because 0 is the field zero value and some user + // might incorrectly leave it instead of setting it to -1 when the length is unknown (but we + // also check that the Content-Length header is empty, so there is no issue with empty bodies). + resp.ContentLength = -1 resp.Header.Del("Content-Length") - resp.Header.Set("Transfer-Encoding", "chunked") - } - // Force connection close otherwise chrome will keep CONNECT tunnel open forever - if !isWebsocket { - resp.Header.Set("Connection", "close") - } - if err := resp.Header.Write(rawClientTls); err != nil { - ctx.Warnf("Cannot write TLS response header from mitm'd client: %v", err) - return false - } - if _, err = io.WriteString(rawClientTls, "\r\n"); err != nil { - ctx.Warnf("Cannot write TLS response header end from mitm'd client: %v", err) - return false + resp.TransferEncoding = []string{"chunked"} } - if isWebsocket { + if isWebSocketHandshake(resp.Header) { ctx.Logf("Response looks like websocket upgrade.") // According to resp.Body documentation: @@ -428,32 +361,21 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request ctx.Warnf("Unable to use Websocket connection") return false } - proxy.proxyWebsocket(ctx, wsConn, rawClientTls) - // We can't reuse connection after WebSocket handshake, - // by returning false here, the underlying connection will be closed + // Set Body to nil so resp.Write only writes the headers + // and returns immediately without blocking on the body + // (or else we wouldn't be able to proxy WebSocket data). + resp.Body = nil + if err := resp.Write(client); err != nil { + ctx.Warnf("Cannot write response header from mitm'd client: %v", err) + return false + } + proxy.proxyWebsocket(ctx, wsConn, client) return false } - if resp.Request.Method == http.MethodHead || - (resp.StatusCode >= 100 && resp.StatusCode < 200) || - resp.StatusCode == http.StatusNoContent || - resp.StatusCode == http.StatusNotModified { - // Don't write out a response body, when it's not allowed - // in RFC7230 - } else { - chunked := newChunkedWriter(rawClientTls) - if _, err := io.Copy(chunked, resp.Body); err != nil { - ctx.Warnf("Cannot write TLS response body from mitm'd client: %v", err) - return false - } - if err := chunked.Close(); err != nil { - ctx.Warnf("Cannot write TLS chunked EOF from mitm'd client: %v", err) - return false - } - if _, err = io.WriteString(rawClientTls, "\r\n"); err != nil { - ctx.Warnf("Cannot write TLS response chunked trailer from mitm'd client: %v", err) - return false - } + if err := resp.Write(client); err != nil { + ctx.Warnf("Cannot write response from mitm'd client: %v", err) + return false } return true @@ -540,7 +462,7 @@ func (proxy *ProxyHttpServer) NewConnectDialToProxyWithHandler( if err != nil { return nil } - if u.Scheme == "" || u.Scheme == "http" { + if u.Scheme == "" || u.Scheme == "http" || u.Scheme == "ws" { if !strings.ContainsRune(u.Host, ':') { u.Host += ":80" } diff --git a/vendor/github.com/elazarl/goproxy/internal/signer/signer.go b/vendor/github.com/elazarl/goproxy/internal/signer/signer.go index 0ff00a5..d62ec1a 100644 --- a/vendor/github.com/elazarl/goproxy/internal/signer/signer.go +++ b/vendor/github.com/elazarl/goproxy/internal/signer/signer.go @@ -108,8 +108,12 @@ func SignHost(ca tls.Certificate, hosts []string) (cert *tls.Certificate, err er return nil, err } - certBytes := [][]byte{derBytes} - certBytes = append(certBytes, ca.Certificate...) + certBytes := make([][]byte, 1+len(ca.Certificate)) + certBytes[0] = derBytes + for i, singleCertBytes := range ca.Certificate { + certBytes[i+1] = singleCertBytes + } + return &tls.Certificate{ Certificate: certBytes, PrivateKey: certpriv, diff --git a/vendor/modules.txt b/vendor/modules.txt index af8e71e..1c84f26 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.7.2 -## explicit; go 1.20 +# github.com/elazarl/goproxy v1.8.1 +## explicit; go 1.23.0 github.com/elazarl/goproxy github.com/elazarl/goproxy/internal/http1parser github.com/elazarl/goproxy/internal/signer