mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-23 22:27:38 -05:00
d48e51cb78
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
233 lines
5.3 KiB
Go
233 lines
5.3 KiB
Go
// Package rewrite is middleware for rewriting requests internally to
|
|
// a different path.
|
|
package rewrite
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/mholt/caddy/caddyhttp/httpserver"
|
|
)
|
|
|
|
// Result is the result of a rewrite
|
|
type Result int
|
|
|
|
const (
|
|
// RewriteIgnored is returned when rewrite is not done on request.
|
|
RewriteIgnored Result = iota
|
|
// RewriteDone is returned when rewrite is done on request.
|
|
RewriteDone
|
|
)
|
|
|
|
// Rewrite is middleware to rewrite request locations internally before being handled.
|
|
type Rewrite struct {
|
|
Next httpserver.Handler
|
|
FileSys http.FileSystem
|
|
Rules []httpserver.HandlerConfig
|
|
}
|
|
|
|
// ServeHTTP implements the httpserver.Handler interface.
|
|
func (rw Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
|
if rule := httpserver.ConfigSelector(rw.Rules).Select(r); rule != nil {
|
|
rule.(Rule).Rewrite(rw.FileSys, r)
|
|
}
|
|
|
|
return rw.Next.ServeHTTP(w, r)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// SimpleRule is a simple rewrite rule.
|
|
type SimpleRule struct {
|
|
From, To string
|
|
}
|
|
|
|
// NewSimpleRule creates a new Simple Rule
|
|
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 {
|
|
|
|
// attempt rewrite
|
|
return To(fs, r, s.To, newReplacer(r))
|
|
}
|
|
|
|
// ComplexRule is a rewrite rule based on a regular expression
|
|
type ComplexRule struct {
|
|
// Path base. Request to this path and subpaths will be rewritten
|
|
Base string
|
|
|
|
// Path to rewrite to
|
|
To string
|
|
|
|
// Extensions to filter by
|
|
Exts []string
|
|
|
|
// Request matcher
|
|
httpserver.RequestMatcher
|
|
|
|
Regexp *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, ext []string, matcher httpserver.RequestMatcher) (ComplexRule, error) {
|
|
// validate regexp if present
|
|
var r *regexp.Regexp
|
|
if pattern != "" {
|
|
var err error
|
|
r, err = regexp.Compile(pattern)
|
|
if err != nil {
|
|
return ComplexRule{}, err
|
|
}
|
|
}
|
|
|
|
// validate extensions if present
|
|
for _, v := range ext {
|
|
if len(v) < 2 || (len(v) < 3 && v[0] == '!') {
|
|
// check if no extension is specified
|
|
if v != "/" && v != "!/" {
|
|
return ComplexRule{}, fmt.Errorf("invalid extension %v", v)
|
|
}
|
|
}
|
|
}
|
|
|
|
// use both IfMatcher and PathMatcher
|
|
matcher = httpserver.MergeRequestMatchers(
|
|
// If condition matcher
|
|
matcher,
|
|
// Base path matcher
|
|
httpserver.PathMatcher(base),
|
|
)
|
|
|
|
return ComplexRule{
|
|
Base: base,
|
|
To: to,
|
|
Exts: ext,
|
|
RequestMatcher: matcher,
|
|
Regexp: r,
|
|
}, nil
|
|
}
|
|
|
|
// BasePath satisfies httpserver.Config
|
|
func (r ComplexRule) BasePath() string { return r.Base }
|
|
|
|
// 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(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 {
|
|
matches := r.regexpMatches(req.URL.Path)
|
|
switch len(matches) {
|
|
case 0:
|
|
// no match
|
|
return
|
|
default:
|
|
// set regexp match variables {1}, {2} ...
|
|
|
|
// url escaped values of ? and #.
|
|
q, f := url.QueryEscape("?"), url.QueryEscape("#")
|
|
|
|
for i := 1; i < len(matches); i++ {
|
|
// Special case of unescaped # and ? by stdlib regexp.
|
|
// Reverse the unescape.
|
|
if strings.ContainsAny(matches[i], "?#") {
|
|
matches[i] = strings.NewReplacer("?", q, "#", f).Replace(matches[i])
|
|
}
|
|
|
|
replacer.Set(fmt.Sprint(i), matches[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
// attempt rewrite
|
|
return To(fs, req, r.To, replacer)
|
|
}
|
|
|
|
// matchExt matches rPath against registered file extensions.
|
|
// Returns true if a match is found and false otherwise.
|
|
func (r ComplexRule) matchExt(rPath string) bool {
|
|
f := filepath.Base(rPath)
|
|
ext := path.Ext(f)
|
|
if ext == "" {
|
|
ext = "/"
|
|
}
|
|
|
|
mustUse := false
|
|
for _, v := range r.Exts {
|
|
use := true
|
|
if v[0] == '!' {
|
|
use = false
|
|
v = v[1:]
|
|
}
|
|
|
|
if use {
|
|
mustUse = true
|
|
}
|
|
|
|
if ext == v {
|
|
return use
|
|
}
|
|
}
|
|
|
|
return !mustUse
|
|
}
|
|
|
|
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.Regexp.FindStringSubmatch(rPath[start:])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func newReplacer(r *http.Request) httpserver.Replacer {
|
|
return httpserver.NewReplacer(r, nil, "")
|
|
}
|