@marcel91200 / caddy Mirror · read-only
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 11, 01:00 UTC

master
go 866 lines 29.4 KB
Raw
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 "reflect"
20 "regexp"
21 "strings"
22 "testing"
23
24 "github.com/caddyserver/caddy/v2"
25 )
26
27 func TestRewrite(t *testing.T) {
28 repl := caddy.NewReplacer()
29
30 for i, tc := range []struct {
31 input, expect *http.Request
32 rule Rewrite
33 }{
34 {
35 input: newRequest(t, "GET", "/"),
36 expect: newRequest(t, "GET", "/"),
37 },
38 {
39 rule: Rewrite{Method: "GET", URI: "/"},
40 input: newRequest(t, "GET", "/"),
41 expect: newRequest(t, "GET", "/"),
42 },
43 {
44 rule: Rewrite{Method: "POST"},
45 input: newRequest(t, "GET", "/"),
46 expect: newRequest(t, "POST", "/"),
47 },
48
49 {
50 rule: Rewrite{URI: "/foo"},
51 input: newRequest(t, "GET", "/"),
52 expect: newRequest(t, "GET", "/foo"),
53 },
54 {
55 rule: Rewrite{URI: "/foo"},
56 input: newRequest(t, "GET", "/bar"),
57 expect: newRequest(t, "GET", "/foo"),
58 },
59 {
60 rule: Rewrite{URI: "foo"},
61 input: newRequest(t, "GET", "/"),
62 expect: newRequest(t, "GET", "foo"),
63 },
64 {
65 rule: Rewrite{URI: "{http.request.uri}"},
66 input: newRequest(t, "GET", "/bar%3Fbaz?c=d"),
67 expect: newRequest(t, "GET", "/bar%3Fbaz?c=d"),
68 },
69 {
70 rule: Rewrite{URI: "{http.request.uri.path}"},
71 input: newRequest(t, "GET", "/bar%3Fbaz"),
72 expect: newRequest(t, "GET", "/bar%3Fbaz"),
73 },
74 {
75 rule: Rewrite{URI: "/foo{http.request.uri.path}"},
76 input: newRequest(t, "GET", "/bar"),
77 expect: newRequest(t, "GET", "/foo/bar"),
78 },
79 {
80 rule: Rewrite{URI: "/index.php?p={http.request.uri.path}"},
81 input: newRequest(t, "GET", "/foo/bar"),
82 expect: newRequest(t, "GET", "/index.php?p=%2Ffoo%2Fbar"),
83 },
84 {
85 rule: Rewrite{URI: "?a=b&{http.request.uri.query}"},
86 input: newRequest(t, "GET", "/"),
87 expect: newRequest(t, "GET", "/?a=b"),
88 },
89 {
90 rule: Rewrite{URI: "/?c=d"},
91 input: newRequest(t, "GET", "/"),
92 expect: newRequest(t, "GET", "/?c=d"),
93 },
94 {
95 rule: Rewrite{URI: "/?c=d"},
96 input: newRequest(t, "GET", "/?a=b"),
97 expect: newRequest(t, "GET", "/?c=d"),
98 },
99 {
100 rule: Rewrite{URI: "?c=d"},
101 input: newRequest(t, "GET", "/foo"),
102 expect: newRequest(t, "GET", "/foo?c=d"),
103 },
104 {
105 rule: Rewrite{URI: "/?c=d"},
106 input: newRequest(t, "GET", "/foo"),
107 expect: newRequest(t, "GET", "/?c=d"),
108 },
109 {
110 rule: Rewrite{URI: "/?{http.request.uri.query}&c=d"},
111 input: newRequest(t, "GET", "/"),
112 expect: newRequest(t, "GET", "/?c=d"),
113 },
114 {
115 rule: Rewrite{URI: "/foo?{http.request.uri.query}&c=d"},
116 input: newRequest(t, "GET", "/"),
117 expect: newRequest(t, "GET", "/foo?c=d"),
118 },
119 {
120 rule: Rewrite{URI: "?{http.request.uri.query}&c=d"},
121 input: newRequest(t, "GET", "/foo"),
122 expect: newRequest(t, "GET", "/foo?c=d"),
123 },
124 {
125 rule: Rewrite{URI: "{http.request.uri.path}?{http.request.uri.query}&c=d"},
126 input: newRequest(t, "GET", "/foo"),
127 expect: newRequest(t, "GET", "/foo?c=d"),
128 },
129 {
130 rule: Rewrite{URI: "{http.request.uri.path}?{http.request.uri.query}&c=d"},
131 input: newRequest(t, "GET", "/foo"),
132 expect: newRequest(t, "GET", "/foo?c=d"),
133 },
134 {
135 rule: Rewrite{URI: "/index.php?{http.request.uri.query}&c=d"},
136 input: newRequest(t, "GET", "/foo"),
137 expect: newRequest(t, "GET", "/index.php?c=d"),
138 },
139 {
140 rule: Rewrite{URI: "?a=b&c=d"},
141 input: newRequest(t, "GET", "/foo"),
142 expect: newRequest(t, "GET", "/foo?a=b&c=d"),
143 },
144 {
145 rule: Rewrite{URI: "/index.php?{http.request.uri.query}&c=d"},
146 input: newRequest(t, "GET", "/?a=b"),
147 expect: newRequest(t, "GET", "/index.php?a=b&c=d"),
148 },
149 {
150 rule: Rewrite{URI: "/index.php?c=d&{http.request.uri.query}"},
151 input: newRequest(t, "GET", "/?a=b"),
152 expect: newRequest(t, "GET", "/index.php?c=d&a=b"),
153 },
154 {
155 rule: Rewrite{URI: "/index.php?{http.request.uri.query}&p={http.request.uri.path}"},
156 input: newRequest(t, "GET", "/foo/bar?a=b"),
157 expect: newRequest(t, "GET", "/index.php?a=b&p=%2Ffoo%2Fbar"),
158 },
159 {
160 rule: Rewrite{URI: "{http.request.uri.path}?"},
161 input: newRequest(t, "GET", "/foo/bar?a=b&c=d"),
162 expect: newRequest(t, "GET", "/foo/bar"),
163 },
164 {
165 rule: Rewrite{URI: "?qs={http.request.uri.query}"},
166 input: newRequest(t, "GET", "/foo?a=b&c=d"),
167 expect: newRequest(t, "GET", "/foo?qs=a%3Db%26c%3Dd"),
168 },
169 {
170 rule: Rewrite{URI: "/foo?{http.request.uri.query}#frag"},
171 input: newRequest(t, "GET", "/foo/bar?a=b"),
172 expect: newRequest(t, "GET", "/foo?a=b#frag"),
173 },
174 {
175 rule: Rewrite{URI: "/foo{http.request.uri}"},
176 input: newRequest(t, "GET", "/bar?a=b"),
177 expect: newRequest(t, "GET", "/foo/bar?a=b"),
178 },
179 {
180 rule: Rewrite{URI: "/foo{http.request.uri}"},
181 input: newRequest(t, "GET", "/bar"),
182 expect: newRequest(t, "GET", "/foo/bar"),
183 },
184 {
185 rule: Rewrite{URI: "/foo{http.request.uri}?c=d"},
186 input: newRequest(t, "GET", "/bar?a=b"),
187 expect: newRequest(t, "GET", "/foo/bar?c=d"),
188 },
189 {
190 rule: Rewrite{URI: "/foo{http.request.uri}?{http.request.uri.query}&c=d"},
191 input: newRequest(t, "GET", "/bar?a=b"),
192 expect: newRequest(t, "GET", "/foo/bar?a=b&c=d"),
193 },
194 {
195 rule: Rewrite{URI: "{http.request.uri}"},
196 input: newRequest(t, "GET", "/bar?a=b"),
197 expect: newRequest(t, "GET", "/bar?a=b"),
198 },
199 {
200 rule: Rewrite{URI: "{http.request.uri.path}bar?c=d"},
201 input: newRequest(t, "GET", "/foo/?a=b"),
202 expect: newRequest(t, "GET", "/foo/bar?c=d"),
203 },
204 {
205 rule: Rewrite{URI: "/i{http.request.uri}"},
206 input: newRequest(t, "GET", "/%C2%B7%E2%88%B5.png"),
207 expect: newRequest(t, "GET", "/i/%C2%B7%E2%88%B5.png"),
208 },
209 {
210 rule: Rewrite{URI: "/i{http.request.uri}"},
211 input: newRequest(t, "GET", "/·∵.png?a=b"),
212 expect: newRequest(t, "GET", "/i/%C2%B7%E2%88%B5.png?a=b"),
213 },
214 {
215 rule: Rewrite{URI: "/i{http.request.uri}"},
216 input: newRequest(t, "GET", "/%C2%B7%E2%88%B5.png?a=b"),
217 expect: newRequest(t, "GET", "/i/%C2%B7%E2%88%B5.png?a=b"),
218 },
219 {
220 rule: Rewrite{URI: "/bar#?"},
221 input: newRequest(t, "GET", "/foo#fragFirst?c=d"), // not a valid query string (is part of fragment)
222 expect: newRequest(t, "GET", "/bar#?"), // I think this is right? but who knows; std lib drops fragment when parsing
223 },
224 {
225 rule: Rewrite{URI: "/bar"},
226 input: newRequest(t, "GET", "/foo#fragFirst?c=d"),
227 expect: newRequest(t, "GET", "/bar#fragFirst?c=d"),
228 },
229 {
230 rule: Rewrite{URI: "/api/admin/panel"},
231 input: newRequest(t, "GET", "/api/admin%2Fpanel"),
232 expect: newRequest(t, "GET", "/api/admin/panel"),
233 },
234
235 {
236 rule: Rewrite{StripPathPrefix: "/prefix"},
237 input: newRequest(t, "GET", "/foo/bar"),
238 expect: newRequest(t, "GET", "/foo/bar"),
239 },
240 {
241 rule: Rewrite{StripPathPrefix: "/prefix"},
242 input: newRequest(t, "GET", "/prefix/foo/bar"),
243 expect: newRequest(t, "GET", "/foo/bar"),
244 },
245 {
246 rule: Rewrite{StripPathPrefix: "prefix"},
247 input: newRequest(t, "GET", "/prefix/foo/bar"),
248 expect: newRequest(t, "GET", "/foo/bar"),
249 },
250 {
251 rule: Rewrite{StripPathPrefix: "/prefix"},
252 input: newRequest(t, "GET", "/prefix"),
253 expect: newRequest(t, "GET", "/"),
254 },
255 {
256 rule: Rewrite{StripPathPrefix: "/prefix"},
257 input: newRequest(t, "GET", "/"),
258 expect: newRequest(t, "GET", "/"),
259 },
260 {
261 rule: Rewrite{StripPathPrefix: "/prefix"},
262 input: newRequest(t, "GET", "/prefix/foo%2Fbar"),
263 expect: newRequest(t, "GET", "/foo%2Fbar"),
264 },
265 {
266 rule: Rewrite{StripPathPrefix: "/prefix"},
267 input: newRequest(t, "GET", "/foo/prefix/bar"),
268 expect: newRequest(t, "GET", "/foo/prefix/bar"),
269 },
270 {
271 // shorter (percent-encoded) path that is not the prefix must be left alone
272 rule: Rewrite{StripPathPrefix: "/aaaaaa"},
273 input: newRequest(t, "GET", "/%61%61"),
274 expect: newRequest(t, "GET", "/%61%61"),
275 },
276 {
277 rule: Rewrite{StripPathPrefix: "//prefix"},
278 // scheme and host needed for URL parser to succeed in setting up test
279 input: newRequest(t, "GET", "http://host//prefix/foo/bar"),
280 expect: newRequest(t, "GET", "http://host/foo/bar"),
281 },
282 {
283 rule: Rewrite{StripPathPrefix: "//prefix"},
284 input: newRequest(t, "GET", "/prefix/foo/bar"),
285 expect: newRequest(t, "GET", "/prefix/foo/bar"),
286 },
287 {
288 rule: Rewrite{StripPathPrefix: "/a%2Fb/c"},
289 input: newRequest(t, "GET", "/a%2Fb/c/d"),
290 expect: newRequest(t, "GET", "/d"),
291 },
292 {
293 rule: Rewrite{StripPathPrefix: "/a%2Fb/c"},
294 input: newRequest(t, "GET", "/a%2fb/c/d"),
295 expect: newRequest(t, "GET", "/d"),
296 },
297 {
298 rule: Rewrite{StripPathPrefix: "/a/b/c"},
299 input: newRequest(t, "GET", "/a%2Fb/c/d"),
300 expect: newRequest(t, "GET", "/d"),
301 },
302 {
303 rule: Rewrite{StripPathPrefix: "/a%2Fb/c"},
304 input: newRequest(t, "GET", "/a/b/c/d"),
305 expect: newRequest(t, "GET", "/a/b/c/d"),
306 },
307 {
308 rule: Rewrite{StripPathPrefix: "//a%2Fb/c"},
309 input: newRequest(t, "GET", "/a/b/c/d"),
310 expect: newRequest(t, "GET", "/a/b/c/d"),
311 },
312 {
313 rule: Rewrite{StripPathPrefix: "/%"},
314 input: newRequest(t, "GET", "/%25file"),
315 expect: newRequest(t, "GET", "/file"),
316 },
317 {
318 rule: Rewrite{StripPathPrefix: "/%2"},
319 input: newRequest(t, "GET", "/%252file"),
320 expect: newRequest(t, "GET", "/file"),
321 },
322 {
323 rule: Rewrite{StripPathPrefix: "/%zz"},
324 input: newRequest(t, "GET", "/%25zzfile"),
325 expect: newRequest(t, "GET", "/file"),
326 },
327
328 {
329 rule: Rewrite{StripPathSuffix: "/suffix"},
330 input: newRequest(t, "GET", "/foo/bar"),
331 expect: newRequest(t, "GET", "/foo/bar"),
332 },
333 {
334 rule: Rewrite{StripPathSuffix: "/suffix"},
335 input: newRequest(t, "GET", "/suffix"),
336 expect: newRequest(t, "GET", "/"),
337 },
338 {
339 rule: Rewrite{StripPathSuffix: "suffix"},
340 input: newRequest(t, "GET", "/foo/bar/suffix"),
341 expect: newRequest(t, "GET", "/foo/bar/"),
342 },
343 {
344 rule: Rewrite{StripPathSuffix: "suffix"},
345 input: newRequest(t, "GET", "/foo%2Fbar/suffix"),
346 expect: newRequest(t, "GET", "/foo%2Fbar/"),
347 },
348 {
349 rule: Rewrite{StripPathSuffix: "%2fsuffix"},
350 input: newRequest(t, "GET", "/foo%2Fbar%2fsuffix"),
351 expect: newRequest(t, "GET", "/foo%2Fbar"),
352 },
353 {
354 rule: Rewrite{StripPathSuffix: "/suffix"},
355 input: newRequest(t, "GET", "/foo/suffix/bar"),
356 expect: newRequest(t, "GET", "/foo/suffix/bar"),
357 },
358 {
359 // a decoded suffix pattern must match a percent-encoded path in
360 // normalized space, mirroring StripPathPrefix (see the "/a/b/c"
361 // vs "/a%2Fb/c/d" case above)
362 rule: Rewrite{StripPathSuffix: "/b/c"},
363 input: newRequest(t, "GET", "/a/b%2Fc"),
364 expect: newRequest(t, "GET", "/a"),
365 },
366 {
367 // decoded suffix char matches its percent-encoded form in the path
368 rule: Rewrite{StripPathSuffix: "bc"},
369 input: newRequest(t, "GET", "/a%62c"), // %62 == 'b'
370 expect: newRequest(t, "GET", "/a"),
371 },
372 {
373 // an escaped suffix pattern requires the path to use the same
374 // escape at that position, so a decoded path must NOT be stripped
375 rule: Rewrite{StripPathSuffix: "%2fsuffix"},
376 input: newRequest(t, "GET", "/foo/bar/suffix"),
377 expect: newRequest(t, "GET", "/foo/bar/suffix"),
378 },
379 {
380 // the suffix pattern's own '%' must be compared literally: here the
381 // path's "%25" decodes to a literal '%', which is not the escape
382 // introducer the pattern asks for, so nothing may be stripped
383 rule: Rewrite{StripPathSuffix: "%41"},
384 input: newRequest(t, "GET", "/file%2541"), // decodes to "/file%41"
385 expect: newRequest(t, "GET", "/file%2541"),
386 },
387 {
388 rule: Rewrite{StripPathSuffix: "%2f"},
389 input: newRequest(t, "GET", "/file%252f"), // decodes to "/file%2f"
390 expect: newRequest(t, "GET", "/file%252f"),
391 },
392 {
393 rule: Rewrite{StripPathSuffix: "%25"},
394 input: newRequest(t, "GET", "/file%2525"), // decodes to "/file%25"
395 expect: newRequest(t, "GET", "/file%2525"),
396 },
397 {
398 // ...but a path that really does use the pattern's escape is stripped
399 rule: Rewrite{StripPathSuffix: "%41"},
400 input: newRequest(t, "GET", "/file%41"),
401 expect: newRequest(t, "GET", "/file"),
402 },
403 {
404 rule: Rewrite{StripPathSuffix: "%2f"},
405 input: newRequest(t, "GET", "/file%2f"),
406 expect: newRequest(t, "GET", "/file"),
407 },
408 {
409 rule: Rewrite{StripPathSuffix: "%25"},
410 input: newRequest(t, "GET", "/file%25"),
411 expect: newRequest(t, "GET", "/file"),
412 },
413 {
414 rule: Rewrite{StripPathSuffix: "%"},
415 input: newRequest(t, "GET", "/file%25"),
416 expect: newRequest(t, "GET", "/file"),
417 },
418 {
419 rule: Rewrite{StripPathSuffix: "%2"},
420 input: newRequest(t, "GET", "/file%252"),
421 expect: newRequest(t, "GET", "/file"),
422 },
423 {
424 rule: Rewrite{StripPathSuffix: "%zz"},
425 input: newRequest(t, "GET", "/file%25zz"),
426 expect: newRequest(t, "GET", "/file"),
427 },
428 {
429 // an escaped suffix pattern must not match the decoded character
430 rule: Rewrite{StripPathSuffix: "%2f"},
431 input: newRequest(t, "GET", "/file/"),
432 expect: newRequest(t, "GET", "/file/"),
433 },
434 {
435 // a decoded suffix pattern still matches the path's escaped form
436 rule: Rewrite{StripPathSuffix: ".html"},
437 input: newRequest(t, "GET", "/a%2ehtml"), // %2e == '.'
438 expect: newRequest(t, "GET", "/a"),
439 },
440
441 {
442 rule: Rewrite{URISubstring: []substrReplacer{{Find: "findme", Replace: "replaced"}}},
443 input: newRequest(t, "GET", "/foo/bar"),
444 expect: newRequest(t, "GET", "/foo/bar"),
445 },
446 {
447 rule: Rewrite{URISubstring: []substrReplacer{{Find: "findme", Replace: "replaced"}}},
448 input: newRequest(t, "GET", "/foo/findme/bar"),
449 expect: newRequest(t, "GET", "/foo/replaced/bar"),
450 },
451 {
452 rule: Rewrite{URISubstring: []substrReplacer{{Find: "findme", Replace: "replaced"}}},
453 input: newRequest(t, "GET", "/foo/findme%2Fbar"),
454 expect: newRequest(t, "GET", "/foo/replaced%2Fbar"),
455 },
456 {
457 rule: Rewrite{URISubstring: []substrReplacer{{Find: "%28", Replace: "("}}},
458 input: newRequest(t, "GET", "/hello/%28%29"),
459 expect: newRequest(t, "GET", "/hello/(%29"),
460 },
461 {
462 rule: Rewrite{URISubstring: []substrReplacer{
463 {Find: "%28", Replace: "("},
464 {Find: "%29", Replace: ")"},
465 }},
466 input: newRequest(t, "GET", "/hello/%28%29"),
467 expect: newRequest(t, "GET", "/hello/()"),
468 },
469
470 {
471 rule: Rewrite{PathRegexp: []*regexReplacer{{Find: "/{2,}", Replace: "/"}}},
472 input: newRequest(t, "GET", "/foo//bar///baz?a=b//c"),
473 expect: newRequest(t, "GET", "/foo/bar/baz?a=b//c"),
474 },
475
476 // regression tests for GHSA-j8px-rmrx-76h9: when the rewrite URI
477 // ends with a literal '?', the first-pass placeholder expansion
478 // may produce a path containing attacker-controlled bytes that
479 // then get split at '?' and fed into buildQueryString, which runs
480 // a SECOND placeholder pass. Bytes injected via a header value (or
481 // any other client-controlled placeholder) must not be treated as
482 // placeholder syntax during this second pass.
483 {
484 // literal header value containing placeholder syntax is not re-expanded into query
485 rule: Rewrite{URI: "/serve/{http.request.header.X-Fwd}?"},
486 input: newRequestWithHeader(t, "GET", "/anything", "X-Fwd", "foo?{env.CADDY_REWRITE_TEST_SECRET}=leak"),
487 expect: newRequest(t, "GET", "/serve/foo?%7Benv.CADDY_REWRITE_TEST_SECRET%7D=leak"),
488 },
489 {
490 // literal header value with placeholder syntax in query position is not re-expanded
491 rule: Rewrite{URI: "/serve/{http.request.header.X-Fwd}?"},
492 input: newRequestWithHeader(t, "GET", "/anything", "X-Fwd", "ok?key={env.CADDY_REWRITE_TEST_SECRET}"),
493 expect: newRequest(t, "GET", "/serve/ok?key=%7Benv.CADDY_REWRITE_TEST_SECRET%7D"),
494 },
495 {
496 // literal header value with embedded file placeholder is not re-expanded
497 rule: Rewrite{URI: "/serve/{http.request.header.X-Fwd}?"},
498 input: newRequestWithHeader(t, "GET", "/anything", "X-Fwd", "ok?path={file./etc/passwd}"),
499 expect: newRequest(t, "GET", "/serve/ok?path=%7Bfile./etc/passwd%7D"),
500 },
501
502 // '=' delimits a key from its value only once; any further '=' bytes
503 // are literal data, such as base64 padding in a signature.
504 {
505 rule: Rewrite{URI: "?sig=YWJjZA=="},
506 input: newRequest(t, "GET", "/hello"),
507 expect: newRequest(t, "GET", "/hello?sig=YWJjZA=="),
508 },
509 {
510 rule: Rewrite{URI: "?x=1&sig=YWJjZA=="},
511 input: newRequest(t, "GET", "/hello"),
512 expect: newRequest(t, "GET", "/hello?x=1&sig=YWJjZA=="),
513 },
514
515 // a query string that arrives via a replacement value is honored even
516 // though the configured URI has no literal '?' (see issue #5208).
517 {
518 rule: Rewrite{URI: "{http.request.header.X-Accel-Redirect}"},
519 input: newRequestWithHeader(t, "GET", "/orig", "X-Accel-Redirect", "/hello?some=param&some_other=param"),
520 expect: newRequest(t, "GET", "/hello?some=param&some_other=param"),
521 },
522 {
523 // an injected query replaces the original one, as a configured query would
524 rule: Rewrite{URI: "{http.request.header.X-Accel-Redirect}"},
525 input: newRequestWithHeader(t, "GET", "/orig?keep=me", "X-Accel-Redirect", "/hello?some=param"),
526 expect: newRequest(t, "GET", "/hello?some=param"),
527 },
528 {
529 // a value ending in '?' clears the query, same as configuring a bare '?'
530 rule: Rewrite{URI: "{http.request.header.X-Accel-Redirect}"},
531 input: newRequestWithHeader(t, "GET", "/orig?keep=me", "X-Accel-Redirect", "/hello?"),
532 expect: newRequest(t, "GET", "/hello"),
533 },
534 {
535 // no '?' in the value means the query is not touched
536 rule: Rewrite{URI: "{http.request.header.X-Accel-Redirect}"},
537 input: newRequestWithHeader(t, "GET", "/orig?keep=me", "X-Accel-Redirect", "/hello"),
538 expect: newRequest(t, "GET", "/hello?keep=me"),
539 },
540 {
541 // an explicitly configured query still wins over an injected one
542 rule: Rewrite{URI: "{http.request.header.X-Accel-Redirect}?a=b"},
543 input: newRequestWithHeader(t, "GET", "/orig", "X-Accel-Redirect", "/hello?some=param"),
544 expect: newRequest(t, "GET", "/hello?a=b"),
545 },
546 {
547 // an escaped '?' in the request path does not split the URI, so the
548 // implicit rewrites of try_files and php_fastcgi stay safe
549 rule: Rewrite{URI: "{http.request.uri.path}"},
550 input: newRequest(t, "GET", "/bar%3Fbaz?keep=me"),
551 expect: newRequest(t, "GET", "/bar%3Fbaz?keep=me"),
552 },
553 {
554 // an S3 presigned URL arriving via a replacement value survives intact
555 rule: Rewrite{URI: "{http.request.header.X-Ph}"},
556 input: newRequestWithHeader(t, "GET", "/orig", "X-Ph", "/f.jpg?X-Amz-Credential=AK%2Ffr-par%2Fs3&X-Amz-Signature=YWJjZA=="),
557 expect: newRequest(t, "GET", "/f.jpg?X-Amz-Credential=AK%2Ffr-par%2Fs3&X-Amz-Signature=YWJjZA=="),
558 },
559
560 // a fragment that arrives via a replacement value is dropped: everything
561 // after '#' is fragment (RFC 3986 section 4.2) and is never sent to the
562 // server, so it must not leak into the path or query.
563 {
564 rule: Rewrite{URI: "{http.request.header.X-Ph}"},
565 input: newRequestWithHeader(t, "GET", "/orig", "X-Ph", "/hello#frag"),
566 expect: newRequest(t, "GET", "/hello"),
567 },
568 {
569 rule: Rewrite{URI: "{http.request.header.X-Ph}"},
570 input: newRequestWithHeader(t, "GET", "/orig", "X-Ph", "/hello?a=b#frag"),
571 expect: newRequest(t, "GET", "/hello?a=b"),
572 },
573 {
574 // a '?' after the injected '#' is fragment, not a query delimiter
575 rule: Rewrite{URI: "{http.request.header.X-Ph}"},
576 input: newRequestWithHeader(t, "GET", "/orig", "X-Ph", "/hello#frag?notquery"),
577 expect: newRequest(t, "GET", "/hello"),
578 },
579 {
580 // a configured fragment still wins over an injected one
581 rule: Rewrite{URI: "{http.request.header.X-Ph}#cfgfrag"},
582 input: newRequestWithHeader(t, "GET", "/orig", "X-Ph", "/hello?a=b#frag"),
583 expect: newRequest(t, "GET", "/hello?a=b#cfgfrag"),
584 },
585 {
586 // an escaped '#' is not a fragment delimiter and is preserved
587 rule: Rewrite{URI: "{http.request.header.X-Ph}"},
588 input: newRequestWithHeader(t, "GET", "/orig", "X-Ph", "/hello%23frag"),
589 expect: newRequest(t, "GET", "/hello%23frag"),
590 },
591 {
592 // a fragment-only value leaves the original query alone
593 rule: Rewrite{URI: "{http.request.header.X-Ph}"},
594 input: newRequestWithHeader(t, "GET", "/orig?keep=me", "X-Ph", "/hello#frag"),
595 expect: newRequest(t, "GET", "/hello?keep=me"),
596 },
597 } {
598 // copy the original input just enough so that we can
599 // compare it after the rewrite to see if it changed
600 urlCopy := *tc.input.URL
601 originalInput := &http.Request{
602 Method: tc.input.Method,
603 RequestURI: tc.input.RequestURI,
604 URL: &urlCopy,
605 }
606
607 // populate the replacer just enough for our tests
608 repl.Set("http.request.uri", tc.input.RequestURI)
609 repl.Set("http.request.uri.path", tc.input.URL.Path)
610 repl.Set("http.request.uri.query", tc.input.URL.RawQuery)
611 for field, vals := range tc.input.Header {
612 repl.Set("http.request.header."+field, strings.Join(vals, ","))
613 }
614
615 // we can't directly call Provision() without a valid caddy.Context
616 // (TODO: fix that) so here we ad-hoc compile the regex
617 for _, rep := range tc.rule.PathRegexp {
618 re, err := regexp.Compile(rep.Find)
619 if err != nil {
620 t.Fatal(err)
621 }
622 rep.re = re
623 }
624
625 changed := tc.rule.Rewrite(tc.input, repl)
626
627 if expected, actual := !reqEqual(originalInput, tc.input), changed; expected != actual {
628 t.Errorf("Test %d: Expected changed=%t but was %t", i, expected, actual)
629 }
630 if expected, actual := tc.expect.Method, tc.input.Method; expected != actual {
631 t.Errorf("Test %d: Expected Method='%s' but got '%s'", i, expected, actual)
632 }
633 if expected, actual := tc.expect.RequestURI, tc.input.RequestURI; expected != actual {
634 t.Errorf("Test %d: Expected RequestURI='%s' but got '%s'", i, expected, actual)
635 }
636 if expected, actual := tc.expect.URL.String(), tc.input.URL.String(); expected != actual {
637 t.Errorf("Test %d: Expected URL='%s' but got '%s'", i, expected, actual)
638 }
639 if expected, actual := tc.expect.URL.RequestURI(), tc.input.URL.RequestURI(); expected != actual {
640 t.Errorf("Test %d: Expected URL.RequestURI()='%s' but got '%s'", i, expected, actual)
641 }
642 if expected, actual := tc.expect.URL.Fragment, tc.input.URL.Fragment; expected != actual {
643 t.Errorf("Test %d: Expected URL.Fragment='%s' but got '%s'", i, expected, actual)
644 }
645 }
646 }
647
648 func TestRewriteEscapesFileMatcherRemainder(t *testing.T) {
649 for i, tc := range []struct {
650 input string
651 remainder string
652 expect string
653 }{
654 {
655 input: "/index.php/foo/bar?a=b",
656 remainder: "/foo/bar",
657 expect: "/index.php/foo/bar?a=b",
658 },
659 {
660 input: "/index.php/..%252fprivate%252fsecret.txt",
661 remainder: "/..%2fprivate%2fsecret.txt",
662 expect: "/index.php/..%252fprivate%252fsecret.txt",
663 },
664 {
665 input: "/index.php/x%3Fa=evil?a=b",
666 remainder: "/x?a=evil",
667 expect: "/index.php/x%3Fa=evil?a=b",
668 },
669 {
670 input: "/index.php/x%23frag?a=b",
671 remainder: "/x#frag",
672 expect: "/index.php/x%23frag?a=b",
673 },
674 } {
675 req := newRequest(t, "GET", tc.input)
676 repl := caddy.NewReplacer()
677 repl.Set("http.matchers.file.relative", "/index.php")
678 repl.Set("http.matchers.file.remainder", tc.remainder)
679
680 Rewrite{URI: "{http.matchers.file.relative}{http.matchers.file.remainder}"}.Rewrite(req, repl)
681
682 if expected, actual := tc.expect, req.URL.RequestURI(); expected != actual {
683 t.Errorf("Test %d: Expected URL.RequestURI()='%s' but got '%s'", i, expected, actual)
684 }
685 }
686 }
687
688 func TestQueryOpsRenameNoOpCases(t *testing.T) {
689 repl := caddy.NewReplacer()
690
691 for i, tc := range []struct {
692 input *http.Request
693 expect map[string][]string
694 ops *queryOps
695 }{
696 {
697 ops: &queryOps{
698 Rename: []queryOpsArguments{{Key: "ID", Val: "id"}},
699 },
700 input: newRequest(t, "GET", "/?page=test&id=5&test=100"),
701 expect: map[string][]string{"id": {"5"}, "page": {"test"}, "test": {"100"}},
702 },
703 {
704 ops: &queryOps{
705 Rename: []queryOpsArguments{{Key: "id", Val: "id"}},
706 },
707 input: newRequest(t, "GET", "/?page=test&id=5&test=100"),
708 expect: map[string][]string{"id": {"5"}, "page": {"test"}, "test": {"100"}},
709 },
710 {
711 ops: &queryOps{
712 Rename: []queryOpsArguments{{Key: "ID", Val: "id"}},
713 },
714 input: newRequest(t, "GET", "/?page=test&ID=5&test=100"),
715 expect: map[string][]string{"id": {"5"}, "page": {"test"}, "test": {"100"}},
716 },
717 {
718 ops: &queryOps{
719 Rename: []queryOpsArguments{{Key: "ID", Val: "id"}},
720 },
721 input: newRequest(t, "GET", "/?page=test&ID=5&id=7&test=100"),
722 expect: map[string][]string{"id": {"5"}, "page": {"test"}, "test": {"100"}},
723 },
724 } {
725 repl.Set("http.request.uri", tc.input.RequestURI)
726 repl.Set("http.request.uri.path", tc.input.URL.Path)
727 repl.Set("http.request.uri.query", tc.input.URL.RawQuery)
728
729 tc.ops.do(tc.input, repl)
730
731 if actual := tc.input.URL.Query(); !reflect.DeepEqual(tc.expect, map[string][]string(actual)) {
732 t.Errorf("Test %d: Expected query=%v but got %v", i, tc.expect, actual)
733 }
734 }
735 }
736
737 func TestQueryOpsReplaceScopedToKey(t *testing.T) {
738 repl := caddy.NewReplacer()
739
740 for i, tc := range []struct {
741 input *http.Request
742 expect map[string][]string
743 ops *queryOps
744 }{
745 {
746 // a keyed replace must only touch the named key, even when
747 // other keys hold the same value
748 ops: &queryOps{
749 Replace: []*queryOpsReplacement{{Key: "a", Search: "foo", Replace: "bar"}},
750 },
751 input: newRequest(t, "GET", "/?a=foo&b=foo"),
752 expect: map[string][]string{"a": {"bar"}, "b": {"foo"}},
753 },
754 {
755 // "*" still replaces across every key
756 ops: &queryOps{
757 Replace: []*queryOpsReplacement{{Key: "*", Search: "foo", Replace: "bar"}},
758 },
759 input: newRequest(t, "GET", "/?a=foo&b=foo"),
760 expect: map[string][]string{"a": {"bar"}, "b": {"bar"}},
761 },
762 {
763 // a keyed replace against a missing key is a no-op
764 ops: &queryOps{
765 Replace: []*queryOpsReplacement{{Key: "missing", Search: "foo", Replace: "bar"}},
766 },
767 input: newRequest(t, "GET", "/?a=foo&b=foo"),
768 expect: map[string][]string{"a": {"foo"}, "b": {"foo"}},
769 },
770 {
771 // the regexp branch must also stay scoped to the named key
772 ops: &queryOps{
773 Replace: []*queryOpsReplacement{{Key: "a", SearchRegexp: "f.o", Replace: "bar"}},
774 },
775 input: newRequest(t, "GET", "/?a=foo&b=foo"),
776 expect: map[string][]string{"a": {"bar"}, "b": {"foo"}},
777 },
778 } {
779 repl.Set("http.request.uri", tc.input.RequestURI)
780 repl.Set("http.request.uri.path", tc.input.URL.Path)
781 repl.Set("http.request.uri.query", tc.input.URL.RawQuery)
782
783 for _, rep := range tc.ops.Replace {
784 if err := rep.Provision(caddy.Context{}); err != nil {
785 t.Fatal(err)
786 }
787 }
788
789 tc.ops.do(tc.input, repl)
790
791 if actual := tc.input.URL.Query(); !reflect.DeepEqual(tc.expect, map[string][]string(actual)) {
792 t.Errorf("Test %d: Expected query=%v but got %v", i, tc.expect, actual)
793 }
794 }
795 }
796
797 func TestStripPathPrefixCanonicalizesRemainder(t *testing.T) {
798 for _, tc := range []struct {
799 name string
800 target string
801 prefix string
802 path string
803 rawPath string
804 }{
805 {name: "empty remainder", target: "/mount", prefix: "/mount", path: "/"},
806 {name: "mid-segment remainder", target: "/mountain", prefix: "/mount", path: "/ain"},
807 {name: "parent segment", target: "/mount../document", prefix: "/mount", path: "/document"},
808 {name: "current segment", target: "/mount./document/", prefix: "/mount", path: "/document/"},
809 {name: "encoded parent segment", target: "/mount%2e%2e/document", prefix: "/mount", path: "/document"},
810 {name: "encoded leading separator", target: "/mount%2Fdocument", prefix: "/mount", path: "/document"},
811 {name: "alternate separator encoding", target: "/mount/a%2Fb", prefix: "/mount", path: "/a/b", rawPath: "/a%2Fb"},
812 {name: "double-encoded parent segment", target: "/mount/%252e%252e/document", prefix: "/mount", path: "/%2e%2e/document"},
813 {name: "non-UTF-8 byte", target: "/mount/%ff", prefix: "/mount", path: "/\xff", rawPath: "/%ff"},
814 {name: "repeated separators", target: "/mount//a//b/", prefix: "/mount//", path: "/a//b/"},
815 {name: "ordinary remainder", target: "/mount/document", prefix: "/mount", path: "/document"},
816 } {
817 t.Run(tc.name, func(t *testing.T) {
818 r := newRequest(t, http.MethodGet, tc.target+"?keep=yes")
819 Rewrite{StripPathPrefix: tc.prefix}.Rewrite(r, caddy.NewReplacer())
820
821 if r.URL.Path != tc.path || r.URL.RawPath != tc.rawPath {
822 t.Errorf("Path/RawPath = %q/%q; want %q/%q", r.URL.Path, r.URL.RawPath, tc.path, tc.rawPath)
823 }
824 if r.URL.RawQuery != "keep=yes" || r.RequestURI != r.URL.EscapedPath()+"?keep=yes" {
825 t.Errorf("query or request target changed inconsistently: %q", r.RequestURI)
826 }
827 })
828 }
829 }
830
831 func newRequest(t *testing.T, method, uri string) *http.Request {
832 req, err := http.NewRequest(method, uri, nil)
833 if err != nil {
834 t.Fatalf("error creating request: %v", err)
835 }
836 req.RequestURI = req.URL.RequestURI() // simulate incoming request
837 return req
838 }
839
840 func newRequestWithHeader(t *testing.T, method, uri, headerKey, headerVal string) *http.Request {
841 req := newRequest(t, method, uri)
842 req.Header.Set(headerKey, headerVal)
843 return req
844 }
845
846 // reqEqual if r1 and r2 are equal enough for our purposes.
847 func reqEqual(r1, r2 *http.Request) bool {
848 if r1.Method != r2.Method {
849 return false
850 }
851 if r1.RequestURI != r2.RequestURI {
852 return false
853 }
854 if (r1.URL == nil && r2.URL != nil) || (r1.URL != nil && r2.URL == nil) {
855 return false
856 }
857 if r1.URL == nil && r2.URL == nil {
858 return true
859 }
860 return r1.URL.Scheme == r2.URL.Scheme &&
861 r1.URL.Host == r2.URL.Host &&
862 r1.URL.Path == r2.URL.Path &&
863 r1.URL.RawPath == r2.URL.RawPath &&
864 r1.URL.RawQuery == r2.URL.RawQuery &&
865 r1.URL.Fragment == r2.URL.Fragment
866 }