use nordselect::filters::{self, Filter};
use nordselect::{Protocol, ServerCategory, Servers};
use std::collections::HashSet;
fn parse_cli_args<'a>() -> clap::ArgMatches<'a> {
use clap::{App, Arg};
App::new("NordSelect")
.version(env!("CARGO_PKG_VERSION"))
.author(env!("CARGO_PKG_AUTHORS"))
.about(env!("CARGO_PKG_DESCRIPTION"))
.arg(
Arg::with_name("multi_ping")
.short("p")
.long("ping")
.help("Use ping tests with simultaneous pings")
.takes_value(false),
)
.arg(
Arg::with_name("single_ping")
.short("s")
.long("sping")
.help("Use ping tests and execute pings linear")
.takes_value(false),
)
.arg(
Arg::with_name("tries")
.short("t")
.long("tries")
.value_name("TRIES")
.default_value("2")
.help("Ping every server TRIES times")
.takes_value(true),
)
.arg(
Arg::with_name("amount")
.short("a")
.long("amount")
.value_name("AMOUNT")
.default_value("10")
.help("Ping only to the least AMOUNT ones loaded")
.takes_value(true),
)
.arg(
Arg::with_name("domain")
.short("d")
.long("domain")
.help("Print the full domain instead of the short identifier (us1.nordvpn.com instead of us1)")
.takes_value(false),
)
.arg(
Arg::with_name("list_filters")
.long("filters")
.help("Show all available filters")
.takes_value(false),
)
.arg(
Arg::with_name("filter")
.required(false)
.multiple(true)
.index(1)
.help("Any restriction put on the server. \
This can be a country ('us'), a protocol ('tcp') or a type \
of server ('p2p'). \
Any filter can be inverted by prepending '!' to it ('!us'). \
See --filters"),
)
.get_matches()
}
fn show_available_filters(data: &Servers) {
println!("PROTOCOLS:\ttcp, udp, pptp, l2tp, tcp_xor, udp_xor, socks, cybersecproxy, sslproxy, cybersecsslproxy, proxy, wg_udp, nordlynx");
println!("SERVERS:\tstandard, dedicated, double, obfuscated, p2p, tor");
let mut flags: Vec<String> = data.flags().iter().map(|&x| x.to_lowercase()).collect();
flags.sort_unstable();
let flags = flags;
let mut iter = flags.iter();
if let Some(flag) = iter.next() {
print!("COUNTRIES:\t{}", flag.to_lowercase());
iter.for_each(|flag| print!(", {}", flag.to_lowercase()));
}
println!();
println!();
println!("REGIONS:");
let iter = nordselect::filters::Region::from_str_options();
let mut iter = iter.iter();
if let Some(flag) = iter.next() {
println!("{}\t{}", flag.0.to_lowercase(), flag.1);
iter.for_each(|flag| println!("{}\t{}", flag.0.to_lowercase(), flag.1));
println!();
}
println!("Any filter can be inverted using !");
}
fn parse_static_filter(filter: &str) -> Option<(Box<dyn Filter>, bool)> {
let mut is_category_filter = false;
let lib_filter = {
let mut category_filter = |category: ServerCategory| -> Box<dyn Filter> {
is_category_filter = true;
Box::new(filters::CategoryFilter::from(category))
};
let protocol_filter = |protocol: Protocol| -> Box<dyn Filter> {
Box::new(filters::ProtocolFilter::from(protocol))
};
match filter {
"p2p" => category_filter(ServerCategory::P2P),
"standard" => category_filter(ServerCategory::Standard),
"double" => category_filter(ServerCategory::Double),
"dedicated" => category_filter(ServerCategory::Dedicated),
"tor" => category_filter(ServerCategory::Tor),
"obfuscated" => category_filter(ServerCategory::Obfuscated),
"tcp" => protocol_filter(Protocol::Tcp),
"udp" => protocol_filter(Protocol::Udp),
"pptp" => protocol_filter(Protocol::Pptp),
"l2tp" => protocol_filter(Protocol::L2tp),
"tcp_xor" => protocol_filter(Protocol::OpenVPNXTcp),
"udp_xor" => protocol_filter(Protocol::OpenVPNXUdp),
"socks" => protocol_filter(Protocol::Socks),
"cybersecproxy" => protocol_filter(Protocol::CyberSecProxy),
"sslproxy" => protocol_filter(Protocol::SslProxy),
"cybersecsslproxy" => protocol_filter(Protocol::CyberSecSslProxy),
"proxy" => protocol_filter(Protocol::Proxy),
"wg_udp" | "nordlynx" => protocol_filter(Protocol::WireGuardUdp),
_ => return None,
}
};
Some((lib_filter, is_category_filter))
}
fn consider_negating_filter<'a>(filter: &'a str) -> (&'a str, bool) {
if filter.len() > 0 && &filter[..1] == "!" {
return (&filter[1..], true);
}
(filter.into(), false)
}
#[test]
fn consider_negating_filter_test() {
assert_eq!(consider_negating_filter("qwe"), ("qwe", false));
assert_eq!(consider_negating_filter("!qwe"), ("qwe", true));
assert_eq!(consider_negating_filter(""), ("", false));
}
fn parse_filters(cli_filters: clap::Values<'_>, data: &Servers) -> Vec<Box<dyn Filter>> {
let flags = data.flags();
let mut lib_filters: Vec<Box<dyn Filter>> = Vec::new();
let mut category_filter_added = false;
let mut included_countries = HashSet::new();
let mut excluded_countries = HashSet::new();
for original_filter in cli_filters.into_iter() {
let (filter, is_negating) = consider_negating_filter(original_filter);
if let Some((lib_filter, is_category_filter)) = parse_static_filter(filter) {
lib_filters.push(if is_negating {
Box::new(filters::NegatingFilter::from(lib_filter))
} else {
lib_filter
});
if is_category_filter {
category_filter_added = true;
}
continue;
}
let filter_upper = filter.to_uppercase();
let contries_to_modify = if is_negating {
&mut excluded_countries
} else {
&mut included_countries
};
if flags.contains(filter_upper.as_str()) {
contries_to_modify.insert(filter_upper);
continue;
}
if let Some(region_countries) = filters::Region::from_str(&filter_upper) {
region_countries.countries().into_iter().for_each(|flag| {
contries_to_modify.insert(flag.into());
()
});
continue;
}
if let Ok(binary) = std::env::current_exe()
.unwrap()
.into_os_string()
.into_string()
{
eprintln!(
"Error: unknown filter: \"{}\". Run `{} --filters` to list all available filters.",
original_filter, binary
);
} else {
eprintln!(
"Error: unknown filter: \"{}\". Use `--filters` to list all available filters.",
original_filter
);
}
std::process::exit(1);
}
if !category_filter_added {
lib_filters.push(Box::new(filters::CategoryFilter::from(
ServerCategory::Standard,
)));
}
if !included_countries.is_empty() {
lib_filters.push(Box::new(filters::CountriesFilter::from(included_countries)));
}
if !excluded_countries.is_empty() {
lib_filters.push(Box::new(filters::NegatingFilter::new(
filters::CountriesFilter::from(excluded_countries),
)));
}
lib_filters
}
fn apply_filters(filters_to_apply: Vec<Box<dyn Filter>>, data: &mut Servers) {
for filter in filters_to_apply.iter() {
data.filter(filter.as_ref())
}
}
fn sort(data: &mut Servers, matches: &clap::ArgMatches<'_>) {
let mut should_sort = true;
let s_ping = matches.is_present("single_ping");
let m_ping = matches.is_present("multi_ping");
if s_ping || m_ping {
let tries_opt = matches.value_of("tries").unwrap().parse();
if let Err(err) = tries_opt {
eprintln!("Could not read tries of pings: {}", err);
std::process::exit(1);
}
let amount_opt = matches.value_of("amount").unwrap().parse();
if let Err(err) = amount_opt {
eprintln!("Could not read amount of pings: {}", err);
std::process::exit(1);
}
let amount = amount_opt.unwrap();
let tries = tries_opt.unwrap();
data.cut(amount);
match {
if s_ping {
nordselect::sorters::PingSorter::ping_single(&data, tries)
} else {
nordselect::sorters::PingSorter::ping_multi(&data, tries)
}
} {
Ok(sorter) => {
data.sort(&sorter);
should_sort = false;
}
Err(error) => {
eprintln!("An error occured when pinging: {}", error);
eprintln!("Results will not include ping results");
match error.to_string().as_str() {
"oping::PingError::LibOpingError: Operation not permitted" => {
eprintln!("");
eprintln!(
"This error means that you did not give permission to nordselect to ping."
);
eprintln!(
"More details can be found at https://github.com/cfallin/rust-oping"
);
if let Ok(exe) = std::env::current_exe() {
if cfg!(unix) {
eprintln!("Hint: to solve this on Linux, execute the following command (as root):");
eprintln!("\tsetcap cap_net_raw+ep {:#?}", exe);
} else if cfg!(windows) {
eprintln!("Hint: ping has not been tested on Windows. Consider using something else.");
}
}
}
_ => {}
}
eprintln!("");
should_sort = true;
}
}
}
if should_sort {
data.sort(&nordselect::sorters::LoadSorter);
}
}
fn main() {
let matches = parse_cli_args();
let mut data = match Servers::from_api() {
Ok(x) => x,
Err(x) => {
eprintln!("Could not download data: {}", x);
std::process::exit(1);
}
};
if matches.is_present("list_filters") {
show_available_filters(&data);
std::process::exit(0);
}
let filters_to_apply = parse_filters(
matches
.values_of("filter")
.unwrap_or(clap::Values::default()),
&data,
);
apply_filters(filters_to_apply, &mut data);
sort(&mut data, &matches);
if let Some(server) = data.perfect_server() {
println!(
"{}",
match matches.is_present("domain") {
true => &server.domain,
false => server.name().unwrap_or(&server.domain),
}
);
} else {
eprintln!("No server found");
std::process::exit(1);
}
}