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
+}