From 1a53c7b4d384905b815d7cd7f01f98dd14cfe83d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=BE=84=E6=BD=AD?=
Date: Fri, 11 Oct 2024 11:39:46 +0800
Subject: [PATCH 01/64] fix mcpbridge endpoint port (#1382)
---
registry/direct/watcher.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/registry/direct/watcher.go b/registry/direct/watcher.go
index f523f1f06a..8ad5f858c2 100644
--- a/registry/direct/watcher.go
+++ b/registry/direct/watcher.go
@@ -152,7 +152,7 @@ func (w *watcher) generateServiceEntry(host string) *v1alpha3.ServiceEntry {
}
endpoint = &v1alpha3.WorkloadEntry{
Address: pair[0],
- Ports: map[string]uint32{"http": uint32(port)},
+ Ports: map[string]uint32{w.Protocol: uint32(port)},
}
} else if w.Type == string(registry.DNS) {
if !domainRegex.MatchString(ep) {
From 952c9ec5dc7f87db307bc02fca07a1a31787476a Mon Sep 17 00:00:00 2001
From: rinfx <893383980@qq.com>
Date: Mon, 14 Oct 2024 12:45:53 +0800
Subject: [PATCH 02/64] Ai proxy support coze (#1387)
---
plugins/wasm-go/extensions/ai-proxy/README.md | 12 +++++
.../wasm-go/extensions/ai-proxy/README_EN.md | 10 +++++
.../extensions/ai-proxy/provider/coze.go | 44 +++++++++++++++++++
.../extensions/ai-proxy/provider/provider.go | 2 +
4 files changed, 68 insertions(+)
create mode 100644 plugins/wasm-go/extensions/ai-proxy/provider/coze.go
diff --git a/plugins/wasm-go/extensions/ai-proxy/README.md b/plugins/wasm-go/extensions/ai-proxy/README.md
index 51156a81d1..2a8a838b1d 100644
--- a/plugins/wasm-go/extensions/ai-proxy/README.md
+++ b/plugins/wasm-go/extensions/ai-proxy/README.md
@@ -669,6 +669,18 @@ provider:
timeout: 1200000
```
+### 使用 original 协议代理 Coze 应用
+
+**配置信息**
+
+```yaml
+provider:
+ type: coze
+ apiTokens:
+ - YOUR_COZE_API_KEY
+ protocol: original
+```
+
### 使用月之暗面配合其原生的文件上下文
提前上传文件至月之暗面,以文件内容作为上下文使用其 AI 服务。
diff --git a/plugins/wasm-go/extensions/ai-proxy/README_EN.md b/plugins/wasm-go/extensions/ai-proxy/README_EN.md
index 57ef01193f..e34546a4e5 100644
--- a/plugins/wasm-go/extensions/ai-proxy/README_EN.md
+++ b/plugins/wasm-go/extensions/ai-proxy/README_EN.md
@@ -656,6 +656,16 @@ providers:
timeout: 1200000
```
+### Using original Protocol Proxy for Coze applications
+
+```yaml
+provider:
+ type: coze
+ apiTokens:
+ - YOUR_COZE_API_KEY
+ protocol: original
+```
+
### Utilizing Moonshot with its Native File Context
Upload files to Moonshot in advance and use its AI services based on file content.
diff --git a/plugins/wasm-go/extensions/ai-proxy/provider/coze.go b/plugins/wasm-go/extensions/ai-proxy/provider/coze.go
new file mode 100644
index 0000000000..c163c95661
--- /dev/null
+++ b/plugins/wasm-go/extensions/ai-proxy/provider/coze.go
@@ -0,0 +1,44 @@
+package provider
+
+import (
+ "errors"
+
+ "github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
+ "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
+ "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
+)
+
+const (
+ cozeDomain = "api.coze.cn"
+)
+
+type cozeProviderInitializer struct{}
+
+func (m *cozeProviderInitializer) ValidateConfig(config ProviderConfig) error {
+ if config.apiTokens == nil || len(config.apiTokens) == 0 {
+ return errors.New("no apiToken found in provider config")
+ }
+ return nil
+}
+
+func (m *cozeProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
+ return &cozeProvider{
+ config: config,
+ contextCache: createContextCache(&config),
+ }, nil
+}
+
+type cozeProvider struct {
+ config ProviderConfig
+ contextCache *contextCache
+}
+
+func (m *cozeProvider) GetProviderType() string {
+ return providerTypeCoze
+}
+
+func (m *cozeProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
+ _ = util.OverwriteRequestHost(cozeDomain)
+ _ = util.OverwriteRequestAuthorization("Bearer " + m.config.GetRandomToken())
+ return types.ActionContinue, nil
+}
diff --git a/plugins/wasm-go/extensions/ai-proxy/provider/provider.go b/plugins/wasm-go/extensions/ai-proxy/provider/provider.go
index facd8bb283..c9d59fd035 100644
--- a/plugins/wasm-go/extensions/ai-proxy/provider/provider.go
+++ b/plugins/wasm-go/extensions/ai-proxy/provider/provider.go
@@ -42,6 +42,7 @@ const (
providerTypeMistral = "mistral"
providerTypeCohere = "cohere"
providerTypeDoubao = "doubao"
+ providerTypeCoze = "coze"
protocolOpenAI = "openai"
protocolOriginal = "original"
@@ -101,6 +102,7 @@ var (
providerTypeMistral: &mistralProviderInitializer{},
providerTypeCohere: &cohereProviderInitializer{},
providerTypeDoubao: &doubaoProviderInitializer{},
+ providerTypeCoze: &cozeProviderInitializer{},
}
)
From 04ce776f14086008424f252bcbfe58eda5dab554 Mon Sep 17 00:00:00 2001
From: Kent Dong
Date: Mon, 14 Oct 2024 18:50:45 +0800
Subject: [PATCH 03/64] feat: Support route fallback by default (#1381)
---
helm/core/templates/fallback-envoyfilter.yaml | 22 +++++++++++++++++++
helm/core/values.yaml | 2 +-
2 files changed, 23 insertions(+), 1 deletion(-)
create mode 100644 helm/core/templates/fallback-envoyfilter.yaml
diff --git a/helm/core/templates/fallback-envoyfilter.yaml b/helm/core/templates/fallback-envoyfilter.yaml
new file mode 100644
index 0000000000..567ee09256
--- /dev/null
+++ b/helm/core/templates/fallback-envoyfilter.yaml
@@ -0,0 +1,22 @@
+apiVersion: networking.istio.io/v1alpha3
+kind: EnvoyFilter
+metadata:
+ name: {{ include "gateway.name" . }}-global-custom-response
+ namespace: {{ .Release.Namespace }}
+ labels:
+ {{- include "gateway.labels" . | nindent 4}}
+spec:
+ configPatches:
+ - applyTo: HTTP_FILTER
+ match:
+ context: GATEWAY
+ listener:
+ filterChain:
+ filter:
+ name: envoy.filters.network.http_connection_manager
+ patch:
+ operation: INSERT_FIRST
+ value:
+ name: envoy.filters.http.custom_response
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.custom_response.v3.CustomResponse
\ No newline at end of file
diff --git a/helm/core/values.yaml b/helm/core/values.yaml
index ce08899207..39582f748d 100644
--- a/helm/core/values.yaml
+++ b/helm/core/values.yaml
@@ -26,7 +26,7 @@ global:
autoscalingv2API: true
local: false # When deploying to a local cluster (e.g.: kind cluster), set this to true.
kind: false # Deprecated. Please use "global.local" instead. Will be removed later.
- enableIstioAPI: false
+ enableIstioAPI: true
enableGatewayAPI: false
# Deprecated
enableHigressIstio: false
From 0a112d1a1e22a2b5122d65ab85c792f540c4cbb6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=BE=84=E6=BD=AD?=
Date: Tue, 15 Oct 2024 11:50:43 +0800
Subject: [PATCH 04/64] fix mcp service port protocol name (#1383)
---
registry/direct/watcher.go | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/registry/direct/watcher.go b/registry/direct/watcher.go
index 8ad5f858c2..533c80d63f 100644
--- a/registry/direct/watcher.go
+++ b/registry/direct/watcher.go
@@ -133,6 +133,7 @@ var domainRegex = regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-
func (w *watcher) generateServiceEntry(host string) *v1alpha3.ServiceEntry {
endpoints := make([]*v1alpha3.WorkloadEntry, 0)
+ protocol := string(common.ParseProtocol(w.Protocol))
for _, ep := range strings.Split(w.Domain, common.CommaSeparator) {
var endpoint *v1alpha3.WorkloadEntry
if w.Type == string(registry.Static) {
@@ -152,7 +153,7 @@ func (w *watcher) generateServiceEntry(host string) *v1alpha3.ServiceEntry {
}
endpoint = &v1alpha3.WorkloadEntry{
Address: pair[0],
- Ports: map[string]uint32{w.Protocol: uint32(port)},
+ Ports: map[string]uint32{protocol: uint32(port)},
}
} else if w.Type == string(registry.DNS) {
if !domainRegex.MatchString(ep) {
@@ -175,8 +176,8 @@ func (w *watcher) generateServiceEntry(host string) *v1alpha3.ServiceEntry {
var ports []*v1alpha3.ServicePort
ports = append(ports, &v1alpha3.ServicePort{
Number: w.Port,
- Name: w.Protocol,
- Protocol: string(common.ParseProtocol(w.Protocol)),
+ Name: protocol,
+ Protocol: protocol,
})
se := &v1alpha3.ServiceEntry{
Hosts: []string{host},
From 85f8eb51660343c9b740e66148d497a16d945c10 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=BE=84=E6=BD=AD?=
Date: Tue, 15 Oct 2024 20:52:03 +0800
Subject: [PATCH 05/64] key-auth consumer support set independent key source
(#1392)
---
.../wasm-cpp/extensions/key_auth/plugin.cc | 204 +++++++++++++-----
plugins/wasm-cpp/extensions/key_auth/plugin.h | 12 +-
.../extensions/key_auth/plugin_test.cc | 63 +++++-
3 files changed, 218 insertions(+), 61 deletions(-)
diff --git a/plugins/wasm-cpp/extensions/key_auth/plugin.cc b/plugins/wasm-cpp/extensions/key_auth/plugin.cc
index a88b6bda7a..ad111760e8 100644
--- a/plugins/wasm-cpp/extensions/key_auth/plugin.cc
+++ b/plugins/wasm-cpp/extensions/key_auth/plugin.cc
@@ -42,10 +42,7 @@ static RegisterContextFactory register_KeyAuth(CONTEXT_FACTORY(PluginContext),
namespace {
-void deniedNoKeyAuthData(const std::string& realm) {
- sendLocalResponse(401, "No API key found in request", "",
- {{"WWW-Authenticate", absl::StrCat("Key realm=", realm)}});
-}
+const std::string OriginalAuthKey("X-HI-ORIGINAL-AUTH");
void deniedInvalidCredentials(const std::string& realm) {
sendLocalResponse(401, "Request denied by Key Auth check. Invalid API key",
@@ -84,6 +81,7 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
}
if (!JsonArrayIterate(
configuration, "consumers", [&](const json& consumer) -> bool {
+ Consumer c;
auto item = consumer.find("name");
if (item == consumer.end()) {
LOG_WARN("can't find 'name' field in consumer.");
@@ -94,6 +92,7 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
!name.first) {
return false;
}
+ c.name = name.first.value();
item = consumer.find("credential");
if (item == consumer.end()) {
LOG_WARN("can't find 'credential' field in consumer.");
@@ -104,6 +103,7 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
!credential.first) {
return false;
}
+ c.credential = credential.first.value();
if (rule.credential_to_name.find(credential.first.value()) !=
rule.credential_to_name.end()) {
LOG_WARN(absl::StrCat("duplicate consumer credential: ",
@@ -113,15 +113,59 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
rule.credentials.insert(credential.first.value());
rule.credential_to_name.emplace(
std::make_pair(credential.first.value(), name.first.value()));
+ item = consumer.find("keys");
+ if (item != consumer.end()) {
+ c.keys = std::vector{OriginalAuthKey};
+ if (!JsonArrayIterate(
+ consumer, "keys", [&](const json& key_json) -> bool {
+ auto key = JsonValueAs(key_json);
+ if (key.second !=
+ Wasm::Common::JsonParserResultDetail::OK) {
+ return false;
+ }
+ c.keys->push_back(key.first.value());
+ return true;
+ })) {
+ LOG_WARN("failed to parse configuration for consumer keys.");
+ return false;
+ }
+ item = consumer.find("in_query");
+ if (item != consumer.end()) {
+ auto in_query = JsonValueAs(item.value());
+ if (in_query.second !=
+ Wasm::Common::JsonParserResultDetail::OK ||
+ !in_query.first) {
+ LOG_WARN(
+ "failed to parse 'in_query' field in consumer "
+ "configuration.");
+ return false;
+ }
+ c.in_query = in_query.first;
+ }
+ item = consumer.find("in_header");
+ if (item != consumer.end()) {
+ auto in_header = JsonValueAs(item.value());
+ if (in_header.second !=
+ Wasm::Common::JsonParserResultDetail::OK ||
+ !in_header.first) {
+ LOG_WARN(
+ "failed to parse 'in_header' field in consumer "
+ "configuration.");
+ return false;
+ }
+ c.in_header = in_header.first;
+ }
+ }
+ rule.consumers.push_back(std::move(c));
return true;
})) {
LOG_WARN("failed to parse configuration for credentials.");
return false;
}
- if (rule.credentials.empty()) {
- LOG_INFO("at least one credential has to be configured for a rule.");
- return false;
- }
+ // if (rule.credentials.empty()) {
+ // LOG_INFO("at least one credential has to be configured for a rule.");
+ // return false;
+ // }
if (!JsonArrayIterate(configuration, "keys", [&](const json& item) -> bool {
auto key = JsonValueAs(item);
if (key.second != Wasm::Common::JsonParserResultDetail::OK) {
@@ -137,6 +181,7 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
LOG_WARN("at least one key has to be configured for a rule.");
return false;
}
+ rule.keys.push_back(OriginalAuthKey);
auto it = configuration.find("realm");
if (it != configuration.end()) {
auto realm_string = JsonValueAs(it.value());
@@ -175,36 +220,102 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
bool PluginRootContext::checkPlugin(
const KeyAuthConfigRule& rule,
const std::optional>& allow_set) {
- auto credential = extractCredential(rule);
- if (credential.empty()) {
- LOG_DEBUG("empty credential");
- deniedNoKeyAuthData(rule.realm);
- return false;
+ if (rule.consumers.empty()) {
+ for (const auto& key : rule.keys) {
+ auto credential = extractCredential(rule.in_header, rule.in_query, key);
+ if (credential.empty()) {
+ LOG_DEBUG("empty credential for key: " + key);
+ continue;
+ }
+
+ auto auth_credential_iter = rule.credentials.find(credential);
+ if (auth_credential_iter == rule.credentials.end()) {
+ LOG_DEBUG("api key not found: " + credential);
+ continue;
+ }
+
+ auto credential_to_name_iter = rule.credential_to_name.find(credential);
+ if (credential_to_name_iter != rule.credential_to_name.end()) {
+ if (allow_set && !allow_set->empty()) {
+ if (allow_set->find(credential_to_name_iter->second) ==
+ allow_set->end()) {
+ deniedUnauthorizedConsumer(rule.realm);
+ LOG_DEBUG("unauthorized consumer: " +
+ credential_to_name_iter->second);
+ return false;
+ }
+ }
+ addRequestHeader("X-Mse-Consumer", credential_to_name_iter->second);
+ }
+ return true;
+ }
+ } else {
+ for (const auto& consumer : rule.consumers) {
+ std::vector keys_to_check =
+ consumer.keys.value_or(rule.keys);
+ bool in_query = consumer.in_query.value_or(rule.in_query);
+ bool in_header = consumer.in_header.value_or(rule.in_header);
+
+ for (const auto& key : keys_to_check) {
+ auto credential = extractCredential(in_header, in_query, key);
+ if (credential.empty()) {
+ LOG_DEBUG("empty credential for key: " + key);
+ continue;
+ }
+
+ if (credential != consumer.credential) {
+ LOG_DEBUG("credential does not match the consumer's credential: " +
+ credential);
+ continue;
+ }
+
+ auto auth_credential_iter = rule.credentials.find(credential);
+ if (auth_credential_iter == rule.credentials.end()) {
+ LOG_DEBUG("api key not found: " + credential);
+ continue;
+ }
+
+ auto credential_to_name_iter = rule.credential_to_name.find(credential);
+ if (credential_to_name_iter != rule.credential_to_name.end()) {
+ if (allow_set && !allow_set->empty()) {
+ if (allow_set->find(credential_to_name_iter->second) ==
+ allow_set->end()) {
+ deniedUnauthorizedConsumer(rule.realm);
+ LOG_DEBUG("unauthorized consumer: " +
+ credential_to_name_iter->second);
+ return false;
+ }
+ }
+ addRequestHeader("X-Mse-Consumer", credential_to_name_iter->second);
+ }
+ return true;
+ }
+ }
}
- auto auth_credential_iter = rule.credentials.find(std::string(credential));
- // Check if the credential is part of the credentials
- // set from our container to grant or deny access.
- if (auth_credential_iter == rule.credentials.end()) {
- LOG_DEBUG(absl::StrCat("api key not found: ", credential));
- deniedInvalidCredentials(rule.realm);
- return false;
+
+ LOG_DEBUG("No valid credentials were found after checking all consumers.");
+ deniedInvalidCredentials(rule.realm);
+ return false;
+}
+
+std::string PluginRootContext::extractCredential(bool in_header, bool in_query,
+ const std::string& key) {
+ if (in_header) {
+ auto header = getRequestHeader(key);
+ if (header->size() != 0) {
+ return header->toString();
+ }
}
- // Check if this credential has a consumer name. If so, check if this
- // consumer is allowed to access. If allow_set is empty, allow all consumers.
- auto credential_to_name_iter =
- rule.credential_to_name.find(std::string(std::string(credential)));
- if (credential_to_name_iter != rule.credential_to_name.end()) {
- if (allow_set && !allow_set.value().empty()) {
- if (allow_set.value().find(credential_to_name_iter->second) ==
- allow_set.value().end()) {
- deniedUnauthorizedConsumer(rule.realm);
- LOG_DEBUG(credential_to_name_iter->second);
- return false;
- }
+ if (in_query) {
+ auto request_path_header = getRequestHeader(":path");
+ auto path = request_path_header->view();
+ auto params = Wasm::Common::Http::parseAndDecodeQueryString(path);
+ auto it = params.find(key);
+ if (it != params.end()) {
+ return it->second;
}
- addRequestHeader("X-Mse-Consumer", credential_to_name_iter->second);
}
- return true;
+ return "";
}
bool PluginRootContext::onConfigure(size_t size) {
@@ -234,31 +345,6 @@ bool PluginRootContext::configure(size_t configuration_size) {
return true;
}
-std::string PluginRootContext::extractCredential(
- const KeyAuthConfigRule& rule) {
- auto request_path_header = getRequestHeader(":path");
- auto path = request_path_header->view();
- LOG_DEBUG(std::string(path));
- if (rule.in_query) {
- auto params = Wasm::Common::Http::parseAndDecodeQueryString(path);
- for (const auto& key : rule.keys) {
- auto it = params.find(key);
- if (it != params.end()) {
- return it->second;
- }
- }
- }
- if (rule.in_header) {
- for (const auto& key : rule.keys) {
- auto header = getRequestHeader(key);
- if (header->size() != 0) {
- return header->toString();
- }
- }
- }
- return "";
-}
-
FilterHeadersStatus PluginContext::onRequestHeaders(uint32_t, bool) {
auto* rootCtx = rootContext();
return rootCtx->checkAuthRule(
diff --git a/plugins/wasm-cpp/extensions/key_auth/plugin.h b/plugins/wasm-cpp/extensions/key_auth/plugin.h
index 4a2b7418d3..4cf329d90c 100644
--- a/plugins/wasm-cpp/extensions/key_auth/plugin.h
+++ b/plugins/wasm-cpp/extensions/key_auth/plugin.h
@@ -36,7 +36,16 @@ namespace key_auth {
#endif
+struct Consumer {
+ std::string name;
+ std::string credential;
+ std::optional> keys;
+ std::optional in_query = std::nullopt;
+ std::optional in_header = std::nullopt;
+};
+
struct KeyAuthConfigRule {
+ std::vector consumers;
std::unordered_set credentials;
std::unordered_map credential_to_name;
std::string realm = "MSE Gateway";
@@ -61,7 +70,8 @@ class PluginRootContext : public RootContext,
private:
bool parsePluginConfig(const json&, KeyAuthConfigRule&) override;
- std::string extractCredential(const KeyAuthConfigRule&);
+ std::string extractCredential(bool in_header, bool in_query,
+ const std::string& key);
};
// Per-stream context.
diff --git a/plugins/wasm-cpp/extensions/key_auth/plugin_test.cc b/plugins/wasm-cpp/extensions/key_auth/plugin_test.cc
index fc70df1b00..2f60811e00 100644
--- a/plugins/wasm-cpp/extensions/key_auth/plugin_test.cc
+++ b/plugins/wasm-cpp/extensions/key_auth/plugin_test.cc
@@ -121,7 +121,7 @@ TEST_F(KeyAuthTest, InQuery) {
"_rules_": [
{
"_match_route_": ["test"],
- "credentials":["abc"],
+ "credentials":["abc","def"],
"keys": ["apiKey", "x-api-key"]
}
]
@@ -144,6 +144,10 @@ TEST_F(KeyAuthTest, InQuery) {
path_ = "/test?hello=123&apiKey=123";
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
+
+ path_ = "/test?hello=123&apiKey=123&x-api-key=def";
+ EXPECT_EQ(context_->onRequestHeaders(0, false),
+ FilterHeadersStatus::Continue);
}
TEST_F(KeyAuthTest, InQueryWithConsumer) {
@@ -173,6 +177,29 @@ TEST_F(KeyAuthTest, InQueryWithConsumer) {
FilterHeadersStatus::StopIteration);
}
+TEST_F(KeyAuthTest, EmptyConsumer) {
+ std::string configuration = R"(
+{
+ "consumers" : [],
+ "keys" : [ "apiKey", "x-api-key" ],
+ "_rules_" : [ {"_match_route_" : ["test"], "allow" : []} ]
+})";
+ BufferBase buffer;
+ buffer.set(configuration);
+ EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
+ .WillOnce([&buffer](WasmBufferType) { return &buffer; });
+ EXPECT_TRUE(root_context_->configure(configuration.size()));
+
+ route_name_ = "test";
+ path_ = "/test?hello=1&apiKey=abc";
+ EXPECT_EQ(context_->onRequestHeaders(0, false),
+ FilterHeadersStatus::StopIteration);
+
+ route_name_ = "test2";
+ EXPECT_EQ(context_->onRequestHeaders(0, false),
+ FilterHeadersStatus::Continue);
+}
+
TEST_F(KeyAuthTest, InHeader) {
std::string configuration = R"(
{
@@ -240,6 +267,40 @@ TEST_F(KeyAuthTest, InHeaderWithConsumer) {
FilterHeadersStatus::StopIteration);
}
+TEST_F(KeyAuthTest, ConsumerDifferentKey) {
+ std::string configuration = R"(
+{
+ "consumers" : [ {"credential" : "abc", "name" : "consumer1", "keys" : [ "apiKey" ]}, {"credential" : "123", "name" : "consumer2"} ],
+ "keys" : [ "apiKey2" ],
+ "_rules_" : [ {"_match_route_" : ["test"], "allow" : ["consumer1"]}, {"_match_route_" : ["test2"], "allow" : ["consumer2"]} ]
+})";
+ BufferBase buffer;
+ buffer.set(configuration);
+ EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
+ .WillOnce([&buffer](WasmBufferType) { return &buffer; });
+ EXPECT_TRUE(root_context_->configure(configuration.size()));
+
+ route_name_ = "test";
+ path_ = "/test?hello=1&apiKey=abc";
+ EXPECT_EQ(context_->onRequestHeaders(0, false),
+ FilterHeadersStatus::Continue);
+
+ route_name_ = "test";
+ path_ = "/test?hello=1&apiKey2=abc";
+ EXPECT_EQ(context_->onRequestHeaders(0, false),
+ FilterHeadersStatus::StopIteration);
+
+ route_name_ = "test";
+ path_ = "/test?hello=123&apiKey2=123";
+ EXPECT_EQ(context_->onRequestHeaders(0, false),
+ FilterHeadersStatus::StopIteration);
+
+ route_name_ = "test2";
+ path_ = "/test?hello=123&apiKey2=123";
+ EXPECT_EQ(context_->onRequestHeaders(0, false),
+ FilterHeadersStatus::Continue);
+}
+
} // namespace key_auth
} // namespace null_plugin
} // namespace proxy_wasm
From e298078065db6550f8a6035c93676de139c75777 Mon Sep 17 00:00:00 2001
From: Jun <108045855+2456868764@users.noreply.github.com>
Date: Wed, 16 Oct 2024 10:21:03 +0800
Subject: [PATCH 06/64] add dns&static registry e2e test (#1393)
---
test/e2e/conformance/base/manifests.yaml | 52 +++++++++++++++++
.../tests/httproute-dns-registry.go | 57 +++++++++++++++++++
.../tests/httproute-dns-registry.yaml | 48 ++++++++++++++++
.../tests/httproute-static-registry.go | 57 +++++++++++++++++++
.../tests/httproute-static-registry.yaml | 48 ++++++++++++++++
5 files changed, 262 insertions(+)
create mode 100644 test/e2e/conformance/tests/httproute-dns-registry.go
create mode 100644 test/e2e/conformance/tests/httproute-dns-registry.yaml
create mode 100644 test/e2e/conformance/tests/httproute-static-registry.go
create mode 100644 test/e2e/conformance/tests/httproute-static-registry.yaml
diff --git a/test/e2e/conformance/base/manifests.yaml b/test/e2e/conformance/base/manifests.yaml
index 235dce14dc..a8afe3b11c 100644
--- a/test/e2e/conformance/base/manifests.yaml
+++ b/test/e2e/conformance/base/manifests.yaml
@@ -80,6 +80,58 @@ spec:
---
apiVersion: v1
kind: Service
+metadata:
+ name: infra-backend-v1-ip
+ namespace: higress-conformance-infra
+spec:
+ selector:
+ app: infra-backend-v1-ip
+ ports:
+ - protocol: TCP
+ port: 8080
+ targetPort: 3000
+ clusterIP: 10.96.254.254
+ type: ClusterIP
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: infra-backend-v1-ip
+ namespace: higress-conformance-infra
+ labels:
+ app: infra-backend-v1-ip
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: infra-backend-v1-ip
+ template:
+ metadata:
+ labels:
+ app: infra-backend-v1-ip
+ spec:
+ containers:
+ - name: infra-backend-v1-ip
+ # From https://github.com/kubernetes-sigs/ingress-controller-conformance/tree/master/images/echoserver
+ # image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echoserver:v20221109-7ee2f3e
+
+ # From https://github.com/Uncle-Justice/echo-server
+ image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echo-server:1.3.0
+ env:
+ - name: POD_NAME
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ - name: NAMESPACE
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.namespace
+ resources:
+ requests:
+ cpu: 10m
+---
+apiVersion: v1
+kind: Service
metadata:
name: infra-backend-v2
namespace: higress-conformance-infra
diff --git a/test/e2e/conformance/tests/httproute-dns-registry.go b/test/e2e/conformance/tests/httproute-dns-registry.go
new file mode 100644
index 0000000000..1864b8d394
--- /dev/null
+++ b/test/e2e/conformance/tests/httproute-dns-registry.go
@@ -0,0 +1,57 @@
+// 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 tests
+
+import (
+ "testing"
+
+ "github.com/alibaba/higress/test/e2e/conformance/utils/http"
+ "github.com/alibaba/higress/test/e2e/conformance/utils/suite"
+)
+
+func init() {
+ Register(HTTPRouteDNSRegistry)
+}
+
+var HTTPRouteDNSRegistry = suite.ConformanceTest{
+ ShortName: "HTTPRouteDNSRegistry",
+ Description: "The Ingress in the higress-conformance-infra namespace uses the dns service registry.",
+ Manifests: []string{"tests/httproute-dns-registry.yaml"},
+ Features: []suite.SupportedFeature{suite.HTTPConformanceFeature},
+ Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
+ testcases := []http.Assertion{
+ {
+ Request: http.AssertionRequest{
+ ActualRequest: http.Request{
+ Host: "foo.com",
+ Path: "/",
+ Method: "GET",
+ },
+ },
+ Response: http.AssertionResponse{
+ ExpectedResponseNoRequest: true,
+ ExpectedResponse: http.Response{
+ StatusCode: 200,
+ },
+ },
+ },
+ }
+ t.Run("HTTPRoute DNS Registry", func(t *testing.T) {
+ for _, testcase := range testcases {
+ http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase)
+ }
+ })
+ },
+}
diff --git a/test/e2e/conformance/tests/httproute-dns-registry.yaml b/test/e2e/conformance/tests/httproute-dns-registry.yaml
new file mode 100644
index 0000000000..8f992ac9ea
--- /dev/null
+++ b/test/e2e/conformance/tests/httproute-dns-registry.yaml
@@ -0,0 +1,48 @@
+# 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.
+
+apiVersion: networking.higress.io/v1
+kind: McpBridge
+metadata:
+ name: default
+ namespace: higress-system
+spec:
+ registries:
+ - type: dns
+ domain: infra-backend-v1.higress-conformance-infra.svc.cluster.local
+ name: infra-backend-v1
+ port: 8080
+ protocol: http
+
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ annotations:
+ higress.io/destination: infra-backend-v1.dns
+ name: httproute-infra-backend-v1-dns-ingress
+ namespace: higress-system
+spec:
+ ingressClassName: higress
+ rules:
+ - host: "foo.com"
+ http:
+ paths:
+ - pathType: Prefix
+ path: /
+ backend:
+ resource:
+ apiGroup: networking.higress.io
+ kind: McpBridge
+ name: default
diff --git a/test/e2e/conformance/tests/httproute-static-registry.go b/test/e2e/conformance/tests/httproute-static-registry.go
new file mode 100644
index 0000000000..b95acf4a6c
--- /dev/null
+++ b/test/e2e/conformance/tests/httproute-static-registry.go
@@ -0,0 +1,57 @@
+// 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 tests
+
+import (
+ "testing"
+
+ "github.com/alibaba/higress/test/e2e/conformance/utils/http"
+ "github.com/alibaba/higress/test/e2e/conformance/utils/suite"
+)
+
+func init() {
+ Register(HTTPRouteStaticRegistry)
+}
+
+var HTTPRouteStaticRegistry = suite.ConformanceTest{
+ ShortName: "HTTPRouteStaticRegistry",
+ Description: "The Ingress in the higress-conformance-infra namespace uses the static service registry.",
+ Manifests: []string{"tests/httproute-static-registry.yaml"},
+ Features: []suite.SupportedFeature{suite.HTTPConformanceFeature},
+ Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
+ testcases := []http.Assertion{
+ {
+ Request: http.AssertionRequest{
+ ActualRequest: http.Request{
+ Host: "foo.com",
+ Path: "/",
+ Method: "GET",
+ },
+ },
+ Response: http.AssertionResponse{
+ ExpectedResponseNoRequest: true,
+ ExpectedResponse: http.Response{
+ StatusCode: 200,
+ },
+ },
+ },
+ }
+ t.Run("HTTPRoute Static Registry", func(t *testing.T) {
+ for _, testcase := range testcases {
+ http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase)
+ }
+ })
+ },
+}
diff --git a/test/e2e/conformance/tests/httproute-static-registry.yaml b/test/e2e/conformance/tests/httproute-static-registry.yaml
new file mode 100644
index 0000000000..310302fbb8
--- /dev/null
+++ b/test/e2e/conformance/tests/httproute-static-registry.yaml
@@ -0,0 +1,48 @@
+# 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.
+
+apiVersion: networking.higress.io/v1
+kind: McpBridge
+metadata:
+ name: default
+ namespace: higress-system
+spec:
+ registries:
+ - type: static
+ domain: 10.96.254.254:8080
+ name: infra-backend-v1-ip
+ port: 8080
+ protocol: http
+
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ annotations:
+ higress.io/destination: infra-backend-v1-ip.static
+ name: httproute-infra-backend-v1-ip-ingress
+ namespace: higress-system
+spec:
+ ingressClassName: higress
+ rules:
+ - host: "foo.com"
+ http:
+ paths:
+ - pathType: Prefix
+ path: /
+ backend:
+ resource:
+ apiGroup: networking.higress.io
+ kind: McpBridge
+ name: default
From d0693d8c4bab0b429d6b58e90589b64bceaac042 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=BE=84=E6=BD=AD?=
Date: Wed, 16 Oct 2024 11:17:44 +0800
Subject: [PATCH 07/64] Update SECURITY.md
---
SECURITY.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/SECURITY.md b/SECURITY.md
index e4f906b953..d643ca19b9 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -4,6 +4,7 @@
| Version | Supported |
| ------- | ------------------ |
+| 2.x.x | :white_check_mark: |
| 1.x.x | :white_check_mark: |
| < 1.0.0 | :x: |
From 51c956f0b3f5f0bdc8823295d11e5920d08d3caa Mon Sep 17 00:00:00 2001
From: Kent Dong
Date: Wed, 16 Oct 2024 18:42:49 +0800
Subject: [PATCH 08/64] fix: Fix clean targets in Makefile (#1397)
---
Makefile.core.mk | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/Makefile.core.mk b/Makefile.core.mk
index 95341d0f5f..55a9662f70 100644
--- a/Makefile.core.mk
+++ b/Makefile.core.mk
@@ -221,11 +221,15 @@ clean-higress: ## Cleans all the intermediate files and folders previously gener
rm -rf $(DIRS_TO_CLEAN)
clean-istio:
+ rm -rf external/api
+ rm -rf external/client-go
rm -rf external/istio
+ rm -rf external/pkg
clean-gateway: clean-istio
rm -rf external/envoy
rm -rf external/proxy
+ rm -rf external/go-control-plane
rm -rf external/package/envoy.tar.gz
clean-env:
From 6f86c31bac81b7604d4a45854a5b811bfa44a171 Mon Sep 17 00:00:00 2001
From: Kent Dong
Date: Wed, 16 Oct 2024 19:00:18 +0800
Subject: [PATCH 09/64] feat: Update submodules: envoy/envoy, istio/isitio
(#1398)
---
envoy/envoy | 2 +-
istio/istio | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/envoy/envoy b/envoy/envoy
index b3541845c1..e9302f5574 160000
--- a/envoy/envoy
+++ b/envoy/envoy
@@ -1 +1 @@
-Subproject commit b3541845c1a78d817c73806299415439c23488d2
+Subproject commit e9302f55742b53b617984cf25872c193357b6159
diff --git a/istio/istio b/istio/istio
index d380470e53..1dbd773596 160000
--- a/istio/istio
+++ b/istio/istio
@@ -1 +1 @@
-Subproject commit d380470e53b6aa45b7a8ab2bf26cbc6c147da06f
+Subproject commit 1dbd77359624ab4af2953a4840927fbd7ea1d668
From e923cbaeccab033efff42f53c51248e547dfefee Mon Sep 17 00:00:00 2001
From: Smoothengineer <160827599+Smoothengineer@users.noreply.github.com>
Date: Thu, 17 Oct 2024 10:23:04 +0530
Subject: [PATCH 10/64] Update README_EN.md (#1402)
---
README_EN.md | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/README_EN.md b/README_EN.md
index 4413ab8c0c..8ce33aa069 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -47,7 +47,7 @@ Powered by [Istio](https://github.com/istio/istio) and [Envoy](https://github.co
Higress can function as a microservice gateway, which can discovery microservices from various service registries, such as Nacos, ZooKeeper, Consul, Eureka, etc.
- It deeply integrates of [Dubbo](https://github.com/apache/dubbo), [Nacos](https://github.com/alibaba/nacos), [Sentinel](https://github.com/alibaba/Sentinel) and other microservice technology stacks.
+ It deeply integrates with [Dubbo](https://github.com/apache/dubbo), [Nacos](https://github.com/alibaba/nacos), [Sentinel](https://github.com/alibaba/Sentinel) and other microservice technology stacks.
- **Security gateway**:
@@ -57,7 +57,7 @@ Powered by [Istio](https://github.com/istio/istio) and [Envoy](https://github.co
- **Easy to use**
- Provide one-stop gateway solutions for traffic scheduling, service management, and security protection, support Console, K8s Ingress, and Gateway API configuration methods, and also support HTTP to Dubbo protocol conversion, and easily complete protocol mapping configuration.
+ Provides one-stop gateway solutions for traffic scheduling, service management, and security protection, support Console, K8s Ingress, and Gateway API configuration methods, and also support HTTP to Dubbo protocol conversion, and easily complete protocol mapping configuration.
- **Easy to expand**
@@ -73,7 +73,7 @@ Powered by [Istio](https://github.com/istio/istio) and [Envoy](https://github.co
- **Security**
- Provides JWT, OIDC, custom authentication and authentication, deeply integrates open source web application firewall.
+ Provides JWT, OIDC, custom authentication and authentication, deeply integrates open-source web application firewall.
## Community
@@ -81,9 +81,9 @@ Powered by [Istio](https://github.com/istio/istio) and [Envoy](https://github.co
### Thanks
-Higress would not be possible without the valuable open-source work of projects in the community. We would like to extend a special thank-you to Envoy and Istio.
+Higress would not be possible without the valuable open-source work of projects in the community. We would like to extend a special thank you to Envoy and Istio.
### Related Repositories
- Higress Console: https://github.com/higress-group/higress-console
-- Higress Standalone: https://github.com/higress-group/higress-standalone
\ No newline at end of file
+- Higress Standalone: https://github.com/higress-group/higress-standalone
From 7e6168a644c0c9c625fbe29331d83e00a48da2fd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=BE=84=E6=BD=AD?=
Date: Thu, 17 Oct 2024 14:31:26 +0800
Subject: [PATCH 11/64] Update README.md
---
README.md | 24 +++++++++++++++++-------
1 file changed, 17 insertions(+), 7 deletions(-)
diff --git a/README.md b/README.md
index 834b787a5d..5ef491b5f3 100644
--- a/README.md
+++ b/README.md
@@ -21,14 +21,15 @@
-Higress 是基于阿里内部多年的 Envoy Gateway 实践沉淀,以开源 [Istio](https://github.com/istio/istio) 与 [Envoy](https://github.com/envoyproxy/envoy) 为核心构建的云原生 API 网关。
+Higress 是一款云原生 API 网关,内核基于 Istio 和 Envoy,可以用 Go/Rust/JS 等编写 Wasm 插件,提供了数十个现成的通用插件,以及开箱即用的控制台(demo 点[这里](http://demo.higress.io/))
-Higress 在阿里内部作为 AI 网关,承载了通义千问 APP、百炼大模型 API、机器学习 PAI 平台等 AI 业务的流量。
+Higress 在阿里内部为解决 Tengine reload 对长连接业务有损,以及 gRPC/Dubbo 负载均衡能力不足而诞生。
-Higress 能够用统一的协议对接国内外所有 LLM 模型厂商,同时具备丰富的 AI 可观测、多模型负载均衡/fallback、AI token 流控、AI 缓存等能力:
+阿里云基于 Higress 构建了云原生 API 网关产品,为大量企业客户提供 99.99% 的网关高可用保障服务能力。
-![](https://img.alicdn.com/imgextra/i1/O1CN01fNnhCp1cV8mYPRFeS_!!6000000003605-0-tps-1080-608.jpg)
+Higress 基于 AI 网关能力,支撑了通义千问 APP、百炼大模型 API、机器学习 PAI 平台等 AI 业务。同时服务国内头部的 AIGC 企业(如零一万物),以及 AI 产品(如 FastGPT)
+![](https://img.alicdn.com/imgextra/i2/O1CN011AbR8023V8R5N0HcA_!!6000000007260-2-tps-1080-606.png)
## Summary
@@ -67,23 +68,32 @@ K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start
- **AI 网关**:
- Higress 提供了一站式的 AI 插件集,可以增强依赖 AI 能力业务的稳定性、灵活性、可观测性,使得业务与 AI 的集成更加便捷和高效。
+ Higress 能够用统一的协议对接国内外所有 LLM 模型厂商,同时具备丰富的 AI 可观测、多模型负载均衡/fallback、AI token 流控、AI 缓存等能力:
+
+ ![](https://img.alicdn.com/imgextra/i1/O1CN01fNnhCp1cV8mYPRFeS_!!6000000003605-0-tps-1080-608.jpg)
- **Kubernetes Ingress 网关**:
Higress 可以作为 K8s 集群的 Ingress 入口网关, 并且兼容了大量 K8s Nginx Ingress 的注解,可以从 K8s Nginx Ingress 快速平滑迁移到 Higress。
支持 [Gateway API](https://gateway-api.sigs.k8s.io/) 标准,支持用户从 Ingress API 平滑迁移到 Gateway API。
+
+ 相比 ingress-nginx,资源开销大幅下降,路由变更生效速度有十倍提升:
+
+ ![](https://img.alicdn.com/imgextra/i1/O1CN01bhEtb229eeMNBWmdP_!!6000000008093-2-tps-750-547.png)
+ ![](https://img.alicdn.com/imgextra/i1/O1CN01bqRets1LsBGyitj4S_!!6000000001354-2-tps-887-489.png)
- **微服务网关**:
Higress 可以作为微服务网关, 能够对接多种类型的注册中心发现服务配置路由,例如 Nacos, ZooKeeper, Consul, Eureka 等。
并且深度集成了 [Dubbo](https://github.com/apache/dubbo), [Nacos](https://github.com/alibaba/nacos), [Sentinel](https://github.com/alibaba/Sentinel) 等微服务技术栈,基于 Envoy C++ 网关内核的出色性能,相比传统 Java 类微服务网关,可以显著降低资源使用率,减少成本。
+
+ ![](https://img.alicdn.com/imgextra/i4/O1CN01v4ZbCj1dBjePSMZ17_!!6000000003698-0-tps-1613-926.jpg)
- **安全防护网关**:
- Higress 可以作为安全防护网关, 提供 WAF 的能力,并且支持多种认证鉴权策略,例如 key-auth, hmac-auth, jwt-auth, basic-auth, oidc 等。
+ Higress 可以作为安全防护网关, 提供 WAF 的能力,并且支持多种认证鉴权策略,例如 key-auth, hmac-auth, jwt-auth, basic-auth, oidc 等。
## 核心优势
@@ -176,4 +186,4 @@ K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start
### 关联仓库
- Higress 控制台:https://github.com/higress-group/higress-console
-- Higress(独立运行版):https://github.com/higress-group/higress-standalone
\ No newline at end of file
+- Higress(独立运行版):https://github.com/higress-group/higress-standalone
From 299621476f61389f2819ffe0df260d7ab28c0c2c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=BE=84=E6=BD=AD?=
Date: Thu, 17 Oct 2024 14:33:08 +0800
Subject: [PATCH 12/64] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 5ef491b5f3..6495691fe4 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
[![license](https://img.shields.io/github/license/alibaba/higress.svg)](https://www.apache.org/licenses/LICENSE-2.0.html)
[**官网**](https://higress.cn/) |
- [**文档**](https://higress.cn/docs/latest/user/quickstart/) |
+ [**文档**](https://higress.cn/docs/latest/overview/what-is-higress/) |
[**博客**](https://higress.cn/blog/) |
[**电子书**](https://higress.cn/docs/ebook/wasm14/) |
[**开发指引**](https://higress.cn/docs/latest/dev/architecture/) |
From c67f494b497fc074a7eca398e9f8a7205f02c1d1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=BE=84=E6=BD=AD?=
Date: Fri, 18 Oct 2024 09:54:10 +0800
Subject: [PATCH 13/64] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 6495691fe4..87468ed9ad 100644
--- a/README.md
+++ b/README.md
@@ -175,7 +175,7 @@ K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start
### 交流群
-![image](https://img.alicdn.com/imgextra/i2/O1CN01qPd7Ix1uZPVEsWjWp_!!6000000006051-0-tps-720-405.jpg)
+![image](https://img.alicdn.com/imgextra/i2/O1CN01BkopaB22ZsvamFftE_!!6000000007135-0-tps-720-405.jpg)
### 技术分享
From 11ff2d1d31594a73d5b870de039ff52a066ad58d Mon Sep 17 00:00:00 2001
From: mamba <371510756@qq.com>
Date: Fri, 18 Oct 2024 13:58:52 +0800
Subject: [PATCH 14/64] [frontend-gray] support grayKey from localStorage
(#1395)
---
.../extensions/frontend-gray/README.md | 25 ++++++++++
.../extensions/frontend-gray/config/config.go | 27 ++++++----
.../extensions/frontend-gray/envoy.yaml | 16 +++---
.../wasm-go/extensions/frontend-gray/main.go | 50 +++++++++++--------
.../extensions/frontend-gray/util/utils.go | 16 +++++-
.../frontend-gray/util/utils_test.go | 24 +++++++++
6 files changed, 115 insertions(+), 43 deletions(-)
diff --git a/plugins/wasm-go/extensions/frontend-gray/README.md b/plugins/wasm-go/extensions/frontend-gray/README.md
index 8dee32008b..dba4b73c0f 100644
--- a/plugins/wasm-go/extensions/frontend-gray/README.md
+++ b/plugins/wasm-go/extensions/frontend-gray/README.md
@@ -17,6 +17,7 @@ description: 前端灰度插件配置参考
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|----------------|--------------|----|-----|----------------------------------------------------------------------------------------------------|
| `grayKey` | string | 非必填 | - | 用户ID的唯一标识,可以来自Cookie或者Header中,比如 userid,如果没有填写则使用`rules[].grayTagKey`和`rules[].grayTagValue`过滤灰度规则 |
+| `localStorageGrayKey` | string | 非必填 | - | 使用JWT鉴权方式,用户ID的唯一标识来自`localStorage`中,如果配置了当前参数,则`grayKey`失效 |
| `graySubKey` | string | 非必填 | - | 用户身份信息可能以JSON形式透出,比如:`userInfo:{ userCode:"001" }`,当前例子`graySubKey`取值为`userCode` |
| `userStickyMaxAge` | int | 非必填 | 172800 | 用户粘滞的时长:单位为秒,默认为`172800`,2天时间 |
| `rules` | array of object | 必填 | - | 用户定义不同的灰度规则,适配不同的灰度场景 |
@@ -168,6 +169,30 @@ cookie存在`appInfo`的JSON数据,其中包含`userId`字段为当前的唯
否则使用`version: base`版本
+### 用户信息存储在LocalStorage
+由于网关插件需要识别用户为唯一身份信息,HTTP协议进行信息传输,只能在Header中传递。如果用户信息存储在LocalStorage,在首页注入一段脚本将LocalStorage中的用户信息设置到cookie中。
+```
+(function() {
+ var grayKey = '@@X_GRAY_KEY';
+ var cookies = document.cookie.split('; ').filter(function(row) {
+ return row.indexOf(grayKey + '=') === 0;
+ });
+
+ try {
+ if (typeof localStorage !== 'undefined' && localStorage !== null) {
+ var storageValue = localStorage.getItem(grayKey);
+ var cookieValue = cookies.length > 0 ? decodeURIComponent(cookies[0].split('=')[1]) : null;
+ if (storageValue && storageValue.indexOf('=') < 0 && cookieValue && cookieValue !== storageValue) {
+ document.cookie = grayKey + '=' + encodeURIComponent(storageValue) + '; path=/;';
+ window.location.reload();
+ }
+ }
+ } catch (error) {
+ // xx
+ }
+})();
+```
+
### rewrite重写配置
> 一般用于CDN部署场景
```yml
diff --git a/plugins/wasm-go/extensions/frontend-gray/config/config.go b/plugins/wasm-go/extensions/frontend-gray/config/config.go
index de689aad2a..ecfbb3a836 100644
--- a/plugins/wasm-go/extensions/frontend-gray/config/config.go
+++ b/plugins/wasm-go/extensions/frontend-gray/config/config.go
@@ -49,17 +49,18 @@ type BodyInjection struct {
}
type GrayConfig struct {
- UserStickyMaxAge string
- TotalGrayWeight int
- GrayKey string
- GraySubKey string
- Rules []*GrayRule
- Rewrite *Rewrite
- Html string
- BaseDeployment *Deployment
- GrayDeployments []*Deployment
- BackendGrayTag string
- Injection *Injection
+ UserStickyMaxAge string
+ TotalGrayWeight int
+ GrayKey string
+ LocalStorageGrayKey string
+ GraySubKey string
+ Rules []*GrayRule
+ Rewrite *Rewrite
+ Html string
+ BaseDeployment *Deployment
+ GrayDeployments []*Deployment
+ BackendGrayTag string
+ Injection *Injection
}
func convertToStringList(results []gjson.Result) []string {
@@ -81,7 +82,11 @@ func convertToStringMap(result gjson.Result) map[string]string {
func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) {
// 解析 GrayKey
+ grayConfig.LocalStorageGrayKey = json.Get("localStorageGrayKey").String()
grayConfig.GrayKey = json.Get("grayKey").String()
+ if grayConfig.LocalStorageGrayKey != "" {
+ grayConfig.GrayKey = grayConfig.LocalStorageGrayKey
+ }
grayConfig.GraySubKey = json.Get("graySubKey").String()
grayConfig.BackendGrayTag = json.Get("backendGrayTag").String()
grayConfig.UserStickyMaxAge = json.Get("userStickyMaxAge").String()
diff --git a/plugins/wasm-go/extensions/frontend-gray/envoy.yaml b/plugins/wasm-go/extensions/frontend-gray/envoy.yaml
index 6dabed21de..239e221bd7 100644
--- a/plugins/wasm-go/extensions/frontend-gray/envoy.yaml
+++ b/plugins/wasm-go/extensions/frontend-gray/envoy.yaml
@@ -73,23 +73,22 @@ static_resources:
],
"rewrite": {
"host": "frontend-gray-cn-shanghai.oss-cn-shanghai-internal.aliyuncs.com",
- "notFoundUri": "/cygtapi/{version}/333.html",
"indexRouting": {
- "/app1": "/cygtapi/{version}/index.html",
- "/": "/cygtapi/{version}/index.html"
+ "/app1": "/mfe/app1/{version}/index.html",
+ "/": "/mfe/app1/{version}/index.html"
},
"fileRouting": {
- "/": "/cygtapi/{version}",
- "/app1": "/cygtapi/{version}"
+ "/": "/mfe/app1/{version}",
+ "/app1": "/mfe/app1/{version}"
}
},
"baseDeployment": {
- "version": "base"
+ "version": "dev"
},
"grayDeployments": [
{
"name": "beta-user",
- "version": "gray",
+ "version": "0.0.1",
"enabled": true
}
],
@@ -107,8 +106,7 @@ static_resources:
""
]
}
- },
- "html": "\n \n\napp1\n\n\n\n\t测试替换html版本\n\t
\n\t版本: {version}\n\t
\n\t\n\n"
+ }
}
- name: envoy.filters.http.router
typed_config:
diff --git a/plugins/wasm-go/extensions/frontend-gray/main.go b/plugins/wasm-go/extensions/frontend-gray/main.go
index 81eb3034da..b1b5d28aff 100644
--- a/plugins/wasm-go/extensions/frontend-gray/main.go
+++ b/plugins/wasm-go/extensions/frontend-gray/main.go
@@ -21,14 +21,13 @@ func main() {
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
wrapper.ProcessResponseHeadersBy(onHttpResponseHeader),
wrapper.ProcessResponseBodyBy(onHttpResponseBody),
- wrapper.ProcessStreamingResponseBodyBy(onStreamingResponseBody),
)
}
func parseConfig(json gjson.Result, grayConfig *config.GrayConfig, log wrapper.Log) error {
// 解析json 为GrayConfig
config.JsonToGrayConfig(json, grayConfig)
- log.Infof("Rewrite: %v, GrayDeployments: %v", json.Get("rewrite"), json.Get("grayDeployments"))
+ log.Debugf("Rewrite: %v, GrayDeployments: %v", json.Get("rewrite"), json.Get("grayDeployments"))
return nil
}
@@ -98,15 +97,17 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig,
} else {
rewritePath = util.PrefixFileRewrite(path, deployment.Version, grayConfig.Rewrite.File)
}
- log.Infof("rewrite path: %s %s %v", path, deployment.Version, rewritePath)
- proxywasm.ReplaceHttpRequestHeader(":path", rewritePath)
+ if path != rewritePath {
+ log.Infof("rewrite path:%s, rewritePath:%s, Version:%v", path, rewritePath, deployment.Version)
+ proxywasm.ReplaceHttpRequestHeader(":path", rewritePath)
+ }
}
-
return types.ActionContinue
}
func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action {
if !util.IsGrayEnabled(grayConfig) {
+ ctx.DontReadResponseBody()
return types.ActionContinue
}
isPageRequest, ok := ctx.GetContext(config.IsPageRequest).(bool)
@@ -117,6 +118,9 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig,
if !isPageRequest {
ctx.DontReadResponseBody()
return types.ActionContinue
+ } else {
+ // 不会进去Streaming 的Body处理
+ ctx.BufferResponseBody()
}
status, err := proxywasm.GetHttpResponseHeader(":status")
@@ -159,8 +163,6 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig,
return types.ActionContinue
}
- // 不会进去Streaming 的Body处理
- ctx.BufferResponseBody()
proxywasm.ReplaceHttpResponseHeader("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate")
frontendVersion := ctx.GetContext(config.XPreHigressTag).(string)
@@ -184,6 +186,11 @@ func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, b
if !ok {
isPageRequest = false // 默认值
}
+ // 只处理首页相关请求
+ if !isPageRequest {
+ return types.ActionContinue
+ }
+
frontendVersion := ctx.GetContext(config.XPreHigressTag).(string)
isNotFound, ok := ctx.GetContext(config.IsNotFound).(bool)
if !ok {
@@ -212,7 +219,8 @@ func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, b
return types.ActionContinue
}
- if isPageRequest && isNotFound && grayConfig.Rewrite.Host != "" && grayConfig.Rewrite.NotFound != "" {
+ // 针对404页面处理
+ if isNotFound && grayConfig.Rewrite.Host != "" && grayConfig.Rewrite.NotFound != "" {
client := wrapper.NewClusterClient(wrapper.RouteCluster{Host: grayConfig.Rewrite.Host})
client.Get(strings.Replace(grayConfig.Rewrite.NotFound, "{version}", frontendVersion, -1), nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
@@ -222,20 +230,18 @@ func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, b
return types.ActionPause
}
- if isPageRequest {
- // 将原始字节转换为字符串
- newBody := string(body)
-
- newBody = util.InjectContent(newBody, grayConfig.Injection)
-
- if err := proxywasm.ReplaceHttpResponseBody([]byte(newBody)); err != nil {
- return types.ActionContinue
- }
+ // 处理响应体HTML
+ newBody := string(body)
+ newBody = util.InjectContent(newBody, grayConfig.Injection)
+ if grayConfig.LocalStorageGrayKey != "" {
+ localStr := strings.ReplaceAll(`
+ `, "@@X_GRAY_KEY", grayConfig.LocalStorageGrayKey)
+ newBody = strings.ReplaceAll(newBody, "", "\n"+localStr)
+ }
+ if err := proxywasm.ReplaceHttpResponseBody([]byte(newBody)); err != nil {
+ return types.ActionContinue
}
-
return types.ActionContinue
}
-
-func onStreamingResponseBody(ctx wrapper.HttpContext, pluginConfig config.GrayConfig, chunk []byte, isLastChunk bool, log wrapper.Log) []byte {
- return chunk
-}
diff --git a/plugins/wasm-go/extensions/frontend-gray/util/utils.go b/plugins/wasm-go/extensions/frontend-gray/util/utils.go
index 80291a2c3c..e67d3e0893 100644
--- a/plugins/wasm-go/extensions/frontend-gray/util/utils.go
+++ b/plugins/wasm-go/extensions/frontend-gray/util/utils.go
@@ -142,8 +142,22 @@ func IsPageRequest(fetchMode string, myPath string) bool {
// 首页Rewrite
func IndexRewrite(path, version string, matchRules map[string]string) string {
- for prefix, rewrite := range matchRules {
+ // Create a slice of keys in matchRules and sort them by length in descending order
+ keys := make([]string, 0, len(matchRules))
+ for prefix := range matchRules {
+ keys = append(keys, prefix)
+ }
+ sort.Slice(keys, func(i, j int) bool {
+ if len(keys[i]) != len(keys[j]) {
+ return len(keys[i]) > len(keys[j]) // Sort by length
+ }
+ return keys[i] < keys[j] // Sort lexicographically
+ })
+
+ // Iterate over sorted keys to find the longest match
+ for _, prefix := range keys {
if strings.HasPrefix(path, prefix) {
+ rewrite := matchRules[prefix]
newPath := strings.Replace(rewrite, "{version}", version, -1)
return newPath
}
diff --git a/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go b/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go
index d51e17f323..b6681c98af 100644
--- a/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go
+++ b/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go
@@ -53,6 +53,30 @@ func TestIndexRewrite(t *testing.T) {
}
}
+func TestIndexRewrite2(t *testing.T) {
+ matchRules := map[string]string{
+ "/": "/{version}/index.html",
+ "/sta": "/sta/{version}/index.html",
+ "/static": "/static/{version}/index.html",
+ }
+
+ var tests = []struct {
+ path, output string
+ }{
+ {"/static123", "/static/v1.0.0/index.html"},
+ {"/static", "/static/v1.0.0/index.html"},
+ {"/sta", "/sta/v1.0.0/index.html"},
+ {"/", "/v1.0.0/index.html"},
+ }
+ for _, test := range tests {
+ testName := test.path
+ t.Run(testName, func(t *testing.T) {
+ output := IndexRewrite(testName, "v1.0.0", matchRules)
+ assert.Equal(t, test.output, output)
+ })
+ }
+}
+
func TestPrefixFileRewrite(t *testing.T) {
matchRules := map[string]string{
// 前缀匹配
From 49bb5ec2b9f9ba628667a8a95d498e33f56e8459 Mon Sep 17 00:00:00 2001
From: Lisheng Zheng
Date: Fri, 18 Oct 2024 15:34:34 +0800
Subject: [PATCH 15/64] fix: add HTTP2 protocol options to skywalking and otel
cluster (#1379)
---
Makefile.core.mk | 8 +-
pkg/ingress/kube/configmap/tracing.go | 145 +++++++++++++++++---------
2 files changed, 102 insertions(+), 51 deletions(-)
diff --git a/Makefile.core.mk b/Makefile.core.mk
index 55a9662f70..b790d3c878 100644
--- a/Makefile.core.mk
+++ b/Makefile.core.mk
@@ -72,17 +72,17 @@ go.test.coverage: prebuild
.PHONY: build
build: prebuild $(OUT)
- GOPROXY=$(GOPROXY) GOOS=$(GOOS_LOCAL) GOARCH=$(GOARCH_LOCAL) LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh $(OUT)/ $(HIGRESS_BINARIES)
+ GOPROXY="$(GOPROXY)" GOOS=$(GOOS_LOCAL) GOARCH=$(GOARCH_LOCAL) LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh $(OUT)/ $(HIGRESS_BINARIES)
.PHONY: build-linux
build-linux: prebuild $(OUT)
- GOPROXY=$(GOPROXY) GOOS=linux GOARCH=$(GOARCH_LOCAL) LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh $(OUT_LINUX)/ $(HIGRESS_BINARIES)
+ GOPROXY="$(GOPROXY)" GOOS=linux GOARCH=$(GOARCH_LOCAL) LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh $(OUT_LINUX)/ $(HIGRESS_BINARIES)
$(AMD64_OUT_LINUX)/higress:
- GOPROXY=$(GOPROXY) GOOS=linux GOARCH=amd64 LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh ./out/linux_amd64/ $(HIGRESS_BINARIES)
+ GOPROXY="$(GOPROXY)" GOOS=linux GOARCH=amd64 LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh ./out/linux_amd64/ $(HIGRESS_BINARIES)
$(ARM64_OUT_LINUX)/higress:
- GOPROXY=$(GOPROXY) GOOS=linux GOARCH=arm64 LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh ./out/linux_arm64/ $(HIGRESS_BINARIES)
+ GOPROXY="$(GOPROXY)" GOOS=linux GOARCH=arm64 LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh ./out/linux_arm64/ $(HIGRESS_BINARIES)
.PHONY: build-hgctl
build-hgctl: prebuild $(OUT)
diff --git a/pkg/ingress/kube/configmap/tracing.go b/pkg/ingress/kube/configmap/tracing.go
index 209c610d8f..0529ccf858 100644
--- a/pkg/ingress/kube/configmap/tracing.go
+++ b/pkg/ingress/kube/configmap/tracing.go
@@ -255,69 +255,120 @@ func (t *TracingController) ConstructEnvoyFilters() ([]*config.Config, error) {
return configs, nil
}
- config := &config.Config{
- Meta: config.Meta{
- GroupVersionKind: gvk.EnvoyFilter,
- Name: higressTracingEnvoyFilterName,
- Namespace: namespace,
- },
- Spec: &networking.EnvoyFilter{
- ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
- {
- ApplyTo: networking.EnvoyFilter_NETWORK_FILTER,
- Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
- Context: networking.EnvoyFilter_GATEWAY,
- ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
- Listener: &networking.EnvoyFilter_ListenerMatch{
- FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
- Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
- Name: "envoy.filters.network.http_connection_manager",
- },
- },
+ configPatches := []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
+ {
+ ApplyTo: networking.EnvoyFilter_NETWORK_FILTER,
+ Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
+ Context: networking.EnvoyFilter_GATEWAY,
+ ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
+ Listener: &networking.EnvoyFilter_ListenerMatch{
+ FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
+ Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
+ Name: "envoy.filters.network.http_connection_manager",
},
},
},
- Patch: &networking.EnvoyFilter_Patch{
- Operation: networking.EnvoyFilter_Patch_MERGE,
- Value: util.BuildPatchStruct(tracingConfig),
- },
},
- {
- ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
- Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
- Context: networking.EnvoyFilter_GATEWAY,
- ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
- Listener: &networking.EnvoyFilter_ListenerMatch{
- FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
- Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
- Name: "envoy.filters.network.http_connection_manager",
- SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{
- Name: "envoy.filters.http.router",
- },
- },
+ },
+ Patch: &networking.EnvoyFilter_Patch{
+ Operation: networking.EnvoyFilter_Patch_MERGE,
+ Value: util.BuildPatchStruct(tracingConfig),
+ },
+ },
+ {
+ ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
+ Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
+ Context: networking.EnvoyFilter_GATEWAY,
+ ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
+ Listener: &networking.EnvoyFilter_ListenerMatch{
+ FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
+ Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
+ Name: "envoy.filters.network.http_connection_manager",
+ SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{
+ Name: "envoy.filters.http.router",
},
},
},
},
- Patch: &networking.EnvoyFilter_Patch{
- Operation: networking.EnvoyFilter_Patch_MERGE,
- Value: util.BuildPatchStruct(`{
+ },
+ },
+ Patch: &networking.EnvoyFilter_Patch{
+ Operation: networking.EnvoyFilter_Patch_MERGE,
+ Value: util.BuildPatchStruct(`{
"name":"envoy.filters.http.router",
"typed_config":{
"@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router",
"start_child_span": true
}
}`),
- },
- },
},
},
}
+ patches := t.constructTracingExtendPatches(tracing)
+ configPatches = append(configPatches, patches...)
+
+ config := &config.Config{
+ Meta: config.Meta{
+ GroupVersionKind: gvk.EnvoyFilter,
+ Name: higressTracingEnvoyFilterName,
+ Namespace: namespace,
+ },
+ Spec: &networking.EnvoyFilter{
+ ConfigPatches: configPatches,
+ },
+ }
+
configs = append(configs, config)
return configs, nil
}
+func tracingClusterName(port, service string) string {
+ return fmt.Sprintf("outbound|%s||%s", port, service)
+}
+
+func (t *TracingController) constructHTTP2ProtocolOptionsPatch(port, service string) *networking.EnvoyFilter_EnvoyConfigObjectPatch {
+ http2ProtocolOptions := `{"typed_extension_protocol_options": {
+ "envoy.extensions.upstreams.http.v3.HttpProtocolOptions": {
+ "@type": "type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions",
+ "explicit_http_config": {
+ "http2_protocol_options": {}
+ }
+ }
+}}`
+
+ return &networking.EnvoyFilter_EnvoyConfigObjectPatch{
+ ApplyTo: networking.EnvoyFilter_CLUSTER,
+ Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
+ Context: networking.EnvoyFilter_GATEWAY,
+ ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Cluster{
+ Cluster: &networking.EnvoyFilter_ClusterMatch{
+ Name: tracingClusterName(port, service),
+ },
+ },
+ },
+ Patch: &networking.EnvoyFilter_Patch{
+ Operation: networking.EnvoyFilter_Patch_MERGE,
+ Value: util.BuildPatchStruct(http2ProtocolOptions),
+ },
+ }
+}
+
+func (t *TracingController) constructTracingExtendPatches(tracing *Tracing) []*networking.EnvoyFilter_EnvoyConfigObjectPatch {
+ if tracing == nil {
+ return nil
+ }
+ var patches []*networking.EnvoyFilter_EnvoyConfigObjectPatch
+ if skywalking := tracing.Skywalking; skywalking != nil {
+ patches = append(patches, t.constructHTTP2ProtocolOptionsPatch(skywalking.Port, skywalking.Service))
+ }
+ if otel := tracing.OpenTelemetry; otel != nil {
+ patches = append(patches, t.constructHTTP2ProtocolOptionsPatch(otel.Port, otel.Service))
+ }
+
+ return patches
+}
+
func (t *TracingController) constructTracingTracer(tracing *Tracing, namespace string) string {
tracingConfig := ""
timeout := float32(tracing.Timeout) / 1000
@@ -338,7 +389,7 @@ func (t *TracingController) constructTracingTracer(tracing *Tracing, namespace s
},
"grpc_service": {
"envoy_grpc": {
- "cluster_name": "outbound|%s||%s"
+ "cluster_name": "%s"
},
"timeout": "%.3fs"
}
@@ -349,7 +400,7 @@ func (t *TracingController) constructTracingTracer(tracing *Tracing, namespace s
}
}
}
-}`, namespace, skywalking.AccessToken, skywalking.Port, skywalking.Service, timeout, tracing.Sampling)
+}`, namespace, skywalking.AccessToken, tracingClusterName(skywalking.Port, skywalking.Service), timeout, tracing.Sampling)
}
if tracing.Zipkin != nil {
@@ -363,7 +414,7 @@ func (t *TracingController) constructTracingTracer(tracing *Tracing, namespace s
"name": "envoy.tracers.zipkin",
"typed_config": {
"@type": "type.googleapis.com/envoy.config.trace.v3.ZipkinConfig",
- "collector_cluster": "outbound|%s||%s",
+ "collector_cluster": "%s",
"collector_endpoint": "/api/v2/spans",
"collector_hostname": "higress-gateway",
"collector_endpoint_version": "HTTP_JSON",
@@ -375,7 +426,7 @@ func (t *TracingController) constructTracingTracer(tracing *Tracing, namespace s
}
}
}
-}`, zipkin.Port, zipkin.Service, tracing.Sampling)
+}`, tracingClusterName(zipkin.Port, zipkin.Service), tracing.Sampling)
}
if tracing.OpenTelemetry != nil {
@@ -392,7 +443,7 @@ func (t *TracingController) constructTracingTracer(tracing *Tracing, namespace s
"service_name": "higress-gateway.%s",
"grpc_service": {
"envoy_grpc": {
- "cluster_name": "outbound|%s||%s"
+ "cluster_name": "%s"
},
"timeout": "%.3fs"
}
@@ -403,7 +454,7 @@ func (t *TracingController) constructTracingTracer(tracing *Tracing, namespace s
}
}
}
-}`, namespace, opentelemetry.Port, opentelemetry.Service, timeout, tracing.Sampling)
+}`, namespace, tracingClusterName(opentelemetry.Port, opentelemetry.Service), timeout, tracing.Sampling)
}
return tracingConfig
}
From 32e5a59ae01e14c5f2accc823e5401c0bce2bb8e Mon Sep 17 00:00:00 2001
From: rinfx <893383980@qq.com>
Date: Fri, 18 Oct 2024 16:32:48 +0800
Subject: [PATCH 16/64] fix special charactor handle in ai-security-guard
plugin (#1394)
---
.../extensions/ai-security-guard/main.go | 27 ++++++++++---------
1 file changed, 14 insertions(+), 13 deletions(-)
diff --git a/plugins/wasm-go/extensions/ai-security-guard/main.go b/plugins/wasm-go/extensions/ai-security-guard/main.go
index 3051fe22ea..ca59f7f6a1 100644
--- a/plugins/wasm-go/extensions/ai-security-guard/main.go
+++ b/plugins/wasm-go/extensions/ai-security-guard/main.go
@@ -81,7 +81,7 @@ func (config *AISecurityConfig) incrementCounter(metricName string, inc uint64)
func urlEncoding(rawStr string) string {
encodedStr := url.PathEscape(rawStr)
- encodedStr = strings.ReplaceAll(encodedStr, "+", "%20")
+ encodedStr = strings.ReplaceAll(encodedStr, "+", "%2B")
encodedStr = strings.ReplaceAll(encodedStr, ":", "%3A")
encodedStr = strings.ReplaceAll(encodedStr, "=", "%3D")
encodedStr = strings.ReplaceAll(encodedStr, "&", "%26")
@@ -106,7 +106,7 @@ func getSign(params map[string]string, secret string) string {
})
canonicalStr := strings.Join(paramArray, "&")
signStr := "POST&%2F&" + urlEncoding(canonicalStr)
- // proxywasm.LogInfo(signStr)
+ proxywasm.LogDebugf("String to sign is: %s", signStr)
return hmacSha1(signStr, secret)
}
@@ -196,10 +196,11 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config AISecurityConfig, log
func onHttpRequestBody(ctx wrapper.HttpContext, config AISecurityConfig, body []byte, log wrapper.Log) types.Action {
log.Debugf("checking request body...")
- content := gjson.GetBytes(body, config.requestContentJsonPath).String()
+ content := gjson.GetBytes(body, config.requestContentJsonPath).Raw
model := gjson.GetBytes(body, "model").Raw
ctx.SetContext("requestModel", model)
- if content != "" {
+ log.Debugf("Raw response content is: %s", content)
+ if len(content) > 0 {
timestamp := time.Now().UTC().Format("2006-01-02T15:04:05Z")
randomID, _ := generateHexID(16)
params := map[string]string{
@@ -212,7 +213,7 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config AISecurityConfig, body []
"AccessKeyId": config.ak,
"Timestamp": timestamp,
"Service": config.requestCheckService,
- "ServiceParameters": `{"content": "` + content + `"}`,
+ "ServiceParameters": fmt.Sprintf(`{"content": %s}`, content),
}
signature := getSign(params, config.sk+"&")
reqParams := url.Values{}
@@ -339,7 +340,7 @@ func onHttpResponseBody(ctx wrapper.HttpContext, config AISecurityConfig, body [
if isStreamingResponse {
content = extractMessageFromStreamingBody(body, config.responseStreamContentJsonPath)
} else {
- content = gjson.GetBytes(body, config.responseContentJsonPath).String()
+ content = gjson.GetBytes(body, config.responseContentJsonPath).Raw
}
log.Debugf("Raw response content is: %s", content)
if len(content) > 0 {
@@ -355,7 +356,7 @@ func onHttpResponseBody(ctx wrapper.HttpContext, config AISecurityConfig, body [
"AccessKeyId": config.ak,
"Timestamp": timestamp,
"Service": config.responseCheckService,
- "ServiceParameters": `{"content": "` + content + `"}`,
+ "ServiceParameters": fmt.Sprintf(`{"content": %s}`, content),
}
signature := getSign(params, config.sk+"&")
reqParams := url.Values{}
@@ -400,10 +401,10 @@ func onHttpResponseBody(ctx wrapper.HttpContext, config AISecurityConfig, body [
jsonData = []byte(denyMessage)
} else if strings.Contains(strings.Join(hdsMap["content-type"], ";"), "event-stream") {
randomID := generateRandomID()
- jsonData = []byte(fmt.Sprintf(OpenAIStreamResponseFormat, randomID, model, respAdvice.Array()[0].Get("Answer").String(), randomID, model))
+ jsonData = []byte(fmt.Sprintf(OpenAIStreamResponseFormat, randomID, model, denyMessage, randomID, model))
} else {
randomID := generateRandomID()
- jsonData = []byte(fmt.Sprintf(OpenAIResponseFormat, randomID, model, respAdvice.Array()[0].Get("Answer").String()))
+ jsonData = []byte(fmt.Sprintf(OpenAIResponseFormat, randomID, model, denyMessage))
}
delete(hdsMap, "content-length")
hdsMap[":status"] = []string{fmt.Sprint(config.denyCode)}
@@ -432,10 +433,10 @@ func extractMessageFromStreamingBody(data []byte, jsonPath string) string {
strChunks := []string{}
for _, chunk := range chunks {
// Example: "choices":[{"index":0,"delta":{"role":"assistant","content":"%s"},"logprobs":null,"finish_reason":null}]
- jsonObj := gjson.GetBytes(chunk, jsonPath)
- if jsonObj.Exists() {
- strChunks = append(strChunks, jsonObj.String())
+ jsonRaw := gjson.GetBytes(chunk, jsonPath).Raw
+ if len(jsonRaw) > 2 {
+ strChunks = append(strChunks, jsonRaw[1:len(jsonRaw)-1])
}
}
- return strings.Join(strChunks, "")
+ return fmt.Sprintf(`"%s"`, strings.Join(strChunks, ""))
}
From d96994767c92a1fe4e802359508ab4a20eddfc9e Mon Sep 17 00:00:00 2001
From: 007gzs <007gzs@gmail.com>
Date: Mon, 21 Oct 2024 09:44:01 +0800
Subject: [PATCH 17/64] Change http_content to Rc in HttpWrapper (#1391)
---
plugins/wasm-rust/Cargo.lock | 50 +++--
plugins/wasm-rust/Cargo.toml | 1 +
.../extensions/ai-data-masking/Cargo.lock | 137 +++++++------
.../extensions/ai-data-masking/src/lib.rs | 4 +-
.../wasm-rust/extensions/demo-wasm/Cargo.lock | 50 +++--
.../wasm-rust/extensions/demo-wasm/src/lib.rs | 109 +++++------
.../extensions/request-block/Cargo.lock | 93 ++++++---
.../extensions/request-block/src/lib.rs | 8 +-
.../wasm-rust/extensions/say-hello/Cargo.lock | 81 ++++++--
plugins/wasm-rust/src/plugin_wrapper.rs | 184 ++++++++++--------
plugins/wasm-rust/src/rule_matcher.rs | 25 +--
11 files changed, 442 insertions(+), 300 deletions(-)
diff --git a/plugins/wasm-rust/Cargo.lock b/plugins/wasm-rust/Cargo.lock
index 899d559b0d..9cf5140c77 100644
--- a/plugins/wasm-rust/Cargo.lock
+++ b/plugins/wasm-rust/Cargo.lock
@@ -32,6 +32,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+[[package]]
+name = "downcast-rs"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+
[[package]]
name = "fnv"
version = "1.0.7"
@@ -63,6 +69,7 @@ dependencies = [
name = "higress-wasm-rust"
version = "0.1.0"
dependencies = [
+ "downcast-rs",
"http",
"lazy_static",
"multimap",
@@ -97,9 +104,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
-version = "0.2.155"
+version = "0.2.159"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
+checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
[[package]]
name = "log"
@@ -124,15 +131,15 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.19.0"
+version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "proc-macro2"
-version = "1.0.86"
+version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
+checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
dependencies = [
"unicode-ident",
]
@@ -140,17 +147,18 @@ dependencies = [
[[package]]
name = "proxy-wasm"
version = "0.2.2"
-source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#73833051f57d483570cf5aaa9d62bd7402fae63b"
+source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#6735737fad486c8a7cc324241f58df4a160e7887"
dependencies = [
+ "downcast-rs",
"hashbrown",
"log",
]
[[package]]
name = "quote"
-version = "1.0.36"
+version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
@@ -163,18 +171,18 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
-version = "1.0.207"
+version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2"
+checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.207"
+version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e"
+checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [
"proc-macro2",
"quote",
@@ -183,9 +191,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.125"
+version = "1.0.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
+checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
dependencies = [
"itoa",
"memchr",
@@ -195,9 +203,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.74"
+version = "2.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7"
+checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
dependencies = [
"proc-macro2",
"quote",
@@ -206,15 +214,15 @@ dependencies = [
[[package]]
name = "unicode-ident"
-version = "1.0.12"
+version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "uuid"
-version = "1.10.0"
+version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
+checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
dependencies = [
"getrandom",
]
diff --git a/plugins/wasm-rust/Cargo.toml b/plugins/wasm-rust/Cargo.toml
index a1e5472c63..e4fc274311 100644
--- a/plugins/wasm-rust/Cargo.toml
+++ b/plugins/wasm-rust/Cargo.toml
@@ -13,3 +13,4 @@ uuid = { version = "1.3.3", features = ["v4"] }
multimap = "0"
http = "1"
lazy_static = "1"
+downcast-rs="1"
diff --git a/plugins/wasm-rust/extensions/ai-data-masking/Cargo.lock b/plugins/wasm-rust/extensions/ai-data-masking/Cargo.lock
index 853fde94fb..914d5b9689 100644
--- a/plugins/wasm-rust/extensions/ai-data-masking/Cargo.lock
+++ b/plugins/wasm-rust/extensions/ai-data-masking/Cargo.lock
@@ -82,11 +82,17 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+[[package]]
+name = "bytes"
+version = "1.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
+
[[package]]
name = "cc"
-version = "1.1.11"
+version = "1.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fb8dd288a69fc53a1996d7ecfbf4a20d59065bff137ce7e56bbd620de191189"
+checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945"
dependencies = [
"shlex",
]
@@ -108,9 +114,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cpufeatures"
-version = "0.2.13"
+version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad"
+checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0"
dependencies = [
"libc",
]
@@ -162,18 +168,18 @@ dependencies = [
[[package]]
name = "derive_builder"
-version = "0.20.0"
+version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7"
+checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
-version = "0.20.0"
+version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d"
+checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
dependencies = [
"darling",
"proc-macro2",
@@ -183,9 +189,9 @@ dependencies = [
[[package]]
name = "derive_builder_macro"
-version = "0.20.0"
+version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b"
+checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
dependencies = [
"derive_builder_core",
"syn",
@@ -201,6 +207,12 @@ dependencies = [
"crypto-common",
]
+[[package]]
+name = "downcast-rs"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+
[[package]]
name = "fancy-regex"
version = "0.13.0"
@@ -278,6 +290,9 @@ dependencies = [
name = "higress-wasm-rust"
version = "0.1.0"
dependencies = [
+ "downcast-rs",
+ "http",
+ "lazy_static",
"multimap",
"proxy-wasm",
"serde",
@@ -285,6 +300,17 @@ dependencies = [
"uuid",
]
+[[package]]
+name = "http"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
[[package]]
name = "ident_case"
version = "1.0.1"
@@ -314,9 +340,9 @@ dependencies = [
[[package]]
name = "jsonpath-rust"
-version = "0.7.0"
+version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d64f9886fc067a709ab27faf63b7d3f4d1ec570a700705408b0b0683e2f43897"
+checksum = "514f8a353ad9e85443b30fefe169ce93794ec7c98054a4312ab08530f15b7bb3"
dependencies = [
"pest",
"pest_derive",
@@ -333,9 +359,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
-version = "0.2.155"
+version = "0.2.159"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
+checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
[[package]]
name = "log"
@@ -366,9 +392,9 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.19.0"
+version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "onig"
@@ -394,9 +420,9 @@ dependencies = [
[[package]]
name = "pest"
-version = "2.7.11"
+version = "2.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95"
+checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442"
dependencies = [
"memchr",
"thiserror",
@@ -405,9 +431,9 @@ dependencies = [
[[package]]
name = "pest_derive"
-version = "2.7.11"
+version = "2.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a"
+checksum = "d214365f632b123a47fd913301e14c946c61d1c183ee245fa76eb752e59a02dd"
dependencies = [
"pest",
"pest_generator",
@@ -415,9 +441,9 @@ dependencies = [
[[package]]
name = "pest_generator"
-version = "2.7.11"
+version = "2.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183"
+checksum = "eb55586734301717aea2ac313f50b2eb8f60d2fc3dc01d190eefa2e625f60c4e"
dependencies = [
"pest",
"pest_meta",
@@ -428,9 +454,9 @@ dependencies = [
[[package]]
name = "pest_meta"
-version = "2.7.11"
+version = "2.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f"
+checksum = "b75da2a70cf4d9cb76833c990ac9cd3923c9a8905a8929789ce347c84564d03d"
dependencies = [
"once_cell",
"pest",
@@ -477,15 +503,15 @@ dependencies = [
[[package]]
name = "pkg-config"
-version = "0.3.30"
+version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
+checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "proc-macro2"
-version = "1.0.86"
+version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
+checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
dependencies = [
"unicode-ident",
]
@@ -493,17 +519,18 @@ dependencies = [
[[package]]
name = "proxy-wasm"
version = "0.2.2"
-source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#73833051f57d483570cf5aaa9d62bd7402fae63b"
+source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#6735737fad486c8a7cc324241f58df4a160e7887"
dependencies = [
+ "downcast-rs",
"hashbrown",
"log",
]
[[package]]
name = "quote"
-version = "1.0.36"
+version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
@@ -525,9 +552,9 @@ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
[[package]]
name = "regex"
-version = "1.10.6"
+version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
+checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
dependencies = [
"aho-corasick",
"memchr",
@@ -537,9 +564,9 @@ dependencies = [
[[package]]
name = "regex-automata"
-version = "0.4.7"
+version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
+checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
dependencies = [
"aho-corasick",
"memchr",
@@ -548,9 +575,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
-version = "0.8.4"
+version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rust-embed"
@@ -603,18 +630,18 @@ dependencies = [
[[package]]
name = "serde"
-version = "1.0.207"
+version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2"
+checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.207"
+version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e"
+checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [
"proc-macro2",
"quote",
@@ -623,9 +650,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.125"
+version = "1.0.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
+checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
dependencies = [
"itoa",
"memchr",
@@ -670,9 +697,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
-version = "2.0.74"
+version = "2.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7"
+checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
dependencies = [
"proc-macro2",
"quote",
@@ -681,18 +708,18 @@ dependencies = [
[[package]]
name = "thiserror"
-version = "1.0.63"
+version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
+checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
-version = "1.0.63"
+version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
+checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
dependencies = [
"proc-macro2",
"quote",
@@ -707,21 +734,21 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "ucd-trie"
-version = "0.1.6"
+version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
+checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "unicode-ident"
-version = "1.0.12"
+version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "uuid"
-version = "1.10.0"
+version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
+checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
dependencies = [
"getrandom",
]
diff --git a/plugins/wasm-rust/extensions/ai-data-masking/src/lib.rs b/plugins/wasm-rust/extensions/ai-data-masking/src/lib.rs
index a6114bdd8b..01573585ac 100644
--- a/plugins/wasm-rust/extensions/ai-data-masking/src/lib.rs
+++ b/plugins/wasm-rust/extensions/ai-data-masking/src/lib.rs
@@ -65,7 +65,7 @@ struct AiDataMaskingRoot {
rule_matcher: SharedRuleMatcher,
}
struct AiDataMasking {
- config: Option,
+ config: Option>,
mask_map: HashMap>,
is_openai: bool,
stream: bool,
@@ -585,7 +585,7 @@ impl HttpContext for AiDataMasking {
}
}
impl HttpContextWrapper for AiDataMasking {
- fn on_config(&mut self, config: &AiDataMaskingConfig) {
+ fn on_config(&mut self, config: Rc) {
self.config = Some(config.clone());
}
fn cache_request_body(&self) -> bool {
diff --git a/plugins/wasm-rust/extensions/demo-wasm/Cargo.lock b/plugins/wasm-rust/extensions/demo-wasm/Cargo.lock
index 85e2edaea3..c1e197caf6 100644
--- a/plugins/wasm-rust/extensions/demo-wasm/Cargo.lock
+++ b/plugins/wasm-rust/extensions/demo-wasm/Cargo.lock
@@ -43,6 +43,12 @@ dependencies = [
"serde",
]
+[[package]]
+name = "downcast-rs"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+
[[package]]
name = "fnv"
version = "1.0.7"
@@ -74,6 +80,7 @@ dependencies = [
name = "higress-wasm-rust"
version = "0.1.0"
dependencies = [
+ "downcast-rs",
"http",
"lazy_static",
"multimap",
@@ -108,9 +115,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
-version = "0.2.157"
+version = "0.2.159"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "374af5f94e54fa97cf75e945cce8a6b201e88a1a07e688b47dfd2a59c66dbd86"
+checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
[[package]]
name = "log"
@@ -135,15 +142,15 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.19.0"
+version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "proc-macro2"
-version = "1.0.86"
+version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
+checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
dependencies = [
"unicode-ident",
]
@@ -151,17 +158,18 @@ dependencies = [
[[package]]
name = "proxy-wasm"
version = "0.2.2"
-source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#73833051f57d483570cf5aaa9d62bd7402fae63b"
+source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#6735737fad486c8a7cc324241f58df4a160e7887"
dependencies = [
+ "downcast-rs",
"hashbrown",
"log",
]
[[package]]
name = "quote"
-version = "1.0.36"
+version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
@@ -174,18 +182,18 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
-version = "1.0.208"
+version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2"
+checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.208"
+version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
+checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [
"proc-macro2",
"quote",
@@ -194,9 +202,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.125"
+version = "1.0.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
+checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
dependencies = [
"itoa",
"memchr",
@@ -206,9 +214,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.75"
+version = "2.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9"
+checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
dependencies = [
"proc-macro2",
"quote",
@@ -217,15 +225,15 @@ dependencies = [
[[package]]
name = "unicode-ident"
-version = "1.0.12"
+version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "uuid"
-version = "1.10.0"
+version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
+checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
dependencies = [
"getrandom",
]
diff --git a/plugins/wasm-rust/extensions/demo-wasm/src/lib.rs b/plugins/wasm-rust/extensions/demo-wasm/src/lib.rs
index 55647a83cc..62dfa055d0 100644
--- a/plugins/wasm-rust/extensions/demo-wasm/src/lib.rs
+++ b/plugins/wasm-rust/extensions/demo-wasm/src/lib.rs
@@ -1,8 +1,6 @@
use higress_wasm_rust::cluster_wrapper::DnsCluster;
use higress_wasm_rust::log::Log;
-use higress_wasm_rust::plugin_wrapper::{
- HttpCallArgStorage, HttpCallbackFn, HttpContextWrapper, RootContextWrapper,
-};
+use higress_wasm_rust::plugin_wrapper::{HttpContextWrapper, RootContextWrapper};
use higress_wasm_rust::rule_matcher::{on_configure, RuleMatcher, SharedRuleMatcher};
use http::Method;
use multimap::MultiMap;
@@ -12,7 +10,7 @@ use proxy_wasm::types::{Bytes, ContextType, DataAction, HeaderAction, LogLevel};
use serde::Deserialize;
use std::cell::RefCell;
use std::ops::DerefMut;
-use std::rc::Rc;
+use std::rc::{Rc, Weak};
use std::time::Duration;
proxy_wasm::main! {{
@@ -37,40 +35,27 @@ fn format_body(body: Option>) -> String {
format!("{:?}", body)
}
-fn test_callback(
- this: &mut DemoWasm,
- status_code: u16,
- headers: &MultiMap,
- body: Option>,
-) {
- this.log.info(&format!(
- "test_callback status_code:{}, headers: {:?}, body: {}",
- status_code,
- headers,
- format_body(body)
- ));
- this.reset_http_request();
-}
struct DemoWasm {
// 每个请求对应的插件实例
log: Log,
- config: Option,
-
- arg_storage: HttpCallArgStorage>>,
+ config: Option>,
+ weak: Weak>>>,
}
impl Context for DemoWasm {}
impl HttpContext for DemoWasm {}
-impl HttpContextWrapper>> for DemoWasm {
+impl HttpContextWrapper for DemoWasm {
+ fn init_self_weak(
+ &mut self,
+ self_weak: Weak>>>,
+ ) {
+ self.weak = self_weak;
+ self.log.info("init_self_rc");
+ }
fn log(&self) -> &Log {
&self.log
}
- fn get_http_call_storage(
- &mut self,
- ) -> Option<&mut HttpCallArgStorage>>> {
- Some(&mut self.arg_storage)
- }
- fn on_config(&mut self, config: &DemoWasmConfig) {
+ fn on_config(&mut self, config: Rc) {
// 获取config
self.log.info(&format!("on_config {}", config.test));
self.config = Some(config.clone())
@@ -101,16 +86,6 @@ impl HttpContextWrapper>> for DemoW
// 是否缓存返回body
true
}
- fn on_http_call_response_detail(
- &mut self,
- _token_id: u32,
- arg: Box>,
- status_code: u16,
- headers: &MultiMap,
- body: Option>,
- ) {
- arg(self, status_code, headers, body)
- }
fn on_http_request_complete_body(&mut self, req_body: &Bytes) -> DataAction {
// 请求body获取完成回调
self.log.info(&format!(
@@ -118,23 +93,41 @@ impl HttpContextWrapper>> for DemoW
String::from_utf8(req_body.clone()).unwrap_or("".to_string())
));
let cluster = DnsCluster::new("httpbin", "httpbin.org", 80);
- if self
- .http_call(
- &cluster,
- &Method::POST,
- "http://httpbin.org/post",
- MultiMap::new(),
- Some("test_body".as_bytes()),
- // Box::new(move |this, _status_code, _headers, _body| this.resume_http_request()),
- Box::new(test_callback),
- Duration::from_secs(5),
- )
- .is_ok()
- {
- DataAction::StopIterationAndBuffer
- } else {
- self.log.info("http_call fail");
- DataAction::Continue
+
+ let self_rc = match self.weak.upgrade() {
+ Some(rc) => rc.clone(),
+ None => {
+ self.log.error("self_weak upgrade error");
+ return DataAction::Continue;
+ }
+ };
+ let http_call_res = self.http_call(
+ &cluster,
+ &Method::POST,
+ "http://httpbin.org/post",
+ MultiMap::new(),
+ Some("test_body".as_bytes()),
+ Box::new(move |status_code, headers, body| {
+ if let Some(this) = self_rc.borrow().downcast_ref::() {
+ this.log.info(&format!(
+ "test_callback status_code:{}, headers: {:?}, body: {}",
+ status_code,
+ headers,
+ format_body(body)
+ ));
+ this.resume_http_request();
+ } else {
+ self_rc.borrow().resume_http_request();
+ }
+ }),
+ Duration::from_secs(5),
+ );
+ match http_call_res {
+ Ok(_) => DataAction::StopIterationAndBuffer,
+ Err(e) => {
+ self.log.info(&format!("http_call fail {:?}", e));
+ DataAction::Continue
+ }
}
}
fn on_http_response_complete_body(&mut self, res_body: &Bytes) -> DataAction {
@@ -185,7 +178,7 @@ impl RootContext for DemoWasmRoot {
}
}
-impl RootContextWrapper>> for DemoWasmRoot {
+impl RootContextWrapper for DemoWasmRoot {
fn rule_matcher(&self) -> &SharedRuleMatcher {
&self.rule_matcher
}
@@ -193,11 +186,11 @@ impl RootContextWrapper>> for DemoW
fn create_http_context_wrapper(
&self,
_context_id: u32,
- ) -> Option>>>> {
+ ) -> Option>> {
Some(Box::new(DemoWasm {
config: None,
log: Log::new(PLUGIN_NAME.to_string()),
- arg_storage: HttpCallArgStorage::new(),
+ weak: Weak::default(),
}))
}
}
diff --git a/plugins/wasm-rust/extensions/request-block/Cargo.lock b/plugins/wasm-rust/extensions/request-block/Cargo.lock
index acc9caeb26..243fe87a49 100644
--- a/plugins/wasm-rust/extensions/request-block/Cargo.lock
+++ b/plugins/wasm-rust/extensions/request-block/Cargo.lock
@@ -29,12 +29,30 @@ version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
+[[package]]
+name = "bytes"
+version = "1.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
+
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+[[package]]
+name = "downcast-rs"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
[[package]]
name = "getrandom"
version = "0.2.15"
@@ -60,6 +78,9 @@ dependencies = [
name = "higress-wasm-rust"
version = "0.1.0"
dependencies = [
+ "downcast-rs",
+ "http",
+ "lazy_static",
"multimap",
"proxy-wasm",
"serde",
@@ -67,17 +88,34 @@ dependencies = [
"uuid",
]
+[[package]]
+name = "http"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
[[package]]
name = "libc"
-version = "0.2.155"
+version = "0.2.159"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
+checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
[[package]]
name = "log"
@@ -102,15 +140,15 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.19.0"
+version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "proc-macro2"
-version = "1.0.86"
+version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
+checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
dependencies = [
"unicode-ident",
]
@@ -118,26 +156,27 @@ dependencies = [
[[package]]
name = "proxy-wasm"
version = "0.2.2"
-source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#73833051f57d483570cf5aaa9d62bd7402fae63b"
+source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#6735737fad486c8a7cc324241f58df4a160e7887"
dependencies = [
+ "downcast-rs",
"hashbrown",
"log",
]
[[package]]
name = "quote"
-version = "1.0.36"
+version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
-version = "1.10.6"
+version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
+checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
dependencies = [
"aho-corasick",
"memchr",
@@ -147,9 +186,9 @@ dependencies = [
[[package]]
name = "regex-automata"
-version = "0.4.7"
+version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
+checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
dependencies = [
"aho-corasick",
"memchr",
@@ -158,9 +197,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
-version = "0.8.4"
+version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "request-block"
@@ -182,18 +221,18 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
-version = "1.0.207"
+version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2"
+checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.207"
+version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e"
+checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [
"proc-macro2",
"quote",
@@ -202,9 +241,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.125"
+version = "1.0.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
+checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
dependencies = [
"itoa",
"memchr",
@@ -214,9 +253,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.74"
+version = "2.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7"
+checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
dependencies = [
"proc-macro2",
"quote",
@@ -225,15 +264,15 @@ dependencies = [
[[package]]
name = "unicode-ident"
-version = "1.0.12"
+version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "uuid"
-version = "1.10.0"
+version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
+checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
dependencies = [
"getrandom",
]
diff --git a/plugins/wasm-rust/extensions/request-block/src/lib.rs b/plugins/wasm-rust/extensions/request-block/src/lib.rs
index 38d50de187..d120acb902 100644
--- a/plugins/wasm-rust/extensions/request-block/src/lib.rs
+++ b/plugins/wasm-rust/extensions/request-block/src/lib.rs
@@ -41,7 +41,7 @@ struct RquestBlockRoot {
struct RquestBlock {
log: Log,
- config: Option,
+ config: Option>,
cache_request: bool,
}
@@ -141,9 +141,9 @@ impl RootContextWrapper for RquestBlockRoot {
impl Context for RquestBlock {}
impl HttpContext for RquestBlock {}
impl HttpContextWrapper for RquestBlock {
- fn on_config(&mut self, _config: &RquestBlockConfig) {
- self.config = Some(_config.clone());
- self.cache_request = !_config.block_bodies.is_empty();
+ fn on_config(&mut self, config: Rc) {
+ self.cache_request = !config.block_bodies.is_empty();
+ self.config = Some(config.clone());
}
fn cache_request_body(&self) -> bool {
self.cache_request
diff --git a/plugins/wasm-rust/extensions/say-hello/Cargo.lock b/plugins/wasm-rust/extensions/say-hello/Cargo.lock
index 5a50473fcd..dc98fd709a 100644
--- a/plugins/wasm-rust/extensions/say-hello/Cargo.lock
+++ b/plugins/wasm-rust/extensions/say-hello/Cargo.lock
@@ -20,12 +20,30 @@ version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
+[[package]]
+name = "bytes"
+version = "1.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
+
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+[[package]]
+name = "downcast-rs"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
[[package]]
name = "getrandom"
version = "0.2.15"
@@ -51,6 +69,9 @@ dependencies = [
name = "higress-wasm-rust"
version = "0.1.0"
dependencies = [
+ "downcast-rs",
+ "http",
+ "lazy_static",
"multimap",
"proxy-wasm",
"serde",
@@ -58,17 +79,34 @@ dependencies = [
"uuid",
]
+[[package]]
+name = "http"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
[[package]]
name = "libc"
-version = "0.2.155"
+version = "0.2.159"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
+checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
[[package]]
name = "log"
@@ -93,15 +131,15 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.19.0"
+version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "proc-macro2"
-version = "1.0.86"
+version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
+checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
dependencies = [
"unicode-ident",
]
@@ -109,17 +147,18 @@ dependencies = [
[[package]]
name = "proxy-wasm"
version = "0.2.2"
-source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#73833051f57d483570cf5aaa9d62bd7402fae63b"
+source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#6735737fad486c8a7cc324241f58df4a160e7887"
dependencies = [
+ "downcast-rs",
"hashbrown",
"log",
]
[[package]]
name = "quote"
-version = "1.0.36"
+version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
@@ -142,18 +181,18 @@ dependencies = [
[[package]]
name = "serde"
-version = "1.0.207"
+version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2"
+checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.207"
+version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e"
+checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [
"proc-macro2",
"quote",
@@ -162,9 +201,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.125"
+version = "1.0.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
+checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
dependencies = [
"itoa",
"memchr",
@@ -174,9 +213,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.74"
+version = "2.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7"
+checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
dependencies = [
"proc-macro2",
"quote",
@@ -185,15 +224,15 @@ dependencies = [
[[package]]
name = "unicode-ident"
-version = "1.0.12"
+version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "uuid"
-version = "1.10.0"
+version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
+checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
dependencies = [
"getrandom",
]
diff --git a/plugins/wasm-rust/src/plugin_wrapper.rs b/plugins/wasm-rust/src/plugin_wrapper.rs
index 25d445f22f..5a00c0bda4 100644
--- a/plugins/wasm-rust/src/plugin_wrapper.rs
+++ b/plugins/wasm-rust/src/plugin_wrapper.rs
@@ -12,7 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+use std::cell::RefCell;
use std::collections::HashMap;
+use std::rc::{Rc, Weak};
use std::time::Duration;
use crate::cluster_wrapper::Cluster;
@@ -28,10 +30,13 @@ use serde::de::DeserializeOwned;
lazy_static! {
static ref LOG: Log = Log::new("plugin_wrapper".to_string());
}
+thread_local! {
+ static HTTP_CALLBACK_DISPATCHER: HttpCallbackDispatcher = HttpCallbackDispatcher::new();
+}
-pub trait RootContextWrapper: RootContext
+pub trait RootContextWrapper: RootContext
where
- PluginConfig: Default + DeserializeOwned + 'static + Clone,
+ PluginConfig: Default + DeserializeOwned + Clone + 'static,
{
// fn create_http_context(&self, context_id: u32) -> Option> {
fn create_http_context_use_wrapper(&self, context_id: u32) -> Option> {
@@ -48,38 +53,48 @@ where
fn create_http_context_wrapper(
&self,
_context_id: u32,
- ) -> Option>> {
+ ) -> Option>> {
None
}
}
-pub type HttpCallbackFn = dyn FnOnce(&mut T, u16, &MultiMap, Option>);
-pub struct HttpCallArgStorage {
- args: HashMap,
+pub type HttpCallbackFn = dyn FnOnce(u16, &MultiMap, Option>);
+
+pub struct HttpCallbackDispatcher {
+ call_fns: RefCell>>,
}
-impl Default for HttpCallArgStorage {
+impl Default for HttpCallbackDispatcher {
fn default() -> Self {
Self::new()
}
}
-impl HttpCallArgStorage {
+impl HttpCallbackDispatcher {
pub fn new() -> Self {
- HttpCallArgStorage {
- args: HashMap::new(),
+ HttpCallbackDispatcher {
+ call_fns: RefCell::new(HashMap::new()),
}
}
- pub fn set(&mut self, token_id: u32, arg: HttpCallArg) {
- self.args.insert(token_id, arg);
+ pub fn set(&self, token_id: u32, arg: Box) {
+ self.call_fns.borrow_mut().insert(token_id, arg);
}
- pub fn pop(&mut self, token_id: u32) -> Option {
- self.args.remove(&token_id)
+ pub fn pop(&self, token_id: u32) -> Option> {
+ self.call_fns.borrow_mut().remove(&token_id)
}
}
-pub trait HttpContextWrapper: HttpContext {
+
+pub trait HttpContextWrapper: HttpContext
+where
+ PluginConfig: Default + DeserializeOwned + Clone + 'static,
+{
+ fn init_self_weak(
+ &mut self,
+ _self_weak: Weak>>>,
+ ) {
+ }
fn log(&self) -> &Log {
&LOG
}
- fn on_config(&mut self, _config: &PluginConfig) {}
+ fn on_config(&mut self, _config: Rc) {}
fn on_http_request_complete_headers(
&mut self,
_headers: &MultiMap,
@@ -105,16 +120,6 @@ pub trait HttpContextWrapper: HttpContext {
DataAction::Continue
}
- #[allow(clippy::too_many_arguments)]
- fn on_http_call_response_detail(
- &mut self,
- _token_id: u32,
- _arg: HttpCallArg,
- _status_code: u16,
- _headers: &MultiMap,
- _body: Option>,
- ) {
- }
fn replace_http_request_body(&mut self, body: &[u8]) {
self.set_http_request_body(0, i32::MAX as usize, body)
}
@@ -122,10 +127,6 @@ pub trait HttpContextWrapper: HttpContext {
self.set_http_response_body(0, i32::MAX as usize, body)
}
- fn get_http_call_storage(&mut self) -> Option<&mut HttpCallArgStorage> {
- None
- }
-
#[allow(clippy::too_many_arguments)]
fn http_call(
&mut self,
@@ -134,7 +135,7 @@ pub trait HttpContextWrapper: HttpContext {
raw_url: &str,
headers: MultiMap,
body: Option<&[u8]>,
- arg: HttpCallArg,
+ call_fn: Box,
timeout: Duration,
) -> Result {
if let Ok(uri) = raw_url.parse::() {
@@ -162,17 +163,13 @@ pub trait HttpContextWrapper: HttpContext {
);
if let Ok(token_id) = ret {
- if let Some(storage) = self.get_http_call_storage() {
- storage.set(token_id, arg);
- self.log().debug(
- &format!(
- "http call start, id: {}, cluster: {}, method: {}, url: {}, body: {:?}, timeout: {:?}",
- token_id, cluster.cluster_name(), method.as_str(), raw_url, body, timeout
- )
- );
- } else {
- return Err(Status::InternalFailure);
- }
+ HTTP_CALLBACK_DISPATCHER.with(|dispatcher| dispatcher.set(token_id, call_fn));
+ self.log().debug(
+ &format!(
+ "http call start, id: {}, cluster: {}, method: {}, url: {}, body: {:?}, timeout: {:?}",
+ token_id, cluster.cluster_name(), method.as_str(), raw_url, body, timeout
+ )
+ );
}
ret
} else {
@@ -181,20 +178,30 @@ pub trait HttpContextWrapper: HttpContext {
}
}
}
-pub struct PluginHttpWrapper {
+
+downcast_rs::impl_downcast!(HttpContextWrapper where PluginConfig: Default + DeserializeOwned + Clone);
+
+pub struct PluginHttpWrapper {
req_headers: MultiMap,
res_headers: MultiMap,
req_body_len: usize,
res_body_len: usize,
- config: Option,
+ config: Option>,
rule_matcher: SharedRuleMatcher,
- http_content: Box>,
+ http_content: Rc>>>,
}
-impl PluginHttpWrapper {
+impl PluginHttpWrapper
+where
+ PluginConfig: Default + DeserializeOwned + Clone + 'static,
+{
pub fn new(
rule_matcher: &SharedRuleMatcher,
- http_content: Box>,
+ http_content: Box>,
) -> Self {
+ let rc_content = Rc::new(RefCell::new(http_content));
+ rc_content
+ .borrow_mut()
+ .init_self_weak(Rc::downgrade(&rc_content));
PluginHttpWrapper {
req_headers: MultiMap::new(),
res_headers: MultiMap::new(),
@@ -202,18 +209,17 @@ impl PluginHttpWrapper {
res_body_len: 0,
config: None,
rule_matcher: rule_matcher.clone(),
- http_content,
+ http_content: rc_content,
}
}
- fn get_http_call_arg(&mut self, token_id: u32) -> Option {
- if let Some(storage) = self.http_content.get_http_call_storage() {
- storage.pop(token_id)
- } else {
- None
- }
+ fn get_http_call_fn(&mut self, token_id: u32) -> Option> {
+ HTTP_CALLBACK_DISPATCHER.with(|dispatcher| dispatcher.pop(token_id))
}
}
-impl Context for PluginHttpWrapper {
+impl Context for PluginHttpWrapper
+where
+ PluginConfig: Default + DeserializeOwned + Clone + 'static,
+{
fn on_http_call_response(
&mut self,
token_id: u32,
@@ -221,7 +227,7 @@ impl Context for PluginHttpWrapper Context for PluginHttpWrapper Context for PluginHttpWrapper {
- self.http_content.log().warn(&format!(
+ self.http_content.borrow().log().warn(&format!(
"http call response header contains non-ASCII characters header: {}",
k
));
}
}
}
- self.http_content.log().warn(&format!(
- "http call end, id: {}, code: {}, normal: {}, body: {:?}",
+ self.http_content.borrow().log().warn(&format!(
+ "http call end, id: {}, code: {}, normal: {}, body: {:?}", /* */
token_id, status_code, normal_response, body
));
- self.http_content.on_http_call_response_detail(
+ call_fn(status_code, &headers, body)
+ } else {
+ self.http_content.borrow_mut().on_http_call_response(
token_id,
- arg,
- status_code,
- &headers,
- body,
+ num_headers,
+ body_size,
+ num_trailers,
)
- } else {
- self.http_content
- .on_http_call_response(token_id, num_headers, body_size, num_trailers)
}
}
fn on_grpc_call_response(&mut self, token_id: u32, status_code: u32, response_size: usize) {
self.http_content
+ .borrow_mut()
.on_grpc_call_response(token_id, status_code, response_size)
}
fn on_grpc_stream_initial_metadata(&mut self, token_id: u32, num_elements: u32) {
self.http_content
+ .borrow_mut()
.on_grpc_stream_initial_metadata(token_id, num_elements)
}
fn on_grpc_stream_message(&mut self, token_id: u32, message_size: usize) {
self.http_content
+ .borrow_mut()
.on_grpc_stream_message(token_id, message_size)
}
fn on_grpc_stream_trailing_metadata(&mut self, token_id: u32, num_elements: u32) {
self.http_content
+ .borrow_mut()
.on_grpc_stream_trailing_metadata(token_id, num_elements)
}
fn on_grpc_stream_close(&mut self, token_id: u32, status_code: u32) {
self.http_content
+ .borrow_mut()
.on_grpc_stream_close(token_id, status_code)
}
fn on_done(&mut self) -> bool {
- self.http_content.on_done()
+ self.http_content.borrow_mut().on_done()
}
}
-impl HttpContext for PluginHttpWrapper
+impl HttpContext for PluginHttpWrapper
where
- PluginConfig: Default + DeserializeOwned + Clone,
+ PluginConfig: Default + DeserializeOwned + Clone + 'static,
{
fn on_http_request_headers(&mut self, num_headers: usize, end_of_stream: bool) -> HeaderAction {
let binding = self.rule_matcher.borrow();
@@ -306,7 +316,7 @@ where
self.req_headers.insert(k, header_value);
}
Err(_) => {
- self.http_content.log().warn(&format!(
+ self.http_content.borrow().log().warn(&format!(
"request http header contains non-ASCII characters header: {}",
k
));
@@ -315,22 +325,25 @@ where
}
if let Some(config) = &self.config {
- self.http_content.on_config(config);
+ self.http_content.borrow_mut().on_config(config.clone());
}
let ret = self
.http_content
+ .borrow_mut()
.on_http_request_headers(num_headers, end_of_stream);
if ret != HeaderAction::Continue {
return ret;
}
self.http_content
+ .borrow_mut()
.on_http_request_complete_headers(&self.req_headers)
}
fn on_http_request_body(&mut self, body_size: usize, end_of_stream: bool) -> DataAction {
- if !self.http_content.cache_request_body() {
+ if !self.http_content.borrow().cache_request_body() {
return self
.http_content
+ .borrow_mut()
.on_http_request_body(body_size, end_of_stream);
}
self.req_body_len += body_size;
@@ -343,11 +356,15 @@ where
req_body = body;
}
}
- self.http_content.on_http_request_complete_body(&req_body)
+ self.http_content
+ .borrow_mut()
+ .on_http_request_complete_body(&req_body)
}
fn on_http_request_trailers(&mut self, num_trailers: usize) -> Action {
- self.http_content.on_http_request_trailers(num_trailers)
+ self.http_content
+ .borrow_mut()
+ .on_http_request_trailers(num_trailers)
}
fn on_http_response_headers(
@@ -361,7 +378,7 @@ where
self.res_headers.insert(k, header_value);
}
Err(_) => {
- self.http_content.log().warn(&format!(
+ self.http_content.borrow().log().warn(&format!(
"response http header contains non-ASCII characters header: {}",
k
));
@@ -371,18 +388,21 @@ where
let ret = self
.http_content
+ .borrow_mut()
.on_http_response_headers(num_headers, end_of_stream);
if ret != HeaderAction::Continue {
return ret;
}
self.http_content
+ .borrow_mut()
.on_http_response_complete_headers(&self.res_headers)
}
fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> DataAction {
- if !self.http_content.cache_response_body() {
+ if !self.http_content.borrow().cache_response_body() {
return self
.http_content
+ .borrow_mut()
.on_http_response_body(body_size, end_of_stream);
}
self.res_body_len += body_size;
@@ -397,14 +417,18 @@ where
res_body = body;
}
}
- self.http_content.on_http_response_complete_body(&res_body)
+ self.http_content
+ .borrow_mut()
+ .on_http_response_complete_body(&res_body)
}
fn on_http_response_trailers(&mut self, num_trailers: usize) -> Action {
- self.http_content.on_http_response_trailers(num_trailers)
+ self.http_content
+ .borrow_mut()
+ .on_http_response_trailers(num_trailers)
}
fn on_log(&mut self) {
- self.http_content.on_log()
+ self.http_content.borrow_mut().on_log()
}
}
diff --git a/plugins/wasm-rust/src/rule_matcher.rs b/plugins/wasm-rust/src/rule_matcher.rs
index 6478344334..dc42b433fc 100644
--- a/plugins/wasm-rust/src/rule_matcher.rs
+++ b/plugins/wasm-rust/src/rule_matcher.rs
@@ -20,6 +20,7 @@ use proxy_wasm::traits::RootContext;
use proxy_wasm::types::LogLevel;
use serde::de::DeserializeOwned;
use serde_json::{from_slice, Map, Value};
+use std::borrow::Borrow;
use std::cell::RefCell;
use std::collections::HashSet;
use std::rc::Rc;
@@ -50,13 +51,13 @@ struct RuleConfig {
category: Category,
routes: HashSet,
hosts: Vec,
- config: PluginConfig,
+ config: Rc,
}
#[derive(Default)]
pub struct RuleMatcher {
rule_config: Vec>,
- global_config: Option,
+ global_config: Option>,
}
impl RuleMatcher
@@ -71,7 +72,7 @@ where
let mut key_count = object.len();
if object.is_empty() {
- self.global_config = Some(PluginConfig::default());
+ self.global_config = Some(Rc::new(PluginConfig::default()));
return Ok(());
}
@@ -86,7 +87,7 @@ where
if key_count > 0 {
match serde_json::from_value::(config.clone()) {
Ok(plugin_config) => {
- self.global_config = Some(plugin_config);
+ self.global_config = Some(Rc::new(plugin_config));
}
Err(err) => {
log(
@@ -134,14 +135,14 @@ where
category,
routes,
hosts,
- config,
+ config: Rc::new(config),
})
}
Ok(())
}
- pub fn get_match_config(&self) -> Option<(i64, &PluginConfig)> {
+ pub fn get_match_config(&self) -> Option<(i64, Rc)> {
let host = get_http_request_header(":authority").unwrap_or_default();
let route_name = get_property(vec!["route_name"]).unwrap_or_default();
@@ -149,7 +150,7 @@ where
match rule.category {
Category::Host => {
if self.host_match(rule, host.as_str()) {
- return Some((i as i64, &rule.config));
+ return Some((i as i64, rule.config.clone()));
}
}
Category::Route => {
@@ -158,7 +159,7 @@ where
.unwrap_or_else(|_| "".to_string())
.as_str(),
) {
- return Some((i as i64, &rule.config));
+ return Some((i as i64, rule.config.clone()));
}
}
}
@@ -166,14 +167,16 @@ where
self.global_config
.as_ref()
- .map(|config| (usize::MAX as i64, config))
+ .map(|config| (usize::MAX as i64, config.clone()))
}
pub fn rewrite_config(&mut self, rewrite: fn(config: &PluginConfig) -> PluginConfig) {
- self.global_config = self.global_config.as_ref().map(rewrite);
+ if let Some(global_config) = &self.global_config {
+ self.global_config = Some(Rc::new(rewrite(global_config.borrow())));
+ }
for rule_config in &mut self.rule_config {
- rule_config.config = rewrite(&rule_config.config);
+ rule_config.config = Rc::new(rewrite(rule_config.config.borrow()));
}
}
From fc6902ded20a4bb0c3abf5e82ae271c59fd075a6 Mon Sep 17 00:00:00 2001
From: Ikko Eltociear Ashimine
Date: Mon, 21 Oct 2024 16:20:45 +0900
Subject: [PATCH 18/64] docs: add Japanese README and CONTRIBUTING files
(#1407)
---
CONTRIBUTING_JP.md | 195 +++++++++++++++++++++++++++++++++++++++++++++
README.md | 2 +-
README_EN.md | 2 +-
README_JP.md | 189 +++++++++++++++++++++++++++++++++++++++++++
4 files changed, 386 insertions(+), 2 deletions(-)
create mode 100644 CONTRIBUTING_JP.md
create mode 100644 README_JP.md
diff --git a/CONTRIBUTING_JP.md b/CONTRIBUTING_JP.md
new file mode 100644
index 0000000000..7916c56dd4
--- /dev/null
+++ b/CONTRIBUTING_JP.md
@@ -0,0 +1,195 @@
+# Higress への貢献
+
+Higress のハッキングに興味がある場合は、温かく歓迎します。まず、このような意欲を非常に奨励します。そして、以下は貢献ガイドのリストです。
+
+[[中文](./CONTRIBUTING.md)] | [[English Contributing Document](./CONTRIBUTING_EN.md)]
+
+## トピック
+
+- [Higress への貢献](#higress-への貢献)
+ - [トピック](#トピック)
+ - [セキュリティ問題の報告](#セキュリティ問題の報告)
+ - [一般的な問題の報告](#一般的な問題の報告)
+ - [コードとドキュメントの貢献](#コードとドキュメントの貢献)
+ - [ワークスペースの準備](#ワークスペースの準備)
+ - [ブランチの定義](#ブランチの定義)
+ - [コミットルール](#コミットルール)
+ - [コミットメッセージ](#コミットメッセージ)
+ - [コミット内容](#コミット内容)
+ - [PR 説明](#pr-説明)
+ - [テストケースの貢献](#テストケースの貢献)
+ - [何かを手伝うための参加](#何かを手伝うための参加)
+ - [コードスタイル](#コードスタイル)
+
+## セキュリティ問題の報告
+
+セキュリティ問題は常に真剣に扱われます。通常の原則として、セキュリティ問題を広めることは推奨しません。Higress のセキュリティ問題を発見した場合は、公開で議論せず、公開の問題を開かないでください。代わりに、[higress@googlegroups.com](mailto:higress@googlegroups.com) にプライベートなメールを送信して報告することをお勧めします。
+
+## 一般的な問題の報告
+
+正直なところ、Higress のすべてのユーザーを非常に親切な貢献者と見なしています。Higress を体験した後、プロジェクトに対するフィードバックがあるかもしれません。その場合は、[NEW ISSUE](https://github.com/alibaba/higress/issues/new/choose) を通じて問題を開くことを自由に行ってください。
+
+Higress プロジェクトを分散型で協力しているため、**よく書かれた**、**詳細な**、**明確な**問題報告を高く評価します。コミュニケーションをより効率的にするために、問題が検索リストに存在するかどうかを検索することを希望します。存在する場合は、新しい問題を開くのではなく、既存の問題のコメントに詳細を追加してください。
+
+問題の詳細をできるだけ標準化するために、問題報告者のために [ISSUE TEMPLATE](./.github/ISSUE_TEMPLATE) を設定しました。テンプレートのフィールドに従って指示に従って記入してください。
+
+問題を開く場合は多くのケースがあります:
+
+* バグ報告
+* 機能要求
+* パフォーマンス問題
+* 機能提案
+* 機能設計
+* 助けが必要
+* ドキュメントが不完全
+* テストの改善
+* プロジェクトに関する質問
+* その他
+
+また、新しい問題を記入する際には、投稿から機密データを削除することを忘れないでください。機密データには、パスワード、秘密鍵、ネットワークの場所、プライベートなビジネスデータなどが含まれる可能性があります。
+
+## コードとドキュメントの貢献
+
+Higress プロジェクトをより良くするためのすべての行動が奨励されます。GitHub では、Higress のすべての改善は PR(プルリクエストの略)を通じて行うことができます。
+
+* タイプミスを見つけた場合は、修正してみてください!
+* バグを見つけた場合は、修正してみてください!
+* 冗長なコードを見つけた場合は、削除してみてください!
+* 欠落しているテストケースを見つけた場合は、追加してみてください!
+* 機能を強化できる場合は、**ためらわないでください**!
+* コードが不明瞭な場合は、コメントを追加して明確にしてください!
+* コードが醜い場合は、リファクタリングしてみてください!
+* ドキュメントの改善に役立つ場合は、さらに良いです!
+* ドキュメントが不正確な場合は、修正してください!
+* ...
+
+実際には、それらを完全にリストすることは不可能です。1つの原則を覚えておいてください:
+
+> あなたからの PR を楽しみにしています。
+
+Higress を PR で改善する準備ができたら、ここで PR ルールを確認することをお勧めします。
+
+* [ワークスペースの準備](#ワークスペースの準備)
+* [ブランチの定義](#ブランチの定義)
+* [コミットルール](#コミットルール)
+* [PR 説明](#pr-説明)
+
+### ワークスペースの準備
+
+PR を提出するために、GitHub ID に登録していることを前提とします。その後、以下の手順で準備を完了できます:
+
+1. Higress を自分のリポジトリに **FORK** します。この作業を行うには、[alibaba/higress](https://github.com/alibaba/higress) のメインページの右上にある Fork ボタンをクリックするだけです。その後、`https://github.com//higress` に自分のリポジトリが作成されます。ここで、`your-username` はあなたの GitHub ユーザー名です。
+
+2. 自分のリポジトリをローカルに **CLONE** します。`git clone git@github.com:/higress.git` を使用してリポジトリをローカルマシンにクローンします。その後、新しいブランチを作成して、行いたい変更を完了できます。
+
+3. リモートを `git@github.com:alibaba/higress.git` に設定します。以下の2つのコマンドを使用します:
+
+```bash
+git remote add upstream git@github.com:alibaba/higress.git
+git remote set-url --push upstream no-pushing
+```
+
+このリモート設定を使用すると、git リモート設定を次のように確認できます:
+
+```shell
+$ git remote -v
+origin git@github.com:/higress.git (fetch)
+origin git@github.com:/higress.git (push)
+upstream git@github.com:alibaba/higress.git (fetch)
+upstream no-pushing (push)
+```
+
+これを追加すると、ローカルブランチを上流ブランチと簡単に同期できます。
+
+### ブランチの定義
+
+現在、プルリクエストを通じたすべての貢献は Higress の [main ブランチ](https://github.com/alibaba/higress/tree/main) に対するものであると仮定します。貢献する前に、ブランチの定義を理解することは非常に役立ちます。
+
+貢献者として、プルリクエストを通じたすべての貢献は main ブランチに対するものであることを再度覚えておいてください。Higress プロジェクトには、リリースブランチ(例:0.6.0、0.6.1)、機能ブランチ、ホットフィックスブランチなど、いくつかの他のブランチがあります。
+
+正式にバージョンをリリースする際には、リリースブランチが作成され、バージョン番号で命名されます。
+
+リリース後、リリースブランチのコミットを main ブランチにマージします。
+
+特定のバージョンにバグがある場合、後のバージョンで修正するか、特定のホットフィックスバージョンで修正するかを決定します。ホットフィックスバージョンで修正することを決定した場合、対応するリリースブランチに基づいてホットフィックスブランチをチェックアウトし、コード修正と検証を行い、main ブランチにマージします。
+
+大きな機能については、開発と検証のために機能ブランチを引き出します。
+
+### コミットルール
+
+実際には、Higress ではコミット時に2つのルールを真剣に考えています:
+
+* [コミットメッセージ](#コミットメッセージ)
+* [コミット内容](#コミット内容)
+
+#### コミットメッセージ
+
+コミットメッセージは、提出された PR の目的をレビュアーがよりよく理解するのに役立ちます。また、コードレビューの手続きを加速するのにも役立ちます。貢献者には、曖昧なメッセージではなく、**明確な**コミットメッセージを使用することを奨励します。一般的に、以下のコミットメッセージタイプを推奨します:
+
+* docs: xxxx. 例:"docs: add docs about Higress cluster installation".
+* feature: xxxx. 例:"feature: use higress config instead of istio config".
+* bugfix: xxxx. 例:"bugfix: fix panic when input nil parameter".
+* refactor: xxxx. 例:"refactor: simplify to make codes more readable".
+* test: xxx. 例:"test: add unit test case for func InsertIntoArray".
+* その他の読みやすく明確な表現方法。
+
+一方で、以下のような方法でのコミットメッセージは推奨しません:
+
+* ~~バグ修正~~
+* ~~更新~~
+* ~~ドキュメント追加~~
+
+迷った場合は、[Git コミットメッセージの書き方](http://chris.beams.io/posts/git-commit/) を参照してください。
+
+#### コミット内容
+
+コミット内容は、1つのコミットに含まれるすべての内容の変更を表します。1つのコミットに、他のコミットの助けを借りずにレビュアーが完全にレビューできる内容を含めるのが最善です。言い換えれば、1つのコミットの内容は CI を通過でき、コードの混乱を避けることができます。簡単に言えば、次の3つの小さなルールを覚えておく必要があります:
+
+* コミットで非常に大きな変更を避ける;
+* 各コミットが完全でレビュー可能であること。
+* コミット時に git config(`user.name`、`user.email`)を確認して、それが GitHub ID に関連付けられていることを確認します。
+
+```bash
+git config --get user.name
+git config --get user.email
+```
+
+* pr を提出する際には、'changes/' フォルダーの下の XXX.md ファイルに現在の変更の簡単な説明を追加してください。
+
+さらに、コード変更部分では、すべての貢献者が Higress の [コードスタイル](#コードスタイル) を読むことをお勧めします。
+
+コミットメッセージやコミット内容に関係なく、コードレビューに重点を置いています。
+
+### PR 説明
+
+PR は Higress プロジェクトファイルを変更する唯一の方法です。レビュアーが目的をよりよく理解できるようにするために、PR 説明は詳細すぎることはありません。貢献者には、[PR テンプレート](./.github/PULL_REQUEST_TEMPLATE.md) に従ってプルリクエストを完了することを奨励します。
+
+### 開発前の準備
+
+```shell
+make prebuild && go mod tidy
+```
+
+## テストケースの貢献
+
+テストケースは歓迎されます。現在、Higress の機能テストケースが高優先度です。
+
+* 単体テストの場合、同じモジュールの test ディレクトリに xxxTest.go という名前のテストファイルを作成する必要があります。
+* 統合テストの場合、統合テストを test ディレクトリに配置できます。
+//TBD
+
+## 何かを手伝うための参加
+
+GitHub を Higress の協力の主要な場所として選択しました。したがって、Higress の最新の更新は常にここにあります。PR を通じた貢献は明確な助けの方法ですが、他の方法も呼びかけています。
+
+* 可能であれば、他の人の質問に返信する;
+* 他のユーザーの問題を解決するのを手伝う;
+* 他の人の PR 設計をレビューするのを手伝う;
+* 他の人の PR のコードをレビューするのを手伝う;
+* Higress について議論して、物事を明確にする;
+* GitHub 以外で Higress 技術を宣伝する;
+* Higress に関するブログを書くなど。
+
+## コードスタイル
+//TBD
+要するに、**どんな助けも貢献です。**
diff --git a/README.md b/README.md
index 87468ed9ad..761f8591a8 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@
- English | 中文
+ English | 中文 | 日本語
diff --git a/README_EN.md b/README_EN.md
index 8ce33aa069..b099fa3e5b 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -15,7 +15,7 @@
- English | 中文
+ English | 中文 | 日本語
Higress is a cloud-native api gateway based on Alibaba's internal gateway practices.
diff --git a/README_JP.md b/README_JP.md
new file mode 100644
index 0000000000..8573576ee0
--- /dev/null
+++ b/README_JP.md
@@ -0,0 +1,189 @@
+
+
+
+ AIゲートウェイ
+
+ AIネイティブAPIゲートウェイ
+
+[![Build Status](https://github.com/alibaba/higress/actions/workflows/build-and-test.yaml/badge.svg?branch=main)](https://github.com/alibaba/higress/actions)
+[![license](https://img.shields.io/github/license/alibaba/higress.svg)](https://www.apache.org/licenses/LICENSE-2.0.html)
+
+[**公式サイト**](https://higress.cn/) |
+ [**ドキュメント**](https://higress.cn/docs/latest/overview/what-is-higress/) |
+ [**ブログ**](https://higress.cn/blog/) |
+ [**電子書籍**](https://higress.cn/docs/ebook/wasm14/) |
+ [**開発ガイド**](https://higress.cn/docs/latest/dev/architecture/) |
+ [**AIプラグイン**](https://higress.cn/plugin/)
+
+
+
+ English | 中文 | 日本語
+
+
+
+Higressは、IstioとEnvoyをベースにしたクラウドネイティブAPIゲートウェイで、Go/Rust/JSなどを使用してWasmプラグインを作成できます。数十の既製の汎用プラグインと、すぐに使用できるコンソールを提供しています(デモは[こちら](http://demo.higress.io/))。
+
+Higressは、Tengineのリロードが長時間接続のビジネスに影響を与える問題や、gRPC/Dubboの負荷分散能力の不足を解決するために、Alibaba内部で誕生しました。
+
+Alibaba Cloudは、Higressを基盤にクラウドネイティブAPIゲートウェイ製品を構築し、多くの企業顧客に99.99%のゲートウェイ高可用性保証サービスを提供しています。
+
+Higressは、AIゲートウェイ機能を基盤に、Tongyi Qianwen APP、Bailian大規模モデルAPI、機械学習PAIプラットフォームなどのAIビジネスをサポートしています。また、国内の主要なAIGC企業(例:ZeroOne)やAI製品(例:FastGPT)にもサービスを提供しています。
+
+![](https://img.alicdn.com/imgextra/i2/O1CN011AbR8023V8R5N0HcA_!!6000000007260-2-tps-1080-606.png)
+
+
+## 目次
+
+- [**クイックスタート**](#クイックスタート)
+- [**機能紹介**](#機能紹介)
+- [**使用シナリオ**](#使用シナリオ)
+- [**主な利点**](#主な利点)
+- [**コミュニティ**](#コミュニティ)
+
+## クイックスタート
+
+HigressはDockerだけで起動でき、個人開発者がローカルで学習用にセットアップしたり、簡易サイトを構築するのに便利です。
+
+```bash
+# 作業ディレクトリを作成
+mkdir higress; cd higress
+# Higressを起動し、設定ファイルを作業ディレクトリに書き込みます
+docker run -d --rm --name higress-ai -v ${PWD}:/data \
+ -p 8001:8001 -p 8080:8080 -p 8443:8443 \
+ higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/all-in-one:latest
+```
+
+リスンポートの説明は以下の通りです:
+
+- 8001ポート:Higress UIコンソールのエントリーポイント
+- 8080ポート:ゲートウェイのHTTPプロトコルエントリーポイント
+- 8443ポート:ゲートウェイのHTTPSプロトコルエントリーポイント
+
+**HigressのすべてのDockerイメージは専用のリポジトリを使用しており、Docker Hubの国内アクセス不可の影響を受けません**
+
+K8sでのHelmデプロイなどの他のインストール方法については、公式サイトの[クイックスタートドキュメント](https://higress.cn/docs/latest/user/quickstart/)を参照してください。
+
+
+## 使用シナリオ
+
+- **AIゲートウェイ**:
+
+ Higressは、国内外のすべてのLLMモデルプロバイダーと統一されたプロトコルで接続でき、豊富なAI可観測性、多モデル負荷分散/フォールバック、AIトークンフロー制御、AIキャッシュなどの機能を備えています。
+
+ ![](https://img.alicdn.com/imgextra/i1/O1CN01fNnhCp1cV8mYPRFeS_!!6000000003605-0-tps-1080-608.jpg)
+
+- **Kubernetes Ingressゲートウェイ**:
+
+ HigressはK8sクラスターのIngressエントリーポイントゲートウェイとして機能し、多くのK8s Nginx Ingressの注釈に対応しています。K8s Nginx IngressからHigressへのスムーズな移行が可能です。
+
+ [Gateway API](https://gateway-api.sigs.k8s.io/)標準をサポートし、ユーザーがIngress APIからGateway APIにスムーズに移行できるようにします。
+
+ ingress-nginxと比較して、リソースの消費が大幅に減少し、ルーティングの変更が10倍速く反映されます。
+
+ ![](https://img.alicdn.com/imgextra/i1/O1CN01bhEtb229eeMNBWmdP_!!6000000008093-2-tps-750-547.png)
+ ![](https://img.alicdn.com/imgextra/i1/O1CN01bqRets1LsBGyitj4S_!!6000000001354-2-tps-887-489.png)
+
+- **マイクロサービスゲートウェイ**:
+
+ Higressはマイクロサービスゲートウェイとして機能し、Nacos、ZooKeeper、Consul、Eurekaなどのさまざまなサービスレジストリからサービスを発見し、ルーティングを構成できます。
+
+ また、[Dubbo](https://github.com/apache/dubbo)、[Nacos](https://github.com/alibaba/nacos)、[Sentinel](https://github.com/alibaba/Sentinel)などのマイクロサービス技術スタックと深く統合されています。Envoy C++ゲートウェイコアの優れたパフォーマンスに基づいて、従来のJavaベースのマイクロサービスゲートウェイと比較して、リソース使用率を大幅に削減し、コストを削減できます。
+
+ ![](https://img.alicdn.com/imgextra/i4/O1CN01v4ZbCj1dBjePSMZ17_!!6000000003698-0-tps-1613-926.jpg)
+
+- **セキュリティゲートウェイ**:
+
+ Higressはセキュリティゲートウェイとして機能し、WAF機能を提供し、key-auth、hmac-auth、jwt-auth、basic-auth、oidcなどのさまざまな認証戦略をサポートします。
+
+## 主な利点
+
+- **プロダクションレベル**
+
+ Alibabaで2年以上のプロダクション検証を経た内部製品から派生し、毎秒数十万のリクエストを処理する大規模なシナリオをサポートします。
+
+ Nginxのリロードによるトラフィックの揺れを完全に排除し、構成変更がミリ秒単位で反映され、ビジネスに影響を与えません。AIビジネスなどの長時間接続シナリオに特に適しています。
+
+- **ストリーム処理**
+
+ リクエスト/レスポンスボディの完全なストリーム処理をサポートし、Wasmプラグインを使用してSSE(Server-Sent Events)などのストリームプロトコルのメッセージをカスタマイズして処理できます。
+
+ AIビジネスなどの大帯域幅シナリオで、メモリ使用量を大幅に削減できます。
+
+- **拡張性**
+
+ AI、トラフィック管理、セキュリティ保護などの一般的な機能をカバーする豊富な公式プラグインライブラリを提供し、90%以上のビジネスシナリオのニーズを満たします。
+
+ Wasmプラグイン拡張を主力とし、サンドボックス隔離を通じてメモリの安全性を確保し、複数のプログラミング言語をサポートし、プラグインバージョンの独立したアップグレードを許可し、トラフィックに影響を与えずにゲートウェイロジックをホットアップデートできます。
+
+- **安全で使いやすい**
+
+ Ingress APIおよびGateway API標準に基づき、すぐに使用できるUIコンソールを提供し、WAF保護プラグイン、IP/Cookie CC保護プラグインをすぐに使用できます。
+
+ Let's Encryptの自動証明書発行および更新をサポートし、K8sを使用せずにデプロイでき、1行のDockerコマンドで起動でき、個人開発者にとって便利です。
+
+
+## 機能紹介
+
+### AIゲートウェイデモ展示
+
+[OpenAIから他の大規模モデルへの移行を30秒で完了
+](https://www.bilibili.com/video/BV1dT421a7w7/?spm_id_from=333.788.recommend_more_video.14)
+
+
+### Higress UIコンソール
+
+- **豊富な可観測性**
+
+ すぐに使用できる可観測性を提供し、Grafana&Prometheusは組み込みのものを使用することも、自分で構築したものを接続することもできます。
+
+ ![](./docs/images/monitor.gif)
+
+
+- **プラグイン拡張メカニズム**
+
+ 公式にはさまざまなプラグインが提供されており、ユーザーは[独自のプラグインを開発](./plugins/wasm-go)し、Docker/OCIイメージとして構築し、コンソールで構成して、プラグインロジックをリアルタイムで変更できます。トラフィックに影響を与えずにプラグインロジックをホットアップデートできます。
+
+ ![](./docs/images/plugin.gif)
+
+
+- **さまざまなサービス発見**
+
+ デフォルトでK8s Serviceサービス発見を提供し、構成を通じてNacos/ZooKeeperなどのレジストリに接続してサービスを発見することも、静的IPまたはDNSに基づいて発見することもできます。
+
+ ![](./docs/images/service-source.gif)
+
+
+- **ドメインと証明書**
+
+ TLS証明書を作成および管理し、ドメインのHTTP/HTTPS動作を構成できます。ドメインポリシーでは、特定のドメインに対してプラグインを適用することができます。
+
+ ![](./docs/images/domain.gif)
+
+
+- **豊富なルーティング機能**
+
+ 上記で定義されたサービス発見メカニズムを通じて、発見されたサービスはサービスリストに表示されます。ルーティングを作成する際に、ドメインを選択し、ルーティングマッチングメカニズムを定義し、ターゲットサービスを選択してルーティングを行います。ルーティングポリシーでは、特定のルーティングに対してプラグインを適用することができます。
+
+ ![](./docs/images/route-service.gif)
+
+
+## コミュニティ
+
+### 感謝
+
+EnvoyとIstioのオープンソースの取り組みがなければ、Higressは実現できませんでした。これらのプロジェクトに最も誠実な敬意を表します。
+
+### 交流グループ
+
+![image](https://img.alicdn.com/imgextra/i2/O1CN01BkopaB22ZsvamFftE_!!6000000007135-0-tps-720-405.jpg)
+
+### 技術共有
+
+WeChat公式アカウント:
+
+![](https://img.alicdn.com/imgextra/i1/O1CN01WnQt0q1tcmqVDU73u_!!6000000005923-0-tps-258-258.jpg)
+
+### 関連リポジトリ
+
+- Higressコンソール:https://github.com/higress-group/higress-console
+- Higress(スタンドアロン版):https://github.com/higress-group/higress-standalone
From badf4b7101526f2a8cb9dd7cc76d6cc06cb55e24 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=BE=84=E6=BD=AD?=
Date: Mon, 21 Oct 2024 15:43:01 +0800
Subject: [PATCH 19/64] ai cache plugin support set skip ai cache header
(#1380)
---
plugins/wasm-go/extensions/ai-cache/README.md | 5 +++++
plugins/wasm-go/extensions/ai-cache/README_EN.md | 4 ++++
plugins/wasm-go/extensions/ai-cache/main.go | 12 ++++++++++++
3 files changed, 21 insertions(+)
diff --git a/plugins/wasm-go/extensions/ai-cache/README.md b/plugins/wasm-go/extensions/ai-cache/README.md
index 97728f5177..1de252f12c 100644
--- a/plugins/wasm-go/extensions/ai-cache/README.md
+++ b/plugins/wasm-go/extensions/ai-cache/README.md
@@ -9,6 +9,11 @@ description: AI 缓存插件配置参考
LLM 结果缓存插件,默认配置方式可以直接用于 openai 协议的结果缓存,同时支持流式和非流式响应的缓存。
+**提示**
+
+携带请求头`x-higress-skip-ai-cache: on`时,当前请求将不会使用缓存中的内容,而是直接转发给后端服务,同时也不会缓存该请求返回响应的内容
+
+
## 运行属性
插件执行阶段:`认证阶段`
diff --git a/plugins/wasm-go/extensions/ai-cache/README_EN.md b/plugins/wasm-go/extensions/ai-cache/README_EN.md
index 81099e509c..7544995999 100644
--- a/plugins/wasm-go/extensions/ai-cache/README_EN.md
+++ b/plugins/wasm-go/extensions/ai-cache/README_EN.md
@@ -6,6 +6,10 @@ description: AI Cache Plugin Configuration Reference
## Function Description
LLM result caching plugin, the default configuration can be directly used for result caching under the OpenAI protocol, and it supports caching of both streaming and non-streaming responses.
+**Tips**
+
+When carrying the request header `x-higress-skip-ai-cache: on`, the current request will not use content from the cache but will be directly forwarded to the backend service. Additionally, the response content from this request will not be cached.
+
## Runtime Properties
Plugin Execution Phase: `Authentication Phase`
Plugin Execution Priority: `10`
diff --git a/plugins/wasm-go/extensions/ai-cache/main.go b/plugins/wasm-go/extensions/ai-cache/main.go
index dc5df1a6a8..7886d5698f 100644
--- a/plugins/wasm-go/extensions/ai-cache/main.go
+++ b/plugins/wasm-go/extensions/ai-cache/main.go
@@ -22,6 +22,7 @@ const (
ToolCallsContextKey = "toolCalls"
StreamContextKey = "stream"
DefaultCacheKeyPrefix = "higress-ai-cache:"
+ SkipCacheHeader = "x-higress-skip-ai-cache"
)
func main() {
@@ -172,6 +173,12 @@ func parseConfig(json gjson.Result, c *PluginConfig, log wrapper.Log) error {
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
+ skipCache, _ := proxywasm.GetHttpRequestHeader(SkipCacheHeader)
+ if skipCache == "on" {
+ ctx.SetContext(SkipCacheHeader, struct{}{})
+ ctx.DontReadRequestBody()
+ return types.ActionContinue
+ }
contentType, _ := proxywasm.GetHttpRequestHeader("content-type")
// The request does not have a body.
if contentType == "" {
@@ -270,6 +277,11 @@ func processSSEMessage(ctx wrapper.HttpContext, config PluginConfig, sseMessage
}
func onHttpResponseHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
+ skipCache := ctx.GetContext(SkipCacheHeader)
+ if skipCache != nil {
+ ctx.DontReadResponseBody()
+ return types.ActionContinue
+ }
contentType, _ := proxywasm.GetHttpResponseHeader("content-type")
if strings.Contains(contentType, "text/event-stream") {
ctx.SetContext(StreamContextKey, struct{}{})
From f8d62a8ac3d8ff56b9784f4e41a120ff514ca979 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=BE=84=E6=BD=AD?=
Date: Mon, 21 Oct 2024 16:46:18 +0800
Subject: [PATCH 20/64] add model router plugin (#1414)
---
plugins/wasm-cpp/WORKSPACE | 6 +-
plugins/wasm-cpp/bazel/wasm.bzl | 6 +-
.../wasm-cpp/extensions/model_router/BUILD | 70 +++++++
.../extensions/model_router/README.md | 64 ++++++
.../extensions/model_router/README_EN.md | 63 ++++++
.../extensions/model_router/plugin.cc | 189 ++++++++++++++++++
.../wasm-cpp/extensions/model_router/plugin.h | 85 ++++++++
.../extensions/model_router/plugin_test.cc | 144 +++++++++++++
8 files changed, 621 insertions(+), 6 deletions(-)
create mode 100644 plugins/wasm-cpp/extensions/model_router/BUILD
create mode 100644 plugins/wasm-cpp/extensions/model_router/README.md
create mode 100644 plugins/wasm-cpp/extensions/model_router/README_EN.md
create mode 100644 plugins/wasm-cpp/extensions/model_router/plugin.cc
create mode 100644 plugins/wasm-cpp/extensions/model_router/plugin.h
create mode 100644 plugins/wasm-cpp/extensions/model_router/plugin_test.cc
diff --git a/plugins/wasm-cpp/WORKSPACE b/plugins/wasm-cpp/WORKSPACE
index dc55483d9c..ed78d0df06 100644
--- a/plugins/wasm-cpp/WORKSPACE
+++ b/plugins/wasm-cpp/WORKSPACE
@@ -16,15 +16,15 @@ load("@io_bazel_rules_docker//repositories:deps.bzl", container_deps = "deps")
container_deps()
-PROXY_WASM_CPP_SDK_SHA = "fd0be8405db25de0264bdb78fae3a82668c03782"
+PROXY_WASM_CPP_SDK_SHA = "eaec483b5b3c7bcb89fd208b5a1fa5d79d626f61"
-PROXY_WASM_CPP_SDK_SHA256 = "c57de2425b5c61d7f630c5061e319b4557ae1f1c7526e5a51c33dc1299471b08"
+PROXY_WASM_CPP_SDK_SHA256 = "1140bc8114d75db56a6ca6b18423d4df50d988d40b4cec929a1eb246cf5a4a3d"
http_archive(
name = "proxy_wasm_cpp_sdk",
sha256 = PROXY_WASM_CPP_SDK_SHA256,
strip_prefix = "proxy-wasm-cpp-sdk-" + PROXY_WASM_CPP_SDK_SHA,
- url = "https://github.com/proxy-wasm/proxy-wasm-cpp-sdk/archive/" + PROXY_WASM_CPP_SDK_SHA + ".tar.gz",
+ url = "https://github.com/higress-group/proxy-wasm-cpp-sdk/archive/" + PROXY_WASM_CPP_SDK_SHA + ".tar.gz",
)
load("@proxy_wasm_cpp_sdk//bazel/dep:deps.bzl", "wasm_dependencies")
diff --git a/plugins/wasm-cpp/bazel/wasm.bzl b/plugins/wasm-cpp/bazel/wasm.bzl
index a100ccc0b6..1f061fbc69 100644
--- a/plugins/wasm-cpp/bazel/wasm.bzl
+++ b/plugins/wasm-cpp/bazel/wasm.bzl
@@ -33,14 +33,14 @@ def wasm_libraries():
urls = ["https://github.com/google/googletest/archive/release-1.10.0.tar.gz"],
)
- PROXY_WASM_CPP_HOST_SHA = "f38347360feaaf5b2a733f219c4d8c9660d626f0"
- PROXY_WASM_CPP_HOST_SHA256 = "bf10de946eb5785813895c2bf16504afc0cd590b9655d9ee52fb1074d0825ea3"
+ PROXY_WASM_CPP_HOST_SHA = "7850d1721fe3dd2ccfb86a06116f76c23b1f1bf8"
+ PROXY_WASM_CPP_HOST_SHA256 = "740690fc1d749849f6e24b5bc48a07dabc0565a7d03b6cd13425dba693956c57"
http_archive(
name = "proxy_wasm_cpp_host",
sha256 = PROXY_WASM_CPP_HOST_SHA256,
strip_prefix = "proxy-wasm-cpp-host-" + PROXY_WASM_CPP_HOST_SHA,
- url = "https://github.com/proxy-wasm/proxy-wasm-cpp-host/archive/" + PROXY_WASM_CPP_HOST_SHA +".tar.gz",
+ url = "https://github.com/higress-group/proxy-wasm-cpp-host/archive/" + PROXY_WASM_CPP_HOST_SHA +".tar.gz",
)
http_archive(
diff --git a/plugins/wasm-cpp/extensions/model_router/BUILD b/plugins/wasm-cpp/extensions/model_router/BUILD
new file mode 100644
index 0000000000..67cfa547db
--- /dev/null
+++ b/plugins/wasm-cpp/extensions/model_router/BUILD
@@ -0,0 +1,70 @@
+# 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.
+
+load("@proxy_wasm_cpp_sdk//bazel/wasm:wasm.bzl", "wasm_cc_binary")
+load("//bazel:wasm.bzl", "declare_wasm_image_targets")
+
+wasm_cc_binary(
+ name = "model_router.wasm",
+ srcs = [
+ "plugin.cc",
+ "plugin.h",
+ ],
+ deps = [
+ "@proxy_wasm_cpp_sdk//:proxy_wasm_intrinsics_higress",
+ "@com_google_absl//absl/strings",
+ "@com_google_absl//absl/time",
+ "//common:json_util",
+ "//common:http_util",
+ "//common:rule_util",
+ ],
+)
+
+cc_library(
+ name = "model_router_lib",
+ srcs = [
+ "plugin.cc",
+ ],
+ hdrs = [
+ "plugin.h",
+ ],
+ copts = ["-DNULL_PLUGIN"],
+ deps = [
+ "@com_google_absl//absl/strings",
+ "@com_google_absl//absl/time",
+ "//common:json_util",
+ "@proxy_wasm_cpp_host//:lib",
+ "//common:http_util_nullvm",
+ "//common:rule_util_nullvm",
+ ],
+)
+
+cc_test(
+ name = "model_router_test",
+ srcs = [
+ "plugin_test.cc",
+ ],
+ copts = ["-DNULL_PLUGIN"],
+ deps = [
+ ":model_router_lib",
+ "@com_google_googletest//:gtest",
+ "@com_google_googletest//:gtest_main",
+ "@proxy_wasm_cpp_host//:lib",
+ ],
+)
+
+declare_wasm_image_targets(
+ name = "model_router",
+ wasm_file = ":model_router.wasm",
+)
diff --git a/plugins/wasm-cpp/extensions/model_router/README.md b/plugins/wasm-cpp/extensions/model_router/README.md
new file mode 100644
index 0000000000..b63be35d8f
--- /dev/null
+++ b/plugins/wasm-cpp/extensions/model_router/README.md
@@ -0,0 +1,64 @@
+## 功能说明
+`model-router`插件实现了基于LLM协议中的model参数路由的功能
+
+## 运行属性
+
+插件执行阶段:`默认阶段`
+插件执行优先级:`260`
+
+## 配置字段
+
+| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
+| ----------- | --------------- | ----------------------- | ------ | ------------------------------------------- |
+| `enable` | bool | 选填 | false | 是否开启基于model参数路由 |
+| `model_key` | string | 选填 | model | 请求body中model参数的位置 |
+| `add_header_key` | string | 选填 | x-higress-llm-provider | 从model参数中解析出的provider名字放到哪个请求header中 |
+
+
+## 效果说明
+
+如下开启基于model参数路由的功能:
+
+```yaml
+enable: true
+```
+
+开启后,插件将请求中 model 参数的 provider 部分(如果有)提取出来,设置到 x-higress-llm-provider 这个请求 header 中,用于后续路由,并将 model 参数重写为模型名称部分。举例来说,原生的 LLM 请求体是:
+
+```json
+{
+ "model": "qwen/qwen-long",
+ "frequency_penalty": 0,
+ "max_tokens": 800,
+ "stream": false,
+ "messages": [{
+ "role": "user",
+ "content": "higress项目主仓库的github地址是什么"
+ }],
+ "presence_penalty": 0,
+ "temperature": 0.7,
+ "top_p": 0.95
+}
+```
+
+经过这个插件后,将添加下面这个请求头(可以用于路由匹配):
+
+x-higress-llm-provider: qwen
+
+原始的 LLM 请求体将被改成:
+
+```json
+{
+ "model": "qwen-long",
+ "frequency_penalty": 0,
+ "max_tokens": 800,
+ "stream": false,
+ "messages": [{
+ "role": "user",
+ "content": "higress项目主仓库的github地址是什么"
+ }],
+ "presence_penalty": 0,
+ "temperature": 0.7,
+ "top_p": 0.95
+}
+```
diff --git a/plugins/wasm-cpp/extensions/model_router/README_EN.md b/plugins/wasm-cpp/extensions/model_router/README_EN.md
new file mode 100644
index 0000000000..4d2eaf1fee
--- /dev/null
+++ b/plugins/wasm-cpp/extensions/model_router/README_EN.md
@@ -0,0 +1,63 @@
+## Function Description
+The `model-router` plugin implements the functionality of routing based on the `model` parameter in the LLM protocol.
+
+## Runtime Properties
+
+Plugin Execution Phase: `Default Phase`
+Plugin Execution Priority: `260`
+
+## Configuration Fields
+
+| Name | Data Type | Filling Requirement | Default Value | Description |
+| -------------------- | ------------- | --------------------- | ---------------------- | ----------------------------------------------------- |
+| `enable` | bool | Optional | false | Whether to enable routing based on the `model` parameter |
+| `model_key` | string | Optional | model | The location of the `model` parameter in the request body |
+| `add_header_key` | string | Optional | x-higress-llm-provider | The header where the parsed provider name from the `model` parameter will be placed |
+
+## Effect Description
+
+To enable routing based on the `model` parameter, use the following configuration:
+
+```yaml
+enable: true
+```
+
+After enabling, the plugin extracts the provider part (if any) from the `model` parameter in the request, and sets it in the `x-higress-llm-provider` request header for subsequent routing. It also rewrites the `model` parameter to the model name part. For example, the original LLM request body is:
+
+```json
+{
+ "model": "openai/gpt-4o",
+ "frequency_penalty": 0,
+ "max_tokens": 800,
+ "stream": false,
+ "messages": [{
+ "role": "user",
+ "content": "What is the GitHub address for the main repository of the Higress project?"
+ }],
+ "presence_penalty": 0,
+ "temperature": 0.7,
+ "top_p": 0.95
+}
+```
+
+After processing by the plugin, the following request header (which can be used for routing matching) will be added:
+
+`x-higress-llm-provider: openai`
+
+The original LLM request body will be modified to:
+
+```json
+{
+ "model": "gpt-4o",
+ "frequency_penalty": 0,
+ "max_tokens": 800,
+ "stream": false,
+ "messages": [{
+ "role": "user",
+ "content": "What is the GitHub address for the main repository of the Higress project?"
+ }],
+ "presence_penalty": 0,
+ "temperature": 0.7,
+ "top_p": 0.95
+}
+```
diff --git a/plugins/wasm-cpp/extensions/model_router/plugin.cc b/plugins/wasm-cpp/extensions/model_router/plugin.cc
new file mode 100644
index 0000000000..457864d268
--- /dev/null
+++ b/plugins/wasm-cpp/extensions/model_router/plugin.cc
@@ -0,0 +1,189 @@
+// 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.
+
+#include "extensions/model_router/plugin.h"
+
+#include
+#include
+
+#include "absl/strings/str_cat.h"
+#include "absl/strings/str_split.h"
+#include "common/http_util.h"
+#include "common/json_util.h"
+
+using ::nlohmann::json;
+using ::Wasm::Common::JsonArrayIterate;
+using ::Wasm::Common::JsonGetField;
+using ::Wasm::Common::JsonObjectIterate;
+using ::Wasm::Common::JsonValueAs;
+
+#ifdef NULL_PLUGIN
+
+namespace proxy_wasm {
+namespace null_plugin {
+namespace model_router {
+
+PROXY_WASM_NULL_PLUGIN_REGISTRY
+
+#endif
+
+static RegisterContextFactory register_ModelRouter(
+ CONTEXT_FACTORY(PluginContext), ROOT_FACTORY(PluginRootContext));
+
+namespace {
+
+constexpr std::string_view SetDecoderBufferLimitKey =
+ "SetRequestBodyBufferLimit";
+constexpr std::string_view DefaultMaxBodyBytes = "10485760";
+
+} // namespace
+
+bool PluginRootContext::parsePluginConfig(const json& configuration,
+ ModelRouterConfigRule& rule) {
+ if (auto it = configuration.find("enable"); it != configuration.end()) {
+ if (it->is_boolean()) {
+ rule.enable_ = it->get();
+ } else {
+ LOG_WARN("Invalid type for enable. Expected boolean.");
+ return false;
+ }
+ }
+
+ if (auto it = configuration.find("model_key"); it != configuration.end()) {
+ if (it->is_string()) {
+ rule.model_key_ = it->get();
+ } else {
+ LOG_WARN("Invalid type for model_key. Expected string.");
+ return false;
+ }
+ }
+
+ if (auto it = configuration.find("add_header_key");
+ it != configuration.end()) {
+ if (it->is_string()) {
+ rule.add_header_key_ = it->get();
+ } else {
+ LOG_WARN("Invalid type for add_header_key. Expected string.");
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool PluginRootContext::onConfigure(size_t size) {
+ // Parse configuration JSON string.
+ if (size > 0 && !configure(size)) {
+ LOG_WARN("configuration has errors initialization will not continue.");
+ return false;
+ }
+ return true;
+}
+
+bool PluginRootContext::configure(size_t configuration_size) {
+ auto configuration_data = getBufferBytes(WasmBufferType::PluginConfiguration,
+ 0, configuration_size);
+ // Parse configuration JSON string.
+ auto result = ::Wasm::Common::JsonParse(configuration_data->view());
+ if (!result) {
+ LOG_WARN(absl::StrCat("cannot parse plugin configuration JSON string: ",
+ configuration_data->view()));
+ return false;
+ }
+ if (!parseAuthRuleConfig(result.value())) {
+ LOG_WARN(absl::StrCat("cannot parse plugin configuration JSON string: ",
+ configuration_data->view()));
+ return false;
+ }
+ return true;
+}
+
+FilterHeadersStatus PluginRootContext::onHeader(
+ const ModelRouterConfigRule& rule) {
+ if (!rule.enable_ || !Wasm::Common::Http::hasRequestBody()) {
+ return FilterHeadersStatus::Continue;
+ }
+ auto content_type_value =
+ getRequestHeader(Wasm::Common::Http::Header::ContentType);
+ if (!absl::StrContains(content_type_value->view(),
+ Wasm::Common::Http::ContentTypeValues::Json)) {
+ return FilterHeadersStatus::Continue;
+ }
+ removeRequestHeader(Wasm::Common::Http::Header::ContentLength);
+ setFilterState(SetDecoderBufferLimitKey, DefaultMaxBodyBytes);
+ return FilterHeadersStatus::StopIteration;
+}
+
+FilterDataStatus PluginRootContext::onBody(const ModelRouterConfigRule& rule,
+ std::string_view body) {
+ const auto& model_key = rule.model_key_;
+ const auto& add_header_key = rule.add_header_key_;
+ auto body_json_opt = ::Wasm::Common::JsonParse(body);
+ if (!body_json_opt) {
+ LOG_WARN(absl::StrCat("cannot parse body to JSON string: ", body));
+ return FilterDataStatus::Continue;
+ }
+ auto body_json = body_json_opt.value();
+ if (body_json.contains(model_key)) {
+ std::string model_value = body_json[model_key];
+ auto pos = model_value.find('/');
+ if (pos != std::string::npos) {
+ const auto& provider = model_value.substr(0, pos);
+ const auto& model = model_value.substr(pos + 1);
+ replaceRequestHeader(add_header_key, provider);
+ body_json[model_key] = model;
+ setBuffer(WasmBufferType::HttpRequestBody, 0,
+ std::numeric_limits::max(), body_json.dump());
+ LOG_DEBUG(absl::StrCat("model route to provider:", provider,
+ ", model:", model));
+ } else {
+ LOG_DEBUG(absl::StrCat("model route not work, model:", model_value));
+ }
+ }
+ return FilterDataStatus::Continue;
+}
+
+FilterHeadersStatus PluginContext::onRequestHeaders(uint32_t, bool) {
+ auto* rootCtx = rootContext();
+ return rootCtx->onHeaders([rootCtx, this](const auto& config) {
+ auto ret = rootCtx->onHeader(config);
+ if (ret == FilterHeadersStatus::StopIteration) {
+ this->config_ = &config;
+ }
+ return ret;
+ });
+}
+
+FilterDataStatus PluginContext::onRequestBody(size_t body_size,
+ bool end_stream) {
+ if (config_ == nullptr) {
+ return FilterDataStatus::Continue;
+ }
+ body_total_size_ += body_size;
+ if (!end_stream) {
+ return FilterDataStatus::StopIterationAndBuffer;
+ }
+ auto body =
+ getBufferBytes(WasmBufferType::HttpRequestBody, 0, body_total_size_);
+ auto* rootCtx = rootContext();
+ return rootCtx->onBody(*config_, body->view());
+}
+
+#ifdef NULL_PLUGIN
+
+} // namespace model_router
+} // namespace null_plugin
+} // namespace proxy_wasm
+
+#endif
diff --git a/plugins/wasm-cpp/extensions/model_router/plugin.h b/plugins/wasm-cpp/extensions/model_router/plugin.h
new file mode 100644
index 0000000000..16cfdf8509
--- /dev/null
+++ b/plugins/wasm-cpp/extensions/model_router/plugin.h
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+
+#include
+
+#include
+#include
+
+#include "common/route_rule_matcher.h"
+#define ASSERT(_X) assert(_X)
+
+#ifndef NULL_PLUGIN
+
+#include "proxy_wasm_intrinsics.h"
+
+#else
+
+#include "include/proxy-wasm/null_plugin.h"
+
+namespace proxy_wasm {
+namespace null_plugin {
+namespace model_router {
+
+#endif
+
+struct ModelRouterConfigRule {
+ bool enable_ = false;
+ std::string model_key_ = "model";
+ std::string add_header_key_ = "x-higress-llm-provider";
+};
+
+// PluginRootContext is the root context for all streams processed by the
+// thread. It has the same lifetime as the worker thread and acts as target for
+// interactions that outlives individual stream, e.g. timer, async calls.
+class PluginRootContext : public RootContext,
+ public RouteRuleMatcher {
+ public:
+ PluginRootContext(uint32_t id, std::string_view root_id)
+ : RootContext(id, root_id) {}
+ ~PluginRootContext() {}
+ bool onConfigure(size_t) override;
+ FilterHeadersStatus onHeader(const ModelRouterConfigRule&);
+ FilterDataStatus onBody(const ModelRouterConfigRule&, std::string_view);
+ bool configure(size_t);
+
+ private:
+ bool parsePluginConfig(const json&, ModelRouterConfigRule&) override;
+};
+
+// Per-stream context.
+class PluginContext : public Context {
+ public:
+ explicit PluginContext(uint32_t id, RootContext* root) : Context(id, root) {}
+ FilterHeadersStatus onRequestHeaders(uint32_t, bool) override;
+ FilterDataStatus onRequestBody(size_t, bool) override;
+
+ private:
+ inline PluginRootContext* rootContext() {
+ return dynamic_cast(this->root());
+ }
+
+ size_t body_total_size_ = 0;
+ const ModelRouterConfigRule* config_ = nullptr;
+};
+
+#ifdef NULL_PLUGIN
+
+} // namespace model_router
+} // namespace null_plugin
+} // namespace proxy_wasm
+
+#endif
diff --git a/plugins/wasm-cpp/extensions/model_router/plugin_test.cc b/plugins/wasm-cpp/extensions/model_router/plugin_test.cc
new file mode 100644
index 0000000000..9ce5998051
--- /dev/null
+++ b/plugins/wasm-cpp/extensions/model_router/plugin_test.cc
@@ -0,0 +1,144 @@
+// 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.
+
+#include "extensions/model_router/plugin.h"
+
+#include
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "include/proxy-wasm/context.h"
+#include "include/proxy-wasm/null.h"
+
+namespace proxy_wasm {
+namespace null_plugin {
+namespace model_router {
+
+NullPluginRegistry* context_registry_;
+RegisterNullVmPluginFactory register_model_router_plugin("model_router", []() {
+ return std::make_unique(model_router::context_registry_);
+});
+
+class MockContext : public proxy_wasm::ContextBase {
+ public:
+ MockContext(WasmBase* wasm) : ContextBase(wasm) {}
+ MOCK_METHOD(BufferInterface*, getBuffer, (WasmBufferType));
+ MOCK_METHOD(WasmResult, log, (uint32_t, std::string_view));
+ MOCK_METHOD(WasmResult, setBuffer,
+ (WasmBufferType, size_t, size_t, std::string_view));
+ MOCK_METHOD(WasmResult, getHeaderMapValue,
+ (WasmHeaderMapType /* type */, std::string_view /* key */,
+ std::string_view* /*result */));
+ MOCK_METHOD(WasmResult, addHeaderMapValue,
+ (WasmHeaderMapType, std::string_view, std::string_view));
+ MOCK_METHOD(WasmResult, getProperty, (std::string_view, std::string*));
+ MOCK_METHOD(WasmResult, setProperty, (std::string_view, std::string_view));
+};
+class ModelRouterTest : public ::testing::Test {
+ protected:
+ ModelRouterTest() {
+ // Initialize test VM
+ test_vm_ = createNullVm();
+ wasm_base_ = std::make_unique(
+ std::move(test_vm_), "test-vm", "", "",
+ std::unordered_map{},
+ AllowedCapabilitiesMap{});
+ wasm_base_->load("model_router");
+ wasm_base_->initialize();
+ // Initialize host side context
+ mock_context_ = std::make_unique(wasm_base_.get());
+ current_context_ = mock_context_.get();
+ // Initialize Wasm sandbox context
+ root_context_ = std::make_unique(0, "");
+ context_ = std::make_unique(1, root_context_.get());
+
+ ON_CALL(*mock_context_, log(testing::_, testing::_))
+ .WillByDefault([](uint32_t, std::string_view m) {
+ std::cerr << m << "\n";
+ return WasmResult::Ok;
+ });
+
+ ON_CALL(*mock_context_, getBuffer(testing::_))
+ .WillByDefault([&](WasmBufferType type) {
+ if (type == WasmBufferType::HttpRequestBody) {
+ return &body_;
+ }
+ return &config_;
+ });
+ ON_CALL(*mock_context_, getHeaderMapValue(WasmHeaderMapType::RequestHeaders,
+ testing::_, testing::_))
+ .WillByDefault([&](WasmHeaderMapType, std::string_view header,
+ std::string_view* result) {
+ if (header == "content-type") {
+ *result = "application/json";
+ } else if (header == "content-length") {
+ *result = "1024";
+ }
+ return WasmResult::Ok;
+ });
+ ON_CALL(*mock_context_, addHeaderMapValue(WasmHeaderMapType::RequestHeaders,
+ testing::_, testing::_))
+ .WillByDefault([&](WasmHeaderMapType, std::string_view header,
+ std::string_view value) { return WasmResult::Ok; });
+ ON_CALL(*mock_context_, getProperty(testing::_, testing::_))
+ .WillByDefault([&](std::string_view path, std::string* result) {
+ *result = route_name_;
+ return WasmResult::Ok;
+ });
+ ON_CALL(*mock_context_, setProperty(testing::_, testing::_))
+ .WillByDefault(
+ [&](std::string_view, std::string_view) { return WasmResult::Ok; });
+ }
+ ~ModelRouterTest() override {}
+ std::unique_ptr wasm_base_;
+ std::unique_ptr test_vm_;
+ std::unique_ptr mock_context_;
+ std::unique_ptr root_context_;
+ std::unique_ptr context_;
+ std::string route_name_;
+ BufferBase body_;
+ BufferBase config_;
+};
+
+TEST_F(ModelRouterTest, RewriteModelAndHeader) {
+ std::string configuration = R"(
+{
+ "enable": true
+ })";
+
+ config_.set(configuration);
+ EXPECT_TRUE(root_context_->configure(configuration.size()));
+
+ std::string request_json = R"({"model": "qwen/qwen-long"})";
+ EXPECT_CALL(*mock_context_,
+ setBuffer(testing::_, testing::_, testing::_, testing::_))
+ .WillOnce([&](WasmBufferType, size_t, size_t, std::string_view body) {
+ EXPECT_EQ(body, R"({"model":"qwen-long"})");
+ return WasmResult::Ok;
+ });
+
+ EXPECT_CALL(
+ *mock_context_,
+ addHeaderMapValue(testing::_, std::string_view("x-higress-llm-provider"),
+ std::string_view("qwen")));
+
+ body_.set(request_json);
+ EXPECT_EQ(context_->onRequestHeaders(0, false),
+ FilterHeadersStatus::StopIteration);
+ EXPECT_EQ(context_->onRequestBody(28, true), FilterDataStatus::Continue);
+}
+
+} // namespace model_router
+} // namespace null_plugin
+} // namespace proxy_wasm
From 871ae179c3b716943a5c724668283c65a90c879f Mon Sep 17 00:00:00 2001
From: 007gzs <007gzs@gmail.com>
Date: Tue, 22 Oct 2024 21:39:01 +0800
Subject: [PATCH 21/64] Ai data masking fix (#1420)
---
plugins/README.md | 1 +
.../extensions/ai-data-masking/src/lib.rs | 9 ++++++---
plugins/wasm-rust/src/plugin_wrapper.rs | 19 ++++++++++++++++++-
plugins/wasm-rust/src/request_wrapper.rs | 2 +-
4 files changed, 26 insertions(+), 5 deletions(-)
diff --git a/plugins/README.md b/plugins/README.md
index dc2ac8b1ca..7cdabde1d8 100644
--- a/plugins/README.md
+++ b/plugins/README.md
@@ -1,5 +1,6 @@
## Wasm 插件
+
目前 Higress 提供了 c++ 和 golang 两种 Wasm 插件开发框架,支持 Wasm 插件路由&域名级匹配生效。
同时提供了多个内置插件,用户可以基于 Higress 提供的官方镜像仓库直接使用这些插件(以 c++ 版本举例):
diff --git a/plugins/wasm-rust/extensions/ai-data-masking/src/lib.rs b/plugins/wasm-rust/extensions/ai-data-masking/src/lib.rs
index 01573585ac..99d0144e97 100644
--- a/plugins/wasm-rust/extensions/ai-data-masking/src/lib.rs
+++ b/plugins/wasm-rust/extensions/ai-data-masking/src/lib.rs
@@ -16,6 +16,7 @@ use fancy_regex::Regex;
use grok::patterns;
use higress_wasm_rust::log::Log;
use higress_wasm_rust::plugin_wrapper::{HttpContextWrapper, RootContextWrapper};
+use higress_wasm_rust::request_wrapper::has_request_body;
use higress_wasm_rust::rule_matcher::{on_configure, RuleMatcher, SharedRuleMatcher};
use jieba_rs::Jieba;
use jsonpath_rust::{JsonPath, JsonPathValue};
@@ -519,7 +520,11 @@ impl HttpContext for AiDataMasking {
_num_headers: usize,
_end_of_stream: bool,
) -> HeaderAction {
- HeaderAction::StopIteration
+ if has_request_body() {
+ HeaderAction::StopIteration
+ } else {
+ HeaderAction::Continue
+ }
}
fn on_http_response_headers(
&mut self,
@@ -669,14 +674,12 @@ impl HttpContextWrapper for AiDataMasking {
}
fn on_http_response_complete_body(&mut self, res_body: &Bytes) -> DataAction {
if self.config.is_none() {
- self.reset_http_response();
return DataAction::Continue;
}
let config = self.config.as_ref().unwrap();
let mut res_body = match String::from_utf8(res_body.clone()) {
Ok(r) => r,
Err(_) => {
- self.reset_http_response();
return DataAction::Continue;
}
};
diff --git a/plugins/wasm-rust/src/plugin_wrapper.rs b/plugins/wasm-rust/src/plugin_wrapper.rs
index 5a00c0bda4..abe188a99f 100644
--- a/plugins/wasm-rust/src/plugin_wrapper.rs
+++ b/plugins/wasm-rust/src/plugin_wrapper.rs
@@ -309,7 +309,9 @@ where
fn on_http_request_headers(&mut self, num_headers: usize, end_of_stream: bool) -> HeaderAction {
let binding = self.rule_matcher.borrow();
self.config = binding.get_match_config().map(|config| config.1.clone());
-
+ if self.config.is_none() {
+ return HeaderAction::Continue;
+ }
for (k, v) in self.get_http_request_headers_bytes() {
match String::from_utf8(v) {
Ok(header_value) => {
@@ -340,6 +342,9 @@ where
}
fn on_http_request_body(&mut self, body_size: usize, end_of_stream: bool) -> DataAction {
+ if self.config.is_none() {
+ return DataAction::Continue;
+ }
if !self.http_content.borrow().cache_request_body() {
return self
.http_content
@@ -362,6 +367,9 @@ where
}
fn on_http_request_trailers(&mut self, num_trailers: usize) -> Action {
+ if self.config.is_none() {
+ return Action::Continue;
+ }
self.http_content
.borrow_mut()
.on_http_request_trailers(num_trailers)
@@ -372,6 +380,9 @@ where
num_headers: usize,
end_of_stream: bool,
) -> HeaderAction {
+ if self.config.is_none() {
+ return HeaderAction::Continue;
+ }
for (k, v) in self.get_http_response_headers_bytes() {
match String::from_utf8(v) {
Ok(header_value) => {
@@ -399,6 +410,9 @@ where
}
fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> DataAction {
+ if self.config.is_none() {
+ return DataAction::Continue;
+ }
if !self.http_content.borrow().cache_response_body() {
return self
.http_content
@@ -423,6 +437,9 @@ where
}
fn on_http_response_trailers(&mut self, num_trailers: usize) -> Action {
+ if self.config.is_none() {
+ return Action::Continue;
+ }
self.http_content
.borrow_mut()
.on_http_response_trailers(num_trailers)
diff --git a/plugins/wasm-rust/src/request_wrapper.rs b/plugins/wasm-rust/src/request_wrapper.rs
index bc9624f6a9..c9a997456c 100644
--- a/plugins/wasm-rust/src/request_wrapper.rs
+++ b/plugins/wasm-rust/src/request_wrapper.rs
@@ -68,7 +68,7 @@ pub fn has_request_body() -> bool {
content_type, content_length_str, transfer_encoding
)
).unwrap();
- if !content_type.is_some_and(|x| !x.is_empty()) {
+ if content_type.is_some_and(|x| !x.is_empty()) {
return true;
}
if let Some(cl) = content_length_str {
From 0d79386ce2e10d5ead08f7384773bde7557b7bda Mon Sep 17 00:00:00 2001
From: Bingkun Zhao <49975170+sjtuzbk@users.noreply.github.com>
Date: Tue, 22 Oct 2024 22:17:56 +0800
Subject: [PATCH 22/64] fix a bug of ip-restriction plugin (#1422)
---
plugins/wasm-go/extensions/ip-restriction/main.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugins/wasm-go/extensions/ip-restriction/main.go b/plugins/wasm-go/extensions/ip-restriction/main.go
index bd2d1eb61b..7752639813 100644
--- a/plugins/wasm-go/extensions/ip-restriction/main.go
+++ b/plugins/wasm-go/extensions/ip-restriction/main.go
@@ -60,7 +60,7 @@ func parseConfig(json gjson.Result, config *RestrictionConfig, log wrapper.Log)
}
status := json.Get("status")
if status.Exists() && status.Uint() > 1 {
- config.Status = uint32(header.Uint())
+ config.Status = uint32(status.Uint())
} else {
config.Status = DefaultDenyStatus
}
From b8f5826a32d07c98c39a5c902218c72f0c23227d Mon Sep 17 00:00:00 2001
From: fengxsong
Date: Wed, 23 Oct 2024 09:03:07 +0800
Subject: [PATCH 23/64] fix: do not create ingressclass when it's empty (#1419)
Signed-off-by: fengxusong
---
helm/core/templates/ingressclass.yaml | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/helm/core/templates/ingressclass.yaml b/helm/core/templates/ingressclass.yaml
index 000e1117e3..47caa33eb3 100644
--- a/helm/core/templates/ingressclass.yaml
+++ b/helm/core/templates/ingressclass.yaml
@@ -1,6 +1,8 @@
+{{- if .Values.global.ingressClass }}
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: {{ .Values.global.ingressClass }}
spec:
- controller: higress.io/higress-controller
\ No newline at end of file
+ controller: higress.io/higress-controller
+{{- end }}
From bb6c43c767d129f83d9799413bb503ed86ec0713 Mon Sep 17 00:00:00 2001
From: mamba <371510756@qq.com>
Date: Wed, 23 Oct 2024 09:34:00 +0800
Subject: [PATCH 24/64] =?UTF-8?q?feat:=20=E3=80=90frontend-gray=E3=80=91?=
=?UTF-8?q?=E6=B7=BB=E5=8A=A0=20skipedRoutes=E4=BB=A5=E5=8F=8AskipedByHead?=
=?UTF-8?q?ers=20=E9=85=8D=E7=BD=AE=20(#1409)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Kent Dong
---
.../extensions/frontend-gray/README.md | 5 +-
.../extensions/frontend-gray/config/config.go | 5 ++
.../extensions/frontend-gray/envoy.yaml | 3 +
.../wasm-go/extensions/frontend-gray/main.go | 32 +++++---
.../extensions/frontend-gray/util/utils.go | 81 ++++++++++++++-----
.../frontend-gray/util/utils_test.go | 20 +++--
6 files changed, 101 insertions(+), 45 deletions(-)
diff --git a/plugins/wasm-go/extensions/frontend-gray/README.md b/plugins/wasm-go/extensions/frontend-gray/README.md
index dba4b73c0f..870100eb2f 100644
--- a/plugins/wasm-go/extensions/frontend-gray/README.md
+++ b/plugins/wasm-go/extensions/frontend-gray/README.md
@@ -20,6 +20,9 @@ description: 前端灰度插件配置参考
| `localStorageGrayKey` | string | 非必填 | - | 使用JWT鉴权方式,用户ID的唯一标识来自`localStorage`中,如果配置了当前参数,则`grayKey`失效 |
| `graySubKey` | string | 非必填 | - | 用户身份信息可能以JSON形式透出,比如:`userInfo:{ userCode:"001" }`,当前例子`graySubKey`取值为`userCode` |
| `userStickyMaxAge` | int | 非必填 | 172800 | 用户粘滞的时长:单位为秒,默认为`172800`,2天时间 |
+| `skippedPathPrefixes` | array of strings | 非必填 | - | 用于排除特定路径,避免当前插件处理这些请求。例如,在 rewrite 场景下,XHR 接口请求 `/api/xxx` 如果经过插件转发逻辑,可能会导致非预期的结果。 |
+| `skippedByHeaders` | map of string to string | 非必填 | - | 用于通过请求头过滤,指定哪些请求不被当前插件
+处理。`skippedPathPrefixes` 的优先级高于当前配置,且页面HTML请求不受本配置的影响。若本配置为空,默认会判断`sec-fetch-mode=cors`以及`upgrade=websocket`两个header头,进行过滤 |
| `rules` | array of object | 必填 | - | 用户定义不同的灰度规则,适配不同的灰度场景 |
| `rewrite` | object | 必填 | - | 重写配置,一般用于OSS/CDN前端部署的重写配置 |
| `baseDeployment` | object | 非必填 | - | 配置Base基线规则的配置 |
@@ -266,4 +269,4 @@ injection:
-
-
```
-通过 `injection`往HTML首页注入代码,可以在`head`标签注入代码,也可以在`body`标签的`first`和`last`位置注入代码。
\ No newline at end of file
+通过 `injection`往HTML首页注入代码,可以在`head`标签注入代码,也可以在`body`标签的`first`和`last`位置注入代码。
diff --git a/plugins/wasm-go/extensions/frontend-gray/config/config.go b/plugins/wasm-go/extensions/frontend-gray/config/config.go
index ecfbb3a836..25596fefef 100644
--- a/plugins/wasm-go/extensions/frontend-gray/config/config.go
+++ b/plugins/wasm-go/extensions/frontend-gray/config/config.go
@@ -12,6 +12,7 @@ const (
XPreHigressTag = "x-pre-higress-tag"
IsPageRequest = "is-page-request"
IsNotFound = "is-not-found"
+ EnabledGray = "enabled-gray"
)
type LogInfo func(format string, args ...interface{})
@@ -61,6 +62,8 @@ type GrayConfig struct {
GrayDeployments []*Deployment
BackendGrayTag string
Injection *Injection
+ SkippedPathPrefixes []string
+ SkippedByHeaders map[string]string
}
func convertToStringList(results []gjson.Result) []string {
@@ -91,6 +94,8 @@ func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) {
grayConfig.BackendGrayTag = json.Get("backendGrayTag").String()
grayConfig.UserStickyMaxAge = json.Get("userStickyMaxAge").String()
grayConfig.Html = json.Get("html").String()
+ grayConfig.SkippedPathPrefixes = convertToStringList(json.Get("skippedPathPrefixes").Array())
+ grayConfig.SkippedByHeaders = convertToStringMap(json.Get("skippedByHeaders"))
if grayConfig.UserStickyMaxAge == "" {
// 默认值2天
diff --git a/plugins/wasm-go/extensions/frontend-gray/envoy.yaml b/plugins/wasm-go/extensions/frontend-gray/envoy.yaml
index 239e221bd7..bc584d5ccd 100644
--- a/plugins/wasm-go/extensions/frontend-gray/envoy.yaml
+++ b/plugins/wasm-go/extensions/frontend-gray/envoy.yaml
@@ -82,6 +82,9 @@ static_resources:
"/app1": "/mfe/app1/{version}"
}
},
+ "skippedPathPrefixes": [
+ "/api/"
+ ],
"baseDeployment": {
"version": "dev"
},
diff --git a/plugins/wasm-go/extensions/frontend-gray/main.go b/plugins/wasm-go/extensions/frontend-gray/main.go
index b1b5d28aff..c5e738eeac 100644
--- a/plugins/wasm-go/extensions/frontend-gray/main.go
+++ b/plugins/wasm-go/extensions/frontend-gray/main.go
@@ -3,6 +3,7 @@ package main
import (
"fmt"
"net/http"
+ "path"
"strings"
"github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/config"
@@ -32,15 +33,18 @@ func parseConfig(json gjson.Result, grayConfig *config.GrayConfig, log wrapper.L
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action {
- if !util.IsGrayEnabled(grayConfig) {
+ requestPath, _ := proxywasm.GetHttpRequestHeader(":path")
+ requestPath = path.Clean(requestPath)
+ enabledGray := util.IsGrayEnabled(grayConfig, requestPath)
+ ctx.SetContext(config.EnabledGray, enabledGray)
+
+ if !enabledGray {
+ ctx.DontReadRequestBody()
return types.ActionContinue
}
cookies, _ := proxywasm.GetHttpRequestHeader("cookie")
- path, _ := proxywasm.GetHttpRequestHeader(":path")
- fetchMode, _ := proxywasm.GetHttpRequestHeader("sec-fetch-mode")
-
- isPageRequest := util.IsPageRequest(fetchMode, path)
+ isPageRequest := util.IsPageRequest(requestPath)
hasRewrite := len(grayConfig.Rewrite.File) > 0 || len(grayConfig.Rewrite.Index) > 0
grayKeyValueByCookie := util.ExtractCookieValueByKey(cookies, grayConfig.GrayKey)
grayKeyValueByHeader, _ := proxywasm.GetHttpRequestHeader(grayConfig.GrayKey)
@@ -73,7 +77,7 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig,
} else {
deployment = util.FilterGrayRule(&grayConfig, grayKeyValue)
}
- log.Infof("index deployment: %v, path: %v, backend: %v, xPreHigressVersion: %s,%s", deployment, path, deployment.BackendVersion, preVersion, preUniqueClientId)
+ log.Infof("index deployment: %v, path: %v, backend: %v, xPreHigressVersion: %s,%s", deployment, requestPath, deployment.BackendVersion, preVersion, preUniqueClientId)
} else {
grayDeployment := util.FilterGrayRule(&grayConfig, grayKeyValue)
deployment = util.GetVersion(grayConfig, grayDeployment, preVersion, isPageRequest)
@@ -91,14 +95,14 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig,
}
if hasRewrite {
- rewritePath := path
+ rewritePath := requestPath
if isPageRequest {
- rewritePath = util.IndexRewrite(path, deployment.Version, grayConfig.Rewrite.Index)
+ rewritePath = util.IndexRewrite(requestPath, deployment.Version, grayConfig.Rewrite.Index)
} else {
- rewritePath = util.PrefixFileRewrite(path, deployment.Version, grayConfig.Rewrite.File)
+ rewritePath = util.PrefixFileRewrite(requestPath, deployment.Version, grayConfig.Rewrite.File)
}
- if path != rewritePath {
- log.Infof("rewrite path:%s, rewritePath:%s, Version:%v", path, rewritePath, deployment.Version)
+ if requestPath != rewritePath {
+ log.Infof("rewrite path:%s, rewritePath:%s, Version:%v", requestPath, rewritePath, deployment.Version)
proxywasm.ReplaceHttpRequestHeader(":path", rewritePath)
}
}
@@ -106,7 +110,8 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig,
}
func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action {
- if !util.IsGrayEnabled(grayConfig) {
+ enabledGray, _ := ctx.GetContext(config.EnabledGray).(bool)
+ if !enabledGray {
ctx.DontReadResponseBody()
return types.ActionContinue
}
@@ -179,7 +184,8 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig,
}
func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, body []byte, log wrapper.Log) types.Action {
- if !util.IsGrayEnabled(grayConfig) {
+ enabledGray, _ := ctx.GetContext(config.EnabledGray).(bool)
+ if !enabledGray {
return types.ActionContinue
}
isPageRequest, ok := ctx.GetContext(config.IsPageRequest).(bool)
diff --git a/plugins/wasm-go/extensions/frontend-gray/util/utils.go b/plugins/wasm-go/extensions/frontend-gray/util/utils.go
index e67d3e0893..da93c68212 100644
--- a/plugins/wasm-go/extensions/frontend-gray/util/utils.go
+++ b/plugins/wasm-go/extensions/frontend-gray/util/utils.go
@@ -47,7 +47,39 @@ func GetRealIpFromXff(xff string) string {
return ""
}
-func IsGrayEnabled(grayConfig config.GrayConfig) bool {
+func IsRequestSkippedByHeaders(grayConfig config.GrayConfig) bool {
+ secFetchMode, _ := proxywasm.GetHttpRequestHeader("sec-fetch-mode")
+ upgrade, _ := proxywasm.GetHttpRequestHeader("upgrade")
+ if len(grayConfig.SkippedByHeaders) == 0 {
+ // 默认不走插件逻辑的header
+ return secFetchMode == "cors" || upgrade == "websocket"
+ }
+ for headerKey, headerValue := range grayConfig.SkippedByHeaders {
+ requestHeader, _ := proxywasm.GetHttpRequestHeader(headerKey)
+ if requestHeader == headerValue {
+ return true
+ }
+ }
+ return false
+}
+
+func IsGrayEnabled(grayConfig config.GrayConfig, requestPath string) bool {
+ // 当前路径中前缀为 SkipedRoute,则不走插件逻辑
+ for _, prefix := range grayConfig.SkippedPathPrefixes {
+ if strings.HasPrefix(requestPath, prefix) {
+ return false
+ }
+ }
+
+ // 如果是首页,进入插件逻辑
+ if IsPageRequest(requestPath) {
+ return true
+ }
+ // 检查header标识,判断是否需要跳过
+ if IsRequestSkippedByHeaders(grayConfig) {
+ return false
+ }
+
// 检查是否存在重写主机
if grayConfig.Rewrite != nil && grayConfig.Rewrite.Host != "" {
return true
@@ -132,29 +164,35 @@ var indexSuffixes = []string{
".html", ".htm", ".jsp", ".php", ".asp", ".aspx", ".erb", ".ejs", ".twig",
}
-func IsPageRequest(fetchMode string, myPath string) bool {
- if fetchMode == "cors" {
- return false
+func IsPageRequest(requestPath string) bool {
+ if requestPath == "/" || requestPath == "" {
+ return true
}
- ext := path.Ext(myPath)
+ ext := path.Ext(requestPath)
return ext == "" || ContainsValue(indexSuffixes, ext)
}
-// 首页Rewrite
-func IndexRewrite(path, version string, matchRules map[string]string) string {
- // Create a slice of keys in matchRules and sort them by length in descending order
+// SortKeysByLengthAndLexicographically 按长度降序和字典序排序键
+func SortKeysByLengthAndLexicographically(matchRules map[string]string) []string {
keys := make([]string, 0, len(matchRules))
for prefix := range matchRules {
keys = append(keys, prefix)
}
sort.Slice(keys, func(i, j int) bool {
if len(keys[i]) != len(keys[j]) {
- return len(keys[i]) > len(keys[j]) // Sort by length
+ return len(keys[i]) > len(keys[j]) // 按长度排序
}
- return keys[i] < keys[j] // Sort lexicographically
+ return keys[i] < keys[j] // 按字典序排序
})
+ return keys
+}
- // Iterate over sorted keys to find the longest match
+// 首页Rewrite
+func IndexRewrite(path, version string, matchRules map[string]string) string {
+ // 使用新的排序函数
+ keys := SortKeysByLengthAndLexicographically(matchRules)
+
+ // 遍历排序后的键以找到最长匹配
for _, prefix := range keys {
if strings.HasPrefix(path, prefix) {
rewrite := matchRules[prefix]
@@ -166,18 +204,21 @@ func IndexRewrite(path, version string, matchRules map[string]string) string {
}
func PrefixFileRewrite(path, version string, matchRules map[string]string) string {
- var matchedPrefix, replacement string
- for prefix, template := range matchRules {
+ // 对规则的键进行排序
+ sortedKeys := SortKeysByLengthAndLexicographically(matchRules)
+
+ // 遍历排序后的键
+ for _, prefix := range sortedKeys {
if strings.HasPrefix(path, prefix) {
- if len(prefix) > len(matchedPrefix) { // 找到更长的前缀
- matchedPrefix = prefix
- replacement = strings.Replace(template, "{version}", version, 1)
- }
+ // 找到第一个匹配的前缀就停止,因为它是最长的匹配
+ replacement := strings.Replace(matchRules[prefix], "{version}", version, 1)
+ newPath := strings.Replace(path, prefix, replacement+"/", 1)
+ return filepath.Clean(newPath)
}
}
- // 将path 中的前缀部分用 replacement 替换掉
- newPath := strings.Replace(path, matchedPrefix, replacement+"/", 1)
- return filepath.Clean(newPath)
+
+ // 如果没有匹配,返回原始路径
+ return path
}
func GetVersion(grayConfig config.GrayConfig, deployment *config.Deployment, xPreHigressVersion string, isPageRequest bool) *config.Deployment {
diff --git a/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go b/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go
index b6681c98af..5fb69bb364 100644
--- a/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go
+++ b/plugins/wasm-go/extensions/frontend-gray/util/utils_test.go
@@ -108,22 +108,20 @@ func TestPrefixFileRewrite(t *testing.T) {
func TestIsPageRequest(t *testing.T) {
var tests = []struct {
- fetchMode string
- p string
- output bool
+ p string
+ output bool
}{
- {"cors", "/js/a.js", false},
- {"no-cors", "/js/a.js", false},
- {"no-cors", "/images/a.png", false},
- {"no-cors", "/index", true},
- {"cors", "/inde", false},
- {"no-cors", "/index.html", true},
- {"no-cors", "/demo.php", true},
+ {"/js/a.js", false},
+ {"/js/a.js", false},
+ {"/images/a.png", false},
+ {"/index", true},
+ {"/index.html", true},
+ {"/demo.php", true},
}
for _, test := range tests {
testPath := test.p
t.Run(testPath, func(t *testing.T) {
- output := IsPageRequest(test.fetchMode, testPath)
+ output := IsPageRequest(testPath)
assert.Equal(t, test.output, output)
})
}
From d76f574ab303950080aff800e3361cef7b9de6eb Mon Sep 17 00:00:00 2001
From: 007gzs <007gzs@gmail.com>
Date: Wed, 23 Oct 2024 13:19:02 +0800
Subject: [PATCH 25/64] plugin ai-data-mask add log (#1423)
---
.../extensions/ai-data-masking/README.md | 2 +-
.../extensions/ai-data-masking/README_EN.md | 2 +-
.../extensions/ai-data-masking/src/lib.rs | 37 +++++++++++++++----
3 files changed, 31 insertions(+), 10 deletions(-)
diff --git a/plugins/wasm-rust/extensions/ai-data-masking/README.md b/plugins/wasm-rust/extensions/ai-data-masking/README.md
index 185d559d13..b892b3dab8 100644
--- a/plugins/wasm-rust/extensions/ai-data-masking/README.md
+++ b/plugins/wasm-rust/extensions/ai-data-masking/README.md
@@ -36,7 +36,7 @@ description: AI 数据脱敏插件配置参考
| deny_openai | bool | true | 对openai协议进行拦截 |
| deny_jsonpath | string | [] | 对指定jsonpath拦截 |
| deny_raw | bool | false | 对原始body拦截 |
-| system_deny | bool | true | 开启内置拦截规则 |
+| system_deny | bool | false | 开启内置拦截规则 |
| deny_code | int | 200 | 拦截时http状态码 |
| deny_message | string | 提问或回答中包含敏感词,已被屏蔽 | 拦截时ai返回消息 |
| deny_raw_message | string | {"errmsg":"提问或回答中包含敏感词,已被屏蔽"} | 非openai拦截时返回内容 |
diff --git a/plugins/wasm-rust/extensions/ai-data-masking/README_EN.md b/plugins/wasm-rust/extensions/ai-data-masking/README_EN.md
index 6b03e2122a..45e1622875 100644
--- a/plugins/wasm-rust/extensions/ai-data-masking/README_EN.md
+++ b/plugins/wasm-rust/extensions/ai-data-masking/README_EN.md
@@ -31,7 +31,7 @@ Plugin Execution Priority: `991`
| deny_openai | bool | true | Intercept openai protocol |
| deny_jsonpath | string | [] | Intercept specified jsonpath |
| deny_raw | bool | false | Intercept raw body |
-| system_deny | bool | true | Enable built-in interception rules |
+| system_deny | bool | false | Enable built-in interception rules |
| deny_code | int | 200 | HTTP status code when intercepted |
| deny_message | string | Sensitive words found in the question or answer have been blocked | AI returned message when intercepted |
| deny_raw_message | string | {"errmsg":"Sensitive words found in the question or answer have been blocked"} | Content returned when not openai intercepted |
diff --git a/plugins/wasm-rust/extensions/ai-data-masking/src/lib.rs b/plugins/wasm-rust/extensions/ai-data-masking/src/lib.rs
index 99d0144e97..89c0da5219 100644
--- a/plugins/wasm-rust/extensions/ai-data-masking/src/lib.rs
+++ b/plugins/wasm-rust/extensions/ai-data-masking/src/lib.rs
@@ -71,6 +71,7 @@ struct AiDataMasking {
is_openai: bool,
stream: bool,
res_body: Bytes,
+ log: Log,
}
fn deserialize_regexp<'de, D>(deserializer: D) -> Result
where
@@ -159,7 +160,7 @@ fn default_deny_raw() -> bool {
false
}
fn default_system_deny() -> bool {
- true
+ false
}
fn default_deny_code() -> u16 {
200
@@ -257,13 +258,13 @@ impl DenyWord {
DenyWord::empty()
}
- fn check(&self, message: &str) -> bool {
+ fn check(&self, message: &str) -> Option {
for word in self.jieba.cut(message, true) {
if self.words.contains(word) {
- return true;
+ return Some(word.to_string());
}
}
- false
+ None
}
}
impl System {
@@ -377,17 +378,30 @@ impl RootContextWrapper for AiDataMaskingRoot {
is_openai: false,
stream: false,
res_body: Bytes::new(),
+ log: Log::new(PLUGIN_NAME.to_string()),
}))
}
}
impl AiDataMasking {
fn check_message(&self, message: &str) -> bool {
if let Some(config) = &self.config {
- config.deny_words.check(message)
- || (config.system_deny && SYSTEM.deny_word.check(message))
- } else {
- false
+ if let Some(word) = config.deny_words.check(message) {
+ self.log().warn(&format!(
+ "custom deny word {} matched from {}",
+ word, message
+ ));
+ return true;
+ } else if config.system_deny {
+ if let Some(word) = SYSTEM.deny_word.check(message) {
+ self.log().warn(&format!(
+ "system deny word {} matched from {}",
+ word, message
+ ));
+ return true;
+ }
+ }
}
+ false
}
fn msg_to_response(&self, msg: &str, raw_msg: &str, content_type: &str) -> (String, String) {
if !self.is_openai {
@@ -509,6 +523,10 @@ impl AiDataMasking {
}
}
}
+ if msg != message {
+ self.log()
+ .debug(&format!("replace_request_msg from {} to {}", message, msg));
+ }
msg
}
}
@@ -590,6 +608,9 @@ impl HttpContext for AiDataMasking {
}
}
impl HttpContextWrapper for AiDataMasking {
+ fn log(&self) -> &Log {
+ &self.log
+ }
fn on_config(&mut self, config: Rc) {
self.config = Some(config.clone());
}
From a5ccb90b282b841b5e56e90d99c07601ad64204c Mon Sep 17 00:00:00 2001
From: Ankur Singh
Date: Thu, 24 Oct 2024 07:08:22 +0530
Subject: [PATCH 26/64] Improve the grammar of the sentence (#1426)
---
CONTRIBUTING_EN.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CONTRIBUTING_EN.md b/CONTRIBUTING_EN.md
index 25539cc314..30b3410838 100644
--- a/CONTRIBUTING_EN.md
+++ b/CONTRIBUTING_EN.md
@@ -1,6 +1,6 @@
# Contributing to Higress
-It is warmly welcomed if you have interest to hack on Higress. First, we encourage this kind of willing very much. And here is a list of contributing guide for you.
+Your interest in contributing to Higress is warmly welcomed. First, we encourage this kind of willing very much. And here is a list of contributing guide for you.
[[中文贡献文档](./CONTRIBUTING_CN.md)]
From cdd71155a9388f2c95bd1de6365c065b0d2fe479 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=BE=84=E6=BD=AD?=
Date: Thu, 24 Oct 2024 11:25:59 +0800
Subject: [PATCH 27/64] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 761f8591a8..c93bf3b15e 100644
--- a/README.md
+++ b/README.md
@@ -59,7 +59,7 @@ docker run -d --rm --name higress-ai -v ${PWD}:/data \
- 8080 端口:网关 HTTP 协议入口
- 8443 端口:网关 HTTPS 协议入口
-**Higress 的所有 Docker 镜像都一直使用自己独享的仓库,不受 Docker Hub 境内不可访问的影响**
+**Higress 的所有 Docker 镜像都一直使用自己独享的仓库,不受 Docker Hub 境内访问受限的影响**
K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start 文档](https://higress.cn/docs/latest/user/quickstart/)。
From e7561c30e55e6dddd4f2fcf12d779994b100b298 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=BA=AA=E5=8D=93=E5=BF=97?=
Date: Thu, 24 Oct 2024 16:58:45 +0800
Subject: [PATCH 28/64] feat: implements text/event-stream(SSE) MIME parser
(#1416)
Co-authored-by: 007gzs <007gzs@gmail.com>
---
plugins/wasm-rust/Makefile | 13 +-
.../wasm-rust/example/sse-timing/Cargo.lock | 270 ++++++++++++++++++
.../wasm-rust/example/sse-timing/Cargo.toml | 15 +
plugins/wasm-rust/example/sse-timing/Makefile | 10 +
.../wasm-rust/example/sse-timing/README.md | 26 ++
.../example/sse-timing/docker-compose.yaml | 35 +++
.../wasm-rust/example/sse-timing/envoy.yaml | 76 +++++
.../wasm-rust/example/sse-timing/src/lib.rs | 198 +++++++++++++
.../example/sse-timing/sse-server/Dockerfile | 5 +
.../example/sse-timing/sse-server/go.mod | 3 +
.../example/sse-timing/sse-server/main.go | 42 +++
plugins/wasm-rust/src/event_stream.rs | 197 +++++++++++++
plugins/wasm-rust/src/lib.rs | 1 +
13 files changed, 885 insertions(+), 6 deletions(-)
create mode 100644 plugins/wasm-rust/example/sse-timing/Cargo.lock
create mode 100644 plugins/wasm-rust/example/sse-timing/Cargo.toml
create mode 100644 plugins/wasm-rust/example/sse-timing/Makefile
create mode 100644 plugins/wasm-rust/example/sse-timing/README.md
create mode 100644 plugins/wasm-rust/example/sse-timing/docker-compose.yaml
create mode 100644 plugins/wasm-rust/example/sse-timing/envoy.yaml
create mode 100644 plugins/wasm-rust/example/sse-timing/src/lib.rs
create mode 100644 plugins/wasm-rust/example/sse-timing/sse-server/Dockerfile
create mode 100644 plugins/wasm-rust/example/sse-timing/sse-server/go.mod
create mode 100644 plugins/wasm-rust/example/sse-timing/sse-server/main.go
create mode 100644 plugins/wasm-rust/src/event_stream.rs
diff --git a/plugins/wasm-rust/Makefile b/plugins/wasm-rust/Makefile
index 25587e9698..5bc1809a38 100644
--- a/plugins/wasm-rust/Makefile
+++ b/plugins/wasm-rust/Makefile
@@ -6,12 +6,6 @@ IMAGE_TAG = $(if $(strip $(PLUGIN_VERSION)),${PLUGIN_VERSION},${BUILD_TIME}-${CO
IMG ?= ${REGISTRY}${PLUGIN_NAME}:${IMAGE_TAG}
.DEFAULT:
-lint-base:
- cargo fmt --all --check
- cargo clippy --workspace --all-features --all-targets
-lint:
- cargo fmt --all --check --manifest-path extensions/${PLUGIN_NAME}/Cargo.toml
- cargo clippy --workspace --all-features --all-targets --manifest-path extensions/${PLUGIN_NAME}/Cargo.toml
build:
DOCKER_BUILDKIT=1 docker build \
--build-arg PLUGIN_NAME=${PLUGIN_NAME} \
@@ -20,3 +14,10 @@ build:
.
@echo ""
@echo "output wasm file: extensions/${PLUGIN_NAME}/plugin.wasm"
+
+lint-base:
+ cargo fmt --all --check
+ cargo clippy --workspace --all-features --all-targets
+lint:
+ cargo fmt --all --check --manifest-path extensions/${PLUGIN_NAME}/Cargo.toml
+ cargo clippy --workspace --all-features --all-targets --manifest-path extensions/${PLUGIN_NAME}/Cargo.toml
diff --git a/plugins/wasm-rust/example/sse-timing/Cargo.lock b/plugins/wasm-rust/example/sse-timing/Cargo.lock
new file mode 100644
index 0000000000..9123a0e01c
--- /dev/null
+++ b/plugins/wasm-rust/example/sse-timing/Cargo.lock
@@ -0,0 +1,270 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "ahash"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
+
+[[package]]
+name = "bytes"
+version = "1.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "downcast-rs"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "getrandom"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+dependencies = [
+ "ahash",
+ "allocator-api2",
+]
+
+[[package]]
+name = "higress-wasm-rust"
+version = "0.1.0"
+dependencies = [
+ "downcast-rs",
+ "http",
+ "lazy_static",
+ "multimap",
+ "proxy-wasm",
+ "serde",
+ "serde_json",
+ "uuid",
+]
+
+[[package]]
+name = "http"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.161"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
+
+[[package]]
+name = "log"
+version = "0.4.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "multimap"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.88"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "proxy-wasm"
+version = "0.2.2"
+source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#6735737fad486c8a7cc324241f58df4a160e7887"
+dependencies = [
+ "downcast-rs",
+ "hashbrown",
+ "log",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+
+[[package]]
+name = "serde"
+version = "1.0.210"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.210"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.132"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sse-timing"
+version = "0.1.0"
+dependencies = [
+ "higress-wasm-rust",
+ "proxy-wasm",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
+
+[[package]]
+name = "uuid"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "zerocopy"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/plugins/wasm-rust/example/sse-timing/Cargo.toml b/plugins/wasm-rust/example/sse-timing/Cargo.toml
new file mode 100644
index 0000000000..44c4c5dfd7
--- /dev/null
+++ b/plugins/wasm-rust/example/sse-timing/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "sse-timing"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+higress-wasm-rust = { path = "../../", version = "0.1.0" }
+proxy-wasm = { git="https://github.com/higress-group/proxy-wasm-rust-sdk", branch="main", version="0.2.2" }
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
diff --git a/plugins/wasm-rust/example/sse-timing/Makefile b/plugins/wasm-rust/example/sse-timing/Makefile
new file mode 100644
index 0000000000..22ec19d791
--- /dev/null
+++ b/plugins/wasm-rust/example/sse-timing/Makefile
@@ -0,0 +1,10 @@
+BUILD_OPTS="--release"
+
+.DEFAULT:
+build:
+ cargo build --target wasm32-wasi ${BUILD_OPTS}
+ find target -name "*.wasm" -d 3 -exec cp "{}" plugin.wasm \;
+
+clean:
+ cargo clean
+ rm -f plugin.wasm
diff --git a/plugins/wasm-rust/example/sse-timing/README.md b/plugins/wasm-rust/example/sse-timing/README.md
new file mode 100644
index 0000000000..b7d8d4bfdc
--- /dev/null
+++ b/plugins/wasm-rust/example/sse-timing/README.md
@@ -0,0 +1,26 @@
+## Proxy-Wasm plugin example: SSE Timing
+
+Proxy-Wasm plugin that traces Server-Side Event(SSE) duration from request start.
+
+### Building
+
+```sh
+$ make
+```
+
+### Using in Envoy
+
+This example can be run with [`docker compose`](https://docs.docker.com/compose/install/)
+and has a matching Envoy configuration.
+
+```sh
+$ docker compose up
+```
+
+#### Access granted.
+
+Send HTTP request to `localhost:10000/`:
+
+```sh
+$ curl localhost:10000/
+```
diff --git a/plugins/wasm-rust/example/sse-timing/docker-compose.yaml b/plugins/wasm-rust/example/sse-timing/docker-compose.yaml
new file mode 100644
index 0000000000..78549a2ac7
--- /dev/null
+++ b/plugins/wasm-rust/example/sse-timing/docker-compose.yaml
@@ -0,0 +1,35 @@
+# Copyright (c) 2023 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.
+
+services:
+ envoy:
+ image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/all-in-one:latest
+ entrypoint: /usr/local/bin/envoy
+ command: -c /etc/envoy/envoy.yaml --component-log-level wasm:debug
+ depends_on:
+ - sse-server
+ hostname: envoy
+ ports:
+ - "10000:10000"
+ volumes:
+ - ./envoy.yaml:/etc/envoy/envoy.yaml
+ - ./target/wasm32-wasi/release:/etc/envoy/proxy-wasm-plugins
+ networks:
+ - envoymesh
+ sse-server:
+ build: sse-server
+ networks:
+ - envoymesh
+networks:
+ envoymesh: {}
\ No newline at end of file
diff --git a/plugins/wasm-rust/example/sse-timing/envoy.yaml b/plugins/wasm-rust/example/sse-timing/envoy.yaml
new file mode 100644
index 0000000000..6281aad0d9
--- /dev/null
+++ b/plugins/wasm-rust/example/sse-timing/envoy.yaml
@@ -0,0 +1,76 @@
+# Copyright (c) 2023 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.
+
+static_resources:
+ listeners:
+ - name: listener_0
+ address:
+ socket_address:
+ protocol: TCP
+ address: 0.0.0.0
+ port_value: 10000
+ filter_chains:
+ - filters:
+ - name: envoy.filters.network.http_connection_manager
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+ stat_prefix: ingress_http
+ route_config:
+ name: local_route
+ virtual_hosts:
+ - name: local_service
+ domains: ["*"]
+ routes:
+ - match:
+ prefix: "/"
+ route:
+ cluster: sse-server
+ http_filters:
+ - name: envoy.filters.http.wasm
+ typed_config:
+ "@type": type.googleapis.com/udpa.type.v1.TypedStruct
+ type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
+ value:
+ config:
+ name: "http_body"
+ configuration:
+ "@type": type.googleapis.com/google.protobuf.StringValue
+ value: |-
+ {
+ "name": "sse_timing",
+ "_rules_": []
+ }
+ vm_config:
+ runtime: "envoy.wasm.runtime.v8"
+ code:
+ local:
+ filename: "/etc/envoy/proxy-wasm-plugins/sse_timing.wasm"
+ - name: envoy.filters.http.router
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+ clusters:
+ - name: sse-server
+ connect_timeout: 30s
+ type: LOGICAL_DNS
+# dns_lookup_family: V4_ONLY
+ lb_policy: ROUND_ROBIN
+ load_assignment:
+ cluster_name: sse-server
+ endpoints:
+ - lb_endpoints:
+ - endpoint:
+ address:
+ socket_address:
+ address: sse-server
+ port_value: 8080
\ No newline at end of file
diff --git a/plugins/wasm-rust/example/sse-timing/src/lib.rs b/plugins/wasm-rust/example/sse-timing/src/lib.rs
new file mode 100644
index 0000000000..ab1fa4f6c4
--- /dev/null
+++ b/plugins/wasm-rust/example/sse-timing/src/lib.rs
@@ -0,0 +1,198 @@
+// Copyright (c) 2023 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.
+
+use higress_wasm_rust::event_stream::EventStream;
+use higress_wasm_rust::log::Log;
+use higress_wasm_rust::rule_matcher::{on_configure, RuleMatcher, SharedRuleMatcher};
+use proxy_wasm::traits::{Context, HttpContext, RootContext};
+use proxy_wasm::types::{ContextType, DataAction, HeaderAction, LogLevel};
+use serde::Deserialize;
+use std::cell::RefCell;
+use std::ops::DerefMut;
+use std::rc::Rc;
+use std::str::from_utf8;
+use std::time::{Duration, SystemTime};
+
+proxy_wasm::main! {{
+ proxy_wasm::set_log_level(LogLevel::Trace);
+ proxy_wasm::set_root_context(|_|Box::new(SseTimingRoot::new()));
+}}
+
+struct SseTimingRoot {
+ log: Rc,
+ rule_matcher: SharedRuleMatcher,
+}
+
+struct SseTiming {
+ log: Rc,
+ rule_matcher: SharedRuleMatcher,
+ vendor: String,
+ is_event_stream: bool,
+ event_stream: EventStream,
+ start_time: SystemTime,
+}
+
+#[derive(Default, Clone, Debug, Deserialize)]
+struct SseTimingConfig {
+ vendor: Option,
+}
+
+impl SseTimingRoot {
+ fn new() -> Self {
+ SseTimingRoot {
+ log: Rc::new(Log::new("sse_timing".to_string())),
+ rule_matcher: Rc::new(RefCell::new(RuleMatcher::default())),
+ }
+ }
+}
+
+impl Context for SseTimingRoot {}
+
+impl RootContext for SseTimingRoot {
+ fn on_configure(&mut self, _plugin_configuration_size: usize) -> bool {
+ on_configure(
+ self,
+ _plugin_configuration_size,
+ self.rule_matcher.borrow_mut().deref_mut(),
+ &self.log,
+ )
+ }
+
+ fn create_http_context(&self, _context_id: u32) -> Option> {
+ Some(Box::new(SseTiming {
+ log: self.log.clone(),
+ rule_matcher: self.rule_matcher.clone(),
+ vendor: "higress".into(),
+ is_event_stream: false,
+ event_stream: EventStream::new(),
+ start_time: self.get_current_time(),
+ }))
+ }
+
+ fn get_type(&self) -> Option {
+ Some(ContextType::HttpContext)
+ }
+}
+
+impl Context for SseTiming {}
+
+impl HttpContext for SseTiming {
+ fn on_http_request_headers(
+ &mut self,
+ _num_headers: usize,
+ _end_of_stream: bool,
+ ) -> HeaderAction {
+ self.start_time = self.get_current_time();
+
+ let binding = self.rule_matcher.borrow();
+ let config = match binding.get_match_config() {
+ None => {
+ return HeaderAction::Continue;
+ }
+ Some(config) => config.1,
+ };
+ match config.vendor.clone() {
+ None => {}
+ Some(vendor) => self.vendor = vendor,
+ }
+ HeaderAction::Continue
+ }
+
+ fn on_http_response_headers(
+ &mut self,
+ _num_headers: usize,
+ _end_of_stream: bool,
+ ) -> HeaderAction {
+ match self.get_http_response_header("Content-Type") {
+ None => self
+ .log
+ .warn("upstream response is not set Content-Type, skipped"),
+ Some(content_type) => {
+ if content_type.starts_with("text/event-stream") {
+ self.is_event_stream = true
+ } else {
+ self.log.warn(format!("upstream response Content-Type is not text/event-stream, but {}, skipped", content_type).as_str())
+ }
+ }
+ }
+ HeaderAction::Continue
+ }
+
+ fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> DataAction {
+ if !self.is_event_stream {
+ return DataAction::Continue;
+ }
+
+ let body = self
+ .get_http_response_body(0, body_size)
+ .unwrap_or_default();
+ self.event_stream.update(body);
+ self.process_event_stream(end_of_stream)
+ }
+}
+
+impl SseTiming {
+ fn process_event_stream(&mut self, end_of_stream: bool) -> DataAction {
+ let mut modified_events = Vec::new();
+
+ loop {
+ match self.event_stream.next() {
+ None => break,
+ Some(raw_event) => {
+ if !raw_event.is_empty() {
+ // according to spec, event-stream must be utf-8 encoding
+ let event = from_utf8(raw_event.as_slice()).unwrap();
+ let processed_event = self.process_event(event.to_string());
+ modified_events.push(processed_event);
+ }
+ }
+ }
+ }
+
+ if end_of_stream {
+ match self.event_stream.flush() {
+ None => {}
+ Some(raw_event) => {
+ if !raw_event.is_empty() {
+ // according to spec, event-stream must be utf-8 encoding
+ let event = from_utf8(raw_event.as_slice()).unwrap();
+ let modified_event = self.process_event(event.into());
+ modified_events.push(modified_event);
+ }
+ }
+ }
+ }
+
+ if !modified_events.is_empty() {
+ let modified_body = modified_events.concat();
+ self.set_http_response_body(0, modified_body.len(), modified_body.as_bytes());
+ DataAction::Continue
+ } else {
+ DataAction::StopIterationNoBuffer
+ }
+ }
+
+ fn process_event(&self, event: String) -> String {
+ let duration = self
+ .get_current_time()
+ .duration_since(self.start_time)
+ .unwrap_or(Duration::ZERO);
+ format!(
+ ": server-timing: {};dur={}\n{}\n\n",
+ self.vendor,
+ duration.as_millis(),
+ event
+ )
+ }
+}
diff --git a/plugins/wasm-rust/example/sse-timing/sse-server/Dockerfile b/plugins/wasm-rust/example/sse-timing/sse-server/Dockerfile
new file mode 100644
index 0000000000..7d251e48f9
--- /dev/null
+++ b/plugins/wasm-rust/example/sse-timing/sse-server/Dockerfile
@@ -0,0 +1,5 @@
+FROM golang:latest AS builder
+WORKDIR /workspace
+COPY . .
+RUN GOOS=linux GOARCH=amd64 go build -o main .
+CMD ./main
\ No newline at end of file
diff --git a/plugins/wasm-rust/example/sse-timing/sse-server/go.mod b/plugins/wasm-rust/example/sse-timing/sse-server/go.mod
new file mode 100644
index 0000000000..63e8515e6c
--- /dev/null
+++ b/plugins/wasm-rust/example/sse-timing/sse-server/go.mod
@@ -0,0 +1,3 @@
+module sse
+
+go 1.22
diff --git a/plugins/wasm-rust/example/sse-timing/sse-server/main.go b/plugins/wasm-rust/example/sse-timing/sse-server/main.go
new file mode 100644
index 0000000000..bb4a3089fd
--- /dev/null
+++ b/plugins/wasm-rust/example/sse-timing/sse-server/main.go
@@ -0,0 +1,42 @@
+package main
+
+import (
+ "log"
+ "net/http"
+ "time"
+)
+
+var events = []string{
+ ": this is a test stream\n\n",
+
+ "data: some text\n",
+ "data: another message\n",
+ "data: with two lines\n\n",
+
+ "event: userconnect\n",
+ "data: {\"username\": \"bobby\", \"time\": \"02:33:48\"}\n\n",
+
+ "event: usermessage\n",
+ "data: {\"username\": \"bobby\", \"time\": \"02:34:11\", \"text\": \"Hi everyone.\"}\n\n",
+
+ "event: userdisconnect\n",
+ "data: {\"username\": \"bobby\", \"time\": \"02:34:23\"}\n\n",
+
+ "event: usermessage\n",
+ "data: {\"username\": \"sean\", \"time\": \"02:34:36\", \"text\": \"Bye, bobby.\"}\n\n",
+}
+
+func main() {
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ log.Println("receive request")
+ w.Header().Set("Content-Type", "text/event-stream")
+ for _, e := range events {
+ _, _ = w.Write([]byte(e))
+ time.Sleep(1 * time.Second)
+ w.(http.Flusher).Flush()
+ }
+ })
+ if err := http.ListenAndServe("0.0.0.0:8080", nil); err != nil {
+ panic(err)
+ }
+}
diff --git a/plugins/wasm-rust/src/event_stream.rs b/plugins/wasm-rust/src/event_stream.rs
new file mode 100644
index 0000000000..f28846317a
--- /dev/null
+++ b/plugins/wasm-rust/src/event_stream.rs
@@ -0,0 +1,197 @@
+// Copyright (c) 2024 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.
+
+/// Parsing MIME type text/event-stream according to https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream
+///
+/// The event stream format is as described by the stream production of the following ABNF
+///
+/// | rule | expression |
+/// |--------|---------------------------|
+/// |stream |= [ bom ] *event |
+/// |event |= *( comment / field ) eol |
+/// |comment |= colon *any-char eol |
+/// |field |= 1*name-char [ colon [ space ] *any-char ] eol |
+/// |eol |= ( cr lf / cr / lf ) |
+///
+/// According to spec, we must judge EOL twice before we can identify a complete event.
+/// However, in the rules of event and field, there is an ambiguous grammar in the judgment of eol,
+/// and it will bring ambiguity (whether the field ends). In order to eliminate this ambiguity,
+/// we believe that CRLF as CR+LF belongs to event and field respectively.
+pub struct EventStream {
+ buffer: Vec,
+ processed_offset: usize,
+}
+
+impl EventStream {
+ pub fn new() -> Self {
+ EventStream {
+ buffer: Vec::new(),
+ processed_offset: 0,
+ }
+ }
+
+ /// Update the event stream by adding new data to the buffer and resetting processed offset if needed.
+ pub fn update(&mut self, data: Vec) {
+ if self.processed_offset > 0 {
+ self.buffer.drain(0..self.processed_offset);
+ self.processed_offset = 0;
+ }
+
+ self.buffer.extend(data);
+ }
+
+ /// Get the next event from the event stream. Return the event data if available, otherwise return None.
+ /// Next will consume all the data in the current buffer. However, if there is a valid event at the end of the buffer,
+ /// it will return the event directly even if the data after the next `update` could be considered part of the same event
+ /// (especially in cases where CRLF hits an ambiguous grammar).
+ /// When this happens, the next call to next may return an empty event.
+ ///
+ /// ```
+ /// let mut parser = EventStream::new();
+ /// parser.update(...);
+ /// loop {
+ /// match parser.next() {
+ /// None => {}
+ /// Some(event) => {
+ /// if !event.is_empty() {
+ /// ...
+ /// }
+ /// }
+ /// }
+ /// }
+ /// ```
+ pub fn next(&mut self) -> Option> {
+ let mut i = self.processed_offset;
+
+ while i < self.buffer.len() {
+ if let Some(size) = self.is_2eol(i) {
+ let event = self.buffer[self.processed_offset..i].to_vec();
+ self.processed_offset = i + size;
+ return Some(event);
+ }
+
+ i += 1;
+ }
+
+ None
+ }
+
+ /// Flush the event stream and return any remaining unprocessed event data. Return None if there is none.
+ pub fn flush(&mut self) -> Option> {
+ if self.processed_offset < self.buffer.len() {
+ let remaining_event = self.buffer[self.processed_offset..].to_vec();
+ self.processed_offset = self.buffer.len();
+ Some(remaining_event)
+ } else {
+ None
+ }
+ }
+
+ fn is_eol(&self, i: usize) -> Option {
+ if i + 1 < self.buffer.len() && self.buffer[i] == b'\r' && self.buffer[i + 1] == b'\n' {
+ Some(2)
+ } else if self.buffer[i] == b'\r' || self.buffer[i] == b'\n' {
+ Some(1)
+ } else {
+ None
+ }
+ }
+
+ fn is_2eol(&self, i: usize) -> Option {
+ let size1 = match self.is_eol(i) {
+ None => return None,
+ Some(size1) => size1,
+ };
+ if i + size1 < self.buffer.len() {
+ match self.is_eol(i + size1) {
+ None => {
+ if size1 == 2 {
+ Some(2)
+ } else {
+ None
+ }
+ }
+ Some(size2) => Some(size1 + size2),
+ }
+ } else if size1 == 2 {
+ Some(2)
+ } else {
+ None
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_crlf_events() {
+ let mut parser = EventStream::new();
+ parser.update(b"event1\n\nevent2\n\n".to_vec());
+
+ assert_eq!(parser.next(), Some(b"event1".to_vec()));
+ assert_eq!(parser.next(), Some(b"event2".to_vec()));
+ }
+
+ #[test]
+ fn test_lf_events() {
+ let mut parser = EventStream::new();
+ parser.update(b"event3\n\r\nevent4\r\n".to_vec());
+
+ assert_eq!(parser.next(), Some(b"event3".to_vec()));
+ assert_eq!(parser.next(), Some(b"event4".to_vec()));
+ }
+
+ #[test]
+ fn test_partial_event() {
+ let mut parser = EventStream::new();
+ parser.update(b"partial_event1".to_vec());
+
+ assert_eq!(parser.next(), None);
+
+ parser.update(b"\n\n".to_vec());
+ assert_eq!(parser.next(), Some(b"partial_event1".to_vec()));
+ }
+
+ #[test]
+ fn test_mixed_eol_events() {
+ let mut parser = EventStream::new();
+ parser.update(b"event5\r\nevent6\r\n\r\nevent7\r\n".to_vec());
+
+ assert_eq!(parser.next(), Some(b"event5".to_vec()));
+ assert_eq!(parser.next(), Some(b"event6".to_vec()));
+ assert_eq!(parser.next(), Some(b"event7".to_vec()));
+ }
+
+ #[test]
+ fn test_mixed2_eol_events() {
+ let mut parser = EventStream::new();
+ parser.update(b"event5\r\nevent6\r\n".to_vec());
+ assert_eq!(parser.next(), Some(b"event5".to_vec()));
+ assert_eq!(parser.next(), Some(b"event6".to_vec()));
+ parser.update(b"\r\nevent7\r\n".to_vec());
+ assert_eq!(parser.next(), Some(b"".to_vec()));
+ assert_eq!(parser.next(), Some(b"event7".to_vec()));
+ }
+
+ #[test]
+ fn test_no_event() {
+ let mut parser = EventStream::new();
+ parser.update(b"no_eol_in_this_string".to_vec());
+
+ assert_eq!(parser.next(), None);
+ assert_eq!(parser.flush(), Some(b"no_eol_in_this_string".to_vec()));
+ }
+}
diff --git a/plugins/wasm-rust/src/lib.rs b/plugins/wasm-rust/src/lib.rs
index 3296ff648a..f6a92be89b 100644
--- a/plugins/wasm-rust/src/lib.rs
+++ b/plugins/wasm-rust/src/lib.rs
@@ -14,6 +14,7 @@
pub mod cluster_wrapper;
pub mod error;
+pub mod event_stream;
mod internal;
pub mod log;
pub mod plugin_wrapper;
From d952fa562bee3c8e5ce434ce32cf141c6991fb6a Mon Sep 17 00:00:00 2001
From: rinfx <893383980@qq.com>
Date: Thu, 24 Oct 2024 17:34:26 +0800
Subject: [PATCH 29/64] bugfix: plugin will block GET request (#1428)
---
.../wasm-go/extensions/ai-security-guard/main.go | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/plugins/wasm-go/extensions/ai-security-guard/main.go b/plugins/wasm-go/extensions/ai-security-guard/main.go
index ca59f7f6a1..5b61589616 100644
--- a/plugins/wasm-go/extensions/ai-security-guard/main.go
+++ b/plugins/wasm-go/extensions/ai-security-guard/main.go
@@ -187,10 +187,6 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config AISecurityConfig, log
log.Debugf("request checking is disabled")
ctx.DontReadRequestBody()
}
- if !config.checkResponse {
- log.Debugf("response checking is disabled")
- ctx.DontReadResponseBody()
- }
return types.ActionContinue
}
@@ -199,7 +195,7 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config AISecurityConfig, body []
content := gjson.GetBytes(body, config.requestContentJsonPath).Raw
model := gjson.GetBytes(body, "model").Raw
ctx.SetContext("requestModel", model)
- log.Debugf("Raw response content is: %s", content)
+ log.Debugf("Raw request content is: %s", content)
if len(content) > 0 {
timestamp := time.Now().UTC().Format("2006-01-02T15:04:05Z")
randomID, _ := generateHexID(16)
@@ -321,6 +317,11 @@ func reconvertHeaders(hs map[string][]string) [][2]string {
}
func onHttpResponseHeaders(ctx wrapper.HttpContext, config AISecurityConfig, log wrapper.Log) types.Action {
+ if !config.checkResponse {
+ log.Debugf("response checking is disabled")
+ ctx.DontReadResponseBody()
+ return types.ActionContinue
+ }
headers, err := proxywasm.GetHttpResponseHeaders()
if err != nil {
log.Warnf("failed to get response headers: %v", err)
@@ -399,7 +400,7 @@ func onHttpResponseBody(ctx wrapper.HttpContext, config AISecurityConfig, body [
var jsonData []byte
if config.protocolOriginal {
jsonData = []byte(denyMessage)
- } else if strings.Contains(strings.Join(hdsMap["content-type"], ";"), "event-stream") {
+ } else if isStreamingResponse {
randomID := generateRandomID()
jsonData = []byte(fmt.Sprintf(OpenAIStreamResponseFormat, randomID, model, denyMessage, randomID, model))
} else {
From 496d365a955ff09816e9c8d2f04442fe3f07e47f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=9F=A9=E8=B4=A4=E6=B6=9B?= <601803023@qq.com>
Date: Thu, 24 Oct 2024 20:57:46 +0800
Subject: [PATCH 30/64] add PILOT_ENABLE_ALPHA_GATEWAY_API and fix gateway
status update (#1421)
---
helm/core/templates/controller-deployment.yaml | 6 ++++++
pkg/config/envs.go | 1 +
pkg/ingress/kube/gateway/istio/conversion.go | 3 ++-
pkg/ingress/kube/gateway/istio/conversion_test.go | 4 ++--
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/helm/core/templates/controller-deployment.yaml b/helm/core/templates/controller-deployment.yaml
index 98bfe8f6d8..c3bc70aea9 100644
--- a/helm/core/templates/controller-deployment.yaml
+++ b/helm/core/templates/controller-deployment.yaml
@@ -69,6 +69,10 @@ spec:
fieldPath: spec.serviceAccountName
- name: DOMAIN_SUFFIX
value: {{ .Values.global.proxy.clusterDomain }}
+ - name: PILOT_ENABLE_ALPHA_GATEWAY_API
+ value: "true"
+ - name: GATEWAY_NAME
+ value: {{ include "gateway.name" . }}
{{- if .Values.controller.env }}
{{- range $key, $val := .Values.controller.env }}
- name: {{ $key }}
@@ -218,6 +222,8 @@ spec:
{{- if .Values.global.enableGatewayAPI }}
- name: PILOT_ENABLE_GATEWAY_API
value: "true"
+ - name: PILOT_ENABLE_ALPHA_GATEWAY_API
+ value: "true"
- name: PILOT_ENABLE_GATEWAY_API_STATUS
value: "true"
- name: PILOT_ENABLE_GATEWAY_API_DEPLOYMENT_CONTROLLER
diff --git a/pkg/config/envs.go b/pkg/config/envs.go
index 38ff64bf26..99a67edc65 100644
--- a/pkg/config/envs.go
+++ b/pkg/config/envs.go
@@ -19,6 +19,7 @@ import "istio.io/pkg/env"
var (
PodNamespace = env.RegisterStringVar("POD_NAMESPACE", "higress-system", "").Get()
PodName = env.RegisterStringVar("POD_NAME", "", "").Get()
+ GatewayName = env.RegisterStringVar("GATEWAY_NAME", "higress-gateway", "").Get()
// Revision is the value of the Istio control plane revision, e.g. "canary",
// and is the value used by the "istio.io/rev" label.
Revision = env.Register("REVISION", "", "").Get()
diff --git a/pkg/ingress/kube/gateway/istio/conversion.go b/pkg/ingress/kube/gateway/istio/conversion.go
index 83c817a717..4cf5dee9e5 100644
--- a/pkg/ingress/kube/gateway/istio/conversion.go
+++ b/pkg/ingress/kube/gateway/istio/conversion.go
@@ -25,6 +25,7 @@ import (
"strings"
higressconfig "github.com/alibaba/higress/pkg/config"
+ "github.com/alibaba/higress/pkg/ingress/kube/util"
istio "istio.io/api/networking/v1alpha3"
"istio.io/istio/pilot/pkg/features"
"istio.io/istio/pilot/pkg/model"
@@ -1880,7 +1881,7 @@ func extractGatewayServices(r GatewayResources, kgw *k8s.GatewaySpec, obj config
if len(name) > 0 {
return []string{fmt.Sprintf("%s.%s.svc.%v", name, obj.Namespace, r.Domain)}, false, nil
}
- return []string{}, true, nil
+ return []string{fmt.Sprintf("%s.%s.svc.%s", higressconfig.GatewayName, higressconfig.PodNamespace, util.GetDomainSuffix())}, true, nil
}
gatewayServices := []string{}
skippedAddresses := []string{}
diff --git a/pkg/ingress/kube/gateway/istio/conversion_test.go b/pkg/ingress/kube/gateway/istio/conversion_test.go
index 986aecbc69..28f8d26962 100644
--- a/pkg/ingress/kube/gateway/istio/conversion_test.go
+++ b/pkg/ingress/kube/gateway/istio/conversion_test.go
@@ -919,7 +919,7 @@ func TestExtractGatewayServices(t *testing.T) {
Namespace: "default",
},
},
- gatewayServices: []string{},
+ gatewayServices: []string{"higress-gateway.higress-system.svc.cluster.local"},
useDefaultService: true,
},
{
@@ -1039,7 +1039,7 @@ func TestExtractGatewayServices(t *testing.T) {
Namespace: "default",
},
},
- gatewayServices: []string{},
+ gatewayServices: []string{"higress-gateway.higress-system.svc.cluster.local"},
useDefaultService: true,
},
}
From d309bf2e2533d406e28d12b5214623765a07825f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=BE=84=E6=BD=AD?=
Date: Fri, 25 Oct 2024 16:48:37 +0800
Subject: [PATCH 31/64] fix model-router plugin (#1432)
---
.../extensions/model_router/plugin.cc | 2 +-
.../extensions/model_router/plugin_test.cc | 56 +++++++++++++++++--
2 files changed, 53 insertions(+), 5 deletions(-)
diff --git a/plugins/wasm-cpp/extensions/model_router/plugin.cc b/plugins/wasm-cpp/extensions/model_router/plugin.cc
index 457864d268..66a90973ff 100644
--- a/plugins/wasm-cpp/extensions/model_router/plugin.cc
+++ b/plugins/wasm-cpp/extensions/model_router/plugin.cc
@@ -101,7 +101,7 @@ bool PluginRootContext::configure(size_t configuration_size) {
configuration_data->view()));
return false;
}
- if (!parseAuthRuleConfig(result.value())) {
+ if (!parseRuleConfig(result.value())) {
LOG_WARN(absl::StrCat("cannot parse plugin configuration JSON string: ",
configuration_data->view()));
return false;
diff --git a/plugins/wasm-cpp/extensions/model_router/plugin_test.cc b/plugins/wasm-cpp/extensions/model_router/plugin_test.cc
index 9ce5998051..dc351ecdc8 100644
--- a/plugins/wasm-cpp/extensions/model_router/plugin_test.cc
+++ b/plugins/wasm-cpp/extensions/model_router/plugin_test.cc
@@ -40,6 +40,11 @@ class MockContext : public proxy_wasm::ContextBase {
MOCK_METHOD(WasmResult, getHeaderMapValue,
(WasmHeaderMapType /* type */, std::string_view /* key */,
std::string_view* /*result */));
+ MOCK_METHOD(WasmResult, replaceHeaderMapValue,
+ (WasmHeaderMapType /* type */, std::string_view /* key */,
+ std::string_view /* value */));
+ MOCK_METHOD(WasmResult, removeHeaderMapValue,
+ (WasmHeaderMapType /* type */, std::string_view /* key */));
MOCK_METHOD(WasmResult, addHeaderMapValue,
(WasmHeaderMapType, std::string_view, std::string_view));
MOCK_METHOD(WasmResult, getProperty, (std::string_view, std::string*));
@@ -87,6 +92,16 @@ class ModelRouterTest : public ::testing::Test {
}
return WasmResult::Ok;
});
+ ON_CALL(*mock_context_,
+ replaceHeaderMapValue(WasmHeaderMapType::RequestHeaders, testing::_,
+ testing::_))
+ .WillByDefault([&](WasmHeaderMapType, std::string_view key,
+ std::string_view value) { return WasmResult::Ok; });
+ ON_CALL(*mock_context_,
+ removeHeaderMapValue(WasmHeaderMapType::RequestHeaders, testing::_))
+ .WillByDefault([&](WasmHeaderMapType, std::string_view key) {
+ return WasmResult::Ok;
+ });
ON_CALL(*mock_context_, addHeaderMapValue(WasmHeaderMapType::RequestHeaders,
testing::_, testing::_))
.WillByDefault([&](WasmHeaderMapType, std::string_view header,
@@ -128,12 +143,45 @@ TEST_F(ModelRouterTest, RewriteModelAndHeader) {
return WasmResult::Ok;
});
- EXPECT_CALL(
- *mock_context_,
- addHeaderMapValue(testing::_, std::string_view("x-higress-llm-provider"),
- std::string_view("qwen")));
+ EXPECT_CALL(*mock_context_,
+ replaceHeaderMapValue(testing::_,
+ std::string_view("x-higress-llm-provider"),
+ std::string_view("qwen")));
+
+ body_.set(request_json);
+ EXPECT_EQ(context_->onRequestHeaders(0, false),
+ FilterHeadersStatus::StopIteration);
+ EXPECT_EQ(context_->onRequestBody(28, true), FilterDataStatus::Continue);
+}
+
+TEST_F(ModelRouterTest, RouteLevelRewriteModelAndHeader) {
+ std::string configuration = R"(
+{
+ "_rules_": [
+ {
+ "_match_route_": ["route-a"],
+ "enable": true
+ }
+]})";
+
+ config_.set(configuration);
+ EXPECT_TRUE(root_context_->configure(configuration.size()));
+
+ std::string request_json = R"({"model": "qwen/qwen-long"})";
+ EXPECT_CALL(*mock_context_,
+ setBuffer(testing::_, testing::_, testing::_, testing::_))
+ .WillOnce([&](WasmBufferType, size_t, size_t, std::string_view body) {
+ EXPECT_EQ(body, R"({"model":"qwen-long"})");
+ return WasmResult::Ok;
+ });
+
+ EXPECT_CALL(*mock_context_,
+ replaceHeaderMapValue(testing::_,
+ std::string_view("x-higress-llm-provider"),
+ std::string_view("qwen")));
body_.set(request_json);
+ route_name_ = "route-a";
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
EXPECT_EQ(context_->onRequestBody(28, true), FilterDataStatus::Continue);
From acec48ed8bf3736a4c05d5c479ea5c27fbb8dc03 Mon Sep 17 00:00:00 2001
From: Yang Beining
Date: Sun, 27 Oct 2024 08:21:04 +0000
Subject: [PATCH 32/64] [ai-cache] Implement a WASM plugin for LLM result
retrieval based on vector similarity (#1290)
---
.../wasm-go/extensions/ai-cache/.gitignore | 2 +-
plugins/wasm-go/extensions/ai-cache/README.md | 124 +++++-
.../extensions/ai-cache/cache/provider.go | 135 ++++++
.../extensions/ai-cache/cache/redis.go | 58 +++
.../extensions/ai-cache/config/config.go | 225 ++++++++++
plugins/wasm-go/extensions/ai-cache/core.go | 275 ++++++++++++
.../ai-cache/embedding/dashscope.go | 187 ++++++++
.../extensions/ai-cache/embedding/provider.go | 101 +++++
.../extensions/ai-cache/embedding/weaviate.go | 27 ++
plugins/wasm-go/extensions/ai-cache/go.mod | 9 +-
plugins/wasm-go/extensions/ai-cache/go.sum | 17 +-
plugins/wasm-go/extensions/ai-cache/main.go | 398 +++++-------------
plugins/wasm-go/extensions/ai-cache/util.go | 155 +++++++
.../extensions/ai-cache/vector/dashvector.go | 256 +++++++++++
.../extensions/ai-cache/vector/provider.go | 167 ++++++++
plugins/wasm-go/extensions/ai-proxy/Makefile | 4 +
plugins/wasm-go/extensions/ai-proxy/go.mod | 2 +-
plugins/wasm-go/extensions/ai-proxy/go.sum | 4 +-
plugins/wasm-go/extensions/ai-proxy/main.go | 3 +-
.../extensions/request-block/Dockerfile | 2 +
.../wasm-go/extensions/request-block/Makefile | 4 +
.../wasm-go/extensions/request-block/main.go | 2 +
plugins/wasm-go/go.mod | 2 +-
plugins/wasm-go/go.sum | 8 +
plugins/wasm-go/pkg/wrapper/redis_wrapper.go | 5 +-
.../e2e/conformance/tests/go-wasm-ai-cache.go | 76 ++++
.../conformance/tests/go-wasm-ai-cache.yaml | 103 +++++
27 files changed, 2015 insertions(+), 336 deletions(-)
create mode 100644 plugins/wasm-go/extensions/ai-cache/cache/provider.go
create mode 100644 plugins/wasm-go/extensions/ai-cache/cache/redis.go
create mode 100644 plugins/wasm-go/extensions/ai-cache/config/config.go
create mode 100644 plugins/wasm-go/extensions/ai-cache/core.go
create mode 100644 plugins/wasm-go/extensions/ai-cache/embedding/dashscope.go
create mode 100644 plugins/wasm-go/extensions/ai-cache/embedding/provider.go
create mode 100644 plugins/wasm-go/extensions/ai-cache/embedding/weaviate.go
create mode 100644 plugins/wasm-go/extensions/ai-cache/util.go
create mode 100644 plugins/wasm-go/extensions/ai-cache/vector/dashvector.go
create mode 100644 plugins/wasm-go/extensions/ai-cache/vector/provider.go
create mode 100644 plugins/wasm-go/extensions/ai-proxy/Makefile
create mode 100644 plugins/wasm-go/extensions/request-block/Dockerfile
create mode 100644 plugins/wasm-go/extensions/request-block/Makefile
create mode 100644 test/e2e/conformance/tests/go-wasm-ai-cache.go
create mode 100644 test/e2e/conformance/tests/go-wasm-ai-cache.yaml
diff --git a/plugins/wasm-go/extensions/ai-cache/.gitignore b/plugins/wasm-go/extensions/ai-cache/.gitignore
index 47db8eedba..8a34bf52ad 100644
--- a/plugins/wasm-go/extensions/ai-cache/.gitignore
+++ b/plugins/wasm-go/extensions/ai-cache/.gitignore
@@ -1,5 +1,5 @@
# File generated by hgctl. Modify as required.
-
+docker-compose-test/
*
!/.gitignore
diff --git a/plugins/wasm-go/extensions/ai-cache/README.md b/plugins/wasm-go/extensions/ai-cache/README.md
index 1de252f12c..ca91bdf5a1 100644
--- a/plugins/wasm-go/extensions/ai-cache/README.md
+++ b/plugins/wasm-go/extensions/ai-cache/README.md
@@ -1,9 +1,15 @@
+## 简介
---
title: AI 缓存
keywords: [higress,ai cache]
description: AI 缓存插件配置参考
---
+**Note**
+
+> 需要数据面的proxy wasm版本大于等于0.2.100
+> 编译时,需要带上版本的tag,例如:`tinygo build -o main.wasm -scheduler=none -target=wasi -gc=custom -tags="custommalloc nottinygc_finalizer proxy_wasm_version_0_2_100" ./`
+>
## 功能说明
@@ -19,33 +25,113 @@ LLM 结果缓存插件,默认配置方式可以直接用于 openai 协议的
插件执行阶段:`认证阶段`
插件执行优先级:`10`
+## 配置说明
+配置分为 3 个部分:向量数据库(vector);文本向量化接口(embedding);缓存数据库(cache),同时也提供了细粒度的 LLM 请求/响应提取参数配置等。
+
## 配置说明
-| Name | Type | Requirement | Default | Description |
-| -------- | -------- | -------- | -------- | -------- |
-| cacheKeyFrom.requestBody | string | optional | "messages.@reverse.0.content" | 从请求 Body 中基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串 |
-| cacheValueFrom.responseBody | string | optional | "choices.0.message.content" | 从响应 Body 中基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串 |
-| cacheStreamValueFrom.responseBody | string | optional | "choices.0.delta.content" | 从流式响应 Body 中基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串 |
-| cacheKeyPrefix | string | optional | "higress-ai-cache:" | Redis缓存Key的前缀 |
-| cacheTTL | integer | optional | 0 | 缓存的过期时间,单位是秒,默认值为0,即永不过期 |
-| redis.serviceName | string | requried | - | redis 服务名称,带服务类型的完整 FQDN 名称,例如 my-redis.dns、redis.my-ns.svc.cluster.local |
-| redis.servicePort | integer | optional | 6379 | redis 服务端口 |
-| redis.timeout | integer | optional | 1000 | 请求 redis 的超时时间,单位为毫秒 |
-| redis.username | string | optional | - | 登陆 redis 的用户名 |
-| redis.password | string | optional | - | 登陆 redis 的密码 |
-| returnResponseTemplate | string | optional | `{"id":"from-cache","choices":[%s],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}` | 返回 HTTP 响应的模版,用 %s 标记需要被 cache value 替换的部分 |
-| returnStreamResponseTemplate | string | optional | `data:{"id":"from-cache","choices":[{"index":0,"delta":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}\n\ndata:[DONE]\n\n` | 返回流式 HTTP 响应的模版,用 %s 标记需要被 cache value 替换的部分 |
+本插件同时支持基于向量数据库的语义化缓存和基于字符串匹配的缓存方法,如果同时配置了向量数据库和缓存数据库,优先使用向量数据库。
+
+*Note*: 向量数据库(vector) 和 缓存数据库(cache) 不能同时为空,否则本插件无法提供缓存服务。
+
+| Name | Type | Requirement | Default | Description |
+| --- | --- | --- | --- | --- |
+| vector | string | optional | "" | 向量存储服务提供者类型,例如 dashvector |
+| embedding | string | optional | "" | 请求文本向量化服务类型,例如 dashscope |
+| cache | string | optional | "" | 缓存服务类型,例如 redis |
+| cacheKeyStrategy | string | optional | "lastQuestion" | 决定如何根据历史问题生成缓存键的策略。可选值: "lastQuestion" (使用最后一个问题), "allQuestions" (拼接所有问题) 或 "disabled" (禁用缓存) |
+| enableSemanticCache | bool | optional | true | 是否启用语义化缓存, 若不启用,则使用字符串匹配的方式来查找缓存,此时需要配置cache服务 |
+
+根据是否需要启用语义缓存,可以只配置组件的组合为:
+1. `cache`: 仅启用字符串匹配缓存
+3. `vector (+ embedding)`: 启用语义化缓存, 其中若 `vector` 未提供字符串表征服务,则需要自行配置 `embedding` 服务
+2. `vector (+ embedding) + cache`: 启用语义化缓存并用缓存服务存储LLM响应以加速
+
+注意若不配置相关组件,则可以忽略相应组件的`required`字段。
+
+
+## 向量数据库服务(vector)
+| Name | Type | Requirement | Default | Description |
+| --- | --- | --- | --- | --- |
+| vector.type | string | required | "" | 向量存储服务提供者类型,例如 dashvector |
+| vector.serviceName | string | required | "" | 向量存储服务名称 |
+| vector.serviceHost | string | required | "" | 向量存储服务域名 |
+| vector.servicePort | int64 | optional | 443 | 向量存储服务端口 |
+| vector.apiKey | string | optional | "" | 向量存储服务 API Key |
+| vector.topK | int | optional | 1 | 返回TopK结果,默认为 1 |
+| vector.timeout | uint32 | optional | 10000 | 请求向量存储服务的超时时间,单位为毫秒。默认值是10000,即10秒 |
+| vector.collectionID | string | optional | "" | dashvector 向量存储服务 Collection ID |
+| vector.threshold | float64 | optional | 1000 | 向量相似度度量阈值 |
+| vector.thresholdRelation | string | optional | lt | 相似度度量方式有 `Cosine`, `DotProduct`, `Euclidean` 等,前两者值越大相似度越高,后者值越小相似度越高。对于 `Cosine` 和 `DotProduct` 选择 `gt`,对于 `Euclidean` 则选择 `lt`。默认为 `lt`,所有条件包括 `lt` (less than,小于)、`lte` (less than or equal to,小等于)、`gt` (greater than,大于)、`gte` (greater than or equal to,大等于) |
+
+## 文本向量化服务(embedding)
+| Name | Type | Requirement | Default | Description |
+| --- | --- | --- | --- | --- |
+| embedding.type | string | required | "" | 请求文本向量化服务类型,例如 dashscope |
+| embedding.serviceName | string | required | "" | 请求文本向量化服务名称 |
+| embedding.serviceHost | string | optional | "" | 请求文本向量化服务域名 |
+| embedding.servicePort | int64 | optional | 443 | 请求文本向量化服务端口 |
+| embedding.apiKey | string | optional | "" | 请求文本向量化服务的 API Key |
+| embedding.timeout | uint32 | optional | 10000 | 请求文本向量化服务的超时时间,单位为毫秒。默认值是10000,即10秒 |
+| embedding.model | string | optional | "" | 请求文本向量化服务的模型名称 |
+
+
+## 缓存服务(cache)
+| cache.type | string | required | "" | 缓存服务类型,例如 redis |
+| --- | --- | --- | --- | --- |
+| cache.serviceName | string | required | "" | 缓存服务名称 |
+| cache.serviceHost | string | required | "" | 缓存服务域名 |
+| cache.servicePort | int64 | optional | 6379 | 缓存服务端口 |
+| cache.username | string | optional | "" | 缓存服务用户名 |
+| cache.password | string | optional | "" | 缓存服务密码 |
+| cache.timeout | uint32 | optional | 10000 | 缓存服务的超时时间,单位为毫秒。默认值是10000,即10秒 |
+| cache.cacheTTL | int | optional | 0 | 缓存过期时间,单位为秒。默认值是 0,即 永不过期|
+| cacheKeyPrefix | string | optional | "higress-ai-cache:" | 缓存 Key 的前缀,默认值为 "higress-ai-cache:" |
+
+
+## 其他配置
+| Name | Type | Requirement | Default | Description |
+| --- | --- | --- | --- | --- |
+| cacheKeyFrom | string | optional | "messages.@reverse.0.content" | 从请求 Body 中基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串 |
+| cacheValueFrom | string | optional | "choices.0.message.content" | 从响应 Body 中基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串 |
+| cacheStreamValueFrom | string | optional | "choices.0.delta.content" | 从流式响应 Body 中基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串 |
+| cacheToolCallsFrom | string | optional | "choices.0.delta.content.tool_calls" | 从请求 Body 中基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串 |
+| responseTemplate | string | optional | `{"id":"ai-cache.hit","choices":[{"index":0,"message":{"role":"assistant","content":%s},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}` | 返回 HTTP 响应的模版,用 %s 标记需要被 cache value 替换的部分 |
+| streamResponseTemplate | string | optional | `data:{"id":"ai-cache.hit","choices":[{"index":0,"delta":{"role":"assistant","content":%s},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}\n\ndata:[DONE]\n\n` | 返回流式 HTTP 响应的模版,用 %s 标记需要被 cache value 替换的部分 |
+
## 配置示例
+### 基础配置
+```yaml
+embedding:
+ type: dashscope
+ serviceName: my_dashscope.dns
+ apiKey: [Your Key]
+
+vector:
+ type: dashvector
+ serviceName: my_dashvector.dns
+ collectionID: [Your Collection ID]
+ serviceDomain: [Your domain]
+ apiKey: [Your key]
+
+cache:
+ type: redis
+ serviceName: my_redis.dns
+ servicePort: 6379
+ timeout: 100
+```
+
+旧版本配置兼容
```yaml
redis:
- serviceName: my-redis.dns
- timeout: 2000
+ serviceName: my_redis.dns
+ servicePort: 6379
+ timeout: 100
```
## 进阶用法
-
当前默认的缓存 key 是基于 GJSON PATH 的表达式:`messages.@reverse.0.content` 提取,含义是把 messages 数组反转后取第一项的 content;
GJSON PATH 支持条件判断语法,例如希望取最后一个 role 为 user 的 content 作为 key,可以写成: `messages.@reverse.#(role=="user").content`;
@@ -55,3 +141,7 @@ GJSON PATH 支持条件判断语法,例如希望取最后一个 role 为 user
还可以支持管道语法,例如希望取到数第二个 role 为 user 的 content 作为 key,可以写成:`messages.@reverse.#(role=="user")#.content|1`。
更多用法可以参考[官方文档](https://github.com/tidwall/gjson/blob/master/SYNTAX.md),可以使用 [GJSON Playground](https://gjson.dev/) 进行语法测试。
+
+## 常见问题
+
+1. 如果返回的错误为 `error status returned by host: bad argument`,请检查`serviceName`是否正确包含了服务的类型后缀(.dns等)。
\ No newline at end of file
diff --git a/plugins/wasm-go/extensions/ai-cache/cache/provider.go b/plugins/wasm-go/extensions/ai-cache/cache/provider.go
new file mode 100644
index 0000000000..1238d21570
--- /dev/null
+++ b/plugins/wasm-go/extensions/ai-cache/cache/provider.go
@@ -0,0 +1,135 @@
+package cache
+
+import (
+ "errors"
+
+ "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
+ "github.com/tidwall/gjson"
+)
+
+const (
+ PROVIDER_TYPE_REDIS = "redis"
+ DEFAULT_CACHE_PREFIX = "higress-ai-cache:"
+)
+
+type providerInitializer interface {
+ ValidateConfig(ProviderConfig) error
+ CreateProvider(ProviderConfig) (Provider, error)
+}
+
+var (
+ providerInitializers = map[string]providerInitializer{
+ PROVIDER_TYPE_REDIS: &redisProviderInitializer{},
+ }
+)
+
+type ProviderConfig struct {
+ // @Title zh-CN redis 缓存服务提供者类型
+ // @Description zh-CN 缓存服务提供者类型,例如 redis
+ typ string
+ // @Title zh-CN redis 缓存服务名称
+ // @Description zh-CN 缓存服务名称
+ serviceName string
+ // @Title zh-CN redis 缓存服务端口
+ // @Description zh-CN 缓存服务端口,默认值为6379
+ servicePort int
+ // @Title zh-CN redis 缓存服务地址
+ // @Description zh-CN Cache 缓存服务地址,非必填
+ serviceHost string
+ // @Title zh-CN 缓存服务用户名
+ // @Description zh-CN 缓存服务用户名,非必填
+ username string
+ // @Title zh-CN 缓存服务密码
+ // @Description zh-CN 缓存服务密码,非必填
+ password string
+ // @Title zh-CN 请求超时
+ // @Description zh-CN 请求缓存服务的超时时间,单位为毫秒。默认值是10000,即10秒
+ timeout uint32
+ // @Title zh-CN 缓存过期时间
+ // @Description zh-CN 缓存过期时间,单位为秒。默认值是0,即永不过期
+ cacheTTL int
+ // @Title 缓存 Key 前缀
+ // @Description 缓存 Key 的前缀,默认值为 "higressAiCache:"
+ cacheKeyPrefix string
+}
+
+func (c *ProviderConfig) GetProviderType() string {
+ return c.typ
+}
+
+func (c *ProviderConfig) FromJson(json gjson.Result) {
+ c.typ = json.Get("type").String()
+ c.serviceName = json.Get("serviceName").String()
+ c.servicePort = int(json.Get("servicePort").Int())
+ if !json.Get("servicePort").Exists() {
+ c.servicePort = 6379
+ }
+ c.serviceHost = json.Get("serviceHost").String()
+ c.username = json.Get("username").String()
+ if !json.Get("username").Exists() {
+ c.username = ""
+ }
+ c.password = json.Get("password").String()
+ if !json.Get("password").Exists() {
+ c.password = ""
+ }
+ c.timeout = uint32(json.Get("timeout").Int())
+ if !json.Get("timeout").Exists() {
+ c.timeout = 10000
+ }
+ c.cacheTTL = int(json.Get("cacheTTL").Int())
+ if !json.Get("cacheTTL").Exists() {
+ c.cacheTTL = 0
+ // c.cacheTTL = 3600000
+ }
+ if json.Get("cacheKeyPrefix").Exists() {
+ c.cacheKeyPrefix = json.Get("cacheKeyPrefix").String()
+ } else {
+ c.cacheKeyPrefix = DEFAULT_CACHE_PREFIX
+ }
+
+}
+
+func (c *ProviderConfig) ConvertLegacyJson(json gjson.Result) {
+ c.FromJson(json.Get("redis"))
+ c.typ = "redis"
+ if json.Get("cacheTTL").Exists() {
+ c.cacheTTL = int(json.Get("cacheTTL").Int())
+ }
+}
+
+func (c *ProviderConfig) Validate() error {
+ if c.typ == "" {
+ return errors.New("cache service type is required")
+ }
+ if c.serviceName == "" {
+ return errors.New("cache service name is required")
+ }
+ if c.cacheTTL < 0 {
+ return errors.New("cache TTL must be greater than or equal to 0")
+ }
+ initializer, has := providerInitializers[c.typ]
+ if !has {
+ return errors.New("unknown cache service provider type: " + c.typ)
+ }
+ if err := initializer.ValidateConfig(*c); err != nil {
+ return err
+ }
+ return nil
+}
+
+func CreateProvider(pc ProviderConfig) (Provider, error) {
+ initializer, has := providerInitializers[pc.typ]
+ if !has {
+ return nil, errors.New("unknown provider type: " + pc.typ)
+ }
+ return initializer.CreateProvider(pc)
+}
+
+type Provider interface {
+ GetProviderType() string
+ Init(username string, password string, timeout uint32) error
+ Get(key string, cb wrapper.RedisResponseCallback) error
+ Set(key string, value string, cb wrapper.RedisResponseCallback) error
+ GetCacheKeyPrefix() string
+}
diff --git a/plugins/wasm-go/extensions/ai-cache/cache/redis.go b/plugins/wasm-go/extensions/ai-cache/cache/redis.go
new file mode 100644
index 0000000000..4cb69744e1
--- /dev/null
+++ b/plugins/wasm-go/extensions/ai-cache/cache/redis.go
@@ -0,0 +1,58 @@
+package cache
+
+import (
+ "errors"
+
+ "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
+)
+
+type redisProviderInitializer struct {
+}
+
+func (r *redisProviderInitializer) ValidateConfig(cf ProviderConfig) error {
+ if len(cf.serviceName) == 0 {
+ return errors.New("cache service name is required")
+ }
+ return nil
+}
+
+func (r *redisProviderInitializer) CreateProvider(cf ProviderConfig) (Provider, error) {
+ rp := redisProvider{
+ config: cf,
+ client: wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
+ FQDN: cf.serviceName,
+ Host: cf.serviceHost,
+ Port: int64(cf.servicePort)}),
+ }
+ err := rp.Init(cf.username, cf.password, cf.timeout)
+ return &rp, err
+}
+
+type redisProvider struct {
+ config ProviderConfig
+ client wrapper.RedisClient
+}
+
+func (rp *redisProvider) GetProviderType() string {
+ return PROVIDER_TYPE_REDIS
+}
+
+func (rp *redisProvider) Init(username string, password string, timeout uint32) error {
+ return rp.client.Init(rp.config.username, rp.config.password, int64(rp.config.timeout))
+}
+
+func (rp *redisProvider) Get(key string, cb wrapper.RedisResponseCallback) error {
+ return rp.client.Get(key, cb)
+}
+
+func (rp *redisProvider) Set(key string, value string, cb wrapper.RedisResponseCallback) error {
+ if rp.config.cacheTTL == 0 {
+ return rp.client.Set(key, value, cb)
+ } else {
+ return rp.client.SetEx(key, value, rp.config.cacheTTL, cb)
+ }
+}
+
+func (rp *redisProvider) GetCacheKeyPrefix() string {
+ return rp.config.cacheKeyPrefix
+}
diff --git a/plugins/wasm-go/extensions/ai-cache/config/config.go b/plugins/wasm-go/extensions/ai-cache/config/config.go
new file mode 100644
index 0000000000..4bd6e2a18f
--- /dev/null
+++ b/plugins/wasm-go/extensions/ai-cache/config/config.go
@@ -0,0 +1,225 @@
+package config
+
+import (
+ "fmt"
+
+ "github.com/alibaba/higress/plugins/wasm-go/extensions/ai-cache/cache"
+ "github.com/alibaba/higress/plugins/wasm-go/extensions/ai-cache/embedding"
+ "github.com/alibaba/higress/plugins/wasm-go/extensions/ai-cache/vector"
+ "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
+ "github.com/tidwall/gjson"
+)
+
+const (
+ CACHE_KEY_STRATEGY_LAST_QUESTION = "lastQuestion"
+ CACHE_KEY_STRATEGY_ALL_QUESTIONS = "allQuestions"
+ CACHE_KEY_STRATEGY_DISABLED = "disabled"
+)
+
+type PluginConfig struct {
+ // @Title zh-CN 返回 HTTP 响应的模版
+ // @Description zh-CN 用 %s 标记需要被 cache value 替换的部分
+ ResponseTemplate string
+ // @Title zh-CN 返回流式 HTTP 响应的模版
+ // @Description zh-CN 用 %s 标记需要被 cache value 替换的部分
+ StreamResponseTemplate string
+
+ cacheProvider cache.Provider
+ embeddingProvider embedding.Provider
+ vectorProvider vector.Provider
+
+ embeddingProviderConfig embedding.ProviderConfig
+ vectorProviderConfig vector.ProviderConfig
+ cacheProviderConfig cache.ProviderConfig
+
+ CacheKeyFrom string
+ CacheValueFrom string
+ CacheStreamValueFrom string
+ CacheToolCallsFrom string
+
+ // @Title zh-CN 启用语义化缓存
+ // @Description zh-CN 控制是否启用语义化缓存功能。true 表示启用,false 表示禁用。
+ EnableSemanticCache bool
+
+ // @Title zh-CN 缓存键策略
+ // @Description zh-CN 决定如何生成缓存键的策略。可选值: "lastQuestion" (使用最后一个问题), "allQuestions" (拼接所有问题) 或 "disabled" (禁用缓存)
+ CacheKeyStrategy string
+}
+
+func (c *PluginConfig) FromJson(json gjson.Result, log wrapper.Log) {
+
+ c.vectorProviderConfig.FromJson(json.Get("vector"))
+ c.embeddingProviderConfig.FromJson(json.Get("embedding"))
+ c.cacheProviderConfig.FromJson(json.Get("cache"))
+ if json.Get("redis").Exists() {
+ // compatible with legacy config
+ c.cacheProviderConfig.ConvertLegacyJson(json)
+ }
+
+ c.CacheKeyStrategy = json.Get("cacheKeyStrategy").String()
+ if c.CacheKeyStrategy == "" {
+ c.CacheKeyStrategy = CACHE_KEY_STRATEGY_LAST_QUESTION // set default value
+ }
+ c.CacheKeyFrom = json.Get("cacheKeyFrom").String()
+ if c.CacheKeyFrom == "" {
+ c.CacheKeyFrom = "messages.@reverse.0.content"
+ }
+ c.CacheValueFrom = json.Get("cacheValueFrom").String()
+ if c.CacheValueFrom == "" {
+ c.CacheValueFrom = "choices.0.message.content"
+ }
+ c.CacheStreamValueFrom = json.Get("cacheStreamValueFrom").String()
+ if c.CacheStreamValueFrom == "" {
+ c.CacheStreamValueFrom = "choices.0.delta.content"
+ }
+ c.CacheToolCallsFrom = json.Get("cacheToolCallsFrom").String()
+ if c.CacheToolCallsFrom == "" {
+ c.CacheToolCallsFrom = "choices.0.delta.content.tool_calls"
+ }
+
+ c.StreamResponseTemplate = json.Get("streamResponseTemplate").String()
+ if c.StreamResponseTemplate == "" {
+ c.StreamResponseTemplate = `data:{"id":"from-cache","choices":[{"index":0,"delta":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}` + "\n\ndata:[DONE]\n\n"
+ }
+ c.ResponseTemplate = json.Get("responseTemplate").String()
+ if c.ResponseTemplate == "" {
+ c.ResponseTemplate = `{"id":"from-cache","choices":[{"index":0,"message":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}`
+ }
+
+ if json.Get("enableSemanticCache").Exists() {
+ c.EnableSemanticCache = json.Get("enableSemanticCache").Bool()
+ } else {
+ c.EnableSemanticCache = true // set default value to true
+ }
+
+ // compatible with legacy config
+ convertLegacyMapFields(c, json, log)
+}
+
+func (c *PluginConfig) Validate() error {
+ // if cache provider is configured, validate it
+ if c.cacheProviderConfig.GetProviderType() != "" {
+ if err := c.cacheProviderConfig.Validate(); err != nil {
+ return err
+ }
+ }
+ if c.embeddingProviderConfig.GetProviderType() != "" {
+ if err := c.embeddingProviderConfig.Validate(); err != nil {
+ return err
+ }
+ }
+ if c.vectorProviderConfig.GetProviderType() != "" {
+ if err := c.vectorProviderConfig.Validate(); err != nil {
+ return err
+ }
+ }
+
+ // cache, vector, and embedding cannot all be empty
+ if c.vectorProviderConfig.GetProviderType() == "" &&
+ c.embeddingProviderConfig.GetProviderType() == "" &&
+ c.cacheProviderConfig.GetProviderType() == "" {
+ return fmt.Errorf("vector, embedding and cache provider cannot be all empty")
+ }
+
+ // Validate the value of CacheKeyStrategy
+ if c.CacheKeyStrategy != CACHE_KEY_STRATEGY_LAST_QUESTION &&
+ c.CacheKeyStrategy != CACHE_KEY_STRATEGY_ALL_QUESTIONS &&
+ c.CacheKeyStrategy != CACHE_KEY_STRATEGY_DISABLED {
+ return fmt.Errorf("invalid CacheKeyStrategy: %s", c.CacheKeyStrategy)
+ }
+
+ // If semantic cache is enabled, ensure necessary components are configured
+ // if c.EnableSemanticCache {
+ // if c.embeddingProviderConfig.GetProviderType() == "" {
+ // return fmt.Errorf("semantic cache is enabled but embedding provider is not configured")
+ // }
+ // // if only configure cache, just warn the user
+ // }
+ return nil
+}
+
+func (c *PluginConfig) Complete(log wrapper.Log) error {
+ var err error
+ if c.embeddingProviderConfig.GetProviderType() != "" {
+ log.Debugf("embedding provider is set to %s", c.embeddingProviderConfig.GetProviderType())
+ c.embeddingProvider, err = embedding.CreateProvider(c.embeddingProviderConfig)
+ if err != nil {
+ return err
+ }
+ } else {
+ log.Info("embedding provider is not configured")
+ c.embeddingProvider = nil
+ }
+ if c.cacheProviderConfig.GetProviderType() != "" {
+ log.Debugf("cache provider is set to %s", c.cacheProviderConfig.GetProviderType())
+ c.cacheProvider, err = cache.CreateProvider(c.cacheProviderConfig)
+ if err != nil {
+ return err
+ }
+ } else {
+ log.Info("cache provider is not configured")
+ c.cacheProvider = nil
+ }
+ if c.vectorProviderConfig.GetProviderType() != "" {
+ log.Debugf("vector provider is set to %s", c.vectorProviderConfig.GetProviderType())
+ c.vectorProvider, err = vector.CreateProvider(c.vectorProviderConfig)
+ if err != nil {
+ return err
+ }
+ } else {
+ log.Info("vector provider is not configured")
+ c.vectorProvider = nil
+ }
+ return nil
+}
+
+func (c *PluginConfig) GetEmbeddingProvider() embedding.Provider {
+ return c.embeddingProvider
+}
+
+func (c *PluginConfig) GetVectorProvider() vector.Provider {
+ return c.vectorProvider
+}
+
+func (c *PluginConfig) GetVectorProviderConfig() vector.ProviderConfig {
+ return c.vectorProviderConfig
+}
+
+func (c *PluginConfig) GetCacheProvider() cache.Provider {
+ return c.cacheProvider
+}
+
+func convertLegacyMapFields(c *PluginConfig, json gjson.Result, log wrapper.Log) {
+ keyMap := map[string]string{
+ "cacheKeyFrom.requestBody": "cacheKeyFrom",
+ "cacheValueFrom.requestBody": "cacheValueFrom",
+ "cacheStreamValueFrom.requestBody": "cacheStreamValueFrom",
+ "returnResponseTemplate": "responseTemplate",
+ "returnStreamResponseTemplate": "streamResponseTemplate",
+ }
+
+ for oldKey, newKey := range keyMap {
+ if json.Get(oldKey).Exists() {
+ log.Debugf("[convertLegacyMapFields] mapping %s to %s", oldKey, newKey)
+ setField(c, newKey, json.Get(oldKey).String(), log)
+ } else {
+ log.Debugf("[convertLegacyMapFields] %s not exists", oldKey)
+ }
+ }
+}
+
+func setField(c *PluginConfig, fieldName string, value string, log wrapper.Log) {
+ switch fieldName {
+ case "cacheKeyFrom":
+ c.CacheKeyFrom = value
+ case "cacheValueFrom":
+ c.CacheValueFrom = value
+ case "cacheStreamValueFrom":
+ c.CacheStreamValueFrom = value
+ case "responseTemplate":
+ c.ResponseTemplate = value
+ case "streamResponseTemplate":
+ c.StreamResponseTemplate = value
+ }
+ log.Debugf("[setField] set %s to %s", fieldName, value)
+}
diff --git a/plugins/wasm-go/extensions/ai-cache/core.go b/plugins/wasm-go/extensions/ai-cache/core.go
new file mode 100644
index 0000000000..19a9b2b856
--- /dev/null
+++ b/plugins/wasm-go/extensions/ai-cache/core.go
@@ -0,0 +1,275 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/alibaba/higress/plugins/wasm-go/extensions/ai-cache/config"
+ "github.com/alibaba/higress/plugins/wasm-go/extensions/ai-cache/vector"
+ "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
+ "github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
+ "github.com/tidwall/resp"
+)
+
+// CheckCacheForKey checks if the key is in the cache, or triggers similarity search if not found.
+func CheckCacheForKey(key string, ctx wrapper.HttpContext, c config.PluginConfig, log wrapper.Log, stream bool, useSimilaritySearch bool) error {
+ activeCacheProvider := c.GetCacheProvider()
+ if activeCacheProvider == nil {
+ log.Debugf("[%s] [CheckCacheForKey] no cache provider configured, performing similarity search", PLUGIN_NAME)
+ return performSimilaritySearch(key, ctx, c, log, key, stream)
+ }
+
+ queryKey := activeCacheProvider.GetCacheKeyPrefix() + key
+ log.Debugf("[%s] [CheckCacheForKey] querying cache with key: %s", PLUGIN_NAME, queryKey)
+
+ err := activeCacheProvider.Get(queryKey, func(response resp.Value) {
+ handleCacheResponse(key, response, ctx, log, stream, c, useSimilaritySearch)
+ })
+
+ if err != nil {
+ log.Errorf("[%s] [CheckCacheForKey] failed to retrieve key: %s from cache, error: %v", PLUGIN_NAME, key, err)
+ return err
+ }
+
+ return nil
+}
+
+// handleCacheResponse processes cache response and handles cache hits and misses.
+func handleCacheResponse(key string, response resp.Value, ctx wrapper.HttpContext, log wrapper.Log, stream bool, c config.PluginConfig, useSimilaritySearch bool) {
+ if err := response.Error(); err == nil && !response.IsNull() {
+ log.Infof("[%s] cache hit for key: %s", PLUGIN_NAME, key)
+ processCacheHit(key, response.String(), stream, ctx, c, log)
+ return
+ }
+
+ log.Infof("[%s] [handleCacheResponse] cache miss for key: %s", PLUGIN_NAME, key)
+ if err := response.Error(); err != nil {
+ log.Errorf("[%s] [handleCacheResponse] error retrieving key: %s from cache, error: %v", PLUGIN_NAME, key, err)
+ }
+
+ if useSimilaritySearch && c.EnableSemanticCache {
+ if err := performSimilaritySearch(key, ctx, c, log, key, stream); err != nil {
+ log.Errorf("[%s] [handleCacheResponse] failed to perform similarity search for key: %s, error: %v", PLUGIN_NAME, key, err)
+ proxywasm.ResumeHttpRequest()
+ }
+ } else {
+ proxywasm.ResumeHttpRequest()
+ }
+}
+
+// processCacheHit handles a successful cache hit.
+func processCacheHit(key string, response string, stream bool, ctx wrapper.HttpContext, c config.PluginConfig, log wrapper.Log) {
+ if strings.TrimSpace(response) == "" {
+ log.Warnf("[%s] [processCacheHit] cached response for key %s is empty", PLUGIN_NAME, key)
+ proxywasm.ResumeHttpRequest()
+ return
+ }
+
+ log.Debugf("[%s] [processCacheHit] cached response for key %s: %s", PLUGIN_NAME, key, response)
+
+ // Escape the response to ensure consistent formatting
+ escapedResponse := strings.Trim(strconv.Quote(response), "\"")
+
+ ctx.SetContext(CACHE_KEY_CONTEXT_KEY, nil)
+
+ if stream {
+ proxywasm.SendHttpResponseWithDetail(200, "ai-cache.hit", [][2]string{{"content-type", "text/event-stream; charset=utf-8"}}, []byte(fmt.Sprintf(c.StreamResponseTemplate, escapedResponse)), -1)
+ } else {
+ proxywasm.SendHttpResponseWithDetail(200, "ai-cache.hit", [][2]string{{"content-type", "application/json; charset=utf-8"}}, []byte(fmt.Sprintf(c.ResponseTemplate, escapedResponse)), -1)
+ }
+}
+
+// performSimilaritySearch determines the appropriate similarity search method to use.
+func performSimilaritySearch(key string, ctx wrapper.HttpContext, c config.PluginConfig, log wrapper.Log, queryString string, stream bool) error {
+ activeVectorProvider := c.GetVectorProvider()
+ if activeVectorProvider == nil {
+ return logAndReturnError(log, "[performSimilaritySearch] no vector provider configured for similarity search")
+ }
+
+ // Check if the active vector provider implements the StringQuerier interface.
+ if _, ok := activeVectorProvider.(vector.StringQuerier); ok {
+ log.Debugf("[%s] [performSimilaritySearch] active vector provider implements StringQuerier interface, performing string query", PLUGIN_NAME)
+ return performStringQuery(key, queryString, ctx, c, log, stream)
+ }
+
+ // Check if the active vector provider implements the EmbeddingQuerier interface.
+ if _, ok := activeVectorProvider.(vector.EmbeddingQuerier); ok {
+ log.Debugf("[%s] [performSimilaritySearch] active vector provider implements EmbeddingQuerier interface, performing embedding query", PLUGIN_NAME)
+ return performEmbeddingQuery(key, ctx, c, log, stream)
+ }
+
+ return logAndReturnError(log, "[performSimilaritySearch] no suitable querier or embedding provider available for similarity search")
+}
+
+// performStringQuery executes the string-based similarity search.
+func performStringQuery(key string, queryString string, ctx wrapper.HttpContext, c config.PluginConfig, log wrapper.Log, stream bool) error {
+ stringQuerier, ok := c.GetVectorProvider().(vector.StringQuerier)
+ if !ok {
+ return logAndReturnError(log, "[performStringQuery] active vector provider does not implement StringQuerier interface")
+ }
+
+ return stringQuerier.QueryString(queryString, ctx, log, func(results []vector.QueryResult, ctx wrapper.HttpContext, log wrapper.Log, err error) {
+ handleQueryResults(key, results, ctx, log, stream, c, err)
+ })
+}
+
+// performEmbeddingQuery executes the embedding-based similarity search.
+func performEmbeddingQuery(key string, ctx wrapper.HttpContext, c config.PluginConfig, log wrapper.Log, stream bool) error {
+ embeddingQuerier, ok := c.GetVectorProvider().(vector.EmbeddingQuerier)
+ if !ok {
+ return logAndReturnError(log, fmt.Sprintf("[performEmbeddingQuery] active vector provider does not implement EmbeddingQuerier interface"))
+ }
+
+ activeEmbeddingProvider := c.GetEmbeddingProvider()
+ if activeEmbeddingProvider == nil {
+ return logAndReturnError(log, fmt.Sprintf("[performEmbeddingQuery] no embedding provider configured for similarity search"))
+ }
+
+ return activeEmbeddingProvider.GetEmbedding(key, ctx, log, func(textEmbedding []float64, err error) {
+ log.Debugf("[%s] [performEmbeddingQuery] GetEmbedding success, length of embedding: %d, error: %v", PLUGIN_NAME, len(textEmbedding), err)
+ if err != nil {
+ handleInternalError(err, fmt.Sprintf("[%s] [performEmbeddingQuery] error getting embedding for key: %s", PLUGIN_NAME, key), log)
+ return
+ }
+ ctx.SetContext(CACHE_KEY_EMBEDDING_KEY, textEmbedding)
+
+ err = embeddingQuerier.QueryEmbedding(textEmbedding, ctx, log, func(results []vector.QueryResult, ctx wrapper.HttpContext, log wrapper.Log, err error) {
+ handleQueryResults(key, results, ctx, log, stream, c, err)
+ })
+ if err != nil {
+ handleInternalError(err, fmt.Sprintf("[%s] [performEmbeddingQuery] error querying vector database for key: %s", PLUGIN_NAME, key), log)
+ }
+ })
+}
+
+// handleQueryResults processes the results of similarity search and determines next actions.
+func handleQueryResults(key string, results []vector.QueryResult, ctx wrapper.HttpContext, log wrapper.Log, stream bool, c config.PluginConfig, err error) {
+ if err != nil {
+ handleInternalError(err, fmt.Sprintf("[%s] [handleQueryResults] error querying vector database for key: %s", PLUGIN_NAME, key), log)
+ return
+ }
+
+ if len(results) == 0 {
+ log.Warnf("[%s] [handleQueryResults] no similar keys found for key: %s", PLUGIN_NAME, key)
+ proxywasm.ResumeHttpRequest()
+ return
+ }
+
+ mostSimilarData := results[0]
+ log.Debugf("[%s] [handleQueryResults] for key: %s, the most similar key found: %s with score: %f", PLUGIN_NAME, key, mostSimilarData.Text, mostSimilarData.Score)
+ simThreshold := c.GetVectorProviderConfig().Threshold
+ simThresholdRelation := c.GetVectorProviderConfig().ThresholdRelation
+ if compare(simThresholdRelation, mostSimilarData.Score, simThreshold) {
+ log.Infof("[%s] key accepted: %s with score: %f", PLUGIN_NAME, mostSimilarData.Text, mostSimilarData.Score)
+ if mostSimilarData.Answer != "" {
+ // direct return the answer if available
+ cacheResponse(ctx, c, key, mostSimilarData.Answer, log)
+ processCacheHit(key, mostSimilarData.Answer, stream, ctx, c, log)
+ } else {
+ if c.GetCacheProvider() != nil {
+ CheckCacheForKey(mostSimilarData.Text, ctx, c, log, stream, false)
+ } else {
+ // Otherwise, do not check the cache, directly return
+ log.Infof("[%s] cache hit for key: %s, but no corresponding answer found in the vector database", PLUGIN_NAME, mostSimilarData.Text)
+ proxywasm.ResumeHttpRequest()
+ }
+ }
+ } else {
+ log.Infof("[%s] score not meet the threshold %f: %s with score %f", PLUGIN_NAME, simThreshold, mostSimilarData.Text, mostSimilarData.Score)
+ proxywasm.ResumeHttpRequest()
+ }
+}
+
+// logAndReturnError logs an error and returns it.
+func logAndReturnError(log wrapper.Log, message string) error {
+ message = fmt.Sprintf("[%s] %s", PLUGIN_NAME, message)
+ log.Errorf(message)
+ return errors.New(message)
+}
+
+// handleInternalError logs an error and resumes the HTTP request.
+func handleInternalError(err error, message string, log wrapper.Log) {
+ if err != nil {
+ log.Errorf("[%s] [handleInternalError] %s: %v", PLUGIN_NAME, message, err)
+ } else {
+ log.Errorf("[%s] [handleInternalError] %s", PLUGIN_NAME, message)
+ }
+ // proxywasm.SendHttpResponse(500, [][2]string{{"content-type", "text/plain"}}, []byte("Internal Server Error"), -1)
+ proxywasm.ResumeHttpRequest()
+}
+
+// Caches the response value
+func cacheResponse(ctx wrapper.HttpContext, c config.PluginConfig, key string, value string, log wrapper.Log) {
+ if strings.TrimSpace(value) == "" {
+ log.Warnf("[%s] [cacheResponse] cached value for key %s is empty", PLUGIN_NAME, key)
+ return
+ }
+
+ activeCacheProvider := c.GetCacheProvider()
+ if activeCacheProvider != nil {
+ queryKey := activeCacheProvider.GetCacheKeyPrefix() + key
+ _ = activeCacheProvider.Set(queryKey, value, nil)
+ log.Debugf("[%s] [cacheResponse] cache set success, key: %s, length of value: %d", PLUGIN_NAME, queryKey, len(value))
+ }
+}
+
+// Handles embedding upload if available
+func uploadEmbeddingAndAnswer(ctx wrapper.HttpContext, c config.PluginConfig, key string, value string, log wrapper.Log) {
+ embedding := ctx.GetContext(CACHE_KEY_EMBEDDING_KEY)
+ if embedding == nil {
+ return
+ }
+
+ emb, ok := embedding.([]float64)
+ if !ok {
+ log.Errorf("[%s] [uploadEmbeddingAndAnswer] embedding is not of expected type []float64", PLUGIN_NAME)
+ return
+ }
+
+ activeVectorProvider := c.GetVectorProvider()
+ if activeVectorProvider == nil {
+ log.Debugf("[%s] [uploadEmbeddingAndAnswer] no vector provider configured for uploading embedding", PLUGIN_NAME)
+ return
+ }
+
+ // Attempt to upload answer embedding first
+ if ansEmbUploader, ok := activeVectorProvider.(vector.AnswerAndEmbeddingUploader); ok {
+ log.Infof("[%s] uploading answer embedding for key: %s", PLUGIN_NAME, key)
+ err := ansEmbUploader.UploadAnswerAndEmbedding(key, emb, value, ctx, log, nil)
+ if err != nil {
+ log.Warnf("[%s] [uploadEmbeddingAndAnswer] failed to upload answer embedding for key: %s, error: %v", PLUGIN_NAME, key, err)
+ } else {
+ return // If successful, return early
+ }
+ }
+
+ // If answer embedding upload fails, attempt normal embedding upload
+ if embUploader, ok := activeVectorProvider.(vector.EmbeddingUploader); ok {
+ log.Infof("[%s] uploading embedding for key: %s", PLUGIN_NAME, key)
+ err := embUploader.UploadEmbedding(key, emb, ctx, log, nil)
+ if err != nil {
+ log.Warnf("[%s] [uploadEmbeddingAndAnswer] failed to upload embedding for key: %s, error: %v", PLUGIN_NAME, key, err)
+ }
+ }
+}
+
+// 主要用于相似度/距离/点积判断
+// 余弦相似度度量的是两个向量在方向上的相似程度。相似度越高,两个向量越接近。
+// 距离度量的是两个向量在空间上的远近程度。距离越小,两个向量越接近。
+// compare 函数根据操作符进行判断并返回结果
+func compare(operator string, value1 float64, value2 float64) bool {
+ switch operator {
+ case "gt":
+ return value1 > value2
+ case "gte":
+ return value1 >= value2
+ case "lt":
+ return value1 < value2
+ case "lte":
+ return value1 <= value2
+ default:
+ return false
+ }
+}
diff --git a/plugins/wasm-go/extensions/ai-cache/embedding/dashscope.go b/plugins/wasm-go/extensions/ai-cache/embedding/dashscope.go
new file mode 100644
index 0000000000..35c897cce5
--- /dev/null
+++ b/plugins/wasm-go/extensions/ai-cache/embedding/dashscope.go
@@ -0,0 +1,187 @@
+package embedding
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "strconv"
+
+ "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
+)
+
+const (
+ DASHSCOPE_DOMAIN = "dashscope.aliyuncs.com"
+ DASHSCOPE_PORT = 443
+ DASHSCOPE_DEFAULT_MODEL_NAME = "text-embedding-v2"
+ DASHSCOPE_ENDPOINT = "/api/v1/services/embeddings/text-embedding/text-embedding"
+)
+
+type dashScopeProviderInitializer struct {
+}
+
+func (d *dashScopeProviderInitializer) ValidateConfig(config ProviderConfig) error {
+ if config.apiKey == "" {
+ return errors.New("[DashScope] apiKey is required")
+ }
+ return nil
+}
+
+func (d *dashScopeProviderInitializer) CreateProvider(c ProviderConfig) (Provider, error) {
+ if c.servicePort == 0 {
+ c.servicePort = DASHSCOPE_PORT
+ }
+ if c.serviceHost == "" {
+ c.serviceHost = DASHSCOPE_DOMAIN
+ }
+ return &DSProvider{
+ config: c,
+ client: wrapper.NewClusterClient(wrapper.FQDNCluster{
+ FQDN: c.serviceName,
+ Host: c.serviceHost,
+ Port: int64(c.servicePort),
+ }),
+ }, nil
+}
+
+func (d *DSProvider) GetProviderType() string {
+ return PROVIDER_TYPE_DASHSCOPE
+}
+
+type Embedding struct {
+ Embedding []float64 `json:"embedding"`
+ TextIndex int `json:"text_index"`
+}
+
+type Input struct {
+ Texts []string `json:"texts"`
+}
+
+type Params struct {
+ TextType string `json:"text_type"`
+}
+
+type Response struct {
+ RequestID string `json:"request_id"`
+ Output Output `json:"output"`
+ Usage Usage `json:"usage"`
+}
+
+type Output struct {
+ Embeddings []Embedding `json:"embeddings"`
+}
+
+type Usage struct {
+ TotalTokens int `json:"total_tokens"`
+}
+
+type EmbeddingRequest struct {
+ Model string `json:"model"`
+ Input Input `json:"input"`
+ Parameters Params `json:"parameters"`
+}
+
+type Document struct {
+ Vector []float64 `json:"vector"`
+ Fields map[string]string `json:"fields"`
+}
+
+type DSProvider struct {
+ config ProviderConfig
+ client wrapper.HttpClient
+}
+
+func (d *DSProvider) constructParameters(texts []string, log wrapper.Log) (string, [][2]string, []byte, error) {
+
+ model := d.config.model
+
+ if model == "" {
+ model = DASHSCOPE_DEFAULT_MODEL_NAME
+ }
+ data := EmbeddingRequest{
+ Model: model,
+ Input: Input{
+ Texts: texts,
+ },
+ Parameters: Params{
+ TextType: "query",
+ },
+ }
+
+ requestBody, err := json.Marshal(data)
+ if err != nil {
+ log.Errorf("failed to marshal request data: %v", err)
+ return "", nil, nil, err
+ }
+
+ if d.config.apiKey == "" {
+ err := errors.New("dashScopeKey is empty")
+ log.Errorf("failed to construct headers: %v", err)
+ return "", nil, nil, err
+ }
+
+ headers := [][2]string{
+ {"Authorization", "Bearer " + d.config.apiKey},
+ {"Content-Type", "application/json"},
+ }
+
+ return DASHSCOPE_ENDPOINT, headers, requestBody, err
+}
+
+type Result struct {
+ ID string `json:"id"`
+ Vector []float64 `json:"vector,omitempty"`
+ Fields map[string]interface{} `json:"fields"`
+ Score float64 `json:"score"`
+}
+
+func (d *DSProvider) parseTextEmbedding(responseBody []byte) (*Response, error) {
+ var resp Response
+ err := json.Unmarshal(responseBody, &resp)
+ if err != nil {
+ return nil, err
+ }
+ return &resp, nil
+}
+
+func (d *DSProvider) GetEmbedding(
+ queryString string,
+ ctx wrapper.HttpContext,
+ log wrapper.Log,
+ callback func(emb []float64, err error)) error {
+ embUrl, embHeaders, embRequestBody, err := d.constructParameters([]string{queryString}, log)
+ if err != nil {
+ log.Errorf("failed to construct parameters: %v", err)
+ return err
+ }
+
+ var resp *Response
+ err = d.client.Post(embUrl, embHeaders, embRequestBody,
+ func(statusCode int, responseHeaders http.Header, responseBody []byte) {
+
+ if statusCode != http.StatusOK {
+ err = errors.New("failed to get embedding due to status code: " + strconv.Itoa(statusCode))
+ callback(nil, err)
+ return
+ }
+
+ log.Debugf("get embedding response: %d, %s", statusCode, responseBody)
+
+ resp, err = d.parseTextEmbedding(responseBody)
+ if err != nil {
+ err = fmt.Errorf("failed to parse response: %v", err)
+ callback(nil, err)
+ return
+ }
+
+ if len(resp.Output.Embeddings) == 0 {
+ err = errors.New("no embedding found in response")
+ callback(nil, err)
+ return
+ }
+
+ callback(resp.Output.Embeddings[0].Embedding, nil)
+
+ }, d.config.timeout)
+ return err
+}
diff --git a/plugins/wasm-go/extensions/ai-cache/embedding/provider.go b/plugins/wasm-go/extensions/ai-cache/embedding/provider.go
new file mode 100644
index 0000000000..909edf129c
--- /dev/null
+++ b/plugins/wasm-go/extensions/ai-cache/embedding/provider.go
@@ -0,0 +1,101 @@
+package embedding
+
+import (
+ "errors"
+
+ "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
+ "github.com/tidwall/gjson"
+)
+
+const (
+ PROVIDER_TYPE_DASHSCOPE = "dashscope"
+)
+
+type providerInitializer interface {
+ ValidateConfig(ProviderConfig) error
+ CreateProvider(ProviderConfig) (Provider, error)
+}
+
+var (
+ providerInitializers = map[string]providerInitializer{
+ PROVIDER_TYPE_DASHSCOPE: &dashScopeProviderInitializer{},
+ }
+)
+
+type ProviderConfig struct {
+ // @Title zh-CN 文本特征提取服务提供者类型
+ // @Description zh-CN 文本特征提取服务提供者类型,例如 DashScope
+ typ string
+ // @Title zh-CN DashScope 文本特征提取服务名称
+ // @Description zh-CN 文本特征提取服务名称
+ serviceName string
+ // @Title zh-CN 文本特征提取服务域名
+ // @Description zh-CN 文本特征提取服务域名
+ serviceHost string
+ // @Title zh-CN 文本特征提取服务端口
+ // @Description zh-CN 文本特征提取服务端口
+ servicePort int64
+ // @Title zh-CN 文本特征提取服务 API Key
+ // @Description zh-CN 文本特征提取服务 API Key
+ apiKey string
+ // @Title zh-CN 文本特征提取服务超时时间
+ // @Description zh-CN 文本特征提取服务超时时间
+ timeout uint32
+ // @Title zh-CN 文本特征提取服务使用的模型
+ // @Description zh-CN 用于文本特征提取的模型名称, 在 DashScope 中默认为 "text-embedding-v1"
+ model string
+}
+
+func (c *ProviderConfig) FromJson(json gjson.Result) {
+ c.typ = json.Get("type").String()
+ c.serviceName = json.Get("serviceName").String()
+ c.serviceHost = json.Get("serviceHost").String()
+ c.servicePort = json.Get("servicePort").Int()
+ c.apiKey = json.Get("apiKey").String()
+ c.timeout = uint32(json.Get("timeout").Int())
+ c.model = json.Get("model").String()
+ if c.timeout == 0 {
+ c.timeout = 10000
+ }
+}
+
+func (c *ProviderConfig) Validate() error {
+ if c.serviceName == "" {
+ return errors.New("embedding service name is required")
+ }
+ if c.apiKey == "" {
+ return errors.New("embedding service API key is required")
+ }
+ if c.typ == "" {
+ return errors.New("embedding service type is required")
+ }
+ initializer, has := providerInitializers[c.typ]
+ if !has {
+ return errors.New("unknown embedding service provider type: " + c.typ)
+ }
+ if err := initializer.ValidateConfig(*c); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (c *ProviderConfig) GetProviderType() string {
+ return c.typ
+}
+
+func CreateProvider(pc ProviderConfig) (Provider, error) {
+ initializer, has := providerInitializers[pc.typ]
+ if !has {
+ return nil, errors.New("unknown provider type: " + pc.typ)
+ }
+ return initializer.CreateProvider(pc)
+}
+
+type Provider interface {
+ GetProviderType() string
+ GetEmbedding(
+ queryString string,
+ ctx wrapper.HttpContext,
+ log wrapper.Log,
+ callback func(emb []float64, err error)) error
+}
diff --git a/plugins/wasm-go/extensions/ai-cache/embedding/weaviate.go b/plugins/wasm-go/extensions/ai-cache/embedding/weaviate.go
new file mode 100644
index 0000000000..b26d9cea8d
--- /dev/null
+++ b/plugins/wasm-go/extensions/ai-cache/embedding/weaviate.go
@@ -0,0 +1,27 @@
+package embedding
+
+// import (
+// "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
+// )
+
+// const (
+// weaviateURL = "172.17.0.1:8081"
+// )
+
+// type weaviateProviderInitializer struct {
+// }
+
+// func (d *weaviateProviderInitializer) ValidateConfig(config ProviderConfig) error {
+// return nil
+// }
+
+// func (d *weaviateProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
+// return &DSProvider{
+// config: config,
+// client: wrapper.NewClusterClient(wrapper.DnsCluster{
+// ServiceName: config.ServiceName,
+// Port: dashScopePort,
+// Domain: dashScopeDomain,
+// }),
+// }, nil
+// }
diff --git a/plugins/wasm-go/extensions/ai-cache/go.mod b/plugins/wasm-go/extensions/ai-cache/go.mod
index c9630cfb8a..e4aae265e0 100644
--- a/plugins/wasm-go/extensions/ai-cache/go.mod
+++ b/plugins/wasm-go/extensions/ai-cache/go.mod
@@ -7,17 +7,18 @@ go 1.19
replace github.com/alibaba/higress/plugins/wasm-go => ../..
require (
- github.com/alibaba/higress/plugins/wasm-go v1.3.6-0.20240528060522-53bccf89f441
+ github.com/alibaba/higress/plugins/wasm-go v1.4.2
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f
- github.com/tidwall/gjson v1.14.3
+ github.com/tidwall/gjson v1.17.3
github.com/tidwall/resp v0.1.1
- github.com/tidwall/sjson v1.2.5
+// github.com/weaviate/weaviate-go-client/v4 v4.15.1
)
require (
- github.com/google/uuid v1.3.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
github.com/magefile/mage v1.14.0 // indirect
+ github.com/stretchr/testify v1.9.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
)
diff --git a/plugins/wasm-go/extensions/ai-cache/go.sum b/plugins/wasm-go/extensions/ai-cache/go.sum
index 8246b4de5e..7ada0c8b70 100644
--- a/plugins/wasm-go/extensions/ai-cache/go.sum
+++ b/plugins/wasm-go/extensions/ai-cache/go.sum
@@ -1,24 +1,21 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
-github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
-github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc h1:t2AT8zb6N/59Y78lyRWedVoVWHNRSCBh0oWCC+bluTQ=
-github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
+github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
-github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
-github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
-github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
+github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
-github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
-github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/plugins/wasm-go/extensions/ai-cache/main.go b/plugins/wasm-go/extensions/ai-cache/main.go
index 7886d5698f..1aca29f0ec 100644
--- a/plugins/wasm-go/extensions/ai-cache/main.go
+++ b/plugins/wasm-go/extensions/ai-cache/main.go
@@ -1,33 +1,33 @@
-// File generated by hgctl. Modify as required.
-// See: https://higress.io/zh-cn/docs/user/wasm-go#2-%E7%BC%96%E5%86%99-maingo-%E6%96%87%E4%BB%B6
-
+// 这个文件中主要将OnHttpRequestHeaders、OnHttpRequestBody、OnHttpResponseHeaders、OnHttpResponseBody这四个函数实现
+// 其中的缓存思路调用cache.go中的逻辑,然后cache.go中的逻辑会调用textEmbeddingProvider和vectorStoreProvider中的逻辑(实例)
package main
import (
- "errors"
- "fmt"
"strings"
+ "github.com/alibaba/higress/plugins/wasm-go/extensions/ai-cache/config"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/tidwall/gjson"
- "github.com/tidwall/resp"
)
const (
- CacheKeyContextKey = "cacheKey"
- CacheContentContextKey = "cacheContent"
- PartialMessageContextKey = "partialMessage"
- ToolCallsContextKey = "toolCalls"
- StreamContextKey = "stream"
- DefaultCacheKeyPrefix = "higress-ai-cache:"
- SkipCacheHeader = "x-higress-skip-ai-cache"
+ PLUGIN_NAME = "ai-cache"
+ CACHE_KEY_CONTEXT_KEY = "cacheKey"
+ CACHE_KEY_EMBEDDING_KEY = "cacheKeyEmbedding"
+ CACHE_CONTENT_CONTEXT_KEY = "cacheContent"
+ PARTIAL_MESSAGE_CONTEXT_KEY = "partialMessage"
+ TOOL_CALLS_CONTEXT_KEY = "toolCalls"
+ STREAM_CONTEXT_KEY = "stream"
+ SKIP_CACHE_HEADER = "x-higress-skip-ai-cache"
+ ERROR_PARTIAL_MESSAGE_KEY = "errorPartialMessage"
)
func main() {
+ // CreateClient()
wrapper.SetCtx(
- "ai-cache",
+ PLUGIN_NAME,
wrapper.ParseConfigBy(parseConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
wrapper.ProcessRequestBodyBy(onHttpRequestBody),
@@ -36,146 +36,26 @@ func main() {
)
}
-// @Name ai-cache
-// @Category protocol
-// @Phase AUTHN
-// @Priority 10
-// @Title zh-CN AI Cache
-// @Description zh-CN 大模型结果缓存
-// @IconUrl
-// @Version 0.1.0
-//
-// @Contact.name johnlanni
-// @Contact.url
-// @Contact.email
-//
-// @Example
-// redis:
-// serviceName: my-redis.dns
-// timeout: 2000
-// cacheKeyFrom:
-// requestBody: "messages.@reverse.0.content"
-// cacheValueFrom:
-// responseBody: "choices.0.message.content"
-// cacheStreamValueFrom:
-// responseBody: "choices.0.delta.content"
-// returnResponseTemplate: |
-// {"id":"from-cache","choices":[{"index":0,"message":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
-// returnStreamResponseTemplate: |
-// data:{"id":"from-cache","choices":[{"index":0,"delta":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
-//
-// data:[DONE]
-//
-// @End
-
-type RedisInfo struct {
- // @Title zh-CN redis 服务名称
- // @Description zh-CN 带服务类型的完整 FQDN 名称,例如 my-redis.dns、redis.my-ns.svc.cluster.local
- ServiceName string `required:"true" yaml:"serviceName" json:"serviceName"`
- // @Title zh-CN redis 服务端口
- // @Description zh-CN 默认值为6379
- ServicePort int `required:"false" yaml:"servicePort" json:"servicePort"`
- // @Title zh-CN 用户名
- // @Description zh-CN 登陆 redis 的用户名,非必填
- Username string `required:"false" yaml:"username" json:"username"`
- // @Title zh-CN 密码
- // @Description zh-CN 登陆 redis 的密码,非必填,可以只填密码
- Password string `required:"false" yaml:"password" json:"password"`
- // @Title zh-CN 请求超时
- // @Description zh-CN 请求 redis 的超时时间,单位为毫秒。默认值是1000,即1秒
- Timeout int `required:"false" yaml:"timeout" json:"timeout"`
-}
-
-type KVExtractor struct {
- // @Title zh-CN 从请求 Body 中基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串
- RequestBody string `required:"false" yaml:"requestBody" json:"requestBody"`
- // @Title zh-CN 从响应 Body 中基于 [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) 语法提取字符串
- ResponseBody string `required:"false" yaml:"responseBody" json:"responseBody"`
-}
-
-type PluginConfig struct {
- // @Title zh-CN Redis 地址信息
- // @Description zh-CN 用于存储缓存结果的 Redis 地址
- RedisInfo RedisInfo `required:"true" yaml:"redis" json:"redis"`
- // @Title zh-CN 缓存 key 的来源
- // @Description zh-CN 往 redis 里存时,使用的 key 的提取方式
- CacheKeyFrom KVExtractor `required:"true" yaml:"cacheKeyFrom" json:"cacheKeyFrom"`
- // @Title zh-CN 缓存 value 的来源
- // @Description zh-CN 往 redis 里存时,使用的 value 的提取方式
- CacheValueFrom KVExtractor `required:"true" yaml:"cacheValueFrom" json:"cacheValueFrom"`
- // @Title zh-CN 流式响应下,缓存 value 的来源
- // @Description zh-CN 往 redis 里存时,使用的 value 的提取方式
- CacheStreamValueFrom KVExtractor `required:"true" yaml:"cacheStreamValueFrom" json:"cacheStreamValueFrom"`
- // @Title zh-CN 返回 HTTP 响应的模版
- // @Description zh-CN 用 %s 标记需要被 cache value 替换的部分
- ReturnResponseTemplate string `required:"true" yaml:"returnResponseTemplate" json:"returnResponseTemplate"`
- // @Title zh-CN 返回流式 HTTP 响应的模版
- // @Description zh-CN 用 %s 标记需要被 cache value 替换的部分
- ReturnStreamResponseTemplate string `required:"true" yaml:"returnStreamResponseTemplate" json:"returnStreamResponseTemplate"`
- // @Title zh-CN 缓存的过期时间
- // @Description zh-CN 单位是秒,默认值为0,即永不过期
- CacheTTL int `required:"false" yaml:"cacheTTL" json:"cacheTTL"`
- // @Title zh-CN Redis缓存Key的前缀
- // @Description zh-CN 默认值是"higress-ai-cache:"
- CacheKeyPrefix string `required:"false" yaml:"cacheKeyPrefix" json:"cacheKeyPrefix"`
- redisClient wrapper.RedisClient `yaml:"-" json:"-"`
-}
-
-func parseConfig(json gjson.Result, c *PluginConfig, log wrapper.Log) error {
- c.RedisInfo.ServiceName = json.Get("redis.serviceName").String()
- if c.RedisInfo.ServiceName == "" {
- return errors.New("redis service name must not by empty")
- }
- c.RedisInfo.ServicePort = int(json.Get("redis.servicePort").Int())
- if c.RedisInfo.ServicePort == 0 {
- if strings.HasSuffix(c.RedisInfo.ServiceName, ".static") {
- // use default logic port which is 80 for static service
- c.RedisInfo.ServicePort = 80
- } else {
- c.RedisInfo.ServicePort = 6379
- }
- }
- c.RedisInfo.Username = json.Get("redis.username").String()
- c.RedisInfo.Password = json.Get("redis.password").String()
- c.RedisInfo.Timeout = int(json.Get("redis.timeout").Int())
- if c.RedisInfo.Timeout == 0 {
- c.RedisInfo.Timeout = 1000
- }
- c.CacheKeyFrom.RequestBody = json.Get("cacheKeyFrom.requestBody").String()
- if c.CacheKeyFrom.RequestBody == "" {
- c.CacheKeyFrom.RequestBody = "messages.@reverse.0.content"
- }
- c.CacheValueFrom.ResponseBody = json.Get("cacheValueFrom.responseBody").String()
- if c.CacheValueFrom.ResponseBody == "" {
- c.CacheValueFrom.ResponseBody = "choices.0.message.content"
- }
- c.CacheStreamValueFrom.ResponseBody = json.Get("cacheStreamValueFrom.responseBody").String()
- if c.CacheStreamValueFrom.ResponseBody == "" {
- c.CacheStreamValueFrom.ResponseBody = "choices.0.delta.content"
- }
- c.ReturnResponseTemplate = json.Get("returnResponseTemplate").String()
- if c.ReturnResponseTemplate == "" {
- c.ReturnResponseTemplate = `{"id":"from-cache","choices":[{"index":0,"message":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}`
- }
- c.ReturnStreamResponseTemplate = json.Get("returnStreamResponseTemplate").String()
- if c.ReturnStreamResponseTemplate == "" {
- c.ReturnStreamResponseTemplate = `data:{"id":"from-cache","choices":[{"index":0,"delta":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}` + "\n\ndata:[DONE]\n\n"
+func parseConfig(json gjson.Result, c *config.PluginConfig, log wrapper.Log) error {
+ // config.EmbeddingProviderConfig.FromJson(json.Get("embeddingProvider"))
+ // config.VectorDatabaseProviderConfig.FromJson(json.Get("vectorBaseProvider"))
+ // config.RedisConfig.FromJson(json.Get("redis"))
+ c.FromJson(json, log)
+ if err := c.Validate(); err != nil {
+ return err
}
- c.CacheKeyPrefix = json.Get("cacheKeyPrefix").String()
- if c.CacheKeyPrefix == "" {
- c.CacheKeyPrefix = DefaultCacheKeyPrefix
+ // Note that initializing the client during the parseConfig phase may cause errors, such as Redis not being usable in Docker Compose.
+ if err := c.Complete(log); err != nil {
+ log.Errorf("complete config failed: %v", err)
+ return err
}
- c.redisClient = wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
- FQDN: c.RedisInfo.ServiceName,
- Port: int64(c.RedisInfo.ServicePort),
- })
- return c.redisClient.Init(c.RedisInfo.Username, c.RedisInfo.Password, int64(c.RedisInfo.Timeout))
+ return nil
}
-func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
- skipCache, _ := proxywasm.GetHttpRequestHeader(SkipCacheHeader)
+func onHttpRequestHeaders(ctx wrapper.HttpContext, c config.PluginConfig, log wrapper.Log) types.Action {
+ skipCache, _ := proxywasm.GetHttpRequestHeader(SKIP_CACHE_HEADER)
if skipCache == "on" {
- ctx.SetContext(SkipCacheHeader, struct{}{})
+ ctx.SetContext(SKIP_CACHE_HEADER, struct{}{})
ctx.DontReadRequestBody()
return types.ActionContinue
}
@@ -185,199 +65,123 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrap
return types.ActionContinue
}
if !strings.Contains(contentType, "application/json") {
- log.Warnf("content is not json, can't process:%s", contentType)
+ log.Warnf("content is not json, can't process: %s", contentType)
ctx.DontReadRequestBody()
return types.ActionContinue
}
- proxywasm.RemoveHttpRequestHeader("Accept-Encoding")
+ _ = proxywasm.RemoveHttpRequestHeader("Accept-Encoding")
// The request has a body and requires delaying the header transmission until a cache miss occurs,
// at which point the header should be sent.
return types.HeaderStopIteration
}
-func TrimQuote(source string) string {
- return strings.Trim(source, `"`)
-}
+func onHttpRequestBody(ctx wrapper.HttpContext, c config.PluginConfig, body []byte, log wrapper.Log) types.Action {
-func onHttpRequestBody(ctx wrapper.HttpContext, config PluginConfig, body []byte, log wrapper.Log) types.Action {
bodyJson := gjson.ParseBytes(body)
// TODO: It may be necessary to support stream mode determination for different LLM providers.
stream := false
if bodyJson.Get("stream").Bool() {
stream = true
- ctx.SetContext(StreamContextKey, struct{}{})
- } else if ctx.GetContext(StreamContextKey) != nil {
- stream = true
+ ctx.SetContext(STREAM_CONTEXT_KEY, struct{}{})
+ }
+
+ var key string
+ if c.CacheKeyStrategy == config.CACHE_KEY_STRATEGY_LAST_QUESTION {
+ log.Debugf("[onHttpRequestBody] cache key strategy is last question, cache key from: %s", c.CacheKeyFrom)
+ key = bodyJson.Get(c.CacheKeyFrom).String()
+ } else if c.CacheKeyStrategy == config.CACHE_KEY_STRATEGY_ALL_QUESTIONS {
+ log.Debugf("[onHttpRequestBody] cache key strategy is all questions, cache key from: messages")
+ messages := bodyJson.Get("messages").Array()
+ var userMessages []string
+ for _, msg := range messages {
+ if msg.Get("role").String() == "user" {
+ userMessages = append(userMessages, msg.Get("content").String())
+ }
+ }
+ key = strings.Join(userMessages, "\n")
+ } else if c.CacheKeyStrategy == config.CACHE_KEY_STRATEGY_DISABLED {
+ log.Info("[onHttpRequestBody] cache key strategy is disabled")
+ ctx.DontReadRequestBody()
+ return types.ActionContinue
+ } else {
+ log.Warnf("[onHttpRequestBody] unknown cache key strategy: %s", c.CacheKeyStrategy)
+ ctx.DontReadRequestBody()
+ return types.ActionContinue
}
- key := TrimQuote(bodyJson.Get(config.CacheKeyFrom.RequestBody).Raw)
+
+ ctx.SetContext(CACHE_KEY_CONTEXT_KEY, key)
+ log.Debugf("[onHttpRequestBody] key: %s", key)
if key == "" {
- log.Debug("parse key from request body failed")
+ log.Debug("[onHttpRequestBody] parse key from request body failed")
+ ctx.DontReadResponseBody()
return types.ActionContinue
}
- ctx.SetContext(CacheKeyContextKey, key)
- err := config.redisClient.Get(config.CacheKeyPrefix+key, func(response resp.Value) {
- if err := response.Error(); err != nil {
- log.Errorf("redis get key:%s failed, err:%v", key, err)
- proxywasm.ResumeHttpRequest()
- return
- }
- if response.IsNull() {
- log.Debugf("cache miss, key:%s", key)
- proxywasm.ResumeHttpRequest()
- return
- }
- log.Debugf("cache hit, key:%s", key)
- ctx.SetContext(CacheKeyContextKey, nil)
- if !stream {
- proxywasm.SendHttpResponseWithDetail(200, "ai-cache.hit", [][2]string{{"content-type", "application/json; charset=utf-8"}}, []byte(fmt.Sprintf(config.ReturnResponseTemplate, response.String())), -1)
- } else {
- proxywasm.SendHttpResponseWithDetail(200, "ai-cache.hit", [][2]string{{"content-type", "text/event-stream; charset=utf-8"}}, []byte(fmt.Sprintf(config.ReturnStreamResponseTemplate, response.String())), -1)
- }
- })
- if err != nil {
- log.Error("redis access failed")
+
+ if err := CheckCacheForKey(key, ctx, c, log, stream, true); err != nil {
+ log.Errorf("[onHttpRequestBody] check cache for key: %s failed, error: %v", key, err)
return types.ActionContinue
}
- return types.ActionPause
-}
-func processSSEMessage(ctx wrapper.HttpContext, config PluginConfig, sseMessage string, log wrapper.Log) string {
- subMessages := strings.Split(sseMessage, "\n")
- var message string
- for _, msg := range subMessages {
- if strings.HasPrefix(msg, "data:") {
- message = msg
- break
- }
- }
- if len(message) < 6 {
- log.Errorf("invalid message:%s", message)
- return ""
- }
- // skip the prefix "data:"
- bodyJson := message[5:]
- if gjson.Get(bodyJson, config.CacheStreamValueFrom.ResponseBody).Exists() {
- tempContentI := ctx.GetContext(CacheContentContextKey)
- if tempContentI == nil {
- content := TrimQuote(gjson.Get(bodyJson, config.CacheStreamValueFrom.ResponseBody).Raw)
- ctx.SetContext(CacheContentContextKey, content)
- return content
- }
- append := TrimQuote(gjson.Get(bodyJson, config.CacheStreamValueFrom.ResponseBody).Raw)
- content := tempContentI.(string) + append
- ctx.SetContext(CacheContentContextKey, content)
- return content
- } else if gjson.Get(bodyJson, "choices.0.delta.content.tool_calls").Exists() {
- // TODO: compatible with other providers
- ctx.SetContext(ToolCallsContextKey, struct{}{})
- return ""
- }
- log.Debugf("unknown message:%s", bodyJson)
- return ""
+ return types.ActionPause
}
-func onHttpResponseHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
- skipCache := ctx.GetContext(SkipCacheHeader)
+func onHttpResponseHeaders(ctx wrapper.HttpContext, c config.PluginConfig, log wrapper.Log) types.Action {
+ skipCache := ctx.GetContext(SKIP_CACHE_HEADER)
if skipCache != nil {
ctx.DontReadResponseBody()
return types.ActionContinue
}
contentType, _ := proxywasm.GetHttpResponseHeader("content-type")
if strings.Contains(contentType, "text/event-stream") {
- ctx.SetContext(StreamContextKey, struct{}{})
+ ctx.SetContext(STREAM_CONTEXT_KEY, struct{}{})
}
+
+ if ctx.GetContext(ERROR_PARTIAL_MESSAGE_KEY) != nil {
+ ctx.DontReadResponseBody()
+ return types.ActionContinue
+ }
+
return types.ActionContinue
}
-func onHttpResponseBody(ctx wrapper.HttpContext, config PluginConfig, chunk []byte, isLastChunk bool, log wrapper.Log) []byte {
- if ctx.GetContext(ToolCallsContextKey) != nil {
- // we should not cache tool call result
+func onHttpResponseBody(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, isLastChunk bool, log wrapper.Log) []byte {
+ log.Debugf("[onHttpResponseBody] is last chunk: %v", isLastChunk)
+ log.Debugf("[onHttpResponseBody] chunk: %s", string(chunk))
+
+ if ctx.GetContext(TOOL_CALLS_CONTEXT_KEY) != nil {
return chunk
}
- keyI := ctx.GetContext(CacheKeyContextKey)
- if keyI == nil {
+
+ key := ctx.GetContext(CACHE_KEY_CONTEXT_KEY)
+ if key == nil {
+ log.Debug("[onHttpResponseBody] key is nil, skip cache")
return chunk
}
+
if !isLastChunk {
- stream := ctx.GetContext(StreamContextKey)
- if stream == nil {
- tempContentI := ctx.GetContext(CacheContentContextKey)
- if tempContentI == nil {
- ctx.SetContext(CacheContentContextKey, chunk)
- return chunk
- }
- tempContent := tempContentI.([]byte)
- tempContent = append(tempContent, chunk...)
- ctx.SetContext(CacheContentContextKey, tempContent)
- } else {
- var partialMessage []byte
- partialMessageI := ctx.GetContext(PartialMessageContextKey)
- if partialMessageI != nil {
- partialMessage = append(partialMessageI.([]byte), chunk...)
- } else {
- partialMessage = chunk
- }
- messages := strings.Split(string(partialMessage), "\n\n")
- for i, msg := range messages {
- if i < len(messages)-1 {
- // process complete message
- processSSEMessage(ctx, config, msg, log)
- }
- }
- if !strings.HasSuffix(string(partialMessage), "\n\n") {
- ctx.SetContext(PartialMessageContextKey, []byte(messages[len(messages)-1]))
- } else {
- ctx.SetContext(PartialMessageContextKey, nil)
- }
+ if err := handleNonLastChunk(ctx, c, chunk, log); err != nil {
+ log.Errorf("[onHttpResponseBody] handle non last chunk failed, error: %v", err)
+ // Set an empty struct in the context to indicate an error in processing the partial message
+ ctx.SetContext(ERROR_PARTIAL_MESSAGE_KEY, struct{}{})
}
return chunk
}
- // last chunk
- key := keyI.(string)
- stream := ctx.GetContext(StreamContextKey)
+
+ stream := ctx.GetContext(STREAM_CONTEXT_KEY)
var value string
+ var err error
if stream == nil {
- var body []byte
- tempContentI := ctx.GetContext(CacheContentContextKey)
- if tempContentI != nil {
- body = append(tempContentI.([]byte), chunk...)
- } else {
- body = chunk
- }
- bodyJson := gjson.ParseBytes(body)
-
- value = TrimQuote(bodyJson.Get(config.CacheValueFrom.ResponseBody).Raw)
- if value == "" {
- log.Warnf("parse value from response body failded, body:%s", body)
- return chunk
- }
+ value, err = processNonStreamLastChunk(ctx, c, chunk, log)
} else {
- if len(chunk) > 0 {
- var lastMessage []byte
- partialMessageI := ctx.GetContext(PartialMessageContextKey)
- if partialMessageI != nil {
- lastMessage = append(partialMessageI.([]byte), chunk...)
- } else {
- lastMessage = chunk
- }
- if !strings.HasSuffix(string(lastMessage), "\n\n") {
- log.Warnf("invalid lastMessage:%s", lastMessage)
- return chunk
- }
- // remove the last \n\n
- lastMessage = lastMessage[:len(lastMessage)-2]
- value = processSSEMessage(ctx, config, string(lastMessage), log)
- } else {
- tempContentI := ctx.GetContext(CacheContentContextKey)
- if tempContentI == nil {
- return chunk
- }
- value = tempContentI.(string)
- }
+ value, err = processStreamLastChunk(ctx, c, chunk, log)
}
- config.redisClient.Set(config.CacheKeyPrefix+key, value, nil)
- if config.CacheTTL != 0 {
- config.redisClient.Expire(config.CacheKeyPrefix+key, config.CacheTTL, nil)
+
+ if err != nil {
+ log.Errorf("[onHttpResponseBody] process last chunk failed, error: %v", err)
+ return chunk
}
+
+ cacheResponse(ctx, c, key.(string), value, log)
+ uploadEmbeddingAndAnswer(ctx, c, key.(string), value, log)
return chunk
}
diff --git a/plugins/wasm-go/extensions/ai-cache/util.go b/plugins/wasm-go/extensions/ai-cache/util.go
new file mode 100644
index 0000000000..983dfbb25a
--- /dev/null
+++ b/plugins/wasm-go/extensions/ai-cache/util.go
@@ -0,0 +1,155 @@
+package main
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/alibaba/higress/plugins/wasm-go/extensions/ai-cache/config"
+ "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
+ "github.com/tidwall/gjson"
+)
+
+func handleNonLastChunk(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, log wrapper.Log) error {
+ stream := ctx.GetContext(STREAM_CONTEXT_KEY)
+ err := error(nil)
+ if stream == nil {
+ err = handleNonStreamChunk(ctx, c, chunk, log)
+ } else {
+ err = handleStreamChunk(ctx, c, chunk, log)
+ }
+ return err
+}
+
+func handleNonStreamChunk(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, log wrapper.Log) error {
+ tempContentI := ctx.GetContext(CACHE_CONTENT_CONTEXT_KEY)
+ if tempContentI == nil {
+ ctx.SetContext(CACHE_CONTENT_CONTEXT_KEY, chunk)
+ return nil
+ }
+ tempContent := tempContentI.([]byte)
+ tempContent = append(tempContent, chunk...)
+ ctx.SetContext(CACHE_CONTENT_CONTEXT_KEY, tempContent)
+ return nil
+}
+
+func handleStreamChunk(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, log wrapper.Log) error {
+ var partialMessage []byte
+ partialMessageI := ctx.GetContext(PARTIAL_MESSAGE_CONTEXT_KEY)
+ log.Debugf("[handleStreamChunk] cache content: %v", ctx.GetContext(CACHE_CONTENT_CONTEXT_KEY))
+ if partialMessageI != nil {
+ partialMessage = append(partialMessageI.([]byte), chunk...)
+ } else {
+ partialMessage = chunk
+ }
+ messages := strings.Split(string(partialMessage), "\n\n")
+ for i, msg := range messages {
+ if i < len(messages)-1 {
+ _, err := processSSEMessage(ctx, c, msg, log)
+ if err != nil {
+ return fmt.Errorf("[handleStreamChunk] processSSEMessage failed, error: %v", err)
+ }
+ }
+ }
+ if !strings.HasSuffix(string(partialMessage), "\n\n") {
+ ctx.SetContext(PARTIAL_MESSAGE_CONTEXT_KEY, []byte(messages[len(messages)-1]))
+ } else {
+ ctx.SetContext(PARTIAL_MESSAGE_CONTEXT_KEY, nil)
+ }
+ return nil
+}
+
+func processNonStreamLastChunk(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, log wrapper.Log) (string, error) {
+ var body []byte
+ tempContentI := ctx.GetContext(CACHE_CONTENT_CONTEXT_KEY)
+ if tempContentI != nil {
+ body = append(tempContentI.([]byte), chunk...)
+ } else {
+ body = chunk
+ }
+ bodyJson := gjson.ParseBytes(body)
+ value := bodyJson.Get(c.CacheValueFrom).String()
+ if strings.TrimSpace(value) == "" {
+ return "", fmt.Errorf("[processNonStreamLastChunk] parse value from response body failed, body:%s", body)
+ }
+ return value, nil
+}
+
+func processStreamLastChunk(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, log wrapper.Log) (string, error) {
+ if len(chunk) > 0 {
+ var lastMessage []byte
+ partialMessageI := ctx.GetContext(PARTIAL_MESSAGE_CONTEXT_KEY)
+ if partialMessageI != nil {
+ lastMessage = append(partialMessageI.([]byte), chunk...)
+ } else {
+ lastMessage = chunk
+ }
+ if !strings.HasSuffix(string(lastMessage), "\n\n") {
+ return "", fmt.Errorf("[processStreamLastChunk] invalid lastMessage:%s", lastMessage)
+ }
+ lastMessage = lastMessage[:len(lastMessage)-2]
+ value, err := processSSEMessage(ctx, c, string(lastMessage), log)
+ if err != nil {
+ return "", fmt.Errorf("[processStreamLastChunk] processSSEMessage failed, error: %v", err)
+ }
+ return value, nil
+ }
+ tempContentI := ctx.GetContext(CACHE_CONTENT_CONTEXT_KEY)
+ if tempContentI == nil {
+ return "", nil
+ }
+ return tempContentI.(string), nil
+}
+
+func processSSEMessage(ctx wrapper.HttpContext, c config.PluginConfig, sseMessage string, log wrapper.Log) (string, error) {
+ subMessages := strings.Split(sseMessage, "\n")
+ var message string
+ for _, msg := range subMessages {
+ if strings.HasPrefix(msg, "data:") {
+ message = msg
+ break
+ }
+ }
+ if len(message) < 6 {
+ return "", fmt.Errorf("[processSSEMessage] invalid message: %s", message)
+ }
+
+ // skip the prefix "data:"
+ bodyJson := message[5:]
+
+ if strings.TrimSpace(bodyJson) == "[DONE]" {
+ return "", nil
+ }
+
+ // Extract values from JSON fields
+ responseBody := gjson.Get(bodyJson, c.CacheStreamValueFrom)
+ toolCalls := gjson.Get(bodyJson, c.CacheToolCallsFrom)
+
+ if toolCalls.Exists() {
+ // TODO: Temporarily store the tool_calls value in the context for processing
+ ctx.SetContext(TOOL_CALLS_CONTEXT_KEY, toolCalls.String())
+ }
+
+ // Check if the ResponseBody field exists
+ if !responseBody.Exists() {
+ if ctx.GetContext(CACHE_CONTENT_CONTEXT_KEY) != nil {
+ log.Debugf("[processSSEMessage] unable to extract content from message; cache content is not nil: %s", message)
+ return "", nil
+ }
+ return "", fmt.Errorf("[processSSEMessage] unable to extract content from message; cache content is nil: %s", message)
+ } else {
+ tempContentI := ctx.GetContext(CACHE_CONTENT_CONTEXT_KEY)
+
+ // If there is no content in the cache, initialize and set the content
+ if tempContentI == nil {
+ content := responseBody.String()
+ ctx.SetContext(CACHE_CONTENT_CONTEXT_KEY, content)
+ return content, nil
+ }
+
+ // Update the content in the cache
+ appendMsg := responseBody.String()
+ content := tempContentI.(string) + appendMsg
+ ctx.SetContext(CACHE_CONTENT_CONTEXT_KEY, content)
+ return content, nil
+ }
+}
diff --git a/plugins/wasm-go/extensions/ai-cache/vector/dashvector.go b/plugins/wasm-go/extensions/ai-cache/vector/dashvector.go
new file mode 100644
index 0000000000..7bdb0a76d0
--- /dev/null
+++ b/plugins/wasm-go/extensions/ai-cache/vector/dashvector.go
@@ -0,0 +1,256 @@
+package vector
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+
+ "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
+)
+
+type dashVectorProviderInitializer struct {
+}
+
+func (d *dashVectorProviderInitializer) ValidateConfig(config ProviderConfig) error {
+ if len(config.apiKey) == 0 {
+ return errors.New("[DashVector] apiKey is required")
+ }
+ if len(config.collectionID) == 0 {
+ return errors.New("[DashVector] collectionID is required")
+ }
+ if len(config.serviceName) == 0 {
+ return errors.New("[DashVector] serviceName is required")
+ }
+ if len(config.serviceHost) == 0 {
+ return errors.New("[DashVector] serviceHost is required")
+ }
+ return nil
+}
+
+func (d *dashVectorProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
+ return &DvProvider{
+ config: config,
+ client: wrapper.NewClusterClient(wrapper.FQDNCluster{
+ FQDN: config.serviceName,
+ Host: config.serviceHost,
+ Port: int64(config.servicePort),
+ }),
+ }, nil
+}
+
+type DvProvider struct {
+ config ProviderConfig
+ client wrapper.HttpClient
+}
+
+func (d *DvProvider) GetProviderType() string {
+ return PROVIDER_TYPE_DASH_VECTOR
+}
+
+// type embeddingRequest struct {
+// Model string `json:"model"`
+// Input input `json:"input"`
+// Parameters params `json:"parameters"`
+// }
+
+// type params struct {
+// TextType string `json:"text_type"`
+// }
+
+// type input struct {
+// Texts []string `json:"texts"`
+// }
+
+// queryResponse 定义查询响应的结构
+type queryResponse struct {
+ Code int `json:"code"`
+ RequestID string `json:"request_id"`
+ Message string `json:"message"`
+ Output []result `json:"output"`
+}
+
+// queryRequest 定义查询请求的结构
+type queryRequest struct {
+ Vector []float64 `json:"vector"`
+ TopK int `json:"topk"`
+ IncludeVector bool `json:"include_vector"`
+}
+
+// result 定义查询结果的结构
+type result struct {
+ ID string `json:"id"`
+ Vector []float64 `json:"vector,omitempty"` // omitempty 使得如果 vector 是空,它将不会被序列化
+ Fields map[string]interface{} `json:"fields"`
+ Score float64 `json:"score"`
+}
+
+func (d *DvProvider) constructEmbeddingQueryParameters(vector []float64) (string, []byte, [][2]string, error) {
+ url := fmt.Sprintf("/v1/collections/%s/query", d.config.collectionID)
+
+ requestData := queryRequest{
+ Vector: vector,
+ TopK: d.config.topK,
+ IncludeVector: false,
+ }
+
+ requestBody, err := json.Marshal(requestData)
+ if err != nil {
+ return "", nil, nil, err
+ }
+
+ header := [][2]string{
+ {"Content-Type", "application/json"},
+ {"dashvector-auth-token", d.config.apiKey},
+ }
+
+ return url, requestBody, header, nil
+}
+
+func (d *DvProvider) parseQueryResponse(responseBody []byte) (queryResponse, error) {
+ var queryResp queryResponse
+ err := json.Unmarshal(responseBody, &queryResp)
+ if err != nil {
+ return queryResponse{}, err
+ }
+ return queryResp, nil
+}
+
+func (d *DvProvider) QueryEmbedding(
+ emb []float64,
+ ctx wrapper.HttpContext,
+ log wrapper.Log,
+ callback func(results []QueryResult, ctx wrapper.HttpContext, log wrapper.Log, err error)) error {
+ url, body, headers, err := d.constructEmbeddingQueryParameters(emb)
+ log.Debugf("url:%s, body:%s, headers:%v", url, string(body), headers)
+ if err != nil {
+ err = fmt.Errorf("failed to construct embedding query parameters: %v", err)
+ return err
+ }
+
+ err = d.client.Post(url, headers, body,
+ func(statusCode int, responseHeaders http.Header, responseBody []byte) {
+ err = nil
+ if statusCode != http.StatusOK {
+ err = fmt.Errorf("failed to query embedding: %d", statusCode)
+ callback(nil, ctx, log, err)
+ return
+ }
+ log.Debugf("query embedding response: %d, %s", statusCode, responseBody)
+ results, err := d.ParseQueryResponse(responseBody, ctx, log)
+ if err != nil {
+ err = fmt.Errorf("failed to parse query response: %v", err)
+ }
+ callback(results, ctx, log, err)
+ },
+ d.config.timeout)
+ if err != nil {
+ err = fmt.Errorf("failed to query embedding: %v", err)
+ }
+ return err
+}
+
+func getStringValue(fields map[string]interface{}, key string) string {
+ if val, ok := fields[key]; ok {
+ return val.(string)
+ }
+ return ""
+}
+
+func (d *DvProvider) ParseQueryResponse(responseBody []byte, ctx wrapper.HttpContext, log wrapper.Log) ([]QueryResult, error) {
+ resp, err := d.parseQueryResponse(responseBody)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(resp.Output) == 0 {
+ return nil, errors.New("no query results found in response")
+ }
+
+ results := make([]QueryResult, 0, len(resp.Output))
+
+ for _, output := range resp.Output {
+ result := QueryResult{
+ Text: getStringValue(output.Fields, "query"),
+ Embedding: output.Vector,
+ Score: output.Score,
+ Answer: getStringValue(output.Fields, "answer"),
+ }
+ results = append(results, result)
+ }
+
+ return results, nil
+}
+
+type document struct {
+ Vector []float64 `json:"vector"`
+ Fields map[string]string `json:"fields"`
+}
+
+type insertRequest struct {
+ Docs []document `json:"docs"`
+}
+
+func (d *DvProvider) constructUploadParameters(emb []float64, queryString string, answer string) (string, []byte, [][2]string, error) {
+ url := "/v1/collections/" + d.config.collectionID + "/docs"
+
+ doc := document{
+ Vector: emb,
+ Fields: map[string]string{
+ "query": queryString,
+ "answer": answer,
+ },
+ }
+
+ requestBody, err := json.Marshal(insertRequest{Docs: []document{doc}})
+ if err != nil {
+ return "", nil, nil, err
+ }
+
+ header := [][2]string{
+ {"Content-Type", "application/json"},
+ {"dashvector-auth-token", d.config.apiKey},
+ }
+
+ return url, requestBody, header, err
+}
+
+func (d *DvProvider) UploadEmbedding(queryString string, queryEmb []float64, ctx wrapper.HttpContext, log wrapper.Log, callback func(ctx wrapper.HttpContext, log wrapper.Log, err error)) error {
+ url, body, headers, err := d.constructUploadParameters(queryEmb, queryString, "")
+ if err != nil {
+ return err
+ }
+ err = d.client.Post(
+ url,
+ headers,
+ body,
+ func(statusCode int, responseHeaders http.Header, responseBody []byte) {
+ log.Debugf("statusCode:%d, responseBody:%s", statusCode, string(responseBody))
+ if statusCode != http.StatusOK {
+ err = fmt.Errorf("failed to upload embedding: %d", statusCode)
+ }
+ callback(ctx, log, err)
+ },
+ d.config.timeout)
+ return err
+}
+
+func (d *DvProvider) UploadAnswerAndEmbedding(queryString string, queryEmb []float64, queryAnswer string, ctx wrapper.HttpContext, log wrapper.Log, callback func(ctx wrapper.HttpContext, log wrapper.Log, err error)) error {
+ url, body, headers, err := d.constructUploadParameters(queryEmb, queryString, queryAnswer)
+ if err != nil {
+ return err
+ }
+ err = d.client.Post(
+ url,
+ headers,
+ body,
+ func(statusCode int, responseHeaders http.Header, responseBody []byte) {
+ log.Debugf("statusCode:%d, responseBody:%s", statusCode, string(responseBody))
+ if statusCode != http.StatusOK {
+ err = fmt.Errorf("failed to upload embedding: %d", statusCode)
+ }
+ callback(ctx, log, err)
+ },
+ d.config.timeout)
+ return err
+}
diff --git a/plugins/wasm-go/extensions/ai-cache/vector/provider.go b/plugins/wasm-go/extensions/ai-cache/vector/provider.go
new file mode 100644
index 0000000000..a04123a166
--- /dev/null
+++ b/plugins/wasm-go/extensions/ai-cache/vector/provider.go
@@ -0,0 +1,167 @@
+package vector
+
+import (
+ "errors"
+
+ "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
+ "github.com/tidwall/gjson"
+)
+
+const (
+ PROVIDER_TYPE_DASH_VECTOR = "dashvector"
+ PROVIDER_TYPE_CHROMA = "chroma"
+)
+
+type providerInitializer interface {
+ ValidateConfig(ProviderConfig) error
+ CreateProvider(ProviderConfig) (Provider, error)
+}
+
+var (
+ providerInitializers = map[string]providerInitializer{
+ PROVIDER_TYPE_DASH_VECTOR: &dashVectorProviderInitializer{},
+ // PROVIDER_TYPE_CHROMA: &chromaProviderInitializer{},
+ }
+)
+
+// QueryResult 定义通用的查询结果的结构体
+type QueryResult struct {
+ Text string // 相似的文本
+ Embedding []float64 // 相似文本的向量
+ Score float64 // 文本的向量相似度或距离等度量
+ Answer string // 相似文本对应的LLM生成的回答
+}
+
+type Provider interface {
+ GetProviderType() string
+}
+
+type EmbeddingQuerier interface {
+ QueryEmbedding(
+ emb []float64,
+ ctx wrapper.HttpContext,
+ log wrapper.Log,
+ callback func(results []QueryResult, ctx wrapper.HttpContext, log wrapper.Log, err error)) error
+}
+
+type EmbeddingUploader interface {
+ UploadEmbedding(
+ queryString string,
+ queryEmb []float64,
+ ctx wrapper.HttpContext,
+ log wrapper.Log,
+ callback func(ctx wrapper.HttpContext, log wrapper.Log, err error)) error
+}
+
+type AnswerAndEmbeddingUploader interface {
+ UploadAnswerAndEmbedding(
+ queryString string,
+ queryEmb []float64,
+ answer string,
+ ctx wrapper.HttpContext,
+ log wrapper.Log,
+ callback func(ctx wrapper.HttpContext, log wrapper.Log, err error)) error
+}
+
+type StringQuerier interface {
+ QueryString(
+ queryString string,
+ ctx wrapper.HttpContext,
+ log wrapper.Log,
+ callback func(results []QueryResult, ctx wrapper.HttpContext, log wrapper.Log, err error)) error
+}
+
+type SimilarityThresholdProvider interface {
+ GetSimilarityThreshold() float64
+}
+
+type ProviderConfig struct {
+ // @Title zh-CN 向量存储服务提供者类型
+ // @Description zh-CN 向量存储服务提供者类型,例如 dashvector、chroma
+ typ string
+ // @Title zh-CN 向量存储服务名称
+ // @Description zh-CN 向量存储服务名称
+ serviceName string
+ // @Title zh-CN 向量存储服务域名
+ // @Description zh-CN 向量存储服务域名
+ serviceHost string
+ // @Title zh-CN 向量存储服务端口
+ // @Description zh-CN 向量存储服务端口
+ servicePort int64
+ // @Title zh-CN 向量存储服务 API Key
+ // @Description zh-CN 向量存储服务 API Key
+ apiKey string
+ // @Title zh-CN 返回TopK结果
+ // @Description zh-CN 返回TopK结果,默认为 1
+ topK int
+ // @Title zh-CN 请求超时
+ // @Description zh-CN 请求向量存储服务的超时时间,单位为毫秒。默认值是10000,即10秒
+ timeout uint32
+ // @Title zh-CN DashVector 向量存储服务 Collection ID
+ // @Description zh-CN DashVector 向量存储服务 Collection ID
+ collectionID string
+ // @Title zh-CN 相似度度量阈值
+ // @Description zh-CN 默认相似度度量阈值,默认为 1000。
+ Threshold float64
+ // @Title zh-CN 相似度度量比较方式
+ // @Description zh-CN 相似度度量比较方式,默认为小于。
+ // 相似度度量方式有 Cosine, DotProduct, Euclidean 等,前两者值越大相似度越高,后者值越小相似度越高。
+ // 所以需要允许自定义比较方式,对于 Cosine 和 DotProduct 选择 gt,对于 Euclidean 则选择 lt。
+ // 默认为 lt,所有条件包括 lt (less than,小于)、lte (less than or equal to,小等于)、gt (greater than,大于)、gte (greater than or equal to,大等于)
+ ThresholdRelation string
+}
+
+func (c *ProviderConfig) GetProviderType() string {
+ return c.typ
+}
+
+func (c *ProviderConfig) FromJson(json gjson.Result) {
+ c.typ = json.Get("type").String()
+ // DashVector
+ c.serviceName = json.Get("serviceName").String()
+ c.serviceHost = json.Get("serviceHost").String()
+ c.servicePort = int64(json.Get("servicePort").Int())
+ if c.servicePort == 0 {
+ c.servicePort = 443
+ }
+ c.apiKey = json.Get("apiKey").String()
+ c.collectionID = json.Get("collectionID").String()
+ c.topK = int(json.Get("topK").Int())
+ if c.topK == 0 {
+ c.topK = 1
+ }
+ c.timeout = uint32(json.Get("timeout").Int())
+ if c.timeout == 0 {
+ c.timeout = 10000
+ }
+ c.Threshold = json.Get("threshold").Float()
+ if c.Threshold == 0 {
+ c.Threshold = 1000
+ }
+ c.ThresholdRelation = json.Get("thresholdRelation").String()
+ if c.ThresholdRelation == "" {
+ c.ThresholdRelation = "lt"
+ }
+}
+
+func (c *ProviderConfig) Validate() error {
+ if c.typ == "" {
+ return errors.New("vector database service is required")
+ }
+ initializer, has := providerInitializers[c.typ]
+ if !has {
+ return errors.New("unknown vector database service provider type: " + c.typ)
+ }
+ if err := initializer.ValidateConfig(*c); err != nil {
+ return err
+ }
+ return nil
+}
+
+func CreateProvider(pc ProviderConfig) (Provider, error) {
+ initializer, has := providerInitializers[pc.typ]
+ if !has {
+ return nil, errors.New("unknown provider type: " + pc.typ)
+ }
+ return initializer.CreateProvider(pc)
+}
diff --git a/plugins/wasm-go/extensions/ai-proxy/Makefile b/plugins/wasm-go/extensions/ai-proxy/Makefile
new file mode 100644
index 0000000000..e5c7fa8de9
--- /dev/null
+++ b/plugins/wasm-go/extensions/ai-proxy/Makefile
@@ -0,0 +1,4 @@
+.DEFAULT:
+build:
+ tinygo build -o ai-proxy.wasm -scheduler=none -target=wasi -gc=custom -tags='custommalloc nottinygc_finalizer proxy_wasm_version_0_2_100' ./main.go
+ mv ai-proxy.wasm ../../../../docker-compose-test/
\ No newline at end of file
diff --git a/plugins/wasm-go/extensions/ai-proxy/go.mod b/plugins/wasm-go/extensions/ai-proxy/go.mod
index 7fed801fab..a5457b90f8 100644
--- a/plugins/wasm-go/extensions/ai-proxy/go.mod
+++ b/plugins/wasm-go/extensions/ai-proxy/go.mod
@@ -10,7 +10,7 @@ require (
github.com/alibaba/higress/plugins/wasm-go v0.0.0
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f
github.com/stretchr/testify v1.8.4
- github.com/tidwall/gjson v1.14.3
+ github.com/tidwall/gjson v1.17.3
)
require (
diff --git a/plugins/wasm-go/extensions/ai-proxy/go.sum b/plugins/wasm-go/extensions/ai-proxy/go.sum
index e5b8b79175..b2d63b5f4b 100644
--- a/plugins/wasm-go/extensions/ai-proxy/go.sum
+++ b/plugins/wasm-go/extensions/ai-proxy/go.sum
@@ -13,8 +13,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
-github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
-github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
+github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
diff --git a/plugins/wasm-go/extensions/ai-proxy/main.go b/plugins/wasm-go/extensions/ai-proxy/main.go
index 9e0fafe179..7b19d03fc2 100644
--- a/plugins/wasm-go/extensions/ai-proxy/main.go
+++ b/plugins/wasm-go/extensions/ai-proxy/main.go
@@ -82,7 +82,8 @@ func onHttpRequestHeader(ctx wrapper.HttpContext, pluginConfig config.PluginConf
providerConfig := pluginConfig.GetProviderConfig()
if apiName == "" && !providerConfig.IsOriginal() {
log.Debugf("[onHttpRequestHeader] unsupported path: %s", path.Path)
- _ = util.SendResponse(404, "ai-proxy.unknown_api", util.MimeTypeTextPlain, "API not found: "+path.Path)
+ // _ = util.SendResponse(404, "ai-proxy.unknown_api", util.MimeTypeTextPlain, "API not found: "+path.Path)
+ log.Debugf("[onHttpRequestHeader] no send response")
return types.ActionContinue
}
ctx.SetContext(ctxKeyApiName, apiName)
diff --git a/plugins/wasm-go/extensions/request-block/Dockerfile b/plugins/wasm-go/extensions/request-block/Dockerfile
new file mode 100644
index 0000000000..9b084e0596
--- /dev/null
+++ b/plugins/wasm-go/extensions/request-block/Dockerfile
@@ -0,0 +1,2 @@
+FROM scratch
+COPY main.wasm plugin.wasm
\ No newline at end of file
diff --git a/plugins/wasm-go/extensions/request-block/Makefile b/plugins/wasm-go/extensions/request-block/Makefile
new file mode 100644
index 0000000000..1210d6ec34
--- /dev/null
+++ b/plugins/wasm-go/extensions/request-block/Makefile
@@ -0,0 +1,4 @@
+.DEFAULT:
+build:
+ tinygo build -o main.wasm -scheduler=none -target=wasi -gc=custom -tags='custommalloc nottinygc_finalizer' ./main.go
+ mv main.wasm ../../../../docker-compose-test/
\ No newline at end of file
diff --git a/plugins/wasm-go/extensions/request-block/main.go b/plugins/wasm-go/extensions/request-block/main.go
index 224d4b26d6..2a43b4df72 100644
--- a/plugins/wasm-go/extensions/request-block/main.go
+++ b/plugins/wasm-go/extensions/request-block/main.go
@@ -177,7 +177,9 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config RequestBlockConfig, lo
}
func onHttpRequestBody(ctx wrapper.HttpContext, config RequestBlockConfig, body []byte, log wrapper.Log) types.Action {
+ log.Infof("My request-block body: %s\n", string(body))
bodyStr := string(body)
+
if !config.caseSensitive {
bodyStr = strings.ToLower(bodyStr)
}
diff --git a/plugins/wasm-go/go.mod b/plugins/wasm-go/go.mod
index 999721f3f6..6373ff646e 100644
--- a/plugins/wasm-go/go.mod
+++ b/plugins/wasm-go/go.mod
@@ -7,7 +7,7 @@ require (
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f
github.com/stretchr/testify v1.8.4
- github.com/tidwall/gjson v1.14.3
+ github.com/tidwall/gjson v1.17.3
github.com/tidwall/resp v0.1.1
)
diff --git a/plugins/wasm-go/go.sum b/plugins/wasm-go/go.sum
index e726b100a5..f396d4d7d9 100644
--- a/plugins/wasm-go/go.sum
+++ b/plugins/wasm-go/go.sum
@@ -4,6 +4,12 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
+github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a h1:luYRvxLTE1xYxrXYj7nmjd1U0HHh8pUPiKfdZ0MhCGE=
+github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
+github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240318034951-d5306e367c43 h1:dCw7F/9ciw4NZN7w68wQRaygZ2zGOWMTIEoRvP1tlWs=
+github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240318034951-d5306e367c43/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
+github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc h1:t2AT8zb6N/59Y78lyRWedVoVWHNRSCBh0oWCC+bluTQ=
+github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
@@ -14,6 +20,8 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
+github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
diff --git a/plugins/wasm-go/pkg/wrapper/redis_wrapper.go b/plugins/wasm-go/pkg/wrapper/redis_wrapper.go
index 10aa9020bd..c619c3e191 100644
--- a/plugins/wasm-go/pkg/wrapper/redis_wrapper.go
+++ b/plugins/wasm-go/pkg/wrapper/redis_wrapper.go
@@ -235,10 +235,11 @@ func (c RedisClusterClient[C]) Set(key string, value interface{}, callback Redis
func (c RedisClusterClient[C]) SetEx(key string, value interface{}, ttl int, callback RedisResponseCallback) error {
args := make([]interface{}, 0)
- args = append(args, "setex")
+ args = append(args, "set")
args = append(args, key)
- args = append(args, ttl)
args = append(args, value)
+ args = append(args, "ex")
+ args = append(args, ttl)
return RedisCall(c.cluster, respString(args), callback)
}
diff --git a/test/e2e/conformance/tests/go-wasm-ai-cache.go b/test/e2e/conformance/tests/go-wasm-ai-cache.go
new file mode 100644
index 0000000000..30ac248916
--- /dev/null
+++ b/test/e2e/conformance/tests/go-wasm-ai-cache.go
@@ -0,0 +1,76 @@
+// 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 tests
+
+import (
+ "testing"
+
+ "github.com/alibaba/higress/test/e2e/conformance/utils/http"
+ "github.com/alibaba/higress/test/e2e/conformance/utils/suite"
+)
+
+func init() {
+ Register(WasmPluginsAiCache)
+}
+
+var WasmPluginsAiCache = suite.ConformanceTest{
+ ShortName: "WasmPluginAiCache",
+ Description: "The Ingress in the higress-conformance-infra namespace test the ai-cache WASM plugin.",
+ Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature},
+ Manifests: []string{"tests/go-wasm-ai-cache.yaml"},
+ Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
+ testcases := []http.Assertion{
+ {
+ Meta: http.AssertionMeta{
+ TestCaseName: "case 1: basic",
+ TargetBackend: "infra-backend-v1",
+ TargetNamespace: "higress-conformance-infra",
+ },
+ Request: http.AssertionRequest{
+ ActualRequest: http.Request{
+ Host: "dashscope.aliyuncs.com",
+ Path: "/v1/chat/completions",
+ Method: "POST",
+ ContentType: http.ContentTypeApplicationJson,
+ Body: []byte(`{
+ "model": "qwen-long",
+ "messages": [{"role":"user","content":"hi"}]}`),
+ },
+ ExpectedRequest: &http.ExpectedRequest{
+ Request: http.Request{
+ Host: "dashscope.aliyuncs.com",
+ Path: "/compatible-mode/v1/chat/completions",
+ Method: "POST",
+ ContentType: http.ContentTypeApplicationJson,
+ Body: []byte(`{
+ "model": "qwen-long",
+ "messages": [{"role":"user","content":"hi"}]}`),
+ },
+ },
+ },
+ Response: http.AssertionResponse{
+ ExpectedResponse: http.Response{
+ StatusCode: 200,
+ },
+ },
+ },
+ }
+ t.Run("WasmPlugins ai-cache", func(t *testing.T) {
+ for _, testcase := range testcases {
+ http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase)
+ }
+ })
+ },
+}
diff --git a/test/e2e/conformance/tests/go-wasm-ai-cache.yaml b/test/e2e/conformance/tests/go-wasm-ai-cache.yaml
new file mode 100644
index 0000000000..c7d6b0c46b
--- /dev/null
+++ b/test/e2e/conformance/tests/go-wasm-ai-cache.yaml
@@ -0,0 +1,103 @@
+# 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.
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ annotations:
+ name: wasmplugin-ai-cache-openai
+ namespace: higress-conformance-infra
+spec:
+ ingressClassName: higress
+ rules:
+ - host: "dashscope.aliyuncs.com"
+ http:
+ paths:
+ - pathType: Prefix
+ path: "/"
+ backend:
+ service:
+ name: infra-backend-v1
+ port:
+ number: 8080
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ annotations:
+ name: wasmplugin-ai-cache-qwen
+ namespace: higress-conformance-infra
+spec:
+ ingressClassName: higress
+ rules:
+ - host: "qwen.ai.com"
+ http:
+ paths:
+ - pathType: Prefix
+ path: "/"
+ backend:
+ service:
+ name: infra-backend-v1
+ port:
+ number: 8080
+---
+apiVersion: extensions.higress.io/v1alpha1
+kind: WasmPlugin
+metadata:
+ name: ai-cache
+ namespace: higress-system
+spec:
+ priority: 400
+ matchRules:
+ - config:
+ embedding:
+ type: "dashscope"
+ serviceName: "qwen"
+ apiKey: "{{secret.qwenApiKey}}"
+ timeout: 12000
+ vector:
+ type: "dashvector"
+ serviceName: "dashvector"
+ collectionID: "{{secret.collectionID}}"
+ serviceDomain: "{{secret.serviceDomain}}"
+ apiKey: "{{secret.apiKey}}"
+ timeout: 12000
+ cache:
+
+ ingress:
+ - higress-conformance-infra/wasmplugin-ai-cache-openai
+ - higress-conformance-infra/wasmplugin-ai-cache-qwen
+ # url: file:///opt/plugins/wasm-go/extensions/ai-cache/plugin.wasm
+ url: oci://registry.cn-shanghai.aliyuncs.com/suchunsv/higress_ai:1.18
+---
+apiVersion: extensions.higress.io/v1alpha1
+kind: WasmPlugin
+metadata:
+ name: ai-proxy
+ namespace: higress-system
+spec:
+ priority: 201
+ matchRules:
+ - config:
+ provider:
+ type: "qwen"
+ qwenEnableCompatible: true
+ apiTokens:
+ - "{{secret.qwenApiKey}}"
+ timeout: 1200000
+ modelMapping:
+ "*": "qwen-long"
+ ingress:
+ - higress-conformance-infra/wasmplugin-ai-cache-openai
+ - higress-conformance-infra/wasmplugin-ai-cache-qwen
+ url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/ai-proxy:1.0.0
From 7c33ebf6ea5ad58ed6d42652fdf3f3a50c4b07ae Mon Sep 17 00:00:00 2001
From: tmsnan <877625878@qq.com>
Date: Mon, 28 Oct 2024 16:26:29 +0800
Subject: [PATCH 33/64] bugfix: remove config check and fix memory leak in
traffic-tag (#1435)
---
.../wasm-go/extensions/traffic-tag/content.go | 4 ----
.../wasm-go/extensions/traffic-tag/main.go | 21 ++++++++++++++----
.../wasm-go/extensions/traffic-tag/parse.go | 3 ---
.../wasm-go/extensions/traffic-tag/utils.go | 22 -------------------
.../wasm-go/extensions/traffic-tag/weight.go | 9 ++------
5 files changed, 19 insertions(+), 40 deletions(-)
diff --git a/plugins/wasm-go/extensions/traffic-tag/content.go b/plugins/wasm-go/extensions/traffic-tag/content.go
index 07750f3314..4d62c02c99 100644
--- a/plugins/wasm-go/extensions/traffic-tag/content.go
+++ b/plugins/wasm-go/extensions/traffic-tag/content.go
@@ -26,10 +26,6 @@ import (
)
func onContentRequestHeaders(conditionGroups []ConditionGroup, log wrapper.Log) bool {
- if len(conditionGroups) == 0 {
- return false
- }
-
for _, cg := range conditionGroups {
if matchCondition(&cg, log) {
addTagHeader(cg.HeaderName, cg.HeaderValue, log)
diff --git a/plugins/wasm-go/extensions/traffic-tag/main.go b/plugins/wasm-go/extensions/traffic-tag/main.go
index 6f69f23d64..0e58f95084 100644
--- a/plugins/wasm-go/extensions/traffic-tag/main.go
+++ b/plugins/wasm-go/extensions/traffic-tag/main.go
@@ -16,6 +16,7 @@ package main
import (
"math/rand"
+ "strings"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
@@ -60,7 +61,6 @@ type TrafficTagConfig struct {
WeightGroups []WeightGroup `json:"weightGroups,omitempty"`
DefaultTagKey string `json:"defaultTagKey,omitempty"`
DefaultTagVal string `json:"defaultTagVal,omitempty"`
- randGen *rand.Rand
}
type ConditionGroup struct {
@@ -93,8 +93,11 @@ func main() {
}
func parseConfig(json gjson.Result, config *TrafficTagConfig, log wrapper.Log) error {
- if err := jsonValidate(json, log); err != nil {
- return err
+
+ jsonStr := strings.TrimSpace(json.Raw)
+ if jsonStr == "{}" || jsonStr == "" {
+ log.Error("plugin config is empty")
+ return nil
}
err := parseContentConfig(json, config, log)
@@ -106,7 +109,17 @@ func parseConfig(json gjson.Result, config *TrafficTagConfig, log wrapper.Log) e
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config TrafficTagConfig, log wrapper.Log) types.Action {
- if add := (onContentRequestHeaders(config.ConditionGroups, log) || onWeightRequestHeaders(config.WeightGroups, config.randGen, log)); !add {
+
+ add := false
+ if len(config.ConditionGroups) != 0 {
+ add = add || onContentRequestHeaders(config.ConditionGroups, log)
+ }
+
+ if !add && len(config.WeightGroups) != 0 {
+ add = add || onWeightRequestHeaders(config.WeightGroups, rand.Uint64(), log)
+ }
+
+ if !add {
setDefaultTag(config.DefaultTagKey, config.DefaultTagVal, log)
}
diff --git a/plugins/wasm-go/extensions/traffic-tag/parse.go b/plugins/wasm-go/extensions/traffic-tag/parse.go
index 7878c3ba9b..c41890c1a7 100644
--- a/plugins/wasm-go/extensions/traffic-tag/parse.go
+++ b/plugins/wasm-go/extensions/traffic-tag/parse.go
@@ -17,10 +17,8 @@ package main
import (
"errors"
"fmt"
- "math/rand"
"strconv"
"strings"
- "time"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
regexp "github.com/wasilibs/go-re2"
@@ -85,7 +83,6 @@ func parseWeightConfig(json gjson.Result, config *TrafficTagConfig, log wrapper.
var parseError error
var accumulatedWeight int64
config.WeightGroups = []WeightGroup{}
- config.randGen = rand.New(rand.NewSource(time.Now().UnixNano()))
// parse default tag key and value
if k, v := json.Get(DefaultTagKey), json.Get(DefaultTagVal); k.Exists() && v.Exists() {
diff --git a/plugins/wasm-go/extensions/traffic-tag/utils.go b/plugins/wasm-go/extensions/traffic-tag/utils.go
index 870150dcc0..dd33efae84 100644
--- a/plugins/wasm-go/extensions/traffic-tag/utils.go
+++ b/plugins/wasm-go/extensions/traffic-tag/utils.go
@@ -15,14 +15,12 @@
package main
import (
- "errors"
"fmt"
"net/url"
"strings"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
- "github.com/tidwall/gjson"
)
func setDefaultTag(k string, v string, log wrapper.Log) {
@@ -75,23 +73,3 @@ func addTagHeader(key string, value string, log wrapper.Log) {
}
log.Infof("ADD HEADER: %s, value: %s", key, value)
}
-
-func jsonValidate(json gjson.Result, log wrapper.Log) error {
- if !json.Exists() {
- log.Error("plugin config is missing in JSON")
- return errors.New("plugin config is missing in JSON")
- }
-
- jsonStr := strings.TrimSpace(json.Raw)
- if jsonStr == "{}" || jsonStr == "" {
- log.Error("plugin config is empty")
- return errors.New("plugin config is empty")
- }
-
- if !gjson.Valid(json.Raw) {
- log.Error("plugin config is invalid JSON")
- return errors.New("plugin config is invalid JSON")
- }
-
- return nil
-}
diff --git a/plugins/wasm-go/extensions/traffic-tag/weight.go b/plugins/wasm-go/extensions/traffic-tag/weight.go
index d6ea49f5d9..f825f1ec7c 100644
--- a/plugins/wasm-go/extensions/traffic-tag/weight.go
+++ b/plugins/wasm-go/extensions/traffic-tag/weight.go
@@ -15,16 +15,11 @@
package main
import (
- "math/rand"
-
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
)
-func onWeightRequestHeaders(weightGroups []WeightGroup, randGen *rand.Rand, log wrapper.Log) bool {
- if len(weightGroups) == 0 {
- return false
- }
- randomValue := randGen.Uint64() % TotalWeight
+func onWeightRequestHeaders(weightGroups []WeightGroup, randomNum uint64, log wrapper.Log) bool {
+ randomValue := randomNum % TotalWeight
log.Debugf("random value for weighted headers : %d", randomValue)
// CDF
for _, wg := range weightGroups {
From 0b42836e8538385df5fb54c3bfb70d3d8af08973 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=BE=84=E6=BD=AD?=
Date: Mon, 28 Oct 2024 18:56:09 +0800
Subject: [PATCH 34/64] Update CODEOWNERS
---
CODEOWNERS | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CODEOWNERS b/CODEOWNERS
index fab96a6ec9..3d36c596c3 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -3,7 +3,7 @@
/istio @SpecialYang @johnlanni
/pkg @SpecialYang @johnlanni @CH3CHO
/plugins @johnlanni @WeixinX @CH3CHO
-/plugins/wasm-rust @007gzs
+/plugins/wasm-rust @007gzs @jizhuozhi
/registry @NameHaibinZhang @2456868764 @johnlanni
/test @Xunzhuo @2456868764 @CH3CHO
/tools @johnlanni @Xunzhuo @2456868764
From b1550e91abbd51cd2d46200131f4227389ccf403 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=BE=84=E6=BD=AD?=
Date: Mon, 28 Oct 2024 19:14:57 +0800
Subject: [PATCH 35/64] rel: Release v2.0.2 (#1438)
---
VERSION | 2 +-
helm/core/Chart.yaml | 4 ++--
helm/higress/Chart.lock | 6 +++---
helm/higress/Chart.yaml | 6 +++---
4 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/VERSION b/VERSION
index 0ac852dded..f3b15f3f8e 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-v2.0.1
+v2.0.2
diff --git a/helm/core/Chart.yaml b/helm/core/Chart.yaml
index 1dca35d253..04d287a95f 100644
--- a/helm/core/Chart.yaml
+++ b/helm/core/Chart.yaml
@@ -1,5 +1,5 @@
apiVersion: v2
-appVersion: 2.0.1
+appVersion: 2.0.2
description: Helm chart for deploying higress gateways
icon: https://higress.io/img/higress_logo_small.png
home: http://higress.io/
@@ -10,4 +10,4 @@ name: higress-core
sources:
- http://github.com/alibaba/higress
type: application
-version: 2.0.1
+version: 2.0.2
diff --git a/helm/higress/Chart.lock b/helm/higress/Chart.lock
index e2397ea62a..9c71c9f1c8 100644
--- a/helm/higress/Chart.lock
+++ b/helm/higress/Chart.lock
@@ -1,9 +1,9 @@
dependencies:
- name: higress-core
repository: file://../core
- version: 2.0.1
+ version: 2.0.2
- name: higress-console
repository: https://higress.io/helm-charts/
version: 1.4.4
-digest: sha256:6e4d77c31c834a404a728ec5a8379dd5df27a7e9b998a08e6524dc6534b07c1d
-generated: "2024-10-09T20:07:21.857942+08:00"
+digest: sha256:a424449caa01a71798c7fec9769ef97be7658354c028a3cede4790e4b6094532
+generated: "2024-10-28T18:50:27.528097+08:00"
diff --git a/helm/higress/Chart.yaml b/helm/higress/Chart.yaml
index 7d683863ba..51fe7f7de2 100644
--- a/helm/higress/Chart.yaml
+++ b/helm/higress/Chart.yaml
@@ -1,5 +1,5 @@
apiVersion: v2
-appVersion: 2.0.1
+appVersion: 2.0.2
description: Helm chart for deploying Higress gateways
icon: https://higress.io/img/higress_logo_small.png
home: http://higress.io/
@@ -12,9 +12,9 @@ sources:
dependencies:
- name: higress-core
repository: "file://../core"
- version: 2.0.1
+ version: 2.0.2
- name: higress-console
repository: "https://higress.io/helm-charts/"
version: 1.4.4
type: application
-version: 2.0.1
+version: 2.0.2
From 7c2d2b285572fdc0f242ea90ad1a57047f9c2506 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=BE=84=E6=BD=AD?=
Date: Tue, 29 Oct 2024 09:01:06 +0800
Subject: [PATCH 36/64] fix destinationrule merge logic (#1439)
---
pkg/ingress/config/ingress_config.go | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/pkg/ingress/config/ingress_config.go b/pkg/ingress/config/ingress_config.go
index e41181676a..9c2fc94b9b 100644
--- a/pkg/ingress/config/ingress_config.go
+++ b/pkg/ingress/config/ingress_config.go
@@ -719,9 +719,9 @@ func (m *IngressConfig) convertDestinationRule(configs []common.WrapperConfig) [
} else if dr.DestinationRule.TrafficPolicy != nil {
portTrafficPolicy := destinationRuleWrapper.DestinationRule.TrafficPolicy.PortLevelSettings[0]
portUpdated := false
- for _, portTrafficPolicy := range dr.DestinationRule.TrafficPolicy.PortLevelSettings {
- if portTrafficPolicy.Port.Number == portTrafficPolicy.Port.Number {
- portTrafficPolicy.Tls = portTrafficPolicy.Tls
+ for _, policy := range dr.DestinationRule.TrafficPolicy.PortLevelSettings {
+ if policy.Port.Number == portTrafficPolicy.Port.Number {
+ policy.Tls = portTrafficPolicy.Tls
portUpdated = true
break
}
From 93c1e5c2bbbb7cf632b118cc1f0526490695990a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=BA=AA=E5=8D=93=E5=BF=97?=
Date: Tue, 29 Oct 2024 17:06:03 +0800
Subject: [PATCH 37/64] feat: lazy formatted log (#1441)
---
plugins/wasm-rust/src/log.rs | 93 ++++++++++++++++++++++++++++++++----
1 file changed, 84 insertions(+), 9 deletions(-)
diff --git a/plugins/wasm-rust/src/log.rs b/plugins/wasm-rust/src/log.rs
index 4656b8669b..e469558673 100644
--- a/plugins/wasm-rust/src/log.rs
+++ b/plugins/wasm-rust/src/log.rs
@@ -12,7 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-use proxy_wasm::hostcalls;
+use proxy_wasm::{hostcalls, types};
+use std::fmt::Arguments;
pub enum LogLevel {
Trace,
@@ -34,14 +35,7 @@ impl Log {
fn log(&self, level: LogLevel, msg: &str) {
let msg = format!("[{}] {}", self.plugin_name, msg);
- let level = match level {
- LogLevel::Trace => proxy_wasm::types::LogLevel::Trace,
- LogLevel::Debug => proxy_wasm::types::LogLevel::Debug,
- LogLevel::Info => proxy_wasm::types::LogLevel::Info,
- LogLevel::Warn => proxy_wasm::types::LogLevel::Warn,
- LogLevel::Error => proxy_wasm::types::LogLevel::Error,
- LogLevel::Critical => proxy_wasm::types::LogLevel::Critical,
- };
+ let level = types::LogLevel::from(level);
hostcalls::log(level, msg.as_str()).unwrap();
}
@@ -68,4 +62,85 @@ impl Log {
pub fn critical(&self, msg: &str) {
self.log(LogLevel::Critical, msg)
}
+
+ fn logf(&self, level: LogLevel, format_args: Arguments) {
+ let level = types::LogLevel::from(level);
+ if let Ok(log_level) = hostcalls::get_log_level() {
+ if (level as i32) < (log_level as i32) {
+ return;
+ }
+ hostcalls::log(
+ level,
+ format!("[{}] {}", self.plugin_name, format_args).as_str(),
+ )
+ .unwrap();
+ }
+ }
+
+ /// ```
+ /// use higress_wasm_rust::log::Log;
+ /// let log = Log::new("foobar".into_string());
+ /// log.tracef(format_args!("Hello, {}!","World"));
+ /// ```
+ pub fn tracef(&self, format_args: Arguments) {
+ self.logf(LogLevel::Trace, format_args)
+ }
+
+ /// ```
+ /// use higress_wasm_rust::log::Log;
+ /// let log = Log::new("foobar".into_string());
+ /// log.debugf(format_args!("Hello, {}!","World"));
+ /// ```
+ pub fn debugf(&self, format_args: Arguments) {
+ self.logf(LogLevel::Debug, format_args)
+ }
+
+ /// ```
+ /// use higress_wasm_rust::log::Log;
+ /// let log = Log::new("foobar".into_string());
+ /// log.infof(format_args!("Hello, {}!","World"));
+ /// ```
+ pub fn infof(&self, format_args: Arguments) {
+ self.logf(LogLevel::Info, format_args)
+ }
+
+ /// ```
+ /// use higress_wasm_rust::log::Log;
+ /// let log = Log::new("foobar".into_string());
+ /// log.warnf(format_args!("Hello, {}!","World"));
+ /// ```
+ pub fn warnf(&self, format_args: Arguments) {
+ self.logf(LogLevel::Warn, format_args)
+ }
+
+ /// ```
+ /// use higress_wasm_rust::log::Log;
+ /// let log = Log::new("foobar".into_string());
+ /// log.errorf(format_args!("Hello, {}!","World"));
+ /// ```
+ pub fn errorf(&self, format_args: Arguments) {
+ self.logf(LogLevel::Error, format_args)
+ }
+
+ /// ```
+ /// use higress_wasm_rust::log::Log;
+ /// let log = Log::new("foobar".into_string());
+ /// log.criticalf(format_args!("Hello, {}!","World"));
+ /// ```
+ pub fn criticalf(&self, format_args: Arguments) {
+ self.logf(LogLevel::Critical, format_args)
+ }
+}
+
+impl From for types::LogLevel {
+ fn from(value: LogLevel) -> Self {
+ match value {
+ LogLevel::Trace => types::LogLevel::Trace,
+ LogLevel::Debug => types::LogLevel::Debug,
+ LogLevel::Info => types::LogLevel::Info,
+ LogLevel::Warn => types::LogLevel::Warn,
+ LogLevel::Error => types::LogLevel::Error,
+ LogLevel::Critical => types::LogLevel::Critical,
+ }
+ }
}
From 2219a1789814a0cdbfd879861f6d91268b60ace2 Mon Sep 17 00:00:00 2001
From: 007gzs <007gzs@gmail.com>
Date: Tue, 29 Oct 2024 19:35:02 +0800
Subject: [PATCH 38/64] [feat] Support redis call with wasm-rust (#1417)
---
plugins/wasm-rust/Cargo.lock | 158 +++-
plugins/wasm-rust/Cargo.toml | 1 +
.../wasm-rust/example/sse-timing/src/lib.rs | 4 +-
.../extensions/ai-data-masking/Cargo.lock | 162 +++-
.../extensions/ai-data-masking/src/lib.rs | 32 +-
.../wasm-rust/extensions/demo-wasm/Cargo.lock | 160 +++-
.../wasm-rust/extensions/demo-wasm/Cargo.toml | 4 +-
.../wasm-rust/extensions/demo-wasm/src/lib.rs | 128 +++-
.../extensions/request-block/Cargo.lock | 158 +++-
.../extensions/request-block/src/lib.rs | 8 +-
.../wasm-rust/extensions/say-hello/Cargo.lock | 158 +++-
plugins/wasm-rust/src/cluster_wrapper.rs | 9 +
plugins/wasm-rust/src/event_stream.rs | 49 +-
plugins/wasm-rust/src/internal.rs | 23 +-
plugins/wasm-rust/src/lib.rs | 1 +
plugins/wasm-rust/src/plugin_wrapper.rs | 63 +-
plugins/wasm-rust/src/redis_wrapper.rs | 724 ++++++++++++++++++
plugins/wasm-rust/src/request_wrapper.rs | 2 +
18 files changed, 1694 insertions(+), 150 deletions(-)
create mode 100644 plugins/wasm-rust/src/redis_wrapper.rs
diff --git a/plugins/wasm-rust/Cargo.lock b/plugins/wasm-rust/Cargo.lock
index 9cf5140c77..63272f9a61 100644
--- a/plugins/wasm-rust/Cargo.lock
+++ b/plugins/wasm-rust/Cargo.lock
@@ -20,11 +20,23 @@ version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
+[[package]]
+name = "arc-swap"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
+
+[[package]]
+name = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
[[package]]
name = "bytes"
-version = "1.7.2"
+version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
+checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
[[package]]
name = "cfg-if"
@@ -32,6 +44,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
[[package]]
name = "downcast-rs"
version = "1.2.1"
@@ -44,6 +66,15 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
[[package]]
name = "getrandom"
version = "0.2.15"
@@ -74,6 +105,7 @@ dependencies = [
"lazy_static",
"multimap",
"proxy-wasm",
+ "redis",
"serde",
"serde_json",
"uuid",
@@ -90,6 +122,16 @@ dependencies = [
"itoa",
]
+[[package]]
+name = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
[[package]]
name = "itoa"
version = "1.0.11"
@@ -104,9 +146,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
-version = "0.2.159"
+version = "0.2.161"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
+checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
[[package]]
name = "log"
@@ -129,12 +171,46 @@ dependencies = [
"serde",
]
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
[[package]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
[[package]]
name = "proc-macro2"
version = "1.0.88"
@@ -147,7 +223,7 @@ dependencies = [
[[package]]
name = "proxy-wasm"
version = "0.2.2"
-source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#6735737fad486c8a7cc324241f58df4a160e7887"
+source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#8c902102091698bec953471c850bdf9799bc344d"
dependencies = [
"downcast-rs",
"hashbrown",
@@ -163,6 +239,21 @@ dependencies = [
"proc-macro2",
]
+[[package]]
+name = "redis"
+version = "0.27.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81cccf17a692ce51b86564334614d72dcae1def0fd5ecebc9f02956da74352b5"
+dependencies = [
+ "arc-swap",
+ "combine",
+ "itoa",
+ "num-bigint",
+ "percent-encoding",
+ "ryu",
+ "url",
+]
+
[[package]]
name = "ryu"
version = "1.0.18"
@@ -171,18 +262,18 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
-version = "1.0.210"
+version = "1.0.211"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
+checksum = "1ac55e59090389fb9f0dd9e0f3c09615afed1d19094284d0b200441f13550793"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.210"
+version = "1.0.211"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
+checksum = "54be4f245ce16bc58d57ef2716271d0d4519e0f6defa147f6e081005bcb278ff"
dependencies = [
"proc-macro2",
"quote",
@@ -191,9 +282,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.128"
+version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
+checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
dependencies = [
"itoa",
"memchr",
@@ -203,21 +294,62 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.79"
+version = "2.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
+checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
+[[package]]
+name = "tinyvec"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893"
+
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
+[[package]]
+name = "unicode-normalization"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "url"
+version = "2.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
[[package]]
name = "uuid"
version = "1.11.0"
diff --git a/plugins/wasm-rust/Cargo.toml b/plugins/wasm-rust/Cargo.toml
index e4fc274311..f58f2c8069 100644
--- a/plugins/wasm-rust/Cargo.toml
+++ b/plugins/wasm-rust/Cargo.toml
@@ -14,3 +14,4 @@ multimap = "0"
http = "1"
lazy_static = "1"
downcast-rs="1"
+redis={version = "0", default-features = false}
diff --git a/plugins/wasm-rust/example/sse-timing/src/lib.rs b/plugins/wasm-rust/example/sse-timing/src/lib.rs
index ab1fa4f6c4..fbff5daf9e 100644
--- a/plugins/wasm-rust/example/sse-timing/src/lib.rs
+++ b/plugins/wasm-rust/example/sse-timing/src/lib.rs
@@ -60,10 +60,10 @@ impl SseTimingRoot {
impl Context for SseTimingRoot {}
impl RootContext for SseTimingRoot {
- fn on_configure(&mut self, _plugin_configuration_size: usize) -> bool {
+ fn on_configure(&mut self, plugin_configuration_size: usize) -> bool {
on_configure(
self,
- _plugin_configuration_size,
+ plugin_configuration_size,
self.rule_matcher.borrow_mut().deref_mut(),
&self.log,
)
diff --git a/plugins/wasm-rust/extensions/ai-data-masking/Cargo.lock b/plugins/wasm-rust/extensions/ai-data-masking/Cargo.lock
index 914d5b9689..e635ae454f 100644
--- a/plugins/wasm-rust/extensions/ai-data-masking/Cargo.lock
+++ b/plugins/wasm-rust/extensions/ai-data-masking/Cargo.lock
@@ -46,6 +46,18 @@ version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
+[[package]]
+name = "arc-swap"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
+
+[[package]]
+name = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
[[package]]
name = "bit-set"
version = "0.5.3"
@@ -84,15 +96,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
-version = "1.7.2"
+version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
+checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
[[package]]
name = "cc"
-version = "1.1.30"
+version = "1.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945"
+checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f"
dependencies = [
"shlex",
]
@@ -112,6 +124,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
[[package]]
name = "cpufeatures"
version = "0.2.14"
@@ -230,6 +252,15 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
[[package]]
name = "fxhash"
version = "0.2.1"
@@ -295,6 +326,7 @@ dependencies = [
"lazy_static",
"multimap",
"proxy-wasm",
+ "redis",
"serde",
"serde_json",
"uuid",
@@ -317,6 +349,16 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+[[package]]
+name = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
[[package]]
name = "itoa"
version = "1.0.11"
@@ -359,9 +401,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
-version = "0.2.159"
+version = "0.2.161"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
+checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
[[package]]
name = "log"
@@ -390,6 +432,34 @@ dependencies = [
"serde",
]
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
[[package]]
name = "once_cell"
version = "1.20.2"
@@ -418,6 +488,12 @@ dependencies = [
"pkg-config",
]
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
[[package]]
name = "pest"
version = "2.7.14"
@@ -519,7 +595,7 @@ dependencies = [
[[package]]
name = "proxy-wasm"
version = "0.2.2"
-source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#6735737fad486c8a7cc324241f58df4a160e7887"
+source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#8c902102091698bec953471c850bdf9799bc344d"
dependencies = [
"downcast-rs",
"hashbrown",
@@ -550,6 +626,21 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+[[package]]
+name = "redis"
+version = "0.27.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81cccf17a692ce51b86564334614d72dcae1def0fd5ecebc9f02956da74352b5"
+dependencies = [
+ "arc-swap",
+ "combine",
+ "itoa",
+ "num-bigint",
+ "percent-encoding",
+ "ryu",
+ "url",
+]
+
[[package]]
name = "regex"
version = "1.11.0"
@@ -630,18 +721,18 @@ dependencies = [
[[package]]
name = "serde"
-version = "1.0.210"
+version = "1.0.211"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
+checksum = "1ac55e59090389fb9f0dd9e0f3c09615afed1d19094284d0b200441f13550793"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.210"
+version = "1.0.211"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
+checksum = "54be4f245ce16bc58d57ef2716271d0d4519e0f6defa147f6e081005bcb278ff"
dependencies = [
"proc-macro2",
"quote",
@@ -650,9 +741,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.128"
+version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
+checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
dependencies = [
"itoa",
"memchr",
@@ -697,9 +788,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
-version = "2.0.79"
+version = "2.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
+checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021"
dependencies = [
"proc-macro2",
"quote",
@@ -726,6 +817,21 @@ dependencies = [
"syn",
]
+[[package]]
+name = "tinyvec"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
[[package]]
name = "typenum"
version = "1.17.0"
@@ -738,12 +844,38 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
+[[package]]
+name = "unicode-bidi"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893"
+
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
+[[package]]
+name = "unicode-normalization"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "url"
+version = "2.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
[[package]]
name = "uuid"
version = "1.11.0"
diff --git a/plugins/wasm-rust/extensions/ai-data-masking/src/lib.rs b/plugins/wasm-rust/extensions/ai-data-masking/src/lib.rs
index 89c0da5219..2f2999d673 100644
--- a/plugins/wasm-rust/extensions/ai-data-masking/src/lib.rs
+++ b/plugins/wasm-rust/extensions/ai-data-masking/src/lib.rs
@@ -97,13 +97,13 @@ where
D: Deserializer<'de>,
{
let value: Value = Deserialize::deserialize(deserializer)?;
- if let Some(_type) = value.as_str() {
- if _type == "replace" {
+ if let Some(t) = value.as_str() {
+ if t == "replace" {
Ok(Type::Replace)
- } else if _type == "hash" {
+ } else if t == "hash" {
Ok(Type::Hash)
} else {
- Err(Error::custom(format!("regexp error value {}", _type)))
+ Err(Error::custom(format!("regexp error value {}", t)))
}
} else {
Err(Error::custom("type error not string".to_string()))
@@ -238,8 +238,8 @@ impl DenyWord {
let mut deny_word = DenyWord::empty();
for word in words {
- let _w = word.into();
- let w = _w.trim();
+ let word_s = word.into();
+ let w = word_s.trim();
if w.is_empty() {
continue;
}
@@ -314,12 +314,12 @@ impl System {
fn grok_to_pattern(&self, pattern: &str) -> (String, bool) {
let mut ok = true;
let mut ret = pattern.to_string();
- for _c in self.grok_regex.captures_iter(pattern) {
- if _c.is_err() {
+ for capture in self.grok_regex.captures_iter(pattern) {
+ if capture.is_err() {
ok = false;
continue;
}
- let c = _c.unwrap();
+ let c = capture.unwrap();
if let (Some(full), Some(name)) = (c.get(0), c.name("pattern")) {
if let Some(p) = self.grok_patterns.get(name.as_str()) {
if let Some(alias) = c.name("alias") {
@@ -347,16 +347,16 @@ impl AiDataMaskingRoot {
impl Context for AiDataMaskingRoot {}
impl RootContext for AiDataMaskingRoot {
- fn on_configure(&mut self, _plugin_configuration_size: usize) -> bool {
+ fn on_configure(&mut self, plugin_configuration_size: usize) -> bool {
on_configure(
self,
- _plugin_configuration_size,
+ plugin_configuration_size,
self.rule_matcher.borrow_mut().deref_mut(),
&self.log,
)
}
- fn create_http_context(&self, _context_id: u32) -> Option> {
- self.create_http_context_use_wrapper(_context_id)
+ fn create_http_context(&self, context_id: u32) -> Option> {
+ self.create_http_context_use_wrapper(context_id)
}
fn get_type(&self) -> Option {
Some(ContextType::HttpContext)
@@ -491,11 +491,11 @@ impl AiDataMasking {
if rule.type_ == Type::Replace && !rule.restore {
msg = rule.regex.replace_all(&msg, &rule.value).to_string();
} else {
- for _m in rule.regex.find_iter(&msg) {
- if _m.is_err() {
+ for mc in rule.regex.find_iter(&msg) {
+ if mc.is_err() {
continue;
}
- let m = _m.unwrap();
+ let m = mc.unwrap();
let from_word = m.as_str();
let to_word = match rule.type_ {
diff --git a/plugins/wasm-rust/extensions/demo-wasm/Cargo.lock b/plugins/wasm-rust/extensions/demo-wasm/Cargo.lock
index c1e197caf6..1c6c135617 100644
--- a/plugins/wasm-rust/extensions/demo-wasm/Cargo.lock
+++ b/plugins/wasm-rust/extensions/demo-wasm/Cargo.lock
@@ -20,11 +20,23 @@ version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
+[[package]]
+name = "arc-swap"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
+
+[[package]]
+name = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
[[package]]
name = "bytes"
-version = "1.7.2"
+version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
+checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
[[package]]
name = "cfg-if"
@@ -32,6 +44,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
[[package]]
name = "demo-wasm"
version = "0.1.0"
@@ -40,7 +62,9 @@ dependencies = [
"http",
"multimap",
"proxy-wasm",
+ "redis",
"serde",
+ "serde_json",
]
[[package]]
@@ -55,6 +79,15 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
[[package]]
name = "getrandom"
version = "0.2.15"
@@ -85,6 +118,7 @@ dependencies = [
"lazy_static",
"multimap",
"proxy-wasm",
+ "redis",
"serde",
"serde_json",
"uuid",
@@ -101,6 +135,16 @@ dependencies = [
"itoa",
]
+[[package]]
+name = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
[[package]]
name = "itoa"
version = "1.0.11"
@@ -115,9 +159,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
-version = "0.2.159"
+version = "0.2.161"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
+checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
[[package]]
name = "log"
@@ -140,12 +184,46 @@ dependencies = [
"serde",
]
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
[[package]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
[[package]]
name = "proc-macro2"
version = "1.0.88"
@@ -158,7 +236,7 @@ dependencies = [
[[package]]
name = "proxy-wasm"
version = "0.2.2"
-source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#6735737fad486c8a7cc324241f58df4a160e7887"
+source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#8c902102091698bec953471c850bdf9799bc344d"
dependencies = [
"downcast-rs",
"hashbrown",
@@ -174,6 +252,21 @@ dependencies = [
"proc-macro2",
]
+[[package]]
+name = "redis"
+version = "0.27.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81cccf17a692ce51b86564334614d72dcae1def0fd5ecebc9f02956da74352b5"
+dependencies = [
+ "arc-swap",
+ "combine",
+ "itoa",
+ "num-bigint",
+ "percent-encoding",
+ "ryu",
+ "url",
+]
+
[[package]]
name = "ryu"
version = "1.0.18"
@@ -182,18 +275,18 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
-version = "1.0.210"
+version = "1.0.211"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
+checksum = "1ac55e59090389fb9f0dd9e0f3c09615afed1d19094284d0b200441f13550793"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.210"
+version = "1.0.211"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
+checksum = "54be4f245ce16bc58d57ef2716271d0d4519e0f6defa147f6e081005bcb278ff"
dependencies = [
"proc-macro2",
"quote",
@@ -202,9 +295,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.128"
+version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
+checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
dependencies = [
"itoa",
"memchr",
@@ -214,21 +307,62 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.79"
+version = "2.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
+checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
+[[package]]
+name = "tinyvec"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893"
+
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
+[[package]]
+name = "unicode-normalization"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "url"
+version = "2.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
[[package]]
name = "uuid"
version = "1.11.0"
diff --git a/plugins/wasm-rust/extensions/demo-wasm/Cargo.toml b/plugins/wasm-rust/extensions/demo-wasm/Cargo.toml
index a517c2b531..3f1e3fd864 100644
--- a/plugins/wasm-rust/extensions/demo-wasm/Cargo.toml
+++ b/plugins/wasm-rust/extensions/demo-wasm/Cargo.toml
@@ -11,5 +11,7 @@ crate-type = ["cdylib"]
higress-wasm-rust = { path = "../../", version = "0.1.0" }
proxy-wasm = { git="https://github.com/higress-group/proxy-wasm-rust-sdk", branch="main", version="0.2.2" }
serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
multimap = "*"
-http = "*"
\ No newline at end of file
+http = "*"
+redis={version = "0", default-features = false}
\ No newline at end of file
diff --git a/plugins/wasm-rust/extensions/demo-wasm/src/lib.rs b/plugins/wasm-rust/extensions/demo-wasm/src/lib.rs
index 62dfa055d0..ed432e69ad 100644
--- a/plugins/wasm-rust/extensions/demo-wasm/src/lib.rs
+++ b/plugins/wasm-rust/extensions/demo-wasm/src/lib.rs
@@ -1,13 +1,16 @@
-use higress_wasm_rust::cluster_wrapper::DnsCluster;
+use higress_wasm_rust::cluster_wrapper::{DnsCluster, StaticIpCluster};
use higress_wasm_rust::log::Log;
use higress_wasm_rust::plugin_wrapper::{HttpContextWrapper, RootContextWrapper};
+use higress_wasm_rust::redis_wrapper::{RedisClient, RedisClientBuilder, RedisClientConfig};
use higress_wasm_rust::rule_matcher::{on_configure, RuleMatcher, SharedRuleMatcher};
use http::Method;
use multimap::MultiMap;
use proxy_wasm::traits::{Context, HttpContext, RootContext};
use proxy_wasm::types::{Bytes, ContextType, DataAction, HeaderAction, LogLevel};
+use redis::Value;
use serde::Deserialize;
+use serde_json::json;
use std::cell::RefCell;
use std::ops::DerefMut;
use std::rc::{Rc, Weak};
@@ -24,6 +27,8 @@ const PLUGIN_NAME: &str = "demo-wasm";
struct DemoWasmConfig {
// 配置文件结构体
test: String,
+ #[serde(default)]
+ password: Option,
}
fn format_body(body: Option>) -> String {
@@ -40,6 +45,8 @@ struct DemoWasm {
log: Log,
config: Option>,
weak: Weak>>>,
+ redis_client: Option,
+ cid: i64,
}
impl Context for DemoWasm {}
@@ -58,7 +65,7 @@ impl HttpContextWrapper for DemoWasm {
fn on_config(&mut self, config: Rc) {
// 获取config
self.log.info(&format!("on_config {}", config.test));
- self.config = Some(config.clone())
+ self.config = Some(config.clone());
}
fn on_http_request_complete_headers(
&mut self,
@@ -67,6 +74,55 @@ impl HttpContextWrapper for DemoWasm {
// 请求header获取完成回调
self.log
.info(&format!("on_http_request_complete_headers {:?}", headers));
+ if let Some(config) = &self.config {
+ let _redis_client = RedisClientBuilder::new(
+ &StaticIpCluster::new("redis", 80, ""),
+ Duration::from_secs(5),
+ )
+ .password(config.password.as_ref())
+ .build();
+
+ let redis_client = RedisClient::new(
+ RedisClientConfig::new(
+ &StaticIpCluster::new("redis", 80, ""),
+ Duration::from_secs(5),
+ )
+ .password(config.password.as_ref()),
+ );
+
+ if let Some(self_rc) = self.weak.upgrade() {
+ let init_res = redis_client.init();
+ self.log.info(&format!("redis init {:?}", init_res));
+ if init_res.is_ok() {
+ let incr_res = redis_client.incr(
+ "connect",
+ Box::new(move |res, status, token_id| {
+ self_rc.borrow().log().info(&format!(
+ "redis incr finish value_res:{:?}, status: {}, token_id: {}",
+ res, status, token_id
+ ));
+ if let Some(this) = self_rc.borrow_mut().downcast_mut::() {
+ if let Ok(Value::Int(value)) = res {
+ this.cid = *value;
+ }
+ }
+ self_rc.borrow().resume_http_request();
+ }),
+ );
+ match incr_res {
+ Ok(s) => {
+ self.log.info(&format!("redis incr ok {}", s));
+ return HeaderAction::StopAllIterationAndBuffer;
+ }
+ Err(e) => self.log.info(&format!("redis incr error {:?}", e)),
+ };
+ }
+ self.redis_client = Some(redis_client);
+ } else {
+ self.log.error("self_weak upgrade error");
+ }
+ }
+
HeaderAction::Continue
}
fn on_http_response_complete_headers(
@@ -76,6 +132,38 @@ impl HttpContextWrapper for DemoWasm {
// 返回header获取完成回调
self.log
.info(&format!("on_http_response_complete_headers {:?}", headers));
+ self.set_http_response_header("Content-Length", None);
+ let self_rc = match self.weak.upgrade() {
+ Some(rc) => rc.clone(),
+ None => {
+ self.log.error("self_weak upgrade error");
+ return HeaderAction::Continue;
+ }
+ };
+ if let Some(redis_client) = &self.redis_client {
+ match redis_client.get(
+ "connect",
+ Box::new(move |res, status, token_id| {
+ if let Some(this) = self_rc.borrow().downcast_ref::() {
+ this.log.info(&format!(
+ "redis get connect value_res:{:?}, status: {}, token_id: {}",
+ res, status, token_id
+ ));
+ this.resume_http_response();
+ } else {
+ self_rc.borrow().resume_http_response();
+ }
+ }),
+ ) {
+ Ok(o) => {
+ self.log.info(&format!("redis get ok {}", o));
+ return HeaderAction::StopIteration;
+ }
+ Err(e) => {
+ self.log.info(&format!("redis get fail {:?}", e));
+ }
+ }
+ }
HeaderAction::Continue
}
fn cache_request_body(&self) -> bool {
@@ -92,6 +180,16 @@ impl HttpContextWrapper for DemoWasm {
"on_http_request_complete_body {}",
String::from_utf8(req_body.clone()).unwrap_or("".to_string())
));
+ DataAction::Continue
+ }
+ fn on_http_response_complete_body(&mut self, res_body: &Bytes) -> DataAction {
+ // 返回body获取完成回调
+ let res_body_string = String::from_utf8(res_body.clone()).unwrap_or("".to_string());
+ self.log.info(&format!(
+ "on_http_response_complete_body {}",
+ res_body_string
+ ));
+
let cluster = DnsCluster::new("httpbin", "httpbin.org", 80);
let self_rc = match self.weak.upgrade() {
@@ -101,6 +199,7 @@ impl HttpContextWrapper for DemoWasm {
return DataAction::Continue;
}
};
+
let http_call_res = self.http_call(
&cluster,
&Method::POST,
@@ -108,34 +207,29 @@ impl HttpContextWrapper for DemoWasm {
MultiMap::new(),
Some("test_body".as_bytes()),
Box::new(move |status_code, headers, body| {
- if let Some(this) = self_rc.borrow().downcast_ref::() {
+ if let Some(this) = self_rc.borrow_mut().downcast_mut::() {
+ let body_string = format_body(body);
this.log.info(&format!(
"test_callback status_code:{}, headers: {:?}, body: {}",
status_code,
headers,
- format_body(body)
+ body_string
));
- this.resume_http_request();
+ let data = json!({"redis_cid": this.cid, "http_call_body": body_string, "res_body": res_body_string});
+ this.replace_http_response_body(data.to_string().as_bytes());
+ this.resume_http_response();
} else {
- self_rc.borrow().resume_http_request();
+ self_rc.borrow().resume_http_response();
}
}),
Duration::from_secs(5),
);
match http_call_res {
- Ok(_) => DataAction::StopIterationAndBuffer,
+ Ok(_) => return DataAction::StopIterationAndBuffer,
Err(e) => {
self.log.info(&format!("http_call fail {:?}", e));
- DataAction::Continue
}
}
- }
- fn on_http_response_complete_body(&mut self, res_body: &Bytes) -> DataAction {
- // 返回body获取完成回调
- self.log.info(&format!(
- "on_http_response_complete_body {}",
- String::from_utf8(res_body.clone()).unwrap_or("".to_string())
- ));
DataAction::Continue
}
}
@@ -147,6 +241,7 @@ impl DemoWasmRoot {
fn new() -> Self {
let log = Log::new(PLUGIN_NAME.to_string());
log.info("DemoWasmRoot::new");
+
DemoWasmRoot {
log,
rule_matcher: Rc::new(RefCell::new(RuleMatcher::default())),
@@ -171,6 +266,7 @@ impl RootContext for DemoWasmRoot {
"DemoWasmRoot::create_http_context({})",
context_id
));
+
self.create_http_context_use_wrapper(context_id)
}
fn get_type(&self) -> Option {
@@ -191,6 +287,8 @@ impl RootContextWrapper for DemoWasmRoot {
config: None,
log: Log::new(PLUGIN_NAME.to_string()),
weak: Weak::default(),
+ redis_client: None,
+ cid: -1,
}))
}
}
diff --git a/plugins/wasm-rust/extensions/request-block/Cargo.lock b/plugins/wasm-rust/extensions/request-block/Cargo.lock
index 243fe87a49..36c0b812b7 100644
--- a/plugins/wasm-rust/extensions/request-block/Cargo.lock
+++ b/plugins/wasm-rust/extensions/request-block/Cargo.lock
@@ -29,11 +29,23 @@ version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
+[[package]]
+name = "arc-swap"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
+
+[[package]]
+name = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
[[package]]
name = "bytes"
-version = "1.7.2"
+version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
+checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
[[package]]
name = "cfg-if"
@@ -41,6 +53,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
[[package]]
name = "downcast-rs"
version = "1.2.1"
@@ -53,6 +75,15 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
[[package]]
name = "getrandom"
version = "0.2.15"
@@ -83,6 +114,7 @@ dependencies = [
"lazy_static",
"multimap",
"proxy-wasm",
+ "redis",
"serde",
"serde_json",
"uuid",
@@ -99,6 +131,16 @@ dependencies = [
"itoa",
]
+[[package]]
+name = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
[[package]]
name = "itoa"
version = "1.0.11"
@@ -113,9 +155,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
-version = "0.2.159"
+version = "0.2.161"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
+checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
[[package]]
name = "log"
@@ -138,12 +180,46 @@ dependencies = [
"serde",
]
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
[[package]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
[[package]]
name = "proc-macro2"
version = "1.0.88"
@@ -156,7 +232,7 @@ dependencies = [
[[package]]
name = "proxy-wasm"
version = "0.2.2"
-source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#6735737fad486c8a7cc324241f58df4a160e7887"
+source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#8c902102091698bec953471c850bdf9799bc344d"
dependencies = [
"downcast-rs",
"hashbrown",
@@ -172,6 +248,21 @@ dependencies = [
"proc-macro2",
]
+[[package]]
+name = "redis"
+version = "0.27.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81cccf17a692ce51b86564334614d72dcae1def0fd5ecebc9f02956da74352b5"
+dependencies = [
+ "arc-swap",
+ "combine",
+ "itoa",
+ "num-bigint",
+ "percent-encoding",
+ "ryu",
+ "url",
+]
+
[[package]]
name = "regex"
version = "1.11.0"
@@ -221,18 +312,18 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
-version = "1.0.210"
+version = "1.0.211"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
+checksum = "1ac55e59090389fb9f0dd9e0f3c09615afed1d19094284d0b200441f13550793"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.210"
+version = "1.0.211"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
+checksum = "54be4f245ce16bc58d57ef2716271d0d4519e0f6defa147f6e081005bcb278ff"
dependencies = [
"proc-macro2",
"quote",
@@ -241,9 +332,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.128"
+version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
+checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
dependencies = [
"itoa",
"memchr",
@@ -253,21 +344,62 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.79"
+version = "2.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
+checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
+[[package]]
+name = "tinyvec"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893"
+
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
+[[package]]
+name = "unicode-normalization"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "url"
+version = "2.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
[[package]]
name = "uuid"
version = "1.11.0"
diff --git a/plugins/wasm-rust/extensions/request-block/src/lib.rs b/plugins/wasm-rust/extensions/request-block/src/lib.rs
index d120acb902..822601cd76 100644
--- a/plugins/wasm-rust/extensions/request-block/src/lib.rs
+++ b/plugins/wasm-rust/extensions/request-block/src/lib.rs
@@ -104,17 +104,17 @@ impl RquestBlockRoot {
impl Context for RquestBlockRoot {}
impl RootContext for RquestBlockRoot {
- fn on_configure(&mut self, _plugin_configuration_size: usize) -> bool {
+ fn on_configure(&mut self, plugin_configuration_size: usize) -> bool {
let ret = on_configure(
self,
- _plugin_configuration_size,
+ plugin_configuration_size,
self.rule_matcher.borrow_mut().deref_mut(),
&self.log,
);
ret
}
- fn create_http_context(&self, _context_id: u32) -> Option> {
- self.create_http_context_use_wrapper(_context_id)
+ fn create_http_context(&self, context_id: u32) -> Option> {
+ self.create_http_context_use_wrapper(context_id)
}
fn get_type(&self) -> Option {
Some(ContextType::HttpContext)
diff --git a/plugins/wasm-rust/extensions/say-hello/Cargo.lock b/plugins/wasm-rust/extensions/say-hello/Cargo.lock
index dc98fd709a..758fc617e3 100644
--- a/plugins/wasm-rust/extensions/say-hello/Cargo.lock
+++ b/plugins/wasm-rust/extensions/say-hello/Cargo.lock
@@ -20,11 +20,23 @@ version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
+[[package]]
+name = "arc-swap"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
+
+[[package]]
+name = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
[[package]]
name = "bytes"
-version = "1.7.2"
+version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
+checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
[[package]]
name = "cfg-if"
@@ -32,6 +44,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
[[package]]
name = "downcast-rs"
version = "1.2.1"
@@ -44,6 +66,15 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
[[package]]
name = "getrandom"
version = "0.2.15"
@@ -74,6 +105,7 @@ dependencies = [
"lazy_static",
"multimap",
"proxy-wasm",
+ "redis",
"serde",
"serde_json",
"uuid",
@@ -90,6 +122,16 @@ dependencies = [
"itoa",
]
+[[package]]
+name = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
[[package]]
name = "itoa"
version = "1.0.11"
@@ -104,9 +146,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
-version = "0.2.159"
+version = "0.2.161"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
+checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
[[package]]
name = "log"
@@ -129,12 +171,46 @@ dependencies = [
"serde",
]
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
[[package]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
[[package]]
name = "proc-macro2"
version = "1.0.88"
@@ -147,7 +223,7 @@ dependencies = [
[[package]]
name = "proxy-wasm"
version = "0.2.2"
-source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#6735737fad486c8a7cc324241f58df4a160e7887"
+source = "git+https://github.com/higress-group/proxy-wasm-rust-sdk?branch=main#8c902102091698bec953471c850bdf9799bc344d"
dependencies = [
"downcast-rs",
"hashbrown",
@@ -163,6 +239,21 @@ dependencies = [
"proc-macro2",
]
+[[package]]
+name = "redis"
+version = "0.27.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81cccf17a692ce51b86564334614d72dcae1def0fd5ecebc9f02956da74352b5"
+dependencies = [
+ "arc-swap",
+ "combine",
+ "itoa",
+ "num-bigint",
+ "percent-encoding",
+ "ryu",
+ "url",
+]
+
[[package]]
name = "ryu"
version = "1.0.18"
@@ -181,18 +272,18 @@ dependencies = [
[[package]]
name = "serde"
-version = "1.0.210"
+version = "1.0.211"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
+checksum = "1ac55e59090389fb9f0dd9e0f3c09615afed1d19094284d0b200441f13550793"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.210"
+version = "1.0.211"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
+checksum = "54be4f245ce16bc58d57ef2716271d0d4519e0f6defa147f6e081005bcb278ff"
dependencies = [
"proc-macro2",
"quote",
@@ -201,9 +292,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.128"
+version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
+checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
dependencies = [
"itoa",
"memchr",
@@ -213,21 +304,62 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.79"
+version = "2.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
+checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
+[[package]]
+name = "tinyvec"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893"
+
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
+[[package]]
+name = "unicode-normalization"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "url"
+version = "2.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
[[package]]
name = "uuid"
version = "1.11.0"
diff --git a/plugins/wasm-rust/src/cluster_wrapper.rs b/plugins/wasm-rust/src/cluster_wrapper.rs
index 2891293fb3..8d3e204458 100644
--- a/plugins/wasm-rust/src/cluster_wrapper.rs
+++ b/plugins/wasm-rust/src/cluster_wrapper.rs
@@ -4,10 +4,12 @@ pub trait Cluster {
fn cluster_name(&self) -> String;
fn host_name(&self) -> String;
}
+
#[derive(Debug, Clone)]
pub struct RouteCluster {
host: String,
}
+
impl RouteCluster {
pub fn new(host: &str) -> Self {
RouteCluster {
@@ -15,6 +17,7 @@ impl RouteCluster {
}
}
}
+
impl Cluster for RouteCluster {
fn cluster_name(&self) -> String {
if let Some(res) = get_property(vec!["cluster_name"]) {
@@ -111,6 +114,7 @@ impl NacosCluster {
}
}
}
+
impl Cluster for NacosCluster {
fn cluster_name(&self) -> String {
let group = if self.group.is_empty() {
@@ -154,6 +158,7 @@ impl StaticIpCluster {
}
}
}
+
impl Cluster for StaticIpCluster {
fn cluster_name(&self) -> String {
format!("outbound|{}||{}.static", self.port, self.service_name)
@@ -184,6 +189,7 @@ impl DnsCluster {
}
}
}
+
impl Cluster for DnsCluster {
fn cluster_name(&self) -> String {
format!("outbound|{}||{}.dns", self.port, self.service_name)
@@ -212,6 +218,7 @@ impl ConsulCluster {
}
}
}
+
impl Cluster for ConsulCluster {
fn cluster_name(&self) -> String {
format!(
@@ -245,10 +252,12 @@ impl FQDNCluster {
}
}
}
+
impl Cluster for FQDNCluster {
fn cluster_name(&self) -> String {
format!("outbound|{}||{}", self.port, self.fqdn)
}
+
fn host_name(&self) -> String {
if self.host.is_empty() {
self.fqdn.clone()
diff --git a/plugins/wasm-rust/src/event_stream.rs b/plugins/wasm-rust/src/event_stream.rs
index f28846317a..97715dcac1 100644
--- a/plugins/wasm-rust/src/event_stream.rs
+++ b/plugins/wasm-rust/src/event_stream.rs
@@ -28,29 +28,15 @@
/// However, in the rules of event and field, there is an ambiguous grammar in the judgment of eol,
/// and it will bring ambiguity (whether the field ends). In order to eliminate this ambiguity,
/// we believe that CRLF as CR+LF belongs to event and field respectively.
+
+#[derive(Default)]
pub struct EventStream {
buffer: Vec,
processed_offset: usize,
}
-impl EventStream {
- pub fn new() -> Self {
- EventStream {
- buffer: Vec::new(),
- processed_offset: 0,
- }
- }
-
- /// Update the event stream by adding new data to the buffer and resetting processed offset if needed.
- pub fn update(&mut self, data: Vec) {
- if self.processed_offset > 0 {
- self.buffer.drain(0..self.processed_offset);
- self.processed_offset = 0;
- }
-
- self.buffer.extend(data);
- }
-
+impl Iterator for EventStream {
+ type Item = Vec;
/// Get the next event from the event stream. Return the event data if available, otherwise return None.
/// Next will consume all the data in the current buffer. However, if there is a valid event at the end of the buffer,
/// it will return the event directly even if the data after the next `update` could be considered part of the same event
@@ -71,7 +57,8 @@ impl EventStream {
/// }
/// }
/// ```
- pub fn next(&mut self) -> Option> {
+ ///
+ fn next(&mut self) -> Option {
let mut i = self.processed_offset;
while i < self.buffer.len() {
@@ -86,6 +73,18 @@ impl EventStream {
None
}
+}
+
+impl EventStream {
+ /// Update the event stream by adding new data to the buffer and resetting processed offset if needed.
+ pub fn update(&mut self, data: Vec) {
+ if self.processed_offset > 0 {
+ self.buffer.drain(0..self.processed_offset);
+ self.processed_offset = 0;
+ }
+
+ self.buffer.extend(data);
+ }
/// Flush the event stream and return any remaining unprocessed event data. Return None if there is none.
pub fn flush(&mut self) -> Option> {
@@ -138,7 +137,7 @@ mod tests {
#[test]
fn test_crlf_events() {
- let mut parser = EventStream::new();
+ let mut parser = EventStream::default();
parser.update(b"event1\n\nevent2\n\n".to_vec());
assert_eq!(parser.next(), Some(b"event1".to_vec()));
@@ -147,7 +146,7 @@ mod tests {
#[test]
fn test_lf_events() {
- let mut parser = EventStream::new();
+ let mut parser = EventStream::default();
parser.update(b"event3\n\r\nevent4\r\n".to_vec());
assert_eq!(parser.next(), Some(b"event3".to_vec()));
@@ -156,7 +155,7 @@ mod tests {
#[test]
fn test_partial_event() {
- let mut parser = EventStream::new();
+ let mut parser = EventStream::default();
parser.update(b"partial_event1".to_vec());
assert_eq!(parser.next(), None);
@@ -167,7 +166,7 @@ mod tests {
#[test]
fn test_mixed_eol_events() {
- let mut parser = EventStream::new();
+ let mut parser = EventStream::default();
parser.update(b"event5\r\nevent6\r\n\r\nevent7\r\n".to_vec());
assert_eq!(parser.next(), Some(b"event5".to_vec()));
@@ -177,7 +176,7 @@ mod tests {
#[test]
fn test_mixed2_eol_events() {
- let mut parser = EventStream::new();
+ let mut parser = EventStream::default();
parser.update(b"event5\r\nevent6\r\n".to_vec());
assert_eq!(parser.next(), Some(b"event5".to_vec()));
assert_eq!(parser.next(), Some(b"event6".to_vec()));
@@ -188,7 +187,7 @@ mod tests {
#[test]
fn test_no_event() {
- let mut parser = EventStream::new();
+ let mut parser = EventStream::default();
parser.update(b"no_eol_in_this_string".to_vec());
assert_eq!(parser.next(), None);
diff --git a/plugins/wasm-rust/src/internal.rs b/plugins/wasm-rust/src/internal.rs
index 4116c5d798..5a562419d4 100644
--- a/plugins/wasm-rust/src/internal.rs
+++ b/plugins/wasm-rust/src/internal.rs
@@ -14,7 +14,7 @@
#![allow(dead_code)]
-use proxy_wasm::hostcalls;
+use proxy_wasm::hostcalls::{self, RedisCallbackFn};
use proxy_wasm::types::{BufferType, Bytes, MapType, Status};
use std::time::{Duration, SystemTime};
@@ -381,3 +381,24 @@ pub(crate) fn send_http_response(
) {
hostcalls::send_http_response(status_code, headers, body).unwrap()
}
+
+pub(crate) fn redis_init(
+ upstream: &str,
+ username: Option<&[u8]>,
+ password: Option<&[u8]>,
+ timeout: Duration,
+) -> Result<(), Status> {
+ hostcalls::redis_init(upstream, username, password, timeout)
+}
+
+pub(crate) fn dispatch_redis_call(
+ upstream: &str,
+ query: &[u8],
+ call_fn: Box,
+) -> Result {
+ hostcalls::dispatch_redis_call(upstream, query, call_fn)
+}
+
+pub(crate) fn get_redis_call_response(start: usize, max_size: usize) -> Option {
+ hostcalls::get_buffer(BufferType::RedisCallResponse, start, max_size).unwrap()
+}
diff --git a/plugins/wasm-rust/src/lib.rs b/plugins/wasm-rust/src/lib.rs
index f6a92be89b..2e6993e040 100644
--- a/plugins/wasm-rust/src/lib.rs
+++ b/plugins/wasm-rust/src/lib.rs
@@ -18,5 +18,6 @@ pub mod event_stream;
mod internal;
pub mod log;
pub mod plugin_wrapper;
+pub mod redis_wrapper;
pub mod request_wrapper;
pub mod rule_matcher;
diff --git a/plugins/wasm-rust/src/plugin_wrapper.rs b/plugins/wasm-rust/src/plugin_wrapper.rs
index abe188a99f..832cdbce5c 100644
--- a/plugins/wasm-rust/src/plugin_wrapper.rs
+++ b/plugins/wasm-rust/src/plugin_wrapper.rs
@@ -30,6 +30,7 @@ use serde::de::DeserializeOwned;
lazy_static! {
static ref LOG: Log = Log::new("plugin_wrapper".to_string());
}
+
thread_local! {
static HTTP_CALLBACK_DISPATCHER: HttpCallbackDispatcher = HttpCallbackDispatcher::new();
}
@@ -49,7 +50,9 @@ where
None => None,
}
}
+
fn rule_matcher(&self) -> &SharedRuleMatcher;
+
fn create_http_context_wrapper(
&self,
_context_id: u32,
@@ -63,20 +66,24 @@ pub type HttpCallbackFn = dyn FnOnce(u16, &MultiMap, Option>>,
}
+
impl Default for HttpCallbackDispatcher {
fn default() -> Self {
Self::new()
}
}
+
impl HttpCallbackDispatcher {
pub fn new() -> Self {
HttpCallbackDispatcher {
call_fns: RefCell::new(HashMap::new()),
}
}
+
pub fn set(&self, token_id: u32, arg: Box) {
self.call_fns.borrow_mut().insert(token_id, arg);
}
+
pub fn pop(&self, token_id: u32) -> Option> {
self.call_fns.borrow_mut().remove(&token_id)
}
@@ -91,31 +98,39 @@ where
_self_weak: Weak>>>,
) {
}
+
fn log(&self) -> &Log {
&LOG
}
+
fn on_config(&mut self, _config: Rc) {}
+
fn on_http_request_complete_headers(
&mut self,
_headers: &MultiMap,
) -> HeaderAction {
HeaderAction::Continue
}
+
fn on_http_response_complete_headers(
&mut self,
_headers: &MultiMap,
) -> HeaderAction {
HeaderAction::Continue
}
+
fn cache_request_body(&self) -> bool {
false
}
+
fn cache_response_body(&self) -> bool {
false
}
+
fn on_http_request_complete_body(&mut self, _req_body: &Bytes) -> DataAction {
DataAction::Continue
}
+
fn on_http_response_complete_body(&mut self, _res_body: &Bytes) -> DataAction {
DataAction::Continue
}
@@ -123,6 +138,7 @@ where
fn replace_http_request_body(&mut self, body: &[u8]) {
self.set_http_request_body(0, i32::MAX as usize, body)
}
+
fn replace_http_response_body(&mut self, body: &[u8]) {
self.set_http_response_body(0, i32::MAX as usize, body)
}
@@ -164,8 +180,8 @@ where
if let Ok(token_id) = ret {
HTTP_CALLBACK_DISPATCHER.with(|dispatcher| dispatcher.set(token_id, call_fn));
- self.log().debug(
- &format!(
+ self.log().debugf(
+ format_args!(
"http call start, id: {}, cluster: {}, method: {}, url: {}, body: {:?}, timeout: {:?}",
token_id, cluster.cluster_name(), method.as_str(), raw_url, body, timeout
)
@@ -173,7 +189,8 @@ where
}
ret
} else {
- self.log().critical(&format!("invalid raw_url:{}", raw_url));
+ self.log()
+ .criticalf(format_args!("invalid raw_url:{}", raw_url));
Err(Status::ParseFailure)
}
}
@@ -182,14 +199,13 @@ where
downcast_rs::impl_downcast!(HttpContextWrapper where PluginConfig: Default + DeserializeOwned + Clone);
pub struct PluginHttpWrapper {
- req_headers: MultiMap,
- res_headers: MultiMap,
req_body_len: usize,
res_body_len: usize,
config: Option>,
rule_matcher: SharedRuleMatcher,
http_content: Rc>>>,
}
+
impl PluginHttpWrapper
where
PluginConfig: Default + DeserializeOwned + Clone + 'static,
@@ -203,8 +219,6 @@ where
.borrow_mut()
.init_self_weak(Rc::downgrade(&rc_content));
PluginHttpWrapper {
- req_headers: MultiMap::new(),
- res_headers: MultiMap::new(),
req_body_len: 0,
res_body_len: 0,
config: None,
@@ -212,10 +226,12 @@ where
http_content: rc_content,
}
}
+
fn get_http_call_fn(&mut self, token_id: u32) -> Option> {
HTTP_CALLBACK_DISPATCHER.with(|dispatcher| dispatcher.pop(token_id))
}
}
+
impl Context for PluginHttpWrapper
where
PluginConfig: Default + DeserializeOwned + Clone + 'static,
@@ -240,24 +256,24 @@ where
status_code = code;
normal_response = true;
} else {
- self.http_content
- .borrow()
- .log()
- .error(&format!("failed to parse status: {}", header_value));
+ self.http_content.borrow().log().errorf(format_args!(
+ "failed to parse status: {}",
+ header_value
+ ));
status_code = 500;
}
}
headers.insert(k, header_value);
}
Err(_) => {
- self.http_content.borrow().log().warn(&format!(
+ self.http_content.borrow().log().warnf(format_args!(
"http call response header contains non-ASCII characters header: {}",
k
));
}
}
}
- self.http_content.borrow().log().warn(&format!(
+ self.http_content.borrow().log().debugf(format_args!(
"http call end, id: {}, code: {}, normal: {}, body: {:?}", /* */
token_id, status_code, normal_response, body
));
@@ -277,21 +293,25 @@ where
.borrow_mut()
.on_grpc_call_response(token_id, status_code, response_size)
}
+
fn on_grpc_stream_initial_metadata(&mut self, token_id: u32, num_elements: u32) {
self.http_content
.borrow_mut()
.on_grpc_stream_initial_metadata(token_id, num_elements)
}
+
fn on_grpc_stream_message(&mut self, token_id: u32, message_size: usize) {
self.http_content
.borrow_mut()
.on_grpc_stream_message(token_id, message_size)
}
+
fn on_grpc_stream_trailing_metadata(&mut self, token_id: u32, num_elements: u32) {
self.http_content
.borrow_mut()
.on_grpc_stream_trailing_metadata(token_id, num_elements)
}
+
fn on_grpc_stream_close(&mut self, token_id: u32, status_code: u32) {
self.http_content
.borrow_mut()
@@ -302,6 +322,7 @@ where
self.http_content.borrow_mut().on_done()
}
}
+
impl HttpContext for PluginHttpWrapper
where
PluginConfig: Default + DeserializeOwned + Clone + 'static,
@@ -312,13 +333,15 @@ where
if self.config.is_none() {
return HeaderAction::Continue;
}
+
+ let mut req_headers = MultiMap::new();
for (k, v) in self.get_http_request_headers_bytes() {
match String::from_utf8(v) {
Ok(header_value) => {
- self.req_headers.insert(k, header_value);
+ req_headers.insert(k, header_value);
}
Err(_) => {
- self.http_content.borrow().log().warn(&format!(
+ self.http_content.borrow().log().warnf(format_args!(
"request http header contains non-ASCII characters header: {}",
k
));
@@ -338,7 +361,7 @@ where
}
self.http_content
.borrow_mut()
- .on_http_request_complete_headers(&self.req_headers)
+ .on_http_request_complete_headers(&req_headers)
}
fn on_http_request_body(&mut self, body_size: usize, end_of_stream: bool) -> DataAction {
@@ -383,13 +406,15 @@ where
if self.config.is_none() {
return HeaderAction::Continue;
}
+
+ let mut res_headers = MultiMap::new();
for (k, v) in self.get_http_response_headers_bytes() {
match String::from_utf8(v) {
Ok(header_value) => {
- self.res_headers.insert(k, header_value);
+ res_headers.insert(k, header_value);
}
Err(_) => {
- self.http_content.borrow().log().warn(&format!(
+ self.http_content.borrow().log().warnf(format_args!(
"response http header contains non-ASCII characters header: {}",
k
));
@@ -406,7 +431,7 @@ where
}
self.http_content
.borrow_mut()
- .on_http_response_complete_headers(&self.res_headers)
+ .on_http_response_complete_headers(&res_headers)
}
fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> DataAction {
diff --git a/plugins/wasm-rust/src/redis_wrapper.rs b/plugins/wasm-rust/src/redis_wrapper.rs
new file mode 100644
index 0000000000..50d30a1f5c
--- /dev/null
+++ b/plugins/wasm-rust/src/redis_wrapper.rs
@@ -0,0 +1,724 @@
+use std::{collections::HashMap, time::Duration};
+
+use proxy_wasm::{hostcalls::RedisCallbackFn, types::Status};
+use redis::{Cmd, ToRedisArgs, Value};
+
+use crate::{cluster_wrapper::Cluster, internal};
+
+pub type RedisValueCallbackFn = dyn FnOnce(&Result, usize, u32);
+
+fn gen_callback(call_fn: Box) -> Box {
+ Box::new(move |token_id, status, response_size| {
+ let res = match internal::get_redis_call_response(0, response_size) {
+ Some(data) => match redis::parse_redis_value(&data) {
+ Ok(v) => Ok(v),
+ Err(e) => Err(e.to_string()),
+ },
+ None => Err("response data not found".to_string()),
+ };
+ call_fn(&res, status, token_id);
+ })
+}
+
+pub struct RedisClientBuilder {
+ upstream: String,
+ username: Option,
+ password: Option,
+ timeout: Duration,
+}
+
+impl RedisClientBuilder {
+ pub fn new(cluster: &dyn Cluster, timeout: Duration) -> Self {
+ RedisClientBuilder {
+ upstream: cluster.cluster_name(),
+ username: None,
+ password: None,
+ timeout,
+ }
+ }
+
+ pub fn username>(mut self, username: Option) -> Self {
+ self.username = username.map(|u| u.as_ref().to_string());
+ self
+ }
+
+ pub fn password>(mut self, password: Option) -> Self {
+ self.password = password.map(|p| p.as_ref().to_string());
+ self
+ }
+
+ pub fn build(self) -> RedisClient {
+ RedisClient {
+ upstream: self.upstream,
+ username: self.username,
+ password: self.password,
+ timeout: self.timeout,
+ }
+ }
+}
+
+pub struct RedisClientConfig {
+ upstream: String,
+ username: Option,
+ password: Option,
+ timeout: Duration,
+}
+
+impl RedisClientConfig {
+ pub fn new(cluster: &dyn Cluster, timeout: Duration) -> Self {
+ RedisClientConfig {
+ upstream: cluster.cluster_name(),
+ username: None,
+ password: None,
+ timeout,
+ }
+ }
+
+ pub fn username>(&mut self, username: Option) -> &Self {
+ self.username = username.map(|u| u.as_ref().to_string());
+ self
+ }
+
+ pub fn password