package ingress import ( "net/url" "regexp" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/yaml.v2" "github.com/cloudflare/cloudflared/cmd/cloudflared/config" ) func Test_parseIngress(t *testing.T) { localhost8000 := MustParseURL(t, "https://localhost:8000") localhost8001 := MustParseURL(t, "https://localhost:8001") type args struct { rawYAML string } tests := []struct { name string args args want Ingress wantErr bool }{ { name: "Empty file", args: args{rawYAML: ""}, wantErr: true, }, { name: "Multiple rules", args: args{rawYAML: ` ingress: - hostname: tunnel1.example.com service: https://localhost:8000 - hostname: "*" service: https://localhost:8001 `}, want: Ingress{Rules: []Rule{ { Hostname: "tunnel1.example.com", Service: localhost8000, }, { Hostname: "*", Service: localhost8001, }, }}, }, { name: "Extra keys", args: args{rawYAML: ` ingress: - hostname: "*" service: https://localhost:8000 extraKey: extraValue `}, want: Ingress{Rules: []Rule{ { Hostname: "*", Service: localhost8000, }, }}, }, { name: "Hostname can be omitted", args: args{rawYAML: ` ingress: - service: https://localhost:8000 `}, want: Ingress{Rules: []Rule{ { Service: localhost8000, }, }}, }, { name: "Invalid service", args: args{rawYAML: ` ingress: - hostname: "*" service: https://local host:8000 `}, wantErr: true, }, { name: "Last rule isn't catchall", args: args{rawYAML: ` ingress: - hostname: example.com service: https://localhost:8000 `}, wantErr: true, }, { name: "First rule is catchall", args: args{rawYAML: ` ingress: - service: https://localhost:8000 - hostname: example.com service: https://localhost:8000 `}, wantErr: true, }, { name: "Catch-all rule can't have a path", args: args{rawYAML: ` ingress: - service: https://localhost:8001 path: /subpath1/(.*)/subpath2 `}, wantErr: true, }, { name: "Invalid regex", args: args{rawYAML: ` ingress: - hostname: example.com service: https://localhost:8000 path: "*/subpath2" - service: https://localhost:8001 `}, wantErr: true, }, { name: "Service must have a scheme", args: args{rawYAML: ` ingress: - service: localhost:8000 `}, wantErr: true, }, { name: "Wildcard not at start", args: args{rawYAML: ` ingress: - hostname: "test.*.example.com" service: https://localhost:8000 `}, wantErr: true, }, { name: "Service can't have a path", args: args{rawYAML: ` ingress: - service: https://localhost:8000/static/ `}, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := ParseIngress(MustReadIngress(tt.args.rawYAML)) if (err != nil) != tt.wantErr { t.Errorf("ParseIngress() error = %v, wantErr %v", err, tt.wantErr) return } assert.Equal(t, tt.want, got) }) } } func MustParseURL(t *testing.T, rawURL string) *url.URL { u, err := url.Parse(rawURL) require.NoError(t, err) return u } func Test_rule_matches(t *testing.T) { type fields struct { Hostname string Path *regexp.Regexp Service *url.URL } type args struct { requestURL *url.URL } tests := []struct { name string fields fields args args want bool }{ { name: "Just hostname, pass", fields: fields{ Hostname: "example.com", }, args: args{ requestURL: MustParseURL(t, "https://example.com"), }, want: true, }, { name: "Entire hostname is wildcard, should match everything", fields: fields{ Hostname: "*", }, args: args{ requestURL: MustParseURL(t, "https://example.com"), }, want: true, }, { name: "Just hostname, fail", fields: fields{ Hostname: "example.com", }, args: args{ requestURL: MustParseURL(t, "https://foo.bar"), }, want: false, }, { name: "Just wildcard hostname, pass", fields: fields{ Hostname: "*.example.com", }, args: args{ requestURL: MustParseURL(t, "https://adam.example.com"), }, want: true, }, { name: "Just wildcard hostname, fail", fields: fields{ Hostname: "*.example.com", }, args: args{ requestURL: MustParseURL(t, "https://tunnel.com"), }, want: false, }, { name: "Just wildcard outside of subdomain in hostname, fail", fields: fields{ Hostname: "*example.com", }, args: args{ requestURL: MustParseURL(t, "https://www.example.com"), }, want: false, }, { name: "Wildcard over multiple subdomains", fields: fields{ Hostname: "*.example.com", }, args: args{ requestURL: MustParseURL(t, "https://adam.chalmers.example.com"), }, want: true, }, { name: "Hostname and path", fields: fields{ Hostname: "*.example.com", Path: regexp.MustCompile("/static/.*\\.html"), }, args: args{ requestURL: MustParseURL(t, "https://www.example.com/static/index.html"), }, want: true, }, } 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 { t.Errorf("rule.matches() = %v, want %v", got, tt.want) } }) } } func BenchmarkFindMatch(b *testing.B) { rulesYAML := ` ingress: - hostname: tunnel1.example.com service: https://localhost:8000 - hostname: tunnel2.example.com service: https://localhost:8001 - hostname: "*" service: https://localhost:8002 ` ing, err := ParseIngress(MustReadIngress(rulesYAML)) if err != nil { b.Error(err) } for n := 0; n < b.N; n++ { ing.FindMatchingRule("tunnel1.example.com", "") ing.FindMatchingRule("tunnel2.example.com", "") ing.FindMatchingRule("tunnel3.example.com", "") } } func MustReadIngress(s string) *config.Configuration { var conf config.Configuration err := yaml.Unmarshal([]byte(s), &conf) if err != nil { panic(err) } return &conf }