From d71257cb7a635d026509158f2f7c6c3ec62adb50 Mon Sep 17 00:00:00 2001 From: vicanso Date: Tue, 14 May 2024 21:01:32 +0800 Subject: [PATCH] feat: support content-disposition for static serve --- README.md | 39 +++++++-- TODO.md | 3 +- docs/phase_chart_zh.md | 39 +++++++-- docs/plugin_zh.md | 15 +++- src/plugin/admin.rs | 39 +++++---- src/plugin/basic_auth.rs | 14 ++- src/plugin/cache.rs | 7 ++ src/plugin/compression.rs | 14 ++- src/plugin/directory.rs | 45 ++++++++-- src/plugin/ip_limit.rs | 10 ++- src/plugin/key_auth.rs | 12 ++- src/plugin/limit.rs | 12 +++ src/plugin/mock.rs | 12 ++- src/plugin/mod.rs | 33 +++++--- src/plugin/ping.rs | 12 ++- src/plugin/redirect_https.rs | 12 ++- src/plugin/request_id.rs | 13 ++- src/plugin/response_headers.rs | 13 ++- src/plugin/stats.rs | 16 ++-- web/src/components/form-editor.tsx | 131 +++++++++++++++++++++++++++-- web/src/i18n/en.ts | 11 ++- web/src/i18n/zh.ts | 10 ++- web/src/pages/plugin-info.tsx | 31 ++----- web/src/states/config.ts | 2 +- 24 files changed, 416 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index b0f03104..34a48238 100644 --- a/README.md +++ b/README.md @@ -45,28 +45,49 @@ All toml configurations are as follows [pingap.toml](./conf/pingap.toml). ```mermaid graph TD; - start("New Request")-->server("HTTP Server"); + server("HTTP Server"); + locationA("Location A"); + locationB("Location B"); + locationPluginListA("Proxy Plugin List A"); + locationPluginListB("Proxy Plugin List B"); + upstreamA1("Upstream A1"); + upstreamA2("Upstream A2"); + upstreamB1("Upstream B1"); + upstreamB2("Upstream B2"); + locationResponsePluginListA("Response Plugin List A"); + locationResponsePluginListB("Response Plugin List B"); - server -- "host:HostA, Path:/api/*" --> locationA("Location A") + start("New Request") --> server - server -- "Path:/rest/*"--> locationB("Location B") + server -- "host:HostA, Path:/api/*" --> locationA - locationA -- "Exec Plugins" --> locationPluginListA("Plugin List A") + server -- "Path:/rest/*"--> locationB - locationB -- "Exec Plugins" --> locationPluginListB("Plugin List B") + locationA -- "Exec Proxy Plugins" --> locationPluginListA - locationPluginListA -- "proxy pass: 10.0.0.1:8001" --> upstreamA1("Upstream A1") --> response + locationB -- "Exec Proxy Plugins" --> locationPluginListB - locationPluginListA -- "proxy pass: 10.0.0.2:8001" --> upstreamA2("Upstream A2") --> response + locationPluginListA -- "proxy pass: 10.0.0.1:8001" --> upstreamA1 + + locationPluginListA -- "proxy pass: 10.0.0.2:8001" --> upstreamA2 locationPluginListA -- "done" --> response - locationPluginListB -- "proxy pass: 10.0.0.1:8002" --> upstreamB1("Upstream B1") --> response + locationPluginListB -- "proxy pass: 10.0.0.1:8002" --> upstreamB1 - locationPluginListB -- "proxy pass: 10.0.0.2:8002" --> upstreamB2("Upstream B2") --> response + locationPluginListB -- "proxy pass: 10.0.0.2:8002" --> upstreamB2 locationPluginListB -- "done" --> response + upstreamA1 -- "Exec Response Plugins" --> locationResponsePluginListA + upstreamA2 -- "Exec Response Plugins" --> locationResponsePluginListA + + upstreamB1 -- "Exec Response Plugins" --> locationResponsePluginListB + upstreamB2 -- "Exec Response Plugins" --> locationResponsePluginListB + + locationResponsePluginListA --> response + locationResponsePluginListB --> response + response("HTTP Response") --> stop("Logging"); ``` diff --git a/TODO.md b/TODO.md index 83975932..9ee39215 100644 --- a/TODO.md +++ b/TODO.md @@ -7,7 +7,8 @@ - [ ] fix not_before not_after of cert - [ ] http headers plugin - [ ] client body size limit plugin -- [ ] support more limit plugin +- [x] support `Content-Disposition` for directory static serve plugin +- [x] support more limit plugin - [x] how to use proxy plugin - [x] server listen multi address - [x] show name of web view editor diff --git a/docs/phase_chart_zh.md b/docs/phase_chart_zh.md index a442b2ba..0c75a7b6 100644 --- a/docs/phase_chart_zh.md +++ b/docs/phase_chart_zh.md @@ -4,28 +4,49 @@ description: Pingap 处理流程 ```mermaid graph TD; - start("新的请求")-->server("HTTP服务"); + server("HTTP服务"); + locationA("Location A"); + locationB("Location B"); + locationPluginListA("转发插件列表A"); + locationPluginListB("转发插件列表B"); + upstreamA1("上游服务A1"); + upstreamA2("上游服务A2"); + upstreamB1("上游服务B1"); + upstreamB2("上游服务B2"); + locationResponsePluginListA("响应插件列表A"); + locationResponsePluginListB("响应插件列表B"); - server -- "host:HostA, Path:/api/*" --> locationA("Location A") + start("新的请求") --> server - server -- "Path:/rest/*"--> locationB("Location B") + server -- "host:HostA, Path:/api/*" --> locationA - locationA -- "顺序执行各插件" --> locationPluginListA("插件列表A") + server -- "Path:/rest/*"--> locationB - locationB -- "顺序执行各插件" --> locationPluginListB("插件列表B") + locationA -- "顺序执行各转发插件" --> locationPluginListA - locationPluginListA -- "转发至: 10.0.0.1:8001" --> upstreamA1("上游服务A1") --> response + locationB -- "顺序执行各转发插件" --> locationPluginListB - locationPluginListA -- "转发至: 10.0.0.2:8001" --> upstreamA2("上游服务A2") --> response + locationPluginListA -- "转发至: 10.0.0.1:8001" --> upstreamA1 + + locationPluginListA -- "转发至: 10.0.0.2:8001" --> upstreamA2 locationPluginListA -- "处理完成" --> response - locationPluginListB -- "转发至: 10.0.0.1:8002" --> upstreamB1("上游服务B1") --> response + locationPluginListB -- "转发至: 10.0.0.1:8002" --> upstreamB1 - locationPluginListB -- "转发至: 10.0.0.2:8002" --> upstreamB2("上游服务B2") --> response + locationPluginListB -- "转发至: 10.0.0.2:8002" --> upstreamB2 locationPluginListB -- "处理完成" --> response + upstreamA1 -- "顺序执行响应插件" --> locationResponsePluginListA + upstreamA2 -- "顺序执行响应插件" --> locationResponsePluginListA + + upstreamB1 -- "顺序执行响应插件" --> locationResponsePluginListB + upstreamB2 -- "顺序执行响应插件" --> locationResponsePluginListB + + locationResponsePluginListA --> response + locationResponsePluginListB --> response + response("HTTP响应") --> stop("日志记录"); ``` diff --git a/docs/plugin_zh.md b/docs/plugin_zh.md index e0aaa579..a3c712bd 100644 --- a/docs/plugin_zh.md +++ b/docs/plugin_zh.md @@ -4,6 +4,14 @@ description: Pingap 插件体系 Pingap中通过Locaton添加各种插件支持更多的应用场景,如鉴权、流控、设置响应头等场景。 +# 插件执行时点 + +现支持将插件添加到以下各阶段时点中执行: + +- `Request`: 请求的最开始阶段,适用于针对一些权限类的拦截等处理 +- `ProxyUpstream`: 请求转发至上流节点之前,因为此流程是在读取缓存之后,因此若不希望针对缓存前限制,但转发至上游前限制的可配置为此阶段。如限制IP访问频繁,但允许高并发读取缓存数据。 +- `UpstreamResponse`: 上游数据响应之后,用于针对上游响应数据做调整时使用。 + # 转发插件 转发插件是在请求转发至upstream之前执行,支持在`request_filter`与`proxy_upstream_filter`阶段执行,均为转发到上游节点前的处理。下面介绍一下`proxy plugin`的具体逻辑,trait如下: @@ -110,11 +118,12 @@ category = "compression" 静态文件目录服务,为指定目录提供静态文件服务,需要注意query部分的参数均为可选值,说明如下: - `chunk_size`: Http chunk的大小,默认为`8192` -- `max_age`: 设置http响应的的缓存时间,默认无。此值对于`text/html`无效,html均设置为不可缓存 -- `private`: 缓存是否设置为`private`,默认为`public` +- `max_age`: 设置http响应的的缓存时间,默认无。此值对于`text/html`无效,html均设置为不可缓存。如设置为`1h`表示缓存有效期1小时 +- `private`: 缓存是否设置为`private`,默认为`public`,,query中只要有`private`即可 - `index`: 设置默认的index文件,默认为`index.html` - `charset`: 指定charset类型,默认无 -- `autoindex`: 是否允许目录以浏览形式展示 +- `autoindex`: 是否允许目录以浏览形式展示,query中只要有`autoindex`即可 +- `download`: 是否支持下载,指定该参数后响应时会设置响应头`Content-Disposition`,query中只要有`download`即可 ```toml [plugins.downloadsServe] diff --git a/src/plugin/admin.rs b/src/plugin/admin.rs index f2731ab5..5b2416bb 100644 --- a/src/plugin/admin.rs +++ b/src/plugin/admin.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{ProxyPlugin, Result}; +use super::{Error, ProxyPlugin, Result}; use crate::config::{ self, save_config, BasicConf, LocationConf, PluginCategory, PluginConf, PluginStep, ServerConf, UpstreamConf, @@ -93,22 +93,6 @@ pub struct AdminServe { pub proxy_step: PluginStep, ip_fail_limit: TtlLruLimit, } -impl AdminServe { - pub fn new(value: &str, proxy_step: PluginStep) -> Result { - debug!("new admin server proxy plugin, {value}, {proxy_step:?}"); - let arr: Vec<&str> = value.split(' ').collect(); - let mut authorization = "".to_string(); - if arr.len() >= 2 { - authorization = arr[1].trim().to_string(); - } - Ok(Self { - path: arr[0].trim().to_string(), - proxy_step, - authorization, - ip_fail_limit: TtlLruLimit::new(512, Duration::from_secs(5 * 60), 5), - }) - } -} #[derive(Serialize, Deserialize)] struct ErrorResponse { @@ -125,6 +109,27 @@ struct BasicInfo { } impl AdminServe { + pub fn new(value: &str, proxy_step: PluginStep) -> Result { + debug!("new admin server proxy plugin, {value}, {proxy_step:?}"); + if ![PluginStep::Request, PluginStep::ProxyUpstream].contains(&proxy_step) { + return Err(Error::Invalid { + category: PluginCategory::Admin.to_string(), + message: "Admin serve plugin should be executed at request or proxy upstream step" + .to_string(), + }); + } + let arr: Vec<&str> = value.split(' ').collect(); + let mut authorization = "".to_string(); + if arr.len() >= 2 { + authorization = arr[1].trim().to_string(); + } + Ok(Self { + path: arr[0].trim().to_string(), + proxy_step, + authorization, + ip_fail_limit: TtlLruLimit::new(512, Duration::from_secs(5 * 60), 5), + }) + } fn auth_validate(&self, req_header: &RequestHeader) -> bool { if self.authorization.is_empty() { return true; diff --git a/src/plugin/basic_auth.rs b/src/plugin/basic_auth.rs index 87831e23..72a797a6 100644 --- a/src/plugin/basic_auth.rs +++ b/src/plugin/basic_auth.rs @@ -36,11 +36,19 @@ pub struct BasicAuth { impl BasicAuth { pub fn new(value: &str, proxy_step: PluginStep) -> Result { debug!("new basic auth proxy plugin, {value}, {proxy_step:?}"); + if ![PluginStep::Request, PluginStep::ProxyUpstream].contains(&proxy_step) { + return Err(Error::Invalid { + category: PluginCategory::BasicAuth.to_string(), + message: "Basic auth plugin should be executed at request or proxy upstream step" + .to_string(), + }); + } let mut authorizations = vec![]; for item in value.split(' ') { - let _ = STANDARD - .decode(item) - .map_err(|e| Error::Base64Decode { source: e })?; + let _ = STANDARD.decode(item).map_err(|e| Error::Base64Decode { + category: PluginCategory::BasicAuth.to_string(), + source: e, + })?; authorizations.push(format!("Basic {item}").as_bytes().to_owned()); } diff --git a/src/plugin/cache.rs b/src/plugin/cache.rs index ee000c82..12cc6025 100644 --- a/src/plugin/cache.rs +++ b/src/plugin/cache.rs @@ -66,7 +66,14 @@ pub struct Cache { impl Cache { pub fn new(value: &str, proxy_step: PluginStep) -> Result { debug!("new cache storage proxy plugin, {value}, {proxy_step:?}"); + if proxy_step != PluginStep::Request { + return Err(Error::Invalid { + category: PluginCategory::Cache.to_string(), + message: "Cache plugin should be executed at request step".to_string(), + }); + } let url_info = Url::parse(value).map_err(|e| Error::Invalid { + category: PluginCategory::Cache.to_string(), message: e.to_string(), })?; let mut lock = 0; diff --git a/src/plugin/compression.rs b/src/plugin/compression.rs index 8c31f67d..81359b37 100644 --- a/src/plugin/compression.rs +++ b/src/plugin/compression.rs @@ -38,6 +38,13 @@ pub struct Compression { impl Compression { pub fn new(value: &str, proxy_step: PluginStep) -> Result { debug!("new compresson proxy plugin, {value}, {proxy_step:?}"); + if ![PluginStep::Request, PluginStep::ProxyUpstream].contains(&proxy_step) { + return Err(Error::Invalid { + category: PluginCategory::Compression.to_string(), + message: "Compression plugin should be executed at request or proxy upstream step" + .to_string(), + }); + } let mut levels: [u32; 3] = [0, 0, 0]; let mut support_compression = false; @@ -45,9 +52,10 @@ impl Compression { if index >= levels.len() { break; } - let level = item - .parse::() - .map_err(|e| Error::ParseInt { source: e })?; + let level = item.parse::().map_err(|e| Error::ParseInt { + category: PluginCategory::Compression.to_string(), + source: e, + })?; if level > 0 { support_compression = true; levels[index] = level; diff --git a/src/plugin/directory.rs b/src/plugin/directory.rs index 47915015..ecf4ac07 100644 --- a/src/plugin/directory.rs +++ b/src/plugin/directory.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{ProxyPlugin, Result}; +use super::{Error, ProxyPlugin, Result}; use crate::config::{PluginCategory, PluginStep}; use crate::http_extra::{HttpChunkResponse, HttpHeader, HttpResponse}; use crate::state::State; @@ -21,6 +21,7 @@ use async_trait::async_trait; use bytesize::ByteSize; use glob::glob; use http::{header, HeaderValue, StatusCode}; +use humantime::parse_duration; use log::{debug, error}; use once_cell::sync::Lazy; use pingora::proxy::Session; @@ -96,6 +97,8 @@ pub struct Directory { // charset for text file charset: Option, proxy_step: PluginStep, + // support download + download: bool, } async fn get_data(file: &PathBuf) -> std::io::Result<(std::fs::Metadata, fs::File)> { @@ -144,6 +147,14 @@ impl Directory { /// Creates a new directory upstream, which will serve static file of directory. pub fn new(value: &str, proxy_step: PluginStep) -> Result { debug!("new serve static file proxy plugin, {value}, {proxy_step:?}"); + if ![PluginStep::Request, PluginStep::ProxyUpstream].contains(&proxy_step) { + return Err(Error::Invalid { + category: PluginCategory::Directory.to_string(), + message: + "Directory serve plugin should be executed at request or proxy upstream step" + .to_string(), + }); + } let mut new_path = value.to_string(); let mut chunk_size = None; let mut max_age = None; @@ -151,6 +162,7 @@ impl Directory { let mut index_file = "index.html".to_string(); let mut charset = None; let mut autoindex = false; + let mut download = false; let file_protocol = "file://"; if !new_path.starts_with(file_protocol) { new_path = format!("{file_protocol}{new_path}").to_string(); @@ -170,14 +182,15 @@ impl Directory { } } "max_age" => { - if let Ok(v) = value.parse::() { - max_age = Some(v); + if let Ok(v) = parse_duration(&value) { + max_age = Some(v.as_secs() as u32); } } - "autoindex" => autoindex = true, - "private" => cache_private = Some(true), "index" => index_file = value.to_string(), "charset" => charset = Some(value.to_string()), + "autoindex" => autoindex = true, + "private" => cache_private = Some(true), + "download" => download = true, _ => {} } } @@ -194,6 +207,7 @@ impl Directory { charset, cache_private, proxy_step, + download, }) } } @@ -211,7 +225,9 @@ fn get_autoindex_html(path: &Path) -> Result { let filepath = f.to_string_lossy(); let mut size = "".to_string(); let mut last_modified = "".to_string(); + let mut is_file = false; if f.is_file() { + is_file = true; let _ = f.metadata().map(|meta| { size = ByteSize(meta.size()).to_string(); last_modified = chrono::DateTime::from_timestamp(meta.mtime(), 0) @@ -226,7 +242,10 @@ fn get_autoindex_html(path: &Path) -> Result { continue; } - let target = format!("./{}", filepath.substring(path.len(), filepath.len())); + let mut target = format!("./{}", filepath.split('/').last().unwrap_or_default()); + if !is_file { + target += "/"; + } file_list_html.push(format!( r###" {name} @@ -272,10 +291,20 @@ impl ProxyPlugin for Directory { return Ok(Some(resp)); } + // Content-Disposition: attachment; filename="example.pdf" + let resp = match get_data(&file).await { Ok((meta, mut f)) => { - let (cacheable, size, headers) = + let (cacheable, size, mut headers) = get_cacheable_and_headers_from_meta(&file, &meta, &self.charset); + if self.download { + if let Ok(value) = HeaderValue::from_str(&format!( + r###"attachment; filename="{}""###, + file.file_name().unwrap_or_default().to_string_lossy() + )) { + headers.push((header::CONTENT_DISPOSITION, value)); + } + } // 4kb if size <= 4096 { let mut buffer = vec![0; size]; @@ -332,7 +361,7 @@ mod tests { #[test] fn test_new_directory() { let dir = Directory::new( - "~/Downloads?chunk_size=1024&max_age=3600&private&index=pingap/index.html", + "~/Downloads?chunk_size=1024&max_age=1h&private&index=pingap/index.html", PluginStep::Request, ) .unwrap(); diff --git a/src/plugin/ip_limit.rs b/src/plugin/ip_limit.rs index b8792f15..02daf5ce 100644 --- a/src/plugin/ip_limit.rs +++ b/src/plugin/ip_limit.rs @@ -12,8 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::ProxyPlugin; -use super::Result; +use super::{Error, ProxyPlugin, Result}; use crate::config::PluginCategory; use crate::config::PluginStep; use crate::http_extra::HttpResponse; @@ -39,6 +38,13 @@ pub struct IpLimit { impl IpLimit { pub fn new(value: &str, proxy_step: PluginStep) -> Result { debug!("new ip limit proxy plugin, {value}, {proxy_step:?}"); + if ![PluginStep::Request, PluginStep::ProxyUpstream].contains(&proxy_step) { + return Err(Error::Invalid { + category: PluginCategory::IpLimit.to_string(), + message: "Ip limit plugin should be executed at request or proxy upstream step" + .to_string(), + }); + } let arr: Vec<&str> = value.split(' ').collect(); let ip = arr[0].trim().to_string(); let mut category = 0; diff --git a/src/plugin/key_auth.rs b/src/plugin/key_auth.rs index 7ca0bc9c..0959479b 100644 --- a/src/plugin/key_auth.rs +++ b/src/plugin/key_auth.rs @@ -14,8 +14,7 @@ use super::ProxyPlugin; use super::{Error, Result}; -use crate::config::PluginCategory; -use crate::config::PluginStep; +use crate::config::{PluginCategory, PluginStep}; use crate::http_extra::HttpResponse; use crate::state::State; use crate::util; @@ -40,9 +39,17 @@ pub struct KeyAuth { impl KeyAuth { pub fn new(value: &str, proxy_step: PluginStep) -> Result { debug!("new key auth proxy plugin, {value}, {proxy_step:?}"); + if ![PluginStep::Request, PluginStep::ProxyUpstream].contains(&proxy_step) { + return Err(Error::Invalid { + category: PluginCategory::KeyAuth.to_string(), + message: "Key auth plugin should be executed at request or proxy upstream step" + .to_string(), + }); + } let arr: Vec<&str> = value.split(' ').collect(); if arr.len() != 2 { return Err(Error::Invalid { + category: PluginCategory::KeyAuth.to_string(), message: "Value for key auth is invalid".to_string(), }); } @@ -55,6 +62,7 @@ impl KeyAuth { query_name = Some(name.substring(1, name.len()).to_string()); } else { header_name = Some(HeaderName::from_str(name).map_err(|e| Error::Invalid { + category: PluginCategory::KeyAuth.to_string(), message: format!("invalid header name, {e}"), })?); } diff --git a/src/plugin/limit.rs b/src/plugin/limit.rs index 7a237883..579cc376 100644 --- a/src/plugin/limit.rs +++ b/src/plugin/limit.rs @@ -47,7 +47,15 @@ pub struct Limiter { impl Limiter { pub fn new(value: &str, proxy_step: PluginStep) -> Result { debug!("new limit proxy plugin, {value}, {proxy_step:?}"); + if ![PluginStep::Request, PluginStep::ProxyUpstream].contains(&proxy_step) { + return Err(Error::Invalid { + category: PluginCategory::Limit.to_string(), + message: "Limit plugin should be executed at request or proxy upstream step" + .to_string(), + }); + } let (category, limit_value) = value.split_once(' ').ok_or(Error::Invalid { + category: PluginCategory::Limit.to_string(), message: value.to_string(), })?; @@ -57,6 +65,7 @@ impl Limiter { let mut interval = Duration::from_secs(10); for item in limit_value.split('&') { let (key, value) = item.split_once('=').ok_or(Error::Invalid { + category: PluginCategory::Limit.to_string(), message: item.to_string(), })?; match key { @@ -71,11 +80,13 @@ impl Limiter { "value" => key_value = value.to_string(), "max" => { max = value.parse::().map_err(|e| Error::Invalid { + category: PluginCategory::Limit.to_string(), message: e.to_string(), })?; } "interval" => { interval = parse_duration(value).map_err(|e| Error::Invalid { + category: PluginCategory::Limit.to_string(), message: e.to_string(), })?; } @@ -136,6 +147,7 @@ impl Limiter { }; if value > self.max { return Err(Error::Exceed { + category: PluginCategory::Limit.to_string(), max: self.max, value, }); diff --git a/src/plugin/mock.rs b/src/plugin/mock.rs index bfa2d2db..7d5efb4e 100644 --- a/src/plugin/mock.rs +++ b/src/plugin/mock.rs @@ -40,7 +40,17 @@ impl MockResponse { /// Creates a new mock response upstream, which will return a mock data. pub fn new(value: &str, proxy_step: PluginStep) -> Result { debug!("new mock proxy plugin, {value}, {proxy_step:?}"); - let info: MockInfo = serde_json::from_str(value).map_err(|e| Error::Json { source: e })?; + if ![PluginStep::Request, PluginStep::ProxyUpstream].contains(&proxy_step) { + return Err(Error::Invalid { + category: PluginCategory::Mock.to_string(), + message: "Mock plugin should be executed at request or proxy upstream step" + .to_string(), + }); + } + let info: MockInfo = serde_json::from_str(value).map_err(|e| Error::Json { + category: PluginCategory::Mock.to_string(), + source: e, + })?; let mut resp = HttpResponse { status: StatusCode::OK, diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs index 016a8752..e0af10f3 100644 --- a/src/plugin/mod.rs +++ b/src/plugin/mod.rs @@ -40,16 +40,29 @@ mod stats; #[derive(Debug, Snafu)] pub enum Error { - #[snafu(display("Invalid {message}"))] - Invalid { message: String }, - #[snafu(display("Parse int {source}"))] - ParseInt { source: ParseIntError }, - #[snafu(display("Exceed limit {value}/{max}"))] - Exceed { max: isize, value: isize }, - #[snafu(display("Json parse error {source}"))] - Json { source: serde_json::Error }, - #[snafu(display("Base64 decode error {source}"))] - Base64Decode { source: base64::DecodeError }, + #[snafu(display("Plugin {category}, invalid {message}"))] + Invalid { category: String, message: String }, + #[snafu(display("Plugin {category}, parse int {source}"))] + ParseInt { + category: String, + source: ParseIntError, + }, + #[snafu(display("Plugin {category}, exceed limit {value}/{max}"))] + Exceed { + category: String, + max: isize, + value: isize, + }, + #[snafu(display("Plugin {category}, json parse error {source}"))] + Json { + category: String, + source: serde_json::Error, + }, + #[snafu(display("Plugin {category}, base64 decode error {source}"))] + Base64Decode { + category: String, + source: base64::DecodeError, + }, } type Result = std::result::Result; diff --git a/src/plugin/ping.rs b/src/plugin/ping.rs index ed17f931..73006e33 100644 --- a/src/plugin/ping.rs +++ b/src/plugin/ping.rs @@ -12,10 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::ProxyPlugin; -use super::Result; -use crate::config::PluginCategory; -use crate::config::PluginStep; +use super::{Error, ProxyPlugin, Result}; +use crate::config::{PluginCategory, PluginStep}; use crate::http_extra::HttpResponse; use crate::state::State; use async_trait::async_trait; @@ -36,6 +34,12 @@ static PONG_RESPONSE: Lazy = Lazy::new(|| HttpResponse { impl Ping { pub fn new(value: &str, proxy_step: PluginStep) -> Result { + if proxy_step != PluginStep::Request { + return Err(Error::Invalid { + category: PluginCategory::Ping.to_string(), + message: "Ping plugin should be executed at request step".to_string(), + }); + } let mut prefix = "".to_string(); if value.trim().len() > 1 { prefix = value.trim().to_string(); diff --git a/src/plugin/redirect_https.rs b/src/plugin/redirect_https.rs index 27d7fa70..df3947c4 100644 --- a/src/plugin/redirect_https.rs +++ b/src/plugin/redirect_https.rs @@ -12,10 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::ProxyPlugin; -use super::Result; -use crate::config::PluginCategory; -use crate::config::PluginStep; +use super::{Error, ProxyPlugin, Result}; +use crate::config::{PluginCategory, PluginStep}; use crate::http_extra::convert_headers; use crate::http_extra::HttpResponse; use crate::state::State; @@ -30,6 +28,12 @@ pub struct RedirectHttps { impl RedirectHttps { pub fn new(value: &str, proxy_step: PluginStep) -> Result { + if proxy_step != PluginStep::Request { + return Err(Error::Invalid { + category: PluginCategory::RedirectHttps.to_string(), + message: "Redirect https plugin should be executed at request step".to_string(), + }); + } let mut prefix = "".to_string(); if value.trim().len() > 1 { prefix = value.trim().to_string(); diff --git a/src/plugin/request_id.rs b/src/plugin/request_id.rs index c48f8b31..b8fa4edc 100644 --- a/src/plugin/request_id.rs +++ b/src/plugin/request_id.rs @@ -12,10 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::ProxyPlugin; -use super::Result; -use crate::config::PluginCategory; -use crate::config::PluginStep; +use super::{Error, ProxyPlugin, Result}; +use crate::config::{PluginCategory, PluginStep}; use crate::http_extra::HttpResponse; use crate::http_extra::HTTP_HEADER_NAME_X_REQUEST_ID; use crate::state::State; @@ -34,6 +32,13 @@ pub struct RequestId { impl RequestId { pub fn new(value: &str, proxy_step: PluginStep) -> Result { debug!("new request id proxy plugin, {value}, {proxy_step:?}"); + if ![PluginStep::Request, PluginStep::ProxyUpstream].contains(&proxy_step) { + return Err(Error::Invalid { + category: PluginCategory::RequestId.to_string(), + message: "Request id should be executed at request or proxy upstream step" + .to_string(), + }); + } let arr: Vec<&str> = value.split(' ').collect(); let algorithm = arr[0].trim().to_string(); let mut size = 8; diff --git a/src/plugin/response_headers.rs b/src/plugin/response_headers.rs index efde827d..0539623d 100644 --- a/src/plugin/response_headers.rs +++ b/src/plugin/response_headers.rs @@ -1,5 +1,3 @@ -use std::str::FromStr; - // Copyright 2024 Tree xie. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +20,7 @@ use http::header::HeaderName; use log::debug; use pingora::http::ResponseHeader; use pingora::proxy::Session; +use std::str::FromStr; use substring::Substring; pub struct ResponseHeaders { @@ -34,6 +33,13 @@ pub struct ResponseHeaders { impl ResponseHeaders { pub fn new(value: &str, proxy_step: PluginStep) -> Result { debug!("new stats proxy plugin, {value}, {proxy_step:?}"); + if proxy_step != PluginStep::UpstreamResponse { + return Err(Error::Invalid { + category: PluginCategory::ResponseHeaders.to_string(), + message: "Response headers plugin should be executed at upstream response step" + .to_string(), + }); + } let mut add_headers = vec![]; let mut remove_headers = vec![]; let mut set_headers = vec![]; @@ -47,6 +53,7 @@ impl ResponseHeaders { match first { '+' => { let header = convert_header(last).map_err(|e| Error::Invalid { + category: PluginCategory::ResponseHeaders.to_string(), message: e.to_string(), })?; if let Some(item) = header { @@ -55,12 +62,14 @@ impl ResponseHeaders { } '-' => { let name = HeaderName::from_str(last).map_err(|e| Error::Invalid { + category: PluginCategory::ResponseHeaders.to_string(), message: e.to_string(), })?; remove_headers.push(name); } _ => { let header = convert_header(item).map_err(|e| Error::Invalid { + category: PluginCategory::ResponseHeaders.to_string(), message: e.to_string(), })?; if let Some(item) = header { diff --git a/src/plugin/stats.rs b/src/plugin/stats.rs index e1cae64b..53e5069f 100644 --- a/src/plugin/stats.rs +++ b/src/plugin/stats.rs @@ -12,12 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::time::Duration; - -use super::ProxyPlugin; -use super::Result; -use crate::config::PluginCategory; -use crate::config::PluginStep; +use super::{Error, ProxyPlugin, Result}; +use crate::config::{PluginCategory, PluginStep}; use crate::http_extra::{HttpResponse, HTTP_HEADER_CONTENT_JSON}; use crate::state::{get_hostname, get_start_time, State}; use crate::util; @@ -28,6 +24,7 @@ use log::debug; use memory_stats::memory_stats; use pingora::proxy::Session; use serde::Serialize; +use std::time::Duration; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -50,6 +47,13 @@ pub struct Stats { impl Stats { pub fn new(value: &str, proxy_step: PluginStep) -> Result { debug!("new stats proxy plugin, {value}, {proxy_step:?}"); + if ![PluginStep::Request, PluginStep::ProxyUpstream].contains(&proxy_step) { + return Err(Error::Invalid { + category: PluginCategory::Stats.to_string(), + message: "Stats plugin should be executed at request or proxy upstream step" + .to_string(), + }); + } Ok(Self { proxy_step, path: value.to_string(), diff --git a/web/src/components/form-editor.tsx b/web/src/components/form-editor.tsx index fe877ee0..91e89609 100644 --- a/web/src/components/form-editor.tsx +++ b/web/src/components/form-editor.tsx @@ -52,6 +52,7 @@ export enum FormItemCategory { WEBHOOK_TYPE = "webhookType", WEBHOOK_NOTIFICATIONS = "webhookNotifications", PLUGIN = "plugin", + PLUGIN_STEP = "pluginStep", PLUGIN_SELECT = "pluginSelect", } @@ -113,7 +114,11 @@ export function formatPluginCategory(value: string) { case PluginCategory.PING: { return "ping"; } + case PluginCategory.RESPONSE_HEADERS: { + return "responseHeaders"; + } } + return ""; } export interface CheckBoxItem { @@ -165,6 +170,7 @@ function FormProxyPluginField({ const arr: string[] = []; const fields: { label: string; + options?: string[] | CheckBoxItem[]; }[] = []; const padding = " "; @@ -222,7 +228,8 @@ function FormProxyPluginField({ arr.push(...value.split(padding)); fields.push( { - label: t("form.limitKey"), + label: t("form.limitCategory"), + options: ["rate", "inflight"], }, { label: t("form.limitValue"), @@ -242,6 +249,7 @@ function FormProxyPluginField({ fields.push( { label: t("form.algoForId"), + options: ["uuid", "nanoid"], }, { label: t("form.lengthForId"), @@ -257,6 +265,18 @@ function FormProxyPluginField({ }, { label: t("form.limitMode"), + options: [ + { + label: t("form.allow"), + value: "0", + option: 0, + }, + { + label: t("form.deny"), + value: "1", + option: 1, + }, + ], }, ); break; @@ -474,6 +494,32 @@ function FormProxyPluginField({ ); } const items = fields.map((item, index) => { + if (item.options) { + return ( + + + { + const arr = newValues.slice(0); + arr[index] = value; + onUpdate(arr.join(padding)); + setNewValues(arr); + }} + /> + + + ); + } return ( void; @@ -541,11 +587,21 @@ function FormSelectField({ }} input={} > - {opts.map((name) => ( - - {name} - - ))} + {opts.map((name) => { + if (typeof name == "string") { + return ( + + {name} + + ); + } + let opt = name as CheckBoxItem; + return ( + + {opt.label} + + ); + })} ); @@ -692,6 +748,53 @@ function FormTwoInputFields({ return {list}; } +function getPluginSteps(category: string) { + const defaultPluginSteps = [ + { + label: "Request", + option: 0, + value: "request", + }, + { + label: "Proxy Upstream", + option: 1, + value: "proxy_upstream", + }, + { + label: "Upstream Response", + option: 2, + value: "upstream_response", + }, + ]; + + const pluginSupportSteps: Record = {}; + pluginSupportSteps[PluginCategory.STATS] = [0, 1]; + pluginSupportSteps[PluginCategory.LIMIT] = [0, 1]; + pluginSupportSteps[PluginCategory.COMPRESSION] = [0, 1]; + pluginSupportSteps[PluginCategory.ADMIN] = [0, 1]; + pluginSupportSteps[PluginCategory.DIRECTORY] = [0, 1]; + pluginSupportSteps[PluginCategory.MOCK] = [0, 1]; + pluginSupportSteps[PluginCategory.REQUEST_ID] = [0, 1]; + pluginSupportSteps[PluginCategory.IP_LIMIT] = [0, 1]; + pluginSupportSteps[PluginCategory.KEY_AUTH] = [0, 1]; + pluginSupportSteps[PluginCategory.BASIC_AUTH] = [0, 1]; + pluginSupportSteps[PluginCategory.CACHE] = [0]; + pluginSupportSteps[PluginCategory.REDIRECT_HTTPS] = [0]; + pluginSupportSteps[PluginCategory.PING] = [0]; + pluginSupportSteps[PluginCategory.RESPONSE_HEADERS] = [2]; + + const steps = pluginSupportSteps[category]; + if (steps) { + const arr = defaultPluginSteps.filter((item) => { + return steps.indexOf(item.option) !== -1; + }); + return arr; + } + return defaultPluginSteps; +} + +// TODO WEB管理界面流程后续优化,暂时仅保证可用 +// 后续调整模块化 export default function FormEditor({ title, description, @@ -714,6 +817,9 @@ export default function FormEditor({ const theme = useTheme(); const [data, setData] = React.useState(getDefaultValues(items)); const [openRemoveDialog, setOpenRemoveDialog] = React.useState(false); + const [pluginCategory, setPluginCategory] = React.useState( + (data["category"] as string) || "", + ); const defaultLocations: string[] = []; const defaultProxyPluginSelected: string[] = []; @@ -765,6 +871,8 @@ export default function FormEditor({ const list = items.map((item) => { let formItem: JSX.Element = <>; switch (item.category) { + case FormItemCategory.PLUGIN_STEP: + item.options = getPluginSteps(pluginCategory || PluginCategory.STATS); case FormItemCategory.CHECKBOX: { let options = (item.options as CheckBoxItem[]) || []; let defaultValue = 0; @@ -1015,7 +1123,13 @@ export default function FormEditor({ ); return ( - {plugin} + + {plugin} + ); }); @@ -1099,6 +1213,7 @@ export default function FormEditor({ values[key] = value; setUpdated(true); setData(values); + setPluginCategory((values["category"] as string) || ""); setTimeout(() => { setShowSuccess(false); }, 6000); diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 75a7eed0..b6547b5f 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -108,7 +108,7 @@ export default { "form.header": "Add response header", "form.setHeader": "Set response header", "form.removeHeader": - "Remove response header, multiple values are separated by spaces", + "Remove response header, multiple values are separated by space", "form.headerName": "Header Name", "form.headerValue": "Header Value", "form.addProxyHeader": "Add proxy request header", @@ -118,16 +118,19 @@ export default { "form.br": "Br Level", "form.zstd": "Zstd Level", "form.adminPath": "Admin Path", - "form.basicAuth": "Basic auth(base64(user:pass))", - "form.limitKey": "The limit key", + "form.basicAuth": + "Basic auth(base64(user:pass)), multiple values are separated by ','", + "form.limitCategory": "The limit category", "form.limitValue": "The limit value", + "form.allow": "Allow", + "form.deny": "Deny", "form.staticDirectory": "The static directory", "form.algoForId": "The algorithm for genenrate id", "form.lengthForId": "The length of id", "form.ipList": "The ip list", "form.limitMode": "The limit mode, 0:allow, 1:deny", "form.keyName": "The key name", - "form.keyValues": "The key value list", + "form.keyValues": "The key value list, multiple values are separated by ','", "form.basicAuthList": "The basic authorization list", "form.cacheStorage": "The cache storage url", "form.redirectPrefix": "The prefix path of redirect path", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index cb428ac3..b76dbb5c 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -117,16 +117,18 @@ export default { "form.br": "Br的压缩级别", "form.zstd": "Zstd的压缩级别", "form.adminPath": "管理后台的路径", - "form.basicAuth": "Basic auth(base64(user:pass))", - "form.limitKey": "限流对应的key", - "form.limitValue": "限流的最大值", + "form.basicAuth": "Basic auth(base64(user:pass)),多个值用','分隔", + "form.limitCategory": "限流类型", + "form.limitValue": "限流的相关限制", + "form.allow": "允许", + "form.deny": "禁止", "form.staticDirectory": "静态文件目录", "form.algoForId": "生成id的算法", "form.lengthForId": "生成的id长度", "form.ipList": "IP列表", "form.limitMode": "限制模式, 0:允许, 1:禁止", "form.keyName": "Key的名称", - "form.keyValues": "Key值的列表", + "form.keyValues": "Key值的列表,多个值用','分隔", "form.basicAuthList": "Basic auth的认证列表", "form.cacheStorage": "缓存的配置", "form.redirectPrefix": "重定向时使用的前缀", diff --git a/web/src/pages/plugin-info.tsx b/web/src/pages/plugin-info.tsx index 5a10f5eb..3d9fd484 100644 --- a/web/src/pages/plugin-info.tsx +++ b/web/src/pages/plugin-info.tsx @@ -34,30 +34,6 @@ export default function ProxyPluginInfo() { const currentNames = Object.keys(proxyPlugins); const arr: FormItem[] = [ - { - id: "step", - label: t("plugin.step"), - defaultValue: proxyPlugin.step, - category: FormItemCategory.CHECKBOX, - span: 6, - options: [ - { - label: "Request", - option: 0, - value: "request", - }, - { - label: "Proxy Upstream", - option: 1, - value: "proxy_upstream", - }, - { - label: "Upstream Response", - option: 2, - value: "upstream_response", - }, - ], - }, { id: "category", label: t("plugin.category"), @@ -137,6 +113,13 @@ export default function ProxyPluginInfo() { }, ], }, + { + id: "step", + label: t("plugin.step"), + defaultValue: proxyPlugin.step, + category: FormItemCategory.PLUGIN_STEP, + span: 6, + }, { id: "value", label: t("plugin.config"), diff --git a/web/src/states/config.ts b/web/src/states/config.ts index a8ab927c..b6e41664 100644 --- a/web/src/states/config.ts +++ b/web/src/states/config.ts @@ -64,7 +64,7 @@ interface Server { interface Plugin { value: string; category: string; - step?: number; + step?: string; remark?: string; }