use std::error::Error; use std::fmt::{Display, format, Formatter}; use std::fs; use std::net::{IpAddr, Ipv6Addr}; use std::path::Path; use actix_web::{web, App, HttpResponse, HttpServer, get, Responder, HttpRequest}; use log::{LevelFilter}; 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> { // 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 user_config = UserConfig::default(); for entry in fs::read_dir("/home").expect("home directory is expected") { let entry = entry.expect("home directory is expected to have at least one directory"); let path = entry.path(); if path.is_dir() { let config_path = format!("{}/.config/{}/config.toml", path.display().to_string().as_str(), env!("CARGO_PKG_NAME")); let mut path_config: UserConfig = match confy::load_path(config_path) { Ok(data) => data, 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() } else { error!(e); return Err(Box::new(e)); } }, _ => { error!(e); return Err(Box::new(e)); } } } }; user_config.domain_configs.append(&mut path_config.domain_configs) } } 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().to_string().as_str()); match confy::load_path(path.clone()) { Ok(data) => { let msg = format!("Using {}", path); info!(msg); Ok(Config { user: user_config, system: data, }) }, Err(e) => { error!(e); Err(Box::new(e)) } } } } #[actix_web::main] async fn main() -> std::io::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 loaded_redirects_msg = format!("Loaded {} redirects from user config", config.user.domain_configs.len()); info!(loaded_redirects_msg); let mut server = HttpServer::new(move || { App::new() .app_data(web::Data::new(config.user.domain_configs.clone())) .service(handle) .service(dry_handle) }); for address in config.system.addresses.iter() { for port in config.system.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))? } } server.run().await } #[get("/")] async fn handle(redirects: web::Data>, request: HttpRequest) -> impl Responder { 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(); } } let fail_msg = format!("No Redirect for {host} found"); return HttpResponse::NotFound().body(fail_msg) } HttpResponse::NotFound().body("Host not specified") } #[get("/dry")] async fn dry_handle(redirects: web::Data>, request: HttpRequest) -> impl Responder { 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()) { let body = format!("Redirecting: {} -> {}", host, redirect.target); return HttpResponse::Ok().body(body); } } let fail_msg = format!("No Redirect for {host} found"); return HttpResponse::NotFound().body(fail_msg) } HttpResponse::NotFound().body("Host not specified") }