1#[cfg(test)]
12#[path = "../../tests/unit/core/tests.rs"]
13mod tests;
14
15use crate::security::{ProviderConfig, SecurityChain, SecurityProvider};
16use serde::{Deserialize, Serialize};
17use std::sync::Arc;
18use std::time::{Duration, Instant};
19use std::{fmt, mem};
20use thiserror::Error;
21use tokio::sync::RwLock;
22use tokio::time::timeout;
23
24use crate::config::Config;
25
26use crate::{debug_fmt, error_fmt, info_fmt, trace_fmt, warn_fmt};
27#[cfg(feature = "opentelemetry")]
28use opentelemetry::{
29 Context, KeyValue, global,
30 trace::Tracer,
31 trace::{SpanBuilder, SpanKind, Status, TraceContextExt},
32};
33#[cfg(feature = "opentelemetry")]
34use opentelemetry_http::HeaderInjector;
35#[cfg(feature = "opentelemetry")]
36use opentelemetry_semantic_conventions::attribute::HTTP_RESPONSE_STATUS_CODE;
37#[cfg(feature = "opentelemetry")]
38use std::borrow::Cow;
39
40#[derive(Error, Debug)]
42pub enum ProxyError {
43 #[error("HTTP client error: {0}")]
45 ClientError(#[from] reqwest::Error),
46
47 #[error("IO error: {0}")]
49 IoError(#[from] std::io::Error),
50
51 #[error("request timed out after {0:?}")]
53 Timeout(Duration),
54
55 #[error("routing error: {0}")]
57 RoutingError(String),
58
59 #[error("filter error: {0}")]
61 FilterError(String),
62
63 #[error("configuration error: {0}")]
65 ConfigError(String),
66
67 #[error("security error: {0}")]
69 SecurityError(String),
70
71 #[error("{0}")]
73 Other(String),
74}
75
76impl From<crate::config::error::ConfigError> for ProxyError {
77 fn from(err: crate::config::error::ConfigError) -> Self {
78 ProxyError::ConfigError(err.to_string())
79 }
80}
81
82impl From<globset::Error> for ProxyError {
83 fn from(e: globset::Error) -> Self {
84 ProxyError::SecurityError(e.to_string())
85 }
86}
87
88impl From<jsonwebtoken::errors::Error> for ProxyError {
89 fn from(e: jsonwebtoken::errors::Error) -> Self {
90 ProxyError::SecurityError(e.to_string())
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
96#[serde(rename_all = "UPPERCASE")]
97pub enum HttpMethod {
98 Get,
99 Post,
100 Put,
101 Delete,
102 Head,
103 Options,
104 Patch,
105 Trace,
106 Connect,
107}
108
109impl fmt::Display for HttpMethod {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 match self {
112 HttpMethod::Get => write!(f, "GET"),
113 HttpMethod::Post => write!(f, "POST"),
114 HttpMethod::Put => write!(f, "PUT"),
115 HttpMethod::Delete => write!(f, "DELETE"),
116 HttpMethod::Head => write!(f, "HEAD"),
117 HttpMethod::Options => write!(f, "OPTIONS"),
118 HttpMethod::Patch => write!(f, "PATCH"),
119 HttpMethod::Trace => write!(f, "TRACE"),
120 HttpMethod::Connect => write!(f, "CONNECT"),
121 }
122 }
123}
124
125impl From<&reqwest::Method> for HttpMethod {
126 fn from(method: &reqwest::Method) -> Self {
127 match *method {
128 reqwest::Method::GET => HttpMethod::Get,
129 reqwest::Method::POST => HttpMethod::Post,
130 reqwest::Method::PUT => HttpMethod::Put,
131 reqwest::Method::DELETE => HttpMethod::Delete,
132 reqwest::Method::HEAD => HttpMethod::Head,
133 reqwest::Method::OPTIONS => HttpMethod::Options,
134 reqwest::Method::PATCH => HttpMethod::Patch,
135 reqwest::Method::TRACE => HttpMethod::Trace,
136 reqwest::Method::CONNECT => HttpMethod::Connect,
137 _ => HttpMethod::Get, }
139 }
140}
141
142impl From<HttpMethod> for reqwest::Method {
143 fn from(method: HttpMethod) -> Self {
144 match method {
145 HttpMethod::Get => reqwest::Method::GET,
146 HttpMethod::Post => reqwest::Method::POST,
147 HttpMethod::Put => reqwest::Method::PUT,
148 HttpMethod::Delete => reqwest::Method::DELETE,
149 HttpMethod::Head => reqwest::Method::HEAD,
150 HttpMethod::Options => reqwest::Method::OPTIONS,
151 HttpMethod::Patch => reqwest::Method::PATCH,
152 HttpMethod::Trace => reqwest::Method::TRACE,
153 HttpMethod::Connect => reqwest::Method::CONNECT,
154 }
155 }
156}
157
158#[derive(Debug)]
160pub struct ProxyRequest {
161 pub method: HttpMethod,
162 pub path: String,
163 pub query: Option<String>,
164 pub headers: reqwest::header::HeaderMap,
165 pub body: reqwest::Body,
166 pub context: Arc<RwLock<RequestContext>>,
167 pub custom_target: Option<String>,
168}
169
170impl Clone for ProxyRequest {
171 fn clone(&self) -> Self {
172 Self {
174 method: self.method,
175 path: self.path.clone(),
176 query: self.query.clone(),
177 headers: self.headers.clone(),
178 body: reqwest::Body::from(""),
179 context: self.context.clone(),
180 custom_target: self.custom_target.clone(),
181 }
182 }
183}
184
185#[derive(Debug)]
187pub struct ProxyResponse {
188 pub status: u16,
189 pub headers: reqwest::header::HeaderMap,
190 pub body: reqwest::Body,
191 pub context: Arc<RwLock<ResponseContext>>,
192}
193
194#[derive(Debug, Default, Clone)]
196pub struct RequestContext {
197 pub client_ip: Option<String>,
199 pub start_time: Option<std::time::Instant>,
201 pub attributes: std::collections::HashMap<String, serde_json::Value>,
203}
204
205#[derive(Debug, Default, Clone)]
207pub struct ResponseContext {
208 pub receive_time: Option<std::time::Instant>,
210 pub attributes: std::collections::HashMap<String, serde_json::Value>,
212}
213
214#[derive(Debug)]
216pub struct ProxyCore {
217 pub config: Arc<Config>,
219 pub client: reqwest::Client,
221 pub router: Arc<dyn Router>,
223 pub global_filters: Arc<RwLock<Vec<Arc<dyn Filter>>>>,
225 pub security_chain: Arc<RwLock<SecurityChain>>,
227}
228
229impl ProxyCore {
230 pub async fn new(config: Arc<Config>, router: Arc<dyn Router>) -> Result<Self, ProxyError> {
232 let timeout_secs: u64 = config.get_or_default("proxy.timeout", 30_u64)?;
234
235 let client_builder = reqwest::Client::builder();
236 let client = client_builder
237 .timeout(Duration::from_secs(timeout_secs))
238 .build()
239 .map_err(ProxyError::ClientError)?;
240
241 let actual_security_config: Vec<ProviderConfig> = match config.get("proxy.security_chain") {
242 Ok(Some(sc)) => sc,
243 Ok(None) => Vec::new(), Err(e) => {
245 warn_fmt!(
246 "Core",
247 "Could not parse 'proxy.security_chain', defaulting to empty: {}",
248 e
249 );
250 Vec::new() }
252 };
253
254 let security_chain = SecurityChain::from_configs(actual_security_config).await?;
255
256 Ok(Self {
257 config,
258 client,
259 router,
260 global_filters: Arc::new(RwLock::new(Vec::new())),
261 security_chain: Arc::new(RwLock::new(security_chain)),
262 })
263 }
264
265 pub async fn add_global_filter(&self, filter: Arc<dyn Filter>) {
267 let mut filters = self.global_filters.write().await;
268 filters.push(filter);
269 }
270
271 pub async fn add_security_provider(&self, p: Arc<dyn SecurityProvider>) {
273 self.security_chain.write().await.add(p);
274 }
275
276 pub async fn process_request(
278 &self,
279 request: ProxyRequest,
280 #[cfg(feature = "opentelemetry")] parent_context: Option<Context>,
281 ) -> Result<ProxyResponse, ProxyError> {
282 let overall_start = Instant::now();
283 let method = request.method.to_string();
284 let path = request.path.clone();
285
286 trace_fmt!("Core", "Processing request: {} {}", method, path);
287
288 #[cfg(feature = "opentelemetry")]
289 let span_context = {
290 let parent = parent_context
291 .as_ref()
292 .cloned()
293 .unwrap_or_else(Context::current);
294
295 let span = global::tracer("foxy::proxy").build_with_context(
296 SpanBuilder {
297 name: Cow::from(format!("{method} {path}")),
298 span_kind: Some(SpanKind::Client),
299 ..Default::default()
300 },
301 &parent,
302 );
303
304 let span_context = &Context::current_with_span(span);
305 span_context.clone()
306 };
307
308 let mut request = match self.security_chain.read().await.apply_pre(request).await {
310 Ok(req) => {
311 trace_fmt!("Core", "Security pre-auth passed for {} {}", method, path);
312 req
313 }
314 Err(e) => {
315 warn_fmt!(
316 "Core",
317 "Security pre-auth failed for {} {}: {}",
318 method,
319 path,
320 e
321 );
322
323 #[cfg(feature = "opentelemetry")]
324 {
325 span_context.span().set_status(Status::Error {
326 description: Cow::from(e.to_string()),
327 });
328 span_context.span().end();
329 }
330
331 return Err(e);
332 }
333 };
334
335 for f in self.global_filters.read().await.iter() {
337 if f.filter_type().is_pre() || f.filter_type().is_both() {
338 trace_fmt!("Core", "Applying global pre-filter: {}", f.name());
339 match f.pre_filter(request).await {
340 Ok(req) => request = req,
341 Err(e) => {
342 error_fmt!("Core", "Global pre-filter '{}' failed: {}", f.name(), e);
343
344 #[cfg(feature = "opentelemetry")]
345 {
346 span_context.span().set_status(Status::Error {
347 description: Cow::from(e.to_string()),
348 });
349 span_context.span().end();
350 }
351
352 return Err(e);
353 }
354 }
355 }
356 }
357
358 let mut route = match self.router.route(&request).await {
359 Ok(r) => {
360 debug_fmt!(
361 "Core",
362 "Request {} {} matched route: {}",
363 method,
364 path,
365 r.id
366 );
367 r
368 }
369 Err(e) => {
370 warn_fmt!("Core", "No route found for {} {}: {}", method, path, e);
371
372 #[cfg(feature = "opentelemetry")]
373 {
374 span_context.span().set_status(Status::Error {
375 description: Cow::from(e.to_string()),
376 });
377 span_context.span().end();
378 }
379
380 return Err(e);
381 }
382 };
383
384 let route_filters = route.filters.clone().unwrap_or_default();
385 for f in &route_filters {
386 if f.filter_type().is_pre() || f.filter_type().is_both() {
387 trace_fmt!("Core", "Applying route pre-filter: {}", f.name());
388 match f.pre_filter(request).await {
389 Ok(req) => request = req,
390 Err(e) => {
391 error_fmt!("Core", "Route pre-filter '{}' failed: {}", f.name(), e);
392 return Err(e);
393 }
394 }
395 }
396 }
397
398 info_fmt!("Core", "Initial target: {}", route.target_base_url);
399
400 if request.custom_target.is_some() {
402 debug_fmt!(
403 "Core",
404 "Attempting to dynamically set target base Url: {}",
405 route.target_base_url
406 );
407 route.target_base_url = request.custom_target.clone().unwrap();
408 debug_fmt!(
409 "Core",
410 "Dynamically set target base Url to: {}",
411 route.target_base_url
412 );
413 }
414
415 let url = format!("{}{}", route.target_base_url, request.path);
416 debug_fmt!("Core", "Forwarding to target: {}", url);
417 let outbound_body = mem::replace(&mut request.body, reqwest::Body::from(""));
418
419 #[cfg(feature = "opentelemetry")]
420 let mut outbound_headers = request.headers.clone();
421 #[cfg(not(feature = "opentelemetry"))]
422 let outbound_headers = request.headers.clone();
423 #[cfg(feature = "opentelemetry")]
424 {
425 span_context
426 .span()
427 .set_attribute(KeyValue::new("target", url.clone()));
428
429 global::get_text_map_propagator(|prop| {
430 prop.inject_context(&span_context, &mut HeaderInjector(&mut outbound_headers));
431 });
432 }
433
434 let final_url = if let Some(q) = &request.query {
436 format!("{url}?{q}")
437 } else {
438 url.clone()
439 };
440
441 let builder = self
442 .client
443 .request(request.method.into(), &final_url)
444 .headers(outbound_headers)
445 .body(outbound_body);
446
447 let request_specific_timeout_ms: Option<u64> = request
453 .context
454 .read()
455 .await
456 .attributes
457 .get("timeout_ms")
458 .and_then(|v| v.as_u64());
459
460 let timeout_duration = if let Some(ms) = request_specific_timeout_ms {
461 Duration::from_millis(ms)
462 } else {
463 self.config
469 .get_or_default("proxy.timeout", 30_u64)
470 .map(Duration::from_secs)?
471 };
472
473 let upstream_start = Instant::now();
474 trace_fmt!(
475 "Core",
476 "Sending request to upstream with timeout: {:?}",
477 timeout_duration
478 );
479
480 let resp = match timeout(timeout_duration, builder.send()).await {
481 Ok(result) => match result {
482 Ok(response) => response,
483 Err(e) => {
484 error_fmt!("Core", "Upstream request failed: {}", e);
485
486 #[cfg(feature = "opentelemetry")]
487 {
488 span_context.span().set_status(Status::Error {
489 description: Cow::from(e.to_string()),
490 });
491 span_context.span().end();
492 }
493
494 return Err(ProxyError::ClientError(e));
495 }
496 },
497 Err(_) => {
498 warn_fmt!(
499 "Core",
500 "Request to {} timed out after {:?}",
501 url,
502 timeout_duration
503 );
504
505 #[cfg(feature = "opentelemetry")]
506 {
507 span_context.span().set_status(Status::Error {
508 description: Cow::from("Request timed out"),
509 });
510 span_context.span().end();
511 }
512
513 return Err(ProxyError::Timeout(timeout_duration));
514 }
515 };
516
517 #[cfg(feature = "opentelemetry")]
518 {
519 let client_span = span_context.span();
520
521 client_span.set_attribute(KeyValue::new(
522 HTTP_RESPONSE_STATUS_CODE,
523 resp.status().as_u16() as i64,
524 ));
525 client_span.end();
526 }
527
528 let upstream_elapsed = upstream_start.elapsed();
529 trace_fmt!(
530 "Core",
531 "Received response from upstream in {:?}",
532 upstream_elapsed
533 );
534
535 let status = resp.status().as_u16();
537 let headers = resp.headers().clone();
538 let body = reqwest::Body::wrap_stream(resp.bytes_stream());
539
540 let mut proxy_resp = ProxyResponse {
541 status,
542 headers,
543 body,
544 context: Arc::new(RwLock::new(ResponseContext::default())),
545 };
546 proxy_resp.context.write().await.receive_time = Some(Instant::now());
547
548 debug_fmt!("Core", "Upstream responded with status: {}", status);
549
550 for f in &route_filters {
552 if f.filter_type().is_post() || f.filter_type().is_both() {
553 trace_fmt!("Core", "Applying route post-filter: {}", f.name());
554 match f.post_filter(request.clone(), proxy_resp).await {
555 Ok(resp) => proxy_resp = resp,
556 Err(e) => {
557 error_fmt!("Core", "Route post-filter '{}' failed: {}", f.name(), e);
558 return Err(e);
559 }
560 }
561 }
562 }
563
564 for f in self.global_filters.read().await.iter() {
565 if f.filter_type().is_post() || f.filter_type().is_both() {
566 trace_fmt!("Core", "Applying global post-filter: {}", f.name());
567 match f.post_filter(request.clone(), proxy_resp).await {
568 Ok(resp) => proxy_resp = resp,
569 Err(e) => {
570 error_fmt!("Core", "Global post-filter '{}' failed: {}", f.name(), e);
571 return Err(e);
572 }
573 }
574 }
575 }
576
577 proxy_resp = match self
579 .security_chain
580 .read()
581 .await
582 .apply_post(request.clone(), proxy_resp)
583 .await
584 {
585 Ok(resp) => {
586 trace_fmt!("Core", "Security post-auth passed for {} {}", method, path);
587 resp
588 }
589 Err(e) => {
590 warn_fmt!(
591 "Core",
592 "Security post-auth failed for {} {}: {}",
593 method,
594 path,
595 e
596 );
597 return Err(e);
598 }
599 };
600
601 let overall_elapsed = overall_start.elapsed();
603 let internal_elapsed = overall_elapsed.saturating_sub(upstream_elapsed);
604
605 debug_fmt!(
606 "Core",
607 "[timing] {} {} -> {} | total={:?} upstream={:?} internal={:?}",
608 request.method,
609 request.path,
610 proxy_resp.status,
611 overall_elapsed,
612 upstream_elapsed,
613 internal_elapsed
614 );
615
616 Ok(proxy_resp)
617 }
618}
619
620#[derive(Debug, Clone, Copy, PartialEq, Eq)]
622pub enum FilterType {
623 Pre,
625 Post,
627 Both,
629}
630
631impl FilterType {
632 pub fn is_pre(&self) -> bool {
634 matches!(self, FilterType::Pre | FilterType::Both)
635 }
636
637 pub fn is_post(&self) -> bool {
639 matches!(self, FilterType::Post | FilterType::Both)
640 }
641
642 pub fn is_both(&self) -> bool {
644 matches!(self, FilterType::Both)
645 }
646}
647
648#[async_trait::async_trait]
650pub trait Filter: fmt::Debug + Send + Sync {
651 fn filter_type(&self) -> FilterType;
653
654 fn name(&self) -> &str;
656
657 async fn pre_filter(&self, request: ProxyRequest) -> Result<ProxyRequest, ProxyError> {
659 Ok(request)
661 }
662
663 async fn post_filter(
665 &self,
666 _request: ProxyRequest,
667 response: ProxyResponse,
668 ) -> Result<ProxyResponse, ProxyError> {
669 Ok(response)
671 }
672}
673
674#[derive(Debug, Clone)]
676pub struct Route {
677 pub id: String,
679 pub target_base_url: String,
681 pub path_pattern: String,
683 pub filters: Option<Vec<Arc<dyn Filter>>>,
685}
686
687#[async_trait::async_trait]
689pub trait Router: fmt::Debug + Send + Sync {
690 async fn route(&self, request: &ProxyRequest) -> Result<Route, ProxyError>;
692
693 async fn get_routes(&self) -> Vec<Route>;
695
696 async fn add_route(&self, route: Route) -> Result<(), ProxyError>;
698
699 async fn remove_route(&self, route_id: &str) -> Result<(), ProxyError>;
701}