mirror of
https://git.proxmox.com/git/proxmox-backup
synced 2025-04-30 12:34:21 +00:00
extract proxmox-subscription crate
and add support for signed subscription keys. Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
This commit is contained in:
parent
9b3b3c88a9
commit
dd16e1dac8
@ -93,7 +93,7 @@ zstd = { version = "0.6", features = [ "bindgen" ] }
|
|||||||
pathpatterns = "0.1.2"
|
pathpatterns = "0.1.2"
|
||||||
pxar = { version = "0.10.1", features = [ "tokio-io" ] }
|
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-io = "1"
|
||||||
proxmox-lang = "1.1"
|
proxmox-lang = "1.1"
|
||||||
proxmox-metrics = "0.2"
|
proxmox-metrics = "0.2"
|
||||||
@ -105,6 +105,7 @@ proxmox-time = "1.1.2"
|
|||||||
proxmox-uuid = "1"
|
proxmox-uuid = "1"
|
||||||
proxmox-serde = { version = "0.1.1", features = [ "serde_json" ] }
|
proxmox-serde = { version = "0.1.1", features = [ "serde_json" ] }
|
||||||
proxmox-shared-memory = "0.2"
|
proxmox-shared-memory = "0.2"
|
||||||
|
proxmox-subscription = { version = "0.1", features = [ "api-types" ] }
|
||||||
proxmox-sys = { version = "0.3.1", features = [ "sortable-macro" ] }
|
proxmox-sys = { version = "0.3.1", features = [ "sortable-macro" ] }
|
||||||
proxmox-compression = "0.1"
|
proxmox-compression = "0.1"
|
||||||
|
|
||||||
|
2
debian/control
vendored
2
debian/control
vendored
@ -65,6 +65,8 @@ Build-Depends: debhelper (>= 12),
|
|||||||
librust-proxmox-serde-0.1+default-dev (>= 0.1.1-~~),
|
librust-proxmox-serde-0.1+default-dev (>= 0.1.1-~~),
|
||||||
librust-proxmox-serde-0.1+serde-json-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-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+default-dev (>= 0.3.1-~~),
|
||||||
librust-proxmox-sys-0.3+logrotate-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-~~),
|
librust-proxmox-sys-0.3+sortable-macro-dev (>= 0.3.1-~~),
|
||||||
|
@ -90,6 +90,10 @@ pub const PROXMOX_BACKUP_INITRAMFS_DBG_FN: &str = concat!(
|
|||||||
pub const PROXMOX_BACKUP_KERNEL_FN: &str =
|
pub const PROXMOX_BACKUP_KERNEL_FN: &str =
|
||||||
concat!(PROXMOX_BACKUP_FILE_RESTORE_BIN_DIR_M!(), "/bzImage");
|
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
|
/// Prepend configuration directory to a file name
|
||||||
///
|
///
|
||||||
/// This is a simply way to get the full path for configuration files.
|
/// This is a simply way to get the full path for configuration files.
|
||||||
|
@ -19,9 +19,10 @@ use pbs_api_types::{
|
|||||||
APTUpdateInfo, NODE_SCHEMA, PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, PROXMOX_CONFIG_DIGEST_SCHEMA,
|
APTUpdateInfo, NODE_SCHEMA, PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, PROXMOX_CONFIG_DIGEST_SCHEMA,
|
||||||
UPID_SCHEMA,
|
UPID_SCHEMA,
|
||||||
};
|
};
|
||||||
|
use pbs_buildcfg::PROXMOX_BACKUP_SUBSCRIPTION_FN;
|
||||||
|
|
||||||
use crate::config::node;
|
use crate::config::node;
|
||||||
use crate::tools::{apt, pbs_simple_http, subscription};
|
use crate::tools::{apt, pbs_simple_http};
|
||||||
use proxmox_rest_server::WorkerTask;
|
use proxmox_rest_server::WorkerTask;
|
||||||
|
|
||||||
#[api(
|
#[api(
|
||||||
@ -255,7 +256,10 @@ fn apt_get_changelog(param: Value) -> Result<Value, Error> {
|
|||||||
})?;
|
})?;
|
||||||
Ok(json!(changelog))
|
Ok(json!(changelog))
|
||||||
} else if changelog_url.starts_with("https://enterprise.proxmox.com/") {
|
} 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,
|
Some(sub) => sub,
|
||||||
None => {
|
None => {
|
||||||
bail!("cannot retrieve changelog from enterprise repo: no subscription info found")
|
bail!("cannot retrieve changelog from enterprise repo: no subscription info found")
|
||||||
|
@ -1,17 +1,87 @@
|
|||||||
use anyhow::{bail, format_err, Error};
|
use anyhow::{bail, format_err, Error};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use proxmox_http::client::{SimpleHttp, SimpleHttpOptions};
|
||||||
use proxmox_router::{Permission, Router, RpcEnvironment};
|
use proxmox_router::{Permission, Router, RpcEnvironment};
|
||||||
use proxmox_schema::api;
|
use proxmox_schema::api;
|
||||||
|
use proxmox_subscription::{SubscriptionInfo, SubscriptionStatus};
|
||||||
|
use proxmox_sys::fs::{file_get_contents, CreateOptions};
|
||||||
|
|
||||||
use pbs_api_types::{
|
use pbs_api_types::{
|
||||||
Authid, NODE_SCHEMA, PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, SUBSCRIPTION_KEY_SCHEMA,
|
Authid, NODE_SCHEMA, PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, SUBSCRIPTION_KEY_SCHEMA,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::tools;
|
use crate::config::node;
|
||||||
use crate::tools::subscription::{self, SubscriptionInfo, SubscriptionStatus};
|
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;
|
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<CreateOptions, Error> {
|
||||||
|
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<openssl::pkey::PKey<openssl::pkey::Public>, 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(
|
#[api(
|
||||||
input: {
|
input: {
|
||||||
properties: {
|
properties: {
|
||||||
@ -33,34 +103,39 @@ use pbs_config::CachedUserInfo;
|
|||||||
)]
|
)]
|
||||||
/// Check and update subscription status.
|
/// Check and update subscription status.
|
||||||
pub fn check_subscription(force: bool) -> Result<(), Error> {
|
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),
|
Err(err) => bail!("could not read subscription status: {}", err),
|
||||||
Ok(Some(info)) => info,
|
Ok(Some(info)) => info,
|
||||||
Ok(None) => return Ok(()),
|
Ok(None) => return Ok(()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let server_id = tools::get_hardware_address()?;
|
let server_id = proxmox_subscription::get_hardware_address()?;
|
||||||
let key = if let Some(key) = info.key {
|
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
|
// 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()))?;
|
proxmox_subscription::files::update_apt_auth(
|
||||||
key
|
APT_AUTH_FN,
|
||||||
|
apt_auth_file_opts(),
|
||||||
|
APT_AUTH_URL,
|
||||||
|
Some(key.to_owned()),
|
||||||
|
Some(server_id.to_owned()),
|
||||||
|
)?;
|
||||||
|
key.to_owned()
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
if !force && info.status == SubscriptionStatus::ACTIVE {
|
if !force && info.status == SubscriptionStatus::Active {
|
||||||
let age = proxmox_time::epoch_i64() - info.checktime.unwrap_or(i64::MAX);
|
// will set to INVALID if last check too long ago
|
||||||
if age < subscription::MAX_LOCAL_KEY_AGE {
|
info.check_age(true);
|
||||||
|
if info.status == SubscriptionStatus::Active {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let info = subscription::check_subscription(key, server_id)?;
|
check_and_write_subscription(key, server_id)
|
||||||
|
|
||||||
subscription::write_subscription(info)
|
|
||||||
.map_err(|e| format_err!("Error writing updated subscription status - {}", e))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[api(
|
#[api(
|
||||||
@ -81,16 +156,17 @@ pub fn get_subscription(
|
|||||||
_param: Value,
|
_param: Value,
|
||||||
rpcenv: &mut dyn RpcEnvironment,
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
) -> Result<SubscriptionInfo, Error> {
|
) -> Result<SubscriptionInfo, Error> {
|
||||||
let url = "https://www.proxmox.com/en/proxmox-backup-server/pricing";
|
let info = match proxmox_subscription::files::read_subscription(
|
||||||
|
PROXMOX_BACKUP_SUBSCRIPTION_FN,
|
||||||
let info = match subscription::read_subscription() {
|
&subscription_signature_key()?,
|
||||||
|
) {
|
||||||
Err(err) => bail!("could not read subscription status: {}", err),
|
Err(err) => bail!("could not read subscription status: {}", err),
|
||||||
Ok(Some(info)) => info,
|
Ok(Some(info)) => info,
|
||||||
Ok(None) => SubscriptionInfo {
|
Ok(None) => SubscriptionInfo {
|
||||||
status: SubscriptionStatus::NOTFOUND,
|
status: SubscriptionStatus::NotFound,
|
||||||
message: Some("There is no subscription key".into()),
|
message: Some("There is no subscription key".into()),
|
||||||
serverid: Some(tools::get_hardware_address()?),
|
serverid: Some(proxmox_subscription::get_hardware_address()?),
|
||||||
url: Some(url.into()),
|
url: Some(PRODUCT_URL.into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -130,14 +206,9 @@ pub fn get_subscription(
|
|||||||
)]
|
)]
|
||||||
/// Set a subscription key and check it.
|
/// Set a subscription key and check it.
|
||||||
pub fn set_subscription(key: String) -> Result<(), Error> {
|
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)?;
|
check_and_write_subscription(key, server_id)
|
||||||
|
|
||||||
subscription::write_subscription(info)
|
|
||||||
.map_err(|e| format_err!("Error writing subscription status - {}", e))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[api(
|
#[api(
|
||||||
@ -155,9 +226,17 @@ pub fn set_subscription(key: String) -> Result<(), Error> {
|
|||||||
)]
|
)]
|
||||||
/// Delete subscription info.
|
/// Delete subscription info.
|
||||||
pub fn delete_subscription() -> Result<(), Error> {
|
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))?;
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ use proxmox_router::{cli::*, ApiHandler, RpcEnvironment};
|
|||||||
use proxmox_sys::fs::CreateOptions;
|
use proxmox_sys::fs::CreateOptions;
|
||||||
|
|
||||||
use proxmox_backup::api2;
|
use proxmox_backup::api2;
|
||||||
use proxmox_backup::tools::subscription;
|
|
||||||
|
|
||||||
async fn wait_for_local_worker(upid_str: &str) -> Result<(), Error> {
|
async fn wait_for_local_worker(upid_str: &str) -> Result<(), Error> {
|
||||||
let upid: pbs_api_types::UPID = upid_str.parse()?;
|
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;
|
let method = &api2::node::subscription::API_METHOD_CHECK_SUBSCRIPTION;
|
||||||
match method.handler {
|
match method.handler {
|
||||||
ApiHandler::Sync(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);
|
log::error!("Error checking subscription - {}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
|
let method = &api2::node::subscription::API_METHOD_GET_SUBSCRIPTION;
|
||||||
let notify = match subscription::read_subscription() {
|
let notify = match method.handler {
|
||||||
Ok(Some(subscription)) => subscription.status == subscription::SubscriptionStatus::ACTIVE,
|
ApiHandler::Sync(handler) => match (handler)(param, method, rpcenv) {
|
||||||
Ok(None) => false,
|
Ok(value) => !value.is_null(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("Error reading subscription - {}", err);
|
log::error!("Error reading subscription - {}", err);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let param = json!({
|
let param = json!({
|
||||||
|
@ -3,8 +3,7 @@
|
|||||||
//! This is a collection of small and useful tools.
|
//! This is a collection of small and useful tools.
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
|
||||||
use anyhow::{bail, format_err, Error};
|
use anyhow::{bail, Error};
|
||||||
use openssl::hash::{hash, DigestBytes, MessageDigest};
|
|
||||||
|
|
||||||
use proxmox_http::{client::SimpleHttp, client::SimpleHttpOptions, ProxyConfig};
|
use proxmox_http::{client::SimpleHttp, client::SimpleHttpOptions, ProxyConfig};
|
||||||
|
|
||||||
@ -17,27 +16,11 @@ mod shared_rate_limiter;
|
|||||||
pub use shared_rate_limiter::SharedRateLimiter;
|
pub use shared_rate_limiter::SharedRateLimiter;
|
||||||
|
|
||||||
pub mod statistics;
|
pub mod statistics;
|
||||||
pub mod subscription;
|
|
||||||
pub mod systemd;
|
pub mod systemd;
|
||||||
pub mod ticket;
|
pub mod ticket;
|
||||||
|
|
||||||
pub mod parallel_handler;
|
pub mod parallel_handler;
|
||||||
|
|
||||||
/// Shortcut for md5 sums.
|
|
||||||
pub fn md5sum(data: &[u8]) -> Result<DigestBytes, Error> {
|
|
||||||
hash(MessageDigest::md5(), data).map_err(Error::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_hardware_address() -> Result<String, Error> {
|
|
||||||
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> {
|
pub fn assert_if_modified(digest1: &str, digest2: &str) -> Result<(), Error> {
|
||||||
if digest1 != digest2 {
|
if digest1 != digest2 {
|
||||||
bail!("detected modified configuration - file changed by other user? Try again.");
|
bail!("detected modified configuration - file changed by other user? Try again.");
|
||||||
|
@ -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<String>,
|
|
||||||
/// timestamp of the last check done
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub checktime: Option<i64>,
|
|
||||||
/// the subscription key, if set and permitted to access
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub key: Option<String>,
|
|
||||||
/// a more human readable status message
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub message: Option<String>,
|
|
||||||
/// human readable productname of the set subscription
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub productname: Option<String>,
|
|
||||||
/// register date of the set subscription
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub regdate: Option<String>,
|
|
||||||
/// next due date of the set subscription
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub nextduedate: Option<String>,
|
|
||||||
/// URL to the web shop
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub url: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<SubscriptionInfo, Error> {
|
|
||||||
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#"
|
|
||||||
<status>Active</status>
|
|
||||||
<companyname>Proxmox</companyname>
|
|
||||||
<serviceid>41108</serviceid>
|
|
||||||
<productid>71</productid>
|
|
||||||
<productname>Proxmox Backup Server Test Subscription -1 year</productname>
|
|
||||||
<regdate>2020-09-19 00:00:00</regdate>
|
|
||||||
<nextduedate>2021-09-19</nextduedate>
|
|
||||||
<billingcycle>Annually</billingcycle>
|
|
||||||
<validdomain>proxmox.com,www.proxmox.com</validdomain>
|
|
||||||
<validdirectory>830000000123456789ABCDEF00000042</validdirectory>
|
|
||||||
<customfields>Notes=Test Key!</customfields>
|
|
||||||
<addons></addons>
|
|
||||||
<md5hash>969f4df84fe157ee4f5a2f71950ad154</md5hash>
|
|
||||||
"#;
|
|
||||||
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<SubscriptionInfo, Error> {
|
|
||||||
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<Option<SubscriptionInfo>, 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::<String>();
|
|
||||||
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<String>, password: Option<String>) -> 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(())
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user