#[cfg(test)]
mod tests;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use std::{fmt, mem};
use thiserror::Error;
use crate::security::{ProviderConfig, SecurityChain, SecurityProvider, SecurityStage};
use tokio::sync::RwLock;
use tokio::time::timeout;
use serde::{Serialize, Deserialize};
use crate::config::Config;
#[derive(Error, Debug)]
pub enum ProxyError {
#[error("HTTP client error: {0}")]
ClientError(#[from] reqwest::Error),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("request timed out after {0:?}")]
Timeout(Duration),
#[error("routing error: {0}")]
RoutingError(String),
#[error("filter error: {0}")]
FilterError(String),
#[error("configuration error: {0}")]
ConfigError(String),
#[error("security error: {0}")]
SecurityError(String),
#[error("{0}")]
Other(String),
}
impl From<crate::config::error::ConfigError> for ProxyError {
fn from(err: crate::config::error::ConfigError) -> Self {
ProxyError::ConfigError(err.to_string())
}
}
impl From<globset::Error> for ProxyError {
fn from(e: globset::Error) -> Self {
ProxyError::SecurityError(e.to_string())
}
}
impl From<jsonwebtoken::errors::Error> for ProxyError {
fn from(e: jsonwebtoken::errors::Error) -> Self {
ProxyError::SecurityError(e.to_string())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum HttpMethod {
Get,
Post,
Put,
Delete,
Head,
Options,
Patch,
Trace,
Connect,
}
impl fmt::Display for HttpMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HttpMethod::Get => write!(f, "GET"),
HttpMethod::Post => write!(f, "POST"),
HttpMethod::Put => write!(f, "PUT"),
HttpMethod::Delete => write!(f, "DELETE"),
HttpMethod::Head => write!(f, "HEAD"),
HttpMethod::Options => write!(f, "OPTIONS"),
HttpMethod::Patch => write!(f, "PATCH"),
HttpMethod::Trace => write!(f, "TRACE"),
HttpMethod::Connect => write!(f, "CONNECT"),
}
}
}
impl From<&reqwest::Method> for HttpMethod {
fn from(method: &reqwest::Method) -> Self {
match *method {
reqwest::Method::GET => HttpMethod::Get,
reqwest::Method::POST => HttpMethod::Post,
reqwest::Method::PUT => HttpMethod::Put,
reqwest::Method::DELETE => HttpMethod::Delete,
reqwest::Method::HEAD => HttpMethod::Head,
reqwest::Method::OPTIONS => HttpMethod::Options,
reqwest::Method::PATCH => HttpMethod::Patch,
reqwest::Method::TRACE => HttpMethod::Trace,
reqwest::Method::CONNECT => HttpMethod::Connect,
_ => HttpMethod::Get, }
}
}
impl From<HttpMethod> for reqwest::Method {
fn from(method: HttpMethod) -> Self {
match method {
HttpMethod::Get => reqwest::Method::GET,
HttpMethod::Post => reqwest::Method::POST,
HttpMethod::Put => reqwest::Method::PUT,
HttpMethod::Delete => reqwest::Method::DELETE,
HttpMethod::Head => reqwest::Method::HEAD,
HttpMethod::Options => reqwest::Method::OPTIONS,
HttpMethod::Patch => reqwest::Method::PATCH,
HttpMethod::Trace => reqwest::Method::TRACE,
HttpMethod::Connect => reqwest::Method::CONNECT,
}
}
}
#[derive(Debug)]
pub struct ProxyRequest {
pub method: HttpMethod,
pub path: String,
pub query: Option<String>,
pub headers: reqwest::header::HeaderMap,
pub body: reqwest::Body,
pub context: Arc<RwLock<RequestContext>>,
}
impl Clone for ProxyRequest {
fn clone(&self) -> Self {
Self {
method: self.method,
path: self.path.clone(),
query: self.query.clone(),
headers: self.headers.clone(),
body: reqwest::Body::from(""),
context: self.context.clone(),
}
}
}
#[derive(Debug)]
pub struct ProxyResponse {
pub status: u16,
pub headers: reqwest::header::HeaderMap,
pub body: reqwest::Body,
pub context: Arc<RwLock<ResponseContext>>,
}
#[derive(Debug, Default, Clone)]
pub struct RequestContext {
pub client_ip: Option<String>,
pub start_time: Option<std::time::Instant>,
pub attributes: std::collections::HashMap<String, serde_json::Value>,
}
#[derive(Debug, Default, Clone)]
pub struct ResponseContext {
pub receive_time: Option<std::time::Instant>,
pub attributes: std::collections::HashMap<String, serde_json::Value>,
}
#[derive(Debug)]
pub struct ProxyCore {
pub config: Arc<Config>,
pub client: reqwest::Client,
pub router: Arc<dyn Router>,
pub global_filters: Arc<RwLock<Vec<Arc<dyn Filter>>>>,
pub security_chain: Arc<RwLock<SecurityChain>>,
}
impl ProxyCore {
pub async fn new(config: Arc<Config>, router: Arc<dyn Router>) -> Result<Self, ProxyError> {
let timeout_secs: u64 = config.get_or_default("proxy.timeout", 30)?;
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(timeout_secs))
.build()
.map_err(ProxyError::ClientError)?;
let security_config = config
.get::<Vec<ProviderConfig>>("proxy.security_chain")
.unwrap_or_default();
let security_chain = SecurityChain::from_configs(
security_config.unwrap_or_default()
).await?;
Ok(Self {
config,
client,
router,
global_filters: Arc::new(RwLock::new(Vec::new())),
security_chain: Arc::new(RwLock::new(security_chain)),
})
}
pub async fn add_global_filter(&self, filter: Arc<dyn Filter>) {
let mut filters = self.global_filters.write().await;
filters.push(filter);
}
pub async fn add_security_provider(&self, p: Arc<dyn SecurityProvider>) {
self.security_chain.write().await.add(p);
}
pub async fn process_request(
&self,
request: ProxyRequest,
) -> Result<ProxyResponse, ProxyError> {
let overall_start = Instant::now();
let method = request.method.to_string();
let path = request.path.clone();
log::trace!("Processing request: {} {}", method, path);
let mut request = match self.security_chain.read().await.apply_pre(request).await {
Ok(req) => {
log::trace!("Security pre-auth passed for {} {}", method, path);
req
},
Err(e) => {
log::warn!("Security pre-auth failed for {} {}: {}", method, path, e);
return Err(e);
}
};
for f in self.global_filters.read().await.iter() {
if f.filter_type().is_pre() || f.filter_type().is_both() {
log::trace!("Applying global pre-filter: {}", f.name());
match f.pre_filter(request).await {
Ok(req) => request = req,
Err(e) => {
log::error!("Global pre-filter '{}' failed: {}", f.name(), e);
return Err(e);
}
}
}
}
let route = match self.router.route(&request).await {
Ok(r) => {
log::debug!("Request {} {} matched route: {}", method, path, r.id);
r
},
Err(e) => {
log::warn!("No route found for {} {}: {}", method, path, e);
return Err(e);
}
};
let route_filters = route.filters.clone().unwrap_or_default();
for f in &route_filters {
if f.filter_type().is_pre() || f.filter_type().is_both() {
log::trace!("Applying route pre-filter: {}", f.name());
match f.pre_filter(request).await {
Ok(req) => request = req,
Err(e) => {
log::error!("Route pre-filter '{}' failed: {}", f.name(), e);
return Err(e);
}
}
}
}
let url = format!("{}{}", route.target_base_url, request.path);
log::debug!("Forwarding to target: {}", url);
let outbound_body = mem::replace(&mut request.body, reqwest::Body::from(""));
let mut builder = self
.client
.request(request.method.into(), &url)
.headers(request.headers.clone())
.body(outbound_body);
if let Some(q) = &request.query {
builder = builder.query(&[(q, "")]);
}
let timeout_dur =
Duration::from_secs(self.config.get_or_default("proxy.timeout", 30).unwrap_or_else(|e| {
log::error!("Failed to get timeout config: {}", e);
30 }));
let upstream_start = Instant::now();
log::trace!("Sending request to upstream with timeout: {:?}", timeout_dur);
let resp = match timeout(timeout_dur, builder.send()).await {
Ok(result) => match result {
Ok(response) => response,
Err(e) => {
log::error!("Upstream request failed: {}", e);
return Err(ProxyError::ClientError(e));
}
},
Err(_) => {
log::warn!("Request to {} timed out after {:?}", url, timeout_dur);
return Err(ProxyError::Timeout(timeout_dur));
}
};
let upstream_elapsed = upstream_start.elapsed();
log::trace!("Received response from upstream in {:?}", upstream_elapsed);
let status = resp.status().as_u16();
let headers = resp.headers().clone();
let body = reqwest::Body::wrap_stream(resp.bytes_stream());
let mut proxy_resp = ProxyResponse {
status,
headers,
body,
context: Arc::new(RwLock::new(ResponseContext::default())),
};
proxy_resp.context.write().await.receive_time = Some(Instant::now());
log::debug!("Upstream responded with status: {}", status);
for f in &route_filters {
if f.filter_type().is_post() || f.filter_type().is_both() {
log::trace!("Applying route post-filter: {}", f.name());
match f.post_filter(request.clone(), proxy_resp).await {
Ok(resp) => proxy_resp = resp,
Err(e) => {
log::error!("Route post-filter '{}' failed: {}", f.name(), e);
return Err(e);
}
}
}
}
for f in self.global_filters.read().await.iter() {
if f.filter_type().is_post() || f.filter_type().is_both() {
log::trace!("Applying global post-filter: {}", f.name());
match f.post_filter(request.clone(), proxy_resp).await {
Ok(resp) => proxy_resp = resp,
Err(e) => {
log::error!("Global post-filter '{}' failed: {}", f.name(), e);
return Err(e);
}
}
}
}
proxy_resp = match self.security_chain.read().await.apply_post(request.clone(), proxy_resp).await {
Ok(resp) => {
log::trace!("Security post-auth passed for {} {}", method, path);
resp
},
Err(e) => {
log::warn!("Security post-auth failed for {} {}: {}", method, path, e);
return Err(e);
}
};
let overall_elapsed = overall_start.elapsed();
let internal_elapsed = overall_elapsed.saturating_sub(upstream_elapsed);
log::debug!(
"[timing] {} {} -> {} | total={:?} upstream={:?} internal={:?}",
request.method,
request.path,
proxy_resp.status,
overall_elapsed,
upstream_elapsed,
internal_elapsed
);
Ok(proxy_resp)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FilterType {
Pre,
Post,
Both,
}
impl FilterType {
pub fn is_pre(&self) -> bool {
matches!(self, FilterType::Pre | FilterType::Both)
}
pub fn is_post(&self) -> bool {
matches!(self, FilterType::Post | FilterType::Both)
}
pub fn is_both(&self) -> bool {
matches!(self, FilterType::Both)
}
}
#[async_trait::async_trait]
pub trait Filter: fmt::Debug + Send + Sync {
fn filter_type(&self) -> FilterType;
fn name(&self) -> &str;
async fn pre_filter(&self, request: ProxyRequest) -> Result<ProxyRequest, ProxyError> {
Ok(request)
}
async fn post_filter(&self, _request: ProxyRequest, response: ProxyResponse) -> Result<ProxyResponse, ProxyError> {
Ok(response)
}
}
#[derive(Debug, Clone)]
pub struct Route {
pub id: String,
pub target_base_url: String,
pub path_pattern: String,
pub filters: Option<Vec<Arc<dyn Filter>>>,
}
#[async_trait::async_trait]
pub trait Router: fmt::Debug + Send + Sync {
async fn route(&self, request: &ProxyRequest) -> Result<Route, ProxyError>;
async fn get_routes(&self) -> Vec<Route>;
async fn add_route(&self, route: Route) -> Result<(), ProxyError>;
async fn remove_route(&self, route_id: &str) -> Result<(), ProxyError>;
}