@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 760 lines 20.3 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 "crypto/tls"
20 "crypto/x509"
21 "encoding/pem"
22 "net/http"
23 "net/http/httptest"
24 "strings"
25 "testing"
26
27 "github.com/caddyserver/caddy/v2"
28 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
29 )
30
31 var (
32 clientCert = []byte(`-----BEGIN CERTIFICATE-----
33 MIIB9jCCAV+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1DYWRk
34 eSBUZXN0IENBMB4XDTE4MDcyNDIxMzUwNVoXDTI4MDcyMTIxMzUwNVowHTEbMBkG
35 A1UEAwwSY2xpZW50LmxvY2FsZG9tYWluMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
36 iQKBgQDFDEpzF0ew68teT3xDzcUxVFaTII+jXH1ftHXxxP4BEYBU4q90qzeKFneF
37 z83I0nC0WAQ45ZwHfhLMYHFzHPdxr6+jkvKPASf0J2v2HDJuTM1bHBbik5Ls5eq+
38 fVZDP8o/VHKSBKxNs8Goc2NTsr5b07QTIpkRStQK+RJALk4x9QIDAQABo0swSTAJ
39 BgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8A
40 AAEwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADgYEANSjz2Sk+
41 eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
42 3Q9fgDkiUod+uIK0IynzIKvw+Cjg+3nx6NQ0IM0zo8c7v398RzB4apbXKZyeeqUH
43 9fNwfEi+OoXR6s+upSKobCmLGLGi9Na5s5g=
44 -----END CERTIFICATE-----`)
45
46 matcherTests = []struct {
47 name string
48 expression *MatchExpression
49 urlTarget string
50 httpMethod string
51 httpHeader *http.Header
52 wantErr bool
53 wantResult bool
54 clientCertificate []byte
55 tlsHandshakeComplete *bool
56 }{
57 {
58 name: "boolean matches succeed for placeholder http.request.tls.client.subject",
59 expression: &MatchExpression{
60 Expr: "{http.request.tls.client.subject} == 'CN=client.localdomain'",
61 },
62 clientCertificate: clientCert,
63 urlTarget: "https://example.com/foo",
64 wantResult: true,
65 },
66 {
67 name: "header matches (MatchHeader)",
68 expression: &MatchExpression{
69 Expr: `header({'Field': 'foo'})`,
70 },
71 urlTarget: "https://example.com/foo",
72 httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
73 wantResult: true,
74 },
75 {
76 name: "header matches an escaped placeholder value (MatchHeader)",
77 expression: &MatchExpression{
78 Expr: `header({'Field': '\\\{foobar}'})`,
79 },
80 urlTarget: "https://example.com/foo",
81 httpHeader: &http.Header{"Field": []string{"{foobar}"}},
82 wantResult: true,
83 },
84 {
85 name: "header matches a placeholder replaced during the header matcher (MatchHeader)",
86 expression: &MatchExpression{
87 Expr: `header({'Field': '\{http.request.uri.path}'})`,
88 },
89 urlTarget: "https://example.com/foo",
90 httpHeader: &http.Header{"Field": []string{"/foo"}},
91 wantResult: true,
92 },
93 {
94 name: "header error, invalid escape sequence (MatchHeader)",
95 expression: &MatchExpression{
96 Expr: `header({'Field': '\\{foobar}'})`,
97 },
98 wantErr: true,
99 },
100 {
101 name: "header error, needs to be JSON syntax with field as key (MatchHeader)",
102 expression: &MatchExpression{
103 Expr: `header('foo')`,
104 },
105 wantErr: true,
106 },
107 {
108 name: "header_regexp matches (MatchHeaderRE)",
109 expression: &MatchExpression{
110 Expr: `header_regexp('Field', 'fo{2}')`,
111 },
112 urlTarget: "https://example.com/foo",
113 httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
114 wantResult: true,
115 },
116 {
117 name: "header_regexp matches with name (MatchHeaderRE)",
118 expression: &MatchExpression{
119 Expr: `header_regexp('foo', 'Field', 'fo{2}')`,
120 },
121 urlTarget: "https://example.com/foo",
122 httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
123 wantResult: true,
124 },
125 {
126 name: "header_regexp does not match (MatchHeaderRE)",
127 expression: &MatchExpression{
128 Expr: `header_regexp('foo', 'Nope', 'fo{2}')`,
129 },
130 urlTarget: "https://example.com/foo",
131 httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
132 wantResult: false,
133 },
134 {
135 name: "header_regexp error (MatchHeaderRE)",
136 expression: &MatchExpression{
137 Expr: `header_regexp('foo')`,
138 },
139 wantErr: true,
140 },
141 {
142 name: "host matches localhost (MatchHost)",
143 expression: &MatchExpression{
144 Expr: `host('localhost')`,
145 },
146 urlTarget: "http://localhost",
147 wantResult: true,
148 },
149 {
150 name: "host matches (MatchHost)",
151 expression: &MatchExpression{
152 Expr: `host('*.example.com')`,
153 },
154 urlTarget: "https://foo.example.com",
155 wantResult: true,
156 },
157 {
158 name: "host does not match (MatchHost)",
159 expression: &MatchExpression{
160 Expr: `host('example.net', '*.example.com')`,
161 },
162 urlTarget: "https://foo.example.org",
163 wantResult: false,
164 },
165 {
166 name: "host error (MatchHost)",
167 expression: &MatchExpression{
168 Expr: `host(80)`,
169 },
170 wantErr: true,
171 },
172 {
173 name: "method does not match (MatchMethod)",
174 expression: &MatchExpression{
175 Expr: `method('PUT')`,
176 },
177 urlTarget: "https://foo.example.com",
178 httpMethod: "GET",
179 wantResult: false,
180 },
181 {
182 name: "method matches (MatchMethod)",
183 expression: &MatchExpression{
184 Expr: `method('DELETE', 'PUT', 'POST')`,
185 },
186 urlTarget: "https://foo.example.com",
187 httpMethod: "PUT",
188 wantResult: true,
189 },
190 {
191 name: "method error not enough arguments (MatchMethod)",
192 expression: &MatchExpression{
193 Expr: `method()`,
194 },
195 wantErr: true,
196 },
197 {
198 name: "path matches substring (MatchPath)",
199 expression: &MatchExpression{
200 Expr: `path('*substring*')`,
201 },
202 urlTarget: "https://example.com/foo/substring/bar.txt",
203 wantResult: true,
204 },
205 {
206 name: "path does not match (MatchPath)",
207 expression: &MatchExpression{
208 Expr: `path('/foo')`,
209 },
210 urlTarget: "https://example.com/foo/bar",
211 wantResult: false,
212 },
213 {
214 name: "path matches end url fragment (MatchPath)",
215 expression: &MatchExpression{
216 Expr: `path('/foo')`,
217 },
218 urlTarget: "https://example.com/FOO",
219 wantResult: true,
220 },
221 {
222 name: "path matches end fragment with substring prefix (MatchPath)",
223 expression: &MatchExpression{
224 Expr: `path('/foo*')`,
225 },
226 urlTarget: "https://example.com/FOOOOO",
227 wantResult: true,
228 },
229 {
230 name: "path matches one of multiple (MatchPath)",
231 expression: &MatchExpression{
232 Expr: `path('/foo', '/foo/*', '/bar', '/bar/*', '/baz', '/baz*')`,
233 },
234 urlTarget: "https://example.com/foo",
235 wantResult: true,
236 },
237 {
238 name: "path_regexp with empty regex matches empty path (MatchPathRE)",
239 expression: &MatchExpression{
240 Expr: `path_regexp('')`,
241 },
242 urlTarget: "https://example.com/",
243 wantResult: true,
244 },
245 {
246 name: "path_regexp with slash regex matches empty path (MatchPathRE)",
247 expression: &MatchExpression{
248 Expr: `path_regexp('/')`,
249 },
250 urlTarget: "https://example.com/",
251 wantResult: true,
252 },
253 {
254 name: "path_regexp matches end url fragment (MatchPathRE)",
255 expression: &MatchExpression{
256 Expr: `path_regexp('^/foo')`,
257 },
258 urlTarget: "https://example.com/foo/",
259 wantResult: true,
260 },
261 {
262 name: "path_regexp does not match fragment at end (MatchPathRE)",
263 expression: &MatchExpression{
264 Expr: `path_regexp('bar_at_start', '^/bar')`,
265 },
266 urlTarget: "https://example.com/foo/bar",
267 wantResult: false,
268 },
269 {
270 name: "url_pattern matches named group (MatchURLPattern)",
271 expression: &MatchExpression{
272 Expr: `url_pattern('/books/:id')`,
273 },
274 urlTarget: "https://example.com/books/123",
275 wantResult: true,
276 },
277 {
278 name: "url_pattern does not match (MatchURLPattern)",
279 expression: &MatchExpression{
280 Expr: `url_pattern('/books/:id')`,
281 },
282 urlTarget: "https://example.com/movies/123",
283 wantResult: false,
284 },
285 {
286 name: "url_pattern with base_url matches host (MatchURLPattern)",
287 expression: &MatchExpression{
288 Expr: `url_pattern('/foo', 'https://example.com')`,
289 },
290 urlTarget: "https://example.com/foo",
291 wantResult: true,
292 },
293 {
294 name: "url_pattern with base_url rejects other host (MatchURLPattern)",
295 expression: &MatchExpression{
296 Expr: `url_pattern('/foo', 'https://example.com')`,
297 },
298 urlTarget: "https://other.com/foo",
299 wantResult: false,
300 },
301 {
302 name: "protocol matches (MatchProtocol)",
303 expression: &MatchExpression{
304 Expr: `protocol('HTTPs')`,
305 },
306 urlTarget: "https://example.com",
307 wantResult: true,
308 },
309 {
310 name: "protocol does not match (MatchProtocol)",
311 expression: &MatchExpression{
312 Expr: `protocol('grpc')`,
313 },
314 urlTarget: "https://example.com",
315 wantResult: false,
316 },
317 {
318 name: "protocol invocation error no args (MatchProtocol)",
319 expression: &MatchExpression{
320 Expr: `protocol()`,
321 },
322 wantErr: true,
323 },
324 {
325 name: "protocol invocation error too many args (MatchProtocol)",
326 expression: &MatchExpression{
327 Expr: `protocol('grpc', 'https')`,
328 },
329 wantErr: true,
330 },
331 {
332 name: "protocol invocation error wrong arg type (MatchProtocol)",
333 expression: &MatchExpression{
334 Expr: `protocol(true)`,
335 },
336 wantErr: true,
337 },
338 {
339 name: "tls('complete') matches when handshake is complete (MatchTLS)",
340 expression: &MatchExpression{
341 Expr: `tls('complete')`,
342 },
343 urlTarget: "https://example.com",
344 tlsHandshakeComplete: boolPtr(true),
345 wantResult: true,
346 },
347 {
348 name: "tls('complete') does not match when handshake is incomplete (MatchTLS)",
349 expression: &MatchExpression{
350 Expr: `tls('complete')`,
351 },
352 urlTarget: "https://example.com",
353 tlsHandshakeComplete: boolPtr(false),
354 wantResult: false,
355 },
356 {
357 name: "tls('early_data') matches an incomplete handshake (MatchTLS)",
358 expression: &MatchExpression{
359 Expr: `tls('early_data')`,
360 },
361 urlTarget: "https://example.com",
362 tlsHandshakeComplete: boolPtr(false),
363 wantResult: true,
364 },
365 {
366 name: "tls('early_data') does not match a complete handshake (MatchTLS)",
367 expression: &MatchExpression{
368 Expr: `tls('early_data')`,
369 },
370 urlTarget: "https://example.com",
371 tlsHandshakeComplete: boolPtr(true),
372 wantResult: false,
373 },
374 {
375 name: "tls() matches any TLS connection regardless of handshake state, complete (MatchTLS)",
376 expression: &MatchExpression{
377 Expr: `tls()`,
378 },
379 urlTarget: "https://example.com",
380 tlsHandshakeComplete: boolPtr(true),
381 wantResult: true,
382 },
383 {
384 name: "tls() matches any TLS connection regardless of handshake state, incomplete (MatchTLS)",
385 expression: &MatchExpression{
386 Expr: `tls()`,
387 },
388 urlTarget: "https://example.com",
389 tlsHandshakeComplete: boolPtr(false),
390 wantResult: true,
391 },
392 {
393 name: "tls('any') is equivalent to tls(), complete (MatchTLS)",
394 expression: &MatchExpression{
395 Expr: `tls('any')`,
396 },
397 urlTarget: "https://example.com",
398 tlsHandshakeComplete: boolPtr(true),
399 wantResult: true,
400 },
401 {
402 name: "tls('any') is equivalent to tls(), incomplete (MatchTLS)",
403 expression: &MatchExpression{
404 Expr: `tls('any')`,
405 },
406 urlTarget: "https://example.com",
407 tlsHandshakeComplete: boolPtr(false),
408 wantResult: true,
409 },
410 {
411 name: "tls invocation error wrong arg type (MatchTLS)",
412 expression: &MatchExpression{
413 Expr: `tls(true)`,
414 },
415 wantErr: true,
416 },
417 {
418 name: "tls invocation error unrecognized string value (MatchTLS)",
419 expression: &MatchExpression{
420 Expr: `tls('bogus')`,
421 },
422 wantErr: true,
423 },
424 {
425 name: "tls('early_data') does not match a plain non-TLS request (MatchTLS)",
426 expression: &MatchExpression{
427 Expr: `tls('early_data')`,
428 },
429 urlTarget: "http://example.com",
430 wantResult: false,
431 },
432 {
433 name: "tls('complete') does not match a plain non-TLS request (MatchTLS)",
434 expression: &MatchExpression{
435 Expr: `tls('complete')`,
436 },
437 urlTarget: "http://example.com",
438 wantResult: false,
439 },
440 {
441 name: "tls() does not match a plain non-TLS request (MatchTLS)",
442 expression: &MatchExpression{
443 Expr: `tls()`,
444 },
445 urlTarget: "http://example.com",
446 wantResult: false,
447 },
448 {
449 name: "tls('any') does not match a plain non-TLS request (MatchTLS)",
450 expression: &MatchExpression{
451 Expr: `tls('any')`,
452 },
453 urlTarget: "http://example.com",
454 wantResult: false,
455 },
456 {
457 name: "query does not match against a specific value (MatchQuery)",
458 expression: &MatchExpression{
459 Expr: `query({"debug": "1"})`,
460 },
461 urlTarget: "https://example.com/foo",
462 wantResult: false,
463 },
464 {
465 name: "query matches against a specific value (MatchQuery)",
466 expression: &MatchExpression{
467 Expr: `query({"debug": "1"})`,
468 },
469 urlTarget: "https://example.com/foo/?debug=1",
470 wantResult: true,
471 },
472 {
473 name: "query matches against multiple values (MatchQuery)",
474 expression: &MatchExpression{
475 Expr: `query({"debug": ["0", "1", {http.request.uri.query.debug}+"1"]})`,
476 },
477 urlTarget: "https://example.com/foo/?debug=1",
478 wantResult: true,
479 },
480 {
481 name: "query matches against a wildcard (MatchQuery)",
482 expression: &MatchExpression{
483 Expr: `query({"debug": ["*"]})`,
484 },
485 urlTarget: "https://example.com/foo/?debug=something",
486 wantResult: true,
487 },
488 {
489 name: "query matches against a placeholder value (MatchQuery)",
490 expression: &MatchExpression{
491 Expr: `query({"debug": {http.request.uri.query.debug}})`,
492 },
493 urlTarget: "https://example.com/foo/?debug=1",
494 wantResult: true,
495 },
496 {
497 name: "query error bad map key type (MatchQuery)",
498 expression: &MatchExpression{
499 Expr: `query({1: "1"})`,
500 },
501 wantErr: true,
502 },
503 {
504 name: "query error typed struct instead of map (MatchQuery)",
505 expression: &MatchExpression{
506 Expr: `query(Message{field: "1"})`,
507 },
508 wantErr: true,
509 },
510 {
511 name: "query error bad map value type (MatchQuery)",
512 expression: &MatchExpression{
513 Expr: `query({"debug": 1})`,
514 },
515 wantErr: true,
516 },
517 {
518 name: "query error no args (MatchQuery)",
519 expression: &MatchExpression{
520 Expr: `query()`,
521 },
522 wantErr: true,
523 },
524 {
525 name: "remote_ip error no args (MatchRemoteIP)",
526 expression: &MatchExpression{
527 Expr: `remote_ip()`,
528 },
529 wantErr: true,
530 },
531 {
532 name: "remote_ip single IP match (MatchRemoteIP)",
533 expression: &MatchExpression{
534 Expr: `remote_ip('192.0.2.1')`,
535 },
536 urlTarget: "https://example.com/foo",
537 wantResult: true,
538 },
539 {
540 name: "vars value (VarsMatcher)",
541 expression: &MatchExpression{
542 Expr: `vars({'foo': 'bar'})`,
543 },
544 urlTarget: "https://example.com/foo",
545 wantResult: true,
546 },
547 {
548 name: "vars matches placeholder, needs escape (VarsMatcher)",
549 expression: &MatchExpression{
550 Expr: `vars({'\{http.request.uri.path}': '/foo'})`,
551 },
552 urlTarget: "https://example.com/foo",
553 wantResult: true,
554 },
555 {
556 name: "vars error wrong syntax (VarsMatcher)",
557 expression: &MatchExpression{
558 Expr: `vars('foo', 'bar')`,
559 },
560 wantErr: true,
561 },
562 {
563 name: "vars error no args (VarsMatcher)",
564 expression: &MatchExpression{
565 Expr: `vars()`,
566 },
567 wantErr: true,
568 },
569 {
570 name: "vars_regexp value (MatchVarsRE)",
571 expression: &MatchExpression{
572 Expr: `vars_regexp('foo', 'ba?r')`,
573 },
574 urlTarget: "https://example.com/foo",
575 wantResult: true,
576 },
577 {
578 name: "vars_regexp value with name (MatchVarsRE)",
579 expression: &MatchExpression{
580 Expr: `vars_regexp('name', 'foo', 'ba?r')`,
581 },
582 urlTarget: "https://example.com/foo",
583 wantResult: true,
584 },
585 {
586 name: "vars_regexp matches placeholder, needs escape (MatchVarsRE)",
587 expression: &MatchExpression{
588 Expr: `vars_regexp('\{http.request.uri.path}', '/fo?o')`,
589 },
590 urlTarget: "https://example.com/foo",
591 wantResult: true,
592 },
593 {
594 name: "vars_regexp error no args (MatchVarsRE)",
595 expression: &MatchExpression{
596 Expr: `vars_regexp()`,
597 },
598 wantErr: true,
599 },
600 }
601 )
602
603 func TestMatchExpressionMatch(t *testing.T) {
604 for _, tst := range matcherTests {
605 tc := tst
606 t.Run(tc.name, func(t *testing.T) {
607 caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
608 defer cancel()
609 err := tc.expression.Provision(caddyCtx)
610 if err != nil {
611 if !tc.wantErr {
612 t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tc.wantErr)
613 }
614 return
615 }
616
617 req := httptest.NewRequest(tc.httpMethod, tc.urlTarget, nil)
618 if tc.httpHeader != nil {
619 req.Header = *tc.httpHeader
620 }
621 repl := caddy.NewReplacer()
622 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
623 ctx = context.WithValue(ctx, VarsCtxKey, map[string]any{
624 "foo": "bar",
625 })
626 req = req.WithContext(ctx)
627 addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
628
629 if tc.clientCertificate != nil {
630 block, _ := pem.Decode(clientCert)
631 if block == nil {
632 t.Fatalf("failed to decode PEM certificate")
633 }
634
635 cert, err := x509.ParseCertificate(block.Bytes)
636 if err != nil {
637 t.Fatalf("failed to decode PEM certificate: %v", err)
638 }
639
640 req.TLS = &tls.ConnectionState{
641 PeerCertificates: []*x509.Certificate{cert},
642 }
643 }
644
645 if tc.tlsHandshakeComplete != nil {
646 if req.TLS == nil {
647 req.TLS = &tls.ConnectionState{}
648 }
649 req.TLS.HandshakeComplete = *tc.tlsHandshakeComplete
650 }
651
652 matches, err := tc.expression.MatchWithError(req)
653 if err != nil {
654 t.Errorf("MatchExpression.Match() error = %v", err)
655 }
656 if matches != tc.wantResult {
657 t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tc.wantResult, tc.expression.Expr)
658 }
659 })
660 }
661 }
662
663 func BenchmarkMatchExpressionMatch(b *testing.B) {
664 for _, tst := range matcherTests {
665 tc := tst
666 if tc.wantErr {
667 continue
668 }
669 b.Run(tst.name, func(b *testing.B) {
670 tc.expression.Provision(caddy.Context{})
671 req := httptest.NewRequest(tc.httpMethod, tc.urlTarget, nil)
672 if tc.httpHeader != nil {
673 req.Header = *tc.httpHeader
674 }
675 repl := caddy.NewReplacer()
676 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
677 ctx = context.WithValue(ctx, VarsCtxKey, map[string]any{
678 "foo": "bar",
679 })
680 req = req.WithContext(ctx)
681 addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
682 if tc.clientCertificate != nil {
683 block, _ := pem.Decode(clientCert)
684 if block == nil {
685 b.Fatalf("failed to decode PEM certificate")
686 }
687
688 cert, err := x509.ParseCertificate(block.Bytes)
689 if err != nil {
690 b.Fatalf("failed to decode PEM certificate: %v", err)
691 }
692
693 req.TLS = &tls.ConnectionState{
694 PeerCertificates: []*x509.Certificate{cert},
695 }
696 }
697 if tc.tlsHandshakeComplete != nil {
698 if req.TLS == nil {
699 req.TLS = &tls.ConnectionState{}
700 }
701 req.TLS.HandshakeComplete = *tc.tlsHandshakeComplete
702 }
703 b.ResetTimer()
704 for b.Loop() {
705 tc.expression.MatchWithError(req)
706 }
707 })
708 }
709 }
710
711 func TestMatchExpressionProvision(t *testing.T) {
712 tests := []struct {
713 name string
714 expression *MatchExpression
715 wantErr bool
716 }{
717 {
718 name: "boolean matches succeed",
719 expression: &MatchExpression{
720 Expr: "{http.request.uri.query} != ''",
721 },
722 wantErr: false,
723 },
724 {
725 name: "reject expressions with non-boolean results",
726 expression: &MatchExpression{
727 Expr: "{http.request.uri.query}",
728 },
729 wantErr: true,
730 },
731 }
732 for _, tt := range tests {
733 t.Run(tt.name, func(t *testing.T) {
734 ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
735 defer cancel()
736 if err := tt.expression.Provision(ctx); (err != nil) != tt.wantErr {
737 t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tt.wantErr)
738 }
739 })
740 }
741 }
742
743 func TestMatchExpressionUnmarshalCaddyfileRepeated(t *testing.T) {
744 for _, input := range []string{`
745 expression {http.request.method} == 'GET'
746 expression {http.request.uri.path}.startsWith('/public/')
747 `, `
748 expression "{http.request.method} == 'GET'"
749 expression {http.request.uri.path}.startsWith('/public/')
750 `} {
751 d := caddyfile.NewTestDispenser(input)
752 m := new(MatchExpression)
753 err := m.UnmarshalCaddyfile(d)
754 if err == nil || !strings.Contains(err.Error(), "repeated expression matchers are not supported") {
755 t.Errorf("UnmarshalCaddyfile() error = %v, want repeated expression error (expression: %s)", err, m.Expr)
756 }
757 }
758 }
759
760 func boolPtr(b bool) *bool { return &b }