├── .editorconfig ├── go.mod ├── LICENSE ├── testdata └── wpad.dat ├── builtin_natives.go ├── Readme.md ├── go.sum ├── gpacw └── main.go ├── parser_test.go ├── proxy_test.go ├── parser.go ├── proxy.go ├── builtin_functions.go └── socks.go /.editorconfig: -------------------------------------------------------------------------------- 1 | # 4 space indentation 2 | [*.go] 3 | indent_style = tab 4 | indent_size = 4 -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/darren/gpac 2 | 3 | go 1.16 4 | 5 | require github.com/dop251/goja v0.0.0-20210427212725-462d53687b0d 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Darren Hoo 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /testdata/wpad.dat: -------------------------------------------------------------------------------- 1 | function FindProxyForURL(url, host) { 2 | // If the hostname matches, send direct. 3 | if ( 4 | dnsDomainIs(host, "intranet.domain.com") || 5 | shExpMatch(host, "(*.abcdomain.com|abcdomain.com)") 6 | ) 7 | return "DIRECT"; 8 | 9 | // If the protocol or URL matches, send direct. 10 | if ( 11 | url.substring(0, 4) == "ftp:" || 12 | shExpMatch(url, "http://abcdomain.com/folder/*") 13 | ) 14 | return "DIRECT"; 15 | 16 | // If the requested website is hosted within the internal network, send direct. 17 | if ( 18 | isPlainHostName(host) || 19 | shExpMatch(host, "*.local") || 20 | isInNet(dnsResolve(host), "10.0.0.0", "255.0.0.0") || 21 | isInNet(dnsResolve(host), "172.16.0.0", "255.240.0.0") || 22 | isInNet(dnsResolve(host), "192.168.0.0", "255.255.0.0") || 23 | isInNet(dnsResolve(host), "127.0.0.0", "255.255.255.0") 24 | ) 25 | return "DIRECT"; 26 | 27 | // If the IP address of the local machine is within a defined 28 | // subnet, send to a specific proxy. 29 | if (isInNet(myIpAddress(), "10.10.5.0", "255.255.255.0")) 30 | return "PROXY 1.2.3.4:8080"; 31 | 32 | // DEFAULT RULE: All other traffic, use below proxies, in fail-over order. 33 | return "PROXY 4.5.6.7:8080; PROXY 7.8.9.10:8080"; 34 | } 35 | -------------------------------------------------------------------------------- /builtin_natives.go: -------------------------------------------------------------------------------- 1 | package gpac 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/dop251/goja" 7 | ) 8 | 9 | var builtinNatives = map[string]func(*goja.Runtime) func(call goja.FunctionCall) goja.Value{ 10 | "dnsResolve": dnsResolve, 11 | "myIpAddress": myIPAddress, 12 | } 13 | 14 | func dnsResolve(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value { 15 | return func(call goja.FunctionCall) goja.Value { 16 | arg := call.Argument(0) 17 | if arg == nil || arg.Equals(goja.Undefined()) { 18 | return goja.Null() 19 | } 20 | 21 | host := arg.String() 22 | ips, err := net.LookupIP(host) 23 | if err != nil { 24 | return goja.Null() 25 | } 26 | 27 | return vm.ToValue(ips[0].String()) 28 | } 29 | } 30 | 31 | func myIPAddress(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value { 32 | return func(call goja.FunctionCall) goja.Value { 33 | ifs, err := net.Interfaces() 34 | if err != nil { 35 | return goja.Null() 36 | } 37 | 38 | for _, ifn := range ifs { 39 | if ifn.Flags&net.FlagUp != net.FlagUp { 40 | continue 41 | } 42 | 43 | addrs, err := ifn.Addrs() 44 | if err != nil { 45 | continue 46 | } 47 | 48 | for _, addr := range addrs { 49 | ip, ok := addr.(*net.IPNet) 50 | if ok && ip.IP.IsGlobalUnicast() { 51 | ipstr := ip.IP.String() 52 | return vm.ToValue(ipstr) 53 | } 54 | } 55 | } 56 | return goja.Null() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ## gpac 2 | 3 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/darren/gpac)](https://pkg.go.dev/github.com/darren/gpac) 4 | 5 | This package provides a pure Go [pac](https://developer.mozilla.org/en-US/docs/Web/HTTP/Proxy_servers_and_tunneling/Proxy_Auto-Configuration_(PAC)_file) parser based on [otto](https://github.com/robertkrimen/otto) 6 | 7 | ## Example usage 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | 15 | "github.com/darren/gpac" 16 | ) 17 | 18 | var scripts = ` 19 | function FindProxyForURL(url, host) { 20 | if (isPlainHostName(host)) return DIRECT; 21 | else return "PROXY 127.0.0.1:8080; PROXY 127.0.0.1:8081; DIRECT"; 22 | } 23 | ` 24 | 25 | func main() { 26 | pac, _ := gpac.New(scripts) 27 | 28 | r, _ := pac.FindProxyForURL("http://www.example.com/") 29 | fmt.Println(r) // returns PROXY 127.0.0.1:8080; PROXY 127.0.0.1:8081; DIRECT 30 | 31 | // Get issues request via a list of proxies and returns at the first request that succeeds 32 | resp, _ := pac.Get("http://www.example.com/") 33 | fmt.Println(resp.Status) 34 | } 35 | ``` 36 | 37 | ## Simple wrapper for `curl` and `wget` 38 | 39 | There's a simple tool that wraps `curl` and `wget` for pac file support. 40 | 41 | ### Install 42 | 43 | ``` 44 | go get github.com/darren/gpac/gpacw 45 | ``` 46 | 47 | ### Usage 48 | 49 | ``` 50 | gpacw wpad.dat curl -v http://example.com 51 | gpacw http://wpad/wpad.dat wget -O /dev/null http://example.com 52 | ``` 53 | 54 | **note** url should be the last argument of the command or it will fail. 55 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 h1:Izz0+t1Z5nI16/II7vuEo/nHjodOg0p7+OiDpjX5t1E= 3 | github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 4 | github.com/dop251/goja v0.0.0-20210427212725-462d53687b0d h1:enuVjS1vVnToj/GuGZ7QegOAIh1jF340Sg6NXcoMohs= 5 | github.com/dop251/goja v0.0.0-20210427212725-462d53687b0d/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= 6 | github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= 7 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= 8 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= 9 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 10 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 11 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 12 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 13 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 14 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 15 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 16 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 17 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 18 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 19 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 20 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 21 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 22 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 23 | -------------------------------------------------------------------------------- /gpacw/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | 11 | "github.com/darren/gpac" 12 | ) 13 | 14 | func usage() { 15 | fmt.Fprintf(os.Stderr, "Usage: %s test.pac curl example.com\n", os.Args[0]) 16 | os.Exit(1) 17 | } 18 | 19 | func run(cmds string, oargs []string, proxy string) error { 20 | var args = oargs 21 | if proxy != "" { 22 | switch cmds { 23 | case "wget": 24 | args = append([]string{ 25 | "-e", "http_proxy=" + proxy, 26 | "-e", "https_proxy=" + proxy, 27 | }, oargs...) 28 | case "curl": 29 | args = append([]string{"-x", proxy}, oargs...) 30 | } 31 | } 32 | 33 | log.Printf("Invoke %s %v", cmds, args) 34 | 35 | cmd := exec.Command(cmds, args...) 36 | 37 | stderr, err := cmd.StderrPipe() 38 | if err != nil { 39 | return err 40 | } 41 | defer stderr.Close() 42 | 43 | stdout, err := cmd.StdoutPipe() 44 | if err != nil { 45 | return err 46 | } 47 | defer stdout.Close() 48 | 49 | go func() { 50 | io.Copy(os.Stdout, stdout) 51 | }() 52 | 53 | go func() { 54 | io.Copy(os.Stderr, stderr) 55 | }() 56 | 57 | if err := cmd.Run(); err != nil { 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func main() { 65 | if len(os.Args) < 4 { 66 | usage() 67 | } 68 | 69 | pacf := os.Args[1] 70 | cmds := os.Args[2] 71 | args := os.Args[3:] 72 | dst := os.Args[len(os.Args)-1] 73 | 74 | if strings.HasPrefix(dst, "http://") && 75 | strings.HasPrefix(dst, "https://") { 76 | dst = "http://" + dst 77 | } 78 | 79 | switch cmds { 80 | case "curl", "wget": 81 | default: 82 | log.Fatalf("Only curl or wget is supported, %s is given", cmds) 83 | } 84 | 85 | pac, err := gpac.From(pacf) 86 | if err != nil { 87 | log.Fatalf("Fail to load pac file: %v", err) 88 | } 89 | 90 | p, err := pac.FindProxy(dst) 91 | if err != nil { 92 | log.Fatal(err) 93 | } 94 | 95 | for _, x := range p { 96 | err = run(cmds, args, x.URL()) 97 | if err == nil { 98 | break 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package gpac_test 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "testing" 8 | 9 | "github.com/darren/gpac" 10 | ) 11 | 12 | func Example() { 13 | pacf, _ := os.Open("testdata/wpad.dat") 14 | defer pacf.Close() 15 | 16 | data, _ := ioutil.ReadAll(pacf) 17 | pac, _ := gpac.New(string(data)) 18 | 19 | r, _ := pac.FindProxyForURL("http://www.example.com/") 20 | 21 | fmt.Println(r) 22 | // Output: 23 | // PROXY 4.5.6.7:8080; PROXY 7.8.9.10:8080 24 | } 25 | 26 | func TestProxyGet(t *testing.T) { 27 | pacf, _ := os.Open("testdata/wpad.dat") 28 | defer pacf.Close() 29 | 30 | data, _ := ioutil.ReadAll(pacf) 31 | 32 | pac, err := gpac.New(string(data)) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | proxies, err := pac.FindProxy("http://www.example.com/") 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | if len(proxies) != 2 { 43 | t.Fatal("Find proxy failed") 44 | } 45 | 46 | if proxies[1].URL() != "7.8.9.10:8080" { 47 | t.Error("Get URL from proxy failed") 48 | } 49 | 50 | } 51 | 52 | func TestProxyGetDirect(t *testing.T) { 53 | dsts := []string{ 54 | "http://localhost/", 55 | "https://intranet.domain.com", 56 | "http://abcdomain.com", 57 | "http://www.abcdomain.com", 58 | "ftp://example.com.com", 59 | } 60 | 61 | pacf, _ := os.Open("testdata/wpad.dat") 62 | defer pacf.Close() 63 | 64 | data, _ := ioutil.ReadAll(pacf) 65 | 66 | pac, err := gpac.New(string(data)) 67 | if err != nil { 68 | t.Fatal(err) 69 | } 70 | 71 | for _, dst := range dsts { 72 | proxies, err := pac.FindProxy(dst) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | 77 | if len(proxies) != 1 { 78 | t.Fatalf("Find proxy failed for %s", dst) 79 | } 80 | 81 | if proxies[0].URL() != "" { 82 | t.Errorf("Get URL from proxy failed: %s, proxies: %+v", dst, proxies) 83 | } 84 | } 85 | } 86 | 87 | // test server started in proxy_test.go 88 | func TestPacGet(t *testing.T) { 89 | pac, err := gpac.New(` 90 | function FindProxyForURL(url, host) { 91 | return "PROXY 127.0.0.1:9991; PROXY 127.0.0.1:9992; PROXY 127.0.0.1:8080; DIRECT" 92 | } 93 | `) 94 | 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | 99 | resp, err := pac.Get("http://localhost:8081") 100 | if err != nil { 101 | t.Fatal(err) 102 | } 103 | 104 | body := readBodyAndClose(resp) 105 | if body != "Example" { 106 | t.Errorf("Response not expected: %s", body) 107 | } 108 | } 109 | 110 | func BenchmarkFind(b *testing.B) { 111 | pacf, _ := os.Open("testdata/wpad.dat") 112 | defer pacf.Close() 113 | 114 | data, _ := ioutil.ReadAll(pacf) 115 | pac, _ := gpac.New(string(data)) 116 | 117 | for n := 0; n < b.N; n++ { 118 | pac.FindProxyForURL("http://www.example.com/") 119 | pac.FindProxyForURL("http://localhost/") 120 | pac.FindProxyForURL("http://192.168.1.1/") 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /proxy_test.go: -------------------------------------------------------------------------------- 1 | package gpac_test 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "testing" 9 | 10 | "github.com/darren/gpac" 11 | ) 12 | 13 | func init() { 14 | var mux http.ServeMux 15 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 16 | io.WriteString(w, "Example") 17 | }) 18 | go func() { 19 | log.Fatal(http.ListenAndServe("127.0.0.1:8080", &mux)) 20 | }() 21 | 22 | go func() { 23 | log.Fatal(http.ListenAndServe("127.0.0.1:8081", &mux)) 24 | }() 25 | } 26 | 27 | func TestParseProxy(t *testing.T) { 28 | proxy := "PROXY 127.0.0.1:8080; SOCKs 127.0.0.1:1080; Direct" 29 | 30 | proxies := gpac.ParseProxy(proxy) 31 | 32 | if len(proxies) != 3 { 33 | t.Error("Parse failed") 34 | return 35 | } 36 | 37 | if proxies[1].Type != "SOCKS" { 38 | t.Error("Should be SOCKS5") 39 | } 40 | 41 | if !proxies[1].IsSOCKS() { 42 | t.Error("Should be SOCKS5") 43 | } 44 | 45 | if !proxies[2].IsDirect() { 46 | t.Error("Should be direct") 47 | } 48 | } 49 | 50 | func TestParseSOCKS(t *testing.T) { 51 | proxy := "SOCKS5 127.0.0.1:1080" 52 | 53 | proxies := gpac.ParseProxy(proxy) 54 | 55 | if len(proxies) != 1 { 56 | t.Error("Parse failed") 57 | return 58 | } 59 | 60 | if !proxies[0].IsSOCKS() { 61 | t.Error("Should be SOCKS5") 62 | } 63 | 64 | if proxies[0].IsDirect() { 65 | t.Error("Should be direct") 66 | } 67 | } 68 | 69 | func readBodyAndClose(resp *http.Response) string { 70 | defer resp.Body.Close() 71 | 72 | buf, err := ioutil.ReadAll(resp.Body) 73 | if err != nil { 74 | return "" 75 | } 76 | 77 | return string(buf) 78 | } 79 | 80 | func testProxyGet(t *testing.T, typ string) { 81 | t.Logf("Test proxy type: %s", typ) 82 | 83 | var p = gpac.Proxy{Type: typ, Address: "127.0.0.1:8080"} 84 | 85 | client := http.Client{ 86 | Transport: &http.Transport{ 87 | Proxy: p.Proxy(), 88 | }, 89 | } 90 | 91 | resp, err := client.Get("http://127.0.0.1:8081") 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | 96 | body := readBodyAndClose(resp) 97 | if body != "Example" { 98 | t.Errorf("Response not expected: %s", body) 99 | } 100 | } 101 | 102 | func testClientGet(t *testing.T, typ string) { 103 | t.Logf("Test Client proxy type: %s", typ) 104 | 105 | var p = gpac.Proxy{Type: typ, Address: "127.0.0.1:8080"} 106 | 107 | resp, err := p.Get("http://127.0.0.1:8081") 108 | if err != nil { 109 | t.Fatal(err) 110 | } 111 | 112 | body := readBodyAndClose(resp) 113 | if body != "Example" { 114 | t.Errorf("Response not expected: %s", body) 115 | } 116 | } 117 | 118 | func testTransport(t *testing.T, typ string) { 119 | t.Logf("Test Transport proxy type: %s", typ) 120 | 121 | var p = gpac.Proxy{Type: typ, Address: "127.0.0.1:8080"} 122 | 123 | req, err := http.NewRequest("GET", "http://127.0.0.1:8081", nil) 124 | if err != nil { 125 | t.Fatal(err) 126 | } 127 | 128 | resp, err := p.Transport().RoundTrip(req) 129 | if err != nil { 130 | t.Fatal(err) 131 | } 132 | 133 | body := readBodyAndClose(resp) 134 | if body != "Example" { 135 | t.Errorf("Response not expected: %s", body) 136 | } 137 | } 138 | 139 | func TestMultiProxyGet(t *testing.T) { 140 | //BUG: SOCKS5 seems not work 141 | knownTypes := []string{"DIRECT", "HTTP"} 142 | for _, typ := range knownTypes { 143 | testProxyGet(t, typ) 144 | testClientGet(t, typ) 145 | testTransport(t, typ) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package gpac 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "strings" 12 | "sync" 13 | 14 | "github.com/dop251/goja" 15 | ) 16 | 17 | // Parser the parsed pac instance 18 | type Parser struct { 19 | vm *goja.Runtime 20 | src string // the FindProxyForURL source code 21 | 22 | sync.Mutex 23 | } 24 | 25 | // FindProxyForURL finding proxy for url 26 | // returns string like: 27 | // PROXY 4.5.6.7:8080; PROXY 7.8.9.10:8080; DIRECT 28 | func (p *Parser) FindProxyForURL(urlstr string) (string, error) { 29 | u, err := url.Parse(urlstr) 30 | if err != nil { 31 | return "", err 32 | } 33 | 34 | f := fmt.Sprintf("FindProxyForURL('%s', '%s')", urlstr, u.Hostname()) 35 | p.Lock() 36 | r, err := p.vm.RunString(f) 37 | p.Unlock() 38 | 39 | if err != nil { 40 | return "", err 41 | } 42 | return r.String(), nil 43 | } 44 | 45 | // FindProxy find the proxy in pac and return a list of Proxy 46 | func (p *Parser) FindProxy(urlstr string) ([]*Proxy, error) { 47 | ps, err := p.FindProxyForURL(urlstr) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | return ParseProxy(ps), nil 53 | } 54 | 55 | // Get issues a GET to the specified URL via the proxy list found 56 | // it stops at the first proxy that succeeds 57 | func (p *Parser) Get(urlstr string) (*http.Response, error) { 58 | req, err := http.NewRequest("GET", urlstr, nil) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | return p.Do(req) 64 | } 65 | 66 | // Do sends an HTTP request via a list of proxies found 67 | // it returns first HTTP response that succeeds 68 | func (p *Parser) Do(req *http.Request) (*http.Response, error) { 69 | ps, err := p.FindProxyForURL(req.URL.String()) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | proxies := ParseProxy(ps) 75 | if len(proxies) == 0 { 76 | return nil, errors.New("no proxies found") 77 | } 78 | 79 | for _, proxy := range proxies { 80 | resp, err := proxy.Do(req) 81 | if err == nil { 82 | return resp, nil 83 | } 84 | } 85 | return nil, errors.New("no request via proxies succeeds") 86 | } 87 | 88 | // Source returns the original javascript snippet of the pac 89 | func (p *Parser) Source() string { 90 | return p.src 91 | } 92 | 93 | // New create a parser from text content 94 | func New(text string) (*Parser, error) { 95 | vm := goja.New() 96 | registerBuiltinNatives(vm) 97 | registerBuiltinJS(vm) 98 | 99 | _, err := vm.RunString(text) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | return &Parser{vm: vm, src: text}, nil 105 | } 106 | 107 | func registerBuiltinJS(vm *goja.Runtime) { 108 | _, err := vm.RunString(builtinJS) 109 | if err != nil { 110 | panic(err) 111 | } 112 | } 113 | 114 | func registerBuiltinNatives(vm *goja.Runtime) { 115 | for name, function := range builtinNatives { 116 | vm.Set(name, function(vm)) 117 | } 118 | } 119 | 120 | func fromReader(r io.ReadCloser) (*Parser, error) { 121 | defer r.Close() 122 | buf, err := ioutil.ReadAll(r) 123 | if err != nil { 124 | return nil, err 125 | } 126 | return New(string(buf)) 127 | } 128 | 129 | // FromFile load pac from file 130 | func FromFile(filename string) (*Parser, error) { 131 | f, err := os.Open(filename) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | return fromReader(f) 137 | } 138 | 139 | // FromURL load pac from url 140 | func FromURL(urlstr string) (*Parser, error) { 141 | resp, err := http.Get(urlstr) 142 | if err != nil { 143 | return nil, err 144 | } 145 | return fromReader(resp.Body) 146 | } 147 | 148 | // From load pac from file or url 149 | func From(dst string) (*Parser, error) { 150 | if strings.HasPrefix(dst, "http://") || 151 | strings.HasPrefix(dst, "https://") { 152 | return FromURL(dst) 153 | } 154 | 155 | return FromFile(dst) 156 | } 157 | -------------------------------------------------------------------------------- /proxy.go: -------------------------------------------------------------------------------- 1 | package gpac 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | "net/url" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | // Proxy is proxy type defined in pac file 15 | // like 16 | // PROXY 127.0.0.1:8080 17 | // SOCKS 127.0.0.1:1080 18 | type Proxy struct { 19 | Type string // Proxy type: PROXY HTTP HTTPS SOCKS DIRECT etc. 20 | Address string // Proxy address 21 | Username string // Proxy username 22 | Password string // Proxy password 23 | 24 | client *http.Client 25 | auth string 26 | tr *http.Transport 27 | } 28 | 29 | func (p *Proxy) Init() { 30 | if p.Username != "" && p.Password != "" { 31 | p.auth = "Basic " + base64.StdEncoding.EncodeToString([]byte(p.Username+":"+p.Password)) 32 | } 33 | 34 | p.tr = &http.Transport{ 35 | Proxy: p.Proxy(), 36 | } 37 | 38 | p.client = &http.Client{ 39 | Transport: p.tr, 40 | } 41 | } 42 | 43 | // IsDirect tests whether it is using direct connection 44 | func (p *Proxy) IsDirect() bool { 45 | return p.Type == "DIRECT" 46 | } 47 | 48 | // IsSOCKS test whether it is a socks proxy 49 | func (p *Proxy) IsSOCKS() bool { 50 | if len(p.Type) >= 5 { 51 | return p.Type[:5] == "SOCKS" 52 | } 53 | return false 54 | } 55 | 56 | // URL returns a url representation for the proxy for curl -x 57 | func (p *Proxy) URL() string { 58 | switch p.Type { 59 | case "DIRECT": 60 | return "" 61 | case "PROXY": 62 | return p.Address 63 | default: 64 | return fmt.Sprintf("%s://%s", strings.ToLower(p.Type), p.Address) 65 | } 66 | } 67 | 68 | // Proxy returns Proxy function that is ready use for http.Transport 69 | func (p *Proxy) Proxy() func(*http.Request) (*url.URL, error) { 70 | var u *url.URL 71 | var ustr string 72 | var err error 73 | 74 | switch p.Type { 75 | case "DIRECT": 76 | break 77 | case "PROXY": 78 | if p.Username != "" && p.Password != "" { 79 | ustr = fmt.Sprintf("http://%s:%s@%s", p.Username, p.Password, p.Address) 80 | } else { 81 | ustr = fmt.Sprintf("http://%s", p.Address) 82 | } 83 | default: 84 | if p.Username != "" && p.Password != "" { 85 | ustr = fmt.Sprintf("%s:%s@%s://%s", p.Username, p.Password, strings.ToLower(p.Type), p.Address) 86 | } else { 87 | ustr = fmt.Sprintf("%s://%s", strings.ToLower(p.Type), p.Address) 88 | } 89 | } 90 | 91 | if ustr != "" { 92 | u, err = url.Parse(ustr) 93 | } 94 | 95 | return func(*http.Request) (*url.URL, error) { 96 | return u, err 97 | } 98 | } 99 | 100 | var zeroDialer net.Dialer 101 | 102 | // Dialer returns a Dial function that will connect to remote address 103 | func (p *Proxy) Dialer() func(ctx context.Context, network, addr string) (net.Conn, error) { 104 | switch p.Type { 105 | case "DIRECT": 106 | return (&net.Dialer{ 107 | Timeout: 30 * time.Second, 108 | KeepAlive: 30 * time.Second, 109 | DualStack: true, 110 | }).DialContext 111 | case "SOCKS", "SOCKS5": 112 | return func(ctx context.Context, network, address string) (net.Conn, error) { 113 | d := socksNewDialer(network, p.Address) 114 | conn, err := d.DialContext(ctx, network, address) 115 | return conn, err 116 | } 117 | case "PROXY", "HTTP": 118 | return func(ctx context.Context, network, address string) (net.Conn, error) { 119 | conn, err := zeroDialer.DialContext(ctx, network, p.Address) 120 | 121 | header := make(http.Header) 122 | if p.auth != "" { 123 | header.Add("Authorization", p.auth) 124 | } 125 | 126 | //log.Printf("Dial %s [address:%s] [p.address: %s] header: %v", network, address, p.Address, header) 127 | 128 | if err == nil { 129 | connectReq := &http.Request{ 130 | Method: "CONNECT", 131 | URL: &url.URL{Opaque: address}, 132 | Host: address, 133 | Header: header, 134 | } 135 | connectReq.Write(conn) 136 | } 137 | return conn, err 138 | } 139 | default: 140 | return func(ctx context.Context, network, address string) (net.Conn, error) { 141 | return nil, fmt.Errorf("%s not support", p.Type) 142 | } 143 | } 144 | } 145 | 146 | // Client returns an http.Client ready for use with this proxy 147 | func (p *Proxy) Client() *http.Client { 148 | return p.client 149 | } 150 | 151 | // Get issues a GET to the specified URL via the proxy 152 | func (p *Proxy) Get(urlstr string) (*http.Response, error) { 153 | return p.Client().Get(urlstr) 154 | } 155 | 156 | // Transport get the http.RoundTripper 157 | func (p *Proxy) Transport() *http.Transport { 158 | return p.tr 159 | } 160 | 161 | // Do sends an HTTP request via the proxy and returns an HTTP response 162 | func (p *Proxy) Do(req *http.Request) (*http.Response, error) { 163 | return p.Client().Do(req) 164 | } 165 | 166 | func (p *Proxy) String() string { 167 | if p.IsDirect() { 168 | return p.Type 169 | } 170 | return fmt.Sprintf("%s %s", p.Type, p.Address) 171 | } 172 | 173 | func split(source string, s rune) []string { 174 | return strings.FieldsFunc(source, func(r rune) bool { 175 | return r == s 176 | }) 177 | } 178 | 179 | // ParseProxy parses proxy string returned by FindProxyForURL 180 | // and returns a slice of proxies 181 | func ParseProxy(pstr string) []*Proxy { 182 | var proxies []*Proxy 183 | for _, p := range split(pstr, ';') { 184 | typeAddr := strings.Fields(p) 185 | if len(typeAddr) == 2 { 186 | typ := strings.ToUpper(typeAddr[0]) 187 | addr := typeAddr[1] 188 | var user, pass string 189 | if at := strings.Index(addr, "@"); at > 0 { 190 | auth := split(addr[:at], ':') 191 | if len(auth) == 2 { 192 | user = auth[0] 193 | pass = auth[1] 194 | } 195 | addr = addr[at+1:] 196 | } 197 | proxy := &Proxy{ 198 | Type: typ, 199 | Address: addr, 200 | Username: user, 201 | Password: pass, 202 | } 203 | proxy.Init() 204 | proxies = append(proxies, proxy) 205 | } else if len(typeAddr) == 1 { 206 | proxies = append(proxies, 207 | &Proxy{ 208 | Type: strings.ToUpper(typeAddr[0]), 209 | }, 210 | ) 211 | } 212 | } 213 | 214 | return proxies 215 | } 216 | -------------------------------------------------------------------------------- /builtin_functions.go: -------------------------------------------------------------------------------- 1 | package gpac 2 | 3 | // This file is extracted from 4 | // https://hg.mozilla.org/mozilla-central/file/tip/netwerk/base/ProxyAutoConfig.cpp 5 | // 6 | // For licence please refer to https://www.mozilla.org/en-US/foundation/licensing/ 7 | 8 | var builtinJS = ` 9 | function dnsDomainIs(host, domain) { 10 | return ( 11 | host.length >= domain.length && 12 | host.substring(host.length - domain.length) == domain 13 | ); 14 | } 15 | 16 | function dnsDomainLevels(host) { 17 | return host.split(".").length - 1; 18 | } 19 | 20 | function isValidIpAddress(ipchars) { 21 | var matches = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec( 22 | ipchars 23 | ); 24 | if (matches == null) { 25 | return false; 26 | } else if ( 27 | matches[1] > 255 || 28 | matches[2] > 255 || 29 | matches[3] > 255 || 30 | matches[4] > 255 31 | ) { 32 | return false; 33 | } 34 | return true; 35 | } 36 | 37 | function convert_addr(ipchars) { 38 | var bytes = ipchars.split("."); 39 | var result = 40 | ((bytes[0] & 0xff) << 24) | 41 | ((bytes[1] & 0xff) << 16) | 42 | ((bytes[2] & 0xff) << 8) | 43 | (bytes[3] & 0xff); 44 | return result; 45 | } 46 | 47 | function isInNet(ipaddr, pattern, maskstr) { 48 | if (!isValidIpAddress(pattern) || !isValidIpAddress(maskstr)) { 49 | return false; 50 | } 51 | if (!isValidIpAddress(ipaddr)) { 52 | ipaddr = dnsResolve(ipaddr); 53 | if (ipaddr == null) { 54 | return false; 55 | } 56 | } 57 | var host = convert_addr(ipaddr); 58 | var pat = convert_addr(pattern); 59 | var mask = convert_addr(maskstr); 60 | return (host & mask) == (pat & mask); 61 | } 62 | 63 | function isPlainHostName(host) { 64 | return host.search("\\.") == -1; 65 | } 66 | 67 | function isResolvable(host) { 68 | var ip = dnsResolve(host); 69 | return ip != null; 70 | } 71 | 72 | function localHostOrDomainIs(host, hostdom) { 73 | return host == hostdom || hostdom.lastIndexOf(host + ".", 0) == 0; 74 | } 75 | 76 | function shExpMatch(url, pattern) { 77 | pattern = pattern.replace(/\./g, "\\."); 78 | pattern = pattern.replace(/\*/g, ".*"); 79 | pattern = pattern.replace(/\?/g, "."); 80 | var newRe = new RegExp("^" + pattern + "$"); 81 | return newRe.test(url); 82 | } 83 | 84 | var wdays = { SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6 }; 85 | var months = { 86 | JAN: 0, 87 | FEB: 1, 88 | MAR: 2, 89 | APR: 3, 90 | MAY: 4, 91 | JUN: 5, 92 | JUL: 6, 93 | AUG: 7, 94 | SEP: 8, 95 | OCT: 9, 96 | NOV: 10, 97 | DEC: 11 98 | }; 99 | 100 | function weekdayRange() { 101 | function getDay(weekday) { 102 | if (weekday in wdays) { 103 | return wdays[weekday]; 104 | } 105 | return -1; 106 | } 107 | var date = new Date(); 108 | var argc = arguments.length; 109 | var wday; 110 | if (argc < 1) return false; 111 | if (arguments[argc - 1] == "GMT") { 112 | argc--; 113 | wday = date.getUTCDay(); 114 | } else { 115 | wday = date.getDay(); 116 | } 117 | var wd1 = getDay(arguments[0]); 118 | var wd2 = argc == 2 ? getDay(arguments[1]) : wd1; 119 | return wd1 == -1 || wd2 == -1 120 | ? false 121 | : wd1 <= wd2 122 | ? wd1 <= wday && wday <= wd2 123 | : wd2 >= wday || wday >= wd1; 124 | } 125 | 126 | function dateRange() { 127 | function getMonth(name) { 128 | if (name in months) { 129 | return months[name]; 130 | } 131 | return -1; 132 | } 133 | var date = new Date(); 134 | var argc = arguments.length; 135 | if (argc < 1) { 136 | return false; 137 | } 138 | var isGMT = arguments[argc - 1] == "GMT"; 139 | 140 | if (isGMT) { 141 | argc--; 142 | } 143 | // function will work even without explict handling of this case 144 | if (argc == 1) { 145 | var tmp = parseInt(arguments[0]); 146 | if (isNaN(tmp)) { 147 | return ( 148 | (isGMT ? date.getUTCMonth() : date.getMonth()) == getMonth(arguments[0]) 149 | ); 150 | } else if (tmp < 32) { 151 | return (isGMT ? date.getUTCDate() : date.getDate()) == tmp; 152 | } else { 153 | return (isGMT ? date.getUTCFullYear() : date.getFullYear()) == tmp; 154 | } 155 | } 156 | var year = date.getFullYear(); 157 | var date1, date2; 158 | date1 = new Date(year, 0, 1, 0, 0, 0); 159 | date2 = new Date(year, 11, 31, 23, 59, 59); 160 | var adjustMonth = false; 161 | for (var i = 0; i < argc >> 1; i++) { 162 | var tmp = parseInt(arguments[i]); 163 | if (isNaN(tmp)) { 164 | var mon = getMonth(arguments[i]); 165 | date1.setMonth(mon); 166 | } else if (tmp < 32) { 167 | adjustMonth = argc <= 2; 168 | date1.setDate(tmp); 169 | } else { 170 | date1.setFullYear(tmp); 171 | } 172 | } 173 | for (var i = argc >> 1; i < argc; i++) { 174 | var tmp = parseInt(arguments[i]); 175 | if (isNaN(tmp)) { 176 | var mon = getMonth(arguments[i]); 177 | date2.setMonth(mon); 178 | } else if (tmp < 32) { 179 | date2.setDate(tmp); 180 | } else { 181 | date2.setFullYear(tmp); 182 | } 183 | } 184 | if (adjustMonth) { 185 | date1.setMonth(date.getMonth()); 186 | date2.setMonth(date.getMonth()); 187 | } 188 | if (isGMT) { 189 | var tmp = date; 190 | tmp.setFullYear(date.getUTCFullYear()); 191 | tmp.setMonth(date.getUTCMonth()); 192 | tmp.setDate(date.getUTCDate()); 193 | tmp.setHours(date.getUTCHours()); 194 | tmp.setMinutes(date.getUTCMinutes()); 195 | tmp.setSeconds(date.getUTCSeconds()); 196 | date = tmp; 197 | } 198 | return date1 <= date2 199 | ? date1 <= date && date <= date2 200 | : date2 >= date || date >= date1; 201 | } 202 | 203 | function timeRange() { 204 | var argc = arguments.length; 205 | var date = new Date(); 206 | var isGMT = false; 207 | 208 | if (argc < 1) { 209 | return false; 210 | } 211 | if (arguments[argc - 1] == "GMT") { 212 | isGMT = true; 213 | argc--; 214 | } 215 | 216 | var hour = isGMT ? date.getUTCHours() : date.getHours(); 217 | var date1, date2; 218 | date1 = new Date(); 219 | date2 = new Date(); 220 | 221 | if (argc == 1) { 222 | return hour == arguments[0]; 223 | } else if (argc == 2) { 224 | return arguments[0] <= hour && hour <= arguments[1]; 225 | } else { 226 | switch (argc) { 227 | case 6: 228 | date1.setSeconds(arguments[2]); 229 | date2.setSeconds(arguments[5]); 230 | case 4: 231 | var middle = argc >> 1; 232 | date1.setHours(arguments[0]); 233 | date1.setMinutes(arguments[1]); 234 | date2.setHours(arguments[middle]); 235 | date2.setMinutes(arguments[middle + 1]); 236 | if (middle == 2) { 237 | date2.setSeconds(59); 238 | } 239 | break; 240 | default: 241 | throw "timeRange: bad number of arguments"; 242 | } 243 | } 244 | 245 | if (isGMT) { 246 | date.setFullYear(date.getUTCFullYear()); 247 | date.setMonth(date.getUTCMonth()); 248 | date.setDate(date.getUTCDate()); 249 | date.setHours(date.getUTCHours()); 250 | date.setMinutes(date.getUTCMinutes()); 251 | date.setSeconds(date.getUTCSeconds()); 252 | } 253 | return date1 <= date2 254 | ? date1 <= date && date <= date2 255 | : date2 >= date || date >= date1; 256 | } 257 | ` 258 | -------------------------------------------------------------------------------- /socks.go: -------------------------------------------------------------------------------- 1 | // Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT. 2 | //go:generate bundle -o socks_bundle.go -dst net/http -prefix socks -underscore golang.org/x/net/internal/socks 3 | 4 | // Package socks provides a SOCKS version 5 client implementation. 5 | // 6 | // SOCKS protocol version 5 is defined in RFC 1928. 7 | // Username/Password authentication for SOCKS version 5 is defined in 8 | // RFC 1929. 9 | // 10 | 11 | package gpac 12 | 13 | import ( 14 | "context" 15 | "errors" 16 | "io" 17 | "net" 18 | "strconv" 19 | "time" 20 | ) 21 | 22 | var ( 23 | socksnoDeadline = time.Time{} 24 | socksaLongTimeAgo = time.Unix(1, 0) 25 | ) 26 | 27 | func (d *socksDialer) connect(ctx context.Context, c net.Conn, address string) (_ net.Addr, ctxErr error) { 28 | host, port, err := sockssplitHostPort(address) 29 | if err != nil { 30 | return nil, err 31 | } 32 | if deadline, ok := ctx.Deadline(); ok && !deadline.IsZero() { 33 | c.SetDeadline(deadline) 34 | defer c.SetDeadline(socksnoDeadline) 35 | } 36 | if ctx != context.Background() { 37 | errCh := make(chan error, 1) 38 | done := make(chan struct{}) 39 | defer func() { 40 | close(done) 41 | if ctxErr == nil { 42 | ctxErr = <-errCh 43 | } 44 | }() 45 | go func() { 46 | select { 47 | case <-ctx.Done(): 48 | c.SetDeadline(socksaLongTimeAgo) 49 | errCh <- ctx.Err() 50 | case <-done: 51 | errCh <- nil 52 | } 53 | }() 54 | } 55 | 56 | b := make([]byte, 0, 6+len(host)) // the size here is just an estimate 57 | b = append(b, socksVersion5) 58 | if len(d.AuthMethods) == 0 || d.Authenticate == nil { 59 | b = append(b, 1, byte(socksAuthMethodNotRequired)) 60 | } else { 61 | ams := d.AuthMethods 62 | if len(ams) > 255 { 63 | return nil, errors.New("too many authentication methods") 64 | } 65 | b = append(b, byte(len(ams))) 66 | for _, am := range ams { 67 | b = append(b, byte(am)) 68 | } 69 | } 70 | if _, ctxErr = c.Write(b); ctxErr != nil { 71 | return 72 | } 73 | 74 | if _, ctxErr = io.ReadFull(c, b[:2]); ctxErr != nil { 75 | return 76 | } 77 | if b[0] != socksVersion5 { 78 | return nil, errors.New("unexpected protocol version " + strconv.Itoa(int(b[0]))) 79 | } 80 | am := socksAuthMethod(b[1]) 81 | if am == socksAuthMethodNoAcceptableMethods { 82 | return nil, errors.New("no acceptable authentication methods") 83 | } 84 | if d.Authenticate != nil { 85 | if ctxErr = d.Authenticate(ctx, c, am); ctxErr != nil { 86 | return 87 | } 88 | } 89 | 90 | b = b[:0] 91 | b = append(b, socksVersion5, byte(d.cmd), 0) 92 | if ip := net.ParseIP(host); ip != nil { 93 | if ip4 := ip.To4(); ip4 != nil { 94 | b = append(b, socksAddrTypeIPv4) 95 | b = append(b, ip4...) 96 | } else if ip6 := ip.To16(); ip6 != nil { 97 | b = append(b, socksAddrTypeIPv6) 98 | b = append(b, ip6...) 99 | } else { 100 | return nil, errors.New("unknown address type") 101 | } 102 | } else { 103 | if len(host) > 255 { 104 | return nil, errors.New("FQDN too long") 105 | } 106 | b = append(b, socksAddrTypeFQDN) 107 | b = append(b, byte(len(host))) 108 | b = append(b, host...) 109 | } 110 | b = append(b, byte(port>>8), byte(port)) 111 | if _, ctxErr = c.Write(b); ctxErr != nil { 112 | return 113 | } 114 | 115 | if _, ctxErr = io.ReadFull(c, b[:4]); ctxErr != nil { 116 | return 117 | } 118 | if b[0] != socksVersion5 { 119 | return nil, errors.New("unexpected protocol version " + strconv.Itoa(int(b[0]))) 120 | } 121 | if cmdErr := socksReply(b[1]); cmdErr != socksStatusSucceeded { 122 | return nil, errors.New("unknown error " + cmdErr.String()) 123 | } 124 | if b[2] != 0 { 125 | return nil, errors.New("non-zero reserved field") 126 | } 127 | l := 2 128 | var a socksAddr 129 | switch b[3] { 130 | case socksAddrTypeIPv4: 131 | l += net.IPv4len 132 | a.IP = make(net.IP, net.IPv4len) 133 | case socksAddrTypeIPv6: 134 | l += net.IPv6len 135 | a.IP = make(net.IP, net.IPv6len) 136 | case socksAddrTypeFQDN: 137 | if _, err := io.ReadFull(c, b[:1]); err != nil { 138 | return nil, err 139 | } 140 | l += int(b[0]) 141 | default: 142 | return nil, errors.New("unknown address type " + strconv.Itoa(int(b[3]))) 143 | } 144 | if cap(b) < l { 145 | b = make([]byte, l) 146 | } else { 147 | b = b[:l] 148 | } 149 | if _, ctxErr = io.ReadFull(c, b); ctxErr != nil { 150 | return 151 | } 152 | if a.IP != nil { 153 | copy(a.IP, b) 154 | } else { 155 | a.Name = string(b[:len(b)-2]) 156 | } 157 | a.Port = int(b[len(b)-2])<<8 | int(b[len(b)-1]) 158 | return &a, nil 159 | } 160 | 161 | func sockssplitHostPort(address string) (string, int, error) { 162 | host, port, err := net.SplitHostPort(address) 163 | if err != nil { 164 | return "", 0, err 165 | } 166 | portnum, err := strconv.Atoi(port) 167 | if err != nil { 168 | return "", 0, err 169 | } 170 | if 1 > portnum || portnum > 0xffff { 171 | return "", 0, errors.New("port number out of range " + port) 172 | } 173 | return host, portnum, nil 174 | } 175 | 176 | // A Command represents a SOCKS command. 177 | type socksCommand int 178 | 179 | func (cmd socksCommand) String() string { 180 | switch cmd { 181 | case socksCmdConnect: 182 | return "socks connect" 183 | case sockscmdBind: 184 | return "socks bind" 185 | default: 186 | return "socks " + strconv.Itoa(int(cmd)) 187 | } 188 | } 189 | 190 | // An AuthMethod represents a SOCKS authentication method. 191 | type socksAuthMethod int 192 | 193 | // A Reply represents a SOCKS command reply code. 194 | type socksReply int 195 | 196 | func (code socksReply) String() string { 197 | switch code { 198 | case socksStatusSucceeded: 199 | return "succeeded" 200 | case 0x01: 201 | return "general SOCKS server failure" 202 | case 0x02: 203 | return "connection not allowed by ruleset" 204 | case 0x03: 205 | return "network unreachable" 206 | case 0x04: 207 | return "host unreachable" 208 | case 0x05: 209 | return "connection refused" 210 | case 0x06: 211 | return "TTL expired" 212 | case 0x07: 213 | return "command not supported" 214 | case 0x08: 215 | return "address type not supported" 216 | default: 217 | return "unknown code: " + strconv.Itoa(int(code)) 218 | } 219 | } 220 | 221 | // Wire protocol constants. 222 | const ( 223 | socksVersion5 = 0x05 224 | 225 | socksAddrTypeIPv4 = 0x01 226 | socksAddrTypeFQDN = 0x03 227 | socksAddrTypeIPv6 = 0x04 228 | 229 | socksCmdConnect socksCommand = 0x01 // establishes an active-open forward proxy connection 230 | sockscmdBind socksCommand = 0x02 // establishes a passive-open forward proxy connection 231 | 232 | socksAuthMethodNotRequired socksAuthMethod = 0x00 // no authentication required 233 | socksAuthMethodUsernamePassword socksAuthMethod = 0x02 // use username/password 234 | socksAuthMethodNoAcceptableMethods socksAuthMethod = 0xff // no acceptable authentication methods 235 | 236 | socksStatusSucceeded socksReply = 0x00 237 | ) 238 | 239 | // An Addr represents a SOCKS-specific address. 240 | // Either Name or IP is used exclusively. 241 | type socksAddr struct { 242 | Name string // fully-qualified domain name 243 | IP net.IP 244 | Port int 245 | } 246 | 247 | func (a *socksAddr) Network() string { return "socks" } 248 | 249 | func (a *socksAddr) String() string { 250 | if a == nil { 251 | return "" 252 | } 253 | port := strconv.Itoa(a.Port) 254 | if a.IP == nil { 255 | return net.JoinHostPort(a.Name, port) 256 | } 257 | return net.JoinHostPort(a.IP.String(), port) 258 | } 259 | 260 | // A Conn represents a forward proxy connection. 261 | type socksConn struct { 262 | net.Conn 263 | 264 | boundAddr net.Addr 265 | } 266 | 267 | // BoundAddr returns the address assigned by the proxy server for 268 | // connecting to the command target address from the proxy server. 269 | func (c *socksConn) BoundAddr() net.Addr { 270 | if c == nil { 271 | return nil 272 | } 273 | return c.boundAddr 274 | } 275 | 276 | // A Dialer holds SOCKS-specific options. 277 | type socksDialer struct { 278 | cmd socksCommand // either CmdConnect or cmdBind 279 | proxyNetwork string // network between a proxy server and a client 280 | proxyAddress string // proxy server address 281 | 282 | // ProxyDial specifies the optional dial function for 283 | // establishing the transport connection. 284 | ProxyDial func(context.Context, string, string) (net.Conn, error) 285 | 286 | // AuthMethods specifies the list of request authention 287 | // methods. 288 | // If empty, SOCKS client requests only AuthMethodNotRequired. 289 | AuthMethods []socksAuthMethod 290 | 291 | // Authenticate specifies the optional authentication 292 | // function. It must be non-nil when AuthMethods is not empty. 293 | // It must return an error when the authentication is failed. 294 | Authenticate func(context.Context, io.ReadWriter, socksAuthMethod) error 295 | } 296 | 297 | // DialContext connects to the provided address on the provided 298 | // network. 299 | // 300 | // The returned error value may be a net.OpError. When the Op field of 301 | // net.OpError contains "socks", the Source field contains a proxy 302 | // server address and the Addr field contains a command target 303 | // address. 304 | // 305 | // See func Dial of the net package of standard library for a 306 | // description of the network and address parameters. 307 | func (d *socksDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { 308 | if err := d.validateTarget(network, address); err != nil { 309 | proxy, dst, _ := d.pathAddrs(address) 310 | return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} 311 | } 312 | if ctx == nil { 313 | proxy, dst, _ := d.pathAddrs(address) 314 | return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: errors.New("nil context")} 315 | } 316 | var err error 317 | var c net.Conn 318 | if d.ProxyDial != nil { 319 | c, err = d.ProxyDial(ctx, d.proxyNetwork, d.proxyAddress) 320 | } else { 321 | var dd net.Dialer 322 | c, err = dd.DialContext(ctx, d.proxyNetwork, d.proxyAddress) 323 | } 324 | if err != nil { 325 | proxy, dst, _ := d.pathAddrs(address) 326 | return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} 327 | } 328 | a, err := d.connect(ctx, c, address) 329 | if err != nil { 330 | c.Close() 331 | proxy, dst, _ := d.pathAddrs(address) 332 | return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} 333 | } 334 | return &socksConn{Conn: c, boundAddr: a}, nil 335 | } 336 | 337 | // DialWithConn initiates a connection from SOCKS server to the target 338 | // network and address using the connection c that is already 339 | // connected to the SOCKS server. 340 | // 341 | // It returns the connection's local address assigned by the SOCKS 342 | // server. 343 | func (d *socksDialer) DialWithConn(ctx context.Context, c net.Conn, network, address string) (net.Addr, error) { 344 | if err := d.validateTarget(network, address); err != nil { 345 | proxy, dst, _ := d.pathAddrs(address) 346 | return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} 347 | } 348 | if ctx == nil { 349 | proxy, dst, _ := d.pathAddrs(address) 350 | return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: errors.New("nil context")} 351 | } 352 | a, err := d.connect(ctx, c, address) 353 | if err != nil { 354 | proxy, dst, _ := d.pathAddrs(address) 355 | return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} 356 | } 357 | return a, nil 358 | } 359 | 360 | // Dial connects to the provided address on the provided network. 361 | // 362 | // Unlike DialContext, it returns a raw transport connection instead 363 | // of a forward proxy connection. 364 | // 365 | // Deprecated: Use DialContext or DialWithConn instead. 366 | func (d *socksDialer) Dial(network, address string) (net.Conn, error) { 367 | if err := d.validateTarget(network, address); err != nil { 368 | proxy, dst, _ := d.pathAddrs(address) 369 | return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} 370 | } 371 | var err error 372 | var c net.Conn 373 | if d.ProxyDial != nil { 374 | c, err = d.ProxyDial(context.Background(), d.proxyNetwork, d.proxyAddress) 375 | } else { 376 | c, err = net.Dial(d.proxyNetwork, d.proxyAddress) 377 | } 378 | if err != nil { 379 | proxy, dst, _ := d.pathAddrs(address) 380 | return nil, &net.OpError{Op: d.cmd.String(), Net: network, Source: proxy, Addr: dst, Err: err} 381 | } 382 | if _, err := d.DialWithConn(context.Background(), c, network, address); err != nil { 383 | return nil, err 384 | } 385 | return c, nil 386 | } 387 | 388 | func (d *socksDialer) validateTarget(network, address string) error { 389 | switch network { 390 | case "tcp", "tcp6", "tcp4": 391 | default: 392 | return errors.New("network not implemented") 393 | } 394 | switch d.cmd { 395 | case socksCmdConnect, sockscmdBind: 396 | default: 397 | return errors.New("command not implemented") 398 | } 399 | return nil 400 | } 401 | 402 | func (d *socksDialer) pathAddrs(address string) (proxy, dst net.Addr, err error) { 403 | for i, s := range []string{d.proxyAddress, address} { 404 | host, port, err := sockssplitHostPort(s) 405 | if err != nil { 406 | return nil, nil, err 407 | } 408 | a := &socksAddr{Port: port} 409 | a.IP = net.ParseIP(host) 410 | if a.IP == nil { 411 | a.Name = host 412 | } 413 | if i == 0 { 414 | proxy = a 415 | } else { 416 | dst = a 417 | } 418 | } 419 | return 420 | } 421 | 422 | // NewDialer returns a new Dialer that dials through the provided 423 | // proxy server's network and address. 424 | func socksNewDialer(network, address string) *socksDialer { 425 | return &socksDialer{proxyNetwork: network, proxyAddress: address, cmd: socksCmdConnect} 426 | } 427 | 428 | const ( 429 | socksauthUsernamePasswordVersion = 0x01 430 | socksauthStatusSucceeded = 0x00 431 | ) 432 | 433 | // UsernamePassword are the credentials for the username/password 434 | // authentication method. 435 | type socksUsernamePassword struct { 436 | Username string 437 | Password string 438 | } 439 | 440 | // Authenticate authenticates a pair of username and password with the 441 | // proxy server. 442 | func (up *socksUsernamePassword) Authenticate(ctx context.Context, rw io.ReadWriter, auth socksAuthMethod) error { 443 | switch auth { 444 | case socksAuthMethodNotRequired: 445 | return nil 446 | case socksAuthMethodUsernamePassword: 447 | if len(up.Username) == 0 || len(up.Username) > 255 || len(up.Password) == 0 || len(up.Password) > 255 { 448 | return errors.New("invalid username/password") 449 | } 450 | b := []byte{socksauthUsernamePasswordVersion} 451 | b = append(b, byte(len(up.Username))) 452 | b = append(b, up.Username...) 453 | b = append(b, byte(len(up.Password))) 454 | b = append(b, up.Password...) 455 | // TODO(mikio): handle IO deadlines and cancelation if 456 | // necessary 457 | if _, err := rw.Write(b); err != nil { 458 | return err 459 | } 460 | if _, err := io.ReadFull(rw, b[:2]); err != nil { 461 | return err 462 | } 463 | if b[0] != socksauthUsernamePasswordVersion { 464 | return errors.New("invalid username/password version") 465 | } 466 | if b[1] != socksauthStatusSucceeded { 467 | return errors.New("username/password authentication failed") 468 | } 469 | return nil 470 | } 471 | return errors.New("unsupported authentication method " + strconv.Itoa(int(auth))) 472 | } 473 | --------------------------------------------------------------------------------