diff --git a/go.mod b/go.mod index ce83509e..d68c2810 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,8 @@ require ( github.com/aws/aws-sdk-go-v2 v1.17.2 github.com/aws/aws-sdk-go-v2/config v1.18.4 github.com/cenkalti/backoff v1.1.0 - github.com/elazarl/goproxy v0.0.0-20171101143503-a96fa3a31826 + github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a + github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 github.com/fsnotify/fsnotify v1.4.9 github.com/go-ldap/ldap/v3 v3.2.3 github.com/google/uuid v1.3.0 diff --git a/go.sum b/go.sum index 775a770e..36c68f19 100644 --- a/go.sum +++ b/go.sum @@ -57,8 +57,10 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 v0.0.0-20171101143503-a96fa3a31826 h1:C0fzkSk9AgMlLF2WiNwwRUy0nIlJjqp8yf1KdmH/bZs= -github.com/elazarl/goproxy v0.0.0-20171101143503-a96fa3a31826/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -217,6 +219,7 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= diff --git a/vendor/github.com/elazarl/goproxy/README.md b/vendor/github.com/elazarl/goproxy/README.md index 50d91efe..495afc2d 100644 --- a/vendor/github.com/elazarl/goproxy/README.md +++ b/vendor/github.com/elazarl/goproxy/README.md @@ -2,14 +2,15 @@ [![GoDoc](https://godoc.org/github.com/elazarl/goproxy?status.svg)](https://godoc.org/github.com/elazarl/goproxy) [![Join the chat at https://gitter.im/elazarl/goproxy](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/elazarl/goproxy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +![Status](https://github.com/elazarl/goproxy/workflows/Go/badge.svg) Package goproxy provides a customizable HTTP proxy library for Go (golang), It supports regular HTTP proxy, HTTPS through CONNECT, and "hijacking" HTTPS connection using "Man in the Middle" style attack. -The intent of the proxy, is to be usable with reasonable amount of traffic -yet, customizable and programmable. +The intent of the proxy is to be usable with reasonable amount of traffic, +yet customizable and programmable. The proxy itself is simply a `net/http` handler. @@ -22,7 +23,7 @@ For example, the URL you should use as proxy when running `./bin/basic` is ## Mailing List -New features would be discussed on the [mailing list](https://groups.google.com/forum/#!forum/goproxy-dev) +New features will be discussed on the [mailing list](https://groups.google.com/forum/#!forum/goproxy-dev) before their development. ## Latest Stable Release @@ -32,13 +33,13 @@ Get the latest goproxy from `gopkg.in/elazarl/goproxy.v1`. # Why not Fiddler2? Fiddler is an excellent software with similar intent. However, Fiddler is not -as customizable as goproxy intend to be. The main difference is, Fiddler is not +as customizable as goproxy intends to be. The main difference is, Fiddler is not intended to be used as a real proxy. A possible use case that suits goproxy but -not Fiddler, is, gathering statistics on page load times for a certain website over a week. +not Fiddler, is gathering statistics on page load times for a certain website over a week. With goproxy you could ask all your users to set their proxy to a dedicated machine running a -goproxy server. Fiddler is a GUI app not designed to be ran like a server for multiple users. +goproxy server. Fiddler is a GUI app not designed to be run like a server for multiple users. # A taste of goproxy @@ -90,16 +91,62 @@ proxy.OnRequest(goproxy.DstHostIs("www.reddit.com")).DoFunc( }) ``` -`DstHostIs` returns a `ReqCondition`, that is a function receiving a `Request` and returning a boolean -we will only process requests that matches the condition. `DstHostIs("www.reddit.com")` will return +`DstHostIs` returns a `ReqCondition`, that is a function receiving a `Request` and returning a boolean. +We will only process requests that match the condition. `DstHostIs("www.reddit.com")` will return a `ReqCondition` accepting only requests directed to "www.reddit.com". `DoFunc` will receive a function that will preprocess the request. We can change the request, or -return a response. If the time is between 8:00am and 17:00pm, we will neglect the request, and -return a precanned text response saying "do not waste your time". +return a response. If the time is between 8:00am and 17:00pm, we will reject the request, and +return a pre-canned text response saying "do not waste your time". See additional examples in the examples directory. + +# Type of handlers for manipulating connect/req/resp behavior + +There are 3 kinds of useful handlers to manipulate the behavior, as follows: + +```go +// handler called after receiving HTTP CONNECT from the client, and before proxy establish connection +// with destination host +httpsHandlers []HttpsHandler + +// handler called before proxy send HTTP request to destination host +reqHandlers []ReqHandler + +// handler called after proxy receives HTTP Response from destination host, and before proxy forward +// the Response to the client. +respHandlers []RespHandler +``` + +Depending on what you want to manipulate, the ways to add handlers to each handler list are: + +```go +// Add handlers to httpsHandlers +proxy.OnRequest(Some ReqConditions).HandleConnect(YourHandlerFunc()) + +// Add handlers to reqHandlers +proxy.OnRequest(Some ReqConditions).Do(YourReqHandlerFunc()) + +// Add handlers to respHandlers +proxy.OnResponse(Some RespConditions).Do(YourRespHandlerFunc()) +``` + +For example: + +```go +// This rejects the HTTPS request to *.reddit.com during HTTP CONNECT phase +proxy.OnRequest(goproxy.ReqHostMatches(regexp.MustCompile("reddit.*:443$"))).HandleConnect(goproxy.AlwaysReject) + +// This will NOT reject the HTTPS request with URL ending with gif, due to the fact that proxy +// only got the URL.Hostname and URL.Port during the HTTP CONNECT phase if the scheme is HTTPS, which is +// quiet common these days. +proxy.OnRequest(goproxy.UrlMatches(regexp.MustCompile(`.*gif$`))).HandleConnect(goproxy.AlwaysReject) + +// The correct way to manipulate the HTTP request using URL.Path as condition is: +proxy.OnRequest(goproxy.UrlMatches(regexp.MustCompile(`.*gif$`))).Do(YourReqHandlerFunc()) +``` + # What's New 1. Ability to `Hijack` CONNECT requests. See @@ -108,14 +155,14 @@ See additional examples in the examples directory. # License -I put the software temporarily under the Go-compatible BSD license, -if this prevents someone from using the software, do let me know and I'll consider changing it. +I put the software temporarily under the Go-compatible BSD license. +If this prevents someone from using the software, do let me know and I'll consider changing it. At any rate, user feedback is very important for me, so I'll be delighted to know if you're using this package. # Beta Software -I've received a positive feedback from a few people who use goproxy in production settings. +I've received positive feedback from a few people who use goproxy in production settings. I believe it is good enough for usage. I'll try to keep reasonable backwards compatibility. In case of a major API change, diff --git a/vendor/github.com/elazarl/goproxy/counterecryptor.go b/vendor/github.com/elazarl/goproxy/counterecryptor.go index 494e7a4f..d1c39d23 100644 --- a/vendor/github.com/elazarl/goproxy/counterecryptor.go +++ b/vendor/github.com/elazarl/goproxy/counterecryptor.go @@ -3,6 +3,7 @@ package goproxy import ( "crypto/aes" "crypto/cipher" + "crypto/ecdsa" "crypto/rsa" "crypto/sha256" "crypto/x509" @@ -21,8 +22,12 @@ func NewCounterEncryptorRandFromKey(key interface{}, seed []byte) (r CounterEncr switch key := key.(type) { case *rsa.PrivateKey: keyBytes = x509.MarshalPKCS1PrivateKey(key) + case *ecdsa.PrivateKey: + if keyBytes, err = x509.MarshalECPrivateKey(key); err != nil { + return + } default: - err = errors.New("only RSA keys supported") + err = errors.New("only RSA and ECDSA keys supported") return } h := sha256.New() diff --git a/vendor/github.com/elazarl/goproxy/ctx.go b/vendor/github.com/elazarl/goproxy/ctx.go index 70b4cf0f..b372f7d4 100644 --- a/vendor/github.com/elazarl/goproxy/ctx.go +++ b/vendor/github.com/elazarl/goproxy/ctx.go @@ -1,6 +1,7 @@ package goproxy import ( + "crypto/tls" "net/http" "regexp" ) @@ -19,14 +20,19 @@ type ProxyCtx struct { // call of RespHandler UserData interface{} // Will connect a request to a response - Session int64 - proxy *ProxyHttpServer + Session int64 + certStore CertStorage + Proxy *ProxyHttpServer } type RoundTripper interface { RoundTrip(req *http.Request, ctx *ProxyCtx) (*http.Response, error) } +type CertStorage interface { + Fetch(hostname string, gen func() (*tls.Certificate, error)) (*tls.Certificate, error) +} + type RoundTripperFunc func(req *http.Request, ctx *ProxyCtx) (*http.Response, error) func (f RoundTripperFunc) RoundTrip(req *http.Request, ctx *ProxyCtx) (*http.Response, error) { @@ -37,11 +43,11 @@ func (ctx *ProxyCtx) RoundTrip(req *http.Request) (*http.Response, error) { if ctx.RoundTripper != nil { return ctx.RoundTripper.RoundTrip(req, ctx) } - return ctx.proxy.Tr.RoundTrip(req) + return ctx.Proxy.Tr.RoundTrip(req) } func (ctx *ProxyCtx) printf(msg string, argv ...interface{}) { - ctx.proxy.Logger.Printf("[%03d] "+msg+"\n", append([]interface{}{ctx.Session & 0xFF}, argv...)...) + ctx.Proxy.Logger.Printf("[%03d] "+msg+"\n", append([]interface{}{ctx.Session & 0xFF}, argv...)...) } // Logf prints a message to the proxy's log. Should be used in a ProxyHttpServer's filter @@ -53,7 +59,7 @@ func (ctx *ProxyCtx) printf(msg string, argv ...interface{}) { // return r, nil // }) func (ctx *ProxyCtx) Logf(msg string, argv ...interface{}) { - if ctx.proxy.Verbose { + if ctx.Proxy.Verbose { ctx.printf("INFO: "+msg, argv...) } } diff --git a/vendor/github.com/elazarl/goproxy/dispatcher.go b/vendor/github.com/elazarl/goproxy/dispatcher.go index 4e7c9cb9..25c949c0 100644 --- a/vendor/github.com/elazarl/goproxy/dispatcher.go +++ b/vendor/github.com/elazarl/goproxy/dispatcher.go @@ -161,6 +161,22 @@ func ContentTypeIs(typ string, types ...string) RespCondition { }) } +// StatusCodeIs returns a RespCondition, testing whether or not the HTTP status +// code is one of the given ints +func StatusCodeIs(codes ...int) RespCondition { + codeSet := make(map[int]bool) + for _, c := range codes { + codeSet[c] = true + } + return RespConditionFunc(func(resp *http.Response, ctx *ProxyCtx) bool { + if resp == nil { + return false + } + _, codeMatch := codeSet[resp.StatusCode] + return codeMatch + }) +} + // ProxyHttpServer.OnRequest Will return a temporary ReqProxyConds struct, aggregating the given condtions. // You will use the ReqProxyConds struct to register a ReqHandler, that would filter // the request, only if all the given ReqCondition matched. diff --git a/vendor/github.com/elazarl/goproxy/doc.go b/vendor/github.com/elazarl/goproxy/doc.go index 50aaa71f..6f44317b 100644 --- a/vendor/github.com/elazarl/goproxy/doc.go +++ b/vendor/github.com/elazarl/goproxy/doc.go @@ -3,7 +3,7 @@ Package goproxy provides a customizable HTTP proxy, supporting hijacking HTTPS connection. The intent of the proxy, is to be usable with reasonable amount of traffic -yet, customizable and programable. +yet, customizable and programmable. The proxy itself is simply an `net/http` handler. @@ -60,10 +60,10 @@ Finally, we have convenience function to throw a quick response proxy.OnResponse(hasGoProxyHeader).DoFunc(func(r*http.Response,ctx *goproxy.ProxyCtx)*http.Response { r.Body.Close() - return goproxy.ForbiddenTextResponse(ctx.Req,"Can't see response with X-GoProxy header!") + return goproxy.NewResponse(ctx.Req, goproxy.ContentTypeText, http.StatusForbidden, "Can't see response with X-GoProxy header!") }) -we close the body of the original repsonse, and return a new 403 response with a short message. +we close the body of the original response, and return a new 403 response with a short message. Example use cases: diff --git a/vendor/github.com/elazarl/goproxy/ext/LICENSE b/vendor/github.com/elazarl/goproxy/ext/LICENSE new file mode 100644 index 00000000..2067e567 --- /dev/null +++ b/vendor/github.com/elazarl/goproxy/ext/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 Elazar Leibovich. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Elazar Leibovich. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/elazarl/goproxy/ext/auth/basic.go b/vendor/github.com/elazarl/goproxy/ext/auth/basic.go index 2641423c..a433f2d0 100644 --- a/vendor/github.com/elazarl/goproxy/ext/auth/basic.go +++ b/vendor/github.com/elazarl/goproxy/ext/auth/basic.go @@ -15,11 +15,14 @@ var unauthorizedMsg = []byte("407 Proxy Authentication Required") func BasicUnauthorized(req *http.Request, realm string) *http.Response { // TODO(elazar): verify realm is well formed return &http.Response{ - StatusCode: 407, - ProtoMajor: 1, - ProtoMinor: 1, - Request: req, - Header: http.Header{"Proxy-Authenticate": []string{"Basic realm=" + realm}}, + StatusCode: 407, + ProtoMajor: 1, + ProtoMinor: 1, + Request: req, + Header: http.Header{ + "Proxy-Authenticate": []string{"Basic realm=" + realm}, + "Proxy-Connection": []string{"close"}, + }, Body: ioutil.NopCloser(bytes.NewBuffer(unauthorizedMsg)), ContentLength: int64(len(unauthorizedMsg)), } diff --git a/vendor/github.com/elazarl/goproxy/https.go b/vendor/github.com/elazarl/goproxy/https.go index b3e0e3ba..e105caf8 100644 --- a/vendor/github.com/elazarl/goproxy/https.go +++ b/vendor/github.com/elazarl/goproxy/https.go @@ -36,6 +36,10 @@ var ( httpsRegexp = regexp.MustCompile(`^https:\/\/`) ) +// 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 +// Hijack func type ConnectAction struct { Action ConnectActionLiteral Hijack func(req *http.Request, client net.Conn, ctx *ProxyCtx) @@ -43,9 +47,25 @@ type ConnectAction struct { } func stripPort(s string) string { - ix := strings.IndexRune(s, ':') - if ix == -1 { - return s + var ix int + if strings.Contains(s, "[") && strings.Contains(s, "]") { + //ipv6 : for 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 + } + } return s[:ix] } @@ -57,15 +77,28 @@ func (proxy *ProxyHttpServer) dial(network, addr string) (c net.Conn, err error) return net.Dial(network, addr) } -func (proxy *ProxyHttpServer) connectDial(network, addr string) (c net.Conn, err error) { - if proxy.ConnectDial == nil { +func (proxy *ProxyHttpServer) connectDial(ctx *ProxyCtx, network, addr string) (c net.Conn, err error) { + if proxy.ConnectDialWithReq == nil && proxy.ConnectDial == nil { return proxy.dial(network, addr) } + + if proxy.ConnectDialWithReq != nil { + return proxy.ConnectDialWithReq(ctx.Req, network, addr) + } + return proxy.ConnectDial(network, addr) } +type halfClosable interface { + net.Conn + CloseWrite() error + CloseRead() error +} + +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} + ctx := &ProxyCtx{Req: r, Session: atomic.AddInt64(&proxy.sess, 1), Proxy: proxy, certStore: proxy.CertStore} hij, ok := w.(http.Hijacker) if !ok { @@ -94,16 +127,16 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request if !hasPort.MatchString(host) { host += ":80" } - targetSiteCon, err := proxy.connectDial("tcp", host) + targetSiteCon, err := proxy.connectDial(ctx, "tcp", host) if err != nil { httpError(proxyClient, ctx, err) return } ctx.Logf("Accepting CONNECT to %s", host) - proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) + proxyClient.Write([]byte("HTTP/1.0 200 Connection established\r\n\r\n")) - targetTCP, targetOK := targetSiteCon.(*net.TCPConn) - proxyClientTCP, clientOK := proxyClient.(*net.TCPConn) + targetTCP, targetOK := targetSiteCon.(halfClosable) + proxyClientTCP, clientOK := proxyClient.(halfClosable) if targetOK && clientOK { go copyAndClose(ctx, targetTCP, proxyClientTCP) go copyAndClose(ctx, proxyClientTCP, targetTCP) @@ -121,13 +154,11 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request } case ConnectHijack: - ctx.Logf("Hijacking CONNECT to %s", host) - proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) todo.Hijack(r, proxyClient, ctx) case ConnectHTTPMitm: proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) ctx.Logf("Assuming CONNECT is plain HTTP tunneling, mitm proxying it") - targetSiteCon, err := proxy.connectDial("tcp", host) + targetSiteCon, err := proxy.connectDial(ctx, "tcp", host) if err != nil { ctx.Warnf("Error dialing to %s: %s", host, err.Error()) return @@ -188,7 +219,7 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request clientTlsReader := bufio.NewReader(rawClientTls) for !isEof(clientTlsReader) { req, err := http.ReadRequest(clientTlsReader) - var ctx = &ProxyCtx{Req: req, Session: atomic.AddInt64(&proxy.sess, 1), proxy: proxy} + var ctx = &ProxyCtx{Req: req, Session: atomic.AddInt64(&proxy.sess, 1), Proxy: proxy, UserData: ctx.UserData} if err != nil && err != io.EOF { return } @@ -209,12 +240,26 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request req, resp := proxy.filterRequest(req, ctx) if resp == nil { + if isWebSocketRequest(req) { + ctx.Logf("Request looks like websocket upgrade.") + proxy.serveWebsocketTLS(ctx, w, req, tlsConfig, rawClientTls) + return + } if err != nil { - ctx.Warnf("Illegal URL %s", "https://"+r.Host+req.URL.Path) + if req.URL != nil { + ctx.Warnf("Illegal URL %s", "https://"+r.Host+req.URL.Path) + } else { + ctx.Warnf("Illegal URL %s", "https://"+r.Host) + } return } removeProxyHeaders(ctx, req) - resp, err = ctx.RoundTrip(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) + }() if err != nil { ctx.Warnf("Cannot read TLS response from mitm'd server %v", err) return @@ -234,10 +279,15 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request ctx.Warnf("Cannot write TLS response HTTP status from mitm'd client: %v", err) return } - // Since we don't know the length of resp, return chunked encoded response - // TODO: use a more reasonable scheme - resp.Header.Del("Content-Length") - resp.Header.Set("Transfer-Encoding", "chunked") + + if resp.Request.Method == "HEAD" { + // don't change Content-Length for HEAD request + } else { + // Since we don't know the length of resp, return chunked encoded response + // TODO: use a more reasonable scheme + resp.Header.Del("Content-Length") + resp.Header.Set("Transfer-Encoding", "chunked") + } // Force connection close otherwise chrome will keep CONNECT tunnel open forever resp.Header.Set("Connection", "close") if err := resp.Header.Write(rawClientTls); err != nil { @@ -248,18 +298,23 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request ctx.Warnf("Cannot write TLS response header end from mitm'd client: %v", err) return } - 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 - } - if err := chunked.Close(); err != nil { - ctx.Warnf("Cannot write TLS chunked EOF from mitm'd client: %v", err) - return - } - if _, err = io.WriteString(rawClientTls, "\r\n"); err != nil { - ctx.Warnf("Cannot write TLS response chunked trailer from mitm'd client: %v", err) - return + + if resp.Request.Method == "HEAD" { + // Don't write out a response body for HEAD request + } 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 + } + if err := chunked.Close(); err != nil { + ctx.Warnf("Cannot write TLS chunked EOF from mitm'd client: %v", err) + return + } + if _, err = io.WriteString(rawClientTls, "\r\n"); err != nil { + ctx.Warnf("Cannot write TLS response chunked trailer from mitm'd client: %v", err) + return + } } } ctx.Logf("Exiting on EOF") @@ -293,7 +348,7 @@ func copyOrWarn(ctx *ProxyCtx, dst io.Writer, src io.Reader, wg *sync.WaitGroup) wg.Done() } -func copyAndClose(ctx *ProxyCtx, dst, src *net.TCPConn) { +func copyAndClose(ctx *ProxyCtx, dst, src halfClosable) { if _, err := io.Copy(dst, src); err != nil { ctx.Warnf("Error copying to client: %s", err) } @@ -314,6 +369,10 @@ func dialerFromEnv(proxy *ProxyHttpServer) func(network, addr string) (net.Conn, } func (proxy *ProxyHttpServer) NewConnectDialToProxy(https_proxy string) func(network, addr string) (net.Conn, error) { + return proxy.NewConnectDialToProxyWithHandler(https_proxy, nil) +} + +func (proxy *ProxyHttpServer) NewConnectDialToProxyWithHandler(https_proxy string, connectReqHandler func(req *http.Request)) func(network, addr string) (net.Conn, error) { u, err := url.Parse(https_proxy) if err != nil { return nil @@ -329,6 +388,9 @@ func (proxy *ProxyHttpServer) NewConnectDialToProxy(https_proxy string) func(net Host: addr, Header: make(http.Header), } + if connectReqHandler != nil { + connectReqHandler(connectReq) + } c, err := proxy.dial(network, u.Host) if err != nil { return nil, err @@ -355,7 +417,7 @@ func (proxy *ProxyHttpServer) NewConnectDialToProxy(https_proxy string) func(net return c, nil } } - if u.Scheme == "https" { + if u.Scheme == "https" || u.Scheme == "wss" { if strings.IndexRune(u.Host, ':') == -1 { u.Host += ":443" } @@ -371,6 +433,9 @@ func (proxy *ProxyHttpServer) NewConnectDialToProxy(https_proxy string) func(net Host: addr, Header: make(http.Header), } + if connectReqHandler != nil { + connectReqHandler(connectReq) + } connectReq.Write(c) // Read response. // Okay to use and discard buffered reader here, because @@ -398,14 +463,28 @@ func (proxy *ProxyHttpServer) NewConnectDialToProxy(https_proxy string) func(net func TLSConfigFromCA(ca *tls.Certificate) func(host string, ctx *ProxyCtx) (*tls.Config, error) { return func(host string, ctx *ProxyCtx) (*tls.Config, error) { - config := *defaultTLSConfig + var err error + var cert *tls.Certificate + + hostname := stripPort(host) + config := defaultTLSConfig.Clone() ctx.Logf("signing for %s", stripPort(host)) - cert, err := signHost(*ca, []string{stripPort(host)}) + + genCert := func() (*tls.Certificate, error) { + return signHost(*ca, []string{hostname}) + } + if ctx.certStore != nil { + cert, err = ctx.certStore.Fetch(hostname, genCert) + } else { + cert, err = genCert() + } + if err != nil { ctx.Warnf("Cannot sign host certificate with provided CA: %s", err) return nil, err } - config.Certificates = append(config.Certificates, cert) - return &config, nil + + config.Certificates = append(config.Certificates, *cert) + return config, nil } } diff --git a/vendor/github.com/elazarl/goproxy/logger.go b/vendor/github.com/elazarl/goproxy/logger.go new file mode 100644 index 00000000..939cf69e --- /dev/null +++ b/vendor/github.com/elazarl/goproxy/logger.go @@ -0,0 +1,5 @@ +package goproxy + +type Logger interface { + Printf(format string, v ...interface{}) +} diff --git a/vendor/github.com/elazarl/goproxy/proxy.go b/vendor/github.com/elazarl/goproxy/proxy.go index fefb3bb0..fa5494c6 100644 --- a/vendor/github.com/elazarl/goproxy/proxy.go +++ b/vendor/github.com/elazarl/goproxy/proxy.go @@ -16,9 +16,11 @@ type ProxyHttpServer struct { // session variable must be aligned in i386 // see http://golang.org/src/pkg/sync/atomic/doc.go#L41 sess int64 + // KeepDestinationHeaders indicates the proxy should retain any headers present in the http.Response before proxying + KeepDestinationHeaders bool // setting Verbose to true will log information on each request sent to the proxy Verbose bool - Logger *log.Logger + Logger Logger NonproxyHandler http.Handler reqHandlers []ReqHandler respHandlers []RespHandler @@ -26,14 +28,19 @@ type ProxyHttpServer struct { Tr *http.Transport // ConnectDial will be used to create TCP connections for CONNECT requests // if nil Tr.Dial will be used - ConnectDial func(network string, addr string) (net.Conn, error) + ConnectDial func(network string, addr string) (net.Conn, error) + ConnectDialWithReq func(req *http.Request, network string, addr string) (net.Conn, error) + CertStore CertStorage + KeepHeader bool } var hasPort = regexp.MustCompile(`:\d+$`) -func copyHeaders(dst, src http.Header) { - for k, _ := range dst { - dst.Del(k) +func copyHeaders(dst, src http.Header, keepDestHeaders bool) { + if !keepDestHeaders { + for k := range dst { + dst.Del(k) + } } for k, vs := range src { for _, v := range vs { @@ -88,16 +95,40 @@ func removeProxyHeaders(ctx *ProxyCtx, r *http.Request) { // The Connection general-header field allows the sender to specify // 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 + } r.Header.Del("Connection") } +type flushWriter struct { + w io.Writer +} + +func (fw flushWriter) Write(p []byte) (int, error) { + n, err := fw.w.Write(p) + if f, ok := fw.w.(http.Flusher); ok { + // only flush if the Writer implements the Flusher interface. + f.Flush() + } + + return n, err +} + // Standard net/http function. Shouldn't be used directly, http.Serve will use it. func (proxy *ProxyHttpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { //r.Header["X-Forwarded-For"] = w.RemoteAddr() if r.Method == "CONNECT" { proxy.handleHttps(w, r) } else { - ctx := &ProxyCtx{Req: r, Session: atomic.AddInt64(&proxy.sess, 1), proxy: proxy} + 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()) @@ -108,22 +139,47 @@ func (proxy *ProxyHttpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) r, resp := proxy.filterRequest(r, ctx) if resp == nil { - removeProxyHeaders(ctx, r) + if isWebSocketRequest(r) { + ctx.Logf("Request looks like websocket upgrade.") + proxy.serveWebsocket(ctx, w, r) + } + + if !proxy.KeepHeader { + removeProxyHeaders(ctx, r) + } resp, err = ctx.RoundTrip(r) if err != nil { ctx.Error = err resp = proxy.filterResponse(nil, ctx) - if resp == nil { - ctx.Logf("error read response %v %v:", r.URL.Host, err.Error()) - http.Error(w, err.Error(), 500) - return - } + + } + if resp != nil { + ctx.Logf("Received response %v", resp.Status) } - ctx.Logf("Received response %v", resp.Status) } - origBody := resp.Body + + var origBody io.ReadCloser + + if resp != nil { + origBody = resp.Body + defer origBody.Close() + } + resp = proxy.filterResponse(resp, ctx) - defer origBody.Close() + + if resp == nil { + var errorString string + if ctx.Error != nil { + errorString = "error read response " + r.URL.Host + " : " + ctx.Error.Error() + ctx.Logf(errorString) + http.Error(w, ctx.Error.Error(), 500) + } else { + errorString = "error read response " + r.URL.Host + ctx.Logf(errorString) + http.Error(w, errorString, 500) + } + return + } ctx.Logf("Copying response to client %v [%d]", resp.Status, resp.StatusCode) // http.ResponseWriter will take care of filling the correct response length // Setting it now, might impose wrong value, contradicting the actual new @@ -134,9 +190,15 @@ func (proxy *ProxyHttpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) if origBody != resp.Body { resp.Header.Del("Content-Length") } - copyHeaders(w.Header(), resp.Header) + copyHeaders(w.Header(), resp.Header, proxy.KeepDestinationHeaders) w.WriteHeader(resp.StatusCode) - nr, err := io.Copy(w, resp.Body) + var copyWriter io.Writer = w + if w.Header().Get("content-type") == "text/event-stream" { + // server-side events, flush the buffered data to the client. + copyWriter = &flushWriter{w: w} + } + + nr, err := io.Copy(copyWriter, resp.Body) if err := resp.Body.Close(); err != nil { ctx.Warnf("Can't close response body %v", err) } @@ -144,7 +206,7 @@ func (proxy *ProxyHttpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) } } -// New proxy server, logs to StdErr by default +// NewProxyHttpServer creates and returns a proxy server, logging to stderr by default func NewProxyHttpServer() *ProxyHttpServer { proxy := ProxyHttpServer{ Logger: log.New(os.Stderr, "", log.LstdFlags), @@ -154,9 +216,10 @@ 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.", 500) }), - Tr: &http.Transport{TLSClientConfig: tlsClientSkipVerify, - Proxy: http.ProxyFromEnvironment}, + Tr: &http.Transport{TLSClientConfig: tlsClientSkipVerify, Proxy: http.ProxyFromEnvironment}, } + proxy.ConnectDial = dialerFromEnv(&proxy) + return &proxy } diff --git a/vendor/github.com/elazarl/goproxy/responses.go b/vendor/github.com/elazarl/goproxy/responses.go index b304b882..e1bf28fc 100644 --- a/vendor/github.com/elazarl/goproxy/responses.go +++ b/vendor/github.com/elazarl/goproxy/responses.go @@ -21,6 +21,7 @@ func NewResponse(r *http.Request, contentType string, status int, body string) * resp.Header = make(http.Header) resp.Header.Add("Content-Type", contentType) resp.StatusCode = status + resp.Status = http.StatusText(status) buf := bytes.NewBufferString(body) resp.ContentLength = int64(buf.Len()) resp.Body = ioutil.NopCloser(buf) diff --git a/vendor/github.com/elazarl/goproxy/signer.go b/vendor/github.com/elazarl/goproxy/signer.go index f6d99fc7..aa511ca9 100644 --- a/vendor/github.com/elazarl/goproxy/signer.go +++ b/vendor/github.com/elazarl/goproxy/signer.go @@ -1,12 +1,17 @@ package goproxy import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rsa" "crypto/sha1" "crypto/tls" "crypto/x509" "crypto/x509/pkix" + "fmt" "math/big" + "math/rand" "net" "runtime" "sort" @@ -32,21 +37,18 @@ func hashSortedBigInt(lst []string) *big.Int { var goproxySignerVersion = ":goroxy1" -func signHost(ca tls.Certificate, hosts []string) (cert tls.Certificate, err error) { +func signHost(ca tls.Certificate, hosts []string) (cert *tls.Certificate, err error) { var x509ca *x509.Certificate // Use the provided ca and not the global GoproxyCa for certificate generation. if x509ca, err = x509.ParseCertificate(ca.Certificate[0]); err != nil { return } - start := time.Unix(0, 0) - end, err := time.Parse("2006-01-02", "2049-12-31") - if err != nil { - panic(err) - } - hash := hashSorted(append(hosts, goproxySignerVersion, ":"+runtime.Version())) - serial := new(big.Int) - serial.SetBytes(hash) + + start := time.Unix(time.Now().Unix()-2592000, 0) // 2592000 = 30 day + end := time.Unix(time.Now().Unix()+31536000, 0) // 31536000 = 365 day + + serial := big.NewInt(rand.Int63()) template := x509.Certificate{ // TODO(elazar): instead of this ugly hack, just encode the certificate and hash the binary form. SerialNumber: serial, @@ -66,22 +68,41 @@ func signHost(ca tls.Certificate, hosts []string) (cert tls.Certificate, err err template.IPAddresses = append(template.IPAddresses, ip) } else { template.DNSNames = append(template.DNSNames, h) + template.Subject.CommonName = h } } + + hash := hashSorted(append(hosts, goproxySignerVersion, ":"+runtime.Version())) var csprng CounterEncryptorRand if csprng, err = NewCounterEncryptorRandFromKey(ca.PrivateKey, hash); err != nil { return } - var certpriv *rsa.PrivateKey - if certpriv, err = rsa.GenerateKey(&csprng, 1024); err != nil { - return + + var certpriv crypto.Signer + switch ca.PrivateKey.(type) { + case *rsa.PrivateKey: + if certpriv, err = rsa.GenerateKey(&csprng, 2048); err != nil { + return + } + case *ecdsa.PrivateKey: + if certpriv, err = ecdsa.GenerateKey(elliptic.P256(), &csprng); err != nil { + return + } + default: + err = fmt.Errorf("unsupported key type %T", ca.PrivateKey) } + var derBytes []byte - if derBytes, err = x509.CreateCertificate(&csprng, &template, x509ca, &certpriv.PublicKey, ca.PrivateKey); err != nil { + if derBytes, err = x509.CreateCertificate(&csprng, &template, x509ca, certpriv.Public(), ca.PrivateKey); err != nil { return } - return tls.Certificate{ + return &tls.Certificate{ Certificate: [][]byte{derBytes, ca.Certificate[0]}, PrivateKey: certpriv, }, nil } + +func init() { + // Avoid deterministic random numbers + rand.Seed(time.Now().UnixNano()) +} diff --git a/vendor/github.com/elazarl/goproxy/websocket.go b/vendor/github.com/elazarl/goproxy/websocket.go new file mode 100644 index 00000000..522b88e3 --- /dev/null +++ b/vendor/github.com/elazarl/goproxy/websocket.go @@ -0,0 +1,121 @@ +package goproxy + +import ( + "bufio" + "crypto/tls" + "io" + "net/http" + "net/url" + "strings" +) + +func headerContains(header http.Header, name string, value string) bool { + for _, v := range header[name] { + for _, s := range strings.Split(v, ",") { + if strings.EqualFold(value, strings.TrimSpace(s)) { + return true + } + } + } + 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, w http.ResponseWriter, req *http.Request, tlsConfig *tls.Config, clientConn *tls.Conn) { + targetURL := url.URL{Scheme: "wss", Host: req.URL.Host, Path: req.URL.Path} + + // Connect to upstream + targetConn, err := tls.Dial("tcp", targetURL.Host, tlsConfig) + 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 wss connection + proxy.proxyWebsocket(ctx, targetConn, clientConn) +} + +func (proxy *ProxyHttpServer) serveWebsocket(ctx *ProxyCtx, w http.ResponseWriter, req *http.Request) { + targetURL := url.URL{Scheme: "ws", Host: req.URL.Host, Path: req.URL.Path} + + targetConn, err := proxy.connectDial(ctx, "tcp", targetURL.Host) + if err != nil { + ctx.Warnf("Error dialing target site: %v", err) + return + } + defer targetConn.Close() + + // Connect to Client + hj, ok := w.(http.Hijacker) + if !ok { + panic("httpserver does not support hijacking") + } + clientConn, _, err := hj.Hijack() + if err != nil { + ctx.Warnf("Hijack 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 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) + ctx.Warnf("Websocket error: %v", err) + errChan <- err + } + + // Start proxying websocket data + go cp(dest, source) + go cp(source, dest) + <-errChan +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 0bef059c..36007f02 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -104,9 +104,11 @@ github.com/cespare/xxhash/v2 # github.com/davecgh/go-spew v1.1.1 ## explicit github.com/davecgh/go-spew/spew -# github.com/elazarl/goproxy v0.0.0-20171101143503-a96fa3a31826 +# github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a ## explicit github.com/elazarl/goproxy +# github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 +## explicit github.com/elazarl/goproxy/ext/auth # github.com/fatih/color v1.15.0 ## explicit; go 1.17