From 028067b62a5a3fe01781fe9ca67d3e0d18d94eec Mon Sep 17 00:00:00 2001 From: Neshura Date: Wed, 10 Apr 2024 22:19:50 +0200 Subject: [PATCH] Add hot reloading of redirect config files --- Cargo.lock | 146 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/main.rs | 91 +++++++++++++++++++++++--------- 3 files changed, 215 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2716d6b..c7114fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -380,6 +380,21 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + [[package]] name = "crypto-common" version = "0.1.6" @@ -450,6 +465,7 @@ dependencies = [ "actix-web", "confy", "log", + "notify", "serde", "systemd-journal-logger", ] @@ -479,6 +495,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys 0.52.0", +] + [[package]] name = "flate2" version = "1.0.28" @@ -504,6 +532,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures-core" version = "0.3.30" @@ -629,6 +666,26 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "itoa" version = "1.0.11" @@ -644,6 +701,26 @@ dependencies = [ "libc", ] +[[package]] +name = "kqueue" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -741,6 +818,25 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.5.0", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "walkdir", + "windows-sys 0.48.0", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -964,6 +1060,15 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1306,12 +1411,53 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index a4502c1..64c9205 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,5 +31,6 @@ systemd-units = { enable = false } actix-web = "4" confy = "0.6" log = "0.4" +notify = "6" systemd-journal-logger = "2" serde = { version = "1.0.197", features = ["derive"] } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 494cd6e..a73b061 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,11 @@ use std::error::Error; use std::fs; use std::net::{IpAddr, Ipv6Addr}; use std::path::{Path, PathBuf}; +use std::sync::RwLock; use actix_web::{web, App, HttpResponse, HttpServer, get, Responder, HttpRequest}; use log::{LevelFilter}; +use notify::{Event, EventKind, RecursiveMode, Watcher}; +use notify::event::{AccessKind, AccessMode}; use systemd_journal_logger::{connected_to_journal, JournalLog}; use serde::{Deserialize, Serialize}; @@ -68,10 +71,11 @@ struct Config { } impl Config { - pub fn load() -> Result> { + pub fn load() -> Result<(Self, Vec), Box> { // get list of home directories // query every home directory for a config file (just attempt a load, an empty config is perfectly fine) // merge all configs into one + let mut directories: Vec = vec![]; let mut user_config = UserConfig::default(); let root_contents = match fs::read_dir("/") { Ok(contents) => { contents } @@ -86,7 +90,10 @@ impl Config { if path.is_dir() { match path.display().to_string().as_str() { "/root" => { - let mut root_configs = Self::load_user_config_directory(path); + let (mut root_configs, root_path) = Self::load_user_config_directory(path); + if let Some(root_directory) = root_path { + directories.push(Path::new(root_directory.as_str()).to_path_buf()); + } user_config.domain_configs.append(&mut root_configs.domain_configs); }, "/home" => { @@ -95,7 +102,10 @@ impl Config { home_contents.for_each(|home_folder| { let home_folder_path = home_folder.expect("Unexpected Error while Unwrapping the listed Home Dir Entry").path(); if home_folder_path.is_dir() { - let mut user_folder_configs = Self::load_user_config_directory(home_folder_path); + let (mut user_folder_configs, user_folder_path) = Self::load_user_config_directory(home_folder_path); + if let Some(user_folder_directory) = user_folder_path { + directories.push(Path::new(user_folder_directory.as_str()).to_path_buf()); + } user_config.domain_configs.append(&mut user_folder_configs.domain_configs); } }) @@ -132,11 +142,12 @@ impl Config { match confy::load_path(path.clone()) { Ok(data) => { let msg = format!("Using {}", path); + directories.push(system_path.to_path_buf()); info!(msg); - Ok(Config { + Ok((Config { user: user_config, system: data, - }) + }, directories)) }, Err(e) => { error!(e); @@ -145,26 +156,30 @@ impl Config { } } - fn load_user_config_directory(path: PathBuf) -> UserConfig { - let config_path = format!("{}/.config/{}/domains.toml", path.display(), env!("CARGO_PKG_NAME")); - match confy::load_path(config_path) { - Ok(data) => data, + fn load_user_config_directory(path: PathBuf) -> (UserConfig, Option) { + let config_path = format!("{}/.config/{}", path.display(), env!("CARGO_PKG_NAME")); + match confy::load_path(config_path.clone() + "/domains.toml") { + Ok(data) => { + let msg = format!("Using {config_path}/domains.toml"); + info!(msg); + (data, Some(config_path)) + }, Err(e) => { match &e { confy::ConfyError::GeneralLoadError(os_error) => { if os_error.raw_os_error() == Some(13) { let msg = format!("Missing read permissions for {}, skipping", path.display().to_string().as_str()); warn!(msg); - UserConfig::default() + (UserConfig::default(), None) } else { error!(e); - UserConfig::default() + (UserConfig::default(), None) } }, _ => { error!(e); - UserConfig::default() + (UserConfig::default(), None) } } } @@ -173,24 +188,28 @@ impl Config { } #[actix_web::main] -async fn main() -> std::io::Result<()> { +async fn main() -> notify::Result<()> { JournalLog::new().expect("Systemd-Logger crate error").install().expect("Systemd-Logger crate error"); log::set_max_level(LevelFilter::Info); - let config = Config::load().expect("Error while loading or generating the config"); + let (config, directories) = Config::load().expect("Error while loading or generating the config"); let loaded_redirects_msg = format!("Loaded {} redirects from user config", config.user.domain_configs.len()); info!(loaded_redirects_msg); + let app_data = web::Data::new(RwLock::new(config)); + let app_data_clone = web::Data::clone(&app_data); + let mut server = HttpServer::new(move || { App::new() - .app_data(web::Data::new(config.user.domain_configs.clone())) + .app_data(web::Data::clone(&app_data_clone)) .service(status) - .service(handle) + .service(do_redirect) }); - - for address in config.system.addresses.iter() { - for port in config.system.ports.iter() { + + for address in app_data.read().expect("Read Lock Failed").system.addresses.iter() { + let ports = app_data.read().expect("Read Lock Failed").system.ports.clone(); + for port in ports.iter() { let msg = if address.is_ipv6() { format!("Listening on [{address}]:{port}") } @@ -201,14 +220,39 @@ async fn main() -> std::io::Result<()> { server = server.bind((*address, *port))? } } - server.run().await + + let mut watcher = notify::recommended_watcher(move |res: Result| { + match res { + Ok(event) => { + if event.kind == EventKind::Access(AccessKind::Close(AccessMode::Write)) { + let (config, _) = Config::load().expect("Error while loading or generating the config"); + let mut tmp_app_data = app_data.write().expect("Write Lock Failed"); + tmp_app_data.system = config.system; + tmp_app_data.user = config.user; + info!("Reloading Configuration"); + } + }, + Err(e) => { + let msg = format!("Error watching files: {e}"); + error!(msg); + } + } + })?; + + for directory in directories.iter() { + watcher.watch(directory, RecursiveMode::NonRecursive)?; + } + + let _ = server.run().await; + + Ok(()) } #[get("/")] -async fn handle(redirects: web::Data>, request: HttpRequest) -> impl Responder { +async fn do_redirect(data: web::Data>, request: HttpRequest) -> impl Responder { + let redirects = &data.read().expect("Read Lock Failed").user.domain_configs; if let Some(host_raw) = request.headers().get("host") { let host = host_raw.to_str().expect("host conversion to string should never fail"); - println!("{host}"); for redirect in redirects.iter() { if redirect.domains.contains(&host.to_owned()) { return HttpResponse::PermanentRedirect().insert_header(("location", redirect.target.to_string().as_str())).finish(); @@ -221,7 +265,8 @@ async fn handle(redirects: web::Data>, request: HttpReques } #[get("/status")] -async fn status(redirects: web::Data>) -> impl Responder { +async fn status(data: web::Data>) -> impl Responder { + let redirects = &data.read().expect("Read Lock Failed").user.domain_configs; let mut body_msg = format!("Redirects Loaded: {}", redirects.len()); for redirect in redirects.iter() { body_msg += "\n[";