api2/node/dns.rs: implement concurrent update protection
This commit is contained in:
		| @ -19,6 +19,12 @@ use crate::tools::common_regex; | ||||
| lazy_static! { | ||||
|     pub static ref IP_FORMAT: Arc<ApiStringFormat> = ApiStringFormat::Pattern(&common_regex::IP_REGEX).into(); | ||||
|  | ||||
|     pub static ref PVE_CONFIG_DIGEST_FORMAT: Arc<ApiStringFormat> = | ||||
|         ApiStringFormat::Pattern(&common_regex::SHA256_HEX_REGEX).into(); | ||||
|  | ||||
|     pub static ref PVE_CONFIG_DIGEST_SCHEMA: Arc<Schema> = | ||||
|         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(); | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -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<Value, Error> { | ||||
|     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<Value, Error> { | ||||
|  | ||||
| fn update_dns(param: Value, _info: &ApiMethod) -> Result<Value, Error> { | ||||
|  | ||||
|     lazy_static! { | ||||
|         static ref MUTEX: Arc<Mutex<usize>> = 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<Value, Error> { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // 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()) | ||||
|              ) | ||||
|         ); | ||||
|  | ||||
|  | ||||
| @ -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(()) | ||||
| } | ||||
|  | ||||
| @ -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(); | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user