Skip to content

Commit 18aa4a4

Browse files
committed
add parsing hosts
1 parent e8bc494 commit 18aa4a4

File tree

2 files changed

+382
-0
lines changed

2 files changed

+382
-0
lines changed

rules/rules.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"fmt"
66
"log/slog"
7+
"strings"
78
)
89

910
type Evaluator interface {
@@ -81,6 +82,89 @@ func isHTTPTokenChar(c byte) bool {
8182
}
8283
}
8384

85+
// Represents a valid host.
86+
// https://datatracker.ietf.org/doc/html/rfc952
87+
// https://datatracker.ietf.org/doc/html/rfc1123#page-13
88+
type host []label
89+
90+
func parseHost(input string) (host host, rest string, err error) {
91+
rest = input
92+
var label label
93+
94+
if input == "" {
95+
return nil, "", errors.New("expected host, got empty string")
96+
}
97+
98+
// There should be at least one label.
99+
label, rest, err = parseLabel(rest)
100+
if err != nil {
101+
return nil, "", err
102+
}
103+
host = append(host, label)
104+
105+
// A host is just a bunch of labels separated by `.` characters.
106+
var found bool
107+
for {
108+
rest, found = strings.CutPrefix(rest, ".")
109+
if !found {
110+
break
111+
}
112+
113+
label, rest, err = parseLabel(rest)
114+
if err != nil {
115+
return nil, "", err
116+
}
117+
host = append(host, label)
118+
}
119+
120+
return host, rest, nil
121+
}
122+
123+
// Represents a valid label in a hostname. For example, wobble in `wib-ble.wobble.com`.
124+
type label string
125+
126+
func parseLabel(rest string) (label, string, error) {
127+
if rest == "" {
128+
return "", "", errors.New("expected label, got empty string")
129+
}
130+
131+
// First try to get a valid leading char. Leading char in a label cannot be a hyphen.
132+
if !isValidLabelChar(rest[0]) || rest[0] == '-' {
133+
return "", "", fmt.Errorf("could not pull label from front of string: %s", rest)
134+
}
135+
136+
// Go until the next character is not a valid char
137+
var i int
138+
for i = 1; i < len(rest) && isValidLabelChar(rest[i]); i += 1 {
139+
}
140+
141+
// Final char in a label cannot be a hyphen.
142+
if rest[i-1] == '-' {
143+
return "", "", fmt.Errorf("invalid label: %s", rest[:i])
144+
}
145+
146+
return label(rest[:i]), rest[i:], nil
147+
}
148+
149+
func isValidLabelChar(c byte) bool {
150+
switch {
151+
// Alpha numeric is fine.
152+
case c >= 'A' && c <= 'Z':
153+
return true
154+
case c >= 'a' && c <= 'z':
155+
return true
156+
case c >= '0' && c <= '9':
157+
return true
158+
159+
// Hyphens are good
160+
case c == '-':
161+
return true
162+
163+
default:
164+
return false
165+
}
166+
}
167+
84168
func parseAllowRule(string) (Rule, error) {
85169
return Rule{}, nil
86170
}

rules/rules_test.go

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,301 @@ func TestParseHTTPToken(t *testing.T) {
129129
})
130130
}
131131
}
132+
133+
func TestParseHost(t *testing.T) {
134+
tests := []struct {
135+
name string
136+
input string
137+
expectedHost host
138+
expectedRest string
139+
expectError bool
140+
}{
141+
{
142+
name: "empty string",
143+
input: "",
144+
expectedHost: nil,
145+
expectedRest: "",
146+
expectError: true,
147+
},
148+
{
149+
name: "simple domain",
150+
input: "google.com",
151+
expectedHost: host{label("google"), label("com")},
152+
expectedRest: "",
153+
expectError: false,
154+
},
155+
{
156+
name: "subdomain",
157+
input: "api.google.com",
158+
expectedHost: host{label("api"), label("google"), label("com")},
159+
expectedRest: "",
160+
expectError: false,
161+
},
162+
{
163+
name: "single label",
164+
input: "localhost",
165+
expectedHost: host{label("localhost")},
166+
expectedRest: "",
167+
expectError: false,
168+
},
169+
{
170+
name: "domain with trailing content",
171+
input: "example.org/path",
172+
expectedHost: host{label("example"), label("org")},
173+
expectedRest: "/path",
174+
expectError: false,
175+
},
176+
{
177+
name: "domain with port",
178+
input: "localhost:8080",
179+
expectedHost: host{label("localhost")},
180+
expectedRest: ":8080",
181+
expectError: false,
182+
},
183+
{
184+
name: "numeric labels",
185+
input: "192.168.1.1",
186+
expectedHost: host{label("192"), label("168"), label("1"), label("1")},
187+
expectedRest: "",
188+
expectError: false,
189+
},
190+
{
191+
name: "hyphenated domain",
192+
input: "my-site.example-domain.co.uk",
193+
expectedHost: host{label("my-site"), label("example-domain"), label("co"), label("uk")},
194+
expectedRest: "",
195+
expectError: false,
196+
},
197+
{
198+
name: "alphanumeric labels",
199+
input: "a1b2c3.test123.com",
200+
expectedHost: host{label("a1b2c3"), label("test123"), label("com")},
201+
expectedRest: "",
202+
expectError: false,
203+
},
204+
{
205+
name: "starts with hyphen",
206+
input: "-invalid.com",
207+
expectedHost: nil,
208+
expectedRest: "",
209+
expectError: true,
210+
},
211+
{
212+
name: "ends with hyphen",
213+
input: "invalid-.com",
214+
expectedHost: nil,
215+
expectedRest: "",
216+
expectError: true,
217+
},
218+
{
219+
name: "label ends with hyphen",
220+
input: "test.invalid-.com",
221+
expectedHost: nil,
222+
expectedRest: "",
223+
expectError: true,
224+
},
225+
{
226+
name: "invalid character",
227+
228+
expectedHost: host{label("test")},
229+
expectedRest: "@example.com",
230+
expectError: false,
231+
},
232+
{
233+
name: "empty label",
234+
input: "test..com",
235+
expectedHost: nil,
236+
expectedRest: "",
237+
expectError: true,
238+
},
239+
{
240+
name: "trailing dot",
241+
input: "example.com.",
242+
expectedHost: nil,
243+
expectedRest: "",
244+
expectError: true,
245+
},
246+
{
247+
name: "single character labels",
248+
input: "a.b.c",
249+
expectedHost: host{label("a"), label("b"), label("c")},
250+
expectedRest: "",
251+
expectError: false,
252+
},
253+
{
254+
name: "mixed case",
255+
input: "Example.COM",
256+
expectedHost: host{label("Example"), label("COM")},
257+
expectedRest: "",
258+
expectError: false,
259+
},
260+
}
261+
262+
for _, tt := range tests {
263+
t.Run(tt.name, func(t *testing.T) {
264+
hostResult, rest, err := parseHost(tt.input)
265+
266+
if tt.expectError {
267+
if err == nil {
268+
t.Errorf("expected error but got none")
269+
}
270+
return
271+
}
272+
273+
if err != nil {
274+
t.Errorf("unexpected error: %v", err)
275+
return
276+
}
277+
278+
if len(hostResult) != len(tt.expectedHost) {
279+
t.Errorf("expected host length %d, got %d", len(tt.expectedHost), len(hostResult))
280+
return
281+
}
282+
283+
for i, expectedLabel := range tt.expectedHost {
284+
if hostResult[i] != expectedLabel {
285+
t.Errorf("expected label[%d] %q, got %q", i, expectedLabel, hostResult[i])
286+
}
287+
}
288+
289+
if rest != tt.expectedRest {
290+
t.Errorf("expected remaining %q, got %q", tt.expectedRest, rest)
291+
}
292+
})
293+
}
294+
}
295+
296+
func TestParseLabel(t *testing.T) {
297+
tests := []struct {
298+
name string
299+
input string
300+
expectedLabel label
301+
expectedRest string
302+
expectError bool
303+
}{
304+
{
305+
name: "empty string",
306+
input: "",
307+
expectedLabel: "",
308+
expectedRest: "",
309+
expectError: true,
310+
},
311+
{
312+
name: "simple label",
313+
input: "test",
314+
expectedLabel: "test",
315+
expectedRest: "",
316+
expectError: false,
317+
},
318+
{
319+
name: "label with dot",
320+
input: "test.com",
321+
expectedLabel: "test",
322+
expectedRest: ".com",
323+
expectError: false,
324+
},
325+
{
326+
name: "label with hyphen",
327+
input: "my-site",
328+
expectedLabel: "my-site",
329+
expectedRest: "",
330+
expectError: false,
331+
},
332+
{
333+
name: "alphanumeric label",
334+
input: "test123",
335+
expectedLabel: "test123",
336+
expectedRest: "",
337+
expectError: false,
338+
},
339+
{
340+
name: "starts with hyphen",
341+
input: "-invalid",
342+
expectedLabel: "",
343+
expectedRest: "",
344+
expectError: true,
345+
},
346+
{
347+
name: "ends with hyphen",
348+
input: "invalid-",
349+
expectedLabel: "",
350+
expectedRest: "",
351+
expectError: true,
352+
},
353+
{
354+
name: "ends with hyphen followed by dot",
355+
input: "invalid-.com",
356+
expectedLabel: "",
357+
expectedRest: "",
358+
expectError: true,
359+
},
360+
{
361+
name: "single character",
362+
input: "a",
363+
expectedLabel: "a",
364+
expectedRest: "",
365+
expectError: false,
366+
},
367+
{
368+
name: "numeric label",
369+
input: "123",
370+
expectedLabel: "123",
371+
expectedRest: "",
372+
expectError: false,
373+
},
374+
{
375+
name: "mixed case",
376+
input: "Test",
377+
expectedLabel: "Test",
378+
expectedRest: "",
379+
expectError: false,
380+
},
381+
{
382+
name: "invalid character",
383+
input: "test@invalid",
384+
expectedLabel: "test",
385+
expectedRest: "@invalid",
386+
expectError: false,
387+
},
388+
{
389+
name: "starts with number",
390+
input: "1test",
391+
expectedLabel: "1test",
392+
expectedRest: "",
393+
expectError: false,
394+
},
395+
{
396+
name: "label with trailing slash",
397+
input: "api/path",
398+
expectedLabel: "api",
399+
expectedRest: "/path",
400+
expectError: false,
401+
},
402+
}
403+
404+
for _, tt := range tests {
405+
t.Run(tt.name, func(t *testing.T) {
406+
labelResult, rest, err := parseLabel(tt.input)
407+
408+
if tt.expectError {
409+
if err == nil {
410+
t.Errorf("expected error but got none")
411+
}
412+
return
413+
}
414+
415+
if err != nil {
416+
t.Errorf("unexpected error: %v", err)
417+
return
418+
}
419+
420+
if labelResult != tt.expectedLabel {
421+
t.Errorf("expected label %q, got %q", tt.expectedLabel, labelResult)
422+
}
423+
424+
if rest != tt.expectedRest {
425+
t.Errorf("expected remaining %q, got %q", tt.expectedRest, rest)
426+
}
427+
})
428+
}
429+
}

0 commit comments

Comments
 (0)