diff --git a/go.mod b/go.mod index b952968..06af306 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.9.1 + github.com/elazarl/goproxy v1.9.2 golang.org/x/net v0.58.0 ) diff --git a/go.sum b/go.sum index 5581fb4..db24121 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ 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.9.1 h1:1gQuANpCxN2UQKWwkNPpWqAk1LnAMYuBxqfIdU8mMoE= -github.com/elazarl/goproxy v1.9.1/go.mod h1:THdE5ix2clxX9lZzcICPpZ67d6CdrPZxdOYsNgU5e30= +github.com/elazarl/goproxy v1.9.2 h1:+vXRRSWrznMtBrAb559qfqC+Cny1Q3rR0l51Yu/3WUw= +github.com/elazarl/goproxy v1.9.2/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= diff --git a/vendor/github.com/elazarl/goproxy/dispatcher.go b/vendor/github.com/elazarl/goproxy/dispatcher.go index 0bd554b..3de633e 100644 --- a/vendor/github.com/elazarl/goproxy/dispatcher.go +++ b/vendor/github.com/elazarl/goproxy/dispatcher.go @@ -43,7 +43,7 @@ func (c RespConditionFunc) HandleResp(resp *http.Response, ctx *ProxyCtx) bool { return c(resp, ctx) } -// UrlHasPrefix returns a ReqCondition checking wether the destination URL the proxy client has requested +// UrlHasPrefix returns a ReqCondition checking whether the destination URL the proxy client has requested // has the given prefix, with or without the host. // For example UrlHasPrefix("host/x") will match requests of the form 'GET host/x', and will match // requests to url 'http://host/x' @@ -132,7 +132,7 @@ func UrlMatches(re *regexp.Regexp) ReqConditionFunc { } } -// DstHostIs returns a ReqCondition testing wether the host in the request url is the given string. +// DstHostIs returns a ReqCondition testing whether the host in the request url is the given string. func DstHostIs(host string) ReqConditionFunc { // Make sure to perform a case-insensitive host check host = strings.ToLower(host) diff --git a/vendor/github.com/elazarl/goproxy/http.go b/vendor/github.com/elazarl/goproxy/http.go index ce32e4f..2dcc0ac 100644 --- a/vendor/github.com/elazarl/goproxy/http.go +++ b/vendor/github.com/elazarl/goproxy/http.go @@ -83,13 +83,13 @@ func (proxy *ProxyHttpServer) handleHttp(w http.ResponseWriter, r *http.Request) // We have already written the "101 Switching Protocols" response, // now we hijack the connection to send WebSocket data - if clientConn, err := proxy.hijackConnection(ctx, w); err == nil { + if clientConn, clientReader, err := proxy.hijackConnection(ctx, w); err == nil { wsConn, ok := resp.Body.(io.ReadWriter) if !ok { ctx.Warnf("Unable to use Websocket connection") return } - proxy.proxyWebsocket(ctx, wsConn, clientConn) + proxy.proxyWebsocket(ctx, wsConn, bufferedClientReader(clientReader, clientConn), clientConn) } return } diff --git a/vendor/github.com/elazarl/goproxy/https.go b/vendor/github.com/elazarl/goproxy/https.go index 5d8e5d8..280670d 100644 --- a/vendor/github.com/elazarl/goproxy/https.go +++ b/vendor/github.com/elazarl/goproxy/https.go @@ -493,11 +493,13 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request hostToUse = r.Host // Fallback, if the internal Host header is missing } - req.URL, err = url.Parse(scheme + "://" + hostToUse + req.URL.String()) + urlToParse := scheme + "://" + hostToUse + req.URL.String() + parsedUrl, err := url.Parse(urlToParse) if err != nil { - ctx.Warnf("Cannot parse URL %s: %v", scheme+"://"+hostToUse+req.URL.String(), err) + ctx.Warnf("Cannot parse URL %s: %v", urlToParse, err) return } + req.URL = parsedUrl } else { // Absolute-form request target req.URL.Scheme = scheme @@ -589,7 +591,11 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request ctx.Warnf("Cannot flush response header from mitm'd client: %v", err) return false } - proxy.proxyWebsocket(ctx, wsConn, client) + // The client may have sent its first WebSocket frame in the + // same write as the upgrade request, so those bytes are still + // in the request parser's buffer. Replay them before the raw + // connection instead of leaving them stranded. + proxy.proxyWebsocket(ctx, wsConn, bufferedClientReader(clientReader.Reader(), client), client) return false } diff --git a/vendor/github.com/elazarl/goproxy/websocket.go b/vendor/github.com/elazarl/goproxy/websocket.go index c10f57c..ce0d466 100644 --- a/vendor/github.com/elazarl/goproxy/websocket.go +++ b/vendor/github.com/elazarl/goproxy/websocket.go @@ -1,6 +1,8 @@ package goproxy import ( + "bufio" + "bytes" "io" "net" "net/http" @@ -23,31 +25,67 @@ func isWebSocketHandshake(header http.Header) bool { headerContains(header, "Upgrade", "websocket") } -func (proxy *ProxyHttpServer) hijackConnection(ctx *ProxyCtx, w http.ResponseWriter) (net.Conn, error) { +func (proxy *ProxyHttpServer) hijackConnection(ctx *ProxyCtx, w http.ResponseWriter) (net.Conn, *bufio.Reader, error) { // Connect to Client hj, ok := w.(http.Hijacker) if !ok { panic("httpserver does not support hijacking") } - clientConn, _, err := hj.Hijack() + clientConn, rw, err := hj.Hijack() if err != nil { ctx.Warnf("Hijack error: %v", err) - return nil, err + return nil, nil, err } - return clientConn, nil + // rw.Reader still holds any bytes the HTTP server buffered while parsing the + // request (e.g. the first WebSocket frame sent in the same write as the + // upgrade). The caller must relay them via bufferedClientReader. + return clientConn, rw.Reader, nil } -func (proxy *ProxyHttpServer) proxyWebsocket(ctx *ProxyCtx, remoteConn io.ReadWriter, proxyClient io.ReadWriter) { +// bufferedClientReader returns a reader that first replays the bytes already +// buffered in br by the HTTP request parser, then continues from conn. +// +// A WebSocket client may send its first frame in the same write as the upgrade +// request. Those bytes are read off the socket into the parser's buffer, so a +// relay that reads only from conn would strand them. +// +// We deliberately replay a copy of the buffered bytes instead of reading through +// br itself: with PreventCanonicalization enabled, br wraps an io.TeeReader that +// retains every byte read from it, so relaying the whole session through br +// would grow that buffer unbounded for the connection's lifetime. +func bufferedClientReader(br *bufio.Reader, conn io.Reader) io.Reader { + if br == nil { + return conn + } + // Peek never advances the reader, and requesting Buffered() bytes always + // succeeds, so this is a snapshot of the currently buffered bytes. + buffered, _ := br.Peek(br.Buffered()) + if len(buffered) == 0 { + return conn + } + return io.MultiReader(bytes.NewReader(buffered), conn) +} + +// proxyWebsocket relays frames between the client and the upstream server until +// one side closes. The client read and write sides are passed separately so the +// caller can hand over a reader that still holds bytes buffered while parsing +// the upgrade request (e.g. a client frame that arrived in the same write). +func (proxy *ProxyHttpServer) proxyWebsocket( + ctx *ProxyCtx, + remoteConn io.ReadWriter, + clientReader io.Reader, + clientWriter io.Writer, +) { // 2 is the number of goroutines, this code is implemented according to // https://stackoverflow.com/questions/52031332/wait-for-one-goroutine-to-finish waitChan := make(chan struct{}, 2) go func() { - _ = copyOrWarn(ctx, remoteConn, proxyClient) + _ = copyOrWarn(ctx, remoteConn, clientReader) waitChan <- struct{}{} }() go func() { - _ = copyOrWarn(ctx, proxyClient, remoteConn) + _ = copyOrWarn(ctx, clientWriter, remoteConn) waitChan <- struct{}{} }() diff --git a/vendor/modules.txt b/vendor/modules.txt index ed4bf42..2f98cfd 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -2,7 +2,7 @@ ## explicit; go 1.18 github.com/BurntSushi/toml github.com/BurntSushi/toml/internal -# github.com/elazarl/goproxy v1.9.1 +# github.com/elazarl/goproxy v1.9.2 ## explicit; go 1.24.0 github.com/elazarl/goproxy github.com/elazarl/goproxy/internal/http1parser