Skip to content

Commit b7e5303

Browse files
committed
net/http: make Transport support international domain names
This CL makes code like this work: res, err := http.Get("https://фу.бар/баз") So far, IDNA support is limited to the http1 and http2 Transports. The http package is currently responsible for converting domain names into Punycode before calling the net layer. The http package also has to Punycode-ify the hostname for the Host & :authority headers for HTTP/1 and HTTP/2, respectively. No automatic translation from Punycode back to Unicode is performed, per Go's historical behavior. Docs are updated where relevant. No changes needed to the Server package. Things are already in ASCII at that point. No changes to the net package, at least yet. Updates x/net/http2 to git rev 57c7820 for https://golang.org/cl/29071 Updates #13835 Change-Id: I1e9a74c60d00a197ea951a9505da5c3c3187099b Reviewed-on: https://go-review.googlesource.com/29072 Reviewed-by: Chris Broadfoot <[email protected]> Run-TryBot: Brad Fitzpatrick <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent 802cb59 commit b7e5303

File tree

4 files changed

+111
-9
lines changed

4 files changed

+111
-9
lines changed

src/net/http/http_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ func TestCleanHost(t *testing.T) {
5151
{"www.google.com foo", "www.google.com"},
5252
{"www.google.com/foo", "www.google.com"},
5353
{" first character is a space", ""},
54+
{"гофер.рф/foo", "xn--c1ae0ajs.xn--p1ai"},
55+
{"bücher.de", "xn--bcher-kva.de"},
56+
{"bücher.de:8080", "xn--bcher-kva.de:8080"},
57+
{"[1::6]:8080", "[1::6]:8080"},
5458
}
5559
for _, tt := range tests {
5660
got := cleanHost(tt.in)

src/net/http/request.go

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@ import (
1818
"io/ioutil"
1919
"mime"
2020
"mime/multipart"
21+
"net"
2122
"net/http/httptrace"
2223
"net/textproto"
2324
"net/url"
2425
"strconv"
2526
"strings"
2627
"sync"
28+
29+
"golang_org/x/net/idna"
2730
)
2831

2932
const (
@@ -175,11 +178,15 @@ type Request struct {
175178
// For server requests Host specifies the host on which the
176179
// URL is sought. Per RFC 2616, this is either the value of
177180
// the "Host" header or the host name given in the URL itself.
178-
// It may be of the form "host:port".
181+
// It may be of the form "host:port". For international domain
182+
// names, Host may be in Punycode or Unicode form. Use
183+
// golang.org/x/net/idna to convert it to either format if
184+
// needed.
179185
//
180186
// For client requests Host optionally overrides the Host
181187
// header to send. If empty, the Request.Write method uses
182-
// the value of URL.Host.
188+
// the value of URL.Host. Host may contain an international
189+
// domain name.
183190
Host string
184191

185192
// Form contains the parsed form data, including both the URL
@@ -573,7 +580,11 @@ func (req *Request) write(w io.Writer, usingProxy bool, extraHeaders Header, wai
573580
return nil
574581
}
575582

576-
// cleanHost strips anything after '/' or ' '.
583+
// cleanHost cleans up the host sent in request's Host header.
584+
//
585+
// It both strips anything after '/' or ' ', and puts the value
586+
// into Punycode form, if necessary.
587+
//
577588
// Ideally we'd clean the Host header according to the spec:
578589
// https://tools.ietf.org/html/rfc7230#section-5.4 (Host = uri-host [ ":" port ]")
579590
// https://tools.ietf.org/html/rfc7230#section-2.7 (uri-host -> rfc3986's host)
@@ -584,9 +595,21 @@ func (req *Request) write(w io.Writer, usingProxy bool, extraHeaders Header, wai
584595
// first offending character.
585596
func cleanHost(in string) string {
586597
if i := strings.IndexAny(in, " /"); i != -1 {
587-
return in[:i]
598+
in = in[:i]
599+
}
600+
host, port, err := net.SplitHostPort(in)
601+
if err != nil { // input was just a host
602+
a, err := idna.ToASCII(in)
603+
if err != nil {
604+
return in // garbage in, garbage out
605+
}
606+
return a
607+
}
608+
a, err := idna.ToASCII(host)
609+
if err != nil {
610+
return in // garbage in, garbage out
588611
}
589-
return in
612+
return net.JoinHostPort(a, port)
590613
}
591614

592615
// removeZone removes IPv6 zone identifier from host.

src/net/http/transport.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"sync"
2828
"time"
2929

30+
"golang_org/x/net/idna"
3031
"golang_org/x/net/lex/httplex"
3132
)
3233

@@ -1943,11 +1944,15 @@ var portMap = map[string]string{
19431944

19441945
// canonicalAddr returns url.Host but always with a ":port" suffix
19451946
func canonicalAddr(url *url.URL) string {
1946-
addr := url.Host
1947-
if !hasPort(addr) {
1948-
return addr + ":" + portMap[url.Scheme]
1947+
addr := url.Hostname()
1948+
if v, err := idna.ToASCII(addr); err == nil {
1949+
addr = v
19491950
}
1950-
return addr
1951+
port := url.Port()
1952+
if port == "" {
1953+
port = portMap[url.Scheme]
1954+
}
1955+
return net.JoinHostPort(addr, port)
19511956
}
19521957

19531958
// bodyEOFSignal is used by the HTTP/1 transport when reading response

src/net/http/transport_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3629,6 +3629,76 @@ func TestTransportReturnsPeekError(t *testing.T) {
36293629
}
36303630
}
36313631

3632+
// Issue 13835: international domain names should work
3633+
func TestTransportIDNA_h1(t *testing.T) { testTransportIDNA(t, h1Mode) }
3634+
func TestTransportIDNA_h2(t *testing.T) { testTransportIDNA(t, h2Mode) }
3635+
func testTransportIDNA(t *testing.T, h2 bool) {
3636+
defer afterTest(t)
3637+
3638+
const uniDomain = "гофер.го"
3639+
const punyDomain = "xn--c1ae0ajs.xn--c1aw"
3640+
3641+
var port string
3642+
cst := newClientServerTest(t, h2, HandlerFunc(func(w ResponseWriter, r *Request) {
3643+
want := punyDomain + ":" + port
3644+
if r.Host != want {
3645+
t.Errorf("Host header = %q; want %q", r.Host, want)
3646+
}
3647+
if h2 {
3648+
if r.TLS == nil {
3649+
t.Errorf("r.TLS == nil")
3650+
} else if r.TLS.ServerName != punyDomain {
3651+
t.Errorf("TLS.ServerName = %q; want %q", r.TLS.ServerName, punyDomain)
3652+
}
3653+
}
3654+
w.Header().Set("Hit-Handler", "1")
3655+
}))
3656+
defer cst.close()
3657+
3658+
ip, port, err := net.SplitHostPort(cst.ts.Listener.Addr().String())
3659+
if err != nil {
3660+
t.Fatal(err)
3661+
}
3662+
3663+
// Install a fake DNS server.
3664+
ctx := context.WithValue(context.Background(), nettrace.LookupIPAltResolverKey{}, func(ctx context.Context, host string) ([]net.IPAddr, error) {
3665+
if host != punyDomain {
3666+
t.Errorf("got DNS host lookup for %q; want %q", host, punyDomain)
3667+
return nil, nil
3668+
}
3669+
return []net.IPAddr{{IP: net.ParseIP(ip)}}, nil
3670+
})
3671+
3672+
req, _ := NewRequest("GET", cst.scheme()+"://"+uniDomain+":"+port, nil)
3673+
trace := &httptrace.ClientTrace{
3674+
GetConn: func(hostPort string) {
3675+
want := net.JoinHostPort(punyDomain, port)
3676+
if hostPort != want {
3677+
t.Errorf("getting conn for %q; want %q", hostPort, want)
3678+
}
3679+
},
3680+
DNSStart: func(e httptrace.DNSStartInfo) {
3681+
if e.Host != punyDomain {
3682+
t.Errorf("DNSStart Host = %q; want %q", e.Host, punyDomain)
3683+
}
3684+
},
3685+
}
3686+
req = req.WithContext(httptrace.WithClientTrace(ctx, trace))
3687+
3688+
res, err := cst.tr.RoundTrip(req)
3689+
if err != nil {
3690+
t.Fatal(err)
3691+
}
3692+
defer res.Body.Close()
3693+
if res.Header.Get("Hit-Handler") != "1" {
3694+
out, err := httputil.DumpResponse(res, true)
3695+
if err != nil {
3696+
t.Fatal(err)
3697+
}
3698+
t.Errorf("Response body wasn't from Handler. Got:\n%s\n", out)
3699+
}
3700+
}
3701+
36323702
var errFakeRoundTrip = errors.New("fake roundtrip")
36333703

36343704
type funcRoundTripper func()

0 commit comments

Comments
 (0)