aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat
-rw-r--r--Cargo.lock22+22 −0
-rw-r--r--Cargo.toml3+2 −1
-rw-r--r--src/backends/mod.rs4+4 −0
-rw-r--r--src/backends/zig.rs (renamed from src/controller_zig.rs)34+20 −14
-rw-r--r--src/config.rs (renamed from src/service_config.rs)0+0 −0
-rw-r--r--src/controller_web.rs36+0 −36
-rw-r--r--src/index.html100+0 −100
-rw-r--r--src/main.rs270+233 −37
-rw-r--r--src/storage.rs (renamed from src/service_storage.rs)2+1 −1
-rw-r--r--src/upstream.rs (renamed from src/service_upstream.rs)0+0 −0
10 files changed, 282 insertions, 189 deletions
diff --git a/Cargo.lock b/Cargo.lock
index bd1d6bc..e9ea97e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -200,6 +200,27 @@ dependencies = [
]
[[package]]
+name = "axum-extra"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76"
+dependencies = [
+ "axum",
+ "axum-core",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
name = "axum-macros"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4944,6 +4965,7 @@ name = "zorian"
version = "0.1.0"
dependencies = [
"axum",
+ "axum-extra",
"bytes",
"cargo-deny",
"chrono",
diff --git a/Cargo.toml b/Cargo.toml
index 11e5f9a..85f596c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,6 +10,7 @@ repository = "https://github.com/mrdimidium/Zorian"
[dependencies]
axum = { version = "0.8.8", features = ["http2", "macros", "multipart"] }
+axum-extra = "0.12.5"
bytes = "1.11.0"
chrono = "0.4"
crc32fast = "1.4.2"
@@ -25,10 +26,10 @@ sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio", "macros", "de
thiserror = "2.0"
tokio = { version = "1.49.0", features = ["full"] }
toml = "0.9.8"
+tower = "0.5.3"
tower-http = { version = "0.6.8", features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
-tower = "0.5.3"
uuid = { version = "1.19.0", features = ["v4","v5"] }
[dev-dependencies]
diff --git a/src/backends/mod.rs b/src/backends/mod.rs
new file mode 100644
--- /dev/null
+++ b/src/backends/mod.rs
@@ -0,0 +1,4 @@
+// SPDX-FileCopyrightText: 2026 Nikolay Govorov <me@govorov.online>
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+pub mod zig;
diff --git a/src/controller_zig.rs b/src/backends/zig.rs
index 7e9e6a8..44ac3bc 100644
--- a/src/controller_zig.rs
+++ b/src/backends/zig.rs
@@ -8,9 +8,9 @@ use semver::Version;
use thiserror::Error;
use tracing::error;
-use crate::service_config;
-use crate::service_storage;
-use crate::service_upstream;
+use crate::config;
+use crate::storage;
+use crate::upstream;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Archive {
@@ -51,16 +51,22 @@ impl<'a> Tarball<'a> {
let archive;
let tarball_type;
- // (?:|-bootstrap|-[a-zA-Z0-9_]+-[a-zA-Z0-9_]+)-(\d+\.\d+\.\d+(?:-dev\.\d+\+[0-9a-f]+)?)\.(?:tar\.xz|zip)(?:\.minisig)?
+ // (?:|-bootstrap|-[a-zA-Z0-9_]+-[a-zA-Z0-9_]+)-(
+ // \d+\.\d+\.\d+(?:-dev\.\d+\+[0-9a-f]+)?
+ // )\.(?:tar\.xz|zip)(?:\.minisig)?
buffer = buffer.strip_prefix("zig-").ok_or(ParseError)?;
- // (?:|bootstrap|[a-zA-Z0-9_]+-[a-zA-Z0-9_]+)-(\d+\.\d+\.\d+(?:-dev\.\d+\+[0-9a-f]+)?)\.(?:tar\.xz|zip)
+ // (?:|bootstrap|[a-zA-Z0-9_]+-[a-zA-Z0-9_]+)-(
+ // \d+\.\d+\.\d+(?:-dev\.\d+\+[0-9a-f]+)?
+ // )\.(?:tar\.xz|zip)
if let Some(it) = buffer.strip_suffix(".minisig") {
buffer = it;
minisig = true;
}
- // (?:|bootstrap|[a-zA-Z0-9_]+-[a-zA-Z0-9_]+)-(\d+\.\d+\.\d+(?:-dev\.\d+\+[0-9a-f]+)?)
+ // (?:|bootstrap|[a-zA-Z0-9_]+-[a-zA-Z0-9_]+)-(
+ // \d+\.\d+\.\d+(?:-dev\.\d+\+[0-9a-f]+)?
+ // )
if let Some(it) = buffer.strip_suffix(".zip") {
buffer = it;
archive = Archive::Zip;
@@ -142,16 +148,16 @@ impl<'a> Tarball<'a> {
}
pub struct ZigController {
- config: Arc<service_config::ConfigService>,
- storage: Arc<service_storage::StorageService>,
- upstream: Arc<service_upstream::UpstreamService>,
+ config: Arc<config::ConfigService>,
+ storage: Arc<storage::StorageService>,
+ upstream: Arc<upstream::UpstreamService>,
}
impl ZigController {
pub fn new(
- config: Arc<service_config::ConfigService>,
- storage: Arc<service_storage::StorageService>,
- upstream: Arc<service_upstream::UpstreamService>,
+ config: Arc<config::ConfigService>,
+ storage: Arc<storage::StorageService>,
+ upstream: Arc<upstream::UpstreamService>,
) -> Self {
Self {
config,
@@ -189,13 +195,13 @@ impl ZigController {
let entry = controller
.upstream
- .fetch(service_upstream::DownloadRequest { url })
+ .fetch(upstream::DownloadRequest { url })
.await?;
match controller.storage.put("zig", &filename, &entry.bytes).await {
Ok(()) => {}
Err(_) => {
- return Err(http::StatusCode::OK);
+ return Err(http::StatusCode::INTERNAL_SERVER_ERROR);
}
}
diff --git a/src/service_config.rs b/src/config.rs
index dd55bfc..dd55bfc 100644
--- a/src/service_config.rs
+++ b/src/config.rs
diff --git a/src/controller_web.rs b/src/controller_web.rs
deleted file mode 100644
--- a/src/controller_web.rs
+++ /dev/null
@@ -1,36 +0,0 @@
-// SPDX-FileCopyrightText: 2026 Nikolay Govorov <me@govorov.online>
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-use axum::{Router, extract, http, response, routing};
-use std::sync::Arc;
-
-/// Handles html pages rendering and static files
-pub struct WebController {
- jinja: minijinja::Environment<'static>,
-}
-
-impl WebController {
- pub fn new() -> Self {
- let mut jinja = minijinja::Environment::new();
- jinja
- .add_template("index", include_str!("./index.html"))
- .unwrap();
-
- Self { jinja }
- }
-
- pub fn router(self: Arc<Self>) -> Router {
- Router::new()
- .route("/", routing::get(Self::index))
- .with_state(self)
- }
-
- async fn index(
- extract::State(controller): extract::State<Arc<Self>>,
- ) -> Result<response::Html<String>, http::StatusCode> {
- let template = controller.jinja.get_template("index").unwrap();
- let rendered = template.render(minijinja::context! {}).unwrap();
-
- Ok(response::Html(rendered))
- }
-}
diff --git a/src/index.html b/src/index.html
deleted file mode 100644
--- a/src/index.html
+++ /dev/null
@@ -1,100 +0,0 @@
-<!--
-SPDX-FileCopyrightText: 2026 Nikolay Govorov <me@govorov.online>
-SPDX-License-Identifier: AGPL-3.0-or-later
--->
-<!DOCTYPE html>
-<html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width,initial-scale=1">
- <meta http-equiv="X-UA-Compatible" content="ie=edge">
-
- <title>Earth PKG — tiny & opinionated packages mirror</title>
- <style>
- :root {
- font-size: 1.125rem;
- line-height: 1.4;
- font-family:
- 'Alegreya Sans', -apple-system, BlinkMacSystemFont,
- 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
- 'Open Sans', 'Helvetica Neue', sans-serif;
- }
-
- html {
- margin: 0;
- padding: 0;
- }
-
- body {
- margin: 0 auto;
- padding: 1.5em 2em;
- max-width: 680px;
- }
-
- code {
- font-size: .75rem;
- }
-
- h1, h2, h3, h4, h5, h6, h7, h8, h9 {
- font-weight: 700;
- line-height: 1.2;
- margin: 0;
- }
-
- h1 {
- font-size: 2.75rem;
- }
-
- h1:first-child {
- margin-top: 0;
- }
-
- th {
- text-align: start;
- }
- </style>
- </head>
-
- <body>
- <h1>
- Earth PKG — tiny & opinionated packages mirror.
- </h1>
-
- <content>
- <p>This site provides a proxy for downloading zig installation files and dependencies.
- On the one hand, this reduces the load on the original project's site
- and makes your infrastructure more reliable by adding redundancy.</p>
-
- <p>Read more about community mirrors in the <a href="https://ziglang.org/download/community-mirrors/">blog post</a>.
- 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</a>.</p>
-
- <h2>Direct usage:</h2>
-
- <p>For simplicity, you can use tools like <a href="https://github.com/prantlf/zigup">prantlf/zigup</a> and
- <a href="https://github.com/mlugg/setup-zig">mlugg/setup-zig</a>.</p>
-
- <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</code>;</li>
- <li>download zig minisign file:<br><code>wget https://pkg.earth/zig/zig-x86_64-linux-0.15.1.tar.xz.minisig</code>;</li>
- <li>check archive integrity:<br><code>minisign -Vm zig-x86_64-linux-0.15.1.tar.xz -P RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U</code>;</li>
- <li>unpack arhive:<br><code>tar -xf "zig-x86_64-linux-0.15.1.tar.xz"</code>;</li>
- <li>check installed zig:<br><code>./zig-x86_64-linux-0.15.1/zig --version</code>.</li>
- </ol>
-
- <city>You can take actual minisign public key in <a href="https://ziglang.org/download/">download page</a></city>.
- </p>
-
- <h2>Privacy policy</h2>
-
- <p>This mirror is a non-profit project available on a voluntary basis. The author has no plans to fund it.</p>
-
- <p>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>
-
- <p>Third-party analytics systems are not used same as client-side trackers.</p>
- </content>
-</html>
diff --git a/src/main.rs b/src/main.rs
index 6d74b09..d2317c8 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,10 +1,25 @@
// SPDX-FileCopyrightText: 2026 Nikolay Govorov <me@govorov.online>
// SPDX-License-Identifier: AGPL-3.0-or-later
+mod backends;
+mod config;
+mod storage;
+mod upstream;
+
+use std::future::Future;
use std::path::PathBuf;
+use std::pin::Pin;
use std::sync::Arc;
+use std::task::{Context, Poll};
-use axum::{Router, http};
+use axum::{
+ Router,
+ body::Body,
+ extract,
+ http::{self, Request, Response},
+ response::{self, IntoResponse},
+ routing,
+};
use tower_http::{
request_id,
trace::{DefaultOnFailure, TraceLayer},
@@ -12,11 +27,7 @@ use tower_http::{
use tracing::{error, info};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
-mod controller_web;
-mod controller_zig;
-mod service_config;
-mod service_storage;
-mod service_upstream;
+use crate::backends::zig::ZigController;
static REQUEST_ID_HEADER: http::HeaderName = http::HeaderName::from_static("x-request-id");
@@ -66,14 +77,14 @@ async fn main() {
Some(path) => {
info!("use config file from {}", path.to_str().unwrap());
- service_config::ConfigService::from_file(&path).unwrap_or_else(|e| {
+ config::ConfigService::from_file(&path).unwrap_or_else(|e| {
error!("{e}");
std::process::exit(1);
})
}
None => {
info!("configuration file path not provided");
- service_config::ConfigService::default()
+ config::ConfigService::default()
}
});
config.validate().unwrap_or_else(|e| {
@@ -81,21 +92,17 @@ async fn main() {
std::process::exit(1);
});
- let storage = Arc::new(
- service_storage::StorageService::new(config.clone())
- .await
- .unwrap(),
- );
- let upstream = Arc::new(service_upstream::UpstreamService::new());
+ let storage = Arc::new(storage::StorageService::new(config.clone()).await.unwrap());
+ let upstream = Arc::new(upstream::UpstreamService::new());
- let web_controller = Arc::new(controller_web::WebController::new());
- let zig_controller = Arc::new(controller_zig::ZigController::new(
+ let web_controller = Arc::new(WebController::default());
+ let zig_controller = Arc::new(ZigController::new(
config.clone(),
storage.clone(),
upstream.clone(),
));
- let accept_logger = TraceLayer::new_for_http()
+ let trace_layer = TraceLayer::new_for_http()
.make_span_with(|req: &http::Request<_>| {
let request_id = req
.headers()
@@ -105,14 +112,15 @@ async fn main() {
tracing::info_span!("http_request", request_id = %request_id)
})
- .on_request(log_request)
- .on_response(log_response)
+ .on_request(())
+ .on_response(())
.on_failure(DefaultOnFailure::new().level(tracing::Level::ERROR));
let app = Router::new()
.merge(web_controller.router())
.merge(zig_controller.router())
- .layer(accept_logger)
+ .layer(LoggingLayer)
+ .layer(trace_layer)
.layer(request_id::PropagateRequestIdLayer::new(
REQUEST_ID_HEADER.clone(),
))
@@ -129,31 +137,68 @@ async fn main() {
axum::serve(listener, app).await.unwrap();
}
-fn log_request<B>(req: &http::Request<B>, _span: &tracing::Span) {
- let headers = req.headers();
+#[derive(Clone)]
+pub struct LoggingLayer;
+
+impl<S> tower::Layer<S> for LoggingLayer {
+ type Service = LoggingService<S>;
+
+ fn layer(&self, inner: S) -> Self::Service {
+ LoggingService { inner }
+ }
+}
+
+#[derive(Clone)]
+pub struct LoggingService<S> {
+ inner: S,
+}
+
+impl<S> tower::Service<Request<Body>> for LoggingService<S>
+where
+ S: tower::Service<Request<Body>, Response = Response<Body>> + Clone + Send + 'static,
+ S::Future: Send,
+{
+ type Response = S::Response;
+ type Error = S::Error;
+ type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
+
+ fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
+ self.inner.poll_ready(cx)
+ }
+
+ fn call(&mut self, req: Request<Body>) -> Self::Future {
+ Box::pin(log_request(self.inner.clone(), req))
+ }
+}
+
+async fn log_request<S>(mut inner: S, req: Request<Body>) -> Result<Response<Body>, S::Error>
+where
+ S: tower::Service<Request<Body>, Response = Response<Body>>,
+{
+ let start = std::time::Instant::now();
+
+ let method = req.method().clone();
+ let uri = req.uri().clone();
+ let headers = req.headers();
let client_ip = headers
.get("x-forwarded-for")
.or_else(|| headers.get("x-real-ip"))
- .and_then(|v| v.to_str().ok());
+ .and_then(|v| v.to_str().ok())
+ .map(String::from);
let user_agent = headers
.get(http::header::USER_AGENT)
- .and_then(|v| v.to_str().ok());
+ .and_then(|v| v.to_str().ok())
+ .map(String::from);
let referer = headers
.get(http::header::REFERER)
- .and_then(|v| v.to_str().ok());
+ .and_then(|v| v.to_str().ok())
+ .map(String::from);
- info!(
- method = %req.method(),
- uri = %req.uri(),
- client_ip,
- user_agent,
- referer,
- "request started"
- );
-}
+ let res = inner.call(req).await?;
-fn log_response<B>(res: &http::Response<B>, latency: std::time::Duration, _span: &tracing::Span) {
+ let latency = start.elapsed();
+ let status = res.status();
let headers = res.headers();
let content_length = headers
.get(http::header::CONTENT_LENGTH)
@@ -163,10 +208,161 @@ fn log_response<B>(res: &http::Response<B>, latency: std::time::Duration, _span:
.and_then(|v| v.to_str().ok());
info!(
- status = %res.status(),
+ method = %method,
+ uri = %uri,
+ status = %status,
latency = ?latency,
+ client_ip = client_ip.as_deref(),
+ user_agent = user_agent.as_deref(),
+ referer = referer.as_deref(),
content_length,
content_type,
- "request finished"
+ "request"
);
+
+ Ok(res)
+}
+
+/// Handles html pages rendering and static files
+pub struct WebController {
+ jinja: minijinja::Environment<'static>,
+}
+
+impl Default for WebController {
+ fn default() -> Self {
+ let mut jinja = minijinja::Environment::new();
+ jinja.add_template("index", HTML).unwrap();
+
+ Self { jinja }
+ }
}
+
+impl WebController {
+ pub fn router(self: Arc<Self>) -> Router {
+ Router::new()
+ .route("/index.css", routing::get(Self::styles))
+ .route("/", routing::get(Self::index))
+ .with_state(self)
+ }
+
+ async fn index(
+ extract::State(controller): extract::State<Arc<Self>>,
+ ) -> Result<response::Response, http::StatusCode> {
+ let template = controller.jinja.get_template("index").unwrap();
+ let rendered = template.render(minijinja::context! {}).unwrap();
+
+ let mut response = response::Html(rendered).into_response();
+ response.headers_mut().insert(
+ http::header::CONTENT_SECURITY_POLICY,
+ http::HeaderValue::from_static(
+ "img-src 'self'; base-uri 'none'; font-src 'self'; style-src 'self'; script-src 'self'; object-src 'none'; default-src 'self'; frame-ancestors 'none'",
+ ),
+ );
+
+ Ok(response)
+ }
+
+ async fn styles() -> Result<axum_extra::response::Css<&'static str>, http::StatusCode> {
+ Ok(axum_extra::response::Css(CSS))
+ }
+}
+
+const HTML: &str = r#"
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width,initial-scale=1">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+
+ <title>Earth PKG — tiny & opinionated packages mirror</title>
+ <link rel="stylesheet" href="index.css">
+ </head>
+
+ <body>
+ <h1>Earth PKG — tiny & opinionated packages mirror.</h1>
+
+ <main>
+ <p>This site provides a proxy for downloading zig installation files and dependencies.
+ On the one hand, this reduces the load on the original project's site
+ and makes your infrastructure more reliable by adding redundancy.</p>
+
+ <p>Read more about community mirrors in the <a href="https://ziglang.org/download/community-mirrors/">blog post</a>.
+ 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</a>.</p>
+
+ <h2>Direct usage:</h2>
+
+ <p>For simplicity, you can use tools like <a href="https://github.com/prantlf/zigup">prantlf/zigup</a> and
+ <a href="https://github.com/mlugg/setup-zig">mlugg/setup-zig</a>.</p>
+
+ <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</code>;</li>
+ <li>download zig minisign file:<br><code>wget https://pkg.earth/zig/zig-x86_64-linux-0.15.1.tar.xz.minisig</code>;</li>
+ <li>check archive integrity:<br><code>minisign -Vm zig-x86_64-linux-0.15.1.tar.xz -P RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U</code>;</li>
+ <li>unpack archive:<br><code>tar -xf "zig-x86_64-linux-0.15.1.tar.xz"</code>;</li>
+ <li>check installed zig:<br><code>./zig-x86_64-linux-0.15.1/zig --version</code>.</li>
+ </ol>
+
+ You can take actual minisign public key in <a href="https://ziglang.org/download/">download page</a>.
+ </p>
+
+ <h2>Privacy policy</h2>
+
+ <p>This mirror is a non-profit project available on a voluntary basis. The author has no plans to fund it.</p>
+
+ <p>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>
+
+ <p>Third-party analytics systems are not used, same as client-side trackers.</p>
+ </main>
+ </body>
+</html>
+"#;
+
+const CSS: &str = "
+:root {
+ font-size: 1.125rem;
+ line-height: 1.4;
+ font-family:
+ 'Alegreya Sans', -apple-system, BlinkMacSystemFont,
+ 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
+ 'Open Sans', 'Helvetica Neue', sans-serif;
+}
+
+html {
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ margin: 0 auto;
+ padding: 1.5em 2em;
+ max-width: 680px;
+}
+
+code {
+ font-size: .75rem;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ font-weight: 700;
+ line-height: 1.2;
+ margin: 0;
+}
+
+h1 {
+ font-size: 2.75rem;
+}
+
+h1:first-child {
+ margin-top: 0;
+}
+
+th {
+ text-align: start;
+}
+";
diff --git a/src/service_storage.rs b/src/storage.rs
index f1f04b2..960afc5 100644
--- a/src/service_storage.rs
+++ b/src/storage.rs
@@ -38,7 +38,7 @@ use tokio::io::AsyncWriteExt;
use tracing::{debug, instrument, warn};
use uuid::Uuid;
-use super::service_config::ConfigService;
+use super::config::ConfigService;
const SQLITE_POOL_SIZE: u32 = 16;
const INLINE_THRESHOLD: usize = 256 * 1024; // 256 KB
diff --git a/src/service_upstream.rs b/src/upstream.rs
index da25a7d..da25a7d 100644
--- a/src/service_upstream.rs
+++ b/src/upstream.rs