feat: add security headers, caching headers, and static file versioning

This commit is contained in:
NetworkCats
2026-03-03 03:36:22 +08:00
parent e1570ddfbf
commit a068e382ae
10 changed files with 152 additions and 44 deletions
+3
View File
@@ -10,6 +10,7 @@ use crate::cli_detect::is_cli_user_agent;
use crate::db::{self, SharedDb};
use crate::error::AppError;
use crate::models::{IpInfo, get_en_name};
use crate::static_assets;
#[derive(serde::Deserialize)]
pub struct IpQuery {
@@ -28,6 +29,7 @@ struct IndexTemplate {
ip: String,
query: String,
site_domain: Arc<str>,
css_version: u64,
asn: String,
org: String,
country: String,
@@ -125,6 +127,7 @@ pub async fn root(
ip: info.ip.clone(),
query: query.ip.unwrap_or_default(),
site_domain: state.site_domain.clone(),
css_version: static_assets::asset_version("style.css"),
asn: format_asn(&info),
org: format_org(&info).to_owned(),
country: format_country(&info).to_owned(),
+2
View File
@@ -3,6 +3,8 @@ pub mod config;
pub mod db;
pub mod error;
pub mod handlers;
pub mod middleware;
pub mod models;
pub mod routes;
pub mod static_assets;
pub mod updater;
+43
View File
@@ -0,0 +1,43 @@
use axum::extract::Request;
use axum::http::header::{self, HeaderValue};
use axum::middleware::Next;
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();
headers.insert(
header::X_CONTENT_TYPE_OPTIONS,
HeaderValue::from_static("nosniff"),
);
headers.insert(header::X_FRAME_OPTIONS, HeaderValue::from_static("DENY"));
headers.insert(
header::REFERRER_POLICY,
HeaderValue::from_static("strict-origin-when-cross-origin"),
);
headers.insert(
header::STRICT_TRANSPORT_SECURITY,
HeaderValue::from_static("max-age=63072000; includeSubDomains; preload"),
);
headers.insert(
header::HeaderName::from_static("x-permitted-cross-domain-policies"),
HeaderValue::from_static("none"),
);
headers.insert(
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
}
+6 -3
View File
@@ -1,8 +1,10 @@
use axum::Router;
use axum::middleware;
use axum::routing::get;
use tower_http::services::ServeDir;
use axum::Router;
use crate::handlers::{self, AppState};
use crate::middleware::security_headers;
use crate::static_assets;
pub fn build_router(state: AppState) -> Router {
Router::new()
@@ -20,6 +22,7 @@ pub fn build_router(state: AppState) -> Router {
.route("/vpn", get(handlers::vpn_handler))
.route("/hosting", get(handlers::hosting_handler))
.route("/tor", get(handlers::tor_handler))
.nest_service("/static", ServeDir::new("static"))
.route("/static/{*path}", get(static_assets::serve_static))
.layer(middleware::from_fn(security_headers))
.with_state(state)
}
+38
View File
@@ -0,0 +1,38 @@
use std::collections::HashMap;
use std::sync::LazyLock;
use axum::extract::Path;
use axum::http::header::{self, HeaderValue};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use static_files::Resource;
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
static ASSETS: LazyLock<HashMap<&'static str, Resource>> = LazyLock::new(generate);
pub fn asset_version(filename: &str) -> u64 {
ASSETS
.get(filename)
.map(|r| r.modified)
.unwrap_or(0)
}
pub async fn serve_static(Path(path): Path<String>) -> Response {
let Some(resource) = ASSETS.get(path.as_str()) else {
return StatusCode::NOT_FOUND.into_response();
};
(
[
(header::CONTENT_TYPE, HeaderValue::from_static(resource.mime_type)),
(
header::CACHE_CONTROL,
HeaderValue::from_static("public, max-age=31536000, immutable"),
),
],
resource.data,
)
.into_response()
}