diff --git a/go.mod b/go.mod index 53b393b1..2fa69e5c 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/aws/aws-sdk-go-v2/config v1.33.5 github.com/aws/aws-sdk-go-v2/credentials v1.20.5 github.com/aws/aws-sdk-go-v2/service/ecr v1.66.0 - github.com/elazarl/goproxy v1.9.0 + github.com/elazarl/goproxy v1.9.2 github.com/evalphobia/logrus_sentry v0.8.2 github.com/getsentry/raven-go v0.2.0 github.com/jarcoal/httpmock v1.4.2 diff --git a/go.sum b/go.sum index f766f915..d996990b 100644 --- a/go.sum +++ b/go.sum @@ -43,8 +43,8 @@ github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBi github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= -github.com/elazarl/goproxy v1.9.0 h1:2j3c13lD5v0QTjxphJSSIHS7w8/m/pzSHtLMPOpznC0= -github.com/elazarl/goproxy v1.9.0/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/evalphobia/logrus_sentry v0.8.2 h1:dotxHq+YLZsT1Bb45bB5UQbfCh3gM/nFFetyN46VoDQ= github.com/evalphobia/logrus_sentry v0.8.2/go.mod h1:pKcp+vriitUqu9KiWj/VRFbRfFNUwz95/UkgG8a6MNc= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= diff --git a/vendor/github.com/elazarl/goproxy/dispatcher.go b/vendor/github.com/elazarl/goproxy/dispatcher.go index 0bd554b1..3de633e3 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 ce32e4f2..2dcc0ac6 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 4f88051b..280670d1 100644 --- a/vendor/github.com/elazarl/goproxy/https.go +++ b/vendor/github.com/elazarl/goproxy/https.go @@ -185,6 +185,19 @@ type halfClosable interface { var _ halfClosable = (*net.TCPConn)(nil) +// connectResponseLine builds a CONNECT tunnel status line that echoes the +// requesting client's HTTP version. These replies were previously hardcoded as +// HTTP/1.0, so an HTTP/1.1 CONNECT received an HTTP/1.0 status line on an +// otherwise HTTP/1.1 connection (issue #802). Falls back to HTTP/1.1 when the +// request version is unset. +func connectResponseLine(r *http.Request, code int, text string) string { + major, minor := r.ProtoMajor, r.ProtoMinor + if major == 0 { + major, minor = 1, 1 + } + return fmt.Sprintf("HTTP/%d.%d %d %s\r\n\r\n", major, minor, code, text) +} + 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} @@ -325,7 +338,7 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request }() wg.Wait() } else { - _, _ = proxyClient.Write([]byte("HTTP/1.0 200 Connection established\r\n\r\n")) + _, _ = proxyClient.Write([]byte(connectResponseLine(r, http.StatusOK, "Connection established"))) targetTCP, targetOK := targetSiteCon.(halfClosable) proxyClientTCP, clientOK := proxyClient.(halfClosable) @@ -384,7 +397,7 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request f.Flush() } } else { - _, _ = proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) + _, _ = proxyClient.Write([]byte(connectResponseLine(r, http.StatusOK, "OK"))) } ctx.Logf("Received CONNECT request, mitm proxying it") // For HTTP/1.x (after Hijack), the MITM loop runs in a goroutine so the HTTP/1.x @@ -474,7 +487,19 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request if !req.URL.IsAbs() { // Origin-form request target (/path) - req.URL, err = url.Parse(scheme + "://" + r.Host + req.URL.String()) + // We prioritize req.Host (from the internal request), over r.Host (from the CONNECT request). + hostToUse := req.Host + if hostToUse == "" { + hostToUse = r.Host // Fallback, if the internal Host header is missing + } + + urlToParse := scheme + "://" + hostToUse + req.URL.String() + parsedUrl, err := url.Parse(urlToParse) + if err != nil { + ctx.Warnf("Cannot parse URL %s: %v", urlToParse, err) + return + } + req.URL = parsedUrl } else { // Absolute-form request target req.URL.Scheme = scheme @@ -520,7 +545,7 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request resp = proxy.filterResponse(resp, ctx) bodyModified := resp.Body != origBody defer resp.Body.Close() - if resp.Body != http.NoBody && (bodyModified || + if responseBodyAllowed(ctx.Req, resp) && resp.Body != http.NoBody && (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. @@ -566,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/responses.go b/vendor/github.com/elazarl/goproxy/responses.go index 734414ca..f86e5170 100644 --- a/vendor/github.com/elazarl/goproxy/responses.go +++ b/vendor/github.com/elazarl/goproxy/responses.go @@ -42,3 +42,25 @@ const ( func TextResponse(r *http.Request, text string) *http.Response { return NewResponse(r, ContentTypeText, http.StatusAccepted, text) } + +// responseBodyAllowed reports whether a response may carry a message body. +// RFC 9112 6.3: a response to HEAD, or with a 1xx, 204, or 304 status, ends at +// the first empty line "regardless of the header fields present", so framing one +// leaves those bytes to be misread as the next response. +// +// Keyed on status and method, not on the body value: an empty HTTP/1 body is +// http.NoBody, but the HTTP/2 transport uses its own unexported value, so a +// bodiless HTTP/2 response is not http.NoBody. +func responseBodyAllowed(req *http.Request, resp *http.Response) bool { + if req != nil && req.Method == http.MethodHead { + return false + } + switch { + case resp.StatusCode >= 100 && resp.StatusCode <= 199: + return false + case resp.StatusCode == http.StatusNoContent, resp.StatusCode == http.StatusNotModified: + return false + default: + return true + } +} diff --git a/vendor/github.com/elazarl/goproxy/websocket.go b/vendor/github.com/elazarl/goproxy/websocket.go index c10f57ca..ce0d466f 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 48c218e4..69e35408 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -145,7 +145,7 @@ github.com/docker/distribution/manifest/schema2 # github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 ## explicit github.com/docker/libtrust -# github.com/elazarl/goproxy v1.9.0 +# github.com/elazarl/goproxy v1.9.2 ## explicit; go 1.24.0 github.com/elazarl/goproxy github.com/elazarl/goproxy/internal/http1parser