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

caddyhttp: Refactor header matching

This allows response matchers to benefit from the same matching logic as the request header matchers (mainly prefix/suffix wildcards).

Matthew Holt committed May 26, 2020 at 17:35 UTC e5bbed10461f8baa3c75e4614edb8d44d3252bb5
2 files changed +61 -45
modules/caddyhttp/matchers.go
+28 -45
@@ -17,7 +17,6 @@ package caddyhttp
17 import (
18 "encoding/json"
19 "fmt"
20 - "log"
20 "net"
21 "net/http"
22 "net/textproto"
@@ -28,6 +27,7 @@ import (
27
28 "github.com/caddyserver/caddy/v2"
29 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
30 + "go.uber.org/zap"
31 )
32
33 type (
@@ -105,7 +105,8 @@ type (
105 MatchRemoteIP struct {
106 Ranges []string `json:"ranges,omitempty"`
107
108 - cidrs []*net.IPNet
108 + cidrs []*net.IPNet
109 + logger *zap.Logger
110 }
111
112 // MatchNot matches requests by negating the results of its matcher
@@ -410,23 +411,28 @@ func (m *MatchHeader) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
411 return nil
412 }
413
413 -// Like req.Header.Get(), but that works with Host header.
414 -// go's http module swallows "Host" header.
415 -func getHeader(r *http.Request, field string) []string {
416 - field = textproto.CanonicalMIMEHeaderKey(field)
414 +// Match returns true if r matches m.
415 +func (m MatchHeader) Match(r *http.Request) bool {
416 + return matchHeaders(r.Header, http.Header(m), r.Host)
417 +}
418
418 - if field == "Host" {
419 - return []string{r.Host}
419 +// getHeaderFieldVals returns the field values for the given fieldName from input.
420 +// The host parameter should be obtained from the http.Request.Host field since
421 +// net/http removes it from the header map.
422 +func getHeaderFieldVals(input http.Header, fieldName, host string) []string {
423 + fieldName = textproto.CanonicalMIMEHeaderKey(fieldName)
424 + if fieldName == "Host" && host != "" {
425 + return []string{host}
426 }
421 -
422 - return r.Header[field]
427 + return input[fieldName]
428 }
429
425 -// Match returns true if r matches m.
426 -func (m MatchHeader) Match(r *http.Request) bool {
427 - for field, allowedFieldVals := range m {
428 - actualFieldVals := getHeader(r, field)
429 -
430 +// matchHeaders returns true if input matches the criteria in against without regex.
431 +// The host parameter should be obtained from the http.Request.Host field since
432 +// net/http removes it from the header map.
433 +func matchHeaders(input, against http.Header, host string) bool {
434 + for field, allowedFieldVals := range against {
435 + actualFieldVals := getHeaderFieldVals(input, field, host)
436 if allowedFieldVals != nil && len(allowedFieldVals) == 0 && actualFieldVals != nil {
437 // a non-nil but empty list of allowed values means
438 // match if the header field exists at all
@@ -501,8 +507,7 @@ func (m *MatchHeaderRE) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
507 // Match returns true if r matches m.
508 func (m MatchHeaderRE) Match(r *http.Request) bool {
509 for field, rm := range m {
504 - actualFieldVals := getHeader(r, field)
505 -
510 + actualFieldVals := getHeaderFieldVals(r.Header, field, r.Host)
511 match := false
512 fieldVal:
513 for _, actualFieldVal := range actualFieldVals {
@@ -700,6 +705,7 @@ func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
705
706 // Provision parses m's IP ranges, either from IP or CIDR expressions.
707 func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
708 + m.logger = ctx.Logger(m)
709 for _, str := range m.Ranges {
710 if strings.Contains(str, "/") {
711 _, ipNet, err := net.ParseCIDR(str)
@@ -748,7 +754,7 @@ func (m MatchRemoteIP) getClientIP(r *http.Request) (net.IP, error) {
754 func (m MatchRemoteIP) Match(r *http.Request) bool {
755 clientIP, err := m.getClientIP(r)
756 if err != nil {
751 - log.Printf("[ERROR] remote_ip matcher: %v", err)
757 + m.logger.Error("getting client IP", zap.Error(err))
758 return false
759 }
760 for _, ipRange := range m.cidrs {
@@ -859,7 +865,9 @@ type ResponseMatcher struct {
865 // in that class (e.g. 3 for all 3xx codes).
866 StatusCode []int `json:"status_code,omitempty"`
867
862 - // If set, each header specified must be one of the specified values.
868 + // If set, each header specified must be one of the
869 + // specified values, with the same logic used by the
870 + // request header matcher.
871 Headers http.Header `json:"headers,omitempty"`
872 }
873
@@ -868,7 +876,7 @@ func (rm ResponseMatcher) Match(statusCode int, hdr http.Header) bool {
876 if !rm.matchStatusCode(statusCode) {
877 return false
878 }
871 - return rm.matchHeaders(hdr)
879 + return matchHeaders(hdr, rm.Headers, "")
880 }
881
882 func (rm ResponseMatcher) matchStatusCode(statusCode int) bool {
@@ -883,31 +891,6 @@ func (rm ResponseMatcher) matchStatusCode(statusCode int) bool {
891 return false
892 }
893
886 -func (rm ResponseMatcher) matchHeaders(hdr http.Header) bool {
887 - for field, allowedFieldVals := range rm.Headers {
888 - actualFieldVals, fieldExists := hdr[textproto.CanonicalMIMEHeaderKey(field)]
889 - if allowedFieldVals != nil && len(allowedFieldVals) == 0 && fieldExists {
890 - // a non-nil but empty list of allowed values means
891 - // match if the header field exists at all
892 - continue
893 - }
894 - var match bool
895 - fieldVals:
896 - for _, actualFieldVal := range actualFieldVals {
897 - for _, allowedFieldVal := range allowedFieldVals {
898 - if actualFieldVal == allowedFieldVal {
899 - match = true
900 - break fieldVals
901 - }
902 - }
903 - }
904 - if !match {
905 - return false
906 - }
907 - }
908 - return true
909 -}
910 -
894 var wordRE = regexp.MustCompile(`\w+`)
895
896 const regexpPlaceholderPrefix = "http.regexp"
modules/caddyhttp/matchers_test.go
+33
@@ -448,6 +448,21 @@ func TestHeaderMatcher(t *testing.T) {
448 input: http.Header{"Field2": []string{"foo"}},
449 expect: false,
450 },
451 + {
452 + match: MatchHeader{"Field1": []string{"foo*"}},
453 + input: http.Header{"Field1": []string{"foo"}},
454 + expect: true,
455 + },
456 + {
457 + match: MatchHeader{"Field1": []string{"foo*"}},
458 + input: http.Header{"Field1": []string{"asdf", "foobar"}},
459 + expect: true,
460 + },
461 + {
462 + match: MatchHeader{"Field1": []string{"*bar"}},
463 + input: http.Header{"Field1": []string{"asdf", "foobar"}},
464 + expect: true,
465 + },
466 {
467 match: MatchHeader{"host": []string{"localhost"}},
468 input: http.Header{},
@@ -814,6 +829,24 @@ func TestResponseMatcher(t *testing.T) {
829 hdr: http.Header{"Foo": []string{"bar"}, "Foo2": []string{"baz"}},
830 expect: true,
831 },
832 + {
833 + require: ResponseMatcher{
834 + Headers: http.Header{
835 + "Foo": []string{"foo*"},
836 + },
837 + },
838 + hdr: http.Header{"Foo": []string{"foobar"}},
839 + expect: true,
840 + },
841 + {
842 + require: ResponseMatcher{
843 + Headers: http.Header{
844 + "Foo": []string{"foo*"},
845 + },
846 + },
847 + hdr: http.Header{"Foo": []string{"foobar"}},
848 + expect: true,
849 + },
850 } {
851 actual := tc.require.Match(tc.status, tc.hdr)
852 if actual != tc.expect {