diff options
author | Abiola Ibrahim <[email protected]> | 2016-06-21 15:59:29 +0100 |
---|---|---|
committer | Matt Holt <[email protected]> | 2016-06-21 08:59:29 -0600 |
commit | d9b6563d88420f5b52bc7e9a835dbef6d88aaf23 (patch) | |
tree | a3296e547ff5926b7c4b6d6f2cc745d669167c68 | |
parent | 0a3f68f0d7c8be3e940b1cda58951ea3629d4620 (diff) | |
download | caddy-d9b6563d88420f5b52bc7e9a835dbef6d88aaf23.tar.gz caddy-d9b6563d88420f5b52bc7e9a835dbef6d88aaf23.zip |
Condition upgrades (if, if_op) for rewrite, redir (#889)
* checkpoint
* Added RequestMatcher interface. Extract 'if' condition into a RequestMatcher.
* Added tests for IfMatcher
* Minor refactors
* Refactors
* Use if_op
* conform with new 0.9 beta function changes.
-rw-r--r-- | caddyhttp/httpserver/condition.go | 199 | ||||
-rw-r--r-- | caddyhttp/httpserver/condition_test.go | 265 | ||||
-rw-r--r-- | caddyhttp/httpserver/middleware.go | 28 | ||||
-rw-r--r-- | caddyhttp/redirect/redirect.go | 3 | ||||
-rw-r--r-- | caddyhttp/redirect/redirect_test.go | 18 | ||||
-rw-r--r-- | caddyhttp/redirect/setup.go | 16 | ||||
-rw-r--r-- | caddyhttp/rewrite/condition.go | 130 | ||||
-rw-r--r-- | caddyhttp/rewrite/condition_test.go | 106 | ||||
-rw-r--r-- | caddyhttp/rewrite/rewrite.go | 30 | ||||
-rw-r--r-- | caddyhttp/rewrite/rewrite_test.go | 4 | ||||
-rw-r--r-- | caddyhttp/rewrite/setup.go | 23 | ||||
-rw-r--r-- | caddyhttp/rewrite/setup_test.go | 11 |
12 files changed, 546 insertions, 287 deletions
diff --git a/caddyhttp/httpserver/condition.go b/caddyhttp/httpserver/condition.go new file mode 100644 index 000000000..0c65a5052 --- /dev/null +++ b/caddyhttp/httpserver/condition.go @@ -0,0 +1,199 @@ +package httpserver + +import ( + "fmt" + "net/http" + "regexp" + "strings" + + "github.com/mholt/caddy/caddyfile" +) + +// SetupIfMatcher parses `if` or `if_type` in the current dispenser block. +// It returns a RequestMatcher and an error if any. +func SetupIfMatcher(c caddyfile.Dispenser) (RequestMatcher, error) { + var matcher IfMatcher + for c.NextBlock() { + switch c.Val() { + case "if": + args1 := c.RemainingArgs() + if len(args1) != 3 { + return matcher, c.ArgErr() + } + ifc, err := newIfCond(args1[0], args1[1], args1[2]) + if err != nil { + return matcher, err + } + matcher.ifs = append(matcher.ifs, ifc) + case "if_op": + if !c.NextArg() { + return matcher, c.ArgErr() + } + switch c.Val() { + case "and": + matcher.isOr = false + case "or": + matcher.isOr = true + default: + return matcher, c.ArgErr() + } + } + } + return matcher, nil +} + +// operators +const ( + isOp = "is" + notOp = "not" + hasOp = "has" + notHasOp = "not_has" + startsWithOp = "starts_with" + endsWithOp = "ends_with" + matchOp = "match" + notMatchOp = "not_match" +) + +func operatorError(operator string) error { + return fmt.Errorf("Invalid operator %v", operator) +} + +// ifCondition is a 'if' condition. +type ifCondition func(string, string) bool + +var ifConditions = map[string]ifCondition{ + isOp: isFunc, + notOp: notFunc, + hasOp: hasFunc, + notHasOp: notHasFunc, + startsWithOp: startsWithFunc, + endsWithOp: endsWithFunc, + matchOp: matchFunc, + notMatchOp: notMatchFunc, +} + +// isFunc is condition for Is operator. +// It checks for equality. +func isFunc(a, b string) bool { + return a == b +} + +// notFunc is condition for Not operator. +// It checks for inequality. +func notFunc(a, b string) bool { + return a != b +} + +// hasFunc is condition for Has operator. +// It checks if b is a substring of a. +func hasFunc(a, b string) bool { + return strings.Contains(a, b) +} + +// notHasFunc is condition for NotHas operator. +// It checks if b is not a substring of a. +func notHasFunc(a, b string) bool { + return !strings.Contains(a, b) +} + +// startsWithFunc is condition for StartsWith operator. +// It checks if b is a prefix of a. +func startsWithFunc(a, b string) bool { + return strings.HasPrefix(a, b) +} + +// endsWithFunc is condition for EndsWith operator. +// It checks if b is a suffix of a. +func endsWithFunc(a, b string) bool { + return strings.HasSuffix(a, b) +} + +// matchFunc is condition for Match operator. +// It does regexp matching of a against pattern in b +// and returns if they match. +func matchFunc(a, b string) bool { + matched, _ := regexp.MatchString(b, a) + return matched +} + +// notMatchFunc is condition for NotMatch operator. +// It does regexp matching of a against pattern in b +// and returns if they do not match. +func notMatchFunc(a, b string) bool { + matched, _ := regexp.MatchString(b, a) + return !matched +} + +// ifCond is statement for a IfMatcher condition. +type ifCond struct { + a string + op string + b string +} + +// newIfCond creates a new If condition. +func newIfCond(a, operator, b string) (ifCond, error) { + if _, ok := ifConditions[operator]; !ok { + return ifCond{}, operatorError(operator) + } + return ifCond{ + a: a, + op: operator, + b: b, + }, nil +} + +// True returns true if the condition is true and false otherwise. +// If r is not nil, it replaces placeholders before comparison. +func (i ifCond) True(r *http.Request) bool { + if c, ok := ifConditions[i.op]; ok { + a, b := i.a, i.b + if r != nil { + replacer := NewReplacer(r, nil, "") + a = replacer.Replace(i.a) + b = replacer.Replace(i.b) + } + return c(a, b) + } + return false +} + +// IfMatcher is a RequestMatcher for 'if' conditions. +type IfMatcher struct { + ifs []ifCond // list of If + isOr bool // if true, conditions are 'or' instead of 'and' +} + +// Match satisfies RequestMatcher interface. +// It returns true if the conditions in m are true. +func (m IfMatcher) Match(r *http.Request) bool { + if m.isOr { + return m.Or(r) + } + return m.And(r) +} + +// And returns true if all conditions in m are true. +func (m IfMatcher) And(r *http.Request) bool { + for _, i := range m.ifs { + if !i.True(r) { + return false + } + } + return true +} + +// Or returns true if any of the conditions in m is true. +func (m IfMatcher) Or(r *http.Request) bool { + for _, i := range m.ifs { + if i.True(r) { + return true + } + } + return false +} + +// IfMatcherKeyword returns if k is a keyword for 'if' config block. +func IfMatcherKeyword(k string) bool { + return k == "if" || k == "if_cond" +} diff --git a/caddyhttp/httpserver/condition_test.go b/caddyhttp/httpserver/condition_test.go new file mode 100644 index 000000000..b64858b73 --- /dev/null +++ b/caddyhttp/httpserver/condition_test.go @@ -0,0 +1,265 @@ +package httpserver + +import ( + "fmt" + "net/http" + "strings" + "testing" + + "github.com/mholt/caddy" +) + +func TestConditions(t *testing.T) { + tests := []struct { + condition string + isTrue bool + }{ + {"a is b", false}, + {"a is a", true}, + {"a not b", true}, + {"a not a", false}, + {"a has a", true}, + {"a has b", false}, + {"ba has b", true}, + {"bab has b", true}, + {"bab has bb", false}, + {"a not_has a", false}, + {"a not_has b", true}, + {"ba not_has b", false}, + {"bab not_has b", false}, + {"bab not_has bb", true}, + {"bab starts_with bb", false}, + {"bab starts_with ba", true}, + {"bab starts_with bab", true}, + {"bab ends_with bb", false}, + {"bab ends_with bab", true}, + {"bab ends_with ab", true}, + {"a match *", false}, + {"a match a", true}, + {"a match .*", true}, + {"a match a.*", true}, + {"a match b.*", false}, + {"ba match b.*", true}, + {"ba match b[a-z]", true}, + {"b0 match b[a-z]", false}, + {"b0a match b[a-z]", false}, + {"b0a match b[a-z]+", false}, + {"b0a match b[a-z0-9]+", true}, + {"a not_match *", true}, + {"a not_match a", false}, + {"a not_match .*", false}, + {"a not_match a.*", false}, + {"a not_match b.*", true}, + {"ba not_match b.*", false}, + {"ba not_match b[a-z]", false}, + {"b0 not_match b[a-z]", true}, + {"b0a not_match b[a-z]", true}, + {"b0a not_match b[a-z]+", true}, + {"b0a not_match b[a-z0-9]+", false}, + } + + for i, test := range tests { + str := strings.Fields(test.condition) + ifCond, err := newIfCond(str[0], str[1], str[2]) + if err != nil { + t.Error(err) + } + isTrue := ifCond.True(nil) + if isTrue != test.isTrue { + t.Errorf("Test %d: expected %v found %v", i, test.isTrue, isTrue) + } + } + + invalidOperators := []string{"ss", "and", "if"} + for _, op := range invalidOperators { + _, err := newIfCond("a", op, "b") + if err == nil { + t.Errorf("Invalid operator %v used, expected error.", op) + } + } + + replaceTests := []struct { + url string + condition string + isTrue bool + }{ + {"/home", "{uri} match /home", true}, + {"/hom", "{uri} match /home", false}, + {"/hom", "{uri} starts_with /home", false}, + {"/hom", "{uri} starts_with /h", true}, + {"/home/.hiddenfile", `{uri} match \/\.(.*)`, true}, + {"/home/.hiddendir/afile", `{uri} match \/\.(.*)`, true}, + } + + for i, test := range replaceTests { + r, err := http.NewRequest("GET", test.url, nil) + if err != nil { + t.Error(err) + } + str := strings.Fields(test.condition) + ifCond, err := newIfCond(str[0], str[1], str[2]) + if err != nil { + t.Error(err) + } + isTrue := ifCond.True(r) + if isTrue != test.isTrue { + t.Errorf("Test %v: expected %v found %v", i, test.isTrue, isTrue) + } + } +} + +func TestIfMatcher(t *testing.T) { + tests := []struct { + conditions []string + isOr bool + isTrue bool + }{ + { + []string{ + "a is a", + "b is b", + "c is c", + }, + false, + true, + }, + { + []string{ + "a is b", + "b is c", + "c is c", + }, + true, + true, + }, + { + []string{ + "a is a", + "b is a", + "c is c", + }, + false, + false, + }, + { + []string{ + "a is b", + "b is c", + "c is a", + }, + true, + false, + }, + { + []string{}, + false, + true, + }, + { + []string{}, + true, + false, + }, + } + + for i, test := range tests { + matcher := IfMatcher{isOr: test.isOr} + for _, condition := range test.conditions { + str := strings.Fields(condition) + ifCond, err := newIfCond(str[0], str[1], str[2]) + if err != nil { + t.Error(err) + } + matcher.ifs = append(matcher.ifs, ifCond) + } + isTrue := matcher.Match(nil) + if isTrue != test.isTrue { + t.Errorf("Test %d: expected %v found %v", i, test.isTrue, isTrue) + } + } +} + +func TestSetupIfMatcher(t *testing.T) { + tests := []struct { + input string + shouldErr bool + expected IfMatcher + }{ + {`test { + if a match b + }`, false, IfMatcher{ + ifs: []ifCond{ + {a: "a", op: "match", b: "b"}, + }, + }}, + {`test { + if a match b + if_op or + }`, false, IfMatcher{ + ifs: []ifCond{ + {a: "a", op: "match", b: "b"}, + }, + isOr: true, + }}, + {`test { + if a match + }`, true, IfMatcher{}, + }, + {`test { + if a isnt b + }`, true, IfMatcher{}, + }, + {`test { + if a match b c + }`, true, IfMatcher{}, + }, + {`test { + if goal has go + if cook not_has go + }`, false, IfMatcher{ + ifs: []ifCond{ + {a: "goal", op: "has", b: "go"}, + {a: "cook", op: "not_has", b: "go"}, + }, + }}, + {`test { + if goal has go + if cook not_has go + if_op and + }`, false, IfMatcher{ + ifs: []ifCond{ + {a: "goal", op: "has", b: "go"}, + {a: "cook", op: "not_has", b: "go"}, + }, + }}, + {`test { + if goal has go + if cook not_has go + if_op not + }`, true, IfMatcher{}, + }, + } + + for i, test := range tests { + c := caddy.NewTestController("http", test.input) + c.Next() + matcher, err := SetupIfMatcher(c.Dispenser) + if err == nil && test.shouldErr { + t.Errorf("Test %d didn't error, but it should have", i) + } else if err != nil && !test.shouldErr { + t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err) + } else if err != nil && test.shouldErr { + continue + } + if _, ok := matcher.(IfMatcher); !ok { + t.Error("RequestMatcher should be of type IfMatcher") + } + if err != nil { + t.Errorf("Expected no error, but got: %v", err) + } + if fmt.Sprint(matcher) != fmt.Sprint(test.expected) { + t.Errorf("Test %v: Expected %v, found %v", i, + fmt.Sprint(test.expected), fmt.Sprint(matcher)) + } + } +} diff --git a/caddyhttp/httpserver/middleware.go b/caddyhttp/httpserver/middleware.go index e5e70de42..42de390ec 100644 --- a/caddyhttp/httpserver/middleware.go +++ b/caddyhttp/httpserver/middleware.go @@ -45,6 +45,16 @@ type ( // ServeHTTP returns a status code and an error. See Handler // documentation for more information. HandlerFunc func(http.ResponseWriter, *http.Request) (int, error) + + // RequestMatcher checks to see if current request should be handled + // by underlying handler. + // + // TODO The long term plan is to get all middleware implement this + // interface and have validation done before requests are dispatched + // to each middleware. + RequestMatcher interface { + Match(r *http.Request) bool + } ) // ServeHTTP implements the Handler interface. @@ -135,6 +145,24 @@ func (p Path) Matches(other string) bool { return strings.HasPrefix(strings.ToLower(string(p)), strings.ToLower(other)) } +// MergeRequestMatchers merges multiple RequestMatchers into one. +// This allows a middleware to use multiple RequestMatchers. +func MergeRequestMatchers(matchers ...RequestMatcher) RequestMatcher { + return requestMatchers(matchers) +} + +type requestMatchers []RequestMatcher + +// Match satisfies RequestMatcher interface. +func (m requestMatchers) Match(r *http.Request) bool { + for _, matcher := range m { + if !matcher.Match(r) { + return false + } + } + return true +} + // currentTime, as it is defined here, returns time.Now(). // It's defined as a variable for mocking time in tests. var currentTime = func() time.Time { return time.Now() } diff --git a/caddyhttp/redirect/redirect.go b/caddyhttp/redirect/redirect.go index edb7caea5..a489e7357 100644 --- a/caddyhttp/redirect/redirect.go +++ b/caddyhttp/redirect/redirect.go @@ -19,7 +19,7 @@ type Redirect struct { // ServeHTTP implements the httpserver.Handler interface. func (rd Redirect) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { for _, rule := range rd.Rules { - if (rule.FromPath == "/" || r.URL.Path == rule.FromPath) && schemeMatches(rule, r) { + if (rule.FromPath == "/" || r.URL.Path == rule.FromPath) && schemeMatches(rule, r) && rule.Match(r) { to := httpserver.NewReplacer(r, nil, "").Replace(rule.To) if rule.Meta { safeTo := html.EscapeString(to) @@ -43,6 +43,7 @@ type Rule struct { FromScheme, FromPath, To string Code int Meta bool + httpserver.RequestMatcher } // Script tag comes first since that will better imitate a redirect in the browser's diff --git a/caddyhttp/redirect/redirect_test.go b/caddyhttp/redirect/redirect_test.go index b6f8f74d0..27998abec 100644 --- a/caddyhttp/redirect/redirect_test.go +++ b/caddyhttp/redirect/redirect_test.go @@ -47,16 +47,16 @@ func TestRedirect(t *testing.T) { return 0, nil }), Rules: []Rule{ - {FromPath: "/from", To: "/to", Code: http.StatusMovedPermanently}, - {FromPath: "/a", To: "/b", Code: http.StatusTemporaryRedirect}, + {FromPath: "/from", To: "/to", Code: http.StatusMovedPermanently, RequestMatcher: httpserver.IfMatcher{}}, + {FromPath: "/a", To: "/b", Code: http.StatusTemporaryRedirect, RequestMatcher: httpserver.IfMatcher{}}, // These http and https schemes would never actually be mixed in the same // redirect rule with Caddy because http and https schemes have different listeners, // so they don't share a redirect rule. So although these tests prove something // impossible with Caddy, it's extra bulletproofing at very little cost. - {FromScheme: "http", FromPath: "/scheme", To: "https://localhost/scheme", Code: http.StatusMovedPermanently}, - {FromScheme: "https", FromPath: "/scheme2", To: "http://localhost/scheme2", Code: http.StatusMovedPermanently}, - {FromScheme: "", FromPath: "/scheme3", To: "https://localhost/scheme3", Code: http.StatusMovedPermanently}, + {FromScheme: "http", FromPath: "/scheme", To: "https://localhost/scheme", Code: http.StatusMovedPermanently, RequestMatcher: httpserver.IfMatcher{}}, + {FromScheme: "https", FromPath: "/scheme2", To: "http://localhost/scheme2", Code: http.StatusMovedPermanently, RequestMatcher: httpserver.IfMatcher{}}, + {FromScheme: "", FromPath: "/scheme3", To: "https://localhost/scheme3", Code: http.StatusMovedPermanently, RequestMatcher: httpserver.IfMatcher{}}, }, } @@ -90,7 +90,7 @@ func TestRedirect(t *testing.T) { func TestParametersRedirect(t *testing.T) { re := Redirect{ Rules: []Rule{ - {FromPath: "/", Meta: false, To: "http://example.com{uri}"}, + {FromPath: "/", Meta: false, To: "http://example.com{uri}", RequestMatcher: httpserver.IfMatcher{}}, }, } @@ -108,7 +108,7 @@ func TestParametersRedirect(t *testing.T) { re = Redirect{ Rules: []Rule{ - {FromPath: "/", Meta: false, To: "http://example.com/a{path}?b=c&{query}"}, + {FromPath: "/", Meta: false, To: "http://example.com/a{path}?b=c&{query}", RequestMatcher: httpserver.IfMatcher{}}, }, } @@ -127,8 +127,8 @@ func TestParametersRedirect(t *testing.T) { func TestMetaRedirect(t *testing.T) { re := Redirect{ Rules: []Rule{ - {FromPath: "/whatever", Meta: true, To: "/something"}, - {FromPath: "/", Meta: true, To: "https://example.com/"}, + {FromPath: "/whatever", Meta: true, To: "/something", RequestMatcher: httpserver.IfMatcher{}}, + {FromPath: "/", Meta: true, To: "https://example.com/", RequestMatcher: httpserver.IfMatcher{}}, }, } diff --git a/caddyhttp/redirect/setup.go b/caddyhttp/redirect/setup.go index d45d2b609..b1f01254a 100644 --- a/caddyhttp/redirect/setup.go +++ b/caddyhttp/redirect/setup.go @@ -63,13 +63,23 @@ func redirParse(c *caddy.Controller) ([]Rule, error) { } for c.Next() { + matcher, err := httpserver.SetupIfMatcher(c.Dispenser) + if err != nil { + return nil, err + } args := c.RemainingArgs() var hadOptionalBlock bool for c.NextBlock() { + if httpserver.IfMatcherKeyword(c.Val()) { + continue + } + hadOptionalBlock = true - var rule Rule + var rule = Rule{ + RequestMatcher: matcher, + } if cfg.TLS.Enabled { rule.FromScheme = "https" @@ -126,7 +136,9 @@ func redirParse(c *caddy.Controller) ([]Rule, error) { } if !hadOptionalBlock { - var rule Rule + var rule = Rule{ + RequestMatcher: matcher, + } if cfg.TLS.Enabled { rule.FromScheme = "https" diff --git a/caddyhttp/rewrite/condition.go b/caddyhttp/rewrite/condition.go deleted file mode 100644 index 97b0e96aa..000000000 --- a/caddyhttp/rewrite/condition.go +++ /dev/null @@ -1,130 +0,0 @@ -package rewrite - -import ( - "fmt" - "net/http" - "regexp" - "strings" - - "github.com/mholt/caddy/caddyhttp/httpserver" -) - -// Operators -const ( - Is = "is" - Not = "not" - Has = "has" - NotHas = "not_has" - StartsWith = "starts_with" - EndsWith = "ends_with" - Match = "match" - NotMatch = "not_match" -) - -func operatorError(operator string) error { - return fmt.Errorf("Invalid operator %v", operator) -} - -func newReplacer(r *http.Request) httpserver.Replacer { - return httpserver.NewReplacer(r, nil, "") -} - -// condition is a rewrite condition. -type condition func(string, string) bool - -var conditions = map[string]condition{ - Is: isFunc, - Not: notFunc, - Has: hasFunc, - NotHas: notHasFunc, - StartsWith: startsWithFunc, - EndsWith: endsWithFunc, - Match: matchFunc, - NotMatch: notMatchFunc, -} - -// isFunc is condition for Is operator. -// It checks for equality. -func isFunc(a, b string) bool { - return a == b -} - -// notFunc is condition for Not operator. -// It checks for inequality. -func notFunc(a, b string) bool { - return a != b -} - -// hasFunc is condition for Has operator. -// It checks if b is a substring of a. -func hasFunc(a, b string) bool { - return strings.Contains(a, b) -} - -// notHasFunc is condition for NotHas operator. -// It checks if b is not a substring of a. -func notHasFunc(a, b string) bool { - return !strings.Contains(a, b) -} - -// startsWithFunc is condition for StartsWith operator. -// It checks if b is a prefix of a. -func startsWithFunc(a, b string) bool { - return strings.HasPrefix(a, b) -} - -// endsWithFunc is condition for EndsWith operator. -// It checks if b is a suffix of a. -func endsWithFunc(a, b string) bool { - return strings.HasSuffix(a, b) -} - -// matchFunc is condition for Match operator. -// It does regexp matching of a against pattern in b -// and returns if they match. -func matchFunc(a, b string) bool { - matched, _ := regexp.MatchString(b, a) - return matched -} - -// notMatchFunc is condition for NotMatch operator. -// It does regexp matching of a against pattern in b -// and returns if they do not match. -func notMatchFunc(a, b string) bool { - matched, _ := regexp.MatchString(b, a) - return !matched -} - -// If is statement for a rewrite condition. -type If struct { - A string - Operator string - B string -} - -// True returns true if the condition is true and false otherwise. -// If r is not nil, it replaces placeholders before comparison. -func (i If) True(r *http.Request) bool { - if c, ok := conditions[i.Operator]; ok { - a, b := i.A, i.B - if r != nil { - replacer := newReplacer(r) - a = replacer.Replace(i.A) - b = replacer.Replace(i.B) - } - return c(a, b) - } - return false -} - -// NewIf creates a new If condition. -func NewIf(a, operator, b string) (If, error) { - if _, ok := conditions[operator]; !ok { - return If{}, operatorError(operator) - } - return If{ - A: a, - Operator: operator, - B: b, - }, nil -} diff --git a/caddyhttp/rewrite/condition_test.go b/caddyhttp/rewrite/condition_test.go deleted file mode 100644 index 3c3b6053a..000000000 --- a/caddyhttp/rewrite/condition_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package rewrite - -import ( - "net/http" - "strings" - "testing" -) - -func TestConditions(t *testing.T) { - tests := []struct { - condition string - isTrue bool - }{ - {"a is b", false}, - {"a is a", true}, - {"a not b", true}, - {"a not a", false}, - {"a has a", true}, - {"a has b", false}, - {"ba has b", true}, - {"bab has b", true}, - {"bab has bb", false}, - {"a not_has a", false}, - {"a not_has b", true}, - {"ba not_has b", false}, - {"bab not_has b", false}, - {"bab not_has bb", true}, - {"bab starts_with bb", false}, - {"bab starts_with ba", true}, - {"bab starts_with bab", true}, - {"bab ends_with bb", false}, - {"bab ends_with bab", true}, - {"bab ends_with ab", true}, - {"a match *", false}, - {"a match a", true}, - {"a match .*", true}, - {"a match a.*", true}, - {"a match b.*", false}, - {"ba match b.*", true}, - {"ba match b[a-z]", true}, - {"b0 match b[a-z]", false}, - {"b0a match b[a-z]", false}, - {"b0a match b[a-z]+", false}, - {"b0a match b[a-z0-9]+", true}, - {"a not_match *", true}, - {"a not_match a", false}, - {"a not_match .*", false}, - {"a not_match a.*", false}, - {"a not_match b.*", true}, - {"ba not_match b.*", false}, - {"ba not_match b[a-z]", false}, - {"b0 not_match b[a-z]", true}, - {"b0a not_match b[a-z]", true}, - {"b0a not_match b[a-z]+", true}, - {"b0a not_match b[a-z0-9]+", false}, - } - - for i, test := range tests { - str := strings.Fields(test.condition) - ifCond, err := NewIf(str[0], str[1], str[2]) - if err != nil { - t.Error(err) - } - isTrue := ifCond.True(nil) - if isTrue != test.isTrue { - t.Errorf("Test %v: expected %v found %v", i, test.isTrue, isTrue) - } - } - - invalidOperators := []string{"ss", "and", "if"} - for _, op := range invalidOperators { - _, err := NewIf("a", op, "b") - if err == nil { - t.Errorf("Invalid operator %v used, expected error.", op) - } - } - - replaceTests := []struct { - url string - condition string - isTrue bool - }{ - {"/home", "{uri} match /home", true}, - {"/hom", "{uri} match /home", false}, - {"/hom", "{uri} starts_with /home", false}, - {"/hom", "{uri} starts_with /h", true}, - {"/home/.hiddenfile", `{uri} match \/\.(.*)`, true}, - {"/home/.hiddendir/afile", `{uri} match \/\.(.*)`, true}, - } - - for i, test := range replaceTests { - r, err := http.NewRequest("GET", test.url, nil) - if err != nil { - t.Error(err) - } - str := strings.Fields(test.condition) - ifCond, err := NewIf(str[0], str[1], str[2]) - if err != nil { - t.Error(err) - } - isTrue := ifCond.True(r) - if isTrue != test.isTrue { - t.Errorf("Test %v: expected %v found %v", i, test.isTrue, isTrue) - } - } -} diff --git a/caddyhttp/rewrite/rewrite.go b/caddyhttp/rewrite/rewrite.go index 7567f5d85..dde85aeb7 100644 --- a/caddyhttp/rewrite/rewrite.go +++ b/caddyhttp/rewrite/rewrite.go @@ -97,15 +97,15 @@ type ComplexRule struct { // Extensions to filter by Exts []string - // Rewrite conditions - Ifs []If + // Request matcher + httpserver.RequestMatcher *regexp.Regexp } // NewComplexRule creates a new RegexpRule. It returns an error if regexp // pattern (pattern) or extensions (ext) are invalid. -func NewComplexRule(base, pattern, to string, status int, ext []string, ifs []If) (*ComplexRule, error) { +func NewComplexRule(base, pattern, to string, status int, ext []string, m httpserver.RequestMatcher) (*ComplexRule, error) { // validate regexp if present var r *regexp.Regexp if pattern != "" { @@ -127,12 +127,12 @@ func NewComplexRule(base, pattern, to string, status int, ext []string, ifs []If } return &ComplexRule{ - Base: base, - To: to, - Status: status, - Exts: ext, - Ifs: ifs, - Regexp: r, + Base: base, + To: to, + Status: status, + Exts: ext, + RequestMatcher: m, + Regexp: r, }, nil } @@ -182,11 +182,9 @@ func (r *ComplexRule) Rewrite(fs http.FileSystem, req *http.Request) (re Result) } } - // validate rewrite conditions - for _, i := range r.Ifs { - if !i.True(req) { - return - } + // validate if conditions + if !r.RequestMatcher.Match(req) { + return } // if status is present, stop rewrite and return it. @@ -230,6 +228,10 @@ func (r *ComplexRule) matchExt(rPath string) bool { return true } +func newReplacer(r *http.Request) httpserver.Replacer { + return httpserver.NewReplacer(r, nil, "") +} + // When a rewrite is performed, this header is added to the request // and is for internal use only, specifically the fastcgi middleware. // It contains the original request URI before the rewrite. diff --git a/caddyhttp/rewrite/rewrite_test.go b/caddyhttp/rewrite/rewrite_test.go index c2c59afa1..1ac03388b 100644 --- a/caddyhttp/rewrite/rewrite_test.go +++ b/caddyhttp/rewrite/rewrite_test.go @@ -42,7 +42,7 @@ func TestRewrite(t *testing.T) { if s := strings.Split(regexpRule[3], "|"); len(s) > 1 { ext = s[:len(s)-1] } - rule, err := NewComplexRule(regexpRule[0], regexpRule[1], regexpRule[2], 0, ext, nil) + rule, err := NewComplexRule(regexpRule[0], regexpRule[1], regexpRule[2], 0, ext, httpserver.IfMatcher{}) if err != nil { t.Fatal(err) } @@ -127,7 +127,7 @@ func TestRewrite(t *testing.T) { for i, s := range statusTests { urlPath := fmt.Sprintf("/status%d", i) - rule, err := NewComplexRule(s.base, s.regexp, s.to, s.status, nil, nil) + rule, err := NewComplexRule(s.base, s.regexp, s.to, s.status, nil, httpserver.IfMatcher{}) if err != nil { t.Fatalf("Test %d: No error expected for rule but found %v", i, err) } diff --git a/caddyhttp/rewrite/setup.go b/caddyhttp/rewrite/setup.go index b81be34f4..b19dd2f49 100644 --- a/caddyhttp/rewrite/setup.go +++ b/caddyhttp/rewrite/setup.go @@ -50,13 +50,19 @@ func rewriteParse(c *caddy.Controller) ([]Rule, error) { args := c.RemainingArgs() - var ifs []If + var matcher httpserver.RequestMatcher switch len(args) { case 1: base = args[0] fallthrough case 0: + // Integrate request matcher for 'if' conditions. + matcher, err = httpserver.SetupIfMatcher(c.Dispenser) + if err != nil { + return nil, err + } + block: for c.NextBlock() { switch c.Val() { case "r", "regexp": @@ -76,16 +82,6 @@ func rewriteParse(c *caddy.Controller) ([]Rule, error) { return nil, c.ArgErr() } ext = args1 - case "if": - args1 := c.RemainingArgs() - if len(args1) != 3 { - return nil, c.ArgErr() - } - ifCond, err := NewIf(args1[0], args1[1], args1[2]) - if err != nil { - return nil, err - } - ifs = append(ifs, ifCond) case "status": if !c.NextArg() { return nil, c.ArgErr() @@ -95,6 +91,9 @@ func rewriteParse(c *caddy.Controller) ([]Rule, error) { return nil, c.Err("status must be 2xx or 4xx") } default: + if httpserver.IfMatcherKeyword(c.Val()) { + continue block + } return nil, c.ArgErr() } } @@ -102,7 +101,7 @@ func rewriteParse(c *caddy.Controller) ([]Rule, error) { if to == "" && status == 0 { return nil, c.ArgErr() } - if rule, err = NewComplexRule(base, pattern, to, status, ext, ifs); err != nil { + if rule, err = NewComplexRule(base, pattern, to, status, ext, matcher); err != nil { return nil, err } regexpRules = append(regexpRules, rule) diff --git a/caddyhttp/rewrite/setup_test.go b/caddyhttp/rewrite/setup_test.go index 3f32a15e9..4ee2727b4 100644 --- a/caddyhttp/rewrite/setup_test.go +++ b/caddyhttp/rewrite/setup_test.go @@ -132,12 +132,6 @@ func TestRewriteParse(t *testing.T) { &ComplexRule{}, }}, {`rewrite { - to /to - if {path} is a - }`, false, []Rule{ - &ComplexRule{Base: "/", To: "/to", Ifs: []If{{A: "{path}", Operator: "is", B: "a"}}}, - }}, - {`rewrite { status 500 }`, true, []Rule{ &ComplexRule{}, @@ -229,11 +223,6 @@ func TestRewriteParse(t *testing.T) { } } - if fmt.Sprint(actualRule.Ifs) != fmt.Sprint(expectedRule.Ifs) { - t.Errorf("Test %d, rule %d: Expected Pattern=%s, got %s", - i, j, fmt.Sprint(expectedRule.Ifs), fmt.Sprint(actualRule.Ifs)) - } - } } |