use super::ProtonVPN;
use super::{ConfigurationChoice, OpenVpnProvider};
use crate::config::providers::{Input, Password, UiClient};
use crate::config::vpn::OpenVpnProtocol;
use crate::util::delete_all_files_in_dir;
use log::{debug, info};
use reqwest::Url;
use std::fmt::Display;
use std::fs::create_dir_all;
use std::fs::File;
use std::io::{Cursor, Read, Write};
use std::net::IpAddr;
use std::path::PathBuf;
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
use zip::ZipArchive;
impl ProtonVPN {
fn build_url(
&self,
category: &ConfigType,
tier: &Tier,
feature: &Feature,
protocol: &OpenVpnProtocol,
) -> anyhow::Result<Url> {
let cat = if tier == &Tier::Free {
"Server".to_string()
} else {
category.url_part()
};
let fet = if tier == &Tier::Free {
"Normal".to_string()
} else {
feature.url_part()
};
Ok(Url::parse(&format!("https://account.protonvpn.com/api/vpn/config?Category={}&Tier={}&Feature={}&Platform=Linux&Protocol={}", cat, tier.url_part(), fet, protocol))?)
}
}
impl OpenVpnProvider for ProtonVPN {
fn provider_dns(&self) -> Option<Vec<IpAddr>> {
None
}
fn prompt_for_auth(&self, uiclient: &dyn UiClient) -> anyhow::Result<(String, String)> {
let username = uiclient.get_input(Input {
prompt:
"ProtonVPN OpenVPN username (see: https://account.protonvpn.com/account#openvpn )"
.to_string(),
validator: None,
})?;
let password = uiclient.get_password(Password {
prompt: "OpenVPN Password".to_string(),
confirm: true,
})?;
Ok((username.trim().to_string(), password.trim().to_string()))
}
fn auth_file_path(&self) -> anyhow::Result<Option<PathBuf>> {
Ok(Some(self.openvpn_dir()?.join("auth.txt")))
}
fn create_openvpn_config(&self, uiclient: &dyn UiClient) -> anyhow::Result<()> {
let openvpn_dir = self.openvpn_dir()?;
let code_map = crate::util::country_map::code_to_country_map();
create_dir_all(&openvpn_dir)?;
delete_all_files_in_dir(&openvpn_dir)?;
let tier = Tier::index_to_variant(uiclient.get_configuration_choice(&Tier::default())?);
let config_choice = if tier != Tier::Free {
ConfigType::index_to_variant(uiclient.get_configuration_choice(&ConfigType::default())?)
} else {
ConfigType::Standard
};
let feature_choice = if tier != Tier::Free {
Feature::index_to_variant(uiclient.get_configuration_choice(&Feature::default())?)
} else {
Feature::Normal
};
let protocol = OpenVpnProtocol::index_to_variant(
uiclient.get_configuration_choice(&OpenVpnProtocol::default())?,
);
let url = self.build_url(&config_choice, &tier, &feature_choice, &protocol)?;
let zipfile = reqwest::blocking::get(url)?;
let mut zip = ZipArchive::new(Cursor::new(zipfile.bytes()?))?;
let openvpn_dir = self.openvpn_dir()?;
create_dir_all(&openvpn_dir)?;
for i in 0..zip.len() {
let mut file_contents: Vec<u8> = Vec::with_capacity(2048);
let mut file = zip.by_index(i).unwrap();
file.read_to_end(&mut file_contents)?;
let file_contents = std::str::from_utf8(&file_contents)?;
let file_contents = file_contents
.split('\n')
.filter(|&x| !(x.starts_with("up ") || x.starts_with("down ")))
.collect::<Vec<&str>>()
.join("\n");
#[allow(deprecated)]
let filename = if let Some("ovpn") = file
.sanitized_name()
.extension()
.map(|x| x.to_str().expect("Could not convert OsStr"))
{
let mut hostname = None;
let mut code = file.name().split('.').next().unwrap();
if code.contains('-') {
let mut iter_split = code.split('-');
let fcode = iter_split.next().unwrap();
hostname = Some(iter_split.next().unwrap());
code = fcode;
}
let country = code_map
.get(code)
.unwrap_or_else(|| panic!("Could not find code in map: {}", code));
let host_str = if let Some(host) = hostname {
format!("-{}", host)
} else {
String::new()
};
format!("{}-{}{}.ovpn", country, code, &host_str)
} else {
file.name().to_string()
};
debug!("Reading file: {}", file.name());
let mut outfile =
File::create(openvpn_dir.join(filename.to_lowercase().replace(' ', "_")))?;
write!(outfile, "{}", file_contents)?;
}
let (user, pass) = self.prompt_for_auth(uiclient)?;
let auth_file = self.auth_file_path()?;
if auth_file.is_some() {
let mut outfile = File::create(auth_file.unwrap())?;
write!(outfile, "{}\n{}", user, pass)?;
info!(
"ProtonVPN OpenVPN config written to {}",
openvpn_dir.display()
);
}
Ok(())
}
}
#[derive(EnumIter, PartialEq)]
enum Tier {
Plus,
Basic,
Free,
}
impl Tier {
fn url_part(&self) -> String {
match self {
Self::Plus => "2".to_string(),
Self::Basic => "1".to_string(),
Self::Free => "0".to_string(),
}
}
fn index_to_variant(index: usize) -> Self {
Self::iter().nth(index).expect("Invalid index")
}
}
impl Display for Tier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::Plus => "Plus",
Self::Basic => "Basic",
Self::Free => "Free",
};
write!(f, "{}", s)
}
}
impl Default for Tier {
fn default() -> Self {
Self::Basic
}
}
impl ConfigurationChoice for Tier {
fn prompt(&self) -> String {
"Choose your ProtonVPN account tier".to_string()
}
fn all_names(&self) -> Vec<String> {
Self::iter().map(|x| format!("{}", x)).collect()
}
fn all_descriptions(&self) -> Option<Vec<String>> {
Some(Self::iter().map(|x| x.description().unwrap()).collect())
}
fn description(&self) -> Option<String> {
Some(
match self {
Self::Plus => "Plus Account provides more VPN servers and SecureCore configuration",
Self::Basic => "Provides core VPN servers",
Self::Free => "Free VPN servers only",
}
.to_string(),
)
}
}
#[derive(EnumIter, PartialEq)]
enum Feature {
P2P,
Tor,
Normal,
}
impl Feature {
fn url_part(&self) -> String {
match self {
Self::P2P => "4".to_string(),
Self::Tor => "2".to_string(),
Self::Normal => "0".to_string(),
}
}
fn index_to_variant(index: usize) -> Self {
Self::iter().nth(index).expect("Invalid index")
}
}
impl Display for Feature {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::P2P => "P2P",
Self::Tor => "Tor",
Self::Normal => "Normal",
};
write!(f, "{}", s)
}
}
impl Default for Feature {
fn default() -> Self {
Self::Normal
}
}
impl ConfigurationChoice for Feature {
fn prompt(&self) -> String {
"Please choose a server feature".to_string()
}
fn all_names(&self) -> Vec<String> {
Self::iter().map(|x| format!("{}", x)).collect()
}
fn all_descriptions(&self) -> Option<Vec<String>> {
Some(Self::iter().map(|x| x.description().unwrap()).collect())
}
fn description(&self) -> Option<String> {
Some(
match self {
Self::P2P => "Connect via torrent optmized network (Plus accounts only)",
Self::Tor => "Connect via Tor network (Plus accounts only)",
Self::Normal => "Standard (available servers depend on account tier)",
}
.to_string(),
)
}
}
#[derive(EnumIter, PartialEq)]
enum ConfigType {
SecureCore,
Standard,
}
impl ConfigType {
fn url_part(&self) -> String {
match self {
Self::SecureCore => "SecureCore".to_string(),
Self::Standard => "Country".to_string(),
}
}
fn index_to_variant(index: usize) -> Self {
Self::iter().nth(index).expect("Invalid index")
}
}
impl Display for ConfigType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::SecureCore => "SecureCore",
Self::Standard => "Standard",
};
write!(f, "{}", s)
}
}
impl Default for ConfigType {
fn default() -> Self {
Self::Standard
}
}
impl ConfigurationChoice for ConfigType {
fn prompt(&self) -> String {
"Please choose the set of OpenVPN configuration files you wish to install".to_string()
}
fn all_names(&self) -> Vec<String> {
Self::iter().map(|x| format!("{}", x)).collect()
}
fn all_descriptions(&self) -> Option<Vec<String>> {
Some(Self::iter().map(|x| x.description().unwrap()).collect())
}
fn description(&self) -> Option<String> {
Some(
match self {
Self::SecureCore => {
"Connect via SecureCore bridge for additional security (Plus accounts only)"
}
Self::Standard => {
"Standard OpenVPN connection (available servers depend on account tier)"
}
}
.to_string(),
)
}
}