diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..24bc185 --- /dev/null +++ b/src/api.rs @@ -0,0 +1,61 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use log::debug; +use serde::{Deserialize, Serialize}; + +const BASE_URL: &str = "https://api.azirevpn.com/v2"; + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct Locations { + pub status: String, + pub locations: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct Location { + pub name: String, + pub city: String, + pub country: String, + pub iso: String, + pub pool: String, + pub pubkey: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct Addresses { + pub status: String, + pub ipv4: WireguardConfigIpv4, + pub ipv6: WireguardConfigIpv6, + pub dns: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct WireguardConfigIpv4 { + pub address: Ipv4Addr, + pub netmask: u8, +} +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct WireguardConfigIpv6 { + pub address: Ipv6Addr, + pub netmask: u8, +} + +pub(crate) fn get_locations() -> anyhow::Result { + let url = format!("{}/locations", BASE_URL); + let response: Locations = ureq::get(&url).call()?.into_json()?; + debug!("response = {:?}", &response); + Ok(response) +} + +pub(crate) fn add_ip(username: &str, token: &str, public_key: &str) -> anyhow::Result { + let url = format!("{}/ip/add", BASE_URL); + let response: Addresses = ureq::post(&url) + .send_form(&[ + ("username", username), + ("token", token), + ("key", public_key), + ])? + .into_json()?; + debug!("response = {:?}", &response); + Ok(response) +} diff --git a/src/dirs.rs b/src/dirs.rs new file mode 100644 index 0000000..03a38b1 --- /dev/null +++ b/src/dirs.rs @@ -0,0 +1,18 @@ +use directories::ProjectDirs; + +use once_cell::sync::OnceCell; +use std::path::PathBuf; + +const QUALIFIER: &str = "com"; +const ORGANIZATION: &str = "Ero-sennin"; +const APPLICATION: &str = "AzireVPN"; + +static PROJECT_DIRS: OnceCell = OnceCell::new(); + +pub(crate) fn get_data_dir() -> PathBuf { + let project_dirs = PROJECT_DIRS.get_or_init(|| { + ProjectDirs::from(QUALIFIER, ORGANIZATION, APPLICATION) + .expect("cannot get project data dir") + }); + project_dirs.data_dir().to_path_buf() +} diff --git a/src/keys.rs b/src/keys.rs new file mode 100644 index 0000000..a3a2b84 --- /dev/null +++ b/src/keys.rs @@ -0,0 +1,67 @@ +use gethostname::gethostname; +use log::debug; +use std::io::Write; + +use std::path::PathBuf; +use std::process; + +use crate::dirs::get_data_dir; + +#[derive(Debug)] +pub(crate) struct WireguardKeyPair { + pub public_key: String, + pub private_key: String, +} + +pub(crate) fn get_keys(machine: Option<&PathBuf>) -> Result { + let hostname: PathBuf; + let machine_subdir: &PathBuf = if let Some(machine) = machine { + machine + } else { + hostname = PathBuf::from(gethostname()); + &hostname + }; + let key_path = get_data_dir().join("keys").join(machine_subdir); + debug!("key path = {:?}", &key_path); + std::fs::create_dir_all(&key_path)?; + let private_key_path = key_path.join("key"); + let private_key = if private_key_path.is_file() { + std::fs::read_to_string(private_key_path)? + } else { + let key = generate_private_key()?; + std::fs::write(private_key_path, key.as_bytes())?; + key + }; + let public_key_path = key_path.join("pubkey"); + let public_key = if public_key_path.is_file() { + std::fs::read_to_string(public_key_path)? + } else { + let key = generate_public_key(&private_key)?; + std::fs::write(public_key_path, key.as_bytes())?; + key + }; + Ok(WireguardKeyPair { + private_key, + public_key, + }) +} + +fn generate_private_key() -> anyhow::Result { + let privkey = process::Command::new("wg").arg("genkey").output()?.stdout; + Ok(String::from_utf8(privkey)?.trim_end().to_string()) +} + +fn generate_public_key(private_key: &str) -> anyhow::Result { + let mut pubkey_cmd = process::Command::new("wg") + .arg("pubkey") + .stdin(process::Stdio::piped()) + .stdout(process::Stdio::piped()) + .spawn()?; + pubkey_cmd + .stdin + .as_mut() + .expect("no stdin") + .write_all(private_key.as_bytes())?; + let pubkey = pubkey_cmd.wait_with_output()?.stdout; + Ok(String::from_utf8(pubkey)?.trim_end().to_string()) +} diff --git a/src/main.rs b/src/main.rs index d95f891..f93dc76 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,15 @@ +mod api; +use std::net::IpAddr; +mod dirs; +mod keys; + use std::io::Write; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::path::PathBuf; -use std::process; use clap::{Parser, Subcommand}; -use directories::ProjectDirs; -use gethostname::gethostname; use log::debug; -use once_cell::sync::OnceCell; -use serde::{Deserialize, Serialize}; -const BASE_URL: &str = "https://api.azirevpn.com/v2"; - -const QUALIFIER: &str = "com"; -const ORGANIZATION: &str = "Ero-sennin"; -const APPLICATION: &str = "AzireVPN"; - -static PROJECT_DIRS: OnceCell = OnceCell::new(); +use crate::keys::{get_keys, WireguardKeyPair}; /// AzireVPN client #[derive(Parser, Debug)] @@ -55,47 +48,6 @@ enum Command { Config(ConfigOpts), } -#[derive(Serialize, Deserialize, Debug)] -struct Locations { - status: String, - locations: Vec, -} - -#[derive(Serialize, Deserialize, Debug)] -struct Location { - name: String, - city: String, - country: String, - iso: String, - pool: String, - pubkey: String, -} - -#[derive(Serialize, Deserialize, Debug)] -struct WireguardConfig { - status: String, - ipv4: WireguardConfigIpv4, - ipv6: WireguardConfigIpv6, - dns: Vec, -} - -#[derive(Serialize, Deserialize, Debug)] -struct WireguardConfigIpv4 { - address: Ipv4Addr, - netmask: u8, -} -#[derive(Serialize, Deserialize, Debug)] -struct WireguardConfigIpv6 { - address: Ipv6Addr, - netmask: u8, -} - -#[derive(Debug)] -struct WireguardKeyPair { - public_key: String, - private_key: String, -} - fn main() -> Result<(), anyhow::Error> { env_logger::init(); let opts = Opts::parse(); @@ -106,16 +58,8 @@ fn main() -> Result<(), anyhow::Error> { Ok(()) } -fn get_data_dir() -> PathBuf { - let project_dirs = PROJECT_DIRS.get_or_init(|| { - ProjectDirs::from(QUALIFIER, ORGANIZATION, APPLICATION) - .expect("cannot get project data dir") - }); - project_dirs.data_dir().to_path_buf() -} - fn list_locations(opts: &Opts) -> Result<(), anyhow::Error> { - let locations: Locations = get_locations()?; + let locations = api::get_locations()?; if opts.json { println!("{}", serde_json::to_string(&locations)?); } else { @@ -135,7 +79,7 @@ fn list_locations(opts: &Opts) -> Result<(), anyhow::Error> { } fn get_config(_opts: &Opts, config_opts: &ConfigOpts) -> Result<(), anyhow::Error> { - let locations = get_locations()?; + let locations = api::get_locations()?; let location = locations .locations .iter() @@ -144,22 +88,13 @@ fn get_config(_opts: &Opts, config_opts: &ConfigOpts) -> Result<(), anyhow::Erro debug!("location = {:?}", &location); let keys = get_keys(config_opts.machine.as_ref())?; debug!("keys = {:?}", &keys); - let url = format!("{}/ip/add", BASE_URL); - let config: WireguardConfig = ureq::post(&url) - .send_form(&[ - ("username", &config_opts.username), - ("token", &config_opts.token), - ("key", &keys.public_key), - ])? - .into_json()?; - debug!("response = {:?}", &config); - debug!("config = {:?}", &config); + let addresses = api::add_ip(&config_opts.username, &config_opts.token, &keys.public_key)?; write_config( &mut std::io::stdout().lock(), config_opts, location, - &config, + &addresses, &keys, ) } @@ -167,8 +102,8 @@ fn get_config(_opts: &Opts, config_opts: &ConfigOpts) -> Result<(), anyhow::Erro fn write_config( output: &mut dyn Write, config_opts: &ConfigOpts, - location: &Location, - config: &WireguardConfig, + location: &api::Location, + config: &api::Addresses, keys: &WireguardKeyPair, ) -> Result<(), anyhow::Error> { writeln!(output, "[Interface]")?; @@ -193,7 +128,6 @@ fn write_config( writeln!(output, "[Peer]")?; writeln!(output, "PublicKey = {}", &location.pubkey)?; - writeln!(output, "Endpoint = {}:51820", &location.pool)?; let allowed_ips: &[&str] = if config_opts.no_ipv6 { @@ -221,62 +155,3 @@ where writeln!(output)?; Ok(()) } - -fn get_locations() -> Result { - let url = format!("{}/locations", BASE_URL); - let locations: Locations = ureq::get(&url).call()?.into_json()?; - Ok(locations) -} - -fn get_keys(machine: Option<&PathBuf>) -> Result { - let hostname: PathBuf; - let machine_subdir: &PathBuf = if let Some(machine) = machine { - machine - } else { - hostname = PathBuf::from(gethostname()); - &hostname - }; - let key_path = get_data_dir().join("keys").join(machine_subdir); - debug!("key path = {:?}", &key_path); - std::fs::create_dir_all(&key_path)?; - let private_key_path = key_path.join("key"); - let private_key = if private_key_path.is_file() { - std::fs::read_to_string(private_key_path)? - } else { - let key = generate_private_key()?; - std::fs::write(private_key_path, key.as_bytes())?; - key - }; - let public_key_path = key_path.join("pubkey"); - let public_key = if public_key_path.is_file() { - std::fs::read_to_string(public_key_path)? - } else { - let key = generate_public_key(&private_key)?; - std::fs::write(public_key_path, key.as_bytes())?; - key - }; - Ok(WireguardKeyPair { - private_key, - public_key, - }) -} - -fn generate_private_key() -> anyhow::Result { - let privkey = process::Command::new("wg").arg("genkey").output()?.stdout; - Ok(String::from_utf8(privkey)?.trim_end().to_string()) -} - -fn generate_public_key(private_key: &str) -> anyhow::Result { - let mut pubkey_cmd = process::Command::new("wg") - .arg("pubkey") - .stdin(process::Stdio::piped()) - .stdout(process::Stdio::piped()) - .spawn()?; - pubkey_cmd - .stdin - .as_mut() - .expect("no stdin") - .write_all(private_key.as_bytes())?; - let pubkey = pubkey_cmd.wait_with_output()?.stdout; - Ok(String::from_utf8(pubkey)?.trim_end().to_string()) -}