@marcel91200 / caddy Mirror · read-only / commits / d418e319
Fork 0

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