diff options
| author | Nikolay Govorov <me@govorov.online> | 2026-02-07 23:58:00 +0000 |
|---|---|---|
| committer | Nikolay Govorov <me@govorov.online> | 2026-02-08 00:04:13 +0000 |
| commit | 3c01f4de09d01578d82f6fdb7a0afbcaf6268193 (patch) | |
| tree | d6289b394f2287d52aa145b2c1f6dc65fc4aa114 | |
| parent | a7b9c0ac6d5f23e7a948032df8ae7e820d73b8b3 (diff) | |
| download | tar tar.gz tar.bz2 tar.lz tar.xz tar.zst zip | |
Add favicon, setup etag for static, drop licenses for another targets
Diffstat
23 files changed, 418 insertions, 372 deletions
diff --git a/Cargo.lock b/Cargo.lock index 219520b..e608820 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -193,6 +193,14 @@ dependencies = [ ] [[package]] +name = "base" +version = "0.0.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1371,16 +1379,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - -[[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2021,8 +2019,8 @@ name = "repos" version = "0.0.0" dependencies = [ "async-trait", + "base", "bytes", - "mime_guess", "proptest", "semver", "serde", @@ -2102,40 +2100,6 @@ dependencies = [ ] [[package]] -name = "rust-embed" -version = "8.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" -dependencies = [ - "rust-embed-impl", - "rust-embed-utils", - "walkdir", -] - -[[package]] -name = "rust-embed-impl" -version = "8.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" -dependencies = [ - "proc-macro2", - "quote", - "rust-embed-utils", - "syn", - "walkdir", -] - -[[package]] -name = "rust-embed-utils" -version = "8.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" -dependencies = [ - "sha2", - "walkdir", -] - -[[package]] name = "rustix" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2220,15 +2184,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] name = "schannel" version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3196,12 +3151,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - -[[package]] name = "unicode-bidi" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3293,16 +3242,6 @@ dependencies = [ ] [[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] name = "want" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3438,15 +3377,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3879,6 +3809,7 @@ dependencies = [ "async-trait", "axum", "axum-server", + "base", "bytes", "bytesize", "chrono", @@ -3889,14 +3820,12 @@ dependencies = [ "hyper-tls", "hyper-util", "maud", - "mime_guess", "opentelemetry", "opentelemetry-appender-tracing", "opentelemetry-otlp", "opentelemetry-semantic-conventions", "opentelemetry_sdk", "repos", - "rust-embed", "rustls", "sd-notify", "serde", diff --git a/Cargo.toml b/Cargo.toml index daef133..19d2306 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: AGPL-3.0-or-later [workspace] -members = ["crates/repos", "crates/zorian", "xtask"] +members = ["crates/*", "xtask"] resolver = "3" [workspace.package] @@ -16,6 +16,7 @@ repository = "https://github.com/mrdimidium/Zorian" [workspace.dependencies] # local dependencies +base = { path = "crates/base" } repos = { path = "crates/repos" } # external @@ -32,8 +33,6 @@ hyper = "1.8" hyper-tls = "0.6" hyper-util = { version = "0.1", features = ["client-legacy", "http1", "http2", "tokio"] } maud = { version = "0.27", features = ["axum"] } -mime_guess = "2.0" -rust-embed = "8.11" rustls = { version = "0.23", default-features = false, features = [ "ring", "std", diff --git a/REUSE.toml b/REUSE.toml index d77fe9a..86e2035 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -12,6 +12,11 @@ SPDX-License-Identifier = "AGPL-3.0-or-later" [[annotations]] path = [ + "pkg/*.svg", + "crates/zorian/src/assets/*.svg", + "crates/zorian/src/assets/*.ico", + "crates/zorian/src/assets/*.png", + "crates/zorian/src/assets/manifest.webmanifest", "crates/zorian/src/assets/jetbrainsmono/OFL.txt", "crates/zorian/src/assets/jetbrainsmono/*.woff2", ] diff --git a/crates/base/Cargo.toml b/crates/base/Cargo.toml new file mode 100644 --- /dev/null +++ b/crates/base/Cargo.toml @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2026 Nikolay Govorov <me@govorov.online> +# SPDX-License-Identifier: AGPL-3.0-or-later + +[package] +name = "base" +version = "0.0.0" +description = "Shared types and utilities" + +authors.workspace = true +edition.workspace = true +license.workspace = true +publish.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +serde.workspace = true + +[dev-dependencies] +serde_json.workspace = true diff --git a/crates/base/src/lib.rs b/crates/base/src/lib.rs new file mode 100644 --- /dev/null +++ b/crates/base/src/lib.rs @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2026 Nikolay Govorov <me@govorov.online> +// SPDX-License-Identifier: AGPL-3.0-or-later + +pub mod serde; + +#[derive(Debug, Clone, Copy)] +pub enum ContentType { + TextPlain, + OctetStream, + ImageXIcon, + ImagePng, + ImageSvgXml, + TextCss, + FontWoff2, + ApplicationManifestJson, +} + +impl ContentType { + pub fn as_str(&self) -> &'static str { + match self { + Self::TextPlain => "text/plain", + Self::OctetStream => "application/octet-stream", + Self::ImageXIcon => "image/x-icon", + Self::ImagePng => "image/png", + Self::ImageSvgXml => "image/svg+xml", + Self::TextCss => "text/css", + Self::FontWoff2 => "font/woff2", + Self::ApplicationManifestJson => "application/manifest+json", + } + } +} diff --git a/crates/zorian/src/utils.rs b/crates/base/src/serde.rs index 0cf2e73..7897b02 100644 --- a/crates/zorian/src/utils.rs +++ b/crates/base/src/serde.rs @@ -4,21 +4,41 @@ use std::net::SocketAddr; use std::time::Duration; -use serde::de::Deserialize; +use ::serde::de::Deserialize; /// Deserializes a duration from seconds (u64). pub fn deserialize_duration<'de, D>(deserializer: D) -> Result<Duration, D::Error> where - D: serde::Deserializer<'de>, + D: ::serde::Deserializer<'de>, { let secs = u64::deserialize(deserializer)?; Ok(Duration::from_secs(secs)) } +/// Deserializes a SocketAddr from a string. +pub fn deserialize_listener_addr<'de, D>(deserializer: D) -> Result<SocketAddr, D::Error> +where + D: ::serde::Deserializer<'de>, +{ + let raw = String::deserialize(deserializer)?; + let raw = raw.trim(); + + // Hostnames are not supported, but localhost shorthands are useful + if let Some(port) = raw.strip_prefix("localhost:") { + return port + .parse::<u16>() + .map(|port| SocketAddr::new(std::net::Ipv4Addr::LOCALHOST.into(), port)) + .map_err(|err| ::serde::de::Error::custom(format!("invalid port in '{raw}': {err}"))); + } + + raw.parse::<SocketAddr>() + .map_err(|err| ::serde::de::Error::custom(format!("invalid address '{raw}': {err}"))) +} + #[cfg(test)] mod deserialize_duration_tests { use super::*; - use serde::Deserialize; + use ::serde::Deserialize; #[derive(Debug, Deserialize)] struct DurationWrapper { @@ -39,30 +59,10 @@ mod deserialize_duration_tests { } } -/// Deserializes a SocketAddr from a string. -pub fn deserialize_listener_addr<'de, D>(deserializer: D) -> Result<SocketAddr, D::Error> -where - D: serde::Deserializer<'de>, -{ - let raw = String::deserialize(deserializer)?; - let raw = raw.trim(); - - // Hostnames are not supported, but localhost shorthands are useful - if let Some(port) = raw.strip_prefix("localhost:") { - return port - .parse::<u16>() - .map(|port| SocketAddr::new(std::net::Ipv4Addr::LOCALHOST.into(), port)) - .map_err(|err| serde::de::Error::custom(format!("invalid port in '{raw}': {err}"))); - } - - raw.parse::<SocketAddr>() - .map_err(|err| serde::de::Error::custom(format!("invalid address '{raw}': {err}",))) -} - #[cfg(test)] mod deserialize_listener_addr_tests { use super::*; - use serde::Deserialize; + use ::serde::Deserialize; #[derive(Debug, Deserialize)] struct AddrWrapper { diff --git a/crates/repos/Cargo.toml b/crates/repos/Cargo.toml index 9924851..38bfc2e 100644 --- a/crates/repos/Cargo.toml +++ b/crates/repos/Cargo.toml @@ -14,9 +14,10 @@ homepage.workspace = true repository.workspace = true [dependencies] +base.workspace = true + async-trait.workspace = true bytes.workspace = true -mime_guess.workspace = true semver.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/repos/src/lib.rs b/crates/repos/src/lib.rs index d6344f7..14fd3a9 100644 --- a/crates/repos/src/lib.rs +++ b/crates/repos/src/lib.rs @@ -10,11 +10,11 @@ use std::time::Duration; use async_trait::async_trait; use bytes::Bytes; -use mime_guess::mime; use serde::{Deserialize, Serialize, de::DeserializeOwned}; use thiserror::Error; use url::Url; +pub use base::ContentType; pub use go::{GoBackend, GoConfig}; pub use zig::{ZigBackend, ZigConfig}; @@ -151,10 +151,10 @@ pub enum VersionType { /// Result of resolving a file request. pub enum ResolvedFile { /// Proxy request to upstream URL - Upstream { mime: mime::Mime, url: Url }, + Upstream { mime: ContentType, url: Url }, /// Return content directly Content { - mime: mime::Mime, + mime: ContentType, data: bytes::Bytes, }, } @@ -460,9 +460,9 @@ impl<S: BackendSpec> Backend<S> { let is_checksum_or_sig = parsed.is_sha256() || parsed.is_signature(); let mime = if is_checksum_or_sig { - mime::TEXT_PLAIN + ContentType::TextPlain } else { - mime::APPLICATION_OCTET_STREAM + ContentType::OctetStream }; let url = parsed.upstream_url(&self.config, &self.source)?; diff --git a/crates/zorian/Cargo.toml b/crates/zorian/Cargo.toml index d3ac7b9..4317d22 100644 --- a/crates/zorian/Cargo.toml +++ b/crates/zorian/Cargo.toml @@ -14,6 +14,7 @@ homepage.workspace = true repository.workspace = true [dependencies] +base.workspace = true repos.workspace = true async-trait.workspace = true @@ -29,8 +30,6 @@ hyper.workspace = true hyper-tls.workspace = true hyper-util.workspace = true maud.workspace = true -mime_guess.workspace = true -rust-embed.workspace = true rustls.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/zorian/src/assets/apple-touch-icon.png b/crates/zorian/src/assets/apple-touch-icon.png new file mode 100644 --- /dev/null +++ b/crates/zorian/src/assets/apple-touch-icon.png Binary files differ diff --git a/crates/zorian/src/assets/favicon-192.png b/crates/zorian/src/assets/favicon-192.png new file mode 100644 --- /dev/null +++ b/crates/zorian/src/assets/favicon-192.png Binary files differ diff --git a/crates/zorian/src/assets/favicon-512.png b/crates/zorian/src/assets/favicon-512.png new file mode 100644 --- /dev/null +++ b/crates/zorian/src/assets/favicon-512.png Binary files differ diff --git a/crates/zorian/src/assets/favicon.ico b/crates/zorian/src/assets/favicon.ico new file mode 100644 --- /dev/null +++ b/crates/zorian/src/assets/favicon.ico Binary files differ diff --git a/crates/zorian/src/assets/favicon.svg b/crates/zorian/src/assets/favicon.svg new file mode 100644 --- /dev/null +++ b/crates/zorian/src/assets/favicon.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns="http://www.w3.org/2000/svg" fill="#f7a41d" viewBox="-150 -150 800 800"> + <path d="M100 0v100H0v400h500V100H400V0H100m0 200h100v100H100zm200 0h100v100H300z"/> +</svg> diff --git a/crates/zorian/src/assets/manifest.webmanifest b/crates/zorian/src/assets/manifest.webmanifest new file mode 100644 --- /dev/null +++ b/crates/zorian/src/assets/manifest.webmanifest @@ -0,0 +1,8 @@ +{ + "name": "Zorian", + "icons": [ + { "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" }, + { "src": "/icon-512.png", "type": "image/png", "sizes": "512x512" }, + { "src": "/icon-512.png", "type": "image/png", "sizes": "512x512", "purpose": "maskable" } + ] +} diff --git a/crates/zorian/src/config.rs b/crates/zorian/src/config.rs index c05ce7e..5719d22 100644 --- a/crates/zorian/src/config.rs +++ b/crates/zorian/src/config.rs @@ -12,7 +12,7 @@ use serde::Deserialize; use thiserror::Error; use tracing::info; -use crate::utils::{deserialize_duration, deserialize_listener_addr}; +use base::serde::{deserialize_duration, deserialize_listener_addr}; use repos::{GoConfig, ZigConfig}; #[derive(Debug, Error)] diff --git a/crates/zorian/src/controller_backend.rs b/crates/zorian/src/controller_backend.rs index 70de572..d14d93b 100644 --- a/crates/zorian/src/controller_backend.rs +++ b/crates/zorian/src/controller_backend.rs @@ -4,7 +4,6 @@ use std::sync::Arc; use axum::{Router, body, extract, http, response, routing}; -use mime_guess::mime; use tracing::error; use crate::proxy; @@ -100,11 +99,11 @@ impl<S: BackendSpec> BackendController<S> { fn build_response( status: http::StatusCode, bytes: bytes::Bytes, - mime: mime::Mime, + mime: repos::ContentType, ) -> response::Response { response::Response::builder() .status(status) - .header(http::header::CONTENT_TYPE, mime.as_ref()) + .header(http::header::CONTENT_TYPE, mime.as_str()) .header(http::header::CONTENT_LENGTH, bytes.len()) .body(body::Body::from(bytes)) .unwrap() diff --git a/crates/zorian/src/controller_web.rs b/crates/zorian/src/controller_web.rs index 11391c8..8e753de 100644 --- a/crates/zorian/src/controller_web.rs +++ b/crates/zorian/src/controller_web.rs @@ -9,19 +9,48 @@ use std::task::{Context, Poll}; use axum::body::Body; use axum::http::{HeaderValue, Method, Request, Response, StatusCode, header}; use axum::{extract, routing}; -use chrono::{DateTime, Utc}; use maud::{Markup, html}; -use rust_embed::Embed; use serde::Deserialize; -use sqlx::types::chrono; use tower::{Layer, Service}; use tracing::error; +use base::ContentType; use repos::{GoBackend, ZigBackend}; -#[derive(Embed)] -#[folder = "src/assets/"] -struct Assets; +fn get_asset(path: &str) -> Option<(&'static [u8], ContentType)> { + Some(match path { + "base.css" => (include_bytes!("assets/base.css"), ContentType::TextCss), + "favicon.svg" => ( + include_bytes!("assets/favicon.svg"), + ContentType::ImageSvgXml, + ), + "favicon.ico" => ( + include_bytes!("assets/favicon.ico"), + ContentType::ImageXIcon, + ), + "favicon-192.png" | "apple-touch-icon.png" => ( + include_bytes!("assets/favicon-192.png"), + ContentType::ImagePng, + ), + "favicon-512.png" => ( + include_bytes!("assets/favicon-512.png"), + ContentType::ImagePng, + ), + "manifest.webmanifest" => ( + include_bytes!("assets/manifest.webmanifest"), + ContentType::ApplicationManifestJson, + ), + "jetbrainsmono/JetBrainsMono[wght].woff2" => ( + include_bytes!("assets/jetbrainsmono/JetBrainsMono[wght].woff2"), + ContentType::FontWoff2, + ), + "jetbrainsmono/JetBrainsMono-Italic[wght].woff2" => ( + include_bytes!("assets/jetbrainsmono/JetBrainsMono-Italic[wght].woff2"), + ContentType::FontWoff2, + ), + _ => return None, + }) +} const CSP: &str = "default-src 'self'; base-uri 'none'; img-src 'self'; font-src 'self'; style-src 'self'; script-src 'self'; object-src 'none'; frame-ancestors 'none'"; @@ -54,6 +83,7 @@ struct LicenseUsedBy { } #[derive(Deserialize)] +#[allow(dead_code)] struct LicenseCrate { name: String, version: String, @@ -79,52 +109,35 @@ impl WebController { impl WebController { pub fn router(self: Arc<Self>) -> axum::Router { - let pages = axum::Router::new() + axum::Router::new() .route("/", axum::routing::get(Self::index)) - .route("/licenses", axum::routing::get(Self::licenses)) + .route("/{*path}", routing::get(Self::assets)) + .route("/about/licenses", axum::routing::get(Self::licenses)) + .layer(CacheLayer) .layer(tower_http::set_header::SetResponseHeaderLayer::overriding( - header::CACHE_CONTROL, - HeaderValue::from_static("no-cache"), - )) - .with_state(self.clone()); - - let assets = axum::Router::new() - .route("/assets/{*path}", routing::get(Self::assets)) - .layer(LastModifiedLayer {}); - - axum::Router::new().merge(pages).merge(assets).layer( - tower_http::set_header::SetResponseHeaderLayer::overriding( header::CONTENT_SECURITY_POLICY, HeaderValue::from_static(CSP), - ), - ) + )) + .with_state(self.clone()) } async fn assets(extract::Path(path): extract::Path<String>) -> Response<Body> { - match Assets::get(&path) { - Some(file) => { - let mime = mime_guess::from_path(&path).first_or_octet_stream(); - let is_font = matches!( - path.rsplit('.').next(), - Some("ttf" | "otf" | "woff" | "woff2" | "eot") - ); - - let mut builder = Response::builder() - .status(StatusCode::OK) - .header(header::CONTENT_TYPE, mime.as_ref()); - - if is_font { - builder = builder - .header(header::CACHE_CONTROL, "public, max-age=31536000, immutable"); - } - - builder.body(Body::from(file.data.into_owned())).unwrap() - } - None => Response::builder() + let Some((data, content_type)) = get_asset(&path) else { + return Response::builder() .status(StatusCode::NOT_FOUND) .body(Body::empty()) - .unwrap(), + .unwrap(); + }; + + let mut builder = Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, content_type.as_str()); + + if matches!(content_type, ContentType::FontWoff2) { + builder = builder.header(header::CACHE_CONTROL, "public, max-age=31536000, immutable"); } + + builder.body(Body::from(data)).unwrap() } async fn index(extract::State(ctrl): extract::State<Arc<Self>>) -> Markup { @@ -152,81 +165,69 @@ impl WebController { Vec::new() }; - html! { - (maud::DOCTYPE) - - html lang="en" { - meta charset="UTF-8"; - meta name="viewport" content="width=device-width,initial-scale=1"; - meta http-equiv="X-UA-Compatible" content="ie=edge"; - - link rel="stylesheet" href="assets/base.css"; - title { "Earth PKG — tiny & opinionated packages mirror" } - } - - body { + WebController::layout( + "Zorian — tiny & opinionated packages mirror", + html! { h1 { "Zorian — tiny & opinionated packages mirror." } - main { - p { - r#"This site provides a caching proxy for downloading Zig and Go installation files. - It reduces load on upstream servers and makes your infrastructure more reliable by adding redundancy."# - } - - p { - "Zorian is open source software licensed under " a href="https://www.gnu.org/licenses/agpl-3.0.html" { "AGPL-3.0" } ". " - "Source code is available on " a href="https://github.com/mrdimidium/zorian" { "GitHub" } ". " - "A list of dependency licenses " a href="/licenses" { "is available" } "." - } + p { + r#"This site provides a caching proxy for downloading Zig and Go installation files. + It reduces load on upstream servers and makes your infrastructure more reliable by adding redundancy."# + } - h2 { "Usage" } + p { + "Zorian is open source software licensed under " a href="https://www.gnu.org/licenses/agpl-3.0.html" { "AGPL-3.0" } ". " + "Source code is available on " a href="https://github.com/mrdimidium/zorian" { "GitHub" } ". " + "A list of dependency licenses " a href="/about/licenses" { "is available" } "." + } - p { - "Replace official download URLs with " code { "https://pkg.earth/{tool}/{filename}" } ". " - "Files are cached automatically after the first download." - } + h2 { "Usage" } - h3 id="zig" { - a href="#zig" { "Zig" } - } + p { + "Replace official download URLs with " code { "https://pkg.earth/{tool}/{filename}" } ". " + "Files are cached automatically after the first download." + } - @if !zig_versions.is_empty() { - details { - summary { "Available versions (" (zig_versions.len()) ")" } + h3 id="zig" { + a href="#zig" { "Zig" } + } - p { "You can take actual minisig public key at " a href="https://ziglang.org/download/" { "ziglang.org/download" } "." } - table { - thead { - tr { - th { "Version" } - th { "Date" } - th { "Docs" } - th { "Targets" } - } + @if !zig_versions.is_empty() { + details { + summary { "Available versions (" (zig_versions.len()) ")" } + + p { "You can take actual minisig public key at " a href="https://ziglang.org/download/" { "ziglang.org/download" } "." } + table { + thead { + tr { + th { "Version" } + th { "Date" } + th { "Docs" } + th { "Targets" } } - tbody { - @for v in zig_versions.iter().rev() { - tr { - td { (v.version) } - td { (v.meta.date.as_deref().unwrap_or("-")) } - td { - @if let Some(ref url) = v.meta.docs { - a href=(url) { "docs" } - } - " " - @if let Some(ref url) = v.meta.std_docs { - a href=(url) { "std" } - } - " " - @if let Some(ref url) = v.meta.notes { - a href=(url) { "notes" } - } + } + tbody { + @for v in zig_versions.iter().rev() { + tr { + td { (v.version) } + td { (v.meta.date.as_deref().unwrap_or("-")) } + td { + @if let Some(ref url) = v.meta.docs { + a href=(url) { "docs" } + } + " " + @if let Some(ref url) = v.meta.std_docs { + a href=(url) { "std" } } - td { - @for file in &v.files { - a href=(format!("/zig/{}", file.filename)) { (&file.meta.target) } - " " - } + " " + @if let Some(ref url) = v.meta.notes { + a href=(url) { "notes" } + } + } + td { + @for file in &v.files { + a href=(format!("/zig/{}", file.filename)) { (&file.meta.target) } + " " } } } @@ -234,53 +235,53 @@ impl WebController { } } } + } - p { - "Read more about community mirrors in the " a href="https://ziglang.org/download/community-mirrors/" { "blog post" } ". " - "Information on how to deploy your own mirror is available " a href="https://github.com/ziglang/www.ziglang.org/blob/main/MIRRORS.md" { "in the documentation" } "." - } + p { + "Read more about community mirrors in the " a href="https://ziglang.org/download/community-mirrors/" { "blog post" } ". " + "Information on how to deploy your own mirror is available " a href="https://github.com/ziglang/www.ziglang.org/blob/main/MIRRORS.md" { "in the documentation" } "." + } - p { - "For simplicity, you can use tools like " a href="https://github.com/prantlf/zigup" { "prantlf/zigup" } - " and " a href="https://github.com/mlugg/setup-zig" { "mlugg/setup-zig" } "." - } + p { + "For simplicity, you can use tools like " a href="https://github.com/prantlf/zigup" { "prantlf/zigup" } + " and " a href="https://github.com/mlugg/setup-zig" { "mlugg/setup-zig" } "." + } - p { - "To install manually:" - ol { - li { "download zig dist file:" br; code { "wget https://pkg.earth/zig/zig-x86_64-linux-0.15.1.tar.xz" } ";" } - li { "download zig minisig file:" br; code { "wget https://pkg.earth/zig/zig-x86_64-linux-0.15.1.tar.xz.minisig" } ";" } - li { "check archive integrity:" br; code { "minisign -Vm zig-x86_64-linux-0.15.1.tar.xz -P RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U" } ";" } - li { "unpack archive:" br; code { "tar -xf 'zig-x86_64-linux-0.15.1.tar.xz'" } ";" } - li { "check installed zig:" br; code { "./zig-x86_64-linux-0.15.1/zig --version" } ";" } - } + p { + "To install manually:" + ol { + li { "download zig dist file:" br; code { "wget https://pkg.earth/zig/zig-x86_64-linux-0.15.1.tar.xz" } ";" } + li { "download zig minisig file:" br; code { "wget https://pkg.earth/zig/zig-x86_64-linux-0.15.1.tar.xz.minisig" } ";" } + li { "check archive integrity:" br; code { "minisign -Vm zig-x86_64-linux-0.15.1.tar.xz -P RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U" } ";" } + li { "unpack archive:" br; code { "tar -xf 'zig-x86_64-linux-0.15.1.tar.xz'" } ";" } + li { "check installed zig:" br; code { "./zig-x86_64-linux-0.15.1/zig --version" } ";" } } + } - h3 id="go" { a href="#go" { "Go" } } + h3 id="go" { a href="#go" { "Go" } } - @if !go_versions.is_empty() { - details { - summary { "Available versions (" (go_versions.len()) ")" } + @if !go_versions.is_empty() { + details { + summary { "Available versions (" (go_versions.len()) ")" } - p { "You can find available versions at " a href="https://go.dev/dl/" { "go.dev/dl" } "." } - table { - thead { - tr { - th { "Version" } - th { "Stable" } - th { "Files" } - } + p { "You can find available versions at " a href="https://go.dev/dl/" { "go.dev/dl" } "." } + table { + thead { + tr { + th { "Version" } + th { "Stable" } + th { "Files" } } - tbody { - @for v in go_versions.iter().rev() { - tr { - td { (v.version) } - td { @if v.meta.stable { "✓" } @else { "" } } - td { - @for file in &v.files { - a href=(format!("/go/{}", file.filename)) { (file.filename) } - " " - } + } + tbody { + @for v in go_versions.iter().rev() { + tr { + td { (v.version) } + td { @if v.meta.stable { "✓" } @else { "" } } + td { + @for file in &v.files { + a href=(format!("/go/{}", file.filename)) { (file.filename) } + " " } } } @@ -288,85 +289,99 @@ impl WebController { } } } + } - p { "To install manually:" } + p { "To install manually:" } - ol { - li { "download go dist file:" br; code { "wget https://pkg.earth/go/go1.23.0.linux-amd64.tar.gz" } ";" } - li { "download go sha256 file:" br; code { "wget https://pkg.earth/go/go1.23.0.linux-amd64.tar.gz.sha256" } ";" } - li { "check archive integrity:" br; code { "sha256sum -c go1.23.0.linux-amd64.tar.gz.sha256" } ";" } - li { "unpack archive:" br; code { "tar -xzf go1.23.0.linux-amd64.tar.gz" } ";" } - li { "check installed go:" br; code { "./go/bin/go version" } ";" } - } + ol { + li { "download go dist file:" br; code { "wget https://pkg.earth/go/go1.23.0.linux-amd64.tar.gz" } ";" } + li { "download go sha256 file:" br; code { "wget https://pkg.earth/go/go1.23.0.linux-amd64.tar.gz.sha256" } ";" } + li { "check archive integrity:" br; code { "sha256sum -c go1.23.0.linux-amd64.tar.gz.sha256" } ";" } + li { "unpack archive:" br; code { "tar -xzf go1.23.0.linux-amd64.tar.gz" } ";" } + li { "check installed go:" br; code { "./go/bin/go version" } ";" } + } - h2 { "Privacy policy" } + h2 { "Privacy policy" } - p { "This mirror is a non-profit project available on a voluntary basis. The author has no plans to fund it." } + p { "This mirror is a non-profit project available on a voluntary basis. The author has no plans to fund it." } - p { r#"Since the mirror is hosted on hardware, we collect access logs to combat bots and brute-force attacks. - The logs are used for security purposes and load planning, are not shared with third parties, - and are deleted after 30 days."# } + p { r#"Since the mirror is hosted on hardware, we collect access logs to combat bots and brute-force attacks. + The logs are used for security purposes and load planning, are not shared with third parties, + and are deleted after 30 days."# } - p { "Third-party analytics systems are not used, same as client-side trackers." } - } - } - } + p { "Third-party analytics systems are not used, same as client-side trackers." } + }, + ) } async fn licenses() -> Markup { let data = &*LICENSES; + WebController::layout( + "Third Party Licenses", + html! { + h1 { "Third Party Licenses" } + + p { "This page lists the licenses of the projects used in Zorian." } + + h2 { "Overview of licenses:" } + ul { + @for ov in &data.overview { + li { a href=(format!("#{}", ov.id)) { (&ov.name) } " (" (ov.count) ")" } + } + } + + h2 { "All license text:" } + @for (i, lic) in data.licenses.iter().enumerate() { + div.license { + @if lic.first_of_kind { + h3 id=(&lic.id) { (&lic.name) } + } + h4 id=(format!("{}-{}", lic.id, i)) { (&lic.name) } + + p { "Used by:" } + + ul .license-used-by { + @for usage in &lic.used_by { + li { + a href=( + usage.crate_.repository.as_deref() + .unwrap_or(&format!("https://crates.io/crates/{}", usage.crate_.name)) + ) { + (&usage.crate_.name) "@" (&usage.crate_.version) + } + } + } + } + + pre .license-text { (&lic.text) } + } + } + }, + ) + } + + fn layout(title: &str, content: Markup) -> Markup { html! { (maud::DOCTYPE) html lang="en" { head { meta charset="UTF-8"; + meta http-equiv="X-UA-Compatible" content="ie=edge"; meta name="viewport" content="width=device-width,initial-scale=1"; - link rel="stylesheet" href="assets/base.css"; - title { "Third Party Licenses — Earth PKG" } + title { (title) } + link rel="stylesheet" href="/base.css"; + + link rel="manifest" href="/manifest.webmanifest"; + link rel="icon" href="/favicon.ico" sizes="32x32"; + link rel="icon" href="/favicon.svg" type="image/svg+xml"; + link rel="apple-touch-icon" href="/apple-touch-icon.png"; } body { - main { - h1 { "Third Party Licenses" } - p { "This page lists the licenses of the projects used in Zorian." } - - h2 { "Overview of licenses:" } - ul { - @for ov in &data.overview { - li { a href=(format!("#{}", ov.id)) { (&ov.name) } " (" (ov.count) ")" } - } - } - - h2 { "All license text:" } - - @for (i, lic) in data.licenses.iter().enumerate() { - div.license { - @if lic.first_of_kind { - h3 id=(&lic.id) { (&lic.name) } - } - h4 id=(format!("{}-{}", lic.id, i)) { (&lic.name) } - - h5 { "Used by:" } - ul .license-used-by { - @for usage in &lic.used_by { - li { - a href=( - usage.crate_.repository.as_deref() - .unwrap_or(&format!("https://crates.io/crates/{}", usage.crate_.name)) - ) { - (&usage.crate_.name) " " (&usage.crate_.version) - } - } - } - } - - pre .license-text { (&lic.text) } - } - } - } + (content) } } } @@ -374,26 +389,21 @@ impl WebController { } #[derive(Clone)] -pub struct LastModifiedLayer {} -impl<S> Layer<S> for LastModifiedLayer { - type Service = LastModifiedService<S>; - fn layer(&self, inner: S) -> Self::Service { - LastModifiedService { - inner, +struct CacheLayer; - // Since the static file is packaged in a binary, we cache it across restarts. - // In the future, we can use the build time. - last_moidified: Utc::now(), - } +impl<S> Layer<S> for CacheLayer { + type Service = CacheService<S>; + fn layer(&self, inner: S) -> Self::Service { + CacheService { inner } } } #[derive(Clone)] -pub struct LastModifiedService<S> { +struct CacheService<S> { inner: S, - last_moidified: DateTime<Utc>, } -impl<S> Service<Request<Body>> for LastModifiedService<S> + +impl<S> Service<Request<Body>> for CacheService<S> where S: Service<Request<Body>, Response = Response<Body>> + Clone + Send + 'static, S::Future: Send + 'static, @@ -409,48 +419,50 @@ where fn call(&mut self, req: Request<Body>) -> Self::Future { let method = req.method().clone(); - let headers = req.headers().clone(); - let last_modified = self.last_moidified; + let if_none_match = req.headers().get(header::IF_NONE_MATCH).cloned(); let mut inner = self.inner.clone(); Box::pin(async move { - let raw = last_modified - .format("%a, %d %b %Y %H:%M:%S GMT") - .to_string(); - let lmh = HeaderValue::from_str(raw.as_str()).unwrap(); + let resp = inner.call(req).await?; + + if !resp.status().is_success() { + return Ok(resp); + } + + let (parts, body) = resp.into_parts(); + let bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap(); + + let mut hasher = crc32fast::Hasher::new(); + hasher.update(&bytes); + let etag = format!("\"{}\"", hex::encode(hasher.finalize().to_be_bytes())); + let etag_header = HeaderValue::from_str(&etag).unwrap(); + let cache_control = parts + .headers + .get(header::CACHE_CONTROL) + .cloned() + .unwrap_or(HeaderValue::from_static("no-cache")); if (method == Method::GET || method == Method::HEAD) - && headers - .get(header::IF_MODIFIED_SINCE) - .and_then(|v| v.to_str().ok()) - .and_then(parse_http_date) - .is_some_and(|ims| last_modified <= ims) + && if_none_match + .as_ref() + .is_some_and(|v| v.as_bytes() == etag.as_bytes()) { - match Response::builder() + let mut resp = Response::builder() .status(StatusCode::NOT_MODIFIED) - .header(header::LAST_MODIFIED, lmh.clone()) .body(Body::empty()) - { - Ok(resp) => { - return Ok(resp); - } - Err(err) => { - error!("build response: {err}") - } - } + .unwrap(); + resp.headers_mut().insert(header::ETAG, etag_header); + resp.headers_mut() + .insert(header::CACHE_CONTROL, cache_control); + return Ok(resp); } - let mut resp = inner.call(req).await?; - resp.headers_mut().insert(header::LAST_MODIFIED, lmh); + let mut resp = Response::from_parts(parts, Body::from(bytes)); + resp.headers_mut().insert(header::ETAG, etag_header); + resp.headers_mut() + .entry(header::CACHE_CONTROL) + .or_insert(HeaderValue::from_static("no-cache")); Ok(resp) }) } } - -/// Parse HTTP-date (IMF-fixdate per RFC 7231) or RFC 2822 -fn parse_http_date(value: &str) -> Option<DateTime<Utc>> { - // IMF-fixdate: "Tue, 14 Jan 2026 12:34:56 GMT" - DateTime::parse_from_rfc2822(value.trim()) - .map(|d| d.with_timezone(&Utc)) - .ok() -} diff --git a/crates/zorian/src/main.rs b/crates/zorian/src/main.rs index 7b8ed42..c43c9d6 100644 --- a/crates/zorian/src/main.rs +++ b/crates/zorian/src/main.rs @@ -5,7 +5,6 @@ mod config; mod proxy; mod storage; mod telemetry; -mod utils; mod controller_backend; mod controller_web; diff --git a/deny.toml b/deny.toml index 0f7b2a5..563ab6a 100644 --- a/deny.toml +++ b/deny.toml @@ -22,9 +22,10 @@ allow = [ ] exceptions = [ # The project itself is distributed under the AGPL, but avoid it in dependencies - { crate = "zorian", allow = ["AGPL-3.0-or-later"] }, - { crate = "repos", allow = ["AGPL-3.0-or-later"] }, { crate = "xtask", allow = ["AGPL-3.0-or-later"] }, + { crate = "base", allow = ["AGPL-3.0-or-later"] }, + { crate = "repos", allow = ["AGPL-3.0-or-later"] }, + { crate = "zorian", allow = ["AGPL-3.0-or-later"] }, ] unused-allowed-license = "allow" diff --git a/pkg/logo.svg b/pkg/logo.svg new file mode 100644 --- /dev/null +++ b/pkg/logo.svg @@ -0,0 +1,30 @@ +<!-- +SPDX-FileCopyrightText: 2026 Nikolay Govorov <me@govorov.online> +SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg viewBox="-150 -150 800 800" xmlns="http://www.w3.org/2000/svg" fill="rgb(247, 164, 29)"> + <rect x="0" y="100" width="100" height="100"></rect> + <rect x="0" y="200" width="100" height="100"></rect> + <rect x="0" y="300" width="100" height="100"></rect> + <rect x="0" y="400" width="100" height="100"></rect> + <rect x="100" y="0" width="100" height="100"></rect> + <rect x="100" y="100" width="100" height="100"></rect> + <rect x="100" y="300" width="100" height="100"></rect> + <rect x="100" y="400" width="100" height="100"></rect> + <rect x="200" y="0" width="100" height="100"></rect> + <rect x="200" y="100" width="100" height="100"></rect> + <rect x="200" y="200" width="100" height="100"></rect> + <rect x="200" y="300" width="100" height="100"></rect> + <rect x="200" y="400" width="100" height="100"></rect> + <rect x="400" y="100" width="100" height="100"></rect> + <rect x="400" y="200" width="100" height="100"></rect> + <rect x="400" y="300" width="100" height="100"></rect> + <rect x="400" y="400" width="100" height="100"></rect> + <rect x="300" y="0" width="100" height="100"></rect> + <rect x="300" y="100" width="100" height="100"></rect> + <rect x="300" y="300" width="100" height="100"></rect> + <rect x="300" y="400" width="100" height="100"></rect> +</svg> diff --git a/xtask/build.rs b/xtask/build.rs new file mode 100644 --- /dev/null +++ b/xtask/build.rs @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2026 Nikolay Govorov <me@govorov.online> +// SPDX-License-Identifier: AGPL-3.0-or-later + +fn main() { + println!( + "cargo:rustc-env=TARGET={}", + std::env::var("TARGET").unwrap() + ); +} diff --git a/xtask/src/licenses.rs b/xtask/src/licenses.rs index 162a090..49736d8 100644 --- a/xtask/src/licenses.rs +++ b/xtask/src/licenses.rs @@ -135,7 +135,7 @@ fn get_all_crates(cargo_toml: &Utf8Path) -> Result<Krates, krates::Error> { builder.ignore_kind(krates::DepKind::Build, krates::Scope::NonWorkspace); } - builder.include_targets(std::iter::empty::<(&str, Vec<String>)>()); + builder.include_targets(std::iter::once((env!("TARGET"), vec![]))); let graph = builder.build(mdc, |filtered: cm::Package| { tracing::debug!("filtered {} {}", filtered.name, filtered.version); |
