Files
caddy/modules/caddyhttp/celmatcher_test.go
Aradhya Jain b1694e7a4c caddyhttp: add CEL expression support to the tls matcher (#8088)
* caddyhttp: add CEL expression support to the tls matcher

MatchTLS was the only core HTTP matcher without a CELLibrary() implementation,
so it could not be used inside an expression matcher. Every sibling matcher
(host, path, path_regexp, method, query, header, header_regexp, protocol,
remote_ip, client_ip, url_pattern, vars, vars_regexp, and the file-server's
file matcher) already has this.

Adds CELLibrary() to MatchTLS, exposing tls_handshake_complete('true') /
tls_handshake_complete('false') in CEL expressions. Mirrors MatchProtocol's
existing string-argument-macro pattern: CELMatcherImpl with cel.StringType,
parsing the argument with strconv.ParseBool and constructing a
MatchTLS{HandshakeComplete: &b} that reuses the matcher's own
MatchWithError. Adds the corresponding CELLibraryProducer interface guard.

Adds 6 cases to celmatcher_test.go's existing table-driven
TestMatchExpressionMatch/BenchmarkMatchExpressionMatch: match on
'true'/'false' against a complete/incomplete handshake, and three
invocation-error cases (no args, wrong arg type, non-boolean string).
Required adding a tlsHandshakeComplete *bool field to the shared test table
and wiring it into req.TLS.HandshakeComplete in both the test and benchmark
request-construction helpers.

Fixes #8087

* address review: use a native bool arg, rename to match the matcher's own name

francislavoie's review on #8088:
- "A boolean as a string argument is very strange." CELMatcherImpl only
  supported string/list(string)/map argument types, so the original
  implementation parsed a string literal with strconv.ParseBool to work
  around that. Instead, extends CELMatcherImpl itself with a cel.BoolType
  case (celMatcherBoolMacroExpander + isCELBoolExpr, mirroring the existing
  string equivalents), so a matcher can accept a real CEL bool literal.
  This is shared infrastructure, so any other matcher needing a bool
  argument in the future gets it for free.
- "I'm not sure why tls_handshake_complete was chosen when the matcher is
  named tls." Renamed the CEL function to tls(...), matching every other
  matcher's naming convention (the function name mirrors the matcher's own
  directive/module name: host(), path(), method(), protocol(), etc.).

Usage is now 'expression tls(true)' / 'expression tls(false)' instead of
'expression tls_handshake_complete(\'true\')'.

Updated the 6 existing test cases accordingly; still covers match/no-match
on both true/false and the two invocation-error cases (no args, wrong arg
type -- now a string instead of a bool, confirming the new type check).

* address review: cover tls() against a plain non-TLS request

steadytao asked for a test proving tls(false) doesn't treat the absence
of TLS as an incomplete handshake. MatchWithError already handles this
correctly (r.TLS == nil returns false regardless of HandshakeComplete),
this just adds the missing coverage for both tls(true) and tls(false)
against a plain http:// request.

Confirmed these tests catch a real regression: removing the r.TLS == nil
guard causes a nil pointer dereference panic, not just a wrong result.

* address review: redesign tls() CEL matcher to preserve all three Caddyfile states

The Caddyfile tls matcher has three states: bare `tls` (any TLS connection,
regardless of handshake completion), `tls early_data` (handshake not yet
complete), and the implicit "handshake complete" case. The previous
tls(true)/tls(false) design could only express the latter two, with no way
to express "any TLS, don't care about handshake state" from CEL.

Redesigns the macro to:

  tls()               -- any TLS connection (mirrors bare `tls`)
  tls('complete')      -- handshake complete
  tls('early_data')    -- handshake not yet complete (mirrors `tls early_data`)

tls() is implemented as sugar for tls('any'): a zero-arg macro that expands
to the same 2-arg call the one-arg string macro produces, just with the
literal injected by the macro itself. Both macros share one function
registration and one matcher factory, so there's a single source of truth
for the string-to-MatchTLS mapping.

This replaces the native-bool design from the prior commit, so also removes
the generic bool-argument support (celMatcherBoolMacroExpander,
isCELBoolExpr, and the CELMatcherImpl case for cel.BoolType) added for it,
since nothing else in the codebase used it and it would otherwise ship as
unreviewed, unused surface area.

Rewrites the existing tls(...) test cases for the new string-based calls,
and adds coverage for the new tls() form: matches regardless of handshake
state (both complete and incomplete), and still correctly fails to match a
plain non-TLS request. Also covers the new invocation-error cases: a
boolean argument is no longer valid, and an unrecognized string (anything
other than 'complete'/'early_data') is now a Provision-time error instead
of silently doing the wrong thing.

Verified: full caddyhttp test suite passes, and confirmed each new/changed
test fails when its corresponding implementation piece is reverted
(the zero-arg macro, and the complete/early_data boolean mapping).

* address review: document and test tls('any') as a supported literal form

steadytao pointed out that the zero-arg tls() macro expands internally to
the same tls_request_string(req, "any") call that the one-arg form uses,
which means tls('any') was already silently reachable as a literal string
even though it was never documented or tested as part of the public API.

Rather than special-casing the macro to bypass the shared string factory
(which would need a second function/overload just for the zero-arg case),
document and test tls('any') as a real, supported third spelling alongside
tls() -- they're already fully equivalent, so there's nothing to hide.
2026-10-24 08:01:24 -04:00

761 lines
20 KiB
Go

// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyhttp
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
var (
clientCert = []byte(`-----BEGIN CERTIFICATE-----
MIIB9jCCAV+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1DYWRk
eSBUZXN0IENBMB4XDTE4MDcyNDIxMzUwNVoXDTI4MDcyMTIxMzUwNVowHTEbMBkG
A1UEAwwSY2xpZW50LmxvY2FsZG9tYWluMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
iQKBgQDFDEpzF0ew68teT3xDzcUxVFaTII+jXH1ftHXxxP4BEYBU4q90qzeKFneF
z83I0nC0WAQ45ZwHfhLMYHFzHPdxr6+jkvKPASf0J2v2HDJuTM1bHBbik5Ls5eq+
fVZDP8o/VHKSBKxNs8Goc2NTsr5b07QTIpkRStQK+RJALk4x9QIDAQABo0swSTAJ
BgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8A
AAEwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADgYEANSjz2Sk+
eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
3Q9fgDkiUod+uIK0IynzIKvw+Cjg+3nx6NQ0IM0zo8c7v398RzB4apbXKZyeeqUH
9fNwfEi+OoXR6s+upSKobCmLGLGi9Na5s5g=
-----END CERTIFICATE-----`)
matcherTests = []struct {
name string
expression *MatchExpression
urlTarget string
httpMethod string
httpHeader *http.Header
wantErr bool
wantResult bool
clientCertificate []byte
tlsHandshakeComplete *bool
}{
{
name: "boolean matches succeed for placeholder http.request.tls.client.subject",
expression: &MatchExpression{
Expr: "{http.request.tls.client.subject} == 'CN=client.localdomain'",
},
clientCertificate: clientCert,
urlTarget: "https://example.com/foo",
wantResult: true,
},
{
name: "header matches (MatchHeader)",
expression: &MatchExpression{
Expr: `header({'Field': 'foo'})`,
},
urlTarget: "https://example.com/foo",
httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
wantResult: true,
},
{
name: "header matches an escaped placeholder value (MatchHeader)",
expression: &MatchExpression{
Expr: `header({'Field': '\\\{foobar}'})`,
},
urlTarget: "https://example.com/foo",
httpHeader: &http.Header{"Field": []string{"{foobar}"}},
wantResult: true,
},
{
name: "header matches a placeholder replaced during the header matcher (MatchHeader)",
expression: &MatchExpression{
Expr: `header({'Field': '\{http.request.uri.path}'})`,
},
urlTarget: "https://example.com/foo",
httpHeader: &http.Header{"Field": []string{"/foo"}},
wantResult: true,
},
{
name: "header error, invalid escape sequence (MatchHeader)",
expression: &MatchExpression{
Expr: `header({'Field': '\\{foobar}'})`,
},
wantErr: true,
},
{
name: "header error, needs to be JSON syntax with field as key (MatchHeader)",
expression: &MatchExpression{
Expr: `header('foo')`,
},
wantErr: true,
},
{
name: "header_regexp matches (MatchHeaderRE)",
expression: &MatchExpression{
Expr: `header_regexp('Field', 'fo{2}')`,
},
urlTarget: "https://example.com/foo",
httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
wantResult: true,
},
{
name: "header_regexp matches with name (MatchHeaderRE)",
expression: &MatchExpression{
Expr: `header_regexp('foo', 'Field', 'fo{2}')`,
},
urlTarget: "https://example.com/foo",
httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
wantResult: true,
},
{
name: "header_regexp does not match (MatchHeaderRE)",
expression: &MatchExpression{
Expr: `header_regexp('foo', 'Nope', 'fo{2}')`,
},
urlTarget: "https://example.com/foo",
httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
wantResult: false,
},
{
name: "header_regexp error (MatchHeaderRE)",
expression: &MatchExpression{
Expr: `header_regexp('foo')`,
},
wantErr: true,
},
{
name: "host matches localhost (MatchHost)",
expression: &MatchExpression{
Expr: `host('localhost')`,
},
urlTarget: "http://localhost",
wantResult: true,
},
{
name: "host matches (MatchHost)",
expression: &MatchExpression{
Expr: `host('*.example.com')`,
},
urlTarget: "https://foo.example.com",
wantResult: true,
},
{
name: "host does not match (MatchHost)",
expression: &MatchExpression{
Expr: `host('example.net', '*.example.com')`,
},
urlTarget: "https://foo.example.org",
wantResult: false,
},
{
name: "host error (MatchHost)",
expression: &MatchExpression{
Expr: `host(80)`,
},
wantErr: true,
},
{
name: "method does not match (MatchMethod)",
expression: &MatchExpression{
Expr: `method('PUT')`,
},
urlTarget: "https://foo.example.com",
httpMethod: "GET",
wantResult: false,
},
{
name: "method matches (MatchMethod)",
expression: &MatchExpression{
Expr: `method('DELETE', 'PUT', 'POST')`,
},
urlTarget: "https://foo.example.com",
httpMethod: "PUT",
wantResult: true,
},
{
name: "method error not enough arguments (MatchMethod)",
expression: &MatchExpression{
Expr: `method()`,
},
wantErr: true,
},
{
name: "path matches substring (MatchPath)",
expression: &MatchExpression{
Expr: `path('*substring*')`,
},
urlTarget: "https://example.com/foo/substring/bar.txt",
wantResult: true,
},
{
name: "path does not match (MatchPath)",
expression: &MatchExpression{
Expr: `path('/foo')`,
},
urlTarget: "https://example.com/foo/bar",
wantResult: false,
},
{
name: "path matches end url fragment (MatchPath)",
expression: &MatchExpression{
Expr: `path('/foo')`,
},
urlTarget: "https://example.com/FOO",
wantResult: true,
},
{
name: "path matches end fragment with substring prefix (MatchPath)",
expression: &MatchExpression{
Expr: `path('/foo*')`,
},
urlTarget: "https://example.com/FOOOOO",
wantResult: true,
},
{
name: "path matches one of multiple (MatchPath)",
expression: &MatchExpression{
Expr: `path('/foo', '/foo/*', '/bar', '/bar/*', '/baz', '/baz*')`,
},
urlTarget: "https://example.com/foo",
wantResult: true,
},
{
name: "path_regexp with empty regex matches empty path (MatchPathRE)",
expression: &MatchExpression{
Expr: `path_regexp('')`,
},
urlTarget: "https://example.com/",
wantResult: true,
},
{
name: "path_regexp with slash regex matches empty path (MatchPathRE)",
expression: &MatchExpression{
Expr: `path_regexp('/')`,
},
urlTarget: "https://example.com/",
wantResult: true,
},
{
name: "path_regexp matches end url fragment (MatchPathRE)",
expression: &MatchExpression{
Expr: `path_regexp('^/foo')`,
},
urlTarget: "https://example.com/foo/",
wantResult: true,
},
{
name: "path_regexp does not match fragment at end (MatchPathRE)",
expression: &MatchExpression{
Expr: `path_regexp('bar_at_start', '^/bar')`,
},
urlTarget: "https://example.com/foo/bar",
wantResult: false,
},
{
name: "url_pattern matches named group (MatchURLPattern)",
expression: &MatchExpression{
Expr: `url_pattern('/books/:id')`,
},
urlTarget: "https://example.com/books/123",
wantResult: true,
},
{
name: "url_pattern does not match (MatchURLPattern)",
expression: &MatchExpression{
Expr: `url_pattern('/books/:id')`,
},
urlTarget: "https://example.com/movies/123",
wantResult: false,
},
{
name: "url_pattern with base_url matches host (MatchURLPattern)",
expression: &MatchExpression{
Expr: `url_pattern('/foo', 'https://example.com')`,
},
urlTarget: "https://example.com/foo",
wantResult: true,
},
{
name: "url_pattern with base_url rejects other host (MatchURLPattern)",
expression: &MatchExpression{
Expr: `url_pattern('/foo', 'https://example.com')`,
},
urlTarget: "https://other.com/foo",
wantResult: false,
},
{
name: "protocol matches (MatchProtocol)",
expression: &MatchExpression{
Expr: `protocol('HTTPs')`,
},
urlTarget: "https://example.com",
wantResult: true,
},
{
name: "protocol does not match (MatchProtocol)",
expression: &MatchExpression{
Expr: `protocol('grpc')`,
},
urlTarget: "https://example.com",
wantResult: false,
},
{
name: "protocol invocation error no args (MatchProtocol)",
expression: &MatchExpression{
Expr: `protocol()`,
},
wantErr: true,
},
{
name: "protocol invocation error too many args (MatchProtocol)",
expression: &MatchExpression{
Expr: `protocol('grpc', 'https')`,
},
wantErr: true,
},
{
name: "protocol invocation error wrong arg type (MatchProtocol)",
expression: &MatchExpression{
Expr: `protocol(true)`,
},
wantErr: true,
},
{
name: "tls('complete') matches when handshake is complete (MatchTLS)",
expression: &MatchExpression{
Expr: `tls('complete')`,
},
urlTarget: "https://example.com",
tlsHandshakeComplete: boolPtr(true),
wantResult: true,
},
{
name: "tls('complete') does not match when handshake is incomplete (MatchTLS)",
expression: &MatchExpression{
Expr: `tls('complete')`,
},
urlTarget: "https://example.com",
tlsHandshakeComplete: boolPtr(false),
wantResult: false,
},
{
name: "tls('early_data') matches an incomplete handshake (MatchTLS)",
expression: &MatchExpression{
Expr: `tls('early_data')`,
},
urlTarget: "https://example.com",
tlsHandshakeComplete: boolPtr(false),
wantResult: true,
},
{
name: "tls('early_data') does not match a complete handshake (MatchTLS)",
expression: &MatchExpression{
Expr: `tls('early_data')`,
},
urlTarget: "https://example.com",
tlsHandshakeComplete: boolPtr(true),
wantResult: false,
},
{
name: "tls() matches any TLS connection regardless of handshake state, complete (MatchTLS)",
expression: &MatchExpression{
Expr: `tls()`,
},
urlTarget: "https://example.com",
tlsHandshakeComplete: boolPtr(true),
wantResult: true,
},
{
name: "tls() matches any TLS connection regardless of handshake state, incomplete (MatchTLS)",
expression: &MatchExpression{
Expr: `tls()`,
},
urlTarget: "https://example.com",
tlsHandshakeComplete: boolPtr(false),
wantResult: true,
},
{
name: "tls('any') is equivalent to tls(), complete (MatchTLS)",
expression: &MatchExpression{
Expr: `tls('any')`,
},
urlTarget: "https://example.com",
tlsHandshakeComplete: boolPtr(true),
wantResult: true,
},
{
name: "tls('any') is equivalent to tls(), incomplete (MatchTLS)",
expression: &MatchExpression{
Expr: `tls('any')`,
},
urlTarget: "https://example.com",
tlsHandshakeComplete: boolPtr(false),
wantResult: true,
},
{
name: "tls invocation error wrong arg type (MatchTLS)",
expression: &MatchExpression{
Expr: `tls(true)`,
},
wantErr: true,
},
{
name: "tls invocation error unrecognized string value (MatchTLS)",
expression: &MatchExpression{
Expr: `tls('bogus')`,
},
wantErr: true,
},
{
name: "tls('early_data') does not match a plain non-TLS request (MatchTLS)",
expression: &MatchExpression{
Expr: `tls('early_data')`,
},
urlTarget: "http://example.com",
wantResult: false,
},
{
name: "tls('complete') does not match a plain non-TLS request (MatchTLS)",
expression: &MatchExpression{
Expr: `tls('complete')`,
},
urlTarget: "http://example.com",
wantResult: false,
},
{
name: "tls() does not match a plain non-TLS request (MatchTLS)",
expression: &MatchExpression{
Expr: `tls()`,
},
urlTarget: "http://example.com",
wantResult: false,
},
{
name: "tls('any') does not match a plain non-TLS request (MatchTLS)",
expression: &MatchExpression{
Expr: `tls('any')`,
},
urlTarget: "http://example.com",
wantResult: false,
},
{
name: "query does not match against a specific value (MatchQuery)",
expression: &MatchExpression{
Expr: `query({"debug": "1"})`,
},
urlTarget: "https://example.com/foo",
wantResult: false,
},
{
name: "query matches against a specific value (MatchQuery)",
expression: &MatchExpression{
Expr: `query({"debug": "1"})`,
},
urlTarget: "https://example.com/foo/?debug=1",
wantResult: true,
},
{
name: "query matches against multiple values (MatchQuery)",
expression: &MatchExpression{
Expr: `query({"debug": ["0", "1", {http.request.uri.query.debug}+"1"]})`,
},
urlTarget: "https://example.com/foo/?debug=1",
wantResult: true,
},
{
name: "query matches against a wildcard (MatchQuery)",
expression: &MatchExpression{
Expr: `query({"debug": ["*"]})`,
},
urlTarget: "https://example.com/foo/?debug=something",
wantResult: true,
},
{
name: "query matches against a placeholder value (MatchQuery)",
expression: &MatchExpression{
Expr: `query({"debug": {http.request.uri.query.debug}})`,
},
urlTarget: "https://example.com/foo/?debug=1",
wantResult: true,
},
{
name: "query error bad map key type (MatchQuery)",
expression: &MatchExpression{
Expr: `query({1: "1"})`,
},
wantErr: true,
},
{
name: "query error typed struct instead of map (MatchQuery)",
expression: &MatchExpression{
Expr: `query(Message{field: "1"})`,
},
wantErr: true,
},
{
name: "query error bad map value type (MatchQuery)",
expression: &MatchExpression{
Expr: `query({"debug": 1})`,
},
wantErr: true,
},
{
name: "query error no args (MatchQuery)",
expression: &MatchExpression{
Expr: `query()`,
},
wantErr: true,
},
{
name: "remote_ip error no args (MatchRemoteIP)",
expression: &MatchExpression{
Expr: `remote_ip()`,
},
wantErr: true,
},
{
name: "remote_ip single IP match (MatchRemoteIP)",
expression: &MatchExpression{
Expr: `remote_ip('192.0.2.1')`,
},
urlTarget: "https://example.com/foo",
wantResult: true,
},
{
name: "vars value (VarsMatcher)",
expression: &MatchExpression{
Expr: `vars({'foo': 'bar'})`,
},
urlTarget: "https://example.com/foo",
wantResult: true,
},
{
name: "vars matches placeholder, needs escape (VarsMatcher)",
expression: &MatchExpression{
Expr: `vars({'\{http.request.uri.path}': '/foo'})`,
},
urlTarget: "https://example.com/foo",
wantResult: true,
},
{
name: "vars error wrong syntax (VarsMatcher)",
expression: &MatchExpression{
Expr: `vars('foo', 'bar')`,
},
wantErr: true,
},
{
name: "vars error no args (VarsMatcher)",
expression: &MatchExpression{
Expr: `vars()`,
},
wantErr: true,
},
{
name: "vars_regexp value (MatchVarsRE)",
expression: &MatchExpression{
Expr: `vars_regexp('foo', 'ba?r')`,
},
urlTarget: "https://example.com/foo",
wantResult: true,
},
{
name: "vars_regexp value with name (MatchVarsRE)",
expression: &MatchExpression{
Expr: `vars_regexp('name', 'foo', 'ba?r')`,
},
urlTarget: "https://example.com/foo",
wantResult: true,
},
{
name: "vars_regexp matches placeholder, needs escape (MatchVarsRE)",
expression: &MatchExpression{
Expr: `vars_regexp('\{http.request.uri.path}', '/fo?o')`,
},
urlTarget: "https://example.com/foo",
wantResult: true,
},
{
name: "vars_regexp error no args (MatchVarsRE)",
expression: &MatchExpression{
Expr: `vars_regexp()`,
},
wantErr: true,
},
}
)
func TestMatchExpressionMatch(t *testing.T) {
for _, tst := range matcherTests {
tc := tst
t.Run(tc.name, func(t *testing.T) {
caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
defer cancel()
err := tc.expression.Provision(caddyCtx)
if err != nil {
if !tc.wantErr {
t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tc.wantErr)
}
return
}
req := httptest.NewRequest(tc.httpMethod, tc.urlTarget, nil)
if tc.httpHeader != nil {
req.Header = *tc.httpHeader
}
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
ctx = context.WithValue(ctx, VarsCtxKey, map[string]any{
"foo": "bar",
})
req = req.WithContext(ctx)
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
if tc.clientCertificate != nil {
block, _ := pem.Decode(clientCert)
if block == nil {
t.Fatalf("failed to decode PEM certificate")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("failed to decode PEM certificate: %v", err)
}
req.TLS = &tls.ConnectionState{
PeerCertificates: []*x509.Certificate{cert},
}
}
if tc.tlsHandshakeComplete != nil {
if req.TLS == nil {
req.TLS = &tls.ConnectionState{}
}
req.TLS.HandshakeComplete = *tc.tlsHandshakeComplete
}
matches, err := tc.expression.MatchWithError(req)
if err != nil {
t.Errorf("MatchExpression.Match() error = %v", err)
}
if matches != tc.wantResult {
t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tc.wantResult, tc.expression.Expr)
}
})
}
}
func BenchmarkMatchExpressionMatch(b *testing.B) {
for _, tst := range matcherTests {
tc := tst
if tc.wantErr {
continue
}
b.Run(tst.name, func(b *testing.B) {
tc.expression.Provision(caddy.Context{})
req := httptest.NewRequest(tc.httpMethod, tc.urlTarget, nil)
if tc.httpHeader != nil {
req.Header = *tc.httpHeader
}
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
ctx = context.WithValue(ctx, VarsCtxKey, map[string]any{
"foo": "bar",
})
req = req.WithContext(ctx)
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
if tc.clientCertificate != nil {
block, _ := pem.Decode(clientCert)
if block == nil {
b.Fatalf("failed to decode PEM certificate")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
b.Fatalf("failed to decode PEM certificate: %v", err)
}
req.TLS = &tls.ConnectionState{
PeerCertificates: []*x509.Certificate{cert},
}
}
if tc.tlsHandshakeComplete != nil {
if req.TLS == nil {
req.TLS = &tls.ConnectionState{}
}
req.TLS.HandshakeComplete = *tc.tlsHandshakeComplete
}
b.ResetTimer()
for b.Loop() {
tc.expression.MatchWithError(req)
}
})
}
}
func TestMatchExpressionProvision(t *testing.T) {
tests := []struct {
name string
expression *MatchExpression
wantErr bool
}{
{
name: "boolean matches succeed",
expression: &MatchExpression{
Expr: "{http.request.uri.query} != ''",
},
wantErr: false,
},
{
name: "reject expressions with non-boolean results",
expression: &MatchExpression{
Expr: "{http.request.uri.query}",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
defer cancel()
if err := tt.expression.Provision(ctx); (err != nil) != tt.wantErr {
t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestMatchExpressionUnmarshalCaddyfileRepeated(t *testing.T) {
for _, input := range []string{`
expression {http.request.method} == 'GET'
expression {http.request.uri.path}.startsWith('/public/')
`, `
expression "{http.request.method} == 'GET'"
expression {http.request.uri.path}.startsWith('/public/')
`} {
d := caddyfile.NewTestDispenser(input)
m := new(MatchExpression)
err := m.UnmarshalCaddyfile(d)
if err == nil || !strings.Contains(err.Error(), "repeated expression matchers are not supported") {
t.Errorf("UnmarshalCaddyfile() error = %v, want repeated expression error (expression: %s)", err, m.Expr)
}
}
}
func boolPtr(b bool) *bool { return &b }