feat: add wasm-rust sdk with say-hello simple extension (#350)

This commit is contained in:
纪卓志
2023-06-19 10:40:53 +08:00
committed by GitHub
parent ea7b581e26
commit 32c2acefda
17 changed files with 1332 additions and 0 deletions

1
.gitignore vendored
View File

@@ -13,4 +13,5 @@ bazel-testlogs
bazel-wasm-cpp
tools/bin/
helm/**/charts/**.tgz
target/
tools/hack/cluster.conf

View File

@@ -0,0 +1 @@
target/

131
plugins/wasm-rust/Cargo.lock generated Normal file
View File

@@ -0,0 +1,131 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "ahash"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "getrandom"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "hashbrown"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
dependencies = [
"ahash",
]
[[package]]
name = "higress-wasm-rust"
version = "0.1.0"
dependencies = [
"proxy-wasm",
"serde",
"serde_json",
"uuid",
]
[[package]]
name = "itoa"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
[[package]]
name = "libc"
version = "0.2.144"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1"
[[package]]
name = "log"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if",
]
[[package]]
name = "once_cell"
version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
[[package]]
name = "proxy-wasm"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "823b744520cd4a54ba7ebacbffe4562e839d6dcd8f89209f96a1ace4f5229cd4"
dependencies = [
"hashbrown",
"log",
]
[[package]]
name = "ryu"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
[[package]]
name = "serde"
version = "1.0.163"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2"
[[package]]
name = "serde_json"
version = "1.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "uuid"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
dependencies = [
"getrandom",
]
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"

View File

@@ -0,0 +1,12 @@
[package]
name = "higress-wasm-rust"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
proxy-wasm = "0.2.1"
serde = "1.0"
serde_json = "1.0"
uuid = { version = "1.3.3", features = ["v4"] }

View File

@@ -0,0 +1,12 @@
FROM rust:1.69 as builder
WORKDIR /workspace
RUN rustup target add wasm32-wasi
ARG PLUGIN_NAME="say-hello"
ARG BUILD_OPTS="--release"
COPY . .
WORKDIR /workspace/extensions/$PLUGIN_NAME
RUN cargo build --target wasm32-wasi $BUILD_OPTS \
&& cp target/wasm32-wasi/release/*.wasm /main.wasm
FROM scratch
COPY --from=builder /main.wasm plugin.wasm

View File

@@ -0,0 +1,16 @@
PLUGIN_NAME ?= say-hello
REGISTRY ?= higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/
BUILD_TIME := $(shell date "+%Y%m%d-%H%M%S")
COMMIT_ID := $(shell git rev-parse --short HEAD 2>/dev/null)
IMAGE_TAG = $(if $(strip $(PLUGIN_VERSION)),${PLUGIN_VERSION},${BUILD_TIME}-${COMMIT_ID})
IMG ?= ${REGISTRY}${PLUGIN_NAME}:${IMAGE_TAG}
.DEFAULT:
build:
DOCKER_BUILDKIT=1 docker build \
--build-arg PLUGIN_NAME=${PLUGIN_NAME} \
-t ${IMG} \
--output extensions/${PLUGIN_NAME} \
.
@echo ""
@echo "output wasm file: extensions/${PLUGIN_NAME}/plugin.wasm"

View File

@@ -0,0 +1,3 @@
## 介绍
此 SDK 用于使用 Rust 语言开发 Higress 的 Wasm 插件。

View File

@@ -0,0 +1,187 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "ahash"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "getrandom"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "hashbrown"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
dependencies = [
"ahash",
]
[[package]]
name = "higress-wasm-rust"
version = "0.1.0"
dependencies = [
"proxy-wasm",
"serde",
"serde_json",
"uuid",
]
[[package]]
name = "itoa"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
[[package]]
name = "libc"
version = "0.2.144"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1"
[[package]]
name = "log"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de"
[[package]]
name = "once_cell"
version = "1.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b"
[[package]]
name = "proc-macro2"
version = "1.0.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b"
dependencies = [
"unicode-ident",
]
[[package]]
name = "proxy-wasm"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "823b744520cd4a54ba7ebacbffe4562e839d6dcd8f89209f96a1ace4f5229cd4"
dependencies = [
"hashbrown",
"log",
]
[[package]]
name = "quote"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ryu"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
[[package]]
name = "say-hello"
version = "0.1.0"
dependencies = [
"higress-wasm-rust",
"proxy-wasm",
"serde",
"serde_json",
]
[[package]]
name = "serde"
version = "1.0.163"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.163"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "syn"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0"
[[package]]
name = "uuid"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
dependencies = [
"getrandom",
]
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"

View File

@@ -0,0 +1,15 @@
[package]
name = "say-hello"
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 = "0.2.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

View File

@@ -0,0 +1,27 @@
# 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/envoy:1.20
hostname: envoy
ports:
- "10000:10000"
volumes:
- ./envoy.yaml:/etc/envoy/envoy.yaml
- ./target/wasm32-wasi/release:/etc/envoy/proxy-wasm-plugins
networks:
- envoymesh
networks:
envoymesh: {}

View File

@@ -0,0 +1,86 @@
# 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:
address:
socket_address:
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
codec_type: AUTO
route_config:
name: local_routes
virtual_hosts:
- name: local_service
domains:
- "*"
routes:
- name: lucy
match:
prefix: "/lucy"
direct_response:
status: 200
- name: index
match:
prefix: "/"
direct_response:
status: 200
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": "Alice",
"_rules_": [
{
"_match_domain_": [
"foo"
],
"name": "Foo"
},
{
"_match_domain_": [
"bar"
],
"name": "Bar"
},
{
"_match_route_": [
"lucy"
],
"name": "Lucy"
}
]
}
vm_config:
runtime: "envoy.wasm.runtime.v8"
code:
local:
filename: "/etc/envoy/proxy-wasm-plugins/say_hello.wasm"
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

View File

@@ -0,0 +1,112 @@
// 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::log::Log;
use higress_wasm_rust::rule_matcher::RuleMatcher;
use proxy_wasm::traits::{Context, HttpContext, RootContext};
use proxy_wasm::types::{Action, ContextType, LogLevel};
use serde::Deserialize;
use serde_json::{from_slice, Value};
use std::cell::RefCell;
use std::rc::Rc;
proxy_wasm::main! {{
proxy_wasm::set_log_level(LogLevel::Trace);
proxy_wasm::set_root_context(|_|Box::new(SayHelloRoot::new()));
}}
struct SayHelloRoot {
log: Log,
rule_matcher: Rc<RefCell<RuleMatcher<SayHelloConfig>>>,
}
struct SayHello {
rule_matcher: Rc<RefCell<RuleMatcher<SayHelloConfig>>>,
}
#[derive(Default, Debug, Deserialize)]
struct SayHelloConfig {
name: String,
}
impl SayHelloRoot {
fn new() -> Self {
SayHelloRoot {
log: Log::new("say_hello".to_string()),
rule_matcher: Rc::new(RefCell::new(RuleMatcher::default())),
}
}
}
impl Context for SayHelloRoot {}
impl RootContext for SayHelloRoot {
fn on_configure(&mut self, _plugin_configuration_size: usize) -> bool {
let config_buffer = match self.get_plugin_configuration() {
None => {
self.log
.error("Error when configuring RootContext, no configuration supplied");
return false;
}
Some(bytes) => bytes,
};
let value = match from_slice::<Value>(config_buffer.as_slice()) {
Err(error) => {
self.log.error(
format!("cannot parse plugin configuration JSON string: {}", error).as_str(),
);
return false;
}
Ok(value) => value,
};
self.rule_matcher
.borrow_mut()
.parse_rule_config(&value)
.is_ok()
}
fn create_http_context(&self, _context_id: u32) -> Option<Box<dyn HttpContext>> {
Some(Box::new(SayHello {
rule_matcher: self.rule_matcher.clone(),
}))
}
fn get_type(&self) -> Option<ContextType> {
Some(ContextType::HttpContext)
}
}
impl Context for SayHello {}
impl HttpContext for SayHello {
fn on_http_request_headers(&mut self, _num_headers: usize, _end_of_stream: bool) -> Action {
let binding = self.rule_matcher.borrow();
let config = match binding.get_match_config() {
None => {
self.send_http_response(200, vec![], Some("Hello, World!".as_bytes()));
return Action::Continue;
}
Some(config) => config,
};
self.send_http_response(
200,
vec![],
Some(format!("Hello, {}!", config.name).as_bytes()),
);
Action::Continue
}
}

View File

@@ -0,0 +1,39 @@
// 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 std::error::Error;
use std::fmt::{Display, Formatter};
#[derive(Debug, Default)]
pub struct WasmRustError {
message: String,
}
impl WasmRustError {
pub const fn new(message: String) -> Self {
WasmRustError { message }
}
}
impl Display for WasmRustError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl Error for WasmRustError {
fn description(&self) -> &str {
&self.message
}
}

View File

@@ -0,0 +1,383 @@
// 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.
#![allow(dead_code)]
use proxy_wasm::hostcalls;
use proxy_wasm::types::{BufferType, Bytes, MapType, Status};
use std::time::{Duration, SystemTime};
pub(crate) fn get_current_time() -> SystemTime {
hostcalls::get_current_time().unwrap()
}
pub(crate) fn get_property(path: Vec<&str>) -> Option<Bytes> {
hostcalls::get_property(path).unwrap()
}
pub(crate) fn set_property(path: Vec<&str>, value: Option<&[u8]>) {
hostcalls::set_property(path, value).unwrap()
}
pub(crate) fn get_shared_data(key: &str) -> (Option<Bytes>, Option<u32>) {
hostcalls::get_shared_data(key).unwrap()
}
pub(crate) fn set_shared_data(
key: &str,
value: Option<&[u8]>,
cas: Option<u32>,
) -> Result<(), Status> {
hostcalls::set_shared_data(key, value, cas)
}
pub(crate) fn register_shared_queue(name: &str) -> u32 {
hostcalls::register_shared_queue(name).unwrap()
}
pub(crate) fn resolve_shared_queue(vm_id: &str, name: &str) -> Option<u32> {
hostcalls::resolve_shared_queue(vm_id, name).unwrap()
}
pub(crate) fn dequeue_shared_queue(queue_id: u32) -> Result<Option<Bytes>, Status> {
hostcalls::dequeue_shared_queue(queue_id)
}
pub(crate) fn enqueue_shared_queue(queue_id: u32, value: Option<&[u8]>) -> Result<(), Status> {
hostcalls::enqueue_shared_queue(queue_id, value)
}
pub(crate) fn dispatch_http_call(
upstream: &str,
headers: Vec<(&str, &str)>,
body: Option<&[u8]>,
trailers: Vec<(&str, &str)>,
timeout: Duration,
) -> Result<u32, Status> {
hostcalls::dispatch_http_call(upstream, headers, body, trailers, timeout)
}
pub(crate) fn get_http_call_response_headers() -> Vec<(String, String)> {
hostcalls::get_map(MapType::HttpCallResponseHeaders).unwrap()
}
pub(crate) fn get_http_call_response_headers_bytes() -> Vec<(String, Bytes)> {
hostcalls::get_map_bytes(MapType::HttpCallResponseHeaders).unwrap()
}
pub(crate) fn get_http_call_response_header(name: &str) -> Option<String> {
hostcalls::get_map_value(MapType::HttpCallResponseHeaders, name).unwrap()
}
pub(crate) fn get_http_call_response_header_bytes(name: &str) -> Option<Bytes> {
hostcalls::get_map_value_bytes(MapType::HttpCallResponseHeaders, name).unwrap()
}
pub(crate) fn get_http_call_response_body(start: usize, max_size: usize) -> Option<Bytes> {
hostcalls::get_buffer(BufferType::HttpCallResponseBody, start, max_size).unwrap()
}
pub(crate) fn get_http_call_response_trailers() -> Vec<(String, String)> {
hostcalls::get_map(MapType::HttpCallResponseTrailers).unwrap()
}
pub(crate) fn get_http_call_response_trailers_bytes() -> Vec<(String, Bytes)> {
hostcalls::get_map_bytes(MapType::HttpCallResponseTrailers).unwrap()
}
pub(crate) fn get_http_call_response_trailer(name: &str) -> Option<String> {
hostcalls::get_map_value(MapType::HttpCallResponseTrailers, name).unwrap()
}
pub(crate) fn get_http_call_response_trailer_bytes(name: &str) -> Option<Bytes> {
hostcalls::get_map_value_bytes(MapType::HttpCallResponseTrailers, name).unwrap()
}
pub(crate) fn dispatch_grpc_call(
upstream_name: &str,
service_name: &str,
method_name: &str,
initial_metadata: Vec<(&str, &[u8])>,
message: Option<&[u8]>,
timeout: Duration,
) -> Result<u32, Status> {
hostcalls::dispatch_grpc_call(
upstream_name,
service_name,
method_name,
initial_metadata,
message,
timeout,
)
}
pub(crate) fn get_grpc_call_response_body(start: usize, max_size: usize) -> Option<Bytes> {
hostcalls::get_buffer(BufferType::GrpcReceiveBuffer, start, max_size).unwrap()
}
pub(crate) fn cancel_grpc_call(token_id: u32) {
hostcalls::cancel_grpc_call(token_id).unwrap()
}
pub(crate) fn open_grpc_stream(
cluster_name: &str,
service_name: &str,
method_name: &str,
initial_metadata: Vec<(&str, &[u8])>,
) -> Result<u32, Status> {
hostcalls::open_grpc_stream(cluster_name, service_name, method_name, initial_metadata)
}
pub(crate) fn get_grpc_stream_initial_metadata() -> Vec<(String, Bytes)> {
hostcalls::get_map_bytes(MapType::GrpcReceiveInitialMetadata).unwrap()
}
pub(crate) fn get_grpc_stream_initial_metadata_value(name: &str) -> Option<Bytes> {
hostcalls::get_map_value_bytes(MapType::GrpcReceiveInitialMetadata, name).unwrap()
}
pub(crate) fn send_grpc_stream_message(token_id: u32, message: Option<&[u8]>, end_stream: bool) {
hostcalls::send_grpc_stream_message(token_id, message, end_stream).unwrap()
}
pub(crate) fn get_grpc_stream_trailing_metadata() -> Vec<(String, Bytes)> {
hostcalls::get_map_bytes(MapType::GrpcReceiveTrailingMetadata).unwrap()
}
pub(crate) fn get_grpc_stream_trailing_metadata_value(name: &str) -> Option<Bytes> {
hostcalls::get_map_value_bytes(MapType::GrpcReceiveTrailingMetadata, name).unwrap()
}
pub(crate) fn cancel_grpc_stream(token_id: u32) {
hostcalls::cancel_grpc_stream(token_id).unwrap()
}
pub(crate) fn close_grpc_stream(token_id: u32) {
hostcalls::close_grpc_stream(token_id).unwrap()
}
pub(crate) fn get_grpc_status() -> (u32, Option<String>) {
hostcalls::get_grpc_status().unwrap()
}
pub(crate) fn call_foreign_function(
function_name: &str,
arguments: Option<&[u8]>,
) -> Result<Option<Bytes>, Status> {
hostcalls::call_foreign_function(function_name, arguments)
}
pub(crate) fn done() {
hostcalls::done().unwrap()
}
pub(crate) fn get_http_request_headers() -> Vec<(String, String)> {
hostcalls::get_map(MapType::HttpRequestHeaders).unwrap()
}
pub(crate) fn get_http_request_headers_bytes() -> Vec<(String, Bytes)> {
hostcalls::get_map_bytes(MapType::HttpRequestHeaders).unwrap()
}
pub(crate) fn set_http_request_headers(headers: Vec<(&str, &str)>) {
hostcalls::set_map(MapType::HttpRequestHeaders, headers).unwrap()
}
pub(crate) fn set_http_request_headers_bytes(headers: Vec<(&str, &[u8])>) {
hostcalls::set_map_bytes(MapType::HttpRequestHeaders, headers).unwrap()
}
pub(crate) fn get_http_request_header(name: &str) -> Option<String> {
hostcalls::get_map_value(MapType::HttpRequestHeaders, name).unwrap()
}
pub(crate) fn get_http_request_header_bytes(name: &str) -> Option<Bytes> {
hostcalls::get_map_value_bytes(MapType::HttpRequestHeaders, name).unwrap()
}
pub(crate) fn set_http_request_header(name: &str, value: Option<&str>) {
hostcalls::set_map_value(MapType::HttpRequestHeaders, name, value).unwrap()
}
pub(crate) fn set_http_request_header_bytes(name: &str, value: Option<&[u8]>) {
hostcalls::set_map_value_bytes(MapType::HttpRequestHeaders, name, value).unwrap()
}
pub(crate) fn add_http_request_header(name: &str, value: &str) {
hostcalls::add_map_value(MapType::HttpRequestHeaders, name, value).unwrap()
}
pub(crate) fn add_http_request_header_bytes(name: &str, value: &[u8]) {
hostcalls::add_map_value_bytes(MapType::HttpRequestHeaders, name, value).unwrap()
}
pub(crate) fn get_http_request_body(start: usize, max_size: usize) -> Option<Bytes> {
hostcalls::get_buffer(BufferType::HttpRequestBody, start, max_size).unwrap()
}
pub(crate) fn set_http_request_body(start: usize, size: usize, value: &[u8]) {
hostcalls::set_buffer(BufferType::HttpRequestBody, start, size, value).unwrap()
}
pub(crate) fn get_http_request_trailers() -> Vec<(String, String)> {
hostcalls::get_map(MapType::HttpRequestTrailers).unwrap()
}
pub(crate) fn get_http_request_trailers_bytes() -> Vec<(String, Bytes)> {
hostcalls::get_map_bytes(MapType::HttpRequestTrailers).unwrap()
}
pub(crate) fn set_http_request_trailers(trailers: Vec<(&str, &str)>) {
hostcalls::set_map(MapType::HttpRequestTrailers, trailers).unwrap()
}
pub(crate) fn set_http_request_trailers_bytes(trailers: Vec<(&str, &[u8])>) {
hostcalls::set_map_bytes(MapType::HttpRequestTrailers, trailers).unwrap()
}
pub(crate) fn get_http_request_trailer(name: &str) -> Option<String> {
hostcalls::get_map_value(MapType::HttpRequestTrailers, name).unwrap()
}
pub(crate) fn get_http_request_trailer_bytes(name: &str) -> Option<Bytes> {
hostcalls::get_map_value_bytes(MapType::HttpRequestTrailers, name).unwrap()
}
pub(crate) fn set_http_request_trailer(name: &str, value: Option<&str>) {
hostcalls::set_map_value(MapType::HttpRequestTrailers, name, value).unwrap()
}
pub(crate) fn set_http_request_trailer_bytes(name: &str, value: Option<&[u8]>) {
hostcalls::set_map_value_bytes(MapType::HttpRequestTrailers, name, value).unwrap()
}
pub(crate) fn add_http_request_trailer(name: &str, value: &str) {
hostcalls::add_map_value(MapType::HttpRequestTrailers, name, value).unwrap()
}
pub(crate) fn add_http_request_trailer_bytes(name: &str, value: &[u8]) {
hostcalls::add_map_value_bytes(MapType::HttpRequestTrailers, name, value).unwrap()
}
pub(crate) fn resume_http_request() {
hostcalls::resume_http_request().unwrap()
}
pub(crate) fn reset_http_request() {
hostcalls::reset_http_request().unwrap()
}
pub(crate) fn get_http_response_headers() -> Vec<(String, String)> {
hostcalls::get_map(MapType::HttpResponseHeaders).unwrap()
}
pub(crate) fn get_http_response_headers_bytes() -> Vec<(String, Bytes)> {
hostcalls::get_map_bytes(MapType::HttpResponseHeaders).unwrap()
}
pub(crate) fn set_http_response_headers(headers: Vec<(&str, &str)>) {
hostcalls::set_map(MapType::HttpResponseHeaders, headers).unwrap()
}
pub(crate) fn set_http_response_headers_bytes(headers: Vec<(&str, &[u8])>) {
hostcalls::set_map_bytes(MapType::HttpResponseHeaders, headers).unwrap()
}
pub(crate) fn get_http_response_header(name: &str) -> Option<String> {
hostcalls::get_map_value(MapType::HttpResponseHeaders, name).unwrap()
}
pub(crate) fn get_http_response_header_bytes(name: &str) -> Option<Bytes> {
hostcalls::get_map_value_bytes(MapType::HttpResponseHeaders, name).unwrap()
}
pub(crate) fn set_http_response_header(name: &str, value: Option<&str>) {
hostcalls::set_map_value(MapType::HttpResponseHeaders, name, value).unwrap()
}
pub(crate) fn set_http_response_header_bytes(name: &str, value: Option<&[u8]>) {
hostcalls::set_map_value_bytes(MapType::HttpResponseHeaders, name, value).unwrap()
}
pub(crate) fn add_http_response_header(name: &str, value: &str) {
hostcalls::add_map_value(MapType::HttpResponseHeaders, name, value).unwrap()
}
pub(crate) fn add_http_response_header_bytes(name: &str, value: &[u8]) {
hostcalls::add_map_value_bytes(MapType::HttpResponseHeaders, name, value).unwrap()
}
pub(crate) fn get_http_response_body(start: usize, max_size: usize) -> Option<Bytes> {
hostcalls::get_buffer(BufferType::HttpResponseBody, start, max_size).unwrap()
}
pub(crate) fn set_http_response_body(start: usize, size: usize, value: &[u8]) {
hostcalls::set_buffer(BufferType::HttpResponseBody, start, size, value).unwrap()
}
pub(crate) fn get_http_response_trailers() -> Vec<(String, String)> {
hostcalls::get_map(MapType::HttpResponseTrailers).unwrap()
}
pub(crate) fn get_http_response_trailers_bytes() -> Vec<(String, Bytes)> {
hostcalls::get_map_bytes(MapType::HttpResponseTrailers).unwrap()
}
pub(crate) fn set_http_response_trailers(trailers: Vec<(&str, &str)>) {
hostcalls::set_map(MapType::HttpResponseTrailers, trailers).unwrap()
}
pub(crate) fn set_http_response_trailers_bytes(trailers: Vec<(&str, &[u8])>) {
hostcalls::set_map_bytes(MapType::HttpResponseTrailers, trailers).unwrap()
}
pub(crate) fn get_http_response_trailer(name: &str) -> Option<String> {
hostcalls::get_map_value(MapType::HttpResponseTrailers, name).unwrap()
}
pub(crate) fn get_http_response_trailer_bytes(name: &str) -> Option<Bytes> {
hostcalls::get_map_value_bytes(MapType::HttpResponseTrailers, name).unwrap()
}
pub(crate) fn set_http_response_trailer(name: &str, value: Option<&str>) {
hostcalls::set_map_value(MapType::HttpResponseTrailers, name, value).unwrap()
}
pub(crate) fn set_http_response_trailer_bytes(name: &str, value: Option<&[u8]>) {
hostcalls::set_map_value_bytes(MapType::HttpResponseTrailers, name, value).unwrap()
}
pub(crate) fn add_http_response_trailer(name: &str, value: &str) {
hostcalls::add_map_value(MapType::HttpResponseTrailers, name, value).unwrap()
}
pub(crate) fn add_http_response_trailer_bytes(name: &str, value: &[u8]) {
hostcalls::add_map_value_bytes(MapType::HttpResponseTrailers, name, value).unwrap()
}
pub(crate) fn resume_http_response() {
hostcalls::resume_http_response().unwrap()
}
pub(crate) fn reset_http_response() {
hostcalls::reset_http_response().unwrap()
}
pub(crate) fn send_http_response(
status_code: u32,
headers: Vec<(&str, &str)>,
body: Option<&[u8]>,
) {
hostcalls::send_http_response(status_code, headers, body).unwrap()
}

View File

@@ -0,0 +1,18 @@
// 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.
pub mod error;
mod internal;
pub mod log;
pub mod rule_matcher;

View File

@@ -0,0 +1,71 @@
// 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 proxy_wasm::hostcalls;
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
Critical,
}
pub struct Log {
plugin_name: String,
}
impl Log {
pub fn new(plugin_name: String) -> Log {
Log { plugin_name }
}
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,
};
hostcalls::log(level, msg.as_str()).unwrap();
}
pub fn trace(&self, msg: &str) {
self.log(LogLevel::Trace, msg)
}
pub fn debug(&self, msg: &str) {
self.log(LogLevel::Debug, msg)
}
pub fn info(&self, msg: &str) {
self.log(LogLevel::Info, msg)
}
pub fn warn(&self, msg: &str) {
self.log(LogLevel::Warn, msg)
}
pub fn error(&self, msg: &str) {
self.log(LogLevel::Error, msg)
}
pub fn critical(&self, msg: &str) {
self.log(LogLevel::Critical, msg)
}
}

View File

@@ -0,0 +1,218 @@
// 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 crate::error::WasmRustError;
use crate::internal::{get_http_request_header, get_property};
use proxy_wasm::hostcalls::log;
use proxy_wasm::types::LogLevel;
use serde_json::{Map, Value};
use std::collections::HashSet;
use serde::de::DeserializeOwned;
enum Category {
Route,
Host,
}
enum MatchType {
Prefix,
Exact,
Suffix,
}
const RULES_KEY: &str = "_rules_";
const MATCH_ROUTE_KEY: &str = "_match_route_";
const MATCH_DOMAIN_KEY: &str = "_match_domain_";
struct HostMatcher {
match_type: MatchType,
host: String,
}
struct RuleConfig<PluginConfig> {
category: Category,
routes: HashSet<String>,
hosts: Vec<HostMatcher>,
config: PluginConfig,
}
#[derive(Default)]
pub struct RuleMatcher<PluginConfig> {
rule_config: Vec<RuleConfig<PluginConfig>>,
global_config: Option<PluginConfig>,
}
impl<PluginConfig> RuleMatcher<PluginConfig>
where
PluginConfig: Default+DeserializeOwned,
{
pub fn parse_rule_config(
&mut self,
config: &Value,
) -> Result<(), WasmRustError> {
let empty_object = Map::new();
let empty_vec = Vec::new();
let object = config.as_object().unwrap_or(&empty_object);
let mut key_count = object.len();
if object.is_empty() {
self.global_config = Some(PluginConfig::default());
return Ok(());
}
let rules = if object.contains_key(RULES_KEY) {
key_count -= 1;
object[RULES_KEY].as_array().unwrap_or(&empty_vec)
} else {
&empty_vec
};
let mut global_config_error: WasmRustError = WasmRustError::default();
if key_count > 0 {
match serde_json::from_value::<PluginConfig>(config.clone()) {
Ok(plugin_config) => {
self.global_config = Some(plugin_config);
}
Err(err) => {
log(
LogLevel::Warn,
format!("parse global config failed, err:{:?}", err).as_str(),
)
.unwrap();
global_config_error = WasmRustError::new(err.to_string());
}
}
}
if rules.is_empty() {
return match self.global_config {
Some(_) => Ok(()),
None => Err(WasmRustError::new(format!(
"parse config failed, no valid rules; global config parse error:{}",
global_config_error
))),
};
}
for rule_json in rules {
let config = match serde_json::from_value::<PluginConfig>(rule_json.clone()) {
Ok(config) => config,
Err(error) => return Err(WasmRustError::new(error.to_string())),
};
let routes = RuleMatcher::<PluginConfig>::parse_route_match_config(rule_json);
let hosts = RuleMatcher::<PluginConfig>::parse_host_match_config(rule_json);
let no_routes = routes.is_empty();
let no_hosts = hosts.is_empty();
if (no_routes && no_hosts) || (!no_routes && !no_hosts) {
return Err(WasmRustError::new("there is only one of '_match_route_' and '_match_domain_' can present in configuration.".to_string()));
}
let category = if no_routes {
Category::Host
} else {
Category::Route
};
self.rule_config.push(RuleConfig {
category,
routes,
hosts,
config,
})
}
Ok(())
}
pub fn get_match_config(&self) -> Option<&PluginConfig> {
let host = get_http_request_header(":authority").unwrap_or_default();
let route_name = get_property(vec!["route_name"]).unwrap_or_default();
for rule in &self.rule_config {
match rule.category {
Category::Host => {
if self.host_match(rule, host.as_str()) {
return Some(&rule.config);
}
}
Category::Route => {
if rule.routes.contains(
String::from_utf8(route_name.to_vec())
.unwrap_or_else(|_| "".to_string())
.as_str(),
) {
return Some(&rule.config);
}
}
}
}
return self.global_config.as_ref();
}
fn parse_route_match_config(config: &Value) -> HashSet<String> {
let empty_vec = Vec::new();
let keys = config[MATCH_ROUTE_KEY].as_array().unwrap_or(&empty_vec);
let mut routes = HashSet::new();
for key in keys {
let route_name = key.as_str().unwrap_or("").to_string();
if !route_name.is_empty() {
routes.insert(route_name);
}
}
routes
}
fn parse_host_match_config(config: &Value) -> Vec<HostMatcher> {
let empty_vec = Vec::new();
let keys = config[MATCH_DOMAIN_KEY].as_array().unwrap_or(&empty_vec);
let mut host_matchers: Vec<HostMatcher> = Vec::new();
for key in keys {
let host = key.as_str().unwrap_or("").to_string();
let mut host_matcher = HostMatcher {
match_type: MatchType::Prefix,
host: String::new(),
};
if let Some(suffix) = host.strip_prefix('*') {
host_matcher.match_type = MatchType::Suffix;
host_matcher.host = suffix.to_string()
} else if let Some(prefix) = host.strip_suffix('*') {
host_matcher.match_type = MatchType::Prefix;
host_matcher.host = prefix.to_string();
} else {
host_matcher.match_type = MatchType::Exact;
host_matcher.host = host
}
host_matchers.push(host_matcher)
}
host_matchers
}
fn host_match(&self, rule: &RuleConfig<PluginConfig>, request_host: &str) -> bool {
for host in &rule.hosts {
let matched = match host.match_type {
MatchType::Prefix => request_host.starts_with(host.host.as_str()),
MatchType::Suffix => request_host.ends_with(host.host.as_str()),
MatchType::Exact => request_host == host.host.as_str(),
};
if matched {
return true;
}
}
false
}
}