├── test ├── forwardproxy │ ├── index.html │ └── pic.png ├── index │ ├── index.html │ └── pic.png ├── upstreamingproxy │ ├── index.html │ └── pic.png ├── parseable_acl.txt └── unparseable_acl.txt ├── .golangci.yml ├── go.mod ├── docker-build ├── README.md ├── Dockerfile ├── gen_caddyfile_and_start.sh └── run.sh ├── .travis.yml ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ └── build.yml ├── ISSUE_TEMPLATE.md └── CONTRIBUTING.md ├── CONTRIBUTING.md ├── acl.go ├── httpclient_test.go ├── caddyfile.go ├── acl_test.go ├── httpclient └── httpclient.go ├── README.md ├── LICENSE ├── forwardproxy_test.go ├── common_test.go ├── probe_resist_test.go └── forwardproxy.go /test/forwardproxy/index.html: -------------------------------------------------------------------------------- 1 | I am ForwardProxy(don't tell anyone) -------------------------------------------------------------------------------- /test/index/index.html: -------------------------------------------------------------------------------- 1 | I am not a ForwardProxy, but I want to be when I grow up! -------------------------------------------------------------------------------- /test/upstreamingproxy/index.html: -------------------------------------------------------------------------------- 1 | I am upstreaming ForwardProxy(don't tell anyone) 2 | -------------------------------------------------------------------------------- /test/index/pic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XTLS/forwardproxy-reality/HEAD/test/index/pic.png -------------------------------------------------------------------------------- /test/forwardproxy/pic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XTLS/forwardproxy-reality/HEAD/test/forwardproxy/pic.png -------------------------------------------------------------------------------- /test/parseable_acl.txt: -------------------------------------------------------------------------------- 1 | 128.12.2.3 2 | 123.32.1.1/32 3 | qwe.com 4 | localhost 5 | lalalala 6 | usetor.usesignal 7 | -------------------------------------------------------------------------------- /test/upstreamingproxy/pic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XTLS/forwardproxy-reality/HEAD/test/upstreamingproxy/pic.png -------------------------------------------------------------------------------- /test/unparseable_acl.txt: -------------------------------------------------------------------------------- 1 | 128.3.3.1/23 2 | previous.line.is.parseable.com 3 | but.next.line.is.not.parseable.com 4 | (za0zaz 5 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # golangci linter currently requires a config file to 2 | # disable checking of the test files 3 | run: 4 | tests: false 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/caddyserver/forwardproxy 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/caddyserver/caddy/v2 v2.4.0-beta.1 7 | go.uber.org/zap v1.16.0 8 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777 9 | ) 10 | -------------------------------------------------------------------------------- /docker-build/README.md: -------------------------------------------------------------------------------- 1 | # caddy-forwardproxy 2 | A docker image for Caddy web server + forwardproxy plugin. 3 | Allows to easily set up private web server with proxying. 4 | ### Build 5 | ```docker build -t caddy-forwardproxy .``` 6 | ### Usage 7 | Please find latest usage instructions in [run.sh](./run.sh). 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.15.x 5 | env: 6 | - GO111MODULE=on 7 | 8 | dist: trusty 9 | 10 | install: 11 | - go get -v -t -d ./... 12 | - go get -v github.com/golangci/golangci-lint/cmd/golangci-lint 13 | 14 | script: 15 | - go test -race -v . 16 | - golangci-lint run -E gofmt -E goimports -E misspell -E ineffassign -E staticcheck -E gosimple -D errcheck 17 | -------------------------------------------------------------------------------- /docker-build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.6 2 | 3 | LABEL description="Docker image for caddy+forwardproxy plugin." 4 | LABEL maintainer="SergeyFrolov@colorado.edu" 5 | 6 | RUN apk add --no-cache ca-certificates bash curl 7 | 8 | RUN curl --fail https://getcaddy.com | bash -s http.forwardproxy 9 | 10 | COPY gen_caddyfile_and_start.sh /bin/ 11 | 12 | VOLUME /root/.caddy 13 | 14 | EXPOSE 80 443 2015 15 | 16 | ENTRYPOINT /bin/gen_caddyfile_and_start.sh 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | ### 1. What does this change do, exactly? 6 | 7 | 8 | ### 2. Please link to the relevant issues. 9 | 10 | 11 | ### 3. Which documentation changes (if any) need to be made because of this PR? 12 | 13 | 14 | ### 4. Checklist 15 | 16 | - [ ] I have written tests and verified that they fail without my change 17 | - [ ] I made pull request as minimal and simple as possible. If change is not small or additional dependencies are required, I opened an issue to propose and discuss the design first 18 | - [ ] I have squashed any insignificant commits 19 | - [ ] This change has comments for package types, values, functions, and non-obvious lines of code 20 | -------------------------------------------------------------------------------- /docker-build/gen_caddyfile_and_start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CADDYFILE="${CADDYFILE:-/etc/caddy/Caddyfile}" 4 | ROOTDIR="${ROOTDIR:-/srv/index}" 5 | SITE_ADDRESS="${SITE_ADDRESS:-localhost}" 6 | 7 | generate_caddyfile() { 8 | mkdir -p "$(dirname "${CADDYFILE}")" 9 | 10 | echo "${SITE_ADDRESS} {" > ${CADDYFILE} 11 | echo " root $ROOTDIR" >> ${CADDYFILE} 12 | 13 | echo " forwardproxy {" >> ${CADDYFILE} 14 | if [[ ! -z ${PROXY_USERNAME} ]]; then 15 | echo " basicauth ${PROXY_USERNAME} ${PROXY_PASSWORD}" >> ${CADDYFILE} 16 | fi 17 | if [[ "${PROBE_RESISTANT}" = true ]]; then 18 | echo " probe_resistance ${SECRET_LINK}" >> ${CADDYFILE} 19 | fi 20 | echo " }" >> ${CADDYFILE} 21 | 22 | echo "}" >> ${CADDYFILE} 23 | } 24 | 25 | if [ -f "${CADDYFILE}" ]; then 26 | echo "Using provided Caddyfile" 27 | else 28 | echo "Caddyfile is not provided: generating new one" 29 | generate_caddyfile 30 | fi 31 | 32 | caddy ${CADDY_OPTS} -conf ${CADDYFILE} 33 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. -------------------------------------------------------------------------------- /docker-build/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | print_help() { 4 | cat <sha256sum.txt 29 | echo "SHA256SUM=$(cut -d' ' -f2 sha256sum.txt)" >>$GITHUB_ENV 30 | - uses: actions/upload-artifact@v2 31 | with: 32 | name: ${{ env.BUNDLE }}.tar.xz caddy executable sha256 ${{ env.SHA256SUM }} 33 | path: sha256sum.txt 34 | - name: Upload caddy assets 35 | if: ${{ github.event_name == 'release' }} 36 | run: hub release edit -a ${{ env.BUNDLE }}.tar.xz -m "" "${GITHUB_REF##*/}" 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | ### 1. Is bug reproducible with [latest](https://caddyserver.com/download) `forwardproxy` build? 6 | 9 | 10 | 11 | ### 2. What are you trying to do? 12 | 13 | 14 | ### 3. What is your entire Caddyfile? 15 | ```text 16 | (paste Caddyfile here) 17 | ``` 18 | 19 | 20 | 21 | ### 4. How is your client configured? 22 | 23 | 24 | ### 5. How did you run Caddy? (give the full command and describe the execution environment). If multiple servers are used (for example with `upstream`), describe those as well. 25 | 26 | 27 | ### 6. Please paste any relevant HTTP request(s) here. 28 | 29 | 30 | 31 | 32 | ### 7. What did you expect to see? 33 | 34 | 35 | ### 8. What did you see instead (give full error messages and/or log)? 36 | 37 | 38 | ### 9. How can someone who is starting from scratch reproduce the bug as minimally as possible? 39 | 40 | -------------------------------------------------------------------------------- /acl.go: -------------------------------------------------------------------------------- 1 | package forwardproxy 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "strings" 7 | ) 8 | 9 | // ACLRule describes an ACL rule. 10 | type ACLRule struct { 11 | Subjects []string `json:"subjects,omitempty"` 12 | Allow bool `json:"allow,omitempty"` 13 | } 14 | 15 | type aclDecision uint8 16 | 17 | const ( 18 | aclDecisionAllow = iota 19 | aclDecisionDeny 20 | aclDecisionNoMatch 21 | ) 22 | 23 | type aclRule interface { 24 | tryMatch(ip net.IP, domain string) aclDecision 25 | } 26 | 27 | type aclIPRule struct { 28 | net net.IPNet 29 | allow bool 30 | } 31 | 32 | func (a *aclIPRule) tryMatch(ip net.IP, domain string) aclDecision { 33 | if !a.net.Contains(ip) { 34 | return aclDecisionNoMatch 35 | } 36 | if a.allow { 37 | return aclDecisionAllow 38 | } 39 | return aclDecisionDeny 40 | 41 | } 42 | 43 | type aclDomainRule struct { 44 | domain string 45 | subdomainsAllowed bool 46 | allow bool 47 | } 48 | 49 | func (a *aclDomainRule) tryMatch(ip net.IP, domain string) aclDecision { 50 | if strings.HasSuffix(domain, ".") { 51 | domain = domain[:len(domain)-1] 52 | } 53 | if domain == a.domain || 54 | a.subdomainsAllowed && strings.HasSuffix(domain, "."+a.domain) { 55 | if a.allow { 56 | return aclDecisionAllow 57 | } 58 | return aclDecisionDeny 59 | } 60 | return aclDecisionNoMatch 61 | } 62 | 63 | type aclAllRule struct { 64 | allow bool 65 | } 66 | 67 | func (a *aclAllRule) tryMatch(ip net.IP, domain string) aclDecision { 68 | if a.allow { 69 | return aclDecisionAllow 70 | } 71 | return aclDecisionDeny 72 | } 73 | 74 | func newACLRule(ruleSubject string, allow bool) (aclRule, error) { 75 | if ruleSubject == "all" { 76 | return &aclAllRule{allow: allow}, nil 77 | } 78 | _, ipNet, err := net.ParseCIDR(ruleSubject) 79 | if err != nil { 80 | ip := net.ParseIP(ruleSubject) 81 | // support specifying just an IP 82 | if ip.To4() != nil { 83 | _, ipNet, err = net.ParseCIDR(ruleSubject + "/32") 84 | } else if ip.To16() != nil { 85 | _, ipNet, err = net.ParseCIDR(ruleSubject + "/128") 86 | } 87 | } 88 | if err == nil { 89 | return &aclIPRule{net: *ipNet, allow: allow}, nil 90 | } 91 | 92 | subdomainsAllowed := false 93 | if strings.HasPrefix(ruleSubject, `*.`) { 94 | subdomainsAllowed = true 95 | ruleSubject = ruleSubject[2:] 96 | } 97 | err = isValidDomainLite(ruleSubject) 98 | if err != nil { 99 | return nil, errors.New(ruleSubject + " could not be parsed as either IP, IP network, or domain: " + err.Error()) 100 | } 101 | return &aclDomainRule{domain: ruleSubject, subdomainsAllowed: subdomainsAllowed, allow: allow}, nil 102 | } 103 | 104 | // isValidDomainLite shamelessly rejects non-LDH names. returns nil if domains seems valid 105 | func isValidDomainLite(domain string) error { 106 | for i := 0; i < len(domain); i++ { 107 | c := domain[i] 108 | if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_' || '0' <= c && c <= '9' || 109 | c == '-' || c == '.' { 110 | continue 111 | } 112 | return errors.New("character " + string(c) + " is not allowed") 113 | } 114 | sections := strings.Split(domain, ".") 115 | for _, s := range sections { 116 | if len(s) == 0 { 117 | return errors.New("empty section between dots in domain name or trailing dot") 118 | } 119 | if len(s) > 63 { 120 | return errors.New("domain name section is too long") 121 | } 122 | } 123 | return nil 124 | } 125 | -------------------------------------------------------------------------------- /httpclient_test.go: -------------------------------------------------------------------------------- 1 | // tests ./httpclient/ but is in root as it needs access to test files in root 2 | package forwardproxy 3 | 4 | import ( 5 | "crypto/tls" 6 | "fmt" 7 | "net" 8 | "sync" 9 | "testing" 10 | "time" 11 | 12 | "github.com/caddyserver/forwardproxy/httpclient" 13 | ) 14 | 15 | func TestHttpClient(t *testing.T) { 16 | _test := func(urlSchemeAndCreds, urlAddress string) { 17 | for _, httpProxyVer := range testHTTPProxyVersions { 18 | for _, httpTargetVer := range testHTTPTargetVersions { 19 | for _, resource := range testResources { 20 | // always dial localhost for testing purposes 21 | proxyURL := fmt.Sprintf("%s@%s", urlSchemeAndCreds, urlAddress) 22 | 23 | dialer, err := httpclient.NewHTTPConnectDialer(proxyURL) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | dialer.DialTLS = func(network string, address string) (net.Conn, string, error) { 28 | // always dial localhost for testing purposes 29 | conn, err := tls.Dial(network, address, &tls.Config{ 30 | InsecureSkipVerify: true, 31 | NextProtos: []string{httpVersionToALPN[httpProxyVer]}, 32 | }) 33 | if err != nil { 34 | return nil, "", err 35 | } 36 | return conn, conn.ConnectionState().NegotiatedProtocol, nil 37 | } 38 | 39 | // always dial localhost for testing purposes 40 | conn, err := dialer.Dial("tcp", caddyTestTarget.addr) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | response, err := getResourceViaProxyConn(conn, caddyTestTarget.addr, resource, httpTargetVer, credentialsCorrect) 45 | if err != nil { 46 | t.Fatal(httpProxyVer, httpTargetVer, err) 47 | } else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil { 48 | t.Fatal(httpProxyVer, httpTargetVer, err) 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | _test("https://"+credentialsCorrectPlain, caddyForwardProxyAuth.addr) 56 | _test("http://"+credentialsCorrectPlain, caddyHTTPForwardProxyAuth.addr) 57 | } 58 | 59 | func TestHttpClientH2Multiplexing(t *testing.T) { 60 | // doesn't actually confirm that it is multiplexed, just that it doesn't break things 61 | // but it was manually inspected in Wireshark when this code was committed 62 | httpProxyVer := "HTTP/2.0" 63 | httpTargetVer := "HTTP/1.1" 64 | 65 | dialer, err := httpclient.NewHTTPConnectDialer("https://" + credentialsCorrectPlain + "@" + caddyForwardProxyAuth.addr) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | dialer.DialTLS = func(network string, address string) (net.Conn, string, error) { 70 | // always dial localhost for testing purposes 71 | conn, err := tls.Dial(network, address, &tls.Config{ 72 | InsecureSkipVerify: true, 73 | NextProtos: []string{httpVersionToALPN[httpProxyVer]}, 74 | }) 75 | if err != nil { 76 | return nil, "", err 77 | } 78 | return conn, conn.ConnectionState().NegotiatedProtocol, nil 79 | } 80 | 81 | retries := 20 82 | sleepInterval := time.Millisecond * 100 83 | 84 | var wg sync.WaitGroup 85 | wg.Add(retries + 1) // + for one serial launch 86 | _test := func() { 87 | defer wg.Done() 88 | for _, resource := range testResources { 89 | // always dial localhost for testing purposes 90 | conn, err := dialer.Dial("tcp", caddyTestTarget.addr) 91 | if err != nil { 92 | t.Fatal(err) 93 | } 94 | response, err := getResourceViaProxyConn(conn, caddyTestTarget.addr, resource, httpTargetVer, credentialsCorrect) 95 | if err != nil { 96 | t.Fatal(httpProxyVer, httpTargetVer, err) 97 | } else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil { 98 | t.Fatal(httpProxyVer, httpTargetVer, err) 99 | } 100 | } 101 | } 102 | 103 | _test() // do serially at least once 104 | 105 | for i := 0; i < retries; i++ { 106 | go _test() 107 | time.Sleep(sleepInterval) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /caddyfile.go: -------------------------------------------------------------------------------- 1 | package forwardproxy 2 | 3 | import ( 4 | "log" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/caddyserver/caddy/v2" 9 | "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" 10 | "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" 11 | "github.com/caddyserver/caddy/v2/modules/caddyhttp" 12 | ) 13 | 14 | func init() { 15 | httpcaddyfile.RegisterHandlerDirective("forward_proxy", parseCaddyfile) 16 | } 17 | 18 | func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { 19 | var fp Handler 20 | err := fp.UnmarshalCaddyfile(h.Dispenser) 21 | return &fp, err 22 | } 23 | 24 | // UnmarshalCaddyfile unmarshals Caddyfile tokens into h. 25 | func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 26 | if !d.Next() { 27 | return d.ArgErr() 28 | } 29 | args := d.RemainingArgs() 30 | if len(args) > 0 { 31 | return d.ArgErr() 32 | } 33 | for nesting := d.Nesting(); d.NextBlock(nesting); { 34 | subdirective := d.Val() 35 | args := d.RemainingArgs() 36 | switch subdirective { 37 | case "basic_auth": 38 | if len(args) != 2 { 39 | return d.ArgErr() 40 | } 41 | if len(args[0]) == 0 { 42 | return d.Err("empty usernames are not allowed") 43 | } 44 | // TODO: Evaluate policy of allowing empty passwords. 45 | if strings.Contains(args[0], ":") { 46 | return d.Err("character ':' in usernames is not allowed") 47 | } 48 | // TODO: Support multiple basicauths. 49 | // TODO: Actually, just try to use Caddy 2's existing basicauth module. 50 | if h.BasicauthUser != "" || h.BasicauthPass != "" { 51 | return d.Err("Multi-user basicauth is not supported") 52 | } 53 | h.BasicauthUser = args[0] 54 | h.BasicauthPass = args[1] 55 | case "hosts": 56 | if len(args) == 0 { 57 | return d.ArgErr() 58 | } 59 | if len(h.Hosts) != 0 { 60 | return d.Err("hosts subdirective specified twice") 61 | } 62 | h.Hosts = caddyhttp.MatchHost(args) 63 | case "ports": 64 | if len(args) == 0 { 65 | return d.ArgErr() 66 | } 67 | if len(h.AllowedPorts) != 0 { 68 | return d.Err("ports subdirective specified twice") 69 | } 70 | h.AllowedPorts = make([]int, len(args)) 71 | for i, p := range args { 72 | intPort, err := strconv.Atoi(p) 73 | if intPort <= 0 || intPort > 65535 || err != nil { 74 | return d.Errf("ports are expected to be space-separated and in 0-65535 range, but got: %s", p) 75 | } 76 | h.AllowedPorts[i] = intPort 77 | } 78 | case "hide_ip": 79 | if len(args) != 0 { 80 | return d.ArgErr() 81 | } 82 | h.HideIP = true 83 | case "hide_via": 84 | if len(args) != 0 { 85 | return d.ArgErr() 86 | } 87 | h.HideVia = true 88 | case "probe_resistance": 89 | if len(args) > 1 { 90 | return d.ArgErr() 91 | } 92 | if len(args) == 1 { 93 | lowercaseArg := strings.ToLower(args[0]) 94 | if lowercaseArg != args[0] { 95 | log.Println("[WARNING] Secret domain appears to have uppercase letters in it, which are not visitable") 96 | } 97 | h.ProbeResistance = &ProbeResistance{Domain: args[0]} 98 | } else { 99 | h.ProbeResistance = &ProbeResistance{} 100 | } 101 | case "serve_pac": 102 | if len(args) > 1 { 103 | return d.ArgErr() 104 | } 105 | if len(h.PACPath) != 0 { 106 | return d.Err("serve_pac subdirective specified twice") 107 | } 108 | if len(args) == 1 { 109 | h.PACPath = args[0] 110 | if !strings.HasPrefix(h.PACPath, "/") { 111 | h.PACPath = "/" + h.PACPath 112 | } 113 | } else { 114 | h.PACPath = "/proxy.pac" 115 | } 116 | case "dial_timeout": 117 | if len(args) != 1 { 118 | return d.ArgErr() 119 | } 120 | timeout, err := caddy.ParseDuration(args[0]) 121 | if err != nil { 122 | return d.ArgErr() 123 | } 124 | if timeout < 0 { 125 | return d.Err("dial_timeout cannot be negative.") 126 | } 127 | h.DialTimeout = caddy.Duration(timeout) 128 | case "upstream": 129 | if len(args) != 1 { 130 | return d.ArgErr() 131 | } 132 | if h.Upstream != "" { 133 | return d.Err("upstream directive specified more than once") 134 | } 135 | h.Upstream = args[0] 136 | case "acl": 137 | for nesting := d.Nesting(); d.NextBlock(nesting); { 138 | aclDirective := d.Val() 139 | args := d.RemainingArgs() 140 | if len(args) == 0 { 141 | return d.ArgErr() 142 | } 143 | var ruleSubjects []string 144 | var err error 145 | aclAllow := false 146 | switch aclDirective { 147 | case "allow": 148 | ruleSubjects = args[:] 149 | aclAllow = true 150 | case "allow_file": 151 | if len(args) != 1 { 152 | return d.Err("allowfile accepts a single filename argument") 153 | } 154 | ruleSubjects, err = readLinesFromFile(args[0]) 155 | if err != nil { 156 | return err 157 | } 158 | aclAllow = true 159 | case "deny": 160 | ruleSubjects = args[:] 161 | case "deny_file": 162 | if len(args) != 1 { 163 | return d.Err("denyfile accepts a single filename argument") 164 | } 165 | ruleSubjects, err = readLinesFromFile(args[0]) 166 | if err != nil { 167 | return err 168 | } 169 | default: 170 | return d.Err("expected acl directive: allow/allowfile/deny/denyfile." + 171 | "got: " + aclDirective) 172 | } 173 | ar := ACLRule{Subjects: ruleSubjects, Allow: aclAllow} 174 | h.ACL = append(h.ACL, ar) 175 | } 176 | default: 177 | return d.ArgErr() 178 | } 179 | } 180 | return nil 181 | } 182 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to forwardproxy 2 | ===================== 3 | 4 | Welcome! Thank you for choosing to be a part of our community. Caddy wouldn't be great without your involvement! 5 | 6 | For starters, we invite you to join [the Caddy forum](https://caddy.community) where you can hang out with other Caddy users and developers. 7 | 8 | ### Contributing code 9 | 10 | We hold contributions to a high standard for quality :bowtie:, so don't be surprised if we ask for revisions—even if it seems small or insignificant. Please don't take it personally. :wink: If your change is on the right track, we can guide you to make it mergable. 11 | 12 | Here are some of the expectations we have of contributors: 13 | 14 | - If your change is more than just a minor alteration, **open an issue to propose your change first.** This way we can avoid confusion, coordinate what everyone is working on, and ensure that changes are in-line with the project's goals and the best interests of its users. If there's already an issue about it, comment on the existing issue to claim it. 15 | 16 | - **Keep pull requests small.** Smaller PRs are more likely to be merged because they are easier to review! We might ask you to break up large PRs into smaller ones. [An example of what we DON'T do.](https://twitter.com/iamdevloper/status/397664295875805184) 17 | 18 | - **Keep related commits together in a PR.** We do want pull requests to be small, but you should also keep multiple related commits in the same PR if they rely on each other. 19 | 20 | - **Write tests.** Tests are essential! Written properly, they ensure your change works, and that other changes in the future won't break your change. CI checks should pass. 21 | 22 | - **Be extra careful.** Forwardproxy aims to help users in distress, and it is our duty to review changes extra meticulously. 23 | 24 | - **Recommended reading** 25 | - [CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments) for an idea of what we look for in good, clean Go code 26 | - [Linus Torvalds describes a good commit message](https://gist.github.com/matthewhudson/1475276) 27 | - [Best Practices for Maintainers](https://opensource.guide/best-practices/) 28 | - [Shrinking Code Review](https://alexgaynor.net/2015/dec/29/shrinking-code-review/) 29 | 30 | 31 | 32 | - **[Squash](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) insignificant commits.** Every commit should be significant. Commits which merely rewrite a comment or fix a typo can be combined into another commit that has more substance. Interactive rebase can do this, or a simpler way is `git reset --soft ` then `git commit -s`. 33 | 34 | ### Getting help using Caddy 35 | 36 | If you have a question about using Caddy, [ask on our forum](https://caddy.community)! There will be more people there who can help you than just the Caddy developers who follow our issue tracker. Issues are not the place for usage questions. 37 | 38 | Many people on the forums could benefit from your experience and expertise, too. Once you've been helped, consider giving back by answering other people's questions and participating in other discussions. 39 | 40 | 41 | ### Reporting bugs 42 | 43 | Please follow the issue template so we have all the needed information. Unredacted—yes, actual values matter. We need to be able to repeat the bug using your instructions. Please simplify the issue as much as possible. The burden is on you to convince us that it is actually a bug in forwardproxy. This is easiest to do when you write clear, concise instructions so we can reproduce the behavior (even if it seems obvious). The more detailed and specific you are, the faster we will be able to help you! 44 | 45 | We suggest reading [How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html). 46 | 47 | Please be kind. :smile: Remember that Caddy comes at no cost to you, and you're getting free support when we fix your issues. If we helped you, please consider helping someone else! 48 | 49 | 50 | ### Suggesting features 51 | 52 | First, [search to see if your feature has already been requested](https://github.com/caddyserver/forwardproxy/issues). If it has, you can add a :+1: reaction to vote for it. If your feature idea is new, open an issue to request the feature. You don't have to follow the bug template for feature requests. Please describe your idea thoroughly so that we know how to implement it! Really vague requests may not be helpful or actionable and without clarification will have to be closed. 53 | 54 | While we really do value your requests and implement many of them, not all features are a good fit for Caddy or forwardproxy. If a feature is not in the best interest of the Caddy project or its users in general, we may politely decline to implement it. 55 | 56 | ## Values 57 | 58 | - A person is always more important than code. People don't like being handled "efficiently". But we can still process issues and pull requests efficiently while being kind, patient, and considerate. 59 | 60 | - The ends justify the means, if the means are good. A good tree won't produce bad fruit. But if we cut corners or are hasty in our process, the end result will not be good. 61 | 62 | 63 | ## Responsible Disclosure 64 | 65 | If you've found a security vulnerability, please email me, forwardproxy author, directly: Sergey dot Frolov at colorado.edu. I'll need enough information to verify the bug and make a patch. It will speed things up if you suggest a working patch. If your report is valid and a patch is released, we will not reveal your identity by default. If you wish to be credited, please give me the name to use. Thanks for responsibly helping forwardproxy users! 66 | 67 | 68 | ## Thank you 69 | 70 | Thanks for your help! Caddy would not be what it is today without your contributions. 71 | -------------------------------------------------------------------------------- /acl_test.go: -------------------------------------------------------------------------------- 1 | package forwardproxy 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | /* 9 | test port blocking working 10 | test blacklist allowed 11 | test blacklist refused with correct status 12 | */ 13 | 14 | func TestWhitelistAllowing(t *testing.T) { 15 | const useTLS = true 16 | for _, httpProxyVer := range testHTTPProxyVersions { 17 | for _, resource := range testResources { 18 | response, err := getViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyWhiteListing.addr, httpProxyVer, 19 | "", useTLS) 20 | if err != nil { 21 | t.Fatal(err) 22 | } else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil { 23 | t.Fatal(err) 24 | } 25 | } 26 | } 27 | } 28 | 29 | func TestWhitelistBlocking(t *testing.T) { 30 | const useTLS = true 31 | for _, httpProxyVer := range testHTTPProxyVersions { 32 | for _, resource := range testResources { 33 | response, err := getViaProxy(caddyHTTPTestTarget.addr, resource, caddyForwardProxyWhiteListing.addr, httpProxyVer, 34 | "", useTLS) 35 | if err != nil { 36 | t.Fatal(err) 37 | } else if response.StatusCode != http.StatusForbidden { 38 | t.Fatal("Expected response \"403 Forbidden\", got:", response.StatusCode) 39 | } 40 | } 41 | } 42 | 43 | for _, httpProxyVer := range testHTTPProxyVersions { 44 | for _, resource := range testResources { 45 | response, err := getViaProxy("google.com:6451", resource, caddyForwardProxyWhiteListing.addr, httpProxyVer, 46 | "", useTLS) 47 | if err != nil { 48 | t.Fatal(err) 49 | } else if response.StatusCode != http.StatusForbidden { 50 | t.Fatal("Expected response \"403 Forbidden\", got:", response.StatusCode) 51 | } 52 | } 53 | } 54 | } 55 | 56 | func TestLocalhostDefaultForbidden(t *testing.T) { 57 | const useTLS = true 58 | for _, httpProxyVer := range testHTTPProxyVersions { 59 | for _, resource := range testResources { 60 | response, err := getViaProxy("localhost:6451", resource, caddyForwardProxyNoBlacklistOverride.addr, httpProxyVer, 61 | "", useTLS) 62 | if err != nil { 63 | t.Fatal(err) 64 | } else if response.StatusCode != http.StatusForbidden { 65 | t.Fatal("Expected response \"403 Forbidden\", got:", response.StatusCode) 66 | } 67 | } 68 | } 69 | 70 | for _, httpProxyVer := range testHTTPProxyVersions { 71 | for _, resource := range testResources { 72 | response, err := getViaProxy("127.0.0.1:808", resource, caddyForwardProxyNoBlacklistOverride.addr, httpProxyVer, 73 | "", useTLS) 74 | if err != nil { 75 | t.Fatal(err) 76 | } else if response.StatusCode != http.StatusForbidden { 77 | t.Fatal("Expected response \"403 Forbidden\", got:", response.StatusCode) 78 | } 79 | } 80 | } 81 | 82 | for _, httpProxyVer := range testHTTPProxyVersions { 83 | for _, resource := range testResources { 84 | response, err := getViaProxy("[::1]:8080", resource, caddyForwardProxyNoBlacklistOverride.addr, httpProxyVer, 85 | "", useTLS) 86 | if err != nil { 87 | t.Fatal(err) 88 | } else if response.StatusCode != http.StatusForbidden { 89 | t.Fatal("Expected response \"403 Forbidden\", got:", response.StatusCode) 90 | } 91 | } 92 | } 93 | } 94 | 95 | func TestLocalNetworksDefaultForbidden(t *testing.T) { 96 | const useTLS = true 97 | for _, httpProxyVer := range testHTTPProxyVersions { 98 | for _, resource := range testResources { 99 | response, err := getViaProxy("10.0.0.0:80", resource, caddyForwardProxyNoBlacklistOverride.addr, httpProxyVer, 100 | "", useTLS) 101 | if err != nil { 102 | t.Fatal(err) 103 | } else if response.StatusCode != http.StatusForbidden { 104 | t.Fatal("Expected response \"403 Forbidden\", got:", response.StatusCode) 105 | } 106 | } 107 | } 108 | 109 | for _, httpProxyVer := range testHTTPProxyVersions { 110 | for _, resource := range testResources { 111 | response, err := getViaProxy("127.222.34.1:443", resource, caddyForwardProxyNoBlacklistOverride.addr, httpProxyVer, 112 | "", useTLS) 113 | if err != nil { 114 | t.Fatal(err) 115 | } else if response.StatusCode != http.StatusForbidden { 116 | t.Fatal("Expected response \"403 Forbidden\", got:", response.StatusCode) 117 | } 118 | } 119 | } 120 | 121 | for _, httpProxyVer := range testHTTPProxyVersions { 122 | for _, resource := range testResources { 123 | response, err := getViaProxy("172.16.0.1:8080", resource, caddyForwardProxyNoBlacklistOverride.addr, httpProxyVer, 124 | "", useTLS) 125 | if err != nil { 126 | t.Fatal(err) 127 | } else if response.StatusCode != http.StatusForbidden { 128 | t.Fatal("Expected response \"403 Forbidden\", got:", response.StatusCode) 129 | } 130 | } 131 | } 132 | 133 | for _, httpProxyVer := range testHTTPProxyVersions { 134 | for _, resource := range testResources { 135 | response, err := getViaProxy("192.168.192.168:888", resource, caddyForwardProxyNoBlacklistOverride.addr, httpProxyVer, 136 | "", useTLS) 137 | if err != nil { 138 | t.Fatal(err) 139 | } else if response.StatusCode != http.StatusForbidden { 140 | t.Fatal("Expected response \"403 Forbidden\", got:", response.StatusCode) 141 | } 142 | } 143 | } 144 | } 145 | 146 | func TestBlacklistBlocking(t *testing.T) { 147 | const useTLS = true 148 | for _, httpProxyVer := range testHTTPProxyVersions { 149 | for _, resource := range testResources { 150 | response, err := getViaProxy(blacklistedDomain, resource, caddyForwardProxyBlackListing.addr, httpProxyVer, 151 | "", useTLS) 152 | if err != nil { 153 | t.Fatal(err) 154 | } else if response.StatusCode != http.StatusForbidden { 155 | t.Fatal("Expected response \"403 Forbidden\", got:", response.StatusCode) 156 | } 157 | } 158 | } 159 | 160 | for _, httpProxyVer := range testHTTPProxyVersions { 161 | for _, resource := range testResources { 162 | response, err := getViaProxy(blacklistedIPv4, resource, caddyForwardProxyBlackListing.addr, httpProxyVer, 163 | "", useTLS) 164 | if err != nil { 165 | t.Fatal(err) 166 | } else if response.StatusCode != http.StatusForbidden { 167 | t.Fatal("Expected response \"403 Forbidden\", got:", response.StatusCode) 168 | } 169 | } 170 | } 171 | 172 | for _, httpProxyVer := range testHTTPProxyVersions { 173 | for _, resource := range testResources { 174 | response, err := getViaProxy("["+blacklistedIPv6+"]:80", resource, caddyForwardProxyBlackListing.addr, httpProxyVer, 175 | "", useTLS) 176 | if err != nil { 177 | t.Fatal(err) 178 | } else if response.StatusCode != http.StatusForbidden { 179 | t.Fatal("Expected response \"403 Forbidden\", got:", response.StatusCode) 180 | } 181 | } 182 | } 183 | } 184 | 185 | func TestBlacklistAllowing(t *testing.T) { 186 | const useTLS = true 187 | for _, httpProxyVer := range testHTTPProxyVersions { 188 | for _, resource := range testResources { 189 | response, err := getViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyBlackListing.addr, httpProxyVer, 190 | "", useTLS) 191 | if err != nil { 192 | t.Fatal(err) 193 | } else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil { 194 | t.Fatal(err) 195 | } 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /httpclient/httpclient.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package httpclient is used by the upstreaming forwardproxy to establish connections to http(s) upstreams. 16 | // it implements x/net/proxy.Dialer interface 17 | package httpclient 18 | 19 | import ( 20 | "bufio" 21 | "context" 22 | "crypto/tls" 23 | "encoding/base64" 24 | "errors" 25 | "io" 26 | "net" 27 | "net/http" 28 | "net/url" 29 | "sync" 30 | 31 | "golang.org/x/net/http2" 32 | ) 33 | 34 | // HTTPConnectDialer allows to configure one-time use HTTP CONNECT client 35 | type HTTPConnectDialer struct { 36 | ProxyURL url.URL 37 | DefaultHeader http.Header 38 | 39 | // TODO: If spkiFp is set, use it as SPKI fingerprint to confirm identity of the 40 | // proxy, instead of relying on standard PKI CA roots 41 | SpkiFP []byte 42 | 43 | Dialer net.Dialer // overridden dialer allow to control establishment of TCP connection 44 | 45 | // overridden DialTLS allows user to control establishment of TLS connection 46 | // MUST return connection with completed Handshake, and NegotiatedProtocol 47 | DialTLS func(network string, address string) (net.Conn, string, error) 48 | 49 | EnableH2ConnReuse bool 50 | cacheH2Mu sync.Mutex 51 | cachedH2ClientConn *http2.ClientConn 52 | cachedH2RawConn net.Conn 53 | } 54 | 55 | // NewHTTPConnectDialer creates a client to issue CONNECT requests and tunnel traffic via HTTPS proxy. 56 | // proxyURLStr must provide Scheme and Host, may provide credentials and port. 57 | // Example: https://username:password@golang.org:443 58 | func NewHTTPConnectDialer(proxyURLStr string) (*HTTPConnectDialer, error) { 59 | proxyURL, err := url.Parse(proxyURLStr) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | if proxyURL.Host == "" { 65 | return nil, errors.New("misparsed `url=" + proxyURLStr + 66 | "`, make sure to specify full url like https://username:password@hostname.com:443/") 67 | } 68 | 69 | switch proxyURL.Scheme { 70 | case "http": 71 | if proxyURL.Port() == "" { 72 | proxyURL.Host = net.JoinHostPort(proxyURL.Host, "80") 73 | } 74 | case "https": 75 | if proxyURL.Port() == "" { 76 | proxyURL.Host = net.JoinHostPort(proxyURL.Host, "443") 77 | } 78 | case "": 79 | return nil, errors.New("specify scheme explicitly (https://)") 80 | default: 81 | return nil, errors.New("scheme " + proxyURL.Scheme + " is not supported") 82 | } 83 | 84 | client := &HTTPConnectDialer{ 85 | ProxyURL: *proxyURL, 86 | DefaultHeader: make(http.Header), 87 | SpkiFP: nil, 88 | EnableH2ConnReuse: true, 89 | } 90 | 91 | if proxyURL.User != nil { 92 | if proxyURL.User.Username() != "" { 93 | password, _ := proxyURL.User.Password() 94 | client.DefaultHeader.Set("Proxy-Authorization", "Basic "+ 95 | base64.StdEncoding.EncodeToString([]byte(proxyURL.User.Username()+":"+password))) 96 | } 97 | } 98 | return client, nil 99 | } 100 | 101 | func (c *HTTPConnectDialer) Dial(network, address string) (net.Conn, error) { 102 | return c.DialContext(context.Background(), network, address) 103 | } 104 | 105 | // Users of context.WithValue should define their own types for keys 106 | type ContextKeyHeader struct{} 107 | 108 | // ctx.Value will be inspected for optional ContextKeyHeader{} key, with `http.Header` value, 109 | // which will be added to outgoing request headers, overriding any colliding c.DefaultHeader 110 | func (c *HTTPConnectDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { 111 | req := (&http.Request{ 112 | Method: "CONNECT", 113 | URL: &url.URL{Host: address}, 114 | Header: make(http.Header), 115 | Host: address, 116 | }).WithContext(ctx) 117 | for k, v := range c.DefaultHeader { 118 | req.Header[k] = v 119 | } 120 | if ctxHeader, ctxHasHeader := ctx.Value(ContextKeyHeader{}).(http.Header); ctxHasHeader { 121 | for k, v := range ctxHeader { 122 | req.Header[k] = v 123 | } 124 | } 125 | 126 | connectHttp2 := func(rawConn net.Conn, h2clientConn *http2.ClientConn) (net.Conn, error) { 127 | req.Proto = "HTTP/2.0" 128 | req.ProtoMajor = 2 129 | req.ProtoMinor = 0 130 | pr, pw := io.Pipe() 131 | req.Body = pr 132 | 133 | resp, err := h2clientConn.RoundTrip(req) 134 | if err != nil { 135 | rawConn.Close() 136 | return nil, err 137 | } 138 | 139 | if resp.StatusCode != http.StatusOK { 140 | rawConn.Close() 141 | return nil, errors.New("Proxy responded with non 200 code: " + resp.Status) 142 | } 143 | return NewHttp2Conn(rawConn, pw, resp.Body), nil 144 | } 145 | 146 | connectHttp1 := func(rawConn net.Conn) (net.Conn, error) { 147 | req.Proto = "HTTP/1.1" 148 | req.ProtoMajor = 1 149 | req.ProtoMinor = 1 150 | 151 | err := req.Write(rawConn) 152 | if err != nil { 153 | rawConn.Close() 154 | return nil, err 155 | } 156 | 157 | resp, err := http.ReadResponse(bufio.NewReader(rawConn), req) 158 | if err != nil { 159 | rawConn.Close() 160 | return nil, err 161 | } 162 | 163 | if resp.StatusCode != http.StatusOK { 164 | rawConn.Close() 165 | return nil, errors.New("Proxy responded with non 200 code: " + resp.Status) 166 | } 167 | return rawConn, nil 168 | } 169 | 170 | if c.EnableH2ConnReuse { 171 | c.cacheH2Mu.Lock() 172 | unlocked := false 173 | if c.cachedH2ClientConn != nil && c.cachedH2RawConn != nil { 174 | if c.cachedH2ClientConn.CanTakeNewRequest() { 175 | rc := c.cachedH2RawConn 176 | cc := c.cachedH2ClientConn 177 | c.cacheH2Mu.Unlock() 178 | unlocked = true 179 | proxyConn, err := connectHttp2(rc, cc) 180 | if err == nil { 181 | return proxyConn, err 182 | } 183 | // else: carry on and try again 184 | } 185 | } 186 | if !unlocked { 187 | c.cacheH2Mu.Unlock() 188 | } 189 | } 190 | 191 | var err error 192 | var rawConn net.Conn 193 | negotiatedProtocol := "" 194 | switch c.ProxyURL.Scheme { 195 | case "http": 196 | rawConn, err = c.Dialer.DialContext(ctx, network, c.ProxyURL.Host) 197 | if err != nil { 198 | return nil, err 199 | } 200 | case "https": 201 | if c.DialTLS != nil { 202 | rawConn, negotiatedProtocol, err = c.DialTLS(network, c.ProxyURL.Host) 203 | if err != nil { 204 | return nil, err 205 | } 206 | } else { 207 | tlsConf := tls.Config{ 208 | NextProtos: []string{"h2", "http/1.1"}, 209 | ServerName: c.ProxyURL.Hostname(), 210 | } 211 | tlsConn, err := tls.Dial(network, c.ProxyURL.Host, &tlsConf) 212 | if err != nil { 213 | return nil, err 214 | } 215 | err = tlsConn.Handshake() 216 | if err != nil { 217 | return nil, err 218 | } 219 | negotiatedProtocol = tlsConn.ConnectionState().NegotiatedProtocol 220 | rawConn = tlsConn 221 | } 222 | default: 223 | return nil, errors.New("scheme " + c.ProxyURL.Scheme + " is not supported") 224 | } 225 | 226 | switch negotiatedProtocol { 227 | case "": 228 | fallthrough 229 | case "http/1.1": 230 | return connectHttp1(rawConn) 231 | case "h2": 232 | t := http2.Transport{} 233 | h2clientConn, err := t.NewClientConn(rawConn) 234 | if err != nil { 235 | rawConn.Close() 236 | return nil, err 237 | } 238 | 239 | proxyConn, err := connectHttp2(rawConn, h2clientConn) 240 | if err != nil { 241 | rawConn.Close() 242 | return nil, err 243 | } 244 | if c.EnableH2ConnReuse { 245 | c.cacheH2Mu.Lock() 246 | c.cachedH2ClientConn = h2clientConn 247 | c.cachedH2RawConn = rawConn 248 | c.cacheH2Mu.Unlock() 249 | } 250 | return proxyConn, err 251 | default: 252 | rawConn.Close() 253 | return nil, errors.New("negotiated unsupported application layer protocol: " + 254 | negotiatedProtocol) 255 | } 256 | } 257 | 258 | func NewHttp2Conn(c net.Conn, pipedReqBody *io.PipeWriter, respBody io.ReadCloser) net.Conn { 259 | return &http2Conn{Conn: c, in: pipedReqBody, out: respBody} 260 | } 261 | 262 | type http2Conn struct { 263 | net.Conn 264 | in *io.PipeWriter 265 | out io.ReadCloser 266 | } 267 | 268 | func (h *http2Conn) Read(p []byte) (n int, err error) { 269 | return h.out.Read(p) 270 | } 271 | 272 | func (h *http2Conn) Write(p []byte) (n int, err error) { 273 | return h.in.Write(p) 274 | } 275 | 276 | func (h *http2Conn) Close() error { 277 | h.in.Close() 278 | return h.out.Close() 279 | } 280 | 281 | func (h *http2Conn) CloseConn() error { 282 | return h.Conn.Close() 283 | } 284 | 285 | func (h *http2Conn) CloseWrite() error { 286 | return h.in.Close() 287 | } 288 | 289 | func (h *http2Conn) CloseRead() error { 290 | return h.out.Close() 291 | } 292 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Secure forward proxy plugin for the Caddy web server 2 | 3 | This plugin enables [Caddy](https://caddyserver.com) to act as a forward proxy, with support for HTTP/2.0 and HTTP/1.1 requests. HTTP/2.0 will usually improve performance due to multiplexing. 4 | 5 | Forward proxy plugin includes common features like Access Control Lists and authentication, as well as some unique features to assist with security and privacy. Default configuration of forward proxy is compliant with existing HTTP standards, but some features force plugin to exhibit non-standard but non-breaking behavior to preserve privacy. 6 | 7 | Probing resistance—one of the signature features of this plugin—attempts to hide the fact that your webserver is also a forward proxy, helping the proxy to stay under the radar. Eventually, forwardproxy plugin implemented a simple *reverse* proxy (`upstream https://user:password@next-hop.com` in Caddyfile) just so users may take advantage of `probe_resistance` when they need a reverse proxy (for example, to build a chain of proxies). Reverse proxy implementation will stay simple, and if you need a powerful reverse proxy, look into Caddy's standard `proxy` directive. 8 | 9 | For a complete list of features and their usage, see Caddyfile syntax: 10 | 11 | ## Caddyfile Syntax (Server Configuration) 12 | 13 | The simplest way to enable the forward proxy without authentication just include the `forward_proxy` directive in your Caddyfile. However, this allows anyone to use your server as a proxy, which might not be desirable. 14 | 15 | Using the `order` directive you must give the order in which `forward_proxy` and other directives should be used. 16 | 17 | In the Caddyfile the addresses must start with `:443` for the `forward_proxy` to work for proxy requests of all origins. 18 | 19 | Simple example that uses forward_proxy as first priority and as second just shows a webpage (using `file_server` directive) to hide that this is a proxy: 20 | 21 | ``` 22 | { 23 | order forward_proxy before file_server 24 | } 25 | :443, example.com { 26 | tls acme@example.com 27 | forward_proxy { 28 | basic_auth abc def 29 | hide_ip 30 | hide_via 31 | probe_resistance 32 | } 33 | file_server { 34 | root /home/user/webpage 35 | } 36 | } 37 | ``` 38 | 39 | ### Security 40 | 41 | - **basic_auth [user] [password]** 42 | Sets basic HTTP auth credentials. This property may be repeated multiple times. Note that this is different from Caddy's built-in `basic_auth` directive. BE SURE TO CHECK THE NAME OF THE SITE THAT IS REQUESTING CREDENTIALS BEFORE YOU ENTER THEM. 43 | _Default: no authentication required._ 44 | 45 | - **probe_resistance [secretlink.tld]** 46 | Attempts to hide the fact that the site is a forward proxy. 47 | Proxy will no longer respond with "407 Proxy Authentication Required" if credentials are incorrect or absent, 48 | and will attempt to mimic a generic Caddy web server as if the forward proxy is not enabled. 49 | Probing resistance works (and makes sense) only if `basic_auth` is set up. 50 | To use your proxy with probe resistance, supply your `basic_auth` credentials to your client configuration. 51 | If your proxy client(browser, operating system, browser extension, etc) 52 | allows you to preconfigure credentials, and sends credentials preemptively, you do not need secret link. 53 | If your proxy client does not preemptively send credentials, you will have to visit your secret link in your browser to trigger the authentication. 54 | Make sure that specified domain name is visitable, does not contain uppercase characters, does not start with dot, etc. 55 | Only this address will trigger a 407 response, prompting browsers to request credentials from user and cache them for the rest of the session. 56 | _Default: no probing resistance._ 57 | 58 | ### Privacy 59 | 60 | - **hide_ip** 61 | If set, forwardproxy will not add user's IP to "Forwarded:" header. 62 | WARNING: there are other side-channels in your browser, that you might want to eliminate, such as WebRTC, see [here](https://www.ivpn.net/knowledgebase/158/My-IP-is-being-leaked-by-WebRTC-How-do-I-disable-it.html) how to disable it. 63 | _Default: no hiding; `Forwarded: for="useraddress"` will be sent out._ 64 | 65 | - **hide_via** 66 | If set, forwardproxy will not add Via header, and prevents simple way to detect proxy usage. 67 | WARNING: there are other side-channels to determine this. 68 | _Default: no hiding; Header in form of `Via: 2.0 caddy` will be sent out._ 69 | 70 | ### Access Control 71 | 72 | - **ports [integer] [integer]...** 73 | Specifies ports forwardproxy will whitelist for all requests. Other ports will be forbidden. 74 | _Default: no restrictions._ 75 | 76 | - **acl { 77 |     acl_directive 78 |     ... 79 |     acl_directive 80 | }** 81 | Specifies **order** and rules for allowed destination IP networks, IP addresses and hostnames. 82 | The hostname in each forwardproxy request will be resolved to an IP address, 83 | and caddy will check the IP address and hostname against the directives in order until a directive matches the request. 84 | acl_directive may be: 85 | - **allow [ip or subnet or hostname] [ip or subnet or hostname]...** 86 | - **allow_file /path/to/whitelist.txt** 87 | - **deny [ip or subnet or hostname] [ip or subnet or hostname]...** 88 | - **deny_file /path/to/blacklist.txt** 89 | 90 | If you don't want unmatched requests to be subject to the default policy, you could finish 91 | your acl rules with one of the following to specify action on unmatched requests: 92 | - **allow all** 93 | - **deny all** 94 | 95 | For hostname, you can specify `*.` as a prefix to match domain and subdomains. For example, 96 | `*.caddyserver.com` will match `caddyserver.com`, `subdomain.caddyserver.com`, but not `fakecaddyserver.com`. 97 | Note that hostname rules, matched early in the chain, will override later IP rules, 98 | so it is advised to put IP rules first, unless domains are highly trusted and should override the 99 | IP rules. Also note that domain-based blacklists are easily circumventable by directly specifying the IP. 100 | For `allow_file`/`deny_file` directives, syntax is the same, and each entry must be separated by newline. 101 | This policy applies to all requests except requests to the proxy's own domain and port. 102 | Whitelisting/blacklisting of ports on per-host/IP basis is not supported. 103 | _Default policy:_ 104 | acl { 105 |     deny 10.0.0.0/8 127.0.0.0/8 172.16.0.0/12 192.168.0.0/16 ::1/128 fe80::/10 106 |     allow all 107 | } 108 | _Default deny rules intend to prohibit access to localhost and local networks and may be expanded in future._ 109 | 110 | ### Timeouts 111 | 112 | - **dial_timeout [integer]** 113 | Sets timeout (in seconds) for establishing TCP connection to target website. Affects all requests. 114 | _Default: 20 seconds._ 115 | 116 | ### Other 117 | 118 | - **serve_pac [/path.pac]** 119 | Generate (in-memory) and serve a [Proxy Auto-Config](https://en.wikipedia.org/wiki/Proxy_auto-config) file on given path. If no path is provided, the PAC file will be served at `/proxy.pac`. NOTE: If you enable probe_resistance, your PAC file should also be served at a secret location; serving it at a predictable path can easily defeat probe resistance. 120 | _Default: no PAC file will be generated or served by Caddy (you still can manually create and serve proxy.pac like a regular file)._ 121 | 122 | - **upstream [`https://username:password@upstreamproxy.site:443`]** 123 | Sets upstream proxy to route all forwardproxy requests through it. 124 | This setting does not affect non-forwardproxy requests nor requests with wrong credentials. 125 | Upstream is incompatible with `acl` and `ports` subdirectives. 126 | Supported schemes to remote host: https. 127 | Supported schemes to localhost: socks5, http, https (certificate check is ignored). 128 | _Default: no upstream proxy._ 129 | 130 | ## Get forwardproxy 131 | 132 | ### Download prebuilt binary 133 | 134 | Linux 64bit binaries are at 135 | 136 | ### Build from source 137 | 138 | 0. Install Golang 1.14 or above and the `git` client 139 | 1. Checkout repository: `git checkout https://github.com/klzgrad/forwardproxy.git` 140 | 2. Change into directory: `cd forwardproxy` 141 | 3. Install caddyservers xcaddy: `go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest` 142 | 4. Build caddy with forwardproxy: `xcaddy build --with github.com/caddyserver/forwardproxy@caddy2=$PWD` 143 | 5. Result is a `caddy` executable that you can e.g. directly start with `sudo ./caddy run` (create your `Caddyfile` in the same directory) 144 | 145 | ### Run as daemon 146 | 147 | Manually install Caddy as a service on Linux with these instructions: [Systemd unit example](https://github.com/klzgrad/naiveproxy/wiki/Run-Caddy-as-a-daemon) 148 | 149 | ## Client Configuration 150 | 151 | Please be aware that client support varies widely, and there are edge cases where clients may not use the proxy when it should or could. It's up to you to be aware of these limitations. 152 | 153 | The basic configuration is simply to use your site address and port (usually for all protocols - HTTP, HTTPS, etc). You can also specify the .pac file if you enabled that. 154 | 155 | Read [this blog post](https://sfrolov.io/2017/08/secure-web-proxy-client-en) about how to configure your specific client. 156 | 157 | ## License 158 | 159 | Licensed under the [Apache License](LICENSE) 160 | 161 | ## Disclaimers 162 | 163 | USE AT YOUR OWN RISK. THIS IS DELIVERED AS-IS. By using this software, you agree and assert that authors, maintainers, and contributors of this software are not responsible or liable for any risks, costs, or problems you may encounter. Consider your threat model and be smart. If you find a flaw or bug, please submit a patch and help make things better! 164 | 165 | Initial version of this plugin was developed by Google. This is not an official Google product. 166 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /forwardproxy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package forwardproxy 16 | 17 | import ( 18 | "bufio" 19 | "crypto/tls" 20 | "fmt" 21 | "io" 22 | "io/ioutil" 23 | "net" 24 | "net/http" 25 | "net/url" 26 | "testing" 27 | "time" 28 | 29 | "github.com/caddyserver/forwardproxy/httpclient" 30 | "golang.org/x/net/http2" 31 | ) 32 | 33 | func dial(proxyAddr, httpProxyVer string, useTLS bool) (net.Conn, error) { 34 | // always dial localhost for testing purposes 35 | if useTLS { 36 | return tls.Dial("tcp", proxyAddr, &tls.Config{ 37 | InsecureSkipVerify: true, 38 | NextProtos: []string{httpVersionToALPN[httpProxyVer]}, 39 | }) 40 | } 41 | return net.Dial("tcp", proxyAddr) 42 | } 43 | 44 | func getViaProxy(targetHost, resource, proxyAddr, httpProxyVer, proxyCredentials string, useTLS bool) (*http.Response, error) { 45 | proxyConn, err := dial(proxyAddr, httpProxyVer, useTLS) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return getResourceViaProxyConn(proxyConn, targetHost, resource, httpProxyVer, proxyCredentials) 50 | } 51 | 52 | // if connect is not successful - that response is returned, otherwise the requested resource 53 | func connectAndGetViaProxy(targetHost, resource, proxyAddr, httpTargetVer, proxyCredentials, httpProxyVer string, useTLS bool) (*http.Response, error) { 54 | proxyConn, err := dial(proxyAddr, httpProxyVer, useTLS) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | req := &http.Request{Header: make(http.Header)} 60 | if len(proxyCredentials) > 0 { 61 | req.Header.Set("Proxy-Authorization", proxyCredentials) 62 | } 63 | req.Host = targetHost 64 | req.URL, err = url.Parse("https://" + req.Host + "/") // TODO: appending "/" causes file server to NOT issue redirect... 65 | if err != nil { 66 | return nil, err 67 | } 68 | req.RequestURI = req.Host 69 | req.Method = "CONNECT" 70 | req.Proto = httpProxyVer 71 | 72 | var resp *http.Response 73 | switch httpProxyVer { 74 | case "HTTP/2.0": 75 | req.ProtoMajor = 2 76 | req.ProtoMinor = 0 77 | pr, pw := io.Pipe() 78 | req.Body = ioutil.NopCloser(pr) 79 | t := http2.Transport{} 80 | clientConn, err := t.NewClientConn(proxyConn) 81 | if err != nil { 82 | return nil, err 83 | } 84 | resp, err = clientConn.RoundTrip(req) 85 | if err != nil { 86 | return resp, err 87 | } 88 | proxyConn = httpclient.NewHttp2Conn(proxyConn, pw, resp.Body) 89 | case "HTTP/1.1": 90 | req.ProtoMajor = 1 91 | req.ProtoMinor = 1 92 | req.Write(proxyConn) 93 | resp, err = http.ReadResponse(bufio.NewReader(proxyConn), req) 94 | if err != nil { 95 | return resp, err 96 | } 97 | default: 98 | panic("proxy ver: " + httpProxyVer) 99 | } 100 | 101 | if err != nil { 102 | return resp, err 103 | } 104 | if resp.StatusCode != http.StatusOK { 105 | return resp, err 106 | } 107 | 108 | return getResourceViaProxyConn(proxyConn, targetHost, resource, httpTargetVer, proxyCredentials) 109 | } 110 | 111 | func getResourceViaProxyConn(proxyConn net.Conn, targetHost, resource, httpTargetVer, proxyCredentials string) (*http.Response, error) { 112 | var err error 113 | 114 | req := &http.Request{Header: make(http.Header)} 115 | if len(proxyCredentials) > 0 { 116 | req.Header.Set("Proxy-Authorization", proxyCredentials) 117 | } 118 | req.Host = targetHost 119 | req.URL, err = url.Parse("http://" + targetHost + resource) 120 | if err != nil { 121 | return nil, err 122 | } 123 | req.RequestURI = req.Host + resource 124 | req.Method = "GET" 125 | req.Proto = httpTargetVer 126 | 127 | switch httpTargetVer { 128 | case "HTTP/2.0": 129 | req.ProtoMajor = 2 130 | req.ProtoMinor = 0 131 | t := http2.Transport{AllowHTTP: true} 132 | clientConn, err := t.NewClientConn(proxyConn) 133 | if err != nil { 134 | return nil, err 135 | } 136 | return clientConn.RoundTrip(req) 137 | case "HTTP/1.1": 138 | req.ProtoMajor = 1 139 | req.ProtoMinor = 1 140 | t := http.Transport{Dial: func(network, addr string) (net.Conn, error) { 141 | return proxyConn, nil 142 | }} 143 | return t.RoundTrip(req) 144 | default: 145 | panic("proxy ver: " + httpTargetVer) 146 | } 147 | } 148 | 149 | // If response is expected: returns nil. 150 | func responseExpected(res *http.Response, expectedResponse []byte) error { 151 | responseLen := len(expectedResponse) + 2 // 2 extra bytes is enough to detected that expectedResponse is longer 152 | response := make([]byte, responseLen) 153 | var nTotal int 154 | for { 155 | n, err := res.Body.Read(response[nTotal:]) 156 | nTotal += n 157 | if err == io.EOF { 158 | break 159 | } 160 | if err != nil { 161 | panic(err) 162 | } 163 | if nTotal == responseLen { 164 | return fmt.Errorf("nTotal == responseLen, but haven't seen io.EOF. Expected response: %s\nGot: %s", 165 | expectedResponse, response) 166 | } 167 | } 168 | response = response[:nTotal] 169 | if len(expectedResponse) != len(response) { 170 | return fmt.Errorf("expected length: %d. Got thus far: %d. Expected response: %s\nGot: %s", 171 | len(expectedResponse), len(response), expectedResponse, response) 172 | } 173 | for i := range response { 174 | if response[i] != expectedResponse[i] { 175 | return fmt.Errorf("response mismatch at character #%d. Expected response: %s\nGot: %s", 176 | i, expectedResponse, response) 177 | } 178 | } 179 | return nil 180 | } 181 | 182 | func TestPassthrough(t *testing.T) { 183 | client := &http.Client{Transport: testTransport, Timeout: 2 * time.Second} 184 | resp, err := client.Get("https://" + caddyForwardProxy.addr) 185 | if err != nil { 186 | t.Fatal(err) 187 | } else if err = responseExpected(resp, caddyForwardProxy.contents[""]); err != nil { 188 | t.Fatal(err) 189 | } 190 | 191 | resp, err = client.Get("https://" + caddyForwardProxy.addr + "/pic.png") 192 | if err != nil { 193 | t.Fatal(err) 194 | } else if err = responseExpected(resp, caddyForwardProxy.contents["/pic.png"]); err != nil { 195 | t.Fatal(err) 196 | } 197 | 198 | resp, err = client.Get("https://" + caddyForwardProxy.addr + "/idontexist") 199 | if err != nil { 200 | t.Fatal(err) 201 | } else if resp.StatusCode != http.StatusNotFound { 202 | t.Fatalf("Expected: 404 StatusNotFound, got %d. Response: %#v\n", resp.StatusCode, resp) 203 | } 204 | } 205 | 206 | func TestGETNoAuth(t *testing.T) { 207 | const useTLS = true 208 | for _, httpProxyVer := range testHTTPProxyVersions { 209 | for _, resource := range testResources { 210 | response, err := getViaProxy(caddyHTTPTestTarget.addr, resource, caddyForwardProxy.addr, httpProxyVer, credentialsEmpty, useTLS) 211 | if err != nil { 212 | t.Fatal(err) 213 | } else if err = responseExpected(response, caddyHTTPTestTarget.contents[resource]); err != nil { 214 | t.Fatal(err) 215 | } 216 | } 217 | } 218 | } 219 | 220 | func TestGETAuthCorrect(t *testing.T) { 221 | const useTLS = true 222 | for _, httpProxyVer := range testHTTPProxyVersions { 223 | for _, resource := range testResources { 224 | response, err := getViaProxy(caddyHTTPTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpProxyVer, credentialsCorrect, useTLS) 225 | if err != nil { 226 | t.Fatal(err) 227 | } else if err = responseExpected(response, caddyHTTPTestTarget.contents[resource]); err != nil { 228 | t.Fatal(err) 229 | } 230 | } 231 | } 232 | } 233 | 234 | func TestGETAuthWrong(t *testing.T) { 235 | const useTLS = true 236 | for _, wrongCreds := range credentialsWrong { 237 | for _, httpProxyVer := range testHTTPProxyVersions { 238 | for _, resource := range testResources { 239 | response, err := getViaProxy(caddyHTTPTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpProxyVer, wrongCreds, useTLS) 240 | if err != nil { 241 | t.Fatal(err) 242 | } 243 | if response.StatusCode != http.StatusProxyAuthRequired { 244 | t.Fatalf("Expected response: 407 StatusProxyAuthRequired, Got: %d %s\n", 245 | response.StatusCode, response.Status) 246 | } 247 | } 248 | } 249 | } 250 | } 251 | 252 | func TestProxySelfGet(t *testing.T) { 253 | const useTLS = true 254 | // GETNoAuth to self 255 | for _, httpTargetVer := range testHTTPTargetVersions { 256 | for _, resource := range testResources { 257 | response, err := getViaProxy(caddyForwardProxy.addr, resource, caddyForwardProxy.addr, httpTargetVer, credentialsEmpty, useTLS) 258 | if err != nil { 259 | t.Fatal(err) 260 | } else if err = responseExpected(response, caddyForwardProxy.contents[resource]); err != nil { 261 | t.Fatal(err) 262 | } 263 | } 264 | } 265 | 266 | // GETAuthCorrect to self 267 | for _, httpTargetVer := range testHTTPTargetVersions { 268 | for _, resource := range testResources { 269 | response, err := getViaProxy(caddyForwardProxyAuth.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, credentialsCorrect, useTLS) 270 | if err != nil { 271 | t.Fatal(err) 272 | } else if err = responseExpected(response, caddyForwardProxyAuth.contents[resource]); err != nil { 273 | t.Fatal(err) 274 | } 275 | } 276 | } 277 | } 278 | 279 | // TODO: self TestProxySelfConnect. 280 | // It requires tls-in-tls, which tests are not currently set up for. 281 | // Low priority since this is a functionality issue, not security, and it would be easily caught in the wild. 282 | 283 | func TestConnectNoAuth(t *testing.T) { 284 | const useTLS = true 285 | for _, httpProxyVer := range testHTTPProxyVersions { 286 | for _, httpTargetVer := range testHTTPTargetVersions { 287 | for _, resource := range testResources { 288 | response, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxy.addr, httpTargetVer, credentialsEmpty, httpProxyVer, useTLS) 289 | if err != nil { 290 | t.Fatal(err) 291 | } else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil { 292 | t.Fatal(err) 293 | } 294 | } 295 | } 296 | } 297 | } 298 | 299 | func TestConnectAuthCorrect(t *testing.T) { 300 | const useTLS = true 301 | for _, httpProxyVer := range testHTTPProxyVersions { 302 | for _, httpTargetVer := range testHTTPTargetVersions { 303 | for _, resource := range testResources { 304 | response, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, credentialsCorrect, httpProxyVer, useTLS) 305 | if err != nil { 306 | t.Fatal(httpProxyVer, httpTargetVer, err) 307 | } else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil { 308 | t.Fatal(httpProxyVer, httpTargetVer, err) 309 | } 310 | } 311 | } 312 | } 313 | } 314 | 315 | func TestConnectAuthWrong(t *testing.T) { 316 | const useTLS = true 317 | for _, wrongCreds := range credentialsWrong { 318 | for _, httpProxyVer := range testHTTPProxyVersions { 319 | for _, httpTargetVer := range testHTTPTargetVersions { 320 | for _, resource := range testResources { 321 | response, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, wrongCreds, httpProxyVer, useTLS) 322 | if err != nil { 323 | t.Fatal(err) 324 | } 325 | if response.StatusCode != http.StatusProxyAuthRequired { 326 | t.Fatalf("Expected response: 407 StatusProxyAuthRequired, Got: %d %s (wrongCreds=%s httpProxyVer=%s httpTargetVer=%s resource=%s)", 327 | response.StatusCode, response.Status, wrongCreds, httpProxyVer, httpTargetVer, resource) 328 | } 329 | } 330 | } 331 | } 332 | } 333 | } 334 | 335 | func TestPAC(t *testing.T) { 336 | client := &http.Client{Transport: testTransport, Timeout: 2 * time.Second} 337 | resp, err := client.Get("https://" + caddyForwardProxy.addr + "/proxy.pac") 338 | if err != nil { 339 | t.Fatal(err) 340 | } 341 | if err = responseExpected(resp, []byte(fmt.Sprintf(pacFile, caddyForwardProxy.addr))); err != nil { 342 | t.Fatal(err) 343 | } 344 | 345 | resp, err = client.Get("https://" + caddyForwardProxyProbeResist.addr + "/superhiddenfile.pac") 346 | if err != nil { 347 | t.Fatal(err) 348 | } 349 | if err = responseExpected(resp, []byte(fmt.Sprintf(pacFile, caddyForwardProxyProbeResist.addr))); err != nil { 350 | t.Fatal(err) 351 | } 352 | } 353 | 354 | func TestCONNECTViaUpstream(t *testing.T) { 355 | const useTLS = true 356 | for range make([]byte, 5) { // do several times to test http2 connection reuse 357 | for _, httpProxyVer := range testHTTPProxyVersions { 358 | for _, httpTargetVer := range testHTTPTargetVersions { 359 | for _, resource := range testResources { 360 | response, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyAuthedUpstreamEnter.addr, 361 | httpTargetVer, credentialsUpstreamCorrect, httpProxyVer, useTLS) 362 | if err != nil { 363 | t.Fatal(err) 364 | } else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil { 365 | t.Fatal(err) 366 | } 367 | } 368 | } 369 | } 370 | } 371 | } 372 | 373 | func TestGETViaUpstream(t *testing.T) { 374 | const useTLS = true 375 | for range make([]byte, 5) { // do several times to test http2 connection reuse 376 | for _, httpProxyVer := range testHTTPProxyVersions { 377 | for _, resource := range testResources { 378 | response, err := getViaProxy(caddyHTTPTestTarget.addr, resource, caddyAuthedUpstreamEnter.addr, httpProxyVer, 379 | credentialsUpstreamCorrect, useTLS) 380 | if err != nil { 381 | t.Fatal(err) 382 | } else if err = responseExpected(response, caddyHTTPTestTarget.contents[resource]); err != nil { 383 | t.Fatal(err) 384 | } 385 | } 386 | } 387 | } 388 | } 389 | 390 | func TestUpstreamPassthrough(t *testing.T) { 391 | // Usptreaming proxy still hosts things as expected 392 | client := &http.Client{Transport: testTransport, Timeout: 2 * time.Second} 393 | resp, err := client.Get("https://" + caddyAuthedUpstreamEnter.addr) 394 | if err != nil { 395 | t.Fatal(err) 396 | } else if err = responseExpected(resp, caddyAuthedUpstreamEnter.contents[""]); err != nil { 397 | t.Fatal(err) 398 | } 399 | 400 | resp, err = client.Get("https://" + caddyAuthedUpstreamEnter.addr + "/pic.png") 401 | if err != nil { 402 | t.Fatal(err) 403 | } else if err = responseExpected(resp, caddyAuthedUpstreamEnter.contents["/pic.png"]); err != nil { 404 | t.Fatal(err) 405 | } 406 | 407 | resp, err = client.Get("https://" + caddyAuthedUpstreamEnter.addr + "/idontexist") 408 | if err != nil { 409 | t.Fatal(err) 410 | } else if resp.StatusCode != http.StatusNotFound { 411 | t.Fatalf("Expected: 404 StatusNotFound, got %d. Response: %#v\n", resp.StatusCode, resp) 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /common_test.go: -------------------------------------------------------------------------------- 1 | package forwardproxy 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "encoding/hex" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "net" 12 | "net/http" 13 | "net/http/httputil" 14 | "os" 15 | "strconv" 16 | "testing" 17 | "time" 18 | 19 | "github.com/caddyserver/caddy/v2" 20 | "github.com/caddyserver/caddy/v2/caddyconfig" 21 | "github.com/caddyserver/caddy/v2/modules/caddyhttp" 22 | "github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver" 23 | "github.com/caddyserver/caddy/v2/modules/caddypki" 24 | "github.com/caddyserver/caddy/v2/modules/caddytls" 25 | ) 26 | 27 | var credentialsEmpty = "" 28 | var credentialsCorrectPlain = "test:pass" 29 | var credentialsCorrect = "Basic dGVzdDpwYXNz" // test:pass 30 | var credentialsUpstreamCorrect = "basic dXBzdHJlYW10ZXN0OnVwc3RyZWFtcGFzcw==" // upstreamtest:upstreampass 31 | var credentialsWrong = []string{ 32 | "", 33 | "\"\"", 34 | "Basic dzp3", 35 | "Basic \"\"", 36 | "Foo bar", 37 | "Tssssssss", 38 | "Basic dpz3 asp", 39 | } 40 | 41 | /* 42 | Test naming: Test{httpVer}Proxy{Method}{Auth}{Credentials}{httpVer} 43 | GET/CONNECT -- get gets, connect connects and gets 44 | Auth/NoAuth 45 | Empty/Correct/Wrong -- tries different credentials 46 | */ 47 | var testResources = []string{"/", "/pic.png"} 48 | var testHTTPProxyVersions = []string{"HTTP/2.0", "HTTP/1.1"} 49 | var testHTTPTargetVersions = []string{"HTTP/1.1"} 50 | var httpVersionToALPN = map[string]string{ 51 | "HTTP/1.1": "http/1.1", 52 | "HTTP/2.0": "h2", 53 | } 54 | 55 | var blacklistedDomain = "google-public-dns-a.google.com" // supposed to ever resolve to one of 2 IP addresses below 56 | var blacklistedIPv4 = "8.8.8.8" 57 | var blacklistedIPv6 = "2001:4860:4860::8888" 58 | 59 | type caddyTestServer struct { 60 | addr string 61 | tls bool 62 | 63 | httpRedirPort string // used in probe-resist tests to simulate default Caddy's http->https redirect 64 | 65 | root string // expected to have index.html and pic.png 66 | directives []string 67 | proxyHandler *Handler 68 | contents map[string][]byte 69 | } 70 | 71 | var ( 72 | caddyForwardProxy caddyTestServer 73 | caddyForwardProxyAuth caddyTestServer // requires auth 74 | caddyHTTPForwardProxyAuth caddyTestServer // requires auth, does not use TLS 75 | caddyForwardProxyProbeResist caddyTestServer // requires auth, and has probing resistance on 76 | caddyDummyProbeResist caddyTestServer // same as caddyForwardProxyProbeResist, but w/o forwardproxy 77 | 78 | caddyForwardProxyWhiteListing caddyTestServer 79 | caddyForwardProxyBlackListing caddyTestServer 80 | caddyForwardProxyNoBlacklistOverride caddyTestServer // to test default blacklist 81 | 82 | // authenticated server upstreams to authenticated https proxy with different credentials 83 | caddyAuthedUpstreamEnter caddyTestServer 84 | 85 | caddyTestTarget caddyTestServer // whitelisted by caddyForwardProxyWhiteListing 86 | caddyHTTPTestTarget caddyTestServer // serves plain http on 6480 87 | ) 88 | 89 | func (c *caddyTestServer) server() *caddyhttp.Server { 90 | host, port, err := net.SplitHostPort(c.addr) 91 | if err != nil { 92 | panic(err) 93 | } 94 | 95 | handlerJSON := func(h caddyhttp.MiddlewareHandler) json.RawMessage { 96 | return caddyconfig.JSONModuleObject(h, "handler", h.(caddy.Module).CaddyModule().ID.Name(), nil) 97 | } 98 | 99 | // create the routes 100 | var routes caddyhttp.RouteList 101 | if c.tls { 102 | // cheap hack for our tests to get TLS certs for the hostnames that 103 | // it needs TLS certs for: create an empty route with a single host 104 | // matcher for that hostname, and auto HTTPS will do the rest 105 | hostMatcherJSON, err := json.Marshal(caddyhttp.MatchHost{host}) 106 | if err != nil { 107 | panic(err) 108 | } 109 | matchersRaw := caddyhttp.RawMatcherSets{ 110 | caddy.ModuleMap{"host": hostMatcherJSON}, 111 | } 112 | routes = append(routes, caddyhttp.Route{MatcherSetsRaw: matchersRaw}) 113 | } 114 | if c.proxyHandler != nil { 115 | if host != "" { 116 | // tell the proxy which hostname to serve the proxy on; this must 117 | // be distinct from the host matcher, since the proxy basically 118 | // does its own host matching 119 | c.proxyHandler.Hosts = caddyhttp.MatchHost{host} 120 | } 121 | routes = append(routes, caddyhttp.Route{ 122 | HandlersRaw: []json.RawMessage{handlerJSON(c.proxyHandler)}, 123 | }) 124 | } 125 | if c.root != "" { 126 | routes = append(routes, caddyhttp.Route{ 127 | HandlersRaw: []json.RawMessage{ 128 | handlerJSON(&fileserver.FileServer{Root: c.root}), 129 | }, 130 | }) 131 | } 132 | 133 | srv := &caddyhttp.Server{ 134 | Listen: []string{":" + port}, 135 | Routes: routes, 136 | } 137 | if c.tls { 138 | srv.TLSConnPolicies = caddytls.ConnectionPolicies{{}} 139 | } else { 140 | srv.AutoHTTPS = &caddyhttp.AutoHTTPSConfig{Disabled: true} 141 | } 142 | 143 | if c.contents == nil { 144 | c.contents = make(map[string][]byte) 145 | } 146 | index, err := ioutil.ReadFile(c.root + "/index.html") 147 | if err != nil { 148 | panic(err) 149 | } 150 | c.contents[""] = index 151 | c.contents["/"] = index 152 | c.contents["/index.html"] = index 153 | c.contents["/pic.png"], err = ioutil.ReadFile(c.root + "/pic.png") 154 | if err != nil { 155 | panic(err) 156 | } 157 | 158 | return srv 159 | } 160 | 161 | // For simulating/mimicing Caddy's built-in auto-HTTPS redirects. Super hacky but w/e. 162 | func (c *caddyTestServer) redirServer() *caddyhttp.Server { 163 | return &caddyhttp.Server{ 164 | Listen: []string{":" + c.httpRedirPort}, 165 | Routes: caddyhttp.RouteList{ 166 | { 167 | Handlers: []caddyhttp.MiddlewareHandler{ 168 | caddyhttp.StaticResponse{ 169 | StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)), 170 | Headers: http.Header{ 171 | "Location": []string{"https://" + c.addr + "/{http.request.uri}"}, 172 | "Connection": []string{"close"}, 173 | }, 174 | Close: true, 175 | }, 176 | }, 177 | }, 178 | }, 179 | } 180 | } 181 | 182 | func TestMain(m *testing.M) { 183 | caddyForwardProxy = caddyTestServer{ 184 | addr: "127.0.19.84:1984", 185 | root: "./test/forwardproxy", 186 | tls: true, 187 | proxyHandler: &Handler{ 188 | PACPath: defaultPACPath, 189 | ACL: []ACLRule{{Allow: true, Subjects: []string{"all"}}}, 190 | }, 191 | } 192 | 193 | caddyForwardProxyAuth = caddyTestServer{ 194 | addr: "127.0.0.1:4891", 195 | root: "./test/forwardproxy", 196 | tls: true, 197 | proxyHandler: &Handler{ 198 | PACPath: defaultPACPath, 199 | ACL: []ACLRule{{Subjects: []string{"all"}, Allow: true}}, 200 | BasicauthUser: "test", 201 | BasicauthPass: "pass", 202 | }, 203 | } 204 | 205 | caddyHTTPForwardProxyAuth = caddyTestServer{ 206 | addr: "127.0.69.73:6973", 207 | root: "./test/forwardproxy", 208 | proxyHandler: &Handler{ 209 | PACPath: defaultPACPath, 210 | ACL: []ACLRule{{Subjects: []string{"all"}, Allow: true}}, 211 | BasicauthUser: "test", 212 | BasicauthPass: "pass", 213 | }, 214 | } 215 | 216 | caddyForwardProxyProbeResist = caddyTestServer{ 217 | addr: "127.0.88.88:8888", 218 | root: "./test/forwardproxy", 219 | tls: true, 220 | proxyHandler: &Handler{ 221 | PACPath: "/superhiddenfile.pac", 222 | ACL: []ACLRule{{Subjects: []string{"all"}, Allow: true}}, 223 | ProbeResistance: &ProbeResistance{Domain: "test.localhost"}, 224 | BasicauthUser: "test", 225 | BasicauthPass: "pass", 226 | }, 227 | httpRedirPort: "8880", 228 | } 229 | 230 | caddyDummyProbeResist = caddyTestServer{ 231 | addr: "127.0.99.99:9999", 232 | root: "./test/forwardproxy", 233 | tls: true, 234 | httpRedirPort: "9980", 235 | } 236 | 237 | caddyTestTarget = caddyTestServer{ 238 | addr: "127.0.64.51:6451", 239 | root: "./test/index", 240 | } 241 | 242 | caddyHTTPTestTarget = caddyTestServer{ 243 | addr: "localhost:6480", 244 | root: "./test/index", 245 | } 246 | 247 | caddyAuthedUpstreamEnter = caddyTestServer{ 248 | addr: "127.0.65.25:6585", 249 | root: "./test/upstreamingproxy", 250 | tls: true, 251 | proxyHandler: &Handler{ 252 | Upstream: "https://test:pass@127.0.0.1:4891", 253 | BasicauthUser: "upstreamtest", 254 | BasicauthPass: "upstreampass", 255 | }, 256 | } 257 | 258 | caddyForwardProxyWhiteListing = caddyTestServer{ 259 | addr: "127.0.87.76:8776", 260 | root: "./test/forwardproxy", 261 | tls: true, 262 | proxyHandler: &Handler{ 263 | ACL: []ACLRule{ 264 | {Subjects: []string{"127.0.64.51"}, Allow: true}, 265 | {Subjects: []string{"all"}, Allow: false}, 266 | }, 267 | AllowedPorts: []int{6451}, 268 | }, 269 | } 270 | 271 | caddyForwardProxyBlackListing = caddyTestServer{ 272 | addr: "127.0.66.76:6676", 273 | root: "./test/forwardproxy", 274 | tls: true, 275 | proxyHandler: &Handler{ 276 | ACL: []ACLRule{ 277 | {Subjects: []string{blacklistedIPv4 + "/30"}, Allow: false}, 278 | {Subjects: []string{blacklistedIPv6}, Allow: false}, 279 | {Subjects: []string{"all"}, Allow: true}, 280 | }, 281 | }, 282 | } 283 | 284 | caddyForwardProxyNoBlacklistOverride = caddyTestServer{ 285 | addr: "127.0.66.76:6679", 286 | root: "./test/forwardproxy", 287 | tls: true, 288 | proxyHandler: &Handler{}, 289 | } 290 | 291 | // done configuring all the servers; now build the HTTP app 292 | httpApp := caddyhttp.App{ 293 | HTTPPort: 1080, // use a high port to avoid permission issues 294 | Servers: map[string]*caddyhttp.Server{ 295 | "caddyForwardProxy": caddyForwardProxy.server(), 296 | "caddyForwardProxyAuth": caddyForwardProxyAuth.server(), 297 | "caddyHTTPForwardProxyAuth": caddyHTTPForwardProxyAuth.server(), 298 | "caddyForwardProxyProbeResist": caddyForwardProxyProbeResist.server(), 299 | "caddyDummyProbeResist": caddyDummyProbeResist.server(), 300 | "caddyTestTarget": caddyTestTarget.server(), 301 | "caddyHTTPTestTarget": caddyHTTPTestTarget.server(), 302 | "caddyAuthedUpstreamEnter": caddyAuthedUpstreamEnter.server(), 303 | "caddyForwardProxyWhiteListing": caddyForwardProxyWhiteListing.server(), 304 | "caddyForwardProxyBlackListing": caddyForwardProxyBlackListing.server(), 305 | "caddyForwardProxyNoBlacklistOverride": caddyForwardProxyNoBlacklistOverride.server(), 306 | 307 | // HTTP->HTTPS redirect simulation servers for those which have a redir port configured 308 | "caddyForwardProxyProbeResist_redir": caddyForwardProxyProbeResist.redirServer(), 309 | "caddyDummyProbeResist_redir": caddyDummyProbeResist.redirServer(), 310 | }, 311 | GracePeriod: caddy.Duration(1 * time.Second), // keep tests fast 312 | } 313 | httpAppJSON, err := json.Marshal(httpApp) 314 | if err != nil { 315 | panic(err) 316 | } 317 | 318 | // ensure we always use internal issuer and not a public CA 319 | tlsApp := caddytls.TLS{ 320 | Automation: &caddytls.AutomationConfig{ 321 | Policies: []*caddytls.AutomationPolicy{ 322 | { 323 | IssuersRaw: []json.RawMessage{json.RawMessage(`{"module": "internal"}`)}, 324 | }, 325 | }, 326 | }, 327 | } 328 | tlsAppJSON, err := json.Marshal(tlsApp) 329 | if err != nil { 330 | panic(err) 331 | } 332 | 333 | // configure the default CA so that we don't try to install trust, just for our tests 334 | falseBool := false 335 | pkiApp := caddypki.PKI{ 336 | CAs: map[string]*caddypki.CA{ 337 | "local": {InstallTrust: &falseBool}, 338 | }, 339 | } 340 | pkiAppJSON, err := json.Marshal(pkiApp) 341 | if err != nil { 342 | panic(err) 343 | } 344 | 345 | // build final config 346 | cfg := &caddy.Config{ 347 | Admin: &caddy.AdminConfig{Disabled: true}, 348 | AppsRaw: caddy.ModuleMap{ 349 | "http": httpAppJSON, 350 | "tls": tlsAppJSON, 351 | "pki": pkiAppJSON, 352 | }, 353 | } 354 | 355 | // start the engines 356 | err = caddy.Run(cfg) 357 | if err != nil { 358 | panic(err) 359 | } 360 | 361 | retCode := m.Run() 362 | 363 | caddy.Stop() 364 | 365 | os.Exit(retCode) 366 | } 367 | 368 | // This is a sanity check confirming that target servers actually directly serve what they are expected to. 369 | // (And that they don't serve what they should not) 370 | func TestTheTest(t *testing.T) { 371 | client := &http.Client{Transport: testTransport, Timeout: 2 * time.Second} 372 | 373 | // Request index 374 | resp, err := client.Get("http://" + caddyTestTarget.addr) 375 | if err != nil { 376 | t.Fatal(err) 377 | } else if err = responseExpected(resp, caddyTestTarget.contents[""]); err != nil { 378 | t.Fatal(err) 379 | } 380 | 381 | // Request pic 382 | resp, err = client.Get("http://" + caddyTestTarget.addr + "/pic.png") 383 | if err != nil { 384 | t.Fatal(err) 385 | } else if err = responseExpected(resp, caddyTestTarget.contents["/pic.png"]); err != nil { 386 | t.Fatal(err) 387 | } 388 | 389 | // Request pic, but expect index. Should fail 390 | resp, err = client.Get("http://" + caddyTestTarget.addr + "/pic.png") 391 | if err != nil { 392 | t.Fatal(err) 393 | } else if err = responseExpected(resp, caddyTestTarget.contents[""]); err == nil { 394 | t.Fatal(err) 395 | } 396 | 397 | // Request index, but expect pic. Should fail 398 | resp, err = client.Get("http://" + caddyTestTarget.addr) 399 | if err != nil { 400 | t.Fatal(err) 401 | } else if err = responseExpected(resp, caddyTestTarget.contents["/pic.png"]); err == nil { 402 | t.Fatal(err) 403 | } 404 | 405 | // Request non-existing resource 406 | resp, err = client.Get("http://" + caddyTestTarget.addr + "/idontexist") 407 | if err != nil { 408 | t.Fatal(err) 409 | } else if resp.StatusCode != http.StatusNotFound { 410 | t.Fatalf("Expected: 404 StatusNotFound, got %d. Response: %#v\n", resp.StatusCode, resp) 411 | } 412 | } 413 | 414 | func debugIoCopy(dst io.Writer, src io.Reader, prefix string) (written int64, err error) { 415 | buf := make([]byte, 32*1024) 416 | flusher, ok := dst.(http.Flusher) 417 | for { 418 | nr, er := src.Read(buf) 419 | fmt.Printf("[%s] Read err %#v\n%s", prefix, er, hex.Dump(buf[0:nr])) 420 | if nr > 0 { 421 | nw, ew := dst.Write(buf[0:nr]) 422 | if ok { 423 | flusher.Flush() 424 | } 425 | fmt.Printf("[%s] Wrote %v %v\n", prefix, nw, ew) 426 | if nw > 0 { 427 | written += int64(nw) 428 | } 429 | if ew != nil { 430 | err = ew 431 | break 432 | } 433 | if nr != nw { 434 | err = io.ErrShortWrite 435 | break 436 | } 437 | } 438 | if er != nil { 439 | if er != io.EOF { 440 | err = er 441 | } 442 | break 443 | } 444 | } 445 | fmt.Printf("[%s] Returning with %#v %#v\n", prefix, written, err) 446 | return 447 | } 448 | 449 | func httpdump(r interface{}) string { 450 | switch v := r.(type) { 451 | case *http.Request: 452 | if v == nil { 453 | return "httpdump: nil" 454 | } 455 | b, err := httputil.DumpRequest(v, true) 456 | if err != nil { 457 | return err.Error() 458 | } 459 | return string(b) 460 | case *http.Response: 461 | if v == nil { 462 | return "httpdump: nil" 463 | } 464 | b, err := httputil.DumpResponse(v, true) 465 | if err != nil { 466 | return err.Error() 467 | } 468 | return string(b) 469 | default: 470 | return "httpdump: wrong type" 471 | } 472 | } 473 | 474 | var testTransport = &http.Transport{ 475 | ResponseHeaderTimeout: 2 * time.Second, 476 | DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 477 | // always dial localhost for testing purposes 478 | return new(net.Dialer).DialContext(ctx, network, addr) 479 | }, 480 | DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 481 | // always dial localhost for testing purposes 482 | conn, err := new(net.Dialer).DialContext(ctx, network, addr) 483 | if err != nil { 484 | return nil, err 485 | } 486 | return tls.Client(conn, &tls.Config{InsecureSkipVerify: true}), nil 487 | }, 488 | } 489 | 490 | const defaultPACPath = "/proxy.pac" 491 | -------------------------------------------------------------------------------- /probe_resist_test.go: -------------------------------------------------------------------------------- 1 | package forwardproxy 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net" 9 | "net/http" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | func TestGETAuthCorrectProbeResist(t *testing.T) { 15 | const useTLS = true 16 | for _, httpProxyVer := range testHTTPProxyVersions { 17 | for _, resource := range testResources { 18 | response, err := getViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyProbeResist.addr, httpProxyVer, credentialsCorrect, useTLS) 19 | if err != nil { 20 | t.Fatal(err) 21 | } else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil { 22 | t.Fatal(err) 23 | } 24 | } 25 | } 26 | } 27 | 28 | func TestGETAuthWrongProbeResist(t *testing.T) { 29 | const useTLS = true 30 | for _, wrongCreds := range credentialsWrong { 31 | for _, httpProxyVer := range testHTTPProxyVersions { 32 | for _, resource := range testResources { 33 | responseProbeResist, err := getViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyProbeResist.addr, httpProxyVer, wrongCreds, useTLS) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | // get response from reference server without forwardproxy and compare them 38 | responseReference, err := getViaProxy(caddyTestTarget.addr, resource, caddyDummyProbeResist.addr, httpProxyVer, wrongCreds, useTLS) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | // as a sanity check, get 407 from simple authenticated forwardproxy 43 | responseForwardProxy, err := getViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpProxyVer, wrongCreds, useTLS) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | if responseProbeResist.StatusCode != responseReference.StatusCode { 48 | t.Fatalf("Expected response: %d, Got: %d\n", 49 | responseReference.StatusCode, responseProbeResist.StatusCode) 50 | } 51 | if err = responsesAreEqual(responseProbeResist, responseReference); err != nil { 52 | t.Fatal(err) 53 | } 54 | if err = responsesAreEqual(responseProbeResist, responseForwardProxy); err == nil { 55 | t.Fatalf("Responses from servers with and without Probe Resistance are expected to be different."+ 56 | "\nResponse from Caddy with ProbeResist: %v\nResponse from Caddy without ProbeResist: %v\n", 57 | responseProbeResist, responseForwardProxy) 58 | } 59 | } 60 | for _, resource := range testResources { 61 | responseProbeResist, err := getViaProxy(caddyForwardProxyProbeResist.addr, resource, caddyForwardProxyProbeResist.addr, httpProxyVer, wrongCreds, useTLS) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | // get response from reference server without forwardproxy and compare them 66 | responseReference, err := getViaProxy(caddyDummyProbeResist.addr, resource, caddyDummyProbeResist.addr, httpProxyVer, wrongCreds, useTLS) 67 | if err != nil { 68 | t.Fatal(err) 69 | } 70 | // as a sanity check, get 407 from simple authenticated forwardproxy 71 | responseForwardProxy, err := getViaProxy(caddyForwardProxyAuth.addr, resource, caddyForwardProxyAuth.addr, httpProxyVer, wrongCreds, useTLS) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | if responseProbeResist.StatusCode != http.StatusOK { 76 | t.Fatalf("Expected response: 200 StatusOK, Got: %d\n", 77 | responseProbeResist.StatusCode) 78 | } 79 | if err = responsesAreEqual(responseProbeResist, responseReference); err != nil { 80 | t.Fatal(err) 81 | } 82 | if err = responsesAreEqual(responseProbeResist, responseForwardProxy); err == nil { 83 | t.Fatalf("Responses from servers with and without Probe Resistance are expected to be different."+ 84 | "\nResponse from Caddy with ProbeResist: %v\nResponse from Caddy without ProbeResist: %v\n", 85 | responseProbeResist, responseForwardProxy) 86 | } 87 | } 88 | } 89 | } 90 | } 91 | 92 | // test that responses on http redirect port are same 93 | func TestGETAuthWrongProbeResistRedir(t *testing.T) { 94 | const useTLS = false 95 | httpProxyVer := "HTTP/1.1" 96 | for _, wrongCreds := range credentialsWrong { 97 | // request test target 98 | for _, resource := range testResources { 99 | responseProbeResist, rPRerr := getViaProxy(caddyTestTarget.addr, resource, changePort(caddyForwardProxyProbeResist.addr, caddyForwardProxyProbeResist.httpRedirPort), httpProxyVer, wrongCreds, useTLS) 100 | // get response from reference server without forwardproxy and compare them 101 | responseReference, rRerr := getViaProxy(caddyTestTarget.addr, resource, changePort(caddyDummyProbeResist.addr, caddyDummyProbeResist.httpRedirPort), httpProxyVer, wrongCreds, useTLS) 102 | if (rPRerr == nil && rRerr != nil) || (rPRerr != nil && rRerr == nil) { 103 | t.Fatalf("Reference error: %s. Probe resist error: %s", rRerr, rPRerr) 104 | } 105 | if responseProbeResist.StatusCode != responseReference.StatusCode { 106 | t.Fatalf("Expected response: %d, Got: %d\n", 107 | responseReference.StatusCode, responseProbeResist.StatusCode) 108 | } 109 | if err := responsesAreEqual(responseProbeResist, responseReference); err != nil { 110 | t.Fatal(err) 111 | } 112 | } 113 | // request self 114 | for _, resource := range testResources { 115 | responseProbeResist, err := getViaProxy(caddyForwardProxyProbeResist.addr, resource, changePort(caddyForwardProxyProbeResist.addr, caddyForwardProxyProbeResist.httpRedirPort), httpProxyVer, wrongCreds, useTLS) 116 | if err != nil { 117 | t.Fatal(err) 118 | } 119 | // get response from reference server without forwardproxy and compare them 120 | responseReference, err := getViaProxy(caddyDummyProbeResist.addr, resource, changePort(caddyDummyProbeResist.addr, caddyDummyProbeResist.httpRedirPort), httpProxyVer, wrongCreds, useTLS) 121 | if err != nil { 122 | t.Fatal(err) 123 | } 124 | if responseProbeResist.StatusCode != responseReference.StatusCode { 125 | t.Fatalf("Expected response: %d, Got: %d\n", 126 | responseReference.StatusCode, responseProbeResist.StatusCode) 127 | } 128 | if err = responsesAreEqual(responseProbeResist, responseReference); err != nil { 129 | t.Fatal(err) 130 | } 131 | } 132 | } 133 | } 134 | 135 | func TestConnectAuthCorrectProbeResist(t *testing.T) { 136 | const useTLS = true 137 | for _, httpProxyVer := range testHTTPProxyVersions { 138 | for _, httpTargetVer := range testHTTPTargetVersions { 139 | for _, resource := range testResources { 140 | response, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyProbeResist.addr, httpTargetVer, credentialsCorrect, httpProxyVer, useTLS) 141 | if err != nil { 142 | t.Fatal(err) 143 | } else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil { 144 | t.Fatal(err) 145 | } 146 | } 147 | } 148 | } 149 | } 150 | 151 | func TestConnectAuthWrongProbeResist(t *testing.T) { 152 | const useTLS = true 153 | for _, wrongCreds := range credentialsWrong { 154 | for _, httpProxyVer := range testHTTPProxyVersions { 155 | for _, httpTargetVer := range testHTTPTargetVersions { 156 | for _, resource := range testResources { 157 | responseProbeResist, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyProbeResist.addr, httpTargetVer, wrongCreds, httpProxyVer, useTLS) 158 | if err != nil { 159 | t.Fatal(err) 160 | } 161 | // get response from reference server without forwardproxy and compare them 162 | responseReference, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyDummyProbeResist.addr, httpTargetVer, wrongCreds, httpProxyVer, useTLS) 163 | if err != nil { 164 | t.Fatal(err) 165 | } 166 | // as a sanity check, get 407 from simple authenticated forwardproxy 167 | responseForwardProxy, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, wrongCreds, httpProxyVer, useTLS) 168 | if err != nil { 169 | t.Fatal(err) 170 | } 171 | if responseProbeResist.StatusCode != responseReference.StatusCode { 172 | t.Fatalf("Expected response: %d, Got: %d\n", 173 | responseReference.StatusCode, responseProbeResist.StatusCode) 174 | } 175 | if err = responsesAreEqual(responseProbeResist, responseReference); err != nil { 176 | t.Fatal(err) 177 | } 178 | if err = responsesAreEqual(responseProbeResist, responseForwardProxy); err == nil { 179 | t.Fatalf("Responses from servers with and without Probe Resistance are expected to be different."+ 180 | "\nResponse from Caddy with ProbeResist: %v\nResponse from Caddy without ProbeResist: %v\n", 181 | responseProbeResist, responseForwardProxy) 182 | } 183 | } 184 | // request self 185 | for _, resource := range testResources { 186 | if httpTargetVer != httpProxyVer { 187 | continue 188 | } 189 | responseProbeResist, err := connectAndGetViaProxy(caddyForwardProxyProbeResist.addr, resource, caddyForwardProxyProbeResist.addr, httpTargetVer, wrongCreds, httpProxyVer, useTLS) 190 | if err != nil { 191 | t.Fatal(err) 192 | } 193 | // get response from reference server without forwardproxy and compare them 194 | responseReference, err := connectAndGetViaProxy(caddyDummyProbeResist.addr, resource, caddyDummyProbeResist.addr, httpTargetVer, wrongCreds, httpProxyVer, useTLS) 195 | if err != nil { 196 | t.Fatal(err) 197 | } 198 | // as a sanity check, get 407 from simple authenticated forwardproxy 199 | responseForwardProxy, err := connectAndGetViaProxy(caddyForwardProxyAuth.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, wrongCreds, httpProxyVer, useTLS) 200 | if err != nil { 201 | t.Fatal(err) 202 | } 203 | if err = responsesAreEqual(responseProbeResist, responseReference); err != nil { 204 | t.Fatal(err) 205 | } 206 | if err = responsesAreEqual(responseProbeResist, responseForwardProxy); err == nil { 207 | t.Fatalf("Responses from servers with and without Probe Resistance are expected to be different."+ 208 | "\nResponse from Caddy with ProbeResist: %v\nResponse from Caddy without ProbeResist: %v\n", 209 | responseProbeResist, responseForwardProxy) 210 | } 211 | } 212 | } 213 | } 214 | } 215 | } 216 | 217 | // test that responses on http redirect port are same 218 | func TestConnectAuthWrongProbeResistRedir(t *testing.T) { 219 | const useTLS = false 220 | httpProxyVer := "HTTP/1.1" 221 | for _, wrongCreds := range credentialsWrong { 222 | for _, httpTargetVer := range testHTTPTargetVersions { 223 | // request test target 224 | for _, resource := range testResources { 225 | responseProbeResist, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, changePort(caddyForwardProxyProbeResist.addr, caddyForwardProxyProbeResist.httpRedirPort), httpTargetVer, wrongCreds, httpProxyVer, useTLS) 226 | if err != nil { 227 | t.Fatal(err) 228 | } 229 | // get response from reference server without forwardproxy and compare them 230 | responseReference, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, changePort(caddyDummyProbeResist.addr, caddyDummyProbeResist.httpRedirPort), httpTargetVer, wrongCreds, httpProxyVer, useTLS) 231 | if err != nil { 232 | t.Fatal(err) 233 | } 234 | if responseProbeResist.StatusCode != responseReference.StatusCode { 235 | t.Fatalf("Expected response: %d, Got: %d\n", 236 | responseReference.StatusCode, responseProbeResist.StatusCode) 237 | } 238 | if err = responsesAreEqual(responseProbeResist, responseReference); err != nil { 239 | t.Fatal(err) 240 | } 241 | } 242 | // request self 243 | for _, resource := range testResources { 244 | responseProbeResist, err := connectAndGetViaProxy(caddyForwardProxyProbeResist.addr, resource, changePort(caddyForwardProxyProbeResist.addr, caddyForwardProxyProbeResist.httpRedirPort), httpTargetVer, wrongCreds, httpProxyVer, useTLS) 245 | if err != nil { 246 | t.Fatal(err) 247 | } 248 | // get response from reference server without forwardproxy and compare them 249 | responseReference, err := connectAndGetViaProxy(caddyDummyProbeResist.addr, resource, changePort(caddyDummyProbeResist.addr, caddyDummyProbeResist.httpRedirPort), httpTargetVer, wrongCreds, httpProxyVer, useTLS) 250 | if err != nil { 251 | t.Fatal(err) 252 | } 253 | if responseProbeResist.StatusCode != responseReference.StatusCode { 254 | t.Fatalf("Expected response: %d, Got: %d\n", 255 | responseReference.StatusCode, responseProbeResist.StatusCode) 256 | } 257 | if err = responsesAreEqual(responseProbeResist, responseReference); err != nil { 258 | t.Fatal(err) 259 | } 260 | } 261 | } 262 | } 263 | } 264 | 265 | // returns nil if are equal 266 | func responsesAreEqual(res1, res2 *http.Response) error { 267 | if res1 == nil { 268 | return errors.New("res1 is nil") 269 | } 270 | if res2 == nil { 271 | return errors.New("res2 is nil") 272 | } 273 | if res1.Status != res2.Status { 274 | return fmt.Errorf("status is different; %s != %s", res1.Status, res2.Status) 275 | } 276 | if res1.StatusCode != res2.StatusCode { 277 | return fmt.Errorf("status code is different; %d != %d", res1.StatusCode, res2.StatusCode) 278 | } 279 | if res1.ProtoMajor != res2.ProtoMajor { 280 | return fmt.Errorf("proto major is different; %d != %d", res1.ProtoMajor, res2.ProtoMajor) 281 | } 282 | if res1.ProtoMinor != res2.ProtoMinor { 283 | return fmt.Errorf("proto minor is different; %d != %d", res1.ProtoMinor, res2.ProtoMinor) 284 | } 285 | if res1.Close != res2.Close { 286 | return fmt.Errorf("close is different; %t != %t", res1.Close, res2.Close) 287 | } 288 | if res1.ContentLength != res2.ContentLength { 289 | return fmt.Errorf("content length is different; %d != %d", res1.ContentLength, res2.ContentLength) 290 | } 291 | if res1.Uncompressed != res2.Uncompressed { 292 | return fmt.Errorf("uncompressed is different; %t != %t", res1.Uncompressed, res2.Uncompressed) 293 | } 294 | if res1.Proto != res2.Proto { 295 | return fmt.Errorf("proto is different; %s != %s", res1.Proto, res2.Proto) 296 | } 297 | if len(res1.TransferEncoding) != len(res2.TransferEncoding) { 298 | return fmt.Errorf("transfer encodings have different lenght; %d != %d", len(res1.TransferEncoding), len(res2.TransferEncoding)) 299 | } 300 | 301 | // returns "" if equal 302 | stringSlicesAreEqual := func(s1, s2 []string) string { 303 | if s1 == nil && s2 == nil { 304 | return "" 305 | } 306 | 307 | if s1 == nil { 308 | return "s1 is nil, whereas s2 is not" 309 | } 310 | if s2 == nil { 311 | return "s2 is nil, whereas s1 is not" 312 | } 313 | 314 | if len(s1) != len(s2) { 315 | return fmt.Sprintf("different length: %d vs %d", len(s1), len(s2)) 316 | } 317 | for i := range s1 { 318 | if s1[i] != s2[i] { 319 | return fmt.Sprintf("different string at position %d: %s vs %s", i, s1[i], s2[i]) 320 | } 321 | } 322 | return "" 323 | } 324 | 325 | errStr := stringSlicesAreEqual(res1.TransferEncoding, res2.TransferEncoding) 326 | if errStr != "" { 327 | return errors.New("TransferEncodings are different: " + errStr) 328 | } 329 | 330 | if len(res1.Header) != len(res2.Header) { 331 | return errors.New("Headers have different length") 332 | } 333 | for k1, v1 := range res1.Header { 334 | k1Lower := strings.ToLower(k1) 335 | if k1Lower == "date" { 336 | continue 337 | } 338 | v2, ok := res2.Header[k1] 339 | if !ok { 340 | return fmt.Errorf("header \"%s: %s\" is absent in res2", k1, v1) 341 | } 342 | // if k1Lower == "location" { 343 | // for i, h := range v2 { 344 | // v2[i] = removeAddressesStr(h) 345 | // } 346 | // for i, h := range v1 { 347 | // v1[i] = removeAddressesStr(h) 348 | // } 349 | // } 350 | if errStr = stringSlicesAreEqual(v1, v2); errStr != "" { 351 | return fmt.Errorf("header \"%s\" is different: %s", k1, errStr) 352 | } 353 | } 354 | // Compare bodies 355 | buf1, err1 := ioutil.ReadAll(res1.Body) 356 | buf2, err2 := ioutil.ReadAll(res2.Body) 357 | n1 := len(buf1) 358 | n2 := len(buf2) 359 | makeBodyError := func(s string) error { 360 | return fmt.Errorf("bodies are different: %s. n1 = %d, n2 = %d. err1 = %v, err2 = %v. buf1 = %s, buf2 = %s", 361 | s, n1, n2, err1, err2, buf1[:n1], buf2[:n2]) 362 | } 363 | if n2 != n1 { 364 | return makeBodyError("Body sizes are different") 365 | } 366 | buf1 = removeAddressesByte(buf1[:n1]) 367 | buf2 = removeAddressesByte(buf2[:n1]) 368 | for i := range buf1 { 369 | if buf1[i] != buf2[i] { 370 | return makeBodyError(fmt.Sprintf("Mismatched character %d", i)) 371 | } 372 | } 373 | if err1 != nil || err2 != nil { 374 | return makeBodyError("Unexpected Read errors") 375 | } 376 | return nil 377 | } 378 | 379 | // Responses from forwardproxy + proberesist and generic caddy can have different addresses present in headers. 380 | // To avoid false positives - remove addresses before comparing. 381 | func removeAddressesByte(b []byte) []byte { 382 | b = bytes.Replace(b, []byte(caddyForwardProxyProbeResist.addr), 383 | bytes.Repeat([]byte{'#'}, len(caddyForwardProxyProbeResist.addr)), -1) 384 | b = bytes.Replace(b, []byte(caddyDummyProbeResist.addr), 385 | bytes.Repeat([]byte{'#'}, len(caddyDummyProbeResist.addr)), -1) 386 | return b 387 | } 388 | 389 | func removeAddressesStr(s string) string { 390 | return string(removeAddressesByte([]byte(s))) 391 | } 392 | 393 | func changePort(inputAddr, toPort string) string { 394 | host, _, err := net.SplitHostPort(inputAddr) 395 | if err != nil { 396 | panic(err) 397 | } 398 | return net.JoinHostPort(host, toPort) 399 | } 400 | -------------------------------------------------------------------------------- /forwardproxy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // Caching is purposefully ignored. 16 | 17 | package forwardproxy 18 | 19 | import ( 20 | "bufio" 21 | "bytes" 22 | "context" 23 | "crypto/subtle" 24 | "crypto/tls" 25 | "encoding/base64" 26 | "errors" 27 | "fmt" 28 | "io" 29 | "io/ioutil" 30 | "math/rand" 31 | "net" 32 | "net/http" 33 | "net/url" 34 | "os" 35 | "strconv" 36 | "strings" 37 | "sync" 38 | "time" 39 | 40 | "github.com/caddyserver/caddy/v2" 41 | "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" 42 | "github.com/caddyserver/caddy/v2/modules/caddyhttp" 43 | "github.com/caddyserver/forwardproxy/httpclient" 44 | "go.uber.org/zap" 45 | "golang.org/x/net/proxy" 46 | ) 47 | 48 | func init() { 49 | caddy.RegisterModule(Handler{}) 50 | 51 | // Used for generating padding lengths. Not needed to be cryptographically secure. 52 | // Does not care about double seeding. 53 | rand.Seed(time.Now().UnixNano()) 54 | } 55 | 56 | // Handler implements a forward proxy. 57 | // 58 | // EXPERIMENTAL: This handler is still experimental and subject to breaking changes. 59 | type Handler struct { 60 | logger *zap.Logger 61 | 62 | // Filename of the PAC file to serve. 63 | PACPath string `json:"pac_path,omitempty"` 64 | 65 | // If true, the Forwarded header will not be augmented with your IP address. 66 | HideIP bool `json:"hide_ip,omitempty"` 67 | 68 | // If true, the Via heaeder will not be added. 69 | HideVia bool `json:"hide_via,omitempty"` 70 | 71 | // Host(s) (and ports) of the proxy. When you configure a client, 72 | // you will give it the host (and port) of the proxy to use. 73 | Hosts caddyhttp.MatchHost `json:"hosts,omitempty"` 74 | 75 | // Optional probe resistance. (See documentation.) 76 | ProbeResistance *ProbeResistance `json:"probe_resistance,omitempty"` 77 | 78 | // How long to wait before timing out initial TCP connections. 79 | DialTimeout caddy.Duration `json:"dial_timeout,omitempty"` 80 | 81 | // Optionally configure an upstream proxy to use. 82 | Upstream string `json:"upstream,omitempty"` 83 | 84 | // Access control list. 85 | ACL []ACLRule `json:"acl,omitempty"` 86 | 87 | // Ports to be allowed to connect to (if non-empty). 88 | AllowedPorts []int `json:"allowed_ports,omitempty"` 89 | 90 | httpTransport *http.Transport 91 | 92 | // overridden dialContext allows us to redirect requests to upstream proxy 93 | dialContext func(ctx context.Context, network, address string) (net.Conn, error) 94 | upstream *url.URL // address of upstream proxy 95 | 96 | aclRules []aclRule 97 | 98 | // TODO: temporary/deprecated - we should try to reuse existing authentication modules instead! 99 | BasicauthUser string `json:"auth_user_deprecated,omitempty"` 100 | BasicauthPass string `json:"auth_pass_deprecated,omitempty"` 101 | authRequired bool 102 | authCredentials [][]byte // slice with base64-encoded credentials 103 | } 104 | 105 | // CaddyModule returns the Caddy module information. 106 | func (Handler) CaddyModule() caddy.ModuleInfo { 107 | return caddy.ModuleInfo{ 108 | ID: "http.handlers.forward_proxy", 109 | New: func() caddy.Module { return new(Handler) }, 110 | } 111 | } 112 | 113 | // Provision ensures that h is set up properly before use. 114 | func (h *Handler) Provision(ctx caddy.Context) error { 115 | h.logger = ctx.Logger(h) 116 | 117 | if h.DialTimeout <= 0 { 118 | h.DialTimeout = caddy.Duration(30 * time.Second) 119 | } 120 | 121 | h.httpTransport = &http.Transport{ 122 | Proxy: http.ProxyFromEnvironment, 123 | MaxIdleConns: 50, 124 | IdleConnTimeout: 60 * time.Second, 125 | TLSHandshakeTimeout: 10 * time.Second, 126 | } 127 | 128 | // TODO: temporary, in an effort to get the tests to pass 129 | if h.BasicauthUser != "" && h.BasicauthPass != "" { 130 | basicAuthBuf := make([]byte, base64.StdEncoding.EncodedLen(len(h.BasicauthUser)+1+len(h.BasicauthPass))) 131 | base64.StdEncoding.Encode(basicAuthBuf, []byte(h.BasicauthUser+":"+h.BasicauthPass)) 132 | h.authRequired = true 133 | h.authCredentials = [][]byte{basicAuthBuf} 134 | } 135 | 136 | // access control lists 137 | for _, rule := range h.ACL { 138 | for _, subj := range rule.Subjects { 139 | ar, err := newACLRule(subj, rule.Allow) 140 | if err != nil { 141 | return err 142 | } 143 | h.aclRules = append(h.aclRules, ar) 144 | } 145 | } 146 | for _, ipDeny := range []string{ 147 | "10.0.0.0/8", 148 | "127.0.0.0/8", 149 | "172.16.0.0/12", 150 | "192.168.0.0/16", 151 | "::1/128", 152 | "fe80::/10", 153 | } { 154 | ar, err := newACLRule(ipDeny, false) 155 | if err != nil { 156 | return err 157 | } 158 | h.aclRules = append(h.aclRules, ar) 159 | } 160 | h.aclRules = append(h.aclRules, &aclAllRule{allow: true}) 161 | 162 | if h.ProbeResistance != nil { 163 | if !h.authRequired { 164 | return fmt.Errorf("probe resistance requires authentication") 165 | } 166 | if len(h.ProbeResistance.Domain) > 0 { 167 | h.logger.Info("Secret domain used to connect to proxy: " + h.ProbeResistance.Domain) 168 | } 169 | } 170 | 171 | dialer := &net.Dialer{ 172 | Timeout: time.Duration(h.DialTimeout), 173 | KeepAlive: 30 * time.Second, 174 | DualStack: true, 175 | } 176 | h.dialContext = dialer.DialContext 177 | h.httpTransport.DialContext = func(ctx context.Context, network string, address string) (net.Conn, error) { 178 | return h.dialContextCheckACL(ctx, network, address) 179 | } 180 | 181 | if h.Upstream != "" { 182 | upstreamURL, err := url.Parse(h.Upstream) 183 | if err != nil { 184 | return fmt.Errorf("bad upstream URL: %v", err) 185 | } 186 | h.upstream = upstreamURL 187 | 188 | if !isLocalhost(h.upstream.Hostname()) && h.upstream.Scheme != "https" { 189 | return errors.New("insecure schemes are only allowed to localhost upstreams") 190 | } 191 | 192 | registerHTTPDialer := func(u *url.URL, _ proxy.Dialer) (proxy.Dialer, error) { 193 | // CONNECT request is proxied as-is, so we don't care about target url, but it could be 194 | // useful in future to implement policies of choosing between multiple upstream servers. 195 | // Given dialer is not used, since it's the same dialer provided by us. 196 | d, err := httpclient.NewHTTPConnectDialer(h.upstream.String()) 197 | if err != nil { 198 | return nil, err 199 | } 200 | d.Dialer = *dialer 201 | if isLocalhost(h.upstream.Hostname()) && h.upstream.Scheme == "https" { 202 | // disabling verification helps with testing the package and setups 203 | // either way, it's impossible to have a legit TLS certificate for "127.0.0.1" - TODO: not true anymore 204 | h.logger.Info("Localhost upstream detected, disabling verification of TLS certificate") 205 | d.DialTLS = func(network string, address string) (net.Conn, string, error) { 206 | conn, err := tls.Dial(network, address, &tls.Config{InsecureSkipVerify: true}) 207 | if err != nil { 208 | return nil, "", err 209 | } 210 | return conn, conn.ConnectionState().NegotiatedProtocol, nil 211 | } 212 | } 213 | return d, nil 214 | } 215 | proxy.RegisterDialerType("https", registerHTTPDialer) 216 | proxy.RegisterDialerType("http", registerHTTPDialer) 217 | 218 | upstreamDialer, err := proxy.FromURL(h.upstream, dialer) 219 | if err != nil { 220 | return errors.New("failed to create proxy to upstream: " + err.Error()) 221 | } 222 | 223 | if ctxDialer, ok := upstreamDialer.(dialContexter); ok { 224 | // upstreamDialer has DialContext - use it 225 | h.dialContext = ctxDialer.DialContext 226 | } else { 227 | // upstreamDialer does not have DialContext - ignore the context :( 228 | h.dialContext = func(ctx context.Context, network string, address string) (net.Conn, error) { 229 | return upstreamDialer.Dial(network, address) 230 | } 231 | } 232 | } 233 | 234 | return nil 235 | } 236 | 237 | func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { 238 | // start by splitting the request host and port 239 | reqHost, _, err := net.SplitHostPort(r.Host) 240 | if err != nil { 241 | reqHost = r.Host // OK; probably just didn't have a port 242 | } 243 | 244 | var authErr error 245 | if h.authRequired { 246 | authErr = h.checkCredentials(r) 247 | } 248 | if h.ProbeResistance != nil && len(h.ProbeResistance.Domain) > 0 && reqHost == h.ProbeResistance.Domain { 249 | return serveHiddenPage(w, authErr) 250 | } 251 | if h.Hosts.Match(r) && (r.Method != http.MethodConnect || authErr != nil) { 252 | // Always pass non-CONNECT requests to hostname 253 | // Pass CONNECT requests only if probe resistance is enabled and not authenticated 254 | if h.shouldServePACFile(r) { 255 | return h.servePacFile(w, r) 256 | } 257 | return next.ServeHTTP(w, r) 258 | } 259 | if authErr != nil { 260 | if h.ProbeResistance != nil { 261 | // probe resistance is requested and requested URI does not match secret domain; 262 | // act like this proxy handler doesn't even exist (pass thru to next handler) 263 | return next.ServeHTTP(w, r) 264 | } 265 | w.Header().Set("Proxy-Authenticate", "Basic realm=\"Caddy Secure Web Proxy\"") 266 | return caddyhttp.Error(http.StatusProxyAuthRequired, authErr) 267 | } 268 | 269 | if r.ProtoMajor != 1 && r.ProtoMajor != 2 && r.ProtoMajor != 3 { 270 | return caddyhttp.Error(http.StatusHTTPVersionNotSupported, 271 | fmt.Errorf("unsupported HTTP major version: %d", r.ProtoMajor)) 272 | } 273 | 274 | ctx := context.Background() 275 | if !h.HideIP { 276 | ctxHeader := make(http.Header) 277 | for k, v := range r.Header { 278 | if kL := strings.ToLower(k); kL == "forwarded" || kL == "x-forwarded-for" { 279 | ctxHeader[k] = v 280 | } 281 | } 282 | ctxHeader.Add("Forwarded", "for=\""+r.RemoteAddr+"\"") 283 | ctx = context.WithValue(ctx, httpclient.ContextKeyHeader{}, ctxHeader) 284 | } 285 | 286 | if r.Method == http.MethodConnect { 287 | if r.ProtoMajor == 2 || r.ProtoMajor == 3 { 288 | if len(r.URL.Scheme) > 0 || len(r.URL.Path) > 0 { 289 | return caddyhttp.Error(http.StatusBadRequest, 290 | fmt.Errorf("CONNECT request has :scheme and/or :path pseudo-header fields")) 291 | } 292 | } 293 | 294 | // HTTP CONNECT Fast Open. We merely close the connection if Open fails. 295 | wFlusher, ok := w.(http.Flusher) 296 | if !ok { 297 | return caddyhttp.Error(http.StatusInternalServerError, 298 | fmt.Errorf("ResponseWriter doesn't implement http.Flusher")) 299 | } 300 | // Creates a padding of [30, 30+32) 301 | paddingLen := rand.Intn(32) + 30 302 | padding := make([]byte, paddingLen) 303 | bits := rand.Uint64() 304 | for i := 0; i < 16; i++ { 305 | // Codes that won't be Huffman coded. 306 | padding[i] = "!#$()+<>?@[]^`{}"[bits&15] 307 | bits >>= 4 308 | } 309 | for i := 16; i < paddingLen; i++ { 310 | padding[i] = '~' 311 | } 312 | w.Header().Set("Padding", string(padding)) 313 | w.WriteHeader(http.StatusOK) 314 | wFlusher.Flush() 315 | 316 | hostPort := r.URL.Host 317 | if hostPort == "" { 318 | hostPort = r.Host 319 | } 320 | targetConn, err := h.dialContextCheckACL(ctx, "tcp", hostPort) 321 | if err != nil { 322 | return err 323 | } 324 | if targetConn == nil { 325 | // safest to check both error and targetConn afterwards, in case fp.dial (potentially unstable 326 | // from x/net/proxy) misbehaves and returns both nil or both non-nil 327 | return caddyhttp.Error(http.StatusForbidden, 328 | fmt.Errorf("hostname %s is not allowed", r.URL.Hostname())) 329 | } 330 | defer targetConn.Close() 331 | 332 | switch r.ProtoMajor { 333 | case 1: // http1: hijack the whole flow 334 | return serveHijack(w, targetConn) 335 | case 2: // http2: keep reading from "request" and writing into same response 336 | fallthrough 337 | case 3: 338 | defer r.Body.Close() 339 | return dualStream(targetConn, r.Body, w, r.Header.Get("Padding") != "") 340 | } 341 | 342 | panic("There was a check for http version, yet it's incorrect") 343 | } 344 | 345 | // Scheme has to be appended to avoid `unsupported protocol scheme ""` error. 346 | // `http://` is used, since this initial request itself is always HTTP, regardless of what client and server 347 | // may speak afterwards. 348 | if r.URL.Scheme == "" { 349 | r.URL.Scheme = "http" 350 | } 351 | if r.URL.Host == "" { 352 | r.URL.Host = r.Host 353 | } 354 | r.Proto = "HTTP/1.1" 355 | r.ProtoMajor = 1 356 | r.ProtoMinor = 1 357 | r.RequestURI = "" 358 | 359 | removeHopByHop(r.Header) 360 | 361 | if !h.HideIP { 362 | r.Header.Add("Forwarded", "for=\""+r.RemoteAddr+"\"") 363 | } 364 | 365 | // https://tools.ietf.org/html/rfc7230#section-5.7.1 366 | if !h.HideVia { 367 | r.Header.Add("Via", strconv.Itoa(r.ProtoMajor)+"."+strconv.Itoa(r.ProtoMinor)+" caddy") 368 | } 369 | 370 | var response *http.Response 371 | if h.upstream == nil { 372 | // non-upstream request uses httpTransport to reuse connections 373 | if r.Body != nil && 374 | (r.Method == "GET" || r.Method == "HEAD" || r.Method == "OPTIONS" || r.Method == "TRACE") { 375 | // make sure request is idempotent and could be retried by saving the Body 376 | // None of those methods are supposed to have body, 377 | // but we still need to copy the r.Body, even if it's empty 378 | rBodyBuf, err := ioutil.ReadAll(r.Body) 379 | if err != nil { 380 | return caddyhttp.Error(http.StatusBadRequest, 381 | fmt.Errorf("failed to read request body: %v", err)) 382 | } 383 | r.GetBody = func() (io.ReadCloser, error) { 384 | return ioutil.NopCloser(bytes.NewReader(rBodyBuf)), nil 385 | } 386 | r.Body, _ = r.GetBody() 387 | } 388 | response, err = h.httpTransport.RoundTrip(r) 389 | } else { 390 | // Upstream requests don't interact well with Transport: connections could always be 391 | // reused, but Transport thinks they go to different Hosts, so it spawns tons of 392 | // useless connections. 393 | // Just use dialContext, which will multiplex via single connection, if http/2 394 | if creds := h.upstream.User.String(); creds != "" { 395 | // set upstream credentials for the request, if needed 396 | r.Header.Set("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(creds))) 397 | } 398 | if r.URL.Port() == "" { 399 | r.URL.Host = net.JoinHostPort(r.URL.Host, "80") 400 | } 401 | upsConn, err := h.dialContext(ctx, "tcp", r.URL.Host) 402 | if err != nil { 403 | return caddyhttp.Error(http.StatusBadGateway, 404 | fmt.Errorf("failed to dial upstream: %v", err)) 405 | } 406 | err = r.Write(upsConn) 407 | if err != nil { 408 | return caddyhttp.Error(http.StatusBadGateway, 409 | fmt.Errorf("failed to write upstream request: %v", err)) 410 | } 411 | response, err = http.ReadResponse(bufio.NewReader(upsConn), r) 412 | if err != nil { 413 | return caddyhttp.Error(http.StatusBadGateway, 414 | fmt.Errorf("failed to read upstream response: %v", err)) 415 | } 416 | } 417 | r.Body.Close() 418 | 419 | if response != nil { 420 | defer response.Body.Close() 421 | } 422 | if err != nil { 423 | if _, ok := err.(caddyhttp.HandlerError); ok { 424 | return err 425 | } 426 | return caddyhttp.Error(http.StatusBadGateway, 427 | fmt.Errorf("failed to read response: %v", err)) 428 | } 429 | 430 | return forwardResponse(w, response) 431 | } 432 | 433 | func (h Handler) checkCredentials(r *http.Request) error { 434 | pa := strings.Split(r.Header.Get("Proxy-Authorization"), " ") 435 | if len(pa) != 2 { 436 | return errors.New("Proxy-Authorization is required! Expected format: ") 437 | } 438 | if strings.ToLower(pa[0]) != "basic" { 439 | return errors.New("Auth type is not supported") 440 | } 441 | for _, creds := range h.authCredentials { 442 | if subtle.ConstantTimeCompare(creds, []byte(pa[1])) == 1 { 443 | // Please do not consider this to be timing-attack-safe code. Simple equality is almost 444 | // mindlessly substituted with constant time algo and there ARE known issues with this code, 445 | // e.g. size of smallest credentials is guessable. TODO: protect from all the attacks! Hash? 446 | return nil 447 | } 448 | } 449 | return errors.New("Invalid credentials") 450 | } 451 | 452 | func (h Handler) shouldServePACFile(r *http.Request) bool { 453 | return len(h.PACPath) > 0 && r.URL.Path == h.PACPath 454 | } 455 | 456 | func (h Handler) servePacFile(w http.ResponseWriter, r *http.Request) error { 457 | fmt.Fprintf(w, pacFile, r.Host) 458 | // fmt.Fprintf(w, pacFile, h.hostname, h.port) 459 | return nil 460 | } 461 | 462 | // dialContextCheckACL enforces Access Control List and calls fp.DialContext 463 | func (h Handler) dialContextCheckACL(ctx context.Context, network, hostPort string) (net.Conn, error) { 464 | var conn net.Conn 465 | 466 | if network != "tcp" && network != "tcp4" && network != "tcp6" { 467 | // return nil, &proxyError{S: "Network " + network + " is not supported", Code: http.StatusBadRequest} 468 | return nil, caddyhttp.Error(http.StatusBadRequest, 469 | fmt.Errorf("network %s is not supported", network)) 470 | } 471 | 472 | host, port, err := net.SplitHostPort(hostPort) 473 | if err != nil { 474 | // return nil, &proxyError{S: err.Error(), Code: http.StatusBadRequest} 475 | return nil, caddyhttp.Error(http.StatusBadRequest, err) 476 | } 477 | 478 | if h.upstream != nil { 479 | // if upstreaming -- do not resolve locally nor check acl 480 | conn, err = h.dialContext(ctx, network, hostPort) 481 | if err != nil { 482 | // return conn, &proxyError{S: err.Error(), Code: http.StatusBadGateway} 483 | return conn, caddyhttp.Error(http.StatusBadGateway, err) 484 | } 485 | return conn, nil 486 | } 487 | 488 | if !h.portIsAllowed(port) { 489 | // return nil, &proxyError{S: "port " + port + " is not allowed", Code: http.StatusForbidden} 490 | return nil, caddyhttp.Error(http.StatusForbidden, 491 | fmt.Errorf("port %s is not allowed", port)) 492 | } 493 | 494 | // in case IP was provided, net.LookupIP will simply return it 495 | IPs, err := net.LookupIP(host) 496 | if err != nil { 497 | // return nil, &proxyError{S: fmt.Sprintf("Lookup of %s failed: %v", host, err), 498 | // Code: http.StatusBadGateway} 499 | return nil, caddyhttp.Error(http.StatusBadGateway, 500 | fmt.Errorf("lookup of %s failed: %v", host, err)) 501 | } 502 | 503 | // This is net.Dial's default behavior: if the host resolves to multiple IP addresses, 504 | // Dial will try each IP address in order until one succeeds 505 | for _, ip := range IPs { 506 | if !h.hostIsAllowed(host, ip) { 507 | continue 508 | } 509 | 510 | conn, err = h.dialContext(ctx, network, net.JoinHostPort(ip.String(), port)) 511 | if err == nil { 512 | return conn, nil 513 | } 514 | } 515 | 516 | return nil, caddyhttp.Error(http.StatusForbidden, fmt.Errorf("no allowed IP addresses for %s", host)) 517 | } 518 | 519 | func (h Handler) hostIsAllowed(hostname string, ip net.IP) bool { 520 | for _, rule := range h.aclRules { 521 | switch rule.tryMatch(ip, hostname) { 522 | case aclDecisionDeny: 523 | return false 524 | case aclDecisionAllow: 525 | return true 526 | } 527 | } 528 | // TODO: convert this to log entry 529 | // fmt.Println("ERROR: no acl match for ", hostname, ip) // shouldn't happen 530 | return false 531 | } 532 | 533 | func (h Handler) portIsAllowed(port string) bool { 534 | portInt, err := strconv.Atoi(port) 535 | if err != nil { 536 | return false 537 | } 538 | if portInt <= 0 || portInt > 65535 { 539 | return false 540 | } 541 | if len(h.AllowedPorts) == 0 { 542 | return true 543 | } 544 | isAllowed := false 545 | for _, p := range h.AllowedPorts { 546 | if p == portInt { 547 | isAllowed = true 548 | break 549 | } 550 | } 551 | return isAllowed 552 | } 553 | 554 | func serveHiddenPage(w http.ResponseWriter, authErr error) error { 555 | const hiddenPage = ` 556 | 557 | Hidden Proxy Page 558 | 559 | 560 |

Hidden Proxy Page!

561 | %s
562 | 563 | ` 564 | const AuthFail = "Please authenticate yourself to the proxy." 565 | const AuthOk = "Congratulations, you are successfully authenticated to the proxy! Go browse all the things!" 566 | 567 | w.Header().Set("Content-Type", "text/html") 568 | if authErr != nil { 569 | w.Header().Set("Proxy-Authenticate", "Basic realm=\"Caddy Secure Web Proxy\"") 570 | w.WriteHeader(http.StatusProxyAuthRequired) 571 | w.Write([]byte(fmt.Sprintf(hiddenPage, AuthFail))) 572 | return authErr 573 | } 574 | w.Write([]byte(fmt.Sprintf(hiddenPage, AuthOk))) 575 | return nil 576 | } 577 | 578 | // Hijacks the connection from ResponseWriter, writes the response and proxies data between targetConn 579 | // and hijacked connection. 580 | func serveHijack(w http.ResponseWriter, targetConn net.Conn) error { 581 | hijacker, ok := w.(http.Hijacker) 582 | if !ok { 583 | return caddyhttp.Error(http.StatusInternalServerError, 584 | fmt.Errorf("ResponseWriter does not implement http.Hijacker")) 585 | } 586 | clientConn, bufReader, err := hijacker.Hijack() 587 | if err != nil { 588 | return caddyhttp.Error(http.StatusInternalServerError, 589 | fmt.Errorf("hijack failed: %v", err)) 590 | } 591 | defer clientConn.Close() 592 | // bufReader may contain unprocessed buffered data from the client. 593 | if bufReader != nil { 594 | // snippet borrowed from `proxy` plugin 595 | if n := bufReader.Reader.Buffered(); n > 0 { 596 | rbuf, err := bufReader.Reader.Peek(n) 597 | if err != nil { 598 | return caddyhttp.Error(http.StatusBadGateway, err) 599 | } 600 | targetConn.Write(rbuf) 601 | } 602 | } 603 | // Since we hijacked the connection, we lost the ability to write and flush headers via w. 604 | // Let's handcraft the response and send it manually. 605 | res := &http.Response{StatusCode: http.StatusOK, 606 | Proto: "HTTP/1.1", 607 | ProtoMajor: 1, 608 | ProtoMinor: 1, 609 | Header: make(http.Header), 610 | } 611 | res.Header.Set("Server", "Caddy") 612 | 613 | err = res.Write(clientConn) 614 | if err != nil { 615 | return caddyhttp.Error(http.StatusInternalServerError, 616 | fmt.Errorf("failed to send response to client: %v", err)) 617 | } 618 | 619 | return dualStream(targetConn, clientConn, clientConn, false) 620 | } 621 | 622 | const ( 623 | NoPadding = 0 624 | AddPadding = 1 625 | RemovePadding = 2 626 | NumFirstPaddings = 8 627 | ) 628 | 629 | // Copies data target->clientReader and clientWriter->target, and flushes as needed 630 | // Returns when clientWriter-> target stream is done. 631 | // Caddy should finish writing target -> clientReader. 632 | func dualStream(target net.Conn, clientReader io.ReadCloser, clientWriter io.Writer, padding bool) error { 633 | stream := func(w io.Writer, r io.Reader, paddingType int) error { 634 | // copy bytes from r to w 635 | buf := bufferPool.Get().([]byte) 636 | buf = buf[0:cap(buf)] 637 | _, _err := flushingIoCopy(w, r, buf, paddingType) 638 | bufferPool.Put(buf) 639 | if cw, ok := w.(closeWriter); ok { 640 | cw.CloseWrite() 641 | } 642 | return _err 643 | } 644 | if padding { 645 | go stream(target, clientReader, RemovePadding) 646 | return stream(clientWriter, target, AddPadding) 647 | } else { 648 | go stream(target, clientReader, NoPadding) 649 | return stream(clientWriter, target, NoPadding) 650 | } 651 | } 652 | 653 | type closeWriter interface { 654 | CloseWrite() error 655 | } 656 | 657 | // flushingIoCopy is analogous to buffering io.Copy(), but also attempts to flush on each iteration. 658 | // If dst does not implement http.Flusher(e.g. net.TCPConn), it will do a simple io.CopyBuffer(). 659 | // Reasoning: http2ResponseWriter will not flush on its own, so we have to do it manually. 660 | func flushingIoCopy(dst io.Writer, src io.Reader, buf []byte, paddingType int) (written int64, err error) { 661 | flusher, hasFlusher := dst.(http.Flusher) 662 | var numPadding int 663 | for { 664 | var nr int 665 | var er error 666 | if paddingType == AddPadding && numPadding < NumFirstPaddings { 667 | numPadding++ 668 | paddingSize := rand.Intn(256) 669 | maxRead := 65536 - 3 - paddingSize 670 | nr, er = src.Read(buf[3:maxRead]) 671 | if nr > 0 { 672 | buf[0] = byte(nr / 256) 673 | buf[1] = byte(nr % 256) 674 | buf[2] = byte(paddingSize) 675 | for i := 0; i < paddingSize; i++ { 676 | buf[3+nr+i] = 0 677 | } 678 | nr += 3 + paddingSize 679 | } 680 | } else if paddingType == RemovePadding && numPadding < NumFirstPaddings { 681 | numPadding++ 682 | nr, er = io.ReadFull(src, buf[0:3]) 683 | if nr > 0 { 684 | nr = int(buf[0])*256 + int(buf[1]) 685 | paddingSize := int(buf[2]) 686 | nr, er = io.ReadFull(src, buf[0:nr]) 687 | if nr > 0 { 688 | var junk [256]byte 689 | _, er = io.ReadFull(src, junk[0:paddingSize]) 690 | } 691 | } 692 | } else { 693 | nr, er = src.Read(buf) 694 | } 695 | if nr > 0 { 696 | nw, ew := dst.Write(buf[0:nr]) 697 | if hasFlusher { 698 | flusher.Flush() 699 | } 700 | if nw > 0 { 701 | written += int64(nw) 702 | } 703 | if ew != nil { 704 | err = ew 705 | break 706 | } 707 | if nr != nw { 708 | err = io.ErrShortWrite 709 | break 710 | } 711 | } 712 | if er != nil { 713 | if er != io.EOF { 714 | err = er 715 | } 716 | break 717 | } 718 | } 719 | return 720 | } 721 | 722 | // Removes hop-by-hop headers, and writes response into ResponseWriter. 723 | func forwardResponse(w http.ResponseWriter, response *http.Response) error { 724 | w.Header().Del("Server") // remove Server: Caddy, append via instead 725 | w.Header().Add("Via", strconv.Itoa(response.ProtoMajor)+"."+strconv.Itoa(response.ProtoMinor)+" caddy") 726 | 727 | for header, values := range response.Header { 728 | for _, val := range values { 729 | w.Header().Add(header, val) 730 | } 731 | } 732 | removeHopByHop(w.Header()) 733 | w.WriteHeader(response.StatusCode) 734 | buf := bufferPool.Get().([]byte) 735 | buf = buf[0:cap(buf)] 736 | _, err := io.CopyBuffer(w, response.Body, buf) 737 | bufferPool.Put(buf) 738 | return err 739 | } 740 | 741 | func removeHopByHop(header http.Header) { 742 | connectionHeaders := header.Get("Connection") 743 | for _, h := range strings.Split(connectionHeaders, ",") { 744 | header.Del(strings.TrimSpace(h)) 745 | } 746 | for _, h := range hopByHopHeaders { 747 | header.Del(h) 748 | } 749 | } 750 | 751 | var hopByHopHeaders = []string{ 752 | "Keep-Alive", 753 | "Proxy-Authenticate", 754 | "Proxy-Authorization", 755 | "Upgrade", 756 | "Connection", 757 | "Proxy-Connection", 758 | "Te", 759 | "Trailer", 760 | "Transfer-Encoding", 761 | } 762 | 763 | const pacFile = ` 764 | function FindProxyForURL(url, host) { 765 | if (host === "127.0.0.1" || host === "::1" || host === "localhost") 766 | return "DIRECT"; 767 | return "HTTPS %s"; 768 | } 769 | ` 770 | 771 | var bufferPool = sync.Pool{ 772 | New: func() interface{} { 773 | return make([]byte, 0, 64*1024) 774 | }, 775 | } 776 | 777 | ////// used during provision only 778 | 779 | func isLocalhost(hostname string) bool { 780 | return hostname == "localhost" || 781 | hostname == "127.0.0.1" || 782 | hostname == "::1" 783 | } 784 | 785 | type dialContexter interface { 786 | DialContext(ctx context.Context, network, address string) (net.Conn, error) 787 | } 788 | 789 | // ProbeResistance configures probe resistance. 790 | type ProbeResistance struct { 791 | Domain string `json:"domain,omitempty"` 792 | } 793 | 794 | func readLinesFromFile(filename string) ([]string, error) { 795 | file, err := os.Open(filename) 796 | if err != nil { 797 | return nil, err 798 | } 799 | defer file.Close() 800 | 801 | var hostnames []string 802 | scanner := bufio.NewScanner(file) 803 | for scanner.Scan() { 804 | hostnames = append(hostnames, scanner.Text()) 805 | } 806 | 807 | return hostnames, scanner.Err() 808 | } 809 | 810 | // Interface guards 811 | var ( 812 | _ caddy.Provisioner = (*Handler)(nil) 813 | _ caddyhttp.MiddlewareHandler = (*Handler)(nil) 814 | _ caddyfile.Unmarshaler = (*Handler)(nil) 815 | ) 816 | --------------------------------------------------------------------------------