[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