[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