diff --git a/go.mod b/go.mod index 54f8159568..8295a89eec 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/cucumber/godog v0.15.0 github.com/cucumber/messages-go/v10 v10.0.3 github.com/docker/go-units v0.5.0 - github.com/elazarl/goproxy v1.6.0 + github.com/elazarl/goproxy v1.7.0 github.com/gorilla/handlers v1.5.2 github.com/h2non/filetype v1.1.3 github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb diff --git a/go.sum b/go.sum index b906c69d72..c746e0e5cd 100644 --- a/go.sum +++ b/go.sum @@ -103,8 +103,8 @@ github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQ github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/elazarl/goproxy v1.6.0 h1:KozdZ0HDR3tGqF5RBqwJToZMsImW9r0XnjkR+xOzQ1E= -github.com/elazarl/goproxy v1.6.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/elazarl/goproxy v1.7.0 h1:EXv2nV4EjM60ZtsEVLYJG4oBXhDGutMKperpHsZ/v+0= +github.com/elazarl/goproxy v1.7.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= diff --git a/vendor/github.com/elazarl/goproxy/README.md b/vendor/github.com/elazarl/goproxy/README.md index 9a530f5169..435eded114 100644 --- a/vendor/github.com/elazarl/goproxy/README.md +++ b/vendor/github.com/elazarl/goproxy/README.md @@ -1,7 +1,11 @@ # GoProxy -[![GoDoc](https://pkg.go.dev/badge/github.com/elazarl/goproxy)](https://pkg.go.dev/github.com/elazarl/goproxy) ![Status](https://github.com/elazarl/goproxy/workflows/Go/badge.svg) +[![GoDoc](https://pkg.go.dev/badge/github.com/elazarl/goproxy)](https://pkg.go.dev/github.com/elazarl/goproxy) +[![Go Report](https://goreportcard.com/badge/github.com/elazarl/goproxy)](https://goreportcard.com/report/github.com/elazarl/goproxy) +[![BSD-3 License](https://img.shields.io/badge/License-BSD%203--Clause-orange.svg)](https://opensource.org/licenses/BSD-3-Clause) +[![Pull Requests](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://makeapullrequest.com) +[![Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go?tab=readme-ov-file#networking) GoProxy is a library to create a `customized` HTTP/HTTPS `proxy server` using Go (aka Golang), with several configurable settings available. @@ -21,6 +25,8 @@ proxy is `localhost:8080`, which is the default one in our example. You also have to [trust](https://github.com/elazarl/goproxy/blob/master/examples/customca/README.md) the proxy CA certificate, to avoid any certificate issue in the clients. +> [✈️ Telegram Group](https://telegram.me/goproxygroup) + ## Features - Perform certain actions only on `specific hosts`, with a single equality comparison or with regex evaluation - Manipulate `requests` and `responses` before sending them to the browser @@ -36,10 +42,30 @@ the proxy CA certificate, to avoid any certificate issue in the clients. 3. HTTPS MITM ("Man in the Middle") proxy server, in which the server generate TLS certificates to parse request/response data and perform actions on them 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) + ## Maintainers - [Elazar Leibovich](https://github.com/elazarl): Creator of the project, Software Engineer - [Erik Pellizzon](https://github.com/ErikPelli): Maintainer, Freelancer (open to collaborations!) +If you need to integrate GoProxy into your project, or you need some custom +features to maintain in your fork, you can contact [Erik](mailto:erikpelli@tutamail.com) +(the current maintainer) by email, and you can discuss together how he +can help you as a paid independent consultant. + ## Contributions If you have any trouble, suggestion, or if you find a bug, feel free to reach out by opening a GitHub `issue`. diff --git a/vendor/github.com/elazarl/goproxy/http.go b/vendor/github.com/elazarl/goproxy/http.go index 38a3be5b2d..985dc44552 100644 --- a/vendor/github.com/elazarl/goproxy/http.go +++ b/vendor/github.com/elazarl/goproxy/http.go @@ -10,7 +10,6 @@ import ( func (proxy *ProxyHttpServer) handleHttp(w http.ResponseWriter, r *http.Request) { ctx := &ProxyCtx{Req: r, Session: atomic.AddInt64(&proxy.sess, 1), Proxy: proxy} - var err error ctx.Logf("Got request %v %v %v %v", r.URL.Path, r.Host, r.Method, r.URL.String()) if !r.URL.IsAbs() { proxy.NonproxyHandler.ServeHTTP(w, r) @@ -19,23 +18,14 @@ func (proxy *ProxyHttpServer) handleHttp(w http.ResponseWriter, r *http.Request) r, resp := proxy.filterRequest(r, ctx) if resp == nil { - if isWebSocketRequest(r) { - ctx.Logf("Request looks like websocket upgrade.") - if conn, err := proxy.hijackConnection(ctx, w); err == nil { - proxy.serveWebsocket(ctx, conn, r) - } - } - if !proxy.KeepHeader { RemoveProxyHeaders(ctx, r) } + + var err error resp, err = ctx.RoundTrip(r) if err != nil { ctx.Error = err - resp = proxy.filterResponse(nil, ctx) - } - if resp != nil { - ctx.Logf("Received response %v", resp.Status) } } @@ -73,6 +63,23 @@ func (proxy *ProxyHttpServer) handleHttp(w http.ResponseWriter, r *http.Request) } copyHeaders(w.Header(), resp.Header, proxy.KeepDestinationHeaders) w.WriteHeader(resp.StatusCode) + + if isWebSocketHandshake(resp.Header) { + ctx.Logf("Response looks like websocket upgrade.") + + // 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 { + wsConn, ok := resp.Body.(io.ReadWriter) + if !ok { + ctx.Warnf("Unable to use Websocket connection") + return + } + proxy.proxyWebsocket(ctx, wsConn, clientConn) + } + return + } + var copyWriter io.Writer = w // Content-Type header may also contain charset definition, so here we need to check the prefix. // Transfer-Encoding can be a list of comma separated values, so we use Contains() for it. diff --git a/vendor/github.com/elazarl/goproxy/https.go b/vendor/github.com/elazarl/goproxy/https.go index 459ee6e433..016e51344a 100644 --- a/vendor/github.com/elazarl/goproxy/https.go +++ b/vendor/github.com/elazarl/goproxy/https.go @@ -210,9 +210,6 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request return } - // Take the original value before filtering the request - closeConn := req.Close - 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 @@ -254,8 +251,9 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request } } resp = proxy.filterResponse(resp, ctx) + defer resp.Body.Close() + err = resp.Write(proxyClient) - _ = resp.Body.Close() if err != nil { httpError(proxyClient, ctx, err) return false @@ -265,11 +263,6 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request }(req); !requestOk { break } - - if closeConn { - ctx.Logf("Non-persistent connection; closing") - break - } } case ConnectMitm: _, _ = proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) @@ -322,9 +315,6 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request req.URL, err = url.Parse("https://" + r.Host + req.URL.String()) } - // Take the original value before filtering the request - closeConn := req.Close - if continueLoop := 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 @@ -364,16 +354,6 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request } return false } - if isWebSocketRequest(req) { - ctx.Logf("Request looks like websocket upgrade.") - if req.URL.Scheme == "http" { - ctx.Logf("Enforced HTTP websocket forwarding over TLS") - proxy.serveWebsocket(ctx, rawClientTls, req) - } else { - proxy.serveWebsocketTLS(ctx, req, tlsConfig, rawClientTls) - } - return false - } if err != nil { if req.URL != nil { ctx.Warnf("Illegal URL %s", "https://"+r.Host+req.URL.Path) @@ -409,7 +389,8 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request return false } - if resp.Request.Method == http.MethodHead { + 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 { @@ -423,7 +404,9 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request resp.Header.Set("Transfer-Encoding", "chunked") } // Force connection close otherwise chrome will keep CONNECT tunnel open forever - resp.Header.Set("Connection", "close") + 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 @@ -433,6 +416,24 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request return false } + if isWebsocket { + ctx.Logf("Response looks like websocket upgrade.") + + // According to resp.Body documentation: + // As of Go 1.12, the Body will also implement io.Writer + // on a successful "101 Switching Protocols" response, + // as used by WebSockets and HTTP/2's "h2c" mode. + wsConn, ok := resp.Body.(io.ReadWriter) + if !ok { + 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 + return false + } + if resp.Request.Method == http.MethodHead || (resp.StatusCode >= 100 && resp.StatusCode < 200) || resp.StatusCode == http.StatusNoContent || @@ -459,11 +460,6 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request }(req); !continueLoop { return } - - if closeConn { - ctx.Logf("Non-persistent connection; closing") - return - } } ctx.Logf("Exiting on EOF") }() diff --git a/vendor/github.com/elazarl/goproxy/proxy.go b/vendor/github.com/elazarl/goproxy/proxy.go index f9a829466b..9cec02399a 100644 --- a/vendor/github.com/elazarl/goproxy/proxy.go +++ b/vendor/github.com/elazarl/goproxy/proxy.go @@ -110,16 +110,13 @@ func RemoveProxyHeaders(ctx *ProxyCtx, r *http.Request) { // options that are desired for that particular connection and MUST NOT // be communicated by proxies over further connections. - // When server reads http request it sets req.Close to true if - // "Connection" header contains "close". - // https://github.com/golang/go/blob/master/src/net/http/request.go#L1080 - // Later, transfer.go adds "Connection: close" back when req.Close is true - // https://github.com/golang/go/blob/master/src/net/http/transfer.go#L275 - // That's why tests that checks "Connection: close" removal fail - if r.Header.Get("Connection") == "close" { - r.Close = false + // We need to keep "Connection: upgrade" header, since it's part of + // the WebSocket handshake, and it won't work without it. + // For all the other cases (close, keep-alive), we already handle them, by + // setting the r.Close variable in the previous lines. + if !isWebSocketHandshake(r.Header) { + r.Header.Del("Connection") } - r.Header.Del("Connection") } type flushWriter struct { diff --git a/vendor/github.com/elazarl/goproxy/websocket.go b/vendor/github.com/elazarl/goproxy/websocket.go index 66d4e9d13c..c10f57cad4 100644 --- a/vendor/github.com/elazarl/goproxy/websocket.go +++ b/vendor/github.com/elazarl/goproxy/websocket.go @@ -1,9 +1,6 @@ package goproxy import ( - "bufio" - "crypto/tls" - "errors" "io" "net" "net/http" @@ -21,48 +18,9 @@ func headerContains(header http.Header, name string, value string) bool { return false } -func isWebSocketRequest(r *http.Request) bool { - return headerContains(r.Header, "Connection", "upgrade") && - headerContains(r.Header, "Upgrade", "websocket") -} - -func (proxy *ProxyHttpServer) serveWebsocketTLS( - ctx *ProxyCtx, - req *http.Request, - tlsConfig *tls.Config, - clientConn *tls.Conn, -) { - // wss - host := req.URL.Host - // Port is optional in req.URL.Host, in this case SplitHostPort returns - // an error, and we add the default port - _, port, err := net.SplitHostPort(req.URL.Host) - if err != nil || port == "" { - host = net.JoinHostPort(req.URL.Host, "443") - } - - targetConn, err := proxy.connectDial(ctx, "tcp", host) - if err != nil { - ctx.Warnf("Error dialing target site: %v", err) - return - } - defer targetConn.Close() - - // Add TLS to the raw TCP connection - targetConn, err = proxy.initializeTLSconnection(ctx, targetConn, tlsConfig, host) - if err != nil { - ctx.Warnf("Websocket TLS connection error: %v", err) - return - } - - // Perform handshake - if err := proxy.websocketHandshake(ctx, req, targetConn, clientConn); err != nil { - ctx.Warnf("Websocket handshake error: %v", err) - return - } - - // Proxy wss connection - proxy.proxyWebsocket(ctx, targetConn, clientConn) +func isWebSocketHandshake(header http.Header) bool { + return headerContains(header, "Connection", "Upgrade") && + headerContains(header, "Upgrade", "websocket") } func (proxy *ProxyHttpServer) hijackConnection(ctx *ProxyCtx, w http.ResponseWriter) (net.Conn, error) { @@ -79,79 +37,20 @@ func (proxy *ProxyHttpServer) hijackConnection(ctx *ProxyCtx, w http.ResponseWri return clientConn, nil } -func (proxy *ProxyHttpServer) serveWebsocket(ctx *ProxyCtx, clientConn net.Conn, req *http.Request) { - // ws - host := req.URL.Host - // Port is optional in req.URL.Host, in this case SplitHostPort returns - // an error, and we add the default port - _, port, err := net.SplitHostPort(req.URL.Host) - if err != nil || port == "" { - host = net.JoinHostPort(req.URL.Host, "80") - } - - targetConn, err := proxy.connectDial(ctx, "tcp", host) - if err != nil { - ctx.Warnf("Error dialing target site: %v", err) - return - } - defer targetConn.Close() - - // Perform handshake - if err := proxy.websocketHandshake(ctx, req, targetConn, clientConn); err != nil { - ctx.Warnf("Websocket handshake error: %v", err) - return - } - - // Proxy ws connection - proxy.proxyWebsocket(ctx, targetConn, clientConn) -} - -func (proxy *ProxyHttpServer) websocketHandshake( - ctx *ProxyCtx, - req *http.Request, - targetSiteConn io.ReadWriter, - clientConn io.ReadWriter, -) error { - // write handshake request to target - err := req.Write(targetSiteConn) - if err != nil { - ctx.Warnf("Error writing upgrade request: %v", err) - return err - } - - targetTLSReader := bufio.NewReader(targetSiteConn) - - // Read handshake response from target - resp, err := http.ReadResponse(targetTLSReader, req) - if err != nil { - ctx.Warnf("Error reading handhsake response %v", err) - return err - } - - // Run response through handlers - resp = proxy.filterResponse(resp, ctx) - - // Proxy handshake back to client - err = resp.Write(clientConn) - if err != nil { - ctx.Warnf("Error writing handshake response: %v", err) - return err - } - return nil -} - -func (proxy *ProxyHttpServer) proxyWebsocket(ctx *ProxyCtx, dest io.ReadWriter, source io.ReadWriter) { - errChan := make(chan error, 2) - cp := func(dst io.Writer, src io.Reader) { - _, err := io.Copy(dst, src) - if err != nil && !errors.Is(err, net.ErrClosed) { - ctx.Warnf("Websocket error: %v", err) - } - errChan <- err - } - - // Start proxying websocket data - go cp(dest, source) - go cp(source, dest) - <-errChan +func (proxy *ProxyHttpServer) proxyWebsocket(ctx *ProxyCtx, remoteConn io.ReadWriter, proxyClient io.ReadWriter) { + // 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) + waitChan <- struct{}{} + }() + + go func() { + _ = copyOrWarn(ctx, proxyClient, remoteConn) + waitChan <- struct{}{} + }() + + // Wait until one end closes the connection + <-waitChan } diff --git a/vendor/modules.txt b/vendor/modules.txt index 206a3fd2e8..60f06263a9 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -278,7 +278,7 @@ github.com/docker/go-connections/tlsconfig # github.com/docker/go-units v0.5.0 ## explicit github.com/docker/go-units -# github.com/elazarl/goproxy v1.6.0 +# github.com/elazarl/goproxy v1.7.0 ## explicit; go 1.20 github.com/elazarl/goproxy github.com/elazarl/goproxy/internal/http1parser