[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

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



commit 92400c9d9191fe7b661086128639f7f228f5b9e2
Author: David Fifield <david@xxxxxxxxxxxxxxx>
Date:   Wed Feb 6 20:42:24 2019 -0700

    http proxy support for uTLS.
---
 meek-client/proxy_http.go |  76 +++++++++++++++++++++
 meek-client/proxy_test.go | 165 ++++++++++++++++++++++++++++++++++++++++++++++
 meek-client/utls.go       |   2 +
 meek-client/utls_test.go  |   1 +
 4 files changed, 244 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
new file mode 100644
index 0000000..c13cf12
--- /dev/null
+++ b/meek-client/proxy_test.go
@@ -0,0 +1,165 @@
+package main
+
+import (
+	"bufio"
+	"io"
+	"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
+// https://tools.ietf.org/html/rfc3986#section-3.2.3 port
+func TestAddrForDial(t *testing.T) {
+	// good tests
+	for _, test := range []struct {
+		URL  string
+		Addr string
+	}{
+		{"http://example.com";, "example.com:80"},
+		{"http://example.com/";, "example.com:80"},
+		{"https://example.com/";, "example.com:443"},
+		{"http://example.com:443/";, "example.com:443"},
+		{"ftp://example.com:21/";, "example.com:21"},
+	} {
+		u, err := url.Parse(test.URL)
+		if err != nil {
+			panic(err)
+		}
+		addr, err := addrForDial(u)
+		if err != nil {
+			t.Errorf("%q â?? error %v", test.URL, err)
+			continue
+		}
+		if addr != test.Addr {
+			t.Errorf("%q â?? %q, expected %q", test.URL, addr, test.Addr)
+		}
+	}
+
+	// bad tests
+	for _, input := range []string{
+		"example.com",
+		"example.com:80",
+		"ftp://example.com/";,
+	} {
+		u, err := url.Parse(input)
+		if err != nil {
+			panic(err)
+		}
+		addr, err := addrForDial(u)
+		if err == nil {
+			t.Errorf("%q â?? %q, expected error", input, addr)
+			continue
+		}
+	}
+}
+
+// Dial the given address with the given proxy, and return the http.Request that
+// the proxy server would have received.
+func requestResultingFromDial(t *testing.T, 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 {
+			t.Error(err)
+			return
+		}
+		defer conn.Close()
+		br := bufio.NewReader(conn)
+		req, err := http.ReadRequest(br)
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		ch <- req
+	}()
+
+	pr, err := makeProxy(ln.Addr())
+	if err != nil {
+		return nil, err
+	}
+	// The Dial fails because the goroutine "server" hangs up. So ignore an
+	// ErrUnexpectedEOF error.
+	_, err = pr.Dial(network, addr)
+	if err != nil && err != io.ErrUnexpectedEOF {
+		return nil, err
+	}
+
+	return <-ch, nil
+}
+
+// Test that the HTTP proxy client sends a correct request.
+func TestProxyHTTPCONNECT(t *testing.T) {
+	req, err := requestResultingFromDial(t, 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(t, 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 1cdeca1..5c1e484 100644
--- a/meek-client/utls.go
+++ b/meek-client/utls.go
@@ -179,6 +179,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)
 	}
diff --git a/meek-client/utls_test.go b/meek-client/utls_test.go
index 0e62dac..fbdd969 100644
--- a/meek-client/utls_test.go
+++ b/meek-client/utls_test.go
@@ -263,6 +263,7 @@ func TestUTLSHTTPWithProxy(t *testing.T) {
 	// Try to access the web server through the non-functional proxy.
 	for _, proxyURL := range []url.URL{
 		url.URL{Scheme: "socks5", Host: proxyLn.Addr().String()},
+		url.URL{Scheme: "http", Host: proxyLn.Addr().String()},
 	} {
 		rt, err := NewUTLSRoundTripper("HelloFirefox_63", &utls.Config{InsecureSkipVerify: true}, &proxyURL)
 		if err != nil {



_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits