pub mod executor;
pub mod stream;
use crate::engine::interfaces::{
HttpMiddleware, L7Middleware, MiddlewareOutput, ParamDef, ParamType, Plugin, ResolvedInputs,
};
use crate::layers::l7::container::Container;
use anyhow::Result;
use async_trait::async_trait;
use executor::CgiConfig;
use serde_json::Value;
use std::{any::Any, borrow::Cow};
pub struct CgiPlugin;
impl Plugin for CgiPlugin {
fn name(&self) -> &str {
"internal.driver.cgi"
}
fn params(&self) -> Vec<ParamDef> {
vec![
ParamDef {
name: Cow::Borrowed("command"),
required: true,
param_type: ParamType::String,
},
ParamDef {
name: Cow::Borrowed("script"),
required: false,
param_type: ParamType::String,
},
ParamDef {
name: Cow::Borrowed("timeout"),
required: false,
param_type: ParamType::Integer,
},
ParamDef {
name: Cow::Borrowed("method"),
required: false,
param_type: ParamType::String,
},
ParamDef {
name: Cow::Borrowed("uri"),
required: true,
param_type: ParamType::String,
},
ParamDef {
name: Cow::Borrowed("query"),
required: false,
param_type: ParamType::String,
},
ParamDef {
name: Cow::Borrowed("remote_addr"),
required: false,
param_type: ParamType::String,
},
ParamDef {
name: Cow::Borrowed("remote_port"),
required: false,
param_type: ParamType::String,
},
ParamDef {
name: Cow::Borrowed("server_port"),
required: false,
param_type: ParamType::String,
},
ParamDef {
name: Cow::Borrowed("server_name"),
required: false,
param_type: ParamType::String,
},
ParamDef {
name: Cow::Borrowed("doc_root"),
required: false,
param_type: ParamType::String,
},
ParamDef {
name: Cow::Borrowed("path_info"),
required: false,
param_type: ParamType::String,
},
ParamDef {
name: Cow::Borrowed("script_name"),
required: false,
param_type: ParamType::String,
},
]
}
fn supported_protocols(&self) -> Vec<Cow<'static, str>> {
vec!["httpx".into()]
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_http_middleware(&self) -> Option<&dyn HttpMiddleware> {
Some(self)
}
fn as_l7_middleware(&self) -> Option<&dyn L7Middleware> {
Some(self)
}
}
#[async_trait]
impl HttpMiddleware for CgiPlugin {
fn output(&self) -> Vec<Cow<'static, str>> {
vec![Cow::Borrowed("success"), Cow::Borrowed("failure")]
}
async fn execute(
&self,
context: &mut (dyn Any + Send),
inputs: ResolvedInputs,
) -> Result<MiddlewareOutput> {
let container = context
.downcast_mut::<Container>()
.ok_or_else(|| anyhow::anyhow!("Context is not a Container"))?;
let get_str = |key: &str| -> String {
inputs
.get(key)
.and_then(Value::as_str)
.unwrap_or("")
.to_owned()
};
let command = get_str("command");
if command.is_empty() {
return Err(anyhow::anyhow!("CGI: 'command' param is required"));
}
let raw_uri = inputs
.get("uri")
.and_then(Value::as_str)
.ok_or_else(|| anyhow::anyhow!("CGI: 'uri' param is required"))?;
let raw_query = get_str("query");
let (final_uri, final_query) = if !raw_query.is_empty() {
let clean_path = raw_uri.split_once('?').map(|(p, _)| p).unwrap_or(raw_uri);
(clean_path.to_owned(), raw_query)
} else {
match raw_uri.split_once('?') {
Some((path, query)) => (path.to_owned(), query.to_owned()),
None => (raw_uri.to_owned(), String::new()),
}
};
let mut script_name = get_str("script_name");
let mut path_info = get_str("path_info");
if path_info.is_empty() && !script_name.is_empty() {
let (derived_script, derived_path) = derive_path_info(&final_uri, &script_name);
script_name = derived_script;
path_info = derived_path;
}
let config = CgiConfig {
command,
script: get_str("script"),
timeout: inputs.get("timeout").and_then(Value::as_u64).unwrap_or(30),
method: inputs
.get("method")
.and_then(Value::as_str)
.unwrap_or("GET")
.to_owned(),
uri: final_uri,
query: final_query,
remote_addr: get_str("remote_addr"),
remote_port: get_str("remote_port"),
server_port: get_str("server_port"),
server_name: get_str("server_name"),
doc_root: get_str("doc_root"),
path_info, script_name,
};
executor::execute(container, config).await
}
}
#[async_trait]
impl L7Middleware for CgiPlugin {
fn output(&self) -> Vec<Cow<'static, str>> {
<Self as HttpMiddleware>::output(self)
}
async fn execute_l7(
&self,
context: &mut (dyn Any + Send),
inputs: ResolvedInputs,
) -> Result<MiddlewareOutput> {
<Self as HttpMiddleware>::execute(self, context, inputs).await
}
}
fn derive_path_info(uri: &str, script_name: &str) -> (String, String) {
if script_name.is_empty() {
return (String::new(), uri.to_owned());
}
let normalize = |p: &str| -> String {
let mut res = String::with_capacity(p.len());
let mut last_slash = false;
for c in p.chars() {
if c == '/' {
if !last_slash {
res.push(c);
}
last_slash = true;
} else {
res.push(c);
last_slash = false;
}
}
if !res.starts_with('/') {
res.insert(0, '/');
}
res
};
let norm_uri = normalize(uri);
let norm_script = normalize(script_name);
let match_base = if norm_script.len() > 1 && norm_script.ends_with('/') {
&norm_script[..norm_script.len() - 1]
} else {
&norm_script
};
if let Some(remainder) = norm_uri.strip_prefix(match_base) {
if remainder.is_empty() {
return (match_base.to_owned(), String::new());
} else if remainder.starts_with('/') {
return (match_base.to_owned(), remainder.to_owned());
} else if match_base == "/" {
return (
"/".to_owned(),
format!("/{}", remainder.trim_start_matches('/')),
);
}
}
(String::new(), norm_uri)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_derive_path_info() {
assert_eq!(
derive_path_info("/cgi-bin/script", "/cgi-bin/script"),
("/cgi-bin/script".to_string(), "".to_string())
);
assert_eq!(
derive_path_info("/cgi-bin/script/foo/bar", "/cgi-bin/script"),
("/cgi-bin/script".to_string(), "/foo/bar".to_string())
);
assert_eq!(
derive_path_info("/cgi-bin/script", "/cgi"),
("".to_string(), "/cgi-bin/script".to_string())
);
assert_eq!(
derive_path_info("/foo/bar", "/"),
("/".to_string(), "/foo/bar".to_string())
);
assert_eq!(
derive_path_info("/foo/bar", ""),
("".to_string(), "/foo/bar".to_string())
);
assert_eq!(
derive_path_info("/api/v1", "/cgi"),
("".to_string(), "/api/v1".to_string())
);
assert_eq!(
derive_path_info("//cgi-bin//script", "/cgi-bin"),
("/cgi-bin".to_string(), "/script".to_string())
);
}
}