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{
"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",
......
......@@ -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
......
......@@ -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"},
......
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