├── 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 |
--------------------------------------------------------------------------------