use crate::server::{OrdinaryAppRouter, OrdinaryAppServerState};
use axum::Router;
use axum::extract::{Request, State};
use axum::http::header::CONTENT_TYPE;
use axum::http::{Method, StatusCode, Uri};
use axum::response::IntoResponse;
use axum::routing::any;
use hashbrown::HashMap;
use ordinary_config::OrdinaryConfig;
use ordinary_utils::headers::{get_request_headers_for_forward, log_response};
use ordinary_utils::middleware::{ServiceKind, apply_common_middleware};
use ordinary_utils::response::get_response_for_forwarded;
use ordinary_utils::{HeadersDebug, WrappedRedactedHashingAlg};
use std::sync::Arc;
use std::time::Instant;
use tracing::{Span, instrument};
use url::Url;
use uuid::Uuid;
pub type ProxyServices = HashMap<String, (Router, Option<u16>)>;
#[allow(clippy::ref_option, clippy::too_many_lines, clippy::too_many_arguments)]
pub fn setup_services(
secure: bool,
log_headers: bool,
log_ips: bool,
server_span: &Option<Span>,
redacted_hash: &Arc<Option<WrappedRedactedHashingAlg>>,
config: &Arc<OrdinaryConfig>,
state: &Arc<OrdinaryAppServerState>,
api_domain: &Option<String>,
) -> anyhow::Result<(ProxyServices, OrdinaryAppRouter)> {
let mut proxy_services =
HashMap::with_capacity(config.proxies.as_ref().map(Vec::len).unwrap_or_default());
let mut proxy_router = Router::new();
if let Some(proxies) = &config.proxies {
for proxy in proxies {
let forwarded_by = format!("_{}", Uuid::now_v7());
let forwarded_proto = format!("http{}", if secure { "s" } else { "" });
let parsed_target = Url::parse(&proxy.target)?;
let target_base_has_no_path = parsed_target.path() == "/";
let target_base = proxy.target.clone();
let proxy_target = proxy.target.clone();
let target_base_without_trailing_slash = match proxy.target.strip_suffix("/") {
Some(t) => t.to_string(),
None => proxy.target.clone(),
};
let target_base_without_trailing_slash_clone =
target_base_without_trailing_slash.clone();
if let Some(route) = &proxy.path {
let mut path_router = Router::new();
let proxy_domain = config.domain.clone();
let proxy_domain_clone = proxy_domain.clone();
let forwarded_by_clone = forwarded_by.clone();
let forwarded_proto_clone = forwarded_proto.clone();
let api_domain = api_domain.clone();
let api_domain_clone = api_domain.clone();
let proxy_domain_clone2 = config.domain.clone();
let forwarded_by_clone2 = forwarded_by.clone();
let forwarded_proto_clone2 = forwarded_proto.clone();
let api_domain_clone2 = api_domain.clone();
let proxy_target_clone = proxy_target.clone();
path_router = path_router.route(
route,
any(
async move |State(state): State<Arc<OrdinaryAppServerState>>,
axum::extract::Path(path): axum::extract::Path<String>,
req: Request| {
let uri = req.uri();
let query = uri.query();
let uri = match query {
Some(q) => {
format!("{target_base_without_trailing_slash}/{path}?{q}")
}
None => format!("{target_base_without_trailing_slash}/{path}"),
};
send_proxy_req(
&state,
req,
&uri,
&forwarded_by_clone,
&forwarded_proto_clone,
state.log_headers,
state.redacted_hash.clone(),
&proxy_domain,
api_domain.as_deref(),
proxy_target_clone.as_str(),
)
.await
.into_response()
},
),
);
if let Some(route) = route.clone().strip_suffix("{*path}") {
path_router = path_router.route(
route,
any(
async move |State(state): State<Arc<OrdinaryAppServerState>>,
req: Request| {
let uri = req.uri();
let query = uri.query();
let uri = match query {
Some(q) => {
if target_base_has_no_path {
format!(
"{target_base_without_trailing_slash_clone}/?{q}"
)
} else {
format!("{target_base}?{q}")
}
}
None => target_base,
};
send_proxy_req(
&state,
req,
&uri,
&forwarded_by,
&forwarded_proto,
state.log_headers,
state.redacted_hash.clone(),
&proxy_domain_clone,
api_domain_clone.as_deref(),
proxy_target.as_str(),
)
.await
.into_response()
},
),
);
}
if let Some(names) = &proxy.middlewares {
path_router = crate::server::middleware::apply_custom_to_router(
path_router,
config,
state,
names,
proxy_domain_clone2,
forwarded_by_clone2,
forwarded_proto_clone2,
api_domain_clone2,
);
}
proxy_router = proxy_router.merge(path_router);
} else if let Some(domain) = &proxy.domain {
let domain = domain.clone();
let domain_clone = domain.clone();
let proxy_domain = domain.clone();
let api_domain = api_domain.clone();
let forwarded_by_clone = forwarded_by.clone();
let forwarded_proto_clone = forwarded_proto.clone();
let proxy_domain_clone = domain.clone();
let api_domain_clone = api_domain.clone();
let server_span = server_span.clone();
let redacted_hash = redacted_hash.clone();
let mut proxy_service = Router::new().fallback(
async move |State(state): State<Arc<OrdinaryAppServerState>>, req: Request| {
if *state.is_killed.read() {
return (
StatusCode::SERVICE_UNAVAILABLE,
[(CONTENT_TYPE, "text/html")],
"<h1>503 Service Unavailable</h1>",
)
.into_response();
}
let src_uri = req.uri();
let path_and_query = src_uri
.path_and_query()
.map_or(src_uri.path(), |pq| pq.as_str());
let target_uri =
format!("{target_base_without_trailing_slash}{path_and_query}");
send_proxy_req(
&state,
req,
&target_uri,
&forwarded_by,
&forwarded_proto,
state.log_headers,
state.redacted_hash.clone(),
&proxy_domain,
api_domain.as_deref(),
proxy_target.as_str(),
)
.await
.into_response()
},
);
if let Some(names) = &proxy.middlewares {
proxy_service = crate::server::middleware::apply_custom_to_router(
proxy_service,
config,
state,
names,
proxy_domain_clone,
forwarded_by_clone,
forwarded_proto_clone,
api_domain_clone,
);
}
let proxy_service = apply_common_middleware(
proxy_service,
state,
server_span,
domain,
log_headers,
log_ips,
redacted_hash,
ServiceKind::Proxy,
None,
);
proxy_services.insert(domain_clone, (proxy_service, proxy.port));
}
}
}
Ok((proxy_services, proxy_router))
}
#[allow(
clippy::too_many_lines,
clippy::similar_names,
clippy::too_many_arguments,
clippy::ref_option
)]
#[instrument(name = "fwd", fields(host, port, path, query), skip_all)]
async fn send_proxy_req(
state: &Arc<OrdinaryAppServerState>,
req: Request,
uri: &str,
forwarded_by: &str,
forwarded_proto: &str,
log_headers: bool,
redacted_hash: Arc<Option<WrappedRedactedHashingAlg>>,
proxy_domain: &str,
api_domain: Option<&str>,
proxy_target: &str,
) -> impl IntoResponse {
Span::current().record("domain", tracing::field::display(proxy_target));
let start = Instant::now();
let via_domain = api_domain.unwrap_or(proxy_domain);
let headers = get_request_headers_for_forward(&req, forwarded_by, forwarded_proto, via_domain);
let method = req.method().clone();
let req = if matches!(method, Method::POST | Method::PUT | Method::PATCH) {
let mut req = req.map(|body| reqwest::Body::wrap_stream(body.into_data_stream()));
match uri.parse::<Uri>() {
Ok(uri) => {
*req.uri_mut() = uri;
}
Err(err) => {
tracing::error!(%err);
return StatusCode::BAD_REQUEST.into_response();
}
}
*req.method_mut() = method;
*req.headers_mut() = headers;
reqwest::Request::try_from(req)
} else {
match uri.parse::<Url>() {
Ok(uri) => {
let mut req = reqwest::Request::new(method, uri);
*req.headers_mut() = headers;
Ok(req)
}
Err(err) => {
tracing::error!(%err);
return StatusCode::BAD_REQUEST.into_response();
}
}
};
let res = match req {
Ok(req) => {
Span::current().record("host", req.url().host().map(tracing::field::display));
Span::current().record("port", req.url().port().map(tracing::field::display));
Span::current().record("path", tracing::field::display(req.url().path()));
Span::current().record("query", req.url().query().map(tracing::field::display));
{
let hd = log_headers.then_some(HeadersDebug(req.headers(), redacted_hash.clone()));
#[cfg(tracing_unstable)]
let headers = log_headers.then_some(tracing::field::valuable(&hd));
#[cfg(not(tracing_unstable))]
let headers = log_headers.then_some(tracing::field::debug(&hd));
tracing::info!(
version = ?req.version(),
method = %req.method(),
headers,
"req"
);
}
state.reqwest_client.execute(req).await
}
Err(err) => {
tracing::error!(%err);
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
};
match res {
Ok(res) => {
let status = res.status().as_u16();
log_response(status, log_headers, &redacted_hash, start, res.headers());
get_response_for_forwarded(via_domain, res).into_response()
}
Err(err) => {
tracing::error!(%err);
StatusCode::BAD_REQUEST.into_response()
}
}
}