@marcel91200 / caddy Mirror · read-only / commits / bf54615e
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 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 +}