git » go-net » commit 57c7820

http2: add Transport support for unicode domain names

author Brad Fitzpatrick
2016-09-12 01:12:11 UTC
committer Brad Fitzpatrick
2016-09-12 18:43:32 UTC
parent 324491d706c63d58003f108b1c13ca4e477a6959

http2: add Transport support for unicode domain names

No tests, because tests are in std. (See TestTransportIDNA_h2)

Updates golang/go#13835

Change-Id: I0a327d9652ea5e6f32dfa279550915af61567bed
Reviewed-on: https://go-review.googlesource.com/29071
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Chris Broadfoot <cbro@golang.org>

http2/transport.go +15 -6
lex/httplex/httplex.go +39 -0
lex/httplex/httplex_test.go +18 -0

diff --git a/http2/transport.go b/http2/transport.go
index 47c66d9..7a8563d 100644
--- a/http2/transport.go
+++ b/http2/transport.go
@@ -26,6 +26,7 @@ import (
 	"time"
 
 	"golang.org/x/net/http2/hpack"
+	"golang.org/x/net/idna"
 	"golang.org/x/net/lex/httplex"
 )
 
@@ -285,14 +286,18 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
 // authorityAddr returns a given authority (a host/IP, or host:port / ip:port)
 // and returns a host:port. The port 443 is added if needed.
 func authorityAddr(scheme string, authority string) (addr string) {
-	if _, _, err := net.SplitHostPort(authority); err == nil {
-		return authority
+	host, port, err := net.SplitHostPort(authority)
+	if err != nil { // authority didn't have a port
+		port = "443"
+		if scheme == "http" {
+			port = "80"
+		}
+		host = authority
 	}
-	port := "443"
-	if scheme == "http" {
-		port = "80"
+	if a, err := idna.ToASCII(host); err == nil {
+		host = a
 	}
-	return net.JoinHostPort(authority, port)
+	return net.JoinHostPort(host, port)
 }
 
 // RoundTripOpt is like RoundTrip, but takes options.
@@ -997,6 +1002,10 @@ func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trail
 	if host == "" {
 		host = req.URL.Host
 	}
+	host, err := httplex.PunycodeHostPort(host)
+	if err != nil {
+		return nil, err
+	}
 
 	var path string
 	if req.Method != "CONNECT" {
diff --git a/lex/httplex/httplex.go b/lex/httplex/httplex.go
index bd0ec24..20f2b89 100644
--- a/lex/httplex/httplex.go
+++ b/lex/httplex/httplex.go
@@ -10,8 +10,11 @@
 package httplex
 
 import (
+	"net"
 	"strings"
 	"unicode/utf8"
+
+	"golang.org/x/net/idna"
 )
 
 var isTokenTable = [127]bool{
@@ -310,3 +313,39 @@ func ValidHeaderFieldValue(v string) bool {
 	}
 	return true
 }
+
+func isASCII(s string) bool {
+	for i := 0; i < len(s); i++ {
+		if s[i] >= utf8.RuneSelf {
+			return false
+		}
+	}
+	return true
+}
+
+// PunycodeHostPort returns the IDNA Punycode version
+// of the provided "host" or "host:port" string.
+func PunycodeHostPort(v string) (string, error) {
+	if isASCII(v) {
+		return v, nil
+	}
+
+	host, port, err := net.SplitHostPort(v)
+	if err != nil {
+		// The input 'v' argument was just a "host" argument,
+		// without a port. This error should not be returned
+		// to the caller.
+		host = v
+		port = ""
+	}
+	host, err = idna.ToASCII(host)
+	if err != nil {
+		// Non-UTF-8? Not representable in Punycode, in any
+		// case.
+		return "", err
+	}
+	if port == "" {
+		return host, nil
+	}
+	return net.JoinHostPort(host, port), nil
+}
diff --git a/lex/httplex/httplex_test.go b/lex/httplex/httplex_test.go
index c4ace19..f47adc9 100644
--- a/lex/httplex/httplex_test.go
+++ b/lex/httplex/httplex_test.go
@@ -99,3 +99,21 @@ func TestHeaderValuesContainsToken(t *testing.T) {
 		}
 	}
 }
+
+func TestPunycodeHostPort(t *testing.T) {
+	tests := []struct {
+		in, want string
+	}{
+		{"www.google.com", "www.google.com"},
+		{"гофер.рф", "xn--c1ae0ajs.xn--p1ai"},
+		{"bücher.de", "xn--bcher-kva.de"},
+		{"bücher.de:8080", "xn--bcher-kva.de:8080"},
+		{"[1::6]:8080", "[1::6]:8080"},
+	}
+	for _, tt := range tests {
+		got, err := PunycodeHostPort(tt.in)
+		if tt.want != got || err != nil {
+			t.Errorf("PunycodeHostPort(%q) = %q, %v, want %q, nil", tt.in, got, err, tt.want)
+		}
+	}
+}