Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 13, 16:45 UTC
Expose several Caddy HTTP Matchers to the CEL Matcher (#4715)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Tristan Swadell committed
Jun 22, 2022 at 15:53 UTC
10f85558ead15e119f8e9abd81c8ad55eb865f8b
7 files changed
+1526
-70
go.mod
+5
-5
@@ -10,7 +10,7 @@ require (
10
github.com/caddyserver/certmagic v0.16.1
11
github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac
12
github.com/go-chi/chi v4.1.2+incompatible
13
- github.com/google/cel-go v0.7.3
13
+ github.com/google/cel-go v0.11.4
14
github.com/google/uuid v1.3.0
15
github.com/klauspost/compress v1.15.6
16
github.com/klauspost/cpuid/v2 v2.0.13
@@ -32,8 +32,8 @@ require (
32
golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2
33
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd
34
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
35
- google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf
36
- google.golang.org/protobuf v1.27.1
35
+ google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21
36
+ google.golang.org/protobuf v1.28.0
37
gopkg.in/natefinch/lumberjack.v2 v2.0.0
38
gopkg.in/yaml.v3 v3.0.1
39
)
@@ -43,7 +43,7 @@ require (
43
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
44
github.com/Masterminds/goutils v1.1.1 // indirect
45
github.com/Masterminds/semver/v3 v3.1.1 // indirect
46
- github.com/antlr/antlr4 v0.0.0-20200503195918-621b933c7a7f // indirect
46
+ github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220418222510-f25a4f6275ed // indirect
47
github.com/beorn7/perks v1.0.1 // indirect
48
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
49
github.com/cespare/xxhash v1.1.0 // indirect
@@ -125,7 +125,7 @@ require (
125
golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect
126
golang.org/x/tools v0.1.7 // indirect
127
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
128
- google.golang.org/grpc v1.44.0 // indirect
128
+ google.golang.org/grpc v1.46.0 // indirect
129
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
130
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
131
howett.net/plist v1.0.0 // indirect
go.sum
+13
-9
@@ -149,8 +149,8 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5
149
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
150
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
151
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
152
-github.com/antlr/antlr4 v0.0.0-20200503195918-621b933c7a7f h1:0cEys61Sr2hUBEXfNV8eyQP01oZuBgoMeHunebPirK8=
153
-github.com/antlr/antlr4 v0.0.0-20200503195918-621b933c7a7f/go.mod h1:T7PbCXFs94rrTttyxjbyT5+/1V8T2TYDejxUfHJjw1Y=
152
+github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220418222510-f25a4f6275ed h1:ue9pVfIcP+QMEjfgo/Ez4ZjNZfonGgR6NgjMaJMu1Cg=
153
+github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220418222510-f25a4f6275ed/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
154
github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=
155
github.com/apache/beam v2.28.0+incompatible/go.mod h1:/8NX3Qi8vGstDLLaeaU7+lzVEu/ACaQhYjeefzQ0y1o=
156
github.com/apache/beam v2.30.0+incompatible/go.mod h1:/8NX3Qi8vGstDLLaeaU7+lzVEu/ACaQhYjeefzQ0y1o=
@@ -238,6 +238,7 @@ github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XP
238
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
239
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
240
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
241
+github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
242
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
243
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
244
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
@@ -311,6 +312,7 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
312
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
313
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
314
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
315
+github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
316
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
317
github.com/envoyproxy/protoc-gen-validate v0.3.0-java/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
318
github.com/etcd-io/gofail v0.0.0-20190801230047-ad7f989257ca/go.mod h1:49H/RkXP8pKaZy4h0d+NW16rSLhyVBt4o6VLJbmOqDE=
@@ -390,6 +392,7 @@ github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP
392
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
393
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
394
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
395
+github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
396
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
397
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
398
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -433,9 +436,8 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
436
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
437
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
438
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
436
-github.com/google/cel-go v0.7.3 h1:8v9BSN0avuGwrHFKNCjfiQ/CE6+D6sW+BDyOVoEeP6o=
437
-github.com/google/cel-go v0.7.3/go.mod h1:4EtyFAHT5xNr0Msu0MJjyGxPUgdr9DlcaPyzLt/kkt8=
438
-github.com/google/cel-spec v0.5.0/go.mod h1:Nwjgxy5CbjlPrtCWjeDjUyKMl8w41YBYGjsyDdqk0xA=
439
+github.com/google/cel-go v0.11.4 h1:wWOnKmLxALl3l9Av221MfIOWRiR01sDVljzg6LZ6Zn0=
440
+github.com/google/cel-go v0.11.4/go.mod h1:Av7CU6r6X3YmcHR9GXqVDaEJYfEtSxl6wvIjUQTriCw=
441
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
442
github.com/google/certificate-transparency-go v1.1.2-0.20210422104406-9f33727a7a18/go.mod h1:6CKh9dscIRoqc2kC6YUFICHZMT9NrClyPrRVFrdw1QQ=
443
github.com/google/certificate-transparency-go v1.1.2-0.20210512142713-bed466244fa6/go.mod h1:aF2dp7Dh81mY8Y/zpzyXps4fQW5zQbDu2CxfpJB6NkI=
@@ -1714,7 +1716,6 @@ google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6D
1716
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
1717
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
1718
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
1717
-google.golang.org/genproto v0.0.0-20201102152239-715cce707fb0/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
1719
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
1720
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
1721
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
@@ -1754,8 +1755,9 @@ google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ6
1755
google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
1756
google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
1757
google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
1757
-google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf h1:SVYXkUz2yZS9FWb2Gm8ivSlbNQzL2Z/NpPKE3RG2jWk=
1758
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
1759
+google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 h1:hrbNEivu7Zn1pxvHk6MBrq9iE22woVILTHqexqBxe6I=
1760
+google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
1761
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
1762
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
1763
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
@@ -1793,8 +1795,9 @@ google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K
1795
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
1796
google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
1797
google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
1796
-google.golang.org/grpc v1.44.0 h1:weqSxi/TMs1SqFRMHCtBgXRs8k3X39QIDEZ0pRcttUg=
1798
google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
1799
+google.golang.org/grpc v1.46.0 h1:oCjezcn6g6A75TGoKYBPgKmVBLexhYLM6MebdrPApP8=
1800
+google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
1801
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
1802
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
1803
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -1809,8 +1812,9 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
1812
google.golang.org/protobuf v1.25.1-0.20200805231151-a709e31e5d12/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
1813
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
1814
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
1812
-google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
1815
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
1816
+google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
1817
+google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
1818
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
1819
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
1820
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
modules/caddyhttp/celmatcher.go
+470
-11
@@ -17,6 +17,7 @@ package caddyhttp
17
import (
18
"crypto/x509/pkix"
19
"encoding/json"
20
+ "errors"
21
"fmt"
22
"net/http"
23
"reflect"
@@ -28,11 +29,15 @@ import (
29
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
30
"github.com/google/cel-go/cel"
31
"github.com/google/cel-go/checker/decls"
32
+ "github.com/google/cel-go/common"
33
+ "github.com/google/cel-go/common/operators"
34
"github.com/google/cel-go/common/types"
35
"github.com/google/cel-go/common/types/ref"
36
"github.com/google/cel-go/common/types/traits"
37
"github.com/google/cel-go/ext"
38
+ "github.com/google/cel-go/interpreter"
39
"github.com/google/cel-go/interpreter/functions"
40
+ "github.com/google/cel-go/parser"
41
"go.uber.org/zap"
42
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
43
"google.golang.org/protobuf/proto"
@@ -96,6 +101,29 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
101
// our type adapter expands CEL's standard type support
102
m.ta = celTypeAdapter{}
103
104
+ // initialize the CEL libraries from the Matcher implementations which
105
+ // have been configured to support CEL.
106
+ matcherLibProducers := []CELLibraryProducer{}
107
+ for _, info := range caddy.GetModules("http.matchers") {
108
+ p, ok := info.New().(CELLibraryProducer)
109
+ if ok {
110
+ matcherLibProducers = append(matcherLibProducers, p)
111
+ }
112
+ }
113
+ // Assemble the compilation and program options from the different library
114
+ // producers into a single cel.Library implementation.
115
+ matcherEnvOpts := []cel.EnvOption{}
116
+ matcherProgramOpts := []cel.ProgramOption{}
117
+ for _, producer := range matcherLibProducers {
118
+ l, err := producer.CELLibrary(ctx)
119
+ if err != nil {
120
+ return fmt.Errorf("error initializing CEL library for %T: %v", producer, err)
121
+ }
122
+ matcherEnvOpts = append(matcherEnvOpts, l.CompileOptions()...)
123
+ matcherProgramOpts = append(matcherProgramOpts, l.ProgramOptions()...)
124
+ }
125
+ matcherLib := cel.Lib(NewMatcherCELLibrary(matcherEnvOpts, matcherProgramOpts))
126
+
127
// create the CEL environment
128
env, err := cel.NewEnv(
129
cel.Declarations(
@@ -107,6 +135,7 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
135
),
136
cel.CustomTypeAdapter(m.ta),
137
ext.Strings(),
138
+ matcherLib,
139
)
140
if err != nil {
141
return fmt.Errorf("setting up CEL environment: %v", err)
@@ -114,7 +143,7 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
143
144
// parse and type-check the expression
145
checked, issues := env.Compile(m.expandedExpr)
117
- if issues != nil && issues.Err() != nil {
146
+ if issues.Err() != nil {
147
return fmt.Errorf("compiling CEL program: %s", issues.Err())
148
}
149
@@ -126,6 +155,7 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
155
156
// compile the "program"
157
m.prg, err = env.Program(checked,
158
+ cel.EvalOptions(cel.OptOptimize),
159
cel.Functions(
160
&functions.Overload{
161
Operator: placeholderFuncName,
@@ -133,7 +163,6 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
163
},
164
),
165
)
136
-
166
if err != nil {
167
return fmt.Errorf("compiling CEL program: %s", err)
168
}
@@ -142,18 +171,17 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
171
172
// Match returns true if r matches m.
173
func (m MatchExpression) Match(r *http.Request) bool {
145
- out, _, err := m.prg.Eval(map[string]interface{}{
146
- "request": celHTTPRequest{r},
147
- })
174
+ celReq := celHTTPRequest{r}
175
+ out, _, err := m.prg.Eval(celReq)
176
if err != nil {
177
m.log.Error("evaluating expression", zap.Error(err))
178
+ SetVar(r.Context(), MatcherErrorVarKey, err)
179
return false
180
}
181
if outBool, ok := out.Value().(bool); ok {
182
return outBool
183
}
184
return false
156
-
185
}
186
187
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
@@ -175,13 +203,15 @@ func (m MatchExpression) caddyPlaceholderFunc(lhs, rhs ref.Val) ref.Val {
203
if !ok {
204
return types.NewErr(
205
"invalid request of type '%v' to "+placeholderFuncName+"(request, placeholderVarName)",
178
- lhs.Type())
206
+ lhs.Type(),
207
+ )
208
}
209
phStr, ok := rhs.(types.String)
210
if !ok {
211
return types.NewErr(
212
"invalid placeholder variable name of type '%v' to "+placeholderFuncName+"(request, placeholderVarName)",
184
- rhs.Type())
213
+ rhs.Type(),
214
+ )
215
}
216
217
repl := celReq.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
@@ -193,10 +223,23 @@ func (m MatchExpression) caddyPlaceholderFunc(lhs, rhs ref.Val) ref.Val {
223
// httpRequestCELType is the type representation of a native HTTP request.
224
var httpRequestCELType = types.NewTypeValue("http.Request", traits.ReceiverType)
225
196
-// cellHTTPRequest wraps an http.Request with
197
-// methods to satisfy the ref.Val interface.
226
+// celHTTPRequest wraps an http.Request with ref.Val interface methods.
227
+//
228
+// This type also implements the interpreter.Activation interface which
229
+// drops allocation costs for CEL expression evaluations by roughly half.
230
type celHTTPRequest struct{ *http.Request }
231
232
+func (cr celHTTPRequest) ResolveName(name string) (interface{}, bool) {
233
+ if name == "request" {
234
+ return cr, true
235
+ }
236
+ return nil, false
237
+}
238
+
239
+func (cr celHTTPRequest) Parent() interpreter.Activation {
240
+ return nil
241
+}
242
+
243
func (cr celHTTPRequest) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
244
return cr.Request, nil
245
}
@@ -250,12 +293,428 @@ func (celTypeAdapter) NativeToValue(value interface{}) ref.Val {
293
return types.DefaultTypeAdapter.NativeToValue(value)
294
}
295
296
+// CELLibraryProducer provide CEL libraries that expose a Matcher
297
+// implementation as a first class function within the CEL expression
298
+// matcher.
299
+type CELLibraryProducer interface {
300
+ // CELLibrary creates a cel.Library which makes it possible to use the
301
+ // target object within CEL expression matchers.
302
+ CELLibrary(caddy.Context) (cel.Library, error)
303
+}
304
+
305
+// CELMatcherImpl creates a new cel.Library based on the following pieces of
306
+// data:
307
+//
308
+// - macroName: the function name to be used within CEL. This will be a macro
309
+// and not a function proper.
310
+// - funcName: the function overload name generated by the CEL macro used to
311
+// represent the matcher.
312
+// - matcherDataTypes: the argument types to the macro.
313
+// - fac: a matcherFactory implementation which converts from CEL constant
314
+// values to a Matcher instance.
315
+//
316
+// Note, macro names and function names must not collide with other macros or
317
+// functions exposed within CEL expressions, or an error will be produced
318
+// during the expression matcher plan time.
319
+//
320
+// The existing CELMatcherImpl support methods are configured to support a
321
+// limited set of function signatures. For strong type validation you may need
322
+// to provide a custom macro which does a more detailed analysis of the CEL
323
+// literal provided to the macro as an argument.
324
+func CELMatcherImpl(macroName, funcName string, matcherDataTypes []*exprpb.Type, fac CELMatcherFactory) (cel.Library, error) {
325
+ requestType := decls.NewObjectType("http.Request")
326
+ var macro parser.Macro
327
+ switch len(matcherDataTypes) {
328
+ case 1:
329
+ matcherDataType := matcherDataTypes[0]
330
+ if isCELStringListType(matcherDataType) {
331
+ macro = parser.NewGlobalVarArgMacro(macroName, celMatcherStringListMacroExpander(funcName))
332
+ } else if isCELStringType(matcherDataType) {
333
+ macro = parser.NewGlobalMacro(macroName, 1, celMatcherStringMacroExpander(funcName))
334
+ } else if isCELJSONType(matcherDataType) {
335
+ macro = parser.NewGlobalMacro(macroName, 1, celMatcherJSONMacroExpander(funcName))
336
+ } else {
337
+ return nil, fmt.Errorf("unsupported matcher data type: %s", cel.FormatType(matcherDataType))
338
+ }
339
+ case 2:
340
+ if isCELStringType(matcherDataTypes[0]) && isCELStringType(matcherDataTypes[1]) {
341
+ macro = parser.NewGlobalMacro(macroName, 2, celMatcherStringListMacroExpander(funcName))
342
+ matcherDataTypes = []*exprpb.Type{CelTypeListString}
343
+ } else {
344
+ return nil, fmt.Errorf(
345
+ "unsupported matcher data type: %s, %s",
346
+ cel.FormatType(matcherDataTypes[0]), cel.FormatType(matcherDataTypes[1]),
347
+ )
348
+ }
349
+ case 3:
350
+ if isCELStringType(matcherDataTypes[0]) && isCELStringType(matcherDataTypes[1]) && isCELStringType(matcherDataTypes[2]) {
351
+ macro = parser.NewGlobalMacro(macroName, 3, celMatcherStringListMacroExpander(funcName))
352
+ matcherDataTypes = []*exprpb.Type{CelTypeListString}
353
+ } else {
354
+ return nil, fmt.Errorf(
355
+ "unsupported matcher data type: %s, %s, %s",
356
+ cel.FormatType(matcherDataTypes[0]), cel.FormatType(matcherDataTypes[1]), cel.FormatType(matcherDataTypes[2]),
357
+ )
358
+ }
359
+ }
360
+ envOptions := []cel.EnvOption{
361
+ cel.Macros(macro),
362
+ cel.Declarations(
363
+ decls.NewFunction(funcName,
364
+ decls.NewOverload(
365
+ funcName,
366
+ append([]*exprpb.Type{requestType}, matcherDataTypes...),
367
+ decls.Bool,
368
+ ),
369
+ ),
370
+ ),
371
+ }
372
+ programOptions := []cel.ProgramOption{
373
+ cel.CustomDecorator(CELMatcherDecorator(funcName, fac)),
374
+ cel.Functions(
375
+ &functions.Overload{
376
+ Operator: funcName,
377
+ Binary: CELMatcherRuntimeFunction(funcName, fac),
378
+ },
379
+ ),
380
+ }
381
+ return NewMatcherCELLibrary(envOptions, programOptions), nil
382
+}
383
+
384
+// CELMatcherFactory converts a constant CEL value into a RequestMatcher.
385
+type CELMatcherFactory func(data ref.Val) (RequestMatcher, error)
386
+
387
+// matcherCELLibrary is a simplistic configurable cel.Library implementation.
388
+type matcherCELLibary struct {
389
+ envOptions []cel.EnvOption
390
+ programOptions []cel.ProgramOption
391
+}
392
+
393
+// NewMatcherCELLibrary creates a matcherLibrary from option setes.
394
+func NewMatcherCELLibrary(envOptions []cel.EnvOption, programOptions []cel.ProgramOption) cel.Library {
395
+ return &matcherCELLibary{
396
+ envOptions: envOptions,
397
+ programOptions: programOptions,
398
+ }
399
+}
400
+
401
+func (lib *matcherCELLibary) CompileOptions() []cel.EnvOption {
402
+ return lib.envOptions
403
+}
404
+
405
+func (lib *matcherCELLibary) ProgramOptions() []cel.ProgramOption {
406
+ return lib.programOptions
407
+}
408
+
409
+// CELMatcherDecorator matches a call overload generated by a CEL macro
410
+// that takes a single argument, and optimizes the implementation to precompile
411
+// the matcher and return a function that references the precompiled and
412
+// provisioned matcher.
413
+func CELMatcherDecorator(funcName string, fac CELMatcherFactory) interpreter.InterpretableDecorator {
414
+ return func(i interpreter.Interpretable) (interpreter.Interpretable, error) {
415
+ call, ok := i.(interpreter.InterpretableCall)
416
+ if !ok {
417
+ return i, nil
418
+ }
419
+ if call.OverloadID() != funcName {
420
+ return i, nil
421
+ }
422
+ callArgs := call.Args()
423
+ reqAttr, ok := callArgs[0].(interpreter.InterpretableAttribute)
424
+ if !ok {
425
+ return nil, errors.New("missing 'request' argument")
426
+ }
427
+ nsAttr, ok := reqAttr.Attr().(interpreter.NamespacedAttribute)
428
+ if !ok {
429
+ return nil, errors.New("missing 'request' argument")
430
+ }
431
+ varNames := nsAttr.CandidateVariableNames()
432
+ if len(varNames) != 1 || len(varNames) == 1 && varNames[0] != "request" {
433
+ return nil, errors.New("missing 'request' argument")
434
+ }
435
+ matcherData, ok := callArgs[1].(interpreter.InterpretableConst)
436
+ if !ok {
437
+ // If the matcher arguments are not constant, then this means
438
+ // they contain a Caddy placeholder reference and the evaluation
439
+ // and matcher provisioning should be handled at dynamically.
440
+ return i, nil
441
+ }
442
+ matcher, err := fac(matcherData.Value())
443
+ if err != nil {
444
+ return nil, err
445
+ }
446
+ return interpreter.NewCall(
447
+ i.ID(), funcName, funcName+"_opt",
448
+ []interpreter.Interpretable{reqAttr},
449
+ func(args ...ref.Val) ref.Val {
450
+ // The request value, guaranteed to be of type celHTTPRequest
451
+ celReq := args[0]
452
+ // If needed this call could be changed to convert the value
453
+ // to a *http.Request using CEL's ConvertToNative method.
454
+ httpReq := celReq.Value().(celHTTPRequest)
455
+ return types.Bool(matcher.Match(httpReq.Request))
456
+ },
457
+ ), nil
458
+ }
459
+}
460
+
461
+// CELMatcherRuntimeFunction creates a function binding for when the input to the matcher
462
+// is dynamically resolved rather than a set of static constant values.
463
+func CELMatcherRuntimeFunction(funcName string, fac CELMatcherFactory) functions.BinaryOp {
464
+ return func(celReq, matcherData ref.Val) ref.Val {
465
+ matcher, err := fac(matcherData)
466
+ if err != nil {
467
+ return types.NewErr(err.Error())
468
+ }
469
+ httpReq := celReq.Value().(celHTTPRequest)
470
+ return types.Bool(matcher.Match(httpReq.Request))
471
+ }
472
+}
473
+
474
+// celMatcherStringListMacroExpander validates that the macro is called
475
+// with a variable number of string arguments (at least one).
476
+//
477
+// The arguments are collected into a single list argument the following
478
+// function call returned: <funcName>(request, [args])
479
+func celMatcherStringListMacroExpander(funcName string) parser.MacroExpander {
480
+ return func(eh parser.ExprHelper, target *exprpb.Expr, args []*exprpb.Expr) (*exprpb.Expr, *common.Error) {
481
+ matchArgs := []*exprpb.Expr{}
482
+ if len(args) == 0 {
483
+ return nil, &common.Error{
484
+ Message: "matcher requires at least one argument",
485
+ }
486
+ }
487
+ for _, arg := range args {
488
+ if isCELStringExpr(arg) {
489
+ matchArgs = append(matchArgs, arg)
490
+ } else {
491
+ return nil, &common.Error{
492
+ Location: eh.OffsetLocation(arg.GetId()),
493
+ Message: "matcher arguments must be string constants",
494
+ }
495
+ }
496
+ }
497
+ return eh.GlobalCall(funcName, eh.Ident("request"), eh.NewList(matchArgs...)), nil
498
+ }
499
+}
500
+
501
+// celMatcherStringMacroExpander validates that the macro is called a single
502
+// string argument.
503
+//
504
+// The following function call is returned: <funcName>(request, arg)
505
+func celMatcherStringMacroExpander(funcName string) parser.MacroExpander {
506
+ return func(eh parser.ExprHelper, target *exprpb.Expr, args []*exprpb.Expr) (*exprpb.Expr, *common.Error) {
507
+ if len(args) != 1 {
508
+ return nil, &common.Error{
509
+ Message: "matcher requires one argument",
510
+ }
511
+ }
512
+ if isCELStringExpr(args[0]) {
513
+ return eh.GlobalCall(funcName, eh.Ident("request"), args[0]), nil
514
+ }
515
+ return nil, &common.Error{
516
+ Location: eh.OffsetLocation(args[0].GetId()),
517
+ Message: "matcher argument must be a string literal",
518
+ }
519
+ }
520
+}
521
+
522
+// celMatcherStringMacroExpander validates that the macro is called a single
523
+// map literal argument.
524
+//
525
+// The following function call is returned: <funcName>(request, arg)
526
+func celMatcherJSONMacroExpander(funcName string) parser.MacroExpander {
527
+ return func(eh parser.ExprHelper, target *exprpb.Expr, args []*exprpb.Expr) (*exprpb.Expr, *common.Error) {
528
+ if len(args) != 1 {
529
+ return nil, &common.Error{
530
+ Message: "matcher requires a map literal argument",
531
+ }
532
+ }
533
+ arg := args[0]
534
+ switch arg.GetExprKind().(type) {
535
+ case *exprpb.Expr_StructExpr:
536
+ structExpr := arg.GetStructExpr()
537
+ if structExpr.GetMessageName() != "" {
538
+ return nil, &common.Error{
539
+ Location: eh.OffsetLocation(arg.GetId()),
540
+ Message: fmt.Sprintf(
541
+ "matcher input must be a map literal, not a %s",
542
+ structExpr.GetMessageName(),
543
+ ),
544
+ }
545
+ }
546
+ for _, entry := range structExpr.GetEntries() {
547
+ isStringPlaceholder := isCELStringExpr(entry.GetMapKey())
548
+ if !isStringPlaceholder {
549
+ return nil, &common.Error{
550
+ Location: eh.OffsetLocation(entry.GetId()),
551
+ Message: "matcher map keys must be string literals",
552
+ }
553
+ }
554
+ isStringListPlaceholder := isCELStringExpr(entry.GetValue()) ||
555
+ isCELStringListLiteral(entry.GetValue())
556
+ if !isStringListPlaceholder {
557
+ return nil, &common.Error{
558
+ Location: eh.OffsetLocation(entry.GetValue().GetId()),
559
+ Message: "matcher map values must be string or list literals",
560
+ }
561
+ }
562
+ }
563
+ return eh.GlobalCall(funcName, eh.Ident("request"), arg), nil
564
+ }
565
+
566
+ return nil, &common.Error{
567
+ Location: eh.OffsetLocation(arg.GetId()),
568
+ Message: "matcher requires a map literal argument",
569
+ }
570
+ }
571
+}
572
+
573
+// CELValueToMapStrList converts a CEL value to a map[string][]string
574
+//
575
+// Earlier validation stages should guarantee that the value has this type
576
+// at compile time, and that the runtime value type is map[string]interface{}.
577
+// The reason for the slight difference in value type is that CEL allows for
578
+// map literals containing heterogeneous values, in this case string and list
579
+// of string.
580
+func CELValueToMapStrList(data ref.Val) (map[string][]string, error) {
581
+ mapStrType := reflect.TypeOf(map[string]interface{}{})
582
+ mapStrRaw, err := data.ConvertToNative(mapStrType)
583
+ if err != nil {
584
+ return nil, err
585
+ }
586
+ mapStrIface := mapStrRaw.(map[string]interface{})
587
+ mapStrListStr := make(map[string][]string, len(mapStrIface))
588
+ for k, v := range mapStrIface {
589
+ switch val := v.(type) {
590
+ case string:
591
+ mapStrListStr[k] = []string{val}
592
+ case types.String:
593
+ mapStrListStr[k] = []string{string(val)}
594
+ case []string:
595
+ mapStrListStr[k] = val
596
+ case []ref.Val:
597
+ convVals := make([]string, len(val))
598
+ for i, elem := range val {
599
+ strVal, ok := elem.(types.String)
600
+ if !ok {
601
+ return nil, fmt.Errorf("unsupported value type in header match: %T", val)
602
+ }
603
+ convVals[i] = string(strVal)
604
+ }
605
+ mapStrListStr[k] = convVals
606
+ default:
607
+ return nil, fmt.Errorf("unsupported value type in header match: %T", val)
608
+ }
609
+ }
610
+ return mapStrListStr, nil
611
+}
612
+
613
+// isCELJSONType returns whether the type corresponds to JSON input.
614
+func isCELJSONType(t *exprpb.Type) bool {
615
+ switch t.GetTypeKind().(type) {
616
+ case *exprpb.Type_MapType_:
617
+ mapType := t.GetMapType()
618
+ return isCELStringType(mapType.GetKeyType()) && mapType.GetValueType().GetDyn() != nil
619
+ }
620
+ return false
621
+}
622
+
623
+// isCELStringType returns whether the type corresponds to a string.
624
+func isCELStringType(t *exprpb.Type) bool {
625
+ switch t.GetTypeKind().(type) {
626
+ case *exprpb.Type_Primitive:
627
+ return t.GetPrimitive() == exprpb.Type_STRING
628
+ }
629
+ return false
630
+}
631
+
632
+// isCELStringExpr indicates whether the expression is a supported string expression
633
+func isCELStringExpr(e *exprpb.Expr) bool {
634
+ return isCELStringLiteral(e) || isCELCaddyPlaceholderCall(e) || isCELConcatCall(e)
635
+}
636
+
637
+// isCELStringLiteral returns whether the expression is a CEL string literal.
638
+func isCELStringLiteral(e *exprpb.Expr) bool {
639
+ switch e.GetExprKind().(type) {
640
+ case *exprpb.Expr_ConstExpr:
641
+ constant := e.GetConstExpr()
642
+ switch constant.GetConstantKind().(type) {
643
+ case *exprpb.Constant_StringValue:
644
+ return true
645
+ }
646
+ }
647
+ return false
648
+}
649
+
650
+// isCELCaddyPlaceholderCall returns whether the expression is a caddy placeholder call.
651
+func isCELCaddyPlaceholderCall(e *exprpb.Expr) bool {
652
+ switch e.GetExprKind().(type) {
653
+ case *exprpb.Expr_CallExpr:
654
+ call := e.GetCallExpr()
655
+ if call.GetFunction() == "caddyPlaceholder" {
656
+ return true
657
+ }
658
+ }
659
+ return false
660
+}
661
+
662
+// isCELConcatCall tests whether the expression is a concat function (+) with string, placeholder, or
663
+// other concat call arguments.
664
+func isCELConcatCall(e *exprpb.Expr) bool {
665
+ switch e.GetExprKind().(type) {
666
+ case *exprpb.Expr_CallExpr:
667
+ call := e.GetCallExpr()
668
+ if call.GetTarget() != nil {
669
+ return false
670
+ }
671
+ if call.GetFunction() != operators.Add {
672
+ return false
673
+ }
674
+ for _, arg := range call.GetArgs() {
675
+ if !isCELStringExpr(arg) {
676
+ return false
677
+ }
678
+ }
679
+ return true
680
+ }
681
+ return false
682
+}
683
+
684
+// isCELStringListType returns whether the type corresponds to a list of strings.
685
+func isCELStringListType(t *exprpb.Type) bool {
686
+ switch t.GetTypeKind().(type) {
687
+ case *exprpb.Type_ListType_:
688
+ return isCELStringType(t.GetListType().GetElemType())
689
+ }
690
+ return false
691
+}
692
+
693
+// isCELStringListLiteral returns whether the expression resolves to a list literal
694
+// containing only string constants or a placeholder call.
695
+func isCELStringListLiteral(e *exprpb.Expr) bool {
696
+ switch e.GetExprKind().(type) {
697
+ case *exprpb.Expr_ListExpr:
698
+ list := e.GetListExpr()
699
+ for _, elem := range list.GetElements() {
700
+ if !isCELStringExpr(elem) {
701
+ return false
702
+ }
703
+ }
704
+ return true
705
+ }
706
+ return false
707
+}
708
+
709
// Variables used for replacing Caddy placeholders in CEL
710
// expressions with a proper CEL function call; this is
711
// just for syntactic sugar.
712
var (
257
- placeholderRegexp = regexp.MustCompile(`{([\w.-]+)}`)
713
+ placeholderRegexp = regexp.MustCompile(`{([a-zA-Z][\w.-]+)}`)
714
placeholderExpansion = `caddyPlaceholder(request, "${1}")`
715
+
716
+ CelTypeListString = decls.NewListType(decls.String)
717
+ CelTypeJson = decls.NewMapType(decls.String, decls.Dyn)
718
)
719
720
var httpRequestObjectType = decls.NewObjectType("http.Request")
modules/caddyhttp/celmatcher_test.go
+424
-42
@@ -19,45 +19,15 @@ import (
19
"crypto/tls"
20
"crypto/x509"
21
"encoding/pem"
22
+ "net/http"
23
"net/http/httptest"
24
"testing"
25
26
"github.com/caddyserver/caddy/v2"
27
)
28
28
-func TestMatchExpressionProvision(t *testing.T) {
29
- tests := []struct {
30
- name string
31
- expression *MatchExpression
32
- wantErr bool
33
- }{
34
- {
35
- name: "boolean matches succeed",
36
- expression: &MatchExpression{
37
- Expr: "{http.request.uri.query} != ''",
38
- },
39
- wantErr: false,
40
- },
41
- {
42
- name: "reject expressions with non-boolean results",
43
- expression: &MatchExpression{
44
- Expr: "{http.request.uri.query}",
45
- },
46
- wantErr: true,
47
- },
48
- }
49
- for _, tt := range tests {
50
- t.Run(tt.name, func(t *testing.T) {
51
- if err := tt.expression.Provision(caddy.Context{}); (err != nil) != tt.wantErr {
52
- t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tt.wantErr)
53
- }
54
- })
55
- }
56
-}
57
-
58
-func TestMatchExpressionMatch(t *testing.T) {
59
-
60
- clientCert := []byte(`-----BEGIN CERTIFICATE-----
29
+var (
30
+ clientCert = []byte(`-----BEGIN CERTIFICATE-----
31
MIIB9jCCAV+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1DYWRk
32
eSBUZXN0IENBMB4XDTE4MDcyNDIxMzUwNVoXDTI4MDcyMTIxMzUwNVowHTEbMBkG
33
A1UEAwwSY2xpZW50LmxvY2FsZG9tYWluMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
@@ -71,9 +41,12 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
41
9fNwfEi+OoXR6s+upSKobCmLGLGi9Na5s5g=
42
-----END CERTIFICATE-----`)
43
74
- tests := []struct {
44
+ matcherTests = []struct {
45
name string
46
expression *MatchExpression
47
+ urlTarget string
48
+ httpMethod string
49
+ httpHeader *http.Header
50
wantErr bool
51
wantResult bool
52
clientCertificate []byte
@@ -84,22 +57,363 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
57
Expr: "{http.request.tls.client.subject} == 'CN=client.localdomain'",
58
},
59
clientCertificate: clientCert,
60
+ urlTarget: "https://example.com/foo",
61
wantResult: true,
62
},
63
+ {
64
+ name: "header matches (MatchHeader)",
65
+ expression: &MatchExpression{
66
+ Expr: `header({'Field': 'foo'})`,
67
+ },
68
+ urlTarget: "https://example.com/foo",
69
+ httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
70
+ wantResult: true,
71
+ },
72
+ {
73
+ name: "header error (MatchHeader)",
74
+ expression: &MatchExpression{
75
+ Expr: `header('foo')`,
76
+ },
77
+ urlTarget: "https://example.com/foo",
78
+ httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
79
+ wantErr: true,
80
+ },
81
+ {
82
+ name: "header_regexp matches (MatchHeaderRE)",
83
+ expression: &MatchExpression{
84
+ Expr: `header_regexp('Field', 'fo{2}')`,
85
+ },
86
+ urlTarget: "https://example.com/foo",
87
+ httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
88
+ wantResult: true,
89
+ },
90
+ {
91
+ name: "header_regexp matches with name (MatchHeaderRE)",
92
+ expression: &MatchExpression{
93
+ Expr: `header_regexp('foo', 'Field', 'fo{2}')`,
94
+ },
95
+ urlTarget: "https://example.com/foo",
96
+ httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
97
+ wantResult: true,
98
+ },
99
+ {
100
+ name: "header_regexp does not match (MatchHeaderRE)",
101
+ expression: &MatchExpression{
102
+ Expr: `header_regexp('foo', 'Nope', 'fo{2}')`,
103
+ },
104
+ urlTarget: "https://example.com/foo",
105
+ httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
106
+ wantResult: false,
107
+ },
108
+ {
109
+ name: "header_regexp error (MatchHeaderRE)",
110
+ expression: &MatchExpression{
111
+ Expr: `header_regexp('foo')`,
112
+ },
113
+ urlTarget: "https://example.com/foo",
114
+ httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
115
+ wantErr: true,
116
+ },
117
+ {
118
+ name: "host matches localhost (MatchHost)",
119
+ expression: &MatchExpression{
120
+ Expr: `host('localhost')`,
121
+ },
122
+ urlTarget: "http://localhost",
123
+ wantResult: true,
124
+ },
125
+ {
126
+ name: "host matches (MatchHost)",
127
+ expression: &MatchExpression{
128
+ Expr: `host('*.example.com')`,
129
+ },
130
+ urlTarget: "https://foo.example.com",
131
+ wantResult: true,
132
+ },
133
+ {
134
+ name: "host does not match (MatchHost)",
135
+ expression: &MatchExpression{
136
+ Expr: `host('example.net', '*.example.com')`,
137
+ },
138
+ urlTarget: "https://foo.example.org",
139
+ wantResult: false,
140
+ },
141
+ {
142
+ name: "host error (MatchHost)",
143
+ expression: &MatchExpression{
144
+ Expr: `host(80)`,
145
+ },
146
+ urlTarget: "http://localhost:80",
147
+ wantErr: true,
148
+ },
149
+ {
150
+ name: "method does not match (MatchMethod)",
151
+ expression: &MatchExpression{
152
+ Expr: `method('PUT')`,
153
+ },
154
+ urlTarget: "https://foo.example.com",
155
+ httpMethod: "GET",
156
+ wantResult: false,
157
+ },
158
+ {
159
+ name: "method matches (MatchMethod)",
160
+ expression: &MatchExpression{
161
+ Expr: `method('DELETE', 'PUT', 'POST')`,
162
+ },
163
+ urlTarget: "https://foo.example.com",
164
+ httpMethod: "PUT",
165
+ wantResult: true,
166
+ },
167
+ {
168
+ name: "method error not enough arguments (MatchMethod)",
169
+ expression: &MatchExpression{
170
+ Expr: `method()`,
171
+ },
172
+ urlTarget: "https://foo.example.com",
173
+ httpMethod: "PUT",
174
+ wantErr: true,
175
+ },
176
+ {
177
+ name: "path matches substring (MatchPath)",
178
+ expression: &MatchExpression{
179
+ Expr: `path('*substring*')`,
180
+ },
181
+ urlTarget: "https://example.com/foo/substring/bar.txt",
182
+ wantResult: true,
183
+ },
184
+ {
185
+ name: "path does not match (MatchPath)",
186
+ expression: &MatchExpression{
187
+ Expr: `path('/foo')`,
188
+ },
189
+ urlTarget: "https://example.com/foo/bar",
190
+ wantResult: false,
191
+ },
192
+ {
193
+ name: "path matches end url fragment (MatchPath)",
194
+ expression: &MatchExpression{
195
+ Expr: `path('/foo')`,
196
+ },
197
+ urlTarget: "https://example.com/FOO",
198
+ wantResult: true,
199
+ },
200
+ {
201
+ name: "path matches end fragment with substring prefix (MatchPath)",
202
+ expression: &MatchExpression{
203
+ Expr: `path('/foo*')`,
204
+ },
205
+ urlTarget: "https://example.com/FOOOOO",
206
+ wantResult: true,
207
+ },
208
+ {
209
+ name: "path matches one of multiple (MatchPath)",
210
+ expression: &MatchExpression{
211
+ Expr: `path('/foo', '/foo/*', '/bar', '/bar/*', '/baz', '/baz*')`,
212
+ },
213
+ urlTarget: "https://example.com/foo",
214
+ wantResult: true,
215
+ },
216
+ {
217
+ name: "path_regexp with empty regex matches empty path (MatchPathRE)",
218
+ expression: &MatchExpression{
219
+ Expr: `path_regexp('')`,
220
+ },
221
+ urlTarget: "https://example.com/",
222
+ wantResult: true,
223
+ },
224
+ {
225
+ name: "path_regexp with slash regex matches empty path (MatchPathRE)",
226
+ expression: &MatchExpression{
227
+ Expr: `path_regexp('/')`,
228
+ },
229
+ urlTarget: "https://example.com/",
230
+ wantResult: true,
231
+ },
232
+ {
233
+ name: "path_regexp matches end url fragment (MatchPathRE)",
234
+ expression: &MatchExpression{
235
+ Expr: `path_regexp('^/foo')`,
236
+ },
237
+ urlTarget: "https://example.com/foo/",
238
+ wantResult: true,
239
+ },
240
+ {
241
+ name: "path_regexp does not match fragment at end (MatchPathRE)",
242
+ expression: &MatchExpression{
243
+ Expr: `path_regexp('bar_at_start', '^/bar')`,
244
+ },
245
+ urlTarget: "https://example.com/foo/bar",
246
+ wantResult: false,
247
+ },
248
+ {
249
+ name: "protocol matches (MatchProtocol)",
250
+ expression: &MatchExpression{
251
+ Expr: `protocol('HTTPs')`,
252
+ },
253
+ urlTarget: "https://example.com",
254
+ wantResult: true,
255
+ },
256
+ {
257
+ name: "protocol does not match (MatchProtocol)",
258
+ expression: &MatchExpression{
259
+ Expr: `protocol('grpc')`,
260
+ },
261
+ urlTarget: "https://example.com",
262
+ wantResult: false,
263
+ },
264
+ {
265
+ name: "protocol invocation error no args (MatchProtocol)",
266
+ expression: &MatchExpression{
267
+ Expr: `protocol()`,
268
+ },
269
+ urlTarget: "https://example.com",
270
+ wantErr: true,
271
+ },
272
+ {
273
+ name: "protocol invocation error too many args (MatchProtocol)",
274
+ expression: &MatchExpression{
275
+ Expr: `protocol('grpc', 'https')`,
276
+ },
277
+ urlTarget: "https://example.com",
278
+ wantErr: true,
279
+ },
280
+ {
281
+ name: "protocol invocation error wrong arg type (MatchProtocol)",
282
+ expression: &MatchExpression{
283
+ Expr: `protocol(true)`,
284
+ },
285
+ urlTarget: "https://example.com",
286
+ wantErr: true,
287
+ },
288
+ {
289
+ name: "query does not match against a specific value (MatchQuery)",
290
+ expression: &MatchExpression{
291
+ Expr: `query({"debug": "1"})`,
292
+ },
293
+ urlTarget: "https://example.com/foo",
294
+ wantResult: false,
295
+ },
296
+ {
297
+ name: "query matches against a specific value (MatchQuery)",
298
+ expression: &MatchExpression{
299
+ Expr: `query({"debug": "1"})`,
300
+ },
301
+ urlTarget: "https://example.com/foo/?debug=1",
302
+ wantResult: true,
303
+ },
304
+ {
305
+ name: "query matches against multiple values (MatchQuery)",
306
+ expression: &MatchExpression{
307
+ Expr: `query({"debug": ["0", "1", {http.request.uri.query.debug}+"1"]})`,
308
+ },
309
+ urlTarget: "https://example.com/foo/?debug=1",
310
+ wantResult: true,
311
+ },
312
+ {
313
+ name: "query matches against a wildcard (MatchQuery)",
314
+ expression: &MatchExpression{
315
+ Expr: `query({"debug": ["*"]})`,
316
+ },
317
+ urlTarget: "https://example.com/foo/?debug=something",
318
+ wantResult: true,
319
+ },
320
+ {
321
+ name: "query matches against a placeholder value (MatchQuery)",
322
+ expression: &MatchExpression{
323
+ Expr: `query({"debug": {http.request.uri.query.debug}})`,
324
+ },
325
+ urlTarget: "https://example.com/foo/?debug=1",
326
+ wantResult: true,
327
+ },
328
+ {
329
+ name: "query error bad map key type (MatchQuery)",
330
+ expression: &MatchExpression{
331
+ Expr: `query({1: "1"})`,
332
+ },
333
+ urlTarget: "https://example.com/foo",
334
+ wantErr: true,
335
+ },
336
+ {
337
+ name: "query error typed struct instead of map (MatchQuery)",
338
+ expression: &MatchExpression{
339
+ Expr: `query(Message{field: "1"})`,
340
+ },
341
+ urlTarget: "https://example.com/foo",
342
+ wantErr: true,
343
+ },
344
+ {
345
+ name: "query error bad map value type (MatchQuery)",
346
+ expression: &MatchExpression{
347
+ Expr: `query({"debug": 1})`,
348
+ },
349
+ urlTarget: "https://example.com/foo/?debug=1",
350
+ wantErr: true,
351
+ },
352
+ {
353
+ name: "query error no args (MatchQuery)",
354
+ expression: &MatchExpression{
355
+ Expr: `query()`,
356
+ },
357
+ urlTarget: "https://example.com/foo/?debug=1",
358
+ wantErr: true,
359
+ },
360
+ {
361
+ name: "remote_ip error no args (MatchRemoteIP)",
362
+ expression: &MatchExpression{
363
+ Expr: `remote_ip()`,
364
+ },
365
+ urlTarget: "https://example.com/foo",
366
+ wantErr: true,
367
+ },
368
+ {
369
+ name: "remote_ip single IP match (MatchRemoteIP)",
370
+ expression: &MatchExpression{
371
+ Expr: `remote_ip('192.0.2.1')`,
372
+ },
373
+ urlTarget: "https://example.com/foo",
374
+ wantResult: true,
375
+ },
376
+ {
377
+ name: "remote_ip forwarded (MatchRemoteIP)",
378
+ expression: &MatchExpression{
379
+ Expr: `remote_ip('forwarded', '192.0.2.1')`,
380
+ },
381
+ urlTarget: "https://example.com/foo",
382
+ wantResult: true,
383
+ },
384
+ {
385
+ name: "remote_ip forwarded not first (MatchRemoteIP)",
386
+ expression: &MatchExpression{
387
+ Expr: `remote_ip('192.0.2.1', 'forwarded')`,
388
+ },
389
+ urlTarget: "https://example.com/foo",
390
+ wantErr: true,
391
+ },
392
}
90
- for _, tt := range tests {
91
- t.Run(tt.name, func(t *testing.T) {
92
- if err := tt.expression.Provision(caddy.Context{}); (err != nil) != tt.wantErr {
93
- t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tt.wantErr)
393
+)
394
+
395
+func TestMatchExpressionMatch(t *testing.T) {
396
+ for _, tst := range matcherTests {
397
+ tc := tst
398
+ t.Run(tc.name, func(t *testing.T) {
399
+ err := tc.expression.Provision(caddy.Context{})
400
+ if err != nil {
401
+ if !tc.wantErr {
402
+ t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tc.wantErr)
403
+ }
404
+ return
405
}
406
96
- req := httptest.NewRequest("GET", "https://example.com/foo", nil)
407
+ req := httptest.NewRequest(tc.httpMethod, tc.urlTarget, nil)
408
+ if tc.httpHeader != nil {
409
+ req.Header = *tc.httpHeader
410
+ }
411
repl := caddy.NewReplacer()
412
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
413
req = req.WithContext(ctx)
414
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
415
102
- if tt.clientCertificate != nil {
416
+ if tc.clientCertificate != nil {
417
block, _ := pem.Decode(clientCert)
418
if block == nil {
419
t.Fatalf("failed to decode PEM certificate")
@@ -115,10 +429,78 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
429
}
430
}
431
118
- if tt.expression.Match(req) != tt.wantResult {
119
- t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tt.wantResult, tt.expression.Expr)
432
+ if tc.expression.Match(req) != tc.wantResult {
433
+ t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tc.wantResult, tc.expression.Expr)
434
+ }
435
+ })
436
+ }
437
+}
438
+
439
+func BenchmarkMatchExpressionMatch(b *testing.B) {
440
+ for _, tst := range matcherTests {
441
+ tc := tst
442
+ if tc.wantErr {
443
+ continue
444
+ }
445
+ b.Run(tst.name, func(b *testing.B) {
446
+ tc.expression.Provision(caddy.Context{})
447
+ req := httptest.NewRequest(tc.httpMethod, tc.urlTarget, nil)
448
+ if tc.httpHeader != nil {
449
+ req.Header = *tc.httpHeader
450
}
451
+ repl := caddy.NewReplacer()
452
+ ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
453
+ req = req.WithContext(ctx)
454
+ addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
455
+ if tc.clientCertificate != nil {
456
+ block, _ := pem.Decode(clientCert)
457
+ if block == nil {
458
+ b.Fatalf("failed to decode PEM certificate")
459
+ }
460
461
+ cert, err := x509.ParseCertificate(block.Bytes)
462
+ if err != nil {
463
+ b.Fatalf("failed to decode PEM certificate: %v", err)
464
+ }
465
+
466
+ req.TLS = &tls.ConnectionState{
467
+ PeerCertificates: []*x509.Certificate{cert},
468
+ }
469
+ }
470
+ b.ResetTimer()
471
+ for i := 0; i < b.N; i++ {
472
+ tc.expression.Match(req)
473
+ }
474
+ })
475
+ }
476
+}
477
+
478
+func TestMatchExpressionProvision(t *testing.T) {
479
+ tests := []struct {
480
+ name string
481
+ expression *MatchExpression
482
+ wantErr bool
483
+ }{
484
+ {
485
+ name: "boolean matches succeed",
486
+ expression: &MatchExpression{
487
+ Expr: "{http.request.uri.query} != ''",
488
+ },
489
+ wantErr: false,
490
+ },
491
+ {
492
+ name: "reject expressions with non-boolean results",
493
+ expression: &MatchExpression{
494
+ Expr: "{http.request.uri.query}",
495
+ },
496
+ wantErr: true,
497
+ },
498
+ }
499
+ for _, tt := range tests {
500
+ t.Run(tt.name, func(t *testing.T) {
501
+ if err := tt.expression.Provision(caddy.Context{}); (err != nil) != tt.wantErr {
502
+ t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tt.wantErr)
503
+ }
504
})
505
}
506
}
modules/caddyhttp/fileserver/matcher.go
+216
-2
@@ -26,6 +26,14 @@ import (
26
"github.com/caddyserver/caddy/v2"
27
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
28
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
29
+ "github.com/google/cel-go/cel"
30
+ "github.com/google/cel-go/checker/decls"
31
+ "github.com/google/cel-go/common"
32
+ "github.com/google/cel-go/common/operators"
33
+ "github.com/google/cel-go/common/types/ref"
34
+ "github.com/google/cel-go/interpreter/functions"
35
+ "github.com/google/cel-go/parser"
36
+ exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
37
)
38
39
func init() {
@@ -139,6 +147,110 @@ func (m *MatchFile) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
147
return nil
148
}
149
150
+// CELLibrary produces options that expose this matcher for use in CEL
151
+// expression matchers.
152
+//
153
+// Example:
154
+// expression file({'root': '/srv', 'try_files': [{http.request.uri.path}, '/index.php'], 'try_policy': 'first_exist', 'split_path': ['.php']})
155
+func (MatchFile) CELLibrary(ctx caddy.Context) (cel.Library, error) {
156
+ requestType := decls.NewObjectType("http.Request")
157
+ envOptions := []cel.EnvOption{
158
+ cel.Macros(parser.NewGlobalVarArgMacro("file", celFileMatcherMacroExpander())),
159
+ cel.Declarations(
160
+ decls.NewFunction("file",
161
+ decls.NewOverload("file_request_map",
162
+ []*exprpb.Type{requestType, caddyhttp.CelTypeJson},
163
+ decls.Bool,
164
+ ),
165
+ ),
166
+ ),
167
+ }
168
+
169
+ matcherFactory := func(data ref.Val) (caddyhttp.RequestMatcher, error) {
170
+ values, err := caddyhttp.CELValueToMapStrList(data)
171
+ if err != nil {
172
+ return nil, err
173
+ }
174
+
175
+ var root string
176
+ if len(values["root"]) > 0 {
177
+ root = values["root"][0]
178
+ }
179
+
180
+ var try_policy string
181
+ if len(values["try_policy"]) > 0 {
182
+ root = values["try_policy"][0]
183
+ }
184
+
185
+ m := MatchFile{
186
+ Root: root,
187
+ TryFiles: values["try_files"],
188
+ TryPolicy: try_policy,
189
+ SplitPath: values["split_path"],
190
+ }
191
+
192
+ err = m.Provision(ctx)
193
+ return m, err
194
+ }
195
+
196
+ programOptions := []cel.ProgramOption{
197
+ cel.CustomDecorator(caddyhttp.CELMatcherDecorator("file_request_map", matcherFactory)),
198
+ cel.Functions(
199
+ &functions.Overload{
200
+ Operator: "file_request_map",
201
+ Binary: caddyhttp.CELMatcherRuntimeFunction("file_request_map", matcherFactory),
202
+ },
203
+ ),
204
+ }
205
+
206
+ return caddyhttp.NewMatcherCELLibrary(envOptions, programOptions), nil
207
+}
208
+
209
+func celFileMatcherMacroExpander() parser.MacroExpander {
210
+ return func(eh parser.ExprHelper, target *exprpb.Expr, args []*exprpb.Expr) (*exprpb.Expr, *common.Error) {
211
+ if len(args) == 0 {
212
+ return nil, &common.Error{
213
+ Message: "matcher requires at least one argument",
214
+ }
215
+ }
216
+ if len(args) == 1 {
217
+ arg := args[0]
218
+ if isCELStringLiteral(arg) || isCELCaddyPlaceholderCall(arg) {
219
+ return eh.GlobalCall("file",
220
+ eh.Ident("request"),
221
+ eh.NewMap(
222
+ eh.NewMapEntry(eh.LiteralString("try_files"), eh.NewList(arg)),
223
+ ),
224
+ ), nil
225
+ }
226
+ if isCELTryFilesLiteral(arg) {
227
+ return eh.GlobalCall("file", eh.Ident("request"), arg), nil
228
+ }
229
+ return nil, &common.Error{
230
+ Location: eh.OffsetLocation(arg.GetId()),
231
+ Message: "matcher requires either a map or string literal argument",
232
+ }
233
+ }
234
+
235
+ for _, arg := range args {
236
+ if !(isCELStringLiteral(arg) || isCELCaddyPlaceholderCall(arg)) {
237
+ return nil, &common.Error{
238
+ Location: eh.OffsetLocation(arg.GetId()),
239
+ Message: "matcher only supports repeated string literal arguments",
240
+ }
241
+ }
242
+ }
243
+ return eh.GlobalCall("file",
244
+ eh.Ident("request"),
245
+ eh.NewMap(
246
+ eh.NewMapEntry(
247
+ eh.LiteralString("try_files"), eh.NewList(args...),
248
+ ),
249
+ ),
250
+ ), nil
251
+ }
252
+}
253
+
254
// Provision sets up m's defaults.
255
func (m *MatchFile) Provision(_ caddy.Context) error {
256
if m.Root == "" {
@@ -359,6 +471,107 @@ func indexFold(haystack, needle string) int {
471
return -1
472
}
473
474
+// isCELMapLiteral returns whether the expression resolves to a map literal containing
475
+// only string keys with or a placeholder call.
476
+func isCELTryFilesLiteral(e *exprpb.Expr) bool {
477
+ switch e.GetExprKind().(type) {
478
+ case *exprpb.Expr_StructExpr:
479
+ structExpr := e.GetStructExpr()
480
+ if structExpr.GetMessageName() != "" {
481
+ return false
482
+ }
483
+ for _, entry := range structExpr.GetEntries() {
484
+ mapKey := entry.GetMapKey()
485
+ mapVal := entry.GetValue()
486
+ if !isCELStringLiteral(mapKey) {
487
+ return false
488
+ }
489
+ mapKeyStr := mapKey.GetConstExpr().GetStringValue()
490
+ if mapKeyStr == "try_files" || mapKeyStr == "split_path" {
491
+ if !isCELStringListLiteral(mapVal) {
492
+ return false
493
+ }
494
+ } else if mapKeyStr == "try_policy" || mapKeyStr == "root" {
495
+ if !(isCELStringExpr(mapVal)) {
496
+ return false
497
+ }
498
+ } else {
499
+ return false
500
+ }
501
+ }
502
+ return true
503
+ }
504
+ return false
505
+}
506
+
507
+// isCELStringExpr indicates whether the expression is a supported string expression
508
+func isCELStringExpr(e *exprpb.Expr) bool {
509
+ return isCELStringLiteral(e) || isCELCaddyPlaceholderCall(e) || isCELConcatCall(e)
510
+}
511
+
512
+// isCELStringLiteral returns whether the expression is a CEL string literal.
513
+func isCELStringLiteral(e *exprpb.Expr) bool {
514
+ switch e.GetExprKind().(type) {
515
+ case *exprpb.Expr_ConstExpr:
516
+ constant := e.GetConstExpr()
517
+ switch constant.GetConstantKind().(type) {
518
+ case *exprpb.Constant_StringValue:
519
+ return true
520
+ }
521
+ }
522
+ return false
523
+}
524
+
525
+// isCELCaddyPlaceholderCall returns whether the expression is a caddy placeholder call.
526
+func isCELCaddyPlaceholderCall(e *exprpb.Expr) bool {
527
+ switch e.GetExprKind().(type) {
528
+ case *exprpb.Expr_CallExpr:
529
+ call := e.GetCallExpr()
530
+ if call.GetFunction() == "caddyPlaceholder" {
531
+ return true
532
+ }
533
+ }
534
+ return false
535
+}
536
+
537
+// isCELConcatCall tests whether the expression is a concat function (+) with string, placeholder, or
538
+// other concat call arguments.
539
+func isCELConcatCall(e *exprpb.Expr) bool {
540
+ switch e.GetExprKind().(type) {
541
+ case *exprpb.Expr_CallExpr:
542
+ call := e.GetCallExpr()
543
+ if call.GetTarget() != nil {
544
+ return false
545
+ }
546
+ if call.GetFunction() != operators.Add {
547
+ return false
548
+ }
549
+ for _, arg := range call.GetArgs() {
550
+ if !isCELStringExpr(arg) {
551
+ return false
552
+ }
553
+ }
554
+ return true
555
+ }
556
+ return false
557
+}
558
+
559
+// isCELStringListLiteral returns whether the expression resolves to a list literal
560
+// containing only string constants or a placeholder call.
561
+func isCELStringListLiteral(e *exprpb.Expr) bool {
562
+ switch e.GetExprKind().(type) {
563
+ case *exprpb.Expr_ListExpr:
564
+ list := e.GetListExpr()
565
+ for _, elem := range list.GetElements() {
566
+ if !isCELStringExpr(elem) {
567
+ return false
568
+ }
569
+ }
570
+ return true
571
+ }
572
+ return false
573
+}
574
+
575
const (
576
tryPolicyFirstExist = "first_exist"
577
tryPolicyLargestSize = "largest_size"
@@ -368,6 +581,7 @@ const (
581
582
// Interface guards
583
var (
371
- _ caddy.Validator = (*MatchFile)(nil)
372
- _ caddyhttp.RequestMatcher = (*MatchFile)(nil)
584
+ _ caddy.Validator = (*MatchFile)(nil)
585
+ _ caddyhttp.RequestMatcher = (*MatchFile)(nil)
586
+ _ caddyhttp.CELLibraryProducer = (*MatchFile)(nil)
587
)
modules/caddyhttp/fileserver/matcher_test.go
+109
@@ -15,12 +15,15 @@
15
package fileserver
16
17
import (
18
+ "context"
19
"net/http"
20
+ "net/http/httptest"
21
"net/url"
22
"os"
23
"runtime"
24
"testing"
25
26
+ "github.com/caddyserver/caddy/v2"
27
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
28
)
29
@@ -259,3 +262,109 @@ func TestFirstSplit(t *testing.T) {
262
t.Errorf("Expected remainder %s but got %s", expectedRemainder, remainder)
263
}
264
}
265
+
266
+var (
267
+ expressionTests = []struct {
268
+ name string
269
+ expression *caddyhttp.MatchExpression
270
+ urlTarget string
271
+ httpMethod string
272
+ httpHeader *http.Header
273
+ wantErr bool
274
+ wantResult bool
275
+ clientCertificate []byte
276
+ }{
277
+ {
278
+ name: "file error no args (MatchFile)",
279
+ expression: &caddyhttp.MatchExpression{
280
+ Expr: `file()`,
281
+ },
282
+ urlTarget: "https://example.com/foo",
283
+ wantErr: true,
284
+ },
285
+ {
286
+ name: "file error bad try files (MatchFile)",
287
+ expression: &caddyhttp.MatchExpression{
288
+ Expr: `file({"try_file": ["bad_arg"]})`,
289
+ },
290
+ urlTarget: "https://example.com/foo",
291
+ wantErr: true,
292
+ },
293
+ {
294
+ name: "file match short pattern index.php (MatchFile)",
295
+ expression: &caddyhttp.MatchExpression{
296
+ Expr: `file("index.php")`,
297
+ },
298
+ urlTarget: "https://example.com/foo",
299
+ wantResult: true,
300
+ },
301
+ {
302
+ name: "file match short pattern foo.txt (MatchFile)",
303
+ expression: &caddyhttp.MatchExpression{
304
+ Expr: `file({http.request.uri.path})`,
305
+ },
306
+ urlTarget: "https://example.com/foo.txt",
307
+ wantResult: true,
308
+ },
309
+ {
310
+ name: "file match index.php (MatchFile)",
311
+ expression: &caddyhttp.MatchExpression{
312
+ Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}, "/index.php"]})`,
313
+ },
314
+ urlTarget: "https://example.com/foo",
315
+ wantResult: true,
316
+ },
317
+ {
318
+ name: "file match long pattern foo.txt (MatchFile)",
319
+ expression: &caddyhttp.MatchExpression{
320
+ Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}]})`,
321
+ },
322
+ urlTarget: "https://example.com/foo.txt",
323
+ wantResult: true,
324
+ },
325
+ {
326
+ name: "file match long pattern foo.txt with concatenation (MatchFile)",
327
+ expression: &caddyhttp.MatchExpression{
328
+ Expr: `file({"root": ".", "try_files": ["./testdata" + {http.request.uri.path}]})`,
329
+ },
330
+ urlTarget: "https://example.com/foo.txt",
331
+ wantResult: true,
332
+ },
333
+ {
334
+ name: "file not match long pattern (MatchFile)",
335
+ expression: &caddyhttp.MatchExpression{
336
+ Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}]})`,
337
+ },
338
+ urlTarget: "https://example.com/nopenope.txt",
339
+ wantResult: false,
340
+ },
341
+ }
342
+)
343
+
344
+func TestMatchExpressionMatch(t *testing.T) {
345
+ for _, tst := range expressionTests {
346
+ tc := tst
347
+ t.Run(tc.name, func(t *testing.T) {
348
+ err := tc.expression.Provision(caddy.Context{})
349
+ if err != nil {
350
+ if !tc.wantErr {
351
+ t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tc.wantErr)
352
+ }
353
+ return
354
+ }
355
+
356
+ req := httptest.NewRequest(tc.httpMethod, tc.urlTarget, nil)
357
+ if tc.httpHeader != nil {
358
+ req.Header = *tc.httpHeader
359
+ }
360
+ repl := caddyhttp.NewTestReplacer(req)
361
+ repl.Set("http.vars.root", "./testdata")
362
+ ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
363
+ req = req.WithContext(ctx)
364
+
365
+ if tc.expression.Match(req) != tc.wantResult {
366
+ t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tc.wantResult, tc.expression.Expr)
367
+ }
368
+ })
369
+ }
370
+}
modules/caddyhttp/matchers.go
+289
-1
@@ -16,6 +16,7 @@ package caddyhttp
16
17
import (
18
"encoding/json"
19
+ "errors"
20
"fmt"
21
"net"
22
"net/http"
@@ -23,6 +24,7 @@ import (
24
"net/url"
25
"path"
26
"path/filepath"
27
+ "reflect"
28
"regexp"
29
"sort"
30
"strconv"
@@ -30,7 +32,12 @@ import (
32
33
"github.com/caddyserver/caddy/v2"
34
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
35
+ "github.com/google/cel-go/cel"
36
+ "github.com/google/cel-go/checker/decls"
37
+ "github.com/google/cel-go/common/types"
38
+ "github.com/google/cel-go/common/types/ref"
39
"go.uber.org/zap"
40
+ exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
41
)
42
43
type (
@@ -291,6 +298,29 @@ outer:
298
return false
299
}
300
301
+// CELLibrary produces options that expose this matcher for use in CEL
302
+// expression matchers.
303
+//
304
+// Example:
305
+// expression host('localhost')
306
+func (MatchHost) CELLibrary(ctx caddy.Context) (cel.Library, error) {
307
+ return CELMatcherImpl(
308
+ "host",
309
+ "host_match_request_list",
310
+ []*exprpb.Type{CelTypeListString},
311
+ func(data ref.Val) (RequestMatcher, error) {
312
+ refStringList := reflect.TypeOf([]string{})
313
+ strList, err := data.ConvertToNative(refStringList)
314
+ if err != nil {
315
+ return nil, err
316
+ }
317
+ matcher := MatchHost(strList.([]string))
318
+ err = matcher.Provision(ctx)
319
+ return matcher, err
320
+ },
321
+ )
322
+}
323
+
324
// fuzzy returns true if the given hostname h is not a specific
325
// hostname, e.g. has placeholders or wildcards.
326
func (MatchHost) fuzzy(h string) bool { return strings.ContainsAny(h, "{*") }
@@ -396,6 +426,33 @@ func (m MatchPath) Match(r *http.Request) bool {
426
return false
427
}
428
429
+// CELLibrary produces options that expose this matcher for use in CEL
430
+// expression matchers.
431
+//
432
+// Example:
433
+// expression path('*substring*', '*suffix')
434
+func (MatchPath) CELLibrary(ctx caddy.Context) (cel.Library, error) {
435
+ return CELMatcherImpl(
436
+ // name of the macro, this is the function name that users see when writing expressions.
437
+ "path",
438
+ // name of the function that the macro will be rewritten to call.
439
+ "path_match_request_list",
440
+ // internal data type of the MatchPath value.
441
+ []*exprpb.Type{CelTypeListString},
442
+ // function to convert a constant list of strings to a MatchPath instance.
443
+ func(data ref.Val) (RequestMatcher, error) {
444
+ refStringList := reflect.TypeOf([]string{})
445
+ strList, err := data.ConvertToNative(refStringList)
446
+ if err != nil {
447
+ return nil, err
448
+ }
449
+ matcher := MatchPath(strList.([]string))
450
+ err = matcher.Provision(ctx)
451
+ return matcher, err
452
+ },
453
+ )
454
+}
455
+
456
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
457
func (m *MatchPath) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
458
for d.Next() {
@@ -440,6 +497,50 @@ func (m MatchPathRE) Match(r *http.Request) bool {
497
return m.MatchRegexp.Match(cleanedPath, repl)
498
}
499
500
+// CELLibrary produces options that expose this matcher for use in CEL
501
+// expression matchers.
502
+//
503
+// Example:
504
+// expression path_regexp('^/bar')
505
+func (MatchPathRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
506
+ unnamedPattern, err := CELMatcherImpl(
507
+ "path_regexp",
508
+ "path_regexp_request_string",
509
+ []*exprpb.Type{decls.String},
510
+ func(data ref.Val) (RequestMatcher, error) {
511
+ pattern := data.(types.String)
512
+ matcher := MatchPathRE{MatchRegexp{Pattern: string(pattern)}}
513
+ err := matcher.Provision(ctx)
514
+ return matcher, err
515
+ },
516
+ )
517
+ if err != nil {
518
+ return nil, err
519
+ }
520
+ namedPattern, err := CELMatcherImpl(
521
+ "path_regexp",
522
+ "path_regexp_request_string_string",
523
+ []*exprpb.Type{decls.String, decls.String},
524
+ func(data ref.Val) (RequestMatcher, error) {
525
+ refStringList := reflect.TypeOf([]string{})
526
+ params, err := data.ConvertToNative(refStringList)
527
+ if err != nil {
528
+ return nil, err
529
+ }
530
+ strParams := params.([]string)
531
+ matcher := MatchPathRE{MatchRegexp{Name: strParams[0], Pattern: strParams[1]}}
532
+ err = matcher.Provision(ctx)
533
+ return matcher, err
534
+ },
535
+ )
536
+ if err != nil {
537
+ return nil, err
538
+ }
539
+ envOpts := append(unnamedPattern.CompileOptions(), namedPattern.CompileOptions()...)
540
+ prgOpts := append(unnamedPattern.ProgramOptions(), namedPattern.ProgramOptions()...)
541
+ return NewMatcherCELLibrary(envOpts, prgOpts), nil
542
+}
543
+
544
// CaddyModule returns the Caddy module information.
545
func (MatchMethod) CaddyModule() caddy.ModuleInfo {
546
return caddy.ModuleInfo{
@@ -469,6 +570,27 @@ func (m MatchMethod) Match(r *http.Request) bool {
570
return false
571
}
572
573
+// CELLibrary produces options that expose this matcher for use in CEL
574
+// expression matchers.
575
+//
576
+// Example:
577
+// expression method('PUT', 'POST')
578
+func (MatchMethod) CELLibrary(_ caddy.Context) (cel.Library, error) {
579
+ return CELMatcherImpl(
580
+ "method",
581
+ "method_request_list",
582
+ []*exprpb.Type{CelTypeListString},
583
+ func(data ref.Val) (RequestMatcher, error) {
584
+ refStringList := reflect.TypeOf([]string{})
585
+ strList, err := data.ConvertToNative(refStringList)
586
+ if err != nil {
587
+ return nil, err
588
+ }
589
+ return MatchMethod(strList.([]string)), nil
590
+ },
591
+ )
592
+}
593
+
594
// CaddyModule returns the Caddy module information.
595
func (MatchQuery) CaddyModule() caddy.ModuleInfo {
596
return caddy.ModuleInfo{
@@ -518,6 +640,26 @@ func (m MatchQuery) Match(r *http.Request) bool {
640
return len(m) == 0 && len(r.URL.Query()) == 0
641
}
642
643
+// CELLibrary produces options that expose this matcher for use in CEL
644
+// expression matchers.
645
+//
646
+// Example:
647
+// expression query({'sort': 'asc'}) || query({'foo': ['*bar*', 'baz']})
648
+func (MatchQuery) CELLibrary(_ caddy.Context) (cel.Library, error) {
649
+ return CELMatcherImpl(
650
+ "query",
651
+ "query_matcher_request_map",
652
+ []*exprpb.Type{CelTypeJson},
653
+ func(data ref.Val) (RequestMatcher, error) {
654
+ mapStrListStr, err := CELValueToMapStrList(data)
655
+ if err != nil {
656
+ return nil, err
657
+ }
658
+ return MatchQuery(url.Values(mapStrListStr)), nil
659
+ },
660
+ )
661
+}
662
+
663
// CaddyModule returns the Caddy module information.
664
func (MatchHeader) CaddyModule() caddy.ModuleInfo {
665
return caddy.ModuleInfo{
@@ -573,6 +715,27 @@ func (m MatchHeader) Match(r *http.Request) bool {
715
return matchHeaders(r.Header, http.Header(m), r.Host, repl)
716
}
717
718
+// CELLibrary produces options that expose this matcher for use in CEL
719
+// expression matchers.
720
+//
721
+// Example:
722
+// expression header({'content-type': 'image/png'})
723
+// expression header({'foo': ['bar', 'baz']}) // match bar or baz
724
+func (MatchHeader) CELLibrary(_ caddy.Context) (cel.Library, error) {
725
+ return CELMatcherImpl(
726
+ "header",
727
+ "header_matcher_request_map",
728
+ []*exprpb.Type{CelTypeJson},
729
+ func(data ref.Val) (RequestMatcher, error) {
730
+ mapStrListStr, err := CELValueToMapStrList(data)
731
+ if err != nil {
732
+ return nil, err
733
+ }
734
+ return MatchHeader(http.Header(mapStrListStr)), nil
735
+ },
736
+ )
737
+}
738
+
739
// getHeaderFieldVals returns the field values for the given fieldName from input.
740
// The host parameter should be obtained from the http.Request.Host field since
741
// net/http removes it from the header map.
@@ -710,6 +873,57 @@ func (m MatchHeaderRE) Validate() error {
873
return nil
874
}
875
876
+// CELLibrary produces options that expose this matcher for use in CEL
877
+// expression matchers.
878
+//
879
+// Example:
880
+// expression header_regexp('foo', 'Field', 'fo+')
881
+func (MatchHeaderRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
882
+ unnamedPattern, err := CELMatcherImpl(
883
+ "header_regexp",
884
+ "header_regexp_request_string_string",
885
+ []*exprpb.Type{decls.String, decls.String},
886
+ func(data ref.Val) (RequestMatcher, error) {
887
+ refStringList := reflect.TypeOf([]string{})
888
+ params, err := data.ConvertToNative(refStringList)
889
+ if err != nil {
890
+ return nil, err
891
+ }
892
+ strParams := params.([]string)
893
+ matcher := MatchHeaderRE{}
894
+ matcher[strParams[0]] = &MatchRegexp{Pattern: strParams[1], Name: ""}
895
+ err = matcher.Provision(ctx)
896
+ return matcher, err
897
+ },
898
+ )
899
+ if err != nil {
900
+ return nil, err
901
+ }
902
+ namedPattern, err := CELMatcherImpl(
903
+ "header_regexp",
904
+ "header_regexp_request_string_string_string",
905
+ []*exprpb.Type{decls.String, decls.String, decls.String},
906
+ func(data ref.Val) (RequestMatcher, error) {
907
+ refStringList := reflect.TypeOf([]string{})
908
+ params, err := data.ConvertToNative(refStringList)
909
+ if err != nil {
910
+ return nil, err
911
+ }
912
+ strParams := params.([]string)
913
+ matcher := MatchHeaderRE{}
914
+ matcher[strParams[1]] = &MatchRegexp{Pattern: strParams[2], Name: strParams[0]}
915
+ err = matcher.Provision(ctx)
916
+ return matcher, err
917
+ },
918
+ )
919
+ if err != nil {
920
+ return nil, err
921
+ }
922
+ envOpts := append(unnamedPattern.CompileOptions(), namedPattern.CompileOptions()...)
923
+ prgOpts := append(unnamedPattern.ProgramOptions(), namedPattern.ProgramOptions()...)
924
+ return NewMatcherCELLibrary(envOpts, prgOpts), nil
925
+}
926
+
927
// CaddyModule returns the Caddy module information.
928
func (MatchProtocol) CaddyModule() caddy.ModuleInfo {
929
return caddy.ModuleInfo{
@@ -743,6 +957,26 @@ func (m *MatchProtocol) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
957
return nil
958
}
959
960
+// CELLibrary produces options that expose this matcher for use in CEL
961
+// expression matchers.
962
+//
963
+// Example:
964
+// expression protocol('https')
965
+func (MatchProtocol) CELLibrary(_ caddy.Context) (cel.Library, error) {
966
+ return CELMatcherImpl(
967
+ "protocol",
968
+ "protocol_request_string",
969
+ []*exprpb.Type{decls.String},
970
+ func(data ref.Val) (RequestMatcher, error) {
971
+ protocolStr, ok := data.(types.String)
972
+ if !ok {
973
+ return nil, errors.New("protocol argument was not a string")
974
+ }
975
+ return MatchProtocol(strings.ToLower(string(protocolStr))), nil
976
+ },
977
+ )
978
+}
979
+
980
// CaddyModule returns the Caddy module information.
981
func (MatchNot) CaddyModule() caddy.ModuleInfo {
982
return caddy.ModuleInfo{
@@ -887,6 +1121,46 @@ func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
1121
return nil
1122
}
1123
1124
+// CELLibrary produces options that expose this matcher for use in CEL
1125
+// expression matchers.
1126
+//
1127
+// Example:
1128
+// expression remote_ip('forwarded', '192.168.0.0/16', '172.16.0.0/12', '10.0.0.0/8')
1129
+func (MatchRemoteIP) CELLibrary(ctx caddy.Context) (cel.Library, error) {
1130
+ return CELMatcherImpl(
1131
+ // name of the macro, this is the function name that users see when writing expressions.
1132
+ "remote_ip",
1133
+ // name of the function that the macro will be rewritten to call.
1134
+ "remote_ip_match_request_list",
1135
+ // internal data type of the MatchPath value.
1136
+ []*exprpb.Type{CelTypeListString},
1137
+ // function to convert a constant list of strings to a MatchPath instance.
1138
+ func(data ref.Val) (RequestMatcher, error) {
1139
+ refStringList := reflect.TypeOf([]string{})
1140
+ strList, err := data.ConvertToNative(refStringList)
1141
+ if err != nil {
1142
+ return nil, err
1143
+ }
1144
+
1145
+ m := MatchRemoteIP{}
1146
+
1147
+ for _, input := range strList.([]string) {
1148
+ if input == "forwarded" {
1149
+ if len(m.Ranges) > 0 {
1150
+ return nil, errors.New("if used, 'forwarded' must be first argument")
1151
+ }
1152
+ m.Forwarded = true
1153
+ continue
1154
+ }
1155
+ m.Ranges = append(m.Ranges, input)
1156
+ }
1157
+
1158
+ err = m.Provision(ctx)
1159
+ return m, err
1160
+ },
1161
+ )
1162
+}
1163
+
1164
// Provision parses m's IP ranges, either from IP or CIDR expressions.
1165
func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
1166
m.logger = ctx.Logger(m)
@@ -1062,7 +1336,9 @@ func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
1336
return nil
1337
}
1338
1065
-var wordRE = regexp.MustCompile(`\w+`)
1339
+var (
1340
+ wordRE = regexp.MustCompile(`\w+`)
1341
+)
1342
1343
const regexpPlaceholderPrefix = "http.regexp"
1344
@@ -1103,6 +1379,18 @@ var (
1379
_ caddyfile.Unmarshaler = (*VarsMatcher)(nil)
1380
_ caddyfile.Unmarshaler = (*MatchVarsRE)(nil)
1381
1382
+ _ CELLibraryProducer = (*MatchHost)(nil)
1383
+ _ CELLibraryProducer = (*MatchPath)(nil)
1384
+ _ CELLibraryProducer = (*MatchPathRE)(nil)
1385
+ _ CELLibraryProducer = (*MatchMethod)(nil)
1386
+ _ CELLibraryProducer = (*MatchQuery)(nil)
1387
+ _ CELLibraryProducer = (*MatchHeader)(nil)
1388
+ _ CELLibraryProducer = (*MatchHeaderRE)(nil)
1389
+ _ CELLibraryProducer = (*MatchProtocol)(nil)
1390
+ _ CELLibraryProducer = (*MatchRemoteIP)(nil)
1391
+ // _ CELLibraryProducer = (*VarsMatcher)(nil)
1392
+ // _ CELLibraryProducer = (*MatchVarsRE)(nil)
1393
+
1394
_ json.Marshaler = (*MatchNot)(nil)
1395
_ json.Unmarshaler = (*MatchNot)(nil)
1396
)