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
Generated
+49 -36
View File
@@ -215,6 +215,16 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "change-detection"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "159fa412eae48a1d94d0b9ecdb85c97ce56eb2a347c62394d3fdbf221adabc1a"
dependencies = [
"path-matchers",
"path-slash 0.1.5",
]
[[package]]
name = "cmake"
version = "0.1.57"
@@ -313,12 +323,6 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-sink"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
[[package]]
name = "futures-task"
version = "0.3.32"
@@ -364,6 +368,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "http"
version = "1.4.0"
@@ -397,12 +407,6 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "http-range-header"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
[[package]]
name = "httparse"
version = "1.10.1"
@@ -760,6 +764,27 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "path-matchers"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36cd9b72a47679ec193a5f0229d9ab686b7bd45e1fbc59ccf953c9f3d83f7b2b"
dependencies = [
"glob",
]
[[package]]
name = "path-slash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "498a099351efa4becc6a19c72aa9270598e8fd274ca47052e37455241c88b696"
[[package]]
name = "path-slash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42"
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -1070,9 +1095,9 @@ dependencies = [
"reqwest",
"serde",
"serde_json",
"static-files",
"tokio",
"tower",
"tower-http",
"tracing",
"tracing-subscriber",
]
@@ -1243,6 +1268,17 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "static-files"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "493af46ab6e8d5409eb02ced7af4bbc97643de491df7b4afcd1cd8fdcc91dc82"
dependencies = [
"change-detection",
"mime_guess",
"path-slash 0.2.1",
]
[[package]]
name = "subtle"
version = "2.6.1"
@@ -1391,19 +1427,6 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tower"
version = "0.5.3"
@@ -1428,24 +1451,14 @@ checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
"bitflags",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-body-util",
"http-range-header",
"httpdate",
"iri-string",
"mime",
"mime_guess",
"percent-encoding",
"pin-project-lite",
"tokio",
"tokio-util",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
+4 -1
View File
@@ -14,7 +14,10 @@ arc-swap = "1"
reqwest = { version = "0.13", default-features = false, features = ["rustls"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tower-http = { version = "0.6", features = ["fs"] }
static-files = "0.3.1"
[build-dependencies]
static-files = "0.3.1"
[dev-dependencies]
http-body-util = "0.1"
+1 -3
View File
@@ -4,7 +4,7 @@ FROM rust:1.93.1-alpine3.23 AS builder
RUN apk add --no-cache musl-dev
WORKDIR /build
COPY Cargo.toml Cargo.lock ./
COPY Cargo.toml Cargo.lock build.rs ./
COPY src ./src
COPY templates ./templates
COPY static ./static
@@ -19,8 +19,6 @@ RUN apk add --no-cache ca-certificates \
&& adduser -S -u 1001 -G rustyip rustyip
COPY --from=builder /build/target/release/rustyip /usr/local/bin/rustyip
COPY --from=builder /build/templates /app/templates
COPY --from=builder /build/static /app/static
WORKDIR /app
RUN chown -R rustyip:rustyip /app
+5
View File
@@ -0,0 +1,5 @@
use static_files::resource_dir;
fn main() -> std::io::Result<()> {
resource_dir("./static").build()
}
+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()
}
+1 -1
View File
@@ -20,7 +20,7 @@
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;600&display=swap">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/simple-line-icons/2.5.5/css/simple-line-icons.min.css">
<link rel="stylesheet" href="/static/style.css">
<link rel="stylesheet" href="/static/style.css?v={{ css_version }}">
</head>
<body>
<main>