mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-10-28 09:12:52 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			263 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			263 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2015 Matthew Holt and The Caddy Authors
 | |
| //
 | |
| // Licensed under the Apache License, Version 2.0 (the "License");
 | |
| // you may not use this file except in compliance with the License.
 | |
| // You may obtain a copy of the License at
 | |
| //
 | |
| //     http://www.apache.org/licenses/LICENSE-2.0
 | |
| //
 | |
| // Unless required by applicable law or agreed to in writing, software
 | |
| // distributed under the License is distributed on an "AS IS" BASIS,
 | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| // See the License for the specific language governing permissions and
 | |
| // limitations under the License.
 | |
| 
 | |
| package caddyhttp
 | |
| 
 | |
| import (
 | |
| 	"crypto/x509/pkix"
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"net/http"
 | |
| 	"reflect"
 | |
| 	"regexp"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	"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/ext"
 | |
| 	"github.com/google/cel-go/interpreter/functions"
 | |
| 	exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
 | |
| 	"google.golang.org/protobuf/proto"
 | |
| 	timestamp "google.golang.org/protobuf/types/known/timestamppb"
 | |
| )
 | |
| 
 | |
| 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. Please refer to
 | |
| // [the standard definitions of CEL functions and operators](https://github.com/google/cel-spec/blob/master/doc/langdef.md#standard-definitions).
 | |
| //
 | |
| // This matcher's JSON interface is actually a string, not a struct.
 | |
| // The generated docs are not correct because this type has custom
 | |
| // marshaling logic.
 | |
| //
 | |
| // 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
 | |
| 	ta           ref.TypeAdapter
 | |
| }
 | |
| 
 | |
| // 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)
 | |
| 
 | |
| 	// our type adapter expands CEL's standard type support
 | |
| 	m.ta = celTypeAdapter{}
 | |
| 
 | |
| 	// create the CEL environment
 | |
| 	env, err := cel.NewEnv(
 | |
| 		cel.Declarations(
 | |
| 			decls.NewVar("request", httpRequestObjectType),
 | |
| 			decls.NewFunction(placeholderFuncName,
 | |
| 				decls.NewOverload(placeholderFuncName+"_httpRequest_string",
 | |
| 					[]*exprpb.Type{httpRequestObjectType, decls.String},
 | |
| 					decls.Any)),
 | |
| 		),
 | |
| 		cel.CustomTypeAdapter(m.ta),
 | |
| 		ext.Strings(),
 | |
| 	)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("setting up CEL environment: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// parse and type-check the expression
 | |
| 	checked, issues := env.Compile(m.expandedExpr)
 | |
| 	if issues != nil && issues.Err() != nil {
 | |
| 		return fmt.Errorf("compiling CEL program: %s", issues.Err())
 | |
| 	}
 | |
| 
 | |
| 	// request matching is a boolean operation, so we don't really know
 | |
| 	// what to do if the expression returns a non-boolean type
 | |
| 	if !proto.Equal(checked.ResultType(), decls.Bool) {
 | |
| 		return fmt.Errorf("CEL request matcher expects return type of bool, not %s", checked.ResultType())
 | |
| 	}
 | |
| 
 | |
| 	// compile the "program"
 | |
| 	m.prg, err = env.Program(checked,
 | |
| 		cel.Functions(
 | |
| 			&functions.Overload{
 | |
| 				Operator: placeholderFuncName,
 | |
| 				Binary:   m.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
 | |
| }
 | |
| 
 | |
| // caddyPlaceholderFunc implements the custom CEL function that accesses the
 | |
| // Replacer on a request and gets values from it.
 | |
| func (m MatchExpression) 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 m.ta.NativeToValue(val)
 | |
| }
 | |
| 
 | |
| // 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 }
 | |
| 
 | |
| var pkixNameCELType = types.NewTypeValue("pkix.Name", traits.ReceiverType)
 | |
| 
 | |
| // celPkixName wraps an pkix.Name with
 | |
| // methods to satisfy the ref.Val interface.
 | |
| type celPkixName struct{ *pkix.Name }
 | |
| 
 | |
| func (pn celPkixName) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
 | |
| 	return pn.Name, nil
 | |
| }
 | |
| func (celPkixName) ConvertToType(typeVal ref.Type) ref.Val {
 | |
| 	panic("not implemented")
 | |
| }
 | |
| func (pn celPkixName) Equal(other ref.Val) ref.Val {
 | |
| 	if o, ok := other.Value().(string); ok {
 | |
| 		return types.Bool(pn.Name.String() == o)
 | |
| 	}
 | |
| 	return types.ValOrErr(other, "%v is not comparable type", other)
 | |
| }
 | |
| func (celPkixName) Type() ref.Type        { return pkixNameCELType }
 | |
| func (pn celPkixName) Value() interface{} { return pn }
 | |
| 
 | |
| // celTypeAdapter can adapt our custom types to a CEL value.
 | |
| type celTypeAdapter struct{}
 | |
| 
 | |
| func (celTypeAdapter) NativeToValue(value interface{}) ref.Val {
 | |
| 	switch v := value.(type) {
 | |
| 	case celHTTPRequest:
 | |
| 		return v
 | |
| 	case pkix.Name:
 | |
| 		return celPkixName{&v}
 | |
| 	case time.Time:
 | |
| 		// TODO: eliminate direct protobuf dependency, sigh -- just wrap stdlib time.Time instead...
 | |
| 		return types.Timestamp{Timestamp: ×tamp.Timestamp{Seconds: v.Unix(), Nanos: int32(v.Nanosecond())}}
 | |
| 	case error:
 | |
| 		types.NewErr(v.Error())
 | |
| 	}
 | |
| 	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"
 | |
| 
 | |
| // Interface guards
 | |
| var (
 | |
| 	_ caddy.Provisioner     = (*MatchExpression)(nil)
 | |
| 	_ RequestMatcher        = (*MatchExpression)(nil)
 | |
| 	_ caddyfile.Unmarshaler = (*MatchExpression)(nil)
 | |
| 	_ json.Marshaler        = (*MatchExpression)(nil)
 | |
| 	_ json.Unmarshaler      = (*MatchExpression)(nil)
 | |
| )
 |