From c42d9cab45b8639e72c3c8e3c177f5ae8a503012 Mon Sep 17 00:00:00 2001 From: "red-hat-konflux[bot]" <126015336+red-hat-konflux[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 05:25:41 +0000 Subject: [PATCH] Update module github.com/elazarl/goproxy to v1 Signed-off-by: red-hat-konflux <126015336+red-hat-konflux[bot]@users.noreply.github.com> --- go.mod | 6 +- go.sum | 24 +- .../github.com/elazarl/goproxy/.golangci.yml | 165 ++++++ vendor/github.com/elazarl/goproxy/README.md | 329 +++++++---- vendor/github.com/elazarl/goproxy/actions.go | 16 +- vendor/github.com/elazarl/goproxy/chunked.go | 59 -- vendor/github.com/elazarl/goproxy/ctx.go | 29 +- .../github.com/elazarl/goproxy/dispatcher.go | 56 +- vendor/github.com/elazarl/goproxy/doc.go | 7 +- vendor/github.com/elazarl/goproxy/h2.go | 69 ++- vendor/github.com/elazarl/goproxy/http.go | 97 +++ vendor/github.com/elazarl/goproxy/https.go | 554 ++++++++++-------- .../goproxy/internal/http1parser/header.go | 43 ++ .../goproxy/internal/http1parser/request.go | 94 +++ .../{ => internal/signer}/counterecryptor.go | 11 +- .../goproxy/{ => internal/signer}/signer.go | 57 +- vendor/github.com/elazarl/goproxy/logger.go | 2 +- vendor/github.com/elazarl/goproxy/proxy.go | 141 ++--- .../github.com/elazarl/goproxy/responses.go | 5 +- .../github.com/elazarl/goproxy/websocket.go | 132 +---- vendor/golang.org/x/net/http2/config.go | 2 +- vendor/golang.org/x/net/http2/config_go124.go | 2 +- vendor/golang.org/x/net/http2/frame.go | 25 +- vendor/golang.org/x/net/http2/http2.go | 41 +- vendor/golang.org/x/net/http2/server.go | 130 ++-- vendor/golang.org/x/net/http2/transport.go | 347 ++--------- vendor/golang.org/x/net/http2/write.go | 3 +- .../x/net/internal/httpcommon/ascii.go | 53 ++ .../httpcommon}/headermap.go | 24 +- .../x/net/internal/httpcommon/request.go | 467 +++++++++++++++ vendor/modules.txt | 13 +- 31 files changed, 1858 insertions(+), 1145 deletions(-) create mode 100644 vendor/github.com/elazarl/goproxy/.golangci.yml delete mode 100644 vendor/github.com/elazarl/goproxy/chunked.go create mode 100644 vendor/github.com/elazarl/goproxy/http.go create mode 100644 vendor/github.com/elazarl/goproxy/internal/http1parser/header.go create mode 100644 vendor/github.com/elazarl/goproxy/internal/http1parser/request.go rename vendor/github.com/elazarl/goproxy/{ => internal/signer}/counterecryptor.go (87%) rename vendor/github.com/elazarl/goproxy/{ => internal/signer}/signer.go (71%) create mode 100644 vendor/golang.org/x/net/internal/httpcommon/ascii.go rename vendor/golang.org/x/net/{http2 => internal/httpcommon}/headermap.go (74%) create mode 100644 vendor/golang.org/x/net/internal/httpcommon/request.go diff --git a/go.mod b/go.mod index dc1deab..1e4f717 100644 --- a/go.mod +++ b/go.mod @@ -2,9 +2,9 @@ module org.jboss.pnc.domain-proxy go 1.23.4 -require github.com/elazarl/goproxy v0.0.0-20241218172127-ac55c7698e0d +require github.com/elazarl/goproxy v1.8.3 require ( - golang.org/x/net v0.32.0 // indirect - golang.org/x/text v0.24.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/text v0.28.0 // indirect ) diff --git a/go.sum b/go.sum index 5bc75a7..d96502e 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,16 @@ -github.com/elazarl/goproxy v0.0.0-20241218172127-ac55c7698e0d h1:r8DboPPvhhSMCWfmBEDoLuNvHetXH8/AZUdaRLNYgXE= -github.com/elazarl/goproxy v0.0.0-20241218172127-ac55c7698e0d/go.mod h1:3TKt+OFpElWuCtt5bphUyO97JT606j9Ffx4S2pfIcCo= -github.com/elazarl/goproxy/ext v0.0.0-20241217120900-7711dfa3811c h1:R+i10jtNSzKJKqEZAYJnR9M8y14k0zrNHqD1xkv/A2M= -github.com/elazarl/goproxy/ext v0.0.0-20241217120900-7711dfa3811c/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= -golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= -golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= +github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elazarl/goproxy v1.8.3 h1:XhiZpzW0NvsGOqSv/F3v4+1F29842yYaJNN+In5Fnuc= +github.com/elazarl/goproxy v1.8.3/go.mod h1:b5xm6W48AUHNpRTCvlnd0YVh+JafCCtsLsJZvvNTz+E= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/vendor/github.com/elazarl/goproxy/.golangci.yml b/vendor/github.com/elazarl/goproxy/.golangci.yml new file mode 100644 index 0000000..aaa111a --- /dev/null +++ b/vendor/github.com/elazarl/goproxy/.golangci.yml @@ -0,0 +1,165 @@ +version: "2" +run: + modules-download-mode: readonly + +# List from https://golangci-lint.run/usage/linters/ +linters: + enable: + - asasalint + - asciicheck + - bidichk + - containedctx + - decorder + - dogsled + - durationcheck + - errchkjson + - errname + - errorlint + - exhaustive + - fatcontext + - forbidigo + - forcetypeassert + - gocheckcompilerdirectives + - gochecksumtype + - gocritic + - godot + - goheader + - gomodguard + - goprintffuncname + - gosec + - gosmopolitan + - grouper + - iface + - importas + - interfacebloat + - lll + - loggercheck + - makezero + - mirror + - misspell + - nakedret + - nilerr + - noctx + - nolintlint + - perfsprint + - prealloc + - predeclared + - reassign + - revive + - staticcheck + - tagalign + - 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 + - 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 + settings: + 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 + - G602 # Slice index out of range + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + 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: + - staticcheck + text: ' should be ' # TODO: Re-enable in V2 + - linters: + - staticcheck + text: 'ST1003: should not use ALL_CAPS in Go names; use CamelCase instead' + paths: + - examples$ + - transport +formatters: + enable: + - gci + - gofmt + - gofumpt + settings: + gci: + sections: + - standard + - default + custom-order: true + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/vendor/github.com/elazarl/goproxy/README.md b/vendor/github.com/elazarl/goproxy/README.md index c111670..b6c7208 100644 --- a/vendor/github.com/elazarl/goproxy/README.md +++ b/vendor/github.com/elazarl/goproxy/README.md @@ -1,57 +1,131 @@ -# 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) +> +> [🎁 Become a Sponsor](https://opencollective.com/goproxy) + +## 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? Help us keep the project maintained and healthy! +Supporting GoProxy allows us to dedicate more time to bug fixes and new features. +In exchange, if you choose a Gold Supporter or Enterprise plan, we'll proudly display your company logo here. + +> [Become a Sponsor](https://opencollective.com/goproxy) + +[![Gold Supporters](https://opencollective.com/goproxy/tiers/gold-sponsor.svg?width=890)](https://opencollective.com/goproxy) +[![Enterprise Supporters](https://opencollective.com/goproxy/tiers/enterprise.svg?width=890)](https://opencollective.com/goproxy) + +## 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 +135,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,100 +147,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(req, 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. -// Reddit URL check is case-insensitive, so the block will work also if the user types something like rEdDit.com. +// 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) -// 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. +// 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 +## 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. -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) +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. -# 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. - -At any rate, user feedback is very important for me, so I'll be delighted to know if you're using this package. - -# Beta Software - -I've received 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/chunked.go b/vendor/github.com/elazarl/goproxy/chunked.go deleted file mode 100644 index 83654f6..0000000 --- a/vendor/github.com/elazarl/goproxy/chunked.go +++ /dev/null @@ -1,59 +0,0 @@ -// Taken from $GOROOT/src/pkg/net/http/chunked -// needed to write https responses to client. -package goproxy - -import ( - "io" - "strconv" -) - -// newChunkedWriter returns a new chunkedWriter that translates writes into HTTP -// "chunked" format before writing them to w. Closing the returned chunkedWriter -// sends the final 0-length chunk that marks the end of the stream. -// -// newChunkedWriter is not needed by normal applications. The http -// package adds chunking automatically if handlers don't set a -// Content-Length header. Using newChunkedWriter inside a handler -// would result in double chunking or chunking with a Content-Length -// length, both of which are wrong. -func newChunkedWriter(w io.Writer) io.WriteCloser { - return &chunkedWriter{w} -} - -// Writing to chunkedWriter translates to writing in HTTP chunked Transfer -// Encoding wire format to the underlying Wire chunkedWriter. -type chunkedWriter struct { - Wire io.Writer -} - -// 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 -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 - } - - head := strconv.FormatInt(int64(len(data)), 16) + "\r\n" - - if _, err = io.WriteString(cw.Wire, head); err != nil { - return 0, err - } - if n, err = cw.Wire.Write(data); err != nil { - return - } - if n != len(data) { - err = io.ErrShortWrite - return - } - _, err = io.WriteString(cw.Wire, "\r\n") - - return -} - -func (cw *chunkedWriter) Close() error { - _, err := io.WriteString(cw.Wire, "0\r\n") - return err -} 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 9c32530..bc15c26 100644 --- a/vendor/github.com/elazarl/goproxy/dispatcher.go +++ b/vendor/github.com/elazarl/goproxy/dispatcher.go @@ -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 { @@ -116,7 +124,7 @@ var IsLocalHost ReqConditionFunc = func(req *http.Request, ctx *ProxyCtx) bool { } // 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) || @@ -124,15 +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 strings.ToLower(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 { @@ -144,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) @@ -170,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 { @@ -195,14 +220,15 @@ 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)) } @@ -289,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)) } 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..2f6342c 100644 --- a/vendor/github.com/elazarl/goproxy/h2.go +++ b/vendor/github.com/elazarl/goproxy/h2.go @@ -2,6 +2,7 @@ package goproxy import ( "bufio" + "context" "crypto/tls" "errors" "io" @@ -12,6 +13,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 +28,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 +42,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.HandshakeContext(context.Background()); 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 +82,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 +112,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 +139,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 +179,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 654423f..9fceacb 100644 --- a/vendor/github.com/elazarl/goproxy/https.go +++ b/vendor/github.com/elazarl/goproxy/https.go @@ -2,6 +2,7 @@ package goproxy import ( "bufio" + "context" "crypto/tls" "errors" "fmt" @@ -10,10 +11,12 @@ import ( "net/http" "net/url" "os" - "strconv" "strings" "sync" "sync/atomic" + + "github.com/elazarl/goproxy/internal/http1parser" + "github.com/elazarl/goproxy/internal/signer" ) type ConnectActionLiteral int @@ -23,21 +26,36 @@ const ( ConnectReject ConnectMitm ConnectHijack + // Deprecated: use ConnectMitm. ConnectHTTPMitm ConnectProxyAuthHijack ) var ( - OkConnect = &ConnectAction{Action: ConnectAccept, TLSConfig: TLSConfigFromCA(&GoproxyCa)} - MitmConnect = &ConnectAction{Action: ConnectMitm, TLSConfig: TLSConfigFromCA(&GoproxyCa)} + OkConnect = &ConnectAction{Action: ConnectAccept, TLSConfig: TLSConfigFromCA(&GoproxyCa)} + MitmConnect = &ConnectAction{Action: ConnectMitm, TLSConfig: TLSConfigFromCA(&GoproxyCa)} + // Deprecated: use MitmConnect. HTTPMitmConnect = &ConnectAction{Action: ConnectHTTPMitm, TLSConfig: TLSConfigFromCA(&GoproxyCa)} RejectConnect = &ConnectAction{Action: ConnectReject, TLSConfig: TLSConfigFromCA(&GoproxyCa)} ) +var _errorRespMaxLength int64 = 500 + +const _tlsRecordTypeHandshake = byte(22) + +type readBufferedConn struct { + net.Conn + r io.Reader +} + +func (c *readBufferedConn) Read(p []byte) (int, error) { + return c.r.Read(p) +} + // 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) @@ -47,9 +65,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, "]", "") @@ -58,26 +75,33 @@ 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) } - return net.Dial(network, addr) + + // if the user didn't specify any dialer, we just use the default one, + // provided by net package + var d net.Dialer + return d.DialContext(ctx.Req.Context(), 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 { @@ -132,243 +156,245 @@ 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")) - ctx.Logf("Assuming CONNECT is plain HTTP tunneling, mitm proxying it") - - var targetSiteCon net.Conn - var remote *bufio.Reader - - for { - client := bufio.NewReader(proxyClient) - req, err := http.ReadRequest(client) - if err != nil && 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 { - // 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) + case ConnectHTTPMitm, ConnectMitm: + _, _ = proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) + ctx.Logf("Received CONNECT request, 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 + // request can take forever, and the server will be stuck when "closed". + // TODO: Allow Server.Close() mechanism to shut down this connection as nicely as possible + go func() { + // Check if this is an HTTP or an HTTPS MITM request + readBuffer := bufio.NewReader(proxyClient) + peek, _ := readBuffer.Peek(1) + isTLS := len(peek) > 0 && peek[0] == _tlsRecordTypeHandshake + + var client net.Conn = &readBufferedConn{Conn: proxyClient, r: readBuffer} + defer func() { + _ = client.Close() + }() + + var tlsConfig *tls.Config + scheme := "http" + if isTLS { + scheme = "https" + tlsConfig = defaultTLSConfig + if todo.TLSConfig != nil { + var err error + tlsConfig, err = todo.TLSConfig(host, ctx) if err != nil { - ctx.Warnf("Error dialing to %s: %s", host, err.Error()) + httpError(proxyClient, ctx, err) return } - remote = bufio.NewReader(targetSiteCon) } - if err := req.Write(targetSiteCon); err != nil { - httpError(proxyClient, ctx, err) + // Create a TLS connection over the TCP connection + rawClientTls := tls.Server(client, tlsConfig) + client = rawClientTls + if err := rawClientTls.HandshakeContext(context.Background()); err != nil { + ctx.Warnf("Cannot handshake client %v %v", r.Host, err) return } - resp, err = http.ReadResponse(remote, req) - if err != nil { - httpError(proxyClient, ctx, err) - return - } - defer resp.Body.Close() - } - resp = proxy.filterResponse(resp, ctx) - if err := resp.Write(proxyClient); err != nil { - httpError(proxyClient, ctx, err) - return - } - } - case ConnectMitm: - 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 - // request can take forever, and the server will be stuck when "closed". - // TODO: Allow Server.Close() mechanism to shut down this connection as nicely as possible - tlsConfig := defaultTLSConfig - if todo.TLSConfig != nil { - var err error - tlsConfig, err = todo.TLSConfig(host, ctx) - if err != nil { - httpError(proxyClient, ctx, err) - return - } - } - go func() { - //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 + + clientReader := http1parser.NewRequestReader(proxy.PreventCanonicalization, client) + for !clientReader.IsEOF() { + req, err := clientReader.ReadRequest() + ctx := &ProxyCtx{ + Req: req, + Session: atomic.AddInt64(&proxy.sess, 1), + Proxy: proxy, + UserData: ctx.UserData, + RoundTripper: ctx.RoundTripper, + } + if err != nil && !errors.Is(err, io.EOF) { + ctx.Warnf("Cannot read request from mitm'd client %v %v", r.Host, err) } if err != nil { - ctx.Warnf("Cannot read TLS request from mitm'd client %v %v", r.Host, err) 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 !strings.HasPrefix(req.URL.String(), "https://") { - req.URL, err = url.Parse("https://" + r.Host + req.URL.String()) + if !strings.HasPrefix(req.URL.String(), scheme+"://") { + req.URL, err = url.Parse(scheme + "://" + 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 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() + + // 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() + + // 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 := clientReader.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, client, tlsConfig, host} + if _, err := tr.RoundTrip(req); err != nil { + ctx.Warnf("HTTP2 connection failed: %v", err) + } else { + ctx.Logf("Exiting on EOF") + } + return false + } if err != nil { - ctx.Warnf("Failed to process HTTP2 client preface: %v", err) - return + if req.URL != nil { + ctx.Warnf("Illegal URL %s", scheme+"://"+r.Host+req.URL.Path) + } else { + ctx.Warnf("Illegal URL %s", scheme+"://"+r.Host) + } + return false } - if !proxy.AllowHTTP2 { - ctx.Warnf("HTTP2 connection failed: disallowed") - return + if !proxy.KeepHeader { + RemoveProxyHeaders(ctx, req) } - 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") + resp, err = ctx.RoundTrip(req) + if err != nil { + ctx.Warnf("Cannot read response from mitm'd server %v", err) + return false } - return + ctx.Logf("resp %v", resp.Status) } - if isWebSocketRequest(req) { - ctx.Logf("Request looks like websocket upgrade.") - if req.URL.Scheme == "http" { - ctx.Logf("Enforced HTTP websocket forwarding over TLS") - proxy.serveWebsocketHttpOverTLS(ctx, w, req, rawClientTls) - } else { - proxy.serveWebsocketTLS(ctx, w, req, tlsConfig, rawClientTls) - } - return + origBody := resp.Body + resp = proxy.filterResponse(resp, ctx) + bodyModified := resp.Body != origBody + defer resp.Body.Close() + if bodyModified || (resp.ContentLength <= 0 && resp.Header.Get("Content-Length") == "") { + // Return chunked encoded response when we don't know the length of the resp, if the body + // has been modified by the response handler or if there is no content length in the response. + // We include 0 in resp.ContentLength <= 0 because 0 is the field zero value and some user + // might incorrectly leave it instead of setting it to -1 when the length is unknown (but we + // also check that the Content-Length header is empty, so there is no issue with empty bodies). + resp.ContentLength = -1 + resp.Header.Del("Content-Length") + resp.TransferEncoding = []string{"chunked"} } - 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) + + // The MITM'd client speaks HTTP/1.1, but the upstream + // response may have been received over HTTP/2. Normalize + // the protocol version so resp.Write() produces a valid + // HTTP/1.1 status line. + resp.Proto = "HTTP/1.1" + resp.ProtoMajor = 1 + resp.ProtoMinor = 1 + + if isWebSocketHandshake(resp.Header) { + 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 } - return - } - if !proxy.KeepHeader { - 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 + // Set Body to nil so resp.Write only writes the headers + // and returns immediately without blocking on the body + // (or else we wouldn't be able to proxy WebSocket data). + resp.Body = nil + if err := resp.Write(client); err != nil { + ctx.Warnf("Cannot write response header from mitm'd client: %v", err) + return false + } + proxy.proxyWebsocket(ctx, wsConn, client) + 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 err := resp.Write(client); err != nil { + ctx.Warnf("Cannot write response from mitm'd client: %v", err) + return false + } - if 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") - } - // 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 true + }(req); !continueLoop { 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 == 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 - } - if err := chunked.Close(); err != nil { - ctx.Warnf("Cannot write TLS chunked EOF from mitm'd client: %v", err) - return - } - if _, err = io.WriteString(rawClientTls, "\r\n"); err != nil { - ctx.Warnf("Cannot write TLS response chunked trailer from mitm'd client: %v", err) - return - } - } } ctx.Logf("Exiting on EOF") }() 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 { @@ -376,57 +402,75 @@ 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 } - if u.Scheme == "" || u.Scheme == "http" { + if u.Scheme == "" || u.Scheme == "http" || u.Scheme == "ws" { if !strings.ContainsRune(u.Host, ':') { u.Host += ":80" } @@ -440,27 +484,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 != http.StatusOK { - resp, err := io.ReadAll(resp.Body) + 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 @@ -471,11 +515,17 @@ 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: http.MethodConnect, URL: &url.URL{Opaque: addr}, @@ -485,23 +535,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 != http.StatusOK { - body, err := io.ReadAll(io.LimitReader(resp.Body, 500)) + 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 @@ -520,7 +570,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) @@ -537,3 +587,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 87% rename from vendor/github.com/elazarl/goproxy/counterecryptor.go rename to vendor/github.com/elazarl/goproxy/internal/signer/counterecryptor.go index 2ce9da9..acb9925 100644 --- a/vendor/github.com/elazarl/goproxy/counterecryptor.go +++ b/vendor/github.com/elazarl/goproxy/internal/signer/counterecryptor.go @@ -1,4 +1,4 @@ -package goproxy +package signer import ( "crypto/aes" @@ -18,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: @@ -32,12 +32,11 @@ func NewCounterEncryptorRandFromKey(key interface{}, seed []byte) (r CounterEncr return } default: - err = errors.New("only RSA, ED25519 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 { @@ -45,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/signer.go b/vendor/github.com/elazarl/goproxy/internal/signer/signer.go similarity index 71% rename from vendor/github.com/elazarl/goproxy/signer.go rename to vendor/github.com/elazarl/goproxy/internal/signer/signer.go index e8704c8..d62ec1a 100644 --- a/vendor/github.com/elazarl/goproxy/signer.go +++ b/vendor/github.com/elazarl/goproxy/internal/signer/signer.go @@ -1,4 +1,4 @@ -package goproxy +package signer import ( "crypto" @@ -6,38 +6,32 @@ import ( "crypto/ed25519" "crypto/elliptic" "crypto/rsa" - "crypto/sha1" + "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 := sha1.New() - for _, s := range c { - h.Write([]byte(s + ",")) - } + h := sha256.New() + h.Write([]byte(strings.Join(c, ","))) 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) { +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 @@ -47,8 +41,9 @@ func signHost(ca tls.Certificate, hosts []string) (cert *tls.Certificate, err er } } - start := time.Unix(time.Now().Unix()-2592000, 0) // 2592000 = 30 day - end := time.Unix(time.Now().Unix()+31536000, 0) // 31536000 = 365 day + 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) @@ -58,9 +53,11 @@ func signHost(ca tls.Certificate, hosts []string) (cert *tls.Certificate, err er template := x509.Certificate{ SerialNumber: big.NewInt(int64(generated)), Issuer: x509ca.Subject, - Subject: x509ca.Subject, - NotBefore: start, - NotAfter: end, + Subject: pkix.Name{ + Organization: []string{"GoProxy untrusted MITM proxy Inc"}, + }, + NotBefore: start, + NotAfter: end, KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, @@ -75,28 +72,28 @@ func signHost(ca tls.Certificate, hosts []string) (cert *tls.Certificate, err er } } - hash := hashSorted(append(hosts, goproxySignerVersion, ":"+runtime.Version())) + hash := hashSorted(append(hosts, _goproxySignerVersion, ":"+runtime.Version())) var csprng CounterEncryptorRand if csprng, err = NewCounterEncryptorRandFromKey(ca.PrivateKey, hash); err != nil { - return + return nil, err } var certpriv crypto.Signer switch ca.PrivateKey.(type) { case *rsa.PrivateKey: if certpriv, err = rsa.GenerateKey(&csprng, 2048); err != nil { - return + return nil, err } case *ecdsa.PrivateKey: if certpriv, err = ecdsa.GenerateKey(elliptic.P256(), &csprng); err != nil { - return + return nil, err } case ed25519.PrivateKey: if _, certpriv, err = ed25519.GenerateKey(&csprng); err != nil { - return + return nil, err } default: - err = fmt.Errorf("unsupported key type %T", ca.PrivateKey) + return nil, fmt.Errorf("unsupported key type %T", ca.PrivateKey) } derBytes, err := x509.CreateCertificate(&csprng, &template, x509ca, certpriv.Public(), ca.PrivateKey) @@ -111,8 +108,12 @@ func signHost(ca tls.Certificate, hosts []string) (cert *tls.Certificate, err er return nil, err } - certBytes := [][]byte{derBytes} - certBytes = append(certBytes, ca.Certificate...) + certBytes := make([][]byte, 1+len(ca.Certificate)) + certBytes[0] = derBytes + for i, singleCertBytes := range ca.Certificate { + certBytes[i+1] = singleCertBytes + } + return &tls.Certificate{ Certificate: certBytes, PrivateKey: certpriv, 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 c8e192a..9cec023 100644 --- a/vendor/github.com/elazarl/goproxy/proxy.go +++ b/vendor/github.com/elazarl/goproxy/proxy.go @@ -1,15 +1,12 @@ package goproxy import ( - "bufio" "io" "log" "net" "net/http" "os" "regexp" - "strings" - "sync/atomic" ) // The basic proxy type. Implements http.Handler. @@ -27,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) @@ -34,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+$`) @@ -50,14 +67,6 @@ func copyHeaders(dst, src http.Header, keepDestHeaders bool) { } } -func isEof(r *bufio.Reader) bool { - _, err := r.Peek(1) - if err == io.EOF { - return true - } - return false -} - func (proxy *ProxyHttpServer) filterRequest(r *http.Request, ctx *ProxyCtx) (req *http.Request, resp *http.Response) { req = r for _, h := range proxy.reqHandlers { @@ -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,13 +89,15 @@ func (proxy *ProxyHttpServer) filterResponse(respOrig *http.Response, ctx *Proxy return } -// RemoveProxyHeaders removes all proxy headers which should not propagate to the next hop +// 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") @@ -98,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 { @@ -126,97 +135,19 @@ 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 == 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 - // 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) + 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), 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}, } diff --git a/vendor/github.com/elazarl/goproxy/responses.go b/vendor/github.com/elazarl/goproxy/responses.go index 4081c7e..91cd5dd 100644 --- a/vendor/github.com/elazarl/goproxy/responses.go +++ b/vendor/github.com/elazarl/goproxy/responses.go @@ -22,6 +22,9 @@ func NewResponse(r *http.Request, contentType string, status int, body string) * resp.Header.Add("Content-Type", contentType) resp.StatusCode = status resp.Status = http.StatusText(status) + resp.Proto = "HTTP/1.1" + resp.ProtoMajor = 1 + resp.ProtoMinor = 1 buf := bytes.NewBufferString(body) resp.ContentLength = int64(buf.Len()) resp.Body = io.NopCloser(buf) @@ -33,7 +36,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/websocket.go b/vendor/github.com/elazarl/goproxy/websocket.go index 753a1e8..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,63 +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) serveWebsocketHttpOverTLS(ctx *ProxyCtx, w http.ResponseWriter, req *http.Request, clientConn *tls.Conn) { - targetURL := url.URL{Scheme: "ws", Host: req.URL.Host, Path: req.URL.Path} - - // Connect to upstream - targetConn, err := proxy.connectDial(ctx, "tcp", targetURL.Host) - if err != nil { - ctx.Warnf("Error dialing target site: %v", err) - return - } - defer targetConn.Close() - - // Perform handshake - if err := proxy.websocketHandshake(ctx, req, targetConn, clientConn); err != nil { - ctx.Warnf("Websocket handshake error: %v", err) - return - } - - // Proxy 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 { @@ -85,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 - } - - // Perform handshake - if err := proxy.websocketHandshake(ctx, req, targetConn, clientConn); err != nil { - ctx.Warnf("Websocket handshake error: %v", err) - return - } - - // Proxy ws connection - proxy.proxyWebsocket(ctx, targetConn, clientConn) -} - -func (proxy *ProxyHttpServer) websocketHandshake(ctx *ProxyCtx, req *http.Request, targetSiteConn io.ReadWriter, clientConn io.ReadWriter) error { - // write handshake request to target - err := req.Write(targetSiteConn) - if err != nil { - ctx.Warnf("Error writing upgrade request: %v", err) - return err - } - - targetTLSReader := bufio.NewReader(targetSiteConn) - - // Read handshake response from target - resp, err := http.ReadResponse(targetTLSReader, req) - if err != nil { - ctx.Warnf("Error reading handhsake response %v", err) - return err + return nil, 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 + return clientConn, 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/golang.org/x/net/http2/config.go b/vendor/golang.org/x/net/http2/config.go index de58dfb..ca645d9 100644 --- a/vendor/golang.org/x/net/http2/config.go +++ b/vendor/golang.org/x/net/http2/config.go @@ -60,7 +60,7 @@ func configFromServer(h1 *http.Server, h2 *Server) http2Config { return conf } -// configFromServer merges configuration settings from h2 and h2.t1.HTTP2 +// configFromTransport merges configuration settings from h2 and h2.t1.HTTP2 // (the net/http Transport). func configFromTransport(h2 *Transport) http2Config { conf := http2Config{ diff --git a/vendor/golang.org/x/net/http2/config_go124.go b/vendor/golang.org/x/net/http2/config_go124.go index e378412..5b516c5 100644 --- a/vendor/golang.org/x/net/http2/config_go124.go +++ b/vendor/golang.org/x/net/http2/config_go124.go @@ -13,7 +13,7 @@ func fillNetHTTPServerConfig(conf *http2Config, srv *http.Server) { fillNetHTTPConfig(conf, srv.HTTP2) } -// fillNetHTTPServerConfig sets fields in conf from tr.HTTP2. +// fillNetHTTPTransportConfig sets fields in conf from tr.HTTP2. func fillNetHTTPTransportConfig(conf *http2Config, tr *http.Transport) { fillNetHTTPConfig(conf, tr.HTTP2) } diff --git a/vendor/golang.org/x/net/http2/frame.go b/vendor/golang.org/x/net/http2/frame.go index 81faec7..db3264d 100644 --- a/vendor/golang.org/x/net/http2/frame.go +++ b/vendor/golang.org/x/net/http2/frame.go @@ -39,7 +39,7 @@ const ( FrameContinuation FrameType = 0x9 ) -var frameName = map[FrameType]string{ +var frameNames = [...]string{ FrameData: "DATA", FrameHeaders: "HEADERS", FramePriority: "PRIORITY", @@ -53,10 +53,10 @@ var frameName = map[FrameType]string{ } func (t FrameType) String() string { - if s, ok := frameName[t]; ok { - return s + if int(t) < len(frameNames) { + return frameNames[t] } - return fmt.Sprintf("UNKNOWN_FRAME_TYPE_%d", uint8(t)) + return fmt.Sprintf("UNKNOWN_FRAME_TYPE_%d", t) } // Flags is a bitmask of HTTP/2 flags. @@ -124,7 +124,7 @@ var flagName = map[FrameType]map[Flags]string{ // might be 0). type frameParser func(fc *frameCache, fh FrameHeader, countError func(string), payload []byte) (Frame, error) -var frameParsers = map[FrameType]frameParser{ +var frameParsers = [...]frameParser{ FrameData: parseDataFrame, FrameHeaders: parseHeadersFrame, FramePriority: parsePriorityFrame, @@ -138,8 +138,8 @@ var frameParsers = map[FrameType]frameParser{ } func typeFrameParser(t FrameType) frameParser { - if f := frameParsers[t]; f != nil { - return f + if int(t) < len(frameParsers) { + return frameParsers[t] } return parseUnknownFrame } @@ -225,6 +225,11 @@ var fhBytes = sync.Pool{ }, } +func invalidHTTP1LookingFrameHeader() FrameHeader { + fh, _ := readFrameHeader(make([]byte, frameHeaderLen), strings.NewReader("HTTP/1.1 ")) + return fh +} + // ReadFrameHeader reads 9 bytes from r and returns a FrameHeader. // Most users should use Framer.ReadFrame instead. func ReadFrameHeader(r io.Reader) (FrameHeader, error) { @@ -503,10 +508,16 @@ func (fr *Framer) ReadFrame() (Frame, error) { return nil, err } if fh.Length > fr.maxReadSize { + if fh == invalidHTTP1LookingFrameHeader() { + return nil, fmt.Errorf("http2: failed reading the frame payload: %w, note that the frame header looked like an HTTP/1.1 header", ErrFrameTooLarge) + } return nil, ErrFrameTooLarge } payload := fr.getReadBuf(fh.Length) if _, err := io.ReadFull(fr.r, payload); err != nil { + if fh == invalidHTTP1LookingFrameHeader() { + return nil, fmt.Errorf("http2: failed reading the frame payload: %w, note that the frame header looked like an HTTP/1.1 header", err) + } return nil, err } f, err := typeFrameParser(fh.Type)(fr.frameCache, fh, fr.countError, payload) diff --git a/vendor/golang.org/x/net/http2/http2.go b/vendor/golang.org/x/net/http2/http2.go index c7601c9..ea5ae62 100644 --- a/vendor/golang.org/x/net/http2/http2.go +++ b/vendor/golang.org/x/net/http2/http2.go @@ -11,8 +11,6 @@ // requires Go 1.6 or later) // // See https://http2.github.io/ for more information on HTTP/2. -// -// See https://http2.golang.org/ for a test server running this code. package http2 // import "golang.org/x/net/http2" import ( @@ -34,11 +32,19 @@ import ( ) var ( - VerboseLogs bool - logFrameWrites bool - logFrameReads bool - inTests bool - disableExtendedConnectProtocol bool + VerboseLogs bool + logFrameWrites bool + logFrameReads bool + inTests bool + + // Enabling extended CONNECT by causes browsers to attempt to use + // WebSockets-over-HTTP/2. This results in problems when the server's websocket + // package doesn't support extended CONNECT. + // + // Disable extended CONNECT by default for now. + // + // Issue #71128. + disableExtendedConnectProtocol = true ) func init() { @@ -51,8 +57,8 @@ func init() { logFrameWrites = true logFrameReads = true } - if strings.Contains(e, "http2xconnect=0") { - disableExtendedConnectProtocol = true + if strings.Contains(e, "http2xconnect=1") { + disableExtendedConnectProtocol = false } } @@ -407,23 +413,6 @@ func (s *sorter) SortStrings(ss []string) { s.v = save } -// validPseudoPath reports whether v is a valid :path pseudo-header -// value. It must be either: -// -// - a non-empty string starting with '/' -// - the string '*', for OPTIONS requests. -// -// For now this is only used a quick check for deciding when to clean -// up Opaque URLs before sending requests from the Transport. -// See golang.org/issue/16847 -// -// We used to enforce that the path also didn't start with "//", but -// Google's GFE accepts such paths and Chrome sends them, so ignore -// that part of the spec. See golang.org/issue/19103. -func validPseudoPath(v string) bool { - return (len(v) > 0 && v[0] == '/') || v == "*" -} - // incomparable is a zero-width, non-comparable type. Adding it to a struct // makes that struct also non-comparable, and generally doesn't add // any size (as long as it's first). diff --git a/vendor/golang.org/x/net/http2/server.go b/vendor/golang.org/x/net/http2/server.go index b55547a..51fca38 100644 --- a/vendor/golang.org/x/net/http2/server.go +++ b/vendor/golang.org/x/net/http2/server.go @@ -50,6 +50,7 @@ import ( "golang.org/x/net/http/httpguts" "golang.org/x/net/http2/hpack" + "golang.org/x/net/internal/httpcommon" ) const ( @@ -812,8 +813,7 @@ const maxCachedCanonicalHeadersKeysSize = 2048 func (sc *serverConn) canonicalHeader(v string) string { sc.serveG.check() - buildCommonHeaderMapsOnce() - cv, ok := commonCanonHeader[v] + cv, ok := httpcommon.CachedCanonicalHeader(v) if ok { return cv } @@ -1068,7 +1068,10 @@ func (sc *serverConn) serve(conf http2Config) { func (sc *serverConn) handlePingTimer(lastFrameReadTime time.Time) { if sc.pingSent { - sc.vlogf("timeout waiting for PING response") + sc.logf("timeout waiting for PING response") + if f := sc.countErrorFunc; f != nil { + f("conn_close_lost_ping") + } sc.conn.Close() return } @@ -2233,25 +2236,25 @@ func (sc *serverConn) newStream(id, pusherID uint32, state streamState) *stream func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*responseWriter, *http.Request, error) { sc.serveG.check() - rp := requestParam{ - method: f.PseudoValue("method"), - scheme: f.PseudoValue("scheme"), - authority: f.PseudoValue("authority"), - path: f.PseudoValue("path"), - protocol: f.PseudoValue("protocol"), + rp := httpcommon.ServerRequestParam{ + Method: f.PseudoValue("method"), + Scheme: f.PseudoValue("scheme"), + Authority: f.PseudoValue("authority"), + Path: f.PseudoValue("path"), + Protocol: f.PseudoValue("protocol"), } // extended connect is disabled, so we should not see :protocol - if disableExtendedConnectProtocol && rp.protocol != "" { + if disableExtendedConnectProtocol && rp.Protocol != "" { return nil, nil, sc.countError("bad_connect", streamError(f.StreamID, ErrCodeProtocol)) } - isConnect := rp.method == "CONNECT" + isConnect := rp.Method == "CONNECT" if isConnect { - if rp.protocol == "" && (rp.path != "" || rp.scheme != "" || rp.authority == "") { + if rp.Protocol == "" && (rp.Path != "" || rp.Scheme != "" || rp.Authority == "") { return nil, nil, sc.countError("bad_connect", streamError(f.StreamID, ErrCodeProtocol)) } - } else if rp.method == "" || rp.path == "" || (rp.scheme != "https" && rp.scheme != "http") { + } else if rp.Method == "" || rp.Path == "" || (rp.Scheme != "https" && rp.Scheme != "http") { // See 8.1.2.6 Malformed Requests and Responses: // // Malformed requests or responses that are detected @@ -2265,15 +2268,16 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res return nil, nil, sc.countError("bad_path_method", streamError(f.StreamID, ErrCodeProtocol)) } - rp.header = make(http.Header) + header := make(http.Header) + rp.Header = header for _, hf := range f.RegularFields() { - rp.header.Add(sc.canonicalHeader(hf.Name), hf.Value) + header.Add(sc.canonicalHeader(hf.Name), hf.Value) } - if rp.authority == "" { - rp.authority = rp.header.Get("Host") + if rp.Authority == "" { + rp.Authority = header.Get("Host") } - if rp.protocol != "" { - rp.header.Set(":protocol", rp.protocol) + if rp.Protocol != "" { + header.Set(":protocol", rp.Protocol) } rw, req, err := sc.newWriterAndRequestNoBody(st, rp) @@ -2282,7 +2286,7 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res } bodyOpen := !f.StreamEnded() if bodyOpen { - if vv, ok := rp.header["Content-Length"]; ok { + if vv, ok := rp.Header["Content-Length"]; ok { if cl, err := strconv.ParseUint(vv[0], 10, 63); err == nil { req.ContentLength = int64(cl) } else { @@ -2298,84 +2302,38 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res return rw, req, nil } -type requestParam struct { - method string - scheme, authority, path string - protocol string - header http.Header -} - -func (sc *serverConn) newWriterAndRequestNoBody(st *stream, rp requestParam) (*responseWriter, *http.Request, error) { +func (sc *serverConn) newWriterAndRequestNoBody(st *stream, rp httpcommon.ServerRequestParam) (*responseWriter, *http.Request, error) { sc.serveG.check() var tlsState *tls.ConnectionState // nil if not scheme https - if rp.scheme == "https" { + if rp.Scheme == "https" { tlsState = sc.tlsState } - needsContinue := httpguts.HeaderValuesContainsToken(rp.header["Expect"], "100-continue") - if needsContinue { - rp.header.Del("Expect") - } - // Merge Cookie headers into one "; "-delimited value. - if cookies := rp.header["Cookie"]; len(cookies) > 1 { - rp.header.Set("Cookie", strings.Join(cookies, "; ")) - } - - // Setup Trailers - var trailer http.Header - for _, v := range rp.header["Trailer"] { - for _, key := range strings.Split(v, ",") { - key = http.CanonicalHeaderKey(textproto.TrimString(key)) - switch key { - case "Transfer-Encoding", "Trailer", "Content-Length": - // Bogus. (copy of http1 rules) - // Ignore. - default: - if trailer == nil { - trailer = make(http.Header) - } - trailer[key] = nil - } - } - } - delete(rp.header, "Trailer") - - var url_ *url.URL - var requestURI string - if rp.method == "CONNECT" && rp.protocol == "" { - url_ = &url.URL{Host: rp.authority} - requestURI = rp.authority // mimic HTTP/1 server behavior - } else { - var err error - url_, err = url.ParseRequestURI(rp.path) - if err != nil { - return nil, nil, sc.countError("bad_path", streamError(st.id, ErrCodeProtocol)) - } - requestURI = rp.path + res := httpcommon.NewServerRequest(rp) + if res.InvalidReason != "" { + return nil, nil, sc.countError(res.InvalidReason, streamError(st.id, ErrCodeProtocol)) } body := &requestBody{ conn: sc, stream: st, - needsContinue: needsContinue, + needsContinue: res.NeedsContinue, } - req := &http.Request{ - Method: rp.method, - URL: url_, + req := (&http.Request{ + Method: rp.Method, + URL: res.URL, RemoteAddr: sc.remoteAddrStr, - Header: rp.header, - RequestURI: requestURI, + Header: rp.Header, + RequestURI: res.RequestURI, Proto: "HTTP/2.0", ProtoMajor: 2, ProtoMinor: 0, TLS: tlsState, - Host: rp.authority, + Host: rp.Authority, Body: body, - Trailer: trailer, - } - req = req.WithContext(st.ctx) - + Trailer: res.Trailer, + }).WithContext(st.ctx) rw := sc.newResponseWriter(st, req) return rw, req, nil } @@ -3270,12 +3228,12 @@ func (sc *serverConn) startPush(msg *startPushRequest) { // we start in "half closed (remote)" for simplicity. // See further comments at the definition of stateHalfClosedRemote. promised := sc.newStream(promisedID, msg.parent.id, stateHalfClosedRemote) - rw, req, err := sc.newWriterAndRequestNoBody(promised, requestParam{ - method: msg.method, - scheme: msg.url.Scheme, - authority: msg.url.Host, - path: msg.url.RequestURI(), - header: cloneHeader(msg.header), // clone since handler runs concurrently with writing the PUSH_PROMISE + rw, req, err := sc.newWriterAndRequestNoBody(promised, httpcommon.ServerRequestParam{ + Method: msg.method, + Scheme: msg.url.Scheme, + Authority: msg.url.Host, + Path: msg.url.RequestURI(), + Header: cloneHeader(msg.header), // clone since handler runs concurrently with writing the PUSH_PROMISE }) if err != nil { // Should not happen, since we've already validated msg.url. diff --git a/vendor/golang.org/x/net/http2/transport.go b/vendor/golang.org/x/net/http2/transport.go index 090d0e1..f26356b 100644 --- a/vendor/golang.org/x/net/http2/transport.go +++ b/vendor/golang.org/x/net/http2/transport.go @@ -25,7 +25,6 @@ import ( "net/http" "net/http/httptrace" "net/textproto" - "sort" "strconv" "strings" "sync" @@ -35,6 +34,7 @@ import ( "golang.org/x/net/http/httpguts" "golang.org/x/net/http2/hpack" "golang.org/x/net/idna" + "golang.org/x/net/internal/httpcommon" ) const ( @@ -375,6 +375,7 @@ type ClientConn struct { doNotReuse bool // whether conn is marked to not be reused for any future requests closing bool closed bool + closedOnIdle bool // true if conn was closed for idleness seenSettings bool // true if we've seen a settings frame, false otherwise seenSettingsChan chan struct{} // closed when seenSettings is true or frame reading fails wantSettingsAck bool // we sent a SETTINGS frame and haven't heard back @@ -1089,10 +1090,12 @@ func (cc *ClientConn) idleStateLocked() (st clientConnIdleState) { // If this connection has never been used for a request and is closed, // then let it take a request (which will fail). + // If the conn was closed for idleness, we're racing the idle timer; + // don't try to use the conn. (Issue #70515.) // // This avoids a situation where an error early in a connection's lifetime // goes unreported. - if cc.nextStreamID == 1 && cc.streamsReserved == 0 && cc.closed { + if cc.nextStreamID == 1 && cc.streamsReserved == 0 && cc.closed && !cc.closedOnIdle { st.canTakeNewRequest = true } @@ -1155,6 +1158,7 @@ func (cc *ClientConn) closeIfIdle() { return } cc.closed = true + cc.closedOnIdle = true nextID := cc.nextStreamID // TODO: do clients send GOAWAY too? maybe? Just Close: cc.mu.Unlock() @@ -1271,23 +1275,6 @@ func (cc *ClientConn) closeForLostPing() { // exported. At least they'll be DeepEqual for h1-vs-h2 comparisons tests. var errRequestCanceled = errors.New("net/http: request canceled") -func commaSeparatedTrailers(req *http.Request) (string, error) { - keys := make([]string, 0, len(req.Trailer)) - for k := range req.Trailer { - k = canonicalHeader(k) - switch k { - case "Transfer-Encoding", "Trailer", "Content-Length": - return "", fmt.Errorf("invalid Trailer key %q", k) - } - keys = append(keys, k) - } - if len(keys) > 0 { - sort.Strings(keys) - return strings.Join(keys, ","), nil - } - return "", nil -} - func (cc *ClientConn) responseHeaderTimeout() time.Duration { if cc.t.t1 != nil { return cc.t.t1.ResponseHeaderTimeout @@ -1299,22 +1286,6 @@ func (cc *ClientConn) responseHeaderTimeout() time.Duration { return 0 } -// checkConnHeaders checks whether req has any invalid connection-level headers. -// per RFC 7540 section 8.1.2.2: Connection-Specific Header Fields. -// Certain headers are special-cased as okay but not transmitted later. -func checkConnHeaders(req *http.Request) error { - if v := req.Header.Get("Upgrade"); v != "" { - return fmt.Errorf("http2: invalid Upgrade request header: %q", req.Header["Upgrade"]) - } - if vv := req.Header["Transfer-Encoding"]; len(vv) > 0 && (len(vv) > 1 || vv[0] != "" && vv[0] != "chunked") { - return fmt.Errorf("http2: invalid Transfer-Encoding request header: %q", vv) - } - if vv := req.Header["Connection"]; len(vv) > 0 && (len(vv) > 1 || vv[0] != "" && !asciiEqualFold(vv[0], "close") && !asciiEqualFold(vv[0], "keep-alive")) { - return fmt.Errorf("http2: invalid Connection request header: %q", vv) - } - return nil -} - // actualContentLength returns a sanitized version of // req.ContentLength, where 0 actually means zero (not unknown) and -1 // means unknown. @@ -1360,25 +1331,7 @@ func (cc *ClientConn) roundTrip(req *http.Request, streamf func(*clientStream)) donec: make(chan struct{}), } - // TODO(bradfitz): this is a copy of the logic in net/http. Unify somewhere? - if !cc.t.disableCompression() && - req.Header.Get("Accept-Encoding") == "" && - req.Header.Get("Range") == "" && - !cs.isHead { - // Request gzip only, not deflate. Deflate is ambiguous and - // not as universally supported anyway. - // See: https://zlib.net/zlib_faq.html#faq39 - // - // Note that we don't request this for HEAD requests, - // due to a bug in nginx: - // http://trac.nginx.org/nginx/ticket/358 - // https://golang.org/issue/5522 - // - // We don't request gzip if the request is for a range, since - // auto-decoding a portion of a gzipped document will just fail - // anyway. See https://golang.org/issue/8923 - cs.requestedGzip = true - } + cs.requestedGzip = httpcommon.IsRequestGzip(req.Method, req.Header, cc.t.disableCompression()) go cs.doRequest(req, streamf) @@ -1492,10 +1445,6 @@ func (cs *clientStream) writeRequest(req *http.Request, streamf func(*clientStre cc := cs.cc ctx := cs.ctx - if err := checkConnHeaders(req); err != nil { - return err - } - // wait for setting frames to be received, a server can change this value later, // but we just wait for the first settings frame var isExtendedConnect bool @@ -1659,26 +1608,39 @@ func (cs *clientStream) encodeAndWriteHeaders(req *http.Request) error { // we send: HEADERS{1}, CONTINUATION{0,} + DATA{0,} (DATA is // sent by writeRequestBody below, along with any Trailers, // again in form HEADERS{1}, CONTINUATION{0,}) - trailers, err := commaSeparatedTrailers(req) - if err != nil { - return err - } - hasTrailers := trailers != "" - contentLen := actualContentLength(req) - hasBody := contentLen != 0 - hdrs, err := cc.encodeHeaders(req, cs.requestedGzip, trailers, contentLen) + cc.hbuf.Reset() + res, err := encodeRequestHeaders(req, cs.requestedGzip, cc.peerMaxHeaderListSize, func(name, value string) { + cc.writeHeader(name, value) + }) if err != nil { - return err + return fmt.Errorf("http2: %w", err) } + hdrs := cc.hbuf.Bytes() // Write the request. - endStream := !hasBody && !hasTrailers + endStream := !res.HasBody && !res.HasTrailers cs.sentHeaders = true err = cc.writeHeaders(cs.ID, endStream, int(cc.maxFrameSize), hdrs) traceWroteHeaders(cs.trace) return err } +func encodeRequestHeaders(req *http.Request, addGzipHeader bool, peerMaxHeaderListSize uint64, headerf func(name, value string)) (httpcommon.EncodeHeadersResult, error) { + return httpcommon.EncodeHeaders(req.Context(), httpcommon.EncodeHeadersParam{ + Request: httpcommon.Request{ + Header: req.Header, + Trailer: req.Trailer, + URL: req.URL, + Host: req.Host, + Method: req.Method, + ActualContentLength: actualContentLength(req), + }, + AddGzipHeader: addGzipHeader, + PeerMaxHeaderListSize: peerMaxHeaderListSize, + DefaultUserAgent: defaultUserAgent, + }, headerf) +} + // cleanupWriteRequest performs post-request tasks. // // If err (the result of writeRequest) is non-nil and the stream is not closed, @@ -2066,218 +2028,6 @@ func (cs *clientStream) awaitFlowControl(maxBytes int) (taken int32, err error) } } -func validateHeaders(hdrs http.Header) string { - for k, vv := range hdrs { - if !httpguts.ValidHeaderFieldName(k) && k != ":protocol" { - return fmt.Sprintf("name %q", k) - } - for _, v := range vv { - if !httpguts.ValidHeaderFieldValue(v) { - // Don't include the value in the error, - // because it may be sensitive. - return fmt.Sprintf("value for header %q", k) - } - } - } - return "" -} - -var errNilRequestURL = errors.New("http2: Request.URI is nil") - -func isNormalConnect(req *http.Request) bool { - return req.Method == "CONNECT" && req.Header.Get(":protocol") == "" -} - -// requires cc.wmu be held. -func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trailers string, contentLength int64) ([]byte, error) { - cc.hbuf.Reset() - if req.URL == nil { - return nil, errNilRequestURL - } - - host := req.Host - if host == "" { - host = req.URL.Host - } - host, err := httpguts.PunycodeHostPort(host) - if err != nil { - return nil, err - } - if !httpguts.ValidHostHeader(host) { - return nil, errors.New("http2: invalid Host header") - } - - var path string - if !isNormalConnect(req) { - path = req.URL.RequestURI() - if !validPseudoPath(path) { - orig := path - path = strings.TrimPrefix(path, req.URL.Scheme+"://"+host) - if !validPseudoPath(path) { - if req.URL.Opaque != "" { - return nil, fmt.Errorf("invalid request :path %q from URL.Opaque = %q", orig, req.URL.Opaque) - } else { - return nil, fmt.Errorf("invalid request :path %q", orig) - } - } - } - } - - // Check for any invalid headers+trailers and return an error before we - // potentially pollute our hpack state. (We want to be able to - // continue to reuse the hpack encoder for future requests) - if err := validateHeaders(req.Header); err != "" { - return nil, fmt.Errorf("invalid HTTP header %s", err) - } - if err := validateHeaders(req.Trailer); err != "" { - return nil, fmt.Errorf("invalid HTTP trailer %s", err) - } - - enumerateHeaders := func(f func(name, value string)) { - // 8.1.2.3 Request Pseudo-Header Fields - // The :path pseudo-header field includes the path and query parts of the - // target URI (the path-absolute production and optionally a '?' character - // followed by the query production, see Sections 3.3 and 3.4 of - // [RFC3986]). - f(":authority", host) - m := req.Method - if m == "" { - m = http.MethodGet - } - f(":method", m) - if !isNormalConnect(req) { - f(":path", path) - f(":scheme", req.URL.Scheme) - } - if trailers != "" { - f("trailer", trailers) - } - - var didUA bool - for k, vv := range req.Header { - if asciiEqualFold(k, "host") || asciiEqualFold(k, "content-length") { - // Host is :authority, already sent. - // Content-Length is automatic, set below. - continue - } else if asciiEqualFold(k, "connection") || - asciiEqualFold(k, "proxy-connection") || - asciiEqualFold(k, "transfer-encoding") || - asciiEqualFold(k, "upgrade") || - asciiEqualFold(k, "keep-alive") { - // Per 8.1.2.2 Connection-Specific Header - // Fields, don't send connection-specific - // fields. We have already checked if any - // are error-worthy so just ignore the rest. - continue - } else if asciiEqualFold(k, "user-agent") { - // Match Go's http1 behavior: at most one - // User-Agent. If set to nil or empty string, - // then omit it. Otherwise if not mentioned, - // include the default (below). - didUA = true - if len(vv) < 1 { - continue - } - vv = vv[:1] - if vv[0] == "" { - continue - } - } else if asciiEqualFold(k, "cookie") { - // Per 8.1.2.5 To allow for better compression efficiency, the - // Cookie header field MAY be split into separate header fields, - // each with one or more cookie-pairs. - for _, v := range vv { - for { - p := strings.IndexByte(v, ';') - if p < 0 { - break - } - f("cookie", v[:p]) - p++ - // strip space after semicolon if any. - for p+1 <= len(v) && v[p] == ' ' { - p++ - } - v = v[p:] - } - if len(v) > 0 { - f("cookie", v) - } - } - continue - } - - for _, v := range vv { - f(k, v) - } - } - if shouldSendReqContentLength(req.Method, contentLength) { - f("content-length", strconv.FormatInt(contentLength, 10)) - } - if addGzipHeader { - f("accept-encoding", "gzip") - } - if !didUA { - f("user-agent", defaultUserAgent) - } - } - - // Do a first pass over the headers counting bytes to ensure - // we don't exceed cc.peerMaxHeaderListSize. This is done as a - // separate pass before encoding the headers to prevent - // modifying the hpack state. - hlSize := uint64(0) - enumerateHeaders(func(name, value string) { - hf := hpack.HeaderField{Name: name, Value: value} - hlSize += uint64(hf.Size()) - }) - - if hlSize > cc.peerMaxHeaderListSize { - return nil, errRequestHeaderListSize - } - - trace := httptrace.ContextClientTrace(req.Context()) - traceHeaders := traceHasWroteHeaderField(trace) - - // Header list size is ok. Write the headers. - enumerateHeaders(func(name, value string) { - name, ascii := lowerHeader(name) - if !ascii { - // Skip writing invalid headers. Per RFC 7540, Section 8.1.2, header - // field names have to be ASCII characters (just as in HTTP/1.x). - return - } - cc.writeHeader(name, value) - if traceHeaders { - traceWroteHeaderField(trace, name, value) - } - }) - - return cc.hbuf.Bytes(), nil -} - -// shouldSendReqContentLength reports whether the http2.Transport should send -// a "content-length" request header. This logic is basically a copy of the net/http -// transferWriter.shouldSendContentLength. -// The contentLength is the corrected contentLength (so 0 means actually 0, not unknown). -// -1 means unknown. -func shouldSendReqContentLength(method string, contentLength int64) bool { - if contentLength > 0 { - return true - } - if contentLength < 0 { - return false - } - // For zero bodies, whether we send a content-length depends on the method. - // It also kinda doesn't matter for http2 either way, with END_STREAM. - switch method { - case "POST", "PUT", "PATCH": - return true - default: - return false - } -} - // requires cc.wmu be held. func (cc *ClientConn) encodeTrailers(trailer http.Header) ([]byte, error) { cc.hbuf.Reset() @@ -2294,7 +2044,7 @@ func (cc *ClientConn) encodeTrailers(trailer http.Header) ([]byte, error) { } for k, vv := range trailer { - lowKey, ascii := lowerHeader(k) + lowKey, ascii := httpcommon.LowerHeader(k) if !ascii { // Skip writing invalid headers. Per RFC 7540, Section 8.1.2, header // field names have to be ASCII characters (just as in HTTP/1.x). @@ -2434,9 +2184,12 @@ func (rl *clientConnReadLoop) cleanup() { // This avoids a situation where new connections are constantly created, // added to the pool, fail, and are removed from the pool, without any error // being surfaced to the user. - const unusedWaitTime = 5 * time.Second + unusedWaitTime := 5 * time.Second + if cc.idleTimeout > 0 && unusedWaitTime > cc.idleTimeout { + unusedWaitTime = cc.idleTimeout + } idleTime := cc.t.now().Sub(cc.lastActive) - if atomic.LoadUint32(&cc.atomicReused) == 0 && idleTime < unusedWaitTime { + if atomic.LoadUint32(&cc.atomicReused) == 0 && idleTime < unusedWaitTime && !cc.closedOnIdle { cc.idleTimer = cc.t.afterFunc(unusedWaitTime-idleTime, func() { cc.t.connPool().MarkDead(cc) }) @@ -2457,6 +2210,13 @@ func (rl *clientConnReadLoop) cleanup() { } cc.cond.Broadcast() cc.mu.Unlock() + + if !cc.seenSettings { + // If we have a pending request that wants extended CONNECT, + // let it continue and fail with the connection error. + cc.extendedConnectAllowed = true + close(cc.seenSettingsChan) + } } // countReadFrameError calls Transport.CountError with a string @@ -2549,9 +2309,6 @@ func (rl *clientConnReadLoop) run() error { if VerboseLogs { cc.vlogf("http2: Transport conn %p received error from processing frame %v: %v", cc, summarizeFrame(f), err) } - if !cc.seenSettings { - close(cc.seenSettingsChan) - } return err } } @@ -2646,7 +2403,7 @@ func (rl *clientConnReadLoop) handleResponse(cs *clientStream, f *MetaHeadersFra Status: status + " " + http.StatusText(statusCode), } for _, hf := range regularFields { - key := canonicalHeader(hf.Name) + key := httpcommon.CanonicalHeader(hf.Name) if key == "Trailer" { t := res.Trailer if t == nil { @@ -2654,7 +2411,7 @@ func (rl *clientConnReadLoop) handleResponse(cs *clientStream, f *MetaHeadersFra res.Trailer = t } foreachHeaderElement(hf.Value, func(v string) { - t[canonicalHeader(v)] = nil + t[httpcommon.CanonicalHeader(v)] = nil }) } else { vv := header[key] @@ -2778,7 +2535,7 @@ func (rl *clientConnReadLoop) processTrailers(cs *clientStream, f *MetaHeadersFr trailer := make(http.Header) for _, hf := range f.RegularFields() { - key := canonicalHeader(hf.Name) + key := httpcommon.CanonicalHeader(hf.Name) trailer[key] = append(trailer[key], hf.Value) } cs.trailer = trailer @@ -3324,7 +3081,7 @@ func (cc *ClientConn) writeStreamReset(streamID uint32, code ErrCode, ping bool, var ( errResponseHeaderListSize = errors.New("http2: response header list larger than advertised limit") - errRequestHeaderListSize = errors.New("http2: request header list larger than peer's advertised limit") + errRequestHeaderListSize = httpcommon.ErrRequestHeaderListSize ) func (cc *ClientConn) logf(format string, args ...interface{}) { @@ -3508,16 +3265,6 @@ func traceFirstResponseByte(trace *httptrace.ClientTrace) { } } -func traceHasWroteHeaderField(trace *httptrace.ClientTrace) bool { - return trace != nil && trace.WroteHeaderField != nil -} - -func traceWroteHeaderField(trace *httptrace.ClientTrace, k, v string) { - if trace != nil && trace.WroteHeaderField != nil { - trace.WroteHeaderField(k, []string{v}) - } -} - func traceGot1xxResponseFunc(trace *httptrace.ClientTrace) func(int, textproto.MIMEHeader) error { if trace != nil { return trace.Got1xxResponse diff --git a/vendor/golang.org/x/net/http2/write.go b/vendor/golang.org/x/net/http2/write.go index 6ff6bee..fdb35b9 100644 --- a/vendor/golang.org/x/net/http2/write.go +++ b/vendor/golang.org/x/net/http2/write.go @@ -13,6 +13,7 @@ import ( "golang.org/x/net/http/httpguts" "golang.org/x/net/http2/hpack" + "golang.org/x/net/internal/httpcommon" ) // writeFramer is implemented by any type that is used to write frames. @@ -351,7 +352,7 @@ func encodeHeaders(enc *hpack.Encoder, h http.Header, keys []string) { } for _, k := range keys { vv := h[k] - k, ascii := lowerHeader(k) + k, ascii := httpcommon.LowerHeader(k) if !ascii { // Skip writing invalid headers. Per RFC 7540, Section 8.1.2, header // field names have to be ASCII characters (just as in HTTP/1.x). diff --git a/vendor/golang.org/x/net/internal/httpcommon/ascii.go b/vendor/golang.org/x/net/internal/httpcommon/ascii.go new file mode 100644 index 0000000..ed14da5 --- /dev/null +++ b/vendor/golang.org/x/net/internal/httpcommon/ascii.go @@ -0,0 +1,53 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package httpcommon + +import "strings" + +// The HTTP protocols are defined in terms of ASCII, not Unicode. This file +// contains helper functions which may use Unicode-aware functions which would +// otherwise be unsafe and could introduce vulnerabilities if used improperly. + +// asciiEqualFold is strings.EqualFold, ASCII only. It reports whether s and t +// are equal, ASCII-case-insensitively. +func asciiEqualFold(s, t string) bool { + if len(s) != len(t) { + return false + } + for i := 0; i < len(s); i++ { + if lower(s[i]) != lower(t[i]) { + return false + } + } + return true +} + +// lower returns the ASCII lowercase version of b. +func lower(b byte) byte { + if 'A' <= b && b <= 'Z' { + return b + ('a' - 'A') + } + return b +} + +// isASCIIPrint returns whether s is ASCII and printable according to +// https://tools.ietf.org/html/rfc20#section-4.2. +func isASCIIPrint(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] < ' ' || s[i] > '~' { + return false + } + } + return true +} + +// asciiToLower returns the lowercase version of s if s is ASCII and printable, +// and whether or not it was. +func asciiToLower(s string) (lower string, ok bool) { + if !isASCIIPrint(s) { + return "", false + } + return strings.ToLower(s), true +} diff --git a/vendor/golang.org/x/net/http2/headermap.go b/vendor/golang.org/x/net/internal/httpcommon/headermap.go similarity index 74% rename from vendor/golang.org/x/net/http2/headermap.go rename to vendor/golang.org/x/net/internal/httpcommon/headermap.go index 149b3dd..92483d8 100644 --- a/vendor/golang.org/x/net/http2/headermap.go +++ b/vendor/golang.org/x/net/internal/httpcommon/headermap.go @@ -1,11 +1,11 @@ -// Copyright 2014 The Go Authors. All rights reserved. +// Copyright 2025 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package http2 +package httpcommon import ( - "net/http" + "net/textproto" "sync" ) @@ -82,13 +82,15 @@ func buildCommonHeaderMaps() { commonLowerHeader = make(map[string]string, len(common)) commonCanonHeader = make(map[string]string, len(common)) for _, v := range common { - chk := http.CanonicalHeaderKey(v) + chk := textproto.CanonicalMIMEHeaderKey(v) commonLowerHeader[chk] = v commonCanonHeader[v] = chk } } -func lowerHeader(v string) (lower string, ascii bool) { +// LowerHeader returns the lowercase form of a header name, +// used on the wire for HTTP/2 and HTTP/3 requests. +func LowerHeader(v string) (lower string, ascii bool) { buildCommonHeaderMapsOnce() if s, ok := commonLowerHeader[v]; ok { return s, true @@ -96,10 +98,18 @@ func lowerHeader(v string) (lower string, ascii bool) { return asciiToLower(v) } -func canonicalHeader(v string) string { +// CanonicalHeader canonicalizes a header name. (For example, "host" becomes "Host".) +func CanonicalHeader(v string) string { buildCommonHeaderMapsOnce() if s, ok := commonCanonHeader[v]; ok { return s } - return http.CanonicalHeaderKey(v) + return textproto.CanonicalMIMEHeaderKey(v) +} + +// CachedCanonicalHeader returns the canonical form of a well-known header name. +func CachedCanonicalHeader(v string) (string, bool) { + buildCommonHeaderMapsOnce() + s, ok := commonCanonHeader[v] + return s, ok } diff --git a/vendor/golang.org/x/net/internal/httpcommon/request.go b/vendor/golang.org/x/net/internal/httpcommon/request.go new file mode 100644 index 0000000..4b70553 --- /dev/null +++ b/vendor/golang.org/x/net/internal/httpcommon/request.go @@ -0,0 +1,467 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package httpcommon + +import ( + "context" + "errors" + "fmt" + "net/http/httptrace" + "net/textproto" + "net/url" + "sort" + "strconv" + "strings" + + "golang.org/x/net/http/httpguts" + "golang.org/x/net/http2/hpack" +) + +var ( + ErrRequestHeaderListSize = errors.New("request header list larger than peer's advertised limit") +) + +// Request is a subset of http.Request. +// It'd be simpler to pass an *http.Request, of course, but we can't depend on net/http +// without creating a dependency cycle. +type Request struct { + URL *url.URL + Method string + Host string + Header map[string][]string + Trailer map[string][]string + ActualContentLength int64 // 0 means 0, -1 means unknown +} + +// EncodeHeadersParam is parameters to EncodeHeaders. +type EncodeHeadersParam struct { + Request Request + + // AddGzipHeader indicates that an "accept-encoding: gzip" header should be + // added to the request. + AddGzipHeader bool + + // PeerMaxHeaderListSize, when non-zero, is the peer's MAX_HEADER_LIST_SIZE setting. + PeerMaxHeaderListSize uint64 + + // DefaultUserAgent is the User-Agent header to send when the request + // neither contains a User-Agent nor disables it. + DefaultUserAgent string +} + +// EncodeHeadersParam is the result of EncodeHeaders. +type EncodeHeadersResult struct { + HasBody bool + HasTrailers bool +} + +// EncodeHeaders constructs request headers common to HTTP/2 and HTTP/3. +// It validates a request and calls headerf with each pseudo-header and header +// for the request. +// The headerf function is called with the validated, canonicalized header name. +func EncodeHeaders(ctx context.Context, param EncodeHeadersParam, headerf func(name, value string)) (res EncodeHeadersResult, _ error) { + req := param.Request + + // Check for invalid connection-level headers. + if err := checkConnHeaders(req.Header); err != nil { + return res, err + } + + if req.URL == nil { + return res, errors.New("Request.URL is nil") + } + + host := req.Host + if host == "" { + host = req.URL.Host + } + host, err := httpguts.PunycodeHostPort(host) + if err != nil { + return res, err + } + if !httpguts.ValidHostHeader(host) { + return res, errors.New("invalid Host header") + } + + // isNormalConnect is true if this is a non-extended CONNECT request. + isNormalConnect := false + var protocol string + if vv := req.Header[":protocol"]; len(vv) > 0 { + protocol = vv[0] + } + if req.Method == "CONNECT" && protocol == "" { + isNormalConnect = true + } else if protocol != "" && req.Method != "CONNECT" { + return res, errors.New("invalid :protocol header in non-CONNECT request") + } + + // Validate the path, except for non-extended CONNECT requests which have no path. + var path string + if !isNormalConnect { + path = req.URL.RequestURI() + if !validPseudoPath(path) { + orig := path + path = strings.TrimPrefix(path, req.URL.Scheme+"://"+host) + if !validPseudoPath(path) { + if req.URL.Opaque != "" { + return res, fmt.Errorf("invalid request :path %q from URL.Opaque = %q", orig, req.URL.Opaque) + } else { + return res, fmt.Errorf("invalid request :path %q", orig) + } + } + } + } + + // Check for any invalid headers+trailers and return an error before we + // potentially pollute our hpack state. (We want to be able to + // continue to reuse the hpack encoder for future requests) + if err := validateHeaders(req.Header); err != "" { + return res, fmt.Errorf("invalid HTTP header %s", err) + } + if err := validateHeaders(req.Trailer); err != "" { + return res, fmt.Errorf("invalid HTTP trailer %s", err) + } + + trailers, err := commaSeparatedTrailers(req.Trailer) + if err != nil { + return res, err + } + + enumerateHeaders := func(f func(name, value string)) { + // 8.1.2.3 Request Pseudo-Header Fields + // The :path pseudo-header field includes the path and query parts of the + // target URI (the path-absolute production and optionally a '?' character + // followed by the query production, see Sections 3.3 and 3.4 of + // [RFC3986]). + f(":authority", host) + m := req.Method + if m == "" { + m = "GET" + } + f(":method", m) + if !isNormalConnect { + f(":path", path) + f(":scheme", req.URL.Scheme) + } + if protocol != "" { + f(":protocol", protocol) + } + if trailers != "" { + f("trailer", trailers) + } + + var didUA bool + for k, vv := range req.Header { + if asciiEqualFold(k, "host") || asciiEqualFold(k, "content-length") { + // Host is :authority, already sent. + // Content-Length is automatic, set below. + continue + } else if asciiEqualFold(k, "connection") || + asciiEqualFold(k, "proxy-connection") || + asciiEqualFold(k, "transfer-encoding") || + asciiEqualFold(k, "upgrade") || + asciiEqualFold(k, "keep-alive") { + // Per 8.1.2.2 Connection-Specific Header + // Fields, don't send connection-specific + // fields. We have already checked if any + // are error-worthy so just ignore the rest. + continue + } else if asciiEqualFold(k, "user-agent") { + // Match Go's http1 behavior: at most one + // User-Agent. If set to nil or empty string, + // then omit it. Otherwise if not mentioned, + // include the default (below). + didUA = true + if len(vv) < 1 { + continue + } + vv = vv[:1] + if vv[0] == "" { + continue + } + } else if asciiEqualFold(k, "cookie") { + // Per 8.1.2.5 To allow for better compression efficiency, the + // Cookie header field MAY be split into separate header fields, + // each with one or more cookie-pairs. + for _, v := range vv { + for { + p := strings.IndexByte(v, ';') + if p < 0 { + break + } + f("cookie", v[:p]) + p++ + // strip space after semicolon if any. + for p+1 <= len(v) && v[p] == ' ' { + p++ + } + v = v[p:] + } + if len(v) > 0 { + f("cookie", v) + } + } + continue + } else if k == ":protocol" { + // :protocol pseudo-header was already sent above. + continue + } + + for _, v := range vv { + f(k, v) + } + } + if shouldSendReqContentLength(req.Method, req.ActualContentLength) { + f("content-length", strconv.FormatInt(req.ActualContentLength, 10)) + } + if param.AddGzipHeader { + f("accept-encoding", "gzip") + } + if !didUA { + f("user-agent", param.DefaultUserAgent) + } + } + + // Do a first pass over the headers counting bytes to ensure + // we don't exceed cc.peerMaxHeaderListSize. This is done as a + // separate pass before encoding the headers to prevent + // modifying the hpack state. + if param.PeerMaxHeaderListSize > 0 { + hlSize := uint64(0) + enumerateHeaders(func(name, value string) { + hf := hpack.HeaderField{Name: name, Value: value} + hlSize += uint64(hf.Size()) + }) + + if hlSize > param.PeerMaxHeaderListSize { + return res, ErrRequestHeaderListSize + } + } + + trace := httptrace.ContextClientTrace(ctx) + + // Header list size is ok. Write the headers. + enumerateHeaders(func(name, value string) { + name, ascii := LowerHeader(name) + if !ascii { + // Skip writing invalid headers. Per RFC 7540, Section 8.1.2, header + // field names have to be ASCII characters (just as in HTTP/1.x). + return + } + + headerf(name, value) + + if trace != nil && trace.WroteHeaderField != nil { + trace.WroteHeaderField(name, []string{value}) + } + }) + + res.HasBody = req.ActualContentLength != 0 + res.HasTrailers = trailers != "" + return res, nil +} + +// IsRequestGzip reports whether we should add an Accept-Encoding: gzip header +// for a request. +func IsRequestGzip(method string, header map[string][]string, disableCompression bool) bool { + // TODO(bradfitz): this is a copy of the logic in net/http. Unify somewhere? + if !disableCompression && + len(header["Accept-Encoding"]) == 0 && + len(header["Range"]) == 0 && + method != "HEAD" { + // Request gzip only, not deflate. Deflate is ambiguous and + // not as universally supported anyway. + // See: https://zlib.net/zlib_faq.html#faq39 + // + // Note that we don't request this for HEAD requests, + // due to a bug in nginx: + // http://trac.nginx.org/nginx/ticket/358 + // https://golang.org/issue/5522 + // + // We don't request gzip if the request is for a range, since + // auto-decoding a portion of a gzipped document will just fail + // anyway. See https://golang.org/issue/8923 + return true + } + return false +} + +// checkConnHeaders checks whether req has any invalid connection-level headers. +// +// https://www.rfc-editor.org/rfc/rfc9114.html#section-4.2-3 +// https://www.rfc-editor.org/rfc/rfc9113.html#section-8.2.2-1 +// +// Certain headers are special-cased as okay but not transmitted later. +// For example, we allow "Transfer-Encoding: chunked", but drop the header when encoding. +func checkConnHeaders(h map[string][]string) error { + if vv := h["Upgrade"]; len(vv) > 0 && (vv[0] != "" && vv[0] != "chunked") { + return fmt.Errorf("invalid Upgrade request header: %q", vv) + } + if vv := h["Transfer-Encoding"]; len(vv) > 0 && (len(vv) > 1 || vv[0] != "" && vv[0] != "chunked") { + return fmt.Errorf("invalid Transfer-Encoding request header: %q", vv) + } + if vv := h["Connection"]; len(vv) > 0 && (len(vv) > 1 || vv[0] != "" && !asciiEqualFold(vv[0], "close") && !asciiEqualFold(vv[0], "keep-alive")) { + return fmt.Errorf("invalid Connection request header: %q", vv) + } + return nil +} + +func commaSeparatedTrailers(trailer map[string][]string) (string, error) { + keys := make([]string, 0, len(trailer)) + for k := range trailer { + k = CanonicalHeader(k) + switch k { + case "Transfer-Encoding", "Trailer", "Content-Length": + return "", fmt.Errorf("invalid Trailer key %q", k) + } + keys = append(keys, k) + } + if len(keys) > 0 { + sort.Strings(keys) + return strings.Join(keys, ","), nil + } + return "", nil +} + +// validPseudoPath reports whether v is a valid :path pseudo-header +// value. It must be either: +// +// - a non-empty string starting with '/' +// - the string '*', for OPTIONS requests. +// +// For now this is only used a quick check for deciding when to clean +// up Opaque URLs before sending requests from the Transport. +// See golang.org/issue/16847 +// +// We used to enforce that the path also didn't start with "//", but +// Google's GFE accepts such paths and Chrome sends them, so ignore +// that part of the spec. See golang.org/issue/19103. +func validPseudoPath(v string) bool { + return (len(v) > 0 && v[0] == '/') || v == "*" +} + +func validateHeaders(hdrs map[string][]string) string { + for k, vv := range hdrs { + if !httpguts.ValidHeaderFieldName(k) && k != ":protocol" { + return fmt.Sprintf("name %q", k) + } + for _, v := range vv { + if !httpguts.ValidHeaderFieldValue(v) { + // Don't include the value in the error, + // because it may be sensitive. + return fmt.Sprintf("value for header %q", k) + } + } + } + return "" +} + +// shouldSendReqContentLength reports whether we should send +// a "content-length" request header. This logic is basically a copy of the net/http +// transferWriter.shouldSendContentLength. +// The contentLength is the corrected contentLength (so 0 means actually 0, not unknown). +// -1 means unknown. +func shouldSendReqContentLength(method string, contentLength int64) bool { + if contentLength > 0 { + return true + } + if contentLength < 0 { + return false + } + // For zero bodies, whether we send a content-length depends on the method. + // It also kinda doesn't matter for http2 either way, with END_STREAM. + switch method { + case "POST", "PUT", "PATCH": + return true + default: + return false + } +} + +// ServerRequestParam is parameters to NewServerRequest. +type ServerRequestParam struct { + Method string + Scheme, Authority, Path string + Protocol string + Header map[string][]string +} + +// ServerRequestResult is the result of NewServerRequest. +type ServerRequestResult struct { + // Various http.Request fields. + URL *url.URL + RequestURI string + Trailer map[string][]string + + NeedsContinue bool // client provided an "Expect: 100-continue" header + + // If the request should be rejected, this is a short string suitable for passing + // to the http2 package's CountError function. + // It might be a bit odd to return errors this way rather than returing an error, + // but this ensures we don't forget to include a CountError reason. + InvalidReason string +} + +func NewServerRequest(rp ServerRequestParam) ServerRequestResult { + needsContinue := httpguts.HeaderValuesContainsToken(rp.Header["Expect"], "100-continue") + if needsContinue { + delete(rp.Header, "Expect") + } + // Merge Cookie headers into one "; "-delimited value. + if cookies := rp.Header["Cookie"]; len(cookies) > 1 { + rp.Header["Cookie"] = []string{strings.Join(cookies, "; ")} + } + + // Setup Trailers + var trailer map[string][]string + for _, v := range rp.Header["Trailer"] { + for _, key := range strings.Split(v, ",") { + key = textproto.CanonicalMIMEHeaderKey(textproto.TrimString(key)) + switch key { + case "Transfer-Encoding", "Trailer", "Content-Length": + // Bogus. (copy of http1 rules) + // Ignore. + default: + if trailer == nil { + trailer = make(map[string][]string) + } + trailer[key] = nil + } + } + } + delete(rp.Header, "Trailer") + + // "':authority' MUST NOT include the deprecated userinfo subcomponent + // for "http" or "https" schemed URIs." + // https://www.rfc-editor.org/rfc/rfc9113.html#section-8.3.1-2.3.8 + if strings.IndexByte(rp.Authority, '@') != -1 && (rp.Scheme == "http" || rp.Scheme == "https") { + return ServerRequestResult{ + InvalidReason: "userinfo_in_authority", + } + } + + var url_ *url.URL + var requestURI string + if rp.Method == "CONNECT" && rp.Protocol == "" { + url_ = &url.URL{Host: rp.Authority} + requestURI = rp.Authority // mimic HTTP/1 server behavior + } else { + var err error + url_, err = url.ParseRequestURI(rp.Path) + if err != nil { + return ServerRequestResult{ + InvalidReason: "bad_path", + } + } + requestURI = rp.Path + } + + return ServerRequestResult{ + URL: url_, + NeedsContinue: needsContinue, + RequestURI: requestURI, + Trailer: trailer, + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 15e0584..c9c6d19 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,13 +1,16 @@ -# github.com/elazarl/goproxy v0.0.0-20241218172127-ac55c7698e0d -## explicit; go 1.20 +# github.com/elazarl/goproxy v1.8.3 +## explicit; go 1.23.0 github.com/elazarl/goproxy -# golang.org/x/net v0.32.0 -## explicit; go 1.18 +github.com/elazarl/goproxy/internal/http1parser +github.com/elazarl/goproxy/internal/signer +# golang.org/x/net v0.43.0 +## explicit; go 1.23.0 golang.org/x/net/http/httpguts golang.org/x/net/http2 golang.org/x/net/http2/hpack golang.org/x/net/idna -# golang.org/x/text v0.24.0 +golang.org/x/net/internal/httpcommon +# golang.org/x/text v0.28.0 ## explicit; go 1.23.0 golang.org/x/text/secure/bidirule golang.org/x/text/transform