plugins/wasm-go/pkg/matcher/rule_matcher.go (258 lines of code) (raw):
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// 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 matcher
import (
"errors"
"fmt"
"strings"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/tidwall/gjson"
)
type Category int
const (
Route Category = iota
Host
Service
RoutePrefix
)
type MatchType int
const (
Prefix MatchType = iota
Exact
Suffix
)
const (
RULES_KEY = "_rules_"
MATCH_ROUTE_KEY = "_match_route_"
MATCH_DOMAIN_KEY = "_match_domain_"
MATCH_SERVICE_KEY = "_match_service_"
MATCH_ROUTE_PREFIX_KEY = "_match_route_prefix_"
)
type HostMatcher struct {
matchType MatchType
host string
}
type RuleConfig[PluginConfig any] struct {
category Category
routes map[string]struct{}
services map[string]struct{}
routePrefixs map[string]struct{}
hosts []HostMatcher
config PluginConfig
}
type RuleMatcher[PluginConfig any] struct {
ruleConfig []RuleConfig[PluginConfig]
globalConfig PluginConfig
hasGlobalConfig bool
}
func (m RuleMatcher[PluginConfig]) GetMatchConfig() (*PluginConfig, error) {
host, err := proxywasm.GetHttpRequestHeader(":authority")
if err != nil {
return nil, err
}
routeName, err := proxywasm.GetProperty([]string{"route_name"})
if err != nil && err != types.ErrorStatusNotFound {
return nil, err
}
serviceName, err := proxywasm.GetProperty([]string{"cluster_name"})
if err != nil && err != types.ErrorStatusNotFound {
return nil, err
}
for _, rule := range m.ruleConfig {
// category == Host
if rule.category == Host {
if m.hostMatch(rule, host) {
return &rule.config, nil
}
}
// category == Route
if rule.category == Route {
if _, ok := rule.routes[string(routeName)]; ok {
return &rule.config, nil
}
}
// category == RoutePrefix
if rule.category == RoutePrefix {
for routePrefix := range rule.routePrefixs {
if strings.HasPrefix(string(routeName), routePrefix) {
return &rule.config, nil
}
}
}
// category == Cluster
if m.serviceMatch(rule, string(serviceName)) {
return &rule.config, nil
}
}
if m.hasGlobalConfig {
return &m.globalConfig, nil
}
return nil, nil
}
func (m *RuleMatcher[PluginConfig]) ParseRuleConfig(config gjson.Result,
parsePluginConfig func(gjson.Result, *PluginConfig) error,
parseOverrideConfig func(gjson.Result, PluginConfig, *PluginConfig) error) error {
var rules []gjson.Result
obj := config.Map()
keyCount := len(obj)
if keyCount == 0 {
// enable globally for empty config
m.hasGlobalConfig = true
return parsePluginConfig(config, &m.globalConfig)
}
if rulesJson, ok := obj[RULES_KEY]; ok {
rules = rulesJson.Array()
keyCount--
}
var pluginConfig PluginConfig
var globalConfigError error
if keyCount > 0 {
err := parsePluginConfig(config, &pluginConfig)
if err != nil {
globalConfigError = err
} else {
m.globalConfig = pluginConfig
m.hasGlobalConfig = true
}
}
if len(rules) == 0 {
if m.hasGlobalConfig {
return nil
}
return fmt.Errorf("parse config failed, no valid rules; global config parse error:%v", globalConfigError)
}
for _, ruleJson := range rules {
var (
rule RuleConfig[PluginConfig]
err error
)
if parseOverrideConfig != nil {
err = parseOverrideConfig(ruleJson, m.globalConfig, &rule.config)
} else {
err = parsePluginConfig(ruleJson, &rule.config)
}
if err != nil {
return err
}
rule.routes = m.parseRouteMatchConfig(ruleJson)
rule.hosts = m.parseHostMatchConfig(ruleJson)
rule.services = m.parseServiceMatchConfig(ruleJson)
rule.routePrefixs = m.parseRoutePrefixMatchConfig(ruleJson)
noRoute := len(rule.routes) == 0
noHosts := len(rule.hosts) == 0
noService := len(rule.services) == 0
noRoutePrefix := len(rule.routePrefixs) == 0
if boolToInt(noRoute)+boolToInt(noService)+boolToInt(noHosts)+boolToInt(noRoutePrefix) != 3 {
return errors.New("there is only one of '_match_route_', '_match_domain_', '_match_service_' and '_match_route_prefix_' can present in configuration.")
}
if !noRoute {
rule.category = Route
} else if !noHosts {
rule.category = Host
} else if !noService {
rule.category = Service
} else {
rule.category = RoutePrefix
}
m.ruleConfig = append(m.ruleConfig, rule)
}
return nil
}
func (m RuleMatcher[PluginConfig]) parseRouteMatchConfig(config gjson.Result) map[string]struct{} {
keys := config.Get(MATCH_ROUTE_KEY).Array()
routes := make(map[string]struct{})
for _, item := range keys {
routeName := item.String()
if routeName != "" {
routes[routeName] = struct{}{}
}
}
return routes
}
func (m RuleMatcher[PluginConfig]) parseRoutePrefixMatchConfig(config gjson.Result) map[string]struct{} {
keys := config.Get(MATCH_ROUTE_PREFIX_KEY).Array()
routePrefixs := make(map[string]struct{})
for _, item := range keys {
routePrefix := item.String()
if routePrefix != "" {
routePrefixs[routePrefix] = struct{}{}
}
}
return routePrefixs
}
func (m RuleMatcher[PluginConfig]) parseServiceMatchConfig(config gjson.Result) map[string]struct{} {
keys := config.Get(MATCH_SERVICE_KEY).Array()
clusters := make(map[string]struct{})
for _, item := range keys {
clusterName := item.String()
if clusterName != "" {
clusters[clusterName] = struct{}{}
}
}
return clusters
}
func (m RuleMatcher[PluginConfig]) parseHostMatchConfig(config gjson.Result) []HostMatcher {
keys := config.Get(MATCH_DOMAIN_KEY).Array()
var hostMatchers []HostMatcher
for _, item := range keys {
host := item.String()
var hostMatcher HostMatcher
if strings.HasPrefix(host, "*") {
hostMatcher.matchType = Suffix
hostMatcher.host = host[1:]
} else if strings.HasSuffix(host, "*") {
hostMatcher.matchType = Prefix
hostMatcher.host = host[:len(host)-1]
} else {
hostMatcher.matchType = Exact
hostMatcher.host = host
}
hostMatchers = append(hostMatchers, hostMatcher)
}
return hostMatchers
}
func stripPortFromHost(reqHost string) string {
// Port removing code is inspired by
// https://github.com/envoyproxy/envoy/blob/v1.17.0/source/common/http/header_utility.cc#L219
portStart := strings.LastIndexByte(reqHost, ':')
if portStart != -1 {
// According to RFC3986 v6 address is always enclosed in "[]".
// section 3.2.2.
v6EndIndex := strings.LastIndexByte(reqHost, ']')
if v6EndIndex == -1 || v6EndIndex < portStart {
if portStart+1 <= len(reqHost) {
return reqHost[:portStart]
}
}
}
return reqHost
}
func (m RuleMatcher[PluginConfig]) hostMatch(rule RuleConfig[PluginConfig], reqHost string) bool {
reqHost = stripPortFromHost(reqHost)
for _, hostMatch := range rule.hosts {
switch hostMatch.matchType {
case Suffix:
if strings.HasSuffix(reqHost, hostMatch.host) {
return true
}
case Prefix:
if strings.HasPrefix(reqHost, hostMatch.host) {
return true
}
case Exact:
if reqHost == hostMatch.host {
return true
}
default:
return false
}
}
return false
}
func (m RuleMatcher[PluginConfig]) serviceMatch(rule RuleConfig[PluginConfig], serviceName string) bool {
parts := strings.Split(serviceName, "|")
if len(parts) != 4 {
return false
}
port := parts[1]
fqdn := parts[3]
for configServiceName := range rule.services {
colonIndex := strings.LastIndexByte(configServiceName, ':')
if colonIndex != -1 && fqdn == string(configServiceName[:colonIndex]) && port == string(configServiceName[colonIndex+1:]) {
return true
} else if fqdn == string(configServiceName) {
return true
}
}
return false
}