@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
go 596 lines 17 KB
Raw
1 package logging
2
3 import (
4 "fmt"
5 "strings"
6 "testing"
7
8 "go.uber.org/zap/zapcore"
9
10 "github.com/caddyserver/caddy/v2"
11 "github.com/caddyserver/caddy/v2/internal"
12 )
13
14 func TestIPMaskSingleValue(t *testing.T) {
15 f := IPMaskFilter{IPv4MaskRaw: 16, IPv6MaskRaw: 32}
16 f.Provision(caddy.Context{})
17
18 out := f.Filter(zapcore.Field{String: "255.255.255.255"})
19 if out.String != "255.255.0.0" {
20 t.Fatalf("field has not been filtered: %s", out.String)
21 }
22
23 out = f.Filter(zapcore.Field{String: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"})
24 if out.String != "ffff:ffff::" {
25 t.Fatalf("field has not been filtered: %s", out.String)
26 }
27
28 out = f.Filter(zapcore.Field{String: "not-an-ip"})
29 if out.String != "not-an-ip" {
30 t.Fatalf("field has been filtered: %s", out.String)
31 }
32 }
33
34 func TestIPMaskInvalidCIDR(t *testing.T) {
35 for _, f := range []IPMaskFilter{
36 {IPv4MaskRaw: 33},
37 {IPv4MaskRaw: -1},
38 {IPv6MaskRaw: 129},
39 {IPv6MaskRaw: -8},
40 } {
41 if err := f.Provision(caddy.Context{}); err == nil {
42 t.Errorf("expected error for ipv4=%d ipv6=%d", f.IPv4MaskRaw, f.IPv6MaskRaw)
43 }
44 }
45
46 f := IPMaskFilter{IPv4MaskRaw: 32, IPv6MaskRaw: 128}
47 if err := f.Provision(caddy.Context{}); err != nil {
48 t.Fatalf("unexpected error: %v", err)
49 }
50 out := f.Filter(zapcore.Field{String: "1.2.3.4, 2001:db8::1"})
51 if out.String != "1.2.3.4, 2001:db8::1" {
52 t.Fatalf("field changed with full masks: %s", out.String)
53 }
54 }
55
56 func TestIPMaskCommaValue(t *testing.T) {
57 f := IPMaskFilter{IPv4MaskRaw: 16, IPv6MaskRaw: 32}
58 f.Provision(caddy.Context{})
59
60 out := f.Filter(zapcore.Field{String: "255.255.255.255, 244.244.244.244"})
61 if out.String != "255.255.0.0, 244.244.0.0" {
62 t.Fatalf("field has not been filtered: %s", out.String)
63 }
64
65 out = f.Filter(zapcore.Field{String: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff, ff00:ffff:ffff:ffff:ffff:ffff:ffff:ffff"})
66 if out.String != "ffff:ffff::, ff00:ffff::" {
67 t.Fatalf("field has not been filtered: %s", out.String)
68 }
69
70 out = f.Filter(zapcore.Field{String: "not-an-ip, 255.255.255.255"})
71 if out.String != "not-an-ip, 255.255.0.0" {
72 t.Fatalf("field has not been filtered: %s", out.String)
73 }
74 }
75
76 func TestIPMaskMultiValue(t *testing.T) {
77 f := IPMaskFilter{IPv4MaskRaw: 16, IPv6MaskRaw: 32}
78 f.Provision(caddy.Context{})
79
80 out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{
81 "255.255.255.255",
82 "244.244.244.244",
83 }})
84 arr, ok := out.Interface.(internal.LoggableStringArray)
85 if !ok {
86 t.Fatalf("field is wrong type: %T", out.Integer)
87 }
88 if arr[0] != "255.255.0.0" {
89 t.Fatalf("field entry 0 has not been filtered: %s", arr[0])
90 }
91 if arr[1] != "244.244.0.0" {
92 t.Fatalf("field entry 1 has not been filtered: %s", arr[1])
93 }
94
95 out = f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{
96 "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
97 "ff00:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
98 }})
99 arr, ok = out.Interface.(internal.LoggableStringArray)
100 if !ok {
101 t.Fatalf("field is wrong type: %T", out.Integer)
102 }
103 if arr[0] != "ffff:ffff::" {
104 t.Fatalf("field entry 0 has not been filtered: %s", arr[0])
105 }
106 if arr[1] != "ff00:ffff::" {
107 t.Fatalf("field entry 1 has not been filtered: %s", arr[1])
108 }
109 }
110
111 func TestQueryFilterSingleValue(t *testing.T) {
112 f := QueryFilter{[]queryFilterAction{
113 {replaceAction, "foo", "REDACTED"},
114 {replaceAction, "notexist", "REDACTED"},
115 {deleteAction, "bar", ""},
116 {deleteAction, "notexist", ""},
117 {hashAction, "hash", ""},
118 }}
119
120 if f.Validate() != nil {
121 t.Fatalf("the filter must be valid")
122 }
123
124 out := f.Filter(zapcore.Field{String: "/path?foo=a&foo=b&bar=c&bar=d&baz=e&hash=hashed"})
125 if out.String != "/path?baz=e&foo=REDACTED&foo=REDACTED&hash=1a06df82" {
126 t.Fatalf("query parameters have not been filtered: %s", out.String)
127 }
128 }
129
130 func TestQueryFilterHashInRequestURI(t *testing.T) {
131 f := QueryFilter{[]queryFilterAction{
132 {replaceAction, "token", "REDACTED"},
133 }}
134
135 // A request-target has no fragment, so '#' is part of the query.
136 for uri, expected := range map[string]string{
137 "/path?x=1#&token=secret": "/path?token=REDACTED&x=1%23",
138 "/path?token=secret#%zz": "/path",
139 } {
140 out := f.Filter(zapcore.Field{String: uri})
141 if out.String != expected {
142 t.Errorf("filtering %q: got %q, expected %q", uri, out.String, expected)
143 }
144 }
145 }
146
147 func TestQueryFilterMultiValue(t *testing.T) {
148 f := QueryFilter{
149 Actions: []queryFilterAction{
150 {Type: replaceAction, Parameter: "foo", Value: "REDACTED"},
151 {Type: replaceAction, Parameter: "notexist", Value: "REDACTED"},
152 {Type: deleteAction, Parameter: "bar"},
153 {Type: deleteAction, Parameter: "notexist"},
154 {Type: hashAction, Parameter: "hash"},
155 },
156 }
157
158 if f.Validate() != nil {
159 t.Fatalf("the filter must be valid")
160 }
161
162 out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{
163 "/path1?foo=a&foo=b&bar=c&bar=d&baz=e&hash=alpha",
164 "/path2?foo=c&foo=d&bar=e&bar=f&baz=g&hash=beta",
165 }})
166 arr, ok := out.Interface.(internal.LoggableStringArray)
167 if !ok {
168 t.Fatalf("field is wrong type: %T", out.Interface)
169 }
170
171 expected1 := "/path1?baz=e&foo=REDACTED&foo=REDACTED&hash=8ed3f6ad"
172 expected2 := "/path2?baz=g&foo=REDACTED&foo=REDACTED&hash=f44e64e7"
173 if arr[0] != expected1 {
174 t.Fatalf("query parameters in entry 0 have not been filtered correctly: got %s, expected %s", arr[0], expected1)
175 }
176 if arr[1] != expected2 {
177 t.Fatalf("query parameters in entry 1 have not been filtered correctly: got %s, expected %s", arr[1], expected2)
178 }
179 }
180
181 func TestValidateQueryFilter(t *testing.T) {
182 f := QueryFilter{[]queryFilterAction{
183 {},
184 }}
185 if f.Validate() == nil {
186 t.Fatalf("empty action type must be invalid")
187 }
188
189 f = QueryFilter{[]queryFilterAction{
190 {Type: "foo"},
191 }}
192 if f.Validate() == nil {
193 t.Fatalf("unknown action type must be invalid")
194 }
195 }
196
197 func TestCookieFilter(t *testing.T) {
198 f := CookieFilter{[]cookieFilterAction{
199 {replaceAction, "foo", "REDACTED"},
200 {deleteAction, "bar", ""},
201 {hashAction, "hash", ""},
202 }}
203
204 out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{
205 "foo=a; foo=b; bar=c; bar=d; baz=e; hash=hashed",
206 }})
207 outval := out.Interface.(internal.LoggableStringArray)
208 expected := internal.LoggableStringArray{
209 "foo=REDACTED; foo=REDACTED; baz=e; hash=1a06df82",
210 }
211 if outval[0] != expected[0] {
212 t.Fatalf("cookies have not been filtered: %s", out.String)
213 }
214 }
215
216 func TestValidateCookieFilter(t *testing.T) {
217 f := CookieFilter{[]cookieFilterAction{
218 {},
219 }}
220 if f.Validate() == nil {
221 t.Fatalf("empty action type must be invalid")
222 }
223
224 f = CookieFilter{[]cookieFilterAction{
225 {Type: "foo"},
226 }}
227 if f.Validate() == nil {
228 t.Fatalf("unknown action type must be invalid")
229 }
230 }
231
232 func TestSetCookieFilter(t *testing.T) {
233 f := SetCookieFilter{[]cookieFilterAction{
234 {replaceAction, "foo", "REDACTED"},
235 {deleteAction, "bar", ""},
236 {hashAction, "hash", ""},
237 }}
238
239 out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{
240 `foo="a"; Path=/; Priority=High`,
241 "bar=c; Path=/; Secure",
242 "hash=hashed; Path=/; HttpOnly",
243 "baz=e; Path=/; SameSite=Strict",
244 }})
245 outval := out.Interface.(internal.LoggableStringArray)
246 expected := internal.LoggableStringArray{
247 `foo="REDACTED"; Path=/; Priority=High`,
248 "hash=1a06df82; Path=/; HttpOnly",
249 "baz=e; Path=/; SameSite=Strict",
250 }
251 if len(outval) != len(expected) {
252 t.Fatalf("unexpected number of Set-Cookie values: got %d, want %d", len(outval), len(expected))
253 }
254 for i := range expected {
255 if outval[i] != expected[i] {
256 t.Fatalf("Set-Cookie entry %d has not been filtered correctly: got %q, want %q", i, outval[i], expected[i])
257 }
258 }
259 }
260
261 func TestSetCookieFilterInvalidHeaderPassesThrough(t *testing.T) {
262 f := SetCookieFilter{[]cookieFilterAction{
263 {hashAction, "session", ""},
264 }}
265
266 out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{
267 "not-a-cookie",
268 }})
269 outval := out.Interface.(internal.LoggableStringArray)
270 if outval[0] != "not-a-cookie" {
271 t.Fatalf("invalid Set-Cookie line should pass through unchanged: got %q", outval[0])
272 }
273 }
274
275 func TestSetCookieFilterHashWithoutAttributes(t *testing.T) {
276 f := SetCookieFilter{[]cookieFilterAction{
277 {hashAction, "session", ""},
278 }}
279
280 out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{
281 "session=secret",
282 }})
283 outval := out.Interface.(internal.LoggableStringArray)
284 expected := "session=2bb80d53"
285 if outval[0] != expected {
286 t.Fatalf("cookie value was not hashed as expected: got %q, want %q", outval[0], expected)
287 }
288 }
289
290 func TestSetCookieFilterCookieNameMatchingIsCaseSensitive(t *testing.T) {
291 f := SetCookieFilter{[]cookieFilterAction{
292 {hashAction, "session", ""},
293 }}
294
295 out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{
296 "Session=secret; Path=/",
297 }})
298 outval := out.Interface.(internal.LoggableStringArray)
299 expected := "Session=secret; Path=/"
300 if outval[0] != expected {
301 t.Fatalf("Set-Cookie value should remain unchanged when the cookie name does not match exactly: got %q, want %q", outval[0], expected)
302 }
303 }
304
305 func TestSetCookieFilterFirstMatchWins(t *testing.T) {
306 f := SetCookieFilter{[]cookieFilterAction{
307 {deleteAction, "session", ""},
308 {hashAction, "session", ""},
309 }}
310
311 out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{
312 "session=secret; Path=/",
313 }})
314 outval := out.Interface.(internal.LoggableStringArray)
315 if len(outval) != 0 {
316 t.Fatalf("expected cookie to be deleted by first matching action, got %q", outval)
317 }
318 }
319
320 func TestSetCookieFilterHashEmptyValue(t *testing.T) {
321 f := SetCookieFilter{[]cookieFilterAction{
322 {hashAction, "session", ""},
323 }}
324
325 out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{
326 "session=; Max-Age=0; Path=/; HttpOnly",
327 }})
328 outval := out.Interface.(internal.LoggableStringArray)
329 expected := "session=e3b0c442; Max-Age=0; Path=/; HttpOnly"
330 if outval[0] != expected {
331 t.Fatalf("empty cookie value was not hashed as expected: got %q, want %q", outval[0], expected)
332 }
333 }
334
335 func TestValidateSetCookieFilter(t *testing.T) {
336 f := SetCookieFilter{[]cookieFilterAction{
337 {},
338 }}
339 if f.Validate() == nil {
340 t.Fatalf("empty action type must be invalid")
341 }
342
343 f = SetCookieFilter{[]cookieFilterAction{
344 {Type: "foo"},
345 }}
346 if f.Validate() == nil {
347 t.Fatalf("unknown action type must be invalid")
348 }
349 }
350
351 func TestRegexpFilterSingleValue(t *testing.T) {
352 f := RegexpFilter{RawRegexp: `secret`, Value: "REDACTED"}
353 f.Provision(caddy.Context{})
354
355 out := f.Filter(zapcore.Field{String: "foo-secret-bar"})
356 if out.String != "foo-REDACTED-bar" {
357 t.Fatalf("field has not been filtered: %s", out.String)
358 }
359 }
360
361 func TestRegexpFilterMultiValue(t *testing.T) {
362 f := RegexpFilter{RawRegexp: `secret`, Value: "REDACTED"}
363 f.Provision(caddy.Context{})
364
365 out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{"foo-secret-bar", "bar-secret-foo"}})
366 arr, ok := out.Interface.(internal.LoggableStringArray)
367 if !ok {
368 t.Fatalf("field is wrong type: %T", out.Integer)
369 }
370 if arr[0] != "foo-REDACTED-bar" {
371 t.Fatalf("field entry 0 has not been filtered: %s", arr[0])
372 }
373 if arr[1] != "bar-REDACTED-foo" {
374 t.Fatalf("field entry 1 has not been filtered: %s", arr[1])
375 }
376 }
377
378 func TestHashFilterSingleValue(t *testing.T) {
379 f := HashFilter{}
380
381 out := f.Filter(zapcore.Field{String: "foo"})
382 if out.String != "2c26b46b" {
383 t.Fatalf("field has not been filtered: %s", out.String)
384 }
385 }
386
387 func TestHashFilterMultiValue(t *testing.T) {
388 f := HashFilter{}
389
390 out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{"foo", "bar"}})
391 arr, ok := out.Interface.(internal.LoggableStringArray)
392 if !ok {
393 t.Fatalf("field is wrong type: %T", out.Integer)
394 }
395 if arr[0] != "2c26b46b" {
396 t.Fatalf("field entry 0 has not been filtered: %s", arr[0])
397 }
398 if arr[1] != "fcde2b2e" {
399 t.Fatalf("field entry 1 has not been filtered: %s", arr[1])
400 }
401 }
402
403 func TestMultiRegexpFilterSingleOperation(t *testing.T) {
404 f := MultiRegexpFilter{
405 Operations: []regexpFilterOperation{
406 {RawRegexp: `secret`, Value: "REDACTED"},
407 },
408 }
409 err := f.Provision(caddy.Context{})
410 if err != nil {
411 t.Fatalf("unexpected error provisioning: %v", err)
412 }
413
414 out := f.Filter(zapcore.Field{String: "foo-secret-bar"})
415 if out.String != "foo-REDACTED-bar" {
416 t.Fatalf("field has not been filtered: %s", out.String)
417 }
418 }
419
420 func TestMultiRegexpFilterMultipleOperations(t *testing.T) {
421 f := MultiRegexpFilter{
422 Operations: []regexpFilterOperation{
423 {RawRegexp: `secret`, Value: "REDACTED"},
424 {RawRegexp: `password`, Value: "HIDDEN"},
425 {RawRegexp: `token`, Value: "XXX"},
426 },
427 }
428 err := f.Provision(caddy.Context{})
429 if err != nil {
430 t.Fatalf("unexpected error provisioning: %v", err)
431 }
432
433 // Test sequential application
434 out := f.Filter(zapcore.Field{String: "my-secret-password-token-data"})
435 expected := "my-REDACTED-HIDDEN-XXX-data"
436 if out.String != expected {
437 t.Fatalf("field has not been filtered correctly: got %s, expected %s", out.String, expected)
438 }
439 }
440
441 func TestMultiRegexpFilterMultiValue(t *testing.T) {
442 f := MultiRegexpFilter{
443 Operations: []regexpFilterOperation{
444 {RawRegexp: `secret`, Value: "REDACTED"},
445 {RawRegexp: `\d+`, Value: "NUM"},
446 },
447 }
448 err := f.Provision(caddy.Context{})
449 if err != nil {
450 t.Fatalf("unexpected error provisioning: %v", err)
451 }
452
453 out := f.Filter(zapcore.Field{Interface: internal.LoggableStringArray{
454 "foo-secret-123",
455 "bar-secret-456",
456 }})
457 arr, ok := out.Interface.(internal.LoggableStringArray)
458 if !ok {
459 t.Fatalf("field is wrong type: %T", out.Interface)
460 }
461 if arr[0] != "foo-REDACTED-NUM" {
462 t.Fatalf("field entry 0 has not been filtered: %s", arr[0])
463 }
464 if arr[1] != "bar-REDACTED-NUM" {
465 t.Fatalf("field entry 1 has not been filtered: %s", arr[1])
466 }
467 }
468
469 func TestMultiRegexpFilterAddOperation(t *testing.T) {
470 f := MultiRegexpFilter{}
471 err := f.AddOperation("secret", "REDACTED")
472 if err != nil {
473 t.Fatalf("unexpected error adding operation: %v", err)
474 }
475 err = f.AddOperation("password", "HIDDEN")
476 if err != nil {
477 t.Fatalf("unexpected error adding operation: %v", err)
478 }
479 err = f.Provision(caddy.Context{})
480 if err != nil {
481 t.Fatalf("unexpected error provisioning: %v", err)
482 }
483
484 if len(f.Operations) != 2 {
485 t.Fatalf("expected 2 operations, got %d", len(f.Operations))
486 }
487
488 out := f.Filter(zapcore.Field{String: "my-secret-password"})
489 expected := "my-REDACTED-HIDDEN"
490 if out.String != expected {
491 t.Fatalf("field has not been filtered correctly: got %s, expected %s", out.String, expected)
492 }
493 }
494
495 func TestMultiRegexpFilterSecurityLimits(t *testing.T) {
496 f := MultiRegexpFilter{}
497
498 // Test maximum operations limit
499 for i := 0; i < 51; i++ {
500 err := f.AddOperation(fmt.Sprintf("pattern%d", i), "replacement")
501 if i < 50 {
502 if err != nil {
503 t.Fatalf("unexpected error adding operation %d: %v", i, err)
504 }
505 } else {
506 if err == nil {
507 t.Fatalf("expected error when adding operation %d (exceeds limit)", i)
508 }
509 }
510 }
511
512 // Test empty pattern validation
513 f2 := MultiRegexpFilter{}
514 err := f2.AddOperation("", "replacement")
515 if err == nil {
516 t.Fatalf("expected error for empty pattern")
517 }
518
519 // Test pattern length limit
520 f3 := MultiRegexpFilter{}
521 longPattern := strings.Repeat("a", 1001)
522 err = f3.AddOperation(longPattern, "replacement")
523 if err == nil {
524 t.Fatalf("expected error for pattern exceeding length limit")
525 }
526 }
527
528 func TestMultiRegexpFilterValidation(t *testing.T) {
529 // Test validation with empty operations
530 f := MultiRegexpFilter{}
531 err := f.Validate()
532 if err == nil {
533 t.Fatalf("expected validation error for empty operations")
534 }
535
536 // Test validation with valid operations
537 err = f.AddOperation("valid", "replacement")
538 if err != nil {
539 t.Fatalf("unexpected error adding operation: %v", err)
540 }
541 err = f.Provision(caddy.Context{})
542 if err != nil {
543 t.Fatalf("unexpected error provisioning: %v", err)
544 }
545 err = f.Validate()
546 if err != nil {
547 t.Fatalf("unexpected validation error: %v", err)
548 }
549 }
550
551 func TestMultiRegexpFilterInputSizeLimit(t *testing.T) {
552 f := MultiRegexpFilter{
553 Operations: []regexpFilterOperation{
554 {RawRegexp: `test`, Value: "REPLACED"},
555 },
556 }
557 err := f.Provision(caddy.Context{})
558 if err != nil {
559 t.Fatalf("unexpected error provisioning: %v", err)
560 }
561
562 // Test with very large input (should be truncated)
563 largeInput := strings.Repeat("test", 300000) // Creates ~1.2MB string
564 out := f.Filter(zapcore.Field{String: largeInput})
565
566 // The input should be truncated to 1MB and still processed
567 if len(out.String) > 1000000 {
568 t.Fatalf("output string not truncated: length %d", len(out.String))
569 }
570
571 // Should still contain replacements within the truncated portion
572 if !strings.Contains(out.String, "REPLACED") {
573 t.Fatalf("replacements not applied to truncated input")
574 }
575 }
576
577 func TestMultiRegexpFilterOverlappingPatterns(t *testing.T) {
578 f := MultiRegexpFilter{
579 Operations: []regexpFilterOperation{
580 {RawRegexp: `secret.*password`, Value: "SENSITIVE"},
581 {RawRegexp: `password`, Value: "HIDDEN"},
582 },
583 }
584 err := f.Provision(caddy.Context{})
585 if err != nil {
586 t.Fatalf("unexpected error provisioning: %v", err)
587 }
588
589 // The first pattern should match and replace the entire "secret...password" portion
590 // Then the second pattern should not find "password" anymore since it was already replaced
591 out := f.Filter(zapcore.Field{String: "my-secret-data-password-end"})
592 expected := "my-SENSITIVE-end"
593 if out.String != expected {
594 t.Fatalf("field has not been filtered correctly: got %s, expected %s", out.String, expected)
595 }
596 }