From a068e382aed867a905112e035ddd84fa1ba7b772 Mon Sep 17 00:00:00 2001 From: NetworkCats Date: Tue, 3 Mar 2026 03:36:22 +0800 Subject: [PATCH] feat: add security headers, caching headers, and static file versioning --- Cargo.lock | 85 +++++++++++++++++++++++++------------------- Cargo.toml | 5 ++- Dockerfile | 4 +-- build.rs | 5 +++ src/handlers.rs | 3 ++ src/lib.rs | 2 ++ src/middleware.rs | 43 ++++++++++++++++++++++ src/routes.rs | 9 +++-- src/static_assets.rs | 38 ++++++++++++++++++++ templates/index.html | 2 +- 10 files changed, 152 insertions(+), 44 deletions(-) create mode 100644 build.rs create mode 100644 src/middleware.rs create mode 100644 src/static_assets.rs diff --git a/Cargo.lock b/Cargo.lock index 1a8ea1b..a8ebee7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/Cargo.toml b/Cargo.toml index 43a2927..24adb78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/Dockerfile b/Dockerfile index d53adff..fcb9038 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..acc2dcb --- /dev/null +++ b/build.rs @@ -0,0 +1,5 @@ +use static_files::resource_dir; + +fn main() -> std::io::Result<()> { + resource_dir("./static").build() +} diff --git a/src/handlers.rs b/src/handlers.rs index c33d776..2301c9d 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -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, + 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(), diff --git a/src/lib.rs b/src/lib.rs index 94825d8..a02a4e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/middleware.rs b/src/middleware.rs new file mode 100644 index 0000000..72fb7a7 --- /dev/null +++ b/src/middleware.rs @@ -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 +} diff --git a/src/routes.rs b/src/routes.rs index 8ea8172..bd9e6d5 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -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) } diff --git a/src/static_assets.rs b/src/static_assets.rs new file mode 100644 index 0000000..b187e14 --- /dev/null +++ b/src/static_assets.rs @@ -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> = 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) -> 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() +} diff --git a/templates/index.html b/templates/index.html index 61b4e11..9594d32 100644 --- a/templates/index.html +++ b/templates/index.html @@ -20,7 +20,7 @@ - +