Repository: elazarl/goproxy Branch: master Commit: ffdf0b284e35 Files: 103 Total size: 367.9 KB Directory structure: gitextract_ij9ic0cm/ ├── .github/ │ ├── FUNDING.yml │ ├── dependabot.yml │ └── workflows/ │ └── go.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── README.md ├── actions.go ├── all.bash ├── ca.pem ├── certs/ │ ├── openssl-gen.sh │ └── openssl.cnf ├── certs.go ├── ctx.go ├── dispatcher.go ├── dispatcher_test.go ├── doc.go ├── examples/ │ ├── base/ │ │ ├── README.md │ │ └── main.go │ ├── cascadeproxy/ │ │ ├── README.md │ │ └── main.go │ ├── cascadeproxy-socks/ │ │ ├── README.md │ │ ├── socks5proxyserver/ │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ └── main.go │ │ └── socksproxy.go │ ├── certstorage/ │ │ ├── README.md │ │ ├── cache.go │ │ └── main.go │ ├── customca/ │ │ ├── README.md │ │ ├── cert.go │ │ └── main.go │ ├── go.mod │ ├── go.sum │ ├── goproxy-httpdump/ │ │ ├── README.md │ │ └── httpdump.go │ ├── goproxy-transparent/ │ │ ├── README.md │ │ ├── proxy.sh │ │ └── transparent.go │ ├── hijack/ │ │ ├── README.md │ │ └── main.go │ ├── html-parser/ │ │ ├── README.md │ │ ├── jquery1.html │ │ ├── jquery2.html │ │ ├── jquery_homepage.html │ │ ├── jquery_test.go │ │ ├── main.go │ │ ├── php_man.html │ │ └── w3schools.html │ ├── image-manipulation/ │ │ ├── README.md │ │ └── main.go │ ├── redirect-https/ │ │ ├── README.md │ │ └── main.go │ ├── remove-https/ │ │ ├── README.md │ │ └── main.go │ ├── request-filtering/ │ │ ├── README.md │ │ └── noreddit.go │ ├── socket-keepalive/ │ │ ├── README.md │ │ └── keepalive.go │ └── websockets/ │ ├── README.md │ ├── localhost-key.pem │ ├── localhost.pem │ └── main.go ├── ext/ │ ├── auth/ │ │ ├── basic.go │ │ └── basic_test.go │ ├── go.mod │ ├── go.sum │ ├── har/ │ │ ├── logger.go │ │ ├── logger_test.go │ │ └── types.go │ ├── html/ │ │ ├── cp1255.html │ │ ├── cp1255.txt │ │ ├── html.go │ │ └── html_test.go │ ├── image/ │ │ ├── image.go │ │ └── image_test.go │ └── limitation/ │ ├── concurrency.go │ └── concurrency_test.go ├── go.mod ├── go.sum ├── h2.go ├── http.go ├── https.go ├── internal/ │ ├── http1parser/ │ │ ├── header.go │ │ ├── header_test.go │ │ ├── request.go │ │ └── request_test.go │ └── signer/ │ ├── counterecryptor.go │ ├── counterecryptor_test.go │ ├── signer.go │ └── signer_test.go ├── key.pem ├── logger.go ├── proxy.go ├── proxy_test.go ├── regretable/ │ ├── regretreader.go │ └── regretreader_test.go ├── responses.go ├── transport/ │ ├── roundtripper.go │ ├── transport.go │ └── util.go ├── websocket.go └── websocket_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ open_collective: goproxy ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: # GitHub Actions Pipeline - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" # Go packages - package-ecosystem: "gomod" directories: - / - /ext - /examples schedule: interval: "daily" ignore: - dependency-name: "github.com/elazarl/goproxy" ================================================ FILE: .github/workflows/go.yml ================================================ name: Code Check on: workflow_dispatch: pull_request: types: - opened - synchronize - reopened jobs: build: name: Build And Test Go code runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod # https://stackoverflow.com/questions/76269119/github-actions-go-lambda-project-different-sha256sums - name: Build run: go build -v -buildvcs=false ./... env: # Make sure to not use dependencies that rely on CGO CGO_ENABLED: 0 # Make sure to detect eventual race conditions # (CGO must be enabled to use -race detector) - name: Test # -count=2 ensures that test fixtures cleanup after themselves # because any leftover state will generally cause the second run to fail. run: go test -race -p 1 -v -shuffle=on -count=2 ./... - name: Linter uses: golangci/golangci-lint-action@v9 with: version: latest env: GOFLAGS: "-buildvcs=false" ================================================ FILE: .gitignore ================================================ bin *.swp ================================================ FILE: .golangci.yml ================================================ 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$ ================================================ FILE: LICENSE ================================================ Copyright (c) 2012 Elazar Leibovich. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Elazar Leibovich. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ # GoProxy ![Status](https://github.com/elazarl/goproxy/workflows/Go/badge.svg) [![GoDoc](https://pkg.go.dev/badge/github.com/elazarl/goproxy)](https://pkg.go.dev/github.com/elazarl/goproxy) [![Go Report](https://goreportcard.com/badge/github.com/elazarl/goproxy)](https://goreportcard.com/report/github.com/elazarl/goproxy) [![BSD-3 License](https://img.shields.io/badge/License-BSD%203--Clause-orange.svg)](https://opensource.org/licenses/BSD-3-Clause) [![Pull Requests](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://makeapullrequest.com) [![Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go?tab=readme-ov-file#networking) GoProxy is a library to create a `customized` HTTP/HTTPS `proxy server` using Go (aka Golang), with several configurable settings available. 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 ``` 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 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 ( "log" "net/http" "github.com/elazarl/goproxy" ) func main() { proxy := goproxy.NewProxyHttpServer() proxy.Verbose = true log.Fatal(http.ListenAndServe(":8080", 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( func(r *http.Request,ctx *goproxy.ProxyCtx)(*http.Request,*http.Response) { r.Header.Set("X-GoProxy","yxorPoG-X") return r,nil }) ``` 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 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. ### 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(req *http.Request,ctx *goproxy.ProxyCtx)(*http.Request,*http.Response) { if h,_,_ := time.Now().Clock(); h >= 8 && h <= 17 { resp := goproxy.NewResponse(req, goproxy.ContentTypeText, http.StatusForbidden, "Don't waste your time!") return req, resp } return req, nil }) ``` `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! ## Request & Response manipulation 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 establishes connection with the destination host httpsHandlers []HttpsHandler // 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 forwards the Response to the client respHandlers []RespHandler ``` 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()) // Add handlers to reqHandlers proxy.OnRequest(some ReqConditions).Do(YourReqHandlerFunc()) // Add handlers to respHandlers proxy.OnResponse(some RespConditions).Do(YourRespHandlerFunc()) ``` Example: ```go // This rejects the HTTPS request to *.reddit.com during HTTP CONNECT phase. // Reddit URL check is case-insensitive because of (?i), so the block will work also if the user types something like rEdDit.com. proxy.OnRequest(goproxy.ReqHostMatches(regexp.MustCompile("(?i)reddit.*:443$"))).HandleConnect(goproxy.AlwaysReject) // Be careful about this example! It shows you a common error that you // need to avoid. // This will NOT reject the HTTPS request with URL ending with .gif because, // if the scheme is HTTPS, the proxy will receive only URL.Hostname // and URL.Port during the HTTP CONNECT phase. proxy.OnRequest(goproxy.UrlMatches(regexp.MustCompile(`.*gif$`))).HandleConnect(goproxy.AlwaysReject) // 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()) ``` ## 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. 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. 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 }) ``` ### 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

================================================ FILE: actions.go ================================================ package goproxy import "net/http" // ReqHandler will "tamper" with the request coming to the proxy server // If Handle returns req,nil the proxy will send the returned request // to the destination server. If it returns nil,resp the proxy will // skip sending any requests, and will simply return the response `resp` // to the client. 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. type FuncReqHandler func(req *http.Request, ctx *ProxyCtx) (*http.Request, *http.Response) // FuncReqHandler.Handle(req,ctx) <=> FuncReqHandler(req,ctx). func (f FuncReqHandler) Handle(req *http.Request, ctx *ProxyCtx) (*http.Request, *http.Response) { return f(req, ctx) } // 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. type RespHandler interface { Handle(resp *http.Response, ctx *ProxyCtx) *http.Response } // 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). func (f FuncRespHandler) Handle(resp *http.Response, ctx *ProxyCtx) *http.Response { return f(resp, ctx) } // When a client send a CONNECT request to a host, the request is filtered through // all the HttpsHandlers the proxy has, and if one returns true, the connection is // sniffed using Man in the Middle attack. // That is, the proxy will create a TLS connection with the client, another TLS // connection with the destination the client wished to connect to, and would // 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). type HttpsHandler interface { HandleConnect(req string, ctx *ProxyCtx) (*ConnectAction, string) } // 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. func (f FuncHttpsHandler) HandleConnect(host string, ctx *ProxyCtx) (*ConnectAction, string) { return f(host, ctx) } ================================================ FILE: all.bash ================================================ #!/bin/bash go test || exit for action in $@; do go $action; done mkdir -p bin find regretable examples/* ext/* -maxdepth 0 -type d | while read d; do (cd $d go build -o ../../bin/$(basename $d) find *_test.go -maxdepth 0 2>/dev/null|while read f;do for action in $@; do go $action; done go test break done) done ================================================ FILE: ca.pem ================================================ -----BEGIN CERTIFICATE----- MIIF9DCCA9ygAwIBAgIJAODqYUwoVjJkMA0GCSqGSIb3DQEBCwUAMIGOMQswCQYD VQQGEwJJTDEPMA0GA1UECAwGQ2VudGVyMQwwCgYDVQQHDANMb2QxEDAOBgNVBAoM B0dvUHJveHkxEDAOBgNVBAsMB0dvUHJveHkxGjAYBgNVBAMMEWdvcHJveHkuZ2l0 aHViLmlvMSAwHgYJKoZIhvcNAQkBFhFlbGF6YXJsQGdtYWlsLmNvbTAeFw0xNzA0 MDUyMDAwMTBaFw0zNzAzMzEyMDAwMTBaMIGOMQswCQYDVQQGEwJJTDEPMA0GA1UE CAwGQ2VudGVyMQwwCgYDVQQHDANMb2QxEDAOBgNVBAoMB0dvUHJveHkxEDAOBgNV BAsMB0dvUHJveHkxGjAYBgNVBAMMEWdvcHJveHkuZ2l0aHViLmlvMSAwHgYJKoZI hvcNAQkBFhFlbGF6YXJsQGdtYWlsLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIP ADCCAgoCggIBAJ4Qy+H6hhoY1s0QRcvIhxrjSHaO/RbaFj3rwqcnpOgFq07gRdI9 3c0TFKQJHpgv6feLRhEvX/YllFYu4J35lM9ZcYY4qlKFuStcX8Jm8fqpgtmAMBzP sqtqDi8M9RQGKENzU9IFOnCV7SAeh45scMuI3wz8wrjBcH7zquHkvqUSYZz035t9 V6WTrHyTEvT4w+lFOVN2bA/6DAIxrjBiF6DhoJqnha0SZtDfv77XpwGG3EhA/qoh hiYrDruYK7zJdESQL44LwzMPupVigqalfv+YHfQjbhT951IVurW2NJgRyBE62dLr lHYdtT9tCTCrd+KJNMJ+jp9hAjdIu1Br/kifU4F4+4ZLMR9Ueji0GkkPKsYdyMnq j0p0PogyvP1l4qmboPImMYtaoFuYmMYlebgC9LN10bL91K4+jLt0I1YntEzrqgJo WsJztYDw543NzSy5W+/cq4XRYgtq1b0RWwuUiswezmMoeyHZ8BQJe2xMjAOllASD fqa8OK3WABHJpy4zUrnUBiMuPITzD/FuDx4C5IwwlC68gHAZblNqpBZCX0nFCtKj YOcI2So5HbQ2OC8QF+zGVuduHUSok4hSy2BBfZ1pfvziqBeetWJwFvapGB44nIHh WKNKvqOxLNIy7e+TGRiWOomrAWM18VSR9LZbBxpJK7PLSzWqYJYTRCZHAgMBAAGj UzBRMB0GA1UdDgQWBBR4uDD9Y6x7iUoHO+32ioOcw1ICZTAfBgNVHSMEGDAWgBR4 uDD9Y6x7iUoHO+32ioOcw1ICZTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB CwUAA4ICAQAaCEupzGGqcdh+L7BzhX7zyd7yzAKUoLxFrxaZY34Xyj3lcx1XoK6F AqsH2JM25GixgadzhNt92JP7vzoWeHZtLfstrPS638Y1zZi6toy4E49viYjFk5J0 C6ZcFC04VYWWx6z0HwJuAS08tZ37JuFXpJGfXJOjZCQyxse0Lg0tuKLMeXDCk2Y3 Ba0noeuNyHRoWXXPyiUoeApkVCU5gIsyiJSWOjhJ5hpJG06rQNfNYexgKrrraEin o0jmEMtJMx5TtD83hSnLCnFGBBq5lkE7jgXME1KsbIE3lJZzRX1mQwUK8CJDYxye i6M/dzSvy0SsPvz8fTAlprXRtWWtJQmxgWENp3Dv+0Pmux/l+ilk7KA4sMXGhsfr bvTOeWl1/uoFTPYiWR/ww7QEPLq23yDFY04Q7Un0qjIk8ExvaY8lCkXMgc8i7sGY VfvOYb0zm67EfAQl3TW8Ky5fl5CcxpVCD360Bzi6hwjYixa3qEeBggOixFQBFWft 8wrkKTHpOQXjn4sDPtet8imm9UYEtzWrFX6T9MFYkBR0/yye0FIh9+YPiTA6WB86 NCNwK5Yl6HuvF97CIH5CdgO+5C7KifUtqTOL8pQKbNwy0S3sNYvB+njGvRpR7pKV BUnFpB/Atptqr4CUlTXrc5IPLAqAfmwk5IKcwy3EXUbruf9Dwz69YA== -----END CERTIFICATE----- ================================================ FILE: certs/openssl-gen.sh ================================================ #!/bin/bash set -ex # generate CA's key openssl genrsa -aes256 -passout pass:1 -out ca.key.pem 4096 openssl rsa -passin pass:1 -in ca.key.pem -out ca.key.pem.tmp mv ca.key.pem.tmp ca.key.pem openssl req -config openssl.cnf -key ca.key.pem -new -x509 -days 7300 -sha256 -extensions v3_ca -out ca.pem ================================================ FILE: certs/openssl.cnf ================================================ [ ca ] default_ca = CA_default [ CA_default ] default_md = sha256 [ v3_ca ] subjectKeyIdentifier=hash authorityKeyIdentifier=keyid:always,issuer basicConstraints = critical,CA:true [ req ] distinguished_name = req_distinguished_name [ req_distinguished_name ] countryName = Country Name (2 letter code) countryName_default = IL countryName_min = 2 countryName_max = 2 stateOrProvinceName = State or Province Name (full name) stateOrProvinceName_default = Center localityName = Locality Name (eg, city) localityName_default = Lod 0.organizationName = Organization Name (eg, company) 0.organizationName_default = GoProxy # we can do this but it is not needed normally :-) #1.organizationName = Second Organization Name (eg, company) #1.organizationName_default = World Wide Web Pty Ltd organizationalUnitName = Organizational Unit Name (eg, section) organizationalUnitName_default = GoProxy commonName = Common Name (e.g. server FQDN or YOUR name) commonName_default = goproxy.github.io commonName_max = 64 emailAddress = Email Address emailAddress_default = elazarl@gmail.com emailAddress_max = 64 ================================================ FILE: certs.go ================================================ package goproxy import ( "crypto/tls" "crypto/x509" ) var GoproxyCa tls.Certificate func init() { // When we included the embedded certificate inside this file, we made // sure that it was valid. // If there is an error here, this is a really exceptional case that requires // a panic. It should NEVER happen! var err error GoproxyCa, err = tls.X509KeyPair(CA_CERT, CA_KEY) if err != nil { panic("Error parsing builtin CA: " + err.Error()) } if GoproxyCa.Leaf, err = x509.ParseCertificate(GoproxyCa.Certificate[0]); err != nil { panic("Error parsing builtin CA leaf: " + err.Error()) } } var tlsClientSkipVerify = &tls.Config{InsecureSkipVerify: true} var defaultTLSConfig = &tls.Config{ InsecureSkipVerify: true, } var CA_CERT = []byte(`-----BEGIN CERTIFICATE----- MIIF9DCCA9ygAwIBAgIJAODqYUwoVjJkMA0GCSqGSIb3DQEBCwUAMIGOMQswCQYD VQQGEwJJTDEPMA0GA1UECAwGQ2VudGVyMQwwCgYDVQQHDANMb2QxEDAOBgNVBAoM B0dvUHJveHkxEDAOBgNVBAsMB0dvUHJveHkxGjAYBgNVBAMMEWdvcHJveHkuZ2l0 aHViLmlvMSAwHgYJKoZIhvcNAQkBFhFlbGF6YXJsQGdtYWlsLmNvbTAeFw0xNzA0 MDUyMDAwMTBaFw0zNzAzMzEyMDAwMTBaMIGOMQswCQYDVQQGEwJJTDEPMA0GA1UE CAwGQ2VudGVyMQwwCgYDVQQHDANMb2QxEDAOBgNVBAoMB0dvUHJveHkxEDAOBgNV BAsMB0dvUHJveHkxGjAYBgNVBAMMEWdvcHJveHkuZ2l0aHViLmlvMSAwHgYJKoZI hvcNAQkBFhFlbGF6YXJsQGdtYWlsLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIP ADCCAgoCggIBAJ4Qy+H6hhoY1s0QRcvIhxrjSHaO/RbaFj3rwqcnpOgFq07gRdI9 3c0TFKQJHpgv6feLRhEvX/YllFYu4J35lM9ZcYY4qlKFuStcX8Jm8fqpgtmAMBzP sqtqDi8M9RQGKENzU9IFOnCV7SAeh45scMuI3wz8wrjBcH7zquHkvqUSYZz035t9 V6WTrHyTEvT4w+lFOVN2bA/6DAIxrjBiF6DhoJqnha0SZtDfv77XpwGG3EhA/qoh hiYrDruYK7zJdESQL44LwzMPupVigqalfv+YHfQjbhT951IVurW2NJgRyBE62dLr lHYdtT9tCTCrd+KJNMJ+jp9hAjdIu1Br/kifU4F4+4ZLMR9Ueji0GkkPKsYdyMnq j0p0PogyvP1l4qmboPImMYtaoFuYmMYlebgC9LN10bL91K4+jLt0I1YntEzrqgJo WsJztYDw543NzSy5W+/cq4XRYgtq1b0RWwuUiswezmMoeyHZ8BQJe2xMjAOllASD fqa8OK3WABHJpy4zUrnUBiMuPITzD/FuDx4C5IwwlC68gHAZblNqpBZCX0nFCtKj YOcI2So5HbQ2OC8QF+zGVuduHUSok4hSy2BBfZ1pfvziqBeetWJwFvapGB44nIHh WKNKvqOxLNIy7e+TGRiWOomrAWM18VSR9LZbBxpJK7PLSzWqYJYTRCZHAgMBAAGj UzBRMB0GA1UdDgQWBBR4uDD9Y6x7iUoHO+32ioOcw1ICZTAfBgNVHSMEGDAWgBR4 uDD9Y6x7iUoHO+32ioOcw1ICZTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB CwUAA4ICAQAaCEupzGGqcdh+L7BzhX7zyd7yzAKUoLxFrxaZY34Xyj3lcx1XoK6F AqsH2JM25GixgadzhNt92JP7vzoWeHZtLfstrPS638Y1zZi6toy4E49viYjFk5J0 C6ZcFC04VYWWx6z0HwJuAS08tZ37JuFXpJGfXJOjZCQyxse0Lg0tuKLMeXDCk2Y3 Ba0noeuNyHRoWXXPyiUoeApkVCU5gIsyiJSWOjhJ5hpJG06rQNfNYexgKrrraEin o0jmEMtJMx5TtD83hSnLCnFGBBq5lkE7jgXME1KsbIE3lJZzRX1mQwUK8CJDYxye i6M/dzSvy0SsPvz8fTAlprXRtWWtJQmxgWENp3Dv+0Pmux/l+ilk7KA4sMXGhsfr bvTOeWl1/uoFTPYiWR/ww7QEPLq23yDFY04Q7Un0qjIk8ExvaY8lCkXMgc8i7sGY VfvOYb0zm67EfAQl3TW8Ky5fl5CcxpVCD360Bzi6hwjYixa3qEeBggOixFQBFWft 8wrkKTHpOQXjn4sDPtet8imm9UYEtzWrFX6T9MFYkBR0/yye0FIh9+YPiTA6WB86 NCNwK5Yl6HuvF97CIH5CdgO+5C7KifUtqTOL8pQKbNwy0S3sNYvB+njGvRpR7pKV BUnFpB/Atptqr4CUlTXrc5IPLAqAfmwk5IKcwy3EXUbruf9Dwz69YA== -----END CERTIFICATE-----`) var CA_KEY = []byte(`-----BEGIN RSA PRIVATE KEY----- MIIJKAIBAAKCAgEAnhDL4fqGGhjWzRBFy8iHGuNIdo79FtoWPevCpyek6AWrTuBF 0j3dzRMUpAkemC/p94tGES9f9iWUVi7gnfmUz1lxhjiqUoW5K1xfwmbx+qmC2YAw HM+yq2oOLwz1FAYoQ3NT0gU6cJXtIB6Hjmxwy4jfDPzCuMFwfvOq4eS+pRJhnPTf m31XpZOsfJMS9PjD6UU5U3ZsD/oMAjGuMGIXoOGgmqeFrRJm0N+/vtenAYbcSED+ qiGGJisOu5grvMl0RJAvjgvDMw+6lWKCpqV+/5gd9CNuFP3nUhW6tbY0mBHIETrZ 0uuUdh21P20JMKt34ok0wn6On2ECN0i7UGv+SJ9TgXj7hksxH1R6OLQaSQ8qxh3I yeqPSnQ+iDK8/WXiqZug8iYxi1qgW5iYxiV5uAL0s3XRsv3Urj6Mu3QjVie0TOuq AmhawnO1gPDnjc3NLLlb79yrhdFiC2rVvRFbC5SKzB7OYyh7IdnwFAl7bEyMA6WU BIN+prw4rdYAEcmnLjNSudQGIy48hPMP8W4PHgLkjDCULryAcBluU2qkFkJfScUK 0qNg5wjZKjkdtDY4LxAX7MZW524dRKiTiFLLYEF9nWl+/OKoF561YnAW9qkYHjic geFYo0q+o7Es0jLt75MZGJY6iasBYzXxVJH0tlsHGkkrs8tLNapglhNEJkcCAwEA AQKCAgAwSuNvxHHqUUJ3XoxkiXy1u1EtX9x1eeYnvvs2xMb+WJURQTYz2NEGUdkR kPO2/ZSXHAcpQvcnpi2e8y2PNmy/uQ0VPATVt6NuWweqxncR5W5j82U/uDlXY8y3 lVbfak4s5XRri0tikHvlP06dNgZ0OPok5qi7d+Zd8yZ3Y8LXfjkykiIrSG1Z2jdt zCWTkNmSUKMGG/1CGFxI41Lb12xuq+C8v4f469Fb6bCUpyCQN9rffHQSGLH6wVb7 +68JO+d49zCATpmx5RFViMZwEcouXxRvvc9pPHXLP3ZPBD8nYu9kTD220mEGgWcZ 3L9dDlZPcSocbjw295WMvHz2QjhrDrb8gXwdpoRyuyofqgCyNxSnEC5M13SjOxtf pjGzjTqh0kDlKXg2/eTkd9xIHjVhFYiHIEeITM/lHCfWwBCYxViuuF7pSRPzTe8U C440b62qZSPMjVoquaMg+qx0n9fKSo6n1FIKHypv3Kue2G0WhDeK6u0U288vQ1t4 Ood3Qa13gZ+9hwDLbM/AoBfVBDlP/tpAwa7AIIU1ZRDNbZr7emFdctx9B6kLINv3 4PDOGM2xrjOuACSGMq8Zcu7LBz35PpIZtviJOeKNwUd8/xHjWC6W0itgfJb5I1Nm V6Vj368pGlJx6Se26lvXwyyrc9pSw6jSAwARBeU4YkNWpi4i6QKCAQEA0T7u3P/9 jZJSnDN1o2PXymDrJulE61yguhc/QSmLccEPZe7or06/DmEhhKuCbv+1MswKDeag /1JdFPGhL2+4G/f/9BK3BJPdcOZSz7K6Ty8AMMBf8AehKTcSBqwkJWcbEvpHpKJ6 eDqn1B6brXTNKMT6fEEXCuZJGPBpNidyLv/xXDcN7kCOo3nGYKfB5OhFpNiL63tw +LntU56WESZwEqr8Pf80uFvsyXQK3a5q5HhIQtxl6tqQuPlNjsDBvCqj0x72mmaJ ZVsVWlv7khUrCwAXz7Y8K7mKKBd2ekF5hSbryfJsxFyvEaWUPhnJpTKV85lAS+tt FQuIp9TvKYlRQwKCAQEAwWJN8jysapdhi67jO0HtYOEl9wwnF4w6XtiOYtllkMmC 06/e9h7RsRyWPMdu3qRDPUYFaVDy6+dpUDSQ0+E2Ot6AHtVyvjeUTIL651mFIo/7 OSUCEc+HRo3SfPXdPhSQ2thNTxl6y9XcFacuvbthgr70KXbvC4k6IEmdpf/0Kgs9 7QTZCG26HDrEZ2q9yMRlRaL2SRD+7Y2xra7gB+cQGFj6yn0Wd/07er49RqMXidQf KR2oYfev2BDtHXoSZFfhFGHlOdLvWRh90D4qZf4vQ+g/EIMgcNSoxjvph1EShmKt sjhTHtoHuu+XmEQvIewk2oCI+JvofBkcnpFrVvUUrQKCAQAaTIufETmgCo0BfuJB N/JOSGIl0NnNryWwXe2gVgVltbsmt6FdL0uKFiEtWJUbOF5g1Q5Kcvs3O/XhBQGa QbNlKIVt+tAv7hm97+Tmn/MUsraWagdk1sCluns0hXxBizT27KgGhDlaVRz05yfv 5CdJAYDuDwxDXXBAhy7iFJEgYSDH00+X61tCJrMNQOh4ycy/DEyBu1EWod+3S85W t3sMjZsIe8P3i+4137Th6eMbdha2+JaCrxfTd9oMoCN5b+6JQXIDM/H+4DTN15PF 540yY7+aZrAnWrmHknNcqFAKsTqfdi2/fFqwoBwCtiEG91WreU6AfEWIiJuTZIru sIibAoIBAAqIwlo5t+KukF+9jR9DPh0S5rCIdvCvcNaN0WPNF91FPN0vLWQW1bFi L0TsUDvMkuUZlV3hTPpQxsnZszH3iK64RB5p3jBCcs+gKu7DT59MXJEGVRCHT4Um YJryAbVKBYIGWl++sZO8+JotWzx2op8uq7o+glMMjKAJoo7SXIiVyC/LHc95urOi 9+PySphPKn0anXPpexmRqGYfqpCDo7rPzgmNutWac80B4/CfHb8iUPg6Z1u+1FNe yKvcZHgW2Wn00znNJcCitufLGyAnMofudND/c5rx2qfBx7zZS7sKUQ/uRYjes6EZ QBbJUA/2/yLv8YYpaAaqj4aLwV8hRpkCggEBAIh3e25tr3avCdGgtCxS7Y1blQ2c ue4erZKmFP1u8wTNHQ03T6sECZbnIfEywRD/esHpclfF3kYAKDRqIP4K905Rb0iH 759ZWt2iCbqZznf50XTvptdmjm5KxvouJzScnQ52gIV6L+QrCKIPelLBEIqCJREh pmcjjocD/UCCSuHgbAYNNnO/JdhnSylz1tIg26I+2iLNyeTKIepSNlsBxnkLmqM1 cj/azKBaT04IOMLaN8xfSqitJYSraWMVNgGJM5vfcVaivZnNh0lZBv+qu6YkdM88 4/avCJ8IutT+FcMM+GbGazOm5ALWqUyhrnbLGc4CQMPfe7Il6NxwcrOxT8w= -----END RSA PRIVATE KEY-----`) ================================================ FILE: ctx.go ================================================ package goproxy import ( "context" "crypto/tls" "mime" "net" "net/http" ) // ProxyCtx is the Proxy context, contains useful information about every request. It is passed to // every user function. Also used as a logger. type ProxyCtx struct { // Will contain the client request from the proxy Req *http.Request // 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 any // Will connect a request to a response Session int64 certStore CertStorage Proxy *ProxyHttpServer } type RoundTripper interface { RoundTrip(req *http.Request, ctx *ProxyCtx) (*http.Response, error) } type CertStorage interface { Fetch(hostname string, gen func() (*tls.Certificate, error)) (*tls.Certificate, error) } type RoundTripperFunc func(req *http.Request, ctx *ProxyCtx) (*http.Response, error) func (f RoundTripperFunc) RoundTrip(req *http.Request, ctx *ProxyCtx) (*http.Response, error) { return f(req, ctx) } func (ctx *ProxyCtx) RoundTrip(req *http.Request) (*http.Response, error) { if ctx.RoundTripper != nil { return ctx.RoundTripper.RoundTrip(req, ctx) } return ctx.Proxy.Tr.RoundTrip(req) } 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 // This message will be printed only if the Verbose field of the ProxyHttpServer is set to true // // proxy.OnRequest().DoFunc(func(r *http.Request,ctx *goproxy.ProxyCtx) (*http.Request, *http.Response){ // nr := atomic.AddInt32(&counter,1) // ctx.Printf("So far %d requests",nr) // return r, nil // }) func (ctx *ProxyCtx) Logf(msg string, argv ...any) { if ctx.Proxy.Verbose { ctx.printf("INFO: "+msg, argv...) } } // Warnf prints a message to the proxy's log. Should be used in a ProxyHttpServer's filter // This message will always be printed. // // proxy.OnRequest().DoFunc(func(r *http.Request,ctx *goproxy.ProxyCtx) (*http.Request, *http.Response){ // f,err := os.OpenFile(cachedContent) // if err != nil { // ctx.Warnf("error open file %v: %v",cachedContent,err) // return r, nil // } // return r, nil // }) func (ctx *ProxyCtx) Warnf(msg string, argv ...any) { ctx.printf("WARN: "+msg, argv...) } // 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 { contentType := ctx.Resp.Header.Get("Content-Type") if _, params, err := mime.ParseMediaType(contentType); err == nil { if cs, ok := params["charset"]; ok { return cs } } return "" } ================================================ FILE: dispatcher.go ================================================ package goproxy import ( "bytes" "io" "net" "net/http" "regexp" "strings" ) // ReqCondition.HandleReq will decide whether or not to use the ReqHandler on an HTTP request // before sending it to the remote server. type ReqCondition interface { RespCondition HandleReq(req *http.Request, ctx *ProxyCtx) bool } // RespCondition.HandleReq will decide whether or not to use the RespHandler on an HTTP response // before sending it to the proxy client. Note that resp might be nil, in case there was an // error sending the request. type RespCondition interface { HandleResp(resp *http.Response, ctx *ProxyCtx) bool } // ReqConditionFunc.HandleReq(req,ctx) <=> ReqConditionFunc(req,ctx). type ReqConditionFunc func(req *http.Request, ctx *ProxyCtx) bool // 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 { return c(req, ctx) } // ReqConditionFunc cannot test responses. It only satisfies RespCondition interface so that // to be usable as RespCondition. func (c ReqConditionFunc) HandleResp(resp *http.Response, ctx *ProxyCtx) bool { return c(ctx.Req, ctx) } func (c RespConditionFunc) HandleResp(resp *http.Response, ctx *ProxyCtx) bool { return c(resp, ctx) } // UrlHasPrefix returns a ReqCondition checking wether the destination URL the proxy client has requested // has the given prefix, with or without the host. // For example UrlHasPrefix("host/x") will match requests of the form 'GET host/x', and will match // requests to url 'http://host/x' 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+relativePath, prefix) || // Scheme value is something like "https", we must include the :// characters strings.HasPrefix(req.URL.Scheme+"://"+req.URL.Host+relativePath, prefix) } } // UrlIs returns a ReqCondition, testing whether or not the request URL is one of the given strings // with or without the host prefix. // UrlIs("google.com/","foo") will match requests 'GET /' to 'google.com', requests `'GET google.com/' to // any host, and requests of the form 'GET foo'. func UrlIs(urls ...string) ReqConditionFunc { urlSet := make(map[string]bool) for _, u := range urls { urlSet[u] = true } return func(req *http.Request, ctx *ProxyCtx) bool { _, pathOk := urlSet[req.URL.Path] _, hostAndOk := urlSet[req.URL.Host+req.URL.Path] return pathOk || hostAndOk } } // ReqHostMatches returns a ReqCondition, testing whether the host to which the request was directed to matches // any of the given regular expressions. func ReqHostMatches(regexps ...*regexp.Regexp) ReqConditionFunc { return func(req *http.Request, ctx *ProxyCtx) bool { for _, re := range regexps { if re.MatchString(req.Host) { return true } } return false } } // ReqHostIs returns a ReqCondition, testing whether the host to which the request is directed to equal // to one of the given strings. func ReqHostIs(hosts ...string) ReqConditionFunc { hostSet := make(map[string]bool) for _, h := range hosts { hostSet[h] = true } return func(req *http.Request, ctx *ProxyCtx) bool { _, ok := hostSet[req.URL.Host] return ok } } // IsLocalHost checks whether the destination host is localhost. var IsLocalHost ReqConditionFunc = func(req *http.Request, ctx *ProxyCtx) bool { h := req.URL.Hostname() if h == "localhost" { return true } if ip := net.ParseIP(h); ip != nil { return ip.IsLoopback() } // In case of IPv6 without a port number Hostname() sometimes returns the invalid value. if ip := net.ParseIP(req.URL.Host); ip != nil { return ip.IsLoopback() } return false } // UrlMatches returns a ReqCondition testing whether the destination URL // of the request matches the given regexp, with or without prefix. func UrlMatches(re *regexp.Regexp) ReqConditionFunc { return func(req *http.Request, ctx *ProxyCtx) bool { return re.MatchString(req.URL.Path) || re.MatchString(req.URL.Host+req.URL.Path) } } // 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 { // 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. func SrcIpIs(ips ...string) ReqCondition { return ReqConditionFunc(func(req *http.Request, ctx *ProxyCtx) bool { for _, ip := range ips { if strings.HasPrefix(req.RemoteAddr, ip+":") { return true } } return false }) } // 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) } } // ContentTypeIs returns a RespCondition testing whether the HTTP response has Content-Type header equal // to one of the given strings. func ContentTypeIs(typ string, types ...string) RespCondition { types = append(types, typ) return RespConditionFunc(func(resp *http.Response, ctx *ProxyCtx) bool { if resp == nil { return false } contentType := resp.Header.Get("Content-Type") for _, typ := range types { if contentType == typ || strings.HasPrefix(contentType, typ+";") { return true } } return false }) } // StatusCodeIs returns a RespCondition, testing whether or not the HTTP status // code is one of the given ints. func StatusCodeIs(codes ...int) RespCondition { codeSet := make(map[int]bool) for _, c := range codes { codeSet[c] = true } return RespConditionFunc(func(resp *http.Response, ctx *ProxyCtx) bool { if resp == nil { return false } _, codeMatch := codeSet[resp.StatusCode] return codeMatch }) } // ProxyHttpServer.OnRequest Will return a temporary ReqProxyConds struct, aggregating the given condtions. // You will use the ReqProxyConds struct to register a ReqHandler, that would filter // the request, only if all the given ReqCondition matched. // Typical usage: // // proxy.OnRequest(UrlIs("example.com/foo"),UrlMatches(regexp.MustParse(`.*\.exampl.\com\./.*`)).Do(...) func (proxy *ProxyHttpServer) OnRequest(conds ...ReqCondition) *ReqProxyConds { return &ReqProxyConds{proxy, conds} } // ReqProxyConds aggregate ReqConditions for a ProxyHttpServer. // Upon calling Do, it will register a ReqHandler that would // 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)). func (pcond *ReqProxyConds) DoFunc(f func(req *http.Request, ctx *ProxyCtx) (*http.Request, *http.Response)) { pcond.Do(FuncReqHandler(f)) } // ReqProxyConds.Do will register the ReqHandler on the proxy, // the ReqHandler will handle the HTTP request if all the conditions // aggregated in the ReqProxyConds are met. Typical usage: // // proxy.OnRequest().Do(handler) // will call handler.Handle(req,ctx) on every request to the proxy // proxy.OnRequest(cond1,cond2).Do(handler) // // given request to the proxy, will test if cond1.HandleReq(req,ctx) && cond2.HandleReq(req,ctx) are true // // if they are, will call handler.Handle(req,ctx) func (pcond *ReqProxyConds) Do(h ReqHandler) { pcond.proxy.reqHandlers = append(pcond.proxy.reqHandlers, FuncReqHandler(func(r *http.Request, ctx *ProxyCtx) (*http.Request, *http.Response) { for _, cond := range pcond.reqConds { if !cond.HandleReq(r, ctx) { return r, nil } } return h.Handle(r, ctx) })) } // HandleConnect is used when proxy receives an HTTP CONNECT request, // it'll then use the HttpsHandler to determine what should it // do with this request. The handler returns a ConnectAction struct, the Action field in the ConnectAction // struct returned will determine what to do with this request. ConnectAccept will simply accept the request // forwarding all bytes from the client to the remote host, ConnectReject will close the connection with the // client, and ConnectMitm, will assume the underlying connection is an HTTPS connection, and will use Man // in the Middle attack to eavesdrop the connection. All regular handler will be active on this eavesdropped // connection. // The ConnectAction struct contains possible tlsConfig that will be used for eavesdropping. If nil, the proxy // will use the default tls configuration. // // proxy.OnRequest().HandleConnect(goproxy.AlwaysReject) // rejects all CONNECT requests func (pcond *ReqProxyConds) HandleConnect(h HttpsHandler) { pcond.proxy.httpsHandlers = append(pcond.proxy.httpsHandlers, FuncHttpsHandler(func(host string, ctx *ProxyCtx) (*ConnectAction, string) { for _, cond := range pcond.reqConds { if !cond.HandleReq(ctx.Req, ctx) { return nil, "" } } return h.HandleConnect(host, ctx) })) } // HandleConnectFunc is equivalent to HandleConnect, // for example, accepting CONNECT request if they contain a password in header // // io.WriteString(h,password) // passHash := h.Sum(nil) // proxy.OnRequest().HandleConnectFunc(func(host string, ctx *ProxyCtx) (*ConnectAction, string) { // c := sha1.New() // io.WriteString(c,ctx.Req.Header.Get("X-GoProxy-Auth")) // if c.Sum(nil) == passHash { // return OkConnect, host // } // return RejectConnect, host // }) func (pcond *ReqProxyConds) HandleConnectFunc(f func(host string, ctx *ProxyCtx) (*ConnectAction, string)) { pcond.HandleConnect(FuncHttpsHandler(f)) } func (pcond *ReqProxyConds) HijackConnect(f func(req *http.Request, client net.Conn, ctx *ProxyCtx)) { pcond.proxy.httpsHandlers = append(pcond.proxy.httpsHandlers, FuncHttpsHandler(func(host string, ctx *ProxyCtx) (*ConnectAction, string) { for _, cond := range pcond.reqConds { if !cond.HandleReq(ctx.Req, ctx) { return nil, "" } } return &ConnectAction{Action: ConnectHijack, Hijack: f}, host })) } // ProxyConds is used to aggregate RespConditions for a ProxyHttpServer. // Upon calling ProxyConds.Do, it will register a RespHandler that would // handle the HTTP response from remote server if all conditions on the HTTP response are met. type ProxyConds struct { proxy *ProxyHttpServer reqConds []ReqCondition respCond []RespCondition } // 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)) } // ProxyConds.Do will register the RespHandler on the proxy, h.Handle(resp,ctx) will be called on every // request that matches the conditions aggregated in pcond. func (pcond *ProxyConds) Do(h RespHandler) { pcond.proxy.respHandlers = append(pcond.proxy.respHandlers, FuncRespHandler(func(resp *http.Response, ctx *ProxyCtx) *http.Response { for _, cond := range pcond.reqConds { if !cond.HandleReq(ctx.Req, ctx) { return resp } } for _, cond := range pcond.respCond { if !cond.HandleResp(resp, ctx) { return resp } } return h.Handle(resp, ctx) })) } // OnResponse is used when adding a response-filter to the HTTP proxy, usual pattern is // // proxy.OnResponse(cond1,cond2).Do(handler) // handler.Handle(resp,ctx) will be used // // if cond1.HandleResp(resp) && cond2.HandleResp(resp) func (proxy *ProxyHttpServer) OnResponse(conds ...RespCondition) *ProxyConds { return &ProxyConds{proxy, make([]ReqCondition, 0), conds} } // AlwaysMitm is a HttpsHandler that always eavesdrop https connections, for example to // eavesdrop all https connections to www.google.com, we can use // // proxy.OnRequest(goproxy.ReqHostIs("www.google.com")).HandleConnect(goproxy.AlwaysMitm) var AlwaysMitm FuncHttpsHandler = func(host string, ctx *ProxyCtx) (*ConnectAction, string) { return MitmConnect, host } // AlwaysReject is a HttpsHandler that drops any CONNECT request, for example, this code will disallow // connections to hosts on any other port than 443 // // proxy.OnRequest(goproxy.Not(goproxy.ReqHostMatches(regexp.MustCompile(":443$"))). // HandleConnect(goproxy.AlwaysReject) var AlwaysReject FuncHttpsHandler = func(host string, ctx *ProxyCtx) (*ConnectAction, string) { return RejectConnect, host } // HandleBytes will return a RespHandler that read the entire body of the request // to a byte array in memory, would run the user supplied f function on the byte arra, // and will replace the body of the original response with the resulting byte array. func HandleBytes(f func(b []byte, ctx *ProxyCtx) []byte) RespHandler { return FuncRespHandler(func(resp *http.Response, ctx *ProxyCtx) *http.Response { b, err := io.ReadAll(resp.Body) if err != nil { ctx.Warnf("Cannot read response %s", err) return resp } resp.Body.Close() resp.Body = io.NopCloser(bytes.NewBuffer(f(b, ctx))) return resp }) } ================================================ FILE: dispatcher_test.go ================================================ package goproxy_test import ( "context" "net" "net/http" "strings" "testing" "github.com/elazarl/goproxy" ) func TestIsLocalHost(t *testing.T) { hosts := []string{ "localhost", "127.0.0.1", "127.0.0.7", "::ffff:127.0.0.1", "::ffff:127.0.0.7", "::1", "0:0:0:0:0:0:0:1", } ports := []string{ "", "80", "443", } for _, host := range hosts { for _, port := range ports { if port == "" && strings.HasPrefix(host, "::ffff:") { continue } addr := host if port != "" { addr = net.JoinHostPort(host, port) } t.Run(addr, func(t *testing.T) { req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://"+addr, http.NoBody) if err != nil { t.Fatal(err) } if !goproxy.IsLocalHost(req, nil) { t.Fatal("expected true") } }) } } } ================================================ FILE: doc.go ================================================ /* Package goproxy provides a customizable HTTP proxy, supporting hijacking HTTPS connection. The intent of the proxy, is to be usable with reasonable amount of traffic yet, customizable and programmable. The proxy itself is simply an `net/http` handler. Typical usage is proxy := goproxy.NewProxyHttpServer() proxy.OnRequest(..conditions..).Do(..requesthandler..) proxy.OnRequest(..conditions..).DoFunc(..requesthandlerFunction..) proxy.OnResponse(..conditions..).Do(..responesHandler..) proxy.OnResponse(..conditions..).DoFunc(..responesHandlerFunction..) http.ListenAndServe(":8080", proxy) Adding a header to each request proxy.OnRequest().DoFunc(func(r *http.Request,ctx *goproxy.ProxyCtx) (*http.Request, *http.Response){ r.Header.Set("X-GoProxy","1") return r, nil }) > Note that the function is called before the proxy sends the request to the server For printing the content type of all incoming responses proxy.OnResponse().DoFunc(func(r *http.Response, ctx *goproxy.ProxyCtx)*http.Response{ println(ctx.Req.Host,"->",r.Header.Get("Content-Type")) return r }) note that we used the ProxyCtx context variable here. It contains the request and the response (Req and Resp, Resp is nil if unavailable) of this specific client interaction with the proxy. To print the content type of all responses from a certain url, we'll add a ReqCondition to the OnResponse function: proxy.OnResponse(goproxy.UrlIs("golang.org/pkg")).DoFunc(func(r *http.Response, ctx *goproxy.ProxyCtx)*http.Response{ println(ctx.Req.Host,"->",r.Header.Get("Content-Type")) return r }) We can write the condition ourselves, conditions can be set on request and on response var random = ReqConditionFunc(func(r *http.Request) bool { return rand.Intn(1) == 0 }) var hasGoProxyHeader = RespConditionFunc(func(resp *http.Response,req *http.Request)bool { return resp.Header.Get("X-GoProxy") != "" }) Caution! If you give a RespCondition to the OnRequest function, you'll get a run time panic! It doesn't make sense to read the response, if you still haven't got it! 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!" ) }) we close the body of the original response, and return a new 403 response with a short message. Example use cases: 1. https://github.com/elazarl/goproxy/tree/master/examples/goproxy-avgsize To measure the average size of an Html served in your site. One can ask all the QA team to access the website by a proxy, and the proxy will measure the average size of all text/html responses from your host. 2. [not yet implemented] All requests to your web servers should be directed through the proxy, when the proxy will detect html pieces sent as a response to AJAX request, it'll send a warning email. 3. https://github.com/elazarl/goproxy/blob/master/examples/goproxy-httpdump/ Generate a real traffic to your website by real users using through proxy. Record the traffic, and try it again for more real load testing. 4. https://github.com/elazarl/goproxy/tree/master/examples/goproxy-no-reddit-at-worktime Will allow browsing to reddit.com between 8:00am and 17:00pm 5. https://github.com/elazarl/goproxy/tree/master/examples/goproxy-jquery-version 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 ================================================ FILE: examples/base/README.md ================================================ # Simple HTTP Proxy This example contains a base HTTP proxy server that listens on port :8080. It only handles explicit CONNECT requests. Start it in one shell: ```sh go build base -v ``` Fetch a website using the proxy: ```sh http_proxy=http://127.0.0.1:8080 wget -O - \ http://ripper234.com/p/introducing-goproxy-light-http-proxy/ ``` The homepage HTML content should be displayed in the console. The proxy should have logged the request being processed: ```sh 2015/04/09 18:19:17 [001] INFO: Got request /p/introducing-goproxy-light-http-proxy/ ripper234.com GET http://ripper234.com/p/introducing-goproxy-light-http-proxy/ 2015/04/09 18:19:17 [001] INFO: Sending request GET http://ripper234.com/p/introducing-goproxy-light-http-proxy/ 2015/04/09 18:19:18 [001] INFO: Received response 200 OK 2015/04/09 18:19:18 [001] INFO: Copying response to client 200 OK [200] 2015/04/09 18:19:18 [001] INFO: Copied 44333 bytes to client error= ``` ================================================ FILE: examples/base/main.go ================================================ package main import ( "flag" "log" "net/http" "github.com/elazarl/goproxy" ) func main() { verbose := flag.Bool("v", false, "should every proxy request be logged to stdout") addr := flag.String("addr", ":8080", "proxy listen address") flag.Parse() proxy := goproxy.NewProxyHttpServer() proxy.Verbose = *verbose log.Fatal(http.ListenAndServe(*addr, proxy)) } ================================================ FILE: examples/cascadeproxy/README.md ================================================ # CascadeProxy `CascadeProxy` is an example that shows an aggregator server that forwards the requests to another proxy server (end proxy). Diagram: ``` client --> middle proxy --> end proxy --> internet ``` This example starts both proxy servers using goproxy, the middle one listens on port `8081`, and the end one on port `8082`. The middle proxy must be an HTTP server, since we use goproxy that expose only it. The end proxy can be any type of proxy supported by Go, including SOCKS5, there is a comment in the part where you can put its address. ================================================ FILE: examples/cascadeproxy/main.go ================================================ package main import ( "crypto/subtle" "encoding/base64" "io" "log" "net/http" "net/url" "time" "github.com/elazarl/goproxy" "github.com/elazarl/goproxy/ext/auth" ) const _proxyAuthHeader = "Proxy-Authorization" func SetBasicAuth(username, password string, req *http.Request) { req.Header.Set(_proxyAuthHeader, "Basic "+base64.StdEncoding.EncodeToString([]byte(username+":"+password))) } func main() { username, password := "foo", "bar" // Start end proxy server endProxy := goproxy.NewProxyHttpServer() endProxy.Verbose = true auth.ProxyBasic(endProxy, "my_realm", func(user, pwd string) bool { return subtle.ConstantTimeCompare([]byte(user), []byte(username)) == 1 && subtle.ConstantTimeCompare([]byte(pwd), []byte(password)) == 1 }) log.Println("serving end proxy server at localhost:8082") go http.ListenAndServe("localhost:8082", endProxy) // Start middle proxy server middleProxy := goproxy.NewProxyHttpServer() middleProxy.Verbose = true middleProxy.KeepHeader = true middleProxy.Tr.Proxy = func(req *http.Request) (*url.URL, error) { // Here we specify the proxy URL of the other server. // If it was a socks5 proxy, we would have used an url like // socks5://localhost:8082 return url.Parse("http://localhost:8082") } connectReqHandler := func(req *http.Request) { SetBasicAuth(username, password, req) } middleProxy.ConnectDial = middleProxy.NewConnectDialToProxyWithHandler("http://localhost:8082", connectReqHandler) middleProxy.OnRequest().Do(goproxy.FuncReqHandler(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { SetBasicAuth(username, password, req) return req, nil })) log.Println("serving middle proxy server at localhost:8081") go http.ListenAndServe("localhost:8081", middleProxy) time.Sleep(1 * time.Second) // Make a single HTTP request, from client to internet, through the 2 proxies middleProxyUrl := "http://localhost:8081" request, err := http.NewRequest(http.MethodGet, "https://ip.cn", nil) if err != nil { log.Fatalf("new request failed:%v", err) } client := &http.Client{ Transport: &http.Transport{ Proxy: func(req *http.Request) (*url.URL, error) { return url.Parse(middleProxyUrl) }, }, } resp, err := client.Do(request) if err != nil { log.Fatalf("get resp failed: %v", err) } defer resp.Body.Close() data, _ := io.ReadAll(resp.Body) if resp.StatusCode != http.StatusOK { log.Fatalf("status %d, data %s", resp.StatusCode, data) } log.Printf("resp: %s", data) } ================================================ FILE: examples/cascadeproxy-socks/README.md ================================================ # CascadeSocksProxy `CascadeSocksProxy` is an example that shows an aggregator server that forwards the requests to another socks proxy server. This example is written base on `cascadeproxy` example. Diagram: ``` client --> goproxy --> socks5 proxy --> internet ``` This example starts a HTTP/HTTPS proxy using goproxy that listens on port `8080`, and forward the requests to the socks5 proxy on `socks5://localhost:1080`. It uses MITM (Man in the Middle) proxy mode to retriece and parse the request, and then forwards it to the destination using the socks5 proxy client implemented in the standard Go `net/http` library. ### Example usage: Aggregator server that have HTTP proxy server run on port `8080` and forward the requests to socks proxy listens on `socks5://localhost:1080` with no auth ```shell ./socks -v -addr ":8080" -socks "localhost:1080" ``` With auth: ```shell ./socks -v -addr ":8080" -socks "localhost:1080" -user "bob" -pass "123" ``` You can run the socks proxy server locally for testing with the following command - this will start a socks5 proxy server on port `1080` with no auth: ```shell ./socks5proxyserver/socks5proxyserver ``` ================================================ FILE: examples/cascadeproxy-socks/socks5proxyserver/go.mod ================================================ module socks5proxyserver go 1.20 require github.com/things-go/go-socks5 v0.0.5 ================================================ FILE: examples/cascadeproxy-socks/socks5proxyserver/go.sum ================================================ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/things-go/go-socks5 v0.0.5 h1:qvKaGcBkfDrUL33SchHN93srAmYGzb4CxSM2DPYufe8= github.com/things-go/go-socks5 v0.0.5/go.mod h1:mtzInf8v5xmsBpHZVbIw2YQYhc4K0jRwzfsH64Uh0IQ= golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= ================================================ FILE: examples/cascadeproxy-socks/socks5proxyserver/main.go ================================================ package main import ( "context" "github.com/things-go/go-socks5" "log" "net" "os" ) func main() { // Create a SOCKS5 server server := socks5.NewServer( socks5.WithLogger(socks5.NewLogger(log.New(os.Stdout, "socks5: ", log.LstdFlags))), socks5.WithDialAndRequest(func(ctx context.Context, network, addr string, request *socks5.Request) (net.Conn, error) { log.Printf("Request from %s to %s", request.RemoteAddr, request.DestAddr) return net.Dial(network, addr) }), ) // Create SOCKS5 proxy on localhost port 1080 if err := server.ListenAndServe("tcp", ":1080"); err != nil { panic(err) } } ================================================ FILE: examples/cascadeproxy-socks/socksproxy.go ================================================ package main import ( "crypto/tls" "flag" "log" "net/http" "net/url" "github.com/elazarl/goproxy" ) type SocksAuth struct { Username, Password string } func createSocksProxy(socksAddr string, auth SocksAuth) func(r *http.Request) (*url.URL, error) { return func(r *http.Request) (*url.URL, error) { Url := &url.URL{ Scheme: "socks5", Host: socksAddr, } if auth.Username != "" { Url.User = url.UserPassword(auth.Username, auth.Password) } return Url, nil } } func main() { verbose := flag.Bool("v", false, "should every proxy request be logged to stdout") addr := flag.String("addr", ":8080", "proxy listen address") socksAddr := flag.String("socks", "127.0.0.1:1080", "socks proxy address") username := flag.String("user", "", "username for SOCKS5 proxy if auth is required") password := flag.String("pass", "", "password for SOCKS5 proxy") flag.Parse() auth := SocksAuth{ Username: *username, Password: *password, } proxyServer := goproxy.NewProxyHttpServer() proxyServer.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { client := &http.Client{ Transport: &http.Transport{ Proxy: createSocksProxy(*socksAddr, auth), TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, }, } // https://stackoverflow.com/questions/19595860/http-request-requesturi-field-when-making-request-in-go req.RequestURI = "" resp, err := client.Do(req) if err != nil { ctx.Logf("Failed to forward request: " + err.Error()) return nil, nil } ctx.Logf("Succesfully forwarded request to socks proxy") return req, resp }) proxyServer.OnRequest().HandleConnect(goproxy.AlwaysMitm) proxyServer.Verbose = *verbose log.Fatalln(http.ListenAndServe(*addr, proxyServer)) } ================================================ FILE: examples/certstorage/README.md ================================================ # CertStorage CertStorage example is important to improve the performance of an HTTPS proxy server, which you can build using goproxy. Without a `proxy.CertStore`, every HTTPS request will generate new TLS certificates and this, repeated for hundreds of request, will destroy your CPU. A lot of people opened issues in the projects complaining about this, because they didn't use a certificates cache. The cache implementation is up to you, maybe you can cache only the most used hostnames, if you want to. ================================================ FILE: examples/certstorage/cache.go ================================================ package main import ( "crypto/tls" "sync" ) // CertStorage is a simple certificate cache that keeps // everything in memory. type CertStorage struct { certs map[string]*tls.Certificate mtx sync.RWMutex } func (cs *CertStorage) Fetch(hostname string, gen func() (*tls.Certificate, error)) (*tls.Certificate, error) { cs.mtx.RLock() cert, ok := cs.certs[hostname] cs.mtx.RUnlock() if ok { return cert, nil } cert, err := gen() if err != nil { return nil, err } cs.mtx.Lock() cs.certs[hostname] = cert cs.mtx.Unlock() return cert, nil } func NewCertStorage() *CertStorage { return &CertStorage{ certs: make(map[string]*tls.Certificate), } } ================================================ FILE: examples/certstorage/main.go ================================================ package main import ( "flag" "github.com/elazarl/goproxy" "log" "net/http" ) func main() { verbose := flag.Bool("v", false, "should every proxy request be logged to stdout") addr := flag.String("addr", ":8080", "proxy listen address") flag.Parse() proxy := goproxy.NewProxyHttpServer() proxy.CertStore = NewCertStorage() proxy.Verbose = *verbose proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { // Log requested URL log.Println(req.URL.String()) return req, nil }) // Start proxy server log.Fatal(http.ListenAndServe(*addr, proxy)) } ================================================ FILE: examples/customca/README.md ================================================ # CustomCA This example shows you how to use a custom CA to sign the HTTPS MITM requests (you can use your own generated certificates). If the client has some kind of SSL pinning to check the TLS certificates, all the request will most likely fail, so make sure to remove it before using this proxy or opening new issues. Proxy server will generate a custom certificate for the target host, for each request, and it's used to read the request data of an HTTPS connection. The client will establish a TLS connection using the generated certificate with the proxy server, the server will read the request data, process it according to the user needs, and then it will do a new request to the real destination. The CA certificate must be trusted by your system, or the client will reject the connection, since it's not recognized. ## Trust CA certificate The default CA certificate used by GoProxy is in the root folder of this project (in files `ca.pem`, and its private key `key.pem`). ### Use your certificate You can trust the default certificate or use your own with GoProxy, and trust it instead of the provided `ca.pem`. If you want to do this, just replace the occurrences of this file in the next paragraphs with your CA certificate filename. You can generate your own self-signed certificate with [openssl](https://stackoverflow.com/questions/10175812/how-to-generate-a-self-signed-ssl-certificate-using-openssl). ### Firefox You have to reach the certificate manager configuration in order to add the certificate to the trusted ones. To reach it, open the settings and type in search bar "Certificates", then click on the button "View Certificates...". In the tab "Authorities", click "Import..." and select the `ca.pem` file. GoProxy CA is now trusted by your browser! ### Chrome Open the certificate manager configuration: > "Settings" > "Privacy and Security" > "Security" > "Manage certificates" Go to the tab "Authorities", click "Import" and select the `ca.pem` file. GoProxy CA is now trusted by your browser! ### System If you want the root certificate to be trusted by all applications in your environment, consider adding it to the system trusted certificates. Here is a couple of guides about how to do it, but we don't provide any support: - [1](https://manuals.gfi.com/en/kerio/connect/content/server-configuration/ssl-certificates/adding-trusted-root-certificates-to-the-server-1605.html) - [2](https://unix.stackexchange.com/questions/90450/adding-a-self-signed-certificate-to-the-trusted-list) #### MkCert Do you want a managed, easy to use solution that automatically generates a root CA certificate for local usage, and automatically adds it to the trusted system certificates? Consider [MkCert](https://github.com/FiloSottile/mkcert). It's enough to just use it and add the generated trusted certificate to GoProxy. ================================================ FILE: examples/customca/cert.go ================================================ package main var _caCert = []byte(`-----BEGIN CERTIFICATE----- MIIDkzCCAnugAwIBAgIJAKe/ZGdfcHdPMA0GCSqGSIb3DQEBCwUAMGAxCzAJBgNV BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX aWRnaXRzIFB0eSBMdGQxGTAXBgNVBAMMEGRlbW8gZm9yIGdvcHJveHkwHhcNMTYw OTI3MTQzNzQ3WhcNMTkwOTI3MTQzNzQ3WjBgMQswCQYDVQQGEwJBVTETMBEGA1UE CAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk MRkwFwYDVQQDDBBkZW1vIGZvciBnb3Byb3h5MIIBIjANBgkqhkiG9w0BAQEFAAOC AQ8AMIIBCgKCAQEA2+W48YZoch72zj0a+ZlyFVY2q2MWmqsEY9f/u53fAeTxvPE6 1/DnqsydnA3FnGvxw9Dz0oZO6xG+PZvp+lhN07NZbuXK1nie8IpxCa342axpu4C0 69lZwxikpGyJO4IL5ywp/qfb5a2DxPTAyQOQ8ROAaydoEmktRp25yicnQ2yeZW// 1SIQxt7gRxQIGmuOQ/Gqr/XN/z2cZdbGJVRUvQXk7N6NhQiCX1zlmp1hzUW9jwC+ JEKKF1XVpQbc94Bo5supxhkKJ70CREPy8TH9mAUcQUZQRohnPvvt/lKneYAGhjHK vhpajwlbMMSocVXFvY7o/IqIE/+ZUeQTs1SUwQIDAQABo1AwTjAdBgNVHQ4EFgQU GnlWcIbfsWJW7GId+6xZIK8YlFEwHwYDVR0jBBgwFoAUGnlWcIbfsWJW7GId+6xZ IK8YlFEwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAoFUjSD15rKlY xudzyVlr6n0fRNhITkiZMX3JlFOvtHNYif8RfK4TH/oHNBTmle69AgixjMgy8GGd H90prytGQ5zCs1tKcCFsN5gRSgdAkc2PpRFOK6u8HwOITV5lV7sjucsddXJcOJbQ 4fyVe47V9TTxI+A7lRnUP2HYTR1Bd0R/IgRAH57d1ZHs7omHIuQ+Ea8ph2ppXMnP DXVOlZ9zfczSnPnQoomqULOU9Fq2ycyi8Y/ROtAHP6O7wCFbYHXhxojdaHSdhkcd troTflFMD2/4O6MtBKbHxSmEG6H0FBYz5xUZhZq7WUH24V3xYsfge29/lOCd5/Xf A+j0RJc/lQ== -----END CERTIFICATE-----`) var _caKey = []byte(`-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEA2+W48YZoch72zj0a+ZlyFVY2q2MWmqsEY9f/u53fAeTxvPE6 1/DnqsydnA3FnGvxw9Dz0oZO6xG+PZvp+lhN07NZbuXK1nie8IpxCa342axpu4C0 69lZwxikpGyJO4IL5ywp/qfb5a2DxPTAyQOQ8ROAaydoEmktRp25yicnQ2yeZW// 1SIQxt7gRxQIGmuOQ/Gqr/XN/z2cZdbGJVRUvQXk7N6NhQiCX1zlmp1hzUW9jwC+ JEKKF1XVpQbc94Bo5supxhkKJ70CREPy8TH9mAUcQUZQRohnPvvt/lKneYAGhjHK vhpajwlbMMSocVXFvY7o/IqIE/+ZUeQTs1SUwQIDAQABAoIBAHK94ww8W0G5QIWL Qwkc9XeGvg4eLUxVknva2Ll4fkZJxY4WveKx9OCd1lv4n7WoacYIwUGIDaQBZShW s/eKnkmqGy+PvpC87gqL4sHvQpuqqJ1LYpxylLEFqduWOuGPUVC2Lc+QnWCycsCS CgqZzsbMq0S+kkKRGSvw32JJneZCzqLgLNssQNVk+Gm6SI3s4jJsGPesjhnvoPaa xZK14uFpltaA05GSTDaQeZJFEdnnb3f/eNPc2xMEfi0S2ZlJ6Q92WJEOepAetDlR cRFi004bNyTb4Bphg8s4+9Cti5is199aFkGCRDWxeqEnc6aMY3Ezu9Qg3uttLVUd uy830GUCgYEA7qS0X+9UH1R02L3aoANyADVbFt2ZpUwQGauw9WM92pH52xeHAw1S ohus6FI3OC8xQq2CN525tGLUbFDZnNZ3YQHqFsfgevfnTs1//gbKXomitev0oFKh VT+WYS4lkgYtPlXzhdGuk32q99T/wIocAguvCUY3PiA7yBz93ReyausCgYEA6+P8 bugMqT8qjoiz1q/YCfxsw9bAGWjlVqme2xmp256AKtxvCf1BPsToAaJU3nFi3vkw ICLxUWAYoMBODJ3YnbOsIZOavdXZwYHv54JqwqFealC3DG0Du6fZYZdiY8pK+E6m 3fiYzP1WoVK5tU4bH8ibuIQvpcI8j7Gy0cV6/AMCgYBHl7fZNAZro72uLD7DVGVF 9LvP/0kR0uDdoqli5JPw12w6szM40i1hHqZfyBJy042WsFDpeHL2z9Nkb1jpeVm1 C4r7rJkGqwqElJf6UHUzqVzb8N6hnkhyN7JYkyyIQzwdgFGfaslRzBiXYxoa3BQM 9Q5c3OjDxY3JuhDa3DoVYwKBgDNqrWJLSD832oHZAEIicBe1IswJKjQfriWWsV6W mHSbdtpg0/88aZVR/DQm+xLFakSp0jifBTS0momngRu06Dtvp2xmLQuF6oIIXY97 2ON1owvPbibSOEcWDgb8pWCU/oRjOHIXts6vxctCKeKAFN93raGphm0+Ck9T72NU BTubAoGBAMEhI/Wy9wAETuXwN84AhmPdQsyCyp37YKt2ZKaqu37x9v2iL8JTbPEz pdBzkA2Gc0Wdb6ekIzRrTsJQl+c/0m9byFHsRsxXW2HnezfOFX1H4qAmF6KWP0ub M8aIn6Rab4sNPSrvKGrU6rFpv/6M33eegzldVnV9ku6uPJI1fFTC -----END RSA PRIVATE KEY-----`) ================================================ FILE: examples/customca/main.go ================================================ package main import ( "crypto/tls" "crypto/x509" "flag" "log" "net/http" "github.com/elazarl/goproxy" ) func main() { verbose := flag.Bool("v", false, "should every proxy request be logged to stdout") addr := flag.String("addr", ":8080", "proxy listen address") flag.Parse() cert, err := parseCA(_caCert, _caKey) if err != nil { log.Fatal(err) } customCaMitm := &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: goproxy.TLSConfigFromCA(cert)} var customAlwaysMitm goproxy.FuncHttpsHandler = func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) { return customCaMitm, host } proxy := goproxy.NewProxyHttpServer() proxy.OnRequest().HandleConnect(customAlwaysMitm) proxy.Verbose = *verbose log.Fatal(http.ListenAndServe(*addr, proxy)) } func parseCA(caCert, caKey []byte) (*tls.Certificate, error) { parsedCert, err := tls.X509KeyPair(caCert, caKey) if err != nil { return nil, err } if parsedCert.Leaf, err = x509.ParseCertificate(parsedCert.Certificate[0]); err != nil { return nil, err } return &parsedCert, nil } ================================================ FILE: examples/go.mod ================================================ module github.com/elazarl/goproxy/examples/goproxy-transparent go 1.23 require ( github.com/coder/websocket v1.8.14 github.com/elazarl/goproxy v1.5.0 github.com/elazarl/goproxy/ext v0.0.0-20250117123040-e9229c451ab8 github.com/inconshreveable/go-vhost v1.0.0 ) require ( golang.org/x/net v0.35.0 // indirect golang.org/x/text v0.22.0 // indirect ) replace github.com/elazarl/goproxy => ../ ================================================ FILE: examples/go.sum ================================================ 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/ext v0.0.0-20250117123040-e9229c451ab8 h1:rGxOExXmpBcmZc4ZnEXBGkcxSReZx7S9ECtuv6BtUYQ= github.com/elazarl/goproxy/ext v0.0.0-20250117123040-e9229c451ab8/go.mod h1:q2JQCFWg+AQfe6O2cbf7LJDB48R68w+q0pBU53v02iM= github.com/inconshreveable/go-vhost v1.0.0 h1:IK4VZTlXL4l9vz2IZoiSFbYaaqUW7dXJAiPriUN5Ur8= github.com/inconshreveable/go-vhost v1.0.0/go.mod h1:aA6DnFhALT3zH0y+A39we+zbrdMC2N0X/q21e6FI0LU= 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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: examples/goproxy-httpdump/README.md ================================================ # Trace HTTP Requests and Responses `goproxy-httpdump` starts an HTTP proxy on :8080. It handles explicit CONNECT requests and traces them in a "db" directory created in the proxy working directory. Each request type and headers are logged in a "log" file, while their bodies are dumped in files prefixed with the request session identifier. Additionally, the example demonstrates how to: - Log information asynchronously (see HttpLogger) - Allow the proxy to be stopped manually while ensuring all pending requests have been processed (in this case, logged). Start it in one shell: ```sh goproxy-httpdump ``` Fetch goproxy homepage in another: ```sh http_proxy=http://127.0.0.1:8080 wget -O - \ http://ripper234.com/p/introducing-goproxy-light-http-proxy/ ``` A "db" directory should have appeared where you started the proxy, containing two files: - log: the request/response traces - 1\_resp: the first response body ================================================ FILE: examples/goproxy-httpdump/httpdump.go ================================================ package main import ( "errors" "flag" "fmt" "io" "log" "net" "net/http" "net/http/httputil" "os" "os/signal" "path" "sync" "time" "github.com/elazarl/goproxy" "github.com/elazarl/goproxy/transport" ) type FileStream struct { path string f *os.File } func NewFileStream(path string) *FileStream { return &FileStream{path, nil} } func (fs *FileStream) Write(b []byte) (nr int, err error) { if fs.f == nil { fs.f, err = os.Create(fs.path) if err != nil { return 0, err } } return fs.f.Write(b) } func (fs *FileStream) Close() error { fmt.Println("Close", fs.path) if fs.f == nil { return errors.New("FileStream was never written into") } return fs.f.Close() } type Meta struct { req *http.Request resp *http.Response err error t time.Time sess int64 bodyPath string from string } func fprintf(nr *int64, err *error, w io.Writer, pat string, a ...any) { if *err != nil { return } var n int n, *err = fmt.Fprintf(w, pat, a...) *nr += int64(n) } func write(nr *int64, err *error, w io.Writer, b []byte) { if *err != nil { return } var n int n, *err = w.Write(b) *nr += int64(n) } func (m *Meta) WriteTo(w io.Writer) (nr int64, err error) { if m.req != nil { fprintf(&nr, &err, w, "Type: request\r\n") } else if m.resp != nil { fprintf(&nr, &err, w, "Type: response\r\n") } fprintf(&nr, &err, w, "ReceivedAt: %v\r\n", m.t) fprintf(&nr, &err, w, "Session: %d\r\n", m.sess) fprintf(&nr, &err, w, "From: %v\r\n", m.from) if m.err != nil { // note the empty response fprintf(&nr, &err, w, "Error: %v\r\n\r\n\r\n\r\n", m.err) } else if m.req != nil { fprintf(&nr, &err, w, "\r\n") buf, err2 := httputil.DumpRequest(m.req, false) if err2 != nil { return nr, err2 } write(&nr, &err, w, buf) } else if m.resp != nil { fprintf(&nr, &err, w, "\r\n") buf, err2 := httputil.DumpResponse(m.resp, false) if err2 != nil { return nr, err2 } write(&nr, &err, w, buf) } return } // HttpLogger is an asynchronous HTTP request/response logger. It traces // requests and responses headers in a "log" file in logger directory and dumps // their bodies in files prefixed with the session identifiers. // Close it to ensure pending items are correctly logged. type HttpLogger struct { path string c chan *Meta errch chan error } func NewLogger(basepath string) (*HttpLogger, error) { f, err := os.Create(path.Join(basepath, "log")) if err != nil { return nil, err } logger := &HttpLogger{basepath, make(chan *Meta), make(chan error)} go func() { for m := range logger.c { if _, err := m.WriteTo(f); err != nil { log.Println("Can't write meta", err) } } logger.errch <- f.Close() }() return logger, nil } func (logger *HttpLogger) LogResp(resp *http.Response, ctx *goproxy.ProxyCtx) { body := path.Join(logger.path, fmt.Sprintf("%d_resp", ctx.Session)) from := "" if ctx.UserData != nil { from = ctx.UserData.(*transport.RoundTripDetails).TCPAddr.String() } if resp == nil { resp = emptyResp } else { resp.Body = NewTeeReadCloser(resp.Body, NewFileStream(body)) } logger.LogMeta(&Meta{ resp: resp, err: ctx.Error, t: time.Now(), sess: ctx.Session, from: from}) } var emptyResp = &http.Response{} var emptyReq = &http.Request{} func (logger *HttpLogger) LogReq(req *http.Request, ctx *goproxy.ProxyCtx) { body := path.Join(logger.path, fmt.Sprintf("%d_req", ctx.Session)) if req == nil { req = emptyReq } else { req.Body = NewTeeReadCloser(req.Body, NewFileStream(body)) } logger.LogMeta(&Meta{ req: req, err: ctx.Error, t: time.Now(), sess: ctx.Session, from: req.RemoteAddr}) } func (logger *HttpLogger) LogMeta(m *Meta) { logger.c <- m } func (logger *HttpLogger) Close() error { close(logger.c) return <-logger.errch } // TeeReadCloser extends io.TeeReader by allowing reader and writer to be // closed. type TeeReadCloser struct { r io.Reader w io.WriteCloser c io.Closer } func NewTeeReadCloser(r io.ReadCloser, w io.WriteCloser) io.ReadCloser { return &TeeReadCloser{io.TeeReader(r, w), w, r} } func (t *TeeReadCloser) Read(b []byte) (int, error) { return t.r.Read(b) } // Close attempts to close the reader and write. It returns an error if both // failed to Close. func (t *TeeReadCloser) Close() error { err1 := t.c.Close() err2 := t.w.Close() if err1 != nil { return err1 } return err2 } // stoppableListener serves stoppableConn and tracks their lifetime to notify // when it is safe to terminate the application. type stoppableListener struct { net.Listener sync.WaitGroup } type stoppableConn struct { net.Conn wg *sync.WaitGroup } func newStoppableListener(l net.Listener) *stoppableListener { return &stoppableListener{l, sync.WaitGroup{}} } func (sl *stoppableListener) Accept() (net.Conn, error) { c, err := sl.Listener.Accept() if err != nil { return c, err } sl.Add(1) return &stoppableConn{c, &sl.WaitGroup}, nil } func (sc *stoppableConn) Close() error { sc.wg.Done() return sc.Conn.Close() } func main() { verbose := flag.Bool("v", false, "should every proxy request be logged to stdout") addr := flag.String("l", ":8080", "on which address should the proxy listen") flag.Parse() proxy := goproxy.NewProxyHttpServer() proxy.Verbose = *verbose if err := os.MkdirAll("db", 0755); err != nil { log.Fatal("Can't create dir", err) } logger, err := NewLogger("db") if err != nil { log.Fatal("can't open log file", err) } tr := transport.Transport{Proxy: transport.ProxyFromEnvironment} // For every incoming request, override the RoundTripper to extract // connection information. Store it is session context log it after // handling the response. proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { ctx.RoundTripper = goproxy.RoundTripperFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (resp *http.Response, err error) { ctx.UserData, resp, err = tr.DetailedRoundTrip(req) return }) logger.LogReq(req, ctx) return req, nil }) proxy.OnResponse().DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { logger.LogResp(resp, ctx) return resp }) l, err := net.Listen("tcp", *addr) if err != nil { log.Fatal("listen:", err) } sl := newStoppableListener(l) ch := make(chan os.Signal) signal.Notify(ch, os.Interrupt) go func() { <-ch log.Println("Got SIGINT exiting") sl.Add(1) sl.Close() logger.Close() sl.Done() }() log.Println("Starting Proxy") http.Serve(sl, proxy) sl.Wait() log.Println("All connections closed - exit") } ================================================ FILE: examples/goproxy-transparent/README.md ================================================ # Transparent Proxy This transparent example in goproxy is meant to show how to transparent proxy and hijack all http and https connections while doing a man-in-the-middle to the TLS session. It requires that goproxy sees all the packets traversing out to the internet. Linux iptables rules deal with changing the source/destination IPs to act transparently, but you do need to set up your network configuration so that goproxy is a mandatory stop on the outgoing route. Primarily you can do this by placing the proxy inline. goproxy does not have any WCCP support itself; patches are welcome. ## Why not explicit? Transparent proxies are more difficult to maintain and set up from a server side, but they require no configuration on the client(s) which could be in unmanaged systems or systems that don't support a proxy configuration. See the [eavesdropper example](https://github.com/elazarl/goproxy/blob/master/examples/goproxy-eavesdropper/main.go) if you want to see an explicit proxy example. ## Potential Issues Support for very old clients using HTTPS will fail. Clients need to send the SNI value in the TLS ClientHello which most modern clients do these days, but old clients will break. If you're routing table allows for it, an explicit http request to goproxy will cause it to fail in an endless loop since it will try to request resources from itself repeatedly. This could be solved in the goproxy code by looking up the hostnames, but it adds a delay that is much easier/faster to handle on the routing side. ## Routing Rules Example routing rules are included in [proxy.sh](https://github.com/elazarl/goproxy/blob/master/examples/goproxy-transparent/proxy.sh) but are best when set up using your distribution's configuration. ================================================ FILE: examples/goproxy-transparent/proxy.sh ================================================ #!/bin/sh # goproxy IP GOPROXY_SERVER="10.10.10.1" # goproxy port GOPROXY_PORT="3129" GOPROXY_PORT_TLS="3128" # DO NOT MODIFY BELOW # Load IPTABLES modules for NAT and IP conntrack support modprobe ip_conntrack modprobe ip_conntrack_ftp echo 1 > /proc/sys/net/ipv4/ip_forward echo 2 > /proc/sys/net/ipv4/conf/all/rp_filter # Clean old firewall iptables -t nat -F iptables -t nat -X iptables -t mangle -F iptables -t mangle -X # Write new rules iptables -t nat -A PREROUTING -s $GOPROXY_SERVER -p tcp --dport $GOPROXY_PORT -j ACCEPT iptables -t nat -A PREROUTING -s $GOPROXY_SERVER -p tcp --dport $GOPROXY_PORT_TLS -j ACCEPT iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination $GOPROXY_SERVER:$GOPROXY_PORT iptables -t nat -A PREROUTING -p tcp --dport 443 -j DNAT --to-destination $GOPROXY_SERVER:$GOPROXY_PORT_TLS # The following line supports using goproxy as an explicit proxy in addition iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination $GOPROXY_SERVER:$GOPROXY_PORT iptables -t nat -A POSTROUTING -j MASQUERADE iptables -t mangle -A PREROUTING -p tcp --dport $GOPROXY_PORT -j DROP iptables -t mangle -A PREROUTING -p tcp --dport $GOPROXY_PORT_TLS -j DROP ================================================ FILE: examples/goproxy-transparent/transparent.go ================================================ package main import ( "bufio" "bytes" "context" "flag" "fmt" "log" "net" "net/http" "net/url" "regexp" "github.com/elazarl/goproxy" "github.com/inconshreveable/go-vhost" ) func orPanic(err error) { if err != nil { panic(err) } } func main() { verbose := flag.Bool("v", true, "should every proxy request be logged to stdout") http_addr := flag.String("httpaddr", ":3129", "proxy http listen address") https_addr := flag.String("httpsaddr", ":3128", "proxy https listen address") flag.Parse() proxy := goproxy.NewProxyHttpServer() proxy.Verbose = *verbose if proxy.Verbose { log.Printf("Server starting up! - configured to listen on http interface %s and https interface %s", *http_addr, *https_addr) } proxy.NonproxyHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { if req.Host == "" { fmt.Fprintln(w, "Cannot handle requests without Host header, e.g., HTTP 1.0") return } req.URL.Scheme = "http" req.URL.Host = req.Host proxy.ServeHTTP(w, req) }) proxy.OnRequest(goproxy.ReqHostMatches(regexp.MustCompile("^.*$"))). HandleConnect(goproxy.AlwaysMitm) proxy.OnRequest(goproxy.ReqHostMatches(regexp.MustCompile("^.*:80$"))). HijackConnect(func(req *http.Request, client net.Conn, ctx *goproxy.ProxyCtx) { defer func() { if e := recover(); e != nil { ctx.Logf("error connecting to remote: %v", e) client.Write([]byte("HTTP/1.1 500 Cannot reach destination\r\n\r\n")) } client.Close() }() clientBuf := bufio.NewReadWriter(bufio.NewReader(client), bufio.NewWriter(client)) remote, err := connectDial(req.Context(), proxy, "tcp", req.URL.Host) orPanic(err) remoteBuf := bufio.NewReadWriter(bufio.NewReader(remote), bufio.NewWriter(remote)) for { req, err := http.ReadRequest(clientBuf.Reader) orPanic(err) orPanic(req.Write(remoteBuf)) orPanic(remoteBuf.Flush()) resp, err := http.ReadResponse(remoteBuf.Reader, req) orPanic(err) orPanic(resp.Write(clientBuf.Writer)) orPanic(clientBuf.Flush()) } }) go func() { log.Fatalln(http.ListenAndServe(*http_addr, proxy)) }() // listen to the TLS ClientHello but make it a CONNECT request instead ln, err := net.Listen("tcp", *https_addr) if err != nil { log.Fatalf("Error listening for https connections - %v", err) } for { c, err := ln.Accept() if err != nil { log.Printf("Error accepting new connection - %v", err) continue } go func(c net.Conn) { tlsConn, err := vhost.TLS(c) if err != nil { log.Printf("Error accepting new connection - %v", err) } if tlsConn.Host() == "" { log.Printf("Cannot support non-SNI enabled clients") return } connectReq := &http.Request{ Method: http.MethodConnect, URL: &url.URL{ Opaque: tlsConn.Host(), Host: net.JoinHostPort(tlsConn.Host(), "443"), }, Host: tlsConn.Host(), Header: make(http.Header), RemoteAddr: c.RemoteAddr().String(), } resp := dumbResponseWriter{tlsConn} proxy.ServeHTTP(resp, connectReq) }(c) } } // copied/converted from https.go func dial(ctx context.Context, proxy *goproxy.ProxyHttpServer, network, addr string) (c net.Conn, err error) { if proxy.Tr.DialContext != nil { return proxy.Tr.DialContext(ctx, network, addr) } var d net.Dialer return d.DialContext(ctx, network, addr) } // copied/converted from https.go func connectDial(ctx context.Context, proxy *goproxy.ProxyHttpServer, network, addr string) (c net.Conn, err error) { if proxy.ConnectDial == nil { return dial(ctx, proxy, network, addr) } return proxy.ConnectDial(network, addr) } type dumbResponseWriter struct { net.Conn } func (dumb dumbResponseWriter) Header() http.Header { panic("Header() should not be called on this ResponseWriter") } func (dumb dumbResponseWriter) Write(buf []byte) (int, error) { if bytes.Equal(buf, []byte("HTTP/1.0 200 OK\r\n\r\n")) { return len(buf), nil // throw away the HTTP OK response from the faux CONNECT request } return dumb.Conn.Write(buf) } func (dumb dumbResponseWriter) WriteHeader(code int) { panic("WriteHeader() should not be called on this ResponseWriter") } func (dumb dumbResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { return dumb, bufio.NewReadWriter(bufio.NewReader(dumb), bufio.NewWriter(dumb)), nil } ================================================ FILE: examples/hijack/README.md ================================================ # Hijack In this example we intercept the data of an HTTP request and decide to modify them before sending to the client. In this mode, we take over on the raw connection and we could send any data that we want. Curl example: ``` $ curl -x localhost:8080 http://google.it -v -k -p * Host localhost:8080 was resolved. * IPv6: ::1 * IPv4: 127.0.0.1 * Trying [::1]:8080... * Connected to localhost (::1) port 8080 * CONNECT tunnel: HTTP/1.1 negotiated * allocate connect buffer * Establish HTTP proxy tunnel to google.it:80 > CONNECT google.it:80 HTTP/1.1 > Host: google.it:80 > User-Agent: curl/8.9.1 > Proxy-Connection: Keep-Alive > < HTTP/1.1 200 Ok < * CONNECT phase completed * CONNECT tunnel established, response 200 > GET / HTTP/1.1 > Host: google.it > User-Agent: curl/8.9.1 > Accept: */* > < HTTP/1.1 200 OK < test: 1234 < Content-Length: 0 ``` ================================================ FILE: examples/hijack/main.go ================================================ package main import ( "bufio" "flag" "log" "net" "net/http" "regexp" "github.com/elazarl/goproxy" ) func main() { proxy := goproxy.NewProxyHttpServer() // Reject all requests to baidu proxy.OnRequest(goproxy.ReqHostMatches(regexp.MustCompile("baidu.*:443$"))). HandleConnect(goproxy.AlwaysReject) // Instead of returning the Internet response, send custom data from // our proxy server, using connection hijack proxy.OnRequest(goproxy.ReqHostMatches(regexp.MustCompile("^.*$"))). HijackConnect(func(req *http.Request, client net.Conn, ctx *goproxy.ProxyCtx) { client.Write([]byte("HTTP/1.1 200 Ok\r\n\r\n")) w := bufio.NewWriter(client) resp := &http.Response{ StatusCode: http.StatusOK, ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{ "test": {"1234"}, }, } resp.Write(w) w.Flush() client.Close() }) verbose := flag.Bool("v", false, "should every proxy request be logged to stdout") addr := flag.String("addr", ":8080", "proxy listen address") flag.Parse() proxy.Verbose = *verbose log.Fatal(http.ListenAndServe(*addr, proxy)) } ================================================ FILE: examples/html-parser/README.md ================================================ # HTML Parser `html-parser` starts an HTTP proxy on :8080. It checks HTML responses, looks for scripts referencing jQuery library and log warnings if different versions of the library are being used for a given host. This is an example of how a proxy can parse the received responses and manipulate them to do useful actions. Start the server: ```sh go build html-parser ``` Make a test request in another shell: ```sh http_proxy=http://127.0.0.1:8080 wget -O - \ http://ripper234.com/p/introducing-goproxy-light-http-proxy/ ``` Goproxy example homepage contains jQuery and a mix of JQuery plugins. First the proxy reports the first use of jQuery it detects for the domain. Then, because the regular expression matching the jQuery sources is imprecise, it reports a mismatch with a plugin reference: ```sh 2015/04/11 11:23:02 [001] WARN: ripper234.com uses //ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js 2015/04/11 11:23:02 [001] WARN: In http://ripper234.com/p/introducing-goproxy-light-http-proxy/, \ Contradicting jqueries //ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js \ http://ripper234.wpengine.netdna-cdn.com/wp-content/plugins/wp-ajax-edit-comments/js/jquery.colorbox.min.js?ver=5.0.36 ``` ================================================ FILE: examples/html-parser/jquery1.html ================================================ ================================================ FILE: examples/html-parser/jquery2.html ================================================ ================================================ FILE: examples/html-parser/jquery_homepage.html ================================================ jQuery: The Write Less, Do More, JavaScript Library

jQuery is a new kind of JavaScript Library.

jQuery is a fast and concise JavaScript Library that simplifies HTML document traversing, event handling, animating, and Ajax interactions for rapid web development. jQuery is designed to change the way that you write JavaScript.

Grab the latest version!

Choose your compression level:
jquery-1.7.2.min.js jquery-1.7.2.js

Current Release: v1.7.2

Learn jQuery Now!

What does jQuery code look like? Here's the quick and dirty:

$("p.neat").addClass("ohmy").show("slow");
Run Code

Congratulations! You just ran a snippet of jQuery code. Wasn't that easy? There's lots of example code throughout the documentation on this site. Be sure to give all the code a test run to see what happens.

Books About jQuery

================================================ FILE: examples/html-parser/jquery_test.go ================================================ package main import ( "bytes" "io" "log" "net/http" "net/http/httptest" "net/url" "os" "strings" "testing" ) func equal(u, v []string) bool { if len(u) != len(v) { return false } for i := range u { if u[i] != v[i] { return false } } return true } func readFile(fname string, t *testing.T) string { b, err := os.ReadFile(fname) if err != nil { t.Fatal("readFile", err) } return string(b) } func TestDefectiveScriptParser(t *testing.T) { if l := len(findScriptSrc(` `)); l != 0 { t.Fail() } urls := findScriptSrc(readFile("w3schools.html", t)) if !equal(urls, []string{"http://partner.googleadservices.com/gampad/google_service.js", "//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"}) { t.Error("w3schools.html", "src scripts are not recognized", urls) } urls = findScriptSrc(readFile("jquery_homepage.html", t)) if !equal(urls, []string{"http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js", "http://code.jquery.com/jquery-1.4.2.min.js", "http://static.jquery.com/files/rocker/scripts/custom.js", "http://static.jquery.com/donate/donate.js"}) { t.Error("jquery_homepage.html", "src scripts are not recognized", urls) } } func proxyWithLog() (*http.Client, *bytes.Buffer) { proxy := NewJQueryVersionProxy() proxyServer := httptest.NewServer(proxy) buf := new(bytes.Buffer) proxy.Logger = log.New(buf, "", 0) proxyUrl, _ := url.Parse(proxyServer.URL) tr := &http.Transport{Proxy: http.ProxyURL(proxyUrl)} client := &http.Client{Transport: tr} return client, buf } func get(t *testing.T, server *httptest.Server, client *http.Client, url string) { resp, err := client.Get(server.URL + url) if err != nil { t.Fatal("cannot get proxy", err) } io.ReadAll(resp.Body) resp.Body.Close() } func TestProxyServiceTwoVersions(t *testing.T) { var fs = httptest.NewServer(http.FileServer(http.Dir("."))) defer fs.Close() client, buf := proxyWithLog() get(t, fs, client, "/w3schools.html") get(t, fs, client, "/php_man.html") if buf.String() != "" && !strings.Contains(buf.String(), " uses jquery ") { t.Error("shouldn't warn on a single URL", buf.String()) } get(t, fs, client, "/jquery1.html") warnings := buf.String() if !strings.Contains(warnings, "http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js") || !strings.Contains(warnings, "jquery.1.4.js") || !strings.Contains(warnings, "Contradicting") { t.Error("contradicting jquery versions (php_man.html, w3schools.html) does not issue warning", warnings) } } func TestProxyService(t *testing.T) { var fs = httptest.NewServer(http.FileServer(http.Dir("."))) defer fs.Close() client, buf := proxyWithLog() get(t, fs, client, "/jquery_homepage.html") warnings := buf.String() if !strings.Contains(warnings, "http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js") || !strings.Contains(warnings, "http://code.jquery.com/jquery-1.4.2.min.js") || !strings.Contains(warnings, "Contradicting") { t.Error("contradicting jquery versions does not issue warning") } } ================================================ FILE: examples/html-parser/main.go ================================================ package main import ( "github.com/elazarl/goproxy" "github.com/elazarl/goproxy/ext/html" "log" "net/http" "regexp" ) var ( // who said we can't parse HTML with regexp? scriptMatcher = regexp.MustCompile(`(?i:]*\ssrc=["']([^"']*)["'])`) ) // findScripts returns all sources of HTML script tags found in input text. func findScriptSrc(html string) []string { srcs := make([]string, 0) matches := scriptMatcher.FindAllStringIndex(html, -1) for _, match := range matches { // -1 to capture the whitespace at the end of the script tag srcMatch := srcAttrMatcher.FindStringSubmatch(html[match[1]-1:]) if srcMatch != nil { srcs = append(srcs, srcMatch[1]) } } return srcs } // NewJQueryVersionProxy creates a proxy checking responses HTML content, looks // for scripts referencing jQuery library and emits warnings if different // versions of the library are being used for a given host. func NewJQueryVersionProxy() *goproxy.ProxyHttpServer { proxy := goproxy.NewProxyHttpServer() m := make(map[string]string) jqueryMatcher := regexp.MustCompile(`(?i:jquery\.)`) proxy.OnResponse(goproxy_html.IsHtml).Do(goproxy_html.HandleString( func(s string, ctx *goproxy.ProxyCtx) string { for _, src := range findScriptSrc(s) { if !jqueryMatcher.MatchString(src) { continue } prev, ok := m[ctx.Req.Host] if ok { if prev != src { ctx.Warnf("In %v, Contradicting jqueries %v %v", ctx.Req.URL, prev, src) break } } else { ctx.Warnf("%s uses jquery %s", ctx.Req.Host, src) m[ctx.Req.Host] = src } } return s })) return proxy } func main() { proxy := NewJQueryVersionProxy() log.Fatal(http.ListenAndServe(":8080", proxy)) } ================================================ FILE: examples/html-parser/php_man.html ================================================ PHP: PHP Manual - Manual

search for in the

 
[edit] Last updated: Fri, 23 Mar 2012

view this page in

PHP Manual

by:
Mehdi Achour
Friedhelm Betz
Antony Dovgal
Nuno Lopes
Hannes Magnusson
Georg Richter
Damien Seguy
Jakub Vrana
2012-03-23
Edited By: Philip Olson


add a note add a note User Contributed Notes PHP Manual
There are no user contributed notes for this page.

 
================================================ FILE: examples/html-parser/w3schools.html ================================================ HTML5 Tutorial

HTML5 Tutorial


HTML5 is The New HTML Standard

HTML5

  • New Elements
  • New Attributes
  • Full CSS3 Support
  • Video and Audio
  • 2D/3D Graphics
  • Local Storage
  • Local SQL Database
  • Web Applications

Examples in Each Chapter

With our HTML editor, you can edit the HTML, and click on a button to view the result.

Example

<!DOCTYPE HTML>
<html>
<body>

<video width="320" height="240" controls="controls">
  <source src="movie.mp4" type="video/mp4" />
  <source src="movie.ogg" type="video/ogg" />
  <source src="movie.webm" type="video/webm" />
Your browser does not support the video tag.
</video>

</body>
</html>

Try it yourself »

Click on the "Try it yourself" button to see how it works

Start learning HTML5 now!

HTML5 References

At W3Schools you will find complete references about tags, global attributes, standard events, and more.

HTML5 Tag Reference


WEB HOSTING
Best Web Hosting
PHP MySQL Hosting
Best Hosting Coupons
UK Reseller Hosting
Cloud Hosting
Top Web Hosting
$3.98 Unlimited Hosting
Premium Website Design
WEB BUILDING
Download XML Editor
FREE Website BUILDER
Free Website Templates Free CSS Templates
CREATE HTML Websites
W3SCHOOLS EXAMS
Get Certified in:
HTML, CSS, JavaScript, XML, PHP, and ASP
W3SCHOOLS BOOKS
New Books:
HTML, CSS
JavaScript, and Ajax
STATISTICS
Browser Statistics
Browser OS
Browser Display
SHARE THIS PAGE


================================================ FILE: examples/image-manipulation/README.md ================================================ # Image Manipulation This example starts a proxy server that manipulate the received images, to make them appear upside down. This directly modify the response received from the response server, and returns the new data to the proxy caller. ================================================ FILE: examples/image-manipulation/main.go ================================================ package main import ( "github.com/elazarl/goproxy" "github.com/elazarl/goproxy/ext/image" "image" "log" "net/http" ) func main() { proxy := goproxy.NewProxyHttpServer() proxy.OnResponse().Do(goproxy_image.HandleImage(func(img image.Image, ctx *goproxy.ProxyCtx) image.Image { dx, dy := img.Bounds().Dx(), img.Bounds().Dy() newImg := image.NewRGBA(img.Bounds()) for i := 0; i < dx; i++ { for j := 0; j <= dy; j++ { newImg.Set(i, j, img.At(i, dy-j-1)) } } return newImg })) proxy.Verbose = true log.Fatal(http.ListenAndServe(":8080", proxy)) } ================================================ FILE: examples/redirect-https/README.md ================================================ # Redirect HTTPS `redirect-https` example redirects all the HTTPS request to HTTP endpoint, by returning a `303 See Other` HTTP response to the client. The client will then make another request using the HTTP scheme. ================================================ FILE: examples/redirect-https/main.go ================================================ package main import ( "bytes" "flag" "github.com/elazarl/goproxy" "io" "log" "net/http" ) func main() { verbose := flag.Bool("v", false, "should every proxy request be logged to stdout") addr := flag.String("addr", ":8080", "proxy listen address") flag.Parse() proxy := goproxy.NewProxyHttpServer() proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { if req.URL.Scheme != "https" { return req, nil } req.URL.Scheme = "http" resp := &http.Response{ StatusCode: http.StatusSeeOther, ProtoMajor: 1, ProtoMinor: 1, Request: req, Header: http.Header{ "Location": []string{req.URL.String()}, }, Body: io.NopCloser(bytes.NewReader(nil)), ContentLength: 0, } return nil, resp }) proxy.Verbose = *verbose log.Fatal(http.ListenAndServe(*addr, proxy)) } ================================================ FILE: examples/remove-https/README.md ================================================ # Remove HTTPS `remove-https` example forwards all the HTTPS request as HTTP requests, effectively removing the https schema from the requests. This example shows you how you can rewrite the request URL, when needed. This is important because shows you how to effectively use MITM, to intercept the request and read its data. ================================================ FILE: examples/remove-https/main.go ================================================ package main import ( "flag" "github.com/elazarl/goproxy" "log" "net/http" ) func main() { verbose := flag.Bool("v", false, "should every proxy request be logged to stdout") addr := flag.String("addr", ":8080", "proxy listen address") flag.Parse() proxy := goproxy.NewProxyHttpServer() proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { if req.URL.Scheme == "https" { req.URL.Scheme = "http" } return req, nil }) proxy.Verbose = *verbose log.Fatal(http.ListenAndServe(*addr, proxy)) } ================================================ FILE: examples/request-filtering/README.md ================================================ # Request Filtering `request-filtering` starts an HTTP proxy on :8080. It denies requests to "www.reddit.com" made between 8am to 5pm inclusive, local server time. Start the server: ```sh $ request-filtering ``` Make a test request in another shell: ```sh $ http_proxy=http://127.0.0.1:8080 wget -O - http://www.reddit.com --2015-04-11 16:59:01-- http://www.reddit.com/ Connecting to 127.0.0.1:8080... connected. Proxy request sent, awaiting response... 403 Forbidden 2015-04-11 16:59:01 ERROR 403: Forbidden. ``` ================================================ FILE: examples/request-filtering/noreddit.go ================================================ package main import ( "github.com/elazarl/goproxy" "log" "net/http" "time" ) func main() { proxy := goproxy.NewProxyHttpServer() proxy.OnRequest(goproxy.DstHostIs("www.reddit.com")).DoFunc( func(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { h, _, _ := time.Now().Clock() if h >= 8 && h <= 17 { return r, goproxy.NewResponse(r, goproxy.ContentTypeText, http.StatusForbidden, "Don't waste your time!") } else { ctx.Warnf("clock: %d, you can waste your time...", h) } return r, nil }) log.Fatalln(http.ListenAndServe(":8080", proxy)) } ================================================ FILE: examples/socket-keepalive/README.md ================================================ # Socket KeepAlive `socket-keepalive` example adds a custom net.Dialer that can be configured by the user, enabling TCP keep alives in this example. By default, Go already uses 15 seconds TCP keep alives for the connections, so this example is not strictly required, as it is provided. TCP keep alives are useful for a connection that can be idle for a while, to avoid the TCP connection close. The TCP connection is closed when the request context expires. ================================================ FILE: examples/socket-keepalive/keepalive.go ================================================ package main import ( "context" "flag" "github.com/elazarl/goproxy" "log" "net" "net/http" ) func main() { verbose := flag.Bool("v", false, "should every proxy request be logged to stdout") addr := flag.String("addr", ":8080", "proxy listen address") flag.Parse() proxy := goproxy.NewProxyHttpServer() proxy.Tr.DialContext = func(ctx context.Context, network, addr string) (c net.Conn, err error) { var d net.Dialer c, err = d.DialContext(ctx, network, addr) if c, ok := c.(*net.TCPConn); err == nil && ok { c.SetKeepAlive(true) go func() { <-ctx.Done() c.Close() }() } return } proxy.Verbose = *verbose log.Fatal(http.ListenAndServe(*addr, proxy)) } ================================================ FILE: examples/websockets/README.md ================================================ # Websockets `websockets` example shows an example of a WebSocket request made through the proxy server. The target server continuously send data to the client, and the proxy will forward them to websocket client. To run this example, it's enough to just run the main. Inside this folder there are also some self-signed certificates valid for localhost, that are automatically used for the WebSocket server. ================================================ FILE: examples/websockets/localhost-key.pem ================================================ -----BEGIN EC PRIVATE KEY----- MHcCAQEEIC1vu3JK+Z6WtbpaTL4LquNQVcwSha53sVIxEdcLNG6WoAoGCCqGSM49 AwEHoUQDQgAE8OpaVYv567gc00WZ8nSTDvcic+8tW7dCgGffiJogyesaERxDi+fg 0E/9WiNrF66Pl05I3r4NFD0EoIlcaSbMqw== -----END EC PRIVATE KEY----- ================================================ FILE: examples/websockets/localhost.pem ================================================ -----BEGIN CERTIFICATE----- MIICozCCAYugAwIBAgIUEdJ4Tu/Hs4HznpHjnb3Akj7uNKgwDQYJKoZIhvcNAQEL BQAwDTELMAkGA1UEAxMCQ0EwHhcNMTYwNTA1MjIxMjAwWhcNMjEwNTA0MjIxMjAw WjBGMQswCQYDVQQGEwJVUzEWMBQGA1UECBMNU2FuIEZyYW5jaXNjbzELMAkGA1UE BxMCQ0ExEjAQBgNVBAMTCWxvY2FsaG9zdDBZMBMGByqGSM49AgEGCCqGSM49AwEH A0IABPDqWlWL+eu4HNNFmfJ0kw73InPvLVu3QoBn34iaIMnrGhEcQ4vn4NBP/Voj axeuj5dOSN6+DRQ9BKCJXGkmzKujgYwwgYkwDgYDVR0PAQH/BAQDAgWgMBMGA1Ud JQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFD8hwmfkMLy1 9iajA7jsiEzBDT6DMB8GA1UdIwQYMBaAFBZgsrPxVqk7nrwIzeqIEl62Nj3IMBQG A1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAPYJUCwgNCVLm oITJdYtroJIOt3LegIQJWWYll4J3/WidfmW4FWD4Qumd0YRjqgIhTcgmxHWhhJXB KFd+hMD+UUs7jdYRQ80Ch/S/r/PzBNLiHkZ+ILklCU2JG25hJjXX48BB5y1eVF7O 9ds5h6KHJqNwG6B5yfB5iPxpd4/Qk/+6vNYODX5LSQqo9MpGotByjMGywcY4TesF 5AYT7pPoRkxucAifTRjw/8k653Zm2ZA9HCo+i9GrW+kyk3XP5Xk8z0GuvOCPnBIu p1jiUOH84pXap2cuR0z3vkp76mPAgsBk64RCBjCJJV8LEQbiPW4W9dQjcvlLkW7X lP5b19hr6g== -----END CERTIFICATE----- ================================================ FILE: examples/websockets/main.go ================================================ package main import ( "context" "crypto/tls" "github.com/coder/websocket" "github.com/elazarl/goproxy" "log" "net/http" "net/url" "os" "os/signal" "time" ) func echo(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, nil) if err != nil { log.Printf("upgrade: %v\n", err) return } defer c.Close(websocket.StatusNormalClosure, "") ctx := context.Background() for { mt, message, err := c.Read(ctx) if err != nil { log.Printf("read: %v\n", err) break } log.Printf("recv: %s\n", message) if err := c.Write(ctx, mt, message); err != nil { log.Printf("write: %v\n", err) break } } } func StartEchoServer() { log.Println("Starting echo server") go func() { http.HandleFunc("/", echo) err := http.ListenAndServeTLS(":12345", "localhost.pem", "localhost-key.pem", nil) if err != nil { log.Fatal(err) } }() } func StartProxy() { log.Println("Starting proxy server") go func() { proxy := goproxy.NewProxyHttpServer() proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) proxy.Verbose = true if err := http.ListenAndServe(":54321", proxy); err != nil { log.Fatal(err) } }() } func main() { interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt) StartEchoServer() StartProxy() proxyUrl := "http://localhost:54321" parsedProxy, err := url.Parse(proxyUrl) if err != nil { log.Fatal("unable to parse proxy URL") } ctx := context.Background() endpointUrl := "wss://localhost:12345" c, _, err := websocket.Dial(ctx, endpointUrl, &websocket.DialOptions{ HTTPClient: &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, Proxy: http.ProxyURL(parsedProxy), }, }, Subprotocols: []string{"p1"}, }) if err != nil { log.Fatal("dial:", err) } done := make(chan struct{}) go func() { defer close(done) for { _, message, err := c.Read(ctx) if err != nil { log.Println("read:", err) return } log.Printf("recv: %s", message) } }() ticker := time.NewTicker(time.Second) defer ticker.Stop() for { select { case t := <-ticker.C: // Message send // Write current time to the websocket client every 1 second if err := c.Write(ctx, websocket.MessageText, []byte(t.String())); err != nil { log.Println("write:", err) return } case <-interrupt: // Server shutdown log.Println("interrupt") // To cleanly close a connection, a client should send a close // frame and wait for the server to close the connection. err := c.Close(websocket.StatusNormalClosure, "") if err != nil { log.Println("write close:", err) return } select { case <-done: case <-time.After(time.Second): } return } } } ================================================ FILE: ext/auth/basic.go ================================================ package auth import ( "bytes" "encoding/base64" "io" "net/http" "strings" "github.com/elazarl/goproxy" ) var unauthorizedMsg = []byte("407 Proxy Authentication Required") func BasicUnauthorized(req *http.Request, realm string) *http.Response { // TODO(elazar): verify realm is well formed return &http.Response{ StatusCode: http.StatusProxyAuthRequired, ProtoMajor: 1, ProtoMinor: 1, Request: req, Header: http.Header{ "Proxy-Authenticate": []string{"Basic realm=" + realm}, "Proxy-Connection": []string{"close"}, }, Body: io.NopCloser(bytes.NewBuffer(unauthorizedMsg)), ContentLength: int64(len(unauthorizedMsg)), } } var proxyAuthorizationHeader = "Proxy-Authorization" func auth(req *http.Request, f func(user, passwd string) bool) bool { authheader := strings.SplitN(req.Header.Get(proxyAuthorizationHeader), " ", 2) req.Header.Del(proxyAuthorizationHeader) if len(authheader) != 2 || authheader[0] != "Basic" { return false } userpassraw, err := base64.StdEncoding.DecodeString(authheader[1]) if err != nil { return false } userpass := strings.SplitN(string(userpassraw), ":", 2) if len(userpass) != 2 { return false } return f(userpass[0], userpass[1]) } // Basic returns a basic HTTP authentication handler for requests // // You probably want to use auth.ProxyBasic(proxy) to enable authentication for all proxy activities func Basic(realm string, f func(user, passwd string) bool) goproxy.ReqHandler { return goproxy.FuncReqHandler(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { if !auth(req, f) { return nil, BasicUnauthorized(req, realm) } return req, nil }) } // BasicConnect returns a basic HTTP authentication handler for CONNECT requests // // You probably want to use auth.ProxyBasic(proxy) to enable authentication for all proxy activities func BasicConnect(realm string, f func(user, passwd string) bool) goproxy.HttpsHandler { return goproxy.FuncHttpsHandler(func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) { if !auth(ctx.Req, f) { ctx.Resp = BasicUnauthorized(ctx.Req, realm) return goproxy.RejectConnect, host } return nil, host }) } // ProxyBasic will force HTTP authentication before any request to the proxy is processed func ProxyBasic(proxy *goproxy.ProxyHttpServer, realm string, f func(user, passwd string) bool) { proxy.OnRequest().Do(Basic(realm, f)) proxy.OnRequest().HandleConnect(BasicConnect(realm, f)) } ================================================ FILE: ext/auth/basic_test.go ================================================ package auth_test import ( "encoding/base64" "io" "net" "net/http" "net/http/httptest" "net/url" "os" "os/exec" "os/signal" "sync/atomic" "testing" "github.com/elazarl/goproxy" "github.com/elazarl/goproxy/ext/auth" ) type ConstantHanlder string func (h ConstantHanlder) ServeHTTP(w http.ResponseWriter, r *http.Request) { io.WriteString(w, string(h)) } func oneShotProxy(proxy *goproxy.ProxyHttpServer) (client *http.Client, s *httptest.Server) { s = httptest.NewServer(proxy) proxyUrl, _ := url.Parse(s.URL) tr := &http.Transport{Proxy: http.ProxyURL(proxyUrl)} client = &http.Client{Transport: tr} return } func times(n int, s string) string { r := make([]byte, 0, n*len(s)) for i := 0; i < n; i++ { r = append(r, s...) } return string(r) } func TestBasicConnectAuthWithCurl(t *testing.T) { expected := ":c>" background := httptest.NewTLSServer(ConstantHanlder(expected)) defer background.Close() proxy := goproxy.NewProxyHttpServer() proxy.OnRequest().HandleConnect(auth.BasicConnect("my_realm", func(user, passwd string) bool { return user == "user" && passwd == "open sesame" })) _, proxyserver := oneShotProxy(proxy) defer proxyserver.Close() cmd := exec.Command("curl", "--silent", "--show-error", "--insecure", "-x", proxyserver.URL, "-U", "user:open sesame", "-p", "--url", background.URL+"/[1-3]", ) out, err := cmd.CombinedOutput() // if curl got error, it'll show up in stderr if err != nil { t.Fatal(err, string(out)) } finalexpected := times(3, expected) if string(out) != finalexpected { t.Error("Expected", finalexpected, "got", string(out)) } } func TestBasicAuthWithCurl(t *testing.T) { expected := ":c>" background := httptest.NewServer(ConstantHanlder(expected)) defer background.Close() proxy := goproxy.NewProxyHttpServer() proxy.OnRequest().Do(auth.Basic("my_realm", func(user, passwd string) bool { return user == "user" && passwd == "open sesame" })) _, proxyserver := oneShotProxy(proxy) defer proxyserver.Close() cmd := exec.Command("curl", "--silent", "--show-error", "-x", proxyserver.URL, "-U", "user:open sesame", "--url", background.URL+"/[1-3]", ) out, err := cmd.CombinedOutput() // if curl got error, it'll show up in stderr if err != nil { t.Fatal(err, string(out)) } finalexpected := times(3, expected) if string(out) != finalexpected { t.Error("Expected", finalexpected, "got", string(out)) } } func TestBasicAuth(t *testing.T) { expected := "hello" background := httptest.NewServer(ConstantHanlder(expected)) defer background.Close() proxy := goproxy.NewProxyHttpServer() proxy.OnRequest().Do(auth.Basic("my_realm", func(user, passwd string) bool { return user == "user" && passwd == "open sesame" })) client, proxyserver := oneShotProxy(proxy) defer proxyserver.Close() // without auth resp, err := client.Get(background.URL) if err != nil { t.Fatal(err) } if resp.Header.Get("Proxy-Authenticate") != "Basic realm=my_realm" { t.Error("Expected Proxy-Authenticate header got", resp.Header.Get("Proxy-Authenticate")) } if resp.StatusCode != http.StatusProxyAuthRequired { t.Error("Expected status 407 Proxy Authentication Required, got", resp.Status) } // with auth req, err := http.NewRequest(http.MethodGet, background.URL, nil) if err != nil { t.Fatal(err) } req.Header.Set("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("user:open sesame"))) resp, err = client.Do(req) if err != nil { t.Fatal(err) } if resp.StatusCode != http.StatusOK { t.Error("Expected status 200 OK, got", resp.Status) } msg, err := io.ReadAll(resp.Body) if err != nil { t.Fatal(err) } if string(msg) != "hello" { t.Errorf("Expected '%s', actual '%s'", expected, string(msg)) } } func TestWithBrowser(t *testing.T) { // an easy way to check if auth works with webserver // to test, run with // $ go test -run TestWithBrowser -- server // configure a browser to use the printed proxy address, use the proxy // and exit with Ctrl-C. It will throw error if your haven't actually used the proxy if os.Args[len(os.Args)-1] != "server" { return } proxy := goproxy.NewProxyHttpServer() println("proxy localhost port 8082") access := int32(0) proxy.OnRequest().Do(auth.Basic("my_realm", func(user, passwd string) bool { atomic.AddInt32(&access, 1) return user == "user" && passwd == "1234" })) l, err := net.Listen("tcp", "localhost:8082") if err != nil { t.Fatal(err) } ch := make(chan os.Signal) signal.Notify(ch, os.Interrupt) go func() { <-ch l.Close() }() http.Serve(l, proxy) if access <= 0 { t.Error("No one accessed the proxy") } } ================================================ FILE: ext/go.mod ================================================ module github.com/elazarl/goproxy/ext go 1.23.0 require ( github.com/elazarl/goproxy v0.0.0-20241217120900-7711dfa3811c github.com/stretchr/testify v1.11.1 golang.org/x/net v0.36.0 golang.org/x/text v0.22.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: ext/go.sum ================================================ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/elazarl/goproxy v0.0.0-20241217120900-7711dfa3811c h1:yWAGp1CjD1mQGLUsADqPn5s1n2AkGAX33XLDUgoXzyo= github.com/elazarl/goproxy v0.0.0-20241217120900-7711dfa3811c/go.mod h1:P73liMk9TZCyF9fXG/RyMeSizmATvpvy3ZS61/1eXn4= 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.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: ext/har/logger.go ================================================ package har import ( "net/http" "time" "github.com/elazarl/goproxy" ) // ExportFunc is a function type that users can implement to handle exported entries type ExportFunc func([]Entry) // Logger implements a HAR logging extension for goproxy type Logger struct { exportFunc ExportFunc exportInterval time.Duration exportThreshold int dataCh chan Entry } // LoggerOption is a function type for configuring the Logger type LoggerOption func(*Logger) // WithExportInterval sets the interval for automatic exports func WithExportInterval(d time.Duration) LoggerOption { return func(l *Logger) { l.exportInterval = d } } // WithExportCount sets the number of requests after which to export entries func WithExportThreshold(threshold int) LoggerOption { return func(l *Logger) { l.exportThreshold = threshold } } // NewLogger creates a new HAR logger instance func NewLogger(exportFunc ExportFunc, opts ...LoggerOption) *Logger { l := &Logger{ exportFunc: exportFunc, exportThreshold: 100, // Default threshold exportInterval: 0, // Default no interval dataCh: make(chan Entry), } // Apply options for _, opt := range opts { opt(l) } go l.exportLoop() return l } // OnRequest handles incoming HTTP requests func (l *Logger) OnRequest(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { ctx.UserData = time.Now() return req, nil } // OnResponse handles HTTP responses func (l *Logger) OnResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { if resp == nil || ctx.Req == nil || ctx.UserData == nil { return resp } startTime, ok := ctx.UserData.(time.Time) if !ok { return resp } entry := Entry{ StartedDateTime: startTime, Time: time.Since(startTime).Milliseconds(), Request: parseRequest(ctx), Response: parseResponse(ctx), Timings: Timings{ Send: 0, Wait: time.Since(startTime).Milliseconds(), Receive: 0, }, } entry.fillIPAddress(ctx.Req) l.dataCh <- entry return resp } func (l *Logger) exportLoop() { var entries []Entry exportIfNeeded := func() { if len(entries) > 0 { go l.exportFunc(entries) entries = nil } } var tickerC <-chan time.Time if l.exportInterval > 0 { ticker := time.NewTicker(l.exportInterval) defer ticker.Stop() tickerC = ticker.C } for { select { case entry, ok := <-l.dataCh: if !ok { exportIfNeeded() return } entries = append(entries, entry) if l.exportThreshold > 0 && len(entries) >= l.exportThreshold { exportIfNeeded() } case <-tickerC: exportIfNeeded() } } } func (l *Logger) Stop() { close(l.dataCh) } ================================================ FILE: ext/har/logger_test.go ================================================ package har import ( "context" "io" "net/http" "net/http/httptest" "net/url" "strings" "sync" "testing" "time" "github.com/elazarl/goproxy" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // ConstantHandler is a simple HTTP handler that returns a constant response type ConstantHandler string func (h ConstantHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") io.WriteString(w, string(h)) } // createTestProxy sets up a test proxy with a HAR logger func createTestProxy(logger *Logger) *httptest.Server { proxy := goproxy.NewProxyHttpServer() proxy.OnRequest().DoFunc(logger.OnRequest) proxy.OnResponse().DoFunc(logger.OnResponse) return httptest.NewServer(proxy) } // createProxyClient creates an HTTP client that uses the given proxy func createProxyClient(proxyURL string) *http.Client { proxyURLParsed, _ := url.Parse(proxyURL) tr := &http.Transport{ Proxy: http.ProxyURL(proxyURLParsed), } return &http.Client{Transport: tr} } func TestHarLoggerBasicFunctionality(t *testing.T) { testCases := []struct { name string method string body string contentType string expectedMethod string }{ { name: "GET Request", method: http.MethodGet, expectedMethod: http.MethodGet, }, { name: "POST Request", method: http.MethodPost, body: `{"test":"data"}`, contentType: "application/json", expectedMethod: http.MethodPost, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { var wg sync.WaitGroup wg.Add(1) var exportedEntries []Entry exportFunc := func(entries []Entry) { exportedEntries = append(exportedEntries, entries...) wg.Done() } logger := NewLogger(exportFunc, WithExportThreshold(1)) // Export after each request defer logger.Stop() background := httptest.NewServer(ConstantHandler("hello world")) defer background.Close() proxyServer := createTestProxy(logger) defer proxyServer.Close() client := createProxyClient(proxyServer.URL) req, err := http.NewRequestWithContext( context.Background(), tc.method, background.URL, strings.NewReader(tc.body), ) require.NoError(t, err, "Should create request") if tc.contentType != "" { req.Header.Set("Content-Type", tc.contentType) } resp, err := client.Do(req) require.NoError(t, err, "Should send request successfully") defer resp.Body.Close() bodyBytes, err := io.ReadAll(resp.Body) require.NoError(t, err, "Should read response body") body := string(bodyBytes) assert.Equal(t, "hello world", body, "Response body should match") wg.Wait() // Wait for export to complete assert.Len(t, exportedEntries, 1, "Should have exactly one exported entry") assert.Equal(t, tc.expectedMethod, exportedEntries[0].Request.Method, "Request method should match") }) } } func TestLoggerThresholdExport(t *testing.T) { var wg sync.WaitGroup var exports [][]Entry var mtx sync.Mutex wg.Add(3) // Expect 3 exports (3,3,1) exportFunc := func(entries []Entry) { mtx.Lock() exports = append(exports, entries) mtx.Unlock() t.Logf("Export occurred with %d entries", len(entries)) wg.Done() } threshold := 3 logger := NewLogger(exportFunc, WithExportThreshold(threshold)) background := httptest.NewServer(ConstantHandler("test")) defer background.Close() proxyServer := createTestProxy(logger) defer proxyServer.Close() client := createProxyClient(proxyServer.URL) // Send 7 requests for i := 0; i < 7; i++ { req, err := http.NewRequestWithContext( context.Background(), http.MethodGet, background.URL, nil, ) require.NoError(t, err) resp, err := client.Do(req) require.NoError(t, err) resp.Body.Close() } // Call Stop to trigger final export of remaining entries logger.Stop() wg.Wait() require.Equal(t, 3, len(exports), "should have 3 export batches") // Count batches by size batchCounts := make(map[int]int) for _, batch := range exports { batchCounts[len(batch)]++ } // Check batch sizes assert.Equal(t, 2, batchCounts[threshold], "should have two batches of threshold size") assert.Equal(t, 1, batchCounts[1], "should have one batch with 1 entry") } func TestHarLoggerExportInterval(t *testing.T) { var wg sync.WaitGroup var mtx sync.Mutex var exports [][]Entry wg.Add(1) // Expect 1 export with all entries exportFunc := func(entries []Entry) { mtx.Lock() exports = append(exports, entries) mtx.Unlock() t.Logf("Export occurred with %d entries", len(entries)) wg.Done() } logger := NewLogger(exportFunc, WithExportInterval(time.Second)) background := httptest.NewServer(ConstantHandler("test")) defer background.Close() proxyServer := createTestProxy(logger) defer proxyServer.Close() client := createProxyClient(proxyServer.URL) // Send 3 requests for i := 0; i < 3; i++ { req, err := http.NewRequestWithContext( context.Background(), http.MethodGet, background.URL, nil, ) require.NoError(t, err) resp, err := client.Do(req) require.NoError(t, err) resp.Body.Close() } wg.Wait() logger.Stop() require.Equal(t, 1, len(exports), "should have 1 export batch") assert.Equal(t, 3, len(exports[0]), "Should have exported 3 entries") } ================================================ FILE: ext/har/types.go ================================================ // Original implementation from abourget/goproxy, adapted for use as an extension. // HAR specification: http://www.softwareishard.com/blog/har-12-spec/ package har import ( "bytes" "io" "net/http" "net/url" "mime" "net" "strings" "time" "github.com/elazarl/goproxy" ) type Har struct { Log Log `json:"log"` } type Log struct { Version string `json:"version"` Creator Creator `json:"creator"` Browser *Browser `json:"browser,omitempty"` Pages []Page `json:"pages,omitempty"` Entries []Entry `json:"entries"` Comment string `json:"comment,omitempty"` } func New() *Har { har := &Har{ Log: Log{ Version: "1.2", Creator: Creator{ Name: "GoProxy", Version: "1.0", }, Pages: make([]Page, 0, 10), Entries: makeNewEntries(), }, } return har } func makeNewEntries() []Entry { const startingEntrySize int = 1000 return make([]Entry, 0, startingEntrySize) } type Creator struct { Name string `json:"name"` Version string `json:"version"` Comment string `json:"comment,omitempty"` } type Browser struct { Name string `json:"name"` Version string `json:"version"` Comment string `json:"comment,omitempty"` } type Page struct { ID string `json:"id,omitempty"` StartedDateTime time.Time `json:"startedDateTime"` Title string `json:"title"` PageTimings PageTimings `json:"pageTimings"` Comment string `json:"comment,omitempty"` } type Entry struct { PageRef string `json:"pageref,omitempty"` StartedDateTime time.Time `json:"startedDateTime"` Time int64 `json:"time"` Request *Request `json:"request"` Response *Response `json:"response"` Cache Cache `json:"cache"` Timings Timings `json:"timings"` ServerIpAddress string `json:"serverIpAddress,omitempty"` Connection string `json:"connection,omitempty"` Comment string `json:"comment,omitempty"` } type Cache struct { BeforeRequest *CacheEntry `json:"beforeRequest,omitempty"` AfterRequest *CacheEntry `json:"afterRequest,omitempty"` } type CacheEntry struct { Expires string `json:"expires,omitempty"` LastAccess string `json:"lastAccess"` ETag string `json:"eTag"` HitCount int `json:"hitCount"` Comment string `json:"comment,omitempty"` } type Request struct { Method string `json:"method"` Url string `json:"url"` HttpVersion string `json:"httpVersion"` Cookies []Cookie `json:"cookies"` Headers []NameValuePair `json:"headers"` QueryString []NameValuePair `json:"queryString"` PostData *PostData `json:"postData,omitempty"` BodySize int64 `json:"bodySize"` HeadersSize int64 `json:"headersSize"` } func (entry *Entry) fillIPAddress(req *http.Request) { host := req.URL.Hostname() // try to parse the host as an IP address if ip := net.ParseIP(host); ip != nil { entry.ServerIpAddress = ip.String() return } } // Shared utility function for reading body content func readBody(ctx *goproxy.ProxyCtx, body io.ReadCloser) ([]byte, error) { content, err := io.ReadAll(body) if err != nil { ctx.Proxy.Logger.Printf("Error reading body: %v", err) return nil, err } return content, nil } // Shared function for handling mime types func parseMediaType(ctx *goproxy.ProxyCtx, header http.Header) string { contentType := header.Get("Content-Type") if contentType == "" { return "" } mediaType, _, err := mime.ParseMediaType(contentType) if err != nil { ctx.Proxy.Logger.Printf("Error parsing media type: %v", err) return "" } return mediaType } func parsePostData(ctx *goproxy.ProxyCtx, req *http.Request) *PostData { mediaType := parseMediaType(ctx, req.Header) if mediaType == "" { return nil } harPostData := &PostData{ MimeType: mediaType, } if err := req.ParseForm(); err != nil { ctx.Proxy.Logger.Printf("Error parsing form: %v", err) return nil } if len(req.PostForm) > 0 { for k, vals := range req.PostForm { for _, v := range vals { param := PostDataParam{ Name: k, Value: v, } harPostData.Params = append(harPostData.Params, param) } } } else if body, err := readBody(ctx, req.Body); err == nil { req.Body = io.NopCloser(bytes.NewBuffer(body)) harPostData.Text = string(body) } return harPostData } type Response struct { Status int `json:"status"` StatusText string `json:"statusText"` HttpVersion string `json:"httpVersion"` Cookies []Cookie `json:"cookies"` Headers []NameValuePair `json:"headers"` Content Content `json:"content"` RedirectUrl string `json:"redirectURL"` BodySize int64 `json:"bodySize"` HeadersSize int64 `json:"headersSize"` Comment string `json:"comment,omitempty"` } func parseResponse(ctx *goproxy.ProxyCtx) *Response { if ctx.Resp == nil { return nil } resp := ctx.Resp harResponse := Response{ Status: resp.StatusCode, StatusText: http.StatusText(resp.StatusCode), HttpVersion: resp.Proto, Cookies: parseCookies(resp.Cookies()), Headers: parseStringArrMap(resp.Header), RedirectUrl: resp.Header.Get("Location"), BodySize: resp.ContentLength, HeadersSize: -1, } if resp.Body == nil { return &harResponse } body, err := readBody(ctx, resp.Body) if err != nil { return &harResponse } resp.Body = io.NopCloser(bytes.NewBuffer(body)) harResponse.Content = Content{ Size: len(body), Text: string(body), MimeType: parseMediaType(ctx, resp.Header), } return &harResponse } func parseRequest(ctx *goproxy.ProxyCtx) *Request { if ctx.Req == nil { ctx.Proxy.Logger.Printf("ParseRequest: nil request") return nil } req := ctx.Req harRequest := &Request{ Method: req.Method, Url: req.URL.String(), HttpVersion: req.Proto, Cookies: parseCookies(req.Cookies()), Headers: parseStringArrMap(req.Header), QueryString: parseStringArrMap(req.URL.Query()), BodySize: req.ContentLength, HeadersSize: -1, } if req.Method != http.MethodPost && req.Method != http.MethodPut { return harRequest } ctx.Proxy.Logger.Printf("ParseRequest: creating PostData, hasBody=%v, hasGetBody=%v", req.Body != nil, req.GetBody != nil) if postData := parsePostData(ctx, req); postData != nil { harRequest.PostData = postData } return harRequest } func parseStringArrMap(stringArrMap map[string][]string) []NameValuePair { harQueryString := make([]NameValuePair, 0, len(stringArrMap)) for k, v := range stringArrMap { escapedKey, err := url.QueryUnescape(k) if err != nil { // Use original key if unescaping fails escapedKey = k } escapedValues, err := url.QueryUnescape(strings.Join(v, ",")) if err != nil { // Use original joined values if unescaping fails escapedValues = strings.Join(v, ",") } harNameValuePair := NameValuePair{ Name: escapedKey, Value: escapedValues, } harQueryString = append(harQueryString, harNameValuePair) } return harQueryString } func parseCookies(cookies []*http.Cookie) []Cookie { harCookies := make([]Cookie, len(cookies)) for i, cookie := range cookies { harCookie := Cookie{ Name: cookie.Name, Domain: cookie.Domain, HttpOnly: cookie.HttpOnly, Path: cookie.Path, Secure: cookie.Secure, Value: cookie.Value, } if !cookie.Expires.IsZero() { harCookie.Expires = &cookie.Expires } harCookies[i] = harCookie } return harCookies } type Cookie struct { Name string `json:"name"` Value string `json:"value"` Path string `json:"path,omitempty"` Domain string `json:"domain,omitempty"` Expires *time.Time `json:"expires,omitempty"` HttpOnly bool `json:"httpOnly,omitempty"` Secure bool `json:"secure,omitempty"` } type NameValuePair struct { Name string `json:"name"` Value string `json:"value"` } type PostData struct { MimeType string `json:"mimeType"` Params []PostDataParam `json:"params,omitempty"` Text string `json:"text,omitempty"` Comment string `json:"comment,omitempty"` } type PostDataParam struct { Name string `json:"name"` Value string `json:"value,omitempty"` FileName string `json:"fileName,omitempty"` ContentType string `json:"contentType,omitempty"` Comment string `json:"comment,omitempty"` } type Content struct { Size int `json:"size"` Compression int `json:"compression,omitempty"` MimeType string `json:"mimeType"` Text string `json:"text,omitempty"` Encoding string `json:"encoding,omitempty"` Comment string `json:"comment,omitempty"` } type PageTimings struct { OnContentLoad int64 `json:"onContentLoad"` OnLoad int64 `json:"onLoad"` Comment string `json:"comment,omitempty"` } type Timings struct { Dns int64 `json:"dns,omitempty"` Blocked int64 `json:"blocked,omitempty"` Connect int64 `json:"connect,omitempty"` Send int64 `json:"send"` Wait int64 `json:"wait"` Receive int64 `json:"receive"` Ssl int64 `json:"ssl,omitempty"` Comment string `json:"comment,omitempty"` } ================================================ FILE: ext/html/cp1255.html ================================================ "
rss
   2012 . " . (4.3.12)
================================================ FILE: ext/html/cp1255.txt ================================================ ================================================ FILE: ext/html/html.go ================================================ // extension to goproxy that will allow you to easily filter web browser related content. package goproxy_html import ( "bytes" "errors" "io" "net/http" "strings" "github.com/elazarl/goproxy" "golang.org/x/net/html/charset" "golang.org/x/text/transform" ) var IsHtml goproxy.RespCondition = goproxy.ContentTypeIs("text/html") var IsCss goproxy.RespCondition = goproxy.ContentTypeIs("text/css") var IsJavaScript goproxy.RespCondition = goproxy.ContentTypeIs("text/javascript", "application/javascript") var IsJson goproxy.RespCondition = goproxy.ContentTypeIs("text/json") var IsXml goproxy.RespCondition = goproxy.ContentTypeIs("text/xml") var IsWebRelatedText goproxy.RespCondition = goproxy.ContentTypeIs( "text/html", "text/css", "text/javascript", "application/javascript", "text/xml", "text/json", ) // HandleString will receive a function that filters a string, and will convert the // request body to a utf8 string, according to the charset specified in the Content-Type // header. // guessing Html charset encoding from the tags is not yet implemented. func HandleString(f func(s string, ctx *goproxy.ProxyCtx) string) goproxy.RespHandler { return HandleStringReader(func(r io.Reader, ctx *goproxy.ProxyCtx) io.Reader { b, err := io.ReadAll(r) if err != nil { ctx.Warnf("Cannot read string from resp body: %v", err) return r } return bytes.NewBufferString(f(string(b), ctx)) }) } // Will receive an input stream which would convert the response to utf-8 // The given function must close the reader r, in order to close the response body. func HandleStringReader(f func(r io.Reader, ctx *goproxy.ProxyCtx) io.Reader) goproxy.RespHandler { return goproxy.FuncRespHandler(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { if ctx.Error != nil { return nil } charsetName := ctx.Charset() if charsetName == "" { charsetName = "utf-8" } if strings.ToLower(charsetName) != "utf-8" { tr, _ := charset.Lookup(charsetName) if tr == nil { ctx.Warnf("Cannot convert from %s to utf-8: not found", charsetName) return resp } // Pass UTF-8 data to the callback f() function and convert its // result back to the original encoding r := transform.NewReader(resp.Body, tr.NewDecoder()) newr := transform.NewReader(f(r, ctx), tr.NewEncoder()) resp.Body = &readFirstCloseBoth{io.NopCloser(newr), resp.Body} } else { //no translation is needed, already at utf-8 resp.Body = &readFirstCloseBoth{io.NopCloser(f(resp.Body, ctx)), resp.Body} } return resp }) } type readFirstCloseBoth struct { r io.ReadCloser c io.Closer } func (rfcb *readFirstCloseBoth) Read(b []byte) (nr int, err error) { return rfcb.r.Read(b) } func (rfcb *readFirstCloseBoth) Close() error { err1 := rfcb.r.Close() err2 := rfcb.c.Close() if err1 != nil && err2 != nil { return errors.New(err1.Error() + ", " + err2.Error()) } if err1 != nil { return err1 } return err2 } ================================================ FILE: ext/html/html_test.go ================================================ package goproxy_html_test import ( "github.com/elazarl/goproxy" "github.com/elazarl/goproxy/ext/html" "io" "net/http" "net/http/httptest" "net/url" "testing" ) type ConstantServer int func (s ConstantServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=iso-8859-8") w.Write([]byte{0xe3, 0xf3}) } func TestCharset(t *testing.T) { s := httptest.NewServer(ConstantServer(1)) defer s.Close() ch := make(chan string, 2) proxy := goproxy.NewProxyHttpServer() proxy.OnResponse().Do(goproxy_html.HandleString( func(s string, ctx *goproxy.ProxyCtx) string { ch <- s return s })) proxyServer := httptest.NewServer(proxy) defer proxyServer.Close() proxyUrl, _ := url.Parse(proxyServer.URL) client := &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyUrl)}} resp, err := client.Get(s.URL + "/cp1255.txt") if err != nil { t.Fatal("GET:", err) } b, err := io.ReadAll(resp.Body) if err != nil { t.Fatal("readAll:", err) } resp.Body.Close() inHandleString := "" select { case inHandleString = <-ch: default: } if len(b) != 2 || b[0] != 0xe3 || b[1] != 0xf3 { t.Error("Did not translate back to 0xe3,0xf3, instead", b) } if inHandleString != "דף" { t.Error("HandleString did not convert DALET & PEH SOFIT (דף) from ISO-8859-8 to utf-8, got", []byte(inHandleString)) } } ================================================ FILE: ext/image/image.go ================================================ package goproxy_image import ( "bytes" . "github.com/elazarl/goproxy" "github.com/elazarl/goproxy/regretable" "image" _ "image/gif" "image/jpeg" "image/png" "io" "net/http" ) var RespIsImage = ContentTypeIs("image/gif", "image/jpeg", "image/pjpeg", "application/octet-stream", "image/png") // "image/tiff" tiff support is in external package, and rarely used, so we omitted it func HandleImage(f func(img image.Image, ctx *ProxyCtx) image.Image) RespHandler { return FuncRespHandler(func(resp *http.Response, ctx *ProxyCtx) *http.Response { if !RespIsImage.HandleResp(resp, ctx) { return resp } if resp.StatusCode != http.StatusOK { // we might get 304 - not modified response without data return resp } contentType := resp.Header.Get("Content-Type") const kb = 1024 regret := regretable.NewRegretableReaderCloserSize(resp.Body, 16*kb) resp.Body = regret img, imgType, err := image.Decode(resp.Body) if err != nil { regret.Regret() ctx.Warnf("%s: %s", ctx.Req.Method+" "+ctx.Req.URL.String()+" Image from "+ctx.Req.RequestURI+"content type"+ contentType+"cannot be decoded returning original image", err) return resp } result := f(img, ctx) buf := bytes.NewBuffer([]byte{}) switch contentType { // No gif image encoder in go - convert to png case "image/gif", "image/png": if err := png.Encode(buf, result); err != nil { ctx.Warnf("Cannot encode image, returning orig %v %v", ctx.Req.URL.String(), err) return resp } resp.Header.Set("Content-Type", "image/png") case "image/jpeg", "image/pjpeg": if err := jpeg.Encode(buf, result, nil); err != nil { ctx.Warnf("Cannot encode image, returning orig %v %v", ctx.Req.URL.String(), err) return resp } case "application/octet-stream": switch imgType { case "jpeg": if err := jpeg.Encode(buf, result, nil); err != nil { ctx.Warnf("Cannot encode image as jpeg, returning orig %v %v", ctx.Req.URL.String(), err) return resp } case "png", "gif": if err := png.Encode(buf, result); err != nil { ctx.Warnf("Cannot encode image as png, returning orig %v %v", ctx.Req.URL.String(), err) return resp } } default: panic("unhandlable type" + contentType) } resp.Body = io.NopCloser(buf) return resp }) } ================================================ FILE: ext/image/image_test.go ================================================ package goproxy_image_test import ( "bytes" "crypto/tls" "github.com/elazarl/goproxy" goproxy_image "github.com/elazarl/goproxy/ext/image" "image" "io" "net/http" "net/http/httptest" "net/url" "os" "testing" ) var acceptAllCerts = &tls.Config{InsecureSkipVerify: true} func oneShotProxy(proxy *goproxy.ProxyHttpServer, t *testing.T) (client *http.Client, s *httptest.Server) { s = httptest.NewServer(proxy) proxyUrl, _ := url.Parse(s.URL) tr := &http.Transport{TLSClientConfig: acceptAllCerts, Proxy: http.ProxyURL(proxyUrl)} client = &http.Client{Transport: tr} return } func getImage(file string, t *testing.T) image.Image { newimage, err := os.ReadFile(file) if err != nil { t.Fatal("Cannot read file", file, err) } img, _, err := image.Decode(bytes.NewReader(newimage)) if err != nil { t.Fatal("Cannot decode image", file, err) } return img } func compareImage(eImg, aImg image.Image, t *testing.T) { if eImg.Bounds().Dx() != aImg.Bounds().Dx() || eImg.Bounds().Dy() != aImg.Bounds().Dy() { t.Error("image sizes different") return } for i := 0; i < eImg.Bounds().Dx(); i++ { for j := 0; j < eImg.Bounds().Dy(); j++ { er, eg, eb, ea := eImg.At(i, j).RGBA() ar, ag, ab, aa := aImg.At(i, j).RGBA() if er != ar || eg != ag || eb != ab || ea != aa { t.Error("images different at", i, j, "vals\n", er, eg, eb, ea, "\n", ar, ag, ab, aa, aa) return } } } } var fs = httptest.NewServer(http.FileServer(http.Dir("."))) func localFile(url string) string { return fs.URL + "/" + url } func TestConstantImageHandler(t *testing.T) { proxy := goproxy.NewProxyHttpServer() football := getImage("test_data/football.png", t) proxy.OnResponse().Do(goproxy_image.HandleImage(func(img image.Image, ctx *goproxy.ProxyCtx) image.Image { return football })) client, l := oneShotProxy(proxy, t) defer l.Close() resp, err := client.Get(localFile("test_data/panda.png")) if err != nil { t.Fatal("Cannot get panda.png", err) } img, _, err := image.Decode(resp.Body) if err != nil { t.Error("decode", err) } else { compareImage(football, img, t) } } func TestImageHandler(t *testing.T) { proxy := goproxy.NewProxyHttpServer() football := getImage("test_data/football.png", t) proxy.OnResponse(goproxy.UrlIs("/test_data/panda.png")).Do(goproxy_image.HandleImage(func(img image.Image, ctx *goproxy.ProxyCtx) image.Image { return football })) client, l := oneShotProxy(proxy, t) defer l.Close() resp, err := client.Get(localFile("test_data/panda.png")) if err != nil { t.Fatal("Cannot get panda.png", err) } img, _, err := image.Decode(resp.Body) if err != nil { t.Error("decode", err) } else { compareImage(football, img, t) } // and again resp, err = client.Get(localFile("test_data/panda.png")) if err != nil { t.Fatal("Cannot get panda.png", err) } img, _, err = image.Decode(resp.Body) if err != nil { t.Error("decode", err) } else { compareImage(football, img, t) } } func fatalOnErr(err error, msg string, t *testing.T) { if err != nil { t.Fatal(msg, err) } } func get(url string, client *http.Client) ([]byte, error) { resp, err := client.Get(url) if err != nil { return nil, err } txt, err := io.ReadAll(resp.Body) defer resp.Body.Close() if err != nil { return nil, err } return txt, nil } func getOrFail(url string, client *http.Client, t *testing.T) []byte { txt, err := get(url, client) if err != nil { t.Fatal("Can't fetch url", url, err) } return txt } func TestReplaceImage(t *testing.T) { proxy := goproxy.NewProxyHttpServer() panda := getImage("test_data/panda.png", t) football := getImage("test_data/football.png", t) proxy.OnResponse(goproxy.UrlIs("/test_data/panda.png")).Do(goproxy_image.HandleImage(func(img image.Image, ctx *goproxy.ProxyCtx) image.Image { return football })) proxy.OnResponse(goproxy.UrlIs("/test_data/football.png")).Do(goproxy_image.HandleImage(func(img image.Image, ctx *goproxy.ProxyCtx) image.Image { return panda })) client, l := oneShotProxy(proxy, t) defer l.Close() imgByPandaReq, _, err := image.Decode(bytes.NewReader(getOrFail(localFile("test_data/panda.png"), client, t))) fatalOnErr(err, "decode panda", t) compareImage(football, imgByPandaReq, t) imgByFootballReq, _, err := image.Decode(bytes.NewReader(getOrFail(localFile("test_data/football.png"), client, t))) fatalOnErr(err, "decode football", t) compareImage(panda, imgByFootballReq, t) } ================================================ FILE: ext/limitation/concurrency.go ================================================ package limitation import ( "net/http" "github.com/elazarl/goproxy" ) // ConcurrentRequests implements a mechanism to limit the number of // concurrently handled HTTP requests, configurable by the user. // The ReqHandler can simply be added to the server with OnRequest(). func ConcurrentRequests(limit int) goproxy.ReqHandler { // Do nothing when the specified limit is invalid if limit <= 0 { return goproxy.FuncReqHandler(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { return req, nil }) } limitation := make(chan struct{}, limit) return goproxy.FuncReqHandler(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { limitation <- struct{}{} // Release semaphore when request finishes go func() { <-req.Context().Done() <-limitation }() return req, nil }) } ================================================ FILE: ext/limitation/concurrency_test.go ================================================ package limitation_test import ( "context" "net/http" "testing" "time" "github.com/elazarl/goproxy" "github.com/elazarl/goproxy/ext/limitation" ) func TestConcurrentRequests(t *testing.T) { mockRequest := &http.Request{Host: "test.com"} ctx := &goproxy.ProxyCtx{} maximumDuration := 100 * time.Millisecond t.Run("empty limitation", func(t *testing.T) { timer := time.NewTimer(maximumDuration) defer timer.Stop() done := make(chan struct{}) go func() { zeroLimiter := limitation.ConcurrentRequests(0) zeroLimiter.Handle(mockRequest, ctx) done <- struct{}{} }() select { case <-timer.C: t.Error("Limiter took too long") case <-done: } }) t.Run("normal limitation", func(t *testing.T) { timer := time.NewTimer(maximumDuration) defer timer.Stop() done := make(chan struct{}) go func() { oneLimiter := limitation.ConcurrentRequests(1) oneLimiter.Handle(mockRequest, ctx) done <- struct{}{} }() select { case <-timer.C: t.Error("Limiter took too long") case <-done: } }) t.Run("more than the limit", func(t *testing.T) { timer := time.NewTimer(maximumDuration) defer timer.Stop() done := make(chan struct{}) go func() { oneLimiter := limitation.ConcurrentRequests(1) oneLimiter.Handle(mockRequest, ctx) oneLimiter.Handle(mockRequest, ctx) done <- struct{}{} }() select { case <-timer.C: // Do nothing, we expect to reach the timeout case <-done: t.Error("Limiter was too fast") } }) t.Run("more than the limit but one request finishes", func(t *testing.T) { timer := time.NewTimer(maximumDuration) defer timer.Stop() done := make(chan struct{}) timeoutCtx, cancel := context.WithCancel(mockRequest.Context()) mockRequestWithCancel := mockRequest.WithContext(timeoutCtx) go func() { oneLimiter := limitation.ConcurrentRequests(1) oneLimiter.Handle(mockRequestWithCancel, ctx) cancel() oneLimiter.Handle(mockRequest, ctx) done <- struct{}{} }() select { case <-timer.C: t.Error("Limiter took too long") case <-done: } }) } ================================================ FILE: go.mod ================================================ module github.com/elazarl/goproxy go 1.23.0 require ( github.com/coder/websocket v1.8.14 github.com/stretchr/testify v1.11.1 golang.org/x/net v0.43.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/text v0.28.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ 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/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/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: h2.go ================================================ package goproxy import ( "bufio" "context" "crypto/tls" "errors" "io" "net" "net/http" "strings" "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. type H2Transport struct { ClientReader io.Reader ClientWriter io.Writer TLSConfig *tls.Config Host string } // 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(_ *http.Request) (*http.Response, error) { raddr := r.Host if !strings.Contains(raddr, ":") { raddr += ":443" } rawServerTLS, err := dial("tcp", raddr) if err != nil { return nil, err } defer rawServerTLS.Close() // Ensure that we only advertise HTTP/2 as the accepted protocol. r.TLSConfig.NextProtos = []string{http2.NextProtoTLS} // Initiate TLS and check remote host name against certificate. rawServerTLS = tls.Client(rawServerTLS, r.TLSConfig) 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 = rawTLSConn.VerifyHostname(raddr[:strings.LastIndex(raddr, ":")]); err != nil { return nil, err } } // Send new client preface to match the one parsed in req. if _, err := io.WriteString(rawServerTLS, http2.ClientPreface); err != nil { return nil, err } serverTLSReader := bufio.NewReader(rawServerTLS) cToS := http2.NewFramer(rawServerTLS, r.ClientReader) sToC := http2.NewFramer(r.ClientWriter, serverTLSReader) errSToC := make(chan error) errCToS := make(chan error) go func() { for { if err := proxyFrame(sToC); err != nil { errSToC <- err break } } }() go func() { for { if err := proxyFrame(cToS); err != nil { errCToS <- err break } } }() for i := 0; i < 2; i++ { select { case err := <-errSToC: if !errors.Is(err, io.EOF) { return nil, err } case err := <-errCToS: if !errors.Is(err, io.EOF) { return nil, err } } } return nil, nil } func dial(network, addr string) (c net.Conn, err error) { addri, err := net.ResolveTCPAddr(network, addr) if err != nil { return } c, err = net.DialTCP(network, nil, addri) return } // proxyFrame reads a single frame from the Framer and, when successful, writes // a ~identical one back to the Framer. func proxyFrame(fr *http2.Framer) error { f, err := fr.ReadFrame() if err != nil { return err } switch f.Header().Type { case http2.FrameData: 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, ok := f.(*http2.HeadersFrame) if !ok { return ErrInvalidH2Frame } terr := fr.WriteHeaders(http2.HeadersFrameParam{ StreamID: tf.StreamID, BlockFragment: tf.HeaderBlockFragment(), EndStream: tf.StreamEnded(), EndHeaders: tf.HeadersEnded(), PadLength: 0, Priority: tf.Priority, }) if terr == nil && tf.StreamEnded() { terr = io.EOF } return terr case http2.FrameContinuation: tf, ok := f.(*http2.ContinuationFrame) if !ok { return ErrInvalidH2Frame } return fr.WriteContinuation(tf.StreamID, tf.HeadersEnded(), tf.HeaderBlockFragment()) case http2.FrameGoAway: tf, ok := f.(*http2.GoAwayFrame) if !ok { return ErrInvalidH2Frame } return fr.WriteGoAway(tf.StreamID, tf.ErrCode, tf.DebugData()) case http2.FramePing: tf, ok := f.(*http2.PingFrame) if !ok { return ErrInvalidH2Frame } return fr.WritePing(tf.IsAck(), tf.Data) case http2.FrameRSTStream: tf, ok := f.(*http2.RSTStreamFrame) if !ok { return ErrInvalidH2Frame } return fr.WriteRSTStream(tf.StreamID, tf.ErrCode) case http2.FrameSettings: tf, ok := f.(*http2.SettingsFrame) if !ok { return ErrInvalidH2Frame } if tf.IsAck() { return fr.WriteSettingsAck() } var settings []http2.Setting // NOTE: If we want to parse headers, need to handle // settings where s.ID == http2.SettingHeaderTableSize and // accordingly update the Framer options. for i := 0; i < tf.NumSettings(); i++ { settings = append(settings, tf.Setting(i)) } return fr.WriteSettings(settings...) case http2.FrameWindowUpdate: tf, ok := f.(*http2.WindowUpdateFrame) if !ok { return ErrInvalidH2Frame } return fr.WriteWindowUpdate(tf.StreamID, tf.Increment) case http2.FramePriority: tf, ok := f.(*http2.PriorityFrame) if !ok { return ErrInvalidH2Frame } return fr.WritePriority(tf.StreamID, tf.PriorityParam) case http2.FramePushPromise: tf, ok := f.(*http2.PushPromiseFrame) if !ok { return ErrInvalidH2Frame } return fr.WritePushPromise(http2.PushPromiseParam{ StreamID: tf.StreamID, PromiseID: tf.PromiseID, BlockFragment: tf.HeaderBlockFragment(), EndHeaders: tf.HeadersEnded(), PadLength: 0, }) default: return errors.New("Unsupported frame: " + string(f.Header().Type)) } } ================================================ FILE: http.go ================================================ 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) } ================================================ FILE: https.go ================================================ package goproxy import ( "bufio" "context" "crypto/tls" "errors" "fmt" "io" "net" "net/http" "net/url" "os" "strings" "sync" "sync/atomic" "github.com/elazarl/goproxy/internal/http1parser" "github.com/elazarl/goproxy/internal/signer" ) type ConnectActionLiteral int const ( ConnectAccept = iota ConnectReject ConnectMitm ConnectHijack // Deprecated: use ConnectMitm. ConnectHTTPMitm ConnectProxyAuthHijack ) var ( 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. type ConnectAction struct { Action ConnectActionLiteral Hijack func(req *http.Request, client net.Conn, ctx *ProxyCtx) TLSConfig func(host string, ctx *ProxyCtx) (*tls.Config, error) } func stripPort(s string) string { var ix int if strings.Contains(s, "[") && strings.Contains(s, "]") { // ipv6 address example: [2606:4700:4700::1111]:443 // strip '[' and ']' s = strings.ReplaceAll(s, "[", "") s = strings.ReplaceAll(s, "]", "") ix = strings.LastIndexAny(s, ":") if ix == -1 { return s } } else { // ipv4 ix = strings.IndexRune(s, ':') if ix == -1 { return s } } return s[:ix] } func (proxy *ProxyHttpServer) dial(ctx *ProxyCtx, network, addr string) (c net.Conn, err error) { if ctx.Dialer != nil { return ctx.Dialer(ctx.Req.Context(), network, addr) } if proxy.Tr != nil && proxy.Tr.DialContext != nil { return proxy.Tr.DialContext(ctx.Req.Context(), network, addr) } // if the user didn't specify any dialer, we just use the default one, // provided by net package 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(ctx, network, addr) } if proxy.ConnectDialWithReq != nil { return proxy.ConnectDialWithReq(ctx.Req, network, addr) } return proxy.ConnectDial(network, addr) } type halfClosable interface { net.Conn CloseWrite() error CloseRead() error } var _ halfClosable = (*net.TCPConn)(nil) func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request) { ctx := &ProxyCtx{Req: r, Session: atomic.AddInt64(&proxy.sess, 1), Proxy: proxy, certStore: proxy.CertStore} hij, ok := w.(http.Hijacker) if !ok { panic("httpserver does not support hijacking") } proxyClient, _, e := hij.Hijack() if e != nil { panic("Cannot hijack connection " + e.Error()) } ctx.Logf("Running %d CONNECT handlers", len(proxy.httpsHandlers)) todo, host := OkConnect, r.URL.Host for i, h := range proxy.httpsHandlers { newtodo, newhost := h.HandleConnect(host, ctx) // If found a result, break the loop immediately if newtodo != nil { todo, host = newtodo, newhost ctx.Logf("on %dth handler: %v %s", i, todo, host) break } } switch todo.Action { case ConnectAccept: if !hasPort.MatchString(host) { host += ":80" } targetSiteCon, err := proxy.connectDial(ctx, "tcp", host) if err != nil { ctx.Warnf("Error dialing to %s: %s", host, err.Error()) httpError(proxyClient, ctx, err) return } ctx.Logf("Accepting CONNECT to %s", host) _, _ = 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 func() { var wg sync.WaitGroup wg.Add(2) go copyAndClose(ctx, targetTCP, proxyClientTCP, &wg) go copyAndClose(ctx, proxyClientTCP, targetTCP, &wg) wg.Wait() // 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, 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 { httpError(proxyClient, ctx, err) return } } // 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 } } 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 { return } // 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(), scheme+"://") { req.URL, err = url.Parse(scheme + "://" + r.Host + req.URL.String()) } 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 { 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.KeepHeader { RemoveProxyHeaders(ctx, req) } resp, err = ctx.RoundTrip(req) if err != nil { ctx.Warnf("Cannot read response from mitm'd server %v", err) return false } ctx.Logf("resp %v", resp.Status) } 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 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 } // 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 } if err := resp.Write(client); err != nil { ctx.Warnf("Cannot write response from mitm'd client: %v", err) return false } return true }(req); !continueLoop { return } } ctx.Logf("Exiting on EOF") }() case ConnectProxyAuthHijack: _, _ = proxyClient.Write([]byte("HTTP/1.1 407 Proxy Authentication Required\r\n")) todo.Hijack(r, proxyClient, ctx) case ConnectReject: if ctx.Resp != nil { if err := ctx.Resp.Write(proxyClient); err != nil { ctx.Warnf("Cannot write response that reject http CONNECT: %v", err) } } _ = proxyClient.Close() } } func httpError(w io.WriteCloser, ctx *ProxyCtx, err error) { 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) 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) } return 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() wg.Done() } func dialerFromEnv(proxy *ProxyHttpServer) func(network, addr string) (net.Conn, error) { httpsProxy := os.Getenv("HTTPS_PROXY") if httpsProxy == "" { httpsProxy = os.Getenv("https_proxy") } if httpsProxy == "" { return nil } return proxy.NewConnectDialToProxy(httpsProxy) } func (proxy *ProxyHttpServer) NewConnectDialToProxy(httpsProxy string) func(network, addr string) (net.Conn, error) { return proxy.NewConnectDialToProxyWithHandler(httpsProxy, nil) } 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" || u.Scheme == "ws" { if !strings.ContainsRune(u.Host, ':') { u.Host += ":80" } return func(network, addr string) (net.Conn, error) { connectReq := &http.Request{ Method: http.MethodConnect, URL: &url.URL{Opaque: addr}, Host: addr, Header: make(http.Header), } if connectReqHandler != nil { connectReqHandler(connectReq) } c, err := proxy.dial(&ProxyCtx{Req: &http.Request{}}, network, u.Host) if err != nil { return nil, err } _ = 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() return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { resp, err := io.ReadAll(io.LimitReader(resp.Body, _errorRespMaxLength)) if err != nil { return nil, err } _ = c.Close() return nil, errors.New("proxy refused connection" + string(resp)) } return c, nil } } if u.Scheme == "https" || u.Scheme == "wss" { if !strings.ContainsRune(u.Host, ':') { u.Host += ":443" } return func(network, addr string) (net.Conn, error) { ctx := &ProxyCtx{Req: &http.Request{}} c, err := proxy.dial(ctx, network, u.Host) if err != nil { return nil, err } 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}, Host: addr, Header: make(http.Header), } if connectReqHandler != nil { connectReqHandler(connectReq) } _ = connectReq.Write(c) // Read response. // Okay to use and discard buffered reader here, because // TLS server will not speak until spoken to. br := bufio.NewReader(c) resp, err := http.ReadResponse(br, connectReq) if err != nil { _ = c.Close() return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(io.LimitReader(resp.Body, _errorRespMaxLength)) if err != nil { return nil, err } _ = c.Close() return nil, errors.New("proxy refused connection" + string(body)) } return c, nil } } return nil } func TLSConfigFromCA(ca *tls.Certificate) func(host string, ctx *ProxyCtx) (*tls.Config, error) { return func(host string, ctx *ProxyCtx) (*tls.Config, error) { var err error var cert *tls.Certificate hostname := stripPort(host) config := defaultTLSConfig.Clone() ctx.Logf("signing for %s", stripPort(host)) genCert := func() (*tls.Certificate, error) { return signer.SignHost(*ca, []string{hostname}) } if ctx.certStore != nil { cert, err = ctx.certStore.Fetch(hostname, genCert) } else { cert, err = genCert() } if err != nil { ctx.Warnf("Cannot sign host certificate with provided CA: %s", err) return nil, err } config.Certificates = append(config.Certificates, *cert) return config, nil } } 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 } ================================================ FILE: internal/http1parser/header.go ================================================ 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) } } ================================================ FILE: internal/http1parser/header_test.go ================================================ package http1parser_test import ( "bufio" "bytes" "net/textproto" "testing" "github.com/elazarl/goproxy/internal/http1parser" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestHttp1ExtractHeaders_Empty(t *testing.T) { http1Data := "POST /index.html HTTP/1.1\r\n" + "\r\n" textParser := textproto.NewReader(bufio.NewReader(bytes.NewReader([]byte(http1Data)))) headers, err := http1parser.Http1ExtractHeaders(textParser) require.NoError(t, err) assert.Empty(t, headers) } func TestHttp1ExtractHeaders(t *testing.T) { http1Data := "POST /index.html HTTP/1.1\r\n" + "Host: www.test.com\r\n" + "Accept: */ /*\r\n" + "Content-Length: 17\r\n" + "lowercase: 3z\r\n" + "\r\n" + `{"hello":"world"}` textParser := textproto.NewReader(bufio.NewReader(bytes.NewReader([]byte(http1Data)))) headers, err := http1parser.Http1ExtractHeaders(textParser) require.NoError(t, err) assert.Len(t, headers, 4) assert.Contains(t, headers, "Content-Length") assert.Contains(t, headers, "lowercase") } func TestHttp1ExtractHeaders_InvalidData(t *testing.T) { http1Data := "POST /index.html HTTP/1.1\r\n" + `{"hello":"world"}` textParser := textproto.NewReader(bufio.NewReader(bytes.NewReader([]byte(http1Data)))) _, err := http1parser.Http1ExtractHeaders(textParser) require.Error(t, err) } ================================================ FILE: internal/http1parser/request.go ================================================ 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)), } } ================================================ FILE: internal/http1parser/request_test.go ================================================ package http1parser_test import ( "bufio" "bytes" "fmt" "io" "net/http" "net/url" "strings" "testing" "github.com/elazarl/goproxy/internal/http1parser" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( _data = "POST /index.html HTTP/1.1\r\n" + "Host: www.test.com\r\n" + "Accept: */*\r\n" + "Content-Length: 17\r\n" + "lowercase: 3z\r\n" + "\r\n" + `{"hello":"world"}` _data2 = "GET /index.html HTTP/1.1\r\n" + "Host: www.test.com\r\n" + "Accept: */*\r\n" + "lowercase: 3z\r\n" + "\r\n" ) func TestCanonicalRequest(t *testing.T) { // Here we are simulating two requests on the same connection http1Data := bytes.NewReader(append([]byte(_data), _data2...)) parser := http1parser.NewRequestReader(false, http1Data) // 1st request req, err := parser.ReadRequest() require.NoError(t, err) assert.NotEmpty(t, req.Header) assert.NotContains(t, req.Header, "lowercase") assert.Contains(t, req.Header, "Lowercase") require.NoError(t, req.Body.Close()) // 2nd request req, err = parser.ReadRequest() require.NoError(t, err) assert.NotEmpty(t, req.Header) // Make sure that the buffers are empty after all requests have been processed assert.True(t, parser.IsEOF()) } func TestNonCanonicalRequest(t *testing.T) { http1Data := bytes.NewReader([]byte(_data)) parser := http1parser.NewRequestReader(true, http1Data) req, err := parser.ReadRequest() require.NoError(t, err) assert.NotEmpty(t, req.Header) assert.Contains(t, req.Header, "lowercase") assert.NotContains(t, req.Header, "Lowercase") } func TestMultipleNonCanonicalRequests(t *testing.T) { http1Data := bytes.NewReader(append([]byte(_data), _data2...)) parser := http1parser.NewRequestReader(true, http1Data) req, err := parser.ReadRequest() require.NoError(t, err) assert.NotEmpty(t, req.Header) assert.Contains(t, req.Header, "lowercase") assert.NotContains(t, req.Header, "Lowercase") body, err := io.ReadAll(req.Body) require.NoError(t, err) assert.Len(t, body, 17) require.NoError(t, req.Body.Close()) req, err = parser.ReadRequest() require.NoError(t, err) assert.NotEmpty(t, req.Header) assert.True(t, parser.IsEOF()) } // reqTest is inspired by https://github.com/golang/go/blob/master/src/net/http/readrequest_test.go type reqTest struct { Raw string Req *http.Request Body string Trailer http.Header Error string } var ( noError = "" noBodyStr = "" noTrailer http.Header ) var reqTests = []reqTest{ // Baseline test; All Request fields included for template use { "GET http://www.techcrunch.com/ HTTP/1.1\r\n" + "Host: www.techcrunch.com\r\n" + "user-agent: Fake\r\n" + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" + "Accept-Language: en-us,en;q=0.5\r\n" + "Accept-Encoding: gzip,deflate\r\n" + "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" + "Keep-Alive: 300\r\n" + "Content-Length: 7\r\n" + "Proxy-Connection: keep-alive\r\n\r\n" + "abcdef\n???", &http.Request{ Method: http.MethodGet, URL: &url.URL{ Scheme: "http", Host: "www.techcrunch.com", Path: "/", }, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{ "Accept": {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"}, "Accept-Language": {"en-us,en;q=0.5"}, "Accept-Encoding": {"gzip,deflate"}, "Accept-Charset": {"ISO-8859-1,utf-8;q=0.7,*;q=0.7"}, "Keep-Alive": {"300"}, "Proxy-Connection": {"keep-alive"}, "Content-Length": {"7"}, "user-agent": {"Fake"}, }, Close: false, ContentLength: 7, Host: "www.techcrunch.com", RequestURI: "http://www.techcrunch.com/", }, "abcdef\n", noTrailer, noError, }, // GET request with no body (the normal case) { "GET / HTTP/1.1\r\n" + "Host: foo.com\r\n\r\n", &http.Request{ Method: http.MethodGet, URL: &url.URL{ Path: "/", }, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, Header: http.Header{}, Close: false, ContentLength: 0, Host: "foo.com", RequestURI: "/", }, noBodyStr, noTrailer, noError, }, } func TestReadRequest(t *testing.T) { for i := range reqTests { tt := &reqTests[i] testName := fmt.Sprintf("Test %d (%q)", i, tt.Raw) t.Run(testName, func(t *testing.T) { r := bufio.NewReader(strings.NewReader(tt.Raw)) parser := http1parser.NewRequestReader(true, r) req, err := parser.ReadRequest() if err != nil && err.Error() == tt.Error { // Test finished, we expected an error return } require.NoError(t, err) // Check request equality (excluding body) rbody := req.Body req.Body = nil assert.Equal(t, tt.Req, req) // Check if the two bodies match var bodyString string if rbody != nil { data, err := io.ReadAll(rbody) require.NoError(t, err) bodyString = string(data) _ = rbody.Close() } assert.Equal(t, tt.Body, bodyString) assert.Equal(t, tt.Trailer, req.Trailer) }) } } ================================================ FILE: internal/signer/counterecryptor.go ================================================ package signer import ( "crypto/aes" "crypto/cipher" "crypto/ecdsa" "crypto/ed25519" "crypto/rsa" "crypto/sha256" "crypto/x509" "errors" ) type CounterEncryptorRand struct { cipher cipher.Block counter []byte rand []byte ix int } func NewCounterEncryptorRandFromKey(key any, seed []byte) (r CounterEncryptorRand, err error) { var keyBytes []byte switch key := key.(type) { case *rsa.PrivateKey: keyBytes = x509.MarshalPKCS1PrivateKey(key) case *ecdsa.PrivateKey: if keyBytes, err = x509.MarshalECPrivateKey(key); err != nil { return } case ed25519.PrivateKey: if keyBytes, err = x509.MarshalPKCS8PrivateKey(key); err != nil { return } default: 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 r, err } r.counter = make([]byte, r.cipher.BlockSize()) if seed != nil { copy(r.counter, h.Sum(seed)[:r.cipher.BlockSize()]) } r.rand = make([]byte, r.cipher.BlockSize()) r.ix = len(r.rand) return r, nil } func (c *CounterEncryptorRand) Seed(b []byte) { if len(b) != len(c.counter) { panic("SetCounter: wrong counter size") } copy(c.counter, b) } func (c *CounterEncryptorRand) refill() { c.cipher.Encrypt(c.rand, c.counter) for i := 0; i < len(c.counter); i++ { if c.counter[i]++; c.counter[i] != 0 { break } } c.ix = 0 } func (c *CounterEncryptorRand) Read(b []byte) (n int, err error) { if c.ix == len(c.rand) { c.refill() } if n = len(c.rand) - c.ix; n > len(b) { n = len(b) } copy(b, c.rand[c.ix:c.ix+n]) c.ix += n return } ================================================ FILE: internal/signer/counterecryptor_test.go ================================================ package signer_test import ( "bytes" "crypto/rsa" "encoding/binary" "io" "math" "math/rand" "testing" "github.com/elazarl/goproxy/internal/signer" ) type RandSeedReader struct { r rand.Rand } func (r *RandSeedReader) Read(b []byte) (n int, err error) { for i := range b { b[i] = byte(r.r.Int() & 0xFF) } return len(b), nil } func fatalOnErr(t *testing.T, err error, msg string) { t.Helper() if err != nil { t.Fatal(msg, err) } } func TestCounterEncDifferentConsecutive(t *testing.T) { k, err := rsa.GenerateKey(&RandSeedReader{*rand.New(rand.NewSource(0xFF43109))}, 128) fatalOnErr(t, err, "rsa.GenerateKey") c, err := signer.NewCounterEncryptorRandFromKey(k, []byte("the quick brown fox run over the lazy dog")) fatalOnErr(t, err, "NewCounterEncryptorRandFromKey") for i := 0; i < 100*1000; i++ { var a, b int64 fatalOnErr(t, binary.Read(&c, binary.BigEndian, &a), "read a") fatalOnErr(t, binary.Read(&c, binary.BigEndian, &b), "read b") if a == b { t.Fatal("two consecutive equal int64", a, b) } } } func TestCounterEncIdenticalStreams(t *testing.T) { k, err := rsa.GenerateKey(&RandSeedReader{*rand.New(rand.NewSource(0xFF43109))}, 128) fatalOnErr(t, err, "rsa.GenerateKey") c1, err := signer.NewCounterEncryptorRandFromKey(k, []byte("the quick brown fox run over the lazy dog")) fatalOnErr(t, err, "NewCounterEncryptorRandFromKey") c2, err := signer.NewCounterEncryptorRandFromKey(k, []byte("the quick brown fox run over the lazy dog")) fatalOnErr(t, err, "NewCounterEncryptorRandFromKey") const nOut = 1000 out1, out2 := make([]byte, nOut), make([]byte, nOut) _, _ = io.ReadFull(&c1, out1) tmp := out2 for len(tmp) > 0 { n := 1 + rand.Intn(256) if n > len(tmp) { n = len(tmp) } n, err := c2.Read(tmp[:n]) fatalOnErr(t, err, "CounterEncryptorRand.Read") tmp = tmp[n:] } if !bytes.Equal(out1, out2) { t.Error("identical CSPRNG does not produce the same output") } } func stddev(data []int) float64 { var sum, sumSqr float64 = 0, 0 for _, h := range data { sum += float64(h) sumSqr += float64(h) * float64(h) } n := float64(len(data)) variance := (sumSqr - ((sum * sum) / n)) / (n - 1) return math.Sqrt(variance) } func TestCounterEncStreamHistogram(t *testing.T) { k, err := rsa.GenerateKey(&RandSeedReader{*rand.New(rand.NewSource(0xFF43109))}, 128) fatalOnErr(t, err, "rsa.GenerateKey") c, err := signer.NewCounterEncryptorRandFromKey(k, []byte("the quick brown fox run over the lazy dog")) fatalOnErr(t, err, "NewCounterEncryptorRandFromKey") nout := 100 * 1000 out := make([]byte, nout) _, _ = io.ReadFull(&c, out) refhist := make([]int, 512) for i := 0; i < nout; i++ { refhist[rand.Intn(256)]++ } hist := make([]int, 512) for _, b := range out { hist[int(b)]++ } refstddev, stddev := stddev(refhist), stddev(hist) // due to lack of time, I guestimate t.Logf("ref:%v - act:%v = %v", refstddev, stddev, math.Abs(refstddev-stddev)) if math.Abs(refstddev-stddev) >= 1 { t.Errorf("stddev of ref histogram different than regular PRNG: %v %v", refstddev, stddev) } } ================================================ FILE: internal/signer/signer.go ================================================ package signer import ( "crypto" "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" "crypto/rsa" "crypto/sha256" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "fmt" "math/big" "math/rand" "net" "runtime" "sort" "strings" "time" ) const _goproxySignerVersion = ":goproxy2" func hashSorted(lst []string) []byte { c := make([]string, len(lst)) copy(c, lst) sort.Strings(c) h := sha256.New() h.Write([]byte(strings.Join(c, ","))) return h.Sum(nil) } func SignHost(ca tls.Certificate, hosts []string) (cert *tls.Certificate, err error) { // Use the provided CA for certificate generation. // Use already parsed Leaf certificate when present. x509ca := ca.Leaf if x509ca == nil { if x509ca, err = x509.ParseCertificate(ca.Certificate[0]); err != nil { return nil, err } } now := time.Now() start := now.Add(-30 * 24 * time.Hour) // -30 days end := now.Add(365 * 24 * time.Hour) // 365 days // Always generate a positive int value // (Two complement is not enabled when the first bit is 0) generated := rand.Uint64() generated >>= 1 template := x509.Certificate{ SerialNumber: big.NewInt(int64(generated)), Issuer: x509ca.Subject, Subject: pkix.Name{ Organization: []string{"GoProxy untrusted MITM proxy Inc"}, }, NotBefore: start, NotAfter: end, KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, } for _, h := range hosts { if ip := net.ParseIP(h); ip != nil { template.IPAddresses = append(template.IPAddresses, ip) } else { template.DNSNames = append(template.DNSNames, h) template.Subject.CommonName = h } } hash := hashSorted(append(hosts, _goproxySignerVersion, ":"+runtime.Version())) var csprng CounterEncryptorRand if csprng, err = NewCounterEncryptorRandFromKey(ca.PrivateKey, hash); err != nil { return nil, err } var certpriv crypto.Signer switch ca.PrivateKey.(type) { case *rsa.PrivateKey: if certpriv, err = rsa.GenerateKey(&csprng, 2048); err != nil { return nil, err } case *ecdsa.PrivateKey: if certpriv, err = ecdsa.GenerateKey(elliptic.P256(), &csprng); err != nil { return nil, err } case ed25519.PrivateKey: if _, certpriv, err = ed25519.GenerateKey(&csprng); err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported key type %T", ca.PrivateKey) } derBytes, err := x509.CreateCertificate(&csprng, &template, x509ca, certpriv.Public(), ca.PrivateKey) if err != nil { return nil, err } // Save an already parsed leaf certificate to use less CPU // when it will be used leafCert, err := x509.ParseCertificate(derBytes) if err != nil { return nil, err } certBytes := 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, Leaf: leafCert, }, nil } ================================================ FILE: internal/signer/signer_test.go ================================================ package signer_test import ( "context" "crypto/tls" "crypto/x509" "io" "net/http" "net/http/httptest" "os" "os/exec" "strings" "testing" "time" "github.com/elazarl/goproxy" "github.com/elazarl/goproxy/internal/signer" ) func orFatal(t *testing.T, msg string, err error) { t.Helper() if err != nil { t.Fatal(msg, err) } } type ConstantHanlder string func (h ConstantHanlder) ServeHTTP(w http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(w, string(h)) } func getBrowser(args []string) string { for i, arg := range args { if arg == "-browser" && i+1 < len(arg) { return args[i+1] } if strings.HasPrefix(arg, "-browser=") { return arg[len("-browser="):] } } return "" } func testSignerX509(t *testing.T, ca tls.Certificate) { t.Helper() cert, err := signer.SignHost(ca, []string{"example.com", "1.1.1.1", "localhost"}) orFatal(t, "singHost", err) cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) orFatal(t, "ParseCertificate", err) certpool := x509.NewCertPool() certpool.AddCert(ca.Leaf) orFatal(t, "VerifyHostname", cert.Leaf.VerifyHostname("example.com")) orFatal(t, "CheckSignatureFrom", cert.Leaf.CheckSignatureFrom(ca.Leaf)) _, err = cert.Leaf.Verify(x509.VerifyOptions{ DNSName: "example.com", Roots: certpool, }) orFatal(t, "Verify", err) } func testSignerTLS(t *testing.T, ca tls.Certificate) { t.Helper() cert, err := signer.SignHost(ca, []string{"example.com", "1.1.1.1", "localhost"}) orFatal(t, "singHost", err) cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) orFatal(t, "ParseCertificate", err) expected := "key verifies with Go" server := httptest.NewUnstartedServer(ConstantHanlder(expected)) defer server.Close() server.TLS = &tls.Config{ Certificates: []tls.Certificate{*cert, ca}, MinVersion: tls.VersionTLS12, } server.StartTLS() certpool := x509.NewCertPool() certpool.AddCert(ca.Leaf) tr := &http.Transport{ TLSClientConfig: &tls.Config{RootCAs: certpool}, } asLocalhost := strings.ReplaceAll(server.URL, "127.0.0.1", "localhost") req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, asLocalhost, nil) orFatal(t, "NewRequest", err) resp, err := tr.RoundTrip(req) orFatal(t, "RoundTrip", err) txt, err := io.ReadAll(resp.Body) orFatal(t, "io.ReadAll", err) if string(txt) != expected { t.Errorf("Expected '%s' got '%s'", expected, string(txt)) } browser := getBrowser(os.Args) if browser != "" { ctx := context.Background() _ = exec.CommandContext(ctx, browser, asLocalhost).Run() time.Sleep(10 * time.Second) } } func TestSignerRsaTls(t *testing.T) { testSignerTLS(t, goproxy.GoproxyCa) } func TestSignerRsaX509(t *testing.T) { testSignerX509(t, goproxy.GoproxyCa) } func TestSignerEcdsaTls(t *testing.T) { testSignerTLS(t, EcdsaCa) } func TestSignerEcdsaX509(t *testing.T) { testSignerX509(t, EcdsaCa) } func BenchmarkSignRsa(b *testing.B) { var cert *tls.Certificate var err error for n := 0; n < b.N; n++ { cert, err = signer.SignHost(goproxy.GoproxyCa, []string{"example.com", "1.1.1.1", "localhost"}) } _ = cert _ = err } func BenchmarkSignEcdsa(b *testing.B) { var cert *tls.Certificate var err error for n := 0; n < b.N; n++ { cert, err = signer.SignHost(EcdsaCa, []string{"example.com", "1.1.1.1", "localhost"}) } _ = cert _ = err } // // Eliptic Curve certificate and key for testing // var EcdsaCaCert = []byte(`-----BEGIN CERTIFICATE----- MIICGDCCAb8CFEkSgqYhlT0+Yyr9anQNJgtclTL0MAoGCCqGSM49BAMDMIGOMQsw CQYDVQQGEwJJTDEPMA0GA1UECAwGQ2VudGVyMQwwCgYDVQQHDANMb2QxEDAOBgNV BAoMB0dvUHJveHkxEDAOBgNVBAsMB0dvUHJveHkxGjAYBgNVBAMMEWdvcHJveHku Z2l0aHViLmlvMSAwHgYJKoZIhvcNAQkBFhFlbGF6YXJsQGdtYWlsLmNvbTAeFw0x OTA1MDcxMTUwMThaFw0zOTA1MDIxMTUwMThaMIGOMQswCQYDVQQGEwJJTDEPMA0G A1UECAwGQ2VudGVyMQwwCgYDVQQHDANMb2QxEDAOBgNVBAoMB0dvUHJveHkxEDAO BgNVBAsMB0dvUHJveHkxGjAYBgNVBAMMEWdvcHJveHkuZ2l0aHViLmlvMSAwHgYJ KoZIhvcNAQkBFhFlbGF6YXJsQGdtYWlsLmNvbTBZMBMGByqGSM49AgEGCCqGSM49 AwEHA0IABDlH4YrdukPFAjbO8x+gR9F8ID7eCU8Orhba/MIblSRrRVedpj08lK+2 svyoAcrcDsynClO9aQtsC9ivZ+Pmr3MwCgYIKoZIzj0EAwMDRwAwRAIgGRSSJVSE 1b1KVU0+w+SRtnR5Wb7jkwnaDNxQ3c3FXoICIBJV/l1hFM7mbd68Oi5zLq/4ZsrL 98Bb3nddk2xys6a9 -----END CERTIFICATE-----`) var EcdsaCaKey = []byte(`-----BEGIN PRIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEsc8m+2aZfagnesg qMgXe8ph4LtVu2VOUYhHttuEDsChRANCAAQ5R+GK3bpDxQI2zvMfoEfRfCA+3glP Dq4W2vzCG5Uka0VXnaY9PJSvtrL8qAHK3A7MpwpTvWkLbAvYr2fj5q9z -----END PRIVATE KEY-----`) var EcdsaCa, ecdsaCaErr = tls.X509KeyPair(EcdsaCaCert, EcdsaCaKey) func init() { if ecdsaCaErr != nil { panic("Error parsing ecdsa CA " + ecdsaCaErr.Error()) } var err error if EcdsaCa.Leaf, err = x509.ParseCertificate(EcdsaCa.Certificate[0]); err != nil { panic("Error parsing ecdsa CA " + err.Error()) } } ================================================ FILE: key.pem ================================================ -----BEGIN RSA PRIVATE KEY----- MIIJKAIBAAKCAgEAnhDL4fqGGhjWzRBFy8iHGuNIdo79FtoWPevCpyek6AWrTuBF 0j3dzRMUpAkemC/p94tGES9f9iWUVi7gnfmUz1lxhjiqUoW5K1xfwmbx+qmC2YAw HM+yq2oOLwz1FAYoQ3NT0gU6cJXtIB6Hjmxwy4jfDPzCuMFwfvOq4eS+pRJhnPTf m31XpZOsfJMS9PjD6UU5U3ZsD/oMAjGuMGIXoOGgmqeFrRJm0N+/vtenAYbcSED+ qiGGJisOu5grvMl0RJAvjgvDMw+6lWKCpqV+/5gd9CNuFP3nUhW6tbY0mBHIETrZ 0uuUdh21P20JMKt34ok0wn6On2ECN0i7UGv+SJ9TgXj7hksxH1R6OLQaSQ8qxh3I yeqPSnQ+iDK8/WXiqZug8iYxi1qgW5iYxiV5uAL0s3XRsv3Urj6Mu3QjVie0TOuq AmhawnO1gPDnjc3NLLlb79yrhdFiC2rVvRFbC5SKzB7OYyh7IdnwFAl7bEyMA6WU BIN+prw4rdYAEcmnLjNSudQGIy48hPMP8W4PHgLkjDCULryAcBluU2qkFkJfScUK 0qNg5wjZKjkdtDY4LxAX7MZW524dRKiTiFLLYEF9nWl+/OKoF561YnAW9qkYHjic geFYo0q+o7Es0jLt75MZGJY6iasBYzXxVJH0tlsHGkkrs8tLNapglhNEJkcCAwEA AQKCAgAwSuNvxHHqUUJ3XoxkiXy1u1EtX9x1eeYnvvs2xMb+WJURQTYz2NEGUdkR kPO2/ZSXHAcpQvcnpi2e8y2PNmy/uQ0VPATVt6NuWweqxncR5W5j82U/uDlXY8y3 lVbfak4s5XRri0tikHvlP06dNgZ0OPok5qi7d+Zd8yZ3Y8LXfjkykiIrSG1Z2jdt zCWTkNmSUKMGG/1CGFxI41Lb12xuq+C8v4f469Fb6bCUpyCQN9rffHQSGLH6wVb7 +68JO+d49zCATpmx5RFViMZwEcouXxRvvc9pPHXLP3ZPBD8nYu9kTD220mEGgWcZ 3L9dDlZPcSocbjw295WMvHz2QjhrDrb8gXwdpoRyuyofqgCyNxSnEC5M13SjOxtf pjGzjTqh0kDlKXg2/eTkd9xIHjVhFYiHIEeITM/lHCfWwBCYxViuuF7pSRPzTe8U C440b62qZSPMjVoquaMg+qx0n9fKSo6n1FIKHypv3Kue2G0WhDeK6u0U288vQ1t4 Ood3Qa13gZ+9hwDLbM/AoBfVBDlP/tpAwa7AIIU1ZRDNbZr7emFdctx9B6kLINv3 4PDOGM2xrjOuACSGMq8Zcu7LBz35PpIZtviJOeKNwUd8/xHjWC6W0itgfJb5I1Nm V6Vj368pGlJx6Se26lvXwyyrc9pSw6jSAwARBeU4YkNWpi4i6QKCAQEA0T7u3P/9 jZJSnDN1o2PXymDrJulE61yguhc/QSmLccEPZe7or06/DmEhhKuCbv+1MswKDeag /1JdFPGhL2+4G/f/9BK3BJPdcOZSz7K6Ty8AMMBf8AehKTcSBqwkJWcbEvpHpKJ6 eDqn1B6brXTNKMT6fEEXCuZJGPBpNidyLv/xXDcN7kCOo3nGYKfB5OhFpNiL63tw +LntU56WESZwEqr8Pf80uFvsyXQK3a5q5HhIQtxl6tqQuPlNjsDBvCqj0x72mmaJ ZVsVWlv7khUrCwAXz7Y8K7mKKBd2ekF5hSbryfJsxFyvEaWUPhnJpTKV85lAS+tt FQuIp9TvKYlRQwKCAQEAwWJN8jysapdhi67jO0HtYOEl9wwnF4w6XtiOYtllkMmC 06/e9h7RsRyWPMdu3qRDPUYFaVDy6+dpUDSQ0+E2Ot6AHtVyvjeUTIL651mFIo/7 OSUCEc+HRo3SfPXdPhSQ2thNTxl6y9XcFacuvbthgr70KXbvC4k6IEmdpf/0Kgs9 7QTZCG26HDrEZ2q9yMRlRaL2SRD+7Y2xra7gB+cQGFj6yn0Wd/07er49RqMXidQf KR2oYfev2BDtHXoSZFfhFGHlOdLvWRh90D4qZf4vQ+g/EIMgcNSoxjvph1EShmKt sjhTHtoHuu+XmEQvIewk2oCI+JvofBkcnpFrVvUUrQKCAQAaTIufETmgCo0BfuJB N/JOSGIl0NnNryWwXe2gVgVltbsmt6FdL0uKFiEtWJUbOF5g1Q5Kcvs3O/XhBQGa QbNlKIVt+tAv7hm97+Tmn/MUsraWagdk1sCluns0hXxBizT27KgGhDlaVRz05yfv 5CdJAYDuDwxDXXBAhy7iFJEgYSDH00+X61tCJrMNQOh4ycy/DEyBu1EWod+3S85W t3sMjZsIe8P3i+4137Th6eMbdha2+JaCrxfTd9oMoCN5b+6JQXIDM/H+4DTN15PF 540yY7+aZrAnWrmHknNcqFAKsTqfdi2/fFqwoBwCtiEG91WreU6AfEWIiJuTZIru sIibAoIBAAqIwlo5t+KukF+9jR9DPh0S5rCIdvCvcNaN0WPNF91FPN0vLWQW1bFi L0TsUDvMkuUZlV3hTPpQxsnZszH3iK64RB5p3jBCcs+gKu7DT59MXJEGVRCHT4Um YJryAbVKBYIGWl++sZO8+JotWzx2op8uq7o+glMMjKAJoo7SXIiVyC/LHc95urOi 9+PySphPKn0anXPpexmRqGYfqpCDo7rPzgmNutWac80B4/CfHb8iUPg6Z1u+1FNe yKvcZHgW2Wn00znNJcCitufLGyAnMofudND/c5rx2qfBx7zZS7sKUQ/uRYjes6EZ QBbJUA/2/yLv8YYpaAaqj4aLwV8hRpkCggEBAIh3e25tr3avCdGgtCxS7Y1blQ2c ue4erZKmFP1u8wTNHQ03T6sECZbnIfEywRD/esHpclfF3kYAKDRqIP4K905Rb0iH 759ZWt2iCbqZznf50XTvptdmjm5KxvouJzScnQ52gIV6L+QrCKIPelLBEIqCJREh pmcjjocD/UCCSuHgbAYNNnO/JdhnSylz1tIg26I+2iLNyeTKIepSNlsBxnkLmqM1 cj/azKBaT04IOMLaN8xfSqitJYSraWMVNgGJM5vfcVaivZnNh0lZBv+qu6YkdM88 4/avCJ8IutT+FcMM+GbGazOm5ALWqUyhrnbLGc4CQMPfe7Il6NxwcrOxT8w= -----END RSA PRIVATE KEY----- ================================================ FILE: logger.go ================================================ package goproxy type Logger interface { Printf(format string, v ...any) } ================================================ FILE: proxy.go ================================================ package goproxy import ( "io" "log" "net" "net/http" "os" "regexp" ) // The basic proxy type. Implements http.Handler. type ProxyHttpServer struct { // session variable must be aligned in i386 // see http://golang.org/src/pkg/sync/atomic/doc.go#L41 sess int64 // KeepDestinationHeaders indicates the proxy should retain any headers present in the http.Response before proxying KeepDestinationHeaders bool // setting Verbose to true will log information on each request sent to the proxy Verbose bool Logger Logger NonproxyHandler http.Handler reqHandlers []ReqHandler 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) ConnectDialWithReq func(req *http.Request, network string, addr string) (net.Conn, error) 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+$`) func copyHeaders(dst, src http.Header, keepDestHeaders bool) { if !keepDestHeaders { for k := range dst { dst.Del(k) } } for k, vs := range src { // direct assignment to avoid canonicalization dst[k] = append([]string(nil), vs...) } } func (proxy *ProxyHttpServer) filterRequest(r *http.Request, ctx *ProxyCtx) (req *http.Request, resp *http.Response) { req = r for _, h := range proxy.reqHandlers { req, resp = h.Handle(req, ctx) // non-nil resp means the handler decided to skip sending the request // and return canned response instead. if resp != nil { break } } return } func (proxy *ProxyHttpServer) filterResponse(respOrig *http.Response, ctx *ProxyCtx) (resp *http.Response) { resp = respOrig for _, h := range proxy.respHandlers { ctx.Resp = resp resp = h.Handle(resp, ctx) } return } // 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 !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") r.Header.Del("Proxy-Authenticate") r.Header.Del("Proxy-Authorization") // Connection, Authenticate and Authorization are single hop Header: // http://www.w3.org/Protocols/rfc2616/rfc2616.txt // 14.10 Connection // The Connection general-header field allows the sender to specify // options that are desired for that particular connection and MUST NOT // be communicated by proxies over further connections. // 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") } } type flushWriter struct { w io.Writer } func (fw flushWriter) Write(p []byte) (int, error) { n, err := fw.w.Write(p) if f, ok := fw.w.(http.Flusher); ok { // only flush if the Writer implements the Flusher interface. f.Flush() } return n, err } // Standard net/http function. Shouldn't be used directly, http.Serve will use it. func (proxy *ProxyHttpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodConnect { proxy.handleHttps(w, r) } else { proxy.handleHttp(w, r) } } // 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.", http.StatusInternalServerError) }), Tr: &http.Transport{TLSClientConfig: tlsClientSkipVerify, Proxy: http.ProxyFromEnvironment}, } proxy.ConnectDial = dialerFromEnv(&proxy) return &proxy } ================================================ FILE: proxy_test.go ================================================ package goproxy_test import ( "bufio" "bytes" "context" "crypto/tls" "crypto/x509" "encoding/base64" "fmt" "io" "log" "net" "net/http" "net/http/httptest" "net/http/httptrace" "net/url" "os" "os/exec" "regexp" "strconv" "strings" "testing" "time" "github.com/elazarl/goproxy" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var ( https = httptest.NewTLSServer(nil) srv = httptest.NewServer(nil) fs = httptest.NewServer(http.FileServer(http.Dir("."))) ) type QueryHandler struct{} func (QueryHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { if err := req.ParseForm(); err != nil { panic(err) } _, _ = io.WriteString(w, req.Form.Get("result")) } type HeadersHandler struct{} // This handlers returns a body with a string containing all the request headers it received. func (HeadersHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { var sb strings.Builder for name, values := range req.Header { for _, value := range values { sb.WriteString(name) sb.WriteString(": ") sb.WriteString(value) sb.WriteString(";") } } _, _ = io.WriteString(w, sb.String()) } func init() { http.DefaultServeMux.Handle("/bobo", ConstantHanlder("bobo")) http.DefaultServeMux.Handle("/query", QueryHandler{}) http.DefaultServeMux.Handle("/headers", HeadersHandler{}) } type ConstantHanlder string func (h ConstantHanlder) ServeHTTP(w http.ResponseWriter, r *http.Request) { _, _ = io.WriteString(w, string(h)) } func get(url string, client *http.Client) ([]byte, error) { req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) if err != nil { return nil, err } resp, err := client.Do(req) if err != nil { return nil, err } txt, err := io.ReadAll(resp.Body) defer resp.Body.Close() if err != nil { return nil, err } return txt, nil } func getOrFail(t *testing.T, url string, client *http.Client) []byte { t.Helper() txt, err := get(url, client) if err != nil { t.Fatal("Can't fetch url", url, err) } return txt } func getCert(t *testing.T, c *tls.Conn) []byte { t.Helper() if err := c.HandshakeContext(context.Background()); err != nil { t.Fatal("cannot handshake", err) } return c.ConnectionState().PeerCertificates[0].Raw } func localFile(url string) string { return fs.URL + "/" + url } func TestSimpleHttpReqWithProxy(t *testing.T) { client, s := oneShotProxy(goproxy.NewProxyHttpServer()) defer s.Close() if r := string(getOrFail(t, srv.URL+"/bobo", client)); r != "bobo" { t.Error("proxy server does not serve constant handlers", r) } if r := string(getOrFail(t, srv.URL+"/bobo", client)); r != "bobo" { t.Error("proxy server does not serve constant handlers", r) } if string(getOrFail(t, https.URL+"/bobo", client)) != "bobo" { t.Error("TLS server does not serve constant handlers, when proxy is used") } } func oneShotProxy(proxy *goproxy.ProxyHttpServer) (client *http.Client, s *httptest.Server) { s = httptest.NewServer(proxy) proxyUrl, _ := url.Parse(s.URL) tr := &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, Proxy: http.ProxyURL(proxyUrl), } client = &http.Client{Transport: tr} return } func TestSimpleHook(t *testing.T) { proxy := goproxy.NewProxyHttpServer() proxy.OnRequest(goproxy.SrcIpIs("127.0.0.1")).DoFunc( func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { req.URL.Path = "/bobo" return req, nil }, ) client, l := oneShotProxy(proxy) defer l.Close() if result := string(getOrFail(t, srv.URL+("/momo"), client)); result != "bobo" { t.Error("Redirecting all requests from 127.0.0.1 to bobo, didn't work." + " (Might break if Go's client sets RemoteAddr to IPv6 address). Got: " + result) } } func TestAlwaysHook(t *testing.T) { proxy := goproxy.NewProxyHttpServer() proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { req.URL.Path = "/bobo" return req, nil }) client, l := oneShotProxy(proxy) defer l.Close() if result := string(getOrFail(t, srv.URL+("/momo"), client)); result != "bobo" { t.Error("Redirecting all requests from 127.0.0.1 to bobo, didn't work." + " (Might break if Go's client sets RemoteAddr to IPv6 address). Got: " + result) } } func TestReplaceResponse(t *testing.T) { proxy := goproxy.NewProxyHttpServer() proxy.OnResponse().DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { resp.StatusCode = http.StatusOK resp.Body = io.NopCloser(bytes.NewBufferString("chico")) return resp }) client, l := oneShotProxy(proxy) defer l.Close() if result := string(getOrFail(t, srv.URL+("/momo"), client)); result != "chico" { t.Error("hooked response, should be chico, instead:", result) } } func TestReplaceReponseForUrl(t *testing.T) { proxy := goproxy.NewProxyHttpServer() proxy.OnResponse(goproxy.UrlIs("/koko")).DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { resp.StatusCode = http.StatusOK resp.Body = io.NopCloser(bytes.NewBufferString("chico")) return resp }) client, l := oneShotProxy(proxy) defer l.Close() if result := string(getOrFail(t, srv.URL+("/koko"), client)); result != "chico" { t.Error("hooked 'koko', should be chico, instead:", result) } if result := string(getOrFail(t, srv.URL+("/bobo"), client)); result != "bobo" { t.Error("still, bobo should stay as usual, instead:", result) } } func TestOneShotFileServer(t *testing.T) { client, l := oneShotProxy(goproxy.NewProxyHttpServer()) defer l.Close() file := "test_data/panda.png" info, err := os.Stat(file) if err != nil { t.Fatal("Cannot find", file) } req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, fs.URL+"/"+file, nil) if err != nil { t.Fatal("Cannot create request", err) } if resp, err := client.Do(req); err == nil { b, err := io.ReadAll(resp.Body) if err != nil { t.Fatal("got", string(b)) } if int64(len(b)) != info.Size() { t.Error("Expected Length", file, info.Size(), "actually", len(b), "starts", string(b[:10])) } } else { t.Fatal("Cannot read from fs server", err) } } func TestContentType(t *testing.T) { proxy := goproxy.NewProxyHttpServer() proxy.OnResponse(goproxy.ContentTypeIs("image/png")).DoFunc( func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { resp.Header.Set("X-Shmoopi", "1") return resp }, ) client, l := oneShotProxy(proxy) defer l.Close() for _, file := range []string{"test_data/panda.png", "test_data/football.png"} { req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, localFile(file), nil) if err != nil { t.Fatal("Cannot create request", err) } if resp, err := client.Do(req); err != nil || resp.Header.Get("X-Shmoopi") != "1" { if err == nil { t.Error("pngs should have X-Shmoopi header = 1, actually", resp.Header.Get("X-Shmoopi")) } else { t.Error("error reading png", err) } } } req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, localFile("baby.jpg"), nil) if err != nil { t.Fatal("Cannot create request", err) } if resp, err := client.Do(req); err != nil || resp.Header.Get("X-Shmoopi") != "" { if err == nil { t.Error("Non png images should NOT have X-Shmoopi header at all", resp.Header.Get("X-Shmoopi")) } else { t.Error("error reading png", err) } } } func panicOnErr(err error, msg string) { if err != nil { log.Fatal(err.Error() + ":-" + msg) } } func TestChangeResp(t *testing.T) { proxy := goproxy.NewProxyHttpServer() proxy.OnResponse().DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { _, _ = resp.Body.Read([]byte{0}) resp.Body = io.NopCloser(new(bytes.Buffer)) return resp }) client, l := oneShotProxy(proxy) defer l.Close() req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, localFile("test_data/panda.png"), nil) if err != nil { t.Fatal("Cannot create request", err) } resp, err := client.Do(req) if err != nil { t.Fatal(err) } _, _ = io.ReadAll(resp.Body) req, err = http.NewRequestWithContext(context.Background(), http.MethodGet, localFile("/bobo"), nil) if err != nil { t.Fatal("Cannot create request", err) } _, err = client.Do(req) if err != nil { t.Fatal(err) } } func TestSimpleMitm(t *testing.T) { proxy := goproxy.NewProxyHttpServer() proxy.OnRequest(goproxy.ReqHostIs(https.Listener.Addr().String())).HandleConnect(goproxy.AlwaysMitm) proxy.OnRequest(goproxy.ReqHostIs("no such host exists")).HandleConnect(goproxy.AlwaysMitm) client, l := oneShotProxy(proxy) defer l.Close() ctx := context.Background() c, err := (&tls.Dialer{ Config: &tls.Config{InsecureSkipVerify: true}, }).DialContext(ctx, "tcp", https.Listener.Addr().String()) if err != nil { t.Fatal("cannot dial to tcp server", err) } tlsConn, ok := c.(*tls.Conn) assert.True(t, ok) origCert := getCert(t, tlsConn) _ = c.Close() c2, err := (&net.Dialer{}).DialContext(ctx, "tcp", l.Listener.Addr().String()) if err != nil { t.Fatal("dialing to proxy", err) } creq, err := http.NewRequestWithContext(context.Background(), http.MethodConnect, https.URL, nil) if err != nil { t.Fatal("create new request", creq) } _ = creq.Write(c2) c2buf := bufio.NewReader(c2) resp, err := http.ReadResponse(c2buf, creq) if err != nil || resp.StatusCode != http.StatusOK { t.Fatal("Cannot CONNECT through proxy", err) } c2tls := tls.Client(c2, &tls.Config{ InsecureSkipVerify: true, }) proxyCert := getCert(t, c2tls) if bytes.Equal(proxyCert, origCert) { t.Errorf("Certificate after mitm is not different\n%v\n%v", base64.StdEncoding.EncodeToString(origCert), base64.StdEncoding.EncodeToString(proxyCert)) } if resp := string(getOrFail(t, https.URL+"/bobo", client)); resp != "bobo" { t.Error("Wrong response when mitm", resp, "expected bobo") } if resp := string(getOrFail(t, https.URL+"/query?result=bar", client)); resp != "bar" { t.Error("Wrong response when mitm", resp, "expected bar") } } func TestMitmMutateRequest(t *testing.T) { proxy := goproxy.NewProxyHttpServer() proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { // We inject a header in the request req.Header.Set("Mitm-Header-Inject", "true") return req, nil }) client, l := oneShotProxy(proxy) defer l.Close() r := string(getOrFail(t, https.URL+"/headers", client)) if !strings.Contains(r, "Mitm-Header-Inject: true") { t.Error("Expected response body to contain the MITM injected header. Got instead: ", r) } } func TestConnectHandler(t *testing.T) { proxy := goproxy.NewProxyHttpServer() althttps := httptest.NewTLSServer(ConstantHanlder("althttps")) proxy.OnRequest().HandleConnectFunc(func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) { u, _ := url.Parse(althttps.URL) return goproxy.OkConnect, u.Host }) client, l := oneShotProxy(proxy) defer l.Close() if resp := string(getOrFail(t, https.URL+"/alturl", client)); resp != "althttps" { t.Error("Proxy should redirect CONNECT requests to local althttps server, expected 'althttps' got ", resp) } } func TestMitmIsFiltered(t *testing.T) { proxy := goproxy.NewProxyHttpServer() proxy.OnRequest(goproxy.ReqHostIs(https.Listener.Addr().String())).HandleConnect(goproxy.AlwaysMitm) proxy.OnRequest(goproxy.UrlIs("/momo")).DoFunc( func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { return nil, goproxy.TextResponse(req, "koko") }, ) client, l := oneShotProxy(proxy) defer l.Close() if resp := string(getOrFail(t, https.URL+"/momo", client)); resp != "koko" { t.Error("Proxy should capture /momo to be koko and not", resp) } if resp := string(getOrFail(t, https.URL+"/bobo", client)); resp != "bobo" { t.Error("But still /bobo should be bobo and not", resp) } } func TestFirstHandlerMatches(t *testing.T) { proxy := goproxy.NewProxyHttpServer() proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { return nil, goproxy.TextResponse(req, "koko") }) proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { panic("should never get here, previous response is no null") }) client, l := oneShotProxy(proxy) defer l.Close() if resp := string(getOrFail(t, srv.URL+"/", client)); resp != "koko" { t.Error("should return always koko and not", resp) } } func TestIcyResponse(t *testing.T) { // TODO: fix this test /*s := constantHttpServer([]byte("ICY 200 OK\r\n\r\nblablabla")) proxy := goproxy.NewProxyHttpServer() proxy.Verbose = true _, l := oneShotProxy(proxy, t) defer l.Close() req, err := http.NewRequest("GET", "http://"+s, nil) panicOnErr(err, "newReq") proxyip := l.URL[len("http://"):] println("got ip: " + proxyip) c, err := net.Dial("tcp", proxyip) panicOnErr(err, "dial") defer c.Close() req.WriteProxy(c) raw, err := io.ReadAll(c) panicOnErr(err, "readAll") if string(raw) != "ICY 200 OK\r\n\r\nblablabla" { t.Error("Proxy did not send the malformed response received") }*/ } type VerifyNoProxyHeaders struct { *testing.T } func (v VerifyNoProxyHeaders) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Connection") != "" || r.Header.Get("Proxy-Connection") != "" || r.Header.Get("Proxy-Authenticate") != "" || r.Header.Get("Proxy-Authorization") != "" { v.Error("Got Connection header from goproxy", r.Header) } } func TestNoProxyHeaders(t *testing.T) { s := httptest.NewServer(VerifyNoProxyHeaders{t}) client, l := oneShotProxy(goproxy.NewProxyHttpServer()) defer l.Close() req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, s.URL, nil) panicOnErr(err, "bad request") req.Header.Add("Proxy-Connection", "close") req.Header.Add("Proxy-Authenticate", "auth") req.Header.Add("Proxy-Authorization", "auth") _, _ = client.Do(req) } func TestNoProxyHeadersHttps(t *testing.T) { s := httptest.NewTLSServer(VerifyNoProxyHeaders{t}) proxy := goproxy.NewProxyHttpServer() proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) client, l := oneShotProxy(proxy) defer l.Close() req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, s.URL, nil) panicOnErr(err, "bad request") req.Header.Add("Proxy-Connection", "close") _, _ = client.Do(req) } type VerifyAcceptEncodingHeader struct { ReceivedHeaderValue string } func (v *VerifyAcceptEncodingHeader) ServeHTTP(w http.ResponseWriter, r *http.Request) { v.ReceivedHeaderValue = r.Header.Get("Accept-Encoding") } func TestAcceptEncoding(t *testing.T) { v := VerifyAcceptEncodingHeader{} s := httptest.NewServer(&v) for i, tc := range []struct { keepAcceptEncoding bool disableCompression bool acceptEncoding string expectedValue string }{ {false, false, "", "gzip"}, {false, false, "identity", "gzip"}, {false, true, "", ""}, {false, true, "identity", ""}, {true, false, "", "gzip"}, {true, false, "identity", "identity"}, {true, true, "", ""}, {true, true, "identity", "identity"}, } { t.Run(strconv.Itoa(i), func(t *testing.T) { proxy := goproxy.NewProxyHttpServer() proxy.KeepAcceptEncoding = tc.keepAcceptEncoding proxy.Tr.DisableCompression = tc.disableCompression client, l := oneShotProxy(proxy) defer l.Close() req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, s.URL, nil) panicOnErr(err, "bad request") // fully control the Accept-Encoding header we send to the proxy tr, ok := client.Transport.(*http.Transport) if !ok { t.Fatal("invalid client transport") } tr.DisableCompression = true if tc.acceptEncoding != "" { req.Header.Add("Accept-Encoding", tc.acceptEncoding) } _, err = client.Do(req) panicOnErr(err, "bad response") if v.ReceivedHeaderValue != tc.expectedValue { t.Errorf("%+v expected Accept-Encoding: %s, got %s", tc, tc.expectedValue, v.ReceivedHeaderValue) } }) } } func TestHeadReqHasContentLength(t *testing.T) { client, l := oneShotProxy(goproxy.NewProxyHttpServer()) defer l.Close() req, err := http.NewRequestWithContext(context.Background(), http.MethodHead, localFile("test_data/panda.png"), nil) if err != nil { t.Fatal("Cannot create request", err) } resp, err := client.Do(req) panicOnErr(err, "resp to HEAD") if resp.Header.Get("Content-Length") == "" { t.Error("Content-Length should exist on HEAD requests") } } func TestChunkedResponse(t *testing.T) { ctx := context.Background() l, err := (&net.ListenConfig{}).Listen(ctx, "tcp", ":10234") panicOnErr(err, "listen") defer l.Close() go func() { for i := 0; i < 2; i++ { c, err := l.Accept() panicOnErr(err, "accept") _, err = http.ReadRequest(bufio.NewReader(c)) panicOnErr(err, "readrequest") _, _ = io.WriteString(c, "HTTP/1.1 200 OK\r\n"+ "Content-Type: text/plain\r\n"+ "Transfer-Encoding: chunked\r\n\r\n"+ "25\r\n"+ "This is the data in the first chunk\r\n\r\n"+ "1C\r\n"+ "and this is the second one\r\n\r\n"+ "3\r\n"+ "con\r\n"+ "8\r\n"+ "sequence\r\n0\r\n\r\n") _ = c.Close() } }() c, err := (&net.Dialer{}).DialContext(ctx, "tcp", "localhost:10234") panicOnErr(err, "dial") defer c.Close() req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "/", nil) _ = req.Write(c) resp, err := http.ReadResponse(bufio.NewReader(c), req) panicOnErr(err, "readresp") b, err := io.ReadAll(resp.Body) panicOnErr(err, "readall") expected := "This is the data in the first chunk\r\nand this is the second one\r\nconsequence" if string(b) != expected { t.Errorf("Got `%v` expected `%v`", string(b), expected) } proxy := goproxy.NewProxyHttpServer() proxy.OnResponse().DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { panicOnErr(ctx.Error, "error reading output") b, err := io.ReadAll(resp.Body) _ = resp.Body.Close() panicOnErr(err, "readall onresp") if enc := resp.Header.Get("Transfer-Encoding"); enc != "" { t.Fatal("Chunked response should be received as plaintext", enc) } resp.Body = io.NopCloser(bytes.NewBufferString(strings.ReplaceAll(string(b), "e", "E"))) return resp }) client, s := oneShotProxy(proxy) defer s.Close() req, err = http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:10234/", nil) if err != nil { t.Fatal("Cannot create request", err) } resp, err = client.Do(req) panicOnErr(err, "client.Get") b, err = io.ReadAll(resp.Body) panicOnErr(err, "readall proxy") if string(b) != strings.ReplaceAll(expected, "e", "E") { t.Error("expected", expected, "w/ e->E. Got", string(b)) } } func TestGoproxyThroughProxy(t *testing.T) { proxy := goproxy.NewProxyHttpServer() proxy2 := goproxy.NewProxyHttpServer() doubleString := func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { b, err := io.ReadAll(resp.Body) panicOnErr(err, "readAll resp") resp.Body = io.NopCloser(bytes.NewBufferString(string(b) + " " + string(b))) return resp } proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) proxy.OnResponse().DoFunc(doubleString) _, l := oneShotProxy(proxy) defer l.Close() proxy2.ConnectDial = proxy2.NewConnectDialToProxy(l.URL) client, l2 := oneShotProxy(proxy2) defer l2.Close() if r := string(getOrFail(t, https.URL+"/bobo", client)); r != "bobo bobo" { t.Error("Expected bobo doubled twice, got", r) } } func TestHttpProxyAddrsFromEnv(t *testing.T) { proxy := goproxy.NewProxyHttpServer() doubleString := func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { b, err := io.ReadAll(resp.Body) panicOnErr(err, "readAll resp") resp.Body = io.NopCloser(bytes.NewBufferString(string(b) + " " + string(b))) return resp } proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) proxy.OnResponse().DoFunc(doubleString) _, l := oneShotProxy(proxy) defer l.Close() t.Setenv("https_proxy", l.URL) proxy2 := goproxy.NewProxyHttpServer() client, l2 := oneShotProxy(proxy2) defer l2.Close() if r := string(getOrFail(t, https.URL+"/bobo", client)); r != "bobo bobo" { t.Error("Expected bobo doubled twice, got", r) } } func TestGoproxyHijackConnect(t *testing.T) { proxy := goproxy.NewProxyHttpServer() proxy.OnRequest(goproxy.ReqHostIs(srv.Listener.Addr().String())). HijackConnect(func(req *http.Request, client net.Conn, ctx *goproxy.ProxyCtx) { t.Logf("URL %+#v\nSTR %s", req.URL, req.URL.String()) getReq, err := http.NewRequestWithContext(req.Context(), http.MethodGet, (&url.URL{ Scheme: "http", Host: req.URL.Host, Path: "/bobo", }).String(), nil) if err != nil { t.Fatal("Cannot create request", err) } httpClient := &http.Client{} resp, err := httpClient.Do(getReq) panicOnErr(err, "http.Get(CONNECT url)") panicOnErr(resp.Write(client), "resp.Write(client)") _ = resp.Body.Close() _ = client.Close() }) client, l := oneShotProxy(proxy) defer l.Close() proxyAddr := l.Listener.Addr().String() conn, err := (&net.Dialer{}).DialContext(context.Background(), "tcp", proxyAddr) panicOnErr(err, "conn "+proxyAddr) buf := bufio.NewReader(conn) writeConnect(conn) if txt := readResponse(buf); txt != "bobo" { t.Error("Expected bobo for CONNECT /foo, got", txt) } if r := string(getOrFail(t, https.URL+"/bobo", client)); r != "bobo" { t.Error("Expected bobo would keep working with CONNECT", r) } } func readResponse(buf *bufio.Reader) string { req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, srv.URL, nil) panicOnErr(err, "NewRequest") resp, err := http.ReadResponse(buf, req) panicOnErr(err, "resp.Read") defer resp.Body.Close() txt, err := io.ReadAll(resp.Body) panicOnErr(err, "resp.Read") return string(txt) } func writeConnect(w io.Writer) { // this will let us use IP address of server as url in http.NewRequest by // passing it as //127.0.0.1:64584 (prefixed with //). // Passing IP address with port alone (without //) will raise error: // "first path segment in URL cannot contain colon" more details on this // here: https://github.com/golang/go/issues/18824 req := &http.Request{ Method: http.MethodConnect, URL: &url.URL{Opaque: srv.Listener.Addr().String()}, Host: srv.Listener.Addr().String(), Header: make(http.Header), } err := req.Write(w) panicOnErr(err, "req(CONNECT).Write") } func TestCurlMinusP(t *testing.T) { proxy := goproxy.NewProxyHttpServer() proxy.OnRequest().HandleConnectFunc(func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) { return goproxy.MitmConnect, host }) called := false proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { called = true return req, nil }) _, l := oneShotProxy(proxy) defer l.Close() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "curl", "-p", "-sS", "--proxy", l.URL, srv.URL+"/bobo") var out bytes.Buffer cmd.Stdout = &out if err := cmd.Run(); err != nil { t.Fatal(err) } if output := out.String(); output != "bobo" { t.Error("Expected bobo, got", output) } if !called { t.Error("handler not called") } } func TestSelfRequest(t *testing.T) { proxy := goproxy.NewProxyHttpServer() _, l := oneShotProxy(proxy) defer l.Close() if !strings.Contains(string(getOrFail(t, l.URL, &http.Client{})), "non-proxy") { t.Fatal("non proxy requests should fail") } } func TestHasGoproxyCA(t *testing.T) { proxy := goproxy.NewProxyHttpServer() proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) s := httptest.NewServer(proxy) proxyUrl, _ := url.Parse(s.URL) goproxyCA := x509.NewCertPool() goproxyCA.AddCert(goproxy.GoproxyCa.Leaf) tr := &http.Transport{TLSClientConfig: &tls.Config{RootCAs: goproxyCA}, Proxy: http.ProxyURL(proxyUrl)} client := &http.Client{Transport: tr} if resp := string(getOrFail(t, https.URL+"/bobo", client)); resp != "bobo" { t.Error("Wrong response when mitm", resp, "expected bobo") } } type TestCertStorage struct { certs map[string]*tls.Certificate hits int misses int } func (tcs *TestCertStorage) Fetch(hostname string, gen func() (*tls.Certificate, error)) (*tls.Certificate, error) { var cert *tls.Certificate var err error cert, ok := tcs.certs[hostname] if ok { log.Printf("hit %v\n", cert == nil) tcs.hits++ } else { cert, err = gen() if err != nil { return nil, err } log.Printf("miss %v\n", cert == nil) tcs.certs[hostname] = cert tcs.misses++ } return cert, err } func (tcs *TestCertStorage) statHits() int { return tcs.hits } func (tcs *TestCertStorage) statMisses() int { return tcs.misses } func newTestCertStorage() *TestCertStorage { tcs := &TestCertStorage{} tcs.certs = make(map[string]*tls.Certificate) return tcs } func TestProxyWithCertStorage(t *testing.T) { tcs := newTestCertStorage() t.Logf("TestProxyWithCertStorage started") proxy := goproxy.NewProxyHttpServer() proxy.CertStore = tcs proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { req.URL.Path = "/bobo" return req, nil }) proxy.OnResponse().DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { resp.Close = true return resp }) s := httptest.NewServer(proxy) proxyUrl, _ := url.Parse(s.URL) goproxyCA := x509.NewCertPool() goproxyCA.AddCert(goproxy.GoproxyCa.Leaf) tr := &http.Transport{TLSClientConfig: &tls.Config{RootCAs: goproxyCA}, Proxy: http.ProxyURL(proxyUrl)} client := &http.Client{Transport: tr} if resp := string(getOrFail(t, https.URL+"/bobo", client)); resp != "bobo" { t.Error("Wrong response when mitm", resp, "expected bobo") } if tcs.statHits() != 0 { t.Fatalf("Expected 0 cache hits, got %d", tcs.statHits()) } if tcs.statMisses() != 1 { t.Fatalf("Expected 1 cache miss, got %d", tcs.statMisses()) } // Another round - this time the certificate can be loaded if resp := string(getOrFail(t, https.URL+"/bobo", client)); resp != "bobo" { t.Error("Wrong response when mitm", resp, "expected bobo") } if tcs.statHits() != 1 { t.Fatalf("Expected 1 cache hit, got %d", tcs.statHits()) } if tcs.statMisses() != 1 { t.Fatalf("Expected 1 cache miss, got %d", tcs.statMisses()) } } func TestHttpsMitmURLRewrite(t *testing.T) { scheme := "https" testCases := []struct { Host string RawPath string AddOpaque bool }{ { Host: "example.com", RawPath: "/blah/v1/data/realtime", AddOpaque: true, }, { Host: "example.com:443", RawPath: "/blah/v1/data/realtime?encodedURL=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile", }, { Host: "example.com:443", RawPath: "/blah/v1/data/realtime?unencodedURL=https://www.googleapis.com/auth/userinfo.profile", }, } for _, tc := range testCases { proxy := goproxy.NewProxyHttpServer() proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) proxy.OnRequest(goproxy.DstHostIs(tc.Host)).DoFunc( func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { return nil, goproxy.TextResponse(req, "Dummy response") }) client, s := oneShotProxy(proxy) defer s.Close() fullURL := scheme + "://" + tc.Host + tc.RawPath req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, fullURL, nil) if err != nil { t.Fatal(err) } if tc.AddOpaque { req.URL.Scheme = scheme req.URL.Opaque = "//" + tc.Host + tc.RawPath } resp, err := client.Do(req) if err != nil { t.Fatal(err) } b, err := io.ReadAll(resp.Body) _ = resp.Body.Close() if err != nil { t.Fatal(err) } body := string(b) if body != "Dummy response" { t.Errorf("Expected proxy to return dummy body content but got %s", body) } if resp.StatusCode != http.StatusAccepted { t.Errorf("Expected status: %d, got: %d", http.StatusAccepted, resp.StatusCode) } } } func TestSimpleHttpRequest(t *testing.T) { proxy := goproxy.NewProxyHttpServer() var server *http.Server go func() { t.Log("serving end proxy server at localhost:5000") server = &http.Server{ Addr: "localhost:5000", Handler: proxy, ReadHeaderTimeout: 10 * time.Second, } err := server.ListenAndServe() if err == nil { t.Error("Error shutdown should always return error", err) } }() time.Sleep(1 * time.Second) u, _ := url.Parse("http://localhost:5000") tr := &http.Transport{ Proxy: http.ProxyURL(u), // Disable HTTP/2. TLSNextProto: make(map[string]func(authority string, c *tls.Conn) http.RoundTripper), } client := http.Client{Transport: tr} req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://example.com", nil) if err != nil { t.Fatal("Cannot create request", err) } resp, err := client.Do(req) if err != nil { t.Error("Error requesting http site", err) } else if resp.StatusCode != http.StatusOK { t.Error("Non-OK status requesting http site", err) } req, err = http.NewRequestWithContext(context.Background(), http.MethodGet, "http://example.invalid", nil) if err != nil { t.Fatal("Cannot create request", err) } resp, _ = client.Do(req) if resp == nil { t.Error("No response requesting invalid http site") } returnNil := func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { return nil } proxy.OnResponse(goproxy.UrlMatches(regexp.MustCompile(".*"))).DoFunc(returnNil) resp, _ = client.Do(req) if resp == nil { t.Error("No response requesting invalid http site") } _ = server.Shutdown(context.TODO()) } func TestResponseContentLength(t *testing.T) { // target server srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("hello world")) })) defer srv.Close() // proxy server proxy := goproxy.NewProxyHttpServer() proxy.OnResponse().DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { buf := &bytes.Buffer{} buf.WriteString("change") resp.Body = io.NopCloser(buf) return resp }) proxySrv := httptest.NewServer(proxy) defer proxySrv.Close() // send request client := &http.Client{} client.Transport = &http.Transport{ Proxy: func(req *http.Request) (*url.URL, error) { return url.Parse(proxySrv.URL) }, } req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, srv.URL, nil) resp, _ := client.Do(req) body, _ := io.ReadAll(resp.Body) _ = resp.Body.Close() if int64(len(body)) != resp.ContentLength { t.Logf("response body: %s", string(body)) t.Logf("response body Length: %d", len(body)) t.Logf("response Content-Length: %d", resp.ContentLength) t.Fatalf("Wrong response Content-Length.") } } func TestMITMResponseHTTP2MissingContentLength(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) if f, ok := w.(http.Flusher); ok { // Force missing Content-Length f.Flush() } _, _ = w.Write([]byte("HTTP/2 response")) }) // Explicitly make an HTTP/2 server srv := httptest.NewUnstartedServer(handler) srv.EnableHTTP2 = true srv.StartTLS() defer srv.Close() // proxy server proxy := goproxy.NewProxyHttpServer() proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { // Connection between the proxy client and the proxy server assert.Equal(t, "HTTP/1.1", req.Proto) return req, nil }) proxy.OnResponse().DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { // Connection between the proxy server and the origin assert.Equal(t, "HTTP/2.0", resp.Proto) return resp }) // Configure proxy transport to use HTTP/2 to communicate with the server proxy.Tr = &http.Transport{ ForceAttemptHTTP2: true, TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, NextProtos: []string{"h2"}, }, } proxySrv := httptest.NewServer(proxy) defer proxySrv.Close() client := &http.Client{ Transport: &http.Transport{ Proxy: func(req *http.Request) (*url.URL, error) { return url.Parse(proxySrv.URL) }, TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, }, } req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, srv.URL, nil) resp, err := client.Do(req) require.NoError(t, err) body, _ := io.ReadAll(resp.Body) _ = resp.Body.Close() assert.EqualValues(t, -1, resp.ContentLength) assert.Equal(t, []string{"chunked"}, resp.TransferEncoding) assert.Len(t, body, 15) } func TestMITMResponseContentLength(t *testing.T) { proxy := goproxy.NewProxyHttpServer() proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) proxy.OnResponse().DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { // Don't touch the body at all return resp }) client, l := oneShotProxy(proxy) defer l.Close() req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, https.URL+"/bobo", nil) require.NoError(t, err) resp, err := client.Do(req) require.NoError(t, err) body, err := io.ReadAll(resp.Body) require.NoError(t, err) _ = resp.Body.Close() assert.EqualValues(t, len(body), resp.ContentLength) } func TestMITMEmptyBody(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write(nil) })) defer srv.Close() proxy := goproxy.NewProxyHttpServer() proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) proxy.OnResponse().DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { return resp }) client, l := oneShotProxy(proxy) defer l.Close() req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, srv.URL, nil) require.NoError(t, err) resp, err := client.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.EqualValues(t, 0, resp.ContentLength) } func TestMITMOverwriteAlreadyEmptyBody(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write(nil) })) defer srv.Close() proxy := goproxy.NewProxyHttpServer() proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) proxy.OnResponse().DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { assert.EqualValues(t, 0, resp.ContentLength) resp.Body = io.NopCloser(bytes.NewReader(nil)) return resp }) client, l := oneShotProxy(proxy) defer l.Close() req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, srv.URL, nil) require.NoError(t, err) resp, err := client.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.EqualValues(t, 0, resp.ContentLength) } func TestMITMOverwriteBodyToEmpty(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("test")) })) defer srv.Close() proxy := goproxy.NewProxyHttpServer() proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) proxy.OnResponse().DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { assert.EqualValues(t, 4, resp.ContentLength) resp.Body = io.NopCloser(bytes.NewReader(nil)) return resp }) client, l := oneShotProxy(proxy) defer l.Close() req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, srv.URL, nil) require.NoError(t, err) resp, err := client.Do(req) require.NoError(t, err) _ = resp.Body.Close() assert.EqualValues(t, 0, resp.ContentLength) } func TestMITMRequestCancel(t *testing.T) { // target server srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("hello world")) })) defer srv.Close() // proxy server proxy := goproxy.NewProxyHttpServer() proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) var request *http.Request proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { request = req return req, nil }) proxySrv := httptest.NewServer(proxy) defer proxySrv.Close() // send request client := &http.Client{} client.Transport = &http.Transport{ Proxy: func(req *http.Request) (*url.URL, error) { return url.Parse(proxySrv.URL) }, TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, srv.URL, nil) require.NoError(t, err) resp, err := client.Do(req) require.NoError(t, err) body, err := io.ReadAll(resp.Body) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, "hello world", string(body)) assert.NotNil(t, request) select { case _, ok := <-request.Context().Done(): assert.False(t, ok) default: assert.Fail(t, "request hasn't been cancelled") } } func TestNewResponseProtoVersion(t *testing.T) { req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "https://example.com/", nil) require.NoError(t, err) resp := goproxy.NewResponse(req, goproxy.ContentTypeText, http.StatusForbidden, "blocked") assert.Equal(t, "HTTP/1.1", resp.Proto) assert.Equal(t, 1, resp.ProtoMajor) assert.Equal(t, 1, resp.ProtoMinor) var buf bytes.Buffer err = resp.Write(&buf) require.NoError(t, err) line, err := buf.ReadString('\n') require.NoError(t, err) assert.True(t, strings.HasPrefix(line, "HTTP/1.1 403"), "expected HTTP/1.1 status line, got: %s", line) } func TestNewResponseMitmWrite(t *testing.T) { proxy := goproxy.NewProxyHttpServer() proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { return nil, goproxy.NewResponse(req, goproxy.ContentTypeText, http.StatusForbidden, "blocked") }) client, l := oneShotProxy(proxy) defer l.Close() req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, https.URL+"/anything", nil) require.NoError(t, err) resp, err := client.Do(req) require.NoError(t, err) defer resp.Body.Close() body, err := io.ReadAll(resp.Body) require.NoError(t, err) assert.Equal(t, http.StatusForbidden, resp.StatusCode) assert.Equal(t, "blocked", string(body)) } func TestPersistentMitmRequest(t *testing.T) { requestCount := 0 backend := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintf(w, "Request number %d", requestCount) requestCount++ })) defer backend.Close() proxy := goproxy.NewProxyHttpServer() proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) proxyServer := httptest.NewServer(proxy) defer proxyServer.Close() proxyURL, err := url.Parse(proxyServer.URL) require.NoError(t, err) client := &http.Client{ Transport: &http.Transport{ Proxy: http.ProxyURL(proxyURL), TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, // We disable HTTP/2 to make sure to test HTTP/1.1 Keep-Alive ForceAttemptHTTP2: false, }, } for i := 0; i < 2; i++ { var connReused bool trace := &httptrace.ClientTrace{ GotConn: func(info httptrace.GotConnInfo) { connReused = info.Reused }, } ctx := httptrace.WithClientTrace(context.Background(), trace) req, err := http.NewRequestWithContext(ctx, http.MethodGet, backend.URL, nil) require.NoError(t, err) resp, err := client.Do(req) require.NoError(t, err) body, err := io.ReadAll(resp.Body) require.NoError(t, err) _ = resp.Body.Close() assert.Equal(t, fmt.Sprintf("Request number %d", i), string(body)) // First request creates the connection, second request reuses it switch i { case 0: assert.False(t, connReused) case 1: assert.True(t, connReused) } } } ================================================ FILE: regretable/regretreader.go ================================================ package regretable import ( "io" ) // Reader in regretable package will allow you to read from a reader, // and then to "regret" reading it, and push back everything you've read. // For example: // // rb := NewRegretableReader(bytes.NewBuffer([]byte{1,2,3})) // var b = make([]byte,1) // rb.Read(b) // b[0] = 1 // rb.Regret() // ioutil.ReadAll(rb.Read) // returns []byte{1,2,3},nil type Reader struct { reader io.Reader overflow bool r, w int buf []byte } const _defaultBufferSize = 500 // The next read from the RegretableReader will be as if the underlying reader // was never read (or from the last point forget is called). func (rb *Reader) Regret() { if rb.overflow { panic("regretting after overflow makes no sense") } rb.r = 0 } // Will "forget" everything read so far. // // rb := NewRegretableReader(bytes.NewBuffer([]byte{1,2,3})) // var b = make([]byte,1) // rb.Read(b) // b[0] = 1 // rb.Forget() // rb.Read(b) // b[0] = 2 // rb.Regret() // ioutil.ReadAll(rb.Read) // returns []byte{2,3},nil func (rb *Reader) Forget() { if rb.overflow { panic("forgetting after overflow makes no sense") } rb.r = 0 rb.w = 0 } // initialize a RegretableReader with underlying reader r, whose buffer is size bytes long. func NewRegretableReaderSize(r io.Reader, size int) *Reader { return &Reader{reader: r, buf: make([]byte, size)} } // initialize a RegretableReader with underlying reader r. func NewRegretableReader(r io.Reader) *Reader { return NewRegretableReaderSize(r, _defaultBufferSize) } // reads from the underlying reader. Will buffer all input until Regret is called. func (rb *Reader) Read(p []byte) (n int, err error) { if rb.overflow { return rb.reader.Read(p) } if rb.r < rb.w { n = copy(p, rb.buf[rb.r:rb.w]) rb.r += n return } n, err = rb.reader.Read(p) bn := copy(rb.buf[rb.w:], p[:n]) rb.w, rb.r = rb.w+bn, rb.w+n if bn < n { rb.overflow = true } return } // ReaderCloser is the same as Reader, but allows closing the underlying reader. type ReaderCloser struct { Reader c io.Closer } // initialize a RegretableReaderCloser with underlying readCloser rc. func NewRegretableReaderCloser(rc io.ReadCloser) *ReaderCloser { return &ReaderCloser{*NewRegretableReader(rc), rc} } // initialize a RegretableReaderCloser with underlying readCloser rc. func NewRegretableReaderCloserSize(rc io.ReadCloser, size int) *ReaderCloser { return &ReaderCloser{*NewRegretableReaderSize(rc, size), rc} } // Closes the underlying readCloser, you cannot regret after closing the stream. func (rbc *ReaderCloser) Close() error { return rbc.c.Close() } ================================================ FILE: regretable/regretreader_test.go ================================================ package regretable_test import ( "bytes" "io" "strings" "testing" "github.com/elazarl/goproxy/regretable" ) func assertEqual(t *testing.T, expected, actual string) { t.Helper() if expected != actual { t.Fatal("Expected", expected, "actual", actual) } } func assertReadAll(t *testing.T, r io.Reader) string { t.Helper() s, err := io.ReadAll(r) if err != nil { t.Fatal("error when reading", err) } return string(s) } func TestRegretableReader(t *testing.T) { buf := new(bytes.Buffer) mb := regretable.NewRegretableReader(buf) word := "12345678" buf.WriteString(word) fivebytes := make([]byte, 5) _, _ = mb.Read(fivebytes) mb.Regret() s, _ := io.ReadAll(mb) if string(s) != word { t.Errorf("Uncommitted read is gone, [%d,%d] actual '%v' expected '%v'\n", len(s), len(word), string(s), word) } } func TestRegretableEmptyRead(t *testing.T) { buf := new(bytes.Buffer) mb := regretable.NewRegretableReader(buf) word := "12345678" buf.WriteString(word) zero := make([]byte, 0) _, _ = mb.Read(zero) mb.Regret() s, err := io.ReadAll(mb) if string(s) != word { t.Error("Uncommitted read is gone, actual:", string(s), "expected:", word, "err:", err) } } func TestRegretableAlsoEmptyRead(t *testing.T) { buf := new(bytes.Buffer) mb := regretable.NewRegretableReader(buf) word := "12345678" buf.WriteString(word) one := make([]byte, 1) zero := make([]byte, 0) five := make([]byte, 5) _, _ = mb.Read(one) _, _ = mb.Read(zero) _, _ = mb.Read(five) mb.Regret() s, _ := io.ReadAll(mb) if string(s) != word { t.Error("Uncommitted read is gone", string(s), "expected", word) } } func TestRegretableRegretBeforeRead(t *testing.T) { buf := new(bytes.Buffer) mb := regretable.NewRegretableReader(buf) word := "12345678" buf.WriteString(word) five := make([]byte, 5) mb.Regret() _, _ = mb.Read(five) s, err := io.ReadAll(mb) if string(s) != "678" { t.Error("Uncommitted read is gone", string(s), len(string(s)), "expected", "678", len("678"), "err:", err) } } func TestRegretableFullRead(t *testing.T) { buf := new(bytes.Buffer) mb := regretable.NewRegretableReader(buf) word := "12345678" buf.WriteString(word) twenty := make([]byte, 20) _, _ = mb.Read(twenty) mb.Regret() s, _ := io.ReadAll(mb) if string(s) != word { t.Error("Uncommitted read is gone", string(s), len(string(s)), "expected", word, len(word)) } } func TestRegretableRegretTwice(t *testing.T) { buf := new(bytes.Buffer) mb := regretable.NewRegretableReader(buf) word := "12345678" buf.WriteString(word) assertEqual(t, word, assertReadAll(t, mb)) mb.Regret() assertEqual(t, word, assertReadAll(t, mb)) mb.Regret() assertEqual(t, word, assertReadAll(t, mb)) } type CloseCounter struct { r io.Reader closed int } func (cc *CloseCounter) Read(b []byte) (int, error) { return cc.r.Read(b) } func (cc *CloseCounter) Close() error { cc.closed++ return nil } func TestRegretableCloserSizeRegrets(t *testing.T) { defer func() { r := recover() if r == nil { t.Error("Did not panic when regretting overread buffer:", r) } stringValue, ok := r.(string) if !ok || !strings.Contains(stringValue, "regret") { t.Error("Invalid panic value when regretting overread buffer:", r) } }() buf := new(bytes.Buffer) buf.WriteString("123456") mb := regretable.NewRegretableReaderCloserSize(io.NopCloser(buf), 3) _, _ = mb.Read(make([]byte, 4)) mb.Regret() } func TestRegretableCloserRegretsClose(t *testing.T) { buf := new(bytes.Buffer) cc := &CloseCounter{buf, 0} mb := regretable.NewRegretableReaderCloser(cc) word := "12345678" buf.WriteString(word) _, _ = mb.Read([]byte{0}) _ = mb.Close() if cc.closed != 1 { t.Error("RegretableReaderCloser ignores Close") } mb.Regret() _ = mb.Close() if cc.closed != 2 { t.Error("RegretableReaderCloser does ignore Close after regret") } // TODO(elazar): return an error if client issues Close more than once after regret } ================================================ FILE: responses.go ================================================ package goproxy import ( "bytes" "io" "net/http" ) // Will generate a valid http response to the given request the response will have // the given contentType, and http status. // Typical usage, refuse to process requests to local addresses: // // proxy.OnRequest(IsLocalHost()).DoFunc(func(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request,*http.Response) { // return nil,NewResponse(r,goproxy.ContentTypeHtml,http.StatusUnauthorized, // `Can't use proxy for local addresses`) // }) func NewResponse(r *http.Request, contentType string, status int, body string) *http.Response { resp := &http.Response{} resp.Request = r resp.TransferEncoding = r.TransferEncoding resp.Header = make(http.Header) 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) return resp } const ( ContentTypeText = "text/plain" ContentTypeHtml = "text/html" ) // Alias for NewResponse(r,ContentTypeText,http.StatusAccepted,text). func TextResponse(r *http.Request, text string) *http.Response { return NewResponse(r, ContentTypeText, http.StatusAccepted, text) } ================================================ FILE: transport/roundtripper.go ================================================ package transport import "net/http" type RoundTripper interface { // RoundTrip executes a single HTTP transaction, returning // the Response for the request req. RoundTrip should not // attempt to interpret the response. In particular, // RoundTrip must return err == nil if it obtained a response, // regardless of the response's HTTP status code. A non-nil // err should be reserved for failure to obtain a response. // Similarly, RoundTrip should not attempt to handle // higher-level protocol details such as redirects, // authentication, or cookies. // // RoundTrip should not modify the request, except for // consuming the Body. The request's URL and Header fields // are guaranteed to be initialized. RoundTrip(*http.Request) (*http.Response, error) DetailedRoundTrip(*http.Request) (*RoundTripDetails, *http.Response, error) } ================================================ FILE: transport/transport.go ================================================ // Copyright 2011 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. // HTTP client implementation. See RFC 2616. // // This is the low-level Transport implementation of RoundTripper. // The high-level interface is in client.go. // This file is DEPRECATED and keep solely for backward compatibility. package transport import ( "bufio" "compress/gzip" "crypto/tls" "encoding/base64" "errors" "fmt" "io" "log" "net" "net/http" "net/url" "os" "strings" "sync" ) // DefaultTransport is the default implementation of Transport and is // used by DefaultClient. It establishes a new network connection for // each call to Do and uses HTTP proxies as directed by the // $HTTP_PROXY and $NO_PROXY (or $http_proxy and $no_proxy) // environment variables. var DefaultTransport RoundTripper = &Transport{Proxy: ProxyFromEnvironment} // DefaultMaxIdleConnsPerHost is the default value of Transport's // MaxIdleConnsPerHost. const DefaultMaxIdleConnsPerHost = 2 // Transport is an implementation of RoundTripper that supports http, // https, and http proxies (for either http or https with CONNECT). // Transport can also cache connections for future re-use. type Transport struct { lk sync.Mutex idleConn map[string][]*persistConn altProto map[string]RoundTripper // nil or map of URI scheme => RoundTripper // TODO: tunable on global max cached connections // TODO: tunable on timeout on cached connections // TODO: optional pipelining // Proxy specifies a function to return a proxy for a given // Request. If the function returns a non-nil error, the // request is aborted with the provided error. // If Proxy is nil or returns a nil *URL, no proxy is used. Proxy func(*http.Request) (*url.URL, error) // Dial specifies the dial function for creating TCP // connections. // If Dial is nil, net.Dial is used. Dial func(net, addr string) (c net.Conn, err error) // TLSClientConfig specifies the TLS configuration to use with // tls.Client. If nil, the default configuration is used. TLSClientConfig *tls.Config DisableKeepAlives bool DisableCompression bool // MaxIdleConnsPerHost, if non-zero, controls the maximum idle // (keep-alive) to keep to keep per-host. If zero, // DefaultMaxIdleConnsPerHost is used. MaxIdleConnsPerHost int } // ProxyFromEnvironment returns the URL of the proxy to use for a // given request, as indicated by the environment variables // $HTTP_PROXY and $NO_PROXY (or $http_proxy and $no_proxy). // An error is returned if the proxy environment is invalid. // A nil URL and nil error are returned if no proxy is defined in the // environment, or a proxy should not be used for the given request. func ProxyFromEnvironment(req *http.Request) (*url.URL, error) { proxy := getenvEitherCase("HTTP_PROXY") if proxy == "" { return nil, nil } if !useProxy(canonicalAddr(req.URL)) { return nil, nil } proxyURL, err := url.Parse(proxy) if err != nil || proxyURL.Scheme == "" { proxyURL, err = url.Parse("http://" + proxy) } if err != nil { return nil, fmt.Errorf("invalid proxy address %q: %w", proxy, err) } return proxyURL, nil } // ProxyURL returns a proxy function (for use in a Transport) // that always returns the same URL. func ProxyURL(fixedURL *url.URL) func(*http.Request) (*url.URL, error) { return func(*http.Request) (*url.URL, error) { return fixedURL, nil } } // transportRequest is a wrapper around a *Request that adds // optional extra headers to write. type transportRequest struct { *http.Request // original request, not to be mutated extra http.Header // extra headers to write, or nil } func (tr *transportRequest) extraHeaders() http.Header { if tr.extra == nil { tr.extra = make(http.Header) } return tr.extra } type RoundTripDetails struct { Host string TCPAddr *net.TCPAddr IsProxy bool Error error } func (t *Transport) DetailedRoundTrip(req *http.Request) (details *RoundTripDetails, resp *http.Response, err error) { if req.URL == nil { return nil, nil, errors.New("http: nil Request.URL") } if req.Header == nil { return nil, nil, errors.New("http: nil Request.Header") } if req.URL.Scheme != "http" && req.URL.Scheme != "https" { t.lk.Lock() var rt RoundTripper if t.altProto != nil { rt = t.altProto[req.URL.Scheme] } t.lk.Unlock() if rt == nil { return nil, nil, &badStringError{"unsupported protocol scheme", req.URL.Scheme} } return rt.DetailedRoundTrip(req) } treq := &transportRequest{Request: req} cm, err := t.connectMethodForRequest(treq) if err != nil { return nil, nil, err } // Get the cached or newly-created connection to either the // host (for http or https), the http proxy, or the http proxy // pre-CONNECTed to https server. In any case, we'll be ready // to send it requests. pconn, err := t.getConn(cm) if err != nil { return nil, nil, err } resp, err = pconn.roundTrip(treq) return &RoundTripDetails{pconn.host, pconn.ip, pconn.isProxy, err}, resp, err } // RoundTrip implements the RoundTripper interface. func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err error) { _, resp, err = t.DetailedRoundTrip(req) return } // RegisterProtocol registers a new protocol with scheme. // The Transport will pass requests using the given scheme to rt. // It is rt's responsibility to simulate HTTP request semantics. // // RegisterProtocol can be used by other packages to provide // implementations of protocol schemes like "ftp" or "file". func (t *Transport) RegisterProtocol(scheme string, rt RoundTripper) { if scheme == "http" || scheme == "https" { panic("protocol " + scheme + " already registered") } t.lk.Lock() defer t.lk.Unlock() if t.altProto == nil { t.altProto = make(map[string]RoundTripper) } if _, exists := t.altProto[scheme]; exists { panic("protocol " + scheme + " already registered") } t.altProto[scheme] = rt } // CloseIdleConnections closes any connections which were previously // connected from previous requests but are now sitting idle in // a "keep-alive" state. It does not interrupt any connections currently // in use. func (t *Transport) CloseIdleConnections() { t.lk.Lock() defer t.lk.Unlock() if t.idleConn == nil { return } for _, conns := range t.idleConn { for _, pconn := range conns { pconn.close() } } t.idleConn = make(map[string][]*persistConn) } // // Private implementation past this point. // func getenvEitherCase(k string) string { if v := os.Getenv(strings.ToUpper(k)); v != "" { return v } return os.Getenv(strings.ToLower(k)) } func (t *Transport) connectMethodForRequest(treq *transportRequest) (*connectMethod, error) { cm := &connectMethod{ targetScheme: treq.URL.Scheme, targetAddr: canonicalAddr(treq.URL), } if t.Proxy != nil { var err error cm.proxyURL, err = t.Proxy(treq.Request) if err != nil { return nil, err } } return cm, nil } // proxyAuth returns the Proxy-Authorization header to set // on requests, if applicable. func (cm *connectMethod) proxyAuth() string { if cm.proxyURL == nil { return "" } if u := cm.proxyURL.User; u != nil { return "Basic " + base64.URLEncoding.EncodeToString([]byte(u.String())) } return "" } // putIdleConn adds pconn to the list of idle persistent connections awaiting // a new request. // If pconn is no longer needed or not in a good state, putIdleConn // returns false. func (t *Transport) putIdleConn(pconn *persistConn) bool { t.lk.Lock() defer t.lk.Unlock() if t.DisableKeepAlives || t.MaxIdleConnsPerHost < 0 { pconn.close() return false } if pconn.isBroken() { return false } key := pconn.cacheKey maxIdleConns := t.MaxIdleConnsPerHost if maxIdleConns == 0 { maxIdleConns = DefaultMaxIdleConnsPerHost } if len(t.idleConn[key]) >= maxIdleConns { pconn.close() return false } t.idleConn[key] = append(t.idleConn[key], pconn) return true } func (t *Transport) getIdleConn(cm *connectMethod) (pconn *persistConn) { t.lk.Lock() defer t.lk.Unlock() if t.idleConn == nil { t.idleConn = make(map[string][]*persistConn) } key := cm.String() for { pconns, ok := t.idleConn[key] if !ok { return nil } if len(pconns) == 1 { pconn = pconns[0] delete(t.idleConn, key) } else { // 2 or more cached connections; pop last // TODO: queue? pconn = pconns[len(pconns)-1] t.idleConn[key] = pconns[0 : len(pconns)-1] } if !pconn.isBroken() { return } } } func (t *Transport) dial(network, addr string) (c net.Conn, raddr string, ip *net.TCPAddr, err error) { if t.Dial != nil { ip, err = net.ResolveTCPAddr("tcp", addr) if err != nil { return } c, err = t.Dial(network, addr) raddr = addr return } addri, err := net.ResolveTCPAddr("tcp", addr) if err != nil { return } c, err = net.DialTCP("tcp", nil, addri) raddr = addr ip = addri return } // getConn dials and creates a new persistConn to the target as // specified in the connectMethod. This includes doing a proxy CONNECT // and/or setting up TLS. If this doesn't return an error, the persistConn // is ready to write requests to. func (t *Transport) getConn(cm *connectMethod) (*persistConn, error) { if pc := t.getIdleConn(cm); pc != nil { return pc, nil } conn, raddr, ip, err := t.dial("tcp", cm.addr()) if err != nil { if cm.proxyURL != nil { err = fmt.Errorf("http: error connecting to proxy %s: %w", cm.proxyURL, err) } return nil, err } pa := cm.proxyAuth() pconn := &persistConn{ t: t, cacheKey: cm.String(), conn: conn, reqch: make(chan requestAndChan, 50), host: raddr, ip: ip, } switch { case cm.proxyURL == nil: // Do nothing. case cm.targetScheme == "http": pconn.isProxy = true if pa != "" { pconn.mutateHeaderFunc = func(h http.Header) { h.Set("Proxy-Authorization", pa) } } case cm.targetScheme == "https": connectReq := &http.Request{ Method: http.MethodConnect, URL: &url.URL{Opaque: cm.targetAddr}, Host: cm.targetAddr, Header: make(http.Header), } if pa != "" { connectReq.Header.Set("Proxy-Authorization", pa) } _ = connectReq.Write(conn) // Read response. // Okay to use and discard buffered reader here, because // TLS server will not speak until spoken to. br := bufio.NewReader(conn) resp, err := http.ReadResponse(br, connectReq) if err != nil { conn.Close() return nil, err } if resp.StatusCode != http.StatusOK { f := strings.SplitN(resp.Status, " ", 2) conn.Close() return nil, errors.New(f[1]) } } if cm.targetScheme == "https" { // Initiate TLS and check remote host name against certificate. conn = tls.Client(conn, t.TLSClientConfig) tlsConn, ok := conn.(*tls.Conn) if !ok { return nil, errors.New("invalid TLS connection") } if err = tlsConn.Handshake(); err != nil { return nil, err } if t.TLSClientConfig == nil || !t.TLSClientConfig.InsecureSkipVerify { if err = tlsConn.VerifyHostname(cm.tlsHost()); err != nil { return nil, err } } pconn.conn = conn } pconn.br = bufio.NewReader(pconn.conn) pconn.bw = bufio.NewWriter(pconn.conn) go pconn.readLoop() return pconn, nil } // useProxy returns true if requests to addr should use a proxy, // according to the NO_PROXY or no_proxy environment variable. // addr is always a canonicalAddr with a host and port. func useProxy(addr string) bool { if len(addr) == 0 { return true } host, _, err := net.SplitHostPort(addr) if err != nil { return false } if host == "localhost" { return false } if ip := net.ParseIP(host); ip != nil { if ip.IsLoopback() { return false } } no_proxy := getenvEitherCase("NO_PROXY") if no_proxy == "*" { return false } addr = strings.ToLower(strings.TrimSpace(addr)) if hasPort(addr) { addr = addr[:strings.LastIndex(addr, ":")] } for _, p := range strings.Split(no_proxy, ",") { p = strings.ToLower(strings.TrimSpace(p)) if len(p) == 0 { continue } if hasPort(p) { p = p[:strings.LastIndex(p, ":")] } if addr == p || (p[0] == '.' && (strings.HasSuffix(addr, p) || addr == p[1:])) { return false } } return true } // connectMethod is the map key (in its String form) for keeping persistent // TCP connections alive for subsequent HTTP requests. // // A connect method may be of the following types: // // Cache key form Description // ----------------- ------------------------- // ||http|foo.com http directly to server, no proxy // ||https|foo.com https directly to server, no proxy // http://proxy.com|https|foo.com http to proxy, then CONNECT to foo.com // http://proxy.com|http http to proxy, http to anywhere after that // // Note: no support to https to the proxy yet. type connectMethod struct { proxyURL *url.URL // nil for no proxy, else full proxy URL targetScheme string // "http" or "https" targetAddr string // Not used if proxy + http targetScheme (4th example in table) } func (cm *connectMethod) String() string { proxyStr := "" if cm.proxyURL != nil { proxyStr = cm.proxyURL.String() } return strings.Join([]string{proxyStr, cm.targetScheme, cm.targetAddr}, "|") } // addr returns the first hop "host:port" to which we need to TCP connect. func (cm *connectMethod) addr() string { if cm.proxyURL != nil { return canonicalAddr(cm.proxyURL) } return cm.targetAddr } // tlsHost returns the host name to match against the peer's // TLS certificate. func (cm *connectMethod) tlsHost() string { h := cm.targetAddr if hasPort(h) { h = h[:strings.LastIndex(h, ":")] } return h } // persistConn wraps a connection, usually a persistent one // (but may be used for non-keep-alive requests as well). type persistConn struct { t *Transport cacheKey string // its connectMethod.String() conn net.Conn br *bufio.Reader // from conn bw *bufio.Writer // to conn reqch chan requestAndChan // written by roundTrip(); read by readLoop() isProxy bool // mutateHeaderFunc is an optional func to modify extra // headers on each outbound request before it's written. (the // original Request given to RoundTrip is not modified) mutateHeaderFunc func(http.Header) lk sync.Mutex // guards numExpectedResponses and broken numExpectedResponses int broken bool // an error has happened on this connection; marked broken so it's not reused. host string ip *net.TCPAddr } func (pc *persistConn) isBroken() bool { pc.lk.Lock() defer pc.lk.Unlock() return pc.broken } func (pc *persistConn) readLoop() { alive := true var lastbody io.ReadCloser // last response body, if any, read on this connection for alive { pb, err := pc.br.Peek(1) pc.lk.Lock() if pc.numExpectedResponses == 0 { pc.closeLocked() pc.lk.Unlock() if len(pb) > 0 { log.Printf("Unsolicited response received on idle HTTP channel starting with %q; err=%v", string(pb), err) } return } pc.lk.Unlock() rc := <-pc.reqch // Advance past the previous response's body, if the // caller hasn't done so. if lastbody != nil { lastbody.Close() // assumed idempotent lastbody = nil } resp, err := http.ReadResponse(pc.br, rc.req) if err != nil { pc.close() } else { hasBody := rc.req.Method != http.MethodHead && resp.ContentLength != 0 if rc.addedGzip && hasBody && resp.Header.Get("Content-Encoding") == "gzip" { resp.Header.Del("Content-Encoding") resp.Header.Del("Content-Length") resp.ContentLength = -1 gzReader, zerr := gzip.NewReader(resp.Body) if zerr != nil { pc.close() err = zerr } else { resp.Body = &readFirstCloseBoth{&discardOnCloseReadCloser{gzReader}, resp.Body} } } resp.Body = &bodyEOFSignal{body: resp.Body} } if err != nil || resp.Close || rc.req.Close { alive = false } hasBody := resp != nil && resp.ContentLength != 0 var waitForBodyRead chan bool if alive { if hasBody { bodyEof, ok := resp.Body.(*bodyEOFSignal) if !ok { alive = false } lastbody = resp.Body waitForBodyRead = make(chan bool) bodyEof.fn = func() { if !pc.t.putIdleConn(pc) { alive = false } waitForBodyRead <- true } } else { // When there's no response body, we immediately // reuse the TCP connection (putIdleConn), but // we need to prevent ClientConn.Read from // closing the Response.Body on the next // loop, otherwise it might close the body // before the client code has had a chance to // read it (even though it'll just be 0, EOF). lastbody = nil if !pc.t.putIdleConn(pc) { alive = false } } } rc.ch <- responseAndError{resp, err} // Wait for the just-returned response body to be fully consumed // before we race and peek on the underlying bufio reader. if waitForBodyRead != nil { <-waitForBodyRead } } } type responseAndError struct { res *http.Response err error } type requestAndChan struct { req *http.Request ch chan responseAndError // did the Transport (as opposed to the client code) add an // Accept-Encoding gzip header? only if it we set it do // we transparently decode the gzip. addedGzip bool } func (pc *persistConn) roundTrip(req *transportRequest) (resp *http.Response, err error) { if pc.mutateHeaderFunc != nil { panic("mutateHeaderFunc not supported in modified Transport") } // Ask for a compressed version if the caller didn't set their // own value for Accept-Encoding. We only attempted to // uncompress the gzip stream if we were the layer that // requested it. requestedGzip := false if !pc.t.DisableCompression && req.Header.Get("Accept-Encoding") == "" { // Request gzip only, not deflate. Deflate is ambiguous and // not as universally supported anyway. // See: http://www.gzip.org/zlib/zlib_faq.html#faq38 requestedGzip = true req.extraHeaders().Set("Accept-Encoding", "gzip") } pc.lk.Lock() pc.numExpectedResponses++ pc.lk.Unlock() // orig: err = req.Request.write(pc.bw, pc.isProxy, req.extra) if pc.isProxy { err = req.Request.WriteProxy(pc.bw) } else { err = req.Request.Write(pc.bw) } if err != nil { pc.close() return nil, err } pc.bw.Flush() ch := make(chan responseAndError, 1) pc.reqch <- requestAndChan{req.Request, ch, requestedGzip} re := <-ch pc.lk.Lock() pc.numExpectedResponses-- pc.lk.Unlock() return re.res, re.err } func (pc *persistConn) close() { pc.lk.Lock() defer pc.lk.Unlock() pc.closeLocked() } func (pc *persistConn) closeLocked() { pc.broken = true pc.conn.Close() pc.mutateHeaderFunc = nil } var portMap = map[string]string{ "http": "80", "https": "443", } // canonicalAddr returns url.Host but always with a ":port" suffix. func canonicalAddr(url *url.URL) string { addr := url.Host if !hasPort(addr) { return addr + ":" + portMap[url.Scheme] } return addr } // bodyEOFSignal wraps a ReadCloser but runs fn (if non-nil) at most // once, right before the final Read() or Close() call returns, but after // EOF has been seen. type bodyEOFSignal struct { body io.ReadCloser fn func() isClosed bool } func (es *bodyEOFSignal) Read(p []byte) (n int, err error) { n, err = es.body.Read(p) if es.isClosed && n > 0 { panic("http: unexpected bodyEOFSignal Read after Close; see issue 1725") } if errors.Is(err, io.EOF) && es.fn != nil { es.fn() es.fn = nil } return } func (es *bodyEOFSignal) Close() (err error) { if es.isClosed { return nil } es.isClosed = true err = es.body.Close() if err == nil && es.fn != nil { es.fn() es.fn = nil } return } type readFirstCloseBoth struct { io.ReadCloser io.Closer } func (r *readFirstCloseBoth) Close() error { if err := r.ReadCloser.Close(); err != nil { r.Closer.Close() return err } if err := r.Closer.Close(); err != nil { return err } return nil } // discardOnCloseReadCloser consumes all its input on Close. type discardOnCloseReadCloser struct { io.ReadCloser } func (d *discardOnCloseReadCloser) Close() error { _, _ = io.Copy(io.Discard, d.ReadCloser) // ignore errors; likely invalid or already closed return d.ReadCloser.Close() } ================================================ FILE: transport/util.go ================================================ package transport import ( "fmt" "strings" ) type badStringError struct { what string str string } func (e *badStringError) Error() string { return fmt.Sprintf("%s %q", e.what, e.str) } func hasPort(s string) bool { return strings.LastIndex(s, ":") > strings.LastIndex(s, "]") } ================================================ FILE: websocket.go ================================================ package goproxy import ( "io" "net" "net/http" "strings" ) func headerContains(header http.Header, name string, value string) bool { for _, v := range header[name] { for _, s := range strings.Split(v, ",") { if strings.EqualFold(value, strings.TrimSpace(s)) { return true } } } return false } func isWebSocketHandshake(header http.Header) bool { return headerContains(header, "Connection", "Upgrade") && headerContains(header, "Upgrade", "websocket") } func (proxy *ProxyHttpServer) hijackConnection(ctx *ProxyCtx, w http.ResponseWriter) (net.Conn, error) { // Connect to Client hj, ok := w.(http.Hijacker) if !ok { panic("httpserver does not support hijacking") } clientConn, _, err := hj.Hijack() if err != nil { ctx.Warnf("Hijack error: %v", err) return nil, err } return clientConn, nil } 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 } ================================================ FILE: websocket_test.go ================================================ package goproxy_test import ( "context" "crypto/tls" "net/http" "net/http/httptest" "net/url" "testing" "time" "github.com/coder/websocket" "github.com/elazarl/goproxy" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestWebSocketMitm(t *testing.T) { // Start a WebSocket echo server backend := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ InsecureSkipVerify: true, }) if err != nil { return } defer func() { _ = c.Close(websocket.StatusNormalClosure, "") }() ctx := r.Context() for { mt, message, err := c.Read(ctx) if err != nil { break } err = c.Write(ctx, mt, append([]byte("ECHO: "), message...)) if err != nil { break } } })) backend.StartTLS() defer backend.Close() // Start goproxy proxy := goproxy.NewProxyHttpServer() proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) proxyServer := httptest.NewServer(proxy) defer proxyServer.Close() // Configure WebSocket client to use proxy proxyURL, err := url.Parse(proxyServer.URL) require.NoError(t, err) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() c, _, err := websocket.Dial(ctx, backend.URL, &websocket.DialOptions{ HTTPClient: &http.Client{ Transport: &http.Transport{ Proxy: http.ProxyURL(proxyURL), TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, }, }, }) require.NoError(t, err) defer func() { _ = c.Close(websocket.StatusNormalClosure, "") }() // Verify bidirectional communication message := []byte("Hello WebSocket") err = c.Write(ctx, websocket.MessageText, message) require.NoError(t, err) mt, response, err := c.Read(ctx) require.NoError(t, err) assert.Equal(t, websocket.MessageText, mt) assert.Equal(t, "ECHO: Hello WebSocket", string(response)) }