mod wireguard;
use std::fmt::Display;
use std::fs::File;
use std::io::{BufRead, BufReader};
use super::{Input, Provider, ShadowsocksProvider, UiClient, WireguardProvider};
use crate::config::vpn::Protocol;
use anyhow::anyhow;
use log::info;
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct AccessToken {
access_token: String,
}
#[derive(Deserialize, Debug, Clone)]
struct UserInfo {
expiry: String,
max_devices: u8,
can_add_devices: bool,
}
#[derive(Deserialize, Debug, Clone)]
struct Device {
name: String,
pubkey: String,
created: String,
ipv4_address: String,
ipv6_address: String,
}
impl Display for Device {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}: {} (created: {})",
self.name, self.pubkey, self.created
)
}
}
pub struct Mullvad {}
fn parse_mullvad_username(raw: &str) -> Option<String> {
let mut username = raw.trim().to_string();
username.retain(|c| !c.is_whitespace() && c.is_ascii_digit());
if username.len() == 16 {
Some(username)
} else {
None
}
}
#[allow(clippy::ptr_arg)] fn validate_mullvad_username(username: &String) -> Result<(), String> {
parse_mullvad_username(username)
.map(|_| ())
.ok_or_else(|| "Mullvad account number should be 16 digits!".to_string())
}
impl Provider for Mullvad {
fn alias(&self) -> String {
"mv".to_string()
}
fn alias_2char(&self) -> String {
"mv".to_string()
}
fn default_protocol(&self) -> Protocol {
Protocol::Wireguard
}
}
impl Mullvad {
fn read_cached_username(&self) -> Option<String> {
let auth_path = self
.provider_dir()
.ok()?
.join("openvpn/mullvad_userpass.txt");
let file = File::open(auth_path).ok()?;
let reader = BufReader::new(file);
let first_line = reader.lines().next()?.ok()?;
parse_mullvad_username(&first_line)
}
fn request_mullvad_username(&self, uiclient: &dyn UiClient) -> anyhow::Result<String> {
if let Some(cached_username) = self.read_cached_username() {
info!(
"Using cached Mullvad account number: {}...{}",
&cached_username[..4],
&cached_username[12..]
);
return Ok(cached_username);
}
let username = uiclient.get_input(Input {
prompt: "Mullvad account number".to_string(),
validator: Some(Box::new(validate_mullvad_username)),
})?;
parse_mullvad_username(&username).ok_or_else(|| {
anyhow!(
"Mullvad account number should be 16 digits!, parsed: {}",
username
)
})
}
}
impl ShadowsocksProvider for Mullvad {
fn password(&self) -> String {
"mullvad".to_string()
}
fn encrypt_method(&self) -> String {
"aes-256-gcm".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_mullvad_username_valid() {
assert_eq!(
parse_mullvad_username("1234567890123456"),
Some("1234567890123456".to_string())
);
}
#[test]
fn test_parse_mullvad_username_with_spaces() {
assert_eq!(
parse_mullvad_username("1234 5678 9012 3456"),
Some("1234567890123456".to_string())
);
}
#[test]
fn test_parse_mullvad_username_with_whitespace() {
assert_eq!(
parse_mullvad_username(" 1234567890123456 \n"),
Some("1234567890123456".to_string())
);
}
#[test]
fn test_parse_mullvad_username_too_short() {
assert_eq!(parse_mullvad_username("12345678901234"), None);
}
#[test]
fn test_parse_mullvad_username_too_long() {
assert_eq!(parse_mullvad_username("12345678901234567890"), None);
}
#[test]
fn test_parse_mullvad_username_with_letters() {
assert_eq!(parse_mullvad_username("1234abcd56789012"), None);
}
#[test]
fn test_parse_mullvad_username_empty() {
assert_eq!(parse_mullvad_username(""), None);
}
#[test]
fn test_validate_mullvad_username_valid() {
assert!(validate_mullvad_username(&"1234567890123456".to_string()).is_ok());
}
#[test]
fn test_validate_mullvad_username_with_spaces() {
assert!(validate_mullvad_username(&"1234 5678 9012 3456".to_string()).is_ok());
}
#[test]
fn test_validate_mullvad_username_too_short() {
let result = validate_mullvad_username(&"123456789012345".to_string());
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
"Mullvad account number should be 16 digits!"
);
}
#[test]
fn test_validate_mullvad_username_too_long() {
let result = validate_mullvad_username(&"12345678901234567".to_string());
assert!(result.is_err());
}
#[test]
fn test_validate_mullvad_username_empty() {
let result = validate_mullvad_username(&"".to_string());
assert!(result.is_err());
}
#[test]
fn test_validate_mullvad_username_non_digits() {
let result = validate_mullvad_username(&"abcdefghijklmnop".to_string());
assert!(result.is_err());
}
#[test]
fn test_provider_alias() {
let mullvad = Mullvad {};
assert_eq!(mullvad.alias(), "mv");
assert_eq!(mullvad.alias_2char(), "mv");
}
#[test]
fn test_default_protocol() {
let mullvad = Mullvad {};
assert_eq!(mullvad.default_protocol(), Protocol::Wireguard);
}
#[test]
fn test_shadowsocks_provider() {
let mullvad = Mullvad {};
assert_eq!(mullvad.password(), "mullvad");
assert_eq!(mullvad.encrypt_method(), "aes-256-gcm");
}
}