@marcel91200 / caddy Mirror · read-only / commits / 10f85558
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 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 )