1use humantime::parse_duration;
16use pingap_config::PluginConf;
17use pingap_core::PluginStep;
18use snafu::Snafu;
19use std::fmt::Write;
20use std::str::FromStr;
21use std::time::Duration;
22
23#[derive(Debug, Snafu)]
24pub enum Error {
25 #[snafu(display("Plugin {category} invalid, message: {message}"))]
26 Invalid { category: String, message: String },
27 #[snafu(display("Plugin {category} not found"))]
28 NotFound { category: String },
29 #[snafu(display("Plugin {category}, base64 decode error {source}"))]
30 Base64Decode {
31 category: String,
32 source: base64::DecodeError,
33 },
34 #[snafu(display("Plugin {category}, exceed limit {value}/{max}"))]
35 Exceed {
36 category: String,
37 max: f64,
38 value: f64,
39 },
40 #[snafu(display("Plugin {category}, regex error {source}"))]
41 Regex {
42 category: String,
43 source: Box<fancy_regex::Error>,
44 },
45 #[snafu(display("Plugin {category}, base64 decode error {source}"))]
46 ParseDuration {
47 category: String,
48 source: humantime::DurationError,
49 },
50}
51
52pub fn get_str_conf(value: &PluginConf, key: &str) -> String {
54 value
55 .get(key)
56 .and_then(|v| v.as_str())
57 .unwrap_or("")
58 .to_string()
59}
60
61pub fn get_duration_conf(value: &PluginConf, key: &str) -> Option<Duration> {
63 value
64 .get(key)
65 .and_then(|v| v.as_str())
66 .and_then(|s| parse_duration(s).ok())
67}
68
69pub fn get_str_slice_conf(value: &PluginConf, key: &str) -> Vec<String> {
70 value
71 .get(key)
72 .and_then(|v| v.as_array())
73 .map(|arr| {
74 arr.iter()
75 .filter_map(|item| item.as_str())
76 .map(String::from) .collect()
78 })
79 .unwrap_or_default()
80}
81
82pub(crate) fn get_bool_conf(value: &PluginConf, key: &str) -> bool {
83 value.get(key).and_then(|v| v.as_bool()).unwrap_or(false)
84}
85
86pub fn get_int_conf(value: &PluginConf, key: &str) -> i64 {
87 get_int_conf_or_default(value, key, 0)
88}
89
90pub fn get_int_conf_or_default(
91 value: &PluginConf,
92 key: &str,
93 default_value: i64,
94) -> i64 {
95 value
96 .get(key)
97 .and_then(|v| v.as_integer()) .unwrap_or(default_value)
99}
100
101pub fn get_step_conf(
102 value: &PluginConf,
103 default_value: PluginStep,
104) -> PluginStep {
105 value
106 .get("step")
107 .and_then(|v| v.as_str())
108 .and_then(|s| PluginStep::from_str(s).ok())
109 .unwrap_or(default_value)
110}
111
112pub fn get_step_conf_in(
119 value: &PluginConf,
120 category: &str,
121 default_value: PluginStep,
122 allowed: &[PluginStep],
123) -> Result<PluginStep, Error> {
124 let Some(step) = value.get("step").and_then(|v| v.as_str()) else {
125 return Ok(default_value);
126 };
127 let invalid = || Error::Invalid {
128 category: category.to_string(),
129 message: format!(
130 "Invalid step({step}), expect one of: {}",
131 allowed
132 .iter()
133 .map(|item| item.to_string())
134 .collect::<Vec<_>>()
135 .join(", ")
136 ),
137 };
138 let step = PluginStep::from_str(step).map_err(|_| invalid())?;
139 if !allowed.contains(&step) {
140 return Err(invalid());
141 }
142 Ok(step)
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
151pub(crate) enum RestrictionCategory {
152 #[default]
153 Allow,
154 Deny,
155}
156
157impl RestrictionCategory {
158 #[inline]
161 pub(crate) fn allows(&self, found: bool) -> bool {
162 match self {
163 Self::Allow => found,
164 Self::Deny => !found,
165 }
166 }
167}
168
169pub(crate) fn get_restriction_category_conf(
172 value: &PluginConf,
173 category: &str,
174) -> Result<RestrictionCategory, Error> {
175 match get_str_conf(value, "type").to_lowercase().as_str() {
176 "" | "allow" => Ok(RestrictionCategory::Allow),
177 "deny" => Ok(RestrictionCategory::Deny),
178 other => Err(Error::Invalid {
179 category: category.to_string(),
180 message: format!("Invalid type({other}), expect allow or deny"),
181 }),
182 }
183}
184
185pub(crate) fn accepts_encoding(accept_encoding: &str, coding: &str) -> bool {
192 accept_encoding.split(',').any(|part| {
193 let mut segments = part.split(';');
194 let name = segments.next().unwrap_or_default().trim();
195 if !name.eq_ignore_ascii_case(coding) {
196 return false;
197 }
198 !segments.any(|seg| {
200 let seg = seg.trim();
201 seg.get(..2).is_some_and(|p| p.eq_ignore_ascii_case("q="))
202 && seg[2..].trim().parse::<f32>().is_ok_and(|q| q <= 0.0)
203 })
204 })
205}
206
207pub fn get_hash_key(conf: &PluginConf) -> String {
215 let mut items: Vec<_> = conf.iter().collect();
216 items.sort_unstable_by_key(|(k, _)| *k);
218
219 let mut buf = String::with_capacity(256);
221 for (i, (key, value)) in items.iter().enumerate() {
222 if i > 0 {
223 buf.push('\n');
224 }
225 let _ = write!(&mut buf, "{key}:{value}");
228 }
229
230 let hash = crc32fast::hash(buf.as_bytes());
231 format!("{hash:X}")
232}
233
234macro_rules! register_plugin {
238 ($category:literal, $ty:ty) => {
239 #[::ctor::ctor(unsafe)]
240 fn init() {
241 $crate::get_plugin_factory().register($category, |params| {
242 Ok(::std::sync::Arc::new(<$ty>::new(params)?))
243 });
244 }
245 };
246}
247
248mod accept_encoding;
249mod basic_auth;
250mod cache;
251mod combined_auth;
252mod compression;
253mod cors;
254mod csrf;
255mod directory;
256mod forward_auth;
257#[cfg(feature = "geo")]
258mod geo_restriction;
259mod ip_restriction;
260mod jwt;
261mod key_auth;
262mod limit;
263mod mock;
264mod ping;
265mod redirect;
266mod referer_restriction;
267mod request_id;
268mod response_headers;
269mod sub_filter;
270mod traffic_splitting;
271mod ua_restriction;
272
273mod plugin;
274
275pub use plugin::get_plugin_factory;
276
277#[cfg(test)]
278mod tests {
279 use super::accepts_encoding;
280 use pretty_assertions::assert_eq;
281
282 #[test]
283 fn test_accepts_encoding() {
284 assert_eq!(true, accepts_encoding("gzip, br", "br"));
285 assert_eq!(true, accepts_encoding("gzip, deflate, br;q=0.9", "br"));
286 assert_eq!(true, accepts_encoding("BR", "br"));
287 assert_eq!(false, accepts_encoding("x-gzip", "gzip"));
289 assert_eq!(false, accepts_encoding("gzipx", "gzip"));
290 assert_eq!(false, accepts_encoding("br;q=0", "br"));
292 }
293}