Add plugins (#27)

This commit is contained in:
澄潭
2022-11-04 17:46:43 +08:00
committed by GitHub
parent 5ac966495c
commit 1a0ed73cd5
92 changed files with 35435 additions and 1 deletions

View File

@@ -0,0 +1,196 @@
// 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"
"strings"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
"github.com/tidwall/gjson"
)
type Category int
const (
Route Category = iota
Host
)
type MatchType int
const (
Prefix MatchType = iota
Exact
Suffix
)
const (
RULES_KEY = "_rules_"
MATCH_ROUTE_KEY = "_match_route_"
MATCH_DOMAIN_KEY = "_match_domain_"
)
type HostMatcher struct {
matchType MatchType
host string
}
type RuleConfig[PluginConfig any] struct {
category Category
routes 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 {
return nil, err
}
for _, rule := range m.ruleConfig {
if rule.category == Host {
if m.hostMatch(rule, host) {
return &rule.config, nil
}
}
// category == Route
if _, ok := rule.routes[string(routeName)]; ok {
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) error {
var rules []gjson.Result
obj := config.Map()
keyCount := len(obj)
if keyCount == 0 {
// enable globally for empty config
m.hasGlobalConfig = true
return nil
}
if rulesJson, ok := obj[RULES_KEY]; ok {
rules = rulesJson.Array()
keyCount--
}
var pluginConfig PluginConfig
if keyCount > 0 {
err := parsePluginConfig(config, &pluginConfig)
if err != nil {
proxywasm.LogInfof("parse global config failed, err:%v", err)
} else {
m.globalConfig = pluginConfig
m.hasGlobalConfig = true
}
}
if len(rules) == 0 {
if m.hasGlobalConfig {
return nil
}
return errors.New("parse config failed, no valid rules")
}
for _, ruleJson := range rules {
var rule RuleConfig[PluginConfig]
err := parsePluginConfig(ruleJson, &rule.config)
if err != nil {
return err
}
rule.routes = m.parseRouteMatchConfig(ruleJson)
rule.hosts = m.parseHostMatchConfig(ruleJson)
noRoute := len(rule.routes) == 0
noHosts := len(rule.hosts) == 0
if (noRoute && noHosts) || (!noRoute && !noHosts) {
return errors.New("there is only one of '_match_route_' and '_match_domain_' can present in configuration.")
}
if !noRoute {
rule.category = Route
} else {
rule.category = Host
}
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]) 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 (m RuleMatcher[PluginConfig]) hostMatch(rule RuleConfig[PluginConfig], reqHost string) bool {
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
}

View File

@@ -0,0 +1,238 @@
// 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 (
"testing"
"github.com/stretchr/testify/assert"
"github.com/tidwall/gjson"
)
type customConfig struct {
name string
age int64
}
func parseConfig(json gjson.Result, config *customConfig) error {
config.name = json.Get("name").String()
config.age = json.Get("age").Int()
return nil
}
func TestHostMatch(t *testing.T) {
cases := []struct {
name string
config RuleConfig[customConfig]
host string
result bool
}{
{
name: "prefix",
config: RuleConfig[customConfig]{
hosts: []HostMatcher{
{
matchType: Prefix,
host: "www.",
},
},
},
host: "www.test.com",
result: true,
},
{
name: "prefix failed",
config: RuleConfig[customConfig]{
hosts: []HostMatcher{
{
matchType: Prefix,
host: "www.",
},
},
},
host: "test.com",
result: false,
},
{
name: "suffix",
config: RuleConfig[customConfig]{
hosts: []HostMatcher{
{
matchType: Suffix,
host: ".example.com",
},
},
},
host: "www.example.com",
result: true,
},
{
name: "suffix failed",
config: RuleConfig[customConfig]{
hosts: []HostMatcher{
{
matchType: Suffix,
host: ".example.com",
},
},
},
host: "example.com",
result: false,
},
{
name: "exact",
config: RuleConfig[customConfig]{
hosts: []HostMatcher{
{
matchType: Exact,
host: "www.example.com",
},
},
},
host: "www.example.com",
result: true,
},
{
name: "exact failed",
config: RuleConfig[customConfig]{
hosts: []HostMatcher{
{
matchType: Exact,
host: "www.example.com",
},
},
},
host: "example.com",
result: false,
},
{
name: "any",
config: RuleConfig[customConfig]{
hosts: []HostMatcher{
{
matchType: Suffix,
host: "",
},
},
},
host: "www.example.com",
result: true,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var m RuleMatcher[customConfig]
assert.Equal(t, c.result, m.hostMatch(c.config, c.host))
})
}
}
func TestParseRuleConfig(t *testing.T) {
cases := []struct {
name string
config string
errMsg string
expected RuleMatcher[customConfig]
}{
{
name: "global config",
config: `{"name":"john", "age":18}`,
expected: RuleMatcher[customConfig]{
globalConfig: customConfig{
name: "john",
age: 18,
},
hasGlobalConfig: true,
},
},
{
name: "rules config",
config: `{"_rules_":[{"_match_domain_":["*.example.com","www.*","*","www.abc.com"],"name":"john", "age":18},{"_match_route_":["test1","test2"],"name":"ann", "age":16}]}`,
expected: RuleMatcher[customConfig]{
ruleConfig: []RuleConfig[customConfig]{
{
category: Host,
hosts: []HostMatcher{
{
matchType: Suffix,
host: ".example.com",
},
{
matchType: Prefix,
host: "www.",
},
{
matchType: Suffix,
host: "",
},
{
matchType: Exact,
host: "www.abc.com",
},
},
routes: map[string]struct{}{},
config: customConfig{
name: "john",
age: 18,
},
},
{
category: Route,
routes: map[string]struct{}{
"test1": {},
"test2": {},
},
config: customConfig{
name: "ann",
age: 16,
},
},
},
},
},
{
name: "no rule",
config: `{"_rules_":[]}`,
errMsg: "parse config failed, no valid rules",
},
{
name: "invalid rule",
config: `{"_rules_":[{"_match_domain_":["*"],"_match_route_":["test"]}]}`,
errMsg: "there is only one of '_match_route_' and '_match_domain_' can present in configuration.",
},
{
name: "invalid rule",
config: `{"_rules_":[{"age":16}]}`,
errMsg: "there is only one of '_match_route_' and '_match_domain_' can present in configuration.",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var actual RuleMatcher[customConfig]
err := actual.ParseRuleConfig(gjson.Parse(c.config), parseConfig)
if err != nil {
if c.errMsg == "" {
t.Errorf("parse failed: %v", err)
}
if err.Error() != c.errMsg {
t.Errorf("expect err: %s, actual err: %s", c.errMsg,
err.Error())
}
return
}
assert.Equal(t, c.expected, actual)
})
}
}

View File

@@ -0,0 +1,112 @@
// 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 wrapper
import (
"fmt"
"strings"
)
type Cluster interface {
ClusterName() string
HostName() string
}
type K8sCluster struct {
ServiceName string
Namespace string
Port int64
Version string
Host string
}
func (c K8sCluster) ClusterName() string {
namespace := "default"
if c.Namespace != "" {
namespace = c.Namespace
}
return fmt.Sprintf("outbound|%d|%s|%s.%s.svc.cluster.local",
c.Port, c.Version, c.ServiceName, namespace)
}
func (c K8sCluster) HostName() string {
if c.Host != "" {
return c.Host
}
return fmt.Sprintf("%s.%s.svc.cluster.local", c.ServiceName, c.Namespace)
}
type NacosCluster struct {
ServiceName string
// use DEFAULT-GROUP by default
Group string
NamespaceID string
Port int64
// set true if use edas/sae registry
IsExtRegistry bool
Version string
Host string
}
func (c NacosCluster) ClusterName() string {
group := "DEFAULT-GROUP"
if c.Group != "" {
group = strings.ReplaceAll(c.Group, "_", "-")
}
tail := "nacos"
if c.IsExtRegistry {
tail += "-ext"
}
return fmt.Sprintf("outbound|%d|%s|%s.%s.%s.%s",
c.Port, c.Version, c.ServiceName, group, c.NamespaceID, tail)
}
func (c NacosCluster) HostName() string {
if c.Host != "" {
return c.Host
}
return c.ServiceName
}
type StaticIpCluster struct {
ServiceName string
Port int64
Host string
}
func (c StaticIpCluster) ClusterName() string {
return fmt.Sprintf("outbound|%d||%s.static", c.Port, c.ServiceName)
}
func (c StaticIpCluster) HostName() string {
if c.Host != "" {
return c.Host
}
return c.ServiceName
}
type DnsCluster struct {
ServiceName string
Domain string
Port int64
}
func (c DnsCluster) ClusterName() string {
return fmt.Sprintf("outbound|%d||%s.dns", c.Port, c.ServiceName)
}
func (c DnsCluster) HostName() string {
return c.Domain
}

View File

@@ -0,0 +1,102 @@
// 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 wrapper
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestClusterAndHost(t *testing.T) {
cases := []struct {
name string
cluster Cluster
expectCluster string
expectHost string
}{
{
name: "k8s",
cluster: K8sCluster{
ServiceName: "foo",
Namespace: "bar",
Port: 8080,
Version: "1.0",
},
expectCluster: "outbound|8080|1.0|foo.bar.svc.cluster.local",
expectHost: "foo.bar.svc.cluster.local",
},
{
name: "k8s default",
cluster: K8sCluster{
ServiceName: "foo",
Port: 8080,
Host: "www.example.com",
},
expectCluster: "outbound|8080||foo.default.svc.cluster.local",
expectHost: "www.example.com",
},
{
name: "nacos",
cluster: NacosCluster{
ServiceName: "foo",
Group: "DEFAULT_GROUP",
NamespaceID: "xxxx",
Port: 8080,
Version: "1.0",
},
expectCluster: "outbound|8080|1.0|foo.DEFAULT-GROUP.xxxx.nacos",
expectHost: "foo",
},
{
name: "nacos ext",
cluster: NacosCluster{
ServiceName: "foo",
NamespaceID: "xxxx",
Port: 8080,
IsExtRegistry: true,
Host: "www.test.com",
},
expectCluster: "outbound|8080||foo.DEFAULT-GROUP.xxxx.nacos-ext",
expectHost: "www.test.com",
},
{
name: "static",
cluster: StaticIpCluster{
ServiceName: "foo",
Port: 8080,
Host: "www.test.com",
},
expectCluster: "outbound|8080||foo.static",
expectHost: "www.test.com",
},
{
name: "dns",
cluster: DnsCluster{
ServiceName: "foo",
Port: 8080,
Domain: "www.test.com",
},
expectCluster: "outbound|8080||foo.dns",
expectHost: "www.test.com",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
assert.Equal(t, c.expectCluster, c.cluster.ClusterName())
assert.Equal(t, c.expectHost, c.cluster.HostName())
})
}
}

View File

@@ -0,0 +1,121 @@
// 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 wrapper
import (
"net/http"
"strconv"
"github.com/google/uuid"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
)
type ResponseCallback func(statusCode int, responseHeaders http.Header, responseBody []byte)
type HttpClient interface {
Get(path string, headers [][2]string, cb ResponseCallback, timeoutMillisecond ...uint32) error
Head(path string, headers [][2]string, cb ResponseCallback, timeoutMillisecond ...uint32) error
Options(path string, headers [][2]string, cb ResponseCallback, timeoutMillisecond ...uint32) error
Post(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
Put(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
Patch(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
Delete(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
Connect(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
Trace(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
}
type ClusterClient[C Cluster] struct {
cluster C
}
func NewClusterClient[C Cluster](cluster C) *ClusterClient[C] {
return &ClusterClient[C]{cluster: cluster}
}
func (c ClusterClient[C]) Get(path string, headers [][2]string, cb ResponseCallback, timeoutMillisecond ...uint32) error {
return HttpCall(c.cluster, http.MethodGet, path, headers, nil, cb, timeoutMillisecond...)
}
func (c ClusterClient[C]) Head(path string, headers [][2]string, cb ResponseCallback, timeoutMillisecond ...uint32) error {
return HttpCall(c.cluster, http.MethodHead, path, headers, nil, cb, timeoutMillisecond...)
}
func (c ClusterClient[C]) Options(path string, headers [][2]string, cb ResponseCallback, timeoutMillisecond ...uint32) error {
return HttpCall(c.cluster, http.MethodOptions, path, headers, nil, cb, timeoutMillisecond...)
}
func (c ClusterClient[C]) Post(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error {
return HttpCall(c.cluster, http.MethodPost, path, headers, body, cb, timeoutMillisecond...)
}
func (c ClusterClient[C]) Put(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error {
return HttpCall(c.cluster, http.MethodPut, path, headers, body, cb, timeoutMillisecond...)
}
func (c ClusterClient[C]) Patch(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error {
return HttpCall(c.cluster, http.MethodPatch, path, headers, body, cb, timeoutMillisecond...)
}
func (c ClusterClient[C]) Delete(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error {
return HttpCall(c.cluster, http.MethodDelete, path, headers, body, cb, timeoutMillisecond...)
}
func (c ClusterClient[C]) Connect(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error {
return HttpCall(c.cluster, http.MethodConnect, path, headers, body, cb, timeoutMillisecond...)
}
func (c ClusterClient[C]) Trace(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error {
return HttpCall(c.cluster, http.MethodTrace, path, headers, body, cb, timeoutMillisecond...)
}
func HttpCall(cluster Cluster, method, path string, headers [][2]string, body []byte,
callback ResponseCallback, timeoutMillisecond ...uint32) error {
for i := len(headers) - 1; i >= 0; i-- {
key := headers[i][0]
if key == ":method" || key == ":path" || key == ":authority" {
headers = append(headers[:i], headers[i+1:]...)
}
}
// default timeout is 500ms
var timeout uint32 = 500
if len(timeoutMillisecond) > 0 {
timeout = timeoutMillisecond[0]
}
headers = append(headers, [2]string{":method", method}, [2]string{":path", path}, [2]string{":authority", cluster.HostName()})
requestID := uuid.New().String()
_, err := proxywasm.DispatchHttpCall(cluster.ClusterName(), headers, body, nil, timeout, func(numHeaders, bodySize, numTrailers int) {
respBody, err := proxywasm.GetHttpCallResponseBody(0, bodySize)
if err != nil {
proxywasm.LogCriticalf("failed to get response body: %v", err)
}
respHeaders, err := proxywasm.GetHttpCallResponseHeaders()
if err != nil {
proxywasm.LogCriticalf("failed to get response headers: %v", err)
}
code := http.StatusBadGateway
var normalResponse bool
headers := make(http.Header)
for _, h := range respHeaders {
if h[0] == ":status" {
code, err = strconv.Atoi(h[1])
if err != nil {
proxywasm.LogErrorf("failed to parse status: %v", err)
code = http.StatusInternalServerError
} else {
normalResponse = true
}
}
headers.Add(h[0], h[1])
}
proxywasm.LogDebugf("http call end, id: %s, code: %d, normal: %t, body: %s",
requestID, code, normalResponse, respBody)
callback(code, headers, respBody)
})
proxywasm.LogDebugf("http call start, id: %s, cluster: %+v, method: %s, path: %s, body: %s, timeout: %d",
requestID, cluster, method, path, body, timeout)
return err
}

View File

@@ -0,0 +1,120 @@
// 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 wrapper
import (
"fmt"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
)
type LogLevel uint32
const (
LogLevelTrace LogLevel = iota
LogLevelDebug
LogLevelInfo
LogLevelWarn
LogLevelError
LogLevelCritical
)
type LogWrapper struct {
pluginName string
}
func (l LogWrapper) log(level LogLevel, msg string) {
msg = fmt.Sprintf("[%s] %s", l.pluginName, msg)
switch level {
case LogLevelTrace:
proxywasm.LogTrace(msg)
case LogLevelDebug:
proxywasm.LogDebug(msg)
case LogLevelInfo:
proxywasm.LogInfo(msg)
case LogLevelWarn:
proxywasm.LogWarn(msg)
case LogLevelError:
proxywasm.LogError(msg)
case LogLevelCritical:
proxywasm.LogCritical(msg)
}
}
func (l LogWrapper) logFormat(level LogLevel, format string, args ...interface{}) {
format = fmt.Sprintf("[%s] %s", l.pluginName, format)
switch level {
case LogLevelTrace:
proxywasm.LogTracef(format, args...)
case LogLevelDebug:
proxywasm.LogDebugf(format, args...)
case LogLevelInfo:
proxywasm.LogInfof(format, args...)
case LogLevelWarn:
proxywasm.LogWarnf(format, args...)
case LogLevelError:
proxywasm.LogErrorf(format, args...)
case LogLevelCritical:
proxywasm.LogCriticalf(format, args...)
}
}
func (l LogWrapper) Trace(msg string) {
l.log(LogLevelTrace, msg)
}
func (l LogWrapper) Tracef(format string, args ...interface{}) {
l.logFormat(LogLevelTrace, format, args...)
}
func (l LogWrapper) Debug(msg string) {
l.log(LogLevelDebug, msg)
}
func (l LogWrapper) Debugf(format string, args ...interface{}) {
l.logFormat(LogLevelDebug, format, args...)
}
func (l LogWrapper) Info(msg string) {
l.log(LogLevelInfo, msg)
}
func (l LogWrapper) Infof(format string, args ...interface{}) {
l.logFormat(LogLevelInfo, format, args...)
}
func (l LogWrapper) Warn(msg string) {
l.log(LogLevelWarn, msg)
}
func (l LogWrapper) Warnf(format string, args ...interface{}) {
l.logFormat(LogLevelWarn, format, args...)
}
func (l LogWrapper) Error(msg string) {
l.log(LogLevelError, msg)
}
func (l LogWrapper) Errorf(format string, args ...interface{}) {
l.logFormat(LogLevelError, format, args...)
}
func (l LogWrapper) Critical(msg string) {
l.log(LogLevelCritical, msg)
}
func (l LogWrapper) Criticalf(format string, args ...interface{}) {
l.logFormat(LogLevelCritical, format, args...)
}

View File

@@ -0,0 +1,235 @@
// 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 wrapper
import (
"unsafe"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
"github.com/tidwall/gjson"
"github.com/mse-group/wasm-extensions-go/pkg/matcher"
)
type ParseConfigFunc[PluginConfig any] func(json gjson.Result, config *PluginConfig, log LogWrapper) error
type onHttpHeadersFunc[PluginConfig any] func(contextID uint32, config PluginConfig, needBody *bool, log LogWrapper) types.Action
type onHttpBodyFunc[PluginConfig any] func(contextID uint32, config PluginConfig, body []byte, log LogWrapper) types.Action
type CommonVmCtx[PluginConfig any] struct {
types.DefaultVMContext
pluginName string
log LogWrapper
parseConfig ParseConfigFunc[PluginConfig]
onHttpRequestHeaders onHttpHeadersFunc[PluginConfig]
onHttpRequestBody onHttpBodyFunc[PluginConfig]
onHttpResponseHeaders onHttpHeadersFunc[PluginConfig]
onHttpResponseBody onHttpBodyFunc[PluginConfig]
}
func SetCtx[PluginConfig any](pluginName string, setFuncs ...SetPluginFunc[PluginConfig]) {
proxywasm.SetVMContext(NewCommonVmCtx(pluginName, setFuncs...))
}
type SetPluginFunc[PluginConfig any] func(*CommonVmCtx[PluginConfig])
func ParseConfigBy[PluginConfig any](f ParseConfigFunc[PluginConfig]) SetPluginFunc[PluginConfig] {
return func(ctx *CommonVmCtx[PluginConfig]) {
ctx.parseConfig = f
}
}
func ProcessRequestHeadersBy[PluginConfig any](f onHttpHeadersFunc[PluginConfig]) SetPluginFunc[PluginConfig] {
return func(ctx *CommonVmCtx[PluginConfig]) {
ctx.onHttpRequestHeaders = f
}
}
func ProcessRequestBodyBy[PluginConfig any](f onHttpBodyFunc[PluginConfig]) SetPluginFunc[PluginConfig] {
return func(ctx *CommonVmCtx[PluginConfig]) {
ctx.onHttpRequestBody = f
}
}
func ProcessResponseHeadersBy[PluginConfig any](f onHttpHeadersFunc[PluginConfig]) SetPluginFunc[PluginConfig] {
return func(ctx *CommonVmCtx[PluginConfig]) {
ctx.onHttpResponseHeaders = f
}
}
func ProcessResponseBodyBy[PluginConfig any](f onHttpBodyFunc[PluginConfig]) SetPluginFunc[PluginConfig] {
return func(ctx *CommonVmCtx[PluginConfig]) {
ctx.onHttpResponseBody = f
}
}
func parseEmptyPluginConfig[PluginConfig any](gjson.Result, *PluginConfig, LogWrapper) error {
return nil
}
func NewCommonVmCtx[PluginConfig any](pluginName string, setFuncs ...SetPluginFunc[PluginConfig]) *CommonVmCtx[PluginConfig] {
ctx := &CommonVmCtx[PluginConfig]{
pluginName: pluginName,
log: LogWrapper{pluginName},
}
for _, set := range setFuncs {
set(ctx)
}
if ctx.parseConfig == nil {
var config PluginConfig
if unsafe.Sizeof(config) != 0 {
msg := "the `parseConfig` is missing in NewCommonVmCtx's arguments"
ctx.log.Critical(msg)
panic(msg)
}
ctx.parseConfig = parseEmptyPluginConfig[PluginConfig]
}
return ctx
}
func (ctx *CommonVmCtx[PluginConfig]) NewPluginContext(uint32) types.PluginContext {
return &CommonPluginCtx[PluginConfig]{
vm: ctx,
}
}
type CommonPluginCtx[PluginConfig any] struct {
types.DefaultPluginContext
matcher.RuleMatcher[PluginConfig]
vm *CommonVmCtx[PluginConfig]
}
func (ctx *CommonPluginCtx[PluginConfig]) OnPluginStart(int) types.OnPluginStartStatus {
data, err := proxywasm.GetPluginConfiguration()
if err != nil && err != types.ErrorStatusNotFound {
ctx.vm.log.Criticalf("error reading plugin configuration: %v", err)
return types.OnPluginStartStatusFailed
}
if len(data) == 0 {
ctx.vm.log.Warn("need config")
return types.OnPluginStartStatusFailed
}
if !gjson.ValidBytes(data) {
ctx.vm.log.Warnf("the plugin configuration is not a valid json: %s", string(data))
return types.OnPluginStartStatusFailed
}
jsonData := gjson.ParseBytes(data)
err = ctx.ParseRuleConfig(jsonData, func(js gjson.Result, cfg *PluginConfig) error {
return ctx.vm.parseConfig(js, cfg, ctx.vm.log)
})
if err != nil {
ctx.vm.log.Warnf("parse rule config failed: %v", err)
return types.OnPluginStartStatusFailed
}
return types.OnPluginStartStatusOK
}
func (ctx *CommonPluginCtx[PluginConfig]) NewHttpContext(contextID uint32) types.HttpContext {
httpCtx := &CommonHttpCtx[PluginConfig]{
plugin: ctx,
contextID: contextID,
}
if ctx.vm.onHttpRequestBody != nil {
httpCtx.needRequestBody = true
}
if ctx.vm.onHttpResponseBody != nil {
httpCtx.needResponseBody = true
}
return httpCtx
}
type CommonHttpCtx[PluginConfig any] struct {
types.DefaultHttpContext
plugin *CommonPluginCtx[PluginConfig]
config *PluginConfig
needRequestBody bool
needResponseBody bool
requestBodySize int
responseBodySize int
contextID uint32
}
func (ctx *CommonHttpCtx[PluginConfig]) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
config, err := ctx.plugin.GetMatchConfig()
if err != nil {
ctx.plugin.vm.log.Errorf("get match config failed, err:%v", err)
return types.ActionContinue
}
if config == nil {
return types.ActionContinue
}
ctx.config = config
if ctx.plugin.vm.onHttpRequestHeaders == nil {
return types.ActionContinue
}
return ctx.plugin.vm.onHttpRequestHeaders(ctx.contextID, *config,
&ctx.needRequestBody, ctx.plugin.vm.log)
}
func (ctx *CommonHttpCtx[PluginConfig]) OnHttpRequestBody(bodySize int, endOfStream bool) types.Action {
if ctx.config == nil {
return types.ActionContinue
}
if ctx.plugin.vm.onHttpRequestBody == nil {
return types.ActionContinue
}
if !ctx.needRequestBody {
return types.ActionContinue
}
ctx.requestBodySize += bodySize
if !endOfStream {
return types.ActionPause
}
body, err := proxywasm.GetHttpRequestBody(0, ctx.requestBodySize)
if err != nil {
ctx.plugin.vm.log.Warnf("get request body failed: %v", err)
return types.ActionContinue
}
return ctx.plugin.vm.onHttpRequestBody(ctx.contextID, *ctx.config, body, ctx.plugin.vm.log)
}
func (ctx *CommonHttpCtx[PluginConfig]) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action {
if ctx.config == nil {
return types.ActionContinue
}
if ctx.plugin.vm.onHttpResponseHeaders == nil {
return types.ActionContinue
}
return ctx.plugin.vm.onHttpResponseHeaders(ctx.contextID, *ctx.config,
&ctx.needResponseBody, ctx.plugin.vm.log)
}
func (ctx *CommonHttpCtx[PluginConfig]) OnHttpResponseBody(bodySize int, endOfStream bool) types.Action {
if ctx.config == nil {
return types.ActionContinue
}
if ctx.plugin.vm.onHttpResponseBody == nil {
return types.ActionContinue
}
if !ctx.needResponseBody {
return types.ActionContinue
}
ctx.responseBodySize += bodySize
if !endOfStream {
return types.ActionPause
}
body, err := proxywasm.GetHttpResponseBody(0, ctx.responseBodySize)
if err != nil {
ctx.plugin.vm.log.Warnf("get response body failed: %v", err)
return types.ActionContinue
}
return ctx.plugin.vm.onHttpResponseBody(ctx.contextID, *ctx.config, body, ctx.plugin.vm.log)
}

View File

@@ -0,0 +1,53 @@
// 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 wrapper
import "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
func GetRequestScheme() string {
scheme, err := proxywasm.GetHttpRequestHeader(":scheme")
if err != nil {
proxywasm.LogError("parse request scheme failed")
return ""
}
return scheme
}
func GetRequestHost() string {
host, err := proxywasm.GetHttpRequestHeader(":authority")
if err != nil {
proxywasm.LogError("parse request host failed")
return ""
}
return host
}
func GetRequestPath() string {
path, err := proxywasm.GetHttpRequestHeader(":path")
if err != nil {
proxywasm.LogError("parse request path failed")
return ""
}
return path
}
func GetRequestMethod() string {
method, err := proxywasm.GetHttpRequestHeader(":method")
if err != nil {
proxywasm.LogError("parse request path failed")
return ""
}
return method
}