1#[cfg(test)]
12mod tests;
13
14use std::collections::HashMap;
15use std::sync::Arc;
16use std::time::{Duration, Instant};
17use std::{fmt, mem};
18use thiserror::Error;
19use crate::security::{ProviderConfig, SecurityChain, SecurityProvider, SecurityStage};
20use tokio::sync::RwLock;
21use tokio::time::timeout;
22use serde::{Serialize, Deserialize};
23
24use crate::config::Config;
25
26#[derive(Error, Debug)]
28pub enum ProxyError {
29 #[error("HTTP client error: {0}")]
31 ClientError(#[from] reqwest::Error),
32
33 #[error("IO error: {0}")]
35 IoError(#[from] std::io::Error),
36
37 #[error("request timed out after {0:?}")]
39 Timeout(Duration),
40
41 #[error("routing error: {0}")]
43 RoutingError(String),
44
45 #[error("filter error: {0}")]
47 FilterError(String),
48
49 #[error("configuration error: {0}")]
51 ConfigError(String),
52
53 #[error("security error: {0}")]
55 SecurityError(String),
56
57 #[error("{0}")]
59 Other(String),
60}
61
62impl From<crate::config::error::ConfigError> for ProxyError {
63 fn from(err: crate::config::error::ConfigError) -> Self {
64 ProxyError::ConfigError(err.to_string())
65 }
66}
67
68impl From<globset::Error> for ProxyError {
69 fn from(e: globset::Error) -> Self {
70 ProxyError::SecurityError(e.to_string())
71 }
72}
73
74impl From<jsonwebtoken::errors::Error> for ProxyError {
75 fn from(e: jsonwebtoken::errors::Error) -> Self {
76 ProxyError::SecurityError(e.to_string())
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "UPPERCASE")]
83pub enum HttpMethod {
84 Get,
85 Post,
86 Put,
87 Delete,
88 Head,
89 Options,
90 Patch,
91 Trace,
92 Connect,
93}
94
95impl fmt::Display for HttpMethod {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 match self {
98 HttpMethod::Get => write!(f, "GET"),
99 HttpMethod::Post => write!(f, "POST"),
100 HttpMethod::Put => write!(f, "PUT"),
101 HttpMethod::Delete => write!(f, "DELETE"),
102 HttpMethod::Head => write!(f, "HEAD"),
103 HttpMethod::Options => write!(f, "OPTIONS"),
104 HttpMethod::Patch => write!(f, "PATCH"),
105 HttpMethod::Trace => write!(f, "TRACE"),
106 HttpMethod::Connect => write!(f, "CONNECT"),
107 }
108 }
109}
110
111impl From<&reqwest::Method> for HttpMethod {
112 fn from(method: &reqwest::Method) -> Self {
113 match *method {
114 reqwest::Method::GET => HttpMethod::Get,
115 reqwest::Method::POST => HttpMethod::Post,
116 reqwest::Method::PUT => HttpMethod::Put,
117 reqwest::Method::DELETE => HttpMethod::Delete,
118 reqwest::Method::HEAD => HttpMethod::Head,
119 reqwest::Method::OPTIONS => HttpMethod::Options,
120 reqwest::Method::PATCH => HttpMethod::Patch,
121 reqwest::Method::TRACE => HttpMethod::Trace,
122 reqwest::Method::CONNECT => HttpMethod::Connect,
123 _ => HttpMethod::Get, }
125 }
126}
127
128impl From<HttpMethod> for reqwest::Method {
129 fn from(method: HttpMethod) -> Self {
130 match method {
131 HttpMethod::Get => reqwest::Method::GET,
132 HttpMethod::Post => reqwest::Method::POST,
133 HttpMethod::Put => reqwest::Method::PUT,
134 HttpMethod::Delete => reqwest::Method::DELETE,
135 HttpMethod::Head => reqwest::Method::HEAD,
136 HttpMethod::Options => reqwest::Method::OPTIONS,
137 HttpMethod::Patch => reqwest::Method::PATCH,
138 HttpMethod::Trace => reqwest::Method::TRACE,
139 HttpMethod::Connect => reqwest::Method::CONNECT,
140 }
141 }
142}
143
144#[derive(Debug)]
146pub struct ProxyRequest {
147 pub method: HttpMethod,
148 pub path: String,
149 pub query: Option<String>,
150 pub headers: reqwest::header::HeaderMap,
151 pub body: reqwest::Body,
152 pub context: Arc<RwLock<RequestContext>>,
153}
154
155impl Clone for ProxyRequest {
156 fn clone(&self) -> Self {
157 Self {
159 method: self.method,
160 path: self.path.clone(),
161 query: self.query.clone(),
162 headers: self.headers.clone(),
163 body: reqwest::Body::from(""),
164 context: self.context.clone(),
165 }
166 }
167}
168
169#[derive(Debug)]
171pub struct ProxyResponse {
172 pub status: u16,
173 pub headers: reqwest::header::HeaderMap,
174 pub body: reqwest::Body,
175 pub context: Arc<RwLock<ResponseContext>>,
176}
177
178#[derive(Debug, Default, Clone)]
180pub struct RequestContext {
181 pub client_ip: Option<String>,
183 pub start_time: Option<std::time::Instant>,
185 pub attributes: std::collections::HashMap<String, serde_json::Value>,
187}
188
189#[derive(Debug, Default, Clone)]
191pub struct ResponseContext {
192 pub receive_time: Option<std::time::Instant>,
194 pub attributes: std::collections::HashMap<String, serde_json::Value>,
196}
197
198#[derive(Debug)]
200pub struct ProxyCore {
201 pub config: Arc<Config>,
203 pub client: reqwest::Client,
205 pub router: Arc<dyn Router>,
207 pub global_filters: Arc<RwLock<Vec<Arc<dyn Filter>>>>,
209 pub security_chain: Arc<RwLock<SecurityChain>>,
211}
212
213impl ProxyCore {
214 pub async fn new(config: Arc<Config>, router: Arc<dyn Router>) -> Result<Self, ProxyError> {
216 let timeout_secs: u64 = config.get_or_default("proxy.timeout", 30)?;
218
219 let client = reqwest::Client::builder()
220 .timeout(Duration::from_secs(timeout_secs))
221 .build()
222 .map_err(ProxyError::ClientError)?;
223
224 let security_config = config
225 .get::<Vec<ProviderConfig>>("proxy.security_chain")
226 .unwrap_or_default();
227
228 let security_chain = SecurityChain::from_configs(
229 security_config.unwrap_or_default()
230 ).await?;
231
232 Ok(Self {
233 config,
234 client,
235 router,
236 global_filters: Arc::new(RwLock::new(Vec::new())),
237 security_chain: Arc::new(RwLock::new(security_chain)),
238 })
239 }
240
241 pub async fn add_global_filter(&self, filter: Arc<dyn Filter>) {
243 let mut filters = self.global_filters.write().await;
244 filters.push(filter);
245 }
246
247 pub async fn add_security_provider(&self, p: Arc<dyn SecurityProvider>) {
249 self.security_chain.write().await.add(p);
250 }
251
252 pub async fn process_request(
254 &self,
255 request: ProxyRequest,
256 ) -> Result<ProxyResponse, ProxyError> {
257 let overall_start = Instant::now();
258 let method = request.method.to_string();
259 let path = request.path.clone();
260
261 log::trace!("Processing request: {} {}", method, path);
262
263 let mut request = match self.security_chain.read().await.apply_pre(request).await {
265 Ok(req) => {
266 log::trace!("Security pre-auth passed for {} {}", method, path);
267 req
268 },
269 Err(e) => {
270 log::warn!("Security pre-auth failed for {} {}: {}", method, path, e);
271 return Err(e);
272 }
273 };
274
275 for f in self.global_filters.read().await.iter() {
277 if f.filter_type().is_pre() || f.filter_type().is_both() {
278 log::trace!("Applying global pre-filter: {}", f.name());
279 match f.pre_filter(request).await {
280 Ok(req) => request = req,
281 Err(e) => {
282 log::error!("Global pre-filter '{}' failed: {}", f.name(), e);
283 return Err(e);
284 }
285 }
286 }
287 }
288
289 let route = match self.router.route(&request).await {
290 Ok(r) => {
291 log::debug!("Request {} {} matched route: {}", method, path, r.id);
292 r
293 },
294 Err(e) => {
295 log::warn!("No route found for {} {}: {}", method, path, e);
296 return Err(e);
297 }
298 };
299
300 let route_filters = route.filters.clone().unwrap_or_default();
301 for f in &route_filters {
302 if f.filter_type().is_pre() || f.filter_type().is_both() {
303 log::trace!("Applying route pre-filter: {}", f.name());
304 match f.pre_filter(request).await {
305 Ok(req) => request = req,
306 Err(e) => {
307 log::error!("Route pre-filter '{}' failed: {}", f.name(), e);
308 return Err(e);
309 }
310 }
311 }
312 }
313
314 let url = format!("{}{}", route.target_base_url, request.path);
316 log::debug!("Forwarding to target: {}", url);
317 let outbound_body = mem::replace(&mut request.body, reqwest::Body::from(""));
318
319 let mut builder = self
320 .client
321 .request(request.method.into(), &url)
322 .headers(request.headers.clone())
323 .body(outbound_body);
324
325 if let Some(q) = &request.query {
326 builder = builder.query(&[(q, "")]);
327 }
328
329 let timeout_dur =
331 Duration::from_secs(self.config.get_or_default("proxy.timeout", 30).unwrap_or_else(|e| {
332 log::error!("Failed to get timeout config: {}", e);
333 30 }));
335
336 let upstream_start = Instant::now();
337 log::trace!("Sending request to upstream with timeout: {:?}", timeout_dur);
338
339 let resp = match timeout(timeout_dur, builder.send()).await {
340 Ok(result) => match result {
341 Ok(response) => response,
342 Err(e) => {
343 log::error!("Upstream request failed: {}", e);
344 return Err(ProxyError::ClientError(e));
345 }
346 },
347 Err(_) => {
348 log::warn!("Request to {} timed out after {:?}", url, timeout_dur);
349 return Err(ProxyError::Timeout(timeout_dur));
350 }
351 };
352
353 let upstream_elapsed = upstream_start.elapsed();
354 log::trace!("Received response from upstream in {:?}", upstream_elapsed);
355
356 let status = resp.status().as_u16();
358 let headers = resp.headers().clone();
359 let body = reqwest::Body::wrap_stream(resp.bytes_stream());
360
361 let mut proxy_resp = ProxyResponse {
362 status,
363 headers,
364 body,
365 context: Arc::new(RwLock::new(ResponseContext::default())),
366 };
367 proxy_resp.context.write().await.receive_time = Some(Instant::now());
368
369 log::debug!("Upstream responded with status: {}", status);
370
371 for f in &route_filters {
373 if f.filter_type().is_post() || f.filter_type().is_both() {
374 log::trace!("Applying route post-filter: {}", f.name());
375 match f.post_filter(request.clone(), proxy_resp).await {
376 Ok(resp) => proxy_resp = resp,
377 Err(e) => {
378 log::error!("Route post-filter '{}' failed: {}", f.name(), e);
379 return Err(e);
380 }
381 }
382 }
383 }
384
385 for f in self.global_filters.read().await.iter() {
386 if f.filter_type().is_post() || f.filter_type().is_both() {
387 log::trace!("Applying global post-filter: {}", f.name());
388 match f.post_filter(request.clone(), proxy_resp).await {
389 Ok(resp) => proxy_resp = resp,
390 Err(e) => {
391 log::error!("Global post-filter '{}' failed: {}", f.name(), e);
392 return Err(e);
393 }
394 }
395 }
396 }
397
398 proxy_resp = match self.security_chain.read().await.apply_post(request.clone(), proxy_resp).await {
400 Ok(resp) => {
401 log::trace!("Security post-auth passed for {} {}", method, path);
402 resp
403 },
404 Err(e) => {
405 log::warn!("Security post-auth failed for {} {}: {}", method, path, e);
406 return Err(e);
407 }
408 };
409
410 let overall_elapsed = overall_start.elapsed();
412 let internal_elapsed = overall_elapsed.saturating_sub(upstream_elapsed);
413
414 log::debug!(
415 "[timing] {} {} -> {} | total={:?} upstream={:?} internal={:?}",
416 request.method,
417 request.path,
418 proxy_resp.status,
419 overall_elapsed,
420 upstream_elapsed,
421 internal_elapsed
422 );
423
424 Ok(proxy_resp)
425 }
426}
427
428#[derive(Debug, Clone, Copy, PartialEq, Eq)]
430pub enum FilterType {
431 Pre,
433 Post,
435 Both,
437}
438
439impl FilterType {
440 pub fn is_pre(&self) -> bool {
442 matches!(self, FilterType::Pre | FilterType::Both)
443 }
444
445 pub fn is_post(&self) -> bool {
447 matches!(self, FilterType::Post | FilterType::Both)
448 }
449
450 pub fn is_both(&self) -> bool {
452 matches!(self, FilterType::Both)
453 }
454}
455
456#[async_trait::async_trait]
458pub trait Filter: fmt::Debug + Send + Sync {
459 fn filter_type(&self) -> FilterType;
461
462 fn name(&self) -> &str;
464
465 async fn pre_filter(&self, request: ProxyRequest) -> Result<ProxyRequest, ProxyError> {
467 Ok(request)
469 }
470
471 async fn post_filter(&self, _request: ProxyRequest, response: ProxyResponse) -> Result<ProxyResponse, ProxyError> {
473 Ok(response)
475 }
476}
477
478#[derive(Debug, Clone)]
480pub struct Route {
481 pub id: String,
483 pub target_base_url: String,
485 pub path_pattern: String,
487 pub filters: Option<Vec<Arc<dyn Filter>>>,
489}
490
491#[async_trait::async_trait]
493pub trait Router: fmt::Debug + Send + Sync {
494 async fn route(&self, request: &ProxyRequest) -> Result<Route, ProxyError>;
496
497 async fn get_routes(&self) -> Vec<Route>;
499
500 async fn add_route(&self, route: Route) -> Result<(), ProxyError>;
502
503 async fn remove_route(&self, route_id: &str) -> Result<(), ProxyError>;
505}