use bridge_updater_lib::{updater::{Updater, UpdaterError}, locale};
use std::{time::Duration, env, process::{Command, Stdio}, io::{self, Write, BufRead}, fs::OpenOptions};
use chrono::prelude::*;
pub use crossterm::{
cursor,
event::{self, Event, KeyCode, KeyEvent, KeyModifiers, read, poll},
execute, queue, style,
terminal::{self, ClearType},
QueueableCommand, ExecutableCommand,
};
enum ImageType {
Captcha,
BridgesQR,
}
enum CountdownResult {
Exit,
TimeOut,
}
enum PromptResult {
BackError(UpdaterError),
WrongConfirm,
}
pub const VERSION: &'static str = env!("CARGO_PKG_VERSION");
async fn run_countdown(updater: &mut Updater) -> io::Result<CountdownResult> {
println!("");
let mut countdown_result = CountdownResult::TimeOut;
match updater.settings.last_retrieval_time {
Some(last_retrieval_time) => {
if let Some(retrieve_date) = DateTime::from_timestamp(last_retrieval_time, 0) {
println!("{}: {}", locale::get_translation().LAST_RETRIEVAL_TIME.to_string(), retrieve_date.with_timezone(&Local).format("%Y-%m-%d %H:%M:%S").to_string().as_str());
} else {
println!("{}: {}", locale::get_translation().LAST_RETRIEVAL_TIME.to_string(), locale::get_translation().RETRIEVAL_TIME_UNKNOWN);
}
},
None => {
println!("{}: {}", locale::get_translation().LAST_RETRIEVAL_TIME.to_string(), locale::get_translation().RETRIEVAL_TIME_UNKNOWN);
}
}
terminal::enable_raw_mode()?;
let mut stdout = io::stdout();
stdout.queue(terminal::SetTitle(locale::get_translation().WAIT_UNTIL_RETRIEVAL))?;
loop {
if poll(Duration::from_millis(100))? {
match read()? {
Event::Key(KeyEvent {modifiers: KeyModifiers::CONTROL, code: KeyCode::Char('c'),..}) | Event::Key(KeyEvent {code: KeyCode::Char('q'),..}) | Event::Key(KeyEvent {modifiers: KeyModifiers::CONTROL, code: KeyCode::Char('z'),..}) => {
countdown_result = CountdownResult::Exit;
break;
},
_ => (),
}
}
let mut write_output_text = String::new();
let now_time = Utc::now().timestamp();
let interval_wait_seconds = (updater.settings.timer_days*24*60*60) + (updater.settings.timer_hours*60*60) + (updater.settings.timer_minutes*60) + updater.settings.timer_seconds;
let mut next_time: i64 = now_time;
if let Some(last_time) = updater.settings.last_retrieval_time {
next_time = last_time + interval_wait_seconds;
}
if now_time > next_time {
break;
}
let mut time_state = String::new();
let until_next: i64 = next_time - now_time;
if until_next > 0 {
let days = until_next/(24*60*60);
let hours = (until_next%(24*60*60))/(60*60);
let minutes = (until_next/60)%60;
let seconds = until_next%60;
if days > 0 {
time_state.push_str(days.to_string().as_str());
time_state.push_str(&(" ".to_string() + locale::get_days(days).to_string().as_str()));
}
if hours > 0 {
if days > 0 {
time_state.push(' ');
}
time_state.push_str(hours.to_string().as_str());
time_state.push_str(&(" ".to_string() + locale::get_hours(hours).to_string().as_str()));
}
if minutes > 0 {
if days > 0 || hours > 0 {
time_state.push(' ');
}
time_state.push_str(minutes.to_string().as_str());
time_state.push_str(&(" ".to_string() + locale::get_minutes(minutes).to_string().as_str()));
}
if seconds > 0 {
if days > 0 || hours > 0 || minutes > 0 {
time_state.push(' ');
}
time_state.push_str(seconds.to_string().as_str());
time_state.push_str(&(" ".to_string() + locale::get_seconds(seconds).to_string().as_str()));
}
write_output_text = time_state;
}
stdout.queue(cursor::SavePosition)?;
stdout.write_all((locale::get_translation().UNTIL_RETRIEVAL.to_string() + ": " + &write_output_text).as_bytes())?;
stdout.flush()?;
stdout.queue(cursor::RestorePosition)?;
stdout.queue(terminal::Clear(terminal::ClearType::FromCursorDown))?;
}
terminal::disable_raw_mode()?;
Ok(countdown_result)
}
enum ShowTarget {
SetOption,
ListSetOptions,
Help,
Version,
}
struct SetOptionPair {
name: String,
value: String,
}
struct UpdaterUtilArguments {
run_oneshot: bool,
set_options: Vec<SetOptionPair>,
no_confirm: bool,
show: Option<ShowTarget>,
}
#[tokio::main]
async fn main() {
let translation = locale::get_translation();
match Updater::new() {
Ok(mut updater) => {
let args_list: Vec<String> = env::args().skip(1).collect();
let mut cmd_args_options = UpdaterUtilArguments {
run_oneshot: false,
set_options: vec![],
no_confirm: false,
show: None,
};
parse_args(&mut cmd_args_options, &args_list);
match cmd_args_options.show {
None => {
if cmd_args_options.run_oneshot == false {
loop {
match run_countdown(&mut updater).await {
Ok(countdown_result) => {
match countdown_result {
CountdownResult::TimeOut => {
println!("\n{}", translation.TIME_TO_RETRIEVE);
if let Err(error) = prompt_update_bridges(&mut updater, cmd_args_options.no_confirm).await {
print_prompt_result(error);
}
},
CountdownResult::Exit => {
break;
},
}
},
_ => {},
}
}
} else {
if let Err(error) = prompt_update_bridges(&mut updater, true).await {
print_prompt_result(error);
}
}
if updater.settings.torrc_disable_malfunctioning == true {
if let Err(error) = prompt_disable_malfunctioning_bridges(&mut updater, cmd_args_options.no_confirm).await {
print_prompt_result(error);
}
}
},
Some(show) => {
match show {
ShowTarget::Help => {
println!(
"
{} [{}]
{}:
-o, --oneshot : {}
-s, --set [{}] [{}] : {}
-ls, --list-set : {}
-n, --no-confirm : {}
-h, --help : {}
-v, --version : {}
",
env::args().next().unwrap(),
translation.CLI_ARGUMENT,
translation.CLI_ARGUMENTS,
translation.CLI_ONESHOT_DESCRIPTION,
translation.CLI_OPTION_NAME, translation.CLI_OPTION_VALUE, translation.CLI_SET_OPTION_DESCRIPTION,
translation.CLI_LIST_OPTIONS_DESCRIPTION,
translation.CLI_NO_CONFIRM_DESCRIPTION,
translation.CLI_HELP_DESCRIPTION,
translation.CLI_VERSION_DESCRIPTION)
},
ShowTarget::ListSetOptions => {
let mut retrieval_time = translation.RETRIEVAL_TIME_UNKNOWN.to_string();
if let Some(last_retrieval_time) = updater.settings.last_retrieval_time {
if let Some(retrieval_timestamp) = DateTime::from_timestamp(last_retrieval_time, 0) {
retrieval_time = retrieval_timestamp.with_timezone(&Local).format("%Y-%m-%d %H:%M:%S").to_string();
}
}
let bridge_transport = match updater.settings.bridge_transport {
0 => {
"none"
},
1 => {
"obfs4"
},
2 => {
"webtunnel"
},
_ => {
"none"
},
};
println!(
"
{}:
last_retrieval_time ({}): {}
timer_days ({}): {}
timer_hours ({}): {}
timer_minutes ({}): {}
timer_seconds ({}): {}
bridge_transport ({}): {}
bridge_ipv6 ({}): {}
save_torrc ({}): {}
save_torrc_path ({}): {}
torrc_disable_old ({}): {}
torrc_keep_old_num ({}): {}
torrc_disable_malfunctioning ({}): {}
save_bridge_file ({}): {}
save_bridge_file_path ({}): {}
use_proxy ({}): {}
proxy_protocol_type ({}): {}
proxy_host ({}): {}
proxy_port ({}): {}
proxy_onion ({}): {}
notifications ({}. {}): {}
run_in_background ({}. {}): {}
",
translation.CLI_OPTIONS_LIST,
translation.SETTINGS_HINT_LAST_RETRIEVAL_TIME, retrieval_time,
translation.SETTINGS_HINT_TIMER_DAYS, updater.settings.timer_days,
translation.SETTINGS_HINT_TIMER_HOURS, updater.settings.timer_hours,
translation.SETTINGS_HINT_TIMER_MINUTES, updater.settings.timer_minutes,
translation.SETTINGS_HINT_TIMER_SECONDS, updater.settings.timer_seconds,
translation.SETTINGS_HINT_BRIDGE_TRANSPORT_TYPE, bridge_transport,
translation.SETTINGS_HINT_BRIDGE_IPV6, updater.settings.bridge_ipv6,
translation.SETTINGS_HINT_SAVE_TORRC, updater.settings.save_torrc,
translation.SETTINGS_HINT_SAVE_TORRC_PATH, updater.settings.save_torrc_path,
translation.SETTINGS_HINT_TORRC_DISABLE_OLD, updater.settings.torrc_disable_old,
translation.SETTINGS_HINT_TORRC_KEEP_OLD_NUM, updater.settings.torrc_keep_old_num,
translation.SETTINGS_HINT_TORRC_DISABLE_MALFUNCTIONING, updater.settings.torrc_disable_malfunctioning,
translation.SETTINGS_HINT_SAVE_BRIDGE_FILE, updater.settings.save_bridge_file,
translation.SETTINGS_HINT_SAVE_BRIDGE_FILE_PATH, updater.settings.save_bridge_file_path,
translation.SETTINGS_HINT_USE_PROXY, updater.settings.use_proxy,
translation.SETTINGS_HINT_PROXY_PROTOCOL_TYPE, updater.settings.proxy_protocol_type,
translation.SETTINGS_HINT_PROXY_HOST, updater.settings.proxy_host,
translation.SETTINGS_HINT_PROXY_PORT, updater.settings.proxy_port,
translation.SETTINGS_HINT_PROXY_ONION, updater.settings.proxy_onion,
translation.SETTINGS_HINT_NOTIFICATIONS, translation.FOR_GUI_FRONTENDS, updater.settings.notifications,
translation.SETTINGS_HINT_RUN_IN_BACKGROUND, translation.FOR_GUI_FRONTENDS, updater.settings.run_in_background);
},
ShowTarget::Version => {
println!("{} v{} - {}", translation.APP_TITLE, VERSION, translation.APP_DESCRIPTION);
},
ShowTarget::SetOption => {
for opt in cmd_args_options.set_options {
match updater.settings.parse_option((&opt.name, &opt.value)) {
Ok(_) => {
println!("{}", translation.CLI_OPTION_SET.replacen("{}", &opt.name, 1).replacen("{}", &opt.value, 1));
},
Err(error) => {
println!("{}", error.print());
},
}
}
},
}
}
}
},
Err(error) => {
println!("{}", error.print());
},
}
}
fn print_prompt_result(result: PromptResult) {
let translation = locale::get_translation();
match result {
PromptResult::BackError(updater_error) => {
println!("{}: {}", translation.UPDATER_ERROR, updater_error.print());
},
PromptResult::WrongConfirm => {
println!("{}", translation.CLI_WRONG_CONFIRM_ACTION);
},
}
}
fn parse_args(cmd_args_options: &mut UpdaterUtilArguments, args: &Vec<String>) {
let mut arg_list = args.iter();
while let Some(arg_read) = arg_list.next() {
let arg_current = arg_read.trim().to_ascii_lowercase();
if arg_current.starts_with("-") {
let mut arg_name = arg_current.clone();
while arg_name.starts_with("-") {
arg_name.remove(0);
}
match arg_name.as_str() {
"o" | "oneshot" => {
cmd_args_options.run_oneshot = true;
},
"s" | "set" => {
cmd_args_options.show = Some(ShowTarget::SetOption);
if let Some(option_name) = arg_list.next() {
if let Some(option_value) = arg_list.next() {
cmd_args_options.set_options.push(SetOptionPair { name: option_name.to_string(), value: option_value.to_string() });
}
}
},
"ls" | "list-set" => {
cmd_args_options.show = Some(ShowTarget::ListSetOptions);
},
"n" | "no-confirm" => {
cmd_args_options.no_confirm = true;
},
"h" | "help" => {
cmd_args_options.show = Some(ShowTarget::Help);
},
"v" | "version" => {
cmd_args_options.show = Some(ShowTarget::Version);
},
_ => (),
}
}
}
}
async fn prompt_ask(prompt_message: &str) -> Option<bool> {
let mut response_choice = false;
io::stdout().flush().unwrap();
print!("{} [{}/{}] ", prompt_message, locale::get_yes_no(true).to_uppercase(), locale::get_yes_no(false));
io::stdout().flush().unwrap();
let mut ask_result = String::new();
let in_read = io::stdin();
let mut in_handle = in_read.lock();
if in_handle.read_line(&mut ask_result).is_ok() {
ask_result = ask_result.trim().to_string();
ask_result.truncate(1);
match ask_result.chars().map(|c| c.to_lowercase()).next() {
Some(mut char_result_unfiltered) => {
match char_result_unfiltered.next() {
Some(char_result) => {
if char_result == locale::get_yes_no(true) || char_result == 'y' {
response_choice = true;
} else if char_result == locale::get_yes_no(false) || char_result == 'n' {
response_choice = false;
} else {
return None;
}
},
None => {
response_choice = true;
},
}
},
None => {
response_choice = true;
},
}
}
Some(response_choice)
}
fn prompt_ask_text(ask_message: &str) -> Option<String> {
io::stdout().flush().unwrap();
print!("\n{}: ", ask_message);
io::stdout().flush().unwrap();
let in_read = io::stdin();
let mut in_handle = in_read.lock();
let mut input_text = String::new();
if in_handle.read_line(&mut input_text).is_ok() {
return Some(input_text.trim().to_string());
}
None
}
async fn prompt_update_bridges(updater: &mut Updater, no_confirm: bool) -> Result<(), PromptResult> {
let mut response_choice = true;
if !no_confirm {
match prompt_ask(locale::get_translation().ASK_UPDATE_BRIDGES).await {
Some(ask_result) => {
response_choice = ask_result;
},
None => {
return Err(PromptResult::WrongConfirm);
}
}
}
if response_choice == true {
println!("\n{}\n", locale::get_translation().CAPTCHA_LOADING_MESSAGE);
if updater.retrieve_captcha().await.is_ok() {
if let Some((_, captcha_image)) = updater.captcha.clone() {
if let Ok(image_save_path) = save_image(&captcha_image, ImageType::Captcha) {
if open_image(image_save_path).is_ok() {
if let Some(input_captcha) = prompt_ask_text(locale::get_translation().CAPTCHA_ENTER) {
match updater.retrieve_bridges(&input_captcha).await {
Ok(()) => {
if let Some((bridges_text, bridges_qr)) = updater.bridges.clone() {
println!("\n{}: \n{}\n", locale::get_translation().YOUR_NEW_BRIDGES, bridges_text);
if let Ok(qr_save_path) = save_image(&bridges_qr, ImageType::BridgesQR) {
if open_image(qr_save_path).is_ok() {
if let Err(error_bridges) = updater.update_bridges() {
println!("\n{}", error_bridges.print());
}
}
}
}
},
Err(error) => {
println!("{}", error.print());
},
}
}
}
}
}
}
} else {
updater.settings.last_retrieval_time = Some(Utc::now().timestamp());
if let Err(error) = updater.settings.save() {
return Err(PromptResult::BackError(UpdaterError::ErrorSettings(error)));
}
}
Ok(())
}
async fn prompt_disable_malfunctioning_bridges(updater: &mut Updater, no_confirm: bool) -> Result<(), PromptResult> {
let mut response_choice = true;
let mut bridge_mal_len: usize = 0;
if !no_confirm {
match updater.find_malfunctioning_bridges() {
Ok(bridges_malfunctioning) => {
bridge_mal_len = bridges_malfunctioning.len();
if bridge_mal_len > 0 {
println!("{} {}", locale::get_translation().FOUND_MALFUNCTIONING.replace("{}", &bridge_mal_len.to_string()), locale::get_bridges(bridge_mal_len as i64));
} else {
return Ok(());
}
match prompt_ask(locale::get_translation().ASK_DISABLE_MALFUNCTIONING_BRIDGES).await {
Some(ask_result) => {
response_choice = ask_result;
},
None => {
return Err(PromptResult::WrongConfirm);
}
}
},
Err(error) => {
return Err(PromptResult::BackError(error));
},
}
}
if response_choice == true && bridge_mal_len > 0 {
match updater.disable_malfunctioning_bridges() {
Ok(()) => {
return Ok(());
},
Err(_) => {
return Err(PromptResult::WrongConfirm);
},
}
}
Ok(())
}
fn save_image(image: &[u8], image_type: ImageType) -> Result<String, io::Error> {
let mut temp_file_name = "bridge_updater_".to_string();
match image_type {
ImageType::Captcha => {
temp_file_name.push_str("captcha_");
},
ImageType::BridgesQR => {
temp_file_name.push_str("qr_");
},
}
temp_file_name.push_str(&Utc::now().timestamp().to_string());
temp_file_name.push_str(".jpg");
let temp_file_path = env::temp_dir().as_path().join(temp_file_name).to_string_lossy().into_owned();
let mut image_file = OpenOptions::new().write(true).create(true).open(temp_file_path.clone())?;
image_file.write_all(image)?;
image_file.sync_all()?;
Ok(temp_file_path)
}
fn open_image(path: String) -> io::Result<()> {
if cfg!(target_os = "windows") {
Command::new("start").arg(path).stdout(Stdio::null()).stderr(Stdio::null()).spawn()?;
} else if cfg!(target_os = "macos") {
Command::new("open").arg(path).stdout(Stdio::null()).stderr(Stdio::null()).spawn()?;
} else if cfg!(target_os = "linux") {
Command::new("xdg-open").arg(path).stdout(Stdio::null()).stderr(Stdio::null()).spawn()?;
}
Ok(())
}