//! Client which makes jsonrpc requests via HTTP to the `peach-dyndns-server` API which runs on the peach-vps. //! Note this is the one service in peach-lib which makes requests to an external server off of the local device. //! //! If the requests are successful, dyndns configurations are saved locally on the PeachCloud device, //! which are then used by the peach-dyndns-cronjob to update the dynamic IP using nsupdate. //! //! There is also one function in this file, dyndns_update_ip, which doesn't interact with the jsonrpc server. //! This function uses nsupdate to actually update dns records directly. //! //! The domain for dyndns updates is stored in /var/lib/peachcloud/config.yml //! The tsig key for authenticating the updates is stored in /var/lib/peachcloud/peach-dyndns/tsig.key use std::{ fs, fs::OpenOptions, io::Write, process::{Command}, str::FromStr, }; use std::ffi::OsStr; use chrono::prelude::*; use jsonrpc_client_core::{expand_params, jsonrpc_client}; use jsonrpc_client_http::HttpTransport; use log::{debug, info}; use regex::Regex; use crate::{ config_manager::{load_peach_config, set_peach_dyndns_config}, error::PeachError, }; use crate::config_manager::get_dyndns_server_address; /// constants for dyndns configuration pub const TSIG_KEY_PATH: &str = "/var/lib/peachcloud/peach-dyndns/tsig.key"; pub const PEACH_DYNDNS_CONFIG_PATH: &str = "/var/lib/peachcloud/peach-dyndns"; pub const DYNDNS_LOG_PATH: &str = "/var/lib/peachcloud/peach-dyndns/latest_result.log"; /// helper function which saves dyndns TSIG key returned by peach-dyndns-server to /var/lib/peachcloud/peach-dyndns/tsig.key pub fn save_dyndns_key(key: &str) -> Result<(), PeachError> { // create directory if it doesn't exist fs::create_dir_all(PEACH_DYNDNS_CONFIG_PATH)?; //.context(SaveTsigKeyError { //path: PEACH_DYNDNS_CONFIG_PATH.to_string(), //})?; // write key text let mut file = OpenOptions::new() .write(true) .create(true) // TODO: consider adding context msg .open(TSIG_KEY_PATH)?; writeln!(file, "{}", key).map_err(|source| PeachError::Write { source, path: TSIG_KEY_PATH.to_string(), })?; Ok(()) } /// Makes a post request to register a new domain with peach-dyns-server /// if the post is successful, the domain is registered with peach-dyndns-server /// a unique TSIG key is returned and saved to disk, /// and peachcloud is configured to start updating the IP of this domain using nsupdate pub fn register_domain(domain: &str) -> std::result::Result { debug!("Creating HTTP transport for dyndns client."); let transport = HttpTransport::new().standalone()?; let http_server = get_dyndns_server_address()?; info!("Using dyndns http server address: {:?}", http_server); debug!("Creating HTTP transport handle on {}.", &http_server); let transport_handle = transport.handle(&http_server)?; info!("Creating client for peach-dyndns service."); let mut client = PeachDynDnsClient::new(transport_handle); info!("Performing register_domain call to peach-dyndns-server"); let key = client.register_domain(domain).call()?; // save new TSIG key save_dyndns_key(&key)?; // save new configuration values let set_config_result = set_peach_dyndns_config(domain, &http_server, TSIG_KEY_PATH, true); match set_config_result { Ok(_) => { let response = "success".to_string(); Ok(response) } Err(err) => Err(err), } } /// Makes a post request to check if a domain is available pub fn is_domain_available(domain: &str) -> std::result::Result { debug!("Creating HTTP transport for dyndns client."); let transport = HttpTransport::new().standalone()?; let http_server = get_dyndns_server_address()?; debug!("Creating HTTP transport handle on {}.", &http_server); let transport_handle = transport.handle(&http_server)?; info!("Creating client for peach_network service."); let mut client = PeachDynDnsClient::new(transport_handle); info!("Performing register_domain call to peach-dyndns-server"); let domain_availability = client.is_domain_available(domain).call()?; info!("Domain availability: {:?}", domain_availability); // convert availability status to a bool let available: bool = FromStr::from_str(&domain_availability)?; Ok(available) } /// Helper function to get public ip address of PeachCloud device. fn get_public_ip_address() -> Result { // TODO: consider other ways to get public IP address let output = Command::new("/usr/bin/curl").arg("ifconfig.me").output()?; let command_output = String::from_utf8(output.stdout)?; Ok(command_output) } /// Reads dyndns configurations from config.yml /// and then uses nsupdate to update the IP address for the configured domain pub fn dyndns_update_ip() -> Result { let peach_config = load_peach_config()?; info!( "Using config: dyn_tsig_key_path: {:?} dyn_domain: {:?} dyn_dns_server_address: {:?} dyn_enabled: {:?} dyn_nameserver: {:?} ", peach_config.dyn_tsig_key_path, peach_config.dyn_domain, peach_config.dyn_dns_server_address, peach_config.dyn_enabled, peach_config.dyn_nameserver, ); if !peach_config.dyn_enabled { info!("dyndns is not enabled, not updating"); Ok(false) } else { // call nsupdate passing appropriate configs let mut nsupdate_command = Command::new("/usr/bin/nsupdate"); nsupdate_command .arg("-k") .arg(&peach_config.dyn_tsig_key_path) .arg("-v"); // pass nsupdate commands via stdin let public_ip_address = get_public_ip_address()?; info!("found public ip address: {}", public_ip_address); let ns_commands = format!( " server {NAMESERVER} zone {ZONE} update delete {DOMAIN} A update add {DOMAIN} 30 A {PUBLIC_IP_ADDRESS} send", NAMESERVER = peach_config.dyn_nameserver, ZONE = peach_config.dyn_domain, DOMAIN = peach_config.dyn_domain, PUBLIC_IP_ADDRESS = public_ip_address, ); info!("ns_commands: {:?}", ns_commands); info!("creating nsupdate temp file"); let temp_file_path = "/var/lib/peachcloud/nsupdate.sh"; // write ns_commands to temp_file fs::write(temp_file_path, ns_commands)?; nsupdate_command.arg(temp_file_path); let nsupdate_output = nsupdate_command.output()?; let args: Vec<&OsStr> = nsupdate_command.get_args().collect(); info!("nsupdate command: {:?}", args); // We only return a successful result if nsupdate was successful if nsupdate_output.status.success() { info!("nsupdate succeeded, returning ok"); // log a timestamp that the update was successful log_successful_nsupdate()?; // return true Ok(true) } else { info!("nsupdate failed, returning error"); let err_msg = String::from_utf8(nsupdate_output.stdout)?; Err(PeachError::NsUpdate { msg: err_msg }) } } } // Helper function to log a timestamp of the latest successful nsupdate pub fn log_successful_nsupdate() -> Result { let now_timestamp = chrono::offset::Utc::now().to_rfc3339(); let mut file = OpenOptions::new() .write(true) .create(true) // TODO: possibly add a context msg here ("failed to open dynamic dns success log") .open(DYNDNS_LOG_PATH)?; write!(file, "{}", now_timestamp).map_err(|source| PeachError::Write { source, path: DYNDNS_LOG_PATH.to_string(), })?; Ok(true) } /// Helper function to return how many seconds since peach-dyndns-updater successfully ran pub fn get_num_seconds_since_successful_dns_update() -> Result, PeachError> { let log_exists = std::path::Path::new(DYNDNS_LOG_PATH).exists(); if !log_exists { Ok(None) } else { let contents = fs::read_to_string(DYNDNS_LOG_PATH).map_err(|source| PeachError::Read { source, path: DYNDNS_LOG_PATH.to_string(), })?; // replace newline if found // TODO: maybe we can use `.trim()` instead let contents = contents.replace('\n', ""); // TODO: consider adding additional context? let time_ran_dt = DateTime::parse_from_rfc3339(&contents).map_err(|source| { PeachError::ParseDateTime { source, path: DYNDNS_LOG_PATH.to_string(), } })?; let current_time: DateTime = Utc::now(); let duration = current_time.signed_duration_since(time_ran_dt); let duration_in_seconds = duration.num_seconds(); Ok(Some(duration_in_seconds)) } } /// helper function which returns a true result if peach-dyndns-updater is enabled /// and has successfully run recently (in the last six minutes) pub fn is_dns_updater_online() -> Result { // first check if it is enabled in peach-config let peach_config = load_peach_config()?; let is_enabled = peach_config.dyn_enabled; // then check if it has successfully run within the last 6 minutes (60*6 seconds) let num_seconds_since_successful_update = get_num_seconds_since_successful_dns_update()?; let ran_recently: bool = match num_seconds_since_successful_update { Some(seconds) => { seconds < (60 * 6) } // if the value is None, then the last time it ran successfully is unknown None => { false } }; // debug log info!("is_dyndns_enabled: {:?}", is_enabled); info!("dyndns_ran_recently: {:?}", ran_recently); // if both are true, then return true Ok(is_enabled && ran_recently) } /// helper function which builds a full dynamic dns domain from a subdomain pub fn get_full_dynamic_domain(subdomain: &str) -> String { format!("{}.dyn.peachcloud.org", subdomain) } /// helper function to get a dyndns subdomain from a dyndns full domain pub fn get_dyndns_subdomain(dyndns_full_domain: &str) -> Option { let re = Regex::new(r"(.*)\.dyn\.peachcloud\.org").ok()?; let caps = re.captures(dyndns_full_domain)?; let subdomain = caps.get(1).map_or("", |m| m.as_str()); Some(subdomain.to_string()) } // helper function which checks if a dyndns domain is new pub fn check_is_new_dyndns_domain(dyndns_full_domain: &str) -> bool { // TODO: return `Result` and replace `unwrap` with `?` operator let peach_config = load_peach_config().unwrap(); let previous_dyndns_domain = peach_config.dyn_domain; dyndns_full_domain != previous_dyndns_domain } jsonrpc_client!(pub struct PeachDynDnsClient { pub fn register_domain(&mut self, domain: &str) -> RpcRequest; pub fn is_domain_available(&mut self, domain: &str) -> RpcRequest; });