Skip to content

Commit

Permalink
Merge pull request jamesmcm#171 from jamesmcm/refactor_dialoguer
Browse files Browse the repository at this point in the history
Refactor dialoguer code into cli_client
  • Loading branch information
jamesmcm committed Jul 23, 2022
2 parents ffcb9bb + 0d91534 commit c5b37eb
Show file tree
Hide file tree
Showing 29 changed files with 679 additions and 395 deletions.
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "vopono"
description = "Launch applications via VPN tunnels using temporary network namespaces"
version = "0.10.0"
version = "0.10.1"
authors = ["James McMurray <[email protected]>"]
edition = "2021"
license = "GPL-3.0-or-later"
Expand Down Expand Up @@ -29,6 +29,8 @@ bs58 = "0.4"
nix = "0.24"
config = "0.13"
basic_tcp_proxy = "0.3"
strum = "0.24"
strum_macros = "0.24"

[package.metadata.rpm]
package = "vopono"
Expand Down
63 changes: 56 additions & 7 deletions src/args.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,60 @@
use clap::ArgEnum;
use clap::Parser;
use std::fmt::Display;
use std::net::IpAddr;
use std::path::PathBuf;
use strum::IntoEnumIterator;
use vopono_core::config::providers::VpnProvider;
use vopono_core::config::vpn::Protocol;
use vopono_core::network::firewall::Firewall;
use vopono_core::network::network_interface::NetworkInterface;

#[derive(Clone)]
pub struct WrappedArg<T: IntoEnumIterator + Clone + Display> {
variant: T,
}

impl<T: IntoEnumIterator + Clone + Display> WrappedArg<T> {
pub fn to_variant(&self) -> T {
self.variant.clone()
}
}

impl<T: IntoEnumIterator + Clone + Display> ArgEnum for WrappedArg<T> {
fn from_str(input: &str, ignore_case: bool) -> core::result::Result<Self, String> {
let use_input = input.trim().to_string();

let found = if ignore_case {
T::iter().find(|x| x.to_string().to_ascii_lowercase() == use_input.to_ascii_lowercase())
} else {
T::iter().find(|x| x.to_string() == use_input)
};

if let Some(f) = found {
Ok(WrappedArg { variant: f })
} else {
// TODO - better error messages
Err(format!("Invalid argument: {}", input))
}
}

fn to_possible_value<'a>(&self) -> Option<clap::PossibleValue<'a>> {
// TODO: Leak necessary?
Some(clap::PossibleValue::new(Box::leak(
self.variant.to_string().into_boxed_str(),
)))
}

fn value_variants<'a>() -> &'a [Self] {
// TODO: Leak necessary?
Box::leak(Box::new(
T::iter()
.map(|x| WrappedArg { variant: x })
.collect::<Vec<Self>>(),
))
}
}

#[derive(Parser)]
#[clap(
name = "vopono",
Expand Down Expand Up @@ -54,22 +103,22 @@ pub enum Command {
pub struct SynchCommand {
/// VPN Provider - will launch interactive menu if not provided
#[clap(arg_enum, ignore_case = true)]
pub vpn_provider: Option<VpnProvider>,
pub vpn_provider: Option<WrappedArg<VpnProvider>>,

/// VPN Protocol (if not given will try to sync both)
#[clap(arg_enum, long = "protocol", short = 'c', ignore_case = true)]
pub protocol: Option<Protocol>,
pub protocol: Option<WrappedArg<Protocol>>,
}

#[derive(Parser)]
pub struct ExecCommand {
/// VPN Provider (must be given unless using custom config)
#[clap(arg_enum, long = "provider", short = 'p', ignore_case = true)]
pub vpn_provider: Option<VpnProvider>,
pub vpn_provider: Option<WrappedArg<VpnProvider>>,

/// VPN Protocol (if not given will use default)
#[clap(arg_enum, long = "protocol", short = 'c', ignore_case = true)]
pub protocol: Option<Protocol>,
pub protocol: Option<WrappedArg<Protocol>>,

/// Network Interface (if not given, will use first active network interface)
#[clap(long = "interface", short = 'i', ignore_case = true)]
Expand Down Expand Up @@ -125,7 +174,7 @@ pub struct ExecCommand {

/// VPN Protocol (if not given will use default)
#[clap(arg_enum, long = "firewall", ignore_case = true)]
pub firewall: Option<Firewall>,
pub firewall: Option<WrappedArg<Firewall>>,

/// Block all IPv6 traffic
#[clap(long = "disable-ipv6")]
Expand Down Expand Up @@ -158,11 +207,11 @@ pub struct ListCommand {
pub struct ServersCommand {
/// VPN Provider
#[clap(arg_enum, ignore_case = true)]
pub vpn_provider: VpnProvider,
pub vpn_provider: WrappedArg<VpnProvider>,

/// VPN Protocol (if not given will list all)
#[clap(arg_enum, long = "protocol", short = 'c', ignore_case = true)]
pub protocol: Option<Protocol>,
pub protocol: Option<WrappedArg<Protocol>>,

/// VPN Server prefix
#[clap(long = "prefix", short = 's')]
Expand Down
74 changes: 74 additions & 0 deletions src/cli_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use vopono_core::config::providers::{
BoolChoice, ConfigurationChoice, Input, InputNumericu16, Password, UiClient,
};

pub struct CliClient {}

impl UiClient for CliClient {
/// Launches a dialoguer single select menu for the enum
fn get_configuration_choice(
&self,
config_choice: &dyn ConfigurationChoice,
) -> anyhow::Result<usize> {
let display_names = config_choice.all_names();
let descriptions = config_choice.all_descriptions();
let parsed = if let Some(descs) = descriptions {
display_names
.iter()
.zip(descs)
.map(|x| format!("{}: {}", x.0, x.1))
.collect::<Vec<String>>()
} else {
display_names
};

let index = dialoguer::Select::new()
.with_prompt(config_choice.prompt())
.items(&parsed)
// TODO: Is this good enough?
.default(0)
.interact()?;
Ok(index)
}
fn get_bool_choice(&self, bool_choice: BoolChoice) -> anyhow::Result<bool> {
Ok(dialoguer::Confirm::new()
.with_prompt(&bool_choice.prompt)
.default(bool_choice.default)
.interact()?)
}

fn get_input(&self, inp: Input) -> anyhow::Result<String> {
let mut d = dialoguer::Input::<String>::new();

d.with_prompt(&inp.prompt);

if inp.validator.is_some() {
d.validate_with(inp.validator.unwrap());
};

Ok(d.interact()?)
}

fn get_input_numeric_u16(&self, inp: InputNumericu16) -> anyhow::Result<u16> {
let mut d = dialoguer::Input::<u16>::new();
d.with_prompt(&inp.prompt);

if inp.default.is_some() {
d.default(inp.default.unwrap());
}
if inp.validator.is_some() {
d.validate_with(inp.validator.unwrap());
}

Ok(d.interact()?)
}

fn get_password(&self, pw: Password) -> anyhow::Result<String> {
let mut req = dialoguer::Password::new();
if pw.confirm {
req.with_confirmation("Confirm password", "Passwords did not match");
};
req.with_prompt(pw.prompt);
Ok(req.interact()?)
}
}
13 changes: 9 additions & 4 deletions src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::{
fs::create_dir_all,
io::{self, Write},
};
use vopono_core::config::providers::VpnProvider;
use vopono_core::config::providers::{UiClient, VpnProvider};
use vopono_core::config::vpn::{verify_auth, Protocol};
use vopono_core::network::application_wrapper::ApplicationWrapper;
use vopono_core::network::firewall::Firewall;
Expand All @@ -21,7 +21,7 @@ use vopono_core::util::vopono_dir;
use vopono_core::util::{get_config_file_protocol, get_config_from_alias};
use vopono_core::util::{get_existing_namespaces, get_target_subnet};

pub fn exec(command: ExecCommand) -> anyhow::Result<()> {
pub fn exec(command: ExecCommand, uiclient: &dyn UiClient) -> anyhow::Result<()> {
// this captures all sigint signals
// ignore for now, they are automatically passed on to the child
let signals = Signals::new(&[SIGINT])?;
Expand Down Expand Up @@ -52,6 +52,7 @@ pub fn exec(command: ExecCommand) -> anyhow::Result<()> {
// Assign firewall from args or vopono config file
let firewall: Firewall = command
.firewall
.map(|x| x.to_variant())
.ok_or_else(|| anyhow!(""))
.or_else(|_| {
vopono_config_settings.get("firewall").map_err(|e| {
Expand Down Expand Up @@ -123,6 +124,7 @@ pub fn exec(command: ExecCommand) -> anyhow::Result<()> {
if let Some(path) = &custom_config {
protocol = command
.protocol
.map(|x| x.to_variant())
.unwrap_or_else(|| get_config_file_protocol(path));
provider = VpnProvider::Custom;

Expand Down Expand Up @@ -153,6 +155,7 @@ pub fn exec(command: ExecCommand) -> anyhow::Result<()> {
// Get server and provider
provider = command
.vpn_provider
.map(|x| x.to_variant())
.or_else(|| {
vopono_config_settings
.get("provider")
Expand Down Expand Up @@ -186,6 +189,7 @@ pub fn exec(command: ExecCommand) -> anyhow::Result<()> {
// Check protocol is valid for provider
protocol = command
.protocol
.map(|x| x.to_variant())
.or_else(|| {
vopono_config_settings
.get("protocol")
Expand All @@ -211,7 +215,7 @@ pub fn exec(command: ExecCommand) -> anyhow::Result<()> {
"Config files for {} {} do not exist, running vopono sync",
provider, protocol
);
synch(provider.clone(), Some(protocol.clone()))?;
synch(provider.clone(), Some(protocol.clone()), uiclient)?;
}
}

Expand Down Expand Up @@ -306,7 +310,7 @@ pub fn exec(command: ExecCommand) -> anyhow::Result<()> {
Protocol::OpenVpn => {
// Handle authentication check
let auth_file = if provider != VpnProvider::Custom {
verify_auth(provider.get_dyn_openvpn_provider()?)?
verify_auth(provider.get_dyn_openvpn_provider()?, uiclient)?
} else {
None
};
Expand Down Expand Up @@ -399,6 +403,7 @@ pub fn exec(command: ExecCommand) -> anyhow::Result<()> {
command.forward_ports.as_ref(),
firewall,
&server_name,
uiclient,
)?;
}
Protocol::OpenFortiVpn => {
Expand Down
7 changes: 4 additions & 3 deletions src/list_configs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use vopono_core::config::vpn::Protocol;
use vopono_core::util::get_configs_from_alias;

pub fn print_configs(cmd: ServersCommand) -> anyhow::Result<()> {
let provider = cmd.vpn_provider;
let provider = cmd.vpn_provider.to_variant();
if provider == VpnProvider::Custom {
bail!("Config listing not implemented for Custom provider files");
}
Expand All @@ -14,6 +14,7 @@ pub fn print_configs(cmd: ServersCommand) -> anyhow::Result<()> {
let protocol = cmd
.protocol
.clone()
.map(|x| x.to_variant())
.unwrap_or_else(|| provider.get_dyn_provider().default_protocol());

// Check config files exist for provider
Expand All @@ -35,7 +36,7 @@ pub fn print_configs(cmd: ServersCommand) -> anyhow::Result<()> {
let prefix = cmd.prefix.unwrap_or_default();
println!("provider\tprotocol\tconfig_file");
if (cmd.protocol.is_none() && provider.get_dyn_openvpn_provider().is_ok())
|| cmd.protocol == Some(Protocol::OpenVpn)
|| cmd.protocol.clone().map(|x| x.to_variant()) == Some(Protocol::OpenVpn)
{
let openvpn_configs = get_configs_from_alias(
&provider.get_dyn_openvpn_provider()?.openvpn_dir()?,
Expand All @@ -52,7 +53,7 @@ pub fn print_configs(cmd: ServersCommand) -> anyhow::Result<()> {
};

if (cmd.protocol.is_none() && provider.get_dyn_wireguard_provider().is_ok())
|| cmd.protocol == Some(Protocol::Wireguard)
|| cmd.protocol.map(|x| x.to_variant()) == Some(Protocol::Wireguard)
{
let wg_configs = get_configs_from_alias(
&provider.get_dyn_wireguard_provider()?.wireguard_dir()?,
Expand Down
13 changes: 10 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
#![allow(dead_code)]

mod args;
mod cli_client;
mod exec;
mod list;
mod list_configs;
mod sync;

use clap::Parser;
use cli_client::CliClient;
use list::output_list;
use list_configs::print_configs;
use log::{debug, warn, LevelFilter};
Expand All @@ -32,6 +34,7 @@ fn main() -> anyhow::Result<()> {
builder.filter_level(log_level);
builder.init();

let uiclient = CliClient {};
match app.cmd {
args::Command::Exec(cmd) => {
clean_dead_locks()?;
Expand All @@ -50,7 +53,7 @@ fn main() -> anyhow::Result<()> {
}
elevate_privileges(app.askpass)?;
clean_dead_namespaces()?;
exec::exec(cmd)?
exec::exec(cmd, &uiclient)?
}
args::Command::List(listcmd) => {
clean_dead_locks()?;
Expand All @@ -59,9 +62,13 @@ fn main() -> anyhow::Result<()> {
args::Command::Synch(synchcmd) => {
// If provider given then sync that, else prompt with menu
if synchcmd.vpn_provider.is_none() {
sync_menu()?;
sync_menu(&uiclient)?;
} else {
synch(synchcmd.vpn_provider.unwrap(), synchcmd.protocol)?;
synch(
synchcmd.vpn_provider.unwrap().to_variant(),
synchcmd.protocol.map(|x| x.to_variant()),
&uiclient,
)?;
}
}
args::Command::Servers(serverscmd) => {
Expand Down
Loading

0 comments on commit c5b37eb

Please sign in to comment.