Compare commits
5 commits
a3cf82135f
...
e846e65cab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e846e65cab | ||
|
|
0fa76efc9d | ||
|
|
fa0401b43c | ||
|
|
e3eeea4b5d | ||
|
|
0ff5d01c94 |
9 changed files with 138 additions and 67 deletions
|
|
@ -1,3 +1,5 @@
|
||||||
IPINFO_TOKEN=<your ipinfo token>
|
IPINFO_TOKEN=<your ipinfo token>
|
||||||
|
SERVER_LATITUDE=1
|
||||||
|
SERVER_LONGITUDE=1
|
||||||
ROCKET_PORT=8000
|
ROCKET_PORT=8000
|
||||||
ROCKET_ADDRESS=0.0.0.0
|
ROCKET_ADDRESS=0.0.0.0
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
[package]
|
[package]
|
||||||
name = "speedtest-rs"
|
name = "speedtest-rs"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
|
license = "LGPL-3.0+"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rocket = { version = "0.5.0-rc.1", features = ["json"] }
|
rocket = { version = "0.5.0-rc.1", features = ["json"] }
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ Supported by all Librespeed frontends, though some features are missing (see bel
|
||||||
- [x] IP Address, ISP
|
- [x] IP Address, ISP
|
||||||
- [x] Multiple Points of Test (optional)
|
- [x] Multiple Points of Test (optional)
|
||||||
- [x] Compatible with PHP frontend predefined endpoints (with `.php` suffixes)
|
- [x] Compatible with PHP frontend predefined endpoints (with `.php` suffixes)
|
||||||
- [ ] Distance from server (optional)
|
- [x] Distance from server (optional)
|
||||||
- [ ] Telemetry (optional)
|
- [ ] Telemetry (optional)
|
||||||
- [ ] Results sharing (optional)
|
- [ ] Results sharing (optional)
|
||||||
- [ ] [Proxy Protocol](https://www.haproxy.org/download/2.3/doc/proxy-protocol.txt)?
|
- [ ] [Proxy Protocol](https://www.haproxy.org/download/2.3/doc/proxy-protocol.txt)?
|
||||||
|
|
@ -55,6 +55,10 @@ cargo build --release
|
||||||
```sh
|
```sh
|
||||||
# your ipinfo.io API token
|
# your ipinfo.io API token
|
||||||
IPINFO_TOKEN=
|
IPINFO_TOKEN=
|
||||||
|
# your server's latitude
|
||||||
|
SERVER_LATITUDE=1
|
||||||
|
# your server's longitude
|
||||||
|
SERVER_LONGITUDE=1
|
||||||
# the port to bind to
|
# the port to bind to
|
||||||
ROCKET_PORT=8000
|
ROCKET_PORT=8000
|
||||||
# the bind address (0.0.0.0 is all interfaces)
|
# the bind address (0.0.0.0 is all interfaces)
|
||||||
|
|
|
||||||
12
src/empty.rs
12
src/empty.rs
|
|
@ -11,10 +11,9 @@ pub struct EmptyResponder {
|
||||||
|
|
||||||
#[post("/empty", data = "<data>")]
|
#[post("/empty", data = "<data>")]
|
||||||
pub async fn empty(data: Data<'_>) -> EmptyResponder {
|
pub async fn empty(data: Data<'_>) -> EmptyResponder {
|
||||||
match data.open(1.gigabytes()).stream_to(io::sink()).await {
|
match data.open(25.megabytes()).stream_to(io::sink()).await {
|
||||||
Ok(_) => print!(""),
|
_ => {}
|
||||||
Err(_) => print!(""),
|
}
|
||||||
};
|
|
||||||
|
|
||||||
EmptyResponder {
|
EmptyResponder {
|
||||||
inner: (),
|
inner: (),
|
||||||
|
|
@ -22,11 +21,6 @@ pub async fn empty(data: Data<'_>) -> EmptyResponder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[options("/empty")]
|
|
||||||
pub async fn empty_options() -> Status {
|
|
||||||
Status::NoContent
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/backend/empty", data = "<data>")]
|
#[post("/backend/empty", data = "<data>")]
|
||||||
pub async fn backend_empty(data: Data<'_>) -> EmptyResponder {
|
pub async fn backend_empty(data: Data<'_>) -> EmptyResponder {
|
||||||
empty(data).await
|
empty(data).await
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ pub async fn garbage(opts: GarbageOptions) -> ByteStream![Vec<u8>] {
|
||||||
|
|
||||||
let chunks = match opts.ckSize {
|
let chunks = match opts.ckSize {
|
||||||
i if i > 1024 => 1024,
|
i if i > 1024 => 1024,
|
||||||
_ => opts.ckSize,
|
i => i,
|
||||||
};
|
};
|
||||||
|
|
||||||
ByteStream! {
|
ByteStream! {
|
||||||
|
|
|
||||||
|
|
@ -2,26 +2,18 @@ use ipinfo::{IpDetails, IpInfo, IpInfoConfig};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use rocket::serde::{json::Json, Serialize};
|
use rocket::serde::{json::Json, Serialize};
|
||||||
use rocket_client_addr::ClientRealAddr;
|
use rocket_client_addr::ClientRealAddr;
|
||||||
|
|
||||||
use std::env::var;
|
use std::env::var;
|
||||||
|
|
||||||
|
use crate::haversine::Units;
|
||||||
use crate::serialized_ip_info::IpDetailsDef;
|
use crate::serialized_ip_info::IpDetailsDef;
|
||||||
use crate::util::get_ip_type;
|
use crate::util::{get_client_server_distance_string, get_ip_type};
|
||||||
|
|
||||||
#[derive(FromFormField, PartialEq)]
|
|
||||||
pub enum Distance {
|
|
||||||
Km,
|
|
||||||
Mi,
|
|
||||||
Nm,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(FromForm)]
|
#[derive(FromForm)]
|
||||||
pub struct GetIPOptions {
|
pub struct GetIPOptions {
|
||||||
#[field(default = true)]
|
#[field(default = true)]
|
||||||
isp: bool,
|
isp: bool,
|
||||||
#[field(default = Distance::Km)]
|
#[field(default = Units::Kilometers)]
|
||||||
distance: Distance,
|
distance: Units,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
|
|
@ -69,6 +61,11 @@ pub async fn get_ip(client_addr: &ClientRealAddr, opts: GetIPOptions) -> Json<Ge
|
||||||
isp = format!("{}, {}", &isp, &ipinfo.country);
|
isp = format!("{}, {}", &isp, &ipinfo.country);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !ipinfo.loc.is_empty() {
|
||||||
|
let distance = get_client_server_distance_string(ipinfo.loc, opts.distance);
|
||||||
|
isp = format!("{} ({})", &isp, &distance);
|
||||||
|
}
|
||||||
|
|
||||||
result.processed_string = Some(format!("{} - {}", &result.processed_string.unwrap(), &isp));
|
result.processed_string = Some(format!("{} - {}", &result.processed_string.unwrap(), &isp));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
46
src/haversine.rs
Normal file
46
src/haversine.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Location {
|
||||||
|
pub latitude: f64,
|
||||||
|
pub longitude: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromFormField, PartialEq)]
|
||||||
|
pub enum Units {
|
||||||
|
Miles,
|
||||||
|
Kilometers,
|
||||||
|
NauticalMiles,
|
||||||
|
Mi,
|
||||||
|
Km,
|
||||||
|
Nm,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Units {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Units::Kilometers | Units::Km => write!(f, "km"),
|
||||||
|
Units::Miles | Units::Mi => write!(f, "mi"),
|
||||||
|
Units::NauticalMiles | Units::Nm => write!(f, "NM"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn distance(start: Location, end: Location, units: &Units) -> f64 {
|
||||||
|
let r = match units {
|
||||||
|
Units::Miles | Units::Km => 3958.8,
|
||||||
|
Units::Kilometers | Units::Mi => 6371.0,
|
||||||
|
Units::NauticalMiles | Units::Nm => 3440.1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let d_lat: f64 = (end.latitude - start.latitude).to_radians();
|
||||||
|
let d_lon: f64 = (end.longitude - start.longitude).to_radians();
|
||||||
|
let lat1: f64 = (start.latitude).to_radians();
|
||||||
|
let lat2: f64 = (end.latitude).to_radians();
|
||||||
|
|
||||||
|
let a: f64 = ((d_lat / 2.0).sin()) * ((d_lat / 2.0).sin())
|
||||||
|
+ ((d_lon / 2.0).sin()) * ((d_lon / 2.0).sin()) * (lat1.cos()) * (lat2.cos());
|
||||||
|
let c: f64 = 2.0 * ((a.sqrt()).atan2((1.0 - a).sqrt()));
|
||||||
|
|
||||||
|
return r * c;
|
||||||
|
}
|
||||||
34
src/main.rs
34
src/main.rs
|
|
@ -7,14 +7,14 @@ extern crate serde_with;
|
||||||
pub mod empty;
|
pub mod empty;
|
||||||
pub mod garbage;
|
pub mod garbage;
|
||||||
pub mod get_ip;
|
pub mod get_ip;
|
||||||
|
pub mod haversine;
|
||||||
pub mod serialized_ip_info;
|
pub mod serialized_ip_info;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use rocket::fs::{relative, FileServer};
|
use rocket::fs::FileServer;
|
||||||
use rocket::http::Method;
|
use rocket::http::Method;
|
||||||
use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors};
|
use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors};
|
||||||
|
|
||||||
|
|
@ -26,7 +26,7 @@ use get_ip::*;
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
|
|
||||||
let cors = setup_cors().await.unwrap();
|
let cors = setup_cors().await?;
|
||||||
let routes = routes![get_ip::get_ip, get_backend_ip_php];
|
let routes = routes![get_ip::get_ip, get_backend_ip_php];
|
||||||
|
|
||||||
let garbage_routes = routes![
|
let garbage_routes = routes![
|
||||||
|
|
@ -40,34 +40,24 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
empty::empty,
|
empty::empty,
|
||||||
backend_empty,
|
backend_empty,
|
||||||
get_empty,
|
get_empty,
|
||||||
empty_options,
|
|
||||||
empty_php,
|
empty_php,
|
||||||
backend_empty_php,
|
backend_empty_php,
|
||||||
get_backend_empty_php
|
get_backend_empty_php
|
||||||
];
|
];
|
||||||
|
|
||||||
let asset_path = relative!("assets");
|
let routes = vec![routes, garbage_routes, empty_routes].concat();
|
||||||
if Path::new(asset_path).exists() {
|
|
||||||
|
let mut rocketship = rocket::build().attach(cors).mount("/", routes);
|
||||||
|
|
||||||
|
let asset_path = std::env::current_dir().unwrap().join("assets");
|
||||||
|
if asset_path.exists() {
|
||||||
let fileserver = FileServer::from(asset_path);
|
let fileserver = FileServer::from(asset_path);
|
||||||
|
|
||||||
rocket::build()
|
rocketship = rocketship.mount("/", fileserver);
|
||||||
.attach(cors)
|
|
||||||
.mount("/", routes)
|
|
||||||
.mount("/", empty_routes)
|
|
||||||
.mount("/", garbage_routes)
|
|
||||||
.mount("/", fileserver)
|
|
||||||
.launch()
|
|
||||||
.await?;
|
|
||||||
} else {
|
|
||||||
rocket::build()
|
|
||||||
.attach(cors)
|
|
||||||
.mount("/", routes)
|
|
||||||
.mount("/", empty_routes)
|
|
||||||
.mount("/", garbage_routes)
|
|
||||||
.launch()
|
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rocketship.launch().await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
75
src/util.rs
75
src/util.rs
|
|
@ -1,27 +1,66 @@
|
||||||
|
use std::env::var;
|
||||||
|
|
||||||
|
use crate::haversine::{distance, Location, Units};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
pub fn get_ip_type(ip: &str) -> (String, bool) {
|
pub fn get_ip_type(ip: &str) -> (String, bool) {
|
||||||
let private_regex: Regex = Regex::new(r"^172\.(1[6-9]|2\d|3[01])\.").unwrap();
|
let private_regex = Regex::new(r"^172\.(1[6-9]|2\d|3[01])\.").unwrap();
|
||||||
|
let cgnat_regex = Regex::new(r"^100\.([6-9][0-9]|1[0-2][0-7])\.").unwrap();
|
||||||
|
|
||||||
let mut is_special_ip: bool = true;
|
let mut is_special_ip: bool = true;
|
||||||
let processed_string = if ip == "::1" {
|
|
||||||
format!("{} - localhost IPv6 access", &ip)
|
let processed_string = match ip {
|
||||||
} else if ip.starts_with("fe80:") {
|
n if n == "::1" => format!("{} - localhost IPv6 access", n),
|
||||||
format!("{} - link-local IPv6 access", &ip)
|
n if n.starts_with("fe80:") => format!("{} - link-local IPv6 access", n),
|
||||||
} else if ip.starts_with("127.") {
|
n if n.starts_with("127.") => format!("{} - localhost IPv4 access", n),
|
||||||
format!("{} - localhost IPv4 access", &ip)
|
n if n.starts_with("10.") => format!("{} - private IPv4 access", n),
|
||||||
} else if ip.starts_with("10.") {
|
n if private_regex.is_match(n) => format!("{} - private IPv4 access", n),
|
||||||
format!("{} - private IPv4 access", &ip)
|
n if n.starts_with("192.168.") => format!("{} - private IPv4 access", n),
|
||||||
} else if private_regex.is_match(&ip) {
|
n if n.starts_with("169.254.") => format!("{} - link-local IPv4 access", n),
|
||||||
format!("{} - private IPv4 access", &ip)
|
n if cgnat_regex.is_match(n) => format!("{} - CGNAT IPv4 access", n),
|
||||||
} else if ip.starts_with("192.168.") {
|
_ => {
|
||||||
format!("{} - private IPv4 access", &ip)
|
is_special_ip = false;
|
||||||
} else if ip.starts_with("192.168.") {
|
ip.to_string()
|
||||||
format!("{} - private IPv4 access", &ip)
|
}
|
||||||
} else {
|
|
||||||
is_special_ip = false;
|
|
||||||
ip.to_string()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
(processed_string, is_special_ip)
|
(processed_string, is_special_ip)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_location_string(location: String) -> Result<Location, String> {
|
||||||
|
let location_parts = location.split(",").collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if location_parts.len() != 2 {
|
||||||
|
return Err(format!("Unknown location format: {}", &location));
|
||||||
|
}
|
||||||
|
|
||||||
|
let latitude = location_parts[0].parse::<f64>().unwrap_or_default();
|
||||||
|
let longitude = location_parts[1].parse::<f64>().unwrap_or_default();
|
||||||
|
|
||||||
|
Ok(Location {
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_client_server_distance_string(client_location: String, units: Units) -> String {
|
||||||
|
let client_location = parse_location_string(client_location).unwrap_or(Location {
|
||||||
|
latitude: 0.0,
|
||||||
|
longitude: 0.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
let server_location = Location {
|
||||||
|
latitude: var("SERVER_LATITUDE")
|
||||||
|
.unwrap_or_default()
|
||||||
|
.parse::<f64>()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
longitude: var("SERVER_LONGITUDE")
|
||||||
|
.unwrap_or_default()
|
||||||
|
.parse::<f64>()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let distance = distance(client_location, server_location, &units);
|
||||||
|
|
||||||
|
format!("{:.2} {}", distance, units)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue