Skip to content

Commit 62edfb2

Browse files
committed
wildcard tests
1 parent ec944b6 commit 62edfb2

File tree

2 files changed

+173
-83
lines changed

2 files changed

+173
-83
lines changed

rules/rules.go

Lines changed: 23 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,6 @@ type Rule struct {
3434

3535
type methodPattern string
3636

37-
// An asterisk is treated as matching any method
38-
func (t methodPattern) matches(input string) bool {
39-
return t == "*" || string(t) == input
40-
}
41-
4237
// Beyond the 9 methods defined in HTTP 1.1, there actually are many more seldom used extension methods by
4338
// various systems.
4439
// https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
@@ -126,11 +121,21 @@ func parseHostPattern(input string) (host []labelPattern, rest string, err error
126121
// Represents a valid label in a hostname. For example, wobble in `wib-ble.wobble.com`.
127122
type labelPattern string
128123

124+
// An `asterisk` is treated as matching anything
125+
func (lp labelPattern) matches(input string) bool {
126+
return lp == "*" || string(lp) == input
127+
}
128+
129129
func parseLabelPattern(rest string) (labelPattern, string, error) {
130130
if rest == "" {
131131
return "", "", errors.New("expected label, got empty string")
132132
}
133133

134+
// If the label is simply an asterisk, good to go.
135+
if rest[0] == '*' {
136+
return "*", rest[1:], nil
137+
}
138+
134139
// First try to get a valid leading char. Leading char in a label cannot be a hyphen.
135140
if !isValidLabelChar(rest[0]) || rest[0] == '-' {
136141
return "", "", fmt.Errorf("could not pull label from front of string: %s", rest)
@@ -216,11 +221,24 @@ func parsePathPattern(input string) ([]segmentPattern, string, error) {
216221
// Represents a valid url path segmentPattern.
217222
type segmentPattern string
218223

224+
// An `*` is treated as matching anything
225+
func (sp segmentPattern) matches(input string) bool {
226+
return sp == "*" || string(sp) == input
227+
}
228+
219229
func parsePathSegmentPattern(input string) (segmentPattern, string, error) {
220230
if input == "" {
221231
return "", "", nil
222232
}
223233

234+
if len(input) > 0 && input[0] == '*' {
235+
if len(input) > 1 && input[1] != '/' {
236+
return "", "", fmt.Errorf("path segment wildcards must be for the entire segment, got: %s", input)
237+
}
238+
239+
return segmentPattern(input[0]), input[1:], nil
240+
}
241+
224242
var i int
225243
for i = 0; i < len(input); i++ {
226244
c := input[i]
@@ -411,84 +429,6 @@ func (re *Engine) Evaluate(method, url string) Result {
411429
}
412430
}
413431

414-
type protocol string
415-
416-
func parseProtocol(input string) (protocol, string, error) {
417-
if input == "" {
418-
return "", "", errors.New("expected protocol, got empty string")
419-
}
420-
421-
// Look for "://" separator
422-
if idx := strings.Index(input, "://"); idx > 0 {
423-
protocolPart := input[:idx]
424-
rest := input[idx+3:]
425-
426-
// Validate protocol characters (scheme per RFC 3986)
427-
// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
428-
if len(protocolPart) == 0 {
429-
return "", "", errors.New("empty protocol")
430-
}
431-
432-
// First character must be alpha
433-
if !((protocolPart[0] >= 'A' && protocolPart[0] <= 'Z') ||
434-
(protocolPart[0] >= 'a' && protocolPart[0] <= 'z')) {
435-
return "", "", errors.New("protocol must start with a letter")
436-
}
437-
438-
// Rest can be alphanumeric, +, -, or .
439-
for i := 1; i < len(protocolPart); i++ {
440-
c := protocolPart[i]
441-
if !((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
442-
(c >= '0' && c <= '9') || c == '+' || c == '-' || c == '.') {
443-
return "", "", fmt.Errorf("invalid character in protocol: %c", c)
444-
}
445-
}
446-
447-
return protocol(protocolPart), rest, nil
448-
}
449-
450-
// No protocol found
451-
return "", input, nil
452-
}
453-
454-
type port uint16
455-
456-
func parsePort(input string) (port, string, error) {
457-
if input == "" {
458-
return 0, "", nil
459-
}
460-
461-
// Port must start with ':'
462-
if input[0] != ':' {
463-
return 0, input, nil
464-
}
465-
466-
// Find the end of the port number
467-
i := 1
468-
for i < len(input) && input[i] >= '0' && input[i] <= '9' {
469-
i++
470-
}
471-
472-
// No digits found after ':'
473-
if i == 1 {
474-
return 0, "", errors.New("expected port number after ':'")
475-
}
476-
477-
portStr := input[1:i]
478-
rest := input[i:]
479-
480-
// Convert to uint16 (port range is 0-65535)
481-
portNum := 0
482-
for _, digit := range portStr {
483-
portNum = portNum*10 + int(digit-'0')
484-
if portNum > 65535 {
485-
return 0, "", errors.New("port number too large (max 65535)")
486-
}
487-
}
488-
489-
return port(portNum), rest, nil
490-
}
491-
492432
// Matches checks if the rule matches the given method and URL using wildcard patterns
493433
func (re *Engine) matches(r Rule, method, url string) bool {
494434
return true

rules/rules_test.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,34 @@ func TestParseHost(t *testing.T) {
257257
expectedRest: "",
258258
expectError: false,
259259
},
260+
{
261+
name: "wildcard subdomain",
262+
input: "*.example.com",
263+
expectedHost: []labelPattern{labelPattern("*"), labelPattern("example"), labelPattern("com")},
264+
expectedRest: "",
265+
expectError: false,
266+
},
267+
{
268+
name: "wildcard domain",
269+
input: "api.*",
270+
expectedHost: []labelPattern{labelPattern("api"), labelPattern("*")},
271+
expectedRest: "",
272+
expectError: false,
273+
},
274+
{
275+
name: "multiple wildcards",
276+
input: "*.*.com",
277+
expectedHost: []labelPattern{labelPattern("*"), labelPattern("*"), labelPattern("com")},
278+
expectedRest: "",
279+
expectError: false,
280+
},
281+
{
282+
name: "wildcard with trailing content",
283+
input: "*.example.com/path",
284+
expectedHost: []labelPattern{labelPattern("*"), labelPattern("example"), labelPattern("com")},
285+
expectedRest: "/path",
286+
expectError: false,
287+
},
260288
}
261289

262290
for _, tt := range tests {
@@ -399,6 +427,27 @@ func TestParseLabel(t *testing.T) {
399427
expectedRest: "/path",
400428
expectError: false,
401429
},
430+
{
431+
name: "wildcard label",
432+
input: "*",
433+
expectedLabel: "*",
434+
expectedRest: "",
435+
expectError: false,
436+
},
437+
{
438+
name: "wildcard with dot",
439+
input: "*.com",
440+
expectedLabel: "*",
441+
expectedRest: ".com",
442+
expectError: false,
443+
},
444+
{
445+
name: "wildcard with trailing content",
446+
input: "*/path",
447+
expectedLabel: "*",
448+
expectedRest: "/path",
449+
expectError: false,
450+
},
402451
}
403452

404453
for _, tt := range tests {
@@ -555,6 +604,34 @@ func TestParsePathSegment(t *testing.T) {
555604
expectedRest: "[bracket]",
556605
expectError: false,
557606
},
607+
{
608+
name: "wildcard segment",
609+
input: "*",
610+
expectedSegment: "*",
611+
expectedRest: "",
612+
expectError: false,
613+
},
614+
{
615+
name: "wildcard with slash",
616+
input: "*/users",
617+
expectedSegment: "*",
618+
expectedRest: "/users",
619+
expectError: false,
620+
},
621+
{
622+
name: "wildcard at end with slash",
623+
input: "*",
624+
expectedSegment: "*",
625+
expectedRest: "",
626+
expectError: false,
627+
},
628+
{
629+
name: "invalid partial wildcard",
630+
input: "*abc",
631+
expectedSegment: "",
632+
expectedRest: "",
633+
expectError: true,
634+
},
558635
}
559636

560637
for _, tt := range tests {
@@ -718,6 +795,41 @@ func TestParsePath(t *testing.T) {
718795
expectedRest: "",
719796
expectError: false,
720797
},
798+
{
799+
name: "path with wildcard segment",
800+
input: "/api/*/users",
801+
expectedSegments: []segmentPattern{"api", "*", "users"},
802+
expectedRest: "",
803+
expectError: false,
804+
},
805+
{
806+
name: "path with multiple wildcards",
807+
input: "/*/v1/*/profile",
808+
expectedSegments: []segmentPattern{"*", "v1", "*", "profile"},
809+
expectedRest: "",
810+
expectError: false,
811+
},
812+
{
813+
name: "path ending with wildcard",
814+
input: "/api/users/*",
815+
expectedSegments: []segmentPattern{"api", "users", "*"},
816+
expectedRest: "",
817+
expectError: false,
818+
},
819+
{
820+
name: "path starting with wildcard",
821+
input: "/*/users",
822+
expectedSegments: []segmentPattern{"*", "users"},
823+
expectedRest: "",
824+
expectError: false,
825+
},
826+
{
827+
name: "path with wildcard and query",
828+
input: "/api/*/users?limit=10",
829+
expectedSegments: []segmentPattern{"api", "*", "users"},
830+
expectedRest: "?limit=10",
831+
expectError: false,
832+
},
721833
}
722834

723835
for _, tt := range tests {
@@ -817,6 +929,44 @@ func TestParseAllowRule(t *testing.T) {
817929
},
818930
expectError: false,
819931
},
932+
{
933+
name: "wildcard domain",
934+
input: "domain=*.example.com",
935+
expectedRule: Rule{
936+
Raw: "domain=*.example.com",
937+
HostPattern: []labelPattern{labelPattern("com"), labelPattern("example"), labelPattern("*")},
938+
},
939+
expectError: false,
940+
},
941+
{
942+
name: "wildcard path",
943+
input: "path=/api/*/users",
944+
expectedRule: Rule{
945+
Raw: "path=/api/*/users",
946+
PathPattern: []segmentPattern{segmentPattern("api"), segmentPattern("*"), segmentPattern("users")},
947+
},
948+
expectError: false,
949+
},
950+
{
951+
name: "wildcard method",
952+
input: "method=*",
953+
expectedRule: Rule{
954+
Raw: "method=*",
955+
MethodPatterns: map[methodPattern]struct{}{methodPattern("*"): {}},
956+
},
957+
expectError: false,
958+
},
959+
{
960+
name: "all wildcards",
961+
input: "method=* domain=*.* path=/*/",
962+
expectedRule: Rule{
963+
Raw: "method=* domain=*.* path=/*/",
964+
MethodPatterns: map[methodPattern]struct{}{methodPattern("*"): {}},
965+
HostPattern: []labelPattern{labelPattern("*"), labelPattern("*")},
966+
PathPattern: []segmentPattern{segmentPattern("*")},
967+
},
968+
expectError: false,
969+
},
820970
{
821971
name: "invalid key",
822972
input: "invalid=value",

0 commit comments

Comments
 (0)