Begin refactor for Caddy 2

All the tests pass. I tried to keep as much as possible the same, but a
few things don't translate well to Caddy 2, notably one test in
probe_resist_test.go on L200, I had to change that test case since I
didn't quite understand why it was the way it was before.

Does not yet have v2 Caddyfile support.
This commit is contained in:
Matthew Holt 2020-04-20 17:22:47 -06:00
parent 247c0bafaa
commit e92fe979d0
13 changed files with 2168 additions and 1359 deletions

29
acl.go
View file

@ -6,6 +6,11 @@ import (
"strings"
)
type ACLRule struct {
Subjects []string `json:"subjects,omitempty"`
Allow bool `json:"allow,omitempty"`
}
type aclDecision uint8
const (
@ -65,7 +70,7 @@ func (a *aclAllRule) tryMatch(ip net.IP, domain string) aclDecision {
return aclDecisionDeny
}
func newAclRule(ruleSubject string, allow bool) (aclRule, error) {
func newACLRule(ruleSubject string, allow bool) (aclRule, error) {
if ruleSubject == "all" {
return &aclAllRule{allow: allow}, nil
}
@ -94,3 +99,25 @@ func newAclRule(ruleSubject string, allow bool) (aclRule, error) {
}
return &aclDomainRule{domain: ruleSubject, subdomainsAllowed: subdomainsAllowed, allow: allow}, nil
}
// isValidDomainLite shamelessly rejects non-LDH names. returns nil if domains seems valid
func isValidDomainLite(domain string) error {
for i := 0; i < len(domain); i++ {
c := domain[i]
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_' || '0' <= c && c <= '9' ||
c == '-' || c == '.' {
continue
}
return errors.New("character " + string(c) + " is not allowed")
}
sections := strings.Split(domain, ".")
for _, s := range sections {
if len(s) == 0 {
return errors.New("empty section between dots in domain name or trailing dot")
}
if len(s) > 63 {
return errors.New("domain name section is too long")
}
}
return nil
}

View file

@ -9,15 +9,14 @@ import (
test port blocking working
test blacklist allowed
test blacklist refused with correct status
*/
func TestWhitelistAllowing(t *testing.T) {
useTls := true
for _, httpProxyVer := range testHttpProxyVersions {
const useTLS, dialLocal = true, true
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyWhiteListing.addr, httpProxyVer,
"", useTls)
"", useTLS, dialLocal)
if err != nil {
t.Fatal(err)
} else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil {
@ -28,11 +27,11 @@ func TestWhitelistAllowing(t *testing.T) {
}
func TestWhitelistBlocking(t *testing.T) {
useTls := true
for _, httpProxyVer := range testHttpProxyVersions {
const useTLS, dialLocal = true, false
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy(caddyHTTPTestTarget.addr, resource, caddyForwardProxyWhiteListing.addr, httpProxyVer,
"", useTls)
"", useTLS, dialLocal)
if err != nil {
t.Fatal(err)
} else if response.StatusCode != http.StatusForbidden {
@ -41,10 +40,10 @@ func TestWhitelistBlocking(t *testing.T) {
}
}
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy("google.com:6451", resource, caddyForwardProxyWhiteListing.addr, httpProxyVer,
"", useTls)
"", useTLS, dialLocal)
if err != nil {
t.Fatal(err)
} else if response.StatusCode != http.StatusForbidden {
@ -55,11 +54,11 @@ func TestWhitelistBlocking(t *testing.T) {
}
func TestLocalhostDefaultForbidden(t *testing.T) {
useTls := true
for _, httpProxyVer := range testHttpProxyVersions {
const useTLS, dialLocal = true, false
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy("localhost:6451", resource, caddyForwardProxyNoBlacklistOverride.addr, httpProxyVer,
"", useTls)
"", useTLS, dialLocal)
if err != nil {
t.Fatal(err)
} else if response.StatusCode != http.StatusForbidden {
@ -68,10 +67,10 @@ func TestLocalhostDefaultForbidden(t *testing.T) {
}
}
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy("127.0.0.1:808", resource, caddyForwardProxyNoBlacklistOverride.addr, httpProxyVer,
"", useTls)
"", useTLS, dialLocal)
if err != nil {
t.Fatal(err)
} else if response.StatusCode != http.StatusForbidden {
@ -80,10 +79,10 @@ func TestLocalhostDefaultForbidden(t *testing.T) {
}
}
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy("[::1]:8080", resource, caddyForwardProxyNoBlacklistOverride.addr, httpProxyVer,
"", useTls)
"", useTLS, dialLocal)
if err != nil {
t.Fatal(err)
} else if response.StatusCode != http.StatusForbidden {
@ -94,11 +93,11 @@ func TestLocalhostDefaultForbidden(t *testing.T) {
}
func TestLocalNetworksDefaultForbidden(t *testing.T) {
useTls := true
for _, httpProxyVer := range testHttpProxyVersions {
const useTLS, dialLocal = true, false
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy("10.0.0.0:80", resource, caddyForwardProxyNoBlacklistOverride.addr, httpProxyVer,
"", useTls)
"", useTLS, dialLocal)
if err != nil {
t.Fatal(err)
} else if response.StatusCode != http.StatusForbidden {
@ -107,10 +106,10 @@ func TestLocalNetworksDefaultForbidden(t *testing.T) {
}
}
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy("127.222.34.1:443", resource, caddyForwardProxyNoBlacklistOverride.addr, httpProxyVer,
"", useTls)
"", useTLS, dialLocal)
if err != nil {
t.Fatal(err)
} else if response.StatusCode != http.StatusForbidden {
@ -119,10 +118,10 @@ func TestLocalNetworksDefaultForbidden(t *testing.T) {
}
}
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy("172.16.0.1:8080", resource, caddyForwardProxyNoBlacklistOverride.addr, httpProxyVer,
"", useTls)
"", useTLS, dialLocal)
if err != nil {
t.Fatal(err)
} else if response.StatusCode != http.StatusForbidden {
@ -131,10 +130,10 @@ func TestLocalNetworksDefaultForbidden(t *testing.T) {
}
}
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy("192.168.192.168:888", resource, caddyForwardProxyNoBlacklistOverride.addr, httpProxyVer,
"", useTls)
"", useTLS, dialLocal)
if err != nil {
t.Fatal(err)
} else if response.StatusCode != http.StatusForbidden {
@ -145,11 +144,11 @@ func TestLocalNetworksDefaultForbidden(t *testing.T) {
}
func TestBlacklistBlocking(t *testing.T) {
useTls := true
for _, httpProxyVer := range testHttpProxyVersions {
const useTLS, dialLocal = true, false
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy(blacklistedDomain, resource, caddyForwardProxyBlackListing.addr, httpProxyVer,
"", useTls)
"", useTLS, dialLocal)
if err != nil {
t.Fatal(err)
} else if response.StatusCode != http.StatusForbidden {
@ -158,10 +157,10 @@ func TestBlacklistBlocking(t *testing.T) {
}
}
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy(blacklistedIPv4, resource, caddyForwardProxyBlackListing.addr, httpProxyVer,
"", useTls)
"", useTLS, dialLocal)
if err != nil {
t.Fatal(err)
} else if response.StatusCode != http.StatusForbidden {
@ -170,10 +169,10 @@ func TestBlacklistBlocking(t *testing.T) {
}
}
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy("["+blacklistedIPv6+"]:80", resource, caddyForwardProxyBlackListing.addr, httpProxyVer,
"", useTls)
"", useTLS, dialLocal)
if err != nil {
t.Fatal(err)
} else if response.StatusCode != http.StatusForbidden {
@ -184,11 +183,11 @@ func TestBlacklistBlocking(t *testing.T) {
}
func TestBlacklistAllowing(t *testing.T) {
useTls := true
for _, httpProxyVer := range testHttpProxyVersions {
const useTLS, dialLocal = true, true
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyBlackListing.addr, httpProxyVer,
"", useTls)
"", useTLS, dialLocal)
if err != nil {
t.Fatal(err)
} else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil {

View file

@ -1,12 +0,0 @@
package main
import (
"github.com/caddyserver/caddy/caddy/caddymain"
_ "github.com/caddyserver/forwardproxy"
)
func main() {
caddymain.EnableTelemetry = false
caddymain.Run()
}

View file

@ -1,19 +1,27 @@
package forwardproxy
import (
"context"
"crypto/tls"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httputil"
"os"
"strings"
"strconv"
"testing"
"time"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver"
"github.com/caddyserver/caddy/v2/modules/caddypki"
"github.com/caddyserver/caddy/v2/modules/caddytls"
)
var credentialsEmpty = ""
@ -36,10 +44,10 @@ GET/CONNECT -- get gets, connect connects and gets
Auth/NoAuth
Empty/Correct/Wrong -- tries different credentials
*/
var testResources = []string{"", "/pic.png"}
var testHttpProxyVersions = []string{"HTTP/2.0", "HTTP/1.1"}
var testHttpTargetVersions = []string{"HTTP/1.1"}
var httpVersionToAlpn = map[string]string{
var testResources = []string{"/", "/pic.png"}
var testHTTPProxyVersions = []string{"HTTP/2.0", "HTTP/1.1"}
var testHTTPTargetVersions = []string{"HTTP/1.1"}
var httpVersionToALPN = map[string]string{
"HTTP/1.1": "http/1.1",
"HTTP/2.0": "h2",
}
@ -49,15 +57,15 @@ var blacklistedIPv4 = "8.8.8.8"
var blacklistedIPv6 = "2001:4860:4860::8888"
type caddyTestServer struct {
*caddy.Instance
addr string // could be http or https
addr string
tls bool
HTTPRedirectPort string // used in probe-resist tests to simulate default Caddy's http->https redirect
root string // expected to have index.html and pic.png
directives []string
proxyEnabled bool
proxyDirectives []string
contents map[string][]byte
httpRedirPort string // used in probe-resist tests to simulate default Caddy's http->https redirect
root string // expected to have index.html and pic.png
directives []string
proxyHandler *Handler
contents map[string][]byte
}
var (
@ -78,40 +86,61 @@ var (
caddyHTTPTestTarget caddyTestServer // serves plain http on 6480
)
func (c *caddyTestServer) marshal() []byte {
mainBlock := []string{c.addr + " {",
"root " + c.root}
mainBlock = append(mainBlock, c.directives...)
if c.proxyEnabled {
if len(c.proxyDirectives) == 0 {
mainBlock = append(mainBlock, "forwardproxy")
} else {
forwardProxyBlock := []string{"forwardproxy {"}
forwardProxyBlock = append(forwardProxyBlock, strings.Join(c.proxyDirectives, "\n"))
forwardProxyBlock = append(forwardProxyBlock, "}")
mainBlock = append(mainBlock, strings.Join(forwardProxyBlock, "\n"))
}
}
mainBlock = append(mainBlock, "}")
if len(c.HTTPRedirectPort) > 0 {
// TODO: this is not good enough, since `func redirPlaintextHost(cfg *SiteConfig) *SiteConfig`
// https://github.com/caddyserver/caddy/blob/master/caddyhttp/httpserver/https.go#L142 can change in future
// and we won't know.
redirectBlock := []string{"http://*:" + c.HTTPRedirectPort + " {",
"redir https://" + c.addr + "{uri}",
"header / Connection close",
"}"}
mainBlock = append(mainBlock, redirectBlock...)
}
return []byte(strings.Join(mainBlock, "\n"))
}
func (c *caddyTestServer) StartTestServer() {
var err error
c.Instance, err = caddy.Start(caddy.CaddyfileInput{Contents: c.marshal(), ServerTypeName: "http"})
func (c *caddyTestServer) server() *caddyhttp.Server {
host, port, err := net.SplitHostPort(c.addr)
if err != nil {
panic(err)
}
handlerJSON := func(h caddyhttp.MiddlewareHandler) json.RawMessage {
return caddyconfig.JSONModuleObject(h, "handler", h.(caddy.Module).CaddyModule().ID.Name(), nil)
}
// create the routes
var routes caddyhttp.RouteList
if c.proxyHandler != nil {
if host != "" {
if c.tls {
// cheap hack for our tests to get TLS certs for the hostnames that
// it needs TLS certs for: create an empty route with a single host
// matcher for that hostname, and auto HTTPS will do the rest
hostMatcherJSON, err := json.Marshal(caddyhttp.MatchHost{host})
if err != nil {
panic(err)
}
matchersRaw := caddyhttp.RawMatcherSets{
caddy.ModuleMap{"host": hostMatcherJSON},
}
routes = append(routes, caddyhttp.Route{MatcherSetsRaw: matchersRaw})
}
// tell the proxy which hostname to serve the proxy on; this must
// be distinct from the host matcher, since the proxy basically
// does its own host matching
c.proxyHandler.Hosts = caddyhttp.MatchHost{host}
}
routes = append(routes, caddyhttp.Route{
HandlersRaw: []json.RawMessage{handlerJSON(c.proxyHandler)},
})
}
if c.root != "" {
routes = append(routes, caddyhttp.Route{
HandlersRaw: []json.RawMessage{
handlerJSON(&fileserver.FileServer{Root: c.root}),
},
})
}
srv := &caddyhttp.Server{
Listen: []string{":" + port},
Routes: routes,
}
if c.tls {
srv.TLSConnPolicies = caddytls.ConnectionPolicies{{}}
} else {
srv.AutoHTTPS = &caddyhttp.AutoHTTPSConfig{Disabled: true}
}
if c.contents == nil {
c.contents = make(map[string][]byte)
}
@ -122,93 +151,216 @@ func (c *caddyTestServer) StartTestServer() {
c.contents[""] = index
c.contents["/"] = index
c.contents["/index.html"] = index
c.contents["/pic.png"], err = ioutil.ReadFile(c.root + "/pic.png")
if err != nil {
panic(err)
}
return srv
}
// For simulating/mimicing Caddy's built-in auto-HTTPS redirects. Super hacky but w/e.
func (c *caddyTestServer) redirServer() *caddyhttp.Server {
return &caddyhttp.Server{
Listen: []string{":" + c.httpRedirPort},
Routes: caddyhttp.RouteList{
{
Handlers: []caddyhttp.MiddlewareHandler{
caddyhttp.StaticResponse{
StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
Headers: http.Header{
"Location": []string{"https://" + c.addr + "/{http.request.uri}"},
"Connection": []string{"close"},
},
Close: true,
},
},
},
},
}
}
func TestMain(m *testing.M) {
caddyForwardProxy = caddyTestServer{addr: "127.0.19.84:1984", root: "./test/forwardproxy",
directives: []string{"tls self_signed"},
proxyEnabled: true, proxyDirectives: []string{"serve_pac",
"acl {\nallow all\n}"}}
caddyForwardProxy.StartTestServer()
caddyForwardProxyAuth = caddyTestServer{addr: "127.0.0.1:4891", root: "./test/forwardproxy",
directives: []string{"tls self_signed"},
proxyEnabled: true, proxyDirectives: []string{"basicauth test pass",
"acl {\nallow all\n}"}}
caddyForwardProxyAuth.StartTestServer()
caddyHTTPForwardProxyAuth = caddyTestServer{addr: "127.0.69.73:6973", root: "./test/forwardproxy",
directives: []string{"tls off"},
proxyEnabled: true, proxyDirectives: []string{"basicauth test pass",
"acl {\nallow all\n}"}}
caddyHTTPForwardProxyAuth.StartTestServer()
caddyForwardProxyProbeResist = caddyTestServer{addr: "127.0.88.88:8888", root: "./test/forwardproxy",
directives: []string{"tls self_signed"}, HTTPRedirectPort: "8880",
proxyEnabled: true, proxyDirectives: []string{"basicauth test pass",
"probe_resistance test.localhost",
"serve_pac superhiddenfile.pac",
"acl {\nallow all\n}"}}
caddyForwardProxyProbeResist.StartTestServer()
caddyDummyProbeResist = caddyTestServer{addr: "127.0.99.99:9999", root: "./test/forwardproxy",
directives: []string{"tls self_signed"}, HTTPRedirectPort: "9980",
proxyEnabled: false}
caddyDummyProbeResist.StartTestServer()
// 127.0.0.1 and localhost are both used to avoid Caddy matching and routing proxy requests internally
caddyTestTarget = caddyTestServer{addr: "127.0.64.51:6451", root: "./test/index",
directives: []string{},
proxyEnabled: false}
caddyTestTarget.StartTestServer()
caddyHTTPTestTarget = caddyTestServer{addr: "localhost:6480", root: "./test/index",
directives: []string{"tls off"},
proxyEnabled: false}
caddyHTTPTestTarget.StartTestServer()
caddyAuthedUpstreamEnter = caddyTestServer{addr: "127.0.65.25:6585", root: "./test/upstreamingproxy",
directives: []string{"tls self_signed"},
proxyEnabled: true, proxyDirectives: []string{"upstream https://test:pass@127.0.0.1:4891",
"basicauth upstreamtest upstreampass"}}
caddyAuthedUpstreamEnter.StartTestServer()
caddyForwardProxyWhiteListing = caddyTestServer{addr: "127.0.87.76:8776", root: "./test/forwardproxy",
directives: []string{"tls self_signed"},
proxyEnabled: true, proxyDirectives: []string{"acl {\nallow 127.0.64.51\n deny all\n}",
"ports 6451"}}
caddyForwardProxyWhiteListing.StartTestServer()
caddyForwardProxyBlackListing = caddyTestServer{addr: "127.0.66.76:6676", root: "./test/forwardproxy",
directives: []string{"tls self_signed"},
proxyEnabled: true, proxyDirectives: []string{"acl {\ndeny " + blacklistedIPv4 + "/30\n" +
"deny " + blacklistedIPv6 + "\nallow all\n}"},
caddyForwardProxy = caddyTestServer{
addr: "127.0.19.84:1984",
root: "./test/forwardproxy",
tls: true,
proxyHandler: &Handler{
PACPath: defaultPACPath,
ACL: []ACLRule{{Allow: true, Subjects: []string{"all"}}},
},
}
caddyForwardProxyBlackListing.StartTestServer()
caddyForwardProxyNoBlacklistOverride = caddyTestServer{addr: "127.0.66.79:6679", root: "./test/forwardproxy",
directives: []string{"tls self_signed"},
proxyEnabled: true, proxyDirectives: []string{}}
caddyForwardProxyNoBlacklistOverride.StartTestServer()
caddyForwardProxyAuth = caddyTestServer{
addr: "127.0.0.1:4891",
root: "./test/forwardproxy",
tls: true,
proxyHandler: &Handler{
PACPath: defaultPACPath,
ACL: []ACLRule{{Subjects: []string{"all"}, Allow: true}},
BasicauthUser: "test",
BasicauthPass: "pass",
},
}
caddyHTTPForwardProxyAuth = caddyTestServer{
addr: "127.0.69.73:6973",
root: "./test/forwardproxy",
proxyHandler: &Handler{
PACPath: defaultPACPath,
ACL: []ACLRule{{Subjects: []string{"all"}, Allow: true}},
BasicauthUser: "test",
BasicauthPass: "pass",
},
}
caddyForwardProxyProbeResist = caddyTestServer{
addr: "127.0.88.88:8888",
root: "./test/forwardproxy",
tls: true,
proxyHandler: &Handler{
PACPath: "/superhiddenfile.pac",
ACL: []ACLRule{{Subjects: []string{"all"}, Allow: true}},
ProbeResistance: &ProbeResistance{Domain: "test.localhost"},
BasicauthUser: "test",
BasicauthPass: "pass",
},
httpRedirPort: "8880",
}
caddyDummyProbeResist = caddyTestServer{
addr: "127.0.99.99:9999",
root: "./test/forwardproxy",
tls: true,
httpRedirPort: "9980",
}
caddyTestTarget = caddyTestServer{
addr: "127.0.64.51:6451",
root: "./test/index",
}
caddyHTTPTestTarget = caddyTestServer{
addr: "localhost:6480",
root: "./test/index",
}
caddyAuthedUpstreamEnter = caddyTestServer{
addr: "127.0.65.25:6585",
root: "./test/upstreamingproxy",
tls: true,
proxyHandler: &Handler{
Upstream: "https://test:pass@127.0.0.1:4891",
BasicauthUser: "upstreamtest",
BasicauthPass: "upstreampass",
},
}
caddyForwardProxyWhiteListing = caddyTestServer{
addr: "127.0.87.76:8776",
root: "./test/forwardproxy",
tls: true,
proxyHandler: &Handler{
ACL: []ACLRule{
{Subjects: []string{"127.0.0.1"}, Allow: true},
{Subjects: []string{"all"}, Allow: false},
},
WhitelistedPorts: []int{6451},
},
}
caddyForwardProxyBlackListing = caddyTestServer{
addr: "127.0.66.76:6676",
root: "./test/forwardproxy",
tls: true,
proxyHandler: &Handler{
ACL: []ACLRule{
{Subjects: []string{blacklistedIPv4 + "/30"}, Allow: false},
{Subjects: []string{blacklistedIPv6}, Allow: false},
{Subjects: []string{"all"}, Allow: true},
},
},
}
caddyForwardProxyNoBlacklistOverride = caddyTestServer{
addr: "127.0.66.76:6679",
root: "./test/forwardproxy",
tls: true,
proxyHandler: &Handler{},
}
// done configuring all the servers; now build the HTTP app
httpApp := caddyhttp.App{
Servers: map[string]*caddyhttp.Server{
"caddyForwardProxy": caddyForwardProxy.server(),
"caddyForwardProxyAuth": caddyForwardProxyAuth.server(),
"caddyHTTPForwardProxyAuth": caddyHTTPForwardProxyAuth.server(),
"caddyForwardProxyProbeResist": caddyForwardProxyProbeResist.server(),
"caddyDummyProbeResist": caddyDummyProbeResist.server(),
"caddyTestTarget": caddyTestTarget.server(),
"caddyHTTPTestTarget": caddyHTTPTestTarget.server(),
"caddyAuthedUpstreamEnter": caddyAuthedUpstreamEnter.server(),
"caddyForwardProxyWhiteListing": caddyForwardProxyWhiteListing.server(),
"caddyForwardProxyBlackListing": caddyForwardProxyBlackListing.server(),
"caddyForwardProxyNoBlacklistOverride": caddyForwardProxyNoBlacklistOverride.server(),
// HTTP->HTTPS redirect simulation servers for those which have a redir port configured
"caddyForwardProxyProbeResist_redir": caddyForwardProxyProbeResist.redirServer(),
"caddyDummyProbeResist_redir": caddyDummyProbeResist.redirServer(),
},
GracePeriod: caddy.Duration(1 * time.Second), // keep tests fast
}
httpAppJSON, err := json.Marshal(httpApp)
if err != nil {
panic(err)
}
// ensure we always use internal issuer and not a public CA
tlsApp := caddytls.TLS{
Automation: &caddytls.AutomationConfig{
Policies: []*caddytls.AutomationPolicy{
{
IssuerRaw: json.RawMessage(`{"module": "internal"}`),
},
},
},
}
tlsAppJSON, err := json.Marshal(tlsApp)
if err != nil {
panic(err)
}
// configure the default CA so that we don't try to install trust, just for our tests
falseBool := false
pkiApp := caddypki.PKI{
CAs: map[string]*caddypki.CA{
"local": {InstallTrust: &falseBool},
},
}
pkiAppJSON, err := json.Marshal(pkiApp)
if err != nil {
panic(err)
}
// build final config
cfg := &caddy.Config{
Admin: &caddy.AdminConfig{Disabled: true},
AppsRaw: caddy.ModuleMap{
"http": httpAppJSON,
"tls": tlsAppJSON,
"pki": pkiAppJSON,
},
}
// start the engines
err = caddy.Run(cfg)
if err != nil {
panic(err)
}
retCode := m.Run()
caddyForwardProxy.Stop()
caddyForwardProxyAuth.Stop()
caddyHTTPForwardProxyAuth.Stop()
caddyForwardProxyProbeResist.Stop()
caddyDummyProbeResist.Stop()
caddyTestTarget.Stop()
caddyHTTPTestTarget.Stop()
caddyAuthedUpstreamEnter.Stop()
caddyForwardProxyWhiteListing.Stop()
caddyForwardProxyBlackListing.Stop()
caddyForwardProxyNoBlacklistOverride.Stop()
caddy.Stop()
os.Exit(retCode)
}
@ -216,11 +368,7 @@ func TestMain(m *testing.M) {
// This is a sanity check confirming that target servers actually directly serve what they are expected to.
// (And that they don't serve what they should not)
func TestTheTest(t *testing.T) {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
ResponseHeaderTimeout: 2 * time.Second,
}
client := &http.Client{Transport: tr, Timeout: 2 * time.Second}
client := &http.Client{Transport: testTransport, Timeout: 2 * time.Second}
// Request index
resp, err := client.Get("http://" + caddyTestTarget.addr)
@ -307,9 +455,8 @@ func httpdump(r interface{}) string {
b, err := httputil.DumpRequest(v, true)
if err != nil {
return err.Error()
} else {
return string(b)
}
return string(b)
case *http.Response:
if v == nil {
return "httpdump: nil"
@ -317,10 +464,27 @@ func httpdump(r interface{}) string {
b, err := httputil.DumpResponse(v, true)
if err != nil {
return err.Error()
} else {
return string(b)
}
return string(b)
default:
return "httpdump: wrong type"
}
}
var testTransport = &http.Transport{
ResponseHeaderTimeout: 2 * time.Second,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
// always dial localhost for testing purposes
return new(net.Dialer).DialContext(ctx, network, localDialAddr(addr))
},
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
// always dial localhost for testing purposes
conn, err := new(net.Dialer).DialContext(ctx, network, localDialAddr(addr))
if err != nil {
return nil, err
}
return tls.Client(conn, &tls.Config{InsecureSkipVerify: true}), nil
},
}
const defaultPACPath = "/proxy.pac"

File diff suppressed because it is too large Load diff

View file

@ -17,54 +17,65 @@ package forwardproxy
import (
"bufio"
"crypto/tls"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"strings"
"testing"
"time"
_ "github.com/caddyserver/caddy/caddyhttp/header"
_ "github.com/caddyserver/caddy/caddyhttp/httpserver"
_ "github.com/caddyserver/caddy/caddyhttp/redirect"
_ "github.com/caddyserver/caddy/caddyhttp/root"
"github.com/caddyserver/forwardproxy/httpclient"
"golang.org/x/net/http2"
)
func dial(proxyAddr, httpProxyVer string, useTls bool) (net.Conn, error) {
if useTls {
return tls.Dial("tcp", proxyAddr, &tls.Config{InsecureSkipVerify: true,
NextProtos: []string{httpVersionToAlpn[httpProxyVer]}})
} else {
return net.Dial("tcp", proxyAddr)
// localDialAddr changes the host portion of addr to be loopback,
// which is useful since we're just testing.
func localDialAddr(addr string) string {
_, port, err := net.SplitHostPort(addr)
if err != nil {
panic(err)
}
return net.JoinHostPort("127.0.0.1", port)
}
func getViaProxy(targetHost, resource, proxyAddr, httpProxyVer, proxyCredentials string, useTls bool) (*http.Response, error) {
proxyConn, err := dial(proxyAddr, httpProxyVer, useTls)
func dial(proxyAddr, httpProxyVer string, useTLS bool) (net.Conn, error) {
// always dial localhost for testing purposes
dialAddr := localDialAddr(proxyAddr)
if useTLS {
return tls.Dial("tcp", dialAddr, &tls.Config{
InsecureSkipVerify: true,
NextProtos: []string{httpVersionToALPN[httpProxyVer]},
})
}
return net.Dial("tcp", dialAddr)
}
func getViaProxy(targetHost, resource, proxyAddr, httpProxyVer, proxyCredentials string, useTLS, dialLocal bool) (*http.Response, error) {
proxyConn, err := dial(proxyAddr, httpProxyVer, useTLS)
if err != nil {
return nil, err
}
return getResourceViaProxyConn(proxyConn, targetHost, resource, httpProxyVer, proxyCredentials)
return getResourceViaProxyConn(proxyConn, targetHost, resource, httpProxyVer, proxyCredentials, dialLocal)
}
// if connect is not successful - that response is returned, otherwise the requested resource
func connectAndGetViaProxy(targetHost, resource, proxyAddr, httpTargetVer, proxyCredentials, httpProxyVer string, useTls bool) (*http.Response, error) {
proxyConn, err := dial(proxyAddr, httpProxyVer, useTls)
func connectAndGetViaProxy(targetHost, resource, proxyAddr, httpTargetVer, proxyCredentials, httpProxyVer string, useTLS, dialLocal bool) (*http.Response, error) {
proxyConn, err := dial(proxyAddr, httpProxyVer, useTLS)
if err != nil {
return nil, err
}
req := http.Request{Header: make(http.Header)}
req := &http.Request{Header: make(http.Header)}
if len(proxyCredentials) > 0 {
req.Header.Set("Proxy-Authorization", proxyCredentials)
}
req.Host = targetHost
if dialLocal {
req.Host = localDialAddr(targetHost)
} else {
req.Host = targetHost
}
req.URL, err = url.Parse("https://" + req.Host)
if err != nil {
return nil, err
@ -85,7 +96,7 @@ func connectAndGetViaProxy(targetHost, resource, proxyAddr, httpTargetVer, proxy
if err != nil {
return nil, err
}
resp, err = clientConn.RoundTrip(&req)
resp, err = clientConn.RoundTrip(req)
if err != nil {
return resp, err
}
@ -94,7 +105,7 @@ func connectAndGetViaProxy(targetHost, resource, proxyAddr, httpTargetVer, proxy
req.ProtoMajor = 1
req.ProtoMinor = 1
req.Write(proxyConn)
resp, err = http.ReadResponse(bufio.NewReader(proxyConn), &req)
resp, err = http.ReadResponse(bufio.NewReader(proxyConn), req)
if err != nil {
return resp, err
}
@ -109,18 +120,22 @@ func connectAndGetViaProxy(targetHost, resource, proxyAddr, httpTargetVer, proxy
return resp, err
}
return getResourceViaProxyConn(proxyConn, targetHost, resource, httpTargetVer, proxyCredentials)
return getResourceViaProxyConn(proxyConn, targetHost, resource, httpTargetVer, proxyCredentials, dialLocal)
}
func getResourceViaProxyConn(proxyConn net.Conn, targetHost, resource, httpTargetVer, proxyCredentials string) (*http.Response, error) {
func getResourceViaProxyConn(proxyConn net.Conn, targetHost, resource, httpTargetVer, proxyCredentials string, dialLocal bool) (*http.Response, error) {
var err error
req := http.Request{Header: make(http.Header)}
req := &http.Request{Header: make(http.Header)}
if len(proxyCredentials) > 0 {
req.Header.Set("Proxy-Authorization", proxyCredentials)
}
req.Host = targetHost
req.URL, err = url.Parse("http://" + req.Host + resource)
if dialLocal {
req.Host = localDialAddr(targetHost)
} else {
req.Host = targetHost
}
req.URL, err = url.Parse("http://" + targetHost + resource)
if err != nil {
return nil, err
}
@ -137,14 +152,14 @@ func getResourceViaProxyConn(proxyConn net.Conn, targetHost, resource, httpTarge
if err != nil {
return nil, err
}
return clientConn.RoundTrip(&req)
return clientConn.RoundTrip(req)
case "HTTP/1.1":
req.ProtoMajor = 1
req.ProtoMinor = 1
t := http.Transport{Dial: func(network, addr string) (net.Conn, error) {
return proxyConn, nil
}}
return t.RoundTrip(&req)
return t.RoundTrip(req)
default:
panic("proxy ver: " + httpTargetVer)
}
@ -165,30 +180,26 @@ func responseExpected(res *http.Response, expectedResponse []byte) error {
panic(err)
}
if nTotal == responseLen {
return errors.New(fmt.Sprintf("nTotal == responseLen, but haven't seen io.EOF. Expected response: %s\nGot: %s\n",
expectedResponse, response))
return fmt.Errorf("nTotal == responseLen, but haven't seen io.EOF. Expected response: %s\nGot: %s",
expectedResponse, response)
}
}
response = response[:nTotal]
if len(expectedResponse) != len(response) {
return errors.New(fmt.Sprintf("Expected length: %d. Got thus far: %d. Expected response: %s\nGot: %s\n",
len(expectedResponse), len(response), expectedResponse, response))
return fmt.Errorf("expected length: %d. Got thus far: %d. Expected response: %s\nGot: %s",
len(expectedResponse), len(response), expectedResponse, response)
}
for i := range response {
if response[i] != expectedResponse[i] {
return errors.New(fmt.Sprintf("Response mismatch at character #%d. Expected response: %s\nGot: %s\n",
i, expectedResponse, response))
return fmt.Errorf("response mismatch at character #%d. Expected response: %s\nGot: %s",
i, expectedResponse, response)
}
}
return nil
}
func TestPassthrough(t *testing.T) {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
ResponseHeaderTimeout: 2 * time.Second,
}
client := &http.Client{Transport: tr, Timeout: 2 * time.Second}
client := &http.Client{Transport: testTransport, Timeout: 2 * time.Second}
resp, err := client.Get("https://" + caddyForwardProxy.addr)
if err != nil {
t.Fatal(err)
@ -212,10 +223,10 @@ func TestPassthrough(t *testing.T) {
}
func TestGETNoAuth(t *testing.T) {
useTls := true
for _, httpProxyVer := range testHttpProxyVersions {
const useTLS, dialLocal = true, false
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy(caddyHTTPTestTarget.addr, resource, caddyForwardProxy.addr, httpProxyVer, credentialsEmpty, useTls)
response, err := getViaProxy(caddyHTTPTestTarget.addr, resource, caddyForwardProxy.addr, httpProxyVer, credentialsEmpty, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
} else if err = responseExpected(response, caddyHTTPTestTarget.contents[resource]); err != nil {
@ -226,10 +237,10 @@ func TestGETNoAuth(t *testing.T) {
}
func TestGETAuthCorrect(t *testing.T) {
useTls := true
for _, httpProxyVer := range testHttpProxyVersions {
const useTLS, dialLocal = true, false
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy(caddyHTTPTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpProxyVer, credentialsCorrect, useTls)
response, err := getViaProxy(caddyHTTPTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpProxyVer, credentialsCorrect, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
} else if err = responseExpected(response, caddyHTTPTestTarget.contents[resource]); err != nil {
@ -240,11 +251,11 @@ func TestGETAuthCorrect(t *testing.T) {
}
func TestGETAuthWrong(t *testing.T) {
useTls := true
const useTLS, dialLocal = true, false
for _, wrongCreds := range credentialsWrong {
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy(caddyHTTPTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpProxyVer, wrongCreds, useTls)
response, err := getViaProxy(caddyHTTPTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpProxyVer, wrongCreds, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
}
@ -258,11 +269,11 @@ func TestGETAuthWrong(t *testing.T) {
}
func TestProxySelfGet(t *testing.T) {
useTls := true
const useTLS, dialLocal = true, false
// GETNoAuth to self
for _, httpTargetVer := range testHttpTargetVersions {
for _, httpTargetVer := range testHTTPTargetVersions {
for _, resource := range testResources {
response, err := getViaProxy(caddyForwardProxy.addr, resource, caddyForwardProxy.addr, httpTargetVer, credentialsEmpty, useTls)
response, err := getViaProxy(caddyForwardProxy.addr, resource, caddyForwardProxy.addr, httpTargetVer, credentialsEmpty, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
} else if err = responseExpected(response, caddyForwardProxy.contents[resource]); err != nil {
@ -272,9 +283,9 @@ func TestProxySelfGet(t *testing.T) {
}
// GETAuthCorrect to self
for _, httpTargetVer := range testHttpTargetVersions {
for _, httpTargetVer := range testHTTPTargetVersions {
for _, resource := range testResources {
response, err := getViaProxy(caddyForwardProxyAuth.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, credentialsCorrect, useTls)
response, err := getViaProxy(caddyForwardProxyAuth.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, credentialsCorrect, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
} else if err = responseExpected(response, caddyForwardProxyAuth.contents[resource]); err != nil {
@ -289,11 +300,11 @@ func TestProxySelfGet(t *testing.T) {
// Low priority since this is a functionality issue, not security, and it would be easily caught in the wild.
func TestConnectNoAuth(t *testing.T) {
useTls := true
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpTargetVer := range testHttpTargetVersions {
const useTLS, dialLocal = true, true
for _, httpProxyVer := range testHTTPProxyVersions {
for _, httpTargetVer := range testHTTPTargetVersions {
for _, resource := range testResources {
response, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxy.addr, httpTargetVer, credentialsEmpty, httpProxyVer, useTls)
response, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxy.addr, httpTargetVer, credentialsEmpty, httpProxyVer, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
} else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil {
@ -305,11 +316,11 @@ func TestConnectNoAuth(t *testing.T) {
}
func TestConnectAuthCorrect(t *testing.T) {
useTls := true
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpTargetVer := range testHttpTargetVersions {
const useTLS, dialLocal = true, true
for _, httpProxyVer := range testHTTPProxyVersions {
for _, httpTargetVer := range testHTTPTargetVersions {
for _, resource := range testResources {
response, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, credentialsCorrect, httpProxyVer, useTls)
response, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, credentialsCorrect, httpProxyVer, useTLS, dialLocal)
if err != nil {
t.Fatal(httpProxyVer, httpTargetVer, err)
} else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil {
@ -321,18 +332,18 @@ func TestConnectAuthCorrect(t *testing.T) {
}
func TestConnectAuthWrong(t *testing.T) {
useTls := true
const useTLS, dialLocal = true, false
for _, wrongCreds := range credentialsWrong {
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpTargetVer := range testHttpTargetVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, httpTargetVer := range testHTTPTargetVersions {
for _, resource := range testResources {
response, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, wrongCreds, httpProxyVer, useTls)
response, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, wrongCreds, httpProxyVer, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
}
if response.StatusCode != http.StatusProxyAuthRequired {
t.Fatalf("Expected response: 407 StatusProxyAuthRequired, Got: %d %s\n",
response.StatusCode, response.Status)
t.Fatalf("Expected response: 407 StatusProxyAuthRequired, Got: %d %s (wrongCreds=%s httpProxyVer=%s httpTargetVer=%s resource=%s)",
response.StatusCode, response.Status, wrongCreds, httpProxyVer, httpTargetVer, resource)
}
}
}
@ -341,17 +352,12 @@ func TestConnectAuthWrong(t *testing.T) {
}
func TestPAC(t *testing.T) {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
ResponseHeaderTimeout: 2 * time.Second,
}
client := &http.Client{Transport: tr, Timeout: 2 * time.Second}
client := &http.Client{Transport: testTransport, Timeout: 2 * time.Second}
resp, err := client.Get("https://" + caddyForwardProxy.addr + "/proxy.pac")
if err != nil {
t.Fatal(err)
}
splitAddr := strings.Split(caddyForwardProxy.addr, ":")
if err = responseExpected(resp, []byte(fmt.Sprintf(pacFile, splitAddr[0], splitAddr[1]))); err != nil {
if err = responseExpected(resp, []byte(fmt.Sprintf(pacFile, caddyForwardProxy.addr))); err != nil {
t.Fatal(err)
}
@ -359,20 +365,19 @@ func TestPAC(t *testing.T) {
if err != nil {
t.Fatal(err)
}
splitAddr = strings.Split(caddyForwardProxyProbeResist.addr, ":")
if err = responseExpected(resp, []byte(fmt.Sprintf(pacFile, splitAddr[0], splitAddr[1]))); err != nil {
if err = responseExpected(resp, []byte(fmt.Sprintf(pacFile, caddyForwardProxyProbeResist.addr))); err != nil {
t.Fatal(err)
}
}
func TestCONNECTViaUpstream(t *testing.T) {
useTls := true
const useTLS, dialLocal = true, true
for range make([]byte, 5) { // do several times to test http2 connection reuse
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpTargetVer := range testHttpTargetVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, httpTargetVer := range testHTTPTargetVersions {
for _, resource := range testResources {
response, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyAuthedUpstreamEnter.addr,
httpTargetVer, credentialsUpstreamCorrect, httpProxyVer, useTls)
httpTargetVer, credentialsUpstreamCorrect, httpProxyVer, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
} else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil {
@ -385,12 +390,12 @@ func TestCONNECTViaUpstream(t *testing.T) {
}
func TestGETViaUpstream(t *testing.T) {
useTls := true
const useTLS, dialLocal = true, true
for range make([]byte, 5) { // do several times to test http2 connection reuse
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy(caddyHTTPTestTarget.addr, resource, caddyAuthedUpstreamEnter.addr, httpProxyVer,
credentialsUpstreamCorrect, useTls)
credentialsUpstreamCorrect, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
} else if err = responseExpected(response, caddyHTTPTestTarget.contents[resource]); err != nil {
@ -403,11 +408,7 @@ func TestGETViaUpstream(t *testing.T) {
func TestUpstreamPassthrough(t *testing.T) {
// Usptreaming proxy still hosts things as expected
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
ResponseHeaderTimeout: 2 * time.Second,
}
client := &http.Client{Transport: tr, Timeout: 2 * time.Second}
client := &http.Client{Transport: testTransport, Timeout: 2 * time.Second}
resp, err := client.Get("https://" + caddyAuthedUpstreamEnter.addr)
if err != nil {
t.Fatal(err)

6
go.mod
View file

@ -1,8 +1,8 @@
module github.com/caddyserver/forwardproxy
go 1.12
go 1.14
require (
github.com/mholt/caddy v1.0.0
golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6
github.com/caddyserver/caddy/v2 v2.0.0-rc.3
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e
)

1089
go.sum

File diff suppressed because it is too large Load diff

View file

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// httpclient is used by the upstreaming forwardproxy to establish connections to http(s) upstreams.
// Package httpclient is used by the upstreaming forwardproxy to establish connections to http(s) upstreams.
// it implements x/net/proxy.Dialer interface
package httpclient
@ -33,7 +33,7 @@ import (
// HTTPConnectDialer allows to configure one-time use HTTP CONNECT client
type HTTPConnectDialer struct {
ProxyUrl url.URL
ProxyURL url.URL
DefaultHeader http.Header
// TODO: If spkiFp is set, use it as SPKI fingerprint to confirm identity of the
@ -52,47 +52,47 @@ type HTTPConnectDialer struct {
cachedH2RawConn net.Conn
}
// NewHTTPClient creates a client to issue CONNECT requests and tunnel traffic via HTTPS proxy.
// proxyUrlStr must provide Scheme and Host, may provide credentials and port.
// NewHTTPConnectDialer creates a client to issue CONNECT requests and tunnel traffic via HTTPS proxy.
// proxyURLStr must provide Scheme and Host, may provide credentials and port.
// Example: https://username:password@golang.org:443
func NewHTTPConnectDialer(proxyUrlStr string) (*HTTPConnectDialer, error) {
proxyUrl, err := url.Parse(proxyUrlStr)
func NewHTTPConnectDialer(proxyURLStr string) (*HTTPConnectDialer, error) {
proxyURL, err := url.Parse(proxyURLStr)
if err != nil {
return nil, err
}
if proxyUrl.Host == "" {
return nil, errors.New("misparsed `url=" + proxyUrlStr +
if proxyURL.Host == "" {
return nil, errors.New("misparsed `url=" + proxyURLStr +
"`, make sure to specify full url like https://username:password@hostname.com:443/")
}
switch proxyUrl.Scheme {
switch proxyURL.Scheme {
case "http":
if proxyUrl.Port() == "" {
proxyUrl.Host = net.JoinHostPort(proxyUrl.Host, "80")
if proxyURL.Port() == "" {
proxyURL.Host = net.JoinHostPort(proxyURL.Host, "80")
}
case "https":
if proxyUrl.Port() == "" {
proxyUrl.Host = net.JoinHostPort(proxyUrl.Host, "443")
if proxyURL.Port() == "" {
proxyURL.Host = net.JoinHostPort(proxyURL.Host, "443")
}
case "":
return nil, errors.New("specify scheme explicitly (https://)")
default:
return nil, errors.New("scheme " + proxyUrl.Scheme + " is not supported")
return nil, errors.New("scheme " + proxyURL.Scheme + " is not supported")
}
client := &HTTPConnectDialer{
ProxyUrl: *proxyUrl,
ProxyURL: *proxyURL,
DefaultHeader: make(http.Header),
SpkiFP: nil,
EnableH2ConnReuse: true,
}
if proxyUrl.User != nil {
if proxyUrl.User.Username() != "" {
password, _ := proxyUrl.User.Password()
if proxyURL.User != nil {
if proxyURL.User.Username() != "" {
password, _ := proxyURL.User.Password()
client.DefaultHeader.Set("Proxy-Authorization", "Basic "+
base64.StdEncoding.EncodeToString([]byte(proxyUrl.User.Username()+":"+password)))
base64.StdEncoding.EncodeToString([]byte(proxyURL.User.Username()+":"+password)))
}
}
return client, nil
@ -191,24 +191,24 @@ func (c *HTTPConnectDialer) DialContext(ctx context.Context, network, address st
var err error
var rawConn net.Conn
negotiatedProtocol := ""
switch c.ProxyUrl.Scheme {
switch c.ProxyURL.Scheme {
case "http":
rawConn, err = c.Dialer.DialContext(ctx, network, c.ProxyUrl.Host)
rawConn, err = c.Dialer.DialContext(ctx, network, c.ProxyURL.Host)
if err != nil {
return nil, err
}
case "https":
if c.DialTLS != nil {
rawConn, negotiatedProtocol, err = c.DialTLS(network, c.ProxyUrl.Host)
rawConn, negotiatedProtocol, err = c.DialTLS(network, c.ProxyURL.Host)
if err != nil {
return nil, err
}
} else {
tlsConf := tls.Config{
NextProtos: []string{"h2", "http/1.1"},
ServerName: c.ProxyUrl.Hostname(),
ServerName: c.ProxyURL.Hostname(),
}
tlsConn, err := tls.Dial(network, c.ProxyUrl.Host, &tlsConf)
tlsConn, err := tls.Dial(network, c.ProxyURL.Host, &tlsConf)
if err != nil {
return nil, err
}
@ -220,7 +220,7 @@ func (c *HTTPConnectDialer) DialContext(ctx context.Context, network, address st
rawConn = tlsConn
}
default:
return nil, errors.New("scheme " + c.ProxyUrl.Scheme + " is not supported")
return nil, errors.New("scheme " + c.ProxyURL.Scheme + " is not supported")
}
switch negotiatedProtocol {

View file

@ -3,6 +3,7 @@ package forwardproxy
import (
"crypto/tls"
"fmt"
"net"
"sync"
"testing"
@ -12,27 +13,36 @@ import (
)
func TestHttpClient(t *testing.T) {
_test := func(proxyUrl string) {
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpTargetVer := range testHttpTargetVersions {
const dialLocal = false
_test := func(urlSchemeAndCreds, urlAddress string) {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, httpTargetVer := range testHTTPTargetVersions {
for _, resource := range testResources {
dialer, err := httpclient.NewHTTPConnectDialer(proxyUrl)
// always dial localhost for testing purposes
proxyURL := fmt.Sprintf("%s@%s", urlSchemeAndCreds, localDialAddr(urlAddress))
dialer, err := httpclient.NewHTTPConnectDialer(proxyURL)
if err != nil {
t.Fatal(err)
}
dialer.DialTLS = func(network string, address string) (net.Conn, string, error) {
conn, err := tls.Dial(network, address, &tls.Config{InsecureSkipVerify: true,
NextProtos: []string{httpVersionToAlpn[httpProxyVer]}})
// always dial localhost for testing purposes
conn, err := tls.Dial(network, localDialAddr(address), &tls.Config{
InsecureSkipVerify: true,
NextProtos: []string{httpVersionToALPN[httpProxyVer]},
})
if err != nil {
return nil, "", err
}
return conn, conn.ConnectionState().NegotiatedProtocol, nil
}
conn, err := dialer.Dial("tcp", caddyTestTarget.addr)
// always dial localhost for testing purposes
conn, err := dialer.Dial("tcp", localDialAddr(caddyTestTarget.addr))
if err != nil {
t.Fatal(err)
}
response, err := getResourceViaProxyConn(conn, caddyTestTarget.addr, resource, httpTargetVer, credentialsCorrect)
response, err := getResourceViaProxyConn(conn, caddyTestTarget.addr, resource, httpTargetVer, credentialsCorrect, dialLocal)
if err != nil {
t.Fatal(httpProxyVer, httpTargetVer, err)
} else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil {
@ -43,8 +53,8 @@ func TestHttpClient(t *testing.T) {
}
}
_test("https://" + credentialsCorrectPlain + "@" + caddyForwardProxyAuth.addr)
_test("http://" + credentialsCorrectPlain + "@" + caddyHTTPForwardProxyAuth.addr)
_test("https://"+credentialsCorrectPlain, caddyForwardProxyAuth.addr)
_test("http://"+credentialsCorrectPlain, caddyHTTPForwardProxyAuth.addr)
}
func TestHttpClientH2Multiplexing(t *testing.T) {
@ -52,14 +62,18 @@ func TestHttpClientH2Multiplexing(t *testing.T) {
// but it was manually inspected in Wireshark when this code was committed
httpProxyVer := "HTTP/2.0"
httpTargetVer := "HTTP/1.1"
const dialLocal = false
dialer, err := httpclient.NewHTTPConnectDialer("https://" + credentialsCorrectPlain + "@" + caddyForwardProxyAuth.addr)
if err != nil {
t.Fatal(err)
}
dialer.DialTLS = func(network string, address string) (net.Conn, string, error) {
conn, err := tls.Dial(network, address, &tls.Config{InsecureSkipVerify: true,
NextProtos: []string{httpVersionToAlpn[httpProxyVer]}})
// always dial localhost for testing purposes
conn, err := tls.Dial(network, localDialAddr(address), &tls.Config{
InsecureSkipVerify: true,
NextProtos: []string{httpVersionToALPN[httpProxyVer]},
})
if err != nil {
return nil, "", err
}
@ -74,11 +88,12 @@ func TestHttpClientH2Multiplexing(t *testing.T) {
_test := func() {
defer wg.Done()
for _, resource := range testResources {
conn, err := dialer.Dial("tcp", caddyTestTarget.addr)
// always dial localhost for testing purposes
conn, err := dialer.Dial("tcp", localDialAddr(caddyTestTarget.addr))
if err != nil {
t.Fatal(err)
}
response, err := getResourceViaProxyConn(conn, caddyTestTarget.addr, resource, httpTargetVer, credentialsCorrect)
response, err := getResourceViaProxyConn(conn, caddyTestTarget.addr, resource, httpTargetVer, credentialsCorrect, dialLocal)
if err != nil {
t.Fatal(httpProxyVer, httpTargetVer, err)
} else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil {

View file

@ -5,16 +5,17 @@ import (
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"strings"
"testing"
)
func TestGETAuthCorrectProbeResist(t *testing.T) {
useTls := true
for _, httpProxyVer := range testHttpProxyVersions {
const useTLS, dialLocal = true, true
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
response, err := getViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyProbeResist.addr, httpProxyVer, credentialsCorrect, useTls)
response, err := getViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyProbeResist.addr, httpProxyVer, credentialsCorrect, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
} else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil {
@ -25,21 +26,21 @@ func TestGETAuthCorrectProbeResist(t *testing.T) {
}
func TestGETAuthWrongProbeResist(t *testing.T) {
useTls := true
const useTLS, dialLocal = true, false
for _, wrongCreds := range credentialsWrong {
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, resource := range testResources {
responseProbeResist, err := getViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyProbeResist.addr, httpProxyVer, wrongCreds, useTls)
responseProbeResist, err := getViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyProbeResist.addr, httpProxyVer, wrongCreds, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
}
// get response from reference server without forwardproxy and compare them
responseReference, err := getViaProxy(caddyTestTarget.addr, resource, caddyDummyProbeResist.addr, httpProxyVer, wrongCreds, useTls)
responseReference, err := getViaProxy(caddyTestTarget.addr, resource, caddyDummyProbeResist.addr, httpProxyVer, wrongCreds, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
}
// as a sanity check, get 407 from simple authenticated forwardproxy
responseForwardProxy, err := getViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpProxyVer, wrongCreds, useTls)
responseForwardProxy, err := getViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpProxyVer, wrongCreds, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
}
@ -55,17 +56,17 @@ func TestGETAuthWrongProbeResist(t *testing.T) {
}
}
for _, resource := range testResources {
responseProbeResist, err := getViaProxy(caddyForwardProxyProbeResist.addr, resource, caddyForwardProxyProbeResist.addr, httpProxyVer, wrongCreds, useTls)
responseProbeResist, err := getViaProxy(caddyForwardProxyProbeResist.addr, resource, caddyForwardProxyProbeResist.addr, httpProxyVer, wrongCreds, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
}
// get response from reference server without forwardproxy and compare them
responseReference, err := getViaProxy(caddyDummyProbeResist.addr, resource, caddyDummyProbeResist.addr, httpProxyVer, wrongCreds, useTls)
responseReference, err := getViaProxy(caddyDummyProbeResist.addr, resource, caddyDummyProbeResist.addr, httpProxyVer, wrongCreds, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
}
// as a sanity check, get 407 from simple authenticated forwardproxy
responseForwardProxy, err := getViaProxy(caddyForwardProxyAuth.addr, resource, caddyForwardProxyAuth.addr, httpProxyVer, wrongCreds, useTls)
responseForwardProxy, err := getViaProxy(caddyForwardProxyAuth.addr, resource, caddyForwardProxyAuth.addr, httpProxyVer, wrongCreds, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
}
@ -86,16 +87,14 @@ func TestGETAuthWrongProbeResist(t *testing.T) {
// test that responses on http redirect port are same
func TestGETAuthWrongProbeResistRedir(t *testing.T) {
useTls := false
const useTLS, dialLocal = false, false
httpProxyVer := "HTTP/1.1"
for _, wrongCreds := range credentialsWrong {
// request test target
for _, resource := range testResources {
responseProbeResist, rPRerr := getViaProxy(caddyTestTarget.addr, resource, stripPort(caddyForwardProxyProbeResist.addr)+":"+caddyForwardProxyProbeResist.HTTPRedirectPort,
httpProxyVer, wrongCreds, useTls)
responseProbeResist, rPRerr := getViaProxy(caddyTestTarget.addr, resource, changePort(caddyForwardProxyProbeResist.addr, caddyForwardProxyProbeResist.httpRedirPort), httpProxyVer, wrongCreds, useTLS, dialLocal)
// get response from reference server without forwardproxy and compare them
responseReference, rRerr := getViaProxy(caddyTestTarget.addr, resource, stripPort(caddyDummyProbeResist.addr)+":"+caddyDummyProbeResist.HTTPRedirectPort,
httpProxyVer, wrongCreds, useTls)
responseReference, rRerr := getViaProxy(caddyTestTarget.addr, resource, changePort(caddyDummyProbeResist.addr, caddyDummyProbeResist.httpRedirPort), httpProxyVer, wrongCreds, useTLS, dialLocal)
if (rPRerr == nil && rRerr != nil) || (rPRerr != nil && rRerr == nil) {
t.Fatalf("Reference error: %s. Probe resist error: %s", rRerr, rPRerr)
}
@ -109,14 +108,12 @@ func TestGETAuthWrongProbeResistRedir(t *testing.T) {
}
// request self
for _, resource := range testResources {
responseProbeResist, err := getViaProxy(caddyForwardProxyProbeResist.addr, resource, stripPort(caddyForwardProxyProbeResist.addr)+":"+caddyForwardProxyProbeResist.HTTPRedirectPort,
httpProxyVer, wrongCreds, useTls)
responseProbeResist, err := getViaProxy(caddyForwardProxyProbeResist.addr, resource, changePort(caddyForwardProxyProbeResist.addr, caddyForwardProxyProbeResist.httpRedirPort), httpProxyVer, wrongCreds, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
}
// get response from reference server without forwardproxy and compare them
responseReference, err := getViaProxy(caddyDummyProbeResist.addr, resource, stripPort(caddyDummyProbeResist.addr)+":"+caddyDummyProbeResist.HTTPRedirectPort,
httpProxyVer, wrongCreds, useTls)
responseReference, err := getViaProxy(caddyDummyProbeResist.addr, resource, changePort(caddyDummyProbeResist.addr, caddyDummyProbeResist.httpRedirPort), httpProxyVer, wrongCreds, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
}
@ -132,11 +129,11 @@ func TestGETAuthWrongProbeResistRedir(t *testing.T) {
}
func TestConnectAuthCorrectProbeResist(t *testing.T) {
useTls := true
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpTargetVer := range testHttpTargetVersions {
const useTLS, dialLocal = true, true
for _, httpProxyVer := range testHTTPProxyVersions {
for _, httpTargetVer := range testHTTPTargetVersions {
for _, resource := range testResources {
response, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyProbeResist.addr, httpTargetVer, credentialsCorrect, httpProxyVer, useTls)
response, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyProbeResist.addr, httpTargetVer, credentialsCorrect, httpProxyVer, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
} else if err = responseExpected(response, caddyTestTarget.contents[resource]); err != nil {
@ -148,22 +145,22 @@ func TestConnectAuthCorrectProbeResist(t *testing.T) {
}
func TestConnectAuthWrongProbeResist(t *testing.T) {
useTls := true
const useTLS, dialLocal = true, false
for _, wrongCreds := range credentialsWrong {
for _, httpProxyVer := range testHttpProxyVersions {
for _, httpTargetVer := range testHttpTargetVersions {
for _, httpProxyVer := range testHTTPProxyVersions {
for _, httpTargetVer := range testHTTPTargetVersions {
for _, resource := range testResources {
responseProbeResist, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyProbeResist.addr, httpTargetVer, wrongCreds, httpProxyVer, useTls)
responseProbeResist, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyProbeResist.addr, httpTargetVer, wrongCreds, httpProxyVer, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
}
// get response from reference server without forwardproxy and compare them
responseReference, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyDummyProbeResist.addr, httpTargetVer, wrongCreds, httpProxyVer, useTls)
responseReference, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyDummyProbeResist.addr, httpTargetVer, wrongCreds, httpProxyVer, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
}
// as a sanity check, get 407 from simple authenticated forwardproxy
responseForwardProxy, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, wrongCreds, httpProxyVer, useTls)
responseForwardProxy, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, wrongCreds, httpProxyVer, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
}
@ -183,25 +180,26 @@ func TestConnectAuthWrongProbeResist(t *testing.T) {
if httpTargetVer != httpProxyVer {
continue
}
responseProbeResist, err := connectAndGetViaProxy(caddyForwardProxyProbeResist.addr, resource, caddyForwardProxyProbeResist.addr, httpTargetVer, wrongCreds, httpProxyVer, useTls)
responseProbeResist, err := connectAndGetViaProxy(caddyForwardProxyProbeResist.addr, resource, caddyForwardProxyProbeResist.addr, httpTargetVer, wrongCreds, httpProxyVer, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
}
// get response from reference server without forwardproxy and compare them
responseReference, err := connectAndGetViaProxy(caddyDummyProbeResist.addr, resource, caddyDummyProbeResist.addr, httpTargetVer, wrongCreds, httpProxyVer, useTls)
responseReference, err := connectAndGetViaProxy(caddyDummyProbeResist.addr, resource, caddyDummyProbeResist.addr, httpTargetVer, wrongCreds, httpProxyVer, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
}
// as a sanity check, get 407 from simple authenticated forwardproxy
responseForwardProxy, err := connectAndGetViaProxy(caddyForwardProxyAuth.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, wrongCreds, httpProxyVer, useTls)
responseForwardProxy, err := connectAndGetViaProxy(caddyForwardProxyAuth.addr, resource, caddyForwardProxyAuth.addr, httpTargetVer, wrongCreds, httpProxyVer, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
}
if err = responsesAreEqual(responseProbeResist, responseReference); err != nil {
t.Fatal(err)
}
if err = responsesAreEqual(responseProbeResist, responseForwardProxy); err == nil {
t.Fatal("Responses from servers with and without forwardproxy are expected to be different.")
// TODO: this test was originally err == nil in Caddy v1, but not sure that makes sense in caddy v2? (similar test above on L174 is unchanged)
if err = responsesAreEqual(responseProbeResist, responseForwardProxy); err != nil {
t.Fatal("Responses from servers with and without probe resistance are expected to be the same:", err)
}
}
}
@ -211,20 +209,18 @@ func TestConnectAuthWrongProbeResist(t *testing.T) {
// test that responses on http redirect port are same
func TestConnectAuthWrongProbeResistRedir(t *testing.T) {
useTls := false
const useTLS, dialLocal = false, false
httpProxyVer := "HTTP/1.1"
for _, wrongCreds := range credentialsWrong {
for _, httpTargetVer := range testHttpTargetVersions {
for _, httpTargetVer := range testHTTPTargetVersions {
// request test target
for _, resource := range testResources {
responseProbeResist, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, stripPort(caddyForwardProxyProbeResist.addr)+":"+caddyForwardProxyProbeResist.HTTPRedirectPort,
httpTargetVer, wrongCreds, httpProxyVer, useTls)
responseProbeResist, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, changePort(caddyForwardProxyProbeResist.addr, caddyForwardProxyProbeResist.httpRedirPort), httpTargetVer, wrongCreds, httpProxyVer, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
}
// get response from reference server without forwardproxy and compare them
responseReference, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, stripPort(caddyDummyProbeResist.addr)+":"+caddyDummyProbeResist.HTTPRedirectPort,
httpTargetVer, wrongCreds, httpProxyVer, useTls)
responseReference, err := connectAndGetViaProxy(caddyTestTarget.addr, resource, changePort(caddyDummyProbeResist.addr, caddyDummyProbeResist.httpRedirPort), httpTargetVer, wrongCreds, httpProxyVer, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
}
@ -238,14 +234,12 @@ func TestConnectAuthWrongProbeResistRedir(t *testing.T) {
}
// request self
for _, resource := range testResources {
responseProbeResist, err := connectAndGetViaProxy(caddyForwardProxyProbeResist.addr, resource, stripPort(caddyForwardProxyProbeResist.addr)+":"+caddyForwardProxyProbeResist.HTTPRedirectPort,
httpTargetVer, wrongCreds, httpProxyVer, useTls)
responseProbeResist, err := connectAndGetViaProxy(caddyForwardProxyProbeResist.addr, resource, changePort(caddyForwardProxyProbeResist.addr, caddyForwardProxyProbeResist.httpRedirPort), httpTargetVer, wrongCreds, httpProxyVer, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
}
// get response from reference server without forwardproxy and compare them
responseReference, err := connectAndGetViaProxy(caddyDummyProbeResist.addr, resource, stripPort(caddyDummyProbeResist.addr)+":"+caddyDummyProbeResist.HTTPRedirectPort,
httpTargetVer, wrongCreds, httpProxyVer, useTls)
responseReference, err := connectAndGetViaProxy(caddyDummyProbeResist.addr, resource, changePort(caddyDummyProbeResist.addr, caddyDummyProbeResist.httpRedirPort), httpTargetVer, wrongCreds, httpProxyVer, useTLS, dialLocal)
if err != nil {
t.Fatal(err)
}
@ -270,36 +264,31 @@ func responsesAreEqual(res1, res2 *http.Response) error {
return errors.New("res2 is nil")
}
if res1.Status != res2.Status {
return errors.New("Status is different")
return fmt.Errorf("status is different; %s != %s", res1.Status, res2.Status)
}
if res1.StatusCode != res2.StatusCode {
return errors.New("StatusCode is different")
return fmt.Errorf("status code is different; %d != %d", res1.StatusCode, res2.StatusCode)
}
if res1.ProtoMajor != res2.ProtoMajor {
return errors.New("ProtoMajor is different")
return fmt.Errorf("proto major is different; %d != %d", res1.ProtoMajor, res2.ProtoMajor)
}
if res1.Close != res2.Close {
return errors.New("Close is different")
}
if res1.ProtoMinor != res2.ProtoMinor {
return errors.New("ProtoMinor is different")
return fmt.Errorf("proto minor is different; %d != %d", res1.ProtoMinor, res2.ProtoMinor)
}
if res1.Close != res2.Close {
return fmt.Errorf("close is different; %t != %t", res1.Close, res2.Close)
}
if res1.ContentLength != res2.ContentLength {
return errors.New("ContentLength is different")
return fmt.Errorf("content length is different; %d != %d", res1.ContentLength, res2.ContentLength)
}
if res1.Uncompressed != res2.Uncompressed {
return errors.New("Uncompressed is different")
return fmt.Errorf("uncompressed is different; %t != %t", res1.Uncompressed, res2.Uncompressed)
}
if res1.Proto != res2.Proto {
return errors.New("Proto is different")
return fmt.Errorf("proto is different; %s != %s", res1.Proto, res2.Proto)
}
if len(res1.TransferEncoding) != len(res2.TransferEncoding) {
return errors.New("TransferEncodings have different length")
return fmt.Errorf("transfer encodings have different lenght; %d != %d", len(res1.TransferEncoding), len(res2.TransferEncoding))
}
// returns "" if equal
@ -341,18 +330,18 @@ func responsesAreEqual(res1, res2 *http.Response) error {
}
v2, ok := res2.Header[k1]
if !ok {
return errors.New(fmt.Sprintf("Header \"%s: %s\" is absent in res2", k1, v1))
}
if k1Lower == "location" {
for i, h := range v2 {
v2[i] = removeAddressesStr(h)
}
for i, h := range v1 {
v1[i] = removeAddressesStr(h)
}
return fmt.Errorf("header \"%s: %s\" is absent in res2", k1, v1)
}
// if k1Lower == "location" {
// for i, h := range v2 {
// v2[i] = removeAddressesStr(h)
// }
// for i, h := range v1 {
// v1[i] = removeAddressesStr(h)
// }
// }
if errStr = stringSlicesAreEqual(v1, v2); errStr != "" {
return errors.New(fmt.Sprintf("Header \"%s\" is different: %s", k1, errStr))
return fmt.Errorf("header \"%s\" is different: %s", k1, errStr)
}
}
// Compare bodies
@ -361,8 +350,8 @@ func responsesAreEqual(res1, res2 *http.Response) error {
n1 := len(buf1)
n2 := len(buf2)
makeBodyError := func(s string) error {
return errors.New(fmt.Sprintf("Bodies are different: %s. n1 = %d, n2 = %d. err1 = %v, err2 = %v. buf1 = %s, buf2 = %s",
s, n1, n2, err1, err2, buf1[:n1], buf2[:n2]))
return fmt.Errorf("bodies are different: %s. n1 = %d, n2 = %d. err1 = %v, err2 = %v. buf1 = %s, buf2 = %s",
s, n1, n2, err1, err2, buf1[:n1], buf2[:n2])
}
if n2 != n1 {
return makeBodyError("Body sizes are different")
@ -393,3 +382,11 @@ func removeAddressesByte(b []byte) []byte {
func removeAddressesStr(s string) string {
return string(removeAddressesByte([]byte(s)))
}
func changePort(inputAddr, toPort string) string {
host, _, err := net.SplitHostPort(inputAddr)
if err != nil {
panic(err)
}
return net.JoinHostPort(host, toPort)
}

409
setup.go
View file

@ -1,409 +0,0 @@
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package forwardproxy
import (
"bufio"
"context"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"log"
"net"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
"github.com/caddyserver/forwardproxy/httpclient"
"golang.org/x/net/proxy"
)
func setup(c *caddy.Controller) error {
httpserver.GetConfig(c).FallbackSite = true
fp := &ForwardProxy{
dialTimeout: time.Second * 20,
hostname: httpserver.GetConfig(c).Host(), port: httpserver.GetConfig(c).Port(),
httpTransport: http.Transport{
Proxy: http.ProxyFromEnvironment,
MaxIdleConns: 50,
IdleConnTimeout: 60 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
},
}
c.Next() // skip the directive name
args := c.RemainingArgs()
if len(args) > 0 {
return c.ArgErr()
}
for c.NextBlock() {
subdirective := c.Val()
args := c.RemainingArgs()
switch subdirective {
case "basicauth":
if len(args) != 2 {
return c.ArgErr()
}
if len(args[0]) == 0 {
return c.Err("empty usernames are not allowed")
}
// TODO: Evaluate policy of allowing empty passwords.
if strings.Contains(args[0], ":") {
return c.Err("character ':' in usernames is not allowed")
}
if fp.authCredentials == nil {
fp.authCredentials = [][]byte{}
}
// base64-encode credentials
buf := make([]byte, base64.StdEncoding.EncodedLen(len(args[0])+1+len(args[1])))
base64.StdEncoding.Encode(buf, []byte(args[0]+":"+args[1]))
fp.authCredentials = append(fp.authCredentials, buf)
fp.authRequired = true
case "ports":
if len(args) == 0 {
return c.ArgErr()
}
if len(fp.whitelistedPorts) != 0 {
return c.Err("ports subdirective specified twice")
}
fp.whitelistedPorts = make([]int, len(args))
for i, p := range args {
intPort, err := strconv.Atoi(p)
if intPort <= 0 || intPort > 65535 || err != nil {
return c.Err("ports are expected to be space-separated" +
" and in 0-65535 range. Got: " + p)
}
fp.whitelistedPorts[i] = intPort
}
case "hide_ip":
if len(args) != 0 {
return c.ArgErr()
}
fp.hideIP = true
case "hide_via":
if len(args) != 0 {
return c.ArgErr()
}
fp.hideVia = true
case "probe_resistance":
if len(args) > 1 {
return c.ArgErr()
}
fp.probeResistEnabled = true
if len(args) == 1 {
lowercaseArg := strings.ToLower(args[0])
if lowercaseArg != args[0] {
log.Println("WARNING: secret domain appears to have uppercase letters in it, which are not visitable")
}
fp.probeResistDomain = args[0]
}
case "serve_pac":
if len(args) > 1 {
return c.ArgErr()
}
if len(fp.pacFilePath) != 0 {
return c.Err("serve_pac subdirective specified twice")
}
if len(args) == 1 {
fp.pacFilePath = args[0]
if !strings.HasPrefix(fp.pacFilePath, "/") {
fp.pacFilePath = "/" + fp.pacFilePath
}
} else {
fp.pacFilePath = "/proxy.pac"
}
log.Printf("Proxy Auto-Config will be served at %s%s\n", fp.hostname, fp.pacFilePath)
case "response_timeout":
if len(args) != 1 {
return c.ArgErr()
}
timeout, err := strconv.Atoi(args[0])
if err != nil {
return c.ArgErr()
}
if timeout < 0 {
return c.Err("response_timeout cannot be negative.")
}
fp.httpTransport.ResponseHeaderTimeout = time.Duration(timeout) * time.Second
case "dial_timeout":
if len(args) != 1 {
return c.ArgErr()
}
timeout, err := strconv.Atoi(args[0])
if err != nil {
return c.ArgErr()
}
if timeout < 0 {
return c.Err("dial_timeout cannot be negative.")
}
fp.dialTimeout = time.Second * time.Duration(timeout)
case "upstream":
if len(args) != 1 {
return c.ArgErr()
}
if fp.upstream != nil {
return c.Err("upstream directive specified more than once")
}
var err error
fp.upstream, err = url.Parse(args[0])
if err != nil {
return c.Err("failed to parse upstream address: " + err.Error())
}
case "acl":
if len(args) != 0 {
return c.Err("acl should be only subdirective on the line")
}
args := c.RemainingArgs()
if len(args) > 0 {
return c.ArgErr()
}
c.Next()
if c.Val() != "{" {
return c.Err("acl directive must be followed by opening curly braces \"{\"")
}
for {
if !c.Next() {
return c.Err("acl blockmust be ended by closing curly braces \"}\"")
}
aclDirective := c.Val()
args := c.RemainingArgs()
if aclDirective == "}" {
break
}
if len(args) == 0 {
return c.ArgErr()
}
var ruleSubjects []string
var err error
aclAllow := false
switch aclDirective {
case "allow":
ruleSubjects = args[:]
aclAllow = true
case "allowfile":
if len(args) != 1 {
return c.Err("allowfile accepts a single filename argument")
}
ruleSubjects, err = readLinesFromFile(args[0])
if err != nil {
return err
}
aclAllow = true
case "deny":
ruleSubjects = args[:]
case "denyfile":
if len(args) != 1 {
return c.Err("denyfile accepts a single filename argument")
}
ruleSubjects, err = readLinesFromFile(args[0])
if err != nil {
return err
}
default:
return c.Err("expected acl directive: allow/allowfile/deny/denyfile." +
"got: " + aclDirective)
}
for _, rs := range ruleSubjects {
ar, err := newAclRule(rs, aclAllow)
if err != nil {
return err
}
fp.aclRules = append(fp.aclRules, ar)
}
}
default:
return c.ArgErr()
}
}
if fp.upstream != nil && (fp.aclRules != nil || len(fp.whitelistedPorts) != 0) {
return c.Err("upstream subdirective is incompatible with acl/ports subdirectives")
}
for _, ipDeny := range []string{
"10.0.0.0/8",
"127.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"::1/128",
"fe80::/10",
} {
ar, err := newAclRule(ipDeny, false)
if err != nil {
panic(err)
}
fp.aclRules = append(fp.aclRules, ar)
}
fp.aclRules = append(fp.aclRules, &aclAllRule{allow: true})
if fp.probeResistEnabled {
if !fp.authRequired {
return c.Err("probing resistance requires authentication: " +
"add `basicauth username password` to forwardproxy")
}
if len(fp.probeResistDomain) > 0 {
log.Printf("Secret domain used to connect to proxy: %s\n", fp.probeResistDomain)
}
}
dialer := &net.Dialer{
Timeout: fp.dialTimeout,
KeepAlive: 30 * time.Second,
DualStack: true,
}
fp.dialContext = dialer.DialContext
fp.httpTransport.DialContext = func(ctx context.Context, network string, address string) (net.Conn, error) {
conn, err := fp.dialContextCheckACL(ctx, network, address)
if err != nil {
return conn, err
}
return conn, nil
}
if fp.upstream != nil {
if !isLocalhost(fp.upstream.Hostname()) && fp.upstream.Scheme != "https" {
return errors.New("insecure schemes are only allowed to localhost upstreams")
}
registerHTTPDialer := func(u *url.URL, _ proxy.Dialer) (proxy.Dialer, error) {
// CONNECT request is proxied as-is, so we don't care about target url, but it could be
// useful in future to implement policies of choosing between multiple upstream servers.
// Given dialer is not used, since it's the same dialer provided by us.
d, err := httpclient.NewHTTPConnectDialer(fp.upstream.String())
if err != nil {
return nil, err
}
d.Dialer = *dialer
if isLocalhost(fp.upstream.Hostname()) && fp.upstream.Scheme == "https" {
// disabling verification helps with testing the package and setups
// either way, it's impossible to have a legit TLS certificate for "127.0.0.1"
log.Println("Localhost upstream detected, disabling verification of TLS certificate")
d.DialTLS = func(network string, address string) (net.Conn, string, error) {
conn, err := tls.Dial(network, address, &tls.Config{InsecureSkipVerify: true})
if err != nil {
return nil, "", err
}
return conn, conn.ConnectionState().NegotiatedProtocol, nil
}
}
return d, nil
}
proxy.RegisterDialerType("https", registerHTTPDialer)
proxy.RegisterDialerType("http", registerHTTPDialer)
upstreamDialer, err := proxy.FromURL(fp.upstream, dialer)
if err != nil {
return errors.New("failed to create proxy to upstream: " + err.Error())
}
if ctxDialer, ok := upstreamDialer.(interface {
DialContext(ctx context.Context, network, address string) (net.Conn, error)
}); ok {
// upstreamDialer has DialContext - use it
fp.dialContext = ctxDialer.DialContext
} else {
// upstreamDialer does not have DialContext - ignore the context :(
fp.dialContext = func(ctx context.Context, network string, address string) (net.Conn, error) {
return upstreamDialer.Dial(network, address)
}
}
}
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
fp.Next = next
return fp
})
makeBuffer := func() interface{} { return make([]byte, 0, 32*1024) }
bufferPool = sync.Pool{New: makeBuffer}
return nil
}
func init() {
caddy.RegisterPlugin("forwardproxy", caddy.Plugin{
ServerType: "http",
Action: setup,
})
}
func isLocalhost(hostname string) bool {
if hostname == "localhost" || hostname == "127.0.0.1" || hostname == "::1" {
return true
}
return false
}
func readLinesFromFile(filename string) ([]string, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
var hostnames []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
hostnames = append(hostnames, scanner.Text())
}
return hostnames, scanner.Err()
}
// isValidDomainLite shamelessly rejects non-LDH names. returns nil if domains seems valid
func isValidDomainLite(domain string) error {
for i := 0; i < len(domain); i++ {
c := domain[i]
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_' || '0' <= c && c <= '9' ||
c == '-' || c == '.' {
continue
}
return errors.New("character " + string(c) + " is not allowed")
}
sections := strings.Split(domain, ".")
for _, s := range sections {
if len(s) == 0 {
return errors.New("empty section between dots in domain name or trailing dot")
}
if len(s) > 63 {
return errors.New("domain name section is too long")
}
}
return nil
}
type ProxyError struct {
S string
Code int
}
func (e *ProxyError) Error() string {
return fmt.Sprintf("[%v] %s", e.Code, e.S)
}
func (e *ProxyError) SplitCodeError() (int, error) {
if e == nil {
return 200, nil
}
return e.Code, errors.New(e.S)
}

View file

@ -1,139 +0,0 @@
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package forwardproxy
import (
"testing"
"github.com/caddyserver/caddy"
)
func TestSetup(t *testing.T) {
c := caddy.NewTestController("http", "forwardproxy string")
err := setup(c)
if err == nil {
t.Fatal("Expected: failure. Got: success. Input: forwardproxy string")
}
testParsing := func(subdirectives []string, shouldSucceed bool) {
input := "forwardproxy"
if len(subdirectives) > 0 {
input += " {\n"
for _, s := range subdirectives {
input += s + "\n"
}
input += "}"
}
c := caddy.NewTestController("http", input)
err := setup(c)
if shouldSucceed && err != nil {
t.Fatalf("Expected: success. Got: %v. Input:\n%s\n", err, input)
}
if !shouldSucceed && err == nil {
t.Fatalf("Expected: failure. Got: success. Input:\n%s\n", input)
}
}
testParsing(nil, true)
testParsing([]string{}, true)
testParsing([]string{"qweqwe"}, false)
testParsing([]string{"0"}, false)
testParsing([]string{"basicauth john"}, false)
testParsing([]string{"basicauth john \"\""}, true)
testParsing([]string{"basicauth john", "basicauth john \"\""}, false)
testParsing([]string{"basicauth john doe"}, true)
testParsing([]string{"basicauth john doe foo"}, false)
testParsing([]string{"basicauth john doe foo bar"}, false)
testParsing([]string{"basicauth \"\" doe"}, false)
testParsing([]string{"basicauth \"\" \"\""}, false)
testParsing([]string{"basicauth 0"}, false)
testParsing([]string{"basicauth 0 0"}, true)
testParsing([]string{"basicauth 0 0 0"}, false)
testParsing([]string{"basicauth 秘密"}, false)
testParsing([]string{"basicauth 秘密 秘密"}, true)
testParsing([]string{"basicauth 秘密 秘密 秘密"}, false)
testParsing([]string{"basicauth cyrillic пароль"}, true)
testParsing([]string{"basicauth john \"\"", "basicauth john doe", "basicauth 0 0", "basicauth 秘密 秘密", "basicauth cyrillic пароль"}, true)
testParsing([]string{"ports"}, false)
testParsing([]string{"ports 0"}, false)
testParsing([]string{"ports 0 1"}, false)
testParsing([]string{"ports -1"}, false)
testParsing([]string{"ports hi!"}, false)
testParsing([]string{"ports 11, 122, 33"}, false)
testParsing([]string{"ports 11, 122, 33"}, false)
testParsing([]string{"ports 11111 99999"}, false)
testParsing([]string{"ports 11 12"}, true)
testParsing([]string{"ports 1"}, true)
testParsing([]string{"ports 1 11 111 332 324 6546 33333"}, true)
testParsing([]string{"ports 1 11 111 332 324 6546 33333", "ports 1 11 111 332 324 6546 33333"}, false)
testParsing([]string{"ports 1", "ports 2"}, false)
testParsing([]string{"hide_ip"}, true)
testParsing([]string{"hide_ip 0"}, false)
testParsing([]string{"hide_ip 0 1"}, false)
testParsing([]string{"hide_via"}, true)
testParsing([]string{"hide_via 0"}, false)
testParsing([]string{"hide_via 0 1"}, false)
testParsing([]string{"probe_resistance"}, false)
testParsing([]string{"probe_resistance local.host"}, false)
testParsing([]string{"probe_resistance local.host very.local.host"}, false)
testParsing([]string{"probe_resistance", "basicauth john doe"}, true)
testParsing([]string{"probe_resistance local.host", "basicauth john doe"}, true)
testParsing([]string{"probe_resistance local.host very.local.host", "basicauth john doe"}, false)
testParsing([]string{"serve_pac"}, true)
testParsing([]string{"serve_pac \"\""}, true)
testParsing([]string{"serve_pac proxyautoconfig.pac"}, true)
testParsing([]string{"serve_pac 1.pac 2.pac"}, false)
testParsing([]string{"response_timeout"}, false)
testParsing([]string{"response_timeout -1"}, false)
testParsing([]string{"response_timeout 1 2"}, false)
testParsing([]string{"response_timeout seven"}, false)
testParsing([]string{"response_timeout 2"}, true)
testParsing([]string{"dial_timeout"}, false)
testParsing([]string{"dial_timeout -1"}, false)
testParsing([]string{"dial_timeout 1 2"}, false)
testParsing([]string{"dial_timeout seven"}, false)
testParsing([]string{"dial_timeout 2"}, true)
testParsing([]string{"upstream proxy.site"}, false)
testParsing([]string{"upstream https://proxy.site https://proxy.site"}, false)
testParsing([]string{"upstream http://localhost:1230"}, true)
testParsing([]string{"upstream socks5://127.0.0.1:999"}, true)
testParsing([]string{"upstream http://proxy.site"}, false)
testParsing([]string{"upstream https://proxy.site https://proxy.site"}, false)
testParsing([]string{"upstream https://proxy.site"}, true)
testParsing([]string{"upstream https://caddyserver.com", "acl {\nallow all\n}"}, false)
testParsing([]string{"upstream https://caddyserver.com", "ports 123"}, false)
testParsing([]string{"upstream https://username:password@caddyserver.com", "ports 123"}, false)
testParsing([]string{"upstream https://username:password@caddyserver.com:90", "ports 123"}, false)
testParsing([]string{"acl {\nallow all\n}"}, true)
testParsing([]string{"acl {\nallow localhost 128.32.22.1/32 1.1.1.1 caddyserver.com\n deny all\n}"}, true)
testParsing([]string{"acl {\nallowfile test/parseable_acl.txt\n}"}, true)
testParsing([]string{"acl {\ndenyfile test/parseable_acl.txt\n}"}, true)
testParsing([]string{"acl {\nallowfile test/unparseable_acl.txt\n}"}, false)
testParsing([]string{"acl {\ndenyfile test/unparseable_acl.txt\n}"}, false)
//testParsing([]string{"acl {\nallow all\n"}, false) // doesn't fail, but should: caddy itself doesn't demand curly brace to be closed
testParsing([]string{"acl {\nallow all\n", "serve_pac"}, false) // this does fail
testParsing([]string{"acl \nallow all\n}"}, false)
testParsing([]string{"acl {allow all\n}"}, false)
//testParsing([]string{"acl {\nallow all}"}, false) // '}' is not on the next line, "all}" parses as regexp
}