From e860f68ebe388b0143e2bfd8f834f3840d5b1f22 Mon Sep 17 00:00:00 2001 From: valkyrie_pilot Date: Fri, 31 May 2024 12:03:53 -0600 Subject: [PATCH] Nonce-based CSP --- Cargo.lock | 78 +-------------------- Cargo.toml | 4 +- assets/main.css | 20 ------ assets/main.js | 31 --------- src/main.rs | 91 ++++++++++++++++++------ templates/404.hbs | 2 +- templates/index.hbs | 164 ++++++++++++++++++++++++-------------------- templates/style.hbs | 22 ++++++ 8 files changed, 184 insertions(+), 228 deletions(-) delete mode 100644 assets/main.css delete mode 100644 assets/main.js create mode 100644 templates/style.hbs diff --git a/Cargo.lock b/Cargo.lock index 713f737..fc13d6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,31 +17,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "getrandom", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "arrayref" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" - -[[package]] -name = "arrayvec" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" - [[package]] name = "askama" version = "0.12.1" @@ -186,31 +161,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" -[[package]] -name = "blake3" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", -] - -[[package]] -name = "bustdir" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03176591ec0985c52fbf5b0676a878a69de1043f1d36eae033fc9c0775376f9e" -dependencies = [ - "ahash", - "askama", - "blake3", - "rand", -] - [[package]] name = "bytes" version = "1.6.0" @@ -229,12 +179,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "constant_time_eq" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" - [[package]] name = "equivalent" version = "1.0.1" @@ -310,7 +254,7 @@ dependencies = [ "askama", "askama_axum", "axum", - "bustdir", + "rand", "thiserror", "tokio", "tower", @@ -1043,23 +987,3 @@ name = "windows_x86_64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" - -[[package]] -name = "zerocopy" -version = "0.7.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/Cargo.toml b/Cargo.toml index a189a84..d61c060 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,11 +13,11 @@ description = "A simple hyper http server to echo back IP addresses" axum = { version = "0.7", features = ["tokio", "http1", "http2"], default-features = false } askama = { version = "0.12", features = ["with-axum"], default-features = false } tokio = { version = "1", features = ["rt-multi-thread", "macros"] } -askama_axum = { version = "0.4", default-features = false } -bustdir = { version = "0.1", features = ["askama"] } tower-http = { version = "0.5", features = ["fs", "set-header"] } +askama_axum = { version = "0.4", default-features = false } thiserror = "1" tower = "0.4" +rand = "0.8" vss = "0.1" [profile.release] diff --git a/assets/main.css b/assets/main.css deleted file mode 100644 index 1048eeb..0000000 --- a/assets/main.css +++ /dev/null @@ -1,20 +0,0 @@ -html, -body { - height: 100%; - display: flex; - align-items: center; - justify-content: center; -} - -.stack { - display: flex; - flex-direction: column; - font-size: 3vw; - font-family: Arial, Verdana, Helvetica, sans-serif; - justify-content: center; - align-items: center; -} - -.home { - font-size: 2vw; -} diff --git a/assets/main.js b/assets/main.js deleted file mode 100644 index e2a06c7..0000000 --- a/assets/main.js +++ /dev/null @@ -1,31 +0,0 @@ -const root_dns_name = document.getElementById("root-dns-name").dataset.dnsName; -const scheme = document.getElementById("scheme").dataset.scheme; -const v4_endpoint = `${scheme}://v4.${root_dns_name}/raw`; -const v6_endpoint = `${scheme}://v6.${root_dns_name}/raw`; -const ip4 = document.getElementById("ip4"); -const ip6 = document.getElementById("ip6"); -const ip4txt = document.getElementById("ip4txt"); -const ip6txt = document.getElementById("ip6txt"); -if (ip4txt.hidden) { - fetch(v4_endpoint) - .then((req) => req.text()) - .then((resp) => { - ip4.innerText = resp.trim(); - ip4txt.hidden = false; - document.getElementById("title").innerText = `Your IP is ${resp}`; - }) - .catch((failed) => { - console.log("Request failed:" + failed); - }); -} -if (ip6txt.hidden) { - fetch(v6_endpoint) - .then((req) => req.text()) - .then((resp) => { - ip6.innerText = resp.trim(); - ip6txt.hidden = false; - }) - .catch((failed) => { - console.log("Request failed:" + failed); - }); -} diff --git a/src/main.rs b/src/main.rs index 963985f..6c35280 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ #![warn(clippy::all, clippy::pedantic, clippy::nursery)] use std::{ - fmt::{Display, Formatter}, + fmt::{Debug, Display, Formatter}, net::{IpAddr, Ipv6Addr, SocketAddr, SocketAddrV6}, str::FromStr, sync::Arc, @@ -12,7 +12,7 @@ use axum::{ extract::{ConnectInfo, FromRequestParts, Request, State}, handler::Handler, http::{ - header::{ACCESS_CONTROL_ALLOW_ORIGIN, CACHE_CONTROL}, + header::{ACCESS_CONTROL_ALLOW_ORIGIN, CACHE_CONTROL, CONTENT_SECURITY_POLICY}, request::Parts, HeaderName, HeaderValue, StatusCode, }, @@ -21,17 +21,10 @@ use axum::{ routing::{any, get}, Router, }; -use bustdir::BustDir; +use rand::{distributions::Alphanumeric, Rng}; use tokio::net::TcpListener; use tower::ServiceBuilder; -use tower_http::{ - services::ServeDir, - set_header::{SetResponseHeader, SetResponseHeaderLayer}, -}; - -mod filters { - pub use bustdir::askama::bust_dir; -} +use tower_http::{services::ServeDir, set_header::SetResponseHeaderLayer}; static ROBOTS_NAME: HeaderName = HeaderName::from_static("x-robots-tag"); static ROBOTS_VALUE: HeaderValue = HeaderValue::from_static("noindex"); @@ -69,7 +62,11 @@ async fn main() { ) .layer(no_cache) .fallback_service(assets) - .with_state(state.clone()); + .layer(axum::middleware::from_fn_with_state( + state.clone(), + nonce_layer, + )) + .with_state(state); println!("Listening on http://localhost:{port} and http://{v6_addr} for ip requests"); let tcp6 = TcpListener::bind(v6_addr).await.unwrap(); @@ -87,30 +84,31 @@ async fn svc(tcp: TcpListener, app: Router) { #[template(path = "index.hbs", escape = "html", ext = "html")] pub struct IndexPage { root_dns_name: Arc, - cb: Arc, ip: IpAddr, proto: String, + nonce: Nonce, } #[derive(Template)] #[template(path = "404.hbs", escape = "html", ext = "html")] pub struct NotFoundPage { - cb: Arc, + nonce: Nonce, } #[allow(clippy::unused_async)] async fn home( IpAddress(ip): IpAddress, XForwardedProto(proto): XForwardedProto, + nonce: Nonce, Accept(accept): Accept, State(state): State, ) -> Result, Error> { if accept.contains("text/html") { let page = IndexPage { root_dns_name: state.root_dns_name, - cb: state.cb, ip, proto, + nonce, }; Ok(Ok(page)) } else { @@ -124,15 +122,14 @@ async fn raw(IpAddress(ip): IpAddress) -> Result { } #[allow(clippy::unused_async)] -async fn not_found(State(state): State) -> NotFoundPage { - NotFoundPage { cb: state.cb } +async fn not_found(nonce: Nonce) -> NotFoundPage { + NotFoundPage { nonce } } #[derive(Clone)] pub struct AppState { header: Option>, root_dns_name: Arc, - cb: Arc, } impl AppState { @@ -145,12 +142,9 @@ impl AppState { let root_dns_name: Arc = std::env::var("ROOT_DNS_NAME") .expect("No ROOT_DNS_NAME in env") .into(); - let bust = BustDir::new("assets").expect("Failed to create cache-bustin hashes"); - let cb = Arc::new(bust); Self { header: client_ip.map(|v| Arc::new(HeaderName::try_from(v).unwrap())), root_dns_name, - cb, } } } @@ -166,7 +160,7 @@ pub struct IpAddress(IpAddr); impl Display for IpAddress { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) + Display::fmt(&self.0, f) } } @@ -228,12 +222,65 @@ impl FromRequestParts for Accept { } } +#[derive(Clone, Debug)] +pub struct Nonce(pub String); + +#[axum::async_trait] +impl FromRequestParts for Nonce { + type Rejection = std::convert::Infallible; + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + Ok(parts + .extensions + .get() + .cloned() + .unwrap_or_else(|| Self("no-noncense".to_string()))) + } +} + +impl Display for Nonce { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.0, f) + } +} + +async fn nonce_layer(State(state): State, mut req: Request, next: Next) -> Response { + let nonce_string = random_string(32); + req.extensions_mut().insert(Nonce(nonce_string.clone())); + let mut resp = next.run(req).await; + let base_dns_name = state.root_dns_name; + let csp_str = format!( + "default-src 'none'; object-src 'none'; img-src 'self'; \ + connect-src v4.{base_dns_name} v6.{base_dns_name}; \ + style-src 'nonce-{nonce_string}'; \ + script-src 'nonce-{nonce_string}' 'unsafe-inline' 'strict-dynamic'; \ + base-uri 'none';" + ); + match HeaderValue::from_str(&csp_str) { + Ok(csp) => { + resp.headers_mut().insert(CONTENT_SECURITY_POLICY, csp); + } + Err(source) => eprintln!("ERROR: {source:?}"), + } + resp +} + +fn random_string(length: usize) -> String { + let rng = rand::thread_rng(); + rng.sample_iter(Alphanumeric) + .take(length) + .map(char::from) + .collect() +} + #[derive(thiserror::Error, Debug)] pub enum Error { #[error("No header found")] NoHeader, #[error("Could not extract connection info")] ConnectInfo, + #[error("Could not get CSP nonce")] + NoNonce, #[error("Could not convert supplied header to string (this is a configuration issue)")] ToStr(#[from] axum::http::header::ToStrError), #[error("Could not convert supplied header to IP address (this is a configuration issue)")] diff --git a/templates/404.hbs b/templates/404.hbs index 3950007..4cfae67 100644 --- a/templates/404.hbs +++ b/templates/404.hbs @@ -7,8 +7,8 @@ name="description" content="How did we get here? This website only has a root page." /> - 404 not found + {% include "style.hbs" %} diff --git a/templates/index.hbs b/templates/index.hbs index 2b68c6d..86fb49d 100644 --- a/templates/index.hbs +++ b/templates/index.hbs @@ -1,80 +1,94 @@ {%- let description = "A simple, fast website to return your IPv4 and IPv6 addresses. No logs are kept. Free and open to all." -%} - - - - {%- match ip -%} - {%- when IpAddr::V4 with (_) -%} - - {%- when IpAddr::V6 with (_) -%} - - {%- endmatch -%} - - - - - - - - - - {%- match ip -%} - {%- when IpAddr::V4 with (ipv4) -%} - Your public IP is {{ ipv4 }} - {%- when IpAddr::V6 with (_) -%} - What's my IP? - {%- endmatch -%} - + + + {%- match ip -%} + {%- when IpAddr::V4 with (_) -%} + + {%- when IpAddr::V6 with (_) -%} + + {%- endmatch -%} + + + + + + + {% include "style.hbs" %} + {%- match ip -%} + {%- when IpAddr::V4 with (ipv4) -%} + Your public IP is {{ ipv4 }} + {%- when IpAddr::V6 with (_) -%} + What's your IP? + {%- endmatch -%} + - - - -
- {%- match ip -%} - {%- when IpAddr::V4 with (ipv4) -%} -
- Your public IPv4 is: {{ ipv4 }} -
- - {%- when IpAddr::V6 with (ipv6) -%} - -
- Your public IPv6 is: {{ ipv6 }} -
- {%- endmatch -%} -
- + + +
+ {%- match ip -%} + {%- when IpAddr::V4 with (ipv4) -%} +
Your public IPv4 is: {{ ipv4 }}
+ + {%- when IpAddr::V6 with (ipv6) -%} + +
Your public IPv6 is: {{ ipv6 }}
+ {%- endmatch -%} +
+{%- match ip -%} +{%- when IpAddr::V4 with (ipv4) -%} + +{%- when IpAddr::V6 with (ipv6) -%} + +{%- endmatch -%} + diff --git a/templates/style.hbs b/templates/style.hbs new file mode 100644 index 0000000..daacdb4 --- /dev/null +++ b/templates/style.hbs @@ -0,0 +1,22 @@ +