http2: use IDNA Lookup profile, not raw Punycode translation For golang/go#81010 Change-Id: I184386da6e62b5cc4108537f297191436a6a6964 Reviewed-on: https://go-review.googlesource.com/c/net/+/819201 Auto-Submit: Damien Neil <dneil@google.com> Reviewed-by: Nicholas Husin <nsh@golang.org> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Nicholas Husin <husin@google.com>
diff --git a/http2/ascii.go b/http2/ascii.go index 17caa20..3a7aed1 100644 --- a/http2/ascii.go +++ b/http2/ascii.go
@@ -4,7 +4,9 @@ package http2 -import "strings" +import ( + "strings" +) // The HTTP protocols are defined in terms of ASCII, not Unicode. This file // contains helper functions which may use Unicode-aware functions which would @@ -43,6 +45,16 @@ return true } +// isASCII returns whether s is ASCII. +func isASCII(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] > 0x7f { + return false + } + } + return true +} + // asciiToLower returns the lowercase version of s if s is ASCII and printable, // and whether or not it was. func asciiToLower(s string) (lower string, ok bool) {
diff --git a/http2/transport_api_test.go b/http2/transport_api_test.go index e7e59d6..b42328f 100644 --- a/http2/transport_api_test.go +++ b/http2/transport_api_test.go
@@ -462,6 +462,61 @@ }) } +func TestAPITransportIDNA(t *testing.T) { + type dialErr struct { + error + address string + } + tr := &http2.Transport{ + DialTLSContext: func(ctx context.Context, network, address string, conf *tls.Config) (net.Conn, error) { + host, _, _ := net.SplitHostPort(address) + return nil, dialErr{ + error: errors.New("dial error"), + address: host, + } + }, + } + for _, test := range []struct { + url string + wantDialAddr string + skip bool + }{{ + url: "https://example.tld/", + wantDialAddr: "example.tld", + }, { + // Invalid host, but permitted because all-ASCII. + url: "https://xn-example-.tld/", + wantDialAddr: "xn-example-.tld", + }, { + url: "https://гофер.го/", + wantDialAddr: "xn--c1ae0ajs.xn--c1aw", + }, { + // IDNA translation returns an error, we try using the untranslated string. + url: "https://a⒈com/", + wantDialAddr: "a⒈com", + }, { + // Unicode name converted to the empty string. + url: "https://\u00ad/", + wantDialAddr: "\u00ad", + // When !http2legacy, this relies on a fix added in Go 1.28. + // Just skip the test here when wrapping net/http. + skip: wrappedAPI, + }} { + if test.skip { + continue + } + req, _ := http.NewRequest("GET", test.url, nil) + _, err := tr.RoundTrip(req) + e, ok := err.(dialErr) + if !ok { + t.Fatalf("RoundTrip error %v; want dialErr", err) + } + if got, want := e.address, test.wantDialAddr; got != want { + t.Errorf("RoundTrip for %q dialed %q, want %q", test.url, got, want) + } + } +} + type testClientConnPool struct { t *testing.T li *synctestNetListener
diff --git a/http2/transport_common.go b/http2/transport_common.go index ea3b25d..4f0333a 100644 --- a/http2/transport_common.go +++ b/http2/transport_common.go
@@ -316,8 +316,15 @@ port = "80" } } - if a, err := idna.ToASCII(host); err == nil { - host = a + // Skip IDNA processing on hosts which are already ASCII. + // This is consistent with net/http and the WHATWG URL Specification. + // (We currently don't follow all of WHATWG, but we're aligned on + // permitting all-ASCII hostnames which fail IDNA validation. + // There are existing, valid domain names which TR #46 processing rejects.) + if !isASCII(host) { + if a, err := idna.Lookup.ToASCII(host); err == nil && a != "" { + host = a + } } // IPv6 address literal, without a port: if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {