[tor-commits] [meek/utls_2] http proxy support for uTLS.

dcf at torproject.org dcf at torproject.org
Sat Feb 2 08:54:26 UTC 2019


commit dbafd218e275718951915bd0262c46a3c615448c
Author: David Fifield <david at 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/05b2092e07f9d10b3803d8fb9775d2f87dc58590/httpclient/httpclient.go
+
+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)
 	}





More information about the tor-commits mailing list