diff --git a/src/api2.rs b/src/api2.rs index 36ee87de..b0c18ab0 100644 --- a/src/api2.rs +++ b/src/api2.rs @@ -19,6 +19,12 @@ use crate::tools::common_regex; lazy_static! { pub static ref IP_FORMAT: Arc = ApiStringFormat::Pattern(&common_regex::IP_REGEX).into(); + pub static ref PVE_CONFIG_DIGEST_FORMAT: Arc = + ApiStringFormat::Pattern(&common_regex::SHA256_HEX_REGEX).into(); + + pub static ref PVE_CONFIG_DIGEST_SCHEMA: Arc = + StringSchema::new("Prevent changes if current configuration file has different SHA256 digest. This can be used to prevent concurrent modifications.") + .format(PVE_CONFIG_DIGEST_FORMAT.clone()).into(); } diff --git a/src/api2/node/dns.rs b/src/api2/node/dns.rs index 5541ffff..eaac08f6 100644 --- a/src/api2/node/dns.rs +++ b/src/api2/node/dns.rs @@ -9,8 +9,9 @@ use crate::api2::*; use lazy_static::lazy_static; use std::io::{BufRead, BufReader}; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use openssl::sha; +use regex::Regex; use serde_json::{json, Value}; @@ -29,8 +30,8 @@ fn read_etc_resolv_conf() -> Result { let data = String::from_utf8(raw)?; lazy_static! { - static ref DOMAIN_REGEX: regex::Regex = regex::Regex::new(r"^\s*(?:search|domain)\s+(\S+)\s*").unwrap(); - static ref SERVER_REGEX: regex::Regex = regex::Regex::new( + static ref DOMAIN_REGEX: Regex = Regex::new(r"^\s*(?:search|domain)\s+(\S+)\s*").unwrap(); + static ref SERVER_REGEX: Regex = Regex::new( concat!(r"^\s*nameserver\s+(", IPRE!(), r")\s*")).unwrap(); } @@ -52,8 +53,23 @@ fn read_etc_resolv_conf() -> Result { fn update_dns(param: Value, _info: &ApiMethod) -> Result { + lazy_static! { + static ref MUTEX: Arc> = Arc::new(Mutex::new(0)); + } + + let guard = MUTEX.lock(); + let search = tools::required_string_param(¶m, "search")?; + let raw = tools::file_get_contents(RESOLV_CONF_FN)?; + let old_digest = tools::digest_to_hex(&sha::sha256(&raw)); + + if let Some(digest) = param["digest"].as_str() { + tools::assert_if_modified(&old_digest, &digest)?; + } + + let old_data = String::from_utf8(raw)?; + let mut data = format!("search {}\n", search); for opt in &["dns1", "dns2", "dns3"] { @@ -62,6 +78,16 @@ fn update_dns(param: Value, _info: &ApiMethod) -> Result { } } + // append other data + lazy_static! { + static ref SKIP_REGEX: Regex = Regex::new(r"^(search|domain|nameserver)\s+").unwrap(); + } + for line in old_data.lines() { + if SKIP_REGEX.is_match(line) { continue; } + data.push_str(line); + data.push('\n'); + } + tools::file_set_contents(RESOLV_CONF_FN, data.as_bytes(), None)?; Ok(Value::Null) @@ -98,6 +124,7 @@ pub fn router() -> Router { ObjectSchema::new("Read DNS settings.") ).returns( ObjectSchema::new("Returns DNS server IPs and sreach domain.") + .required("digest", PVE_CONFIG_DIGEST_SCHEMA.clone()) .optional("search", SEARCH_DOMAIN_SCHEMA.clone()) .optional("dns1", FIRST_DNS_SERVER_SCHEMA.clone()) .optional("dns2", SECOND_DNS_SERVER_SCHEMA.clone()) @@ -112,6 +139,7 @@ pub fn router() -> Router { .optional("dns1", FIRST_DNS_SERVER_SCHEMA.clone()) .optional("dns2", SECOND_DNS_SERVER_SCHEMA.clone()) .optional("dns3", THIRD_DNS_SERVER_SCHEMA.clone()) + .optional("digest", PVE_CONFIG_DIGEST_SCHEMA.clone()) ) ); diff --git a/src/tools.rs b/src/tools.rs index 39b3fada..a706d84c 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -412,3 +412,9 @@ pub fn digest_to_hex(digest: &[u8]) -> String { unsafe { String::from_utf8_unchecked(buf) } } +pub fn assert_if_modified(digest1: &str, digest2: &str) -> Result<(), Error> { + if digest1 != digest2 { + bail!("detected modified configuration - file changed by other user? Try again."); + } + Ok(()) +} diff --git a/src/tools/common_regex.rs b/src/tools/common_regex.rs index 887e86be..322476be 100644 --- a/src/tools/common_regex.rs +++ b/src/tools/common_regex.rs @@ -34,4 +34,6 @@ macro_rules! IPRE { () => (concat!(r"(?:", IPV4RE!(), "|", IPV6RE!(), ")")) } lazy_static! { pub static ref IP_REGEX: Regex = Regex::new(IPRE!()).unwrap(); + + pub static ref SHA256_HEX_REGEX: Regex = Regex::new("^[a-f0-9]{64}$").unwrap(); }