@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 1,598 lines 38.9 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 caddyhttp
16
17 import (
18 "context"
19 "fmt"
20 "net/http"
21 "net/http/httptest"
22 "net/url"
23 "os"
24 "runtime"
25 "testing"
26
27 "github.com/caddyserver/caddy/v2"
28 )
29
30 func TestHostMatcher(t *testing.T) {
31 err := os.Setenv("GO_BENCHMARK_DOMAIN", "localhost")
32 if err != nil {
33 t.Errorf("error while setting up environment: %v", err)
34 }
35
36 for i, tc := range []struct {
37 match MatchHost
38 input string
39 expect bool
40 }{
41 {
42 match: MatchHost{},
43 input: "example.com",
44 expect: false,
45 },
46 {
47 match: MatchHost{"example.com"},
48 input: "example.com",
49 expect: true,
50 },
51 {
52 match: MatchHost{"EXAMPLE.COM"},
53 input: "example.com",
54 expect: true,
55 },
56 {
57 match: MatchHost{"example.com"},
58 input: "EXAMPLE.COM",
59 expect: true,
60 },
61 {
62 match: MatchHost{"example.com"},
63 input: "foo.example.com",
64 expect: false,
65 },
66 {
67 match: MatchHost{"example.com"},
68 input: "EXAMPLE.COM",
69 expect: true,
70 },
71 {
72 match: MatchHost{"foo.example.com"},
73 input: "foo.example.com",
74 expect: true,
75 },
76 {
77 match: MatchHost{"foo.example.com"},
78 input: "bar.example.com",
79 expect: false,
80 },
81 {
82 match: MatchHost{"éxàmplê.com"},
83 input: "xn--xmpl-0na6cm.com",
84 expect: true,
85 },
86 {
87 match: MatchHost{"*.example.com"},
88 input: "example.com",
89 expect: false,
90 },
91 {
92 match: MatchHost{"*.example.com"},
93 input: "SUB.EXAMPLE.COM",
94 expect: true,
95 },
96 {
97 match: MatchHost{"*.example.com"},
98 input: "foo.example.com",
99 expect: true,
100 },
101 {
102 match: MatchHost{"*.example.com"},
103 input: "foo.bar.example.com",
104 expect: false,
105 },
106 {
107 match: MatchHost{"*.example.com", "example.net"},
108 input: "example.net",
109 expect: true,
110 },
111 {
112 match: MatchHost{"example.net", "*.example.com"},
113 input: "foo.example.com",
114 expect: true,
115 },
116 {
117 match: MatchHost{"*.example.net", "*.*.example.com"},
118 input: "foo.bar.example.com",
119 expect: true,
120 },
121 {
122 match: MatchHost{"*.example.net", "sub.*.example.com"},
123 input: "sub.foo.example.com",
124 expect: true,
125 },
126 {
127 match: MatchHost{"*.example.net", "sub.*.example.com"},
128 input: "sub.foo.example.net",
129 expect: false,
130 },
131 {
132 match: MatchHost{"www.*.*"},
133 input: "www.example.com",
134 expect: true,
135 },
136 {
137 match: MatchHost{"example.com"},
138 input: "example.com:5555",
139 expect: true,
140 },
141 {
142 match: MatchHost{"{env.GO_BENCHMARK_DOMAIN}"},
143 input: "localhost",
144 expect: true,
145 },
146 {
147 match: MatchHost{"{env.GO_NONEXISTENT}"},
148 input: "localhost",
149 expect: false,
150 },
151 } {
152 req := &http.Request{Host: tc.input}
153 repl := caddy.NewReplacer()
154 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
155 req = req.WithContext(ctx)
156
157 if err := tc.match.Provision(caddy.Context{}); err != nil {
158 t.Errorf("Test %d %v: provisioning failed: %v", i, tc.match, err)
159 }
160
161 actual, err := tc.match.MatchWithError(req)
162 if err != nil {
163 t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
164 }
165 if actual != tc.expect {
166 t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input)
167 continue
168 }
169 }
170 }
171
172 func TestPathMatcher(t *testing.T) {
173 t.Setenv("CADDY_TEST_PATH_PREFIX", "admin")
174
175 for i, tc := range []struct {
176 match MatchPath // not URI-encoded because not parsing from a URI
177 input string // should be valid URI encoding (escaped) since it will become part of a request
178 headers http.Header // request headers, for header placeholders in the match
179 expect bool
180 provisionErr bool
181 }{
182 {
183 match: MatchPath{},
184 input: "/",
185 expect: false,
186 },
187 {
188 match: MatchPath{"/"},
189 input: "/",
190 expect: true,
191 },
192 {
193 match: MatchPath{"/foo/bar"},
194 input: "/",
195 expect: false,
196 },
197 {
198 match: MatchPath{"/foo/bar"},
199 input: "/foo/bar",
200 expect: true,
201 },
202 {
203 match: MatchPath{"/foo/bar/"},
204 input: "/foo/bar",
205 expect: false,
206 },
207 {
208 match: MatchPath{"/foo/bar/"},
209 input: "/foo/bar/",
210 expect: true,
211 },
212 {
213 match: MatchPath{"/foo/bar/", "/other"},
214 input: "/other/",
215 expect: false,
216 },
217 {
218 match: MatchPath{"/foo/bar/", "/other"},
219 input: "/other",
220 expect: true,
221 },
222 {
223 match: MatchPath{"*.ext"},
224 input: "/foo/bar.ext",
225 expect: true,
226 },
227 {
228 match: MatchPath{"*.php"},
229 input: "/index.PHP",
230 expect: true,
231 },
232 {
233 match: MatchPath{"*.ext"},
234 input: "/foo/bar.ext",
235 expect: true,
236 },
237 {
238 match: MatchPath{"/foo/*/baz"},
239 input: "/foo/bar/baz",
240 expect: true,
241 },
242 {
243 match: MatchPath{"/foo/*/baz/bam"},
244 input: "/foo/bar/bam",
245 expect: false,
246 },
247 {
248 match: MatchPath{"*substring*"},
249 input: "/foo/substring/bar.txt",
250 expect: true,
251 },
252 {
253 match: MatchPath{"/foo"},
254 input: "/foo/bar",
255 expect: false,
256 },
257 {
258 match: MatchPath{"/foo"},
259 input: "/foo/bar",
260 expect: false,
261 },
262 {
263 match: MatchPath{"/foo"},
264 input: "/FOO",
265 expect: true,
266 },
267 {
268 match: MatchPath{"/foo*"},
269 input: "/FOOOO",
270 expect: true,
271 },
272 {
273 match: MatchPath{"/foo/bar.txt"},
274 input: "/foo/BAR.txt",
275 expect: true,
276 },
277 {
278 match: MatchPath{"/foo*"},
279 input: "//foo/bar",
280 expect: true,
281 },
282 {
283 match: MatchPath{"/foo"},
284 input: "//foo",
285 expect: true,
286 },
287 {
288 match: MatchPath{"//foo"},
289 input: "/foo",
290 expect: false,
291 },
292 {
293 match: MatchPath{"//foo"},
294 input: "//foo",
295 expect: true,
296 },
297 {
298 match: MatchPath{"/foo//*"},
299 input: "/foo//bar",
300 expect: true,
301 },
302 {
303 match: MatchPath{"/foo//*"},
304 input: "/foo/%2Fbar",
305 expect: true,
306 },
307 {
308 match: MatchPath{"/foo/%2F*"},
309 input: "/foo/%2Fbar",
310 expect: true,
311 },
312 {
313 match: MatchPath{"/foo/%2F*"},
314 input: "/foo//bar",
315 expect: false,
316 },
317 {
318 match: MatchPath{"/foo//bar"},
319 input: "/foo//bar",
320 expect: true,
321 },
322 {
323 match: MatchPath{"/foo/*//bar"},
324 input: "/foo///bar",
325 expect: true,
326 },
327 {
328 match: MatchPath{"/foo/%*//bar"},
329 input: "/foo///bar",
330 expect: true,
331 },
332 {
333 match: MatchPath{"/foo/%*//bar"},
334 input: "/foo//%2Fbar",
335 expect: true,
336 },
337 {
338 match: MatchPath{"/foo*"},
339 input: "/%2F/foo",
340 expect: true,
341 },
342 {
343 match: MatchPath{"*"},
344 input: "/",
345 expect: true,
346 },
347 {
348 match: MatchPath{"*"},
349 input: "/foo/bar",
350 expect: true,
351 },
352 {
353 match: MatchPath{"**"},
354 input: "/",
355 expect: true,
356 },
357 {
358 match: MatchPath{"**"},
359 input: "/foo/bar",
360 expect: true,
361 },
362 // notice these next three test cases are the same normalized path but are written differently
363 {
364 match: MatchPath{"/%25@.txt"},
365 input: "/%25@.txt",
366 expect: true,
367 },
368 {
369 match: MatchPath{"/%25@.txt"},
370 input: "/%25%40.txt",
371 expect: true,
372 },
373 {
374 match: MatchPath{"/%25%40.txt"},
375 input: "/%25%40.txt",
376 expect: true,
377 },
378 {
379 match: MatchPath{"/bands/*/*"},
380 input: "/bands/AC%2FDC/T.N.T",
381 expect: false, // because * operates in normalized space
382 },
383 {
384 match: MatchPath{"/bands/%*/%*"},
385 input: "/bands/AC%2FDC/T.N.T",
386 expect: true,
387 },
388 {
389 match: MatchPath{"/bands/%*/%*"},
390 input: "/bands/AC/DC/T.N.T",
391 expect: false,
392 },
393 {
394 match: MatchPath{"/bands/%*"},
395 input: "/bands/AC/DC",
396 expect: false, // not a suffix match
397 },
398 {
399 match: MatchPath{"/bands/%*"},
400 input: "/bands/AC%2FDC",
401 expect: true,
402 },
403 {
404 // a lone trailing wildcard is a fast prefix match in normalized
405 // space, so it does match an encoded separator
406 match: MatchPath{"/bands/*"},
407 input: "/bands/AC%2FDC",
408 expect: true,
409 },
410 {
411 match: MatchPath{"/bands/%*/"},
412 input: "/bands/AC%2FDC/",
413 expect: true,
414 },
415 {
416 match: MatchPath{"/bands/*/"},
417 input: "/bands/AC%2FDC/",
418 expect: false, // because * operates in normalized space
419 },
420 {
421 match: MatchPath{"/foo%2fbar/baz"},
422 input: "/foo%2Fbar/baz",
423 expect: true,
424 },
425 {
426 match: MatchPath{"/foo%2fbar/baz"},
427 input: "/foo/bar/baz",
428 expect: false,
429 },
430 {
431 match: MatchPath{"/foo/bar/baz"},
432 input: "/foo%2fbar/baz",
433 expect: true,
434 },
435 {
436 match: MatchPath{"/admin%2fpanel"},
437 input: "/ADMIN%2fpanel",
438 expect: true,
439 },
440 {
441 match: MatchPath{"/admin%2fpa*el"},
442 input: "/ADMIN%2fPaAzZLm123NEL",
443 expect: true,
444 },
445 {
446 match: MatchPath{"/foo%2fbar"},
447 input: "/foo%2fbarbaz",
448 expect: false,
449 },
450 {
451 match: MatchPath{"/foo%2f"},
452 input: "/foo%2fx",
453 expect: false,
454 },
455 {
456 match: MatchPath{"/admin%2fpanel"},
457 input: "/admin%2fpanelX",
458 expect: false,
459 },
460 {
461 match: MatchPath{"/{env.CADDY_TEST_PATH_PREFIX}/*"},
462 input: "/admin/panel",
463 expect: true,
464 },
465 {
466 match: MatchPath{"/{env.CADDY_TEST_PATH_PREFIX}/*"},
467 input: "/other/panel",
468 expect: false,
469 },
470 {
471 match: MatchPath{"/{http.request.header.X-Prefix}/*"},
472 input: "/admin/panel",
473 headers: http.Header{"X-Prefix": []string{"Admin"}},
474 expect: true,
475 },
476 {
477 match: MatchPath{"/{http.request.header.X-Prefix}/*"},
478 input: "/ADMIN/panel",
479 headers: http.Header{"X-Prefix": []string{"Admin"}},
480 expect: true,
481 },
482 {
483 match: MatchPath{"/{http.request.header.X-Prefix}%2f*"},
484 input: "/admin%2fpanel",
485 headers: http.Header{"X-Prefix": []string{"Admin"}},
486 expect: true,
487 },
488 } {
489 err := tc.match.Provision(caddy.Context{})
490 if err == nil && tc.provisionErr {
491 t.Errorf("Test %d %v: Expected error provisioning, but there was no error", i, tc.match)
492 }
493 if err != nil && !tc.provisionErr {
494 t.Errorf("Test %d %v: Expected no error provisioning, but there was an error: %v", i, tc.match, err)
495 }
496 if tc.provisionErr {
497 continue // if it's not supposed to provision properly, pointless to test it
498 }
499
500 u, err := url.ParseRequestURI(tc.input)
501 if err != nil {
502 t.Fatalf("Test %d (%v): Invalid request URI (should be rejected by Go's HTTP server): %v", i, tc.input, err)
503 }
504 req := &http.Request{URL: u, Header: tc.headers}
505 repl := caddy.NewReplacer()
506 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
507 req = req.WithContext(ctx)
508 addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
509
510 actual, err := tc.match.MatchWithError(req)
511 if err != nil {
512 t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
513 }
514 if actual != tc.expect {
515 t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input)
516 continue
517 }
518 }
519 }
520
521 func TestPathMatcherWindows(t *testing.T) {
522 // only Windows has this bug where it will ignore
523 // trailing dots and spaces in a filename
524 if runtime.GOOS != "windows" {
525 return
526 }
527
528 repl := caddy.NewReplacer()
529
530 for _, tc := range []struct {
531 name string
532 path string
533 requestTarget string
534 match MatchPath
535 }{
536 {
537 name: "trailing dots and spaces",
538 path: "/index.php . . ..",
539 match: MatchPath{"*.php"},
540 },
541 {
542 name: "encoded backslash path separator",
543 requestTarget: `/private%5csecret.txt`,
544 match: MatchPath{"/private/*"},
545 },
546 {
547 name: "encoded backslash path separator with escaped wildcard",
548 requestTarget: `/private%5csecret.txt`,
549 match: MatchPath{"/private/%*"},
550 },
551 {
552 name: "uppercase encoded backslash path separator with escaped wildcard",
553 requestTarget: `/private%5Csecret.txt`,
554 match: MatchPath{"/private/%*"},
555 },
556 {
557 name: "encoded backslash in escaped pattern",
558 requestTarget: `/private%5csecret.txt`,
559 match: MatchPath{"/private%5c%*"},
560 },
561 {
562 name: "trailing dot on a middle path component",
563 path: "/private./secret.txt",
564 match: MatchPath{"/private/*"},
565 },
566 {
567 name: "trailing space on a middle path component",
568 path: "/private /secret.txt",
569 match: MatchPath{"/private/*"},
570 },
571 {
572 // escaped-space matcher (pattern contains '%'): the trailing
573 // dot/space normalization must also apply on the escaped-path
574 // branch, otherwise /private.%5csecret.txt bypasses /private%2f*.
575 name: "trailing dot before encoded backslash, escaped-space matcher",
576 requestTarget: `/private.%5csecret.txt`,
577 match: MatchPath{"/private%2f*"},
578 },
579 {
580 name: "encoded trailing dot before encoded backslash, escaped-space matcher",
581 requestTarget: `/private%2e%5csecret.txt`,
582 match: MatchPath{"/private%2f*"},
583 },
584 {
585 name: "encoded trailing space before encoded backslash, escaped-space matcher",
586 requestTarget: `/private%20%5csecret.txt`,
587 match: MatchPath{"/private%2f*"},
588 },
589 } {
590 t.Run(tc.name, func(t *testing.T) {
591 u := &url.URL{Path: tc.path}
592 if tc.requestTarget != "" {
593 var err error
594 u, err = url.ParseRequestURI(tc.requestTarget)
595 if err != nil {
596 t.Fatalf("Parsing request target: %v", err)
597 }
598 }
599 req := &http.Request{URL: u}
600 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
601 req = req.WithContext(ctx)
602
603 matched, err := tc.match.MatchWithError(req)
604 if err != nil {
605 t.Errorf("Expected no error, but got: %v", err)
606 }
607 if !matched {
608 t.Errorf("Expected %q to match %v", req.URL.Path, tc.match)
609 }
610 })
611 }
612 }
613
614 func TestPathREMatcherWindows(t *testing.T) {
615 // Windows treats backslashes as path separators and ignores trailing
616 // dots and spaces per path component, so the path_regexp matcher must
617 // normalize them the same way the path matcher does (see #5613);
618 // otherwise a guard such as `path_regexp ^/private/` is bypassed by e.g.
619 // GET /private\secret.txt (or %5c) or GET /private./secret.txt.
620 if runtime.GOOS != "windows" {
621 return
622 }
623
624 repl := caddy.NewReplacer()
625
626 for _, tc := range []struct {
627 name string
628 path string
629 requestTarget string
630 match MatchPathRE
631 }{
632 {
633 name: "literal backslash path separator",
634 path: `/private\secret.txt`,
635 match: MatchPathRE{MatchRegexp{Pattern: "^/private/"}},
636 },
637 {
638 name: "encoded backslash path separator",
639 requestTarget: `/private%5csecret.txt`,
640 match: MatchPathRE{MatchRegexp{Pattern: "^/private/"}},
641 },
642 {
643 name: "uppercase encoded backslash path separator",
644 requestTarget: `/private%5Csecret.txt`,
645 match: MatchPathRE{MatchRegexp{Pattern: "^/private/"}},
646 },
647 {
648 name: "trailing dot on a middle path component",
649 path: `/private./secret.txt`,
650 match: MatchPathRE{MatchRegexp{Pattern: "^/private/"}},
651 },
652 {
653 name: "trailing space on a middle path component",
654 path: `/private /secret.txt`,
655 match: MatchPathRE{MatchRegexp{Pattern: "^/private/"}},
656 },
657 } {
658 t.Run(tc.name, func(t *testing.T) {
659 if err := tc.match.Provision(caddy.Context{}); err != nil {
660 t.Fatalf("Provisioning: %v", err)
661 }
662
663 u := &url.URL{Path: tc.path}
664 if tc.requestTarget != "" {
665 var err error
666 u, err = url.ParseRequestURI(tc.requestTarget)
667 if err != nil {
668 t.Fatalf("Parsing request target: %v", err)
669 }
670 }
671 req := &http.Request{URL: u}
672 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
673 req = req.WithContext(ctx)
674
675 matched, err := tc.match.MatchWithError(req)
676 if err != nil {
677 t.Errorf("Expected no error, but got: %v", err)
678 }
679 if !matched {
680 t.Errorf("Expected %q to match %v", req.URL.Path, tc.match.Pattern)
681 }
682 })
683 }
684 }
685
686 func TestPathREMatcher(t *testing.T) {
687 for i, tc := range []struct {
688 match MatchPathRE
689 input string
690 expect bool
691 expectRepl map[string]string
692 }{
693 {
694 match: MatchPathRE{},
695 input: "/",
696 expect: true,
697 },
698 {
699 match: MatchPathRE{MatchRegexp{Pattern: "/"}},
700 input: "/",
701 expect: true,
702 },
703 {
704 match: MatchPathRE{MatchRegexp{Pattern: "^/foo"}},
705 input: "/foo",
706 expect: true,
707 },
708 {
709 match: MatchPathRE{MatchRegexp{Pattern: "^/foo"}},
710 input: "/foo/",
711 expect: true,
712 },
713 {
714 match: MatchPathRE{MatchRegexp{Pattern: "^/foo"}},
715 input: "//foo",
716 expect: true,
717 },
718 {
719 match: MatchPathRE{MatchRegexp{Pattern: "^/foo"}},
720 input: "//foo/",
721 expect: true,
722 },
723 {
724 match: MatchPathRE{MatchRegexp{Pattern: "^/foo"}},
725 input: "/%2F/foo/",
726 expect: true,
727 },
728 {
729 match: MatchPathRE{MatchRegexp{Pattern: "/bar"}},
730 input: "/foo/",
731 expect: false,
732 },
733 {
734 match: MatchPathRE{MatchRegexp{Pattern: "^/bar"}},
735 input: "/foo/bar",
736 expect: false,
737 },
738 {
739 match: MatchPathRE{MatchRegexp{Pattern: "^/foo/(.*)/baz$", Name: "name"}},
740 input: "/foo/bar/baz",
741 expect: true,
742 expectRepl: map[string]string{"name.1": "bar"},
743 },
744 {
745 match: MatchPathRE{MatchRegexp{Pattern: "^/foo/(?P<myparam>.*)/baz$", Name: "name"}},
746 input: "/foo/bar/baz",
747 expect: true,
748 expectRepl: map[string]string{"name.myparam": "bar"},
749 },
750 {
751 match: MatchPathRE{MatchRegexp{Pattern: "^/%@.txt"}},
752 input: "/%25@.txt",
753 expect: true,
754 },
755 {
756 match: MatchPathRE{MatchRegexp{Pattern: "^/%25@.txt"}},
757 input: "/%25@.txt",
758 expect: false,
759 },
760 } {
761 // compile the regexp and validate its name
762 err := tc.match.Provision(caddy.Context{})
763 if err != nil {
764 t.Errorf("Test %d %v: Provisioning: %v", i, tc.match, err)
765 continue
766 }
767 err = tc.match.Validate()
768 if err != nil {
769 t.Errorf("Test %d %v: Validating: %v", i, tc.match, err)
770 continue
771 }
772
773 // set up the fake request and its Replacer
774 u, err := url.ParseRequestURI(tc.input)
775 if err != nil {
776 t.Fatalf("Test %d: Bad input URI: %v", i, err)
777 }
778 req := &http.Request{URL: u}
779 repl := caddy.NewReplacer()
780 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
781 req = req.WithContext(ctx)
782 addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
783
784 actual, err := tc.match.MatchWithError(req)
785 if err != nil {
786 t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
787 }
788 if actual != tc.expect {
789 t.Errorf("Test %d [%v]: Expected %t, got %t for input '%s'",
790 i, tc.match.Pattern, tc.expect, actual, tc.input)
791 continue
792 }
793
794 for key, expectVal := range tc.expectRepl {
795 placeholder := fmt.Sprintf("{http.regexp.%s}", key)
796 actualVal := repl.ReplaceAll(placeholder, "<empty>")
797 if actualVal != expectVal {
798 t.Errorf("Test %d [%v]: Expected placeholder {http.regexp.%s} to be '%s' but got '%s'",
799 i, tc.match.Pattern, key, expectVal, actualVal)
800 continue
801 }
802 }
803 }
804 }
805
806 func TestHeaderMatcher(t *testing.T) {
807 repl := caddy.NewReplacer()
808 repl.Set("a", "foobar")
809
810 for i, tc := range []struct {
811 match MatchHeader
812 input http.Header // make sure these are canonical cased (std lib will do that in a real request)
813 host string
814 expect bool
815 }{
816 {
817 match: MatchHeader{"Field": []string{"foo"}},
818 input: http.Header{"Field": []string{"foo"}},
819 expect: true,
820 },
821 {
822 match: MatchHeader{"Field": []string{"foo", "bar"}},
823 input: http.Header{"Field": []string{"bar"}},
824 expect: true,
825 },
826 {
827 match: MatchHeader{"Field": []string{"foo", "bar"}},
828 input: http.Header{"Alakazam": []string{"kapow"}},
829 expect: false,
830 },
831 {
832 match: MatchHeader{"Field": []string{"foo", "bar"}},
833 input: http.Header{"Field": []string{"kapow"}},
834 expect: false,
835 },
836 {
837 match: MatchHeader{"Field": []string{"foo", "bar"}},
838 input: http.Header{"Field": []string{"kapow", "foo"}},
839 expect: true,
840 },
841 {
842 match: MatchHeader{"Field1": []string{"foo"}, "Field2": []string{"bar"}},
843 input: http.Header{"Field1": []string{"foo"}, "Field2": []string{"bar"}},
844 expect: true,
845 },
846 {
847 match: MatchHeader{"field1": []string{"foo"}, "field2": []string{"bar"}},
848 input: http.Header{"Field1": []string{"foo"}, "Field2": []string{"bar"}},
849 expect: true,
850 },
851 {
852 match: MatchHeader{"field1": []string{"foo"}, "field2": []string{"bar"}},
853 input: http.Header{"Field1": []string{"foo"}, "Field2": []string{"kapow"}},
854 expect: false,
855 },
856 {
857 match: MatchHeader{"field1": []string{"*"}},
858 input: http.Header{"Field1": []string{"foo"}},
859 expect: true,
860 },
861 {
862 match: MatchHeader{"field1": []string{"*"}},
863 input: http.Header{"Field2": []string{"foo"}},
864 expect: false,
865 },
866 {
867 match: MatchHeader{"Field1": []string{"foo*"}},
868 input: http.Header{"Field1": []string{"foo"}},
869 expect: true,
870 },
871 {
872 match: MatchHeader{"Field1": []string{"foo*"}},
873 input: http.Header{"Field1": []string{"asdf", "foobar"}},
874 expect: true,
875 },
876 {
877 match: MatchHeader{"Field1": []string{"*bar"}},
878 input: http.Header{"Field1": []string{"asdf", "foobar"}},
879 expect: true,
880 },
881 {
882 match: MatchHeader{"host": []string{"localhost"}},
883 input: http.Header{},
884 host: "localhost",
885 expect: true,
886 },
887 {
888 match: MatchHeader{"host": []string{"localhost"}},
889 input: http.Header{},
890 host: "caddyserver.com",
891 expect: false,
892 },
893 {
894 match: MatchHeader{"Must-Not-Exist": nil},
895 input: http.Header{},
896 expect: true,
897 },
898 {
899 match: MatchHeader{"Must-Not-Exist": nil},
900 input: http.Header{"Must-Not-Exist": []string{"do not match"}},
901 expect: false,
902 },
903 {
904 match: MatchHeader{"Foo": []string{"{a}"}},
905 input: http.Header{"Foo": []string{"foobar"}},
906 expect: true,
907 },
908 {
909 match: MatchHeader{"Foo": []string{"{a}"}},
910 input: http.Header{"Foo": []string{"asdf"}},
911 expect: false,
912 },
913 {
914 match: MatchHeader{"Foo": []string{"{a}*"}},
915 input: http.Header{"Foo": []string{"foobar-baz"}},
916 expect: true,
917 },
918 } {
919 req := &http.Request{Header: tc.input, Host: tc.host}
920 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
921 req = req.WithContext(ctx)
922
923 actual, err := tc.match.MatchWithError(req)
924 if err != nil {
925 t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
926 }
927 if actual != tc.expect {
928 t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input)
929 continue
930 }
931 }
932 }
933
934 func TestQueryMatcher(t *testing.T) {
935 for i, tc := range []struct {
936 scenario string
937 match MatchQuery
938 input string
939 expect bool
940 }{
941 {
942 scenario: "non match against a specific value",
943 match: MatchQuery{"debug": []string{"1"}},
944 input: "/",
945 expect: false,
946 },
947 {
948 scenario: "match against a specific value",
949 match: MatchQuery{"debug": []string{"1"}},
950 input: "/?debug=1",
951 expect: true,
952 },
953 {
954 scenario: "match against a wildcard",
955 match: MatchQuery{"debug": []string{"*"}},
956 input: "/?debug=something",
957 expect: true,
958 },
959 {
960 scenario: "non match against a wildcarded",
961 match: MatchQuery{"debug": []string{"*"}},
962 input: "/?other=something",
963 expect: false,
964 },
965 {
966 scenario: "match against an empty value",
967 match: MatchQuery{"debug": []string{""}},
968 input: "/?debug",
969 expect: true,
970 },
971 {
972 scenario: "non match against an empty value",
973 match: MatchQuery{"debug": []string{""}},
974 input: "/?someparam",
975 expect: false,
976 },
977 {
978 scenario: "empty matcher value should match empty query",
979 match: MatchQuery{},
980 input: "/?",
981 expect: true,
982 },
983 {
984 scenario: "nil matcher value should NOT match a non-empty query",
985 match: MatchQuery{},
986 input: "/?foo=bar",
987 expect: false,
988 },
989 {
990 scenario: "non-nil matcher should NOT match an empty query",
991 match: MatchQuery{"": nil},
992 input: "/?",
993 expect: false,
994 },
995 {
996 scenario: "match against a placeholder value",
997 match: MatchQuery{"debug": []string{"{http.vars.debug}"}},
998 input: "/?debug=1",
999 expect: true,
1000 },
1001 {
1002 scenario: "match against a placeholder key",
1003 match: MatchQuery{"{http.vars.key}": []string{"1"}},
1004 input: "/?somekey=1",
1005 expect: true,
1006 },
1007 {
1008 scenario: "do not match when not all query params are present",
1009 match: MatchQuery{"debug": []string{"1"}, "foo": []string{"bar"}},
1010 input: "/?debug=1",
1011 expect: false,
1012 },
1013 {
1014 scenario: "match when all query params are present",
1015 match: MatchQuery{"debug": []string{"1"}, "foo": []string{"bar"}},
1016 input: "/?debug=1&foo=bar",
1017 expect: true,
1018 },
1019 {
1020 scenario: "do not match when the value of a query param does not match",
1021 match: MatchQuery{"debug": []string{"1"}, "foo": []string{"bar"}},
1022 input: "/?debug=2&foo=bar",
1023 expect: false,
1024 },
1025 {
1026 scenario: "do not match when all the values the query params do not match",
1027 match: MatchQuery{"debug": []string{"1"}, "foo": []string{"bar"}},
1028 input: "/?debug=2&foo=baz",
1029 expect: false,
1030 },
1031 {
1032 scenario: "match against two values for the same key",
1033 match: MatchQuery{"debug": []string{"1"}},
1034 input: "/?debug=1&debug=2",
1035 expect: true,
1036 },
1037 {
1038 scenario: "match against two values for the same key",
1039 match: MatchQuery{"debug": []string{"2", "1"}},
1040 input: "/?debug=2&debug=1",
1041 expect: true,
1042 },
1043 } {
1044
1045 u, _ := url.Parse(tc.input)
1046
1047 req := &http.Request{URL: u}
1048 repl := caddy.NewReplacer()
1049 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
1050 repl.Set("http.vars.debug", "1")
1051 repl.Set("http.vars.key", "somekey")
1052 req = req.WithContext(ctx)
1053 actual, err := tc.match.MatchWithError(req)
1054 if err != nil {
1055 t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
1056 }
1057 if actual != tc.expect {
1058 t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input)
1059 continue
1060 }
1061 }
1062 }
1063
1064 func TestHeaderREMatcher(t *testing.T) {
1065 for i, tc := range []struct {
1066 match MatchHeaderRE
1067 input http.Header // make sure these are canonical cased (std lib will do that in a real request)
1068 host string
1069 expect bool
1070 expectRepl map[string]string
1071 }{
1072 {
1073 match: MatchHeaderRE{"Field": &MatchRegexp{Pattern: "foo"}},
1074 input: http.Header{"Field": []string{"foo"}},
1075 expect: true,
1076 },
1077 {
1078 match: MatchHeaderRE{"Field": &MatchRegexp{Pattern: "$foo^"}},
1079 input: http.Header{"Field": []string{"foobar"}},
1080 expect: false,
1081 },
1082 {
1083 match: MatchHeaderRE{"Field": &MatchRegexp{Pattern: "^foo(.*)$", Name: "name"}},
1084 input: http.Header{"Field": []string{"foobar"}},
1085 expect: true,
1086 expectRepl: map[string]string{"name.1": "bar"},
1087 },
1088 {
1089 match: MatchHeaderRE{"Field": &MatchRegexp{Pattern: "^foo.*$", Name: "name"}},
1090 input: http.Header{"Field": []string{"barfoo", "foobar"}},
1091 expect: true,
1092 },
1093 {
1094 match: MatchHeaderRE{"host": &MatchRegexp{Pattern: "^localhost$", Name: "name"}},
1095 input: http.Header{},
1096 host: "localhost",
1097 expect: true,
1098 },
1099 {
1100 match: MatchHeaderRE{"host": &MatchRegexp{Pattern: "^local$", Name: "name"}},
1101 input: http.Header{},
1102 host: "localhost",
1103 expect: false,
1104 },
1105 } {
1106 // compile the regexp and validate its name
1107 err := tc.match.Provision(caddy.Context{})
1108 if err != nil {
1109 t.Errorf("Test %d %v: Provisioning: %v", i, tc.match, err)
1110 continue
1111 }
1112 err = tc.match.Validate()
1113 if err != nil {
1114 t.Errorf("Test %d %v: Validating: %v", i, tc.match, err)
1115 continue
1116 }
1117
1118 // set up the fake request and its Replacer
1119 req := &http.Request{Header: tc.input, URL: new(url.URL), Host: tc.host}
1120 repl := caddy.NewReplacer()
1121 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
1122 req = req.WithContext(ctx)
1123 addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
1124
1125 actual, err := tc.match.MatchWithError(req)
1126 if err != nil {
1127 t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
1128 }
1129 if actual != tc.expect {
1130 t.Errorf("Test %d [%v]: Expected %t, got %t for input '%s'",
1131 i, tc.match, tc.expect, actual, tc.input)
1132 continue
1133 }
1134
1135 for key, expectVal := range tc.expectRepl {
1136 placeholder := fmt.Sprintf("{http.regexp.%s}", key)
1137 actualVal := repl.ReplaceAll(placeholder, "<empty>")
1138 if actualVal != expectVal {
1139 t.Errorf("Test %d [%v]: Expected placeholder {http.regexp.%s} to be '%s' but got '%s'",
1140 i, tc.match, key, expectVal, actualVal)
1141 continue
1142 }
1143 }
1144 }
1145 }
1146
1147 func BenchmarkHeaderREMatcher(b *testing.B) {
1148 i := 0
1149 match := MatchHeaderRE{"Field": &MatchRegexp{Pattern: "^foo(.*)$", Name: "name"}}
1150 input := http.Header{"Field": []string{"foobar"}}
1151 var host string
1152 err := match.Provision(caddy.Context{})
1153 if err != nil {
1154 b.Errorf("Test %d %v: Provisioning: %v", i, match, err)
1155 }
1156 err = match.Validate()
1157 if err != nil {
1158 b.Errorf("Test %d %v: Validating: %v", i, match, err)
1159 }
1160
1161 // set up the fake request and its Replacer
1162 req := &http.Request{Header: input, URL: new(url.URL), Host: host}
1163 repl := caddy.NewReplacer()
1164 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
1165 req = req.WithContext(ctx)
1166 addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
1167 for b.Loop() {
1168 match.MatchWithError(req)
1169 }
1170 }
1171
1172 func TestVarREMatcher(t *testing.T) {
1173 for i, tc := range []struct {
1174 desc string
1175 match MatchVarsRE
1176 input VarsMiddleware
1177 headers http.Header
1178 expect bool
1179 expectRepl map[string]string
1180 }{
1181 {
1182 desc: "match static value within var set by the VarsMiddleware succeeds",
1183 match: MatchVarsRE{"Var1": &MatchRegexp{Pattern: "foo"}},
1184 input: VarsMiddleware{"Var1": "here is foo val"},
1185 expect: true,
1186 },
1187 {
1188 desc: "value set by VarsMiddleware not satisfying regexp matcher fails to match",
1189 match: MatchVarsRE{"Var1": &MatchRegexp{Pattern: "$foo^"}},
1190 input: VarsMiddleware{"Var1": "foobar"},
1191 expect: false,
1192 },
1193 {
1194 desc: "successfully matched value is captured and its placeholder is added to replacer",
1195 match: MatchVarsRE{"Var1": &MatchRegexp{Pattern: "^foo(.*)$", Name: "name"}},
1196 input: VarsMiddleware{"Var1": "foobar"},
1197 expect: true,
1198 expectRepl: map[string]string{"name.1": "bar"},
1199 },
1200 {
1201 desc: "matching against a value of standard variables succeeds",
1202 match: MatchVarsRE{"{http.request.method}": &MatchRegexp{Pattern: "^G.[tT]$"}},
1203 input: VarsMiddleware{},
1204 expect: true,
1205 },
1206 {
1207 desc: "matching against value of var set by the VarsMiddleware and referenced by its placeholder succeeds",
1208 match: MatchVarsRE{"{http.vars.Var1}": &MatchRegexp{Pattern: "[vV]ar[0-9]"}},
1209 input: VarsMiddleware{"Var1": "var1Value"},
1210 expect: true,
1211 },
1212 {
1213 desc: "placeholder key value containing braces is not double-expanded",
1214 match: MatchVarsRE{"{http.request.header.X-Input}": &MatchRegexp{Pattern: ".+", Name: "val"}},
1215 input: VarsMiddleware{},
1216 headers: http.Header{"X-Input": []string{"{env.HOME}"}},
1217 expect: true,
1218 expectRepl: map[string]string{"val.0": "{env.HOME}"},
1219 },
1220 } {
1221 t.Run(tc.desc, func(t *testing.T) {
1222 t.Parallel()
1223 // compile the regexp and validate its name
1224 err := tc.match.Provision(caddy.Context{})
1225 if err != nil {
1226 t.Errorf("Test %d %v: Provisioning: %v", i, tc.match, err)
1227 return
1228 }
1229 err = tc.match.Validate()
1230 if err != nil {
1231 t.Errorf("Test %d %v: Validating: %v", i, tc.match, err)
1232 return
1233 }
1234
1235 // set up the fake request and its Replacer
1236 req := &http.Request{URL: new(url.URL), Method: http.MethodGet, Header: tc.headers}
1237 repl := caddy.NewReplacer()
1238 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
1239 ctx = context.WithValue(ctx, VarsCtxKey, make(map[string]any))
1240 req = req.WithContext(ctx)
1241
1242 addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
1243
1244 tc.input.ServeHTTP(httptest.NewRecorder(), req, emptyHandler)
1245
1246 actual, err := tc.match.MatchWithError(req)
1247 if err != nil {
1248 t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
1249 }
1250 if actual != tc.expect {
1251 t.Errorf("Test %d [%v]: Expected %t, got %t for input '%s'",
1252 i, tc.match, tc.expect, actual, tc.input)
1253 return
1254 }
1255
1256 for key, expectVal := range tc.expectRepl {
1257 placeholder := fmt.Sprintf("{http.regexp.%s}", key)
1258 actualVal := repl.ReplaceAll(placeholder, "<empty>")
1259 if actualVal != expectVal {
1260 t.Errorf("Test %d [%v]: Expected placeholder {http.regexp.%s} to be '%s' but got '%s'",
1261 i, tc.match, key, expectVal, actualVal)
1262 return
1263 }
1264 }
1265 })
1266 }
1267 }
1268
1269 func TestNotMatcher(t *testing.T) {
1270 for i, tc := range []struct {
1271 host, path string
1272 match MatchNot
1273 expect bool
1274 }{
1275 {
1276 host: "example.com", path: "/",
1277 match: MatchNot{},
1278 expect: true,
1279 },
1280 {
1281 host: "example.com", path: "/foo",
1282 match: MatchNot{
1283 MatcherSets: []MatcherSet{
1284 {
1285 MatchPath{"/foo"},
1286 },
1287 },
1288 },
1289 expect: false,
1290 },
1291 {
1292 host: "example.com", path: "/bar",
1293 match: MatchNot{
1294 MatcherSets: []MatcherSet{
1295 {
1296 MatchPath{"/foo"},
1297 },
1298 },
1299 },
1300 expect: true,
1301 },
1302 {
1303 host: "example.com", path: "/bar",
1304 match: MatchNot{
1305 MatcherSets: []MatcherSet{
1306 {
1307 MatchPath{"/foo"},
1308 },
1309 {
1310 MatchHost{"example.com"},
1311 },
1312 },
1313 },
1314 expect: false,
1315 },
1316 {
1317 host: "example.com", path: "/bar",
1318 match: MatchNot{
1319 MatcherSets: []MatcherSet{
1320 {
1321 MatchPath{"/bar"},
1322 },
1323 {
1324 MatchHost{"example.com"},
1325 },
1326 },
1327 },
1328 expect: false,
1329 },
1330 {
1331 host: "example.com", path: "/foo",
1332 match: MatchNot{
1333 MatcherSets: []MatcherSet{
1334 {
1335 MatchPath{"/bar"},
1336 },
1337 {
1338 MatchHost{"sub.example.com"},
1339 },
1340 },
1341 },
1342 expect: true,
1343 },
1344 {
1345 host: "example.com", path: "/foo",
1346 match: MatchNot{
1347 MatcherSets: []MatcherSet{
1348 {
1349 MatchPath{"/foo"},
1350 MatchHost{"example.com"},
1351 },
1352 },
1353 },
1354 expect: false,
1355 },
1356 {
1357 host: "example.com", path: "/foo",
1358 match: MatchNot{
1359 MatcherSets: []MatcherSet{
1360 {
1361 MatchPath{"/bar"},
1362 MatchHost{"example.com"},
1363 },
1364 },
1365 },
1366 expect: true,
1367 },
1368 } {
1369 req := &http.Request{Host: tc.host, URL: &url.URL{Path: tc.path}}
1370 repl := caddy.NewReplacer()
1371 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
1372 req = req.WithContext(ctx)
1373
1374 actual, err := tc.match.MatchWithError(req)
1375 if err != nil {
1376 t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
1377 }
1378 if actual != tc.expect {
1379 t.Errorf("Test %d %+v: Expected %t, got %t for: host=%s path=%s'", i, tc.match, tc.expect, actual, tc.host, tc.path)
1380 continue
1381 }
1382 }
1383 }
1384
1385 func TestMethodMatcher(t *testing.T) {
1386 for i, tc := range []struct {
1387 scenario string
1388 match MatchMethod
1389 input string
1390 expect bool
1391 }{
1392 {
1393 scenario: "match uppercase GET",
1394 match: MatchMethod{"GET"},
1395 input: http.MethodGet,
1396 expect: true,
1397 },
1398 {
1399 scenario: "match lowercase get after Provision uppercases it",
1400 match: MatchMethod{"get"},
1401 input: http.MethodGet,
1402 expect: true,
1403 },
1404 {
1405 scenario: "match mixed case Post after Provision uppercases it",
1406 match: MatchMethod{"Post"},
1407 input: http.MethodPost,
1408 expect: true,
1409 },
1410 {
1411 scenario: "no match for wrong method",
1412 match: MatchMethod{"GET"},
1413 input: http.MethodPost,
1414 expect: false,
1415 },
1416 {
1417 scenario: "match one of multiple methods",
1418 match: MatchMethod{"get", "post"},
1419 input: http.MethodPost,
1420 expect: true,
1421 },
1422 } {
1423 req := &http.Request{Method: tc.input}
1424 if err := tc.match.Provision(caddy.Context{}); err != nil {
1425 t.Errorf("Test %d %v: provisioning failed: %v", i, tc.scenario, err)
1426 }
1427 actual, err := tc.match.MatchWithError(req)
1428 if err != nil {
1429 t.Errorf("Test %d %v: matching failed: %v", i, tc.scenario, err)
1430 }
1431 if actual != tc.expect {
1432 t.Errorf("Test %d %v: Expected %t, got %t for method=%s", i, tc.scenario, tc.expect, actual, tc.input)
1433 continue
1434 }
1435 }
1436 }
1437
1438 func BenchmarkLargeHostMatcher(b *testing.B) {
1439 // this benchmark simulates a large host matcher (thousands of entries) where each
1440 // value is an exact hostname (not a placeholder or wildcard) - compare the results
1441 // of this with and without the binary search (comment out the various fast path
1442 // sections in Match) to conduct experiments
1443
1444 const n = 10000
1445 lastHost := fmt.Sprintf("%d.example.com", n-1)
1446 req := &http.Request{Host: lastHost}
1447 repl := caddy.NewReplacer()
1448 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
1449 req = req.WithContext(ctx)
1450
1451 matcher := make(MatchHost, n)
1452 for i := 0; i < n; i++ {
1453 matcher[i] = fmt.Sprintf("%d.example.com", i)
1454 }
1455 err := matcher.Provision(caddy.Context{})
1456 if err != nil {
1457 b.Fatal(err)
1458 }
1459
1460 for b.Loop() {
1461 matcher.MatchWithError(req)
1462 }
1463 }
1464
1465 func BenchmarkHostMatcherWithoutPlaceholder(b *testing.B) {
1466 req := &http.Request{Host: "localhost"}
1467 repl := caddy.NewReplacer()
1468 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
1469 req = req.WithContext(ctx)
1470
1471 match := MatchHost{"localhost"}
1472
1473 for b.Loop() {
1474 match.MatchWithError(req)
1475 }
1476 }
1477
1478 func BenchmarkHostMatcherWithPlaceholder(b *testing.B) {
1479 err := os.Setenv("GO_BENCHMARK_DOMAIN", "localhost")
1480 if err != nil {
1481 b.Errorf("error while setting up environment: %v", err)
1482 }
1483
1484 req := &http.Request{Host: "localhost"}
1485 repl := caddy.NewReplacer()
1486 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
1487 req = req.WithContext(ctx)
1488 match := MatchHost{"{env.GO_BENCHMARK_DOMAIN}"}
1489
1490 for b.Loop() {
1491 match.MatchWithError(req)
1492 }
1493 }
1494
1495 func TestHasWindowsShortName(t *testing.T) {
1496 for _, tc := range []struct {
1497 name string
1498 path string
1499 want bool
1500 }{
1501 {
1502 name: "final component",
1503 path: "/DIRECT~1.TXT",
1504 want: true,
1505 },
1506 {
1507 name: "parent component with long final filename",
1508 path: "/PROTEC~1/this-is-a-long-final-filename.txt",
1509 want: true,
1510 },
1511 {
1512 name: "middle component",
1513 path: "/public/PROTEC~1/file.txt",
1514 want: true,
1515 },
1516 {
1517 name: "backslash separator",
1518 path: `\public\PROTEC~1\file.txt`,
1519 want: true,
1520 },
1521 {
1522 name: "two digit short name suffix",
1523 path: "/PROTE~12/file.txt",
1524 want: true,
1525 },
1526 {
1527 name: "trailing dots and spaces",
1528 path: "/PROTEC~1. /file.txt",
1529 want: true,
1530 },
1531 {
1532 name: "permitted punctuation",
1533 path: "/$%'-_~1.!#&",
1534 want: true,
1535 },
1536 {
1537 name: "ordinary tilde name",
1538 path: "/ordinary~name/file.txt",
1539 want: false,
1540 },
1541 {
1542 name: "space in short name shape",
1543 path: "/my f~1.txt",
1544 want: false,
1545 },
1546 {
1547 name: "space in long tilde name",
1548 path: "/my file~1.txt",
1549 want: false,
1550 },
1551 {
1552 name: "non-ASCII short name shape in parent component",
1553 path: "/public/café~1/this-is-a-long-final-filename.txt",
1554 want: true,
1555 },
1556 {
1557 name: "non-ASCII ordinary tilde name",
1558 path: "/café~name.txt",
1559 want: false,
1560 },
1561 {
1562 name: "forbidden punctuation",
1563 path: "/MY+F~1.TXT",
1564 want: false,
1565 },
1566 {
1567 name: "tilde followed by non-digit",
1568 path: "/SHORT~A/file.txt",
1569 want: false,
1570 },
1571 {
1572 name: "characters after numeric suffix",
1573 path: "/SHORT~1-file.txt",
1574 want: false,
1575 },
1576 {
1577 name: "base too long for 8.3",
1578 path: "/TOOLONG~1/file.txt",
1579 want: false,
1580 },
1581 {
1582 name: "extension too long for 8.3",
1583 path: "/SHORT~1.LONG/file.txt",
1584 want: false,
1585 },
1586 {
1587 name: "multiple extension separators",
1588 path: "/SHORT~1.T.X/file.txt",
1589 want: false,
1590 },
1591 } {
1592 t.Run(tc.name, func(t *testing.T) {
1593 if got := HasWindowsShortName(tc.path); got != tc.want {
1594 t.Fatalf("HasWindowsShortName(%q) = %t, want %t", tc.path, got, tc.want)
1595 }
1596 })
1597 }
1598 }