2026-04-09 13:51:50 +08:00
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
|
|
|
use axum::extract::Request;
|
|
|
|
|
use axum::http::Method;
|
|
|
|
|
use axum::response::{IntoResponse, Response};
|
2026-04-04 01:40:24 +08:00
|
|
|
use axum::routing::any;
|
|
|
|
|
use axum::Router;
|
2026-04-09 13:51:50 +08:00
|
|
|
use tower::ServiceExt;
|
|
|
|
|
use tower_http::services::{ServeDir, ServeFile};
|
|
|
|
|
use tracing::warn;
|
2026-03-31 19:19:04 +08:00
|
|
|
|
2026-05-08 00:18:12 +08:00
|
|
|
use aether_runtime::{prometheus_response, ConcurrencyError};
|
|
|
|
|
use aether_runtime_state::RuntimeSemaphoreError;
|
2026-04-05 20:23:16 +08:00
|
|
|
|
|
|
|
|
use super::{api, handlers::proxy::proxy_request, middleware, state::AppState};
|
2026-04-04 01:40:24 +08:00
|
|
|
|
|
|
|
|
pub fn build_router() -> Result<Router, reqwest::Error> {
|
|
|
|
|
Ok(build_router_with_state(AppState::new()?))
|
2026-03-31 19:19:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-09 13:51:50 +08:00
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
struct FrontendStaticState {
|
|
|
|
|
static_dir: PathBuf,
|
|
|
|
|
index_html: PathBuf,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:19:04 +08:00
|
|
|
pub fn build_router_with_state(state: AppState) -> Router {
|
|
|
|
|
let cors_state = state.clone();
|
|
|
|
|
let mut router = Router::<AppState>::new();
|
|
|
|
|
router = api::mount_core_routes(router);
|
|
|
|
|
router = api::mount_operational_routes(router);
|
|
|
|
|
router = api::mount_ai_routes(router);
|
|
|
|
|
router = api::mount_public_support_routes(router);
|
|
|
|
|
router = api::mount_oauth_routes(router);
|
|
|
|
|
router = api::mount_internal_routes(router);
|
|
|
|
|
router = api::mount_admin_routes(router);
|
|
|
|
|
let mut router = router
|
|
|
|
|
.route("/{*path}", any(proxy_request))
|
2026-04-05 20:23:16 +08:00
|
|
|
.layer(axum::middleware::from_fn(middleware::access_log_middleware))
|
2026-03-31 19:19:04 +08:00
|
|
|
.with_state(state);
|
|
|
|
|
if cors_state.frontdoor_cors().is_some() {
|
|
|
|
|
router = router.layer(axum::middleware::from_fn_with_state(
|
|
|
|
|
cors_state,
|
|
|
|
|
middleware::frontdoor_cors_middleware,
|
|
|
|
|
));
|
|
|
|
|
}
|
2026-04-22 21:10:16 +08:00
|
|
|
middleware::apply_cf_header_stripping(router)
|
2026-03-31 19:19:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-09 13:51:50 +08:00
|
|
|
pub fn attach_static_frontend(router: Router, static_dir: impl Into<PathBuf>) -> Router {
|
|
|
|
|
let static_dir = static_dir.into();
|
|
|
|
|
let index_html = static_dir.join("index.html");
|
2026-04-22 21:10:16 +08:00
|
|
|
middleware::apply_cf_header_stripping(router.layer(axum::middleware::from_fn_with_state(
|
2026-04-09 13:51:50 +08:00
|
|
|
FrontendStaticState {
|
|
|
|
|
static_dir,
|
|
|
|
|
index_html,
|
|
|
|
|
},
|
|
|
|
|
frontend_static_middleware,
|
2026-04-22 21:10:16 +08:00
|
|
|
)))
|
2026-04-09 13:51:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn frontend_static_middleware(
|
|
|
|
|
axum::extract::State(frontend): axum::extract::State<FrontendStaticState>,
|
|
|
|
|
request: Request,
|
|
|
|
|
next: axum::middleware::Next,
|
|
|
|
|
) -> Response {
|
|
|
|
|
let path = request.uri().path().to_string();
|
|
|
|
|
if !matches!(request.method(), &Method::GET | &Method::HEAD)
|
|
|
|
|
|| frontend_path_bypasses_static(&path)
|
|
|
|
|
{
|
|
|
|
|
return next.run(request).await;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if frontend_path_targets_static_asset(&path) {
|
|
|
|
|
return serve_static_asset(&frontend.static_dir, request).await;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serve_frontend_index(&frontend.index_html, request).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn frontend_path_bypasses_static(path: &str) -> bool {
|
|
|
|
|
matches!(
|
|
|
|
|
path,
|
|
|
|
|
"/health" | "/test-connection" | crate::constants::READYZ_PATH
|
|
|
|
|
) || path.starts_with("/api/")
|
|
|
|
|
|| path.starts_with("/v1/")
|
|
|
|
|
|| path.starts_with("/v1beta/")
|
|
|
|
|
|| path.starts_with("/upload/")
|
|
|
|
|
|| path.starts_with("/_gateway/")
|
|
|
|
|
|| path.starts_with("/.well-known/")
|
2026-05-12 15:33:30 +08:00
|
|
|
|| path.starts_with("/install/")
|
|
|
|
|
|| path.starts_with("/i/")
|
2026-04-09 13:51:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn frontend_path_targets_static_asset(path: &str) -> bool {
|
|
|
|
|
path.rsplit('/')
|
|
|
|
|
.next()
|
|
|
|
|
.is_some_and(|segment| !segment.is_empty() && segment.contains('.'))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn serve_static_asset(static_dir: &PathBuf, request: Request) -> Response {
|
|
|
|
|
match ServeDir::new(static_dir).oneshot(request).await {
|
|
|
|
|
Ok(response) => response.into_response(),
|
|
|
|
|
Err(err) => {
|
|
|
|
|
warn!(error = %err, "failed to serve frontend static asset");
|
|
|
|
|
axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn serve_frontend_index(index_html: &PathBuf, request: Request) -> Response {
|
|
|
|
|
match ServeFile::new(index_html).oneshot(request).await {
|
|
|
|
|
Ok(response) => response.into_response(),
|
|
|
|
|
Err(err) => {
|
|
|
|
|
warn!(error = %err, "failed to serve frontend index");
|
|
|
|
|
axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:19:04 +08:00
|
|
|
pub(crate) async fn metrics(
|
|
|
|
|
axum::extract::State(state): axum::extract::State<AppState>,
|
|
|
|
|
) -> impl axum::response::IntoResponse {
|
|
|
|
|
prometheus_response(&state.metric_samples().await)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub(crate) enum RequestAdmissionError {
|
|
|
|
|
Local(ConcurrencyError),
|
2026-05-08 00:18:12 +08:00
|
|
|
Distributed(RuntimeSemaphoreError),
|
2026-03-31 19:19:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 01:40:24 +08:00
|
|
|
pub async fn serve_tcp(bind: &str) -> Result<(), Box<dyn std::error::Error>> {
|
2026-03-31 19:19:04 +08:00
|
|
|
let listener = tokio::net::TcpListener::bind(bind).await?;
|
2026-04-04 01:40:24 +08:00
|
|
|
let router = build_router()?;
|
2026-03-31 19:19:04 +08:00
|
|
|
axum::serve(
|
|
|
|
|
listener,
|
|
|
|
|
router.into_make_service_with_connect_info::<std::net::SocketAddr>(),
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|