use crate::data_store::DataStore; use colored::*; use indoc::eprintdoc; use log::{Level, LevelFilter}; use serde::{de::DeserializeOwned, Serialize}; use shared::{interface_config::ServerInfo, Interface, PeerDiff, INNERNET_PUBKEY_HEADER}; use std::{ffi::OsStr, io, path::Path, time::Duration}; use ureq::{Agent, AgentBuilder}; static LOGGER: Logger = Logger; struct Logger; const BASE_MODULES: &[&str] = &["innernet", "shared"]; fn target_is_base(target: &str) -> bool { BASE_MODULES .iter() .any(|module| module == &target || target.starts_with(&format!("{}::", module))) } impl log::Log for Logger { fn enabled(&self, metadata: &log::Metadata) -> bool { metadata.level() <= log::max_level() && (log::max_level() == LevelFilter::Trace || target_is_base(metadata.target())) } fn log(&self, record: &log::Record) { if self.enabled(record.metadata()) { let level_str = match record.level() { Level::Error => "[E]".red(), Level::Warn => "[!]".yellow(), Level::Info => "[*]".dimmed(), Level::Debug => "[D]".blue(), Level::Trace => "[T]".purple(), }; if record.level() <= LevelFilter::Debug && !target_is_base(record.target()) { println!( "{} {} {}", level_str, format!("[{}]", record.target()).dimmed(), record.args() ); } else { println!("{} {}", level_str, record.args()); } } } fn flush(&self) {} } pub fn init_logger(verbosity: u64) { let level = match verbosity { 0 => log::LevelFilter::Info, 1 => log::LevelFilter::Debug, _ => log::LevelFilter::Trace, }; log::set_max_level(level); log::set_logger(&LOGGER).unwrap(); } pub fn human_duration(duration: Duration) -> String { match duration.as_secs() { n if n < 1 => "just now".cyan().to_string(), n if n < 60 => format!("{} {} ago", n, "seconds".cyan()), n if n < 60 * 60 => { let mins = n / 60; let secs = n % 60; format!( "{} {}, {} {} ago", mins, if mins == 1 { "minute" } else { "minutes" }.cyan(), secs, if secs == 1 { "second" } else { "seconds" }.cyan(), ) }, n => { let hours = n / (60 * 60); let mins = (n / 60) % 60; format!( "{} {}, {} {} ago", hours, if hours == 1 { "hour" } else { "hours" }.cyan(), mins, if mins == 1 { "minute" } else { "minutes" }.cyan(), ) }, } } pub fn human_size(bytes: u64) -> String { const KB: u64 = 1024; const MB: u64 = 1024 * KB; const GB: u64 = 1024 * MB; const TB: u64 = 1024 * GB; match bytes { n if n < 2 * KB => format!("{} {}", n, "B".cyan()), n if n < 2 * MB => format!("{:.2} {}", n as f64 / KB as f64, "KiB".cyan()), n if n < 2 * GB => format!("{:.2} {}", n as f64 / MB as f64, "MiB".cyan()), n if n < 2 * TB => format!("{:.2} {}", n as f64 / GB as f64, "GiB".cyan()), n => format!("{:.2} {}", n as f64 / TB as f64, "TiB".cyan()), } } pub fn permissions_helptext(config_dir: &Path, data_dir: &Path, e: &io::Error) { if e.raw_os_error() == Some(1) { let current_exe = std::env::current_exe() .ok() .map(|s| s.to_string_lossy().to_string()) .unwrap_or_else(|| "".into()); eprintdoc!( "{}: innernet can't access the device info. You either need to run innernet as root, or give innernet CAP_NET_ADMIN capabilities: sudo setcap cap_net_admin+eip {} ", "ERROR".bold().red(), current_exe ); } else if e.kind() == io::ErrorKind::PermissionDenied { eprintdoc!( "{}: innernet can't access its config/data folders. You either need to run innernet as root, or give the user/group running innernet permissions to access {config} and {data}. For non-root permissions, it's recommended to create an \"innernet\" group, and run for example: sudo chgrp -R innernet {config} {data} sudo chmod -R g+rwX {config} {data} ", "ERROR".bold().red(), config = config_dir.to_string_lossy(), data = data_dir.to_string_lossy(), ); } } pub fn print_peer_diff(store: &DataStore, diff: &PeerDiff) { let public_key = diff.public_key().to_base64(); let text = match (diff.old, diff.new) { (None, Some(_)) => "added".green(), (Some(_), Some(_)) => "modified".yellow(), (Some(_), None) => "removed".red(), _ => unreachable!("PeerDiff can't be None -> None"), }; // Grab the peer name from either the new data, or the historical data (if the peer is removed). let peer_hostname = match diff.new { Some(peer) => Some(peer.name.clone()), None => store .peers() .iter() .find(|p| p.public_key == public_key) .map(|p| p.name.clone()), }; let peer_name = peer_hostname.as_deref().unwrap_or("[unknown]"); log::info!( " peer {} ({}...) was {}.", peer_name.yellow(), &public_key[..10].dimmed(), text ); for change in diff.changes() { log::debug!(" {}", change); } } pub fn all_installed(config_dir: &Path) -> Result, std::io::Error> { // All errors are bubbled up when enumerating a directory let entries: Vec<_> = std::fs::read_dir(config_dir)? .into_iter() .collect::>()?; let installed: Vec<_> = entries .into_iter() .filter(|entry| match entry.file_type() { Ok(f) => f.is_file(), _ => false, }) .filter_map(|entry| { let path = entry.path(); match (path.extension(), path.file_stem()) { (Some(extension), Some(stem)) if extension == OsStr::new("conf") => { Some(stem.to_string_lossy().to_string()) }, _ => None, } }) .map(|name| name.parse()) .collect::>()?; Ok(installed) } pub struct Api<'a> { agent: Agent, server: &'a ServerInfo, } impl<'a> Api<'a> { pub fn new(server: &'a ServerInfo) -> Self { let agent = AgentBuilder::new() .timeout(Duration::from_secs(5)) .redirects(0) .build(); Self { agent, server } } pub fn http(&self, verb: &str, endpoint: &str) -> Result { self.request::<(), _>(verb, endpoint, None) } pub fn http_form( &self, verb: &str, endpoint: &str, form: S, ) -> Result { self.request(verb, endpoint, Some(form)) } fn request( &self, verb: &str, endpoint: &str, form: Option, ) -> Result { let request = self .agent .request( verb, &format!("http://{}/v1{}", self.server.internal_endpoint, endpoint), ) .set(INNERNET_PUBKEY_HEADER, &self.server.public_key); let response = if let Some(form) = form { request.send_json(serde_json::to_value(form).map_err(|e| { io::Error::new( io::ErrorKind::InvalidData, format!("failed to serialize JSON request: {}", e), ) })?)? } else { request.call()? }; let mut response = response.into_string()?; // A little trick for serde to parse an empty response as `()`. if response.is_empty() { response = "null".into(); } Ok(serde_json::from_str(&response).map_err(|e| { io::Error::new( io::ErrorKind::InvalidData, format!( "failed to deserialize JSON response from the server: {}, response={}", e, &response ), ) })?) } }