feat: add host-based routing and IPv4 domain handler with CORS

This commit is contained in:
NetworkCats
2026-03-11 04:38:10 +08:00
parent dfdfda0e81
commit 8567630f7a
4 changed files with 114 additions and 13 deletions
+1
View File
@@ -5,6 +5,7 @@ edition = "2024"
[dependencies]
axum = "0.8.8"
tower = { version = "0.5", features = ["util"] }
tokio = { version = "1.49.0", features = ["rt-multi-thread", "macros", "net", "signal", "time", "fs", "io-util"] }
askama = "0.15.4"
maxminddb = "0.27.3"
+43
View File
@@ -656,6 +656,45 @@ pub async fn health() -> StatusCode {
StatusCode::OK
}
fn extract_ipv4_client_ip(headers: &HeaderMap, dev_mode: bool) -> Result<IpAddr, AppError> {
if let Some(ip) = headers
.get("X-IPv4-Client-IP")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.trim().parse().ok())
{
return Ok(ip);
}
if dev_mode {
return Ok(DEV_FALLBACK_IP);
}
Err(AppError::MissingClientIp)
}
pub async fn ipv4_ip_handler(
State(state): State<AppState>,
headers: HeaderMap,
) -> Result<Response, AppError> {
let ip = extract_ipv4_client_ip(&headers, state.dev_mode)?;
let origin = format!("https://{}", state.site_domain);
Ok((
[
(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin),
(
header::ACCESS_CONTROL_ALLOW_METHODS,
"GET".to_owned(),
),
(
header::HeaderName::from_static("access-control-max-age"),
"86400".to_owned(),
),
],
format!("{ip}\n"),
)
.into_response())
}
pub async fn json_handler(
State(state): State<AppState>,
headers: HeaderMap,
@@ -915,3 +954,7 @@ pub async fn not_found(State(state): State<AppState>, headers: HeaderMap) -> Res
"The page you are looking for does not exist.",
)
}
pub async fn ipv4_not_found() -> StatusCode {
StatusCode::NOT_FOUND
}
+22 -11
View File
@@ -5,8 +5,29 @@ use axum::response::Response;
pub async fn security_headers(request: Request, next: Next) -> Response {
let mut response = next.run(request).await;
let headers = response.headers_mut();
apply_common_security_headers(response.headers_mut());
response.headers_mut().insert(
header::HeaderName::from_static("cross-origin-opener-policy"),
HeaderValue::from_static("same-origin"),
);
response.headers_mut().insert(
header::HeaderName::from_static("cross-origin-resource-policy"),
HeaderValue::from_static("same-origin"),
);
response
}
pub async fn ipv4_domain_security_headers(request: Request, next: Next) -> Response {
let mut response = next.run(request).await;
apply_common_security_headers(response.headers_mut());
response.headers_mut().insert(
header::HeaderName::from_static("cross-origin-resource-policy"),
HeaderValue::from_static("cross-origin"),
);
response
}
fn apply_common_security_headers(headers: &mut axum::http::HeaderMap) {
headers.insert(
header::X_CONTENT_TYPE_OPTIONS,
HeaderValue::from_static("nosniff"),
@@ -28,14 +49,4 @@ pub async fn security_headers(request: Request, next: Next) -> Response {
header::HeaderName::from_static("permissions-policy"),
HeaderValue::from_static("camera=(), microphone=(), geolocation=(), interest-cohort=()"),
);
headers.insert(
header::HeaderName::from_static("cross-origin-opener-policy"),
HeaderValue::from_static("same-origin"),
);
headers.insert(
header::HeaderName::from_static("cross-origin-resource-policy"),
HeaderValue::from_static("same-origin"),
);
response
}
+48 -2
View File
@@ -1,12 +1,16 @@
use std::sync::Arc;
use axum::Router;
use axum::middleware;
use axum::response::IntoResponse;
use axum::routing::get;
use tower::ServiceExt as _;
use crate::handlers::{self, AppState};
use crate::middleware::security_headers;
use crate::middleware::{ipv4_domain_security_headers, security_headers};
use crate::static_assets;
pub fn build_router(state: AppState) -> Router {
fn build_main_router(state: AppState) -> Router {
Router::new()
.route("/", get(handlers::root_redirect))
.route("/favicon.ico", get(handlers::favicon))
@@ -32,3 +36,45 @@ pub fn build_router(state: AppState) -> Router {
.layer(middleware::from_fn(security_headers))
.with_state(state)
}
fn build_ipv4_router(state: AppState) -> Router {
Router::new()
.route("/", get(handlers::ipv4_ip_handler))
.route("/health", get(handlers::health))
.fallback(handlers::ipv4_not_found)
.layer(middleware::from_fn(ipv4_domain_security_headers))
.with_state(state)
}
pub fn build_router(state: AppState) -> Router {
let ipv4_domain: Arc<str> = state.ipv4_domain.clone();
let main_router = build_main_router(state.clone());
let ipv4_router = build_ipv4_router(state);
let svc = tower::service_fn(move |req: axum::http::Request<axum::body::Body>| {
let ipv4_domain = ipv4_domain.clone();
let main_router = main_router.clone();
let ipv4_router = ipv4_router.clone();
async move {
let is_ipv4_domain = !ipv4_domain.is_empty()
&& req
.headers()
.get("host")
.and_then(|v| v.to_str().ok())
.is_some_and(|host| {
let host = host.split(':').next().unwrap_or(host);
host.eq_ignore_ascii_case(&ipv4_domain)
});
// Router<()> is an infallible service; unwrap is safe.
let response = if is_ipv4_domain {
ipv4_router.oneshot(req).await.into_response()
} else {
main_router.oneshot(req).await.into_response()
};
Ok::<_, std::convert::Infallible>(response)
}
});
Router::new().fallback_service(svc)
}