summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md2
-rw-r--r--Cargo.lock27
-rw-r--r--mullvad-daemon/src/bin/list-relays.rs5
-rw-r--r--mullvad-daemon/src/bin/problem-report.rs5
-rw-r--r--mullvad-daemon/src/main.rs15
-rw-r--r--mullvad-rpc/Cargo.toml8
-rw-r--r--mullvad-rpc/src/cached_dns_resolver.rs415
-rw-r--r--mullvad-rpc/src/lib.rs72
8 files changed, 525 insertions, 24 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cdba807db5..497c50c1af 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -39,6 +39,8 @@ Line wrap the file at 100 chars. Th
### Changed
- Change all occurrences of "MullvadVPN" into "Mullvad VPN", this affects
paths and window captions etc.
+- Bundle an IP address with the app and introduce a disk cache fallback method for when the DNS
+ resolution of the Mullvad API server hostname fails.
### Fixed
- Fix a bug in account input field that advanced the cursor to the end regardless its prior
diff --git a/Cargo.lock b/Cargo.lock
index 4f4f3b0431..0152eb979c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -255,6 +255,16 @@ dependencies = [
]
[[package]]
+name = "filetime"
+version = "0.1.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)",
+ "redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
name = "fnv"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -400,7 +410,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "jsonrpc-client-core"
version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
+source = "git+https://github.com/mullvad/jsonrpc-client-rs#853f03cfeaf45af070c5e5ea608479094cfce79c"
dependencies = [
"error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -413,13 +423,13 @@ dependencies = [
[[package]]
name = "jsonrpc-client-http"
version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
+source = "git+https://github.com/mullvad/jsonrpc-client-rs#853f03cfeaf45af070c5e5ea608479094cfce79c"
dependencies = [
"error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.11.21 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "jsonrpc-client-core 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "jsonrpc-client-core 0.3.0 (git+https://github.com/mullvad/jsonrpc-client-rs)",
"log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"native-tls 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -666,15 +676,17 @@ version = "0.1.0"
dependencies = [
"chrono 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "filetime 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)",
"futures 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.11.21 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "jsonrpc-client-core 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "jsonrpc-client-http 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "jsonrpc-client-core 0.3.0 (git+https://github.com/mullvad/jsonrpc-client-rs)",
+ "jsonrpc-client-http 0.3.0 (git+https://github.com/mullvad/jsonrpc-client-rs)",
"log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"mullvad-types 0.1.0",
"native-tls 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tempdir 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -1496,6 +1508,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum errno 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b2c858c42ac0b88532f48fca88b0ed947cad4f1f64d904bcd6c9f138f7b95d70"
"checksum error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3"
"checksum fern 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "50475651fccc56343c766e4d1889428ea753308a977e1315db358ada28cc8c9d"
+"checksum filetime 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "714653f3e34871534de23771ac7b26e999651a0a228f47beb324dfdf1dd4b10f"
"checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
"checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
"checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
@@ -1513,8 +1526,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08"
"checksum ipnetwork 0.12.7 (registry+https://github.com/rust-lang/crates.io-index)" = "2134e210e2a024b5684f90e1556d5f71a1ce7f8b12e9ac9924c67fb36f63b336"
"checksum itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c"
-"checksum jsonrpc-client-core 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7542bf397d7b5ecd2e0922b45195164eedefa1956966c1cd2326b7fc3d2f82ff"
-"checksum jsonrpc-client-http 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "09e43134972b6c9de21cb67e3a803782cc0642048f3467681fde78cfcf734f77"
+"checksum jsonrpc-client-core 0.3.0 (git+https://github.com/mullvad/jsonrpc-client-rs)" = "<none>"
+"checksum jsonrpc-client-http 0.3.0 (git+https://github.com/mullvad/jsonrpc-client-rs)" = "<none>"
"checksum jsonrpc-core 8.0.1 (git+https://github.com/paritytech/jsonrpc?tag=v8.0.1)" = "<none>"
"checksum jsonrpc-core 8.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ddf83704f4e79979a424d1082dd2c1e52683058056c9280efa19ac5f6bc9033c"
"checksum jsonrpc-macros 8.0.0 (git+https://github.com/paritytech/jsonrpc?tag=v8.0.1)" = "<none>"
diff --git a/mullvad-daemon/src/bin/list-relays.rs b/mullvad-daemon/src/bin/list-relays.rs
index 7a1fcb092a..581ef06d1a 100644
--- a/mullvad-daemon/src/bin/list-relays.rs
+++ b/mullvad-daemon/src/bin/list-relays.rs
@@ -17,7 +17,10 @@ error_chain!{}
quick_main!(run);
fn run() -> Result<()> {
- let rpc_http_handle = mullvad_rpc::standalone().chain_err(|| "Unable to connect RPC")?;
+ let mut rpc_manager = mullvad_rpc::MullvadRpcFactory::new();
+ let rpc_http_handle = rpc_manager
+ .new_connection()
+ .chain_err(|| "Unable to connect RPC")?;
let mut client = mullvad_rpc::RelayListProxy::new(rpc_http_handle);
let relays = client
diff --git a/mullvad-daemon/src/bin/problem-report.rs b/mullvad-daemon/src/bin/problem-report.rs
index 7ec0003f34..a5e8ac0577 100644
--- a/mullvad-daemon/src/bin/problem-report.rs
+++ b/mullvad-daemon/src/bin/problem-report.rs
@@ -159,8 +159,9 @@ fn send_problem_report(user_email: &str, user_message: &str, report_path: &Path)
let report_content = read_file_lossy(report_path, REPORT_MAX_SIZE)
.chain_err(|| ErrorKind::ReadLogError(report_path.to_path_buf()))?;
let metadata = collect_metadata();
- let mut rpc_client =
- mullvad_rpc::ProblemReportProxy::connect().chain_err(|| ErrorKind::RpcError)?;
+ let mut rpc_manager = mullvad_rpc::MullvadRpcFactory::new();
+ let mut rpc_client = mullvad_rpc::ProblemReportProxy::connect(&mut rpc_manager)
+ .chain_err(|| ErrorKind::RpcError)?;
rpc_client
.problem_report(user_email, user_message, &report_content, &metadata)
.call()
diff --git a/mullvad-daemon/src/main.rs b/mullvad-daemon/src/main.rs
index aa3db9c98a..e81b99e40e 100644
--- a/mullvad-daemon/src/main.rs
+++ b/mullvad-daemon/src/main.rs
@@ -86,6 +86,9 @@ use std::fs;
error_chain!{
errors {
+ NoCacheDir {
+ description("Unable to create cache directory")
+ }
DaemonIsAlreadyRunning {
description("Another instance of the daemon is already running")
}
@@ -218,10 +221,13 @@ impl Daemon {
ErrorKind::DaemonIsAlreadyRunning
);
+ let cache_dir = get_cache_dir()?;
+ let mut rpc_manager = mullvad_rpc::MullvadRpcFactory::with_cache_dir(&cache_dir);
+
let (rpc_handle, http_handle, tokio_remote) =
- mullvad_rpc::event_loop::create(|core| {
+ mullvad_rpc::event_loop::create(move |core| {
let handle = core.handle();
- let rpc = mullvad_rpc::shared(&handle);
+ let rpc = rpc_manager.new_connection_on_event_loop(&handle);
let http = mullvad_rpc::rest::create_http_client(&handle);
let remote = core.remote();
(rpc, http, remote)
@@ -891,6 +897,11 @@ fn get_resource_dir() -> PathBuf {
}
}
+fn get_cache_dir() -> Result<PathBuf> {
+ app_dirs::app_root(app_dirs::AppDataType::UserCache, &::APP_INFO)
+ .chain_err(|| ErrorKind::NoCacheDir)
+}
+
#[cfg(unix)]
fn running_as_admin() -> bool {
let uid = unsafe { libc::getuid() };
diff --git a/mullvad-rpc/Cargo.toml b/mullvad-rpc/Cargo.toml
index 79bb8fa0be..cf6360b6b0 100644
--- a/mullvad-rpc/Cargo.toml
+++ b/mullvad-rpc/Cargo.toml
@@ -9,8 +9,8 @@ license = "GPL-3.0"
chrono = { version = "0.4", features = ["serde"] }
error-chain = "0.11"
futures = "0.1.15"
-jsonrpc-client-core = "0.3"
-jsonrpc-client-http = "0.3"
+jsonrpc-client-core = { git = "https://github.com/mullvad/jsonrpc-client-rs" }
+jsonrpc-client-http = { git = "https://github.com/mullvad/jsonrpc-client-rs" }
serde_json = "1.0"
tokio-core = "0.1"
hyper = "0.11"
@@ -19,3 +19,7 @@ native-tls = "0.1"
log = "0.4"
mullvad-types = { path = "../mullvad-types" }
+
+[dev-dependencies]
+filetime = "0.1"
+tempdir = "0.3"
diff --git a/mullvad-rpc/src/cached_dns_resolver.rs b/mullvad-rpc/src/cached_dns_resolver.rs
new file mode 100644
index 0000000000..141671a662
--- /dev/null
+++ b/mullvad-rpc/src/cached_dns_resolver.rs
@@ -0,0 +1,415 @@
+use std::fs::File;
+use std::io::{self, Read, Write};
+use std::net::{IpAddr, ToSocketAddrs};
+use std::path::{Path, PathBuf};
+use std::sync::mpsc;
+use std::thread;
+use std::time::{Duration, SystemTime, UNIX_EPOCH};
+
+use error_chain::ChainedError;
+
+
+static DNS_TIMEOUT: Duration = Duration::from_secs(2);
+static MAX_CACHE_AGE: Duration = Duration::from_secs(3600);
+static EXPIRED_CACHE_TIMESTAMP: SystemTime = UNIX_EPOCH;
+
+error_chain! {
+ errors {
+ DnsTimeout(host: String) {
+ description("DNS resolution for a host took too long")
+ display("DNS resolution for host \"{}\" took too long", host)
+ }
+
+ HostNotFound(host: String) {
+ description("DNS resolution for a host didn't return any IP addresses")
+ display("DNS resolution for host \"{}\" didn't return any IP addresses", host)
+ }
+
+ InvalidAddress {
+ description("Address loaded from file is invalid")
+ }
+
+ ResolveFailure(host: String) {
+ description("Failed to resolve IP address for host")
+ display("Failed to resolve IP address for host: {}", host)
+ }
+ }
+
+ foreign_links {
+ FileAccessError(io::Error);
+ }
+}
+
+
+pub trait DnsResolver {
+ fn resolve(&mut self, host: &str) -> Result<IpAddr>;
+}
+
+pub struct SystemDnsResolver;
+
+impl SystemDnsResolver {
+ fn resolve_in_background_thread(host: &str) -> mpsc::Receiver<Result<IpAddr>> {
+ let host = host.to_owned();
+ let (tx, rx) = mpsc::channel();
+
+ thread::spawn(move || {
+ let _ = tx.send(Self::resolve_hostname(&host));
+ });
+
+ rx
+ }
+
+ fn resolve_hostname(host: &str) -> Result<IpAddr> {
+ (host, 0)
+ .to_socket_addrs()
+ .chain_err(|| ErrorKind::ResolveFailure(host.to_owned()))?
+ .next()
+ .map(|socket_address| socket_address.ip())
+ .ok_or_else(|| ErrorKind::HostNotFound(host.to_owned()).into())
+ }
+}
+
+impl DnsResolver for SystemDnsResolver {
+ fn resolve(&mut self, host: &str) -> Result<IpAddr> {
+ Self::resolve_in_background_thread(host)
+ .recv_timeout(DNS_TIMEOUT)
+ .chain_err(|| ErrorKind::DnsTimeout(host.to_owned()))
+ .and_then(|result| result)
+ }
+}
+
+pub struct CachedDnsResolver<R: DnsResolver = SystemDnsResolver> {
+ hostname: String,
+ dns_resolver: R,
+ cache_file: PathBuf,
+ cached_address: IpAddr,
+ last_updated: SystemTime,
+}
+
+impl CachedDnsResolver<SystemDnsResolver> {
+ pub fn new(hostname: String, cache_file: PathBuf, fallback_address: IpAddr) -> Self {
+ Self::with_dns_resolver(SystemDnsResolver, hostname, cache_file, fallback_address)
+ }
+}
+
+impl<R: DnsResolver> CachedDnsResolver<R> {
+ pub fn with_dns_resolver(
+ dns_resolver: R,
+ hostname: String,
+ cache_file: PathBuf,
+ fallback_address: IpAddr,
+ ) -> Self {
+ let (cached_address, last_updated) =
+ Self::load_initial_cached_address(&cache_file, fallback_address);
+
+ CachedDnsResolver {
+ hostname,
+ dns_resolver,
+ cache_file,
+ cached_address,
+ last_updated,
+ }
+ }
+
+ pub fn resolve(&mut self) -> IpAddr {
+ if let Ok(cache_age) = self.last_updated.elapsed() {
+ if cache_age > MAX_CACHE_AGE {
+ self.resolve_into_cache();
+ }
+ } else {
+ warn!("System time changed, assuming cached IP address has expired");
+ self.resolve_into_cache();
+ }
+
+ self.cached_address
+ }
+
+ fn load_initial_cached_address(
+ cache_file: &Path,
+ fallback_address: IpAddr,
+ ) -> (IpAddr, SystemTime) {
+ match Self::load_from_file(cache_file) {
+ Ok(previously_cached_address) => match Self::read_file_modification_time(cache_file) {
+ Ok(last_updated) => (previously_cached_address, last_updated),
+ Err(error) => {
+ warn!("Failed to read modification time of file: {}", error);
+ (previously_cached_address, EXPIRED_CACHE_TIMESTAMP)
+ }
+ },
+ Err(error) => {
+ info!(
+ "Failed to load previously cached IP address, using fallback: {}",
+ error.display_chain(),
+ );
+
+ (fallback_address, EXPIRED_CACHE_TIMESTAMP)
+ }
+ }
+ }
+
+ fn load_from_file(file_path: &Path) -> Result<IpAddr> {
+ let mut file = File::open(file_path)?;
+ let mut address = String::new();
+
+ file.read_to_string(&mut address)?;
+
+ address
+ .trim()
+ .parse()
+ .chain_err(|| ErrorKind::InvalidAddress)
+ }
+
+ fn read_file_modification_time(cache_file: &Path) -> io::Result<SystemTime> {
+ cache_file
+ .metadata()
+ .and_then(|metadata| metadata.modified())
+ }
+
+ fn resolve_into_cache(&mut self) {
+ if let Ok(address) = self.dns_resolver.resolve(&self.hostname) {
+ self.cached_address = address;
+ self.last_updated = SystemTime::now();
+
+ if let Err(error) = self.update_cache_file() {
+ warn!("Failed to update cache file with new IP address: {}", error);
+ }
+ }
+ }
+
+ fn update_cache_file(&mut self) -> io::Result<()> {
+ let mut cache_file = File::create(&self.cache_file)?;
+
+ writeln!(cache_file, "{}", self.cached_address)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ extern crate filetime;
+ extern crate tempdir;
+
+ use std::fs::{self, File};
+ use std::io::{Read, Write};
+ use std::sync::atomic::{AtomicBool, Ordering};
+ use std::sync::Arc;
+
+ use self::filetime::FileTime;
+ use self::tempdir::TempDir;
+ use super::*;
+
+ #[test]
+ fn uses_previously_cached_address() {
+ let (_temp_dir, cache_dir) = create_test_dirs();
+ let mock_resolver = MockDnsResolver::with_address("192.168.1.206".parse().unwrap());
+ let mock_resolver_was_called = mock_resolver.was_called_handle();
+ let cached_address = "127.0.0.1".parse().unwrap();
+
+ write_address(&cache_dir, cached_address);
+
+ let mut cache = create_cached_dns_resolver(mock_resolver, &cache_dir, None);
+ let address = cache.resolve();
+
+ assert!(!mock_resolver_was_called.load(Ordering::Acquire));
+ assert_eq!(address, cached_address);
+ }
+
+ #[test]
+ fn old_cache_file_is_updated() {
+ let (_temp_dir, cache_dir) = create_test_dirs();
+ let cached_address = "127.0.0.1".parse().unwrap();
+ let mock_address = "192.168.1.206".parse().unwrap();
+ let mock_resolver = MockDnsResolver::with_address(mock_address);
+
+ let cache_file_path = write_address(&cache_dir, cached_address);
+
+ make_file_old(&cache_file_path);
+
+ let mut cache = create_cached_dns_resolver(mock_resolver, &cache_dir, None);
+ let address = cache.resolve();
+
+ assert_eq!(get_cached_address(&cache_dir), address.to_string());
+ assert_eq!(address, mock_address);
+ }
+
+ #[test]
+ fn old_cache_file_is_used_if_resolution_fails() {
+ let (_temp_dir, cache_dir) = create_test_dirs();
+ let mock_resolver = MockDnsResolver::that_fails();
+ let cached_address = "127.0.0.1".parse().unwrap();
+
+ let cache_file_path = write_address(&cache_dir, cached_address);
+
+ make_file_old(&cache_file_path);
+
+ let mut cache = create_cached_dns_resolver(mock_resolver, &cache_dir, None);
+ let address = cache.resolve();
+
+ assert_eq!(address, cached_address);
+ }
+
+ #[test]
+ fn caches_resolved_ip() {
+ let (_temp_dir, cache_dir) = create_test_dirs();
+ let mock_address = "192.168.1.206".parse().unwrap();
+ let mock_resolver = MockDnsResolver::with_address(mock_address);
+
+ let mut cache = create_cached_dns_resolver(mock_resolver, &cache_dir, None);
+ let address = cache.resolve();
+
+ assert_eq!(address, mock_address);
+ assert_eq!(get_cached_address(&cache_dir), address.to_string());
+ }
+
+ #[test]
+ fn resolves_even_if_impossible_to_store_in_cache() {
+ let (temp_dir, cache_dir) = create_test_dirs();
+ let mock_address = "192.168.1.206".parse().unwrap();
+ let mock_resolver = MockDnsResolver::with_address(mock_address);
+
+ let mut cache = create_cached_dns_resolver(mock_resolver, &cache_dir, None);
+
+ ::std::mem::drop(temp_dir);
+
+ assert_eq!(cache.resolve(), mock_address);
+ }
+
+ #[test]
+ fn uses_fallback_address() {
+ let (_temp_dir, cache_dir) = create_test_dirs();
+ let fallback_address = "192.168.1.31".parse().unwrap();
+ let mock_resolver = MockDnsResolver::that_fails();
+ let mock_resolver_was_called = mock_resolver.was_called_handle();
+
+ let mut cache =
+ create_cached_dns_resolver(mock_resolver, &cache_dir, Some(fallback_address));
+ let address = cache.resolve();
+
+ assert!(mock_resolver_was_called.load(Ordering::Acquire));
+ assert_eq!(address, fallback_address);
+ }
+
+ #[test]
+ fn ignores_fallback_address_if_resolution_succeeds() {
+ let (_temp_dir, cache_dir) = create_test_dirs();
+ let fallback_address = "192.168.1.31".parse().unwrap();
+ let mock_address = "192.168.1.206".parse().unwrap();
+ let mock_resolver = MockDnsResolver::with_address(mock_address);
+
+ let mut cache =
+ create_cached_dns_resolver(mock_resolver, &cache_dir, Some(fallback_address));
+ let address = cache.resolve();
+
+ assert_eq!(address, mock_address);
+ }
+
+ #[test]
+ fn invalid_cache_file_leads_to_fallback_address_usage() {
+ let (_temp_dir, cache_dir) = create_test_dirs();
+ let fallback_address = "192.168.1.31".parse().unwrap();
+ let mock_resolver = MockDnsResolver::that_fails();
+ let mock_resolver_was_called = mock_resolver.was_called_handle();
+
+ write_invalid_address(&cache_dir);
+
+ let mut cache =
+ create_cached_dns_resolver(mock_resolver, &cache_dir, Some(fallback_address));
+ let address = cache.resolve();
+
+ assert!(mock_resolver_was_called.load(Ordering::Acquire));
+ assert_eq!(address, fallback_address);
+ }
+
+ fn create_test_dirs() -> (TempDir, PathBuf) {
+ let temp_dir = TempDir::new("ip-cache-test").unwrap();
+ let cache_dir = temp_dir.path().join("cache");
+
+ fs::create_dir(&cache_dir).unwrap();
+
+ (temp_dir, cache_dir)
+ }
+
+ fn write_invalid_address(dir: &Path) -> PathBuf {
+ let file_path = dir.join("api_ip_address.txt");
+ let mut file = File::create(&file_path).unwrap();
+
+ writeln!(file, "400.30.12.9").unwrap();
+
+ file_path
+ }
+
+ fn write_address(dir: &Path, address: IpAddr) -> PathBuf {
+ let file_path = dir.join("api_ip_address.txt");
+ let mut file = File::create(&file_path).unwrap();
+
+ writeln!(file, "{}", address).unwrap();
+
+ file_path
+ }
+
+ fn make_file_old(file: &Path) {
+ let file_metadata = file.metadata().unwrap();
+ let last_access_time = FileTime::from_last_access_time(&file_metadata);
+ let fake_modification_time = FileTime::from_seconds_since_1970(100_000, 0);
+
+ filetime::set_file_times(&file, last_access_time, fake_modification_time).unwrap();
+ }
+
+ fn get_cached_address(cache_dir: &Path) -> String {
+ let cache_file_path = cache_dir.join("api_ip_address.txt");
+
+ assert!(cache_file_path.exists());
+
+ let mut cache_file = File::open(cache_file_path).unwrap();
+ let mut cached_address = String::new();
+
+ cache_file.read_to_string(&mut cached_address).unwrap();
+
+ cached_address.trim().to_string()
+ }
+
+ fn create_cached_dns_resolver(
+ mock_resolver: MockDnsResolver,
+ cache_dir: &Path,
+ fallback_address: Option<IpAddr>,
+ ) -> CachedDnsResolver<MockDnsResolver> {
+ let hostname = "dummy.host".to_owned();
+ let filename = "api_ip_address.txt";
+ let cache_file = cache_dir.join(filename);
+ let fallback_address = fallback_address.unwrap_or(IpAddr::from([10, 0, 109, 91]));
+
+ CachedDnsResolver::with_dns_resolver(mock_resolver, hostname, cache_file, fallback_address)
+ }
+
+ struct MockDnsResolver {
+ address: Option<IpAddr>,
+ called: Arc<AtomicBool>,
+ }
+
+ impl MockDnsResolver {
+ pub fn with_address(address: IpAddr) -> Self {
+ MockDnsResolver {
+ address: Some(address),
+ called: Arc::new(AtomicBool::new(false)),
+ }
+ }
+
+ pub fn that_fails() -> Self {
+ MockDnsResolver {
+ address: None,
+ called: Arc::new(AtomicBool::new(false)),
+ }
+ }
+
+ pub fn was_called_handle(&self) -> Arc<AtomicBool> {
+ self.called.clone()
+ }
+ }
+
+ impl DnsResolver for MockDnsResolver {
+ fn resolve(&mut self, host: &str) -> Result<IpAddr> {
+ self.called.store(true, Ordering::Release);
+ self.address
+ .ok_or_else(|| ErrorKind::ResolveFailure(host.to_owned()).into())
+ }
+ }
+}
diff --git a/mullvad-rpc/src/lib.rs b/mullvad-rpc/src/lib.rs
index 595ba5697f..6a1a682a1b 100644
--- a/mullvad-rpc/src/lib.rs
+++ b/mullvad-rpc/src/lib.rs
@@ -25,6 +25,7 @@ extern crate mullvad_types;
use chrono::offset::Utc;
use chrono::DateTime;
+use jsonrpc_client_http::header::Host;
use jsonrpc_client_http::HttpTransport;
use tokio_core::reactor::Handle;
@@ -36,22 +37,74 @@ use mullvad_types::relay_list::RelayList;
use mullvad_types::version;
use std::collections::HashMap;
+use std::net::IpAddr;
+use std::path::Path;
pub mod event_loop;
pub mod rest;
+mod cached_dns_resolver;
+use cached_dns_resolver::CachedDnsResolver;
-static MASTER_API_URI: &str = "https://api.mullvad.net/rpc/";
+static MASTER_API_HOST: &str = "api.mullvad.net";
-/// Create and returns a `HttpHandle` running on the given core handle.
-pub fn shared(handle: &Handle) -> Result<HttpHandle, HttpError> {
- HttpTransport::shared(handle)?.handle(MASTER_API_URI)
+/// A type that helps with the creation of RPC connections.
+pub struct MullvadRpcFactory {
+ address_cache: Option<CachedDnsResolver>,
}
-/// Spawns a tokio core on a new thread and returns a `HttpHandle` running on that core.
-pub fn standalone() -> Result<HttpHandle, HttpError> {
- HttpTransport::new()?.handle(MASTER_API_URI)
+impl MullvadRpcFactory {
+ /// Create a new `MullvadRpcFactory`.
+ pub fn new() -> Self {
+ MullvadRpcFactory {
+ address_cache: None,
+ }
+ }
+
+ /// Create a new `MullvadRpcFactory` using the specified cache directory.
+ pub fn with_cache_dir(cache_dir: &Path) -> Self {
+ let hostname = MASTER_API_HOST.to_owned();
+ let cache_file = cache_dir.join("api_ip_address.txt");
+ let fallback_address = IpAddr::from([193, 138, 219, 46]);
+
+ let cached_dns_resolver = CachedDnsResolver::new(hostname, cache_file, fallback_address);
+
+ MullvadRpcFactory {
+ address_cache: Some(cached_dns_resolver),
+ }
+ }
+
+ /// Spawns a tokio core on a new thread and returns a `HttpHandle` running on that core.
+ pub fn new_connection(&mut self) -> Result<HttpHandle, HttpError> {
+ self.setup_connection(HttpTransport::new()?)
+ }
+
+ /// Create and returns a `HttpHandle` running on the given core handle.
+ pub fn new_connection_on_event_loop(
+ &mut self,
+ handle: &Handle,
+ ) -> Result<HttpHandle, HttpError> {
+ self.setup_connection(HttpTransport::shared(handle)?)
+ }
+
+ fn setup_connection(&mut self, transport: HttpTransport) -> Result<HttpHandle, HttpError> {
+ let mut handle = transport.handle(&self.api_uri())?;
+
+ handle.set_header(Host::new(MASTER_API_HOST, None));
+
+ Ok(handle)
+ }
+
+ fn api_uri(&mut self) -> String {
+ let address = if let Some(ref mut address_cache) = self.address_cache {
+ address_cache.resolve().to_string()
+ } else {
+ MASTER_API_HOST.to_owned()
+ };
+
+ format!("https://{}/rpc/", address)
+ }
}
jsonrpc_client!(pub struct AccountsProxy {
@@ -69,9 +122,8 @@ jsonrpc_client!(pub struct ProblemReportProxy {
});
impl ProblemReportProxy<HttpHandle> {
- pub fn connect() -> Result<Self, HttpError> {
- let transport = HttpTransport::new()?.handle(MASTER_API_URI)?;
- Ok(ProblemReportProxy::new(transport))
+ pub fn connect(manager: &mut MullvadRpcFactory) -> Result<Self, HttpError> {
+ Ok(ProblemReportProxy::new(manager.new_connection()?))
}
}