use std::{
fs, process, fmt,
ffi::OsStr,
collections::HashSet,
ops::Deref
};
use chrono::{Datelike, DateTime, Local, Timelike};
use reqwest::blocking;
use rust_utils::{
utils,
logging::LogLevel
};
use lazy_static::lazy_static;
use cfg_if::cfg_if;
use crate::{
LOG,
DEBUG,
PRINT_STDOUT,
LOG_DIR,
stats::*,
devices::DeviceList,
config::{GlobalConfig, Config}
};
use cursive::{
theme::{BaseColor, Color, ColorStyle},
utils::markup::StyledString
};
use json::JsonValue;
#[cfg(feature = "sagemcom")]
use qrcode::QrCode;
pub mod tracker;
pub mod archive;
cfg_if! {
if #[cfg(feature = "nokia")] {
mod trashcan;
use trashcan::TrashcanClient;
}
}
cfg_if! {
if #[cfg(feature = "sagemcom")] {
mod sagemcom;
use sagemcom::SagemcomClient;
}
}
lazy_static! {
static ref NEED_AUTH: bool = !utils::run_command("nmcli", false, ["networking", "connectivity", "check"]).success;
}
#[derive(Debug)]
pub enum ClientError {
LoginFailed,
GatewayUnreachable(String),
JSONParseError(json::Error, String),
JSONValueNotFound(&'static str, String),
NotSupported
}
#[derive(PartialEq, Eq)]
pub enum GatewayModel {
Nokia,
Sagemcom
}
impl actix_web::ResponseError for ClientError { }
impl fmt::Display for ClientError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Self::LoginFailed => write!(f, "Unable to log into the T-Mobile gateway. Is the username/password in global.toml correct?"),
Self::GatewayUnreachable(ref why) => write!(f, "Unable to reach the T-Mobile gateway: {why}"),
Self::JSONParseError(ref why, ref data) => write!(f, "JSON data failed to parse correctly!\nError: {why}\nJSON data:\n{data}"),
Self::JSONValueNotFound(ref name, ref data) => write!(f, "JSON value \"{name}\" was not found! JSON data:\n{data}"),
Self::NotSupported => write!(f, "This gateway does not support this feature")
}
}
}
pub trait JSONUnwrappable {
type Output;
fn unwrap_json(self, name: &'static str, data: &JsonValue) -> ClientResult<Self::Output>;
}
impl<T> JSONUnwrappable for Option<T> {
type Output = T;
fn unwrap_json(self, name: &'static str, data: &JsonValue) -> ClientResult<T> {
match self {
Some(t) => Ok(t),
None => Err(ClientError::JSONValueNotFound(name, data.to_string()))
}
}
}
pub type ClientResult<T> = Result<T, ClientError>;
pub trait GatewayClient: Send + Sync {
fn reboot_gateway_no_log(&self) -> ClientResult<()>;
fn get_devices(&self) -> ClientResult<DeviceList>;
fn get_5g_stats(&self) -> ClientResult<CellInfo>;
fn get_4g_stats(&self) -> ClientResult<CellInfo>;
fn get_conn_status(&self) -> bool;
fn get_info(&self) -> ClientResult<GatewayInfo>;
fn get_adv_stats(&self) -> ClientResult<AdvancedGatewayStats>;
#[cfg(feature = "nokia")]
fn get_download(&self) -> ClientResult<u128> { Err(ClientError::NotSupported) }
#[cfg(feature = "nokia")]
fn get_upload(&self) -> ClientResult<u128> { Err(ClientError::NotSupported) }
fn get_networks(&self) -> ClientResult<Vec<NetworkInfo>>;
#[cfg(feature = "sagemcom")]
fn set_networks(&self, _networks: &[NetworkInfo]) -> ClientResult<()> { Err(ClientError::NotSupported) }
#[cfg(feature = "sagemcom")]
fn set_admin_pswd(&self, _password: &str) -> ClientResult<()> { Err(ClientError::NotSupported) }
fn webui_pic(&self) -> &'static str;
fn model(&self) -> GatewayModel;
fn reboot_gateway(&self, print_stdout: bool) -> ClientResult<()> {
LOG.line(LogLevel::Info, "Rebooting gateway...", print_stdout);
self.reboot_gateway_no_log()
}
fn get_all_stats(&self) -> ClientResult<GatewayStats> {
let conn_status = self.get_conn_status();
let info_5g = self.get_5g_stats()?;
let info_4g = self.get_4g_stats()?;
#[cfg(feature = "nokia")]
let download = self.get_download().unwrap_or(0);
#[cfg(feature = "nokia")]
let upload = self.get_upload().unwrap_or(0);
let networks = self.get_networks()?;
let devices = self.get_devices()?;
Ok(GatewayStats {
conn_status,
devices,
#[cfg(feature = "nokia")]
download,
#[cfg(feature = "nokia")]
upload,
info_4g,
info_5g,
networks
})
}
fn get_tracker_entry(&self, now: DateTime<Local>, reboot: bool) -> ClientResult<tracker::SignalTrackerEntry> {
let info_4g = self.get_4g_stats()?;
let info_5g = self.get_5g_stats()?;
let conn_stat = self.get_conn_status();
let time = (now.hour(), now.minute());
let date = (now.year() as u32, now.month(), now.day());
Ok(tracker::SignalTrackerEntry {
date,
time,
conn_stat,
info_4g,
info_5g,
reboot
})
}
}
pub struct BoxedClient(Box<dyn GatewayClient>);
#[allow(clippy::new_without_default)]
impl BoxedClient {
pub fn new() -> BoxedClient {
let config = GlobalConfig::load();
let exe = utils::get_execname();
let print_stdout = &exe == "gatewaymon" || &exe == "tmo-webui";
let client: Box<dyn GatewayClient> =
if blocking::get(format!("http://{}/TMI/v1/gateway?get=all", config.gateway_ip)).is_ok() {
#[cfg(feature = "sagemcom")]
{
LOG.line_basic("Gateway model: Sagemcom", print_stdout);
Box::new(SagemcomClient::new())
}
#[cfg(not(feature = "sagemcom"))]
{
LOG.line(LogLevel::Error, "The Sagemcom gateway model is not supported!", true);
process::exit(101);
}
}
else if blocking::get(format!("http://{}/fastmile_radio_status_web_app.cgi", config.gateway_ip)).is_ok() {
#[cfg(feature = "nokia")]
{
LOG.line_basic("Gateway model: Nokia", print_stdout);
Box::new(TrashcanClient::new())
}
#[cfg(not(feature = "nokia"))]
{
LOG.line(LogLevel::Error, "The Nokia gateway model is not supported!", true);
process::exit(101);
}
}
else {
LOG.line(LogLevel::Error, "This is not a T-Mobile Home Internet network!", true);
process::exit(101);
};
BoxedClient(client)
}
}
impl Deref for BoxedClient {
type Target = Box<dyn GatewayClient>;
fn deref(&self) -> &Box<dyn GatewayClient> { &self.0 }
}
fn parse_json(source: &str) -> ClientResult<JsonValue> {
match json::parse(source) {
Ok(j) => Ok(j),
Err(why) => Err(ClientError::JSONParseError(why, source.to_string()))
}
}
fn parse_login_json(source: &str) -> ClientResult<JsonValue> {
match parse_json(source) {
Ok(j) => Ok(j),
Err(why) => {
LOG.line(LogLevel::Error, format!("JSON Error: {why}"), false);
LOG.line(LogLevel::Error, "Received input:", false);
for line in source.lines() {
LOG.line(LogLevel::Error, line, false);
}
Err(ClientError::LoginFailed)
}
}
}
pub fn get_log_dates(archive: bool) -> Vec<(u32, u32, u32)> {
if archive {
if let Some(dates) = archive::get_log_dates() {
return dates;
}
}
let files = fs::read_dir(&*LOG_DIR).unwrap().map(|file| {
let name_t = file.unwrap().file_name();
name_t.to_str().unwrap().to_string()
}).collect::<Vec<String>>();
let mut dates = Vec::new();
for file in files.iter().rev().filter(|file| file.contains("gatewaymon")) {
let mut split1 = file.split('.');
let mut split2 = split1.next().unwrap().split('-');
split2.next();
dates.push((
split2.next().unwrap().parse().unwrap(),
split2.next().unwrap().parse().unwrap(),
split2.next().unwrap().parse().unwrap()
));
}
let mut uniques = HashSet::new();
dates.retain(|d| uniques.insert(*d));
dates.sort_unstable();
dates.reverse();
dates
}
pub fn get_log(date: (u32, u32, u32), archive: bool) -> StyledString {
let raw = get_log_plain(date, archive);
let mut styled = StyledString::new();
for line in raw.lines() {
let style = if line.contains("DEBUG]") {
ColorStyle::from(Color::Dark(BaseColor::Cyan))
}
else if line.contains("WARN]") {
ColorStyle::from(Color::Light(BaseColor::Yellow))
}
else if line.contains("ERROR]") {
ColorStyle::from(Color::Light(BaseColor::Red))
}
else if line.contains("FATAL]") {
ColorStyle::from((Color::Light(BaseColor::Red), Color::Dark(BaseColor::Black)))
}
else {
ColorStyle::from(Color::Dark(BaseColor::Green))
};
styled.append_styled(line, style);
styled.append("\n");
}
styled
}
pub fn get_log_plain(date: (u32, u32, u32), archive: bool) -> String {
if archive {
if let Some(log) = archive::get_log(date) {
return log;
}
}
if let Ok(log) = fs::read_to_string(format!("{}/gatewaymon-{}-{}-{}.log", *LOG_DIR, date.0, date.1, date.2)) {
return log;
}
String::new()
}
pub fn check_net_env(full: bool) -> usize {
let output_str = if full {
LOG.line(*DEBUG, "Performing full check of network environment...", true);
if ping_req() {
return 0;
}
else {
nmcli(["networking", "connectivity", "check"], false)
}
}
else {
nmcli(["-t", "-f", "CONNECTIVITY", "general"], false)
};
if output_str.contains("none") { 2 }
else if output_str.contains("full") { 0 }
else { 1 }
}
pub fn is_valid_net() -> bool {
let config = GlobalConfig::load();
let networks = connected_networks();
if networks.is_empty() {
return false;
}
for network in networks {
if network.1.contains("ethernet") {
return true;
}
for name in &config.valid_networks {
if network.0 == *name {
return true;
}
}
}
false
}
pub fn connected_networks() -> Vec<(String, String)> {
let out = nmcli(["-t", "-f", "NAME,TYPE", "con", "show", "--active"], true);
if !out.contains(':') || out.is_empty() {
return vec![];
}
out.split('\n').map(|line: &str| {
let vals: Vec<&str> = line.split(':').collect();
(vals[0].to_string(), vals[1].to_string())
}).collect()
}
pub fn nmcli<A: IntoIterator<Item = S> + Clone, S: AsRef<OsStr>>(args: A, report_err: bool) -> String {
let args_dbg = args.clone();
let cmd = utils::run_command("nmcli", *NEED_AUTH, args);
if !cmd.success && report_err {
let err_msg = format!("Error executing nmcli command: {}", cmd.output);
for line in err_msg.lines() {
LOG.line(LogLevel::Error, line, true)
}
let mut cmd_ex = String::from("Command executed: nmcli");
for arg in args_dbg {
let arg_fmt = format!(" {}", arg.as_ref().to_str().unwrap());
cmd_ex.push_str(&arg_fmt);
}
LOG.line(LogLevel::Error, cmd_ex, true)
}
cmd.output
}
#[cfg(feature = "sagemcom")]
pub fn net_qr_code(name: &str, password: &str, security: &str) -> QrCode { QrCode::new(format!("WIFI:T:{security};S:{name};P:{password};H:;;")).unwrap() }
fn ping_req() -> bool {
let ping_cmd = utils::run_command("ping", false, ["paradisemod.net", "-c1", "-w10"]);
if let LogLevel::Debug(dbg) = *DEBUG {
if dbg {
LOG.line(LogLevel::Debug(true), "Ping output:", *PRINT_STDOUT);
for line in ping_cmd.output.lines() {
LOG.line(LogLevel::Debug(true), line, *PRINT_STDOUT);
}
}
}
ping_cmd.success
}