Skip to content

Commit 959e4bd

Browse files
committed
mutate rest in parse allow rule to make pattern clearer
1 parent 3a578be commit 959e4bd

File tree

1 file changed

+107
-100
lines changed

1 file changed

+107
-100
lines changed

rulesengine/rules.go

Lines changed: 107 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,125 @@ import (
66
"strings"
77
)
88

9-
// Rule represents an allow rule with optional HTTP method restrictions
9+
// Rule represents an allow rule passed to the cli with --allow or read from the config file.
10+
// Rules have a specific grammar that we need to parse carefully.
11+
// Example: --allow="method=GET,PATCH domain=wibble.wobble.com, path=/posts/*"
1012
type Rule struct {
1113

12-
// The path segments of the url
13-
// nil means all paths allowed
14-
// a path segment of `*` acts as a wild card.
15-
// sub paths automatically match
14+
// The path segments of the url.
15+
// - nil means all paths allowed
16+
// - a path segment of `*` acts as a wild card.
17+
// - sub paths automatically match
1618
PathPattern []segmentPattern
1719

18-
// The labels of the host, i.e. ["google", "com"]
19-
// nil means all hosts allowed
20-
// A label of `*` acts as a wild card.
21-
// subdomains automatically match
20+
// The labels of the host, i.e. ["google", "com"].
21+
// - nil means all hosts allowed
22+
// - A label of `*` acts as a wild card.
23+
// - subdomains automatically match
2224
HostPattern []labelPattern
2325

24-
// The allowed http methods
25-
// nil means all methods allowed
26+
// The allowed http methods.
27+
// - nil means all methods allowed
2628
MethodPatterns map[methodPattern]struct{}
2729

2830
// Raw rule string for logging
2931
Raw string
3032
}
3133

34+
// ParseAllowSpecs parses a slice of --allow specs into allow Rules.
35+
func ParseAllowSpecs(allowStrings []string) ([]Rule, error) {
36+
var out []Rule
37+
for _, s := range allowStrings {
38+
r, err := parseAllowRule(s)
39+
if err != nil {
40+
return nil, fmt.Errorf("failed to parse allow '%s': %v", s, err)
41+
}
42+
out = append(out, r)
43+
}
44+
return out, nil
45+
}
46+
47+
// parseAllowRule takes an allow rule string and tries to parse it as a rule.
48+
func parseAllowRule(ruleStr string) (Rule, error) {
49+
rule := Rule{
50+
Raw: ruleStr,
51+
}
52+
53+
// Functions called by this function used a really common pattern: recursive descent parsing.
54+
// All the helper functions for parsing an allow rule will be called like `thing, rest, err := parseThing(rest)`.
55+
// What's going on here is that we try to parse some expected text from the front of the string.
56+
// If we succeed, we get back the thing we parsed and the remaining text. If we fail, we get back a non nil error.
57+
rest := ruleStr
58+
var key string
59+
var err error
60+
61+
// Ann allow rule can have as many key=value pairs as needed, we go until there's no more text in the rule.
62+
for rest != "" {
63+
// Parse the key
64+
key, rest, err = parseKey(rest)
65+
if err != nil {
66+
return Rule{}, fmt.Errorf("failed to parse key: %v", err)
67+
}
68+
69+
// Parse the value based on the key type
70+
switch key {
71+
case "method":
72+
// Initialize Methods map if needed
73+
if rule.MethodPatterns == nil {
74+
rule.MethodPatterns = make(map[methodPattern]struct{})
75+
}
76+
77+
var method methodPattern
78+
for {
79+
method, rest, err = parseMethodPattern(rest)
80+
if err != nil {
81+
return Rule{}, fmt.Errorf("failed to parse method: %v", err)
82+
}
83+
84+
rule.MethodPatterns[method] = struct{}{}
85+
86+
// Check if there's a comma for more methods
87+
if rest != "" && rest[0] == ',' {
88+
rest = rest[1:] // Skip the comma
89+
continue
90+
}
91+
92+
break
93+
}
94+
95+
case "domain":
96+
var host []labelPattern
97+
host, rest, err = parseHostPattern(rest)
98+
if err != nil {
99+
return Rule{}, fmt.Errorf("failed to parse domain: %v", err)
100+
}
101+
102+
// Convert labels to strings
103+
rule.HostPattern = append(rule.HostPattern, host...)
104+
105+
case "path":
106+
var segments []segmentPattern
107+
segments, rest, err = parsePathPattern(rest)
108+
if err != nil {
109+
return Rule{}, fmt.Errorf("failed to parse path: %v", err)
110+
}
111+
112+
// Convert segments to strings
113+
rule.PathPattern = append(rule.PathPattern, segments...)
114+
115+
default:
116+
return Rule{}, fmt.Errorf("unknown key: %s", key)
117+
}
118+
119+
// Skip whitespace or comma separators
120+
for rest != "" && (rest[0] == ' ' || rest[0] == '\t' || rest[0] == ',') {
121+
rest = rest[1:]
122+
}
123+
}
124+
125+
return rule, nil
126+
}
127+
32128
type methodPattern string
33129

34130
// Beyond the 9 methods defined in HTTP 1.1, there actually are many more seldom used extension methods by
@@ -300,92 +396,3 @@ func parseKey(rule string) (string, string, error) {
300396

301397
return "", "", errors.New("expected key")
302398
}
303-
304-
func parseAllowRule(ruleStr string) (Rule, error) {
305-
rule := Rule{
306-
Raw: ruleStr,
307-
}
308-
309-
rest := ruleStr
310-
311-
for rest != "" {
312-
// Parse the key
313-
key, valueRest, err := parseKey(rest)
314-
if err != nil {
315-
return Rule{}, fmt.Errorf("failed to parse key: %v", err)
316-
}
317-
318-
// Parse the value based on the key type
319-
switch key {
320-
case "method":
321-
// Handle comma-separated methods
322-
methodsRest := valueRest
323-
324-
// Initialize Methods map if needed
325-
if rule.MethodPatterns == nil {
326-
rule.MethodPatterns = make(map[methodPattern]struct{})
327-
}
328-
329-
for {
330-
token, remaining, err := parseMethodPattern(methodsRest)
331-
if err != nil {
332-
return Rule{}, fmt.Errorf("failed to parse method: %v", err)
333-
}
334-
335-
rule.MethodPatterns[token] = struct{}{}
336-
337-
// Check if there's a comma for more methods
338-
if remaining != "" && remaining[0] == ',' {
339-
methodsRest = remaining[1:] // Skip the comma
340-
continue
341-
}
342-
343-
rest = remaining
344-
break
345-
}
346-
347-
case "domain":
348-
hostLabels, remaining, err := parseHostPattern(valueRest)
349-
if err != nil {
350-
return Rule{}, fmt.Errorf("failed to parse domain: %v", err)
351-
}
352-
353-
// Convert labels to strings
354-
rule.HostPattern = append(rule.HostPattern, hostLabels...)
355-
rest = remaining
356-
357-
case "path":
358-
segments, remaining, err := parsePathPattern(valueRest)
359-
if err != nil {
360-
return Rule{}, fmt.Errorf("failed to parse path: %v", err)
361-
}
362-
363-
// Convert segments to strings
364-
rule.PathPattern = append(rule.PathPattern, segments...)
365-
rest = remaining
366-
367-
default:
368-
return Rule{}, fmt.Errorf("unknown key: %s", key)
369-
}
370-
371-
// Skip whitespace or comma separators
372-
for rest != "" && (rest[0] == ' ' || rest[0] == '\t' || rest[0] == ',') {
373-
rest = rest[1:]
374-
}
375-
}
376-
377-
return rule, nil
378-
}
379-
380-
// ParseAllowSpecs parses a slice of --allow specs into allow Rules.
381-
func ParseAllowSpecs(allowStrings []string) ([]Rule, error) {
382-
var out []Rule
383-
for _, s := range allowStrings {
384-
r, err := parseAllowRule(s)
385-
if err != nil {
386-
return nil, fmt.Errorf("failed to parse allow '%s': %v", s, err)
387-
}
388-
out = append(out, r)
389-
}
390-
return out, nil
391-
}

0 commit comments

Comments
 (0)