commit 92400c9d9191fe7b661086128639f7f228f5b9e2
Author: David Fifield <da...@bamsoftware.com>
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@lists.torproject.org
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits

Reply via email to