TUN-6801: Add punycode alternatives for ingress rules

This commit is contained in:
Devin Carr 2022-09-22 16:19:06 -07:00
parent be0305ec58
commit b3e26420c0
4 changed files with 98 additions and 32 deletions

View File

@ -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

View File

@ -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: `

View File

@ -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

View File

@ -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)
}
})