commit dbafd218e275718951915bd0262c46a3c615448c Author: David Fifield david@bamsoftware.com Date: Thu Jan 31 21:16:42 2019 -0700
http proxy support for uTLS. --- meek-client/proxy_http.go | 76 +++++++++++++++++++++++++++++++++ meek-client/proxy_test.go | 104 ++++++++++++++++++++++++++++++++++++++++++++++ meek-client/utls.go | 2 + 3 files changed, 182 insertions(+)
diff --git a/meek-client/proxy_http.go b/meek-client/proxy_http.go new file mode 100644 index 0000000..1136eb2 --- /dev/null +++ b/meek-client/proxy_http.go @@ -0,0 +1,76 @@ +package main + +import ( + "bufio" + "encoding/base64" + "fmt" + "net" + "net/http" + "net/url" + + "golang.org/x/net/proxy" +) + +// https://tools.ietf.org/html/rfc7231#section-4.3.6 +// Conceivably we could also proxy over HTTP/2: +// https://httpwg.org/specs/rfc7540.html#CONNECT +// https://github.com/caddyserver/forwardproxy/blob/05b2092e07f9d10b3803d8fb977... + +type httpProxy struct { + network, addr string + auth *proxy.Auth + forward proxy.Dialer +} + +func (pr *httpProxy) Dial(network, addr string) (net.Conn, error) { + connectReq := &http.Request{ + Method: "CONNECT", + URL: &url.URL{Opaque: addr}, + Host: addr, + Header: make(http.Header), + } + // http.Transport has a ProxyConnectHeader field that we are ignoring + // here. + if pr.auth != nil { + connectReq.Header.Set("Proxy-Authorization", "basic "+ + base64.StdEncoding.EncodeToString([]byte(pr.auth.User+":"+pr.auth.Password))) + } + + conn, err := pr.forward.Dial(pr.network, pr.addr) + if err != nil { + return nil, err + } + + err = connectReq.Write(conn) + if err != nil { + conn.Close() + return nil, err + } + + // The Go stdlib says: "Okay to use and discard buffered reader here, + // because TLS server will not speak until spoken to." + br := bufio.NewReader(conn) + resp, err := http.ReadResponse(br, connectReq) + if br.Buffered() != 0 { + panic(br.Buffered()) + } + if err != nil { + conn.Close() + return nil, err + } + if resp.StatusCode != 200 { + conn.Close() + return nil, fmt.Errorf("proxy server returned %q", resp.Status) + } + + return conn, nil +} + +func ProxyHTTP(network, addr string, auth *proxy.Auth, forward proxy.Dialer) (*httpProxy, error) { + return &httpProxy{ + network: network, + addr: addr, + auth: auth, + forward: forward, + }, nil +} diff --git a/meek-client/proxy_test.go b/meek-client/proxy_test.go index 5e87e56..783165a 100644 --- a/meek-client/proxy_test.go +++ b/meek-client/proxy_test.go @@ -1,10 +1,21 @@ package main
import ( + "bufio" + "net" + "net/http" "net/url" "testing" + + "golang.org/x/net/proxy" )
+const testHost = "test.example" +const testPort = "1234" +const testAddr = testHost + ":" + testPort +const testUsername = "username" +const testPassword = "password" + // Test that addrForDial returns a numeric port number. It needs to be numeric // because we pass it as part of the authority-form URL in HTTP proxy requests. // https://tools.ietf.org/html/rfc7230#section-5.3.3 authority-form @@ -52,3 +63,96 @@ func TestAddrForDial(t *testing.T) { } } } + +// Dial the given address with the given proxy, and return the http.Request that +// the proxy server would have received. +func requestResultingFromDial(makeProxy func(addr net.Addr) (*httpProxy, error), network, addr string) (*http.Request, error) { + ch := make(chan *http.Request, 1) + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, err + } + defer ln.Close() + + go func() { + defer func() { + close(ch) + }() + conn, err := ln.Accept() + if err != nil { + panic(err) + } + defer conn.Close() + br := bufio.NewReader(conn) + req, err := http.ReadRequest(br) + if err != nil { + panic(err) + } + ch <- req + }() + + pr, err := makeProxy(ln.Addr()) + if err != nil { + return nil, err + } + // The Dial fails because the goroutine "server" hangs up. + _, _ = pr.Dial(network, addr) + + return <-ch, nil +} + +// Test that the HTTP proxy client sends a correct request. +func TestProxyHTTPCONNECT(t *testing.T) { + req, err := requestResultingFromDial(func(addr net.Addr) (*httpProxy, error) { + return ProxyHTTP("tcp", addr.String(), nil, proxy.Direct) + }, "tcp", testAddr) + if err != nil { + panic(err) + } + if req.Method != "CONNECT" { + t.Errorf("expected method %q, got %q", "CONNECT", req.Method) + } + if req.URL.Hostname() != testHost || req.URL.Port() != testPort { + t.Errorf("expected URL %q, got %q", testAddr, req.URL.String()) + } + if req.Host != testAddr { + t.Errorf("expected %q, got %q", "Host: "+req.Host, "Host: "+testAddr) + } +} + +// Test that the HTTP proxy client sends authorization credentials. +func TestProxyHTTPProxyAuthorization(t *testing.T) { + auth := &proxy.Auth{ + User: testUsername, + Password: testPassword, + } + req, err := requestResultingFromDial(func(addr net.Addr) (*httpProxy, error) { + return ProxyHTTP("tcp", addr.String(), auth, proxy.Direct) + }, "tcp", testAddr) + if err != nil { + panic(err) + } + pa := req.Header.Get("Proxy-Authorization") + if pa == "" { + t.Fatalf("didn't get a Proxy-Authorization header") + } + // The standard library Request.BasicAuth does parsing of basic + // authentication, but only in the Authorization header, not + // Proxy-Authorization. + newReq := &http.Request{ + Header: http.Header{ + "Authorization": []string{pa}, + }, + } + username, password, ok := newReq.BasicAuth() + if !ok { + panic("shouldn't fail") + } + if username != testUsername { + t.Errorf("expected username %q, got %q", testUsername, username) + } + if password != testPassword { + t.Errorf("expected password %q, got %q", testPassword, password) + } +} diff --git a/meek-client/utls.go b/meek-client/utls.go index 1f9d32d..cbb2aaa 100644 --- a/meek-client/utls.go +++ b/meek-client/utls.go @@ -170,6 +170,8 @@ func makeProxyDialer(proxyURL *url.URL) (proxy.Dialer, error) { switch proxyURL.Scheme { case "socks5": proxyDialer, err = proxy.SOCKS5("tcp", proxyAddr, auth, proxyDialer) + case "http": + proxyDialer, err = ProxyHTTP("tcp", proxyAddr, auth, proxyDialer) default: return nil, fmt.Errorf("cannot use proxy scheme %q with uTLS", proxyURL.Scheme) }