Skip to content

Commit

Permalink
feat: mvp
Browse files Browse the repository at this point in the history
  • Loading branch information
norskeld committed Feb 25, 2024
1 parent 326fdec commit 6baeb6d
Show file tree
Hide file tree
Showing 8 changed files with 548 additions and 2 deletions.
10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
anyhow = "1.0.80"
clap = { version = "4.5.1", features = ["derive"] }
indicatif = "0.17.8"
reqwest = { version = "0.11.24", features = ["json"] }
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.114"
thiserror = "1.0.57"
tokio = { version = "1.36.0", features = ["full"] }

[profile.release]
lto = "thin"
panic = "abort"
Expand Down
81 changes: 81 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use std::time::Duration;

use clap::builder::PossibleValue;
use clap::{Parser, ValueEnum};
use indicatif::{ProgressBar, ProgressStyle};

use crate::relays::Protocol;

#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct Cli {
/// Filter servers by used protocol.
#[arg(short, long, value_parser = clap::value_parser!(Protocol))]
pub protocol: Option<Protocol>,

/// Filter servers by maximum physical distance (in km).
#[arg(short, long, default_value_t = 500)]
pub distance: usize,

/// Filter servers by maximum rtt (in ms).
#[arg(short, long)]
pub rtt: Option<usize>,

/// How many pings to send for each relay.
#[arg(short, long, default_value_t = 4)]
pub count: usize,

/// Specify ping timeout (in ms).
#[arg(long, default_value_t = 750)]
pub timeout: u64,

/// Specify the latitude.
#[arg(long, requires = "longitude")]
pub latitude: Option<f64>,

/// Specify the longitude.
#[arg(long, requires = "latitude")]
pub longitude: Option<f64>,
}

impl ValueEnum for Protocol {
fn value_variants<'a>() -> &'a [Self] {
&[Self::OpenVPN, Self::WireGuard]
}

fn to_possible_value(&self) -> Option<PossibleValue> {
Some(match self {
| Protocol::OpenVPN => PossibleValue::new("openvpn"),
| Protocol::WireGuard => PossibleValue::new("wireguard"),
})
}
}

/// Small wrapper around the `indicatif` spinner.
pub struct Spinner {
spinner: ProgressBar,
}

impl Spinner {
pub fn new() -> Self {

Check failure on line 60 in src/cli.rs

View workflow job for this annotation

GitHub Actions / cargo clippy

you should consider adding a `Default` implementation for `Spinner`
let style = ProgressStyle::default_spinner()
.tick_strings(&[" ", "· ", "·· ", "···", " ··", " ·", " "]);

let spinner = ProgressBar::new_spinner();

spinner.set_style(style);
spinner.enable_steady_tick(Duration::from_millis(150));

Self { spinner }
}

/// Sets the message of the spinner.
pub fn set_message(&self, message: &'static str) {
self.spinner.set_message(message);
}

/// Stops the spinner and clears the message.
pub fn stop(&self) {
self.spinner.finish_and_clear();
}
}
73 changes: 73 additions & 0 deletions src/coord.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum CoordError {
#[error("Failed to fetch coordinates")]
FetchFailed(reqwest::Error),
#[error("Failed to parse response")]
ParseResponseFailed(reqwest::Error),
#[error("Failed to get latitude and longitude from the response")]
GetCoordsFailed,
}

/// Represents a point on Earth.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Coord {
latitude: f64,
longitude: f64,
}

impl Coord {
/// Constructs a new `Coord`.
pub fn new(latitude: f64, longitude: f64) -> Self {
Self {
latitude,
longitude,
}
}

/// Fetches the current coordinates using the Mullvad API.
pub async fn fetch() -> Result<Self, CoordError> {
let response = reqwest::get("https://am.i.mullvad.net/json")
.await
.map_err(CoordError::FetchFailed)?;

let data = response
.json::<Value>()
.await
.map_err(CoordError::ParseResponseFailed)?;

let lat = data["latitude"].as_f64();
let lon = data["longitude"].as_f64();

lat
.zip(lon)
.map(|(latitude, longitude)| Self::new(latitude, longitude))
.ok_or_else(|| CoordError::GetCoordsFailed)
}

/// Finds the distance (in meters) between two coordinates using the haversine formula.
pub fn distance_to(&self, other: &Self) -> f64 {
// Earth radius in meters. This is *average*, since Earth is not a sphere, but a spheroid.
const R: f64 = 6_371_000f64;

// Turn latitudes and longitudes into radians.
let phi1 = self.latitude.to_radians();
let phi2 = other.latitude.to_radians();
let lam1 = self.longitude.to_radians();
let lam2 = other.longitude.to_radians();

// The haversine function. Computes half a versine of the given angle `theta`.
let haversine = |theta: f64| (1.0 - theta.cos()) / 2.0;

let hav_delta_phi = haversine(phi2 - phi1);
let hav_delta_lam = phi1.cos() * phi2.cos() * haversine(lam2 - lam1);
let hav_delta = hav_delta_phi + hav_delta_lam;

let distance = (2.0 * R * hav_delta.sqrt().asin() * 1_000.0).round() / 1_000.0;

distance

Check failure on line 71 in src/coord.rs

View workflow job for this annotation

GitHub Actions / cargo clippy

returning the result of a `let` binding from a block
}
}
65 changes: 65 additions & 0 deletions src/filters.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use std::fmt::Debug;

use crate::coord::Coord;
use crate::relays::{Protocol, Relay};

#[derive(PartialEq)]
pub enum FilterStage {
/// Such filters apply when loading them from the relays file.
Load,
/// Such filters apply after pinging relays.
Ping,
}

/// Filter trait to dynamically dispatch filters.
pub trait Filter: Debug {
/// Returns the stage of the filter.
fn stage(&self) -> FilterStage;

/// Filter predicate.
fn matches(&self, relay: &Relay) -> bool;
}

#[derive(Debug)]
pub struct FilterByDistance {
user: Coord,
distance: f64,
}

impl FilterByDistance {
pub fn new(user: Coord, distance: f64) -> Self {
Self { user, distance }
}
}

impl Filter for FilterByDistance {
fn stage(&self) -> FilterStage {
FilterStage::Load
}

fn matches(&self, relay: &Relay) -> bool {
(relay.coord.distance_to(&self.user) / 1_000.0) < self.distance
}
}

#[derive(Debug)]
pub struct FilterByProtocol(Option<Protocol>);

impl FilterByProtocol {
pub fn new(protocol: Option<Protocol>) -> Self {
Self(protocol)
}
}

impl Filter for FilterByProtocol {
fn stage(&self) -> FilterStage {
FilterStage::Load
}

fn matches(&self, relay: &Relay) -> bool {
self
.0
.as_ref()
.map_or(true, |use_protocol| relay.protocol == *use_protocol)
}
}
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub mod cli;
pub mod coord;
pub mod filters;
pub mod pinger;
pub mod relays;
71 changes: 69 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,70 @@
fn main() {
println!("Hello, world!");
use std::thread;
use std::time::Duration;

use clap::Parser;
use pingmole::cli::{Cli, Spinner};
use pingmole::coord::Coord;
use pingmole::filters::{FilterByDistance, FilterByProtocol};
use pingmole::pinger::RelayPinger;
use pingmole::relays::RelaysLoader;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
let spinner = Spinner::new();

// -----------------------------------------------------------------------------------------------
// 1. Get current location, either via arguments or via Mullvad API.
spinner.set_message("Getting current location");

let user = match cli.latitude.zip(cli.longitude) {
| Some((latitude, longitude)) => Coord::new(latitude, longitude),
| None => Coord::fetch().await?,
};

thread::sleep(std::time::Duration::from_secs(1));

// -----------------------------------------------------------------------------------------------
// 2. Load relays from file and filter them.
spinner.set_message("Loading relays");

let loader = RelaysLoader::new(
RelaysLoader::resolve_path()?,
vec![
Box::new(FilterByDistance::new(user, cli.distance as f64)),
Box::new(FilterByProtocol::new(cli.protocol)),
],
);

let relays = loader.load()?;

thread::sleep(std::time::Duration::from_secs(1));

// -----------------------------------------------------------------------------------------------
// 3. Pinging relays.
spinner.set_message("Pinging relays");

let mut tasks = Vec::new();
let mut timings = Vec::new();

for relay in relays {
let mut pinger = RelayPinger::new(relay);

pinger.set_count(cli.count);
pinger.set_timeout(Duration::from_millis(cli.timeout));

tasks.push(tokio::spawn(pinger.execute()));
}

for task in tasks {
timings.push(task.await?);
}

// -----------------------------------------------------------------------------------------------
// 4. Print results.
spinner.stop();

dbg!(timings);

Ok(())
}
Loading

0 comments on commit 6baeb6d

Please sign in to comment.