Commit d48e51cb authored by Simon Lightfoot's avatar Simon Lightfoot

Changed IfCond to store the condition function and the compiled regular expression.

Updated ifCondition test to deep test all fields.
Changed NewComplexRule to not return a pointer.
Corrected panic detection in formatting.
Fixed failing test cases.
Fixed review bug for test.
Fixes bug caused by Replacer running on the regular expressions in IfMatcher. We also now compile regular expressions up front to detect errors.
Fixes rewrite bugs that come from formatting a rule as a string and failing with nil dereference caused by embedding Regexp pointer in a Rule. Re: Issue #1794
parent e7f08bff
...@@ -53,59 +53,8 @@ const ( ...@@ -53,59 +53,8 @@ const (
matchOp = "match" matchOp = "match"
) )
func operatorError(operator string) error {
return fmt.Errorf("Invalid operator %v", operator)
}
// ifCondition is a 'if' condition. // ifCondition is a 'if' condition.
type ifCondition func(string, string) bool type ifFunc func(a, b string) bool
var ifConditions = map[string]ifCondition{
isOp: isFunc,
notOp: notFunc,
hasOp: hasFunc,
startsWithOp: startsWithFunc,
endsWithOp: endsWithFunc,
matchOp: matchFunc,
}
// 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 !isFunc(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)
}
// 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
}
// ifCond is statement for a IfMatcher condition. // ifCond is statement for a IfMatcher condition.
type ifCond struct { type ifCond struct {
...@@ -113,40 +62,79 @@ type ifCond struct { ...@@ -113,40 +62,79 @@ type ifCond struct {
op string op string
b string b string
neg bool neg bool
rex *regexp.Regexp
f ifFunc
} }
// newIfCond creates a new If condition. // newIfCond creates a new If condition.
func newIfCond(a, operator, b string) (ifCond, error) { func newIfCond(a, op, b string) (ifCond, error) {
neg := false i := ifCond{a: a, op: op, b: b}
if strings.HasPrefix(operator, "not_") { if strings.HasPrefix(op, "not_") {
neg = true i.neg = true
operator = operator[4:] i.op = op[4:]
} }
if _, ok := ifConditions[operator]; !ok {
return ifCond{}, operatorError(operator) switch i.op {
case isOp:
// It checks for equality.
i.f = i.isFunc
case notOp:
// It checks for inequality.
i.f = i.notFunc
case hasOp:
// It checks if b is a substring of a.
i.f = strings.Contains
case startsWithOp:
// It checks if b is a prefix of a.
i.f = strings.HasPrefix
case endsWithOp:
// It checks if b is a suffix of a.
i.f = strings.HasSuffix
case matchOp:
// It does regexp matching of a against pattern in b and returns if they match.
var err error
if i.rex, err = regexp.Compile(i.b); err != nil {
return ifCond{}, fmt.Errorf("Invalid regular expression: '%s', %v", i.b, err)
}
i.f = i.matchFunc
default:
return ifCond{}, fmt.Errorf("Invalid operator %v", i.op)
} }
return ifCond{
a: a, return i, nil
op: operator, }
b: b,
neg: neg, // isFunc is condition for Is operator.
}, nil func (i ifCond) isFunc(a, b string) bool {
return a == b
}
// notFunc is condition for Not operator.
func (i ifCond) notFunc(a, b string) bool {
return a != b
}
// matchFunc is condition for Match operator.
func (i ifCond) matchFunc(a, b string) bool {
return i.rex.MatchString(a)
} }
// True returns true if the condition is true and false otherwise. // True returns true if the condition is true and false otherwise.
// If r is not nil, it replaces placeholders before comparison. // If r is not nil, it replaces placeholders before comparison.
func (i ifCond) True(r *http.Request) bool { func (i ifCond) True(r *http.Request) bool {
if c, ok := ifConditions[i.op]; ok { if i.f != nil {
a, b := i.a, i.b a, b := i.a, i.b
if r != nil { if r != nil {
replacer := NewReplacer(r, nil, "") replacer := NewReplacer(r, nil, "")
a = replacer.Replace(i.a) a = replacer.Replace(i.a)
b = replacer.Replace(i.b) if i.op != matchOp {
b = replacer.Replace(i.b)
}
} }
if i.neg { if i.neg {
return !c(a, b) return !i.f(a, b)
} }
return c(a, b) return i.f(a, b)
} }
return i.neg // false if not negated, true otherwise return i.neg // false if not negated, true otherwise
} }
......
...@@ -2,8 +2,8 @@ package httpserver ...@@ -2,8 +2,8 @@ package httpserver
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"regexp"
"strings" "strings"
"testing" "testing"
...@@ -14,66 +14,72 @@ func TestConditions(t *testing.T) { ...@@ -14,66 +14,72 @@ func TestConditions(t *testing.T) {
tests := []struct { tests := []struct {
condition string condition string
isTrue bool isTrue bool
shouldErr bool
}{ }{
{"a is b", false}, {"a is b", false, false},
{"a is a", true}, {"a is a", true, false},
{"a not b", true}, {"a not b", true, false},
{"a not a", false}, {"a not a", false, false},
{"a has a", true}, {"a has a", true, false},
{"a has b", false}, {"a has b", false, false},
{"ba has b", true}, {"ba has b", true, false},
{"bab has b", true}, {"bab has b", true, false},
{"bab has bb", false}, {"bab has bb", false, false},
{"a not_has a", false}, {"a not_has a", false, false},
{"a not_has b", true}, {"a not_has b", true, false},
{"ba not_has b", false}, {"ba not_has b", false, false},
{"bab not_has b", false}, {"bab not_has b", false, false},
{"bab not_has bb", true}, {"bab not_has bb", true, false},
{"bab starts_with bb", false}, {"bab starts_with bb", false, false},
{"bab starts_with ba", true}, {"bab starts_with ba", true, false},
{"bab starts_with bab", true}, {"bab starts_with bab", true, false},
{"bab not_starts_with bb", true}, {"bab not_starts_with bb", true, false},
{"bab not_starts_with ba", false}, {"bab not_starts_with ba", false, false},
{"bab not_starts_with bab", false}, {"bab not_starts_with bab", false, false},
{"bab ends_with bb", false}, {"bab ends_with bb", false, false},
{"bab ends_with bab", true}, {"bab ends_with bab", true, false},
{"bab ends_with ab", true}, {"bab ends_with ab", true, false},
{"bab not_ends_with bb", true}, {"bab not_ends_with bb", true, false},
{"bab not_ends_with ab", false}, {"bab not_ends_with ab", false, false},
{"bab not_ends_with bab", false}, {"bab not_ends_with bab", false, false},
{"a match *", false}, {"a match *", false, true},
{"a match a", true}, {"a match a", true, false},
{"a match .*", true}, {"a match .*", true, false},
{"a match a.*", true}, {"a match a.*", true, false},
{"a match b.*", false}, {"a match b.*", false, false},
{"ba match b.*", true}, {"ba match b.*", true, false},
{"ba match b[a-z]", true}, {"ba match b[a-z]", true, false},
{"b0 match b[a-z]", false}, {"b0 match b[a-z]", false, false},
{"b0a match b[a-z]", false}, {"b0a match b[a-z]", false, false},
{"b0a match b[a-z]+", false}, {"b0a match b[a-z]+", false, false},
{"b0a match b[a-z0-9]+", true}, {"b0a match b[a-z0-9]+", true, false},
{"a not_match *", true}, {"bac match b[a-z]{2}", true, false},
{"a not_match a", false}, {"a not_match *", false, true},
{"a not_match .*", false}, {"a not_match a", false, false},
{"a not_match a.*", false}, {"a not_match .*", false, false},
{"a not_match b.*", true}, {"a not_match a.*", false, false},
{"ba not_match b.*", false}, {"a not_match b.*", true, false},
{"ba not_match b[a-z]", false}, {"ba not_match b.*", false, false},
{"b0 not_match b[a-z]", true}, {"ba not_match b[a-z]", false, false},
{"b0a not_match b[a-z]", true}, {"b0 not_match b[a-z]", true, false},
{"b0a not_match b[a-z]+", true}, {"b0a not_match b[a-z]", true, false},
{"b0a not_match b[a-z0-9]+", false}, {"b0a not_match b[a-z]+", true, false},
{"b0a not_match b[a-z0-9]+", false, false},
{"bac not_match b[a-z]{2}", false, false},
} }
for i, test := range tests { for i, test := range tests {
str := strings.Fields(test.condition) str := strings.Fields(test.condition)
ifCond, err := newIfCond(str[0], str[1], str[2]) ifCond, err := newIfCond(str[0], str[1], str[2])
if err != nil { if err != nil {
t.Error(err) if !test.shouldErr {
t.Error(err)
}
continue
} }
isTrue := ifCond.True(nil) isTrue := ifCond.True(nil)
if isTrue != test.isTrue { if isTrue != test.isTrue {
t.Errorf("Test %d: expected %v found %v", i, test.isTrue, isTrue) t.Errorf("Test %d: '%s' expected %v found %v", i, test.condition, test.isTrue, isTrue)
} }
} }
...@@ -192,6 +198,7 @@ func TestIfMatcher(t *testing.T) { ...@@ -192,6 +198,7 @@ func TestIfMatcher(t *testing.T) {
} }
func TestSetupIfMatcher(t *testing.T) { func TestSetupIfMatcher(t *testing.T) {
rex_b, _ := regexp.Compile("b")
tests := []struct { tests := []struct {
input string input string
shouldErr bool shouldErr bool
...@@ -201,7 +208,7 @@ func TestSetupIfMatcher(t *testing.T) { ...@@ -201,7 +208,7 @@ func TestSetupIfMatcher(t *testing.T) {
if a match b if a match b
}`, false, IfMatcher{ }`, false, IfMatcher{
ifs: []ifCond{ ifs: []ifCond{
{a: "a", op: "match", b: "b", neg: false}, {a: "a", op: "match", b: "b", neg: false, rex: rex_b},
}, },
}}, }},
{`test { {`test {
...@@ -209,7 +216,7 @@ func TestSetupIfMatcher(t *testing.T) { ...@@ -209,7 +216,7 @@ func TestSetupIfMatcher(t *testing.T) {
if_op or if_op or
}`, false, IfMatcher{ }`, false, IfMatcher{
ifs: []ifCond{ ifs: []ifCond{
{a: "a", op: "match", b: "b", neg: false}, {a: "a", op: "match", b: "b", neg: false, rex: rex_b},
}, },
isOr: true, isOr: true,
}}, }},
...@@ -255,6 +262,7 @@ func TestSetupIfMatcher(t *testing.T) { ...@@ -255,6 +262,7 @@ func TestSetupIfMatcher(t *testing.T) {
for i, test := range tests { for i, test := range tests {
c := caddy.NewTestController("http", test.input) c := caddy.NewTestController("http", test.input)
c.Next() c.Next()
matcher, err := SetupIfMatcher(c) matcher, err := SetupIfMatcher(c)
if err == nil && test.shouldErr { if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i) t.Errorf("Test %d didn't error, but it should have", i)
...@@ -263,15 +271,60 @@ func TestSetupIfMatcher(t *testing.T) { ...@@ -263,15 +271,60 @@ func TestSetupIfMatcher(t *testing.T) {
} else if err != nil && test.shouldErr { } else if err != nil && test.shouldErr {
continue continue
} }
if _, ok := matcher.(IfMatcher); !ok {
test_if, ok := matcher.(IfMatcher)
if !ok {
t.Error("RequestMatcher should be of type IfMatcher") t.Error("RequestMatcher should be of type IfMatcher")
} }
if err != nil { if err != nil {
t.Errorf("Expected no error, but got: %v", err) 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, if len(test_if.ifs) != len(test.expected.ifs) {
fmt.Sprint(test.expected), fmt.Sprint(matcher)) t.Errorf("Test %d: Expected %d ifConditions, found %v", i,
len(test.expected.ifs), len(test_if.ifs))
}
for j, if_c := range test_if.ifs {
expected_c := test.expected.ifs[j]
if if_c.a != expected_c.a {
t.Errorf("Test %d, ifCond %d: Expected A=%s, got %s",
i, j, if_c.a, expected_c.a)
}
if if_c.op != expected_c.op {
t.Errorf("Test %d, ifCond %d: Expected Op=%s, got %s",
i, j, if_c.op, expected_c.op)
}
if if_c.b != expected_c.b {
t.Errorf("Test %d, ifCond %d: Expected B=%s, got %s",
i, j, if_c.b, expected_c.b)
}
if if_c.neg != expected_c.neg {
t.Errorf("Test %d, ifCond %d: Expected Neg=%v, got %v",
i, j, if_c.neg, expected_c.neg)
}
if expected_c.rex != nil && if_c.rex == nil {
t.Errorf("Test %d, ifCond %d: Expected Rex=%v, got <nil>",
i, j, expected_c.rex)
}
if expected_c.rex == nil && if_c.rex != nil {
t.Errorf("Test %d, ifCond %d: Expected Rex=<nil>, got %v",
i, j, if_c.rex)
}
if expected_c.rex != nil && if_c.rex != nil {
if if_c.rex.String() != expected_c.rex.String() {
t.Errorf("Test %d, ifCond %d: Expected Rex=%v, got %v",
i, j, if_c.rex, expected_c.rex)
}
}
} }
} }
} }
......
...@@ -84,19 +84,19 @@ type ComplexRule struct { ...@@ -84,19 +84,19 @@ type ComplexRule struct {
// Request matcher // Request matcher
httpserver.RequestMatcher httpserver.RequestMatcher
*regexp.Regexp Regexp *regexp.Regexp
} }
// NewComplexRule creates a new RegexpRule. It returns an error if regexp // NewComplexRule creates a new RegexpRule. It returns an error if regexp
// pattern (pattern) or extensions (ext) are invalid. // pattern (pattern) or extensions (ext) are invalid.
func NewComplexRule(base, pattern, to string, ext []string, matcher httpserver.RequestMatcher) (*ComplexRule, error) { func NewComplexRule(base, pattern, to string, ext []string, matcher httpserver.RequestMatcher) (ComplexRule, error) {
// validate regexp if present // validate regexp if present
var r *regexp.Regexp var r *regexp.Regexp
if pattern != "" { if pattern != "" {
var err error var err error
r, err = regexp.Compile(pattern) r, err = regexp.Compile(pattern)
if err != nil { if err != nil {
return nil, err return ComplexRule{}, err
} }
} }
...@@ -105,7 +105,7 @@ func NewComplexRule(base, pattern, to string, ext []string, matcher httpserver.R ...@@ -105,7 +105,7 @@ func NewComplexRule(base, pattern, to string, ext []string, matcher httpserver.R
if len(v) < 2 || (len(v) < 3 && v[0] == '!') { if len(v) < 2 || (len(v) < 3 && v[0] == '!') {
// check if no extension is specified // check if no extension is specified
if v != "/" && v != "!/" { if v != "/" && v != "!/" {
return nil, fmt.Errorf("invalid extension %v", v) return ComplexRule{}, fmt.Errorf("invalid extension %v", v)
} }
} }
} }
...@@ -118,7 +118,7 @@ func NewComplexRule(base, pattern, to string, ext []string, matcher httpserver.R ...@@ -118,7 +118,7 @@ func NewComplexRule(base, pattern, to string, ext []string, matcher httpserver.R
httpserver.PathMatcher(base), httpserver.PathMatcher(base),
) )
return &ComplexRule{ return ComplexRule{
Base: base, Base: base,
To: to, To: to,
Exts: ext, Exts: ext,
...@@ -128,13 +128,13 @@ func NewComplexRule(base, pattern, to string, ext []string, matcher httpserver.R ...@@ -128,13 +128,13 @@ func NewComplexRule(base, pattern, to string, ext []string, matcher httpserver.R
} }
// BasePath satisfies httpserver.Config // BasePath satisfies httpserver.Config
func (r *ComplexRule) BasePath() string { return r.Base } func (r ComplexRule) BasePath() string { return r.Base }
// Match satisfies httpserver.Config. // Match satisfies httpserver.Config.
// //
// Though ComplexRule embeds a RequestMatcher, additional // Though ComplexRule embeds a RequestMatcher, additional
// checks are needed which requires a custom implementation. // checks are needed which requires a custom implementation.
func (r *ComplexRule) Match(req *http.Request) bool { func (r ComplexRule) Match(req *http.Request) bool {
// validate RequestMatcher // validate RequestMatcher
// includes if and path // includes if and path
if !r.RequestMatcher.Match(req) { if !r.RequestMatcher.Match(req) {
...@@ -155,7 +155,7 @@ func (r *ComplexRule) Match(req *http.Request) bool { ...@@ -155,7 +155,7 @@ func (r *ComplexRule) Match(req *http.Request) bool {
} }
// Rewrite rewrites the internal location of the current request. // Rewrite rewrites the internal location of the current request.
func (r *ComplexRule) Rewrite(fs http.FileSystem, req *http.Request) (re Result) { func (r ComplexRule) Rewrite(fs http.FileSystem, req *http.Request) (re Result) {
replacer := newReplacer(req) replacer := newReplacer(req)
// validate regexp if present // validate regexp if present
...@@ -189,7 +189,7 @@ func (r *ComplexRule) Rewrite(fs http.FileSystem, req *http.Request) (re Result) ...@@ -189,7 +189,7 @@ func (r *ComplexRule) Rewrite(fs http.FileSystem, req *http.Request) (re Result)
// matchExt matches rPath against registered file extensions. // matchExt matches rPath against registered file extensions.
// Returns true if a match is found and false otherwise. // Returns true if a match is found and false otherwise.
func (r *ComplexRule) matchExt(rPath string) bool { func (r ComplexRule) matchExt(rPath string) bool {
f := filepath.Base(rPath) f := filepath.Base(rPath)
ext := path.Ext(f) ext := path.Ext(f)
if ext == "" { if ext == "" {
...@@ -216,14 +216,14 @@ func (r *ComplexRule) matchExt(rPath string) bool { ...@@ -216,14 +216,14 @@ func (r *ComplexRule) matchExt(rPath string) bool {
return !mustUse return !mustUse
} }
func (r *ComplexRule) regexpMatches(rPath string) []string { func (r ComplexRule) regexpMatches(rPath string) []string {
if r.Regexp != nil { if r.Regexp != nil {
// include trailing slash in regexp if present // include trailing slash in regexp if present
start := len(r.Base) start := len(r.Base)
if strings.HasSuffix(r.Base, "/") { if strings.HasSuffix(r.Base, "/") {
start-- start--
} }
return r.FindStringSubmatch(rPath[start:]) return r.Regexp.FindStringSubmatch(rPath[start:])
} }
return nil return nil
} }
......
...@@ -3,6 +3,7 @@ package rewrite ...@@ -3,6 +3,7 @@ package rewrite
import ( import (
"fmt" "fmt"
"regexp" "regexp"
"strings"
"testing" "testing"
"github.com/mholt/caddy" "github.com/mholt/caddy"
...@@ -97,14 +98,14 @@ func TestRewriteParse(t *testing.T) { ...@@ -97,14 +98,14 @@ func TestRewriteParse(t *testing.T) {
r .* r .*
to /to /index.php? to /to /index.php?
}`, false, []Rule{ }`, false, []Rule{
&ComplexRule{Base: "/", To: "/to /index.php?", Regexp: regexp.MustCompile(".*")}, ComplexRule{Base: "/", To: "/to /index.php?", Regexp: regexp.MustCompile(".*")},
}}, }},
{`rewrite { {`rewrite {
regexp .* regexp .*
to /to to /to
ext / html txt ext / html txt
}`, false, []Rule{ }`, false, []Rule{
&ComplexRule{Base: "/", To: "/to", Exts: []string{"/", "html", "txt"}, Regexp: regexp.MustCompile(".*")}, ComplexRule{Base: "/", To: "/to", Exts: []string{"/", "html", "txt"}, Regexp: regexp.MustCompile(".*")},
}}, }},
{`rewrite /path { {`rewrite /path {
r rr r rr
...@@ -115,27 +116,27 @@ func TestRewriteParse(t *testing.T) { ...@@ -115,27 +116,27 @@ func TestRewriteParse(t *testing.T) {
to /to /to2 to /to /to2
} }
`, false, []Rule{ `, false, []Rule{
&ComplexRule{Base: "/path", To: "/dest", Regexp: regexp.MustCompile("rr")}, ComplexRule{Base: "/path", To: "/dest", Regexp: regexp.MustCompile("rr")},
&ComplexRule{Base: "/", To: "/to /to2", Regexp: regexp.MustCompile("[a-z]+")}, ComplexRule{Base: "/", To: "/to /to2", Regexp: regexp.MustCompile("[a-z]+")},
}}, }},
{`rewrite { {`rewrite {
r .* r .*
}`, true, []Rule{ }`, true, []Rule{
&ComplexRule{}, ComplexRule{},
}}, }},
{`rewrite { {`rewrite {
}`, true, []Rule{ }`, true, []Rule{
&ComplexRule{}, ComplexRule{},
}}, }},
{`rewrite /`, true, []Rule{ {`rewrite /`, true, []Rule{
&ComplexRule{}, ComplexRule{},
}}, }},
{`rewrite { {`rewrite {
if {path} match / if {path} match /
to /to to /to
}`, false, []Rule{ }`, false, []Rule{
&ComplexRule{Base: "/", To: "/to"}, ComplexRule{Base: "/", To: "/to"},
}}, }},
} }
...@@ -156,8 +157,8 @@ func TestRewriteParse(t *testing.T) { ...@@ -156,8 +157,8 @@ func TestRewriteParse(t *testing.T) {
} }
for j, e := range test.expected { for j, e := range test.expected {
actualRule := actual[j].(*ComplexRule) actualRule := actual[j].(ComplexRule)
expectedRule := e.(*ComplexRule) expectedRule := e.(ComplexRule)
if actualRule.Base != expectedRule.Base { if actualRule.Base != expectedRule.Base {
t.Errorf("Test %d, rule %d: Expected Base=%s, got %s", t.Errorf("Test %d, rule %d: Expected Base=%s, got %s",
...@@ -175,13 +176,15 @@ func TestRewriteParse(t *testing.T) { ...@@ -175,13 +176,15 @@ func TestRewriteParse(t *testing.T) {
} }
if actualRule.Regexp != nil { if actualRule.Regexp != nil {
if actualRule.String() != expectedRule.String() { if actualRule.Regexp.String() != expectedRule.Regexp.String() {
t.Errorf("Test %d, rule %d: Expected Pattern=%s, got %s", t.Errorf("Test %d, rule %d: Expected Pattern=%s, got %s",
i, j, expectedRule.String(), actualRule.String()) i, j, actualRule.Regexp.String(), expectedRule.Regexp.String())
} }
} }
}
if rules_fmt := fmt.Sprintf("%v", actual); strings.HasPrefix(rules_fmt, "%!") {
t.Errorf("Test %d: Failed to string encode: %#v", i, rules_fmt)
} }
} }
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment