Skip to content

Commit b3e2642

Browse files
committed
TUN-6801: Add punycode alternatives for ingress rules
1 parent be0305e commit b3e2642

File tree

4 files changed

+98
-32
lines changed

4 files changed

+98
-32
lines changed

ingress/ingress.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/pkg/errors"
1212
"github.com/rs/zerolog"
1313
"github.com/urfave/cli/v2"
14+
"golang.org/x/net/idna"
1415

1516
"github.com/cloudflare/cloudflared/config"
1617
"github.com/cloudflare/cloudflared/ingress/middleware"
@@ -275,6 +276,16 @@ func validateIngress(ingress []config.UnvalidatedIngressRule, defaults OriginReq
275276
return Ingress{}, err
276277
}
277278

279+
isCatchAllRule := (r.Hostname == "" || r.Hostname == "*") && r.Path == ""
280+
punycodeHostname := ""
281+
if !isCatchAllRule {
282+
punycode, err := idna.Lookup.ToASCII(r.Hostname)
283+
// Don't provide the punycode hostname if it is the same as the original hostname
284+
if err == nil && punycode != r.Hostname {
285+
punycodeHostname = punycode
286+
}
287+
}
288+
278289
var pathRegexp *Regexp
279290
if r.Path != "" {
280291
var err error
@@ -286,11 +297,12 @@ func validateIngress(ingress []config.UnvalidatedIngressRule, defaults OriginReq
286297
}
287298

288299
rules[i] = Rule{
289-
Hostname: r.Hostname,
290-
Service: service,
291-
Path: pathRegexp,
292-
Handlers: handlers,
293-
Config: cfg,
300+
Hostname: r.Hostname,
301+
punycodeHostname: punycodeHostname,
302+
Service: service,
303+
Path: pathRegexp,
304+
Handlers: handlers,
305+
Config: cfg,
294306
}
295307
}
296308
return Ingress{Rules: rules, Defaults: defaults}, nil

ingress/ingress_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,36 @@ ingress:
130130
},
131131
},
132132
},
133+
{
134+
name: "Unicode domain",
135+
args: args{rawYAML: `
136+
ingress:
137+
- hostname: môô.cloudflare.com
138+
service: https://localhost:8000
139+
- service: https://localhost:8001
140+
`},
141+
want: []Rule{
142+
{
143+
Hostname: "môô.cloudflare.com",
144+
punycodeHostname: "xn--m-xgaa.cloudflare.com",
145+
Service: &httpService{url: localhost8000},
146+
Config: defaultConfig,
147+
},
148+
{
149+
Service: &httpService{url: localhost8001},
150+
Config: defaultConfig,
151+
},
152+
},
153+
},
154+
{
155+
name: "Invalid unicode domain",
156+
args: args{rawYAML: fmt.Sprintf(`
157+
ingress:
158+
- hostname: %s
159+
service: https://localhost:8000
160+
`, string(rune(0xd8f3))+".cloudflare.com")},
161+
wantErr: true,
162+
},
133163
{
134164
name: "Invalid service",
135165
args: args{rawYAML: `

ingress/rule.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ type Rule struct {
1414
// Requests for this hostname will be proxied to this rule's service.
1515
Hostname string `json:"hostname"`
1616

17+
// punycodeHostname is an additional optional hostname converted to punycode.
18+
punycodeHostname string
19+
1720
// Path is an optional regex that can specify path-driven ingress rules.
1821
Path *Regexp `json:"path"`
1922

@@ -50,9 +53,18 @@ func (r Rule) MultiLineString() string {
5053

5154
// Matches checks if the rule matches a given hostname/path combination.
5255
func (r *Rule) Matches(hostname, path string) bool {
53-
hostMatch := r.Hostname == "" || r.Hostname == "*" || matchHost(r.Hostname, hostname)
56+
hostMatch := false
57+
if r.Hostname == "" || r.Hostname == "*" {
58+
hostMatch = true
59+
} else {
60+
hostMatch = matchHost(r.Hostname, hostname)
61+
}
62+
punycodeHostMatch := false
63+
if r.punycodeHostname != "" {
64+
punycodeHostMatch = matchHost(r.punycodeHostname, hostname)
65+
}
5466
pathMatch := r.Path == nil || r.Path.Regexp == nil || r.Path.Regexp.MatchString(path)
55-
return hostMatch && pathMatch
67+
return (hostMatch || punycodeHostMatch) && pathMatch
5668
}
5769

5870
// Regexp adds unmarshalling from json for regexp.Regexp

ingress/rule_test.go

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,50 @@ import (
1414
)
1515

1616
func Test_rule_matches(t *testing.T) {
17-
type fields struct {
18-
Hostname string
19-
Path *Regexp
20-
Service OriginService
21-
}
2217
type args struct {
2318
requestURL *url.URL
2419
}
2520
tests := []struct {
26-
name string
27-
fields fields
28-
args args
29-
want bool
21+
name string
22+
rule Rule
23+
args args
24+
want bool
3025
}{
3126
{
3227
name: "Just hostname, pass",
33-
fields: fields{
28+
rule: Rule{
3429
Hostname: "example.com",
3530
},
3631
args: args{
3732
requestURL: MustParseURL(t, "https://example.com"),
3833
},
3934
want: true,
4035
},
36+
{
37+
name: "Unicode hostname with unicode request, pass",
38+
rule: Rule{
39+
Hostname: "môô.cloudflare.com",
40+
punycodeHostname: "xn--m-xgaa.cloudflare.com",
41+
},
42+
args: args{
43+
requestURL: MustParseURL(t, "https://môô.cloudflare.com"),
44+
},
45+
want: true,
46+
},
47+
{
48+
name: "Unicode hostname with punycode request, pass",
49+
rule: Rule{
50+
Hostname: "môô.cloudflare.com",
51+
punycodeHostname: "xn--m-xgaa.cloudflare.com",
52+
},
53+
args: args{
54+
requestURL: MustParseURL(t, "https://xn--m-xgaa.cloudflare.com"),
55+
},
56+
want: true,
57+
},
4158
{
4259
name: "Entire hostname is wildcard, should match everything",
43-
fields: fields{
60+
rule: Rule{
4461
Hostname: "*",
4562
},
4663
args: args{
@@ -50,7 +67,7 @@ func Test_rule_matches(t *testing.T) {
5067
},
5168
{
5269
name: "Just hostname, fail",
53-
fields: fields{
70+
rule: Rule{
5471
Hostname: "example.com",
5572
},
5673
args: args{
@@ -60,7 +77,7 @@ func Test_rule_matches(t *testing.T) {
6077
},
6178
{
6279
name: "Just wildcard hostname, pass",
63-
fields: fields{
80+
rule: Rule{
6481
Hostname: "*.example.com",
6582
},
6683
args: args{
@@ -70,7 +87,7 @@ func Test_rule_matches(t *testing.T) {
7087
},
7188
{
7289
name: "Just wildcard hostname, fail",
73-
fields: fields{
90+
rule: Rule{
7491
Hostname: "*.example.com",
7592
},
7693
args: args{
@@ -80,7 +97,7 @@ func Test_rule_matches(t *testing.T) {
8097
},
8198
{
8299
name: "Just wildcard outside of subdomain in hostname, fail",
83-
fields: fields{
100+
rule: Rule{
84101
Hostname: "*example.com",
85102
},
86103
args: args{
@@ -90,7 +107,7 @@ func Test_rule_matches(t *testing.T) {
90107
},
91108
{
92109
name: "Wildcard over multiple subdomains",
93-
fields: fields{
110+
rule: Rule{
94111
Hostname: "*.example.com",
95112
},
96113
args: args{
@@ -100,7 +117,7 @@ func Test_rule_matches(t *testing.T) {
100117
},
101118
{
102119
name: "Hostname and path",
103-
fields: fields{
120+
rule: Rule{
104121
Hostname: "*.example.com",
105122
Path: &Regexp{Regexp: regexp.MustCompile("/static/.*\\.html")},
106123
},
@@ -111,7 +128,7 @@ func Test_rule_matches(t *testing.T) {
111128
},
112129
{
113130
name: "Hostname and empty Regex",
114-
fields: fields{
131+
rule: Rule{
115132
Hostname: "example.com",
116133
Path: &Regexp{},
117134
},
@@ -122,7 +139,7 @@ func Test_rule_matches(t *testing.T) {
122139
},
123140
{
124141
name: "Hostname and nil path",
125-
fields: fields{
142+
rule: Rule{
126143
Hostname: "example.com",
127144
Path: nil,
128145
},
@@ -134,13 +151,8 @@ func Test_rule_matches(t *testing.T) {
134151
}
135152
for _, tt := range tests {
136153
t.Run(tt.name, func(t *testing.T) {
137-
r := Rule{
138-
Hostname: tt.fields.Hostname,
139-
Path: tt.fields.Path,
140-
Service: tt.fields.Service,
141-
}
142154
u := tt.args.requestURL
143-
if got := r.Matches(u.Hostname(), u.Path); got != tt.want {
155+
if got := tt.rule.Matches(u.Hostname(), u.Path); got != tt.want {
144156
t.Errorf("rule.matches() = %v, want %v", got, tt.want)
145157
}
146158
})

0 commit comments

Comments
 (0)