commit 02

This commit is contained in:
Moritz Ruth 2025-02-28 17:54:38 +01:00
parent 7a65b34ef4
commit 556035d278
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
9 changed files with 492 additions and 279 deletions

299
Cargo.lock generated
View file

@ -237,6 +237,7 @@ dependencies = [
"blocking",
"futures-lite 2.6.0",
"once_cell",
"tokio 1.43.0",
]
[[package]]
@ -315,6 +316,18 @@ dependencies = [
"pin-project-lite 0.2.16",
]
[[package]]
name = "async-native-tls"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e9e7a929bd34c68a82d58a4de7f86fffdaf97fb2af850162a7bb19dd7269b33"
dependencies = [
"async-std",
"native-tls",
"thiserror 1.0.69",
"url",
]
[[package]]
name = "async-process"
version = "2.3.0"
@ -336,20 +349,20 @@ dependencies = [
[[package]]
name = "async-session"
version = "3.0.0"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07da4ce523b4e2ebaaf330746761df23a465b951a83d84bbce4233dabedae630"
checksum = "345022a2eed092cd105cc1b26fd61c341e100bd5fcbbd792df4baf31c2cc631f"
dependencies = [
"anyhow",
"async-lock 2.8.0",
"async-std",
"async-trait",
"base64 0.13.1",
"base64 0.12.3",
"bincode",
"blake3",
"chrono",
"hmac 0.11.0",
"log",
"rand 0.8.5",
"hmac 0.8.1",
"kv-log-macro",
"rand 0.7.3",
"serde",
"serde_json",
"sha2",
@ -473,6 +486,12 @@ version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270"
[[package]]
name = "base64"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
[[package]]
name = "base64"
version = "0.13.1"
@ -561,6 +580,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
[[package]]
name = "bytes"
version = "1.10.0"
@ -602,8 +627,10 @@ checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]
@ -658,6 +685,17 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "config"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b076e143e1d9538dde65da30f8481c2a6c44040edb8e02b9bf1351edb92ce3"
dependencies = [
"lazy_static",
"nom",
"serde",
]
[[package]]
name = "const_fn"
version = "0.4.11"
@ -727,6 +765,15 @@ dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
@ -753,16 +800,6 @@ dependencies = [
"subtle",
]
[[package]]
name = "crypto-mac"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e"
dependencies = [
"generic-array",
"subtle",
]
[[package]]
name = "ctr"
version = "0.6.0"
@ -807,6 +844,33 @@ dependencies = [
"syn 2.0.98",
]
[[package]]
name = "dashmap"
version = "5.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
dependencies = [
"cfg-if 1.0.0",
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]]
name = "deadpool"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d126179d86aee4556e54f5f3c6bf6d9884e7cc52cef82f77ee6f90a7747616d"
dependencies = [
"async-trait",
"config",
"crossbeam-queue",
"num_cpus",
"serde",
"tokio 1.43.0",
]
[[package]]
name = "derive_arbitrary"
version = "1.4.1"
@ -1219,18 +1283,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2"
dependencies = [
"atomic-waker",
"bytes",
"bytes 1.10.0",
"fnv",
"futures-core",
"futures-sink",
"http",
"indexmap",
"slab",
"tokio",
"tokio 1.43.0",
"tokio-util",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hashbrown"
version = "0.15.2"
@ -1259,6 +1329,16 @@ dependencies = [
"hmac 0.10.1",
]
[[package]]
name = "hmac"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840"
dependencies = [
"crypto-mac 0.8.0",
"digest",
]
[[package]]
name = "hmac"
version = "0.10.1"
@ -1269,23 +1349,13 @@ dependencies = [
"digest",
]
[[package]]
name = "hmac"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
dependencies = [
"crypto-mac 0.11.0",
"digest",
]
[[package]]
name = "http"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
dependencies = [
"bytes",
"bytes 1.10.0",
"fnv",
"itoa",
]
@ -1296,7 +1366,7 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"bytes 1.10.0",
"http",
]
@ -1306,7 +1376,7 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
dependencies = [
"bytes",
"bytes 1.10.0",
"futures-util",
"http",
"http-body",
@ -1319,10 +1389,17 @@ version = "6.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1947510dc91e2bf586ea5ffb412caad7673264e14bb39fb9078da114a94ce1a5"
dependencies = [
"async-h1",
"async-native-tls",
"async-std",
"async-trait",
"cfg-if 1.0.0",
"dashmap",
"deadpool",
"futures",
"http-types",
"log",
"tokio 0.2.25",
]
[[package]]
@ -1365,7 +1442,7 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
dependencies = [
"bytes",
"bytes 1.10.0",
"futures-channel",
"futures-util",
"h2",
@ -1375,7 +1452,7 @@ dependencies = [
"itoa",
"pin-project-lite 0.2.16",
"smallvec",
"tokio",
"tokio 1.43.0",
"want",
]
@ -1391,7 +1468,7 @@ dependencies = [
"hyper-util",
"rustls",
"rustls-pki-types",
"tokio",
"tokio 1.43.0",
"tokio-rustls",
"tower-service",
]
@ -1402,12 +1479,12 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"bytes 1.10.0",
"http-body-util",
"hyper",
"hyper-util",
"native-tls",
"tokio",
"tokio 1.43.0",
"tokio-native-tls",
"tower-service",
]
@ -1418,7 +1495,7 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
dependencies = [
"bytes",
"bytes 1.10.0",
"futures-channel",
"futures-util",
"http",
@ -1426,7 +1503,7 @@ dependencies = [
"hyper",
"pin-project-lite 0.2.16",
"socket2 0.5.8",
"tokio",
"tokio 1.43.0",
"tower-service",
"tracing",
]
@ -1612,7 +1689,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
dependencies = [
"equivalent",
"hashbrown",
"hashbrown 0.15.2",
]
[[package]]
@ -1684,6 +1761,19 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "lexical-core"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
dependencies = [
"arrayvec",
"bitflags 1.3.2",
"cfg-if 1.0.0",
"ryu",
"static_assertions",
]
[[package]]
name = "libc"
version = "0.2.170"
@ -1708,6 +1798,16 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "lockfree-object-pool"
version = "0.1.6"
@ -1782,6 +1882,17 @@ dependencies = [
"tempfile",
]
[[package]]
name = "nom"
version = "5.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b"
dependencies = [
"lexical-core",
"memchr",
"version_check",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -1791,6 +1902,16 @@ dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi 0.3.9",
"libc",
]
[[package]]
name = "object"
version = "0.32.2"
@ -1868,6 +1989,19 @@ version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "parking_lot_core"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if 1.0.0",
"libc",
"redox_syscall",
"smallvec",
"windows-targets 0.52.6",
]
[[package]]
name = "percent-encoding"
version = "2.3.1"
@ -2097,6 +2231,15 @@ dependencies = [
"rand_core 0.5.1",
]
[[package]]
name = "redox_syscall"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f"
dependencies = [
"bitflags 2.8.0",
]
[[package]]
name = "regex"
version = "1.11.1"
@ -2133,7 +2276,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da"
dependencies = [
"base64 0.22.1",
"bytes",
"bytes 1.10.0",
"encoding_rs",
"futures-core",
"futures-util",
@ -2159,7 +2302,7 @@ dependencies = [
"serde_urlencoded",
"sync_wrapper",
"system-configuration",
"tokio",
"tokio 1.43.0",
"tokio-native-tls",
"tokio-util",
"tower",
@ -2187,14 +2330,10 @@ dependencies = [
]
[[package]]
name = "routefinder"
version = "0.4.0"
name = "route-recognizer"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44ef95cc607e41a7021da5cfb0f357ee0805113af2e9e6c617857c260940db4"
dependencies = [
"smartcow",
"smartstring",
]
checksum = "56770675ebc04927ded3e60633437841581c285dc6236109ea25fbf3beb7b59e"
[[package]]
name = "rustc-demangle"
@ -2298,6 +2437,12 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "2.11.1"
@ -2492,24 +2637,6 @@ version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
[[package]]
name = "smartcow"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05e3ed3ccf93c7425507e5e2261a3fc90d14267d491f360b9b679ae0a4ce693e"
dependencies = [
"smartstring",
]
[[package]]
name = "smartstring"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e714dff2b33f2321fdcd475b71cec79781a692d846f37f415fb395a1d2bcd48e"
dependencies = [
"static_assertions",
]
[[package]]
name = "socket2"
version = "0.4.10"
@ -2534,22 +2661,23 @@ dependencies = [
name = "sscdc"
version = "0.1.0"
dependencies = [
"async-h1",
"async-std",
"camino",
"color-eyre",
"env_logger",
"figment",
"futures",
"http-body-util",
"hyper",
"hyper-util",
"http-client",
"log",
"once_cell",
"regex",
"reqwest",
"serde",
"serde_json",
"serde_regex",
"tide",
"tokio",
"tokio 1.43.0",
"toml",
"validator",
"zip",
@ -2844,9 +2972,9 @@ dependencies = [
[[package]]
name = "tide"
version = "0.17.0-beta.1"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5a885fbeb66af9d607a731ce167e3fdfe65e49a68f37f4bbd8618b5efc6ad51"
checksum = "c459573f0dd2cc734b539047f57489ea875af8ee950860ded20cf93a79a1dee0"
dependencies = [
"async-h1",
"async-session",
@ -2860,7 +2988,7 @@ dependencies = [
"kv-log-macro",
"log",
"pin-project-lite 0.2.16",
"routefinder",
"route-recognizer",
"serde",
"serde_json",
]
@ -2913,6 +3041,17 @@ dependencies = [
"zerovec",
]
[[package]]
name = "tokio"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092"
dependencies = [
"bytes 0.5.6",
"pin-project-lite 0.1.12",
"slab",
]
[[package]]
name = "tokio"
version = "1.43.0"
@ -2920,7 +3059,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
dependencies = [
"backtrace",
"bytes",
"bytes 1.10.0",
"libc",
"mio",
"pin-project-lite 0.2.16",
@ -2948,7 +3087,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
"tokio 1.43.0",
]
[[package]]
@ -2958,7 +3097,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37"
dependencies = [
"rustls",
"tokio",
"tokio 1.43.0",
]
[[package]]
@ -2967,11 +3106,11 @@ version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
dependencies = [
"bytes",
"bytes 1.10.0",
"futures-core",
"futures-sink",
"pin-project-lite 0.2.16",
"tokio",
"tokio 1.43.0",
]
[[package]]
@ -3018,7 +3157,7 @@ dependencies = [
"futures-util",
"pin-project-lite 0.2.16",
"sync_wrapper",
"tokio",
"tokio 1.43.0",
"tower-layer",
"tower-service",
]

View file

@ -17,9 +17,10 @@ log = "0.4.26"
env_logger = "0.11.6"
serde_json = "1.0.139"
camino = "1.1.9"
tide = "0.17.0-beta.1"
tide = { version = "0.16.0"}
zip = { version = "2.2.3", default-features = false, features = ["deflate"] }
futures = "0.3.31"
hyper = { version = "1.6.0", features = ["client", "http1"] }
hyper-util = { version = "0.1.10", features = ["client", "tokio"] }
http-body-util = "0.1.2"
async-std = { version = "1.13.0", features = ["tokio1"] }
async-h1 = "2.3.4"
http-client = { version = "6.5.3", features = ["async-h1", "tokio"] }
once_cell = "1.20.3"

View file

@ -1,7 +0,0 @@
port = 8000
sites_directory = "./sites"
sockets_directory = "./sockets"
[scopes.moritzruth_de]
domain_pattern = "moritzruth\\.de"
secret = "Q9hqq3hq^^MY2^JnD23isbqFm3oXAqiHArtp`F@dNgJ4fYs`LqEzXoK%&5W%Yi9#cjdY3DA%UYc%kxCMqAwJ%K^LxsWCFH~nvyzp"

1
rustfmt.toml Normal file
View file

@ -0,0 +1 @@
max_width = 160

View file

@ -1,24 +1,73 @@
use crate::config::Config;
use crate::sites::SitesWorker;
use camino::Utf8Path;
use color_eyre::Result;
use color_eyre::eyre::{WrapErr, eyre};
use log::error;
use serde::Deserialize;
use std::fs;
use std::os::unix::net::UnixListener;
use tide::Request;
use std::str::pattern::Pattern;
use std::sync::Arc;
use tide::http::headers::HeaderName;
use tide::{Request, StatusCode};
use tokio::fs;
use tokio::task::JoinHandle;
#[derive(Clone)]
struct State {
sites_worker: Arc<SitesWorker>,
config: &'static Config,
}
#[derive(Deserialize)]
struct NotifyBody {
pub site_archive_url: String,
pub id: String,
pub download_url: String,
}
pub async fn start_api_server(socket_path: &Utf8Path) -> Result<()> {
fs::remove_file(&socket_path)?;
let listener = UnixListener::bind(&socket_path)?;
let mut server = tide::new();
server
.at("/notify")
.post(|req: Request<()>| async { Ok("hey") });
pub async fn start_api_server(
api_socket_path: &Utf8Path,
sites_worker: Arc<SitesWorker>,
config: &'static Config,
) -> color_eyre::Result<JoinHandle<color_eyre::Result<!>>> {
let mut server = tide::with_state(State { sites_worker, config });
server.bind(listener).await?;
Ok(())
server.at("/notify").post(handle_notify);
server.at("/version").get(|_| async { Ok(env!("CARGO_PKG_VERSION")) });
let path = api_socket_path.to_path_buf().into_std_path_buf();
fs::remove_file(&path).await?;
Ok(tokio::spawn(async {
server.listen(path).await.wrap_err("IO error in API server")?;
Err(eyre!("The API unexpectedly stopped without error."))
}))
}
async fn handle_notify(mut request: Request<State>) -> tide::Result {
let provided_secret = request
.header("Authorization")
.and_then(|h| h.get(0))
.and_then(|h| h.to_string().strip_prefix(&"Bearer ").map(|s| s.to_owned()))
.ok_or(tide::Error::from_str(StatusCode::Unauthorized, "No secret was provided."))?;
let domain = request.host().unwrap().to_string();
let (_, scope) = request
.state()
.config
.scopes
.iter()
.find(|(_, s)| s.secret == provided_secret)
.ok_or(tide::Error::from_str(StatusCode::Unauthorized, "The provided secret is invalid."))?;
if scope.domain_pattern.is_match(&domain) {
let body = request.body_json::<NotifyBody>().await?;
let sites_worker = Arc::clone(&request.state().sites_worker);
sites_worker.set_current_version(domain, body.id, body.download_url).await.unwrap();
Ok(StatusCode::NoContent.into())
} else {
Err(tide::Error::from_str(StatusCode::Unauthorized, "The provided secret is not valid for this domain."))
}
}

View file

@ -1,102 +1,94 @@
use async_std::os::unix::net::UnixStream;
use camino::{Utf8Path, Utf8PathBuf};
use color_eyre::Result;
use color_eyre::eyre::{OptionExt, eyre};
use http_body_util::BodyExt;
use hyper::{Request, StatusCode};
use hyper_util::rt::TokioIo;
use http_client::http_types::{Method, StatusCode, Url};
use http_client::{Request, Response};
use log::debug;
use serde_json::json;
use std::process::Stdio;
use std::str::FromStr;
use tokio::io::AsyncWriteExt;
use tokio::net::UnixStream;
use tokio::process::Command;
use tokio::time;
#[derive(Clone)]
pub struct CaddyController {
caddy_admin_socket_path: Utf8PathBuf,
api_socket_path: Utf8PathBuf,
admin_api_socket_path: Utf8PathBuf,
}
impl CaddyController {
pub async fn upsert_site_configuration(
&self,
domain: &String,
content_path: &Utf8Path,
) -> Result<()> {
pub async fn upsert_site_configuration(&mut self, domain: &String, content_path: &Utf8Path) -> Result<()> {
let configuration_object = json!({
"@id": domain,
"match": [{ "host": [domain] }],
"handle": [
{
"group": "1",
"match": [{ "path": ["/sscdc/*"] }],
"handle": [
{
"handler": "rewrite",
"strip_path_prefix": "/_sscdc"
},
{
"handler": "reverse_proxy",
"upstreams": [{ "dial": format!("unix/{}", &self.api_socket_path) }],
}
]
},
{
"group": "1",
"handle": [
{ "handler": "vars", "root": content_path.to_string() },
{
"handler": "file_server",
"precompressed": {
"br": {},
"gzip": {}
"handle": [{
"handler": "subroute",
"routes": [
{
"group": "1",
"match": [{ "path": ["/_sscdc/*"] }],
"handle": [
{
"handler": "rewrite",
"strip_path_prefix": "/_sscdc"
},
"precompressed_order": ["br", "gzip"]
}
]
}
],
{
"handler": "reverse_proxy",
"upstreams": [{ "dial": format!("unix/{}", &self.api_socket_path) }],
}
]
},
{
"group": "1",
"handle": [
{ "handler": "vars", "root": content_path.to_string() },
{
"handler": "file_server",
"precompressed": {
"br": {},
"gzip": {}
},
"precompressed_order": ["br", "gzip"]
}
]
}
]
}],
"terminal": true
});
let stream = UnixStream::connect(&self.caddy_admin_socket_path).await?;
let (mut sender, connection) =
hyper::client::conn::http1::handshake(TokioIo::new(stream)).await?;
tokio::task::spawn(async move { connection.await.unwrap() });
let response = sender
.send_request(
Request::builder()
.uri(&format!("/id/{}", domain))
.body(configuration_object.to_string().boxed())?,
)
.await?;
let mut request = Request::post(Url::from_str(&format!("http://localhost/id/{}", domain))?);
request.insert_header("Content-Type", "application/json");
request.set_body(configuration_object.to_string());
let mut response = request_uds(&self.admin_api_socket_path, request.clone()).await.unwrap();
match response.status() {
StatusCode::OK => {}
StatusCode::NOT_FOUND => {
StatusCode::Ok => {
debug!("Caddy configuration for {domain} was updated.")
}
StatusCode::NotFound => {
// The site does not yet exist.
let response = sender
.send_request(
Request::builder()
.uri("/config/apps/http/servers/srv0/routes")
.body(configuration_object.to_string().boxed())?,
)
.await?;
*request.url_mut() = Url::from_str("http://localhost/config/apps/http/servers/srv0/routes")?;
let mut response = request_uds(&self.admin_api_socket_path, request).await.unwrap();
let status = response.status();
if !status.is_success() {
return Err(eyre!(
"The configuration update request to Caddy failed with status code {}",
status
"The configuration update request to Caddy failed with status code {}:\n{}",
status,
response.body_string().await.unwrap()
));
}
debug!("Caddy configuration for {domain} was created.")
}
c => {
return Err(eyre!(
"The configuration update request to Caddy failed with status code {}",
c
"The configuration update request to Caddy failed with status code {}:\n{}",
c,
response.body_string().await.unwrap()
));
}
}
@ -105,14 +97,13 @@ impl CaddyController {
}
}
fn get_initial_caddy_configuration_object(
admin_api_socket_path: &Utf8Path,
api_socket_path: &Utf8Path,
) -> serde_json::Value {
fn get_initial_caddy_configuration_object(admin_api_socket_path: &Utf8Path, api_socket_path: &Utf8Path) -> serde_json::Value {
json!({
"admin": {
"listen": format!("unix/{}", admin_api_socket_path),
"config": { "persist": false }
"enforce_origin": false,
"origins": ["localhost"],
"config": { "persist": false },
},
"logging": {
"sink": {
@ -134,7 +125,7 @@ fn get_initial_caddy_configuration_object(
"srv0": {
"listen": [":80"],
"routes": [{
"match": [{ "path": ["/sscdc/*"] }],
"match": [{ "path": ["/_sscdc/*"] }],
"handle": [
{
"handler": "rewrite",
@ -154,24 +145,23 @@ fn get_initial_caddy_configuration_object(
})
}
pub async fn start_caddy(
api_socket_path: &Utf8Path,
sockets_directory_path: &Utf8Path,
) -> Result<CaddyController> {
async fn request_uds(path: &Utf8Path, request: Request) -> http_client::http_types::Result<Response> {
let stream = UnixStream::connect(path.as_std_path()).await?;
async_h1::connect(stream, request).await
}
pub async fn start_caddy(api_socket_path: &Utf8Path, sockets_directory_path: &Utf8Path) -> Result<CaddyController> {
let caddy_admin_socket_path = sockets_directory_path.join("caddy-admin-api.sock");
let process = Command::new("caddy")
.args(&["run", "--config", "-"])
.stdin(Stdio::piped())
.spawn()?;
let initial_configuration_object =
get_initial_caddy_configuration_object(&caddy_admin_socket_path, api_socket_path);
// Stop not properly cleaned up proxy.
let _ = request_uds(&caddy_admin_socket_path, Request::new(Method::Post, "http://localhost/stop")).await;
// Spawn a new proxy.
let process = Command::new("caddy").args(&["run", "--config", "-"]).stdin(Stdio::piped()).spawn()?;
let initial_configuration_object = get_initial_caddy_configuration_object(&caddy_admin_socket_path, api_socket_path);
let initial_configuration_string = initial_configuration_object.to_string();
log::debug!(
"Initial caddy configuration: {}",
initial_configuration_string
);
debug!("Initial caddy configuration: {}", initial_configuration_string);
process
.stdin
@ -179,8 +169,10 @@ pub async fn start_caddy(
.write_all(initial_configuration_string.as_ref())
.await?;
time::sleep(time::Duration::from_secs(1)).await;
Ok(CaddyController {
caddy_admin_socket_path,
api_socket_path: api_socket_path.to_path_buf(),
admin_api_socket_path: caddy_admin_socket_path,
})
}

View file

@ -4,6 +4,7 @@ use figment::Figment;
use serde::Deserialize;
use std::collections::HashMap;
use color_eyre::eyre::WrapErr;
use once_cell::sync::Lazy;
use regex::Regex;
use validator::Validate;
@ -16,11 +17,13 @@ pub struct Config {
pub scopes: HashMap<String, ConfigScope>
}
static SECRET_PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-zA-Z0-9]*$").unwrap());
#[derive(Deserialize, Debug, Validate)]
pub struct ConfigScope {
#[serde(with = "serde_regex")]
pub domain_pattern: Regex,
#[validate(length(equal = 100))]
#[validate(length(equal = 100), regex(path = *SECRET_PATTERN))]
pub secret: String
}
@ -30,5 +33,6 @@ pub fn load_config() -> Result<Config> {
.extract()
.wrap_err("Failed to load the configuration.")?;
config.validate().wrap_err("Failed to validate the configuration.")?;
Ok(config)
}

View file

@ -1,4 +1,6 @@
#![feature(never_type)]
#![feature(duration_constructors)]
#![feature(pattern)]
use crate::api::start_api_server;
use crate::caddy::start_caddy;
@ -9,6 +11,7 @@ use color_eyre::Result;
use color_eyre::eyre::WrapErr;
use log::LevelFilter;
use std::fs;
use std::sync::Arc;
mod api;
mod caddy;
@ -18,30 +21,25 @@ mod sites;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
color_eyre::install()?;
env_logger::Builder::new()
.filter_module("sscdc", LevelFilter::Info)
.parse_default_env()
.init();
env_logger::Builder::new().filter_module("sscdc", LevelFilter::Info).parse_default_env().init();
log::info!("Loading configuration…");
let config = load_config()?;
let sockets_directory_path = Utf8Path::new(&config.sockets_directory);
fs::create_dir_all(&sockets_directory_path)
.wrap_err("Failed to access or create the sockets directory.")?;
fs::create_dir_all(&sockets_directory_path).wrap_err("Failed to access or create the sockets directory.")?;
let sockets_directory_path = sockets_directory_path.canonicalize_utf8().unwrap();
log::info!("Starting internal API server…");
let api_socket_path = sockets_directory_path.join("api.sock");
start_api_server(&api_socket_path).await?;
log::debug!("The internal API server is listening at {api_socket_path}");
log::info!("Starting the reverse proxy…");
let caddy_controller = start_caddy(&api_socket_path, &sockets_directory_path).await?;
let sites_worker = start_sites_worker(config.sites_directory.to_string().into(), caddy_controller).await?;
let (sites_worker, sites_worker_join_handle) = start_sites_worker(config.sites_directory.to_string().into(), caddy_controller).await?;
log::info!("Starting internal API server…");
start_api_server(&api_socket_path, Arc::new(sites_worker), Box::leak(Box::new(config))).await?;
log::debug!("The internal API server is listening at {api_socket_path}");
log::info!("Startup complete.");
sites_worker.join_handle.await??;
sites_worker_join_handle.await??;
}

View file

@ -1,16 +1,19 @@
use crate::caddy::CaddyController;
use camino::{Utf8Path, Utf8PathBuf};
use color_eyre::Result;
use color_eyre::eyre::WrapErr;
use color_eyre::eyre::{eyre, WrapErr};
use futures::StreamExt;
use log::{debug, info};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, RwLockReadGuard, RwLockWriteGuard};
use std::time::Duration;
use tide::http::bail;
use tokio::fs;
use tokio::io::{AsyncWriteExt, BufWriter};
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
use tokio::sync::{Mutex, RwLock};
use tokio::sync::{RwLock};
use tokio::task::JoinHandle;
use futures::StreamExt;
#[derive(Serialize, Deserialize)]
pub struct SiteState {
@ -25,24 +28,20 @@ impl SiteState {
}
async fn write_to_site_directory(&self, site_directory_path: &Utf8Path) -> Result<()> {
fs::write(
site_directory_path.join("site.toml"),
toml::to_string(&self).unwrap(),
)
.await?;
fs::create_dir_all(site_directory_path).await?;
let mut file = fs::File::create(site_directory_path.join("site.toml")).await?;
file.write_all(toml::to_string(&self).unwrap().as_bytes()).await?;
Ok(())
}
fn is_outdated(&self) -> bool {
self.get_current_version_if_outdated().is_some()
}
fn get_current_version_if_outdated(&self) -> Option<&SiteStateVersion> {
if let Some(current_version) = &self.current_version {
if let Some(active_version) = &self.active_version {
if current_version.id != active_version.id {
return Some(&active_version)
return Some(current_version);
}
} else {
return Some(current_version)
}
}
@ -68,25 +67,21 @@ pub struct SitesWorker {
sites: Arc<Sites>,
sites_directory_path: Utf8PathBuf,
download_tasks_sender: UnboundedSender<String>,
pub join_handle: JoinHandle<Result<!>>,
}
impl SitesWorker {
pub async fn set_current_version(
&self,
domain: String,
id: String,
download_url: String,
) -> Result<()> {
pub async fn set_current_version(&self, domain: String, id: String, download_url: String) -> Result<()> {
let mut sites = self.sites.write().await;
let site = sites.entry(domain.clone()).or_insert_with(|| Arc::new(Site {
domain: domain.clone(),
path: self.sites_directory_path.join(&domain),
state: RwLock::new(SiteState {
current_version: None,
active_version: None,
}),
}));
let site = sites.entry(domain.clone()).or_insert_with(|| {
Arc::new(Site {
domain: domain.clone(),
path: self.sites_directory_path.join(&domain),
state: RwLock::new(SiteState {
current_version: None,
active_version: None,
}),
})
});
let mut state = site.state.write().await;
state.current_version = Some(SiteStateVersion { id, download_url });
@ -131,35 +126,58 @@ pub async fn load_sites(sites_directory_path: &Utf8Path) -> Result<Sites> {
Ok(RwLock::new(sites))
}
async fn handle_download(sites: &Sites, caddy_controller: &CaddyController, domain: String) -> Result<()> {
async fn handle_download(reqwest_client: &mut reqwest::Client, sites: &Sites, caddy_controller: &mut CaddyController, domain: String) -> Result<()> {
let site = {
let sites = sites.read().await;
match sites.get(&domain) {
None => return Ok(()),
Some(a) => Arc::clone(a)
None => {
debug!("Skipping download for {domain} because it is no longer managed.");
return Ok(());
}
Some(a) => Arc::clone(a),
}
};
let mut state = site.state.write().await;
let current_version = match state.get_current_version_if_outdated() {
None => return Ok(()),
Some(v) => v
None => {
debug!("Skipping download for {domain} because it is not outdated.");
return Ok(());
}
Some(v) => v,
};
info!("Starting download for {domain} ({}).", current_version.id);
// Download
let mut archive_file = fs::File::create(site.path.join(format!("{}.zip", current_version.id))).await?;
let archive_file_path = site.path.join(format!("{}.zip", current_version.id));
let mut archive_file = fs::OpenOptions::new()
.truncate(true)
.create(true)
.write(true)
.read(true)
.open(&archive_file_path).await?;
let mut file_writer = BufWriter::new(&mut archive_file);
let mut response = reqwest::get(&current_version.download_url).await?;
let mut response = reqwest_client.get(&current_version.download_url).send().await?;
let status = response.status();
if !status.is_success() {
return Err(eyre!("Download request failed with status code {status}"))
}
while let Some(chunk) = response.chunk().await? {
file_writer.write_all(&chunk).await?;
}
file_writer.flush().await?;
let extraction_directory_path = site.path.join(&current_version.id);
let _ = fs::remove_dir_all(&extraction_directory_path).await;
fs::create_dir_all(&extraction_directory_path).await?;
debug!("Finished download for {domain} ({}), now unpacking…", current_version.id);
// Unpack to temp dir
tokio::task::spawn_blocking({
let archive_file = archive_file.into_std().await;
@ -171,63 +189,81 @@ async fn handle_download(sites: &Sites, caddy_controller: &CaddyController, doma
Ok(())
}
}).await??;
})
.await??;
// Update and write state
state.active_version = Some(current_version.clone());
let current_version = current_version.clone();
let old_active_version = state.active_version.replace(current_version);
state.write_to_site_directory(&site.path).await?;
// Update Caddy configuration
info!("Download for {domain} successful, now updating proxy configuration…");
caddy_controller.upsert_site_configuration(&domain, &extraction_directory_path).await?;
// Cleanup
fs::remove_file(archive_file_path).await?;
if let Some(old_active_version) = old_active_version {
fs::remove_dir_all(site.path.join(old_active_version.id)).await?;
}
info!("Cleanup finished for {domain}");
Ok(())
}
async fn sites_worker(
mut download_tasks_receiver: UnboundedReceiver<String>,
sites: Arc<Sites>,
caddy_controller: CaddyController,
) -> Result<!> {
async fn sites_worker(mut download_tasks_receiver: UnboundedReceiver<String>, sites: Arc<Sites>, mut caddy_controller: CaddyController) -> Result<!> {
let mut client = reqwest::Client::builder()
.read_timeout(Duration::from_secs(60))
.timeout(Duration::from_hours(1))
.build()?;
loop {
let domain = download_tasks_receiver.recv().await.unwrap();
handle_download(&sites, &caddy_controller, domain).await?;
handle_download(&mut client, &sites, &mut caddy_controller, domain).await?;
}
}
pub async fn start_sites_worker(
sites_directory_path: Utf8PathBuf,
caddy_controller: CaddyController,
) -> Result<SitesWorker> {
pub async fn start_sites_worker(sites_directory_path: Utf8PathBuf, mut caddy_controller: CaddyController) -> Result<(SitesWorker, JoinHandle<Result<!>>)> {
let (download_tasks_sender, download_tasks_receiver) = tokio::sync::mpsc::unbounded_channel();
info!("Discovering managed sites…");
let sites = Arc::new(load_sites(&sites_directory_path).await?);
let mut total_count = 0;
let mut outdated_count = 0;
for (_, site) in sites.read().await.iter() {
let state = site.state.read().await;
if let Some(current_version) = &state.current_version {
let mut is_outdated = true;
total_count += 1;
if let Some(active_version) = &state.active_version {
if current_version.id != active_version.id {
download_tasks_sender.send(site.domain.clone()).unwrap()
}
is_outdated = current_version.id != active_version.id;
caddy_controller.upsert_site_configuration(&site.domain, &site.path.join(&active_version.id)).await?;
caddy_controller
.upsert_site_configuration(&site.domain, &site.path.join(&active_version.id))
.await?;
}
}
if state.is_outdated() {
download_tasks_sender.send(site.domain.clone()).unwrap()
if is_outdated {
outdated_count += 1;
download_tasks_sender.send(site.domain.clone()).unwrap()
}
}
}
let join_handle = tokio::spawn(sites_worker(
download_tasks_receiver,
Arc::clone(&sites),
caddy_controller,
));
info!("Discovered {total_count} site(s), {outdated_count} outdated.");
Ok(SitesWorker {
sites,
sites_directory_path,
download_tasks_sender,
let join_handle = tokio::spawn(sites_worker(download_tasks_receiver, Arc::clone(&sites), caddy_controller));
Ok((
SitesWorker {
sites,
sites_directory_path,
download_tasks_sender,
},
join_handle,
})
))
}