feat: add rust demo plugin request block (#1091)

Co-authored-by: Yi <lynskylate@gmail.com>
This commit is contained in:
007gzs
2024-07-22 15:49:06 +08:00
committed by GitHub
parent c0f2cafdc8
commit ef31e09310
15 changed files with 1047 additions and 7 deletions

View File

@@ -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"

View File

@@ -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"

View File

@@ -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`

View File

@@ -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<RquestBlockConfig>,
}
struct RquestBlock {
log: Log,
config: Option<RquestBlockConfig>,
cache_request: bool,
}
fn deserialize_block_regexp_urls<'de, D>(deserializer: D) -> Result<Vec<Regex>, 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<String>,
block_exact_urls: Vec<String>,
block_headers: Vec<String>,
block_bodies: Vec<String>,
#[serde(deserialize_with = "deserialize_block_regexp_urls")]
block_regexp_urls: Vec<Regex>,
}
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<Box<dyn HttpContext>> {
self.create_http_context_use_wrapper(_context_id)
}
fn get_type(&self) -> Option<ContextType> {
Some(ContextType::HttpContext)
}
}
impl RootContextWrapper<RquestBlockConfig> for RquestBlockRoot {
fn rule_matcher(&self) -> &SharedRuleMatcher<RquestBlockConfig> {
&self.rule_matcher
}
fn create_http_context_wrapper(
&self,
_context_id: u32,
) -> Option<Box<dyn HttpContextWrapper<RquestBlockConfig>>> {
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<RquestBlockConfig> 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<String, String>) -> 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<String> = 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
}
}