ordinary-app 0.8.2

Application server for Ordinary
Documentation
// Copyright (C) 2026 Ordinary Labs, LLC.
//
// SPDX-License-Identifier: AGPL-3.0-only

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()
        }
    }
}