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, "]") {