innernet/client/src/util.rs

268 lines
8.4 KiB
Rust

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(|| "<innernet path>".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<Vec<Interface>, std::io::Error> {
// All errors are bubbled up when enumerating a directory
let entries: Vec<_> = std::fs::read_dir(config_dir)?
.into_iter()
.collect::<Result<_, _>>()?;
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::<Result<_, _>>()?;
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<T: DeserializeOwned>(&self, verb: &str, endpoint: &str) -> Result<T, ureq::Error> {
self.request::<(), _>(verb, endpoint, None)
}
pub fn http_form<S: Serialize, T: DeserializeOwned>(
&self,
verb: &str,
endpoint: &str,
form: S,
) -> Result<T, ureq::Error> {
self.request(verb, endpoint, Some(form))
}
fn request<S: Serialize, T: DeserializeOwned>(
&self,
verb: &str,
endpoint: &str,
form: Option<S>,
) -> Result<T, ureq::Error> {
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
),
)
})?)
}
}