Skip to content

Commit

Permalink
Merge pull request #121 from niki-on-github/feature/airvpn
Browse files Browse the repository at this point in the history
Add AirVPN Provider with optional auth file
  • Loading branch information
jamesmcm authored Dec 20, 2021
2 parents 0923feb + 7dd5f59 commit 9336b78
Show file tree
Hide file tree
Showing 15 changed files with 273 additions and 50 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ webbrowser = "0.5"
basic_tcp_proxy = "0.3"
signal-hook = "0.3"
config = "0.11"
serde_json = "1.0"
bs58 = "0.4"

[package.metadata.rpm]
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ as normal.
vopono includes built-in killswitches for both Wireguard and OpenVPN.

Currently Mullvad, AzireVPN, MozillaVPN, TigerVPN, ProtonVPN, iVPN,
NordVPN, HMA (HideMyAss) and PrivateInternetAccess are supported directly, with custom
NordVPN, AirVPN, HMA (HideMyAss) and PrivateInternetAccess are supported directly, with custom
configuration files also supported with the `--custom` argument.

For custom connections the OpenConnect and OpenFortiVPN protocols are
Expand All @@ -34,6 +34,7 @@ lynx all running through different VPN connections:
| MozillaVPN |||
| NordVPN |||
| HMA (HideMyAss) |||
| airVPN |||

## Usage

Expand Down Expand Up @@ -172,7 +173,6 @@ $ rustc --version
for running `transmission-daemon`) does not work correctly when vopono
is run as root - see issue [#84](https://github.com/jamesmcm/vopono/issues/84)


## License

vopono is licensed under the GPL Version 3.0 (or above), see the LICENSE
Expand Down
2 changes: 1 addition & 1 deletion src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ pub fn exec(command: ExecCommand) -> anyhow::Result<()> {
Protocol::OpenVpn => {
// Handle authentication check
let auth_file = if provider != VpnProvider::Custom {
Some(verify_auth(provider.get_dyn_openvpn_provider()?)?)
verify_auth(provider.get_dyn_openvpn_provider()?)?
} else {
None
};
Expand Down
16 changes: 16 additions & 0 deletions src/providers/airvpn/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
mod openvpn;

use super::{ConfigurationChoice, OpenVpnProvider, Provider};
use crate::vpn::Protocol;

pub struct AirVPN {}

impl Provider for AirVPN {
fn alias(&self) -> String {
"AirVPN".to_string()
}

fn default_protocol(&self) -> Protocol {
Protocol::OpenVpn
}
}
173 changes: 173 additions & 0 deletions src/providers/airvpn/openvpn.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
use super::AirVPN;
use super::{ConfigurationChoice, OpenVpnProvider};
use crate::util::delete_all_files_in_dir;
use log::debug;
use serde_json::Value;
use std::collections::HashMap;
use std::env;
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 OpenVpnProvider for AirVPN {
fn provider_dns(&self) -> Option<Vec<IpAddr>> {
None
}

fn prompt_for_auth(&self) -> anyhow::Result<(String, String)> {
//NOTE: not required for AirVPN
Ok(("unused".to_string(), "unused".to_string()))
}

fn auth_file_path(&self) -> anyhow::Result<Option<PathBuf>> {
//NOTE: not required for AirVPN auth is inside ovpn file
Ok(None)
}

fn create_openvpn_config(&self) -> anyhow::Result<()> {
let use_country_code: bool = true;
let config_choice = ConfigType::choose_one()?;
let client = reqwest::blocking::Client::new();

let status_response = client
.get("https://airvpn.org/api/status/")
.send()?
.text()?;

let deserialized_json: HashMap<String, Value> =
serde_json::from_str(&status_response).unwrap();
let all_servers_array = deserialized_json
.get("servers")
.unwrap()
.as_array()
.unwrap();

let mut request_server_names = "".to_string();
for item in all_servers_array {
let public_name = item
.as_object()
.unwrap()
.get("public_name")
.unwrap()
.to_string()
.replace("\"", "");
if !request_server_names.is_empty() {
// separate server names with '%2C'
request_server_names.push_str("%2C");
}
request_server_names.push_str(&public_name);
}

let generator_url = config_choice
.url()?
.replace("{servers}", request_server_names.as_str());
let zipfile = client
.get(generator_url)
.header(
"API-KEY",
env::var("AIRVPN_API_KEY")
.expect("AIRVPN_API_KEY is not defined in your environment variables"),
)
.send()?;
let mut zip = ZipArchive::new(Cursor::new(zipfile.bytes()?))?;
let openvpn_dir = self.openvpn_dir()?;
let country_map = crate::util::country_map::code_to_country_map();
create_dir_all(&openvpn_dir)?;
delete_all_files_in_dir(&openvpn_dir)?;
for i in 0..zip.len() {
let mut file_contents: Vec<u8> = Vec::with_capacity(4096);
let mut file = zip.by_index(i).unwrap();
file.read_to_end(&mut file_contents)?;

//TODO: sanitized_name is now deprecated but there is not a simple alternative
#[allow(deprecated)]
let filename = if let Some("ovpn") = file
.sanitized_name()
.extension()
.map(|x| x.to_str().expect("Could not convert OsStr"))
{
let fname = file.name();
let fname_vec: Vec<&str> = fname.split('_').collect();
let country_code = fname_vec[1].split('-').next().unwrap().to_lowercase();
let city = fname_vec[1].split('-').collect::<Vec<&str>>()[1];
let server_name = fname_vec[2];
debug!("country_code: {}", country_code.to_string());
debug!("city: {}", city.to_string());
debug!("server_name: {}", server_name.to_string());
let country = country_map.get(country_code.as_str());
if country.is_none() || use_country_code {
format!("{}-{}.ovpn", country_code, server_name)
} else {
format!("{}-{}.ovpn", country.unwrap(), server_name)
}
} else {
file.name().to_string()
};

debug!("Reading file: {}", file.name());
let mut outfile =
File::create(openvpn_dir.join(filename.to_lowercase().replace(' ', "_")))?;
outfile.write_all(file_contents.as_slice())?;
}

Ok(())
}
}

#[derive(EnumIter, PartialEq)]
enum ConfigType {
UDP443,
TCP443,
}

impl ConfigType {
fn url(&self) -> anyhow::Result<String> {
let s = match self {
Self::UDP443 => "https://airvpn.org/api/generator/?protocols=openvpn_1_udp_443&download=zip&system=linux&iplayer_exit=ipv4&servers={servers}",
Self::TCP443 => "https://airvpn.org/api/generator/?protocols=openvpn_1_tcp_443&download=zip&system=linux&iplayer_exit=ipv4&servers={servers}",
};

Ok(s.parse()?)
}
}

impl Display for ConfigType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::UDP443 => "UDP",
Self::TCP443 => "TCP",
};
write!(f, "{}", s)
}
}

impl Default for ConfigType {
fn default() -> Self {
Self::UDP443
}
}

impl ConfigurationChoice for ConfigType {
fn prompt() -> String {
"Please choose the set of OpenVPN configuration files you wish to install".to_string()
}

fn variants() -> Vec<Self> {
ConfigType::iter().collect()
}
fn description(&self) -> Option<String> {
Some(
match self {
Self::UDP443 => "Protocol: UDP, Port: 443, Entry IP: 1",
Self::TCP443 => "Protocol: TCP, Port: 443, Entry IP: 1",
}
.to_string(),
)
}
}
19 changes: 11 additions & 8 deletions src/providers/azirevpn/openvpn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ impl OpenVpnProvider for AzireVPN {
self.request_userpass()
}

fn auth_file_path(&self) -> anyhow::Result<PathBuf> {
Ok(self.openvpn_dir()?.join("auth.txt"))
fn auth_file_path(&self) -> anyhow::Result<Option<PathBuf>> {
Ok(Some(self.openvpn_dir()?.join("auth.txt")))
}

fn create_openvpn_config(&self) -> anyhow::Result<()> {
Expand Down Expand Up @@ -57,12 +57,15 @@ impl OpenVpnProvider for AzireVPN {

// Write OpenVPN credentials file
let (user, pass) = self.prompt_for_auth()?;
let mut outfile = File::create(self.auth_file_path()?)?;
write!(outfile, "{}\n{}", user, pass)?;
info!(
"AzireVPN OpenVPN config written to {}",
openvpn_dir.display()
);
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!(
"AzireVPN OpenVPN config written to {}",
openvpn_dir.display()
);
}
Ok(())
}
}
13 changes: 8 additions & 5 deletions src/providers/hma/openvpn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ impl OpenVpnProvider for HMA {
Ok((username.trim().to_string(), password.trim().to_string()))
}

fn auth_file_path(&self) -> anyhow::Result<PathBuf> {
Ok(self.openvpn_dir()?.join("auth.txt"))
fn auth_file_path(&self) -> anyhow::Result<Option<PathBuf>> {
Ok(Some(self.openvpn_dir()?.join("auth.txt")))
}

fn create_openvpn_config(&self) -> anyhow::Result<()> {
Expand Down Expand Up @@ -99,9 +99,12 @@ impl OpenVpnProvider for HMA {

// Write OpenVPN credentials file
let (user, pass) = self.prompt_for_auth()?;
let mut outfile = File::create(self.auth_file_path()?)?;
write!(outfile, "{}\n{}", user, pass)?;
info!("HMA OpenVPN config written to {}", openvpn_dir.display());
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!("HMA OpenVPN config written to {}", openvpn_dir.display());
}
Ok(())
}
}
Expand Down
13 changes: 8 additions & 5 deletions src/providers/ivpn/openvpn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ impl OpenVpnProvider for IVPN {
Ok((username, "password".to_string()))
}

fn auth_file_path(&self) -> anyhow::Result<PathBuf> {
Ok(self.openvpn_dir()?.join("auth.txt"))
fn auth_file_path(&self) -> anyhow::Result<Option<PathBuf>> {
Ok(Some(self.openvpn_dir()?.join("auth.txt")))
}

fn create_openvpn_config(&self) -> anyhow::Result<()> {
Expand Down Expand Up @@ -122,9 +122,12 @@ impl OpenVpnProvider for IVPN {

// Write OpenVPN credentials file
let (user, pass) = self.prompt_for_auth()?;
let mut outfile = File::create(self.auth_file_path()?)?;
write!(outfile, "{}\n{}", user, pass)?;
info!("IVPN OpenVPN config written to {}", openvpn_dir.display());
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!("IVPN OpenVPN config written to {}", openvpn_dir.display());
}
Ok(())
}
}
6 changes: 5 additions & 1 deletion src/providers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod airvpn;
mod azirevpn;
mod hma;
mod ivpn;
Expand Down Expand Up @@ -38,6 +39,7 @@ pub enum VpnProvider {
ProtonVPN,
MozillaVPN,
AzireVPN,
AirVPN,
IVPN,
NordVPN,
HMA,
Expand All @@ -55,6 +57,7 @@ impl VpnProvider {
Self::ProtonVPN => Box::new(protonvpn::ProtonVPN {}),
Self::MozillaVPN => Box::new(mozilla::MozillaVPN {}),
Self::AzireVPN => Box::new(azirevpn::AzireVPN {}),
Self::AirVPN => Box::new(airvpn::AirVPN {}),
Self::IVPN => Box::new(ivpn::IVPN {}),
Self::NordVPN => Box::new(nordvpn::NordVPN {}),
Self::HMA => Box::new(hma::HMA {}),
Expand All @@ -69,6 +72,7 @@ impl VpnProvider {
Self::TigerVPN => Ok(Box::new(tigervpn::TigerVPN {})),
Self::ProtonVPN => Ok(Box::new(protonvpn::ProtonVPN {})),
Self::AzireVPN => Ok(Box::new(azirevpn::AzireVPN {})),
Self::AirVPN => Ok(Box::new(airvpn::AirVPN {})),
Self::IVPN => Ok(Box::new(ivpn::IVPN {})),
Self::NordVPN => Ok(Box::new(nordvpn::NordVPN {})),
Self::HMA => Ok(Box::new(hma::HMA {})),
Expand Down Expand Up @@ -124,7 +128,7 @@ pub trait OpenVpnProvider: Provider {
fn create_openvpn_config(&self) -> anyhow::Result<()>;
fn provider_dns(&self) -> Option<Vec<IpAddr>>;
fn prompt_for_auth(&self) -> anyhow::Result<(String, String)>;
fn auth_file_path(&self) -> anyhow::Result<PathBuf>;
fn auth_file_path(&self) -> anyhow::Result<Option<PathBuf>>;

fn openvpn_dir(&self) -> anyhow::Result<PathBuf> {
Ok(self.provider_dir()?.join("openvpn"))
Expand Down
11 changes: 7 additions & 4 deletions src/providers/mullvad/openvpn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ impl OpenVpnProvider for Mullvad {
Ok((username, "m".to_string()))
}

fn auth_file_path(&self) -> anyhow::Result<PathBuf> {
Ok(self.openvpn_dir()?.join("mullvad_userpass.txt"))
fn auth_file_path(&self) -> anyhow::Result<Option<PathBuf>> {
Ok(Some(self.openvpn_dir()?.join("mullvad_userpass.txt")))
}

fn create_openvpn_config(&self) -> anyhow::Result<()> {
Expand Down Expand Up @@ -179,8 +179,11 @@ impl OpenVpnProvider for Mullvad {

// Write OpenVPN credentials file
let (user, pass) = self.prompt_for_auth()?;
let mut outfile = File::create(self.auth_file_path()?)?;
write!(outfile, "{}\n{}", user, pass)?;
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)?;
}
Ok(())
}
}
Expand Down
Loading

0 comments on commit 9336b78

Please sign in to comment.