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 }