diff --git a/ingress/ingress.go b/ingress/ingress.go index b1d87878..b4600453 100644 --- a/ingress/ingress.go +++ b/ingress/ingress.go @@ -11,6 +11,7 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/urfave/cli/v2" + "golang.org/x/net/idna" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/ingress/middleware" @@ -275,6 +276,16 @@ func validateIngress(ingress []config.UnvalidatedIngressRule, defaults OriginReq return Ingress{}, err } + isCatchAllRule := (r.Hostname == "" || r.Hostname == "*") && r.Path == "" + punycodeHostname := "" + if !isCatchAllRule { + punycode, err := idna.Lookup.ToASCII(r.Hostname) + // Don't provide the punycode hostname if it is the same as the original hostname + if err == nil && punycode != r.Hostname { + punycodeHostname = punycode + } + } + var pathRegexp *Regexp if r.Path != "" { var err error @@ -286,11 +297,12 @@ func validateIngress(ingress []config.UnvalidatedIngressRule, defaults OriginReq } rules[i] = Rule{ - Hostname: r.Hostname, - Service: service, - Path: pathRegexp, - Handlers: handlers, - Config: cfg, + Hostname: r.Hostname, + punycodeHostname: punycodeHostname, + Service: service, + Path: pathRegexp, + Handlers: handlers, + Config: cfg, } } return Ingress{Rules: rules, Defaults: defaults}, nil diff --git a/ingress/ingress_test.go b/ingress/ingress_test.go index 2f1c2850..1e09fac7 100644 --- a/ingress/ingress_test.go +++ b/ingress/ingress_test.go @@ -130,6 +130,36 @@ ingress: }, }, }, + { + name: "Unicode domain", + args: args{rawYAML: ` +ingress: + - hostname: môô.cloudflare.com + service: https://localhost:8000 + - service: https://localhost:8001 +`}, + want: []Rule{ + { + Hostname: "môô.cloudflare.com", + punycodeHostname: "xn--m-xgaa.cloudflare.com", + Service: &httpService{url: localhost8000}, + Config: defaultConfig, + }, + { + Service: &httpService{url: localhost8001}, + Config: defaultConfig, + }, + }, + }, + { + name: "Invalid unicode domain", + args: args{rawYAML: fmt.Sprintf(` +ingress: + - hostname: %s + service: https://localhost:8000 +`, string(rune(0xd8f3))+".cloudflare.com")}, + wantErr: true, + }, { name: "Invalid service", args: args{rawYAML: ` diff --git a/ingress/rule.go b/ingress/rule.go index c7733254..43c7ad5e 100644 --- a/ingress/rule.go +++ b/ingress/rule.go @@ -14,6 +14,9 @@ type Rule struct { // Requests for this hostname will be proxied to this rule's service. Hostname string `json:"hostname"` + // punycodeHostname is an additional optional hostname converted to punycode. + punycodeHostname string + // Path is an optional regex that can specify path-driven ingress rules. Path *Regexp `json:"path"` @@ -50,9 +53,18 @@ func (r Rule) MultiLineString() string { // Matches checks if the rule matches a given hostname/path combination. func (r *Rule) Matches(hostname, path string) bool { - hostMatch := r.Hostname == "" || r.Hostname == "*" || matchHost(r.Hostname, hostname) + hostMatch := false + if r.Hostname == "" || r.Hostname == "*" { + hostMatch = true + } else { + hostMatch = matchHost(r.Hostname, hostname) + } + punycodeHostMatch := false + if r.punycodeHostname != "" { + punycodeHostMatch = matchHost(r.punycodeHostname, hostname) + } pathMatch := r.Path == nil || r.Path.Regexp == nil || r.Path.Regexp.MatchString(path) - return hostMatch && pathMatch + return (hostMatch || punycodeHostMatch) && pathMatch } // Regexp adds unmarshalling from json for regexp.Regexp diff --git a/ingress/rule_test.go b/ingress/rule_test.go index 8ab86132..1c051137 100644 --- a/ingress/rule_test.go +++ b/ingress/rule_test.go @@ -14,23 +14,18 @@ import ( ) func Test_rule_matches(t *testing.T) { - type fields struct { - Hostname string - Path *Regexp - Service OriginService - } type args struct { requestURL *url.URL } tests := []struct { - name string - fields fields - args args - want bool + name string + rule Rule + args args + want bool }{ { name: "Just hostname, pass", - fields: fields{ + rule: Rule{ Hostname: "example.com", }, args: args{ @@ -38,9 +33,31 @@ func Test_rule_matches(t *testing.T) { }, want: true, }, + { + name: "Unicode hostname with unicode request, pass", + rule: Rule{ + Hostname: "môô.cloudflare.com", + punycodeHostname: "xn--m-xgaa.cloudflare.com", + }, + args: args{ + requestURL: MustParseURL(t, "https://môô.cloudflare.com"), + }, + want: true, + }, + { + name: "Unicode hostname with punycode request, pass", + rule: Rule{ + Hostname: "môô.cloudflare.com", + punycodeHostname: "xn--m-xgaa.cloudflare.com", + }, + args: args{ + requestURL: MustParseURL(t, "https://xn--m-xgaa.cloudflare.com"), + }, + want: true, + }, { name: "Entire hostname is wildcard, should match everything", - fields: fields{ + rule: Rule{ Hostname: "*", }, args: args{ @@ -50,7 +67,7 @@ func Test_rule_matches(t *testing.T) { }, { name: "Just hostname, fail", - fields: fields{ + rule: Rule{ Hostname: "example.com", }, args: args{ @@ -60,7 +77,7 @@ func Test_rule_matches(t *testing.T) { }, { name: "Just wildcard hostname, pass", - fields: fields{ + rule: Rule{ Hostname: "*.example.com", }, args: args{ @@ -70,7 +87,7 @@ func Test_rule_matches(t *testing.T) { }, { name: "Just wildcard hostname, fail", - fields: fields{ + rule: Rule{ Hostname: "*.example.com", }, args: args{ @@ -80,7 +97,7 @@ func Test_rule_matches(t *testing.T) { }, { name: "Just wildcard outside of subdomain in hostname, fail", - fields: fields{ + rule: Rule{ Hostname: "*example.com", }, args: args{ @@ -90,7 +107,7 @@ func Test_rule_matches(t *testing.T) { }, { name: "Wildcard over multiple subdomains", - fields: fields{ + rule: Rule{ Hostname: "*.example.com", }, args: args{ @@ -100,7 +117,7 @@ func Test_rule_matches(t *testing.T) { }, { name: "Hostname and path", - fields: fields{ + rule: Rule{ Hostname: "*.example.com", Path: &Regexp{Regexp: regexp.MustCompile("/static/.*\\.html")}, }, @@ -111,7 +128,7 @@ func Test_rule_matches(t *testing.T) { }, { name: "Hostname and empty Regex", - fields: fields{ + rule: Rule{ Hostname: "example.com", Path: &Regexp{}, }, @@ -122,7 +139,7 @@ func Test_rule_matches(t *testing.T) { }, { name: "Hostname and nil path", - fields: fields{ + rule: Rule{ Hostname: "example.com", Path: nil, }, @@ -134,13 +151,8 @@ func Test_rule_matches(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - r := Rule{ - Hostname: tt.fields.Hostname, - Path: tt.fields.Path, - Service: tt.fields.Service, - } u := tt.args.requestURL - if got := r.Matches(u.Hostname(), u.Path); got != tt.want { + if got := tt.rule.Matches(u.Hostname(), u.Path); got != tt.want { t.Errorf("rule.matches() = %v, want %v", got, tt.want) } })