@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 10, 22:30 UTC

master
go 686 lines 14.9 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package caddy
16
17 import (
18 "fmt"
19 "os"
20 "path/filepath"
21 "runtime"
22 "strings"
23 "sync"
24 "testing"
25 "time"
26 )
27
28 func TestReplacer(t *testing.T) {
29 type testCase struct {
30 input, expect, empty string
31 }
32
33 rep := testReplacer()
34
35 // ReplaceAll
36 for i, tc := range []testCase{
37 {
38 input: "{",
39 expect: "{",
40 },
41 {
42 input: `\{`,
43 expect: `{`,
44 },
45 {
46 input: "foo{",
47 expect: "foo{",
48 },
49 {
50 input: `foo\{`,
51 expect: `foo{`,
52 },
53 {
54 input: "foo{bar",
55 expect: "foo{bar",
56 },
57 {
58 input: `foo\{bar`,
59 expect: `foo{bar`,
60 },
61 {
62 input: "foo{bar}",
63 expect: "foo",
64 },
65 {
66 input: `foo\{bar\}`,
67 expect: `foo{bar}`,
68 },
69 {
70 input: "}",
71 expect: "}",
72 },
73 {
74 input: `\}`,
75 expect: `}`,
76 },
77 {
78 input: "{}",
79 expect: "",
80 },
81 {
82 input: `\{\}`,
83 expect: `{}`,
84 },
85 {
86 input: `{"json": "object"}`,
87 expect: "",
88 },
89 {
90 input: `\{"json": "object"}`,
91 expect: `{"json": "object"}`,
92 },
93 {
94 input: `\{"json": "object"\}`,
95 expect: `{"json": "object"}`,
96 },
97 {
98 input: `\{"json": "object{bar}"\}`,
99 expect: `{"json": "object"}`,
100 },
101 {
102 input: `\{"json": \{"nested": "object"\}\}`,
103 expect: `{"json": {"nested": "object"}}`,
104 },
105 {
106 input: `\{"json": \{"nested": "{bar}"\}\}`,
107 expect: `{"json": {"nested": ""}}`,
108 },
109 {
110 input: `pre \{"json": \{"nested": "{bar}"\}\}`,
111 expect: `pre {"json": {"nested": ""}}`,
112 },
113 {
114 input: `\{"json": \{"nested": "{bar}"\}\} post`,
115 expect: `{"json": {"nested": ""}} post`,
116 },
117 {
118 input: `pre \{"json": \{"nested": "{bar}"\}\} post`,
119 expect: `pre {"json": {"nested": ""}} post`,
120 },
121 {
122 input: `{{`,
123 expect: "{{",
124 },
125 {
126 input: `{{}`,
127 expect: "",
128 },
129 {
130 input: `{"json": "object"\}`,
131 expect: "",
132 },
133 {
134 input: `{unknown}`,
135 empty: "-",
136 expect: "-",
137 },
138 {
139 input: `back\slashes`,
140 expect: `back\slashes`,
141 },
142 {
143 input: `double back\\slashes`,
144 expect: `double back\\slashes`,
145 },
146 {
147 input: `placeholder {with \{ brace} in name`,
148 expect: `placeholder in name`,
149 },
150 {
151 input: `placeholder {with \} brace} in name`,
152 expect: `placeholder in name`,
153 },
154 {
155 input: `placeholder {with \} \} braces} in name`,
156 expect: `placeholder in name`,
157 },
158 {
159 input: `\{'group':'default','max_age':3600,'endpoints':[\{'url':'https://some.domain.local/a/d/g'\}],'include_subdomains':true\}`,
160 expect: `{'group':'default','max_age':3600,'endpoints':[{'url':'https://some.domain.local/a/d/g'}],'include_subdomains':true}`,
161 },
162 {
163 input: `{}{}{}{\\\\}\\\\`,
164 expect: `{\\\}\\\\`,
165 },
166 {
167 input: string([]byte{0x26, 0x00, 0x83, 0x7B, 0x84, 0x07, 0x5C, 0x7D, 0x84}),
168 expect: string([]byte{0x26, 0x00, 0x83, 0x7B, 0x84, 0x07, 0x7D, 0x84}),
169 },
170 {
171 input: `\\}`,
172 expect: `\}`,
173 },
174 } {
175 actual := rep.ReplaceAll(tc.input, tc.empty)
176 if actual != tc.expect {
177 t.Errorf("Test %d: '%s': expected '%s' but got '%s'",
178 i, tc.input, tc.expect, actual)
179 }
180 }
181 }
182
183 func TestReplacerSet(t *testing.T) {
184 rep := testReplacer()
185
186 for _, tc := range []struct {
187 variable string
188 value any
189 }{
190 {
191 variable: "test1",
192 value: "val1",
193 },
194 {
195 variable: "asdf",
196 value: "123",
197 },
198 {
199 variable: "numbers",
200 value: 123.456,
201 },
202 {
203 variable: "äöü",
204 value: "öö_äü",
205 },
206 {
207 variable: "with space",
208 value: "space value",
209 },
210 {
211 variable: "1",
212 value: "test-123",
213 },
214 {
215 variable: "mySuper_IP",
216 value: "1.2.3.4",
217 },
218 {
219 variable: "testEmpty",
220 value: "",
221 },
222 } {
223 rep.Set(tc.variable, tc.value)
224
225 // test if key is added
226 if val, ok := rep.static[tc.variable]; ok {
227 if val != tc.value {
228 t.Errorf("Expected value '%s' for key '%s' got '%s'", tc.value, tc.variable, val)
229 }
230 } else {
231 t.Errorf("Expected existing key '%s' found nothing", tc.variable)
232 }
233 }
234
235 // test if all keys are still there (by length)
236 length := len(rep.static)
237 if len(rep.static) != 8 {
238 t.Errorf("Expected length '%v' got '%v'", 7, length)
239 }
240 }
241
242 func TestReplacerReplaceKnown(t *testing.T) {
243 rep := Replacer{
244 mapMutex: &sync.RWMutex{},
245 providers: []replacementProvider{
246 // split our possible vars to two functions (to test if both functions are called)
247 ReplacerFunc(func(key string) (val any, ok bool) {
248 switch key {
249 case "test1":
250 return "val1", true
251 case "asdf":
252 return "123", true
253 case "äöü":
254 return "öö_äü", true
255 case "with space":
256 return "space value", true
257 default:
258 return "NOOO", false
259 }
260 }),
261 ReplacerFunc(func(key string) (val any, ok bool) {
262 switch key {
263 case "1":
264 return "test-123", true
265 case "mySuper_IP":
266 return "1.2.3.4", true
267 case "testEmpty":
268 return "", true
269 default:
270 return "NOOO", false
271 }
272 }),
273 },
274 }
275
276 for _, tc := range []struct {
277 testInput string
278 expected string
279 }{
280 {
281 // test vars without space
282 testInput: "{test1}{asdf}{äöü}{1}{with space}{mySuper_IP}",
283 expected: "val1123öö_äütest-123space value1.2.3.4",
284 },
285 {
286 // test vars with space
287 testInput: "{test1} {asdf} {äöü} {1} {with space} {mySuper_IP} ",
288 expected: "val1 123 öö_äü test-123 space value 1.2.3.4 ",
289 },
290 {
291 // test with empty val
292 testInput: "{test1} {testEmpty} {asdf} {1} ",
293 expected: "val1 EMPTY 123 test-123 ",
294 },
295 {
296 // test vars with not finished placeholders
297 testInput: "{te{test1}{as{{df{1}",
298 expected: "{teval1{as{{dftest-123",
299 },
300 {
301 // test with non existing vars
302 testInput: "{test1} {nope} {1} ",
303 expected: "val1 {nope} test-123 ",
304 },
305 } {
306 actual := rep.ReplaceKnown(tc.testInput, "EMPTY")
307
308 // test if all are replaced as expected
309 if actual != tc.expected {
310 t.Errorf("Expected '%s' got '%s' for '%s'", tc.expected, actual, tc.testInput)
311 }
312 }
313 }
314
315 func TestReplacerDelete(t *testing.T) {
316 rep := Replacer{
317 mapMutex: &sync.RWMutex{},
318 static: map[string]any{
319 "key1": "val1",
320 "key2": "val2",
321 "key3": "val3",
322 "key4": "val4",
323 },
324 }
325
326 startLen := len(rep.static)
327
328 toDel := []string{
329 "key2", "key4",
330 }
331
332 for _, key := range toDel {
333 rep.Delete(key)
334
335 // test if key is removed from static map
336 if _, ok := rep.static[key]; ok {
337 t.Errorf("Expected '%s' to be removed. It is still in static map.", key)
338 }
339 }
340
341 // check if static slice is smaller
342 expected := startLen - len(toDel)
343 actual := len(rep.static)
344 if len(rep.static) != expected {
345 t.Errorf("Expected length '%v' got length '%v'", expected, actual)
346 }
347 }
348
349 func TestReplacerMap(t *testing.T) {
350 rep := testReplacer()
351
352 for i, tc := range []ReplacerFunc{
353 func(key string) (val any, ok bool) {
354 return "", false
355 },
356 func(key string) (val any, ok bool) {
357 return "", false
358 },
359 } {
360 rep.Map(tc)
361
362 // test if function (which listens on specific key) is added by checking length
363 if len(rep.providers) == i+1 {
364 // check if the last function is the one we just added
365 pTc := fmt.Sprintf("%p", tc)
366 pRep := fmt.Sprintf("%p", rep.providers[i])
367 if pRep != pTc {
368 t.Errorf("Expected func pointer '%s' got '%s'", pTc, pRep)
369 }
370 } else {
371 t.Errorf("Expected providers length '%v' got length '%v'", i+1, len(rep.providers))
372 }
373 }
374 }
375
376 func TestReplacerNew(t *testing.T) {
377 repl := NewReplacer()
378
379 if len(repl.providers) != 3 {
380 t.Errorf("Expected providers length '%v' got length '%v'", 3, len(repl.providers))
381 }
382
383 // test if default global replacements are added as the first provider
384 hostname, _ := os.Hostname()
385 wd, _ := os.Getwd()
386 os.Setenv("CADDY_REPLACER_TEST", "envtest")
387 defer os.Setenv("CADDY_REPLACER_TEST", "")
388
389 for _, tc := range []struct {
390 variable string
391 value string
392 }{
393 {
394 variable: "system.hostname",
395 value: hostname,
396 },
397 {
398 variable: "system.slash",
399 value: string(filepath.Separator),
400 },
401 {
402 variable: "system.os",
403 value: runtime.GOOS,
404 },
405 {
406 variable: "system.arch",
407 value: runtime.GOARCH,
408 },
409 {
410 variable: "system.wd",
411 value: wd,
412 },
413 {
414 variable: "env.CADDY_REPLACER_TEST",
415 value: "envtest",
416 },
417 } {
418 if val, ok := repl.providers[0].replace(tc.variable); ok {
419 if val != tc.value {
420 t.Errorf("Expected value '%s' for key '%s' got '%s'", tc.value, tc.variable, val)
421 }
422 } else {
423 t.Errorf("Expected key '%s' to be recognized by first provider", tc.variable)
424 }
425 }
426
427 // test if file provider is added as the second provider
428 for _, tc := range []struct {
429 variable string
430 value string
431 }{
432 {
433 variable: "file.caddytest/integration/testdata/foo.txt",
434 value: "foo",
435 },
436 {
437 variable: "file.caddytest/integration/testdata/foo_with_trailing_newline.txt",
438 value: "foo",
439 },
440 {
441 variable: "file.caddytest/integration/testdata/foo_with_multiple_trailing_newlines.txt",
442 value: "foo" + getEOL(),
443 },
444 } {
445 if val, ok := repl.providers[1].replace(tc.variable); ok {
446 if val != tc.value {
447 t.Errorf("Expected value '%s' for key '%s' got '%s'", tc.value, tc.variable, val)
448 }
449 } else {
450 t.Errorf("Expected key '%s' to be recognized by second provider", tc.variable)
451 }
452 }
453 }
454
455 func getEOL() string {
456 if os.PathSeparator == '\\' {
457 return "\r\n" // Windows EOL
458 }
459 return "\n" // Unix and modern macOS EOL
460 }
461
462 func TestReplacerNewWithoutFile(t *testing.T) {
463 repl := NewReplacer().WithoutFile()
464
465 for _, tc := range []struct {
466 variable string
467 value string
468 notFound bool
469 }{
470 {
471 variable: "file.caddytest/integration/testdata/foo.txt",
472 notFound: true,
473 },
474 {
475 variable: "system.os",
476 value: runtime.GOOS,
477 },
478 } {
479 if val, ok := repl.Get(tc.variable); ok && !tc.notFound {
480 if val != tc.value {
481 t.Errorf("Expected value '%s' for key '%s' got '%s'", tc.value, tc.variable, val)
482 }
483 } else if !tc.notFound {
484 t.Errorf("Expected key '%s' to be recognized", tc.variable)
485 }
486 }
487 }
488
489 func TestReplacerTimeNowRFC3339(t *testing.T) {
490 originalNowFunc := nowFunc
491 t.Cleanup(func() { nowFunc = originalNowFunc })
492
493 for _, tc := range []struct {
494 name string
495 now time.Time
496 want string
497 }{
498 {
499 name: "UTC",
500 now: time.Date(2026, time.October, 3, 4, 30, 0, 123456789, time.UTC),
501 want: "2026-10-03T04:30:00Z",
502 },
503 {
504 name: "numeric offset",
505 now: time.Date(2026, time.October, 3, 10, 15, 0, 123456789, time.FixedZone("NPT", 5*60*60+45*60)),
506 want: "2026-10-03T10:15:00+05:45",
507 },
508 } {
509 t.Run(tc.name, func(t *testing.T) {
510 nowFunc = func() time.Time { return tc.now }
511 got, ok := NewReplacer().GetString("time.now.rfc3339")
512 if !ok {
513 t.Fatal("time.now.rfc3339 placeholder was not found")
514 }
515 if got != tc.want {
516 t.Fatalf("time.now.rfc3339 = %q; want %q", got, tc.want)
517 }
518 })
519 }
520 }
521
522 func TestReplacerRepeatedFilePlaceholders(t *testing.T) {
523 dir := t.TempDir()
524 fileA := filepath.Join(dir, "a.txt")
525 fileB := filepath.Join(dir, "b.txt")
526 missing := filepath.Join(dir, "missing.txt")
527 if err := os.WriteFile(fileA, []byte("alpha\n"), 0o600); err != nil {
528 t.Fatal(err)
529 }
530 if err := os.WriteFile(fileB, []byte("beta"), 0o600); err != nil {
531 t.Fatal(err)
532 }
533 phA, phB, phMissing := "{file."+fileA+"}", "{file."+fileB+"}", "{file."+missing+"}"
534
535 for i, tc := range []struct {
536 repl func() *Replacer
537 known bool
538 input string
539 expected string
540 }{
541 {
542 repl: NewReplacer,
543 input: phA + "|" + phA + "|" + phB + "|" + phA,
544 expected: "alpha|alpha|beta|alpha",
545 },
546 {
547 repl: NewReplacer,
548 input: strings.Repeat(phA, 20000),
549 expected: strings.Repeat("alpha", 20000),
550 },
551 {
552 repl: NewReplacer,
553 known: true,
554 input: phMissing + "|" + phMissing,
555 expected: "EMPTY|EMPTY",
556 },
557 {
558 repl: NewReplacer,
559 known: false,
560 input: phMissing + "|" + phMissing,
561 expected: "EMPTY|EMPTY",
562 },
563 {
564 repl: NewEmptyReplacer,
565 known: true,
566 input: phMissing + "|" + phMissing,
567 expected: phMissing + "|" + phMissing,
568 },
569 {
570 repl: func() *Replacer { return NewReplacer().WithoutFile() },
571 known: true,
572 input: phA + "|" + phA,
573 expected: phA + "|" + phA,
574 },
575 } {
576 var actual string
577 if tc.known {
578 actual = tc.repl().ReplaceKnown(tc.input, "EMPTY")
579 } else {
580 actual = tc.repl().ReplaceAll(tc.input, "EMPTY")
581 }
582 if actual != tc.expected {
583 t.Errorf("Test %d: expected %.80q, got %.80q", i, tc.expected, actual)
584 }
585 }
586 }
587
588 func TestFileCacheEviction(t *testing.T) {
589 big := strings.Repeat("x", maxFileCacheBytes/2+1)
590 var loads int
591 load := func(key string) (any, bool) {
592 loads++
593 if key == "file.unknown" {
594 return nil, false
595 }
596 return key + big, true
597 }
598
599 var c fileCache
600 for i, tc := range []struct {
601 key string
602 found bool
603 wantLoads int
604 }{
605 {key: "file.a", found: true, wantLoads: 1},
606 {key: "file.a", found: true, wantLoads: 1},
607 {key: "file.b", found: true, wantLoads: 2}, // evicts file.a
608 {key: "file.b", found: true, wantLoads: 2},
609 {key: "file.a", found: true, wantLoads: 3},
610 {key: "file.unknown", found: false, wantLoads: 4},
611 {key: "file.unknown", found: false, wantLoads: 5},
612 } {
613 val, found := c.get(tc.key, load)
614 if found != tc.found {
615 t.Errorf("Test %d: expected found=%t, got %t", i, tc.found, found)
616 }
617 if tc.found && val != tc.key+big {
618 t.Errorf("Test %d: wrong value for %s", i, tc.key)
619 }
620 if loads != tc.wantLoads {
621 t.Errorf("Test %d: expected %d loads, got %d", i, tc.wantLoads, loads)
622 }
623 if c.size > maxFileCacheBytes {
624 t.Errorf("Test %d: cache holds %d bytes, over the %d limit", i, c.size, maxFileCacheBytes)
625 }
626 }
627 }
628
629 func BenchmarkReplacerRepeatedFile(b *testing.B) {
630 file := filepath.Join(b.TempDir(), "a.txt")
631 if err := os.WriteFile(file, []byte("alpha"), 0o600); err != nil {
632 b.Fatal(err)
633 }
634 input := strings.Repeat("{file."+file+"}", 1000)
635 rep := NewReplacer()
636 for b.Loop() {
637 rep.ReplaceAll(input, "")
638 }
639 }
640
641 func BenchmarkReplacer(b *testing.B) {
642 type testCase struct {
643 name, input, empty string
644 }
645
646 rep := testReplacer()
647 rep.Set("str", "a string")
648 rep.Set("int", 123.456)
649
650 for _, bm := range []testCase{
651 {
652 name: "no placeholder",
653 input: `simple string`,
654 },
655 {
656 name: "string replacement",
657 input: `str={str}`,
658 },
659 {
660 name: "int replacement",
661 input: `int={int}`,
662 },
663 {
664 name: "placeholder",
665 input: `{"json": "object"}`,
666 },
667 {
668 name: "escaped placeholder",
669 input: `\{"json": \{"nested": "{bar}"\}\}`,
670 },
671 } {
672 b.Run(bm.name, func(b *testing.B) {
673 for b.Loop() {
674 rep.ReplaceAll(bm.input, bm.empty)
675 }
676 })
677 }
678 }
679
680 func testReplacer() Replacer {
681 return Replacer{
682 providers: make([]replacementProvider, 0),
683 static: make(map[string]any),
684 mapMutex: &sync.RWMutex{},
685 }
686 }