From 32c2acefdaf2e83f683894a68a33b6a402807828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BA=AA=E5=8D=93=E5=BF=97?= Date: Mon, 19 Jun 2023 10:40:53 +0800 Subject: [PATCH] feat: add wasm-rust sdk with say-hello simple extension (#350) --- .gitignore | 1 + plugins/wasm-rust/.dockerignore | 1 + plugins/wasm-rust/Cargo.lock | 131 ++++++ plugins/wasm-rust/Cargo.toml | 12 + plugins/wasm-rust/Dockerfile | 12 + plugins/wasm-rust/Makefile | 16 + plugins/wasm-rust/README.md | 3 + .../wasm-rust/extensions/say-hello/Cargo.lock | 187 +++++++++ .../wasm-rust/extensions/say-hello/Cargo.toml | 15 + .../extensions/say-hello/docker-compose.yaml | 27 ++ .../wasm-rust/extensions/say-hello/envoy.yaml | 86 ++++ .../wasm-rust/extensions/say-hello/src/lib.rs | 112 +++++ plugins/wasm-rust/src/error.rs | 39 ++ plugins/wasm-rust/src/internal.rs | 383 ++++++++++++++++++ plugins/wasm-rust/src/lib.rs | 18 + plugins/wasm-rust/src/log.rs | 71 ++++ plugins/wasm-rust/src/rule_matcher.rs | 218 ++++++++++ 17 files changed, 1332 insertions(+) create mode 100644 plugins/wasm-rust/.dockerignore create mode 100644 plugins/wasm-rust/Cargo.lock create mode 100644 plugins/wasm-rust/Cargo.toml create mode 100644 plugins/wasm-rust/Dockerfile create mode 100644 plugins/wasm-rust/Makefile create mode 100644 plugins/wasm-rust/README.md create mode 100644 plugins/wasm-rust/extensions/say-hello/Cargo.lock create mode 100644 plugins/wasm-rust/extensions/say-hello/Cargo.toml create mode 100644 plugins/wasm-rust/extensions/say-hello/docker-compose.yaml create mode 100644 plugins/wasm-rust/extensions/say-hello/envoy.yaml create mode 100644 plugins/wasm-rust/extensions/say-hello/src/lib.rs create mode 100644 plugins/wasm-rust/src/error.rs create mode 100644 plugins/wasm-rust/src/internal.rs create mode 100644 plugins/wasm-rust/src/lib.rs create mode 100644 plugins/wasm-rust/src/log.rs create mode 100644 plugins/wasm-rust/src/rule_matcher.rs diff --git a/.gitignore b/.gitignore index 88083abf6d..94c7c701cd 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ bazel-testlogs bazel-wasm-cpp tools/bin/ helm/**/charts/**.tgz +target/ tools/hack/cluster.conf \ No newline at end of file diff --git a/plugins/wasm-rust/.dockerignore b/plugins/wasm-rust/.dockerignore new file mode 100644 index 0000000000..9f970225ad --- /dev/null +++ b/plugins/wasm-rust/.dockerignore @@ -0,0 +1 @@ +target/ \ No newline at end of file diff --git a/plugins/wasm-rust/Cargo.lock b/plugins/wasm-rust/Cargo.lock new file mode 100644 index 0000000000..293edb2ed5 --- /dev/null +++ b/plugins/wasm-rust/Cargo.lock @@ -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" diff --git a/plugins/wasm-rust/Cargo.toml b/plugins/wasm-rust/Cargo.toml new file mode 100644 index 0000000000..19ddc6d09a --- /dev/null +++ b/plugins/wasm-rust/Cargo.toml @@ -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"] } diff --git a/plugins/wasm-rust/Dockerfile b/plugins/wasm-rust/Dockerfile new file mode 100644 index 0000000000..90d569675e --- /dev/null +++ b/plugins/wasm-rust/Dockerfile @@ -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 \ No newline at end of file diff --git a/plugins/wasm-rust/Makefile b/plugins/wasm-rust/Makefile new file mode 100644 index 0000000000..0ae33e988b --- /dev/null +++ b/plugins/wasm-rust/Makefile @@ -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" \ No newline at end of file diff --git a/plugins/wasm-rust/README.md b/plugins/wasm-rust/README.md new file mode 100644 index 0000000000..225b522b77 --- /dev/null +++ b/plugins/wasm-rust/README.md @@ -0,0 +1,3 @@ +## 介绍 + +此 SDK 用于使用 Rust 语言开发 Higress 的 Wasm 插件。 diff --git a/plugins/wasm-rust/extensions/say-hello/Cargo.lock b/plugins/wasm-rust/extensions/say-hello/Cargo.lock new file mode 100644 index 0000000000..3b0ef8ce79 --- /dev/null +++ b/plugins/wasm-rust/extensions/say-hello/Cargo.lock @@ -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" diff --git a/plugins/wasm-rust/extensions/say-hello/Cargo.toml b/plugins/wasm-rust/extensions/say-hello/Cargo.toml new file mode 100644 index 0000000000..23fffae9ce --- /dev/null +++ b/plugins/wasm-rust/extensions/say-hello/Cargo.toml @@ -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" \ No newline at end of file diff --git a/plugins/wasm-rust/extensions/say-hello/docker-compose.yaml b/plugins/wasm-rust/extensions/say-hello/docker-compose.yaml new file mode 100644 index 0000000000..5d8d0e0047 --- /dev/null +++ b/plugins/wasm-rust/extensions/say-hello/docker-compose.yaml @@ -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: {} \ No newline at end of file diff --git a/plugins/wasm-rust/extensions/say-hello/envoy.yaml b/plugins/wasm-rust/extensions/say-hello/envoy.yaml new file mode 100644 index 0000000000..2d83a3c48b --- /dev/null +++ b/plugins/wasm-rust/extensions/say-hello/envoy.yaml @@ -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 \ No newline at end of file diff --git a/plugins/wasm-rust/extensions/say-hello/src/lib.rs b/plugins/wasm-rust/extensions/say-hello/src/lib.rs new file mode 100644 index 0000000000..7c6302c6b1 --- /dev/null +++ b/plugins/wasm-rust/extensions/say-hello/src/lib.rs @@ -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>>, +} + +struct SayHello { + rule_matcher: Rc>>, +} + +#[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::(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> { + Some(Box::new(SayHello { + rule_matcher: self.rule_matcher.clone(), + })) + } + + fn get_type(&self) -> Option { + 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 + } +} diff --git a/plugins/wasm-rust/src/error.rs b/plugins/wasm-rust/src/error.rs new file mode 100644 index 0000000000..f2d5e30ab5 --- /dev/null +++ b/plugins/wasm-rust/src/error.rs @@ -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 + } +} diff --git a/plugins/wasm-rust/src/internal.rs b/plugins/wasm-rust/src/internal.rs new file mode 100644 index 0000000000..4116c5d798 --- /dev/null +++ b/plugins/wasm-rust/src/internal.rs @@ -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 { + 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, Option) { + hostcalls::get_shared_data(key).unwrap() +} + +pub(crate) fn set_shared_data( + key: &str, + value: Option<&[u8]>, + cas: Option, +) -> 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 { + hostcalls::resolve_shared_queue(vm_id, name).unwrap() +} + +pub(crate) fn dequeue_shared_queue(queue_id: u32) -> Result, 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 { + 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 { + hostcalls::get_map_value(MapType::HttpCallResponseHeaders, name).unwrap() +} + +pub(crate) fn get_http_call_response_header_bytes(name: &str) -> Option { + hostcalls::get_map_value_bytes(MapType::HttpCallResponseHeaders, name).unwrap() +} + +pub(crate) fn get_http_call_response_body(start: usize, max_size: usize) -> Option { + 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 { + hostcalls::get_map_value(MapType::HttpCallResponseTrailers, name).unwrap() +} + +pub(crate) fn get_http_call_response_trailer_bytes(name: &str) -> Option { + 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 { + 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 { + 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 { + 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 { + 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 { + 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) { + hostcalls::get_grpc_status().unwrap() +} + +pub(crate) fn call_foreign_function( + function_name: &str, + arguments: Option<&[u8]>, +) -> Result, 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 { + hostcalls::get_map_value(MapType::HttpRequestHeaders, name).unwrap() +} + +pub(crate) fn get_http_request_header_bytes(name: &str) -> Option { + 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 { + 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 { + hostcalls::get_map_value(MapType::HttpRequestTrailers, name).unwrap() +} + +pub(crate) fn get_http_request_trailer_bytes(name: &str) -> Option { + 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 { + hostcalls::get_map_value(MapType::HttpResponseHeaders, name).unwrap() +} + +pub(crate) fn get_http_response_header_bytes(name: &str) -> Option { + 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 { + 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 { + hostcalls::get_map_value(MapType::HttpResponseTrailers, name).unwrap() +} + +pub(crate) fn get_http_response_trailer_bytes(name: &str) -> Option { + 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() +} diff --git a/plugins/wasm-rust/src/lib.rs b/plugins/wasm-rust/src/lib.rs new file mode 100644 index 0000000000..2e8ab129a0 --- /dev/null +++ b/plugins/wasm-rust/src/lib.rs @@ -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; diff --git a/plugins/wasm-rust/src/log.rs b/plugins/wasm-rust/src/log.rs new file mode 100644 index 0000000000..4656b8669b --- /dev/null +++ b/plugins/wasm-rust/src/log.rs @@ -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) + } +} diff --git a/plugins/wasm-rust/src/rule_matcher.rs b/plugins/wasm-rust/src/rule_matcher.rs new file mode 100644 index 0000000000..b8de27662c --- /dev/null +++ b/plugins/wasm-rust/src/rule_matcher.rs @@ -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 { + category: Category, + routes: HashSet, + hosts: Vec, + config: PluginConfig, +} + +#[derive(Default)] +pub struct RuleMatcher { + rule_config: Vec>, + global_config: Option, +} + +impl RuleMatcher +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::(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::(rule_json.clone()) { + Ok(config) => config, + Err(error) => return Err(WasmRustError::new(error.to_string())), + }; + let routes = RuleMatcher::::parse_route_match_config(rule_json); + let hosts = RuleMatcher::::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 { + 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 { + let empty_vec = Vec::new(); + let keys = config[MATCH_DOMAIN_KEY].as_array().unwrap_or(&empty_vec); + let mut host_matchers: Vec = 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, 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 + } +}