Commit 5f6a0a4c authored by Matt Holt's avatar Matt Holt

Merge pull request #479 from abiosoft/rewrite-patch

rewrite: not_has, not_match, multiple to, and status codes
parents cae98808 0a04fa40
...@@ -2,6 +2,7 @@ package setup ...@@ -2,6 +2,7 @@ package setup
import ( import (
"net/http" "net/http"
"strconv"
"strings" "strings"
"github.com/mholt/caddy/middleware" "github.com/mholt/caddy/middleware"
...@@ -33,6 +34,7 @@ func rewriteParse(c *Controller) ([]rewrite.Rule, error) { ...@@ -33,6 +34,7 @@ func rewriteParse(c *Controller) ([]rewrite.Rule, error) {
var err error var err error
var base = "/" var base = "/"
var pattern, to string var pattern, to string
var status int
var ext []string var ext []string
args := c.RemainingArgs() args := c.RemainingArgs()
...@@ -40,9 +42,6 @@ func rewriteParse(c *Controller) ([]rewrite.Rule, error) { ...@@ -40,9 +42,6 @@ func rewriteParse(c *Controller) ([]rewrite.Rule, error) {
var ifs []rewrite.If var ifs []rewrite.If
switch len(args) { switch len(args) {
case 2:
rule = rewrite.NewSimpleRule(args[0], args[1])
simpleRules = append(simpleRules, rule)
case 1: case 1:
base = args[0] base = args[0]
fallthrough fallthrough
...@@ -76,20 +75,31 @@ func rewriteParse(c *Controller) ([]rewrite.Rule, error) { ...@@ -76,20 +75,31 @@ func rewriteParse(c *Controller) ([]rewrite.Rule, error) {
return nil, err return nil, err
} }
ifs = append(ifs, ifCond) ifs = append(ifs, ifCond)
case "status":
if !c.NextArg() {
return nil, c.ArgErr()
}
status, _ = strconv.Atoi(c.Val())
if status < 400 || status > 499 {
return nil, c.Err("status must be 4xx")
}
default: default:
return nil, c.ArgErr() return nil, c.ArgErr()
} }
} }
// ensure to is specified // ensure to or status is specified
if to == "" { if to == "" && status == 0 {
return nil, c.ArgErr() return nil, c.ArgErr()
} }
if rule, err = rewrite.NewComplexRule(base, pattern, to, ext, ifs); err != nil { if rule, err = rewrite.NewComplexRule(base, pattern, to, status, ext, ifs); err != nil {
return nil, err return nil, err
} }
regexpRules = append(regexpRules, rule) regexpRules = append(regexpRules, rule)
// the only unhandled case is 2 and above
default: default:
return nil, c.ArgErr() rule = rewrite.NewSimpleRule(args[0], strings.Join(args[1:], " "))
simpleRules = append(simpleRules, rule)
} }
} }
......
...@@ -50,8 +50,8 @@ func TestRewriteParse(t *testing.T) { ...@@ -50,8 +50,8 @@ func TestRewriteParse(t *testing.T) {
}}, }},
{`rewrite a`, true, []rewrite.Rule{}}, {`rewrite a`, true, []rewrite.Rule{}},
{`rewrite`, true, []rewrite.Rule{}}, {`rewrite`, true, []rewrite.Rule{}},
{`rewrite a b c`, true, []rewrite.Rule{ {`rewrite a b c`, false, []rewrite.Rule{
rewrite.SimpleRule{From: "a", To: "b"}, rewrite.SimpleRule{From: "a", To: "b c"},
}}, }},
} }
...@@ -137,6 +137,33 @@ func TestRewriteParse(t *testing.T) { ...@@ -137,6 +137,33 @@ func TestRewriteParse(t *testing.T) {
}`, false, []rewrite.Rule{ }`, false, []rewrite.Rule{
&rewrite.ComplexRule{Base: "/", To: "/to", Ifs: []rewrite.If{rewrite.If{A: "{path}", Operator: "is", B: "a"}}}, &rewrite.ComplexRule{Base: "/", To: "/to", Ifs: []rewrite.If{rewrite.If{A: "{path}", Operator: "is", B: "a"}}},
}}, }},
{`rewrite {
status 400
}`, false, []rewrite.Rule{
&rewrite.ComplexRule{Base: "/", Regexp: regexp.MustCompile(".*"), Status: 400},
}},
{`rewrite {
to /to
status 400
}`, false, []rewrite.Rule{
&rewrite.ComplexRule{Base: "/", To: "/to", Regexp: regexp.MustCompile(".*"), Status: 400},
}},
{`rewrite {
status 399
}`, true, []rewrite.Rule{
&rewrite.ComplexRule{},
}},
{`rewrite {
status 0
}`, true, []rewrite.Rule{
&rewrite.ComplexRule{},
}},
{`rewrite {
to /to
status 0
}`, true, []rewrite.Rule{
&rewrite.ComplexRule{},
}},
} }
for i, test := range regexpTests { for i, test := range regexpTests {
......
...@@ -14,9 +14,11 @@ const ( ...@@ -14,9 +14,11 @@ const (
Is = "is" Is = "is"
Not = "not" Not = "not"
Has = "has" Has = "has"
NotHas = "not_has"
StartsWith = "starts_with" StartsWith = "starts_with"
EndsWith = "ends_with" EndsWith = "ends_with"
Match = "match" Match = "match"
NotMatch = "not_match"
) )
func operatorError(operator string) error { func operatorError(operator string) error {
...@@ -34,9 +36,11 @@ var conditions = map[string]condition{ ...@@ -34,9 +36,11 @@ var conditions = map[string]condition{
Is: isFunc, Is: isFunc,
Not: notFunc, Not: notFunc,
Has: hasFunc, Has: hasFunc,
NotHas: notHasFunc,
StartsWith: startsWithFunc, StartsWith: startsWithFunc,
EndsWith: endsWithFunc, EndsWith: endsWithFunc,
Match: matchFunc, Match: matchFunc,
NotMatch: notMatchFunc,
} }
// isFunc is condition for Is operator. // isFunc is condition for Is operator.
...@@ -57,6 +61,12 @@ func hasFunc(a, b string) bool { ...@@ -57,6 +61,12 @@ func hasFunc(a, b string) bool {
return strings.Contains(a, b) 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. // startsWithFunc is condition for StartsWith operator.
// It checks if b is a prefix of a. // It checks if b is a prefix of a.
func startsWithFunc(a, b string) bool { func startsWithFunc(a, b string) bool {
...@@ -71,11 +81,20 @@ func endsWithFunc(a, b string) bool { ...@@ -71,11 +81,20 @@ func endsWithFunc(a, b string) bool {
// matchFunc is condition for Match operator. // matchFunc is condition for Match operator.
// It does regexp matching of a against pattern in b // It does regexp matching of a against pattern in b
// and returns if they match.
func matchFunc(a, b string) bool { func matchFunc(a, b string) bool {
matched, _ := regexp.MatchString(b, a) matched, _ := regexp.MatchString(b, a)
return matched 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. // If is statement for a rewrite condition.
type If struct { type If struct {
A string A string
......
...@@ -20,6 +20,11 @@ func TestConditions(t *testing.T) { ...@@ -20,6 +20,11 @@ func TestConditions(t *testing.T) {
{"ba has b", true}, {"ba has b", true},
{"bab has b", true}, {"bab has b", true},
{"bab has bb", false}, {"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 bb", false},
{"bab starts_with ba", true}, {"bab starts_with ba", true},
{"bab starts_with bab", true}, {"bab starts_with bab", true},
...@@ -37,6 +42,17 @@ func TestConditions(t *testing.T) { ...@@ -37,6 +42,17 @@ func TestConditions(t *testing.T) {
{"b0a match b[a-z]", false}, {"b0a match b[a-z]", false},
{"b0a match b[a-z]+", false}, {"b0a match b[a-z]+", false},
{"b0a match b[a-z0-9]+", true}, {"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 { for i, test := range tests {
......
...@@ -13,6 +13,19 @@ import ( ...@@ -13,6 +13,19 @@ import (
"github.com/mholt/caddy/middleware" "github.com/mholt/caddy/middleware"
) )
// RewriteResult is the result of a rewrite
type RewriteResult int
const (
// RewriteIgnored is returned when rewrite is not done on request.
RewriteIgnored RewriteResult = iota
// RewriteDone is returned when rewrite is done on request.
RewriteDone
// RewriteStatus is returned when rewrite is not needed and status code should be set
// for the request.
RewriteStatus
)
// Rewrite is middleware to rewrite request locations internally before being handled. // Rewrite is middleware to rewrite request locations internally before being handled.
type Rewrite struct { type Rewrite struct {
Next middleware.Handler Next middleware.Handler
...@@ -22,9 +35,18 @@ type Rewrite struct { ...@@ -22,9 +35,18 @@ type Rewrite struct {
// ServeHTTP implements the middleware.Handler interface. // ServeHTTP implements the middleware.Handler interface.
func (rw Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { func (rw Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
outer:
for _, rule := range rw.Rules { for _, rule := range rw.Rules {
if ok := rule.Rewrite(rw.FileSys, r); ok { switch result := rule.Rewrite(rw.FileSys, r); result {
case RewriteDone:
break outer
case RewriteIgnored:
break break
case RewriteStatus:
// only valid for complex rules.
if cRule, ok := rule.(*ComplexRule); ok && cRule.Status != 0 {
return cRule.Status, nil
}
} }
} }
return rw.Next.ServeHTTP(w, r) return rw.Next.ServeHTTP(w, r)
...@@ -33,7 +55,7 @@ func (rw Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) ...@@ -33,7 +55,7 @@ func (rw Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
// Rule describes an internal location rewrite rule. // Rule describes an internal location rewrite rule.
type Rule interface { type Rule interface {
// Rewrite rewrites the internal location of the current request. // Rewrite rewrites the internal location of the current request.
Rewrite(http.FileSystem, *http.Request) bool Rewrite(http.FileSystem, *http.Request) RewriteResult
} }
// SimpleRule is a simple rewrite rule. // SimpleRule is a simple rewrite rule.
...@@ -47,7 +69,7 @@ func NewSimpleRule(from, to string) SimpleRule { ...@@ -47,7 +69,7 @@ func NewSimpleRule(from, to string) SimpleRule {
} }
// Rewrite rewrites the internal location of the current request. // Rewrite rewrites the internal location of the current request.
func (s SimpleRule) Rewrite(fs http.FileSystem, r *http.Request) bool { func (s SimpleRule) Rewrite(fs http.FileSystem, r *http.Request) RewriteResult {
if s.From == r.URL.Path { if s.From == r.URL.Path {
// take note of this rewrite for internal use by fastcgi // take note of this rewrite for internal use by fastcgi
// all we need is the URI, not full URL // all we need is the URI, not full URL
...@@ -56,7 +78,7 @@ func (s SimpleRule) Rewrite(fs http.FileSystem, r *http.Request) bool { ...@@ -56,7 +78,7 @@ func (s SimpleRule) Rewrite(fs http.FileSystem, r *http.Request) bool {
// attempt rewrite // attempt rewrite
return To(fs, r, s.To, newReplacer(r)) return To(fs, r, s.To, newReplacer(r))
} }
return false return RewriteIgnored
} }
// ComplexRule is a rewrite rule based on a regular expression // ComplexRule is a rewrite rule based on a regular expression
...@@ -67,6 +89,10 @@ type ComplexRule struct { ...@@ -67,6 +89,10 @@ type ComplexRule struct {
// Path to rewrite to // Path to rewrite to
To string To string
// If set, neither performs rewrite nor proceeds
// with request. Only returns code.
Status int
// Extensions to filter by // Extensions to filter by
Exts []string Exts []string
...@@ -78,7 +104,7 @@ type ComplexRule struct { ...@@ -78,7 +104,7 @@ type ComplexRule struct {
// NewRegexpRule creates a new RegexpRule. It returns an error if regexp // NewRegexpRule 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, ifs []If) (*ComplexRule, error) { func NewComplexRule(base, pattern, to string, status int, ext []string, ifs []If) (*ComplexRule, error) {
// validate regexp if present // validate regexp if present
var r *regexp.Regexp var r *regexp.Regexp
if pattern != "" { if pattern != "" {
...@@ -102,6 +128,7 @@ func NewComplexRule(base, pattern, to string, ext []string, ifs []If) (*ComplexR ...@@ -102,6 +128,7 @@ func NewComplexRule(base, pattern, to string, ext []string, ifs []If) (*ComplexR
return &ComplexRule{ return &ComplexRule{
Base: base, Base: base,
To: to, To: to,
Status: status,
Exts: ext, Exts: ext,
Ifs: ifs, Ifs: ifs,
Regexp: r, Regexp: r,
...@@ -109,33 +136,33 @@ func NewComplexRule(base, pattern, to string, ext []string, ifs []If) (*ComplexR ...@@ -109,33 +136,33 @@ func NewComplexRule(base, pattern, to string, ext []string, ifs []If) (*ComplexR
} }
// 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) bool { func (r *ComplexRule) Rewrite(fs http.FileSystem, req *http.Request) (re RewriteResult) {
rPath := req.URL.Path rPath := req.URL.Path
replacer := newReplacer(req) replacer := newReplacer(req)
// validate base // validate base
if !middleware.Path(rPath).Matches(r.Base) { if !middleware.Path(rPath).Matches(r.Base) {
return false return
} }
// validate extensions // validate extensions
if !r.matchExt(rPath) { if !r.matchExt(rPath) {
return false return
}
// include trailing slash in regexp if present
start := len(r.Base)
if strings.HasSuffix(r.Base, "/") {
start--
} }
// validate regexp if present // validate regexp if present
if r.Regexp != nil { if r.Regexp != nil {
// include trailing slash in regexp if present
start := len(r.Base)
if strings.HasSuffix(r.Base, "/") {
start--
}
matches := r.FindStringSubmatch(rPath[start:]) matches := r.FindStringSubmatch(rPath[start:])
switch len(matches) { switch len(matches) {
case 0: case 0:
// no match // no match
return false return
default: default:
// set regexp match variables {1}, {2} ... // set regexp match variables {1}, {2} ...
for i := 1; i < len(matches); i++ { for i := 1; i < len(matches); i++ {
...@@ -147,10 +174,15 @@ func (r *ComplexRule) Rewrite(fs http.FileSystem, req *http.Request) bool { ...@@ -147,10 +174,15 @@ func (r *ComplexRule) Rewrite(fs http.FileSystem, req *http.Request) bool {
// validate rewrite conditions // validate rewrite conditions
for _, i := range r.Ifs { for _, i := range r.Ifs {
if !i.True(req) { if !i.True(req) {
return false return
} }
} }
// if status is present, stop rewrite and return it.
if r.Status != 0 {
return RewriteStatus
}
// attempt rewrite // attempt rewrite
return To(fs, req, r.To, replacer) return To(fs, req, r.To, replacer)
} }
......
...@@ -41,7 +41,7 @@ func TestRewrite(t *testing.T) { ...@@ -41,7 +41,7 @@ func TestRewrite(t *testing.T) {
if s := strings.Split(regexpRule[3], "|"); len(s) > 1 { if s := strings.Split(regexpRule[3], "|"); len(s) > 1 {
ext = s[:len(s)-1] ext = s[:len(s)-1]
} }
rule, err := NewComplexRule(regexpRule[0], regexpRule[1], regexpRule[2], ext, nil) rule, err := NewComplexRule(regexpRule[0], regexpRule[1], regexpRule[2], 0, ext, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -106,6 +106,51 @@ func TestRewrite(t *testing.T) { ...@@ -106,6 +106,51 @@ func TestRewrite(t *testing.T) {
i, test.expectedTo, rec.Body.String()) i, test.expectedTo, rec.Body.String())
} }
} }
statusTests := []struct {
status int
base string
to string
regexp string
statusExpected bool
}{
{400, "/status", "", "", true},
{400, "/ignore", "", "", false},
{400, "/", "", "^/ignore", false},
{400, "/", "", "(.*)", true},
{400, "/status", "", "", true},
}
for i, s := range statusTests {
urlPath := fmt.Sprintf("/status%d", i)
rule, err := NewComplexRule(s.base, s.regexp, s.to, s.status, nil, nil)
if err != nil {
t.Fatalf("Test %d: No error expected for rule but found %v", i, err)
}
rw.Rules = []Rule{rule}
req, err := http.NewRequest("GET", urlPath, nil)
if err != nil {
t.Fatalf("Test %d: Could not create HTTP request: %v", i, err)
}
rec := httptest.NewRecorder()
code, err := rw.ServeHTTP(rec, req)
if err != nil {
t.Fatalf("Test %d: No error expected for handler but found %v", i, err)
}
if s.statusExpected {
if rec.Body.String() != "" {
t.Errorf("Test %d: Expected empty body but found %s", i, rec.Body.String())
}
if code != s.status {
t.Errorf("Test %d: Expected status code %d found %d", i, s.status, code)
}
} else {
if code != 0 {
t.Errorf("Test %d: Expected no status code found %d", i, code)
}
}
}
} }
func urlPrinter(w http.ResponseWriter, r *http.Request) (int, error) { func urlPrinter(w http.ResponseWriter, r *http.Request) (int, error) {
......
...@@ -13,7 +13,7 @@ import ( ...@@ -13,7 +13,7 @@ import (
// To attempts rewrite. It attempts to rewrite to first valid path // To attempts rewrite. It attempts to rewrite to first valid path
// or the last path if none of the paths are valid. // or the last path if none of the paths are valid.
// Returns true if rewrite is successful and false otherwise. // Returns true if rewrite is successful and false otherwise.
func To(fs http.FileSystem, r *http.Request, to string, replacer middleware.Replacer) bool { func To(fs http.FileSystem, r *http.Request, to string, replacer middleware.Replacer) RewriteResult {
tos := strings.Fields(to) tos := strings.Fields(to)
// try each rewrite paths // try each rewrite paths
...@@ -38,7 +38,7 @@ func To(fs http.FileSystem, r *http.Request, to string, replacer middleware.Repl ...@@ -38,7 +38,7 @@ func To(fs http.FileSystem, r *http.Request, to string, replacer middleware.Repl
// Let the user know we got here. Rewrite is expected but // Let the user know we got here. Rewrite is expected but
// the resulting url is invalid. // the resulting url is invalid.
log.Printf("[ERROR] rewrite: resulting path '%v' is invalid. error: %v", t, err) log.Printf("[ERROR] rewrite: resulting path '%v' is invalid. error: %v", t, err)
return false return RewriteIgnored
} }
// take note of this rewrite for internal use by fastcgi // take note of this rewrite for internal use by fastcgi
...@@ -56,7 +56,7 @@ func To(fs http.FileSystem, r *http.Request, to string, replacer middleware.Repl ...@@ -56,7 +56,7 @@ func To(fs http.FileSystem, r *http.Request, to string, replacer middleware.Repl
r.URL.Fragment = u.Fragment r.URL.Fragment = u.Fragment
} }
return true return RewriteDone
} }
// isValidFile checks if file exists on the filesystem. // isValidFile checks if file exists on the filesystem.
......
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