mirror of
https://github.com/caddyserver/caddy.git
synced 2025-02-05 08:38:26 +03:00
rewrite: Rename parameters; implement custom query string parser
Our new parser also preserves original parameter order, rather than re-encoding using the std lib (which sorts). The renamed parameters are a breaking change but they're new enough that I don't think anyone is using them.
This commit is contained in:
parent
ba514f9660
commit
d418e319ab
3 changed files with 104 additions and 56 deletions
|
@ -58,7 +58,7 @@ func parseCaddyfileStripPrefix(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHand
|
||||||
if !h.NextArg() {
|
if !h.NextArg() {
|
||||||
return nil, h.ArgErr()
|
return nil, h.ArgErr()
|
||||||
}
|
}
|
||||||
rewr.StripPathPrefix = h.Val()
|
rewr.StripPrefix = h.Val()
|
||||||
if h.NextArg() {
|
if h.NextArg() {
|
||||||
return nil, h.ArgErr()
|
return nil, h.ArgErr()
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,7 @@ func parseCaddyfileStripSuffix(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHand
|
||||||
if !h.NextArg() {
|
if !h.NextArg() {
|
||||||
return nil, h.ArgErr()
|
return nil, h.ArgErr()
|
||||||
}
|
}
|
||||||
rewr.StripPathSuffix = h.Val()
|
rewr.StripSuffix = h.Val()
|
||||||
if h.NextArg() {
|
if h.NextArg() {
|
||||||
return nil, h.ArgErr()
|
return nil, h.ArgErr()
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ func init() {
|
||||||
// Rewrite is a middleware which can rewrite HTTP requests.
|
// Rewrite is a middleware which can rewrite HTTP requests.
|
||||||
//
|
//
|
||||||
// These rewrite properties are applied to a request in this order:
|
// These rewrite properties are applied to a request in this order:
|
||||||
// Method, URI, StripPathPrefix, StripPathSuffix, URISubstring.
|
// Method, URI, StripPrefix, StripSuffix, URISubstring.
|
||||||
//
|
//
|
||||||
// TODO: This module is still a WIP and may experience breaking changes.
|
// TODO: This module is still a WIP and may experience breaking changes.
|
||||||
type Rewrite struct {
|
type Rewrite struct {
|
||||||
|
@ -44,10 +44,10 @@ type Rewrite struct {
|
||||||
URI string `json:"uri,omitempty"`
|
URI string `json:"uri,omitempty"`
|
||||||
|
|
||||||
// Strips the given prefix from the beginning of the URI path.
|
// Strips the given prefix from the beginning of the URI path.
|
||||||
StripPathPrefix string `json:"strip_path_prefix,omitempty"`
|
StripPrefix string `json:"strip_prefix,omitempty"`
|
||||||
|
|
||||||
// Strips the given suffix from the end of the URI path.
|
// Strips the given suffix from the end of the URI path.
|
||||||
StripPathSuffix string `json:"strip_path_suffix,omitempty"`
|
StripSuffix string `json:"strip_suffix,omitempty"`
|
||||||
|
|
||||||
// Performs substring replacements on the URI.
|
// Performs substring replacements on the URI.
|
||||||
URISubstring []replacer `json:"uri_substring,omitempty"`
|
URISubstring []replacer `json:"uri_substring,omitempty"`
|
||||||
|
@ -102,9 +102,9 @@ func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
|
||||||
return next.ServeHTTP(w, r)
|
return next.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// rewrite performs the rewrites on r using repl, which
|
// rewrite performs the rewrites on r using repl, which should
|
||||||
// should have been obtained from r, but is passed in for
|
// have been obtained from r, but is passed in for efficiency.
|
||||||
// efficiency. It returns true if any changes were made to r.
|
// It returns true if any changes were made to r.
|
||||||
func (rewr Rewrite) rewrite(r *http.Request, repl *caddy.Replacer, logger *zap.Logger) bool {
|
func (rewr Rewrite) rewrite(r *http.Request, repl *caddy.Replacer, logger *zap.Logger) bool {
|
||||||
oldMethod := r.Method
|
oldMethod := r.Method
|
||||||
oldURI := r.RequestURI
|
oldURI := r.RequestURI
|
||||||
|
@ -114,53 +114,46 @@ func (rewr Rewrite) rewrite(r *http.Request, repl *caddy.Replacer, logger *zap.L
|
||||||
r.Method = strings.ToUpper(repl.ReplaceAll(rewr.Method, ""))
|
r.Method = strings.ToUpper(repl.ReplaceAll(rewr.Method, ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
// uri (which consists of path, query string, and maybe fragment?)
|
// uri (path, query string, and fragment just because)
|
||||||
if rewr.URI != "" {
|
if uri := rewr.URI; uri != "" {
|
||||||
newURI := repl.ReplaceAll(rewr.URI, "")
|
// find the bounds of each part of the URI that exist
|
||||||
|
pathStart, qsStart, fragStart := -1, -1, -1
|
||||||
newU, err := url.Parse(newURI)
|
pathEnd, qsEnd := -1, -1
|
||||||
if err != nil {
|
for i, ch := range uri {
|
||||||
logger.Error("parsing new URI",
|
switch {
|
||||||
zap.String("raw_input", rewr.URI),
|
case ch == '?' && qsStart < 0:
|
||||||
zap.String("input", newURI),
|
pathEnd, qsStart = i, i+1
|
||||||
zap.Error(err),
|
case ch == '#' && fragStart < 0:
|
||||||
)
|
qsEnd, fragStart = i, i+1
|
||||||
}
|
case pathStart < 0 && qsStart < 0 && fragStart < 0:
|
||||||
|
pathStart = i
|
||||||
if newU.Path != "" {
|
|
||||||
r.URL.Path = newU.Path
|
|
||||||
}
|
|
||||||
if strings.Contains(newURI, "?") {
|
|
||||||
// you'll notice we check for existence of a question mark
|
|
||||||
// instead of RawQuery != "". We do this because if the user
|
|
||||||
// wants to remove an existing query string, they do that by
|
|
||||||
// appending "?" to the path: "/foo?" -- in this case, then,
|
|
||||||
// RawQuery is "" but we still want to set it to that; hence,
|
|
||||||
// we check for a "?", which always starts a query string
|
|
||||||
inputQuery := newU.Query()
|
|
||||||
outputQuery := make(url.Values)
|
|
||||||
for k := range inputQuery {
|
|
||||||
// overwrite existing values; we don't simply keep
|
|
||||||
// appending because it can cause rewrite rules like
|
|
||||||
// "{path}{query}&a=b" with rehandling enabled to go
|
|
||||||
// on forever: "/foo.html?a=b&a=b&a=b..."
|
|
||||||
outputQuery.Set(k, inputQuery.Get(k))
|
|
||||||
}
|
}
|
||||||
// this sorts the keys, oh well
|
|
||||||
r.URL.RawQuery = outputQuery.Encode()
|
|
||||||
}
|
}
|
||||||
if newU.Fragment != "" {
|
if pathStart >= 0 && pathEnd < 0 {
|
||||||
r.URL.Fragment = newU.Fragment
|
pathEnd = len(uri)
|
||||||
|
}
|
||||||
|
if qsStart >= 0 && qsEnd < 0 {
|
||||||
|
qsEnd = len(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pathStart >= 0 {
|
||||||
|
r.URL.Path = repl.ReplaceAll(uri[pathStart:pathEnd], "")
|
||||||
|
}
|
||||||
|
if qsStart >= 0 {
|
||||||
|
r.URL.RawQuery = buildQueryString(uri[qsStart:qsEnd], repl)
|
||||||
|
}
|
||||||
|
if fragStart >= 0 {
|
||||||
|
r.URL.Fragment = repl.ReplaceAll(uri[fragStart:], "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// strip path prefix or suffix
|
// strip path prefix or suffix
|
||||||
if rewr.StripPathPrefix != "" {
|
if rewr.StripPrefix != "" {
|
||||||
prefix := repl.ReplaceAll(rewr.StripPathPrefix, "")
|
prefix := repl.ReplaceAll(rewr.StripPrefix, "")
|
||||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, prefix)
|
r.URL.Path = strings.TrimPrefix(r.URL.Path, prefix)
|
||||||
}
|
}
|
||||||
if rewr.StripPathSuffix != "" {
|
if rewr.StripSuffix != "" {
|
||||||
suffix := repl.ReplaceAll(rewr.StripPathSuffix, "")
|
suffix := repl.ReplaceAll(rewr.StripSuffix, "")
|
||||||
r.URL.Path = strings.TrimSuffix(r.URL.Path, suffix)
|
r.URL.Path = strings.TrimSuffix(r.URL.Path, suffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,6 +169,57 @@ func (rewr Rewrite) rewrite(r *http.Request, repl *caddy.Replacer, logger *zap.L
|
||||||
return r.Method != oldMethod || r.RequestURI != oldURI
|
return r.Method != oldMethod || r.RequestURI != oldURI
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildQueryString takes an input query string and
|
||||||
|
// performs replacements on each component, returning
|
||||||
|
// the resulting query string.
|
||||||
|
func buildQueryString(qs string, repl *caddy.Replacer) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
var wroteKey bool
|
||||||
|
|
||||||
|
for len(qs) > 0 {
|
||||||
|
// determine the end of this component
|
||||||
|
nextEq, nextAmp := strings.Index(qs, "="), strings.Index(qs, "&")
|
||||||
|
end := min(nextEq, nextAmp)
|
||||||
|
if end == -1 {
|
||||||
|
end = len(qs) // if there is nothing left, go to end of string
|
||||||
|
}
|
||||||
|
|
||||||
|
// consume the component and write the result
|
||||||
|
comp := qs[:end]
|
||||||
|
comp, _ = repl.ReplaceFunc(comp, func(name, val string) (string, error) {
|
||||||
|
if name == "http.request.uri.query" {
|
||||||
|
return val, nil // already escaped
|
||||||
|
}
|
||||||
|
return url.QueryEscape(val), nil
|
||||||
|
})
|
||||||
|
if end < len(qs) {
|
||||||
|
end++ // consume delimiter
|
||||||
|
}
|
||||||
|
qs = qs[end:]
|
||||||
|
|
||||||
|
if wroteKey {
|
||||||
|
sb.WriteRune('=')
|
||||||
|
} else if sb.Len() > 0 {
|
||||||
|
sb.WriteRune('&')
|
||||||
|
}
|
||||||
|
|
||||||
|
// remember that we just wrote a key, which is if the next
|
||||||
|
// delimiter is an equals sign or if there is no ampersand
|
||||||
|
wroteKey = nextEq < nextAmp || nextAmp < 0
|
||||||
|
|
||||||
|
sb.WriteString(comp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func min(a, b int) int {
|
||||||
|
if b < a {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
// replacer describes a simple and fast substring replacement.
|
// replacer describes a simple and fast substring replacement.
|
||||||
type replacer struct {
|
type replacer struct {
|
||||||
// The substring to find. Supports placeholders.
|
// The substring to find. Supports placeholders.
|
||||||
|
|
|
@ -104,7 +104,7 @@ func TestRewrite(t *testing.T) {
|
||||||
expect: newRequest(t, "GET", "/foo?c=d"),
|
expect: newRequest(t, "GET", "/foo?c=d"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rule: Rewrite{URI: "{http.request.uri.path}{http.request.uri.query_string}&c=d"},
|
rule: Rewrite{URI: "{http.request.uri.path}?{http.request.uri.query}&c=d"},
|
||||||
input: newRequest(t, "GET", "/foo"),
|
input: newRequest(t, "GET", "/foo"),
|
||||||
expect: newRequest(t, "GET", "/foo?c=d"),
|
expect: newRequest(t, "GET", "/foo?c=d"),
|
||||||
},
|
},
|
||||||
|
@ -126,7 +126,7 @@ func TestRewrite(t *testing.T) {
|
||||||
{
|
{
|
||||||
rule: Rewrite{URI: "/index.php?c=d&{http.request.uri.query}"},
|
rule: Rewrite{URI: "/index.php?c=d&{http.request.uri.query}"},
|
||||||
input: newRequest(t, "GET", "/?a=b"),
|
input: newRequest(t, "GET", "/?a=b"),
|
||||||
expect: newRequest(t, "GET", "/index.php?a=b&c=d"),
|
expect: newRequest(t, "GET", "/index.php?c=d&a=b"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rule: Rewrite{URI: "/index.php?{http.request.uri.query}&p={http.request.uri.path}"},
|
rule: Rewrite{URI: "/index.php?{http.request.uri.query}&p={http.request.uri.path}"},
|
||||||
|
@ -138,35 +138,40 @@ func TestRewrite(t *testing.T) {
|
||||||
input: newRequest(t, "GET", "/foo/bar?a=b&c=d"),
|
input: newRequest(t, "GET", "/foo/bar?a=b&c=d"),
|
||||||
expect: newRequest(t, "GET", "/foo/bar"),
|
expect: newRequest(t, "GET", "/foo/bar"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
rule: Rewrite{URI: "/foo?{http.request.uri.query}#frag"},
|
||||||
|
input: newRequest(t, "GET", "/foo/bar?a=b"),
|
||||||
|
expect: newRequest(t, "GET", "/foo?a=b#frag"),
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
rule: Rewrite{StripPathPrefix: "/prefix"},
|
rule: Rewrite{StripPrefix: "/prefix"},
|
||||||
input: newRequest(t, "GET", "/foo/bar"),
|
input: newRequest(t, "GET", "/foo/bar"),
|
||||||
expect: newRequest(t, "GET", "/foo/bar"),
|
expect: newRequest(t, "GET", "/foo/bar"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rule: Rewrite{StripPathPrefix: "/prefix"},
|
rule: Rewrite{StripPrefix: "/prefix"},
|
||||||
input: newRequest(t, "GET", "/prefix/foo/bar"),
|
input: newRequest(t, "GET", "/prefix/foo/bar"),
|
||||||
expect: newRequest(t, "GET", "/foo/bar"),
|
expect: newRequest(t, "GET", "/foo/bar"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rule: Rewrite{StripPathPrefix: "/prefix"},
|
rule: Rewrite{StripPrefix: "/prefix"},
|
||||||
input: newRequest(t, "GET", "/foo/prefix/bar"),
|
input: newRequest(t, "GET", "/foo/prefix/bar"),
|
||||||
expect: newRequest(t, "GET", "/foo/prefix/bar"),
|
expect: newRequest(t, "GET", "/foo/prefix/bar"),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
rule: Rewrite{StripPathSuffix: "/suffix"},
|
rule: Rewrite{StripSuffix: "/suffix"},
|
||||||
input: newRequest(t, "GET", "/foo/bar"),
|
input: newRequest(t, "GET", "/foo/bar"),
|
||||||
expect: newRequest(t, "GET", "/foo/bar"),
|
expect: newRequest(t, "GET", "/foo/bar"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rule: Rewrite{StripPathSuffix: "suffix"},
|
rule: Rewrite{StripSuffix: "suffix"},
|
||||||
input: newRequest(t, "GET", "/foo/bar/suffix"),
|
input: newRequest(t, "GET", "/foo/bar/suffix"),
|
||||||
expect: newRequest(t, "GET", "/foo/bar/"),
|
expect: newRequest(t, "GET", "/foo/bar/"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rule: Rewrite{StripPathSuffix: "/suffix"},
|
rule: Rewrite{StripSuffix: "/suffix"},
|
||||||
input: newRequest(t, "GET", "/foo/suffix/bar"),
|
input: newRequest(t, "GET", "/foo/suffix/bar"),
|
||||||
expect: newRequest(t, "GET", "/foo/suffix/bar"),
|
expect: newRequest(t, "GET", "/foo/suffix/bar"),
|
||||||
},
|
},
|
||||||
|
@ -193,7 +198,6 @@ func TestRewrite(t *testing.T) {
|
||||||
// populate the replacer just enough for our tests
|
// populate the replacer just enough for our tests
|
||||||
repl.Set("http.request.uri.path", tc.input.URL.Path)
|
repl.Set("http.request.uri.path", tc.input.URL.Path)
|
||||||
repl.Set("http.request.uri.query", tc.input.URL.RawQuery)
|
repl.Set("http.request.uri.query", tc.input.URL.RawQuery)
|
||||||
repl.Set("http.request.uri.query_string", "?"+tc.input.URL.RawQuery)
|
|
||||||
|
|
||||||
changed := tc.rule.rewrite(tc.input, repl, nil)
|
changed := tc.rule.rewrite(tc.input, repl, nil)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue