@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 485 lines 15.2 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 "os"
19 "path/filepath"
20 "runtime"
21 "strings"
22 "testing"
23 )
24
25 func writeFile(t *testing.T, path, content string) {
26 t.Helper()
27 if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
28 t.Fatal(err)
29 }
30 }
31
32 func discoverImportedFiles(rootFile string, rootInput []byte) ([]string, error) {
33 rootPath, err := canonicalPath(rootFile)
34 if err != nil {
35 return nil, err
36 }
37 rootInfo, err := os.Stat(rootPath)
38 if err != nil {
39 return nil, err
40 }
41 files, err := discoverImportedFileContents(rootFile, rootInput, rootInfo)
42 if err != nil {
43 return nil, err
44 }
45 paths := make([]string, len(files))
46 for i, file := range files {
47 paths[i] = file.path
48 }
49 return paths, nil
50 }
51
52 func TestDiscoverImportedFiles(t *testing.T) {
53 dir := t.TempDir()
54 root := filepath.Join(dir, "Caddyfile")
55 writeFile(t, root, "import sites/*.caddy\nimport mysnip\n")
56 os.MkdirAll(filepath.Join(dir, "sites"), 0o755)
57 writeFile(t, filepath.Join(dir, "sites", "a.caddy"), "(mysnip) {\n\trespond 200\n}\nlocalhost {\n\timport mysnip\n}\n")
58 rootInput, _ := os.ReadFile(root)
59 files, err := discoverImportedFiles(root, rootInput)
60 if err != nil {
61 t.Fatal(err)
62 }
63 // sites/a.caddy is discovered; "mysnip" is a snippet (defined in a.caddy), not a file
64 wantPath, err := canonicalPath(filepath.Join(dir, "sites", "a.caddy"))
65 if err != nil {
66 t.Fatal(err)
67 }
68 want := []string{wantPath}
69 if len(files) != 1 || files[0] != want[0] {
70 t.Errorf("got %v, want %v", files, want)
71 }
72 }
73
74 func TestFormatImports(t *testing.T) {
75 dir := t.TempDir()
76 root := filepath.Join(dir, "Caddyfile")
77 writeFile(t, root, "import sites/a.caddy\n")
78 os.MkdirAll(filepath.Join(dir, "sites"), 0o755)
79 // deliberately messy imported file to prove it gets formatted at baseline 0
80 writeFile(t, filepath.Join(dir, "sites", "a.caddy"), "localhost {\nrespond 200\n}\n")
81 results, err := FormatImports(root, FormatOptions{})
82 if err != nil {
83 t.Fatal(err)
84 }
85 if len(results) != 2 {
86 t.Fatalf("got %d results, want 2", len(results))
87 }
88 // results[0] is the root; results[1] is the imported file, formatted
89 var imported *FormattedFile
90 for i := range results {
91 if strings.HasSuffix(results[i].Path, "a.caddy") {
92 imported = &results[i]
93 }
94 }
95 if imported == nil {
96 t.Fatal("imported file not in results")
97 }
98 want := "localhost {\n\trespond 200\n}\n"
99 if string(imported.Content) != want {
100 t.Errorf("imported formatted = %q, want %q", imported.Content, want)
101 }
102 }
103
104 func TestDiscoverIgnoresNonDirectiveImport(t *testing.T) {
105 dir := t.TempDir()
106 root := filepath.Join(dir, "Caddyfile")
107 writeFile(t, root, "localhost {\n\tbasic_auth / import password\n}\n")
108 rootInput, _ := os.ReadFile(root)
109 files, err := discoverImportedFiles(root, rootInput)
110 if err != nil {
111 t.Fatal(err)
112 }
113 if len(files) != 0 {
114 t.Errorf("got %v, want none ('import' here is an argument)", files)
115 }
116 }
117
118 func TestDiscoverImportBeforeSnippetDeclarationIsFile(t *testing.T) {
119 dir := t.TempDir()
120 root := filepath.Join(dir, "Caddyfile")
121 writeFile(t, root, "import foo\n\n(foo) {\n\trespond ok\n}\n")
122 writeFile(t, filepath.Join(dir, "foo"), "localhost {\n\trespond from-file\n}\n")
123
124 rootInput, err := os.ReadFile(root)
125 if err != nil {
126 t.Fatal(err)
127 }
128 files, err := discoverImportedFiles(root, rootInput)
129 if err != nil {
130 t.Fatal(err)
131 }
132 if len(files) != 1 || files[0] != mustCanonicalPath(t, filepath.Join(dir, "foo")) {
133 t.Fatalf("got %v, want foo file", files)
134 }
135 }
136
137 func TestDiscoverImportInsideNamedRoute(t *testing.T) {
138 dir := t.TempDir()
139 root := filepath.Join(dir, "Caddyfile")
140 writeFile(t, root, "&(foo) {\n\timport handler\n}\n\nlocalhost {\n\tinvoke foo\n}\n")
141 writeFile(t, filepath.Join(dir, "handler"), "respond \"hi\"\n")
142
143 rootInput, err := os.ReadFile(root)
144 if err != nil {
145 t.Fatal(err)
146 }
147 files, err := discoverImportedFiles(root, rootInput)
148 if err != nil {
149 t.Fatal(err)
150 }
151 // Named route blocks expand their imports through a separate parser; it must
152 // still report to the observer or --imports silently skips the file.
153 if len(files) != 1 || files[0] != mustCanonicalPath(t, filepath.Join(dir, "handler")) {
154 t.Fatalf("got %v, want handler file", files)
155 }
156 }
157
158 func TestDiscoverRecursiveImportArgs(t *testing.T) {
159 dir := t.TempDir()
160 root := filepath.Join(dir, "Caddyfile")
161 selector := filepath.Join(dir, "selector.caddy")
162 child := filepath.Join(dir, "child.caddy")
163 writeFile(t, root, "import selector.caddy child.caddy\n")
164 writeFile(t, selector, "import {args[0]}\n")
165 writeFile(t, child, "localhost {\n\trespond ok\n}\n")
166
167 results, err := FormatImports(root, FormatOptions{})
168 if err != nil {
169 t.Fatal(err)
170 }
171 want := []string{mustCanonicalPath(t, root), mustCanonicalPath(t, selector), mustCanonicalPath(t, child)}
172 if len(results) != len(want) {
173 t.Fatalf("got %d results, want %d: %v", len(results), len(want), results)
174 }
175 for i := range want {
176 if results[i].Path != want[i] {
177 t.Errorf("result %d path = %q, want %q", i, results[i].Path, want[i])
178 }
179 }
180 }
181
182 func TestDiscoverRepeatedImportArgs(t *testing.T) {
183 for _, tc := range []struct {
184 name string
185 root string
186 }{
187 {"file", "import selector.caddy a.caddy\nimport selector.caddy b.caddy\n"},
188 {"snippet", "(select) {\nimport selector.caddy {args[0]}\n}\nimport select a.caddy\nimport select b.caddy\n"},
189 } {
190 t.Run(tc.name, func(t *testing.T) {
191 dir := t.TempDir()
192 root := filepath.Join(dir, "Caddyfile")
193 selector := filepath.Join(dir, "selector.caddy")
194 a := filepath.Join(dir, "a.caddy")
195 b := filepath.Join(dir, "b.caddy")
196 writeFile(t, root, tc.root)
197 writeFile(t, selector, "import {args[0]}\n")
198 writeFile(t, a, "a.local {\nrespond a\n}\n")
199 writeFile(t, b, "b.local {\nrespond b\n}\n")
200
201 blocks, err := Parse(root, []byte(tc.root))
202 if err != nil || len(blocks) != 2 {
203 t.Fatalf("expected two valid server blocks, got %d: %v", len(blocks), err)
204 }
205 results, err := FormatImports(root, FormatOptions{})
206 if err != nil {
207 t.Fatal(err)
208 }
209 want := []string{root, selector, a, b}
210 if len(results) != len(want) {
211 t.Fatalf("got paths %v, want %v", resultPaths(results), want)
212 }
213 for i, path := range want {
214 if results[i].Path != mustCanonicalPath(t, path) {
215 t.Errorf("result %d path = %q, want %q", i, results[i].Path, path)
216 }
217 }
218 if want := "b.local {\n\trespond b\n}\n"; string(results[3].Content) != want {
219 t.Errorf("second descendant content = %q, want %q", results[3].Content, want)
220 }
221 })
222 }
223 }
224
225 func TestDiscoverRepeatedSymlinkImportsResolveRelativePaths(t *testing.T) {
226 dir := t.TempDir()
227 root := filepath.Join(dir, "Caddyfile")
228 selector := filepath.Join(dir, "selector.caddy")
229 writeFile(t, root, "import a/selector.caddy\nimport b/selector.caddy\n")
230 writeFile(t, selector, "import child.caddy\n")
231 for _, name := range []string{"a", "b"} {
232 subdir := filepath.Join(dir, name)
233 if err := os.Mkdir(subdir, 0o755); err != nil {
234 t.Fatal(err)
235 }
236 if err := os.Symlink(selector, filepath.Join(subdir, "selector.caddy")); err != nil {
237 t.Skipf("symlinks unavailable: %v", err)
238 }
239 writeFile(t, filepath.Join(subdir, "child.caddy"), name+".local {\nrespond 200\n}\n")
240 }
241 results, err := FormatImports(root, FormatOptions{})
242 if err != nil {
243 t.Fatal(err)
244 }
245 want := []string{root, selector, filepath.Join(dir, "a", "child.caddy"), filepath.Join(dir, "b", "child.caddy")}
246 if len(results) != len(want) {
247 t.Fatalf("got paths %v, want %v", resultPaths(results), want)
248 }
249 for i, path := range want {
250 if results[i].Path != mustCanonicalPath(t, path) {
251 t.Errorf("result %d path = %q, want %q", i, results[i].Path, path)
252 }
253 }
254 }
255
256 func TestDiscoverRepeatedImportBlocks(t *testing.T) {
257 dir := t.TempDir()
258 root := filepath.Join(dir, "Caddyfile")
259 selector := filepath.Join(dir, "selector.caddy")
260 input := "import selector.caddy {\nbody {\nimport a.caddy\n}\n}\n" +
261 "import selector.caddy {\nbody {\nimport b.caddy\n}\n}\n"
262 writeFile(t, root, input)
263 writeFile(t, selector, "localhost {\n{blocks.body}\n}\n")
264 writeFile(t, filepath.Join(dir, "a.caddy"), "respond a\n")
265 writeFile(t, filepath.Join(dir, "b.caddy"), "respond b\n")
266 if blocks, err := Parse(root, []byte(input)); err != nil || len(blocks) != 2 {
267 t.Fatalf("expected two valid server blocks, got %d: %v", len(blocks), err)
268 }
269 results, err := FormatImports(root, FormatOptions{})
270 if err != nil {
271 t.Fatal(err)
272 }
273 want := []string{root, selector, filepath.Join(dir, "a.caddy"), filepath.Join(dir, "b.caddy")}
274 if len(results) != len(want) {
275 t.Fatalf("got paths %v, want %v", resultPaths(results), want)
276 }
277 for i, path := range want {
278 if results[i].Path != mustCanonicalPath(t, path) {
279 t.Errorf("result %d path = %q, want %q", i, results[i].Path, path)
280 }
281 }
282 }
283
284 func TestDiscoverSkipsActiveSymlinkAncestor(t *testing.T) {
285 dir := t.TempDir()
286 root := filepath.Join(dir, "Caddyfile")
287 child := filepath.Join(dir, "child.caddy")
288 writeFile(t, root, "import child.caddy\n")
289 writeFile(t, child, "import dir-link/child.caddy\nlocalhost {\nrespond ok\n}\n")
290 if err := os.Symlink(dir, filepath.Join(dir, "dir-link")); err != nil {
291 t.Skipf("symlinks unavailable: %v", err)
292 }
293 results, err := FormatImports(root, FormatOptions{})
294 if err != nil {
295 t.Fatal(err)
296 }
297 if len(results) != 2 || results[1].Path != mustCanonicalPath(t, child) {
298 t.Fatalf("got paths %v, want root and one physical child", resultPaths(results))
299 }
300 }
301
302 func TestDiscoverDeduplicatesSymlinksAndCycles(t *testing.T) {
303 if runtime.GOOS == "windows" {
304 t.Skip("symlink creation is not generally available on Windows")
305 }
306 dir := t.TempDir()
307 root := filepath.Join(dir, "Caddyfile")
308 child := filepath.Join(dir, "child.caddy")
309 writeFile(t, root, "import child.caddy\nimport child-link.caddy\nimport dir-link/Caddyfile\n")
310 writeFile(t, child, "localhost {\n\trespond ok\n}\n")
311 if err := os.Symlink(child, filepath.Join(dir, "child-link.caddy")); err != nil {
312 t.Fatal(err)
313 }
314 if err := os.Symlink(dir, filepath.Join(dir, "dir-link")); err != nil {
315 t.Fatal(err)
316 }
317
318 results, err := FormatImports(root, FormatOptions{})
319 if err != nil {
320 t.Fatal(err)
321 }
322 if len(results) != 2 {
323 t.Fatalf("got paths %v, want root and one physical child", resultPaths(results))
324 }
325 if results[1].Path != mustCanonicalPath(t, child) {
326 t.Fatalf("imported path = %q, want %q", results[1].Path, child)
327 }
328 }
329
330 func TestDiscoverExpandsEnvironmentBeforeLexing(t *testing.T) {
331 dir := t.TempDir()
332 root := filepath.Join(dir, "Caddyfile")
333 child := filepath.Join(dir, "child # file.caddy")
334 t.Setenv("CADDYFILE_IMPORT_LINE", `"import" "child # file.caddy" # generated comment`)
335 writeFile(t, root, "{$CADDYFILE_IMPORT_LINE}\n")
336 writeFile(t, child, "localhost {\n\trespond ok\n}\n")
337
338 results, err := FormatImports(root, FormatOptions{})
339 if err != nil {
340 t.Fatal(err)
341 }
342 if len(results) != 2 || results[1].Path != mustCanonicalPath(t, child) {
343 t.Fatalf("got paths %v, want generated quoted import %q", resultPaths(results), child)
344 }
345 }
346
347 func TestDiscoverIgnoresImportsInUnusedSnippetAndImportBlock(t *testing.T) {
348 dir := t.TempDir()
349 root := filepath.Join(dir, "Caddyfile")
350 template := filepath.Join(dir, "template.caddy")
351 dead := filepath.Join(dir, "dead.caddy")
352 writeFile(t, root, `(unused) {
353 import dead.caddy
354 }
355
356 import template.caddy {
357 unused {
358 import dead.caddy
359 }
360 }
361 `)
362 writeFile(t, template, "localhost {\n\trespond ok\n}\n")
363 writeFile(t, dead, "dead.example {\n\trespond dead\n}\n")
364
365 results, err := FormatImports(root, FormatOptions{})
366 if err != nil {
367 t.Fatal(err)
368 }
369 if len(results) != 2 || results[1].Path != mustCanonicalPath(t, template) {
370 t.Fatalf("got paths %v, want only active template import", resultPaths(results))
371 }
372 }
373
374 func TestDiscoverQuotedImportDirective(t *testing.T) {
375 dir := t.TempDir()
376 root := filepath.Join(dir, "Caddyfile")
377 child := filepath.Join(dir, "child.caddy")
378 writeFile(t, root, `"import" child.caddy`+"\n")
379 writeFile(t, child, "localhost {\n\trespond ok\n}\n")
380
381 results, err := FormatImports(root, FormatOptions{})
382 if err != nil {
383 t.Fatal(err)
384 }
385 if len(results) != 2 || results[1].Path != mustCanonicalPath(t, child) {
386 t.Fatalf("got paths %v, want quoted import", resultPaths(results))
387 }
388 }
389
390 func TestDiscoverUnreadableImportReturnsError(t *testing.T) {
391 if runtime.GOOS == "windows" {
392 t.Skip("Unix permission mode test")
393 }
394 dir := t.TempDir()
395 root := filepath.Join(dir, "Caddyfile")
396 child := filepath.Join(dir, "child.caddy")
397 writeFile(t, root, "import child.caddy\n")
398 writeFile(t, child, "localhost\n")
399 if err := os.Chmod(child, 0); err != nil {
400 t.Fatal(err)
401 }
402 t.Cleanup(func() { _ = os.Chmod(child, 0o600) })
403 if file, err := os.Open(child); err == nil {
404 file.Close()
405 t.Skip("test process can read mode-000 files")
406 }
407
408 if _, err := FormatImports(root, FormatOptions{}); err == nil {
409 t.Fatal("expected unreadable matched import to return an error")
410 }
411 }
412
413 func TestFormatImportsNormalizesAllPaths(t *testing.T) {
414 dir := t.TempDir()
415 realDir := filepath.Join(dir, "real")
416 linkDir := filepath.Join(dir, "link")
417 if err := os.MkdirAll(realDir, 0o755); err != nil {
418 t.Fatal(err)
419 }
420 if err := os.MkdirAll(linkDir, 0o755); err != nil {
421 t.Fatal(err)
422 }
423 realRoot := filepath.Join(realDir, "Caddyfile")
424 rootLink := filepath.Join(linkDir, "Caddyfile")
425 child := filepath.Join(linkDir, "child.caddy")
426 writeFile(t, realRoot, "import child.caddy\n")
427 writeFile(t, child, "localhost {\n\trespond ok\n}\n")
428 if err := os.Symlink(realRoot, rootLink); err != nil {
429 if runtime.GOOS == "windows" {
430 t.Skipf("cannot create symlink: %v", err)
431 }
432 t.Fatal(err)
433 }
434
435 results, err := FormatImports(rootLink, FormatOptions{})
436 if err != nil {
437 t.Fatal(err)
438 }
439 want := []string{mustCanonicalPath(t, realRoot), mustCanonicalPath(t, child)}
440 if got := resultPaths(results); len(got) != len(want) || got[0] != want[0] || got[1] != want[1] {
441 t.Fatalf("got paths %v, want canonical paths %v", got, want)
442 }
443 }
444
445 func TestDiscoveryRetainsImportedContent(t *testing.T) {
446 dir := t.TempDir()
447 root := filepath.Join(dir, "Caddyfile")
448 child := filepath.Join(dir, "child.caddy")
449 writeFile(t, root, "import child.caddy\n")
450 writeFile(t, child, "localhost {\nrespond old\n}\n")
451 rootInput, err := os.ReadFile(root)
452 if err != nil {
453 t.Fatal(err)
454 }
455 rootInfo, err := os.Stat(root)
456 if err != nil {
457 t.Fatal(err)
458 }
459
460 files, err := discoverImportedFileContents(root, rootInput, rootInfo)
461 if err != nil {
462 t.Fatal(err)
463 }
464 writeFile(t, child, "localhost {\n\trespond replacement\n}\n")
465 if len(files) != 1 || string(files[0].content) != "localhost {\nrespond old\n}\n" {
466 t.Fatalf("discovery did not retain the bytes read by the parser: %+v", files)
467 }
468 }
469
470 func resultPaths(results []FormattedFile) []string {
471 paths := make([]string, len(results))
472 for i := range results {
473 paths[i] = results[i].Path
474 }
475 return paths
476 }
477
478 func mustCanonicalPath(t *testing.T, path string) string {
479 t.Helper()
480 canonical, err := canonicalPath(path)
481 if err != nil {
482 t.Fatal(err)
483 }
484 return canonical
485 }