Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
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.
Matthew Holt committed
Jan 10, 2020 at 16:59 UTC
d418e319ab626e4b7dab676a7db3405d24a4b58c
3 files changed
+102
-54
modules/caddyhttp/rewrite/caddyfile.go
+2
-2
@@ -58,7 +58,7 @@ func parseCaddyfileStripPrefix(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHand
58
if !h.NextArg() {
59
return nil, h.ArgErr()
60
}
61
- rewr.StripPathPrefix = h.Val()
61
+ rewr.StripPrefix = h.Val()
62
if h.NextArg() {
63
return nil, h.ArgErr()
64
}
@@ -77,7 +77,7 @@ func parseCaddyfileStripSuffix(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHand
77
if !h.NextArg() {
78
return nil, h.ArgErr()
79
}
80
- rewr.StripPathSuffix = h.Val()
80
+ rewr.StripSuffix = h.Val()
81
if h.NextArg() {
82
return nil, h.ArgErr()
83
}
modules/caddyhttp/rewrite/rewrite.go
+87
-43
@@ -32,7 +32,7 @@ func init() {
32
// Rewrite is a middleware which can rewrite HTTP requests.
33
//
34
// These rewrite properties are applied to a request in this order:
35
-// Method, URI, StripPathPrefix, StripPathSuffix, URISubstring.
35
+// Method, URI, StripPrefix, StripSuffix, URISubstring.
36
//
37
// TODO: This module is still a WIP and may experience breaking changes.
38
type Rewrite struct {
@@ -44,10 +44,10 @@ type Rewrite struct {
44
URI string `json:"uri,omitempty"`
45
46
// Strips the given prefix from the beginning of the URI path.
47
- StripPathPrefix string `json:"strip_path_prefix,omitempty"`
47
+ StripPrefix string `json:"strip_prefix,omitempty"`
48
49
// Strips the given suffix from the end of the URI path.
50
- StripPathSuffix string `json:"strip_path_suffix,omitempty"`
50
+ StripSuffix string `json:"strip_suffix,omitempty"`
51
52
// Performs substring replacements on the URI.
53
URISubstring []replacer `json:"uri_substring,omitempty"`
@@ -102,9 +102,9 @@ func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
102
return next.ServeHTTP(w, r)
103
}
104
105
-// rewrite performs the rewrites on r using repl, which
106
-// should have been obtained from r, but is passed in for
107
-// efficiency. It returns true if any changes were made to r.
105
+// rewrite performs the rewrites on r using repl, which should
106
+// have been obtained from r, but is passed in for efficiency.
107
+// It returns true if any changes were made to r.
108
func (rewr Rewrite) rewrite(r *http.Request, repl *caddy.Replacer, logger *zap.Logger) bool {
109
oldMethod := r.Method
110
oldURI := r.RequestURI
@@ -114,53 +114,46 @@ func (rewr Rewrite) rewrite(r *http.Request, repl *caddy.Replacer, logger *zap.L
114
r.Method = strings.ToUpper(repl.ReplaceAll(rewr.Method, ""))
115
}
116
117
- // uri (which consists of path, query string, and maybe fragment?)
118
- if rewr.URI != "" {
119
- newURI := repl.ReplaceAll(rewr.URI, "")
120
-
121
- newU, err := url.Parse(newURI)
122
- if err != nil {
123
- logger.Error("parsing new URI",
124
- zap.String("raw_input", rewr.URI),
125
- zap.String("input", newURI),
126
- zap.Error(err),
127
- )
117
+ // uri (path, query string, and fragment just because)
118
+ if uri := rewr.URI; uri != "" {
119
+ // find the bounds of each part of the URI that exist
120
+ pathStart, qsStart, fragStart := -1, -1, -1
121
+ pathEnd, qsEnd := -1, -1
122
+ for i, ch := range uri {
123
+ switch {
124
+ case ch == '?' && qsStart < 0:
125
+ pathEnd, qsStart = i, i+1
126
+ case ch == '#' && fragStart < 0:
127
+ qsEnd, fragStart = i, i+1
128
+ case pathStart < 0 && qsStart < 0 && fragStart < 0:
129
+ pathStart = i
130
+ }
131
+ }
132
+ if pathStart >= 0 && pathEnd < 0 {
133
+ pathEnd = len(uri)
134
+ }
135
+ if qsStart >= 0 && qsEnd < 0 {
136
+ qsEnd = len(uri)
137
}
138
130
- if newU.Path != "" {
131
- r.URL.Path = newU.Path
139
+ if pathStart >= 0 {
140
+ r.URL.Path = repl.ReplaceAll(uri[pathStart:pathEnd], "")
141
}
133
- if strings.Contains(newURI, "?") {
134
- // you'll notice we check for existence of a question mark
135
- // instead of RawQuery != "". We do this because if the user
136
- // wants to remove an existing query string, they do that by
137
- // appending "?" to the path: "/foo?" -- in this case, then,
138
- // RawQuery is "" but we still want to set it to that; hence,
139
- // we check for a "?", which always starts a query string
140
- inputQuery := newU.Query()
141
- outputQuery := make(url.Values)
142
- for k := range inputQuery {
143
- // overwrite existing values; we don't simply keep
144
- // appending because it can cause rewrite rules like
145
- // "{path}{query}&a=b" with rehandling enabled to go
146
- // on forever: "/foo.html?a=b&a=b&a=b..."
147
- outputQuery.Set(k, inputQuery.Get(k))
148
- }
149
- // this sorts the keys, oh well
150
- r.URL.RawQuery = outputQuery.Encode()
142
+ if qsStart >= 0 {
143
+ r.URL.RawQuery = buildQueryString(uri[qsStart:qsEnd], repl)
144
}
152
- if newU.Fragment != "" {
153
- r.URL.Fragment = newU.Fragment
145
+ if fragStart >= 0 {
146
+ r.URL.Fragment = repl.ReplaceAll(uri[fragStart:], "")
147
}
148
}
149
150
// strip path prefix or suffix
158
- if rewr.StripPathPrefix != "" {
159
- prefix := repl.ReplaceAll(rewr.StripPathPrefix, "")
151
+ if rewr.StripPrefix != "" {
152
+ prefix := repl.ReplaceAll(rewr.StripPrefix, "")
153
r.URL.Path = strings.TrimPrefix(r.URL.Path, prefix)
154
}
162
- if rewr.StripPathSuffix != "" {
163
- suffix := repl.ReplaceAll(rewr.StripPathSuffix, "")
155
+ if rewr.StripSuffix != "" {
156
+ suffix := repl.ReplaceAll(rewr.StripSuffix, "")
157
r.URL.Path = strings.TrimSuffix(r.URL.Path, suffix)
158
}
159
@@ -176,6 +169,57 @@ func (rewr Rewrite) rewrite(r *http.Request, repl *caddy.Replacer, logger *zap.L
169
return r.Method != oldMethod || r.RequestURI != oldURI
170
}
171
172
+// buildQueryString takes an input query string and
173
+// performs replacements on each component, returning
174
+// the resulting query string.
175
+func buildQueryString(qs string, repl *caddy.Replacer) string {
176
+ var sb strings.Builder
177
+ var wroteKey bool
178
+
179
+ for len(qs) > 0 {
180
+ // determine the end of this component
181
+ nextEq, nextAmp := strings.Index(qs, "="), strings.Index(qs, "&")
182
+ end := min(nextEq, nextAmp)
183
+ if end == -1 {
184
+ end = len(qs) // if there is nothing left, go to end of string
185
+ }
186
+
187
+ // consume the component and write the result
188
+ comp := qs[:end]
189
+ comp, _ = repl.ReplaceFunc(comp, func(name, val string) (string, error) {
190
+ if name == "http.request.uri.query" {
191
+ return val, nil // already escaped
192
+ }
193
+ return url.QueryEscape(val), nil
194
+ })
195
+ if end < len(qs) {
196
+ end++ // consume delimiter
197
+ }
198
+ qs = qs[end:]
199
+
200
+ if wroteKey {
201
+ sb.WriteRune('=')
202
+ } else if sb.Len() > 0 {
203
+ sb.WriteRune('&')
204
+ }
205
+
206
+ // remember that we just wrote a key, which is if the next
207
+ // delimiter is an equals sign or if there is no ampersand
208
+ wroteKey = nextEq < nextAmp || nextAmp < 0
209
+
210
+ sb.WriteString(comp)
211
+ }
212
+
213
+ return sb.String()
214
+}
215
+
216
+func min(a, b int) int {
217
+ if b < a {
218
+ return b
219
+ }
220
+ return a
221
+}
222
+
223
// replacer describes a simple and fast substring replacement.
224
type replacer struct {
225
// The substring to find. Supports placeholders.
modules/caddyhttp/rewrite/rewrite_test.go
+13
-9
@@ -104,7 +104,7 @@ func TestRewrite(t *testing.T) {
104
expect: newRequest(t, "GET", "/foo?c=d"),
105
},
106
{
107
- rule: Rewrite{URI: "{http.request.uri.path}{http.request.uri.query_string}&c=d"},
107
+ rule: Rewrite{URI: "{http.request.uri.path}?{http.request.uri.query}&c=d"},
108
input: newRequest(t, "GET", "/foo"),
109
expect: newRequest(t, "GET", "/foo?c=d"),
110
},
@@ -126,7 +126,7 @@ func TestRewrite(t *testing.T) {
126
{
127
rule: Rewrite{URI: "/index.php?c=d&{http.request.uri.query}"},
128
input: newRequest(t, "GET", "/?a=b"),
129
- expect: newRequest(t, "GET", "/index.php?a=b&c=d"),
129
+ expect: newRequest(t, "GET", "/index.php?c=d&a=b"),
130
},
131
{
132
rule: Rewrite{URI: "/index.php?{http.request.uri.query}&p={http.request.uri.path}"},
@@ -138,35 +138,40 @@ func TestRewrite(t *testing.T) {
138
input: newRequest(t, "GET", "/foo/bar?a=b&c=d"),
139
expect: newRequest(t, "GET", "/foo/bar"),
140
},
141
+ {
142
+ rule: Rewrite{URI: "/foo?{http.request.uri.query}#frag"},
143
+ input: newRequest(t, "GET", "/foo/bar?a=b"),
144
+ expect: newRequest(t, "GET", "/foo?a=b#frag"),
145
+ },
146
147
{
143
- rule: Rewrite{StripPathPrefix: "/prefix"},
148
+ rule: Rewrite{StripPrefix: "/prefix"},
149
input: newRequest(t, "GET", "/foo/bar"),
150
expect: newRequest(t, "GET", "/foo/bar"),
151
},
152
{
148
- rule: Rewrite{StripPathPrefix: "/prefix"},
153
+ rule: Rewrite{StripPrefix: "/prefix"},
154
input: newRequest(t, "GET", "/prefix/foo/bar"),
155
expect: newRequest(t, "GET", "/foo/bar"),
156
},
157
{
153
- rule: Rewrite{StripPathPrefix: "/prefix"},
158
+ rule: Rewrite{StripPrefix: "/prefix"},
159
input: newRequest(t, "GET", "/foo/prefix/bar"),
160
expect: newRequest(t, "GET", "/foo/prefix/bar"),
161
},
162
163
{
159
- rule: Rewrite{StripPathSuffix: "/suffix"},
164
+ rule: Rewrite{StripSuffix: "/suffix"},
165
input: newRequest(t, "GET", "/foo/bar"),
166
expect: newRequest(t, "GET", "/foo/bar"),
167
},
168
{
164
- rule: Rewrite{StripPathSuffix: "suffix"},
169
+ rule: Rewrite{StripSuffix: "suffix"},
170
input: newRequest(t, "GET", "/foo/bar/suffix"),
171
expect: newRequest(t, "GET", "/foo/bar/"),
172
},
173
{
169
- rule: Rewrite{StripPathSuffix: "/suffix"},
174
+ rule: Rewrite{StripSuffix: "/suffix"},
175
input: newRequest(t, "GET", "/foo/suffix/bar"),
176
expect: newRequest(t, "GET", "/foo/suffix/bar"),
177
},
@@ -193,7 +198,6 @@ func TestRewrite(t *testing.T) {
198
// populate the replacer just enough for our tests
199
repl.Set("http.request.uri.path", tc.input.URL.Path)
200
repl.Set("http.request.uri.query", tc.input.URL.RawQuery)
196
- repl.Set("http.request.uri.query_string", "?"+tc.input.URL.RawQuery)
201
202
changed := tc.rule.rewrite(tc.input, repl, nil)
203