diff --git a/go.mod b/go.mod index 31704de..9e68abc 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.25.0 require ( github.com/aws/aws-sdk-go v1.55.8 github.com/aws/aws-sdk-go-v2 v1.41.1 - github.com/elazarl/goproxy v0.0.0-20240726154733-8b0c20506380 + github.com/elazarl/goproxy v1.7.2 github.com/evalphobia/logrus_sentry v0.8.2 github.com/getsentry/raven-go v0.2.0 github.com/jarcoal/httpmock v1.4.1 diff --git a/go.sum b/go.sum index 2bca4f5..cb89a33 100644 --- a/go.sum +++ b/go.sum @@ -17,10 +17,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 v0.0.0-20240726154733-8b0c20506380 h1:1NyRx2f4W4WBRyg0Kys0ZbaNmDDzZ2R/C7DTi+bbsJ0= -github.com/elazarl/goproxy v0.0.0-20240726154733-8b0c20506380/go.mod h1:thX175TtLTzLj3p7N/Q9IiKZ7NF+p72cvL91emV0hzo= -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/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= 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/.golangci.yml b/vendor/github.com/elazarl/goproxy/.golangci.yml new file mode 100644 index 0000000..69e36d9 --- /dev/null +++ b/vendor/github.com/elazarl/goproxy/.golangci.yml @@ -0,0 +1,160 @@ +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 + - containedctx + - decorder + - dogsled + - durationcheck + - errchkjson + - errname + - errorlint + - exhaustive + - fatcontext + - forbidigo + - forcetypeassert + - gci + - gocheckcompilerdirectives + - gochecksumtype + - gocritic + - godot + - gofmt + - gofumpt + - goheader + - gomodguard + - goprintffuncname + - gosec + - gosmopolitan + - grouper + - iface + - importas + - interfacebloat + - lll + - loggercheck + - makezero + - mirror + - misspell + - nakedret + - nilerr + - noctx + - nolintlint + - perfsprint + - prealloc + - predeclared + - reassign + - revive + - stylecheck + - tagalign + - tenv + - testableexamples + - testifylint + - testpackage + - thelper + - tparallel + - unconvert + - usestdlibvars + - wastedassign + - whitespace + + disable: + - bodyclose + - canonicalheader + - contextcheck # Re-enable in V2 + - copyloopvar + - cyclop + - depguard + - dupl + - dupword + - err113 + - exhaustruct + - funlen + - ginkgolinter + - gochecknoglobals + - gochecknoinits + - gocognit + - goconst + - gocyclo + - godox + - goimports + - gomoddirectives + - inamedparam + - intrange + - ireturn + - maintidx + - mnd + - musttag + - nestif # TODO: Re-enable in V2 + - nilnil + - nlreturn + - nonamedreturns + - nosprintfhostport + - paralleltest + - promlinter + - protogetter + - rowserrcheck + - sloglint + - spancheck + - sqlclosecheck + - tagliatelle + - unparam + - varnamelen + - 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 diff --git a/vendor/github.com/elazarl/goproxy/README.md b/vendor/github.com/elazarl/goproxy/README.md index 495afc2..435eded 100644 --- a/vendor/github.com/elazarl/goproxy/README.md +++ b/vendor/github.com/elazarl/goproxy/README.md @@ -1,57 +1,134 @@ -# Introduction +# GoProxy -[![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) +[![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. +The target of this project is to offer an `optimized` proxy server, usable with +reasonable amount of traffic, yet `customizable` and `programmable`. + +The proxy itself is simply a `net/http` handler, so you can add multiple +middlewares (panic recover, logging, compression, etc.) over it. It can be +easily integrated with any other HTTP network library. + +In order to use goproxy, one should set their browser (or any other client) +to use goproxy as an HTTP proxy. +Here is how you do that in [Chrome](https://www.wikihow.com/Connect-to-a-Proxy-Server) +and in [Firefox](http://www.wikihow.com/Enter-Proxy-Settings-in-Firefox). +If you decide to start with the `base` example, the URL you should use as +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 +- Use a `custom http.Transport` to perform requests to the target server +- You can specify a `MITM certificates cache`, to reuse them later for other requests to the same host, thus saving CPU. Not enabled by default, but you should use it in production! +- Redirect normal HTTP traffic to a `custom handler`, when the target is a `relative path` (e.g. `/ping`) +- You can choose the logger to use, by implementing the `Logger` interface +- You can `disable` the HTTP request headers `canonicalization`, by setting `PreventCanonicalization` to true + +## Proxy modes +1. Regular HTTP proxy +2. HTTPS through CONNECT +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`. +This is an `open source` project managed by volunteers, and we're happy +to discuss anything that can improve it. + +Make sure to explain everything, including the reason behind the issue +and what you want to change, to make the problem easier to understand. +You can also directly open a `Pull Request`, if it's a small code change, but +you need to explain in the description everything. +If you open a pull request named `refactoring` with `5,000` lines changed, +we won't merge it... `:D` + +The code for this project is released under the `BSD 3-Clause` license, +making it useful for `commercial` uses as well. + +### Submit your case study +So, you have introduced & integrated GoProxy into one of your personal projects +or a project inside the company you work for. + +We're happy to learn about new `creative solutions` made with this library, +so feel free to `contact` the maintainer listed above via e-mail, to explaining +why you found this project useful for your needs. + +If you have signed a `Non Disclosure Agreement` with the company, you +can propose them to write a `blog post` on their official website about +this topic, so this information will be public by their choice, and you can +`share the link` of the blog post with us :) + +The purpose of case studies is to share with the `community` why all the +`contributors` to this project are `improving` the world with their help and +what people are building using it. + +### Linter +The codebase uses an automatic lint check over your Pull Request code. +Before opening it, you should check if your changes respect it, by running +the linter in your local machine, so you won't have any surprise. + +To install the linter: +```sh +go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest +``` -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 proxy itself is simply a `net/http` handler. - -In order to use goproxy, one should set their browser to use goproxy as an HTTP -proxy. Here is how you do that [in Chrome](https://support.google.com/chrome/answer/96815?hl=en) -and [in Firefox](http://www.wikihow.com/Enter-Proxy-Settings-in-Firefox). - -For example, the URL you should use as proxy when running `./bin/basic` is -`localhost:8080`, as this is the default binding for the basic proxy. - -## Mailing List - -New features will be discussed on the [mailing list](https://groups.google.com/forum/#!forum/goproxy-dev) -before their development. - -## Latest Stable Release - -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 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. -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 run like a server for multiple users. +This will create an executable in your `$GOPATH/bin` folder +(`$GOPATH` is an environment variable, usually +its value is equivalent to `~/go`, check its value in your machine if you +aren't sure about it). +Make sure to include the bin folder in the path of your shell, to be able to +directly use the `golangci-lint run` command. -# A taste of goproxy +## A taste of GoProxy -To get a taste of `goproxy`, a basic HTTP/HTTPS transparent proxy +To get a taste of `goproxy`, here you are a basic HTTP/HTTPS proxy +that just forward data to the destination: ```go package main import ( - "github.com/elazarl/goproxy" "log" "net/http" + + "github.com/elazarl/goproxy" ) func main() { @@ -61,7 +138,9 @@ func main() { } ``` -This line will add `X-GoProxy: yxorPoG-X` header to all requests sent through the proxy +### Request handler +This line will add `X-GoProxy: yxorPoG-X` header to all requests sent through the proxy, +before sending them to the destination: ```go proxy.OnRequest().DoFunc( @@ -71,99 +150,157 @@ proxy.OnRequest().DoFunc( }) ``` -`DoFunc` will process all incoming requests to the proxy. It will add a header to the request -and return it. The proxy will send the modified request. +When the `OnRequest()` input is empty, the function specified in `DoFunc` +will process all incoming requests to the proxy. In this case, it will add +a header to the request and return it to the caller. +The proxy will send the modified request to the destination. +You can also use `Do` instead of `DoFunc`, if you implement the specified +interface in your type. -Note that we returned nil value as the response. Had we returned a response, goproxy would -have discarded the request and sent the new response to the client. +> ⚠️ Note we returned a nil value as the response. +> If the returned response is not nil, goproxy will discard the request +> and send the specified response to the client. -In order to refuse connections to reddit at work time +### Conditional Request handler +Refuse connections to www.reddit.com between 8 and 17 in the server +local timezone: ```go proxy.OnRequest(goproxy.DstHostIs("www.reddit.com")).DoFunc( - func(r *http.Request,ctx *goproxy.ProxyCtx)(*http.Request,*http.Response) { + func(req *http.Request,ctx *goproxy.ProxyCtx)(*http.Request,*http.Response) { if h,_,_ := time.Now().Clock(); h >= 8 && h <= 17 { - return r,goproxy.NewResponse(r, - goproxy.ContentTypeText,http.StatusForbidden, - "Don't waste your time!") + resp := goproxy.NewResponse(r, goproxy.ContentTypeText, http.StatusForbidden, "Don't waste your time!") + return req, resp } - return r,nil + return req,nil }) ``` -`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 reject the request, and -return a pre-canned text response saying "do not waste your time". - -See additional examples in the examples directory. +`DstHostIs` returns a `ReqCondition`, which is a function receiving a `*http.Request` +and returning a boolean that checks if the request satisfies the condition (and that will be processed). +`DstHostIs("www.reddit.com")` will return a `ReqCondition` that returns true +when the request is directed to "www.reddit.com". +The host equality check is `case-insensitive`, to reflect the behaviour of DNS +resolvers, so even if the user types "www.rEdDit.com", the comparison will +satisfy the condition. +When the hour is between 8:00am and 5:59pm, we directly return +a response in `DoFunc()`, so the remote destination will not receive the +request and the client will receive the `"Don't waste your time!"` response. + +### Let's start +```go +import "github.com/elazarl/goproxy" +``` +There are some proxy usage examples in the `examples` folder, which +cover the most common cases. Take a look at them and good luck! -# Type of handlers for manipulating connect/req/resp behavior +## Request & Response manipulation -There are 3 kinds of useful handlers to manipulate the behavior, as follows: +There are 3 different types of handlers to manipulate the behavior of the proxy, as follows: ```go -// handler called after receiving HTTP CONNECT from the client, and before proxy establish connection -// with destination host +// handler called after receiving HTTP CONNECT from the client, and +// before proxy establishes connection with the destination host httpsHandlers []HttpsHandler - -// handler called before proxy send HTTP request to destination host + +// handler called before proxy sends 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. + +// handler called after proxy receives HTTP Response from destination host, +// and before proxy forwards the Response to the client respHandlers []RespHandler ``` -Depending on what you want to manipulate, the ways to add handlers to each handler list are: +Depending on what you want to manipulate, the ways to add handlers to each of the previous lists are: ```go // Add handlers to httpsHandlers -proxy.OnRequest(Some ReqConditions).HandleConnect(YourHandlerFunc()) +proxy.OnRequest(some ReqConditions).HandleConnect(YourHandlerFunc()) // Add handlers to reqHandlers -proxy.OnRequest(Some ReqConditions).Do(YourReqHandlerFunc()) +proxy.OnRequest(some ReqConditions).Do(YourReqHandlerFunc()) // Add handlers to respHandlers -proxy.OnResponse(Some RespConditions).Do(YourRespHandlerFunc()) +proxy.OnResponse(some RespConditions).Do(YourRespHandlerFunc()) ``` -For example: +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. +// This rejects the HTTPS request to *.reddit.com during HTTP CONNECT phase. +// Reddit URL check is case-insensitive because of (?i), so the block will work also if the user types something like rEdDit.com. +proxy.OnRequest(goproxy.ReqHostMatches(regexp.MustCompile("(?i)reddit.*:443$"))).HandleConnect(goproxy.AlwaysReject) + +// Be careful about this example! It shows you a common error that you +// need to avoid. +// This will NOT reject the HTTPS request with URL ending with .gif because, +// if the scheme is HTTPS, the proxy will receive only URL.Hostname +// and URL.Port during the HTTP CONNECT phase. proxy.OnRequest(goproxy.UrlMatches(regexp.MustCompile(`.*gif$`))).HandleConnect(goproxy.AlwaysReject) -// The correct way to manipulate the HTTP request using URL.Path as condition is: +// To fix the previous example, here there is the correct way to manipulate +// an HTTP request using URL.Path (target path) as a condition. proxy.OnRequest(goproxy.UrlMatches(regexp.MustCompile(`.*gif$`))).Do(YourReqHandlerFunc()) ``` -# What's New - -1. Ability to `Hijack` CONNECT requests. See -[the eavesdropper example](https://github.com/elazarl/goproxy/blob/master/examples/goproxy-eavesdropper/main.go#L27) -2. Transparent proxy support for http/https including MITM certificate generation for TLS. See the [transparent example.](https://github.com/elazarl/goproxy/tree/master/examples/goproxy-transparent) - -# 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. +## Error handling +### Generic error +If an error occurs while handling a request through the proxy, by default +the proxy returns HTTP error `500` (Internal Server Error) with the `error +message` as the `body` content. -At any rate, user feedback is very important for me, so I'll be delighted to know if you're using this package. +If you want to override this behaviour, you can define your own +`RespHandler` that changes the error response. +Among the context parameters, `ctx.Error` contains the `error` occurred, +if any, or the `nil` value, if no error happened. -# Beta Software - -I've received positive feedback from a few people who use goproxy in production settings. -I believe it is good enough for usage. +You can handle it as you wish, including returning a custom JSON as the body. +Example of an error handler: +``` +proxy.OnResponse().DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { + var dnsError *net.DNSError + if errors.As(ctx.Error, &dnsError) { + // Do not leak our DNS server's address + dnsError.Server = "" + return goproxy.NewResponse(ctx.Req, goproxy.ContentTypeText, http.StatusBadGateway, dnsError.Error()) + } + return resp +}) +``` -I'll try to keep reasonable backwards compatibility. In case of a major API change, -I'll change the import path. +### Connection error +If an error occurs while sending data to the target remote server (or to +the proxy client), the `proxy.ConnectionErrHandler` is called to handle the +error, if present, else a `default handler` will be used. +The error is passed as `function parameter` and not inside the proxy context, +so you don't have to check the ctx.Error field in this handler. + +In this handler you have access to the raw connection with the proxy +client (as an `io.Writer`), so you could send any HTTP data over it, +if needed, containing the error data. +There is no guarantee that the connection hasn't already been closed, so +the `Write()` could return an error. + +The `connection` will be `automatically closed` by the proxy library after the +error handler call, so you don't have to worry about it. + +## Project Status +This project has been created `10 years` ago, and has reached a stage of +`maturity`. It can be safely used in `production`, and many projects +already do that. + +If there will be any `breaking change` in the future, a `new version` of the +Go module will be released (e.g. v2). + +## Trusted, as a direct dependency, by: +

+ Stripe + Dependabot + Go Git + Google + Grafana + Fly.io + Kubernetes / Minikube + New Relic +

diff --git a/vendor/github.com/elazarl/goproxy/actions.go b/vendor/github.com/elazarl/goproxy/actions.go index e1a3e7f..94eb90c 100644 --- a/vendor/github.com/elazarl/goproxy/actions.go +++ b/vendor/github.com/elazarl/goproxy/actions.go @@ -11,10 +11,10 @@ type ReqHandler interface { Handle(req *http.Request, ctx *ProxyCtx) (*http.Request, *http.Response) } -// A wrapper that would convert a function to a ReqHandler interface type +// A wrapper that would convert a function to a ReqHandler interface type. type FuncReqHandler func(req *http.Request, ctx *ProxyCtx) (*http.Request, *http.Response) -// FuncReqHandler.Handle(req,ctx) <=> FuncReqHandler(req,ctx) +// FuncReqHandler.Handle(req,ctx) <=> FuncReqHandler(req,ctx). func (f FuncReqHandler) Handle(req *http.Request, ctx *ProxyCtx) (*http.Request, *http.Response) { return f(req, ctx) } @@ -22,15 +22,15 @@ func (f FuncReqHandler) Handle(req *http.Request, ctx *ProxyCtx) (*http.Request, // after the proxy have sent the request to the destination server, it will // "filter" the response through the RespHandlers it has. // The proxy server will send to the client the response returned by the RespHandler. -// In case of error, resp will be nil, and ctx.RoundTrip.Error will contain the error +// In case of error, resp will be nil, and ctx.RoundTrip.Error will contain the error. type RespHandler interface { Handle(resp *http.Response, ctx *ProxyCtx) *http.Response } -// A wrapper that would convert a function to a RespHandler interface type +// A wrapper that would convert a function to a RespHandler interface type. type FuncRespHandler func(resp *http.Response, ctx *ProxyCtx) *http.Response -// FuncRespHandler.Handle(req,ctx) <=> FuncRespHandler(req,ctx) +// FuncRespHandler.Handle(req,ctx) <=> FuncRespHandler(req,ctx). func (f FuncRespHandler) Handle(resp *http.Response, ctx *ProxyCtx) *http.Response { return f(resp, ctx) } @@ -43,15 +43,15 @@ func (f FuncRespHandler) Handle(resp *http.Response, ctx *ProxyCtx) *http.Respon // send back and forth all messages from the server to the client and vice versa. // The request and responses sent in this Man In the Middle channel are filtered // through the usual flow (request and response filtered through the ReqHandlers -// and RespHandlers) +// and RespHandlers). type HttpsHandler interface { HandleConnect(req string, ctx *ProxyCtx) (*ConnectAction, string) } -// A wrapper that would convert a function to a HttpsHandler interface type +// A wrapper that would convert a function to a HttpsHandler interface type. type FuncHttpsHandler func(host string, ctx *ProxyCtx) (*ConnectAction, string) -// FuncHttpsHandler should implement the RespHandler interface +// FuncHttpsHandler should implement the RespHandler interface. func (f FuncHttpsHandler) HandleConnect(host string, ctx *ProxyCtx) (*ConnectAction, string) { return f(host, ctx) } diff --git a/vendor/github.com/elazarl/goproxy/certs.go b/vendor/github.com/elazarl/goproxy/certs.go index 4731971..4a8bdda 100644 --- a/vendor/github.com/elazarl/goproxy/certs.go +++ b/vendor/github.com/elazarl/goproxy/certs.go @@ -5,13 +5,21 @@ import ( "crypto/x509" ) +var GoproxyCa tls.Certificate + func init() { - if goproxyCaErr != nil { - panic("Error parsing builtin CA " + goproxyCaErr.Error()) - } + // When we included the embedded certificate inside this file, we made + // sure that it was valid. + // If there is an error here, this is a really exceptional case that requires + // a panic. It should NEVER happen! var err error + GoproxyCa, err = tls.X509KeyPair(CA_CERT, CA_KEY) + if err != nil { + panic("Error parsing builtin CA: " + err.Error()) + } + if GoproxyCa.Leaf, err = x509.ParseCertificate(GoproxyCa.Certificate[0]); err != nil { - panic("Error parsing builtin CA " + err.Error()) + panic("Error parsing builtin CA leaf: " + err.Error()) } } @@ -107,5 +115,3 @@ pmcjjocD/UCCSuHgbAYNNnO/JdhnSylz1tIg26I+2iLNyeTKIepSNlsBxnkLmqM1 cj/azKBaT04IOMLaN8xfSqitJYSraWMVNgGJM5vfcVaivZnNh0lZBv+qu6YkdM88 4/avCJ8IutT+FcMM+GbGazOm5ALWqUyhrnbLGc4CQMPfe7Il6NxwcrOxT8w= -----END RSA PRIVATE KEY-----`) - -var GoproxyCa, goproxyCaErr = tls.X509KeyPair(CA_CERT, CA_KEY) diff --git a/vendor/github.com/elazarl/goproxy/chunked.go b/vendor/github.com/elazarl/goproxy/chunked.go index 83654f6..8b34b68 100644 --- a/vendor/github.com/elazarl/goproxy/chunked.go +++ b/vendor/github.com/elazarl/goproxy/chunked.go @@ -28,9 +28,8 @@ type chunkedWriter struct { // 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 +// 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 @@ -42,15 +41,14 @@ func (cw *chunkedWriter) Write(data []byte) (n int, err error) { return 0, err } if n, err = cw.Wire.Write(data); err != nil { - return + return n, err } if n != len(data) { err = io.ErrShortWrite - return + return n, err } _, err = io.WriteString(cw.Wire, "\r\n") - - return + return n, err } func (cw *chunkedWriter) Close() error { diff --git a/vendor/github.com/elazarl/goproxy/ctx.go b/vendor/github.com/elazarl/goproxy/ctx.go index b372f7d..4180d14 100644 --- a/vendor/github.com/elazarl/goproxy/ctx.go +++ b/vendor/github.com/elazarl/goproxy/ctx.go @@ -1,9 +1,11 @@ package goproxy import ( + "context" "crypto/tls" + "mime" + "net" "net/http" - "regexp" ) // ProxyCtx is the Proxy context, contains useful information about every request. It is passed to @@ -14,11 +16,14 @@ type ProxyCtx struct { // Will contain the remote server's response (if available. nil if the request wasn't send yet) Resp *http.Response RoundTripper RoundTripper + // Specify a custom connection dialer that will be used only for the current + // request, including WebSocket connection upgrades + Dialer func(ctx context.Context, network string, addr string) (net.Conn, error) // will contain the recent error that occurred while trying to send receive or parse traffic Error error // A handle for the user to keep data in the context, from the call of ReqHandler to the // call of RespHandler - UserData interface{} + UserData any // Will connect a request to a response Session int64 certStore CertStorage @@ -46,8 +51,8 @@ func (ctx *ProxyCtx) RoundTrip(req *http.Request) (*http.Response, error) { 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...)...) +func (ctx *ProxyCtx) printf(msg string, argv ...any) { + ctx.Proxy.Logger.Printf("[%03d] "+msg+"\n", append([]any{ctx.Session & 0xFFFF}, argv...)...) } // Logf prints a message to the proxy's log. Should be used in a ProxyHttpServer's filter @@ -58,7 +63,7 @@ func (ctx *ProxyCtx) printf(msg string, argv ...interface{}) { // ctx.Printf("So far %d requests",nr) // return r, nil // }) -func (ctx *ProxyCtx) Logf(msg string, argv ...interface{}) { +func (ctx *ProxyCtx) Logf(msg string, argv ...any) { if ctx.Proxy.Verbose { ctx.printf("INFO: "+msg, argv...) } @@ -75,19 +80,19 @@ func (ctx *ProxyCtx) Logf(msg string, argv ...interface{}) { // } // return r, nil // }) -func (ctx *ProxyCtx) Warnf(msg string, argv ...interface{}) { +func (ctx *ProxyCtx) Warnf(msg string, argv ...any) { ctx.printf("WARN: "+msg, argv...) } -var charsetFinder = regexp.MustCompile("charset=([^ ;]*)") - // Will try to infer the character set of the request from the headers. // Returns the empty string if we don't know which character set it used. // Currently it will look for charset= in the Content-Type header of the request. func (ctx *ProxyCtx) Charset() string { - charsets := charsetFinder.FindStringSubmatch(ctx.Resp.Header.Get("Content-Type")) - if charsets == nil { - return "" + contentType := ctx.Resp.Header.Get("Content-Type") + if _, params, err := mime.ParseMediaType(contentType); err == nil { + if cs, ok := params["charset"]; ok { + return cs + } } - return charsets[1] + return "" } diff --git a/vendor/github.com/elazarl/goproxy/dispatcher.go b/vendor/github.com/elazarl/goproxy/dispatcher.go index 25c949c..bc15c26 100644 --- a/vendor/github.com/elazarl/goproxy/dispatcher.go +++ b/vendor/github.com/elazarl/goproxy/dispatcher.go @@ -2,7 +2,7 @@ package goproxy import ( "bytes" - "io/ioutil" + "io" "net" "net/http" "regexp" @@ -10,7 +10,7 @@ import ( ) // ReqCondition.HandleReq will decide whether or not to use the ReqHandler on an HTTP request -// before sending it to the remote server +// before sending it to the remote server. type ReqCondition interface { RespCondition HandleReq(req *http.Request, ctx *ProxyCtx) bool @@ -23,10 +23,10 @@ type RespCondition interface { HandleResp(resp *http.Response, ctx *ProxyCtx) bool } -// ReqConditionFunc.HandleReq(req,ctx) <=> ReqConditionFunc(req,ctx) +// ReqConditionFunc.HandleReq(req,ctx) <=> ReqConditionFunc(req,ctx). type ReqConditionFunc func(req *http.Request, ctx *ProxyCtx) bool -// RespConditionFunc.HandleResp(resp,ctx) <=> RespConditionFunc(resp,ctx) +// RespConditionFunc.HandleResp(resp,ctx) <=> RespConditionFunc(resp,ctx). type RespConditionFunc func(resp *http.Response, ctx *ProxyCtx) bool func (c ReqConditionFunc) HandleReq(req *http.Request, ctx *ProxyCtx) bool { @@ -49,9 +49,17 @@ func (c RespConditionFunc) HandleResp(resp *http.Response, ctx *ProxyCtx) bool { // requests to url 'http://host/x' func UrlHasPrefix(prefix string) ReqConditionFunc { return func(req *http.Request, ctx *ProxyCtx) bool { + // Make sure to include the / as the first path character when we do a match + // using the host + relativePath := req.URL.Path + if length := len(relativePath); length == 0 || (length > 0 && relativePath[0] != '/') { + relativePath = "/" + relativePath + } + // We use the original value to distinguish between "" and "/" in the user specified string return strings.HasPrefix(req.URL.Path, prefix) || - strings.HasPrefix(req.URL.Host+req.URL.Path, prefix) || - strings.HasPrefix(req.URL.Scheme+req.URL.Host+req.URL.Path, prefix) + strings.HasPrefix(req.URL.Host+relativePath, prefix) || + // Scheme value is something like "https", we must include the :// characters + strings.HasPrefix(req.URL.Scheme+"://"+req.URL.Host+relativePath, prefix) } } @@ -85,7 +93,7 @@ func ReqHostMatches(regexps ...*regexp.Regexp) ReqConditionFunc { } // ReqHostIs returns a ReqCondition, testing whether the host to which the request is directed to equal -// to one of the given strings +// to one of the given strings. func ReqHostIs(hosts ...string) ReqConditionFunc { hostSet := make(map[string]bool) for _, h := range hosts { @@ -97,19 +105,26 @@ func ReqHostIs(hosts ...string) ReqConditionFunc { } } -var localHostIpv4 = regexp.MustCompile(`127\.0\.0\.\d+`) - -// IsLocalHost checks whether the destination host is explicitly local host -// (buggy, there can be IPv6 addresses it doesn't catch) +// IsLocalHost checks whether the destination host is localhost. var IsLocalHost ReqConditionFunc = func(req *http.Request, ctx *ProxyCtx) bool { - return req.URL.Host == "::1" || - req.URL.Host == "0:0:0:0:0:0:0:1" || - localHostIpv4.MatchString(req.URL.Host) || - req.URL.Host == "localhost" + h := req.URL.Hostname() + if h == "localhost" { + return true + } + if ip := net.ParseIP(h); ip != nil { + return ip.IsLoopback() + } + + // In case of IPv6 without a port number Hostname() sometimes returns the invalid value. + if ip := net.ParseIP(req.URL.Host); ip != nil { + return ip.IsLoopback() + } + + return false } // UrlMatches returns a ReqCondition testing whether the destination URL -// of the request matches the given regexp, with or without prefix +// of the request matches the given regexp, with or without prefix. func UrlMatches(re *regexp.Regexp) ReqConditionFunc { return func(req *http.Request, ctx *ProxyCtx) bool { return re.MatchString(req.URL.Path) || @@ -117,14 +132,32 @@ 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 wether 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) + var port string + + // Check if the user specified a custom port that we need to match + if strings.Contains(host, ":") { + hostOnly, portOnly, err := net.SplitHostPort(host) + if err == nil { + host = hostOnly + port = portOnly + } + } + return func(req *http.Request, ctx *ProxyCtx) bool { - return req.URL.Host == host + // Check port matching only if it was specified + if port != "" && port != req.URL.Port() { + return false + } + + return strings.ToLower(req.URL.Hostname()) == host } } -// SrcIpIs returns a ReqCondition testing whether the source IP of the request is one of the given strings +// SrcIpIs returns a ReqCondition testing whether the source IP of the request is one of the given strings. func SrcIpIs(ips ...string) ReqCondition { return ReqConditionFunc(func(req *http.Request, ctx *ProxyCtx) bool { for _, ip := range ips { @@ -136,7 +169,7 @@ func SrcIpIs(ips ...string) ReqCondition { }) } -// Not returns a ReqCondition negating the given ReqCondition +// Not returns a ReqCondition negating the given ReqCondition. func Not(r ReqCondition) ReqConditionFunc { return func(req *http.Request, ctx *ProxyCtx) bool { return !r.HandleReq(req, ctx) @@ -162,7 +195,7 @@ 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 +// code is one of the given ints. func StatusCodeIs(codes ...int) RespCondition { codeSet := make(map[int]bool) for _, c := range codes { @@ -181,19 +214,21 @@ func StatusCodeIs(codes ...int) RespCondition { // You will use the ReqProxyConds struct to register a ReqHandler, that would filter // the request, only if all the given ReqCondition matched. // Typical usage: +// // proxy.OnRequest(UrlIs("example.com/foo"),UrlMatches(regexp.MustParse(`.*\.exampl.\com\./.*`)).Do(...) func (proxy *ProxyHttpServer) OnRequest(conds ...ReqCondition) *ReqProxyConds { return &ReqProxyConds{proxy, conds} } -// ReqProxyConds aggregate ReqConditions for a ProxyHttpServer. Upon calling Do, it will register a ReqHandler that would +// ReqProxyConds aggregate ReqConditions for a ProxyHttpServer. +// Upon calling Do, it will register a ReqHandler that would // handle the request if all conditions on the HTTP request are met. type ReqProxyConds struct { proxy *ProxyHttpServer reqConds []ReqCondition } -// DoFunc is equivalent to proxy.OnRequest().Do(FuncReqHandler(f)) +// DoFunc is equivalent to proxy.OnRequest().Do(FuncReqHandler(f)). func (pcond *ReqProxyConds) DoFunc(f func(req *http.Request, ctx *ProxyCtx) (*http.Request, *http.Response)) { pcond.Do(FuncReqHandler(f)) } @@ -201,6 +236,7 @@ func (pcond *ReqProxyConds) DoFunc(f func(req *http.Request, ctx *ProxyCtx) (*ht // ReqProxyConds.Do will register the ReqHandler on the proxy, // the ReqHandler will handle the HTTP request if all the conditions // aggregated in the ReqProxyConds are met. Typical usage: +// // proxy.OnRequest().Do(handler) // will call handler.Handle(req,ctx) on every request to the proxy // proxy.OnRequest(cond1,cond2).Do(handler) // // given request to the proxy, will test if cond1.HandleReq(req,ctx) && cond2.HandleReq(req,ctx) are true @@ -227,6 +263,7 @@ func (pcond *ReqProxyConds) Do(h ReqHandler) { // connection. // The ConnectAction struct contains possible tlsConfig that will be used for eavesdropping. If nil, the proxy // will use the default tls configuration. +// // proxy.OnRequest().HandleConnect(goproxy.AlwaysReject) // rejects all CONNECT requests func (pcond *ReqProxyConds) HandleConnect(h HttpsHandler) { pcond.proxy.httpsHandlers = append(pcond.proxy.httpsHandlers, @@ -242,6 +279,7 @@ func (pcond *ReqProxyConds) HandleConnect(h HttpsHandler) { // HandleConnectFunc is equivalent to HandleConnect, // for example, accepting CONNECT request if they contain a password in header +// // io.WriteString(h,password) // passHash := h.Sum(nil) // proxy.OnRequest().HandleConnectFunc(func(host string, ctx *ProxyCtx) (*ConnectAction, string) { @@ -277,7 +315,7 @@ type ProxyConds struct { respCond []RespCondition } -// ProxyConds.DoFunc is equivalent to proxy.OnResponse().Do(FuncRespHandler(f)) +// ProxyConds.DoFunc is equivalent to proxy.OnResponse().Do(FuncRespHandler(f)). func (pcond *ProxyConds) DoFunc(f func(resp *http.Response, ctx *ProxyCtx) *http.Response) { pcond.Do(FuncRespHandler(f)) } @@ -302,6 +340,7 @@ func (pcond *ProxyConds) Do(h RespHandler) { } // OnResponse is used when adding a response-filter to the HTTP proxy, usual pattern is +// // proxy.OnResponse(cond1,cond2).Do(handler) // handler.Handle(resp,ctx) will be used // // if cond1.HandleResp(resp) && cond2.HandleResp(resp) func (proxy *ProxyHttpServer) OnResponse(conds ...RespCondition) *ProxyConds { @@ -310,6 +349,7 @@ func (proxy *ProxyHttpServer) OnResponse(conds ...RespCondition) *ProxyConds { // AlwaysMitm is a HttpsHandler that always eavesdrop https connections, for example to // eavesdrop all https connections to www.google.com, we can use +// // proxy.OnRequest(goproxy.ReqHostIs("www.google.com")).HandleConnect(goproxy.AlwaysMitm) var AlwaysMitm FuncHttpsHandler = func(host string, ctx *ProxyCtx) (*ConnectAction, string) { return MitmConnect, host @@ -317,6 +357,7 @@ var AlwaysMitm FuncHttpsHandler = func(host string, ctx *ProxyCtx) (*ConnectActi // AlwaysReject is a HttpsHandler that drops any CONNECT request, for example, this code will disallow // connections to hosts on any other port than 443 +// // proxy.OnRequest(goproxy.Not(goproxy.ReqHostMatches(regexp.MustCompile(":443$"))). // HandleConnect(goproxy.AlwaysReject) var AlwaysReject FuncHttpsHandler = func(host string, ctx *ProxyCtx) (*ConnectAction, string) { @@ -328,14 +369,14 @@ var AlwaysReject FuncHttpsHandler = func(host string, ctx *ProxyCtx) (*ConnectAc // and will replace the body of the original response with the resulting byte array. func HandleBytes(f func(b []byte, ctx *ProxyCtx) []byte) RespHandler { return FuncRespHandler(func(resp *http.Response, ctx *ProxyCtx) *http.Response { - b, err := ioutil.ReadAll(resp.Body) + b, err := io.ReadAll(resp.Body) if err != nil { ctx.Warnf("Cannot read response %s", err) return resp } resp.Body.Close() - resp.Body = ioutil.NopCloser(bytes.NewBuffer(f(b, ctx))) + resp.Body = io.NopCloser(bytes.NewBuffer(f(b, ctx))) return resp }) } diff --git a/vendor/github.com/elazarl/goproxy/doc.go b/vendor/github.com/elazarl/goproxy/doc.go index 6f44317..1ba20bf 100644 --- a/vendor/github.com/elazarl/goproxy/doc.go +++ b/vendor/github.com/elazarl/goproxy/doc.go @@ -23,7 +23,7 @@ Adding a header to each request return r, nil }) -Note that the function is called before the proxy sends the request to the server +> Note that the function is called before the proxy sends the request to the server For printing the content type of all incoming responses @@ -60,7 +60,9 @@ 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.NewResponse(ctx.Req, goproxy.ContentTypeText, http.StatusForbidden, "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 response, and return a new 403 response with a short message. @@ -95,6 +97,5 @@ Will warn if multiple versions of jquery are used in the same domain. 6. https://github.com/elazarl/goproxy/blob/master/examples/goproxy-upside-down-ternet/ Modifies image files in an HTTP response via goproxy's image extension found in ext/. - */ package goproxy diff --git a/vendor/github.com/elazarl/goproxy/h2.go b/vendor/github.com/elazarl/goproxy/h2.go index 7c0f357..6d50948 100644 --- a/vendor/github.com/elazarl/goproxy/h2.go +++ b/vendor/github.com/elazarl/goproxy/h2.go @@ -12,6 +12,8 @@ import ( "golang.org/x/net/http2" ) +var ErrInvalidH2Frame = errors.New("invalid H2 frame") + // H2Transport is an implementation of RoundTripper that abstracts an entire // HTTP/2 session, sending all client frames to the server and responses back // to the client. @@ -25,10 +27,10 @@ type H2Transport struct { // RoundTrip executes an HTTP/2 session (including all contained streams). // The request and response are ignored but any error encountered during the // proxying from the session is returned as a result of the invocation. -func (r *H2Transport) RoundTrip(prefaceReq *http.Request) (*http.Response, error) { +func (r *H2Transport) RoundTrip(_ *http.Request) (*http.Response, error) { raddr := r.Host if !strings.Contains(raddr, ":") { - raddr = raddr + ":443" + raddr += ":443" } rawServerTLS, err := dial("tcp", raddr) if err != nil { @@ -39,11 +41,15 @@ func (r *H2Transport) RoundTrip(prefaceReq *http.Request) (*http.Response, error r.TLSConfig.NextProtos = []string{http2.NextProtoTLS} // Initiate TLS and check remote host name against certificate. rawServerTLS = tls.Client(rawServerTLS, r.TLSConfig) - if err = rawServerTLS.(*tls.Conn).Handshake(); err != nil { + rawTLSConn, ok := rawServerTLS.(*tls.Conn) + if !ok { + return nil, errors.New("invalid TLS connection") + } + if err = rawTLSConn.Handshake(); err != nil { return nil, err } if r.TLSConfig == nil || !r.TLSConfig.InsecureSkipVerify { - if err = rawServerTLS.(*tls.Conn).VerifyHostname(raddr[:strings.LastIndex(raddr, ":")]); err != nil { + if err = rawTLSConn.VerifyHostname(raddr[:strings.LastIndex(raddr, ":")]); err != nil { return nil, err } } @@ -75,11 +81,11 @@ func (r *H2Transport) RoundTrip(prefaceReq *http.Request) (*http.Response, error for i := 0; i < 2; i++ { select { case err := <-errSToC: - if err != io.EOF { + if !errors.Is(err, io.EOF) { return nil, err } case err := <-errCToS: - if err != io.EOF { + if !errors.Is(err, io.EOF) { return nil, err } } @@ -105,14 +111,20 @@ func proxyFrame(fr *http2.Framer) error { } switch f.Header().Type { case http2.FrameData: - tf := f.(*http2.DataFrame) + tf, ok := f.(*http2.DataFrame) + if !ok { + return ErrInvalidH2Frame + } terr := fr.WriteData(tf.StreamID, tf.StreamEnded(), tf.Data()) if terr == nil && tf.StreamEnded() { terr = io.EOF } return terr case http2.FrameHeaders: - tf := f.(*http2.HeadersFrame) + tf, ok := f.(*http2.HeadersFrame) + if !ok { + return ErrInvalidH2Frame + } terr := fr.WriteHeaders(http2.HeadersFrameParam{ StreamID: tf.StreamID, BlockFragment: tf.HeaderBlockFragment(), @@ -126,19 +138,34 @@ func proxyFrame(fr *http2.Framer) error { } return terr case http2.FrameContinuation: - tf := f.(*http2.ContinuationFrame) + tf, ok := f.(*http2.ContinuationFrame) + if !ok { + return ErrInvalidH2Frame + } return fr.WriteContinuation(tf.StreamID, tf.HeadersEnded(), tf.HeaderBlockFragment()) case http2.FrameGoAway: - tf := f.(*http2.GoAwayFrame) + tf, ok := f.(*http2.GoAwayFrame) + if !ok { + return ErrInvalidH2Frame + } return fr.WriteGoAway(tf.StreamID, tf.ErrCode, tf.DebugData()) case http2.FramePing: - tf := f.(*http2.PingFrame) + tf, ok := f.(*http2.PingFrame) + if !ok { + return ErrInvalidH2Frame + } return fr.WritePing(tf.IsAck(), tf.Data) case http2.FrameRSTStream: - tf := f.(*http2.RSTStreamFrame) + tf, ok := f.(*http2.RSTStreamFrame) + if !ok { + return ErrInvalidH2Frame + } return fr.WriteRSTStream(tf.StreamID, tf.ErrCode) case http2.FrameSettings: - tf := f.(*http2.SettingsFrame) + tf, ok := f.(*http2.SettingsFrame) + if !ok { + return ErrInvalidH2Frame + } if tf.IsAck() { return fr.WriteSettingsAck() } @@ -151,13 +178,22 @@ func proxyFrame(fr *http2.Framer) error { } return fr.WriteSettings(settings...) case http2.FrameWindowUpdate: - tf := f.(*http2.WindowUpdateFrame) + tf, ok := f.(*http2.WindowUpdateFrame) + if !ok { + return ErrInvalidH2Frame + } return fr.WriteWindowUpdate(tf.StreamID, tf.Increment) case http2.FramePriority: - tf := f.(*http2.PriorityFrame) + tf, ok := f.(*http2.PriorityFrame) + if !ok { + return ErrInvalidH2Frame + } return fr.WritePriority(tf.StreamID, tf.PriorityParam) case http2.FramePushPromise: - tf := f.(*http2.PushPromiseFrame) + tf, ok := f.(*http2.PushPromiseFrame) + if !ok { + return ErrInvalidH2Frame + } return fr.WritePushPromise(http2.PushPromiseParam{ StreamID: tf.StreamID, PromiseID: tf.PromiseID, diff --git a/vendor/github.com/elazarl/goproxy/http.go b/vendor/github.com/elazarl/goproxy/http.go new file mode 100644 index 0000000..985dc44 --- /dev/null +++ b/vendor/github.com/elazarl/goproxy/http.go @@ -0,0 +1,97 @@ +package goproxy + +import ( + "io" + "net/http" + "strings" + "sync/atomic" +) + +func (proxy *ProxyHttpServer) handleHttp(w http.ResponseWriter, r *http.Request) { + ctx := &ProxyCtx{Req: r, Session: atomic.AddInt64(&proxy.sess, 1), Proxy: proxy} + + 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) + return + } + r, resp := proxy.filterRequest(r, ctx) + + if resp == nil { + if !proxy.KeepHeader { + RemoveProxyHeaders(ctx, r) + } + + var err error + resp, err = ctx.RoundTrip(r) + if err != nil { + ctx.Error = err + } + } + + var origBody io.ReadCloser + + if resp != nil { + origBody = resp.Body + defer origBody.Close() + } + + resp = proxy.filterResponse(resp, ctx) + + 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(), http.StatusInternalServerError) + } else { + errorString = "error read response " + r.URL.Host + ctx.Logf(errorString) + http.Error(w, errorString, http.StatusInternalServerError) + } + 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 + // body the user returned. + // We keep the original body to remove the header only if things changed. + // This will prevent problems with HEAD requests where there's no body, yet, + // the Content-Length header should be set. + if origBody != resp.Body { + resp.Header.Del("Content-Length") + } + 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. + if strings.HasPrefix(w.Header().Get("content-type"), "text/event-stream") || + strings.Contains(w.Header().Get("transfer-encoding"), "chunked") { + // 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) + } + ctx.Logf("Copied %v bytes to client error=%v", nr, err) +} diff --git a/vendor/github.com/elazarl/goproxy/https.go b/vendor/github.com/elazarl/goproxy/https.go index 271b55c..016e513 100644 --- a/vendor/github.com/elazarl/goproxy/https.go +++ b/vendor/github.com/elazarl/goproxy/https.go @@ -2,20 +2,22 @@ package goproxy import ( "bufio" + "context" "crypto/tls" "errors" "fmt" "io" - "io/ioutil" "net" "net/http" "net/url" "os" - "regexp" "strconv" "strings" "sync" "sync/atomic" + + "github.com/elazarl/goproxy/internal/http1parser" + "github.com/elazarl/goproxy/internal/signer" ) type ConnectActionLiteral int @@ -34,13 +36,14 @@ var ( MitmConnect = &ConnectAction{Action: ConnectMitm, TLSConfig: TLSConfigFromCA(&GoproxyCa)} HTTPMitmConnect = &ConnectAction{Action: ConnectHTTPMitm, TLSConfig: TLSConfigFromCA(&GoproxyCa)} RejectConnect = &ConnectAction{Action: ConnectReject, TLSConfig: TLSConfigFromCA(&GoproxyCa)} - httpsRegexp = regexp.MustCompile(`^https:\/\/`) ) +var _errorRespMaxLength int64 = 500 + // 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 +// Hijack func. type ConnectAction struct { Action ConnectActionLiteral Hijack func(req *http.Request, client net.Conn, ctx *ProxyCtx) @@ -50,9 +53,8 @@ type ConnectAction struct { func stripPort(s string) string { var ix int if strings.Contains(s, "[") && strings.Contains(s, "]") { - //ipv6 : for example : [2606:4700:4700::1111]:443 - - //strip '[' and ']' + // ipv6 address example: [2606:4700:4700::1111]:443 + // strip '[' and ']' s = strings.ReplaceAll(s, "[", "") s = strings.ReplaceAll(s, "]", "") @@ -61,26 +63,32 @@ func stripPort(s string) string { return s } } else { - //ipv4 + // ipv4 ix = strings.IndexRune(s, ':') if ix == -1 { return s } - } return s[:ix] } -func (proxy *ProxyHttpServer) dial(network, addr string) (c net.Conn, err error) { - if proxy.Tr.Dial != nil { - return proxy.Tr.Dial(network, addr) +func (proxy *ProxyHttpServer) dial(ctx *ProxyCtx, network, addr string) (c net.Conn, err error) { + if ctx.Dialer != nil { + return ctx.Dialer(ctx.Req.Context(), network, addr) } + + if proxy.Tr != nil && proxy.Tr.DialContext != nil { + return proxy.Tr.DialContext(ctx.Req.Context(), network, addr) + } + + // if the user didn't specify any dialer, we just use the default one, + // provided by net package return net.Dial(network, addr) } 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) + return proxy.dial(ctx, network, addr) } if proxy.ConnectDialWithReq != nil { @@ -135,67 +143,129 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request return } ctx.Logf("Accepting CONNECT to %s", host) - proxyClient.Write([]byte("HTTP/1.0 200 Connection established\r\n\r\n")) + _, _ = proxyClient.Write([]byte("HTTP/1.0 200 Connection established\r\n\r\n")) targetTCP, targetOK := targetSiteCon.(halfClosable) proxyClientTCP, clientOK := proxyClient.(halfClosable) if targetOK && clientOK { - go copyAndClose(ctx, targetTCP, proxyClientTCP) - go copyAndClose(ctx, proxyClientTCP, targetTCP) - } else { go func() { var wg sync.WaitGroup wg.Add(2) - go copyOrWarn(ctx, targetSiteCon, proxyClient, &wg) - go copyOrWarn(ctx, proxyClient, targetSiteCon, &wg) + go copyAndClose(ctx, targetTCP, proxyClientTCP, &wg) + go copyAndClose(ctx, proxyClientTCP, targetTCP, &wg) wg.Wait() - proxyClient.Close() - targetSiteCon.Close() + // Make sure to close the underlying TCP socket. + // CloseRead() and CloseWrite() keep it open until its timeout, + // causing error when there are thousands of requests. + proxyClientTCP.Close() + targetTCP.Close() + }() + } else { + // There is a race with the runtime here. In the case where the + // connection to the target site times out, we cannot control which + // io.Copy loop will receive the timeout signal first. This means + // that in some cases the error passed to the ConnErrorHandler will + // be the timeout error, and in other cases it will be an error raised + // by the use of a closed network connection. + // + // 2020/05/28 23:42:17 [001] WARN: Error copying to client: read tcp 127.0.0.1:33742->127.0.0.1:34763: i/o timeout + // 2020/05/28 23:42:17 [001] WARN: Error copying to client: read tcp 127.0.0.1:45145->127.0.0.1:60494: use of closed + // network connection + // + // It's also not possible to synchronize these connection closures due to + // TCP connections which are half-closed. When this happens, only the one + // side of the connection breaks out of its io.Copy loop. The other side + // of the connection remains open until it either times out or is reset by + // the client. + go func() { + err := copyOrWarn(ctx, targetSiteCon, proxyClient) + if err != nil && proxy.ConnectionErrHandler != nil { + proxy.ConnectionErrHandler(proxyClient, ctx, err) + } + _ = targetSiteCon.Close() + }() + go func() { + _ = copyOrWarn(ctx, proxyClient, targetSiteCon) + _ = proxyClient.Close() }() } case ConnectHijack: todo.Hijack(r, proxyClient, ctx) case ConnectHTTPMitm: - proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) + _, _ = 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(ctx, "tcp", host) - if err != nil { - ctx.Warnf("Error dialing to %s: %s", host, err.Error()) - return - } - for { - client := bufio.NewReader(proxyClient) - remote := bufio.NewReader(targetSiteCon) - req, err := http.ReadRequest(client) - if err != nil && err != io.EOF { + + 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 } - req, resp := proxy.filterRequest(req, ctx) - if resp == nil { - if err := req.Write(targetSiteCon); err != nil { - httpError(proxyClient, ctx, err) - 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) + } + + 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) + }() + if err != nil { + httpError(proxyClient, ctx, err) + return false + } } - resp, err = http.ReadResponse(remote, req) + resp = proxy.filterResponse(resp, ctx) + defer resp.Body.Close() + + err = resp.Write(proxyClient) if err != nil { httpError(proxyClient, ctx, err) - return + return false } - defer resp.Body.Close() - } - resp = proxy.filterResponse(resp, ctx) - if err := resp.Write(proxyClient); err != nil { - httpError(proxyClient, ctx, err) - return + + return true + }(req); !requestOk { + break } } case ConnectMitm: - proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) + _, _ = 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 @@ -211,142 +281,190 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request } } go func() { - //TODO: cache connections to the remote website + // 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 := bufio.NewReader(rawClientTls) - for !isEof(clientTlsReader) { - req, err := http.ReadRequest(clientTlsReader) - var ctx = &ProxyCtx{Req: req, Session: atomic.AddInt64(&proxy.sess, 1), Proxy: proxy, UserData: ctx.UserData} - if err != nil && err != io.EOF { - return + + clientTlsReader := http1parser.NewRequestReader(proxy.PreventCanonicalization, rawClientTls) + for !clientTlsReader.IsEOF() { + req, err := clientTlsReader.ReadRequest() + ctx := &ProxyCtx{ + Req: req, + Session: atomic.AddInt64(&proxy.sess, 1), + Proxy: proxy, + UserData: ctx.UserData, + RoundTripper: ctx.RoundTripper, } - if err != nil { + if err != nil && !errors.Is(err, io.EOF) { ctx.Warnf("Cannot read TLS request from mitm'd client %v %v", r.Host, err) + } + if err != nil { return } - req.RemoteAddr = r.RemoteAddr // since we're converting the request, need to carry over the original connecting IP as well + + // 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) - if !httpsRegexp.MatchString(req.URL.String()) { + if !strings.HasPrefix(req.URL.String(), "https://") { req.URL, err = url.Parse("https://" + r.Host + req.URL.String()) } - // Bug fix which goproxy fails to provide request - // information URL in the context when does HTTPS MITM - ctx.Req = req - - req, resp := proxy.filterRequest(req, ctx) - if resp == nil { - if req.Method == "PRI" { - // Handle HTTP/2 connections. - - // NOTE: As of 1.22, golang's http module will not recognize or - // 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. - _, err := clientTlsReader.Discard(6) - if err != nil { - ctx.Warnf("Failed to process HTTP2 client preface: %v", err) - return + 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 + // processing (same behaviour of the stdlib) + requestContext, finishRequest := context.WithCancel(req.Context()) + req = req.WithContext(requestContext) + defer finishRequest() + + // Bug fix which goproxy fails to provide request + // information URL in the context when does HTTPS MITM + ctx.Req = req + + req, resp := proxy.filterRequest(req, ctx) + if resp == nil { + if req.Method == "PRI" { + // Handle HTTP/2 connections. + + // NOTE: As of 1.22, golang's http module will not recognize or + // 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() + _, err := reader.Discard(6) + if err != nil { + ctx.Warnf("Failed to process HTTP2 client preface: %v", err) + return false + } + if !proxy.AllowHTTP2 { + ctx.Warnf("HTTP2 connection failed: disallowed") + return false + } + tr := H2Transport{reader, rawClientTls, tlsConfig.Clone(), host} + if _, err := tr.RoundTrip(req); err != nil { + ctx.Warnf("HTTP2 connection failed: %v", err) + } else { + ctx.Logf("Exiting on EOF") + } + return false } - if !proxy.AllowHTTP2 { - ctx.Warnf("HTTP2 connection failed: disallowed") - return + if err != nil { + 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 false } - tr := H2Transport{clientTlsReader, rawClientTls, tlsConfig.Clone(), host} - if _, err := tr.RoundTrip(req); err != nil { - ctx.Warnf("HTTP2 connection failed: %v", err) - } else { - ctx.Logf("Exiting on EOF") + if !proxy.KeepHeader { + RemoveProxyHeaders(ctx, req) } - return - } - if isWebSocketRequest(req) { - ctx.Logf("Request looks like websocket upgrade.") - proxy.serveWebsocketTLS(ctx, w, req, tlsConfig, rawClientTls) - return - } - if err != nil { - if req.URL != nil { - ctx.Warnf("Illegal URL %s", "https://"+r.Host+req.URL.Path) - } else { - ctx.Warnf("Illegal URL %s", "https://"+r.Host) + 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 false } - return + ctx.Logf("resp %v", resp.Status) } - 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) - }() - if err != nil { - ctx.Warnf("Cannot read TLS response from mitm'd server %v", err) - return + resp = proxy.filterResponse(resp, ctx) + 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 } - ctx.Logf("resp %v", resp.Status) - } - resp = proxy.filterResponse(resp, ctx) - defer resp.Body.Close() - - text := resp.Status - statusCode := strconv.Itoa(resp.StatusCode) + " " - if strings.HasPrefix(text, statusCode) { - text = text[len(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 - } - - 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 { - ctx.Warnf("Cannot write TLS response header from mitm'd client: %v", err) - return - } - if _, err = io.WriteString(rawClientTls, "\r\n"); err != nil { - ctx.Warnf("Cannot write TLS response header end 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 + 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 + resp.Header.Del("Content-Length") + resp.Header.Set("Transfer-Encoding", "chunked") } - if err := chunked.Close(); err != nil { - ctx.Warnf("Cannot write TLS chunked EOF from mitm'd client: %v", err) - return + // 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 chunked trailer from mitm'd client: %v", err) - return + ctx.Warnf("Cannot write TLS response header end from mitm'd client: %v", err) + 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 || + 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 + } } + + return true + }(req); !continueLoop { + return } } ctx.Logf("Exiting on EOF") }() case ConnectProxyAuthHijack: - proxyClient.Write([]byte("HTTP/1.1 407 Proxy Authentication Required\r\n")) + _, _ = proxyClient.Write([]byte("HTTP/1.1 407 Proxy Authentication Required\r\n")) todo.Hijack(r, proxyClient, ctx) case ConnectReject: if ctx.Resp != nil { @@ -354,53 +472,71 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request ctx.Warnf("Cannot write response that reject http CONNECT: %v", err) } } - proxyClient.Close() + _ = proxyClient.Close() } } func httpError(w io.WriteCloser, ctx *ProxyCtx, err error) { - errStr := fmt.Sprintf("HTTP/1.1 502 Bad Gateway\r\nContent-Type: text/plain\r\nContent-Length: %d\r\n\r\n%s", len(err.Error()), err.Error()) - if _, err := io.WriteString(w, errStr); err != nil { - ctx.Warnf("Error responding to client: %s", err) + if ctx.Proxy.ConnectionErrHandler != nil { + ctx.Proxy.ConnectionErrHandler(w, ctx, err) + } else { + errorMessage := err.Error() + errStr := fmt.Sprintf( + "HTTP/1.1 502 Bad Gateway\r\nContent-Type: text/plain\r\nContent-Length: %d\r\n\r\n%s", + len(errorMessage), + errorMessage, + ) + if _, err := io.WriteString(w, errStr); err != nil { + ctx.Warnf("Error responding to client: %s", err) + } } if err := w.Close(); err != nil { ctx.Warnf("Error closing client connection: %s", err) } } -func copyOrWarn(ctx *ProxyCtx, dst io.Writer, src io.Reader, wg *sync.WaitGroup) { - if _, err := io.Copy(dst, src); err != nil { +func copyOrWarn(ctx *ProxyCtx, dst io.Writer, src io.Reader) error { + _, err := io.Copy(dst, src) + if err != nil && errors.Is(err, net.ErrClosed) { + // Discard closed connection errors + err = nil + } else if err != nil { ctx.Warnf("Error copying to client: %s", err) } - wg.Done() + return err } -func copyAndClose(ctx *ProxyCtx, dst, src halfClosable) { - if _, err := io.Copy(dst, src); err != nil { - ctx.Warnf("Error copying to client: %s", err) +func copyAndClose(ctx *ProxyCtx, dst, src halfClosable, wg *sync.WaitGroup) { + _, err := io.Copy(dst, src) + if err != nil && !errors.Is(err, net.ErrClosed) { + ctx.Warnf("Error copying to client: %s", err.Error()) } - dst.CloseWrite() - src.CloseRead() + _ = dst.CloseWrite() + _ = src.CloseRead() + wg.Done() } func dialerFromEnv(proxy *ProxyHttpServer) func(network, addr string) (net.Conn, error) { - https_proxy := os.Getenv("HTTPS_PROXY") - if https_proxy == "" { - https_proxy = os.Getenv("https_proxy") + httpsProxy := os.Getenv("HTTPS_PROXY") + if httpsProxy == "" { + httpsProxy = os.Getenv("https_proxy") } - if https_proxy == "" { + if httpsProxy == "" { return nil } - return proxy.NewConnectDialToProxy(https_proxy) + return proxy.NewConnectDialToProxy(httpsProxy) } -func (proxy *ProxyHttpServer) NewConnectDialToProxy(https_proxy string) func(network, addr string) (net.Conn, error) { - return proxy.NewConnectDialToProxyWithHandler(https_proxy, nil) +func (proxy *ProxyHttpServer) NewConnectDialToProxy(httpsProxy string) func(network, addr string) (net.Conn, error) { + return proxy.NewConnectDialToProxyWithHandler(httpsProxy, 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) +func (proxy *ProxyHttpServer) NewConnectDialToProxyWithHandler( + httpsProxy string, + connectReqHandler func(req *http.Request), +) func(network, addr string) (net.Conn, error) { + u, err := url.Parse(httpsProxy) if err != nil { return nil } @@ -410,7 +546,7 @@ func (proxy *ProxyHttpServer) NewConnectDialToProxyWithHandler(https_proxy strin } return func(network, addr string) (net.Conn, error) { connectReq := &http.Request{ - Method: "CONNECT", + Method: http.MethodConnect, URL: &url.URL{Opaque: addr}, Host: addr, Header: make(http.Header), @@ -418,27 +554,27 @@ func (proxy *ProxyHttpServer) NewConnectDialToProxyWithHandler(https_proxy strin if connectReqHandler != nil { connectReqHandler(connectReq) } - c, err := proxy.dial(network, u.Host) + c, err := proxy.dial(&ProxyCtx{Req: &http.Request{}}, network, u.Host) if err != nil { return nil, err } - connectReq.Write(c) + _ = connectReq.Write(c) // Read response. // Okay to use and discard buffered reader here, because // TLS server will not speak until spoken to. br := bufio.NewReader(c) resp, err := http.ReadResponse(br, connectReq) if err != nil { - c.Close() + _ = c.Close() return nil, err } defer resp.Body.Close() - if resp.StatusCode != 200 { - resp, err := ioutil.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + resp, err := io.ReadAll(io.LimitReader(resp.Body, _errorRespMaxLength)) if err != nil { return nil, err } - c.Close() + _ = c.Close() return nil, errors.New("proxy refused connection" + string(resp)) } return c, nil @@ -449,13 +585,19 @@ func (proxy *ProxyHttpServer) NewConnectDialToProxyWithHandler(https_proxy strin u.Host += ":443" } return func(network, addr string) (net.Conn, error) { - c, err := proxy.dial(network, u.Host) + ctx := &ProxyCtx{Req: &http.Request{}} + c, err := proxy.dial(ctx, network, u.Host) if err != nil { return nil, err } - c = tls.Client(c, proxy.Tr.TLSClientConfig) + + c, err = proxy.initializeTLSconnection(ctx, c, proxy.Tr.TLSClientConfig, u.Host) + if err != nil { + return nil, err + } + connectReq := &http.Request{ - Method: "CONNECT", + Method: http.MethodConnect, URL: &url.URL{Opaque: addr}, Host: addr, Header: make(http.Header), @@ -463,23 +605,23 @@ func (proxy *ProxyHttpServer) NewConnectDialToProxyWithHandler(https_proxy strin if connectReqHandler != nil { connectReqHandler(connectReq) } - connectReq.Write(c) + _ = connectReq.Write(c) // Read response. // Okay to use and discard buffered reader here, because // TLS server will not speak until spoken to. br := bufio.NewReader(c) resp, err := http.ReadResponse(br, connectReq) if err != nil { - c.Close() + _ = c.Close() return nil, err } defer resp.Body.Close() - if resp.StatusCode != 200 { - body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 500)) + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(io.LimitReader(resp.Body, _errorRespMaxLength)) if err != nil { return nil, err } - c.Close() + _ = c.Close() return nil, errors.New("proxy refused connection" + string(body)) } return c, nil @@ -498,7 +640,7 @@ func TLSConfigFromCA(ca *tls.Certificate) func(host string, ctx *ProxyCtx) (*tls ctx.Logf("signing for %s", stripPort(host)) genCert := func() (*tls.Certificate, error) { - return signHost(*ca, []string{hostname}) + return signer.SignHost(*ca, []string{hostname}) } if ctx.certStore != nil { cert, err = ctx.certStore.Fetch(hostname, genCert) @@ -515,3 +657,29 @@ func TLSConfigFromCA(ca *tls.Certificate) func(host string, ctx *ProxyCtx) (*tls return config, nil } } + +func (proxy *ProxyHttpServer) initializeTLSconnection( + ctx *ProxyCtx, + targetConn net.Conn, + tlsConfig *tls.Config, + addr string, +) (net.Conn, error) { + // Infer target ServerName, it's a copy of implementation inside tls.Dial() + if tlsConfig.ServerName == "" { + colonPos := strings.LastIndex(addr, ":") + if colonPos == -1 { + colonPos = len(addr) + } + hostname := addr[:colonPos] + // Make a copy to avoid polluting argument or default. + c := tlsConfig.Clone() + c.ServerName = hostname + tlsConfig = c + } + + tlsConn := tls.Client(targetConn, tlsConfig) + if err := tlsConn.HandshakeContext(ctx.Req.Context()); err != nil { + return nil, err + } + return tlsConn, nil +} diff --git a/vendor/github.com/elazarl/goproxy/internal/http1parser/header.go b/vendor/github.com/elazarl/goproxy/internal/http1parser/header.go new file mode 100644 index 0000000..d4ef3e6 --- /dev/null +++ b/vendor/github.com/elazarl/goproxy/internal/http1parser/header.go @@ -0,0 +1,43 @@ +package http1parser + +import ( + "errors" + "net/textproto" + "strings" +) + +var ErrBadProto = errors.New("bad protocol") + +// Http1ExtractHeaders is an HTTP/1.0 and HTTP/1.1 header-only parser, +// to extract the original header names for the received request. +// Fully inspired by readMIMEHeader() in +// https://github.com/golang/go/blob/master/src/net/textproto/reader.go +func Http1ExtractHeaders(r *textproto.Reader) ([]string, error) { + // Discard first line, it doesn't contain useful information, and it has + // already been validated in http.ReadRequest() + if _, err := r.ReadLine(); err != nil { + return nil, err + } + + // The first line cannot start with a leading space. + if buf, err := r.R.Peek(1); err == nil && (buf[0] == ' ' || buf[0] == '\t') { + return nil, ErrBadProto + } + + var headerNames []string + for { + kv, err := r.ReadContinuedLine() + if len(kv) == 0 { + // We have finished to parse the headers if we receive empty + // data without an error + return headerNames, err + } + + // Key ends at first colon. + k, _, ok := strings.Cut(kv, ":") + if !ok { + return nil, ErrBadProto + } + headerNames = append(headerNames, k) + } +} diff --git a/vendor/github.com/elazarl/goproxy/internal/http1parser/request.go b/vendor/github.com/elazarl/goproxy/internal/http1parser/request.go new file mode 100644 index 0000000..0e37bc2 --- /dev/null +++ b/vendor/github.com/elazarl/goproxy/internal/http1parser/request.go @@ -0,0 +1,94 @@ +package http1parser + +import ( + "bufio" + "bytes" + "errors" + "io" + "net/http" + "net/textproto" +) + +type RequestReader struct { + preventCanonicalization bool + reader *bufio.Reader + // Used only when preventCanonicalization value is true + cloned *bytes.Buffer +} + +func NewRequestReader(preventCanonicalization bool, conn io.Reader) *RequestReader { + if !preventCanonicalization { + return &RequestReader{ + preventCanonicalization: false, + reader: bufio.NewReader(conn), + } + } + + var cloned bytes.Buffer + reader := bufio.NewReader(io.TeeReader(conn, &cloned)) + return &RequestReader{ + preventCanonicalization: true, + reader: reader, + cloned: &cloned, + } +} + +// IsEOF returns true if there is no more data that can be read from the +// buffer and the underlying connection is closed. +func (r *RequestReader) IsEOF() bool { + _, err := r.reader.Peek(1) + return errors.Is(err, io.EOF) +} + +// Reader is used to take over the buffered connection data +// (e.g. with HTTP/2 data). +// After calling this function, make sure to consume all the data related +// to the current request. +func (r *RequestReader) Reader() *bufio.Reader { + return r.reader +} + +func (r *RequestReader) ReadRequest() (*http.Request, error) { + if !r.preventCanonicalization { + // Just call the HTTP library function if the preventCanonicalization + // configuration is disabled + return http.ReadRequest(r.reader) + } + + req, err := http.ReadRequest(r.reader) + if err != nil { + return nil, err + } + + httpDataReader := getRequestReader(r.reader, r.cloned) + headers, _ := Http1ExtractHeaders(httpDataReader) + + for _, headerName := range headers { + canonicalizedName := textproto.CanonicalMIMEHeaderKey(headerName) + if canonicalizedName == headerName { + continue + } + + // Rewrite header keys to the non-canonical parsed value + values, ok := req.Header[canonicalizedName] + if ok { + req.Header.Del(canonicalizedName) + req.Header[headerName] = values + } + } + + return req, nil +} + +func getRequestReader(r *bufio.Reader, cloned *bytes.Buffer) *textproto.Reader { + // "Cloned" buffer uses the raw connection as the data source. + // However, the *bufio.Reader can read also bytes of another unrelated + // request on the same connection, since it's buffered, so we have to + // ignore them before passing the data to our headers parser. + // Data related to the next request will remain inside the buffer for + // later usage. + data := cloned.Next(cloned.Len() - r.Buffered()) + return &textproto.Reader{ + R: bufio.NewReader(bytes.NewReader(data)), + } +} diff --git a/vendor/github.com/elazarl/goproxy/counterecryptor.go b/vendor/github.com/elazarl/goproxy/internal/signer/counterecryptor.go similarity index 79% rename from vendor/github.com/elazarl/goproxy/counterecryptor.go rename to vendor/github.com/elazarl/goproxy/internal/signer/counterecryptor.go index d1c39d2..acb9925 100644 --- a/vendor/github.com/elazarl/goproxy/counterecryptor.go +++ b/vendor/github.com/elazarl/goproxy/internal/signer/counterecryptor.go @@ -1,9 +1,10 @@ -package goproxy +package signer import ( "crypto/aes" "crypto/cipher" "crypto/ecdsa" + "crypto/ed25519" "crypto/rsa" "crypto/sha256" "crypto/x509" @@ -17,7 +18,7 @@ type CounterEncryptorRand struct { ix int } -func NewCounterEncryptorRandFromKey(key interface{}, seed []byte) (r CounterEncryptorRand, err error) { +func NewCounterEncryptorRandFromKey(key any, seed []byte) (r CounterEncryptorRand, err error) { var keyBytes []byte switch key := key.(type) { case *rsa.PrivateKey: @@ -26,13 +27,16 @@ func NewCounterEncryptorRandFromKey(key interface{}, seed []byte) (r CounterEncr if keyBytes, err = x509.MarshalECPrivateKey(key); err != nil { return } + case ed25519.PrivateKey: + if keyBytes, err = x509.MarshalPKCS8PrivateKey(key); err != nil { + return + } default: - err = errors.New("only RSA and ECDSA keys supported") - return + return r, errors.New("only RSA, ED25519 and ECDSA keys supported") } h := sha256.New() if r.cipher, err = aes.NewCipher(h.Sum(keyBytes)[:aes.BlockSize]); err != nil { - return + return r, err } r.counter = make([]byte, r.cipher.BlockSize()) if seed != nil { @@ -40,7 +44,7 @@ func NewCounterEncryptorRandFromKey(key interface{}, seed []byte) (r CounterEncr } r.rand = make([]byte, r.cipher.BlockSize()) r.ix = len(r.rand) - return + return r, nil } func (c *CounterEncryptorRand) Seed(b []byte) { diff --git a/vendor/github.com/elazarl/goproxy/internal/signer/signer.go b/vendor/github.com/elazarl/goproxy/internal/signer/signer.go new file mode 100644 index 0000000..0ff00a5 --- /dev/null +++ b/vendor/github.com/elazarl/goproxy/internal/signer/signer.go @@ -0,0 +1,118 @@ +package signer + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rsa" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "math/big" + "math/rand" + "net" + "runtime" + "sort" + "strings" + "time" +) + +const _goproxySignerVersion = ":goproxy2" + +func hashSorted(lst []string) []byte { + c := make([]string, len(lst)) + copy(c, lst) + sort.Strings(c) + h := sha256.New() + h.Write([]byte(strings.Join(c, ","))) + return h.Sum(nil) +} + +func SignHost(ca tls.Certificate, hosts []string) (cert *tls.Certificate, err error) { + // Use the provided CA for certificate generation. + // Use already parsed Leaf certificate when present. + x509ca := ca.Leaf + if x509ca == nil { + if x509ca, err = x509.ParseCertificate(ca.Certificate[0]); err != nil { + return nil, err + } + } + + now := time.Now() + start := now.Add(-30 * 24 * time.Hour) // -30 days + end := now.Add(365 * 24 * time.Hour) // 365 days + + // Always generate a positive int value + // (Two complement is not enabled when the first bit is 0) + generated := rand.Uint64() + generated >>= 1 + + template := x509.Certificate{ + SerialNumber: big.NewInt(int64(generated)), + Issuer: x509ca.Subject, + Subject: pkix.Name{ + Organization: []string{"GoProxy untrusted MITM proxy Inc"}, + }, + NotBefore: start, + NotAfter: end, + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + for _, h := range hosts { + if ip := net.ParseIP(h); ip != nil { + 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 nil, err + } + + var certpriv crypto.Signer + switch ca.PrivateKey.(type) { + case *rsa.PrivateKey: + if certpriv, err = rsa.GenerateKey(&csprng, 2048); err != nil { + return nil, err + } + case *ecdsa.PrivateKey: + if certpriv, err = ecdsa.GenerateKey(elliptic.P256(), &csprng); err != nil { + return nil, err + } + case ed25519.PrivateKey: + if _, certpriv, err = ed25519.GenerateKey(&csprng); err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("unsupported key type %T", ca.PrivateKey) + } + + derBytes, err := x509.CreateCertificate(&csprng, &template, x509ca, certpriv.Public(), ca.PrivateKey) + if err != nil { + return nil, err + } + + // Save an already parsed leaf certificate to use less CPU + // when it will be used + leafCert, err := x509.ParseCertificate(derBytes) + if err != nil { + return nil, err + } + + certBytes := [][]byte{derBytes} + certBytes = append(certBytes, ca.Certificate...) + return &tls.Certificate{ + Certificate: certBytes, + PrivateKey: certpriv, + Leaf: leafCert, + }, nil +} diff --git a/vendor/github.com/elazarl/goproxy/logger.go b/vendor/github.com/elazarl/goproxy/logger.go index 939cf69..a7c674c 100644 --- a/vendor/github.com/elazarl/goproxy/logger.go +++ b/vendor/github.com/elazarl/goproxy/logger.go @@ -1,5 +1,5 @@ package goproxy type Logger interface { - Printf(format string, v ...interface{}) + Printf(format string, v ...any) } diff --git a/vendor/github.com/elazarl/goproxy/proxy.go b/vendor/github.com/elazarl/goproxy/proxy.go index 3deecfb..9cec023 100644 --- a/vendor/github.com/elazarl/goproxy/proxy.go +++ b/vendor/github.com/elazarl/goproxy/proxy.go @@ -1,14 +1,12 @@ package goproxy import ( - "bufio" "io" "log" "net" "net/http" "os" "regexp" - "sync/atomic" ) // The basic proxy type. Implements http.Handler. @@ -26,6 +24,12 @@ type ProxyHttpServer struct { respHandlers []RespHandler httpsHandlers []HttpsHandler Tr *http.Transport + // ConnectionErrHandler will be invoked to return a custom response + // to clients (written using conn parameter), when goproxy fails to connect + // to a target proxy. + // The error is passed as function parameter and not inside the proxy + // context, to avoid race conditions. + ConnectionErrHandler func(conn io.Writer, ctx *ProxyCtx, err error) // 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) @@ -33,6 +37,20 @@ type ProxyHttpServer struct { CertStore CertStorage KeepHeader bool AllowHTTP2 bool + // When PreventCanonicalization is true, the header names present in + // the request sent through the proxy are directly passed to the destination server, + // instead of following the HTTP RFC for their canonicalization. + // This is useful when the header name isn't treated as a case-insensitive + // value by the target server, because they don't follow the specs. + PreventCanonicalization bool + // KeepAcceptEncoding, if true, prevents the proxy from dropping + // Accept-Encoding headers from the client. + // + // Note that the outbound http.Transport may still choose to add + // Accept-Encoding: gzip if the client did not explicitly send an + // Accept-Encoding header. To disable this behavior, set + // Tr.DisableCompression to true. + KeepAcceptEncoding bool } var hasPort = regexp.MustCompile(`:\d+$`) @@ -44,24 +62,15 @@ func copyHeaders(dst, src http.Header, keepDestHeaders bool) { } } for k, vs := range src { - for _, v := range vs { - dst.Add(k, v) - } - } -} - -func isEof(r *bufio.Reader) bool { - _, err := r.Peek(1) - if err == io.EOF { - return true + // direct assignment to avoid canonicalization + dst[k] = append([]string(nil), vs...) } - return false } func (proxy *ProxyHttpServer) filterRequest(r *http.Request, ctx *ProxyCtx) (req *http.Request, resp *http.Response) { req = r for _, h := range proxy.reqHandlers { - req, resp = h.Handle(r, ctx) + req, resp = h.Handle(req, ctx) // non-nil resp means the handler decided to skip sending the request // and return canned response instead. if resp != nil { @@ -70,6 +79,7 @@ func (proxy *ProxyHttpServer) filterRequest(r *http.Request, ctx *ProxyCtx) (req } return } + func (proxy *ProxyHttpServer) filterResponse(respOrig *http.Response, ctx *ProxyCtx) (resp *http.Response) { resp = respOrig for _, h := range proxy.respHandlers { @@ -79,12 +89,15 @@ func (proxy *ProxyHttpServer) filterResponse(respOrig *http.Response, ctx *Proxy return } -func removeProxyHeaders(ctx *ProxyCtx, r *http.Request) { +// RemoveProxyHeaders removes all proxy headers which should not propagate to the next hop. +func RemoveProxyHeaders(ctx *ProxyCtx, r *http.Request) { r.RequestURI = "" // this must be reset when serving a request with the client ctx.Logf("Sending request %v %v", r.Method, r.URL.String()) - // If no Accept-Encoding header exists, Transport will add the headers it can accept - // and would wrap the response body with the relevant reader. - r.Header.Del("Accept-Encoding") + if !ctx.Proxy.KeepAcceptEncoding { + // If no Accept-Encoding header exists, Transport will add the headers it can accept + // and would wrap the response body with the relevant reader. + r.Header.Del("Accept-Encoding") + } // curl can add that, see // https://jdebp.eu./FGA/web-proxy-connection-header.html r.Header.Del("Proxy-Connection") @@ -97,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 { @@ -125,102 +135,22 @@ func (fw flushWriter) Write(p []byte) (int, error) { // 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" { + if r.Method == http.MethodConnect { proxy.handleHttps(w, r) } else { - 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) - return - } - r, resp := proxy.filterRequest(r, ctx) - - if resp == nil { - 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("Received response %v", resp.Status) - } - } - - var origBody io.ReadCloser - - if resp != nil { - origBody = resp.Body - defer origBody.Close() - } - - resp = proxy.filterResponse(resp, ctx) - - 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 - // body the user returned. - // We keep the original body to remove the header only if things changed. - // This will prevent problems with HEAD requests where there's no body, yet, - // the Content-Length header should be set. - if origBody != resp.Body { - resp.Header.Del("Content-Length") - } - copyHeaders(w.Header(), resp.Header, proxy.KeepDestinationHeaders) - w.WriteHeader(resp.StatusCode) - 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) - } - ctx.Logf("Copied %v bytes to client error=%v", nr, err) + proxy.handleHttp(w, r) } } -// NewProxyHttpServer creates and returns a proxy server, logging 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), - reqHandlers: []ReqHandler{}, - respHandlers: []RespHandler{}, - httpsHandlers: []HttpsHandler{}, + Logger: log.New(os.Stderr, "", log.LstdFlags), 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) + http.Error(w, "This is a proxy server. Does not respond to non-proxy requests.", http.StatusInternalServerError) }), 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 e1bf28f..78b93a5 100644 --- a/vendor/github.com/elazarl/goproxy/responses.go +++ b/vendor/github.com/elazarl/goproxy/responses.go @@ -2,7 +2,7 @@ package goproxy import ( "bytes" - "io/ioutil" + "io" "net/http" ) @@ -24,7 +24,7 @@ func NewResponse(r *http.Request, contentType string, status int, body string) * resp.Status = http.StatusText(status) buf := bytes.NewBufferString(body) resp.ContentLength = int64(buf.Len()) - resp.Body = ioutil.NopCloser(buf) + resp.Body = io.NopCloser(buf) return resp } @@ -33,7 +33,7 @@ const ( ContentTypeHtml = "text/html" ) -// Alias for NewResponse(r,ContentTypeText,http.StatusAccepted,text) +// Alias for NewResponse(r,ContentTypeText,http.StatusAccepted,text). func TextResponse(r *http.Request, text string) *http.Response { return NewResponse(r, ContentTypeText, http.StatusAccepted, text) } diff --git a/vendor/github.com/elazarl/goproxy/signer.go b/vendor/github.com/elazarl/goproxy/signer.go deleted file mode 100644 index aa511ca..0000000 --- a/vendor/github.com/elazarl/goproxy/signer.go +++ /dev/null @@ -1,108 +0,0 @@ -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" - "time" -) - -func hashSorted(lst []string) []byte { - c := make([]string, len(lst)) - copy(c, lst) - sort.Strings(c) - h := sha1.New() - for _, s := range c { - h.Write([]byte(s + ",")) - } - return h.Sum(nil) -} - -func hashSortedBigInt(lst []string) *big.Int { - rv := new(big.Int) - rv.SetBytes(hashSorted(lst)) - return rv -} - -var goproxySignerVersion = ":goroxy1" - -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(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, - Issuer: x509ca.Subject, - Subject: pkix.Name{ - Organization: []string{"GoProxy untrusted MITM proxy Inc"}, - }, - NotBefore: start, - NotAfter: end, - - KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - BasicConstraintsValid: true, - } - for _, h := range hosts { - if ip := net.ParseIP(h); ip != nil { - 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 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.Public(), ca.PrivateKey); err != nil { - return - } - 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 index 522b88e..c10f57c 100644 --- a/vendor/github.com/elazarl/goproxy/websocket.go +++ b/vendor/github.com/elazarl/goproxy/websocket.go @@ -1,11 +1,9 @@ package goproxy import ( - "bufio" - "crypto/tls" "io" + "net" "net/http" - "net/url" "strings" ) @@ -20,42 +18,12 @@ 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 isWebSocketHandshake(header http.Header) bool { + return headerContains(header, "Connection", "Upgrade") && + headerContains(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() - +func (proxy *ProxyHttpServer) hijackConnection(ctx *ProxyCtx, w http.ResponseWriter) (net.Conn, error) { // Connect to Client hj, ok := w.(http.Hijacker) if !ok { @@ -64,58 +32,25 @@ func (proxy *ProxyHttpServer) serveWebsocket(ctx *ProxyCtx, w http.ResponseWrite clientConn, _, err := hj.Hijack() if err != nil { ctx.Warnf("Hijack error: %v", err) - return + return nil, err } - - // 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) + return clientConn, nil } -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 +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 94bc529..20d8569 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -95,9 +95,11 @@ github.com/docker/distribution/manifest/schema2 # github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 ## explicit github.com/docker/libtrust -# github.com/elazarl/goproxy v0.0.0-20240726154733-8b0c20506380 -## explicit; go 1.18 +# github.com/elazarl/goproxy v1.7.2 +## explicit; go 1.20 github.com/elazarl/goproxy +github.com/elazarl/goproxy/internal/http1parser +github.com/elazarl/goproxy/internal/signer # github.com/evalphobia/logrus_sentry v0.8.2 ## explicit github.com/evalphobia/logrus_sentry