diff --git a/Cargo.lock b/Cargo.lock index 71081a7..82390f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/Cargo.toml b/Cargo.toml index bc3372f..01bd145 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" \ No newline at end of file +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" \ No newline at end of file diff --git a/run/config.toml b/run/config.toml deleted file mode 100644 index 1e4f25c..0000000 --- a/run/config.toml +++ /dev/null @@ -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" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..1cbd329 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +max_width = 160 \ No newline at end of file diff --git a/src/api.rs b/src/api.rs index 2dd0215..c0c375e 100644 --- a/src/api.rs +++ b/src/api.rs @@ -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, + 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, + config: &'static Config, +) -> 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) -> 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::().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.")) + } } diff --git a/src/caddy.rs b/src/caddy.rs index 7db3402..f027451 100644 --- a/src/caddy.rs +++ b/src/caddy.rs @@ -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 { +async fn request_uds(path: &Utf8Path, request: Request) -> http_client::http_types::Result { + 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 { 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, }) } diff --git a/src/config.rs b/src/config.rs index 2e2aadc..57159cf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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 } +static SECRET_PATTERN: Lazy = 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 { .extract() .wrap_err("Failed to load the configuration.")?; + config.validate().wrap_err("Failed to validate the configuration.")?; Ok(config) } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 91b1bae..6fd52e0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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??; } diff --git a/src/sites.rs b/src/sites.rs index 6fa32c3..2363b29 100644 --- a/src/sites.rs +++ b/src/sites.rs @@ -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_directory_path: Utf8PathBuf, download_tasks_sender: UnboundedSender, - pub join_handle: JoinHandle>, } 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,103 +126,144 @@ pub async fn load_sites(sites_directory_path: &Utf8Path) -> Result { 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(¤t_version.download_url).await?; - + let mut response = reqwest_client.get(¤t_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(¤t_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; let extraction_directory_path = extraction_directory_path.clone(); - + move || -> Result<()> { let mut archive = zip::ZipArchive::new(archive_file)?; archive.extract(&extraction_directory_path)?; 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 - caddy_controller.upsert_site_configuration(&domain, &extraction_directory_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, - sites: Arc, - caddy_controller: CaddyController, -) -> Result { +async fn sites_worker(mut download_tasks_receiver: UnboundedReceiver, sites: Arc, 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 { +pub async fn start_sites_worker(sites_directory_path: Utf8PathBuf, mut caddy_controller: CaddyController) -> Result<(SitesWorker, JoinHandle>)> { 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 { - if let Some(active_version) = &state.active_version { - if current_version.id != active_version.id { - download_tasks_sender.send(site.domain.clone()).unwrap() - } - - 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 let Some(current_version) = &state.current_version { + let mut is_outdated = true; + total_count += 1; + + if let Some(active_version) = &state.active_version { + is_outdated = current_version.id != active_version.id; + + caddy_controller + .upsert_site_configuration(&site.domain, &site.path.join(&active_version.id)) + .await?; + } + + 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, - }) + )) }