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 {