mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-11-03 19:17:29 -05: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)
 | 
						|
)
 |