diff options
| -rw-r--r-- | CHANGELOG.md | 2 | ||||
| -rw-r--r-- | Cargo.lock | 27 | ||||
| -rw-r--r-- | mullvad-daemon/src/bin/list-relays.rs | 5 | ||||
| -rw-r--r-- | mullvad-daemon/src/bin/problem-report.rs | 5 | ||||
| -rw-r--r-- | mullvad-daemon/src/main.rs | 15 | ||||
| -rw-r--r-- | mullvad-rpc/Cargo.toml | 8 | ||||
| -rw-r--r-- | mullvad-rpc/src/cached_dns_resolver.rs | 415 | ||||
| -rw-r--r-- | mullvad-rpc/src/lib.rs | 72 |
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()?)) } } |
