#[cfg(test)]
mod tests;
use std::cmp;
use std::sync::Arc;
use std::time::Instant;
use async_trait::async_trait;
use bytes::Bytes;
use futures_util::{stream, StreamExt, TryStreamExt};
use http_body_util::BodyExt;
use log::{trace, debug, info, warn, error, Level};
use regex::Regex;
use serde::{Serialize, Deserialize};
use crate::core::{
Filter, FilterType, ProxyRequest, ProxyResponse, ProxyError
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingFilterConfig {
#[serde(default = "default_true")]
pub log_request_headers: bool,
#[serde(default = "default_false")]
pub log_request_body: bool,
#[serde(default = "default_true")]
pub log_response_headers: bool,
#[serde(default = "default_false")]
pub log_response_body: bool,
#[serde(default = "default_log_level")]
pub log_level: String,
#[serde(default = "default_max_body_size")]
pub max_body_size: usize,
}
fn default_true() -> bool {
true
}
fn default_false() -> bool {
false
}
fn default_log_level() -> String {
"trace".to_string()
}
fn default_max_body_size() -> usize {
1024 }
impl Default for LoggingFilterConfig {
fn default() -> Self {
Self {
log_request_headers: true,
log_request_body: false,
log_response_headers: true,
log_response_body: false,
log_level: "trace".to_string(),
max_body_size: 1024,
}
}
}
#[derive(Debug)]
pub struct LoggingFilter {
config: LoggingFilterConfig,
}
impl LoggingFilter {
pub fn new(config: LoggingFilterConfig) -> Self {
Self { config }
}
pub fn default() -> Self {
Self::new(LoggingFilterConfig::default())
}
fn get_log_level(&self) -> Level {
match self.config.log_level.to_lowercase().as_str() {
"error" => Level::Error,
"warn" => Level::Warn,
"info" => Level::Info,
"debug" => Level::Debug,
"trace" => Level::Trace,
_ => Level::Trace,
}
}
fn log(&self, message: &str) {
match self.get_log_level() {
Level::Error => error!("{}", message),
Level::Warn => warn!("{}", message),
Level::Info => info!("{}", message),
Level::Debug => debug!("{}", message),
Level::Trace => trace!("{}", message),
}
}
fn format_headers(&self, headers: &reqwest::header::HeaderMap) -> String {
let mut header_lines = Vec::new();
for (name, value) in headers.iter() {
if let Ok(value_str) = value.to_str() {
header_lines.push(format!("{}: {}", name, value_str));
}
}
header_lines.join("\n")
}
fn format_body(&self, body: &[u8]) -> String {
if body.is_empty() {
return "[Empty body]".to_string();
}
let body_size = body.len();
if body_size > self.config.max_body_size {
return format!(
"[Body truncated, showing {}/{} bytes]\n{}",
self.config.max_body_size,
body_size,
String::from_utf8_lossy(&body[0..self.config.max_body_size])
);
}
String::from_utf8_lossy(body).to_string()
}
}
#[async_trait]
impl Filter for LoggingFilter {
fn filter_type(&self) -> FilterType {
FilterType::Both
}
fn name(&self) -> &str {
"logging"
}
async fn pre_filter(&self, mut request: ProxyRequest) -> Result<ProxyRequest, ProxyError> {
if self.config.log_request_headers {
self.log(&format!(">> {} {}", request.method, request.path));
for (k, v) in request.headers.iter() {
self.log(&format!(">> {}: {:?}", k, v));
}
}
if self.config.log_request_body {
let (new_body, snippet) = tee_body(request.body, 1_000).await?;
let truncated = if snippet.len() == 1000 {"(truncated)"} else {""};
self.log(&format!(">> Request Body:\n{}{}", snippet, truncated));
request.body = new_body;
}
Ok(request)
}
async fn post_filter(
&self,
_req: ProxyRequest,
mut response: ProxyResponse,
) -> Result<ProxyResponse, ProxyError> {
if self.config.log_response_headers {
self.log(&format!("<< {}", response.status));
for (k, v) in response.headers.iter() {
self.log(&format!("<< {}: {:?}", k, v));
}
}
if self.config.log_response_body {
let (new_body, snippet) = tee_body(response.body, 1_000).await?;
let truncated = if snippet.len() == 1000 {"(truncated)"} else {""};
self.log(&format!(">> Response Body:\n{}{}", snippet, truncated));
response.body = new_body;
}
Ok(response)
}
}
async fn tee_body(
body: reqwest::Body,
limit: usize,
) -> Result<(reqwest::Body, String), ProxyError> {
let mut stream_in = body.into_data_stream();
let mut captured = Vec::<u8>::with_capacity(limit);
let mut chunks = Vec::new();
while captured.len() < limit {
match stream_in.next().await {
Some(Ok(chunk)) => {
let chunk_clone = chunk.clone();
chunks.push(Ok(chunk));
if captured.len() < limit {
let remaining = limit - captured.len();
let take = cmp::min(remaining, chunk_clone.len());
captured.extend_from_slice(&chunk_clone[..take]);
}
}
Some(Err(e)) => return Err(ProxyError::Other(e.to_string())),
None => break, }
}
let combined_stream = futures_util::stream::iter(chunks)
.chain(stream_in.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)));
let new_body = reqwest::Body::wrap_stream(combined_stream);
let snippet = String::from_utf8_lossy(&captured).to_string();
Ok((new_body, snippet))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HeaderFilterConfig {
#[serde(default)]
pub add_request_headers: std::collections::HashMap<String, String>,
#[serde(default)]
pub remove_request_headers: Vec<String>,
#[serde(default)]
pub add_response_headers: std::collections::HashMap<String, String>,
#[serde(default)]
pub remove_response_headers: Vec<String>,
}
impl Default for HeaderFilterConfig {
fn default() -> Self {
Self {
add_request_headers: std::collections::HashMap::new(),
remove_request_headers: Vec::new(),
add_response_headers: std::collections::HashMap::new(),
remove_response_headers: Vec::new(),
}
}
}
#[derive(Debug)]
pub struct HeaderFilter {
config: HeaderFilterConfig,
}
impl HeaderFilter {
pub fn new(config: HeaderFilterConfig) -> Self {
Self { config }
}
pub fn default() -> Self {
Self::new(HeaderFilterConfig::default())
}
fn apply_headers(&self, headers: &mut reqwest::header::HeaderMap,
add_headers: &std::collections::HashMap<String, String>,
remove_headers: &[String]) {
for header_name in remove_headers {
if let Ok(name) = reqwest::header::HeaderName::from_bytes(header_name.as_bytes()) {
headers.remove(&name);
}
}
for (name, value) in add_headers {
if let (Ok(header_name), Ok(header_value)) = (
reqwest::header::HeaderName::from_bytes(name.as_bytes()),
reqwest::header::HeaderValue::from_str(value)
) {
headers.insert(header_name, header_value);
}
}
}
}
#[async_trait]
impl Filter for HeaderFilter {
fn filter_type(&self) -> FilterType {
FilterType::Both
}
fn name(&self) -> &str {
"header"
}
async fn pre_filter(&self, mut request: ProxyRequest) -> Result<ProxyRequest, ProxyError> {
self.apply_headers(
&mut request.headers,
&self.config.add_request_headers,
&self.config.remove_request_headers
);
Ok(request)
}
async fn post_filter(&self, _request: ProxyRequest, mut response: ProxyResponse) -> Result<ProxyResponse, ProxyError> {
self.apply_headers(
&mut response.headers,
&self.config.add_response_headers,
&self.config.remove_response_headers
);
Ok(response)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeoutFilterConfig {
pub timeout_ms: u64,
}
impl Default for TimeoutFilterConfig {
fn default() -> Self {
Self {
timeout_ms: 30000, }
}
}
#[derive(Debug)]
pub struct TimeoutFilter {
config: TimeoutFilterConfig,
}
impl TimeoutFilter {
pub fn new(config: TimeoutFilterConfig) -> Self {
Self { config }
}
pub fn default() -> Self {
Self::new(TimeoutFilterConfig::default())
}
}
#[async_trait]
impl Filter for TimeoutFilter {
fn filter_type(&self) -> FilterType {
FilterType::Pre
}
fn name(&self) -> &str {
"timeout"
}
async fn pre_filter(&self, request: ProxyRequest) -> Result<ProxyRequest, ProxyError> {
match request.context.write().await {
mut context => {
context.attributes.insert(
"timeout_ms".to_string(),
serde_json::to_value(self.config.timeout_ms).unwrap()
);
}
}
Ok(request)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PathRewriteFilterConfig {
pub pattern: String,
pub replacement: String,
#[serde(default = "default_true")]
pub rewrite_request: bool,
#[serde(default = "default_false")]
pub rewrite_response: bool,
}
#[derive(Debug)]
pub struct PathRewriteFilter {
config: PathRewriteFilterConfig,
regex: Regex,
}
impl PathRewriteFilter {
pub fn new(config: PathRewriteFilterConfig) -> Self {
let regex = Regex::new(&config.pattern)
.expect("Failed to compile path rewrite pattern");
Self { config, regex }
}
pub fn default() -> Self {
Self::new(PathRewriteFilterConfig {
pattern: "(.*)".to_string(),
replacement: "$1".to_string(),
rewrite_request: true,
rewrite_response: false,
})
}
}
#[async_trait]
impl Filter for PathRewriteFilter {
fn filter_type(&self) -> FilterType {
FilterType::Both
}
fn name(&self) -> &str {
"path_rewrite"
}
async fn pre_filter(&self, mut request: ProxyRequest) -> Result<ProxyRequest, ProxyError> {
if self.config.rewrite_request {
let rewritten_path = self.regex.replace_all(&request.path, &self.config.replacement).to_string();
if rewritten_path != request.path {
debug!("Rewriting path from {} to {}", request.path, rewritten_path);
request.path = rewritten_path;
}
}
Ok(request)
}
async fn post_filter(&self, _request: ProxyRequest, response: ProxyResponse) -> Result<ProxyResponse, ProxyError> {
Ok(response)
}
}
#[derive(Debug)]
pub struct FilterFactory;
impl FilterFactory {
pub fn create_filter(filter_type: &str, config: serde_json::Value) -> Result<Arc<dyn Filter>, ProxyError> {
match filter_type {
"logging" => {
let config: LoggingFilterConfig = serde_json::from_value(config)
.map_err(|e| ProxyError::FilterError(format!("Invalid logging filter config: {}", e)))?;
Ok(Arc::new(LoggingFilter::new(config)))
},
"header" => {
let config: HeaderFilterConfig = serde_json::from_value(config)
.map_err(|e| ProxyError::FilterError(format!("Invalid header filter config: {}", e)))?;
Ok(Arc::new(HeaderFilter::new(config)))
},
"timeout" => {
let config: TimeoutFilterConfig = serde_json::from_value(config)
.map_err(|e| ProxyError::FilterError(format!("Invalid timeout filter config: {}", e)))?;
Ok(Arc::new(TimeoutFilter::new(config)))
},
"path_rewrite" => {
let config: PathRewriteFilterConfig = serde_json::from_value(config)
.map_err(|e| ProxyError::FilterError(format!("Invalid path rewrite filter config: {}", e)))?;
Ok(Arc::new(PathRewriteFilter::new(config)))
},
_ => Err(ProxyError::FilterError(format!("Unknown filter type: {}", filter_type))),
}
}
}