From ef31e0931063d7e54b72c043b77242bc593d96e9 Mon Sep 17 00:00:00 2001 From: 007gzs <007gzs@gmail.com> Date: Mon, 22 Jul 2024 15:49:06 +0800 Subject: [PATCH] feat: add rust demo plugin request block (#1091) Co-authored-by: Yi --- .github/workflows/build-and-test-plugin.yaml | 2 +- plugins/wasm-rust/Cargo.lock | 10 + plugins/wasm-rust/Cargo.toml | 1 + .../extensions/request-block/Cargo.lock | 243 +++++++++++++++++ .../extensions/request-block/Cargo.toml | 17 ++ .../extensions/request-block/README.md | 88 +++++++ .../extensions/request-block/src/lib.rs | 248 ++++++++++++++++++ plugins/wasm-rust/src/lib.rs | 1 + plugins/wasm-rust/src/plugin_wrapper.rs | 164 ++++++++++++ test/README_CN.md | 2 + .../tests/rust-wasm-request-block.go | 166 ++++++++++++ .../tests/rust-wasm-request-block.yaml | 72 +++++ test/e2e/conformance/utils/suite/features.go | 15 +- test/e2e/conformance/utils/suite/suite.go | 7 +- tools/hack/build-wasm-plugins.sh | 18 ++ 15 files changed, 1047 insertions(+), 7 deletions(-) create mode 100644 plugins/wasm-rust/extensions/request-block/Cargo.lock create mode 100644 plugins/wasm-rust/extensions/request-block/Cargo.toml create mode 100644 plugins/wasm-rust/extensions/request-block/README.md create mode 100644 plugins/wasm-rust/extensions/request-block/src/lib.rs create mode 100644 plugins/wasm-rust/src/plugin_wrapper.rs create mode 100644 test/e2e/conformance/tests/rust-wasm-request-block.go create mode 100644 test/e2e/conformance/tests/rust-wasm-request-block.yaml diff --git a/.github/workflows/build-and-test-plugin.yaml b/.github/workflows/build-and-test-plugin.yaml index a1f38e63e..eb5f1610e 100644 --- a/.github/workflows/build-and-test-plugin.yaml +++ b/.github/workflows/build-and-test-plugin.yaml @@ -30,7 +30,7 @@ jobs: strategy: matrix: # TODO(Xunzhuo): Enable C WASM Filters in CI - wasmPluginType: [ GO ] + wasmPluginType: [ GO, RUST ] steps: - uses: actions/checkout@v3 diff --git a/plugins/wasm-rust/Cargo.lock b/plugins/wasm-rust/Cargo.lock index 293edb2ed..b50df574c 100644 --- a/plugins/wasm-rust/Cargo.lock +++ b/plugins/wasm-rust/Cargo.lock @@ -43,6 +43,7 @@ dependencies = [ name = "higress-wasm-rust" version = "0.1.0" dependencies = [ + "multimap", "proxy-wasm", "serde", "serde_json", @@ -70,6 +71,15 @@ dependencies = [ "cfg-if", ] +[[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.17.1" diff --git a/plugins/wasm-rust/Cargo.toml b/plugins/wasm-rust/Cargo.toml index 19ddc6d09..f220badc9 100644 --- a/plugins/wasm-rust/Cargo.toml +++ b/plugins/wasm-rust/Cargo.toml @@ -10,3 +10,4 @@ proxy-wasm = "0.2.1" serde = "1.0" serde_json = "1.0" uuid = { version = "1.3.3", features = ["v4"] } +multimap = "0" diff --git a/plugins/wasm-rust/extensions/request-block/Cargo.lock b/plugins/wasm-rust/extensions/request-block/Cargo.lock new file mode 100644 index 000000000..5ad328023 --- /dev/null +++ b/plugins/wasm-rust/extensions/request-block/Cargo.lock @@ -0,0 +1,243 @@ +# 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 = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[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 = [ + "multimap", + "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 = "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.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 = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "request-block" +version = "0.1.0" +dependencies = [ + "higress-wasm-rust", + "multimap", + "proxy-wasm", + "regex", + "serde", + "serde_json", +] + +[[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" +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/request-block/Cargo.toml b/plugins/wasm-rust/extensions/request-block/Cargo.toml new file mode 100644 index 000000000..f22de1644 --- /dev/null +++ b/plugins/wasm-rust/extensions/request-block/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "request-block" +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" +regex = "1" +multimap = "0" \ No newline at end of file diff --git a/plugins/wasm-rust/extensions/request-block/README.md b/plugins/wasm-rust/extensions/request-block/README.md new file mode 100644 index 000000000..82d041a53 --- /dev/null +++ b/plugins/wasm-rust/extensions/request-block/README.md @@ -0,0 +1,88 @@ +# 功能说明 +`request-block`插件实现了基于 URL、请求头等特征屏蔽 HTTP 请求,可以用于防护部分站点资源不对外部暴露 + +# 配置字段 + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| -------- | -------- | -------- | -------- | -------- | +| block_urls | array of string | 选填,`block_urls`,`block_headers`,`block_bodies` 中至少必填一项 | - | 配置用于匹配需要屏蔽 URL 的字符串 | +| block_exact_urls | array of string | 选填,`block_urls`,`block_headers`,`block_bodies` 中至少必填一项 | - | 配置用于匹配需要精确屏蔽 URL 的字符串 | +| block_regexp_urls | array of string | 选填,`block_urls`,`block_headers`,`block_bodies` 中至少必填一项 | - | 配置用于匹配需要屏蔽 URL 的正则表达式 | +| block_headers | array of string | 选填,`block_urls`,`block_headers`,`block_bodies` 中至少必填一项 | - | 配置用于匹配需要屏蔽请求 Header 的字符串 | +| block_bodies | array of string | 选填,`block_urls`,`block_headers`,`block_bodies` 中至少必填一项 | - | 配置用于匹配需要屏蔽请求 Body 的字符串 | +| blocked_code | number | 选填 | 403 | 配置请求被屏蔽时返回的 HTTP 状态码 | +| blocked_message | string | 选填 | - | 配置请求被屏蔽时返回的 HTTP 应答 Body | +| case_sensitive | bool | 选填 | true | 配置匹配时是否区分大小写,默认区分 | + +# 配置示例 + +## 屏蔽请求 url 路径 +```yaml +block_urls: +- swagger.html +- foo=bar +case_sensitive: false +``` + +根据该配置,下列请求将被禁止访问: + +```bash +curl http://example.com?foo=Bar +curl http://exmaple.com/Swagger.html +``` + +## 屏蔽请求 header +```yaml +block_headers: +- example-key +- example-value +``` + +根据该配置,下列请求将被禁止访问: + +```bash +curl http://example.com -H 'example-key: 123' +curl http://exmaple.com -H 'my-header: example-value' +``` + +## 屏蔽请求 body +```yaml +block_bodies: +- "hello world" +case_sensitive: false +``` + +根据该配置,下列请求将被禁止访问: + +```bash +curl http://example.com -d 'Hello World' +curl http://exmaple.com -d 'hello world' +``` + +## 对特定路由或域名开启 +```yaml +# 使用 _rules_ 字段进行细粒度规则配置 +_rules_: +# 规则一:按路由名称匹配生效 +- _match_route_: + - route-a + - route-b + block_bodies: + - "hello world" +# 规则二:按域名匹配生效 +- _match_domain_: + - "*.example.com" + - test.com + block_urls: + - "swagger.html" + block_bodies: + - "hello world" +``` +此例 `_match_route_` 中指定的 `route-a` 和 `route-b` 即在创建网关路由时填写的路由名称,当匹配到这两个路由时,将使用此段配置; +此例 `_match_domain_` 中指定的 `*.example.com` 和 `test.com` 用于匹配请求的域名,当发现域名匹配时,将使用此段配置; +配置的匹配生效顺序,将按照 `_rules_` 下规则的排列顺序,匹配第一个规则后生效对应配置,后续规则将被忽略。 + +# 请求 Body 大小限制 + +当配置了 `block_bodies` 时,仅支持小于 32 MB 的请求 Body 进行匹配。若请求 Body 大于此限制,并且不存在匹配到的 `block_urls` 和 `block_headers` 项时,不会对该请求执行屏蔽操作 +当配置了 `block_bodies` 时,若请求 Body 超过全局配置 DownstreamConnectionBufferLimits,将返回 `413 Payload Too Large` diff --git a/plugins/wasm-rust/extensions/request-block/src/lib.rs b/plugins/wasm-rust/extensions/request-block/src/lib.rs new file mode 100644 index 000000000..5e1dec170 --- /dev/null +++ b/plugins/wasm-rust/extensions/request-block/src/lib.rs @@ -0,0 +1,248 @@ +// 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::plugin_wrapper::{HttpContextWrapper, RootContextWrapper}; +use higress_wasm_rust::rule_matcher::{on_configure, RuleMatcher, SharedRuleMatcher}; +use multimap::MultiMap; +use proxy_wasm::traits::{Context, HttpContext, RootContext}; +use proxy_wasm::types::{Action, Bytes, ContextType, LogLevel}; +use regex::Regex; +use serde::de::Error; +use serde::Deserialize; +use serde::Deserializer; +use serde_json::Value; +use std::cell::RefCell; +use std::ops::DerefMut; +use std::rc::Rc; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_|Box::new(RquestBlockRoot::new())); +}} + +const PLUGIN_NAME: &str = "request-block"; + +struct RquestBlockRoot { + log: Log, + rule_matcher: SharedRuleMatcher, +} + +struct RquestBlock { + log: Log, + config: Option, + cache_request: bool, +} + +fn deserialize_block_regexp_urls<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let mut ret = Vec::new(); + let value: Value = Deserialize::deserialize(deserializer)?; + let block_regexp_urls = value + .as_array() + .ok_or(Error::custom("block_regexp_urls error not list"))?; + + for block_regexp_url in block_regexp_urls { + let reg_exp = block_regexp_url + .as_str() + .ok_or(Error::custom("block_regexp_urls error not str"))?; + if let Ok(reg) = Regex::new(reg_exp) { + ret.push(reg); + } else { + return Err(Error::custom(format!( + "block_regexp_urls error field {}", + reg_exp + ))); + } + } + Ok(ret) +} +fn blocked_code_default() -> u32 { + 403 +} +fn case_sensitive_default() -> bool { + true +} +#[derive(Default, Debug, Deserialize, Clone)] +#[serde(default)] +pub struct RquestBlockConfig { + #[serde(default = "blocked_code_default")] + blocked_code: u32, + blocked_message: String, + #[serde(default = "case_sensitive_default")] + case_sensitive: bool, + block_urls: Vec, + block_exact_urls: Vec, + block_headers: Vec, + block_bodies: Vec, + #[serde(deserialize_with = "deserialize_block_regexp_urls")] + block_regexp_urls: Vec, +} + +impl RquestBlockRoot { + fn new() -> Self { + RquestBlockRoot { + log: Log::new(PLUGIN_NAME.to_string()), + rule_matcher: Rc::new(RefCell::new(RuleMatcher::default())), + } + } +} + +impl Context for RquestBlockRoot {} + +impl RootContext for RquestBlockRoot { + fn on_configure(&mut self, _plugin_configuration_size: usize) -> bool { + let ret = on_configure( + self, + _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 get_type(&self) -> Option { + Some(ContextType::HttpContext) + } +} + +impl RootContextWrapper for RquestBlockRoot { + fn rule_matcher(&self) -> &SharedRuleMatcher { + &self.rule_matcher + } + + fn create_http_context_wrapper( + &self, + _context_id: u32, + ) -> Option>> { + Some(Box::new(RquestBlock { + cache_request: false, + config: None, + log: Log::new(PLUGIN_NAME.to_string()), + })) + } +} + +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 cache_request_body(&self) -> bool { + self.cache_request + } + fn on_http_request_headers_ok(&mut self, headers: &MultiMap) -> Action { + if self.config.is_none() { + return Action::Continue; + } + let config = self.config.as_ref().unwrap(); + if !config.block_urls.is_empty() + || !config.block_exact_urls.is_empty() + || !config.block_regexp_urls.is_empty() + { + let value = headers.get(":path"); + + if value.is_none() { + self.log.warn("get path failed"); + return Action::Continue; + } + let mut request_url = value.unwrap().clone(); + + if config.case_sensitive { + request_url = request_url.to_lowercase(); + } + for block_exact_url in &config.block_exact_urls { + if *block_exact_url == request_url { + self.send_http_response( + config.blocked_code, + Vec::new(), + Some(config.blocked_message.as_bytes()), + ); + return Action::Pause; + } + } + for block_url in &config.block_urls { + if request_url.contains(block_url) { + self.send_http_response( + config.blocked_code, + Vec::new(), + Some(config.blocked_message.as_bytes()), + ); + return Action::Pause; + } + } + + for block_reg_exp in &config.block_regexp_urls { + if block_reg_exp.is_match(&request_url) { + self.send_http_response( + config.blocked_code, + Vec::new(), + Some(config.blocked_message.as_bytes()), + ); + return Action::Pause; + } + } + } + if !config.block_headers.is_empty() { + let mut header_strs: Vec = Vec::new(); + for (k, v) in headers { + header_strs.push(k.clone()); + header_strs.push(v.join("\n")); + } + let header_str = header_strs.join("\n"); + for block_header in &config.block_headers { + if header_str.contains(block_header) { + self.send_http_response( + config.blocked_code, + Vec::new(), + Some(config.blocked_message.as_bytes()), + ); + return Action::Pause; + } + } + } + Action::Continue + } + fn on_http_request_body_ok(&mut self, req_body: &Bytes) -> Action { + if self.config.is_none() { + return Action::Continue; + } + let config = self.config.as_ref().unwrap(); + if config.block_bodies.is_empty() { + return Action::Continue; + } + let mut body = req_body.clone(); + if config.case_sensitive { + body = body.to_ascii_lowercase(); + } + for block_body in &config.block_bodies { + let s = block_body.as_bytes(); + if body.windows(s.len()).any(|window| window == s) { + self.send_http_response( + config.blocked_code, + Vec::new(), + Some(config.blocked_message.as_bytes()), + ); + return Action::Pause; + } + } + Action::Continue + } +} diff --git a/plugins/wasm-rust/src/lib.rs b/plugins/wasm-rust/src/lib.rs index 2e8ab129a..1e38d0cb0 100644 --- a/plugins/wasm-rust/src/lib.rs +++ b/plugins/wasm-rust/src/lib.rs @@ -15,4 +15,5 @@ pub mod error; mod internal; pub mod log; +pub mod plugin_wrapper; pub mod rule_matcher; diff --git a/plugins/wasm-rust/src/plugin_wrapper.rs b/plugins/wasm-rust/src/plugin_wrapper.rs new file mode 100644 index 000000000..bd431472d --- /dev/null +++ b/plugins/wasm-rust/src/plugin_wrapper.rs @@ -0,0 +1,164 @@ +// 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::rule_matcher::SharedRuleMatcher; +use multimap::MultiMap; +use proxy_wasm::traits::{Context, HttpContext, RootContext}; +use proxy_wasm::types::{Action, Bytes}; +use serde::de::DeserializeOwned; + +pub trait RootContextWrapper: RootContext +where + PluginConfig: Default + DeserializeOwned + 'static + Clone, +{ + // fn create_http_context(&self, _context_id: u32) -> Option> { + fn create_http_context_use_wrapper(&self, _context_id: u32) -> Option> { + // trait 继承没法重写 RootContext 的 create_http_context,先写个函数让上层调下吧 + match self.create_http_context_wrapper(_context_id) { + Some(http_context) => Some(Box::new(PluginHttpWrapper::new( + self.rule_matcher(), + http_context, + ))), + None => None, + } + } + fn rule_matcher(&self) -> &SharedRuleMatcher; + fn create_http_context_wrapper( + &self, + _context_id: u32, + ) -> Option>> { + None + } +} +pub trait HttpContextWrapper: HttpContext { + fn on_config(&mut self, _config: &PluginConfig) {} + fn on_http_request_headers_ok(&mut self, _headers: &MultiMap) -> Action { + Action::Continue + } + fn cache_request_body(&self) -> bool { + false + } + fn cache_response_body(&self) -> bool { + false + } + fn on_http_request_body_ok(&mut self, _req_body: &Bytes) -> Action { + Action::Continue + } + fn on_http_response_body_ok(&mut self, _res_body: &Bytes) -> Action { + Action::Continue + } +} +pub struct PluginHttpWrapper { + req_headers: MultiMap, + req_body: Bytes, + res_body: Bytes, + config: Option, + rule_matcher: SharedRuleMatcher, + http_content: Box>, +} +impl PluginHttpWrapper { + pub fn new( + rule_matcher: &SharedRuleMatcher, + http_content: Box>, + ) -> Self { + PluginHttpWrapper { + req_headers: MultiMap::new(), + req_body: Bytes::new(), + res_body: Bytes::new(), + config: None, + rule_matcher: rule_matcher.clone(), + http_content, + } + } +} +impl Context for PluginHttpWrapper {} +impl HttpContext for PluginHttpWrapper +where + PluginConfig: Default + DeserializeOwned + Clone, +{ + fn on_http_request_headers(&mut self, _num_headers: usize, _end_of_stream: bool) -> Action { + let binding = self.rule_matcher.borrow(); + self.config = match binding.get_match_config() { + None => None, + Some(config) => Some(config.1.clone()), + }; + for (k, v) in self.get_http_request_headers() { + self.req_headers.insert(k, v); + } + if let Some(config) = &self.config { + self.http_content.on_config(config); + } + let ret = self + .http_content + .on_http_request_headers(_num_headers, _end_of_stream); + if ret != Action::Continue { + return ret; + } + self.http_content + .on_http_request_headers_ok(&self.req_headers) + } + + fn on_http_request_body(&mut self, _body_size: usize, _end_of_stream: bool) -> Action { + let mut ret = self + .http_content + .on_http_request_body(_body_size, _end_of_stream); + if !self.http_content.cache_request_body() { + return ret; + } + if _body_size > 0 { + if let Some(body) = self.get_http_request_body(0, _body_size) { + self.req_body.extend(body) + } + } + if _end_of_stream && ret == Action::Continue { + ret = self.http_content.on_http_request_body_ok(&self.req_body); + } + ret + } + + fn on_http_request_trailers(&mut self, _num_trailers: usize) -> Action { + self.http_content.on_http_request_trailers(_num_trailers) + } + + fn on_http_response_headers(&mut self, _num_headers: usize, _end_of_stream: bool) -> Action { + self.http_content + .on_http_response_headers(_num_headers, _end_of_stream) + } + + fn on_http_response_body(&mut self, _body_size: usize, _end_of_stream: bool) -> Action { + let mut ret = self + .http_content + .on_http_response_body(_body_size, _end_of_stream); + if !self.http_content.cache_response_body() { + return ret; + } + if _body_size > 0 { + if let Some(body) = self.get_http_response_body(0, _body_size) { + self.res_body.extend(body); + } + } + if _end_of_stream && ret == Action::Continue { + ret = self.http_content.on_http_response_body_ok(&self.res_body); + } + ret + } + + fn on_http_response_trailers(&mut self, _num_trailers: usize) -> Action { + self.http_content.on_http_response_trailers(_num_trailers) + } + + fn on_log(&mut self) { + self.http_content.on_log() + } +} diff --git a/test/README_CN.md b/test/README_CN.md index eafbfd721..0f8cf338e 100644 --- a/test/README_CN.md +++ b/test/README_CN.md @@ -22,6 +22,8 @@ Higress 提供了运行 Ingress API 一致性测试和 wasmplugin 测试的 make + 为测试构建所有 GO WasmPlugins: `make higress-wasmplugin-test` + 仅为一个 GO WasmPlugin 构建测试: `PLUGIN_NAME=request-block make higress-wasmplugin-test` + 仅为一个 CPP WasmPlugin 构建测试: `PLUGIN_TYPE=CPP PLUGIN_NAME=key_auth make higress-wasmplugin-test` + + 为测试构建所有 Rust WasmPlugins: `PLUGIN_TYPE=RUST make higress-wasmplugin-test` + + 仅为一个 Rust WasmPlugin 构建测试: `PLUGIN_TYPE=RUST PLUGIN_NAME=request-block make higress-wasmplugin-test` + 仅运行指定测试,用逗号分隔 `TEST_SHORTNAME=WasmPluginsIPRestrictionAllow,WasmPluginsIPRestrictionDeny make higress-wasmplugin-test` 可以分为以下步骤: diff --git a/test/e2e/conformance/tests/rust-wasm-request-block.go b/test/e2e/conformance/tests/rust-wasm-request-block.go new file mode 100644 index 000000000..37651041b --- /dev/null +++ b/test/e2e/conformance/tests/rust-wasm-request-block.go @@ -0,0 +1,166 @@ +// 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(RustWasmPluginsRequestBlock) +} + +var RustWasmPluginsRequestBlock = suite.ConformanceTest{ + ShortName: "RustWasmPluginsRequestBlock", + Description: "The Ingress in the higress-conformance-infra namespace test the rust request-block wasmplugins.", + Manifests: []string{"tests/rust-wasm-request-block.yaml"}, + Features: []suite.SupportedFeature{suite.WASMRustConformanceFeature}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + testcases := []http.Assertion{ + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/swagger.html", + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/env/info", + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/web/info", + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + }, + }, + { + // post blocked body + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Method: "POST", + ContentType: http.ContentTypeTextPlain, + Body: []byte(`hello world`), + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 403, + }, + }, + }, + { + // check body echoed back in expected request(same as ActualRequest if not set) + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + CompareTarget: http.CompareTargetRequest, + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Method: "POST", + ContentType: http.ContentTypeTextPlain, + Body: []byte(`hello higress`), + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + }, + }, + { + // check body echoed back in expected response + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-echo-body-v1", + TargetNamespace: "higress-conformance-infra", + CompareTarget: http.CompareTargetResponse, + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo2.com", + Path: "/foo", + Method: "POST", + ContentType: http.ContentTypeTextPlain, + Body: []byte(`hello higress`), + UnfollowRedirect: true, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + ContentType: http.ContentTypeTextPlain, + Body: []byte(`hello higress`), + }, + }, + }, + } + t.Run("WasmPlugins request-block", 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/rust-wasm-request-block.yaml b/test/e2e/conformance/tests/rust-wasm-request-block.yaml new file mode 100644 index 000000000..56a2965d8 --- /dev/null +++ b/test/e2e/conformance/tests/rust-wasm-request-block.yaml @@ -0,0 +1,72 @@ +# 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: + nginx.ingress.kubernetes.io/app-root: "/foo" + name: httproute-app-root + namespace: higress-conformance-infra +spec: + ingressClassName: higress + rules: + - host: "foo.com" + http: + paths: + - pathType: Prefix + path: "/" + backend: + service: + name: infra-backend-v1 + port: + number: 8080 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + nginx.ingress.kubernetes.io/app-root: "/foo" + name: httproute-app-root2 + namespace: higress-conformance-infra +spec: + ingressClassName: higress + rules: + - host: "foo2.com" + http: + paths: + - pathType: Prefix + path: "/" + backend: + service: + name: infra-backend-echo-body-v1 + port: + number: 8080 +--- +apiVersion: extensions.higress.io/v1alpha1 +kind: WasmPlugin +metadata: + name: request-block + namespace: higress-system +spec: + defaultConfig: + block_urls: + - "swagger.html" + block_regexp_urls: + - "/env.*" + block_exact_urls: + - "/web/info" + block_bodies: + - "hello world" + url: file:///opt/plugins/wasm-rust/extensions/request-block/plugin.wasm diff --git a/test/e2e/conformance/utils/suite/features.go b/test/e2e/conformance/utils/suite/features.go index 0c558a1b6..3108943f4 100644 --- a/test/e2e/conformance/utils/suite/features.go +++ b/test/e2e/conformance/utils/suite/features.go @@ -23,8 +23,9 @@ const ( HTTPConformanceFeature SupportedFeature = "http" // extended: extensibility - WASMGoConformanceFeature SupportedFeature = "wasm-go" - WASMCPPConformanceFeature SupportedFeature = "wasm-cpp" + WASMGoConformanceFeature SupportedFeature = "wasm-go" + WASMCPPConformanceFeature SupportedFeature = "wasm-cpp" + WASMRustConformanceFeature SupportedFeature = "wasm-rust" // extended: service discovery DubboConformanceFeature SupportedFeature = "dubbo" @@ -36,6 +37,13 @@ const ( EnvoyConfigConformanceFeature SupportedFeature = "envoy-config" ) +var WasmPluginTypeMap = map[string]SupportedFeature{ + "": WASMGoConformanceFeature, // default + "GO": WASMGoConformanceFeature, + "CPP": WASMCPPConformanceFeature, + "RUST": WASMRustConformanceFeature, +} + var AllFeatures = sets.Set{}. Insert(string(HTTPConformanceFeature)). Insert(string(DubboConformanceFeature)). @@ -46,4 +54,5 @@ var AllFeatures = sets.Set{}. var ExperimentFeatures = sets.Set{}. Insert(string(WASMGoConformanceFeature)). - Insert(string(WASMCPPConformanceFeature)) + Insert(string(WASMCPPConformanceFeature)). + Insert(string(WASMRustConformanceFeature)) diff --git a/test/e2e/conformance/utils/suite/suite.go b/test/e2e/conformance/utils/suite/suite.go index b7302e5a4..dd9e76221 100644 --- a/test/e2e/conformance/utils/suite/suite.go +++ b/test/e2e/conformance/utils/suite/suite.go @@ -95,10 +95,11 @@ func New(s Options) *ConformanceTestSuite { } if s.IsWasmPluginTest { - if s.WasmPluginType == "CPP" { - s.SupportedFeatures.Insert(string(WASMCPPConformanceFeature)) + feature, ok := WasmPluginTypeMap[s.WasmPluginType] + if ok { + s.SupportedFeatures.Insert(string(feature)) } else { - s.SupportedFeatures.Insert(string(WASMGoConformanceFeature)) + panic("WasmPluginType [" + s.WasmPluginType + "] not support") } } else if s.IsEnvoyConfigTest { s.SupportedFeatures.Insert(string(EnvoyConfigConformanceFeature)) diff --git a/tools/hack/build-wasm-plugins.sh b/tools/hack/build-wasm-plugins.sh index 713b6666b..ec1c336b9 100755 --- a/tools/hack/build-wasm-plugins.sh +++ b/tools/hack/build-wasm-plugins.sh @@ -29,6 +29,24 @@ then echo "🚀 Build CPP WasmPlugin: $INNER_PLUGIN_NAME" PLUGIN_NAME=${INNER_PLUGIN_NAME} make build fi +elif [ "$TYPE" == "RUST" ] +then + cd ./plugins/wasm-rust/ + if [ ! -n "$INNER_PLUGIN_NAME" ]; then + EXTENSIONS_DIR=$(pwd)"/extensions/" + echo "🚀 Build all Rust WasmPlugins under folder of $EXTENSIONS_DIR" + for file in `ls $EXTENSIONS_DIR` + do + if [ -d $EXTENSIONS_DIR$file ]; then + name=${file##*/} + echo "🚀 Build Rust WasmPlugin: $name" + PLUGIN_NAME=${name} BUILDER_REGISTRY="docker.io/alihigress/plugins-rust-" make build + fi + done + else + echo "🚀 Build Rust WasmPlugin: $INNER_PLUGIN_NAME" + PLUGIN_NAME=${INNER_PLUGIN_NAME} make build + fi else echo "Not specify plugin language, so just compile wasm-go as default" cd ./plugins/wasm-go/