mirror of
https://github.com/caddyserver/caddy.git
synced 2025-06-05 20:26:52 +08:00
Merge branch 'master' into replacer-patch
This commit is contained in:
@ -5,6 +5,8 @@ import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@ -103,8 +105,12 @@ func generateStaticHTML(md Markdown, cfg *Config) error {
|
||||
reqPath = filepath.ToSlash(reqPath)
|
||||
reqPath = "/" + reqPath
|
||||
|
||||
// Create empty requests and url to cater for template values.
|
||||
req, _ := http.NewRequest("", "/", nil)
|
||||
urlVar, _ := url.Parse("/")
|
||||
|
||||
// Generate the static file
|
||||
ctx := middleware.Context{Root: md.FileSys}
|
||||
ctx := middleware.Context{Root: md.FileSys, Req: req, URL: urlVar}
|
||||
_, err = md.Process(cfg, reqPath, body, ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
111
middleware/rewrite/condition.go
Normal file
111
middleware/rewrite/condition.go
Normal file
@ -0,0 +1,111 @@
|
||||
package rewrite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
)
|
||||
|
||||
const (
|
||||
// Operators
|
||||
Is = "is"
|
||||
Not = "not"
|
||||
Has = "has"
|
||||
StartsWith = "starts_with"
|
||||
EndsWith = "ends_with"
|
||||
Match = "match"
|
||||
)
|
||||
|
||||
func operatorError(operator string) error {
|
||||
return fmt.Errorf("Invalid operator %v", operator)
|
||||
}
|
||||
|
||||
func newReplacer(r *http.Request) middleware.Replacer {
|
||||
return middleware.NewReplacer(r, nil, "")
|
||||
}
|
||||
|
||||
// condition is a rewrite condition.
|
||||
type condition func(string, string) bool
|
||||
|
||||
var conditions = map[string]condition{
|
||||
Is: isFunc,
|
||||
Not: notFunc,
|
||||
Has: hasFunc,
|
||||
StartsWith: startsWithFunc,
|
||||
EndsWith: endsWithFunc,
|
||||
Match: 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 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
|
||||
func matchFunc(a, b string) bool {
|
||||
matched, _ := regexp.MatchString(b, a)
|
||||
return matched
|
||||
}
|
||||
|
||||
// If is statement for a rewrite condition.
|
||||
type If struct {
|
||||
A string
|
||||
Operator string
|
||||
B string
|
||||
}
|
||||
|
||||
// True returns true if the condition is true and false otherwise.
|
||||
// If r is not nil, it replaces placeholders before comparison.
|
||||
func (i If) True(r *http.Request) bool {
|
||||
if c, ok := conditions[i.Operator]; ok {
|
||||
a, b := i.A, i.B
|
||||
if r != nil {
|
||||
replacer := newReplacer(r)
|
||||
a = replacer.Replace(i.A)
|
||||
b = replacer.Replace(i.B)
|
||||
}
|
||||
return c(a, b)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NewIf creates a new If condition.
|
||||
func NewIf(a, operator, b string) (If, error) {
|
||||
if _, ok := conditions[operator]; !ok {
|
||||
return If{}, operatorError(operator)
|
||||
}
|
||||
return If{
|
||||
A: a,
|
||||
Operator: operator,
|
||||
B: b,
|
||||
}, nil
|
||||
}
|
90
middleware/rewrite/condition_test.go
Normal file
90
middleware/rewrite/condition_test.go
Normal file
@ -0,0 +1,90 @@
|
||||
package rewrite
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConditions(t *testing.T) {
|
||||
tests := []struct {
|
||||
condition string
|
||||
isTrue bool
|
||||
}{
|
||||
{"a is b", false},
|
||||
{"a is a", true},
|
||||
{"a not b", true},
|
||||
{"a not a", false},
|
||||
{"a has a", true},
|
||||
{"a has b", false},
|
||||
{"ba has b", true},
|
||||
{"bab has b", true},
|
||||
{"bab has bb", false},
|
||||
{"bab starts_with bb", false},
|
||||
{"bab starts_with ba", true},
|
||||
{"bab starts_with bab", true},
|
||||
{"bab ends_with bb", false},
|
||||
{"bab ends_with bab", true},
|
||||
{"bab ends_with ab", true},
|
||||
{"a match *", false},
|
||||
{"a match a", true},
|
||||
{"a match .*", true},
|
||||
{"a match a.*", true},
|
||||
{"a match b.*", false},
|
||||
{"ba match b.*", true},
|
||||
{"ba match b[a-z]", true},
|
||||
{"b0 match b[a-z]", false},
|
||||
{"b0a match b[a-z]", false},
|
||||
{"b0a match b[a-z]+", false},
|
||||
{"b0a match b[a-z0-9]+", true},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
str := strings.Fields(test.condition)
|
||||
ifCond, err := NewIf(str[0], str[1], str[2])
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
isTrue := ifCond.True(nil)
|
||||
if isTrue != test.isTrue {
|
||||
t.Errorf("Test %v: expected %v found %v", i, test.isTrue, isTrue)
|
||||
}
|
||||
}
|
||||
|
||||
invalidOperators := []string{"ss", "and", "if"}
|
||||
for _, op := range invalidOperators {
|
||||
_, err := NewIf("a", op, "b")
|
||||
if err == nil {
|
||||
t.Errorf("Invalid operator %v used, expected error.", op)
|
||||
}
|
||||
}
|
||||
|
||||
replaceTests := []struct {
|
||||
url string
|
||||
condition string
|
||||
isTrue bool
|
||||
}{
|
||||
{"/home", "{uri} match /home", true},
|
||||
{"/hom", "{uri} match /home", false},
|
||||
{"/hom", "{uri} starts_with /home", false},
|
||||
{"/hom", "{uri} starts_with /h", true},
|
||||
{"/home/.hiddenfile", `{uri} match \/\.(.*)`, true},
|
||||
{"/home/.hiddendir/afile", `{uri} match \/\.(.*)`, true},
|
||||
}
|
||||
|
||||
for i, test := range replaceTests {
|
||||
r, err := http.NewRequest("GET", test.url, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
str := strings.Fields(test.condition)
|
||||
ifCond, err := NewIf(str[0], str[1], str[2])
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
isTrue := ifCond.True(r)
|
||||
if isTrue != test.isTrue {
|
||||
t.Errorf("Test %v: expected %v found %v", i, test.isTrue, isTrue)
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,6 @@ package rewrite
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@ -16,14 +15,15 @@ import (
|
||||
|
||||
// Rewrite is middleware to rewrite request locations internally before being handled.
|
||||
type Rewrite struct {
|
||||
Next middleware.Handler
|
||||
Rules []Rule
|
||||
Next middleware.Handler
|
||||
FileSys http.FileSystem
|
||||
Rules []Rule
|
||||
}
|
||||
|
||||
// ServeHTTP implements the middleware.Handler interface.
|
||||
func (rw Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
for _, rule := range rw.Rules {
|
||||
if ok := rule.Rewrite(r); ok {
|
||||
if ok := rule.Rewrite(rw.FileSys, r); ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -33,7 +33,7 @@ func (rw Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
||||
// Rule describes an internal location rewrite rule.
|
||||
type Rule interface {
|
||||
// Rewrite rewrites the internal location of the current request.
|
||||
Rewrite(*http.Request) bool
|
||||
Rewrite(http.FileSystem, *http.Request) bool
|
||||
}
|
||||
|
||||
// SimpleRule is a simple rewrite rule.
|
||||
@ -47,23 +47,20 @@ func NewSimpleRule(from, to string) SimpleRule {
|
||||
}
|
||||
|
||||
// Rewrite rewrites the internal location of the current request.
|
||||
func (s SimpleRule) Rewrite(r *http.Request) bool {
|
||||
func (s SimpleRule) Rewrite(fs http.FileSystem, r *http.Request) bool {
|
||||
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())
|
||||
|
||||
// replace variables
|
||||
to := path.Clean(middleware.NewReplacer(r, nil, "").Replace(s.To))
|
||||
|
||||
r.URL.Path = to
|
||||
return true
|
||||
// attempt rewrite
|
||||
return To(fs, r, s.To)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RegexpRule is a rewrite rule based on a regular expression
|
||||
type RegexpRule struct {
|
||||
// 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
|
||||
|
||||
@ -73,18 +70,26 @@ type RegexpRule struct {
|
||||
// Extensions to filter by
|
||||
Exts []string
|
||||
|
||||
// Rewrite conditions
|
||||
Ifs []If
|
||||
|
||||
*regexp.Regexp
|
||||
}
|
||||
|
||||
// NewRegexpRule creates a new RegexpRule. It returns an error if regexp
|
||||
// pattern (pattern) or extensions (ext) are invalid.
|
||||
func NewRegexpRule(base, pattern, to string, ext []string) (*RegexpRule, error) {
|
||||
r, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func NewComplexRule(base, pattern, to string, ext []string, ifs []If) (*ComplexRule, error) {
|
||||
// validate regexp if present
|
||||
var r *regexp.Regexp
|
||||
if pattern != "" {
|
||||
var err error
|
||||
r, err = regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// validate extensions
|
||||
// validate extensions if present
|
||||
for _, v := range ext {
|
||||
if len(v) < 2 || (len(v) < 3 && v[0] == '!') {
|
||||
// check if no extension is specified
|
||||
@ -94,16 +99,17 @@ func NewRegexpRule(base, pattern, to string, ext []string) (*RegexpRule, error)
|
||||
}
|
||||
}
|
||||
|
||||
return &RegexpRule{
|
||||
base,
|
||||
to,
|
||||
ext,
|
||||
r,
|
||||
return &ComplexRule{
|
||||
Base: base,
|
||||
To: to,
|
||||
Exts: ext,
|
||||
Ifs: ifs,
|
||||
Regexp: r,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Rewrite rewrites the internal location of the current request.
|
||||
func (r *RegexpRule) Rewrite(req *http.Request) bool {
|
||||
func (r *ComplexRule) Rewrite(fs http.FileSystem, req *http.Request) bool {
|
||||
rPath := req.URL.Path
|
||||
|
||||
// validate base
|
||||
@ -122,36 +128,27 @@ func (r *RegexpRule) Rewrite(req *http.Request) bool {
|
||||
start--
|
||||
}
|
||||
|
||||
// validate regexp
|
||||
if !r.MatchString(rPath[start:]) {
|
||||
return false
|
||||
// validate regexp if present
|
||||
if r.Regexp != nil {
|
||||
if !r.MatchString(rPath[start:]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// replace variables
|
||||
to := path.Clean(middleware.NewReplacer(req, nil, "").Replace(r.To))
|
||||
|
||||
// validate resulting path
|
||||
url, err := url.Parse(to)
|
||||
if err != nil {
|
||||
return false
|
||||
// validate rewrite conditions
|
||||
for _, i := range r.Ifs {
|
||||
if !i.True(req) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// take note of this rewrite for internal use by fastcgi
|
||||
// all we need is the URI, not full URL
|
||||
req.Header.Set(headerFieldName, req.URL.RequestURI())
|
||||
|
||||
// perform rewrite
|
||||
req.URL.Path = url.Path
|
||||
if url.RawQuery != "" {
|
||||
// overwrite query string if present
|
||||
req.URL.RawQuery = url.RawQuery
|
||||
}
|
||||
return true
|
||||
// attempt rewrite
|
||||
return To(fs, req, r.To)
|
||||
}
|
||||
|
||||
// matchExt matches rPath against registered file extensions.
|
||||
// Returns true if a match is found and false otherwise.
|
||||
func (r *RegexpRule) matchExt(rPath string) bool {
|
||||
func (r *ComplexRule) matchExt(rPath string) bool {
|
||||
f := filepath.Base(rPath)
|
||||
ext := path.Ext(f)
|
||||
if ext == "" {
|
||||
|
@ -4,9 +4,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
)
|
||||
@ -19,9 +18,10 @@ func TestRewrite(t *testing.T) {
|
||||
NewSimpleRule("/a", "/b"),
|
||||
NewSimpleRule("/b", "/b{uri}"),
|
||||
},
|
||||
FileSys: http.Dir("."),
|
||||
}
|
||||
|
||||
regexpRules := [][]string{
|
||||
regexps := [][]string{
|
||||
{"/reg/", ".*", "/to", ""},
|
||||
{"/r/", "[a-z]+", "/toaz", "!.html|"},
|
||||
{"/url/", "a([a-z0-9]*)s([A-Z]{2})", "/to/{path}", ""},
|
||||
@ -33,12 +33,12 @@ func TestRewrite(t *testing.T) {
|
||||
{"/ab/", `.*\.jpg`, "/ajpg", ""},
|
||||
}
|
||||
|
||||
for _, regexpRule := range regexpRules {
|
||||
for _, regexpRule := range regexps {
|
||||
var ext []string
|
||||
if s := strings.Split(regexpRule[3], "|"); len(s) > 1 {
|
||||
ext = s[:len(s)-1]
|
||||
}
|
||||
rule, err := NewRegexpRule(regexpRule[0], regexpRule[1], regexpRule[2], ext)
|
||||
rule, err := NewComplexRule(regexpRule[0], regexpRule[1], regexpRule[2], ext, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
0
middleware/rewrite/testdata/testdir/empty
vendored
Normal file
0
middleware/rewrite/testdata/testdir/empty
vendored
Normal file
1
middleware/rewrite/testdata/testfile
vendored
Normal file
1
middleware/rewrite/testdata/testfile
vendored
Normal file
@ -0,0 +1 @@
|
||||
empty
|
86
middleware/rewrite/to.go
Normal file
86
middleware/rewrite/to.go
Normal file
@ -0,0 +1,86 @@
|
||||
package rewrite
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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) bool {
|
||||
tos := strings.Fields(to)
|
||||
replacer := newReplacer(r)
|
||||
|
||||
// try each rewrite paths
|
||||
t := ""
|
||||
for _, v := range tos {
|
||||
t = path.Clean(replacer.Replace(v))
|
||||
|
||||
// add trailing slash for directories, if present
|
||||
if strings.HasSuffix(v, "/") && !strings.HasSuffix(t, "/") {
|
||||
t += "/"
|
||||
}
|
||||
|
||||
// validate file
|
||||
if isValidFile(fs, t) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// validate resulting path
|
||||
u, err := url.Parse(t)
|
||||
if err != nil {
|
||||
// Let the user know we got here. Rewrite is expected but
|
||||
// the resulting url is invalid.
|
||||
log.Printf("[ERROR] rewrite: resulting path '%v' is invalid. error: %v", t, err)
|
||||
return false
|
||||
}
|
||||
|
||||
// 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())
|
||||
|
||||
// perform rewrite
|
||||
r.URL.Path = u.Path
|
||||
if u.RawQuery != "" {
|
||||
// overwrite query string if present
|
||||
r.URL.RawQuery = u.RawQuery
|
||||
}
|
||||
if u.Fragment != "" {
|
||||
// overwrite fragment if present
|
||||
r.URL.Fragment = u.Fragment
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// isValidFile checks if file exists on the filesystem.
|
||||
// if file ends with `/`, it is validated as a directory.
|
||||
func isValidFile(fs http.FileSystem, file string) bool {
|
||||
if fs == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
f, err := fs.Open(file)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
stat, err := f.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// directory
|
||||
if strings.HasSuffix(file, "/") {
|
||||
return stat.IsDir()
|
||||
}
|
||||
|
||||
// file
|
||||
return !stat.IsDir()
|
||||
}
|
44
middleware/rewrite/to_test.go
Normal file
44
middleware/rewrite/to_test.go
Normal file
@ -0,0 +1,44 @@
|
||||
package rewrite
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTo(t *testing.T) {
|
||||
fs := http.Dir("testdata")
|
||||
tests := []struct {
|
||||
url string
|
||||
to string
|
||||
expected string
|
||||
}{
|
||||
{"/", "/somefiles", "/somefiles"},
|
||||
{"/somefiles", "/somefiles /index.php{uri}", "/index.php/somefiles"},
|
||||
{"/somefiles", "/testfile /index.php{uri}", "/testfile"},
|
||||
{"/somefiles", "/testfile/ /index.php{uri}", "/index.php/somefiles"},
|
||||
{"/somefiles", "/somefiles /index.php{uri}", "/index.php/somefiles"},
|
||||
{"/?a=b", "/somefiles /index.php?{query}", "/index.php?a=b"},
|
||||
{"/?a=b", "/testfile /index.php?{query}", "/testfile?a=b"},
|
||||
{"/?a=b", "/testdir /index.php?{query}", "/index.php?a=b"},
|
||||
{"/?a=b", "/testdir/ /index.php?{query}", "/testdir/?a=b"},
|
||||
}
|
||||
|
||||
uri := func(r *url.URL) string {
|
||||
uri := r.Path
|
||||
if r.RawQuery != "" {
|
||||
uri += "?" + r.RawQuery
|
||||
}
|
||||
return uri
|
||||
}
|
||||
for i, test := range tests {
|
||||
r, err := http.NewRequest("GET", test.url, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
To(fs, r, test.to)
|
||||
if uri(r.URL) != test.expected {
|
||||
t.Errorf("Test %v: expected %v found %v", i, test.expected, uri(r.URL))
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user