@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 1,244 lines 28.3 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 caddyfile
16
17 import (
18 "bytes"
19 "os"
20 "path/filepath"
21 "runtime"
22 "strings"
23 "testing"
24 )
25
26 func TestParseVariadic(t *testing.T) {
27 args := make([]string, 10)
28 for i, tc := range []struct {
29 input string
30 result bool
31 }{
32 {
33 input: "",
34 result: false,
35 },
36 {
37 input: "{args[1",
38 result: false,
39 },
40 {
41 input: "1]}",
42 result: false,
43 },
44 {
45 input: "{args[:]}aaaaa",
46 result: false,
47 },
48 {
49 input: "aaaaa{args[:]}",
50 result: false,
51 },
52 {
53 input: "{args.}",
54 result: false,
55 },
56 {
57 input: "{args.1}",
58 result: false,
59 },
60 {
61 input: "{args[]}",
62 result: false,
63 },
64 {
65 input: "{args[:]}",
66 result: true,
67 },
68 {
69 input: "{args[:]}",
70 result: true,
71 },
72 {
73 input: "{args[0:]}",
74 result: true,
75 },
76 {
77 input: "{args[:0]}",
78 result: true,
79 },
80 {
81 input: "{args[-1:]}",
82 result: false,
83 },
84 {
85 input: "{args[:11]}",
86 result: false,
87 },
88 {
89 input: "{args[10:0]}",
90 result: false,
91 },
92 {
93 input: "{args[0:10]}",
94 result: true,
95 },
96 {
97 input: "{args[0]}:{args[1]}:{args[2]}",
98 result: false,
99 },
100 } {
101 token := Token{
102 File: "test",
103 Line: 1,
104 Text: tc.input,
105 }
106 if v, _, _ := parseVariadic(token, len(args)); v != tc.result {
107 t.Errorf("Test %d error expectation failed Expected: %t, got %t", i, tc.result, v)
108 }
109 }
110 }
111
112 func TestAllTokens(t *testing.T) {
113 input := []byte("a b c\nd e")
114 expected := []string{"a", "b", "c", "d", "e"}
115 tokens, err := allTokens("TestAllTokens", input)
116 if err != nil {
117 t.Fatalf("Expected no error, got %v", err)
118 }
119 if len(tokens) != len(expected) {
120 t.Fatalf("Expected %d tokens, got %d", len(expected), len(tokens))
121 }
122
123 for i, val := range expected {
124 if tokens[i].Text != val {
125 t.Errorf("Token %d should be '%s' but was '%s'", i, val, tokens[i].Text)
126 }
127 }
128 }
129
130 func TestParseOneAndImport(t *testing.T) {
131 testParseOne := func(input string) (ServerBlock, error) {
132 p := testParser(input)
133 p.Next() // parseOne doesn't call Next() to start, so we must
134 err := p.parseOne()
135 return p.block, err
136 }
137
138 for i, test := range []struct {
139 input string
140 shouldErr bool
141 keys []string
142 numTokens []int // number of tokens to expect in each segment
143 }{
144 {`localhost`, false, []string{
145 "localhost",
146 }, []int{}},
147
148 {`localhost
149 dir1`, false, []string{
150 "localhost",
151 }, []int{1}},
152
153 {
154 `localhost:1234
155 dir1 foo bar`, false, []string{
156 "localhost:1234",
157 }, []int{3},
158 },
159
160 {`localhost {
161 dir1
162 }`, false, []string{
163 "localhost",
164 }, []int{1}},
165
166 {`localhost:1234 {
167 dir1 foo bar
168 dir2
169 }`, false, []string{
170 "localhost:1234",
171 }, []int{3, 1}},
172
173 {`http://localhost https://localhost
174 dir1 foo bar`, false, []string{
175 "http://localhost",
176 "https://localhost",
177 }, []int{3}},
178
179 {`http://localhost https://localhost {
180 dir1 foo bar
181 }`, false, []string{
182 "http://localhost",
183 "https://localhost",
184 }, []int{3}},
185
186 {`http://localhost, https://localhost {
187 dir1 foo bar
188 }`, false, []string{
189 "http://localhost",
190 "https://localhost",
191 }, []int{3}},
192
193 {`http://localhost, {
194 }`, true, []string{
195 "http://localhost",
196 }, []int{}},
197
198 {`host1:80, http://host2.com
199 dir1 foo bar
200 dir2 baz`, false, []string{
201 "host1:80",
202 "http://host2.com",
203 }, []int{3, 2}},
204
205 {`http://host1.com,
206 http://host2.com,
207 https://host3.com`, false, []string{
208 "http://host1.com",
209 "http://host2.com",
210 "https://host3.com",
211 }, []int{}},
212
213 {`http://host1.com:1234, https://host2.com
214 dir1 foo {
215 bar baz
216 }
217 dir2`, false, []string{
218 "http://host1.com:1234",
219 "https://host2.com",
220 }, []int{6, 1}},
221
222 {`127.0.0.1
223 dir1 {
224 bar baz
225 }
226 dir2 {
227 foo bar
228 }`, false, []string{
229 "127.0.0.1",
230 }, []int{5, 5}},
231
232 {`localhost
233 dir1 {
234 foo`, true, []string{
235 "localhost",
236 }, []int{3}},
237
238 {`localhost
239 dir1 {
240 }`, false, []string{
241 "localhost",
242 }, []int{3}},
243
244 {`localhost
245 dir1 {
246 } }`, true, []string{
247 "localhost",
248 }, []int{}},
249
250 {`localhost{
251 dir1
252 }`, true, []string{}, []int{}},
253
254 {`localhost
255 dir1 {
256 nested {
257 foo
258 }
259 }
260 dir2 foo bar`, false, []string{
261 "localhost",
262 }, []int{7, 3}},
263
264 {``, false, []string{}, []int{}},
265
266 {`localhost
267 dir1 arg1
268 import testdata/import_test1.txt`, false, []string{
269 "localhost",
270 }, []int{2, 3, 1}},
271
272 {`import testdata/import_test2.txt`, false, []string{
273 "host1",
274 }, []int{1, 2}},
275
276 {`import testdata/not_found.txt`, true, []string{}, []int{}},
277
278 // empty file should just log a warning, and result in no tokens
279 {`import testdata/empty.txt`, false, []string{}, []int{}},
280
281 {`import testdata/only_white_space.txt`, false, []string{}, []int{}},
282
283 // import path/to/dir/* should skip any files that start with a . when iterating over them.
284 {`localhost
285 dir1 arg1
286 import testdata/glob/*`, false, []string{
287 "localhost",
288 }, []int{2, 3, 1}},
289
290 // import path/to/dir/.* should continue to read all dotfiles in a dir.
291 {`import testdata/glob/.*`, false, []string{
292 "host1",
293 }, []int{1, 2}},
294
295 {`""`, false, []string{}, []int{}},
296
297 {``, false, []string{}, []int{}},
298
299 // Unexpected next token after '{' on same line
300 {`localhost
301 dir1 { a b }`, true, []string{"localhost"}, []int{}},
302
303 // Unexpected '{' on a new line
304 {`localhost
305 dir1
306 {
307 a b
308 }`, true, []string{"localhost"}, []int{}},
309
310 // Workaround with quotes
311 {`localhost
312 dir1 "{" a b "}"`, false, []string{"localhost"}, []int{5}},
313
314 // Unexpected '{}' at end of line
315 {`localhost
316 dir1 {}`, true, []string{"localhost"}, []int{}},
317 // Workaround with quotes
318 {`localhost
319 dir1 "{}"`, false, []string{"localhost"}, []int{2}},
320
321 // quoted braces are literal arguments: they must not open/close blocks or swallow directives
322 {
323 "localhost {\n dir1 \"{\" `}`\n dir2 \"}\"\n dir3 \"{\"\n}",
324 false,
325 []string{"localhost"},
326 []int{3, 2, 2},
327 },
328
329 // quoted "{" as the last argument before a real block
330 {`localhost {
331 dir1 "{" {
332 a b
333 }
334 dir2 foo
335 }`, false, []string{"localhost"}, []int{6, 2}},
336
337 // import with args
338 {`import testdata/import_args0.txt a`, false, []string{"a"}, []int{}},
339 {`import testdata/import_args1.txt a b`, false, []string{"a", "b"}, []int{}},
340 {`import testdata/import_args*.txt a b`, false, []string{"a"}, []int{2}},
341
342 // test cases found by fuzzing!
343 {`import }{$"`, true, []string{}, []int{}},
344 {`import /*/*.txt`, true, []string{}, []int{}},
345 {`import /???/?*?o`, true, []string{}, []int{}},
346 {`import /??`, true, []string{}, []int{}},
347 {`import /[a-z]`, true, []string{}, []int{}},
348 {`import {$}`, true, []string{}, []int{}},
349 {`import {%}`, true, []string{}, []int{}},
350 {`import {$$}`, true, []string{}, []int{}},
351 {`import {%%}`, true, []string{}, []int{}},
352 } {
353 result, err := testParseOne(test.input)
354
355 if test.shouldErr && err == nil {
356 t.Errorf("Test %d: Expected an error, but didn't get one", i)
357 }
358 if !test.shouldErr && err != nil {
359 t.Errorf("Test %d: Expected no error, but got: %v", i, err)
360 }
361
362 // t.Logf("%+v\n", result)
363 if len(result.Keys) != len(test.keys) {
364 t.Errorf("Test %d: Expected %d keys, got %d",
365 i, len(test.keys), len(result.Keys))
366 continue
367 }
368 for j, addr := range result.GetKeysText() {
369 if addr != test.keys[j] {
370 t.Errorf("Test %d, key %d: Expected '%s', but was '%s'",
371 i, j, test.keys[j], addr)
372 }
373 }
374
375 if len(result.Segments) != len(test.numTokens) {
376 t.Errorf("Test %d: Expected %d segments, had %d",
377 i, len(test.numTokens), len(result.Segments))
378 continue
379 }
380
381 for j, seg := range result.Segments {
382 if len(seg) != test.numTokens[j] {
383 t.Errorf("Test %d, segment %d: Expected %d tokens, counted %d",
384 i, j, test.numTokens[j], len(seg))
385 continue
386 }
387 }
388 }
389 }
390
391 func TestRecursiveImport(t *testing.T) {
392 testParseOne := func(input string) (ServerBlock, error) {
393 p := testParser(input)
394 p.Next() // parseOne doesn't call Next() to start, so we must
395 err := p.parseOne()
396 return p.block, err
397 }
398
399 isExpected := func(got ServerBlock) bool {
400 textKeys := got.GetKeysText()
401 if len(textKeys) != 1 || textKeys[0] != "localhost" {
402 t.Errorf("got keys unexpected: expect localhost, got %v", textKeys)
403 return false
404 }
405 if len(got.Segments) != 2 {
406 t.Errorf("got wrong number of segments: expect 2, got %d", len(got.Segments))
407 return false
408 }
409 if len(got.Segments[0]) != 1 || len(got.Segments[1]) != 2 {
410 t.Errorf("got unexpected tokens: %v", got.Segments)
411 return false
412 }
413 return true
414 }
415
416 recursiveFile1, err := filepath.Abs("testdata/recursive_import_test1")
417 if err != nil {
418 t.Fatal(err)
419 }
420 recursiveFile2, err := filepath.Abs("testdata/recursive_import_test2")
421 if err != nil {
422 t.Fatal(err)
423 }
424
425 // test relative recursive import
426 err = os.WriteFile(recursiveFile1, []byte(
427 `localhost
428 dir1
429 import recursive_import_test2`,
430 ), 0o644)
431 if err != nil {
432 t.Fatal(err)
433 }
434 defer os.Remove(recursiveFile1)
435
436 err = os.WriteFile(recursiveFile2, []byte("dir2 1"), 0o644)
437 if err != nil {
438 t.Fatal(err)
439 }
440 defer os.Remove(recursiveFile2)
441
442 // import absolute path
443 result, err := testParseOne("import " + recursiveFile1)
444 if err != nil {
445 t.Fatal(err)
446 }
447 if !isExpected(result) {
448 t.Error("absolute+relative import failed")
449 }
450
451 // import relative path
452 result, err = testParseOne("import testdata/recursive_import_test1")
453 if err != nil {
454 t.Fatal(err)
455 }
456 if !isExpected(result) {
457 t.Error("relative+relative import failed")
458 }
459
460 // test absolute recursive import
461 err = os.WriteFile(recursiveFile1, []byte(
462 `localhost
463 dir1
464 import `+recursiveFile2,
465 ), 0o644)
466 if err != nil {
467 t.Fatal(err)
468 }
469
470 // import absolute path
471 result, err = testParseOne("import " + recursiveFile1)
472 if err != nil {
473 t.Fatal(err)
474 }
475 if !isExpected(result) {
476 t.Error("absolute+absolute import failed")
477 }
478
479 // import relative path
480 result, err = testParseOne("import testdata/recursive_import_test1")
481 if err != nil {
482 t.Fatal(err)
483 }
484 if !isExpected(result) {
485 t.Error("relative+absolute import failed")
486 }
487 }
488
489 // TestParseImportInaccessibleFile ensures that an import path which exists
490 // but cannot be accessed reports the underlying OS error instead of the
491 // misleading "File to import not found" (issue #8161). A path through a
492 // regular file fails stat with ENOTDIR, unlike a genuinely missing file,
493 // which must keep the "not found" message. Windows reports that same path
494 // as not existing instead, so the through-a-file case is skipped there;
495 // the permission-denied case from the issue is unaffected.
496 func TestParseImportInaccessibleFile(t *testing.T) {
497 testParseOne := func(input string) (ServerBlock, error) {
498 p := testParser(input)
499 p.Next() // parseOne doesn't call Next() to start, so we must
500 err := p.parseOne()
501 return p.block, err
502 }
503
504 if runtime.GOOS != "windows" {
505 _, err := testParseOne("import testdata/import_test1.txt/child.txt")
506 if err == nil {
507 t.Fatal("expected error importing through a regular file, got nil")
508 }
509 if strings.Contains(err.Error(), "not found") {
510 t.Errorf("expected OS error to be reported, got: %v", err)
511 }
512 if !strings.Contains(err.Error(), "child.txt") {
513 t.Errorf("expected error to name the import path, got: %v", err)
514 }
515 }
516
517 _, err := testParseOne("import testdata/not_found.txt")
518 if err == nil {
519 t.Fatal("expected error importing a missing file, got nil")
520 }
521 if !strings.Contains(err.Error(), "File to import not found") {
522 t.Errorf("expected 'File to import not found' for a missing file, got: %v", err)
523 }
524 }
525
526 func TestDirectiveImport(t *testing.T) {
527 testParseOne := func(input string) (ServerBlock, error) {
528 p := testParser(input)
529 p.Next() // parseOne doesn't call Next() to start, so we must
530 err := p.parseOne()
531 return p.block, err
532 }
533
534 isExpected := func(got ServerBlock) bool {
535 textKeys := got.GetKeysText()
536 if len(textKeys) != 1 || textKeys[0] != "localhost" {
537 t.Errorf("got keys unexpected: expect localhost, got %v", textKeys)
538 return false
539 }
540 if len(got.Segments) != 2 {
541 t.Errorf("got wrong number of segments: expect 2, got %d", len(got.Segments))
542 return false
543 }
544 if len(got.Segments[0]) != 1 || len(got.Segments[1]) != 8 {
545 t.Errorf("got unexpected tokens: %v", got.Segments)
546 return false
547 }
548 return true
549 }
550
551 directiveFile, err := filepath.Abs("testdata/directive_import_test")
552 if err != nil {
553 t.Fatal(err)
554 }
555
556 err = os.WriteFile(directiveFile, []byte(`prop1 1
557 prop2 2`), 0o644)
558 if err != nil {
559 t.Fatal(err)
560 }
561 defer os.Remove(directiveFile)
562
563 // import from existing file
564 result, err := testParseOne(`localhost
565 dir1
566 proxy {
567 import testdata/directive_import_test
568 transparent
569 }`)
570 if err != nil {
571 t.Fatal(err)
572 }
573 if !isExpected(result) {
574 t.Error("directive import failed")
575 }
576
577 // import from nonexistent file
578 _, err = testParseOne(`localhost
579 dir1
580 proxy {
581 import testdata/nonexistent_file
582 transparent
583 }`)
584 if err == nil {
585 t.Fatal("expected error when importing a nonexistent file")
586 }
587 }
588
589 func TestParseAll(t *testing.T) {
590 for i, test := range []struct {
591 input string
592 shouldErr bool
593 keys [][]string // keys per server block, in order
594 }{
595 {`localhost`, false, [][]string{
596 {"localhost"},
597 }},
598
599 {`localhost:1234`, false, [][]string{
600 {"localhost:1234"},
601 }},
602
603 {`localhost:1234 {
604 }
605 localhost:2015 {
606 }`, false, [][]string{
607 {"localhost:1234"},
608 {"localhost:2015"},
609 }},
610
611 {`localhost:1234, http://host2`, false, [][]string{
612 {"localhost:1234", "http://host2"},
613 }},
614
615 {`foo.example.com , example.com`, false, [][]string{
616 {"foo.example.com", "example.com"},
617 }},
618
619 {`localhost:1234, http://host2,`, true, [][]string{}},
620
621 {`http://host1.com, http://host2.com {
622 }
623 https://host3.com, https://host4.com {
624 }`, false, [][]string{
625 {"http://host1.com", "http://host2.com"},
626 {"https://host3.com", "https://host4.com"},
627 }},
628
629 {`import testdata/import_glob*.txt`, false, [][]string{
630 {"glob0.host0"},
631 {"glob0.host1"},
632 {"glob1.host0"},
633 {"glob2.host0"},
634 }},
635
636 {`import notfound/*`, false, [][]string{}}, // glob needn't error with no matches
637 {`import notfound/file.conf`, true, [][]string{}}, // but a specific file should
638
639 // recursive self-import
640 {`import testdata/import_recursive0.txt`, true, [][]string{}},
641 {`import testdata/import_recursive3.txt
642 import testdata/import_recursive1.txt`, true, [][]string{}},
643
644 // cyclic imports
645 {`(A) {
646 import A
647 }
648 :80
649 import A
650 `, true, [][]string{}},
651 {`(A) {
652 import B
653 }
654 (B) {
655 import A
656 }
657 :80
658 import A
659 `, true, [][]string{}},
660
661 // missing closing brace of the site block; the last directive
662 // having its own block must not mask it (issue #8374)
663 {`:8080 {
664 header {
665 X-Foo "bar"
666 }
667 `, true, [][]string{}},
668
669 // same, with the last directive's block written inline-nested
670 {`:8080 {
671 file_server {
672 browse
673 }
674 `, true, [][]string{}},
675
676 // unclosed global options block whose last directive has a block
677 {`{
678 servers {
679 protocols h1
680 }
681 `, true, [][]string{}},
682
683 // missing closing brace, last directive has no block
684 {`:8080 {
685 file_server
686 `, true, [][]string{}},
687
688 // extra closing brace after a site without braces (issue #8374)
689 {`:8080
690 file_server
691 }
692 `, true, [][]string{}},
693
694 // a closing brace must not separate two braceless sites
695 {`:8080
696 root /www/example.com
697 file_server
698 }
699
700 :3000
701 reverse_proxy localhost:9000
702 `, true, [][]string{}},
703
704 // valid controls: nested directive block as the last directive,
705 // site block closed afterwards
706 {`:8080 {
707 header {
708 X-Foo "bar"
709 }
710 }`, false, [][]string{
711 {":8080"},
712 }},
713
714 {`:8080 {
715 handle {
716 header {
717 X-Foo "bar"
718 }
719 }
720 }`, false, [][]string{
721 {":8080"},
722 }},
723 } {
724 p := testParser(test.input)
725 blocks, err := p.parseAll()
726
727 if test.shouldErr && err == nil {
728 t.Errorf("Test %d: Expected an error, but didn't get one", i)
729 }
730 if !test.shouldErr && err != nil {
731 t.Errorf("Test %d: Expected no error, but got: %v", i, err)
732 }
733
734 if len(blocks) != len(test.keys) {
735 t.Errorf("Test %d: Expected %d server blocks, got %d",
736 i, len(test.keys), len(blocks))
737 continue
738 }
739 for j, block := range blocks {
740 if len(block.Keys) != len(test.keys[j]) {
741 t.Errorf("Test %d: Expected %d keys in block %d, got %d: %v",
742 i, len(test.keys[j]), j, len(block.Keys), block.Keys)
743 continue
744 }
745 for k, addr := range block.GetKeysText() {
746 if addr != test.keys[j][k] {
747 t.Errorf("Test %d, block %d, key %d: Expected '%s', but got '%s'",
748 i, j, k, test.keys[j][k], addr)
749 }
750 }
751 }
752 }
753 }
754
755 func TestEnvironmentReplacement(t *testing.T) {
756 os.Setenv("FOOBAR", "foobar")
757 os.Setenv("CHAINED", "$FOOBAR")
758
759 for i, test := range []struct {
760 input string
761 expect string
762 }{
763 {
764 input: "",
765 expect: "",
766 },
767 {
768 input: "foo",
769 expect: "foo",
770 },
771 {
772 input: "{$NOT_SET}",
773 expect: "",
774 },
775 {
776 input: "foo{$NOT_SET}bar",
777 expect: "foobar",
778 },
779 {
780 input: "{$FOOBAR}",
781 expect: "foobar",
782 },
783 {
784 input: "foo {$FOOBAR} bar",
785 expect: "foo foobar bar",
786 },
787 {
788 input: "foo{$FOOBAR}bar",
789 expect: "foofoobarbar",
790 },
791 {
792 input: "foo\n{$FOOBAR}\nbar",
793 expect: "foo\nfoobar\nbar",
794 },
795 {
796 input: "{$FOOBAR} {$FOOBAR}",
797 expect: "foobar foobar",
798 },
799 {
800 input: "{$FOOBAR}{$FOOBAR}",
801 expect: "foobarfoobar",
802 },
803 {
804 input: "{$CHAINED}",
805 expect: "$FOOBAR", // should not chain env expands
806 },
807 {
808 input: "{$FOO:default}",
809 expect: "default",
810 },
811 {
812 input: "foo{$BAR:bar}baz",
813 expect: "foobarbaz",
814 },
815 {
816 input: "foo{$BAR:$FOOBAR}baz",
817 expect: "foo$FOOBARbaz", // should not chain env expands
818 },
819 {
820 input: "{$FOOBAR",
821 expect: "{$FOOBAR",
822 },
823 {
824 input: "{$LONGER_NAME $FOOBAR}",
825 expect: "",
826 },
827 {
828 input: "{$}",
829 expect: "{$}",
830 },
831 {
832 input: "{$$}",
833 expect: "",
834 },
835 {
836 input: "{$",
837 expect: "{$",
838 },
839 {
840 input: "}{$",
841 expect: "}{$",
842 },
843 } {
844 actual := replaceEnvVars([]byte(test.input))
845 if !bytes.Equal(actual, []byte(test.expect)) {
846 t.Errorf("Test %d: Expected: '%s' but got '%s'", i, test.expect, actual)
847 }
848 }
849 }
850
851 func TestImportReplacementInJSONWithBrace(t *testing.T) {
852 for i, test := range []struct {
853 args []string
854 input string
855 expect string
856 }{
857 {
858 args: []string{"123"},
859 input: "{args[0]}",
860 expect: "123",
861 },
862 {
863 args: []string{"123"},
864 input: `{"key":"{args[0]}"}`,
865 expect: `{"key":"123"}`,
866 },
867 {
868 args: []string{"123", "123"},
869 input: `{"key":[{args[0]},{args[1]}]}`,
870 expect: `{"key":[123,123]}`,
871 },
872 } {
873 repl := makeArgsReplacer(test.args)
874 actual := repl.ReplaceKnown(test.input, "")
875 if actual != test.expect {
876 t.Errorf("Test %d: Expected: '%s' but got '%s'", i, test.expect, actual)
877 }
878 }
879 }
880
881 func TestArgsReplacerOutOfBoundsIndex(t *testing.T) {
882 for i, test := range []struct {
883 args []string
884 input string
885 expect string
886 }{
887 {
888 args: []string{"a", "b"},
889 input: "{args[1]}",
890 expect: "b",
891 },
892 {
893 args: []string{"a", "b"},
894 input: "{args[2]}",
895 expect: "{args[2]}",
896 },
897 {
898 args: []string{"a", "b"},
899 input: "{args[-1]}",
900 expect: "{args[-1]}",
901 },
902 {
903 args: []string{"a", "b"},
904 input: "{args.-1}",
905 expect: "{args.-1}",
906 },
907 {
908 args: nil,
909 input: "{args[-1]}",
910 expect: "{args[-1]}",
911 },
912 } {
913 repl := makeArgsReplacer(test.args)
914 actual := repl.ReplaceKnown(test.input, "")
915 if actual != test.expect {
916 t.Errorf("Test %d: Expected: '%s' but got '%s'", i, test.expect, actual)
917 }
918 }
919 }
920
921 func TestSnippets(t *testing.T) {
922 p := testParser(`
923 (common) {
924 gzip foo
925 errors stderr
926 }
927 http://example.com {
928 import common
929 }
930 `)
931 blocks, err := p.parseAll()
932 if err != nil {
933 t.Fatal(err)
934 }
935 if len(blocks) != 1 {
936 t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
937 }
938 if actual, expected := blocks[0].GetKeysText()[0], "http://example.com"; expected != actual {
939 t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual)
940 }
941 if len(blocks[0].Segments) != 2 {
942 t.Fatalf("Server block should have tokens from import, got: %+v", blocks[0])
943 }
944 if actual, expected := blocks[0].Segments[0][0].Text, "gzip"; expected != actual {
945 t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
946 }
947 if actual, expected := blocks[0].Segments[1][1].Text, "stderr"; expected != actual {
948 t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
949 }
950 }
951
952 func TestSnippetWithQuotedBraces(t *testing.T) {
953 // quoted braces inside a snippet are literal arguments and must not corrupt block nesting
954 p := testParser(`
955 (quoted) {
956 dir1 "}"
957 dir2 "{"
958 }
959 example.com {
960 import quoted
961 dir3 foo
962 }
963 `)
964 blocks, err := p.parseAll()
965 if err != nil {
966 t.Fatal(err)
967 }
968 if len(blocks) != 1 {
969 t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
970 }
971 if actual := len(blocks[0].Segments); actual != 3 {
972 t.Fatalf("Expected 3 segments, got %d: %+v", actual, blocks[0].Segments)
973 }
974 for i, expected := range []string{"}", "{", "foo"} {
975 if seg := blocks[0].Segments[i]; len(seg) != 2 || seg[1].Text != expected {
976 t.Errorf("Segment %d: expected 2 tokens with arg '%s', got %+v", i, expected, seg)
977 }
978 }
979 }
980
981 func writeStringToTempFileOrDie(t *testing.T, str string) (pathToFile string) {
982 file, err := os.CreateTemp("", t.Name())
983 if err != nil {
984 panic(err) // get a stack trace so we know where this was called from.
985 }
986 t.Cleanup(func() {
987 if err := os.Remove(file.Name()); err != nil {
988 t.Logf("Failed to remove temp file %s: %v", file.Name(), err)
989 }
990 })
991 if _, err := file.WriteString(str); err != nil {
992 panic(err)
993 }
994 if err := file.Close(); err != nil {
995 panic(err)
996 }
997 return file.Name()
998 }
999
1000 func TestImportedFilesIgnoreNonDirectiveImportTokens(t *testing.T) {
1001 fileName := writeStringToTempFileOrDie(t, `
1002 http://example.com {
1003 # This isn't an import directive, it's just an arg with value 'import'
1004 basic_auth / import password
1005 }
1006 `)
1007 // Parse the root file that imports the other one.
1008 p := testParser(`import ` + fileName)
1009 blocks, err := p.parseAll()
1010 if err != nil {
1011 t.Fatal(err)
1012 }
1013 auth := blocks[0].Segments[0]
1014 line := auth[0].Text + " " + auth[1].Text + " " + auth[2].Text + " " + auth[3].Text
1015 if line != "basic_auth / import password" {
1016 // Previously, it would be changed to:
1017 // basic_auth / import /path/to/test/dir/password
1018 // referencing a file that (probably) doesn't exist and changing the
1019 // password!
1020 t.Errorf("Expected basic_auth tokens to be 'basic_auth / import password' but got %#q", line)
1021 }
1022 }
1023
1024 func TestSnippetAcrossMultipleFiles(t *testing.T) {
1025 // Make the derived Caddyfile that expects (common) to be defined.
1026 fileName := writeStringToTempFileOrDie(t, `
1027 http://example.com {
1028 import common
1029 }
1030 `)
1031
1032 // Parse the root file that defines (common) and then imports the other one.
1033 p := testParser(`
1034 (common) {
1035 gzip foo
1036 }
1037 import ` + fileName + `
1038 `)
1039
1040 blocks, err := p.parseAll()
1041 if err != nil {
1042 t.Fatal(err)
1043 }
1044 if len(blocks) != 1 {
1045 t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
1046 }
1047 if actual, expected := blocks[0].GetKeysText()[0], "http://example.com"; expected != actual {
1048 t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual)
1049 }
1050 if len(blocks[0].Segments) != 1 {
1051 t.Fatalf("Server block should have tokens from import")
1052 }
1053 if actual, expected := blocks[0].Segments[0][0].Text, "gzip"; expected != actual {
1054 t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
1055 }
1056 }
1057
1058 func TestRejectsGlobalMatcher(t *testing.T) {
1059 p := testParser(`
1060 @rejected path /foo
1061
1062 (common) {
1063 gzip foo
1064 errors stderr
1065 }
1066
1067 http://example.com {
1068 import common
1069 }
1070 `)
1071 _, err := p.parseAll()
1072 if err == nil {
1073 t.Fatal("Expected an error, but got nil")
1074 }
1075 expected := "request matchers may not be defined globally, they must be in a site block; found @rejected, at Testfile:2"
1076 if err.Error() != expected {
1077 t.Errorf("Expected error to be '%s' but got '%v'", expected, err)
1078 }
1079 }
1080
1081 func TestRejectAnonymousImportBlock(t *testing.T) {
1082 p := testParser(`
1083 (site) {
1084 http://{args[0]} https://{args[0]} {
1085 {block}
1086 }
1087 }
1088
1089 import site test.domain {
1090 {
1091 header_up Host {host}
1092 header_up X-Real-IP {remote_host}
1093 }
1094 }
1095 `)
1096 _, err := p.parseAll()
1097 if err == nil {
1098 t.Fatal("Expected an error, but got nil")
1099 }
1100 expected := "anonymous blocks are not supported"
1101 if !strings.HasPrefix(err.Error(), "anonymous blocks are not supported") {
1102 t.Errorf("Expected error to start with '%s' but got '%v'", expected, err)
1103 }
1104 }
1105
1106 func TestAcceptSiteImportWithBraces(t *testing.T) {
1107 p := testParser(`
1108 (site) {
1109 http://{args[0]} https://{args[0]} {
1110 {block}
1111 }
1112 }
1113
1114 import site test.domain {
1115 reverse_proxy http://192.168.1.1:8080 {
1116 header_up Host {host}
1117 }
1118 }
1119 `)
1120 _, err := p.parseAll()
1121 if err != nil {
1122 t.Errorf("Expected error to be nil but got '%v'", err)
1123 }
1124 }
1125
1126 func TestGlobalOptionsAfterImportedSnippetsGivesHelpfulError(t *testing.T) {
1127 tempDir := t.TempDir()
1128 importFile1 := filepath.Join(tempDir, "matcher_snippet_1.caddy")
1129 importFile2 := filepath.Join(tempDir, "matcher_snippet_2.caddy")
1130
1131 err := os.WriteFile(importFile1, []byte(`(matcher1)`), 0o644)
1132 if err != nil {
1133 t.Fatalf("writing first import file: %v", err)
1134 }
1135
1136 err = os.WriteFile(importFile2, []byte(`(matcher2)`), 0o644)
1137 if err != nil {
1138 t.Fatalf("writing second import file: %v", err)
1139 }
1140
1141 _, err = Parse("Testfile", []byte(`import `+importFile1+`
1142 import `+importFile2+`
1143 {
1144 debug
1145 }`))
1146 if err == nil {
1147 t.Fatal("Expected an error, but got nil")
1148 }
1149
1150 expected := "global options block must appear before import directives; move the global options block to the top of the Caddyfile"
1151 if !strings.HasPrefix(err.Error(), expected) {
1152 t.Errorf("Expected error to start with '%s' but got '%v'", expected, err)
1153 }
1154 }
1155
1156 func TestImportedSnippetDefinitionRetainsBlockPlaceholder(t *testing.T) {
1157 tempDir := t.TempDir()
1158 importFile := filepath.Join(tempDir, "snippets.caddy")
1159
1160 err := os.WriteFile(importFile, []byte(`
1161 (site) {
1162 http://{args[0]} {
1163 respond "before"
1164 {block}
1165 respond "after"
1166 }
1167 }
1168 `), 0o644)
1169 if err != nil {
1170 t.Fatalf("writing imported snippet file: %v", err)
1171 }
1172
1173 for _, tc := range []struct {
1174 name string
1175 input string
1176 expectedDirectives []string
1177 }{
1178 {
1179 name: "with nested block",
1180 input: `
1181 import ` + importFile + `
1182
1183 import site example.com {
1184 redir https://example.net
1185 }
1186 `,
1187 expectedDirectives: []string{"respond", "redir", "respond"},
1188 },
1189 {
1190 name: "without nested block",
1191 input: `
1192 import ` + importFile + `
1193
1194 import site example.com
1195 `,
1196 expectedDirectives: []string{"respond", "respond"},
1197 },
1198 } {
1199 t.Run(tc.name, func(t *testing.T) {
1200 p := testParser(tc.input)
1201 blocks, err := p.parseAll()
1202 if err != nil {
1203 t.Fatalf("parseAll: %v", err)
1204 }
1205
1206 if len(blocks) != 1 {
1207 t.Fatalf("expected exactly one server block, got %d", len(blocks))
1208 }
1209
1210 if actual := blocks[0].GetKeysText(); len(actual) != 1 || actual[0] != "http://example.com" {
1211 t.Fatalf("expected server block key http://example.com, got %v", actual)
1212 }
1213
1214 if len(blocks[0].Segments) != len(tc.expectedDirectives) {
1215 t.Fatalf("expected %d segments, got %d", len(tc.expectedDirectives), len(blocks[0].Segments))
1216 }
1217
1218 for i, directive := range tc.expectedDirectives {
1219 if actual := blocks[0].Segments[i].Directive(); actual != directive {
1220 t.Fatalf("segment %d: expected directive %q, got %q", i, directive, actual)
1221 }
1222 }
1223 })
1224 }
1225 }
1226
1227 func TestResolveImportGlob(t *testing.T) {
1228 dir := t.TempDir()
1229 os.WriteFile(filepath.Join(dir, "a.caddy"), []byte("a\n"), 0o600)
1230 os.WriteFile(filepath.Join(dir, "b.caddy"), []byte("b\n"), 0o600)
1231 os.WriteFile(filepath.Join(dir, ".hidden.caddy"), []byte("h\n"), 0o600)
1232 importer := filepath.Join(dir, "Caddyfile")
1233 matches, _, err := resolveImportGlob(importer, "*.caddy")
1234 if err != nil {
1235 t.Fatal(err)
1236 }
1237 if len(matches) != 2 { // dotfile skipped for glob
1238 t.Errorf("got %d matches, want 2: %v", len(matches), matches)
1239 }
1240 }
1241
1242 func testParser(input string) parser {
1243 return parser{Dispenser: NewTestDispenser(input)}
1244 }