use std::error::Error; use std::{fs, io}; use std::net::{IpAddr, Ipv6Addr}; use std::os::unix; use std::os::unix::fs::MetadataExt; 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}; macro_rules! info { ($msg:tt) => { match connected_to_journal() { true => log::info!("[INFO] {}", $msg), false => println!("[INFO] {}", $msg), } }; } macro_rules! warn { ($msg:tt) => { match connected_to_journal() { true => log::warn!("[WARN] {}", $msg), false => println!("[WARN] {}", $msg), } }; } macro_rules! error { ($msg:tt) => { match connected_to_journal() { true => log::error!("[ERROR] {}", $msg), false => eprintln!("[ERROR] {}", $msg), } }; } #[derive(Clone, Serialize, Deserialize)] struct DomainLinkConfig { domains: Vec, target: String, } #[derive(Clone, Serialize, Deserialize, Default)] struct UserConfig { domain_configs: Vec } #[derive(Clone, Serialize, Deserialize)] struct SystemConfig { ports: Vec, addresses: Vec, } impl Default for SystemConfig { fn default() -> Self { Self { ports: vec![8000], addresses: vec![IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0))], } } } #[derive(Clone)] struct Config { user: UserConfig, system: SystemConfig, } impl Config { 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 } Err(e) => { error!(e); return Err(Box::new(e)); } }; root_contents.for_each(|directory| { let path = directory.expect("Unexpected Error while Unwrapping the listed Dir Entry").path(); if path.is_dir() { match path.display().to_string().as_str() { "/root" => { 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" => { match fs::read_dir(path) { Ok(home_contents) => { 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, 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); } }) }, Err(e) => { error!(e); } } }, _ => {} } } }); let etc_path = format!("/etc/{}", env!("CARGO_PKG_NAME")); let usr_path = format!("/usr/local/share/{}", env!("CARGO_PKG_NAME")); let system_config_paths = vec![ Path::new(&etc_path), Path::new(&usr_path), ]; let mut system_path= system_config_paths[1]; for path in system_config_paths { let cfg_path = path.join("config.toml"); if cfg_path.exists() { system_path = path; break; } }; let path = format!("{}/config.toml", system_path.display()); match confy::load_path(path.clone()) { Ok(data) => { let msg = format!("Using {}", path); directories.push(system_path.to_path_buf()); info!(msg); Ok((Config { user: user_config, system: data, }, directories)) }, Err(e) => { error!(e); Err(Box::new(e)) } } } 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) => { if data.domain_configs.is_empty() { match Self::fix_path_ownership(path, vec![".config", env!("CARGO_PKG_NAME"), "domains.toml"]) { Ok(_) => (), Err(e) => { error!(e); } }; } 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(), None) } else { error!(e); (UserConfig::default(), None) } }, _ => { error!(e); (UserConfig::default(), None) } } } } } fn fix_path_ownership(root: PathBuf, paths: Vec<&str>) -> io::Result<()> { let root_metadata = fs::metadata(&root)?; let uid = root_metadata.uid(); let gid = root_metadata.gid(); println!("uid: {uid}, gid: {gid}"); match paths.len() { 1 => { let new_root = root.join(paths[0]); println!("{}", &new_root.display()); unix::fs::chown(new_root, Some(uid), Some(gid)) }, _ => { let new_root = root.join(paths[0]); println!("{}", &new_root.display()); let ret = unix::fs::chown(&new_root, Some(uid), Some(gid)); let mut new_paths = paths.clone(); new_paths.remove(0); match Self::fix_path_ownership(new_root, new_paths) { Ok(_) => ret, Err(e) => { error!(e); Err(e) } } } } } } #[actix_web::main] 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, 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::clone(&app_data_clone)) .service(status) .service(do_redirect) }); 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}") } else { format!("Listening on {address}:{port}") }; info!(msg); server = server.bind((*address, *port))? } } 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 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"); 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(); } } let fail_msg = format!("No Redirect for {host} found"); return HttpResponse::NotFound().body(fail_msg) } HttpResponse::NotFound().body("Host not specified") } #[get("/status")] 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["; for (idx, domain) in redirect.domains.iter().enumerate() { body_msg += domain; if idx != (redirect.domains.len() - 1) { body_msg += ", "; } } body_msg += format!("] => '{}'", redirect.target).as_str(); } HttpResponse::Ok().body(body_msg) }