]> Cypherpunks repositories - gostls13.git/commitdiff
net/http: add support for socks5 proxy
authorMichel Lespinasse <walken@google.com>
Sat, 21 Jan 2017 00:52:23 +0000 (16:52 -0800)
committerMichel Lespinasse <walken@google.com>
Thu, 2 Mar 2017 00:41:44 +0000 (00:41 +0000)
See #18508

This commit adds http Client support for socks5 proxies.

Change-Id: Ib015f3819801da13781d5acdd780149ae1f5857b
Reviewed-on: https://go-review.googlesource.com/35488
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>

src/go/build/deps_test.go
src/net/http/transport.go
src/net/http/transport_test.go

index c26ad06aeb0aaa42afc6ad00888c2f24f8201665..f8ba53288e660da5bc6a759e5f0cf72ad5ac0039 100644 (file)
@@ -394,6 +394,7 @@ var pkgDeps = map[string][]string{
                "golang_org/x/net/http2/hpack",
                "golang_org/x/net/idna",
                "golang_org/x/net/lex/httplex",
+               "golang_org/x/net/proxy",
                "golang_org/x/text/unicode/norm",
                "golang_org/x/text/width",
                "internal/nettrace",
index 571943d6e5cd9935d43005c138e7211a97820964..2aa00de50a2a0621c18c01a0806e1111ffc9e5e2 100644 (file)
@@ -29,6 +29,7 @@ import (
        "time"
 
        "golang_org/x/net/lex/httplex"
+       "golang_org/x/net/proxy"
 )
 
 // DefaultTransport is the default implementation of Transport and is
@@ -275,13 +276,17 @@ func ProxyFromEnvironment(req *Request) (*url.URL, error) {
                return nil, nil
        }
        proxyURL, err := url.Parse(proxy)
-       if err != nil || !strings.HasPrefix(proxyURL.Scheme, "http") {
+       if err != nil ||
+               (proxyURL.Scheme != "http" &&
+                       proxyURL.Scheme != "https" &&
+                       proxyURL.Scheme != "socks5") {
                // proxy was bogus. Try prepending "http://" to it and
                // see if that parses correctly. If not, we fall
                // through and complain about the original one.
                if proxyURL, err := url.Parse("http://" + proxy); err == nil {
                        return proxyURL, nil
                }
+
        }
        if err != nil {
                return nil, fmt.Errorf("invalid proxy address %q: %v", proxy, err)
@@ -964,6 +969,23 @@ func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (*persistC
        }
 }
 
+type oneConnDialer <-chan net.Conn
+
+func newOneConnDialer(c net.Conn) proxy.Dialer {
+       ch := make(chan net.Conn, 1)
+       ch <- c
+       return oneConnDialer(ch)
+}
+
+func (d oneConnDialer) Dial(network, addr string) (net.Conn, error) {
+       select {
+       case c := <-d:
+               return c, nil
+       default:
+               return nil, io.EOF
+       }
+}
+
 func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (*persistConn, error) {
        pconn := &persistConn{
                t:             t,
@@ -1020,6 +1042,23 @@ func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (*persistCon
        switch {
        case cm.proxyURL == nil:
                // Do nothing. Not using a proxy.
+       case cm.proxyURL.Scheme == "socks5":
+               conn := pconn.conn
+               var auth *proxy.Auth
+               if u := cm.proxyURL.User; u != nil {
+                       auth = &proxy.Auth{}
+                       auth.User = u.Username()
+                       auth.Password, _ = u.Password()
+               }
+               p, err := proxy.SOCKS5("", cm.addr(), auth, newOneConnDialer(conn))
+               if err != nil {
+                       conn.Close()
+                       return nil, err
+               }
+               if _, err := p.Dial("tcp", cm.targetAddr); err != nil {
+                       conn.Close()
+                       return nil, err
+               }
        case cm.targetScheme == "http":
                pconn.isProxy = true
                if pa := cm.proxyAuth(); pa != "" {
@@ -1193,19 +1232,21 @@ func useProxy(addr string) bool {
 //
 // A connect method may be of the following types:
 //
-// Cache key form                Description
-// -----------------             -------------------------
-// |http|foo.com                 http directly to server, no proxy
-// |https|foo.com                https directly to server, no proxy
-// http://proxy.com|https|foo.com  http to proxy, then CONNECT to foo.com
-// http://proxy.com|http           http to proxy, http to anywhere after that
+// Cache key form                    Description
+// -----------------                 -------------------------
+// |http|foo.com                     http directly to server, no proxy
+// |https|foo.com                    https directly to server, no proxy
+// http://proxy.com|https|foo.com    http to proxy, then CONNECT to foo.com
+// http://proxy.com|http             http to proxy, http to anywhere after that
+// socks5://proxy.com|http|foo.com   socks5 to proxy, then http to foo.com
+// socks5://proxy.com|https|foo.com  socks5 to proxy, then https to foo.com
 //
 // Note: no support to https to the proxy yet.
 //
 type connectMethod struct {
        proxyURL     *url.URL // nil for no proxy, else full proxy URL
        targetScheme string   // "http" or "https"
-       targetAddr   string   // Not used if proxy + http targetScheme (4th example in table)
+       targetAddr   string   // Not used if http proxy + http targetScheme (4th example in table)
 }
 
 func (cm *connectMethod) key() connectMethodKey {
@@ -1213,7 +1254,7 @@ func (cm *connectMethod) key() connectMethodKey {
        targetAddr := cm.targetAddr
        if cm.proxyURL != nil {
                proxyStr = cm.proxyURL.String()
-               if cm.targetScheme == "http" {
+               if strings.HasPrefix(cm.proxyURL.Scheme, "http") && cm.targetScheme == "http" {
                        targetAddr = ""
                }
        }
@@ -1982,8 +2023,9 @@ func (pc *persistConn) closeLocked(err error) {
 }
 
 var portMap = map[string]string{
-       "http":  "80",
-       "https": "443",
+       "http":   "80",
+       "https":  "443",
+       "socks5": "1080",
 }
 
 // canonicalAddr returns url.Host but always with a ":port" suffix
index 085bb3cd4b2d48bd3daaf3a1653be9661de8c66b..ce98157ed5b949ba721f3c53b7615800fdc53f83 100644 (file)
@@ -16,6 +16,7 @@ import (
        "context"
        "crypto/rand"
        "crypto/tls"
+       "encoding/binary"
        "errors"
        "fmt"
        "internal/nettrace"
@@ -943,6 +944,98 @@ func TestTransportExpect100Continue(t *testing.T) {
        }
 }
 
+func TestSocks5Proxy(t *testing.T) {
+       defer afterTest(t)
+       ch := make(chan string, 1)
+       ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
+               ch <- "real server"
+       }))
+       defer ts.Close()
+       l := newLocalListener(t)
+       defer l.Close()
+       go func() {
+               defer close(ch)
+               s, err := l.Accept()
+               if err != nil {
+                       t.Errorf("socks5 proxy Accept(): %v", err)
+                       return
+               }
+               defer s.Close()
+               var buf [22]byte
+               if _, err := io.ReadFull(s, buf[:3]); err != nil {
+                       t.Errorf("socks5 proxy initial read: %v", err)
+                       return
+               }
+               if want := []byte{5, 1, 0}; !bytes.Equal(buf[:3], want) {
+                       t.Errorf("socks5 proxy initial read: got %v, want %v", buf[:3], want)
+                       return
+               }
+               if _, err := s.Write([]byte{5, 0}); err != nil {
+                       t.Errorf("socks5 proxy initial write: %v", err)
+                       return
+               }
+               if _, err := io.ReadFull(s, buf[:4]); err != nil {
+                       t.Errorf("socks5 proxy second read: %v", err)
+                       return
+               }
+               if want := []byte{5, 1, 0}; !bytes.Equal(buf[:3], want) {
+                       t.Errorf("socks5 proxy second read: got %v, want %v", buf[:3], want)
+                       return
+               }
+               var ipLen int
+               switch buf[3] {
+               case 1:
+                       ipLen = 4
+               case 4:
+                       ipLen = 16
+               default:
+                       t.Fatalf("socks5 proxy second read: unexpected address type %v", buf[4])
+               }
+               if _, err := io.ReadFull(s, buf[4:ipLen+6]); err != nil {
+                       t.Errorf("socks5 proxy address read: %v", err)
+                       return
+               }
+               ip := net.IP(buf[4 : ipLen+4])
+               port := binary.BigEndian.Uint16(buf[ipLen+4 : ipLen+6])
+               copy(buf[:3], []byte{5, 0, 0})
+               if _, err := s.Write(buf[:ipLen+6]); err != nil {
+                       t.Errorf("socks5 proxy connect write: %v", err)
+                       return
+               }
+               done := make(chan struct{})
+               srv := &Server{Handler: HandlerFunc(func(w ResponseWriter, r *Request) {
+                       done <- struct{}{}
+               })}
+               srv.Serve(&oneConnListener{conn: s})
+               <-done
+               srv.Shutdown(context.Background())
+               ch <- fmt.Sprintf("proxy for %s:%d", ip, port)
+       }()
+
+       pu, err := url.Parse("socks5://" + l.Addr().String())
+       if err != nil {
+               t.Fatal(err)
+       }
+       c := &Client{Transport: &Transport{Proxy: ProxyURL(pu)}}
+       if _, err := c.Head(ts.URL); err != nil {
+               t.Error(err)
+       }
+       var got string
+       select {
+       case got = <-ch:
+       case <-time.After(5 * time.Second):
+               t.Fatal("timeout connecting to socks5 proxy")
+       }
+       tsu, err := url.Parse(ts.URL)
+       if err != nil {
+               t.Fatal(err)
+       }
+       want := "proxy for " + tsu.Host
+       if got != want {
+               t.Errorf("got %q, want %q", got, want)
+       }
+}
+
 func TestTransportProxy(t *testing.T) {
        defer afterTest(t)
        ch := make(chan string, 1)
@@ -960,11 +1053,18 @@ func TestTransportProxy(t *testing.T) {
                t.Fatal(err)
        }
        c := &Client{Transport: &Transport{Proxy: ProxyURL(pu)}}
-       c.Head(ts.URL)
-       got := <-ch
+       if _, err := c.Head(ts.URL); err != nil {
+               t.Error(err)
+       }
+       var got string
+       select {
+       case got = <-ch:
+       case <-time.After(5 * time.Second):
+               t.Fatal("timeout connecting to http proxy")
+       }
        want := "proxy for " + ts.URL + "/"
        if got != want {
-               t.Errorf("want %q, got %q", want, got)
+               t.Errorf("got %q, want %q", got, want)
        }
 }
 
@@ -2160,6 +2260,7 @@ var proxyFromEnvTests = []proxyFromEnvTest{
        {env: "https://cache.corp.example.com", want: "https://cache.corp.example.com"},
        {env: "http://127.0.0.1:8080", want: "http://127.0.0.1:8080"},
        {env: "https://127.0.0.1:8080", want: "https://127.0.0.1:8080"},
+       {env: "socks5://127.0.0.1", want: "socks5://127.0.0.1"},
 
        // Don't use secure for http
        {req: "http://insecure.tld/", env: "http.proxy.tld", httpsenv: "secure.proxy.tld", want: "http://http.proxy.tld"},