From 8ae0d6a509fd1b871457cf742369af04346933a8 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 10 May 2019 21:07:02 -0600 Subject: caddyhttp: Implement better HTTP matchers including regexp; add tests --- modules/caddyhttp/matchers_test.go | 346 +++++++++++++++++++++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 modules/caddyhttp/matchers_test.go (limited to 'modules/caddyhttp/matchers_test.go') diff --git a/modules/caddyhttp/matchers_test.go b/modules/caddyhttp/matchers_test.go new file mode 100644 index 0000000..07c98e3 --- /dev/null +++ b/modules/caddyhttp/matchers_test.go @@ -0,0 +1,346 @@ +package caddyhttp + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" +) + +func TestHostMatcher(t *testing.T) { + for i, tc := range []struct { + match matchHost + input string + expect bool + }{ + { + match: matchHost{}, + input: "example.com", + expect: false, + }, + { + match: matchHost{"example.com"}, + input: "example.com", + expect: true, + }, + { + match: matchHost{"example.com"}, + input: "foo.example.com", + expect: false, + }, + { + match: matchHost{"example.com"}, + input: "EXAMPLE.COM", + expect: true, + }, + { + match: matchHost{"foo.example.com"}, + input: "foo.example.com", + expect: true, + }, + { + match: matchHost{"foo.example.com"}, + input: "bar.example.com", + expect: false, + }, + { + match: matchHost{"*.example.com"}, + input: "example.com", + expect: false, + }, + { + match: matchHost{"*.example.com"}, + input: "foo.example.com", + expect: true, + }, + { + match: matchHost{"*.example.com"}, + input: "foo.bar.example.com", + expect: false, + }, + { + match: matchHost{"*.example.com", "example.net"}, + input: "example.net", + expect: true, + }, + { + match: matchHost{"example.net", "*.example.com"}, + input: "foo.example.com", + expect: true, + }, + { + match: matchHost{"*.example.net", "*.*.example.com"}, + input: "foo.bar.example.com", + expect: true, + }, + { + match: matchHost{"*.example.net", "sub.*.example.com"}, + input: "sub.foo.example.com", + expect: true, + }, + { + match: matchHost{"*.example.net", "sub.*.example.com"}, + input: "sub.foo.example.net", + expect: false, + }, + } { + req := &http.Request{Host: tc.input} + actual := tc.match.Match(req) + if actual != tc.expect { + t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input) + continue + } + } +} + +func TestPathMatcher(t *testing.T) { + for i, tc := range []struct { + match matchPath + input string + expect bool + }{ + { + match: matchPath{}, + input: "/", + expect: false, + }, + { + match: matchPath{"/"}, + input: "/", + expect: true, + }, + { + match: matchPath{"/foo/bar"}, + input: "/", + expect: false, + }, + { + match: matchPath{"/foo/bar"}, + input: "/foo/bar", + expect: true, + }, + { + match: matchPath{"/foo/bar/"}, + input: "/foo/bar", + expect: false, + }, + { + match: matchPath{"/foo/bar/", "/other"}, + input: "/other/", + expect: true, + }, + } { + req := &http.Request{URL: &url.URL{Path: tc.input}} + actual := tc.match.Match(req) + if actual != tc.expect { + t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input) + continue + } + } +} + +func TestPathREMatcher(t *testing.T) { + for i, tc := range []struct { + match matchPathRE + input string + expect bool + expectRepl map[string]string + }{ + { + match: matchPathRE{}, + input: "/", + expect: true, + }, + { + match: matchPathRE{matchRegexp{Pattern: "/"}}, + input: "/", + expect: true, + }, + { + match: matchPathRE{matchRegexp{Pattern: "/foo"}}, + input: "/foo", + expect: true, + }, + { + match: matchPathRE{matchRegexp{Pattern: "/foo"}}, + input: "/foo/", + expect: true, + }, + { + match: matchPathRE{matchRegexp{Pattern: "/bar"}}, + input: "/foo/", + expect: false, + }, + { + match: matchPathRE{matchRegexp{Pattern: "^/bar"}}, + input: "/foo/bar", + expect: false, + }, + { + match: matchPathRE{matchRegexp{Pattern: "^/foo/(.*)/baz$", Name: "name"}}, + input: "/foo/bar/baz", + expect: true, + expectRepl: map[string]string{"name.1": "bar"}, + }, + { + match: matchPathRE{matchRegexp{Pattern: "^/foo/(?P.*)/baz$", Name: "name"}}, + input: "/foo/bar/baz", + expect: true, + expectRepl: map[string]string{"name.myparam": "bar"}, + }, + } { + // compile the regexp and validate its name + err := tc.match.Provision() + if err != nil { + t.Errorf("Test %d %v: Provisioning: %v", i, tc.match, err) + continue + } + err = tc.match.Validate() + if err != nil { + t.Errorf("Test %d %v: Validating: %v", i, tc.match, err) + continue + } + + // set up the fake request and its Replacer + req := &http.Request{URL: &url.URL{Path: tc.input}} + repl := &Replacer{req: req, resp: httptest.NewRecorder(), custom: make(map[string]string)} + ctx := context.WithValue(req.Context(), ReplacerCtxKey, repl) + req = req.WithContext(ctx) + + actual := tc.match.Match(req) + if actual != tc.expect { + t.Errorf("Test %d [%v]: Expected %t, got %t for input '%s'", + i, tc.match.Pattern, tc.expect, actual, tc.input) + continue + } + + for key, expectVal := range tc.expectRepl { + placeholder := fmt.Sprintf("{http.matchers.path_regexp.%s}", key) + actualVal := repl.Replace(placeholder, "") + if actualVal != expectVal { + t.Errorf("Test %d [%v]: Expected placeholder {http.matchers.path_regexp.%s} to be '%s' but got '%s'", + i, tc.match.Pattern, key, expectVal, actualVal) + continue + } + } + } +} + +func TestHeaderMatcher(t *testing.T) { + for i, tc := range []struct { + match matchHeader + input http.Header // make sure these are canonical cased (std lib will do that in a real request) + expect bool + }{ + { + match: matchHeader{"Field": []string{"foo"}}, + input: http.Header{"Field": []string{"foo"}}, + expect: true, + }, + { + match: matchHeader{"Field": []string{"foo", "bar"}}, + input: http.Header{"Field": []string{"bar"}}, + expect: true, + }, + { + match: matchHeader{"Field": []string{"foo", "bar"}}, + input: http.Header{"Alakazam": []string{"kapow"}}, + expect: false, + }, + { + match: matchHeader{"Field": []string{"foo", "bar"}}, + input: http.Header{"Field": []string{"kapow"}}, + expect: false, + }, + { + match: matchHeader{"Field": []string{"foo", "bar"}}, + input: http.Header{"Field": []string{"kapow", "foo"}}, + expect: true, + }, + { + match: matchHeader{"Field1": []string{"foo"}, "Field2": []string{"bar"}}, + input: http.Header{"Field1": []string{"foo"}, "Field2": []string{"bar"}}, + expect: true, + }, + { + match: matchHeader{"field1": []string{"foo"}, "field2": []string{"bar"}}, + input: http.Header{"Field1": []string{"foo"}, "Field2": []string{"bar"}}, + expect: true, + }, + { + match: matchHeader{"field1": []string{"foo"}, "field2": []string{"bar"}}, + input: http.Header{"Field1": []string{"foo"}, "Field2": []string{"kapow"}}, + expect: false, + }, + } { + req := &http.Request{Header: tc.input} + actual := tc.match.Match(req) + if actual != tc.expect { + t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input) + continue + } + } +} + +func TestHeaderREMatcher(t *testing.T) { + for i, tc := range []struct { + match matchHeaderRE + input http.Header // make sure these are canonical cased (std lib will do that in a real request) + expect bool + expectRepl map[string]string + }{ + { + match: matchHeaderRE{"Field": &matchRegexp{Pattern: "foo"}}, + input: http.Header{"Field": []string{"foo"}}, + expect: true, + }, + { + match: matchHeaderRE{"Field": &matchRegexp{Pattern: "$foo^"}}, + input: http.Header{"Field": []string{"foobar"}}, + expect: false, + }, + { + match: matchHeaderRE{"Field": &matchRegexp{Pattern: "^foo(.*)$", Name: "name"}}, + input: http.Header{"Field": []string{"foobar"}}, + expect: true, + expectRepl: map[string]string{"name.1": "bar"}, + }, + } { + // compile the regexp and validate its name + err := tc.match.Provision() + if err != nil { + t.Errorf("Test %d %v: Provisioning: %v", i, tc.match, err) + continue + } + err = tc.match.Validate() + if err != nil { + t.Errorf("Test %d %v: Validating: %v", i, tc.match, err) + continue + } + + // set up the fake request and its Replacer + req := &http.Request{Header: tc.input, URL: new(url.URL)} + repl := &Replacer{req: req, resp: httptest.NewRecorder(), custom: make(map[string]string)} + ctx := context.WithValue(req.Context(), ReplacerCtxKey, repl) + req = req.WithContext(ctx) + + actual := tc.match.Match(req) + if actual != tc.expect { + t.Errorf("Test %d [%v]: Expected %t, got %t for input '%s'", + i, tc.match, tc.expect, actual, tc.input) + continue + } + + for key, expectVal := range tc.expectRepl { + placeholder := fmt.Sprintf("{http.matchers.header_regexp.%s}", key) + actualVal := repl.Replace(placeholder, "") + if actualVal != expectVal { + t.Errorf("Test %d [%v]: Expected placeholder {http.matchers.header_regexp.%s} to be '%s' but got '%s'", + i, tc.match, key, expectVal, actualVal) + continue + } + } + } +} -- cgit v1.2.3