From dd16e1dac8976b523c1cf895efe280b73008eca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Gr=C3=BCnbichler?= Date: Tue, 21 Jun 2022 14:35:28 +0200 Subject: [PATCH] extract proxmox-subscription crate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit and add support for signed subscription keys. Signed-off-by: Fabian Grünbichler --- Cargo.toml | 3 +- debian/control | 2 + pbs-buildcfg/src/lib.rs | 4 + src/api2/node/apt.rs | 8 +- src/api2/node/subscription.rs | 139 +++++++++--- src/bin/proxmox-daily-update.rs | 21 +- src/tools/mod.rs | 19 +- src/tools/subscription.rs | 390 -------------------------------- 8 files changed, 135 insertions(+), 451 deletions(-) delete mode 100644 src/tools/subscription.rs diff --git a/Cargo.toml b/Cargo.toml index 709467d2..d0df116a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,7 +93,7 @@ zstd = { version = "0.6", features = [ "bindgen" ] } pathpatterns = "0.1.2" pxar = { version = "0.10.1", features = [ "tokio-io" ] } -proxmox-http = { version = "0.6.4", features = [ "client", "http-helpers", "websocket" ] } +proxmox-http = { version = "0.6.4", features = [ "client", "client-trait", "http-helpers", "proxmox-async", "websocket" ] } proxmox-io = "1" proxmox-lang = "1.1" proxmox-metrics = "0.2" @@ -105,6 +105,7 @@ proxmox-time = "1.1.2" proxmox-uuid = "1" proxmox-serde = { version = "0.1.1", features = [ "serde_json" ] } proxmox-shared-memory = "0.2" +proxmox-subscription = { version = "0.1", features = [ "api-types" ] } proxmox-sys = { version = "0.3.1", features = [ "sortable-macro" ] } proxmox-compression = "0.1" diff --git a/debian/control b/debian/control index 5c33f38a..34cf6d41 100644 --- a/debian/control +++ b/debian/control @@ -65,6 +65,8 @@ Build-Depends: debhelper (>= 12), librust-proxmox-serde-0.1+default-dev (>= 0.1.1-~~), librust-proxmox-serde-0.1+serde-json-dev (>= 0.1.1-~~), librust-proxmox-shared-memory-0.2+default-dev, + librust-proxmox-subscription-0.1+default-dev, + librust-proxmox-subscription-0.1+api-types-dev, librust-proxmox-sys-0.3+default-dev (>= 0.3.1-~~), librust-proxmox-sys-0.3+logrotate-dev (>= 0.3.1-~~), librust-proxmox-sys-0.3+sortable-macro-dev (>= 0.3.1-~~), diff --git a/pbs-buildcfg/src/lib.rs b/pbs-buildcfg/src/lib.rs index d09e273e..8ad2836e 100644 --- a/pbs-buildcfg/src/lib.rs +++ b/pbs-buildcfg/src/lib.rs @@ -90,6 +90,10 @@ pub const PROXMOX_BACKUP_INITRAMFS_DBG_FN: &str = concat!( pub const PROXMOX_BACKUP_KERNEL_FN: &str = concat!(PROXMOX_BACKUP_FILE_RESTORE_BIN_DIR_M!(), "/bzImage"); +pub const PROXMOX_BACKUP_SUBSCRIPTION_FN: &str = configdir!("/subscription"); +pub const PROXMOX_BACKUP_SUBSCRIPTION_SIGNATURE_KEY_FN: &str = + "/usr/share/keyrings/proxmox-offline-signing-key.pub"; + /// Prepend configuration directory to a file name /// /// This is a simply way to get the full path for configuration files. diff --git a/src/api2/node/apt.rs b/src/api2/node/apt.rs index 0267e103..0bb1b3ad 100644 --- a/src/api2/node/apt.rs +++ b/src/api2/node/apt.rs @@ -19,9 +19,10 @@ use pbs_api_types::{ APTUpdateInfo, NODE_SCHEMA, PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, PROXMOX_CONFIG_DIGEST_SCHEMA, UPID_SCHEMA, }; +use pbs_buildcfg::PROXMOX_BACKUP_SUBSCRIPTION_FN; use crate::config::node; -use crate::tools::{apt, pbs_simple_http, subscription}; +use crate::tools::{apt, pbs_simple_http}; use proxmox_rest_server::WorkerTask; #[api( @@ -255,7 +256,10 @@ fn apt_get_changelog(param: Value) -> Result { })?; Ok(json!(changelog)) } else if changelog_url.starts_with("https://enterprise.proxmox.com/") { - let sub = match subscription::read_subscription()? { + let sub = match proxmox_subscription::files::read_subscription( + PROXMOX_BACKUP_SUBSCRIPTION_FN, + &super::subscription::subscription_signature_key()?, + )? { Some(sub) => sub, None => { bail!("cannot retrieve changelog from enterprise repo: no subscription info found") diff --git a/src/api2/node/subscription.rs b/src/api2/node/subscription.rs index 3bd2da44..cba532b9 100644 --- a/src/api2/node/subscription.rs +++ b/src/api2/node/subscription.rs @@ -1,17 +1,87 @@ use anyhow::{bail, format_err, Error}; use serde_json::Value; +use proxmox_http::client::{SimpleHttp, SimpleHttpOptions}; use proxmox_router::{Permission, Router, RpcEnvironment}; use proxmox_schema::api; +use proxmox_subscription::{SubscriptionInfo, SubscriptionStatus}; +use proxmox_sys::fs::{file_get_contents, CreateOptions}; use pbs_api_types::{ Authid, NODE_SCHEMA, PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, SUBSCRIPTION_KEY_SCHEMA, }; -use crate::tools; -use crate::tools::subscription::{self, SubscriptionInfo, SubscriptionStatus}; +use crate::config::node; +use crate::tools::{DEFAULT_USER_AGENT_STRING, PROXMOX_BACKUP_TCP_KEEPALIVE_TIME}; + +use pbs_buildcfg::{PROXMOX_BACKUP_SUBSCRIPTION_FN, PROXMOX_BACKUP_SUBSCRIPTION_SIGNATURE_KEY_FN}; use pbs_config::CachedUserInfo; +const PRODUCT_URL: &str = "https://www.proxmox.com/en/proxmox-backup-server/pricing"; +const APT_AUTH_FN: &str = "/etc/apt/auth.conf.d/pbs.conf"; +const APT_AUTH_URL: &str = "enterprise.proxmox.com/debian/pbs"; + +fn subscription_file_opts() -> Result { + let backup_user = pbs_config::backup_user()?; + let mode = nix::sys::stat::Mode::from_bits_truncate(0o0640); + Ok(CreateOptions::new() + .perm(mode) + .owner(nix::unistd::ROOT) + .group(backup_user.gid)) +} + +fn apt_auth_file_opts() -> CreateOptions { + let mode = nix::sys::stat::Mode::from_bits_truncate(0o0600); + CreateOptions::new().perm(mode).owner(nix::unistd::ROOT) +} + +pub(crate) fn subscription_signature_key() -> Result, Error> { + let key = file_get_contents(PROXMOX_BACKUP_SUBSCRIPTION_SIGNATURE_KEY_FN)?; + openssl::pkey::PKey::public_key_from_pem(&key).map_err(|err| { + format_err!( + "Failed parsing public key from '{}' - {}", + PROXMOX_BACKUP_SUBSCRIPTION_SIGNATURE_KEY_FN, + err + ) + }) +} + +fn check_and_write_subscription(key: String, server_id: String) -> Result<(), Error> { + let proxy_config = if let Ok((node_config, _digest)) = node::config() { + node_config.http_proxy() + } else { + None + }; + + let client = SimpleHttp::with_options(SimpleHttpOptions { + proxy_config, + user_agent: Some(DEFAULT_USER_AGENT_STRING.to_string()), + tcp_keepalive: Some(PROXMOX_BACKUP_TCP_KEEPALIVE_TIME), + }); + + let info = proxmox_subscription::check::check_subscription( + key, + server_id, + PRODUCT_URL.to_string(), + client, + )?; + + proxmox_subscription::files::write_subscription( + PROXMOX_BACKUP_SUBSCRIPTION_FN, + subscription_file_opts()?, + &info, + ) + .map_err(|e| format_err!("Error writing updated subscription status - {}", e))?; + + proxmox_subscription::files::update_apt_auth( + APT_AUTH_FN, + apt_auth_file_opts(), + APT_AUTH_URL, + info.key, + info.serverid, + ) +} + #[api( input: { properties: { @@ -33,34 +103,39 @@ use pbs_config::CachedUserInfo; )] /// Check and update subscription status. pub fn check_subscription(force: bool) -> Result<(), Error> { - let info = match subscription::read_subscription() { + let mut info = match proxmox_subscription::files::read_subscription( + PROXMOX_BACKUP_SUBSCRIPTION_FN, + &subscription_signature_key()?, + ) { Err(err) => bail!("could not read subscription status: {}", err), Ok(Some(info)) => info, Ok(None) => return Ok(()), }; - let server_id = tools::get_hardware_address()?; - let key = if let Some(key) = info.key { + let server_id = proxmox_subscription::get_hardware_address()?; + let key = if let Some(key) = info.key.as_ref() { // always update apt auth if we have a key to ensure user can access enterprise repo - subscription::update_apt_auth(Some(key.to_owned()), Some(server_id.to_owned()))?; - key + proxmox_subscription::files::update_apt_auth( + APT_AUTH_FN, + apt_auth_file_opts(), + APT_AUTH_URL, + Some(key.to_owned()), + Some(server_id.to_owned()), + )?; + key.to_owned() } else { String::new() }; - if !force && info.status == SubscriptionStatus::ACTIVE { - let age = proxmox_time::epoch_i64() - info.checktime.unwrap_or(i64::MAX); - if age < subscription::MAX_LOCAL_KEY_AGE { + if !force && info.status == SubscriptionStatus::Active { + // will set to INVALID if last check too long ago + info.check_age(true); + if info.status == SubscriptionStatus::Active { return Ok(()); } } - let info = subscription::check_subscription(key, server_id)?; - - subscription::write_subscription(info) - .map_err(|e| format_err!("Error writing updated subscription status - {}", e))?; - - Ok(()) + check_and_write_subscription(key, server_id) } #[api( @@ -81,16 +156,17 @@ pub fn get_subscription( _param: Value, rpcenv: &mut dyn RpcEnvironment, ) -> Result { - let url = "https://www.proxmox.com/en/proxmox-backup-server/pricing"; - - let info = match subscription::read_subscription() { + let info = match proxmox_subscription::files::read_subscription( + PROXMOX_BACKUP_SUBSCRIPTION_FN, + &subscription_signature_key()?, + ) { Err(err) => bail!("could not read subscription status: {}", err), Ok(Some(info)) => info, Ok(None) => SubscriptionInfo { - status: SubscriptionStatus::NOTFOUND, + status: SubscriptionStatus::NotFound, message: Some("There is no subscription key".into()), - serverid: Some(tools::get_hardware_address()?), - url: Some(url.into()), + serverid: Some(proxmox_subscription::get_hardware_address()?), + url: Some(PRODUCT_URL.into()), ..Default::default() }, }; @@ -130,14 +206,9 @@ pub fn get_subscription( )] /// Set a subscription key and check it. pub fn set_subscription(key: String) -> Result<(), Error> { - let server_id = tools::get_hardware_address()?; + let server_id = proxmox_subscription::get_hardware_address()?; - let info = subscription::check_subscription(key, server_id)?; - - subscription::write_subscription(info) - .map_err(|e| format_err!("Error writing subscription status - {}", e))?; - - Ok(()) + check_and_write_subscription(key, server_id) } #[api( @@ -155,9 +226,17 @@ pub fn set_subscription(key: String) -> Result<(), Error> { )] /// Delete subscription info. pub fn delete_subscription() -> Result<(), Error> { - subscription::delete_subscription() + proxmox_subscription::files::delete_subscription(PROXMOX_BACKUP_SUBSCRIPTION_FN) .map_err(|err| format_err!("Deleting subscription failed: {}", err))?; + proxmox_subscription::files::update_apt_auth( + APT_AUTH_FN, + apt_auth_file_opts(), + APT_AUTH_URL, + None, + None, + )?; + Ok(()) } diff --git a/src/bin/proxmox-daily-update.rs b/src/bin/proxmox-daily-update.rs index eabb4ec6..32c1d1ee 100644 --- a/src/bin/proxmox-daily-update.rs +++ b/src/bin/proxmox-daily-update.rs @@ -5,7 +5,6 @@ use proxmox_router::{cli::*, ApiHandler, RpcEnvironment}; use proxmox_sys::fs::CreateOptions; use proxmox_backup::api2; -use proxmox_backup::tools::subscription; async fn wait_for_local_worker(upid_str: &str) -> Result<(), Error> { let upid: pbs_api_types::UPID = upid_str.parse()?; @@ -27,20 +26,22 @@ async fn do_update(rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> { let method = &api2::node::subscription::API_METHOD_CHECK_SUBSCRIPTION; match method.handler { ApiHandler::Sync(handler) => { - if let Err(err) = (handler)(param, method, rpcenv) { + if let Err(err) = (handler)(param.clone(), method, rpcenv) { log::error!("Error checking subscription - {}", err); } } _ => unreachable!(), } - - let notify = match subscription::read_subscription() { - Ok(Some(subscription)) => subscription.status == subscription::SubscriptionStatus::ACTIVE, - Ok(None) => false, - Err(err) => { - log::error!("Error reading subscription - {}", err); - false - } + let method = &api2::node::subscription::API_METHOD_GET_SUBSCRIPTION; + let notify = match method.handler { + ApiHandler::Sync(handler) => match (handler)(param, method, rpcenv) { + Ok(value) => !value.is_null(), + Err(err) => { + log::error!("Error reading subscription - {}", err); + false + } + }, + _ => unreachable!(), }; let param = json!({ diff --git a/src/tools/mod.rs b/src/tools/mod.rs index 35946bd6..408249c2 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -3,8 +3,7 @@ //! This is a collection of small and useful tools. use std::any::Any; -use anyhow::{bail, format_err, Error}; -use openssl::hash::{hash, DigestBytes, MessageDigest}; +use anyhow::{bail, Error}; use proxmox_http::{client::SimpleHttp, client::SimpleHttpOptions, ProxyConfig}; @@ -17,27 +16,11 @@ mod shared_rate_limiter; pub use shared_rate_limiter::SharedRateLimiter; pub mod statistics; -pub mod subscription; pub mod systemd; pub mod ticket; pub mod parallel_handler; -/// Shortcut for md5 sums. -pub fn md5sum(data: &[u8]) -> Result { - hash(MessageDigest::md5(), data).map_err(Error::from) -} - -pub fn get_hardware_address() -> Result { - static FILENAME: &str = "/etc/ssh/ssh_host_rsa_key.pub"; - - let contents = proxmox_sys::fs::file_get_contents(FILENAME) - .map_err(|e| format_err!("Error getting host key - {}", e))?; - let digest = md5sum(&contents).map_err(|e| format_err!("Error digesting host key - {}", e))?; - - Ok(hex::encode(&digest).to_uppercase()) -} - 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."); diff --git a/src/tools/subscription.rs b/src/tools/subscription.rs deleted file mode 100644 index 0cc112dc..00000000 --- a/src/tools/subscription.rs +++ /dev/null @@ -1,390 +0,0 @@ -use anyhow::{bail, format_err, Error}; -use lazy_static::lazy_static; -use regex::Regex; -use serde::{Deserialize, Serialize}; -use serde_json::json; - -use proxmox_schema::api; - -use proxmox_http::client::SimpleHttp; -use proxmox_http::uri::json_object_to_query; -use proxmox_sys::fs::{replace_file, CreateOptions}; - -use crate::config::node; -use crate::tools::{self, pbs_simple_http}; - -/// How long the local key is valid for in between remote checks -pub const MAX_LOCAL_KEY_AGE: i64 = 15 * 24 * 3600; -const MAX_KEY_CHECK_FAILURE_AGE: i64 = 5 * 24 * 3600; - -const SHARED_KEY_DATA: &str = "kjfdlskfhiuewhfk947368"; -const SUBSCRIPTION_FN: &str = "/etc/proxmox-backup/subscription"; -const APT_AUTH_FN: &str = "/etc/apt/auth.conf.d/pbs.conf"; - -#[api()] -#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -/// Subscription status -pub enum SubscriptionStatus { - // FIXME: remove? - /// newly set subscription, not yet checked - NEW, - /// no subscription set - NOTFOUND, - /// subscription set and active - ACTIVE, - /// subscription set but invalid for this server - INVALID, -} -impl Default for SubscriptionStatus { - fn default() -> Self { - SubscriptionStatus::NOTFOUND - } -} -impl std::fmt::Display for SubscriptionStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SubscriptionStatus::NEW => write!(f, "New"), - SubscriptionStatus::NOTFOUND => write!(f, "NotFound"), - SubscriptionStatus::ACTIVE => write!(f, "Active"), - SubscriptionStatus::INVALID => write!(f, "Invalid"), - } - } -} - -#[api( - properties: { - status: { - type: SubscriptionStatus, - }, - }, -)] -#[derive(Debug, Default, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -/// Proxmox subscription information -pub struct SubscriptionInfo { - /// Subscription status from the last check - pub status: SubscriptionStatus, - /// the server ID, if permitted to access - #[serde(skip_serializing_if = "Option::is_none")] - pub serverid: Option, - /// timestamp of the last check done - #[serde(skip_serializing_if = "Option::is_none")] - pub checktime: Option, - /// the subscription key, if set and permitted to access - #[serde(skip_serializing_if = "Option::is_none")] - pub key: Option, - /// a more human readable status message - #[serde(skip_serializing_if = "Option::is_none")] - pub message: Option, - /// human readable productname of the set subscription - #[serde(skip_serializing_if = "Option::is_none")] - pub productname: Option, - /// register date of the set subscription - #[serde(skip_serializing_if = "Option::is_none")] - pub regdate: Option, - /// next due date of the set subscription - #[serde(skip_serializing_if = "Option::is_none")] - pub nextduedate: Option, - /// URL to the web shop - #[serde(skip_serializing_if = "Option::is_none")] - pub url: Option, -} - -async fn register_subscription( - key: &str, - server_id: &str, - checktime: i64, -) -> Result<(String, String), Error> { - // WHCMS sample code feeds the key into this, but it's just a challenge, so keep it simple - let rand = hex::encode(&proxmox_sys::linux::random_data(16)?); - let challenge = format!("{}{}", checktime, rand); - - let params = json!({ - "licensekey": key, - "dir": server_id, - "domain": "www.proxmox.com", - "ip": "localhost", - "check_token": challenge, - }); - - let proxy_config = if let Ok((node_config, _digest)) = node::config() { - node_config.http_proxy() - } else { - None - }; - - let client = pbs_simple_http(proxy_config); - - let uri = "https://shop.proxmox.com/modules/servers/licensing/verify.php"; - let query = json_object_to_query(params)?; - let response = client - .post(uri, Some(query), Some("application/x-www-form-urlencoded")) - .await?; - let body = SimpleHttp::response_body_string(response).await?; - - Ok((body, challenge)) -} - -fn parse_status(value: &str) -> SubscriptionStatus { - match value.to_lowercase().as_str() { - "active" => SubscriptionStatus::ACTIVE, - "new" => SubscriptionStatus::NEW, - "notfound" => SubscriptionStatus::NOTFOUND, - "invalid" => SubscriptionStatus::INVALID, - _ => SubscriptionStatus::INVALID, - } -} - -fn parse_register_response( - body: &str, - key: String, - server_id: String, - checktime: i64, - challenge: &str, -) -> Result { - lazy_static! { - static ref ATTR_RE: Regex = Regex::new(r"<([^>]+)>([^<]+)]+>").unwrap(); - } - - let mut info = SubscriptionInfo { - key: Some(key), - status: SubscriptionStatus::NOTFOUND, - checktime: Some(checktime), - url: Some("https://www.proxmox.com/en/proxmox-backup-server/pricing".into()), - ..Default::default() - }; - let mut md5hash = String::new(); - let is_server_id = |id: &&str| *id == server_id; - - for caps in ATTR_RE.captures_iter(body) { - let (key, value) = (&caps[1], &caps[2]); - match key { - "status" => info.status = parse_status(value), - "productname" => info.productname = Some(value.into()), - "regdate" => info.regdate = Some(value.into()), - "nextduedate" => info.nextduedate = Some(value.into()), - "message" if value == "Directory Invalid" => { - info.message = Some("Invalid Server ID".into()) - } - "message" => info.message = Some(value.into()), - "validdirectory" => { - if value.split(',').find(is_server_id) == None { - bail!("Server ID does not match"); - } - info.serverid = Some(server_id.to_owned()); - } - "md5hash" => md5hash = value.to_owned(), - _ => (), - } - } - - if let SubscriptionStatus::ACTIVE = info.status { - let response_raw = format!("{}{}", SHARED_KEY_DATA, challenge); - let expected = hex::encode(&tools::md5sum(response_raw.as_bytes())?); - if expected != md5hash { - bail!( - "Subscription API challenge failed, expected {} != got {}", - expected, - md5hash - ); - } - } - Ok(info) -} - -#[test] -fn test_parse_register_response() -> Result<(), Error> { - let response = r#" -Active -Proxmox -41108 -71 -Proxmox Backup Server Test Subscription -1 year -2020-09-19 00:00:00 -2021-09-19 -Annually -proxmox.com,www.proxmox.com -830000000123456789ABCDEF00000042 -Notes=Test Key! - -969f4df84fe157ee4f5a2f71950ad154 -"#; - let key = "pbst-123456789a".to_string(); - let server_id = "830000000123456789ABCDEF00000042".to_string(); - let checktime = 1600000000; - let salt = "cf44486bddb6ad0145732642c45b2957"; - - let info = parse_register_response( - response, - key.to_owned(), - server_id.to_owned(), - checktime, - salt, - )?; - - assert_eq!( - info, - SubscriptionInfo { - key: Some(key), - serverid: Some(server_id), - status: SubscriptionStatus::ACTIVE, - checktime: Some(checktime), - url: Some("https://www.proxmox.com/en/proxmox-backup-server/pricing".into()), - message: None, - nextduedate: Some("2021-09-19".into()), - regdate: Some("2020-09-19 00:00:00".into()), - productname: Some("Proxmox Backup Server Test Subscription -1 year".into()), - } - ); - Ok(()) -} - -/// queries the up to date subscription status and parses the response -pub fn check_subscription(key: String, server_id: String) -> Result { - let now = proxmox_time::epoch_i64(); - - let (response, challenge) = - proxmox_async::runtime::block_on(register_subscription(&key, &server_id, now)) - .map_err(|err| format_err!("Error checking subscription: {}", err))?; - - parse_register_response(&response, key, server_id, now, &challenge) - .map_err(|err| format_err!("Error parsing subscription check response: {}", err)) -} - -/// reads in subscription information and does a basic integrity verification -pub fn read_subscription() -> Result, Error> { - let cfg = proxmox_sys::fs::file_read_optional_string(&SUBSCRIPTION_FN)?; - let cfg = if let Some(cfg) = cfg { - cfg - } else { - return Ok(None); - }; - - let mut cfg = cfg.lines(); - - // first line is key in plain - let _key = if let Some(key) = cfg.next() { - key - } else { - return Ok(None); - }; - // second line is checksum of encoded data - let checksum = if let Some(csum) = cfg.next() { - csum - } else { - return Ok(None); - }; - - let encoded: String = cfg.collect::(); - let decoded = base64::decode(&encoded)?; - let decoded = std::str::from_utf8(&decoded)?; - - let info: SubscriptionInfo = serde_json::from_str(decoded)?; - - let new_checksum = format!( - "{}{}{}", - info.checktime.unwrap_or(0), - encoded, - SHARED_KEY_DATA - ); - let new_checksum = base64::encode(tools::md5sum(new_checksum.as_bytes())?); - - if checksum != new_checksum { - return Ok(Some(SubscriptionInfo { - status: SubscriptionStatus::INVALID, - message: Some("checksum mismatch".to_string()), - ..info - })); - } - - let age = proxmox_time::epoch_i64() - info.checktime.unwrap_or(0); - if age < -5400 { - // allow some delta for DST changes or time syncs, 1.5h - return Ok(Some(SubscriptionInfo { - status: SubscriptionStatus::INVALID, - message: Some("last check date too far in the future".to_string()), - ..info - })); - } else if age > MAX_LOCAL_KEY_AGE + MAX_KEY_CHECK_FAILURE_AGE { - if let SubscriptionStatus::ACTIVE = info.status { - return Ok(Some(SubscriptionInfo { - status: SubscriptionStatus::INVALID, - message: Some("subscription information too old".to_string()), - ..info - })); - } - } - - Ok(Some(info)) -} - -/// writes out subscription status -pub fn write_subscription(info: SubscriptionInfo) -> Result<(), Error> { - let key = info.key.to_owned(); - let server_id = info.serverid.to_owned(); - - let raw = if info.key == None || info.checktime == None { - String::new() - } else if let SubscriptionStatus::NEW = info.status { - format!("{}\n", info.key.unwrap()) - } else { - let encoded = base64::encode(serde_json::to_string(&info)?); - let csum = format!( - "{}{}{}", - info.checktime.unwrap_or(0), - encoded, - SHARED_KEY_DATA - ); - let csum = base64::encode(tools::md5sum(csum.as_bytes())?); - format!("{}\n{}\n{}\n", info.key.unwrap(), csum, encoded) - }; - - let backup_user = pbs_config::backup_user()?; - let mode = nix::sys::stat::Mode::from_bits_truncate(0o0640); - let file_opts = CreateOptions::new() - .perm(mode) - .owner(nix::unistd::ROOT) - .group(backup_user.gid); - - let subscription_file = std::path::Path::new(SUBSCRIPTION_FN); - replace_file(subscription_file, raw.as_bytes(), file_opts, true)?; - - update_apt_auth(key, server_id)?; - - Ok(()) -} - -/// deletes subscription from server -pub fn delete_subscription() -> Result<(), Error> { - let subscription_file = std::path::Path::new(SUBSCRIPTION_FN); - nix::unistd::unlink(subscription_file)?; - update_apt_auth(None, None)?; - Ok(()) -} - -/// updates apt authentication for repo access -pub fn update_apt_auth(key: Option, password: Option) -> Result<(), Error> { - let auth_conf = std::path::Path::new(APT_AUTH_FN); - match (key, password) { - (Some(key), Some(password)) => { - let conf = format!( - "machine enterprise.proxmox.com/debian/pbs\n login {}\n password {}\n", - key, password, - ); - let mode = nix::sys::stat::Mode::from_bits_truncate(0o0640); - let file_opts = CreateOptions::new().perm(mode).owner(nix::unistd::ROOT); - - // we use a namespaced .conf file, so just overwrite.. - replace_file(auth_conf, conf.as_bytes(), file_opts, true) - .map_err(|e| format_err!("Error saving apt auth config - {}", e))?; - } - _ => match nix::unistd::unlink(auth_conf) { - Ok(()) => Ok(()), - Err(nix::errno::Errno::ENOENT) => Ok(()), // ignore not existing - Err(err) => Err(err), - } - .map_err(|e| format_err!("Error clearing apt auth config - {}", e))?, - } - Ok(()) -}