From f2019d57c4ab0dffc27f1e43c2440a678d01af76 Mon Sep 17 00:00:00 2001 From: Sergey Frolov Date: Thu, 17 Aug 2017 14:26:57 -0400 Subject: [PATCH] Serve PAC files from memory --- README.md | 7 ++++++- common_test.go | 6 ++++-- forwardproxy.go | 25 ++++++++++++++++++++++++- forwardproxy_test.go | 26 ++++++++++++++++++++++++++ setup.go | 30 ++++++++++++++++++++++++++---- setup_test.go | 5 +++++ 6 files changed, 91 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5640c28..acb88d8 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ forwardproxy { basicauth caddyuser2 秘密 ports 80 443 hide_ip + serve_pac proxy.pac experimental_probe_resist secretlink-7qS4+3dqm.localhost response_timeout 30 dial_timeout 30 @@ -28,7 +29,11 @@ Default: no restrictions. * hide_ip If set, forwardproxy will not add user's IP to "Forwarded:" header. Default: no hiding, "_Forwarded: for="useraddress"_" will be sent out. -* experimental_probe_resist secretlink.tld +* serve_pac path_to_pac_file.pac(optional) +Generate in memory and serve [Proxy Auto-Config](https://en.wikipedia.org/wiki/Proxy_auto-config) file on given path. +If no path is provided, PAC file will be served at /proxy.pac +Default: no PAC file generated by Caddy (you still can manually create and serve proxy.pac like a regular file) +* experimental_probe_resist secretlink.tld(optional) EXPERIMENTAL, HERE BE DRAGONS. Attempts to hide the fact that the site is a forwardproxy. Proxy will no longer respond with _"407 Proxy Authentication Required"_ if credentials are incorrect or absent, diff --git a/common_test.go b/common_test.go index a04f94f..2c78f75 100644 --- a/common_test.go +++ b/common_test.go @@ -111,7 +111,7 @@ func (c *caddyTestServer) StartTestServer() { func TestMain(m *testing.M) { caddyForwardProxy = caddyTestServer{addr: "127.0.0.1:1984", root: "./test/forwardproxy", directives: []string{"tls self_signed"}, - proxyEnabled: true} + proxyEnabled: true, proxyDirectives: []string{"serve_pac"}} caddyForwardProxy.StartTestServer() caddyForwardProxyAuth = caddyTestServer{addr: "127.0.0.1:4891", root: "./test/forwardproxy", @@ -121,7 +121,9 @@ func TestMain(m *testing.M) { caddyForwardProxyProbeResist = caddyTestServer{addr: "127.0.0.1:8888", root: "./test/forwardproxy", directives: []string{"tls self_signed"}, HTTPRedirectPort: "8880", - proxyEnabled: true, proxyDirectives: []string{"basicauth test pass", "experimental_probe_resist test.localhost"}} + proxyEnabled: true, proxyDirectives: []string{"basicauth test pass", + "experimental_probe_resist test.localhost", + "serve_pac superhiddenfile.pac"}} caddyForwardProxyProbeResist.StartTestServer() caddyDummyProbeResist = caddyTestServer{addr: "127.0.0.1:9999", root: "./test/forwardproxy", diff --git a/forwardproxy.go b/forwardproxy.go index e1bdb20..f721df8 100644 --- a/forwardproxy.go +++ b/forwardproxy.go @@ -39,9 +39,11 @@ type ForwardProxy struct { hideIP bool whitelistedPorts []int probeResistDomain string + pacFilePath string probeResistEnabled bool dialTimeout time.Duration // for initial tcp connection hostname string // do not intercept requests to the hostname (except for hidden link) + port string // port on which chain with forwardproxy is listening on } var bufferPool sync.Pool @@ -185,7 +187,7 @@ func serveHiddenPage(w http.ResponseWriter, authErr error) (int, error) { ` const AuthFail = "Please authenticate yourself to the proxy." - const AuthOk = "Congratulations, you are succussfully authenticated to the proxy! Go browse all the things!" + const AuthOk = "Congratulations, you are successfully authenticated to the proxy! Go browse all the things!" if authErr != nil { w.Header().Set("Proxy-Authenticate", "Basic") @@ -197,6 +199,24 @@ func serveHiddenPage(w http.ResponseWriter, authErr error) (int, error) { return 0, nil } +func (fp *ForwardProxy) shouldServePacFile(r *http.Request) bool { + if len(fp.pacFilePath) > 0 && r.URL.Path == fp.pacFilePath { + return true + } + return false +} + +const pacFile = ` +function FindProxyForURL(url, host) { + return "HTTPS %s:%s"; +} +` + +func (fp *ForwardProxy) servePacFile(w http.ResponseWriter) (int, error) { + fmt.Fprintf(w, pacFile, fp.hostname, fp.port) + return 0, nil +} + func (fp *ForwardProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { var authErr error if fp.authRequired { @@ -208,6 +228,9 @@ func (fp *ForwardProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, if isSubdomain(stripPort(r.Host), fp.hostname) && (r.Method != http.MethodConnect || authErr != nil) { // Always pass non-CONNECT requests to hostname // Pass CONNECT requests only if probe resistance is enabled and not authenticated + if fp.shouldServePacFile(r) { + return fp.servePacFile(w) + } return fp.Next.ServeHTTP(w, r) } if authErr != nil { diff --git a/forwardproxy_test.go b/forwardproxy_test.go index baca8ec..ec9c4ac 100644 --- a/forwardproxy_test.go +++ b/forwardproxy_test.go @@ -29,6 +29,7 @@ import ( "net/url" "testing" "time" + "strings" ) func dial(proxyAddr string, useTls bool) (net.Conn, error) { @@ -319,3 +320,28 @@ 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} + 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 { + t.Fatal(err) + } + + resp, err = client.Get("https://" + caddyForwardProxyProbeResist.addr + "/superhiddenfile.pac") + 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 { + t.Fatal(err) + } +} diff --git a/setup.go b/setup.go index 4248728..058322e 100644 --- a/setup.go +++ b/setup.go @@ -19,6 +19,7 @@ import ( "errors" "github.com/mholt/caddy" "github.com/mholt/caddy/caddyhttp/httpserver" + "log" "net" "net/http" "strconv" @@ -29,8 +30,8 @@ import ( func setup(c *caddy.Controller) error { httpserver.GetConfig(c).FallbackSite = true - fp := &ForwardProxy{dialTimeout: time.Second * 20, hostname: httpserver.GetConfig(c).Host(), - httpTransport: *http.DefaultTransport.(*http.Transport)} + fp := &ForwardProxy{dialTimeout: time.Second * 20, httpTransport: *http.DefaultTransport.(*http.Transport), + hostname: httpserver.GetConfig(c).Host(), port: httpserver.GetConfig(c).Port()} fp.httpTransport.DialTLS = func(network, addr string) (net.Conn, error) { return nil, &http.ProtocolError{ErrorString: "Proxy does not fetch TLS resources, use CONNECT instead"} } @@ -94,6 +95,22 @@ func setup(c *caddy.Controller) error { if len(args) == 1 { fp.probeResistDomain = args[0] } + case "serve_pac": + if len(args) > 1 { + return c.ArgErr() + } + if len(fp.pacFilePath) != 0 { + return errors.New("Parse error: 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() @@ -123,8 +140,13 @@ func setup(c *caddy.Controller) error { } } - if fp.probeResistEnabled && !fp.authRequired { - return errors.New("Parse error: probing resistance requires authentication") + if fp.probeResistEnabled { + if !fp.authRequired { + return errors.New("Parse error: probing resistance requires authentication") + } + if len(fp.probeResistDomain) > 0 { + log.Printf("Secret domain used to connect to proxy: %s\n", fp.probeResistDomain) + } } fp.httpTransport.DialContext = (&net.Dialer{ diff --git a/setup_test.go b/setup_test.go index 5d4c2ad..75df089 100644 --- a/setup_test.go +++ b/setup_test.go @@ -91,6 +91,11 @@ func TestSetup(t *testing.T) { testParsing([]string{"experimental_probe_resist local.host", "basicauth john doe"}, true) testParsing([]string{"experimental_probe_resist 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)