diff options
-rw-r--r-- | go.mod | 2 | ||||
-rw-r--r-- | go.sum | 5 | ||||
-rw-r--r-- | modules/caddyhttp/celmatcher.go | 221 | ||||
-rw-r--r-- | modules/caddyhttp/matchers.go | 3 |
4 files changed, 228 insertions, 3 deletions
@@ -10,6 +10,7 @@ require ( github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac github.com/go-acme/lego/v3 v3.5.0 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e + github.com/google/cel-go v0.3.2 github.com/ilibs/json5 v1.0.1 github.com/jsternberg/zap-logfmt v1.2.0 github.com/klauspost/compress v1.10.3 @@ -29,6 +30,7 @@ require ( go.uber.org/zap v1.14.1 golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6 golang.org/x/net v0.0.0-20200301022130-244492dfa37a + google.golang.org/genproto v0.0.0-20200305110556-506484158171 gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/square/go-jose.v2 v2.4.1 // indirect gopkg.in/yaml.v2 v2.2.8 @@ -104,6 +104,8 @@ github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDa github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q= +github.com/antlr/antlr4 v0.0.0-20190819145818-b43a4c3a8015 h1:StuiJFxQUsxSCzcby6NFZRdEhPkXD5vxN7TZ4MD6T84= +github.com/antlr/antlr4 v0.0.0-20190819145818-b43a4c3a8015/go.mod h1:T7PbCXFs94rrTttyxjbyT5+/1V8T2TYDejxUfHJjw1Y= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/asaskevich/govalidator v0.0.0-20180315120708-ccb8e960c48f/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= @@ -298,6 +300,9 @@ github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039/go.mod h1:qOQCunE github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/cel-go v0.3.2 h1:72Lj/nrfpWSJkuXdeEGB/7jfdwVFtV8kPJSL2Mt9rog= +github.com/google/cel-go v0.3.2/go.mod h1:DoRSdzaJzNiP1lVuWhp/RjSnHLDQr/aNPlyqSBasBqA= +github.com/google/cel-spec v0.3.0/go.mod h1:MjQm800JAGhOZXI7vatnVpmIaFTR6L8FHcKk+piiKpI= github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/certificate-transparency-go v1.1.0/go.mod h1:i+Q7XY+ArBveOUT36jiHGfuSK1fHICIg6sUkRxPAbCs= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= diff --git a/modules/caddyhttp/celmatcher.go b/modules/caddyhttp/celmatcher.go new file mode 100644 index 0000000..fcedea4 --- /dev/null +++ b/modules/caddyhttp/celmatcher.go @@ -0,0 +1,221 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyhttp + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + "regexp" + "strings" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/google/cel-go/cel" + "github.com/google/cel-go/checker/decls" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/common/types/traits" + "github.com/google/cel-go/interpreter/functions" + exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" +) + +func init() { + caddy.RegisterModule(MatchExpression{}) +} + +// MatchExpression matches requests by evaluating a +// [CEL](https://github.com/google/cel-spec) expression. +// This enables complex logic to be expressed using a comfortable, +// familiar syntax. +// +// COMPATIBILITY NOTE: This module is still experimental and is not +// subject to Caddy's compatibility guarantee. +type MatchExpression struct { + // The CEL expression to evaluate. Any Caddy placeholders + // will be expanded and situated into proper CEL function + // calls before evaluating. + Expr string + + expandedExpr string + prg cel.Program +} + +// CaddyModule returns the Caddy module information. +func (MatchExpression) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "http.matchers.expression", + New: func() caddy.Module { return new(MatchExpression) }, + } +} + +// MarshalJSON marshals m's expression. +func (m MatchExpression) MarshalJSON() ([]byte, error) { + return json.Marshal(m.Expr) +} + +// UnmarshalJSON unmarshals m's expression. +func (m *MatchExpression) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &m.Expr) +} + +// Provision sets ups m. +func (m *MatchExpression) Provision(_ caddy.Context) error { + // replace placeholders with a function call - this is just some + // light (and possibly naïve) syntactic sugar + m.expandedExpr = placeholderRegexp.ReplaceAllString(m.Expr, placeholderExpansion) + + // create the CEL environment + env, err := cel.NewEnv( + cel.Declarations( + decls.NewIdent("request", httpRequestObjectType, nil), + decls.NewFunction(placeholderFuncName, + decls.NewOverload(placeholderFuncName+"_httpRequest_string", + []*exprpb.Type{httpRequestObjectType, decls.String}, + decls.String)), + ), + cel.CustomTypeAdapter(celHTTPRequestTypeAdapter{}), + ) + if err != nil { + return fmt.Errorf("setting up CEL environment: %v", err) + } + + // parse the expression + parsed, issues := env.Parse(m.expandedExpr) + if issues != nil && issues.Err() != nil { + return fmt.Errorf("parsing CEL program: %s", issues.Err()) + } + + // type-check it + checked, issues := env.Check(parsed) + if issues != nil && issues.Err() != nil { + return fmt.Errorf("type-checking CEL program: %s", issues.Err()) + } + + // compile the "program" + m.prg, err = env.Program(checked, + cel.Functions( + &functions.Overload{ + Operator: placeholderFuncName, + Binary: caddyPlaceholderFunc, + }, + ), + ) + + if err != nil { + return fmt.Errorf("compiling CEL program: %s", err) + } + return nil +} + +// Match returns true if r matches m. +func (m MatchExpression) Match(r *http.Request) bool { + out, _, _ := m.prg.Eval(map[string]interface{}{ + "request": celHTTPRequest{r}, + }) + if outBool, ok := out.Value().(bool); ok { + return outBool + } + return false + +} + +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (m *MatchExpression) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + m.Expr = strings.Join(d.RemainingArgs(), " ") + } + return nil +} + +// httpRequestCELType is the type representation of a native HTTP request. +var httpRequestCELType = types.NewTypeValue("http.Request", traits.ReceiverType) + +// cellHTTPRequest wraps an http.Request with +// methods to satisfy the ref.Val interface. +type celHTTPRequest struct { + *http.Request +} + +func (cr celHTTPRequest) ConvertToNative(typeDesc reflect.Type) (interface{}, error) { + return cr.Request, nil +} +func (celHTTPRequest) ConvertToType(typeVal ref.Type) ref.Val { + panic("not implemented") +} +func (cr celHTTPRequest) Equal(other ref.Val) ref.Val { + if o, ok := other.Value().(celHTTPRequest); ok { + return types.Bool(o.Request == cr.Request) + } + return types.ValOrErr(other, "%v is not comparable type", other) +} +func (celHTTPRequest) Type() ref.Type { return httpRequestCELType } +func (cr celHTTPRequest) Value() interface{} { return cr } + +// celHTTPRequestTypeAdapter can adapt a +// celHTTPRequest to a CEL value. +type celHTTPRequestTypeAdapter struct{} + +func (celHTTPRequestTypeAdapter) NativeToValue(value interface{}) ref.Val { + if celReq, ok := value.(celHTTPRequest); ok { + return celReq + } + return types.DefaultTypeAdapter.NativeToValue(value) +} + +// Variables used for replacing Caddy placeholders in CEL +// expressions with a proper CEL function call; this is +// just for syntactic sugar. +var ( + placeholderRegexp = regexp.MustCompile(`{([\w.-]+)}`) + placeholderExpansion = `caddyPlaceholder(request, "${1}")` +) + +var httpRequestObjectType = decls.NewObjectType("http.Request") + +// The name of the CEL function which accesses Replacer values. +const placeholderFuncName = "caddyPlaceholder" + +// caddyPlaceholderFunc implements the custom CEL function that +// accesses the Replacer on a request and gets values from it. +func caddyPlaceholderFunc(lhs, rhs ref.Val) ref.Val { + celReq, ok := lhs.(celHTTPRequest) + if !ok { + return types.NewErr( + "invalid request of type '%v' to "+placeholderFuncName+"(request, placeholderVarName)", + lhs.Type()) + } + phStr, ok := rhs.(types.String) + if !ok { + return types.NewErr( + "invalid placeholder variable name of type '%v' to "+placeholderFuncName+"(request, placeholderVarName)", + rhs.Type()) + } + + repl := celReq.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + val, _ := repl.Get(string(phStr)) + + return types.String(val) +} + +// Interface guards +var ( + _ caddy.Provisioner = (*MatchExpression)(nil) + _ RequestMatcher = (*MatchExpression)(nil) + _ caddyfile.Unmarshaler = (*MatchExpression)(nil) + _ json.Marshaler = (*MatchExpression)(nil) + _ json.Unmarshaler = (*MatchExpression)(nil) +) diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index 6bfb31e..043831f 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -107,9 +107,6 @@ type ( Matchers MatcherSet `json:"-"` } - - // MatchTable matches requests by values in the table. - MatchTable string // TODO: finish implementing ) func init() { |