mirror of
https://github.com/alibaba/higress.git
synced 2026-03-01 07:00:50 +08:00
301 lines
8.0 KiB
Go
301 lines
8.0 KiB
Go
// 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
|
|
}
|