Files
DesktopUI/src/serviceclient.rs
T
Adam Ierymenko c241b5751f docs
2021-09-03 08:25:50 -04:00

509 lines
19 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* (c)2021 ZeroTier, Inc.
* https://www.zerotier.com/
*/
use std::cell::Cell;
use std::collections::{HashMap, LinkedList};
use std::ffi::CString;
use std::io::Write;
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, SystemTime};
use serde_json::{Map, Value};
const QUERY_TIMEOUT_MS: u64 = 2000;
/// Client that queries and caches JSON state from the service.
/// Currently it doesn't do much parsing since the JSON is just shoved
/// through to the JavaScript UI for most of what's done with it.
pub struct ServiceClient {
refresh_base_paths: Vec<&'static str>,
auth_token: String,
port: u16,
base_url: String,
saved_networks: Map<String, Value>,
state_hash: HashMap<String, u64>,
state: Map<String, Value>,
post_queue: LinkedList<(String, String)>,
delete_queue: LinkedList<String>,
dirty: Arc<AtomicBool>,
online: bool,
}
pub fn ms_since_epoch() -> i64 {
SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).map_or(0, |t| t.as_millis() as i64)
}
pub fn get_auth_token_and_port(spawn_elevated: bool) -> Option<(String, u16)> {
let mut port = 0_u16;
let mut token = String::new();
#[cfg(windows)]
let home = std::env::var("USERPROFILE");
#[cfg(not(windows))]
let home = std::env::var("HOME");
for p in [crate::GLOBAL_SERVICE_HOME_V2, crate::GLOBAL_SERVICE_HOME_V1] {
let p = Path::new(p);
for port_path in [p.join("zerotier.port"), p.join("zerotier-one.port")] {
let _ = std::fs::read(port_path).map(|pp| String::from_utf8(pp).map(|pp| u16::from_str_radix(pp.trim(), 10).map(|pp| port = pp)));
if port != 0 {
break;
}
}
}
for attempt in 0..2 {
let _ = home.clone().map(|mut p| {
#[cfg(target_os = "macos")]
p.push_str("/Library/Application Support/ZeroTier/One/authtoken.secret");
#[cfg(windows)]
p.push_str("\\AppData\\Local\\ZeroTier\\One\\authtoken.secret");
#[cfg(all(unix, not(target_os = "macos")))]
p.push_str("/.zeroTierOneAuthToken");
let _ = std::fs::read(p).map(|tok| String::from_utf8(tok).map(|tok| token = tok.trim().into()));
});
if token.is_empty() {
let _ = home.clone().map(|mut p| {
#[cfg(target_os = "macos")]
p.push_str("/Library/Application Support/ZeroTier/authtoken.secret");
#[cfg(windows)]
p.push_str("\\AppData\\Local\\ZeroTier\\authtoken.secret");
#[cfg(all(unix, not(target_os = "macos")))]
p.push_str("/.zerotier-local-auth");
let _ = std::fs::read(p).map(|tok| String::from_utf8(tok).map(|tok| token = tok.trim().into()));
});
}
if token.is_empty() {
for p in [crate::GLOBAL_SERVICE_HOME_V2, crate::GLOBAL_SERVICE_HOME_V1] {
let p = Path::new(p);
let _ = std::fs::read(p.join("authtoken.secret")).map(|tok| String::from_utf8(tok).map(|tok| token = tok.trim().into()));
if !token.is_empty() {
break;
}
}
if token.is_empty() {
if spawn_elevated && attempt == 0 {
let _ = runas::Command::new(std::env::current_exe().unwrap()).arg("copy_authtoken").gui(true).status();
}
} else {
let _ = home.clone().map(|mut p| {
// Save in both places for now for backward compatibility.
#[cfg(target_os = "macos")]
p.push_str("/Library/Application Support/ZeroTier/authtoken.secret");
#[cfg(windows)]
p.push_str("\\AppData\\Local\\ZeroTier\\authtoken.secret");
#[cfg(all(unix, not(target_os = "macos")))]
p.push_str("/.zerotier-local-auth");
if std::fs::write(&p, token.as_bytes()).is_ok() {
unsafe {
let cstr = CString::new(p.as_str()).unwrap();
crate::c_lock_down_file(cstr.as_ptr(), 0);
}
}
p.clear();
#[cfg(target_os = "macos")]
p.push_str("/Library/Application Support/ZeroTier/One/authtoken.secret");
#[cfg(windows)]
p.push_str("\\AppData\\Local\\ZeroTier\\One\\authtoken.secret");
#[cfg(all(unix, not(target_os = "macos")))]
p.push_str("/.zeroTierOneAuthToken");
if std::fs::write(&p, token.as_bytes()).is_ok() {
unsafe {
let cstr = CString::new(p.as_str()).unwrap();
crate::c_lock_down_file(cstr.as_ptr(), 0);
}
}
});
}
}
if !token.is_empty() {
break;
}
}
if token.is_empty() {
None
} else {
if port == 0 {
port = 9993;
}
Some((token, port))
}
}
const SEP_BYTE: [u8; 1] = [0_u8];
fn hash_result(v: &Value, h: &mut crc64::Crc64) {
let _ = h.write(&SEP_BYTE);
match v {
Value::Array(a) => {
for x in a.iter() {
hash_result(x, h);
}
},
Value::Object(o) => {
for x in o.iter() {
let _ = h.write(x.0.as_bytes());
if !x.0.eq("clock") && !x.0.eq("netconfRevision") { // omit fields that change meaninglessly
hash_result(x.1, h);
}
}
},
Value::Bool(b) => {
let _ = h.write(&[*b as u8]);
},
Value::Number(n) => {
let _ = h.write(n.to_string().as_bytes());
},
Value::String(s) => {
let _ = h.write(s.as_bytes());
}
_ => {}
}
}
impl ServiceClient {
/// Create a new service client and return the client and a flag that can be atomically checked to indicate changes.
pub fn new(refresh_base_paths: Vec<&'static str>) -> (ServiceClient, Arc<AtomicBool>) {
let dirty_flag = Arc::new(AtomicBool::new(true));
(ServiceClient {
refresh_base_paths,
auth_token: String::new(),
port: 0,
base_url: String::new(),
saved_networks: std::fs::read(unsafe { crate::NETWORK_CACHE_PATH.as_str() }).map_or_else(|_| {
serde_json::Map::new()
}, |j| {
serde_json::from_slice(j.as_slice()).map_or_else(|_| serde_json::Map::new(), |r| r)
}),
state_hash: HashMap::new(),
state: Map::new(),
post_queue: LinkedList::new(),
delete_queue: LinkedList::new(),
dirty: dirty_flag.clone(),
online: false,
}, dirty_flag)
}
#[inline(always)]
pub fn is_initialized(&self) -> bool {
self.port != 0 && !self.auth_token.is_empty()
}
#[inline(always)]
pub fn is_online(&self) -> bool {
self.is_initialized() && self.online
}
pub fn with<T: AsRef<str>, R, F: FnOnce(&Value) -> R>(&self, path: &[T], f: F) -> R {
let mut m = Some(&self.state);
let v = Cell::new(&Value::Null);
for s in path.iter() {
m.map_or_else(|| {
v.replace(&Value::Null); // null if looking past tree of maps
}, |mv| {
mv.get((*s).as_ref()).map_or_else(|| {
v.replace(&Value::Null); // null if key not found
}, |nv| {
m = nv.as_object(); // sets to None if not a map
if m.is_none() {
v.replace(nv); // if not a map, it's a value
}
});
});
}
f(v.into_inner())
}
#[inline(always)]
pub fn get<T: AsRef<str>>(&self, path: &[T]) -> Value {
self.with(path, |v| v.clone())
}
#[inline(always)]
pub fn get_str<T: AsRef<str>>(&self, path: &[T]) -> String {
self.get(path).as_str().map_or_else(|| String::new(), |s| s.into())
}
#[inline(always)]
pub fn get_all_json(&self) -> String {
serde_json::to_string(&self.state).unwrap()
}
pub fn networks(&self) -> Vec<(String, Map<String, Value>)> {
let mut nw: Vec<(String, Map<String, Value>)> = Vec::new();
self.with(&["network"], |nws| {
let _ = nws.as_array().map(|a| a.iter().for_each(|network| {
let _ = network.as_object().map(|network| {
network.get("id").map(|id| {
id.as_str().map(|id| {
nw.push((id.into(), network.clone()))
});
});
});
}));
});
nw.sort_by(|a, b| (*a).0.cmp(&((*b).0)) );
nw
}
/*
pub fn network_has_error(&self, nwid: &str) -> bool {
self.state.get("network").map_or(true, |network| network.as_array().map_or(true, |network| {
let mut has_error = true;
for n in network.iter() {
if n.as_object().map_or(false, |n| {
n.get("id").map_or(false, |id| id.as_str().map_or(false, |id| id == nwid)) && n.get("status").map_or(false, |status| status.as_str().map_or(false, |status| status == "OK"))
}) {
has_error = false;
break;
}
}
has_error
}))
}
*/
pub fn sso_auth_needed_networks(&self, reauth_ms_before_timeout: i64) -> Vec<(String, String, String)> {
let mut nw: Vec<(String, String, String)> = Vec::new();
let now = ms_since_epoch();
self.with(&["network"], |nws| {
let _ = nws.as_array().map(|a| a.iter().for_each(|network| {
let _ = network.as_object().map(|network| {
let id = network.get("id").map_or("", |id| id.as_str().unwrap_or(""));
let sso_enabled = network.get("ssoEnabled").map_or(false, |sso_enabled| sso_enabled.as_bool().unwrap_or(false));
let auth_expiry_time = network.get("authenticationExpiryTime").map_or(-1, |auth_expiry_time| auth_expiry_time.as_i64().unwrap_or(-1));
let auth_url = network.get("authenticationURL").map_or("", |auth_url| auth_url.as_str().unwrap_or(""));
let status = network.get("status").map_or("", |status| status.as_str().unwrap_or(""));
if sso_enabled && !auth_url.is_empty() {
let remaining = auth_expiry_time - now;
if status == "AUTHENTICATION_REQUIRED" || remaining <= reauth_ms_before_timeout {
nw.push((id.into(), auth_url.into(), status.into()));
}
}
});
}));
});
nw.sort_by(|a, b| (*a).0.cmp(&((*b).0)) );
nw
}
pub fn saved_networks(&self) -> Vec<(String, String, String)> {
let mut nw: Vec<(String, String, String)> = Vec::new();
for kv in self.saved_networks.iter() {
let _ = kv.1.as_object().map(|n| {
n.get("id").map(|id| {
id.as_str().map(|id| {
if id.len() == 16 {
nw.push((id.into(), n.get("name").map_or_else(|| {
String::new()
}, |name| {
name.as_str().unwrap_or("").into()
}), n.get("settings").map_or_else(|| {
String::new()
}, |name| {
name.as_str().unwrap_or("").into()
})));
}
})
})
});
}
nw.sort_by(|a, b| (*a).0.cmp(&((*b).0)) );
nw
}
/*
pub fn peers(&self) -> Vec<(String, Map<String, Value>)> {
let mut pp: Vec<(String, Map<String, Value>)> = Vec::new();
self.with(&["peer"], |nws| {
let _ = nws.as_array().map(|a| a.iter().for_each(|peer| {
let _ = peer.as_object().map(|peer| {
peer.get("address").map(|id| {
id.as_str().map(|id| {
pp.push((id.into(), peer.clone()));
});
});
});
}));
});
pp.sort_by(|a, b| (*a).0.cmp(&((*b).0)) );
pp
}
*/
fn http_get(&self, path: &str) -> (u16, String) {
if self.auth_token.is_empty() || self.base_url.is_empty() {
(0, String::new())
} else {
ureq::get(format!("{}{}", self.base_url, path).as_str()).timeout(Duration::from_millis(QUERY_TIMEOUT_MS)).set("X-ZT1-Auth", self.auth_token.as_str()).call().map_or_else(|_| {
(0, String::new())
}, |res| {
let status = res.status();
let body = res.into_string();
body.map_or_else(|_| {
(0, String::new())
}, |res| {
(status, res)
})
})
}
}
fn http_post(&self, path: &str, payload: &str) -> (u16, String) {
//println!("POST {} {}", path, payload);
if self.auth_token.is_empty() || self.base_url.is_empty() {
(0, String::new())
} else {
ureq::post(format!("{}{}", self.base_url, path).as_str()).timeout(Duration::from_millis(QUERY_TIMEOUT_MS)).set("X-ZT1-Auth", self.auth_token.as_str()).send_string(payload).map_or_else(|_| {
(0, String::new())
}, |res| {
let status = res.status();
let body = res.into_string();
body.map_or_else(|_| {
(0, String::new())
}, |res| {
(status, res)
})
})
}
}
#[inline(always)]
pub fn enqueue_post(&mut self, path: String, payload: String) {
self.post_queue.push_back((path, payload));
}
#[inline(always)]
pub fn enqueue_delete(&mut self, path: String) {
self.delete_queue.push_back(path);
}
/// Check auth token and port for running service and update if changed.
pub fn sync_client_config(&mut self) {
get_auth_token_and_port(true).map(|token_port| {
if self.auth_token != token_port.0 || self.port != token_port.1 {
self.auth_token = token_port.0.clone();
self.port = token_port.1;
self.base_url = format!("http://127.0.0.1:{}/", self.port);
}
});
}
/// Send enqueued posts, if there are any.
pub fn do_posts(&mut self) -> bool {
if !self.is_initialized() || !self.is_online() {
self.sync_client_config();
}
if self.is_initialized() {
let mut posted = false;
loop {
let pq = self.post_queue.pop_front();
if pq.is_some() {
let pq = pq.unwrap();
posted = true;
let _ = self.http_post(pq.0.as_str(), pq.1.as_str());
} else {
break;
}
}
loop {
let pq = self.delete_queue.pop_front();
if pq.is_some() {
let pq = pq.unwrap();
posted = true;
let _ = ureq::delete(format!("{}{}", self.base_url, pq).as_str()).timeout(Duration::from_millis(QUERY_TIMEOUT_MS)).set("X-ZT1-Auth", self.auth_token.as_str()).call().map_or(0_u16, |res| res.status());
} else {
break;
}
}
if posted {
self.dirty.store(true, Ordering::Relaxed);
}
posted
} else {
false
}
}
pub fn remember_network(&mut self, id: String, name: String, settings: String) {
let mut n: serde_json::Map<String, Value> = serde_json::Map::new();
n.insert("id".into(), Value::from(id.clone()));
n.insert("name".into(), Value::from(name));
n.insert("settings".into(), Value::from(settings));
self.saved_networks.insert(id, Value::from(n));
self.state.insert("saved_networks".into(), serde_json::Value::from(self.saved_networks.clone()));
let _ = serde_json::to_vec(&self.saved_networks).map(|json| std::fs::write(unsafe { crate::NETWORK_CACHE_PATH.as_str() }, &json));
self.dirty.store(true, Ordering::Relaxed);
}
pub fn forget_network(&mut self, id: &String) {
self.saved_networks.remove(id);
self.state.insert("saved_networks".into(), serde_json::Value::from(self.saved_networks.clone()));
let _ = serde_json::to_vec(&self.saved_networks).map(|json| std::fs::write(unsafe { crate::NETWORK_CACHE_PATH.as_str() }, &json));
self.dirty.store(true, Ordering::Relaxed);
}
/// Submit queued posts and get current service state.
pub fn sync(&mut self) {
if !self.is_initialized() || !self.is_online() {
self.sync_client_config();
self.state.insert("saved_networks".into(), serde_json::Value::from(self.saved_networks.clone()));
}
if self.is_initialized() {
let mut dirty = false;
for endpoint in self.refresh_base_paths.iter() {
let endpoint = *endpoint;
let data = self.http_get(endpoint);
if data.0 == 200 {
let endpoint = String::from(endpoint);
let data = serde_json::from_str::<Value>(data.1.as_str()).unwrap_or(Value::Null);
let mut c64 = crc64::Crc64::new();
hash_result(&data, &mut c64);
let c64 = c64.get();
self.online = true;
if self.state_hash.insert(endpoint.clone(), c64).unwrap_or(0) != c64 {
self.state.insert(endpoint, data);
self.dirty.store(true, Ordering::Relaxed);
dirty = true;
}
} else {
self.online = false;
}
}
if dirty {
self.state.insert("saved_networks".into(), serde_json::Value::from(self.saved_networks.clone()));
}
}
}
}