Proposal: Middleware Config (#908)

* Prototype middleware Config

* Refactors

* Minor refactors
This commit is contained in:
Abiola Ibrahim 2016-07-09 01:12:52 +01:00 committed by Matt Holt
parent cf03c9a6c8
commit 87c389f73d
6 changed files with 127 additions and 69 deletions

View File

@ -5,7 +5,6 @@ import (
"net/http"
"os"
"path"
"strings"
"time"
)
@ -48,13 +47,23 @@ type (
// 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
}
// HandlerConfig is a middleware configuration.
// This makes it possible for middlewares to have a common
// configuration interface.
//
// TODO The long term plan is to get all middleware implement this
// interface for configurations.
HandlerConfig interface {
RequestMatcher
BasePath() string
}
// ConfigSelector selects a configuration.
ConfigSelector []HandlerConfig
)
// ServeHTTP implements the Handler interface.
@ -62,6 +71,20 @@ func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err
return f(w, r)
}
// Select selects a Config.
// This chooses the config with the longest length.
func (c ConfigSelector) Select(r *http.Request) (config HandlerConfig) {
for i := range c {
if !c[i].Match(r) {
continue
}
if config == nil || len(c[i].BasePath()) > len(config.BasePath()) {
config = c[i]
}
}
return config
}
// IndexFile looks for a file in /root/fpath/indexFile for each string
// in indexFiles. If an index file is found, it returns the root-relative
// path to the file and true. If no index file is found, empty string
@ -130,21 +153,6 @@ func initCaseSettings() {
}
}
// Path represents a URI path.
type Path string
// Matches checks to see if other matches p.
//
// Path matching will probably not always be a direct
// comparison; this method assures that paths can be
// easily and consistently matched.
func (p Path) Matches(other string) bool {
if CaseSensitivePath {
return strings.HasPrefix(string(p), other)
}
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 {

View File

@ -0,0 +1,29 @@
package httpserver
import (
"net/http"
"strings"
)
// Path represents a URI path.
type Path string
// Matches checks to see if other matches p.
//
// Path matching will probably not always be a direct
// comparison; this method assures that paths can be
// easily and consistently matched.
func (p Path) Matches(other string) bool {
if CaseSensitivePath {
return strings.HasPrefix(string(p), other)
}
return strings.HasPrefix(strings.ToLower(string(p)), strings.ToLower(other))
}
// PathMatcher is a Path RequestMatcher.
type PathMatcher string
// Match satisfies RequestMatcher.
func (p PathMatcher) Match(r *http.Request) bool {
return Path(r.URL.Path).Matches(string(p))
}

View File

@ -31,18 +31,13 @@ const (
type Rewrite struct {
Next httpserver.Handler
FileSys http.FileSystem
Rules []Rule
Rules []httpserver.HandlerConfig
}
// ServeHTTP implements the httpserver.Handler interface.
func (rw Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
outer:
for _, rule := range rw.Rules {
switch result := rule.Rewrite(rw.FileSys, r); result {
case RewriteDone:
break outer
case RewriteIgnored:
break
if rule := httpserver.ConfigSelector(rw.Rules).Select(r); rule != nil {
switch result := rule.(Rule).Rewrite(rw.FileSys, r); result {
case RewriteStatus:
// only valid for complex rules.
if cRule, ok := rule.(*ComplexRule); ok && cRule.Status != 0 {
@ -55,6 +50,7 @@ outer:
// Rule describes an internal location rewrite rule.
type Rule interface {
httpserver.HandlerConfig
// Rewrite rewrites the internal location of the current request.
Rewrite(http.FileSystem, *http.Request) Result
}
@ -69,17 +65,20 @@ func NewSimpleRule(from, to string) SimpleRule {
return SimpleRule{from, to}
}
// BasePath satisfies httpserver.Config
func (s SimpleRule) BasePath() string { return s.From }
// Match satisfies httpserver.Config
func (s SimpleRule) Match(r *http.Request) bool { return s.From == r.URL.Path }
// Rewrite rewrites the internal location of the current request.
func (s SimpleRule) Rewrite(fs http.FileSystem, r *http.Request) Result {
if s.From == r.URL.Path {
// take note of this rewrite for internal use by fastcgi
// all we need is the URI, not full URL
r.Header.Set(headerFieldName, r.URL.RequestURI())
// take note of this rewrite for internal use by fastcgi
// all we need is the URI, not full URL
r.Header.Set(headerFieldName, r.URL.RequestURI())
// attempt rewrite
return To(fs, r, s.To, newReplacer(r))
}
return RewriteIgnored
// attempt rewrite
return To(fs, r, s.To, newReplacer(r))
}
// ComplexRule is a rewrite rule based on a regular expression
@ -105,7 +104,7 @@ type ComplexRule struct {
// 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, m httpserver.RequestMatcher) (*ComplexRule, error) {
func NewComplexRule(base, pattern, to string, status int, ext []string, matcher httpserver.RequestMatcher) (*ComplexRule, error) {
// validate regexp if present
var r *regexp.Regexp
if pattern != "" {
@ -126,40 +125,58 @@ func NewComplexRule(base, pattern, to string, status int, ext []string, m httpse
}
}
// use both IfMatcher and PathMatcher
matcher = httpserver.MergeRequestMatchers(
// If condition matcher
matcher,
// Base path matcher
httpserver.PathMatcher(base),
)
return &ComplexRule{
Base: base,
To: to,
Status: status,
Exts: ext,
RequestMatcher: m,
RequestMatcher: matcher,
Regexp: r,
}, nil
}
// Rewrite rewrites the internal location of the current request.
func (r *ComplexRule) Rewrite(fs http.FileSystem, req *http.Request) (re Result) {
rPath := req.URL.Path
replacer := newReplacer(req)
// BasePath satisfies httpserver.Config
func (r *ComplexRule) BasePath() string { return r.Base }
// validate base
if !httpserver.Path(rPath).Matches(r.Base) {
return
// Match satisfies httpserver.Config.
//
// Though ComplexRule embeds a RequestMatcher, additional
// checks are needed which requires a custom implementation.
func (r *ComplexRule) Match(req *http.Request) bool {
// validate RequestMatcher
// includes if and path
if !r.RequestMatcher.Match(req) {
return false
}
// validate extensions
if !r.matchExt(rPath) {
return
if !r.matchExt(req.URL.Path) {
return false
}
// if regex is nil, ignore
if r.Regexp == nil {
return true
}
// otherwise validate regex
return r.regexpMatches(req.URL.Path) != nil
}
// Rewrite rewrites the internal location of the current request.
func (r *ComplexRule) Rewrite(fs http.FileSystem, req *http.Request) (re Result) {
replacer := newReplacer(req)
// validate regexp if present
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.regexpMatches(req.URL.Path)
switch len(matches) {
case 0:
// no match
@ -182,11 +199,6 @@ func (r *ComplexRule) Rewrite(fs http.FileSystem, req *http.Request) (re Result)
}
}
// validate if conditions
if !r.RequestMatcher.Match(req) {
return
}
// if status is present, stop rewrite and return it.
if r.Status != 0 {
return RewriteStatus
@ -228,6 +240,18 @@ func (r *ComplexRule) matchExt(rPath string) bool {
return true
}
func (r *ComplexRule) regexpMatches(rPath string) []string {
if r.Regexp != nil {
// include trailing slash in regexp if present
start := len(r.Base)
if strings.HasSuffix(r.Base, "/") {
start--
}
return r.FindStringSubmatch(rPath[start:])
}
return nil
}
func newReplacer(r *http.Request) httpserver.Replacer {
return httpserver.NewReplacer(r, nil, "")
}

View File

@ -13,7 +13,7 @@ import (
func TestRewrite(t *testing.T) {
rw := Rewrite{
Next: httpserver.HandlerFunc(urlPrinter),
Rules: []Rule{
Rules: []httpserver.HandlerConfig{
NewSimpleRule("/from", "/to"),
NewSimpleRule("/a", "/b"),
NewSimpleRule("/b", "/b{uri}"),
@ -131,7 +131,7 @@ func TestRewrite(t *testing.T) {
if err != nil {
t.Fatalf("Test %d: No error expected for rule but found %v", i, err)
}
rw.Rules = []Rule{rule}
rw.Rules = []httpserver.HandlerConfig{rule}
req, err := http.NewRequest("GET", urlPath, nil)
if err != nil {
t.Fatalf("Test %d: Could not create HTTP request: %v", i, err)

View File

@ -36,9 +36,8 @@ func setup(c *caddy.Controller) error {
return nil
}
func rewriteParse(c *caddy.Controller) ([]Rule, error) {
var simpleRules []Rule
var regexpRules []Rule
func rewriteParse(c *caddy.Controller) ([]httpserver.HandlerConfig, error) {
var rules []httpserver.HandlerConfig
for c.Next() {
var rule Rule
@ -105,16 +104,15 @@ func rewriteParse(c *caddy.Controller) ([]Rule, error) {
if rule, err = NewComplexRule(base, pattern, to, status, ext, matcher); err != nil {
return nil, err
}
regexpRules = append(regexpRules, rule)
rules = append(rules, rule)
// the only unhandled case is 2 and above
default:
rule = NewSimpleRule(args[0], strings.Join(args[1:], " "))
simpleRules = append(simpleRules, rule)
rules = append(rules, rule)
}
}
// put simple rules in front to avoid regexp computation for them
return append(simpleRules, regexpRules...), nil
return rules, nil
}

View File

@ -12,7 +12,6 @@ import (
// To attempts rewrite. It attempts to rewrite to first valid path
// or the last path if none of the paths are valid.
// Returns true if rewrite is successful and false otherwise.
func To(fs http.FileSystem, r *http.Request, to string, replacer httpserver.Replacer) Result {
tos := strings.Fields(to)