@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 361 lines 10.9 KB
Raw
1 package httpcaddyfile
2
3 import (
4 "reflect"
5 "slices"
6 "sort"
7 "strings"
8 "sync"
9 "testing"
10
11 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
12 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
13 )
14
15 func TestHostsFromKeys(t *testing.T) {
16 for i, tc := range []struct {
17 keys []Address
18 expectNormalMode []string
19 expectLoggerMode []string
20 }{
21 {
22 []Address{
23 {Original: "foo", Host: "foo"},
24 },
25 []string{"foo"},
26 []string{"foo"},
27 },
28 {
29 []Address{
30 {Original: "foo", Host: "foo"},
31 {Original: "bar", Host: "bar"},
32 },
33 []string{"bar", "foo"},
34 []string{"bar", "foo"},
35 },
36 {
37 []Address{
38 {Original: ":2015", Port: "2015"},
39 },
40 []string{},
41 []string{},
42 },
43 {
44 []Address{
45 {Original: ":443", Port: "443"},
46 },
47 []string{},
48 []string{},
49 },
50 {
51 []Address{
52 {Original: "foo", Host: "foo"},
53 {Original: ":2015", Port: "2015"},
54 },
55 []string{},
56 []string{"foo"},
57 },
58 {
59 []Address{
60 {Original: "example.com:2015", Host: "example.com", Port: "2015"},
61 },
62 []string{"example.com"},
63 []string{"example.com:2015"},
64 },
65 {
66 []Address{
67 {Original: "example.com:80", Host: "example.com", Port: "80"},
68 },
69 []string{"example.com"},
70 []string{"example.com"},
71 },
72 {
73 []Address{
74 {Original: "https://:2015/foo", Scheme: "https", Port: "2015", Path: "/foo"},
75 },
76 []string{},
77 []string{},
78 },
79 {
80 []Address{
81 {Original: "https://example.com:2015/foo", Scheme: "https", Host: "example.com", Port: "2015", Path: "/foo"},
82 },
83 []string{"example.com"},
84 []string{"example.com:2015"},
85 },
86 } {
87 sb := serverBlock{parsedKeys: tc.keys}
88
89 // test in normal mode
90 actual := sb.hostsFromKeys(false)
91 sort.Strings(actual)
92 if !reflect.DeepEqual(tc.expectNormalMode, actual) {
93 t.Errorf("Test %d (loggerMode=false): Expected: %v Actual: %v", i, tc.expectNormalMode, actual)
94 }
95
96 // test in logger mode
97 actual = sb.hostsFromKeys(true)
98 sort.Strings(actual)
99 if !reflect.DeepEqual(tc.expectLoggerMode, actual) {
100 t.Errorf("Test %d (loggerMode=true): Expected: %v Actual: %v", i, tc.expectLoggerMode, actual)
101 }
102 }
103 }
104
105 // test_ordered_plugin stands in for a plugin directive in the tests below.
106 // it registers from init() like a real plugin would, which also keeps it to
107 // once per process, since registering twice panics.
108 func init() {
109 RegisterHandlerDirective("test_ordered_plugin", func(h Helper) (caddyhttp.MiddlewareHandler, error) {
110 h.Next()
111 return caddyhttp.StaticError{StatusCode: "500"}, nil
112 })
113 RegisterDirectiveOrder("test_ordered_plugin", Before, "respond")
114
115 RegisterHandlerDirective("test_named_route", func(h Helper) (caddyhttp.MiddlewareHandler, error) {
116 h.Next()
117 body := "site"
118 if h.IsNamedRoute() {
119 body = "named"
120 }
121 return caddyhttp.StaticResponse{Body: body}, nil
122 })
123 RegisterDirectiveOrder("test_named_route", Before, "respond")
124 }
125
126 func TestHelperIsNamedRoute(t *testing.T) {
127 adapted := adaptCaddyfile(t, `&(shared) {
128 handle /nested {
129 test_named_route
130 }
131 }
132
133 :8080 {
134 test_named_route
135 invoke shared
136 }
137 `)
138
139 for _, body := range []string{`"body":"named"`, `"body":"site"`} {
140 if !strings.Contains(adapted, body) {
141 t.Errorf("expected %s in the adapted config: %s", body, adapted)
142 }
143 }
144 }
145
146 // caddyfiles shared by the directive order tests below. the handle block in
147 // orderTestPlain is there so that the subroute path gets covered too, not
148 // just the top level of the site block, and orderTestReordered moves respond
149 // ahead of root so it has to adapt to something different.
150 const (
151 orderTestPlain = `:8080 {
152 root * /srv
153 handle /foo {
154 respond 200
155 root * /other
156 }
157 }
158 `
159 orderTestReordered = "{\n\torder respond first\n}\n" + orderTestPlain
160
161 // covers every positional, since first and last build the new order in
162 // their own way and last can append in place
163 orderTestConcurrent = `{
164 order redir before respond
165 order vars after root
166 order root first
167 order respond last
168 }
169 :8080 {
170 respond 200
171 }
172 `
173 )
174
175 // adaptCaddyfile adapts the given Caddyfile with a fresh options map.
176 func adaptCaddyfile(t *testing.T, input string) string {
177 t.Helper()
178 return adaptWithOptions(t, input, nil)
179 }
180
181 // adaptWithOptions adapts the given Caddyfile with the caller's options map.
182 func adaptWithOptions(t *testing.T, input string, options map[string]any) string {
183 t.Helper()
184 adapter := caddyfile.Adapter{ServerType: ServerType{}}
185 out, _, err := adapter.Adapt([]byte(input), options)
186 if err != nil {
187 t.Fatalf("adapting: %v", err)
188 }
189 return string(out)
190 }
191
192 // adaptConcurrently adapts a Caddyfile using the order global option from
193 // several goroutines at once. A non-nil options map is shared by all of
194 // them; nil gives each adaptation its own. Run with -race.
195 func adaptConcurrently(t *testing.T, options map[string]any) {
196 t.Helper()
197
198 var wg sync.WaitGroup
199 for range 8 {
200 wg.Add(1)
201 go func() {
202 defer wg.Done()
203 for range 200 {
204 adapter := caddyfile.Adapter{ServerType: ServerType{}}
205 if _, _, err := adapter.Adapt([]byte(orderTestConcurrent), options); err != nil {
206 t.Errorf("adapting concurrently: %v", err)
207 return
208 }
209 }
210 }()
211 }
212 wg.Wait()
213 }
214
215 // handlerComesFirst reports whether handler a appears before handler b in
216 // the adapted JSON.
217 func handlerComesFirst(adapted, a, b string) bool {
218 ai := strings.Index(adapted, `"handler":"`+a+`"`)
219 bi := strings.Index(adapted, `"handler":"`+b+`"`)
220 return ai >= 0 && bi >= 0 && ai < bi
221 }
222
223 // TestAdaptDirectiveOrderIsolation makes sure the order global option only
224 // applies to the adaptation it was written in.
225 func TestAdaptDirectiveOrderIsolation(t *testing.T) {
226 baseline := adaptCaddyfile(t, orderTestPlain)
227
228 // this one moves respond to the front, so it has to produce something
229 // different than the default order does
230 if out := adaptCaddyfile(t, orderTestReordered); out == baseline {
231 t.Fatal("the order option had no effect, so this test proves nothing")
232 }
233
234 if out := adaptCaddyfile(t, orderTestPlain); out != baseline {
235 t.Errorf("order option leaked into the next adaptation:\nExpected: %s\nActual: %s", baseline, out)
236 }
237 }
238
239 // TestAdaptDirectiveOrderAfterError makes sure a failed adaptation leaves
240 // nothing behind for the next one.
241 func TestAdaptDirectiveOrderAfterError(t *testing.T) {
242 baseline := adaptCaddyfile(t, orderTestPlain)
243
244 const broken = `{
245 order respond before not_a_directive
246 }
247 :8080 {
248 respond 200
249 }
250 `
251 adapter := caddyfile.Adapter{ServerType: ServerType{}}
252 if _, _, err := adapter.Adapt([]byte(broken), nil); err == nil {
253 t.Fatal("expected an error for an order option pointing at an unknown directive")
254 }
255
256 if out := adaptCaddyfile(t, orderTestPlain); out != baseline {
257 t.Errorf("failed adaptation left the order broken:\nExpected: %s\nActual: %s", baseline, out)
258 }
259 }
260
261 // TestAdaptDirectiveOrderWithReusedOptions makes sure that handing the same
262 // options map to one adaptation after another doesn't carry the first one's
263 // order into the second, and that the caller's map is left alone.
264 func TestAdaptDirectiveOrderWithReusedOptions(t *testing.T) {
265 baseline := adaptCaddyfile(t, orderTestPlain)
266
267 options := map[string]any{}
268
269 if out := adaptWithOptions(t, orderTestReordered, options); out == baseline {
270 t.Fatal("the order option had no effect, so this test proves nothing")
271 }
272 if out := adaptWithOptions(t, orderTestPlain, options); out != baseline {
273 t.Errorf("reused options map carried the order into the next adaptation:\nExpected: %s\nActual: %s", baseline, out)
274 }
275 if len(options) > 0 {
276 t.Errorf("adapting wrote into the caller's options map: %v", options)
277 }
278
279 // an order the caller put in the map itself is ignored as well, and the
280 // slice behind it is never written to; if it were used here, root would
281 // not be an ordered handler and adapting would fail
282 callerOrder := []string{"respond"}
283 options["order"] = callerOrder
284 if out := adaptWithOptions(t, orderTestPlain, options); out != baseline {
285 t.Errorf("an order from the caller's map was used:\nExpected: %s\nActual: %s", baseline, out)
286 }
287 if !slices.Equal(callerOrder, []string{"respond"}) {
288 t.Errorf("adapting modified the caller's order: %v", callerOrder)
289 }
290 }
291
292 // TestConcurrentAdaptDirectiveOrder makes sure that adapting several
293 // Caddyfiles at once, all of them using the order global option, doesn't
294 // race or corrupt the order shared by the whole process.
295 // See https://github.com/caddyserver/caddy/issues/7994
296 func TestConcurrentAdaptDirectiveOrder(t *testing.T) {
297 baseline := slices.Clone(directiveOrder)
298 defaultBaseline := slices.Clone(defaultDirectiveOrder)
299
300 adaptConcurrently(t, nil)
301
302 if !slices.Equal(directiveOrder, baseline) {
303 t.Errorf("adaptation changed the process-wide order:\nExpected: %v\nActual: %v", baseline, directiveOrder)
304 }
305 if !slices.Equal(defaultDirectiveOrder, defaultBaseline) {
306 t.Errorf("adaptation changed the default order:\nExpected: %v\nActual: %v", defaultBaseline, defaultDirectiveOrder)
307 }
308 }
309
310 // TestConcurrentAdaptDirectiveOrderWithReusedOptions is the same, except
311 // every goroutine is handed the same options map.
312 func TestConcurrentAdaptDirectiveOrderWithReusedOptions(t *testing.T) {
313 baseline := slices.Clone(directiveOrder)
314 defaultBaseline := slices.Clone(defaultDirectiveOrder)
315 options := map[string]any{}
316
317 adaptConcurrently(t, options)
318
319 if !slices.Equal(directiveOrder, baseline) {
320 t.Errorf("adaptation changed the process-wide order:\nExpected: %v\nActual: %v", baseline, directiveOrder)
321 }
322 if !slices.Equal(defaultDirectiveOrder, defaultBaseline) {
323 t.Errorf("adaptation changed the default order:\nExpected: %v\nActual: %v", defaultBaseline, defaultDirectiveOrder)
324 }
325 if len(options) > 0 {
326 t.Errorf("adapting wrote into the caller's options map: %v", options)
327 }
328 }
329
330 // TestRegisterDirectiveOrderWithOrderOption makes sure an order registered
331 // by a plugin is still honored, and can still be overridden by the order
332 // global option without the override sticking around afterwards.
333 func TestRegisterDirectiveOrderWithOrderOption(t *testing.T) {
334 if !slices.Contains(directiveOrder, "test_ordered_plugin") {
335 t.Fatal("plugin directive was not added to the order")
336 }
337 if !slices.Contains(defaultDirectiveOrder, "respond") || slices.Contains(defaultDirectiveOrder, "test_ordered_plugin") {
338 t.Errorf("registering a plugin order corrupted the default order: %v", defaultDirectiveOrder)
339 }
340
341 const site = `:8080 {
342 respond 200
343 test_ordered_plugin
344 }
345 `
346 // with no order option, the registered position wins
347 if out := adaptCaddyfile(t, site); !handlerComesFirst(out, "error", "static_response") {
348 t.Errorf("expected the plugin directive before respond, got: %s", out)
349 }
350
351 // the order option overrides it
352 overridden := adaptCaddyfile(t, "{\n\torder test_ordered_plugin after respond\n}\n"+site)
353 if !handlerComesFirst(overridden, "static_response", "error") {
354 t.Errorf("expected respond before the plugin directive, got: %s", overridden)
355 }
356
357 // and the override is gone again
358 if out := adaptCaddyfile(t, site); !handlerComesFirst(out, "error", "static_response") {
359 t.Errorf("the order option leaked past its own adaptation, got: %s", out)
360 }
361 }