Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
ResponseMatcher for conditional logic of response headers
Matthew Holt committed
May 28, 2019 at 18:53 UTC
bf54615efcaa98ab2e5c83e19a0e7b57897ca2c8
3 files changed
+194
-3
modules/caddyhttp/headers/headers.go
+8
-3
@@ -33,16 +33,18 @@ type HeaderOps struct {
33
// optionally deferred until response time.
34
type RespHeaderOps struct {
35
*HeaderOps
36
- Deferred bool `json:"deferred"`
36
+ Require *caddyhttp.ResponseMatcher `json:"require,omitempty"`
37
+ Deferred bool `json:"deferred,omitempty"`
38
}
39
40
func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
41
repl := r.Context().Value(caddy2.ReplacerCtxKey).(caddy2.Replacer)
42
apply(h.Request, r.Header, repl)
42
- if h.Response.Deferred {
43
+ if h.Response.Deferred || h.Response.Require != nil {
44
w = &responseWriterWrapper{
45
ResponseWriterWrapper: &caddyhttp.ResponseWriterWrapper{ResponseWriter: w},
46
replacer: repl,
47
+ require: h.Response.Require,
48
headerOps: h.Response.HeaderOps,
49
}
50
} else {
@@ -75,6 +77,7 @@ func apply(ops *HeaderOps, hdr http.Header, repl caddy2.Replacer) {
77
type responseWriterWrapper struct {
78
*caddyhttp.ResponseWriterWrapper
79
replacer caddy2.Replacer
80
+ require *caddyhttp.ResponseMatcher
81
headerOps *HeaderOps
82
wroteHeader bool
83
}
@@ -91,7 +94,9 @@ func (rww *responseWriterWrapper) WriteHeader(status int) {
94
return
95
}
96
rww.wroteHeader = true
94
- apply(rww.headerOps, rww.ResponseWriterWrapper.Header(), rww.replacer)
97
+ if rww.require == nil || rww.require.Match(status, rww.ResponseWriterWrapper.Header()) {
98
+ apply(rww.headerOps, rww.ResponseWriterWrapper.Header(), rww.replacer)
99
+ }
100
rww.ResponseWriterWrapper.WriteHeader(status)
101
}
102
modules/caddyhttp/matchers.go
+55
@@ -298,6 +298,61 @@ func (mre *MatchRegexp) Match(input string, repl caddy2.Replacer, scope string)
298
return true
299
}
300
301
+// ResponseMatcher is a type which can determine if a given response
302
+// status code and its headers match some criteria.
303
+type ResponseMatcher struct {
304
+ // If set, one of these status codes would be required.
305
+ // A one-digit status can be used to represent all codes
306
+ // in that class (e.g. 3 for all 3xx codes).
307
+ StatusCode []int `json:"status_code,omitempty"`
308
+
309
+ // If set, each header specified must be one of the specified values.
310
+ Headers http.Header `json:"headers,omitempty"`
311
+}
312
+
313
+// Match returns true if the given statusCode and hdr match rm.
314
+func (rm ResponseMatcher) Match(statusCode int, hdr http.Header) bool {
315
+ if !rm.matchStatusCode(statusCode) {
316
+ return false
317
+ }
318
+ return rm.matchHeaders(hdr)
319
+}
320
+
321
+func (rm ResponseMatcher) matchStatusCode(statusCode int) bool {
322
+ if rm.StatusCode == nil {
323
+ return true
324
+ }
325
+ for _, code := range rm.StatusCode {
326
+ if statusCode == code {
327
+ return true
328
+ }
329
+ if code < 100 && statusCode >= code*100 && statusCode < (code+1)*100 {
330
+ return true
331
+ }
332
+ }
333
+ return false
334
+}
335
+
336
+func (rm ResponseMatcher) matchHeaders(hdr http.Header) bool {
337
+ for field, allowedFieldVals := range rm.Headers {
338
+ var match bool
339
+ actualFieldVals := hdr[textproto.CanonicalMIMEHeaderKey(field)]
340
+ fieldVals:
341
+ for _, actualFieldVal := range actualFieldVals {
342
+ for _, allowedFieldVal := range allowedFieldVals {
343
+ if actualFieldVal == allowedFieldVal {
344
+ match = true
345
+ break fieldVals
346
+ }
347
+ }
348
+ }
349
+ if !match {
350
+ return false
351
+ }
352
+ }
353
+ return true
354
+}
355
+
356
var wordRE = regexp.MustCompile(`\w+`)
357
358
// Interface guards
modules/caddyhttp/matchers_test.go
+131
@@ -368,3 +368,134 @@ func TestHeaderREMatcher(t *testing.T) {
368
}
369
}
370
}
371
+
372
+func TestResponseMatcher(t *testing.T) {
373
+ for i, tc := range []struct {
374
+ require ResponseMatcher
375
+ status int
376
+ hdr http.Header // make sure these are canonical cased (std lib will do that in a real request)
377
+ expect bool
378
+ }{
379
+ {
380
+ require: ResponseMatcher{},
381
+ status: 200,
382
+ expect: true,
383
+ },
384
+ {
385
+ require: ResponseMatcher{
386
+ StatusCode: []int{200},
387
+ },
388
+ status: 200,
389
+ expect: true,
390
+ },
391
+ {
392
+ require: ResponseMatcher{
393
+ StatusCode: []int{2},
394
+ },
395
+ status: 200,
396
+ expect: true,
397
+ },
398
+ {
399
+ require: ResponseMatcher{
400
+ StatusCode: []int{201},
401
+ },
402
+ status: 200,
403
+ expect: false,
404
+ },
405
+ {
406
+ require: ResponseMatcher{
407
+ StatusCode: []int{2},
408
+ },
409
+ status: 301,
410
+ expect: false,
411
+ },
412
+ {
413
+ require: ResponseMatcher{
414
+ StatusCode: []int{3},
415
+ },
416
+ status: 301,
417
+ expect: true,
418
+ },
419
+ {
420
+ require: ResponseMatcher{
421
+ StatusCode: []int{3},
422
+ },
423
+ status: 399,
424
+ expect: true,
425
+ },
426
+ {
427
+ require: ResponseMatcher{
428
+ StatusCode: []int{3},
429
+ },
430
+ status: 400,
431
+ expect: false,
432
+ },
433
+ {
434
+ require: ResponseMatcher{
435
+ StatusCode: []int{3, 4},
436
+ },
437
+ status: 400,
438
+ expect: true,
439
+ },
440
+ {
441
+ require: ResponseMatcher{
442
+ StatusCode: []int{3, 401},
443
+ },
444
+ status: 401,
445
+ expect: true,
446
+ },
447
+ {
448
+ require: ResponseMatcher{
449
+ Headers: http.Header{
450
+ "Foo": []string{"bar"},
451
+ },
452
+ },
453
+ hdr: http.Header{"Foo": []string{"bar"}},
454
+ expect: true,
455
+ },
456
+ {
457
+ require: ResponseMatcher{
458
+ Headers: http.Header{
459
+ "Foo2": []string{"bar"},
460
+ },
461
+ },
462
+ hdr: http.Header{"Foo": []string{"bar"}},
463
+ expect: false,
464
+ },
465
+ {
466
+ require: ResponseMatcher{
467
+ Headers: http.Header{
468
+ "Foo": []string{"bar", "baz"},
469
+ },
470
+ },
471
+ hdr: http.Header{"Foo": []string{"baz"}},
472
+ expect: true,
473
+ },
474
+ {
475
+ require: ResponseMatcher{
476
+ Headers: http.Header{
477
+ "Foo": []string{"bar"},
478
+ "Foo2": []string{"baz"},
479
+ },
480
+ },
481
+ hdr: http.Header{"Foo": []string{"baz"}},
482
+ expect: false,
483
+ },
484
+ {
485
+ require: ResponseMatcher{
486
+ Headers: http.Header{
487
+ "Foo": []string{"bar"},
488
+ "Foo2": []string{"baz"},
489
+ },
490
+ },
491
+ hdr: http.Header{"Foo": []string{"bar"}, "Foo2": []string{"baz"}},
492
+ expect: true,
493
+ },
494
+ } {
495
+ actual := tc.require.Match(tc.status, tc.hdr)
496
+ if actual != tc.expect {
497
+ t.Errorf("Test %d %v: Expected %t, got %t for HTTP %d %v", i, tc.require, tc.expect, actual, tc.status, tc.hdr)
498
+ continue
499
+ }
500
+ }
501
+}