Commit 36f55a8b authored by Michel Lespinasse's avatar Michel Lespinasse

net/http: add support for socks5 proxy

See #18508

This commit adds http Client support for socks5 proxies.

Change-Id: Ib015f3819801da13781d5acdd780149ae1f5857b
Reviewed-on: https://go-review.googlesource.com/35488Reviewed-by: 's avatarBrad Fitzpatrick <bradfitz@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
parent 4be4da63
...@@ -394,6 +394,7 @@ var pkgDeps = map[string][]string{ ...@@ -394,6 +394,7 @@ var pkgDeps = map[string][]string{
"golang_org/x/net/http2/hpack", "golang_org/x/net/http2/hpack",
"golang_org/x/net/idna", "golang_org/x/net/idna",
"golang_org/x/net/lex/httplex", "golang_org/x/net/lex/httplex",
"golang_org/x/net/proxy",
"golang_org/x/text/unicode/norm", "golang_org/x/text/unicode/norm",
"golang_org/x/text/width", "golang_org/x/text/width",
"internal/nettrace", "internal/nettrace",
......
...@@ -29,6 +29,7 @@ import ( ...@@ -29,6 +29,7 @@ import (
"time" "time"
"golang_org/x/net/lex/httplex" "golang_org/x/net/lex/httplex"
"golang_org/x/net/proxy"
) )
// DefaultTransport is the default implementation of Transport and is // DefaultTransport is the default implementation of Transport and is
...@@ -275,13 +276,17 @@ func ProxyFromEnvironment(req *Request) (*url.URL, error) { ...@@ -275,13 +276,17 @@ func ProxyFromEnvironment(req *Request) (*url.URL, error) {
return nil, nil return nil, nil
} }
proxyURL, err := url.Parse(proxy) 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 // proxy was bogus. Try prepending "http://" to it and
// see if that parses correctly. If not, we fall // see if that parses correctly. If not, we fall
// through and complain about the original one. // through and complain about the original one.
if proxyURL, err := url.Parse("http://" + proxy); err == nil { if proxyURL, err := url.Parse("http://" + proxy); err == nil {
return proxyURL, nil return proxyURL, nil
} }
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid proxy address %q: %v", proxy, err) return nil, fmt.Errorf("invalid proxy address %q: %v", proxy, err)
...@@ -964,6 +969,23 @@ func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (*persistC ...@@ -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) { func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (*persistConn, error) {
pconn := &persistConn{ pconn := &persistConn{
t: t, t: t,
...@@ -1020,6 +1042,23 @@ func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (*persistCon ...@@ -1020,6 +1042,23 @@ func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (*persistCon
switch { switch {
case cm.proxyURL == nil: case cm.proxyURL == nil:
// Do nothing. Not using a proxy. // 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": case cm.targetScheme == "http":
pconn.isProxy = true pconn.isProxy = true
if pa := cm.proxyAuth(); pa != "" { if pa := cm.proxyAuth(); pa != "" {
...@@ -1193,19 +1232,21 @@ func useProxy(addr string) bool { ...@@ -1193,19 +1232,21 @@ func useProxy(addr string) bool {
// //
// A connect method may be of the following types: // A connect method may be of the following types:
// //
// Cache key form Description // Cache key form Description
// ----------------- ------------------------- // ----------------- -------------------------
// |http|foo.com http directly to server, no proxy // |http|foo.com http directly to server, no proxy
// |https|foo.com https 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|https|foo.com http to proxy, then CONNECT to foo.com
// http://proxy.com|http http to proxy, http to anywhere after that // 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. // Note: no support to https to the proxy yet.
// //
type connectMethod struct { type connectMethod struct {
proxyURL *url.URL // nil for no proxy, else full proxy URL proxyURL *url.URL // nil for no proxy, else full proxy URL
targetScheme string // "http" or "https" 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 { func (cm *connectMethod) key() connectMethodKey {
...@@ -1213,7 +1254,7 @@ func (cm *connectMethod) key() connectMethodKey { ...@@ -1213,7 +1254,7 @@ func (cm *connectMethod) key() connectMethodKey {
targetAddr := cm.targetAddr targetAddr := cm.targetAddr
if cm.proxyURL != nil { if cm.proxyURL != nil {
proxyStr = cm.proxyURL.String() proxyStr = cm.proxyURL.String()
if cm.targetScheme == "http" { if strings.HasPrefix(cm.proxyURL.Scheme, "http") && cm.targetScheme == "http" {
targetAddr = "" targetAddr = ""
} }
} }
...@@ -1982,8 +2023,9 @@ func (pc *persistConn) closeLocked(err error) { ...@@ -1982,8 +2023,9 @@ func (pc *persistConn) closeLocked(err error) {
} }
var portMap = map[string]string{ var portMap = map[string]string{
"http": "80", "http": "80",
"https": "443", "https": "443",
"socks5": "1080",
} }
// canonicalAddr returns url.Host but always with a ":port" suffix // canonicalAddr returns url.Host but always with a ":port" suffix
......
...@@ -16,6 +16,7 @@ import ( ...@@ -16,6 +16,7 @@ import (
"context" "context"
"crypto/rand" "crypto/rand"
"crypto/tls" "crypto/tls"
"encoding/binary"
"errors" "errors"
"fmt" "fmt"
"internal/nettrace" "internal/nettrace"
...@@ -943,6 +944,98 @@ func TestTransportExpect100Continue(t *testing.T) { ...@@ -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) { func TestTransportProxy(t *testing.T) {
defer afterTest(t) defer afterTest(t)
ch := make(chan string, 1) ch := make(chan string, 1)
...@@ -960,11 +1053,18 @@ func TestTransportProxy(t *testing.T) { ...@@ -960,11 +1053,18 @@ func TestTransportProxy(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
c := &Client{Transport: &Transport{Proxy: ProxyURL(pu)}} c := &Client{Transport: &Transport{Proxy: ProxyURL(pu)}}
c.Head(ts.URL) if _, err := c.Head(ts.URL); err != nil {
got := <-ch 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 + "/" want := "proxy for " + ts.URL + "/"
if got != want { 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{ ...@@ -2160,6 +2260,7 @@ var proxyFromEnvTests = []proxyFromEnvTest{
{env: "https://cache.corp.example.com", want: "https://cache.corp.example.com"}, {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: "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: "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 // Don't use secure for http
{req: "http://insecure.tld/", env: "http.proxy.tld", httpsenv: "secure.proxy.tld", want: "http://http.proxy.tld"}, {req: "http://insecure.tld/", env: "http.proxy.tld", httpsenv: "secure.proxy.tld", want: "http://http.proxy.tld"},
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment