@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 436 lines 10.8 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 headers
16
17 import (
18 "context"
19 "fmt"
20 "net/http"
21 "net/http/httptest"
22 "reflect"
23 "testing"
24
25 "github.com/caddyserver/caddy/v2"
26 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
27 )
28
29 func TestHandler(t *testing.T) {
30 for i, tc := range []struct {
31 handler Handler
32 reqHeader http.Header
33 respHeader http.Header
34 respStatusCode int
35 expectedReqHeader http.Header
36 expectedRespHeader http.Header
37 }{
38 {
39 handler: Handler{
40 Request: &HeaderOps{
41 Add: http.Header{
42 "Expose-Secrets": []string{"always"},
43 },
44 },
45 },
46 reqHeader: http.Header{
47 "Expose-Secrets": []string{"i'm serious"},
48 },
49 expectedReqHeader: http.Header{
50 "Expose-Secrets": []string{"i'm serious", "always"},
51 },
52 },
53 {
54 handler: Handler{
55 Request: &HeaderOps{
56 Set: http.Header{
57 "Who-Wins": []string{"batman"},
58 },
59 },
60 },
61 reqHeader: http.Header{
62 "Who-Wins": []string{"joker"},
63 },
64 expectedReqHeader: http.Header{
65 "Who-Wins": []string{"batman"},
66 },
67 },
68 {
69 handler: Handler{
70 Request: &HeaderOps{
71 Delete: []string{"Kick-Me"},
72 },
73 },
74 reqHeader: http.Header{
75 "Kick-Me": []string{"if you can"},
76 "Keep-Me": []string{"i swear i'm innocent"},
77 },
78 expectedReqHeader: http.Header{
79 "Keep-Me": []string{"i swear i'm innocent"},
80 },
81 },
82 {
83 handler: Handler{
84 Request: &HeaderOps{
85 Delete: []string{
86 "*-suffix",
87 "prefix-*",
88 "*_*",
89 },
90 },
91 },
92 reqHeader: http.Header{
93 "Header-Suffix": []string{"lalala"},
94 "Prefix-Test": []string{"asdf"},
95 "Host_Header": []string{"silly django... sigh"}, // see issue #4830
96 "Keep-Me": []string{"foofoofoo"},
97 },
98 expectedReqHeader: http.Header{
99 "Keep-Me": []string{"foofoofoo"},
100 },
101 },
102 {
103 handler: Handler{
104 Request: &HeaderOps{
105 Replace: map[string][]Replacement{
106 "Best-Server": {
107 Replacement{
108 Search: "NGINX",
109 Replace: "the Caddy web server",
110 },
111 Replacement{
112 SearchRegexp: `Apache(\d+)`,
113 Replace: "Caddy",
114 },
115 },
116 },
117 },
118 },
119 reqHeader: http.Header{
120 "Best-Server": []string{"it's NGINX, undoubtedly", "I love Apache2"},
121 },
122 expectedReqHeader: http.Header{
123 "Best-Server": []string{"it's the Caddy web server, undoubtedly", "I love Caddy"},
124 },
125 },
126 {
127 handler: Handler{
128 Response: &RespHeaderOps{
129 Require: &caddyhttp.ResponseMatcher{
130 Headers: http.Header{
131 "Cache-Control": nil,
132 },
133 },
134 HeaderOps: &HeaderOps{
135 Add: http.Header{
136 "Cache-Control": []string{"no-cache"},
137 },
138 },
139 },
140 },
141 respHeader: http.Header{},
142 expectedRespHeader: http.Header{
143 "Cache-Control": []string{"no-cache"},
144 },
145 },
146 { // same as above, but checks that response headers are left alone when "Require" conditions are unmet
147 handler: Handler{
148 Response: &RespHeaderOps{
149 Require: &caddyhttp.ResponseMatcher{
150 Headers: http.Header{
151 "Cache-Control": nil,
152 },
153 },
154 HeaderOps: &HeaderOps{
155 Add: http.Header{
156 "Cache-Control": []string{"no-cache"},
157 },
158 },
159 },
160 },
161 respHeader: http.Header{
162 "Cache-Control": []string{"something"},
163 },
164 expectedRespHeader: http.Header{
165 "Cache-Control": []string{"something"},
166 },
167 },
168 {
169 handler: Handler{
170 Response: &RespHeaderOps{
171 Require: &caddyhttp.ResponseMatcher{
172 Headers: http.Header{
173 "Cache-Control": []string{"no-cache"},
174 },
175 },
176 HeaderOps: &HeaderOps{
177 Delete: []string{"Cache-Control"},
178 },
179 },
180 },
181 respHeader: http.Header{
182 "Cache-Control": []string{"no-cache"},
183 },
184 expectedRespHeader: http.Header{},
185 },
186 {
187 handler: Handler{
188 Response: &RespHeaderOps{
189 Require: &caddyhttp.ResponseMatcher{
190 StatusCode: []int{5},
191 },
192 HeaderOps: &HeaderOps{
193 Add: http.Header{
194 "Fail-5xx": []string{"true"},
195 },
196 },
197 },
198 },
199 respStatusCode: 503,
200 respHeader: http.Header{},
201 expectedRespHeader: http.Header{
202 "Fail-5xx": []string{"true"},
203 },
204 },
205 {
206 handler: Handler{
207 Request: &HeaderOps{
208 Replace: map[string][]Replacement{
209 "Case-Insensitive": {
210 Replacement{
211 Search: "issue4330",
212 Replace: "issue #4330",
213 },
214 },
215 },
216 },
217 },
218 reqHeader: http.Header{
219 "case-insensitive": []string{"issue4330"},
220 "Other-Header": []string{"issue4330"},
221 },
222 expectedReqHeader: http.Header{
223 "case-insensitive": []string{"issue #4330"},
224 "Other-Header": []string{"issue4330"},
225 },
226 },
227 } {
228 rr := httptest.NewRecorder()
229
230 req := &http.Request{Header: tc.reqHeader}
231 repl := caddy.NewReplacer()
232 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
233 req = req.WithContext(ctx)
234
235 tc.handler.Provision(caddy.Context{})
236
237 next := nextHandler(func(w http.ResponseWriter, r *http.Request) error {
238 for k, hdrs := range tc.respHeader {
239 for _, v := range hdrs {
240 w.Header().Add(k, v)
241 }
242 }
243
244 status := 200
245 if tc.respStatusCode != 0 {
246 status = tc.respStatusCode
247 }
248 w.WriteHeader(status)
249
250 if tc.expectedReqHeader != nil && !reflect.DeepEqual(r.Header, tc.expectedReqHeader) {
251 return fmt.Errorf("expected request header %v, got %v", tc.expectedReqHeader, r.Header)
252 }
253
254 return nil
255 })
256
257 if err := tc.handler.ServeHTTP(rr, req, next); err != nil {
258 t.Errorf("Test %d: %v", i, err)
259 continue
260 }
261
262 actual := rr.Header()
263 if tc.expectedRespHeader != nil && !reflect.DeepEqual(actual, tc.expectedRespHeader) {
264 t.Errorf("Test %d: expected response header %v, got %v", i, tc.expectedRespHeader, actual)
265 continue
266 }
267 }
268 }
269
270 func TestHeaderOpsSetMultipleCookies(t *testing.T) {
271 want := []string{"a=1; Path=/", "b=2; Path=/"}
272 for _, tc := range []struct {
273 fieldName, provisionedName string
274 }{
275 {"Set-Cookie", "Set-Cookie"},
276 {"set-cookie", "Set-Cookie"},
277 {"sEt-CoOkIe", "Set-Cookie"},
278 {"{cookie_header}", "{cookie_header}"},
279 {"set-{cookie_suffix}", "set-{cookie_suffix}"},
280 } {
281 t.Run(tc.fieldName, func(t *testing.T) {
282 ops := HeaderOps{Set: http.Header{tc.fieldName: want}}
283 if err := ops.Provision(caddy.Context{}); err != nil {
284 t.Fatal(err)
285 }
286 if !reflect.DeepEqual(ops.Set, http.Header{tc.provisionedName: want}) {
287 t.Fatalf("provisioned Set = %v, want field %q", ops.Set, tc.provisionedName)
288 }
289 repl := caddy.NewReplacer()
290 repl.Set("cookie_header", "set-cookie")
291 repl.Set("cookie_suffix", "cookie")
292 hdr := http.Header{"Set-Cookie": {"old=1"}}
293 ops.ApplyTo(hdr, repl)
294 if got := hdr.Values("Set-Cookie"); !reflect.DeepEqual(got, want) {
295 t.Errorf("Set-Cookie values = %q, want %q", got, want)
296 }
297 })
298 }
299 }
300
301 func TestHandlerProvisionSetCollision(t *testing.T) {
302 for _, name := range []string{"request", "response"} {
303 t.Run(name, func(t *testing.T) {
304 ops := &HeaderOps{Set: http.Header{
305 "set-cookie": {"b=2; Path=/"},
306 "Set-Cookie": {"a=1; Path=/"},
307 }}
308 h := Handler{Request: ops}
309 if name == "response" {
310 h = Handler{Response: &RespHeaderOps{HeaderOps: ops}}
311 }
312 if err := h.Provision(caddy.Context{}); err != nil {
313 t.Fatal(err)
314 }
315 want := http.Header{"Set-Cookie": {"a=1; Path=/", "b=2; Path=/"}}
316 if !reflect.DeepEqual(ops.Set, want) {
317 t.Fatalf("provisioned Set = %v, want %v", ops.Set, want)
318 }
319 hdr := make(http.Header)
320 ops.ApplyTo(hdr, caddy.NewReplacer())
321 if !reflect.DeepEqual(hdr, want) {
322 t.Errorf("headers = %v, want %v", hdr, want)
323 }
324 })
325 }
326 }
327
328 type nextHandler func(http.ResponseWriter, *http.Request) error
329
330 func (f nextHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
331 return f(w, r)
332 }
333
334 func TestContainsPlaceholders(t *testing.T) {
335 for i, tc := range []struct {
336 input string
337 expected bool
338 }{
339 {"static", false},
340 {"{placeholder}", true},
341 {"prefix-{placeholder}-suffix", true},
342 {"{}", false},
343 {"no-braces", false},
344 {"{unclosed", false},
345 {"unopened}", false},
346 } {
347 actual := containsPlaceholders(tc.input)
348 if actual != tc.expected {
349 t.Errorf("Test %d: containsPlaceholders(%q) = %v, expected %v", i, tc.input, actual, tc.expected)
350 }
351 }
352 }
353
354 func TestHeaderProvisionSkipsPlaceholders(t *testing.T) {
355 ops := &HeaderOps{
356 Replace: map[string][]Replacement{
357 "Static": {
358 Replacement{SearchRegexp: ":443", Replace: "STATIC"},
359 },
360 "Dynamic": {
361 Replacement{SearchRegexp: ":{http.request.local.port}", Replace: "DYNAMIC"},
362 },
363 },
364 }
365
366 err := ops.Provision(caddy.Context{})
367 if err != nil {
368 t.Fatalf("Provision failed: %v", err)
369 }
370
371 // Static regex should be precompiled
372 if ops.Replace["Static"][0].re == nil {
373 t.Error("Expected static regex to be precompiled")
374 }
375
376 // Dynamic regex with placeholder should not be precompiled
377 if ops.Replace["Dynamic"][0].re != nil {
378 t.Error("Expected dynamic regex with placeholder to not be precompiled")
379 }
380 }
381
382 func TestPlaceholderInSearchRegexp(t *testing.T) {
383 handler := Handler{
384 Response: &RespHeaderOps{
385 HeaderOps: &HeaderOps{
386 Replace: map[string][]Replacement{
387 "Test-Header": {
388 Replacement{
389 SearchRegexp: ":{http.request.local.port}",
390 Replace: "PLACEHOLDER-WORKS",
391 },
392 },
393 },
394 },
395 },
396 }
397
398 // Provision the handler
399 err := handler.Provision(caddy.Context{})
400 if err != nil {
401 t.Fatalf("Provision failed: %v", err)
402 }
403
404 replacement := handler.Response.HeaderOps.Replace["Test-Header"][0]
405 t.Logf("After provision - SearchRegexp: %q, re: %v", replacement.SearchRegexp, replacement.re)
406
407 rr := httptest.NewRecorder()
408
409 req := httptest.NewRequest("GET", "http://localhost:443/", nil)
410 repl := caddy.NewReplacer()
411 repl.Set("http.request.local.port", "443")
412
413 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
414 req = req.WithContext(ctx)
415
416 rr.Header().Set("Test-Header", "prefix:443suffix")
417 t.Logf("Initial header: %v", rr.Header())
418
419 next := nextHandler(func(w http.ResponseWriter, r *http.Request) error {
420 w.WriteHeader(200)
421 return nil
422 })
423
424 err = handler.ServeHTTP(rr, req, next)
425 if err != nil {
426 t.Fatalf("ServeHTTP failed: %v", err)
427 }
428
429 t.Logf("Final header: %v", rr.Header())
430
431 result := rr.Header().Get("Test-Header")
432 expected := "prefixPLACEHOLDER-WORKSsuffix"
433 if result != expected {
434 t.Errorf("Expected header value %q, got %q", expected, result)
435 }
436 }