feat: Attempt ipinfo.io if ipapi.co returns no results (#204)

* feat: Attempt ipinfo.io if ipapi.co returns no results

* fix: Use the GET method

* feat: Added tests

* chore: DRY
This commit is contained in:
Maciej Hirsz
2019-11-28 13:31:58 +01:00
committed by GitHub
parent a021622cf7
commit c48ea807e7
2 changed files with 103 additions and 14 deletions
+101 -12
View File
@@ -4,6 +4,7 @@ use std::sync::Arc;
use actix::prelude::*;
use rustc_hash::FxHashMap;
use parking_lot::RwLock;
use serde::Deserialize;
use crate::chain::{Chain, LocateNode};
use crate::types::{NodeId, NodeLocation};
@@ -52,6 +53,34 @@ pub struct LocateRequest {
pub chain: Addr<Chain>,
}
#[derive(Deserialize)]
pub struct IPApiLocate {
city: Box<str>,
loc: Box<str>,
}
impl IPApiLocate {
fn into_node_location(self) -> Option<NodeLocation> {
let IPApiLocate { city, loc } = self;
let mut loc = loc.split(",").map(|n| n.parse());
let latitude = loc.next()?.ok()?;
let longitude = loc.next()?.ok()?;
// Guarantee that the iterator has been exhausted
if loc.next().is_some() {
return None;
}
Some(NodeLocation {
latitude,
longitude,
city,
})
}
}
impl Handler<LocateRequest> for Locator {
type Result = ();
@@ -66,18 +95,9 @@ impl Handler<LocateRequest> for Locator {
return
}
let ip_req = format!("https://ipapi.co/{}/json", ip);
let mut response = match self.client.post(&ip_req).send() {
Ok(response) => response,
Err(err) => return debug!("POST error for ip location: {:?}", err),
};
let location = match response.json::<NodeLocation>() {
Ok(location) => Some(Arc::new(location)),
Err(err) => {
debug!("JSON error for ip location: {:?}", err);
None
}
let location = match self.iplocate(ip) {
Ok(location) => location,
Err(err) => return debug!("GET error for ip location: {:?}", err),
};
self.cache.write().insert(ip, location.clone());
@@ -87,3 +107,72 @@ impl Handler<LocateRequest> for Locator {
}
}
}
impl Locator {
fn iplocate(&self, ip: Ipv4Addr) -> Result<Option<Arc<NodeLocation>>, reqwest::Error> {
let location = self.iplocate_ipapi_co(ip)?;
match location {
Some(location) => Ok(Some(location)),
None => self.iplocate_ipinfo_io(ip),
}
}
fn iplocate_ipapi_co(&self, ip: Ipv4Addr) -> Result<Option<Arc<NodeLocation>>, reqwest::Error> {
let location = self.query(&format!("https://ipapi.co/{}/json", ip))?.map(Arc::new);
Ok(location)
}
fn iplocate_ipinfo_io(&self, ip: Ipv4Addr) -> Result<Option<Arc<NodeLocation>>, reqwest::Error> {
let location = self.query(&format!("https://ipinfo.io/{}/json", ip))?.and_then(|loc: IPApiLocate| {
loc.into_node_location().map(Arc::new)
});
Ok(location)
}
fn query<T>(&self, url: &str) -> Result<Option<T>, reqwest::Error>
where
for<'de> T: Deserialize<'de>,
{
match self.client.get(url).send()?.json::<T>() {
Ok(result) => Ok(Some(result)),
Err(err) => {
debug!("JSON error for ip location: {:?}", err);
Ok(None)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ipapi_locate_to_node_location() {
let ipapi = IPApiLocate {
loc: "12.5,56.25".into(),
city: "Foobar".into(),
};
let location = ipapi.into_node_location().unwrap();
assert_eq!(location.latitude, 12.5);
assert_eq!(location.longitude, 56.25);
assert_eq!(&*location.city, "Foobar");
}
#[test]
fn ipapi_locate_to_node_location_too_many() {
let ipapi = IPApiLocate {
loc: "12.5,56.25,1.0".into(),
city: "Foobar".into(),
};
let location = ipapi.into_node_location();
assert!(location.is_none());
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
use num_traits::{Zero, NumOps, Bounded, ops::saturating::Saturating};
use num_traits::{Zero, NumOps, Bounded};
use std::iter::Sum;
use std::convert::TryFrom;
@@ -10,7 +10,7 @@ pub struct NumStats<T> {
sum: T,
}
impl<T: Saturating + NumOps + Zero + Bounded + Copy + Sum + TryFrom<usize>> NumStats<T> {
impl<T: NumOps + Zero + Bounded + Copy + Sum + TryFrom<usize>> NumStats<T> {
pub fn new(size: usize) -> Self {
NumStats {
stack: vec![T::zero(); size].into_boxed_slice(),