diff --git a/caddyhttp/rewrite/rewrite.go b/caddyhttp/rewrite/rewrite.go index 1c8d848a6..f7f56cd39 100644 --- a/caddyhttp/rewrite/rewrite.go +++ b/caddyhttp/rewrite/rewrite.go @@ -63,22 +63,38 @@ type Rule interface { // SimpleRule is a simple rewrite rule. type SimpleRule struct { - From, To string + Regexp *regexp.Regexp + To string + Negate bool } // NewSimpleRule creates a new Simple Rule -func NewSimpleRule(from, to string) SimpleRule { - return SimpleRule{from, to} +func NewSimpleRule(from, to string, negate bool) (*SimpleRule, error) { + r, err := regexp.Compile(from) + if err != nil { + return nil, err + } + return &SimpleRule{ + Regexp: r, + To: to, + Negate: negate, + }, nil } // BasePath satisfies httpserver.Config -func (s SimpleRule) BasePath() string { return s.From } +func (s SimpleRule) BasePath() string { return "/" } // Match satisfies httpserver.Config -func (s SimpleRule) Match(r *http.Request) bool { return s.From == r.URL.Path } +func (s *SimpleRule) Match(r *http.Request) bool { + matches := regexpMatches(s.Regexp, "/", r.URL.Path) + if s.Negate { + return len(matches) == 0 + } + return len(matches) > 0 +} // Rewrite rewrites the internal location of the current request. -func (s SimpleRule) Rewrite(fs http.FileSystem, r *http.Request) Result { +func (s *SimpleRule) Rewrite(fs http.FileSystem, r *http.Request) Result { // attempt rewrite return To(fs, r, s.To, newReplacer(r)) @@ -165,7 +181,7 @@ func (r ComplexRule) Match(req *http.Request) bool { return true } // otherwise validate regex - return r.regexpMatches(req.URL.Path) != nil + return regexpMatches(r.Regexp, r.Base, req.URL.Path) != nil } // Rewrite rewrites the internal location of the current request. @@ -174,7 +190,7 @@ func (r ComplexRule) Rewrite(fs http.FileSystem, req *http.Request) (re Result) // validate regexp if present if r.Regexp != nil { - matches := r.regexpMatches(req.URL.Path) + matches := regexpMatches(r.Regexp, r.Base, req.URL.Path) switch len(matches) { case 0: // no match @@ -230,14 +246,14 @@ func (r ComplexRule) matchExt(rPath string) bool { return !mustUse } -func (r ComplexRule) regexpMatches(rPath string) []string { - if r.Regexp != nil { +func regexpMatches(regexp *regexp.Regexp, base, rPath string) []string { + if regexp != nil { // include trailing slash in regexp if present - start := len(r.Base) - if strings.HasSuffix(r.Base, "/") { + start := len(base) + if strings.HasSuffix(base, "/") { start-- } - return r.Regexp.FindStringSubmatch(rPath[start:]) + return regexp.FindStringSubmatch(rPath[start:]) } return nil } diff --git a/caddyhttp/rewrite/rewrite_test.go b/caddyhttp/rewrite/rewrite_test.go index d2c4d9859..b0700ec0b 100644 --- a/caddyhttp/rewrite/rewrite_test.go +++ b/caddyhttp/rewrite/rewrite_test.go @@ -29,9 +29,9 @@ func TestRewrite(t *testing.T) { rw := Rewrite{ Next: httpserver.HandlerFunc(urlPrinter), Rules: []httpserver.HandlerConfig{ - NewSimpleRule("/from", "/to"), - NewSimpleRule("/a", "/b"), - NewSimpleRule("/b", "/b{uri}"), + newSimpleRule(t, "^/from$", "/to"), + newSimpleRule(t, "^/a$", "/b"), + newSimpleRule(t, "^/b$", "/b{uri}"), }, FileSys: http.Dir("."), } @@ -131,6 +131,45 @@ func TestRewrite(t *testing.T) { } } +// TestWordpress is a test for wordpress usecase. +func TestWordpress(t *testing.T) { + rw := Rewrite{ + Next: httpserver.HandlerFunc(urlPrinter), + Rules: []httpserver.HandlerConfig{ + // both rules are same, thanks to Go regexp (confusion). + newSimpleRule(t, "^/wp-admin", "{path} {path}/ /index.php?{query}", true), + newSimpleRule(t, "^\\/wp-admin", "{path} {path}/ /index.php?{query}", true), + }, + FileSys: http.Dir("."), + } + tests := []struct { + from string + expectedTo string + }{ + {"/wp-admin", "/wp-admin"}, + {"/wp-admin/login.php", "/wp-admin/login.php"}, + {"/not-wp-admin/login.php?not=admin", "/index.php?not=admin"}, + {"/loophole", "/index.php"}, + {"/user?name=john", "/index.php?name=john"}, + } + + for i, test := range tests { + req, err := http.NewRequest("GET", test.from, nil) + if err != nil { + t.Fatalf("Test %d: Could not create HTTP request: %v", i, err) + } + ctx := context.WithValue(req.Context(), httpserver.OriginalURLCtxKey, *req.URL) + req = req.WithContext(ctx) + + rec := httptest.NewRecorder() + rw.ServeHTTP(rec, req) + + if got, want := rec.Body.String(), test.expectedTo; got != want { + t.Errorf("Test %d: Expected URL to be '%s' but was '%s'", i, want, got) + } + } +} + func urlPrinter(w http.ResponseWriter, r *http.Request) (int, error) { fmt.Fprint(w, r.URL.String()) return 0, nil diff --git a/caddyhttp/rewrite/setup.go b/caddyhttp/rewrite/setup.go index abaf61271..f73d76a70 100644 --- a/caddyhttp/rewrite/setup.go +++ b/caddyhttp/rewrite/setup.go @@ -58,6 +58,7 @@ func rewriteParse(c *caddy.Controller) ([]httpserver.HandlerConfig, error) { var base = "/" var pattern, to string var ext []string + var negate bool args := c.RemainingArgs() @@ -111,7 +112,14 @@ func rewriteParse(c *caddy.Controller) ([]httpserver.HandlerConfig, error) { // the only unhandled case is 2 and above default: - rule = NewSimpleRule(args[0], strings.Join(args[1:], " ")) + if args[0] == "not" { + negate = true + args = args[1:] + } + rule, err = NewSimpleRule(args[0], strings.Join(args[1:], " "), negate) + if err != nil { + return nil, err + } rules = append(rules, rule) } diff --git a/caddyhttp/rewrite/setup_test.go b/caddyhttp/rewrite/setup_test.go index 68256e969..e192242d0 100644 --- a/caddyhttp/rewrite/setup_test.go +++ b/caddyhttp/rewrite/setup_test.go @@ -50,6 +50,19 @@ func TestSetup(t *testing.T) { } } +// newSimpleRule is convenience test function for SimpleRule. +func newSimpleRule(t *testing.T, from, to string, negate ...bool) Rule { + var n bool + if len(negate) > 0 { + n = negate[0] + } + rule, err := NewSimpleRule(from, to, n) + if err != nil { + t.Fatal(err) + } + return rule +} + func TestRewriteParse(t *testing.T) { simpleTests := []struct { input string @@ -57,17 +70,20 @@ func TestRewriteParse(t *testing.T) { expected []Rule }{ {`rewrite /from /to`, false, []Rule{ - SimpleRule{From: "/from", To: "/to"}, + newSimpleRule(t, "/from", "/to"), }}, {`rewrite /from /to rewrite a b`, false, []Rule{ - SimpleRule{From: "/from", To: "/to"}, - SimpleRule{From: "a", To: "b"}, + newSimpleRule(t, "/from", "/to"), + newSimpleRule(t, "a", "b"), }}, {`rewrite a`, true, []Rule{}}, {`rewrite`, true, []Rule{}}, {`rewrite a b c`, false, []Rule{ - SimpleRule{From: "a", To: "b c"}, + newSimpleRule(t, "a", "b c"), + }}, + {`rewrite not a b c`, false, []Rule{ + newSimpleRule(t, "a", "b c", true), }}, } @@ -88,17 +104,22 @@ func TestRewriteParse(t *testing.T) { } for j, e := range test.expected { - actualRule := actual[j].(SimpleRule) - expectedRule := e.(SimpleRule) + actualRule := actual[j].(*SimpleRule) + expectedRule := e.(*SimpleRule) - if actualRule.From != expectedRule.From { + if actualRule.Regexp.String() != expectedRule.Regexp.String() { t.Errorf("Test %d, rule %d: Expected From=%s, got %s", - i, j, expectedRule.From, actualRule.From) + i, j, expectedRule.Regexp.String(), actualRule.Regexp.String()) } if actualRule.To != expectedRule.To { t.Errorf("Test %d, rule %d: Expected To=%s, got %s", - i, j, expectedRule.To, actualRule.To) + i, j, expectedRule.Regexp.String(), actualRule.Regexp.String()) + } + + if actualRule.Negate != expectedRule.Negate { + t.Errorf("Test %d, rule %d: Expected Negate=%v, got %v", + i, j, expectedRule.Negate, actualRule.Negate) } } }