1pub mod abuse;
5pub mod agent;
6pub mod assets;
7pub mod auth;
8pub mod billing;
9pub mod config;
10pub mod crawl_policy;
11pub mod error;
12pub mod fmt;
13pub mod health;
14pub mod layout;
15pub mod metrics;
16pub mod rankorg;
17pub mod render;
18pub mod routes;
19pub mod serp;
20pub mod state;
21pub mod turnstile;
22
23use std::net::SocketAddr;
24
25use axum::Router;
26use axum::extract::Request;
27use axum::http::{HeaderValue, header};
28use axum::middleware::{self, Next};
29use axum::response::Response;
30use axum::routing::get;
31use tokio::net::TcpListener;
32
33pub use config::{Config, Mode};
34pub use state::AppState;
35
36pub fn app(state: AppState) -> Router {
38 Router::new()
39 .merge(auth::router())
40 .merge(routes::router())
41 .merge(routes::mcp::routes(&state))
42 .route("/healthz", get(health::healthz))
43 .route("/readyz", get(health::readyz))
44 .route("/assets/{file}", get(assets::serve))
45 .fallback(error::not_found)
46 .layer(middleware::from_fn_with_state(
47 state.clone(),
48 auth::origin::check_origin,
49 ))
50 .layer(middleware::from_fn(error::error_pages))
51 .layer(middleware::from_fn(security_headers))
52 .with_state(state)
53}
54
55pub async fn serve(
57 state: AppState,
58 listener: TcpListener,
59 shutdown: impl Future<Output = ()> + Send + 'static,
60) -> std::io::Result<()> {
61 let token = state.shutdown.clone();
64 let shutdown = async move {
65 shutdown.await;
66 token.cancel();
67 };
68 axum::serve(
70 listener,
71 app(state).into_make_service_with_connect_info::<SocketAddr>(),
72 )
73 .with_graceful_shutdown(shutdown)
74 .await
75}
76
77async fn security_headers(req: Request, next: Next) -> Response {
80 let mut res = next.run(req).await;
81 let h = res.headers_mut();
82 h.insert(
83 header::X_CONTENT_TYPE_OPTIONS,
84 HeaderValue::from_static("nosniff"),
85 );
86 h.insert(header::X_FRAME_OPTIONS, HeaderValue::from_static("DENY"));
87 h.insert(
88 header::REFERRER_POLICY,
89 HeaderValue::from_static("strict-origin-when-cross-origin"),
90 );
91 let is_html = h
92 .get(header::CONTENT_TYPE)
93 .and_then(|v| v.to_str().ok())
94 .is_some_and(|v| v.starts_with("text/html"));
95 if is_html && !h.contains_key(header::CACHE_CONTROL) {
96 h.insert(
97 header::CACHE_CONTROL,
98 HeaderValue::from_static("private, no-cache"),
99 );
100 }
101 res
102}