@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 465 lines 9.28 KB
Raw
1 package httpcaddyfile
2
3 import (
4 "encoding/json"
5 "fmt"
6 "slices"
7 "strings"
8 "testing"
9
10 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
11 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
12 )
13
14 func TestWindowsRootTrailingBackslash(t *testing.T) {
15 adapter := caddyfile.Adapter{ServerType: ServerType{}}
16 input := "http://localhost {\nroot * C:\\site\\\nrespond ok\n}\n"
17 rootJSON, err := json.Marshal(`C:\site\`)
18 if err != nil {
19 t.Fatal(err)
20 }
21 for _, in := range []string{input, strings.ReplaceAll(input, "\n", "\r\n")} {
22 before, _, err := adapter.Adapt([]byte(in), nil)
23 if err != nil {
24 t.Fatal(err)
25 }
26 if !strings.Contains(string(before), `"root":`+string(rootJSON)) {
27 t.Fatalf("Windows root not preserved in adapted config: %s", before)
28 }
29 after, _, err := adapter.Adapt(caddyfile.Format([]byte(in)), nil)
30 if err != nil {
31 t.Fatal(err)
32 }
33 if string(before) != string(after) {
34 t.Fatalf("formatting changed adapted JSON:\nbefore: %s\nafter: %s", before, after)
35 }
36 }
37 }
38
39 func TestMatcherSyntax(t *testing.T) {
40 for i, tc := range []struct {
41 input string
42 expectError bool
43 expectContains string
44 }{
45 {
46 input: `http://localhost
47 @debug {
48 query showdebug=1
49 }
50 `,
51 expectError: false,
52 },
53 {
54 input: `http://localhost
55 @debug {
56 query bad format
57 }
58 `,
59 expectError: true,
60 },
61 {
62 input: `http://localhost
63 @debug {
64 not {
65 path /somepath*
66 }
67 }
68 `,
69 expectError: false,
70 },
71 {
72 input: `http://localhost
73 @debug {
74 not path /somepath*
75 }
76 `,
77 expectError: false,
78 },
79 {
80 input: `http://localhost
81 @debug not path /somepath*
82 `,
83 expectError: false,
84 },
85 {
86 input: `http://localhost {
87 @test {
88 path /test
89 }
90 @test {
91 path /other
92 }
93 respond @test "hello"
94 }
95 `,
96 expectError: true,
97 expectContains: "is defined more than once",
98 },
99 {
100 input: `(snippet) {
101 @{args[0]} {
102 path /{args[0]}
103 }
104 respond @{args[0]} "hello"
105 }
106 http://localhost {
107 import snippet foo
108 import snippet bar
109 }
110 `,
111 expectError: false,
112 },
113 {
114 input: `@matcher {
115 path /matcher-not-allowed/outside-of-site-block/*
116 }
117 http://localhost
118 `,
119 expectError: true,
120 },
121 } {
122
123 adapter := caddyfile.Adapter{
124 ServerType: ServerType{},
125 }
126
127 _, _, err := adapter.Adapt([]byte(tc.input), nil)
128
129 if err != nil != tc.expectError {
130 t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
131 continue
132 }
133
134 if err != nil && tc.expectContains != "" {
135 if !strings.Contains(err.Error(), tc.expectContains) {
136 t.Errorf("Test %d error message mismatch: expected to contain %q, got %q",
137 i, tc.expectContains, err.Error())
138 }
139 }
140 }
141 }
142
143 func TestSpecificity(t *testing.T) {
144 for i, tc := range []struct {
145 input string
146 expect int
147 }{
148 {"", 0},
149 {"*", 0},
150 {"*.*", 1},
151 {"{placeholder}", 0},
152 {"/{placeholder}", 1},
153 {"foo", 3},
154 {"example.com", 11},
155 {"a.example.com", 13},
156 {"*.example.com", 12},
157 {"/foo", 4},
158 {"/foo*", 4},
159 {"{placeholder}.example.com", 12},
160 {"{placeholder.example.com", 24},
161 {"}.", 2},
162 {"}{", 2},
163 {"{}", 0},
164 {"{{{}}", 1},
165 } {
166 actual := specificity(tc.input)
167 if actual != tc.expect {
168 t.Errorf("Test %d (%s): Expected %d but got %d", i, tc.input, tc.expect, actual)
169 }
170 }
171 }
172
173 func TestGlobalOptions(t *testing.T) {
174 for i, tc := range []struct {
175 input string
176 expectError bool
177 }{
178 {
179 input: `
180 {
181 email test@example.com
182 }
183 :80
184 `,
185 expectError: false,
186 },
187 {
188 input: `
189 {
190 admin off
191 }
192 :80
193 `,
194 expectError: false,
195 },
196 {
197 input: `
198 {
199 admin 127.0.0.1:2020
200 }
201 :80
202 `,
203 expectError: false,
204 },
205 {
206 input: `
207 {
208 admin {
209 disabled false
210 }
211 }
212 :80
213 `,
214 expectError: true,
215 },
216 {
217 input: `
218 {
219 admin {
220 enforce_origin
221 origins 192.168.1.1:2020 127.0.0.1:2020
222 }
223 }
224 :80
225 `,
226 expectError: false,
227 },
228 {
229 input: `
230 {
231 admin 127.0.0.1:2020 {
232 enforce_origin
233 origins 192.168.1.1:2020 127.0.0.1:2020
234 }
235 }
236 :80
237 `,
238 expectError: false,
239 },
240 {
241 input: `
242 {
243 admin 192.168.1.1:2020 127.0.0.1:2020 {
244 enforce_origin
245 origins 192.168.1.1:2020 127.0.0.1:2020
246 }
247 }
248 :80
249 `,
250 expectError: true,
251 },
252 {
253 input: `
254 {
255 admin off {
256 enforce_origin
257 origins 192.168.1.1:2020 127.0.0.1:2020
258 }
259 }
260 :80
261 `,
262 expectError: true,
263 },
264 } {
265
266 adapter := caddyfile.Adapter{
267 ServerType: ServerType{},
268 }
269
270 _, _, err := adapter.Adapt([]byte(tc.input), nil)
271
272 if err != nil != tc.expectError {
273 t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
274 continue
275 }
276 }
277 }
278
279 func TestDefaultSNIWithoutHTTPS(t *testing.T) {
280 caddyfileStr := `{
281 default_sni my-sni.com
282 }
283 example.com {
284 }`
285
286 adapter := caddyfile.Adapter{
287 ServerType: ServerType{},
288 }
289
290 result, _, err := adapter.Adapt([]byte(caddyfileStr), nil)
291 if err != nil {
292 t.Fatalf("Failed to adapt Caddyfile: %v", err)
293 }
294
295 var config struct {
296 Apps struct {
297 HTTP struct {
298 Servers map[string]*caddyhttp.Server `json:"servers"`
299 } `json:"http"`
300 } `json:"apps"`
301 }
302
303 if err := json.Unmarshal(result, &config); err != nil {
304 t.Fatalf("Failed to unmarshal JSON config: %v", err)
305 }
306
307 server, ok := config.Apps.HTTP.Servers["srv0"]
308 if !ok {
309 t.Fatalf("Expected server 'srv0' to be created")
310 }
311
312 if len(server.TLSConnPolicies) == 0 {
313 t.Fatalf("Expected TLS connection policies to be generated, got none")
314 }
315
316 found := false
317 for _, policy := range server.TLSConnPolicies {
318 if policy.DefaultSNI == "my-sni.com" {
319 found = true
320 break
321 }
322 }
323
324 if !found {
325 t.Errorf("Expected default_sni 'my-sni.com' in TLS connection policies, but it was missing. Generated JSON: %s", string(result))
326 }
327 }
328
329 func TestTLSConnectionPoliciesAreIsolatedAcrossListeners(t *testing.T) {
330 caddyfileStr := `(mtls) {
331 tls {
332 client_auth {
333 mode require_and_verify
334 trust_pool file /tmp/root.crt
335 }
336 }
337 }
338
339 abc.domain.com, abc.domain.com:2000 {
340 import mtls
341 respond "first"
342 }
343
344 domain.domain.com {
345 import mtls
346 respond "second"
347 }
348
349 ghi.domain.com, ghi.domain.com:2000 {
350 import mtls
351 respond "third"
352 }`
353
354 adapter := caddyfile.Adapter{ServerType: ServerType{}}
355 result, _, err := adapter.Adapt([]byte(caddyfileStr), nil)
356 if err != nil {
357 t.Fatalf("Failed to adapt Caddyfile: %v", err)
358 }
359
360 var config struct {
361 Apps struct {
362 HTTP struct {
363 Servers map[string]*caddyhttp.Server `json:"servers"`
364 } `json:"http"`
365 } `json:"apps"`
366 }
367 if err := json.Unmarshal(result, &config); err != nil {
368 t.Fatalf("Failed to unmarshal JSON config: %v", err)
369 }
370
371 for listener, expectedNames := range map[string][]string{
372 ":2000": {"abc.domain.com", "ghi.domain.com"},
373 ":443": {"abc.domain.com", "domain.domain.com", "ghi.domain.com"},
374 } {
375 var server *caddyhttp.Server
376 for _, candidate := range config.Apps.HTTP.Servers {
377 if slices.Contains(candidate.Listen, listener) {
378 server = candidate
379 break
380 }
381 }
382 if server == nil {
383 t.Fatalf("Expected a server listening on %s", listener)
384 }
385
386 var names []string
387 for _, policy := range server.TLSConnPolicies {
388 if policy.ClientAuthentication == nil {
389 continue
390 }
391 if policy.ClientAuthentication.Mode != "require_and_verify" {
392 t.Fatalf("Expected %s client authentication on %s, got %s", "require_and_verify", listener, policy.ClientAuthentication.Mode)
393 }
394 var policyNames []string
395 if err := json.Unmarshal(policy.MatchersRaw["sni"], &policyNames); err != nil {
396 t.Fatalf("Failed to unmarshal SNI matcher for %s: %v", listener, err)
397 }
398 names = append(names, policyNames...)
399 }
400
401 slices.Sort(names)
402 if !slices.Equal(names, expectedNames) {
403 t.Errorf("Expected client authentication for %v on %s, got %v", expectedNames, listener, names)
404 }
405 }
406 }
407
408 func TestClientAuthConsolidation(t *testing.T) {
409 const requestAuth = `client_auth {
410 mode request
411 }`
412 const verifyAuth = `client_auth {
413 mode require_and_verify
414 trust_pool file /tmp/root.crt
415 }`
416
417 tlsBlock := func(auth string) string {
418 if auth == "" {
419 return ""
420 }
421 return fmt.Sprintf("tls {\n\t\t\t%s\n\t\t}", auth)
422 }
423
424 for _, tc := range []struct {
425 name string
426 firstAuth string
427 secondAuth string
428 wantErr bool
429 }{
430 {"conflicting, laxer policy first", requestAuth, verifyAuth, true},
431 {"conflicting, stricter policy first", verifyAuth, requestAuth, true},
432 {"only first sets client auth", verifyAuth, "", false},
433 {"only second sets client auth", "", verifyAuth, false},
434 {"identical client auth", verifyAuth, verifyAuth, false},
435 } {
436 t.Run(tc.name, func(t *testing.T) {
437 caddyfileStr := fmt.Sprintf(`example.com {
438 %s
439 handle /public/* {
440 respond "public"
441 }
442 }
443
444 example.com:443 {
445 %s
446 handle /admin* {
447 respond "admin"
448 }
449 }`, tlsBlock(tc.firstAuth), tlsBlock(tc.secondAuth))
450
451 adapter := caddyfile.Adapter{ServerType: ServerType{}}
452 result, _, err := adapter.Adapt([]byte(caddyfileStr), nil)
453 if tc.wantErr {
454 if err == nil {
455 t.Fatalf("expected conflicting client auth to be rejected, got: %s", result)
456 }
457 if !strings.Contains(err.Error(), "conflicting client auth configuration") {
458 t.Fatalf("expected conflicting client auth error, got: %v", err)
459 }
460 } else if err != nil {
461 t.Fatalf("expected adapt to succeed, got: %v", err)
462 }
463 })
464 }
465 }