mod wireguard;
use super::{Provider, WireguardProvider};
use crate::config::vpn::Protocol;
use base64::Engine;
use reqwest::blocking::Client;
use serde::Deserialize;
#[derive(serde::Serialize)]
struct AccessTokenRequest<'a> {
code: &'a str,
code_verifier: &'a str,
}
#[derive(Deserialize, Debug)]
struct User {
devices: Vec<Device>,
}
#[derive(Deserialize)]
struct Login {
user: User,
token: String,
}
#[derive(Deserialize, Debug, Clone)]
struct Device {
name: String,
pubkey: String,
ipv4_address: ipnet::Ipv4Net,
ipv6_address: ipnet::Ipv6Net,
}
#[allow(dead_code)]
#[derive(Deserialize, Debug)]
struct Error {
errno: u32,
error: String,
}
impl std::fmt::Display for Device {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}: {}", self.name, self.pubkey,)
}
}
pub struct MozillaVPN {}
impl Provider for MozillaVPN {
fn alias(&self) -> String {
"mozilla".to_string()
}
fn alias_2char(&self) -> String {
"mz".to_string()
}
fn default_protocol(&self) -> Protocol {
Protocol::Wireguard
}
}
impl MozillaVPN {
const V2_URL: &'static str = "https://vpn.mozilla.org/api/v2";
fn base_url(&self) -> &'static str {
"https://vpn.mozilla.org/api/v1"
}
fn get_login(&self, client: &Client) -> anyhow::Result<Login> {
use base64::prelude::BASE64_URL_SAFE_NO_PAD;
use rand::Rng;
use sha2::Digest;
let mut code_verifier_random = [0u8; 32];
let mut rng = rand::rng();
rng.fill_bytes(&mut code_verifier_random);
let mut code_verifier = [0u8; 43];
BASE64_URL_SAFE_NO_PAD.encode_slice(code_verifier_random, &mut code_verifier)?;
let mut code_challenge = String::with_capacity(43);
BASE64_URL_SAFE_NO_PAD
.encode_string(sha2::Sha256::digest(code_verifier), &mut code_challenge);
use tiny_http::{Method, Server};
let server = Server::http("127.0.0.1:0").unwrap();
let login_url = format!(
"{}/vpn/login/linux?code_challenge_method=S256&code_challenge={}&port={}",
Self::V2_URL,
code_challenge,
server
.server_addr()
.to_ip()
.expect("Failed to get SocketAddr")
.port()
);
eprint!("Please visit {login_url}");
match webbrowser::open(&login_url) {
Ok(_) => eprint!(" Link opened in browser."),
Err(_) => eprint!(" Failed to open link in browser, please visit it manually."),
}
eprintln!();
let code;
let code_url_regex = regex::Regex::new(r"\A/\?code=([0-9a-f]{80})\z").unwrap();
for request in server.incoming_requests() {
if let Some(caps) = code_url_regex.captures(request.url())
&& *request.method() == Method::Get
{
code = caps.get(1).unwrap();
let response = client
.post(format!("{}/vpn/login/verify", Self::V2_URL))
.header("User-Agent", "Why do you need a user agent???")
.json(&AccessTokenRequest {
code: code.as_str(),
code_verifier: std::str::from_utf8(&code_verifier).unwrap(),
})
.send()
.unwrap();
return Ok(response.json::<Login>().unwrap());
}
}
unreachable!("Server closed without receiving code")
}
}
fn validate_hostname(hostname: &str) -> bool {
!hostname.is_empty()
&& hostname
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_hostname_valid() {
assert!(validate_hostname("my-device"));
assert!(validate_hostname("device123"));
assert!(validate_hostname("MyDevice"));
assert!(validate_hostname("a"));
assert!(validate_hostname("device-name-123"));
}
#[test]
fn test_validate_hostname_invalid() {
assert!(!validate_hostname("my_device")); assert!(!validate_hostname("my device")); assert!(!validate_hostname("device@home")); assert!(!validate_hostname("device.local")); assert!(!validate_hostname("déjà")); }
#[test]
fn test_validate_hostname_empty() {
assert!(!validate_hostname(""));
}
#[test]
fn test_provider_alias() {
let mozilla = MozillaVPN {};
assert_eq!(mozilla.alias(), "mozilla");
assert_eq!(mozilla.alias_2char(), "mz");
}
#[test]
fn test_default_protocol() {
let mozilla = MozillaVPN {};
assert_eq!(mozilla.default_protocol(), Protocol::Wireguard);
}
}