@marcel91200 / caddy Mirror · read-only / commits / 09a85170
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: query string enh.; substring replace; add tests (see #2891)

Matthew Holt committed Dec 12, 2019 at 14:32 UTC 09a851706541317a36c7cc8ee58152a0a2fa3279
3 files changed +342 -55
modules/caddyhttp/replacer.go
+1 -5
@@ -105,11 +105,7 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon
105 case "http.request.uri.query":
106 return req.URL.RawQuery, true
107 case "http.request.uri.query_string":
108 - qs := req.URL.Query().Encode()
109 - if qs != "" {
110 - qs = "?" + qs
111 - }
112 - return qs, true
108 + return "?" + req.URL.Query().Encode(), true
109
110 // original request, before any internal changes
111 case "http.request.orig_method":
modules/caddyhttp/rewrite/rewrite.go
+99 -50
@@ -35,8 +35,9 @@ type Rewrite struct {
35 Method string `json:"method,omitempty"`
36 URI string `json:"uri,omitempty"`
37
38 - StripPathPrefix string `json:"strip_path_prefix,omitempty"`
39 - StripPathSuffix string `json:"strip_path_suffix,omitempty"`
38 + StripPathPrefix string `json:"strip_path_prefix,omitempty"`
39 + StripPathSuffix string `json:"strip_path_suffix,omitempty"`
40 + URISubstring []replacer `json:"uri_substring,omitempty"`
41
42 HTTPRedirect caddyhttp.WeakString `json:"http_redirect,omitempty"`
43 Rehandle bool `json:"rehandle,omitempty"`
@@ -61,49 +62,76 @@ func (rewr *Rewrite) Provision(ctx caddy.Context) error {
62 // Validate ensures rewr's configuration is valid.
63 func (rewr Rewrite) Validate() error {
64 if rewr.HTTPRedirect != "" && rewr.Rehandle {
64 - return fmt.Errorf("cannot be configured to both write a redirect response and rehandle internally")
65 + return fmt.Errorf("cannot be configured to both redirect externally and rehandle internally")
66 }
67 return nil
68 }
69
70 func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
71 repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer)
71 - var changed bool
72
73 logger := rewr.logger.With(
74 zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: r}),
75 )
76
77 - // rewrite the method
77 + changed := rewr.rewrite(r, repl, logger)
78 +
79 + if changed {
80 + logger.Debug("rewrote request",
81 + zap.String("method", r.Method),
82 + zap.String("uri", r.RequestURI),
83 + )
84 + if rewr.Rehandle {
85 + return caddyhttp.ErrRehandle
86 + }
87 + if rewr.HTTPRedirect != "" {
88 + statusCode, err := strconv.Atoi(repl.ReplaceAll(rewr.HTTPRedirect.String(), ""))
89 + if err != nil {
90 + return caddyhttp.Error(http.StatusInternalServerError, err)
91 + }
92 + w.Header().Set("Location", r.RequestURI)
93 + w.WriteHeader(statusCode)
94 + return nil
95 + }
96 + }
97 +
98 + return next.ServeHTTP(w, r)
99 +}
100 +
101 +// rewrite performs the rewrites on r using repl, which
102 +// should have been obtained from r, but is passed in for
103 +// efficiency. It returns true if any changes were made to r.
104 +func (rewr Rewrite) rewrite(r *http.Request, repl caddy.Replacer, logger *zap.Logger) bool {
105 + oldMethod := r.Method
106 + oldURI := r.RequestURI
107 +
108 + // method
109 if rewr.Method != "" {
79 - method := r.Method
110 r.Method = strings.ToUpper(repl.ReplaceAll(rewr.Method, ""))
81 - if r.Method != method {
82 - changed = true
83 - }
111 }
112
86 - // rewrite the URI
113 + // uri (which consists of path, query string, and maybe fragment?)
114 if rewr.URI != "" {
88 - oldURI := r.RequestURI
115 newURI := repl.ReplaceAll(rewr.URI, "")
116
91 - u, err := url.Parse(newURI)
117 + newU, err := url.Parse(newURI)
118 if err != nil {
93 - return caddyhttp.Error(http.StatusInternalServerError, err)
119 + logger.Error("parsing new URI",
120 + zap.String("raw_input", rewr.URI),
121 + zap.String("input", newURI),
122 + zap.Error(err),
123 + )
124 }
125
96 - r.RequestURI = newURI
97 - r.URL.Path = u.Path
98 - if u.RawQuery != "" {
99 - r.URL.RawQuery = u.RawQuery
126 + if newU.Path != "" {
127 + r.URL.Path = newU.Path
128 }
101 - if u.Fragment != "" {
102 - r.URL.Fragment = u.Fragment
129 + if newU.RawQuery != "" {
130 + newU.RawQuery = strings.TrimPrefix(newU.RawQuery, "&")
131 + r.URL.RawQuery = newU.RawQuery
132 }
104 -
105 - if newURI != oldURI {
106 - changed = true
133 + if newU.Fragment != "" {
134 + r.URL.Fragment = newU.Fragment
135 }
136 }
137
@@ -111,42 +139,63 @@ func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
139 if rewr.StripPathPrefix != "" {
140 prefix := repl.ReplaceAll(rewr.StripPathPrefix, "")
141 r.URL.Path = strings.TrimPrefix(r.URL.Path, prefix)
114 - newURI := r.URL.String()
115 - if newURI != r.RequestURI {
116 - changed = true
117 - }
118 - r.RequestURI = newURI
142 }
143 if rewr.StripPathSuffix != "" {
144 suffix := repl.ReplaceAll(rewr.StripPathSuffix, "")
145 r.URL.Path = strings.TrimSuffix(r.URL.Path, suffix)
123 - newURI := r.URL.String()
124 - if newURI != r.RequestURI {
125 - changed = true
126 - }
127 - r.RequestURI = newURI
146 }
147
130 - if changed {
131 - logger.Debug("rewrote request",
132 - zap.String("method", r.Method),
133 - zap.String("uri", r.RequestURI),
134 - )
135 - if rewr.Rehandle {
136 - return caddyhttp.ErrRehandle
137 - }
138 - if rewr.HTTPRedirect != "" {
139 - statusCode, err := strconv.Atoi(repl.ReplaceAll(rewr.HTTPRedirect.String(), ""))
140 - if err != nil {
141 - return caddyhttp.Error(http.StatusInternalServerError, err)
142 - }
143 - w.Header().Set("Location", r.RequestURI)
144 - w.WriteHeader(statusCode)
145 - return nil
146 - }
148 + // substring replacements in URI
149 + for _, rep := range rewr.URISubstring {
150 + rep.do(r, repl)
151 }
152
149 - return next.ServeHTTP(w, r)
153 + // update the encoded copy of the URI
154 + r.RequestURI = r.URL.RequestURI()
155 +
156 + // return true if anything changed
157 + return r.Method != oldMethod || r.RequestURI != oldURI
158 +}
159 +
160 +// replacer describes a simple and fast substring replacement.
161 +type replacer struct {
162 + // The substring to find. Supports placeholders.
163 + Find string `json:"find,omitempty"`
164 +
165 + // The substring to replace. Supports placeholders.
166 + Replace string `json:"replace,omitempty"`
167 +
168 + // Maximum number of replacements per string.
169 + // Set to <= 0 for no limit (default).
170 + Limit int `json:"limit,omitempty"`
171 +}
172 +
173 +// do performs the replacement on r and returns true if any changes were made.
174 +func (rep replacer) do(r *http.Request, repl caddy.Replacer) bool {
175 + if rep.Find == "" || rep.Replace == "" {
176 + return false
177 + }
178 +
179 + lim := rep.Limit
180 + if lim == 0 {
181 + lim = -1
182 + }
183 +
184 + find := repl.ReplaceAll(rep.Find, "")
185 + replace := repl.ReplaceAll(rep.Replace, "")
186 +
187 + oldPath := r.URL.Path
188 + oldQuery := r.URL.RawQuery
189 +
190 + r.URL.Path = strings.Replace(oldPath, find, replace, lim)
191 + r.URL.RawQuery = strings.Replace(oldQuery, find, replace, lim)
192 +
193 + // changed := r.URL.Path != oldPath && r.URL.RawQuery != oldQuery
194 + // if changed {
195 + // r.RequestURI = r.URL.RequestURI()
196 + // }
197 +
198 + return r.URL.Path != oldPath && r.URL.RawQuery != oldQuery
199 }
200
201 // Interface guard
modules/caddyhttp/rewrite/rewrite_test.go new
+242
@@ -0,0 +1,242 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package rewrite
16 +
17 +import (
18 + "net/http"
19 + "testing"
20 +
21 + "github.com/caddyserver/caddy/v2"
22 +)
23 +
24 +func TestRewrite(t *testing.T) {
25 + repl := caddy.NewReplacer()
26 +
27 + for i, tc := range []struct {
28 + input, expect *http.Request
29 + rule Rewrite
30 + }{
31 + {
32 + input: newRequest(t, "GET", "/"),
33 + expect: newRequest(t, "GET", "/"),
34 + },
35 + {
36 + rule: Rewrite{Method: "GET", URI: "/"},
37 + input: newRequest(t, "GET", "/"),
38 + expect: newRequest(t, "GET", "/"),
39 + },
40 + {
41 + rule: Rewrite{Method: "POST"},
42 + input: newRequest(t, "GET", "/"),
43 + expect: newRequest(t, "POST", "/"),
44 + },
45 +
46 + {
47 + rule: Rewrite{URI: "/foo"},
48 + input: newRequest(t, "GET", "/"),
49 + expect: newRequest(t, "GET", "/foo"),
50 + },
51 + {
52 + rule: Rewrite{URI: "/foo"},
53 + input: newRequest(t, "GET", "/bar"),
54 + expect: newRequest(t, "GET", "/foo"),
55 + },
56 + {
57 + rule: Rewrite{URI: "foo"},
58 + input: newRequest(t, "GET", "/"),
59 + expect: newRequest(t, "GET", "foo"),
60 + },
61 + {
62 + rule: Rewrite{URI: "/foo{http.request.uri.path}"},
63 + input: newRequest(t, "GET", "/bar"),
64 + expect: newRequest(t, "GET", "/foo/bar"),
65 + },
66 + {
67 + rule: Rewrite{URI: "/?c=d"},
68 + input: newRequest(t, "GET", "/"),
69 + expect: newRequest(t, "GET", "/?c=d"),
70 + },
71 + {
72 + rule: Rewrite{URI: "/?c=d"},
73 + input: newRequest(t, "GET", "/?a=b"),
74 + expect: newRequest(t, "GET", "/?c=d"),
75 + },
76 + {
77 + rule: Rewrite{URI: "?c=d"},
78 + input: newRequest(t, "GET", "/foo"),
79 + expect: newRequest(t, "GET", "/foo?c=d"),
80 + },
81 + {
82 + rule: Rewrite{URI: "/?c=d"},
83 + input: newRequest(t, "GET", "/foo"),
84 + expect: newRequest(t, "GET", "/?c=d"),
85 + },
86 + {
87 + rule: Rewrite{URI: "/?{http.request.uri.query}&c=d"},
88 + input: newRequest(t, "GET", "/"),
89 + expect: newRequest(t, "GET", "/?c=d"),
90 + },
91 + {
92 + rule: Rewrite{URI: "/foo?{http.request.uri.query}&c=d"},
93 + input: newRequest(t, "GET", "/"),
94 + expect: newRequest(t, "GET", "/foo?c=d"),
95 + },
96 + {
97 + rule: Rewrite{URI: "?{http.request.uri.query}&c=d"},
98 + input: newRequest(t, "GET", "/foo"),
99 + expect: newRequest(t, "GET", "/foo?c=d"),
100 + },
101 + {
102 + rule: Rewrite{URI: "{http.request.uri.path}?{http.request.uri.query}&c=d"},
103 + input: newRequest(t, "GET", "/foo"),
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"},
108 + input: newRequest(t, "GET", "/foo"),
109 + expect: newRequest(t, "GET", "/foo?c=d"),
110 + },
111 + {
112 + rule: Rewrite{URI: "/index.php?{http.request.uri.query}&c=d"},
113 + input: newRequest(t, "GET", "/foo"),
114 + expect: newRequest(t, "GET", "/index.php?c=d"),
115 + },
116 + {
117 + rule: Rewrite{URI: "?a=b&c=d"},
118 + input: newRequest(t, "GET", "/foo"),
119 + expect: newRequest(t, "GET", "/foo?a=b&c=d"),
120 + },
121 + {
122 + rule: Rewrite{URI: "/index.php?{http.request.uri.query}&c=d"},
123 + input: newRequest(t, "GET", "/?a=b"),
124 + expect: newRequest(t, "GET", "/index.php?a=b&c=d"),
125 + },
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?c=d&a=b"),
130 + },
131 + {
132 + rule: Rewrite{URI: "/index.php?{http.request.uri.query}&p={http.request.uri.path}"},
133 + input: newRequest(t, "GET", "/foo/bar?a=b"),
134 + expect: newRequest(t, "GET", "/index.php?a=b&p=/foo/bar"),
135 + },
136 +
137 + {
138 + rule: Rewrite{StripPathPrefix: "/prefix"},
139 + input: newRequest(t, "GET", "/foo/bar"),
140 + expect: newRequest(t, "GET", "/foo/bar"),
141 + },
142 + {
143 + rule: Rewrite{StripPathPrefix: "/prefix"},
144 + input: newRequest(t, "GET", "/prefix/foo/bar"),
145 + expect: newRequest(t, "GET", "/foo/bar"),
146 + },
147 + {
148 + rule: Rewrite{StripPathPrefix: "/prefix"},
149 + input: newRequest(t, "GET", "/foo/prefix/bar"),
150 + expect: newRequest(t, "GET", "/foo/prefix/bar"),
151 + },
152 +
153 + {
154 + rule: Rewrite{StripPathSuffix: "/suffix"},
155 + input: newRequest(t, "GET", "/foo/bar"),
156 + expect: newRequest(t, "GET", "/foo/bar"),
157 + },
158 + {
159 + rule: Rewrite{StripPathSuffix: "suffix"},
160 + input: newRequest(t, "GET", "/foo/bar/suffix"),
161 + expect: newRequest(t, "GET", "/foo/bar/"),
162 + },
163 + {
164 + rule: Rewrite{StripPathSuffix: "/suffix"},
165 + input: newRequest(t, "GET", "/foo/suffix/bar"),
166 + expect: newRequest(t, "GET", "/foo/suffix/bar"),
167 + },
168 +
169 + {
170 + rule: Rewrite{URISubstring: []replacer{{Find: "findme", Replace: "replaced"}}},
171 + input: newRequest(t, "GET", "/foo/bar"),
172 + expect: newRequest(t, "GET", "/foo/bar"),
173 + },
174 + {
175 + rule: Rewrite{URISubstring: []replacer{{Find: "findme", Replace: "replaced"}}},
176 + input: newRequest(t, "GET", "/foo/findme/bar"),
177 + expect: newRequest(t, "GET", "/foo/replaced/bar"),
178 + },
179 + } {
180 + // copy the original input just enough so that we can
181 + // compare it after the rewrite to see if it changed
182 + originalInput := &http.Request{
183 + Method: tc.input.Method,
184 + RequestURI: tc.input.RequestURI,
185 + URL: &*tc.input.URL,
186 + }
187 +
188 + // populate the replacer just enough for our tests
189 + repl.Set("http.request.uri.path", tc.input.URL.Path)
190 + repl.Set("http.request.uri.query", tc.input.URL.RawQuery)
191 + repl.Set("http.request.uri.query_string", "?"+tc.input.URL.Query().Encode())
192 +
193 + changed := tc.rule.rewrite(tc.input, repl, nil)
194 +
195 + if expected, actual := !reqEqual(originalInput, tc.input), changed; expected != actual {
196 + t.Errorf("Test %d: Expected changed=%t but was %t", i, expected, actual)
197 + }
198 + if expected, actual := tc.expect.Method, tc.input.Method; expected != actual {
199 + t.Errorf("Test %d: Expected Method='%s' but got '%s'", i, expected, actual)
200 + }
201 + if expected, actual := tc.expect.RequestURI, tc.input.RequestURI; expected != actual {
202 + t.Errorf("Test %d: Expected RequestURI='%s' but got '%s'", i, expected, actual)
203 + }
204 + if expected, actual := tc.expect.URL.String(), tc.input.URL.String(); expected != actual {
205 + t.Errorf("Test %d: Expected URL='%s' but got '%s'", i, expected, actual)
206 + }
207 + if expected, actual := tc.expect.URL.RequestURI(), tc.input.URL.RequestURI(); expected != actual {
208 + t.Errorf("Test %d: Expected URL.RequestURI()='%s' but got '%s'", i, expected, actual)
209 + }
210 + }
211 +}
212 +
213 +func newRequest(t *testing.T, method, uri string) *http.Request {
214 + req, err := http.NewRequest(method, uri, nil)
215 + if err != nil {
216 + t.Fatalf("error creating request: %v", err)
217 + }
218 + req.RequestURI = req.URL.RequestURI() // simulate incoming request
219 + return req
220 +}
221 +
222 +// reqEqual if r1 and r2 are equal enough for our purposes.
223 +func reqEqual(r1, r2 *http.Request) bool {
224 + if r1.Method != r2.Method {
225 + return false
226 + }
227 + if r1.RequestURI != r2.RequestURI {
228 + return false
229 + }
230 + if (r1.URL == nil && r2.URL != nil) || (r1.URL != nil && r2.URL == nil) {
231 + return false
232 + }
233 + if r1.URL == nil && r2.URL == nil {
234 + return true
235 + }
236 + return r1.URL.Scheme == r2.URL.Scheme &&
237 + r1.URL.Host == r2.URL.Host &&
238 + r1.URL.Path == r2.URL.Path &&
239 + r1.URL.RawPath == r2.URL.RawPath &&
240 + r1.URL.RawQuery == r2.URL.RawQuery &&
241 + r1.URL.Fragment == r2.URL.Fragment
242 +}