1 use anyhow
::{Error, format_err, bail}
;
2 use lazy_static
::lazy_static
;
4 use serde
::{Deserialize, Serialize}
;
9 use crate::config
::node
;
14 use proxmox
::tools
::fs
::{replace_file, CreateOptions}
;
15 use proxmox_http
::client
::SimpleHttp
;
17 /// How long the local key is valid for in between remote checks
18 pub const MAX_LOCAL_KEY_AGE
: i64 = 15 * 24 * 3600;
19 const MAX_KEY_CHECK_FAILURE_AGE
: i64 = 5 * 24 * 3600;
21 const SHARED_KEY_DATA
: &str = "kjfdlskfhiuewhfk947368";
22 const SUBSCRIPTION_FN
: &str = "/etc/proxmox-backup/subscription";
23 const APT_AUTH_FN
: &str = "/etc/apt/auth.conf.d/pbs.conf";
26 #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
27 #[serde(rename_all = "lowercase")]
28 /// Subscription status
29 pub enum SubscriptionStatus
{
31 /// newly set subscription, not yet checked
33 /// no subscription set
35 /// subscription set and active
37 /// subscription set but invalid for this server
40 impl Default
for SubscriptionStatus
{
41 fn default() -> Self { SubscriptionStatus::NOTFOUND }
43 impl std
::fmt
::Display
for SubscriptionStatus
{
44 fn fmt(&self, f
: &mut std
::fmt
::Formatter
<'_
>) -> std
::fmt
::Result
{
46 SubscriptionStatus
::NEW
=> write
!(f
, "New"),
47 SubscriptionStatus
::NOTFOUND
=> write
!(f
, "NotFound"),
48 SubscriptionStatus
::ACTIVE
=> write
!(f
, "Active"),
49 SubscriptionStatus
::INVALID
=> write
!(f
, "Invalid"),
57 type: SubscriptionStatus
,
61 #[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
62 #[serde(rename_all="kebab-case")]
63 /// Proxmox subscription information
64 pub struct SubscriptionInfo
{
65 /// Subscription status from the last check
66 pub status
: SubscriptionStatus
,
67 /// the server ID, if permitted to access
68 #[serde(skip_serializing_if="Option::is_none")]
69 pub serverid
: Option
<String
>,
70 /// timestamp of the last check done
71 #[serde(skip_serializing_if="Option::is_none")]
72 pub checktime
: Option
<i64>,
73 /// the subscription key, if set and permitted to access
74 #[serde(skip_serializing_if="Option::is_none")]
75 pub key
: Option
<String
>,
76 /// a more human readable status message
77 #[serde(skip_serializing_if="Option::is_none")]
78 pub message
: Option
<String
>,
79 /// human readable productname of the set subscription
80 #[serde(skip_serializing_if="Option::is_none")]
81 pub productname
: Option
<String
>,
82 /// register date of the set subscription
83 #[serde(skip_serializing_if="Option::is_none")]
84 pub regdate
: Option
<String
>,
85 /// next due date of the set subscription
86 #[serde(skip_serializing_if="Option::is_none")]
87 pub nextduedate
: Option
<String
>,
88 /// URL to the web shop
89 #[serde(skip_serializing_if="Option::is_none")]
90 pub url
: Option
<String
>,
93 async
fn register_subscription(
97 ) -> Result
<(String
, String
), Error
> {
98 // WHCMS sample code feeds the key into this, but it's just a challenge, so keep it simple
99 let rand
= proxmox
::tools
::bin_to_hex(&proxmox
::sys
::linux
::random_data(16)?
);
100 let challenge
= format
!("{}{}", checktime
, rand
);
105 "domain": "www.proxmox.com",
107 "check_token": challenge
,
110 let proxy_config
= if let Ok((node_config
, _digest
)) = node
::config() {
111 node_config
.http_proxy()
116 let mut client
= pbs_simple_http(proxy_config
);
118 let uri
= "https://shop.maurer-it.com/modules/servers/licensing/verify.php";
119 let query
= tools
::json_object_to_query(params
)?
;
120 let response
= client
.post(uri
, Some(query
), Some("application/x-www-form-urlencoded")).await?
;
121 let body
= SimpleHttp
::response_body_string(response
).await?
;
123 Ok((body
, challenge
))
126 fn parse_status(value
: &str) -> SubscriptionStatus
{
127 match value
.to_lowercase().as_str() {
128 "active" => SubscriptionStatus
::ACTIVE
,
129 "new" => SubscriptionStatus
::NEW
,
130 "notfound" => SubscriptionStatus
::NOTFOUND
,
131 "invalid" => SubscriptionStatus
::INVALID
,
132 _
=> SubscriptionStatus
::INVALID
,
136 fn parse_register_response(
142 ) -> Result
<SubscriptionInfo
, Error
> {
144 static ref ATTR_RE
: Regex
= Regex
::new(r
"<([^>]+)>([^<]+)</[^>]+>").unwrap();
147 let mut info
= SubscriptionInfo
{
149 status
: SubscriptionStatus
::NOTFOUND
,
150 checktime
: Some(checktime
),
151 url
: Some("https://www.proxmox.com/en/proxmox-backup-server/pricing".into()),
154 let mut md5hash
= String
::new();
155 let is_server_id
= |id
: &&str| *id
== server_id
;
157 for caps
in ATTR_RE
.captures_iter(body
) {
158 let (key
, value
) = (&caps
[1], &caps
[2]);
160 "status" => info
.status
= parse_status(value
),
161 "productname" => info
.productname
= Some(value
.into()),
162 "regdate" => info
.regdate
= Some(value
.into()),
163 "nextduedate" => info
.nextduedate
= Some(value
.into()),
164 "message" if value
== "Directory Invalid" =>
165 info
.message
= Some("Invalid Server ID".into()),
166 "message" => info
.message
= Some(value
.into()),
167 "validdirectory" => {
168 if value
.split('
,'
).find(is_server_id
) == None
{
169 bail
!("Server ID does not match");
171 info
.serverid
= Some(server_id
.to_owned());
173 "md5hash" => md5hash
= value
.to_owned(),
178 if let SubscriptionStatus
::ACTIVE
= info
.status
{
179 let response_raw
= format
!("{}{}", SHARED_KEY_DATA
, challenge
);
180 let expected
= proxmox
::tools
::bin_to_hex(&tools
::md5sum(response_raw
.as_bytes())?
);
181 if expected
!= md5hash
{
182 bail
!("Subscription API challenge failed, expected {} != got {}", expected
, md5hash
);
189 fn test_parse_register_response() -> Result
<(), Error
> {
191 <status>Active</status>
192 <companyname>Proxmox</companyname>
193 <serviceid>41108</serviceid>
194 <productid>71</productid>
195 <productname>Proxmox Backup Server Test Subscription -1 year</productname>
196 <regdate>2020-09-19 00:00:00</regdate>
197 <nextduedate>2021-09-19</nextduedate>
198 <billingcycle>Annually</billingcycle>
199 <validdomain>proxmox.com,www.proxmox.com</validdomain>
200 <validdirectory>830000000123456789ABCDEF00000042</validdirectory>
201 <customfields>Notes=Test Key!</customfields>
203 <md5hash>969f4df84fe157ee4f5a2f71950ad154</md5hash>
205 let key
= "pbst-123456789a".to_string();
206 let server_id
= "830000000123456789ABCDEF00000042".to_string();
207 let checktime
= 1600000000;
208 let salt
= "cf44486bddb6ad0145732642c45b2957";
210 let info
= parse_register_response(response
, key
.to_owned(), server_id
.to_owned(), checktime
, salt
)?
;
212 assert_eq
!(info
, SubscriptionInfo
{
214 serverid
: Some(server_id
),
215 status
: SubscriptionStatus
::ACTIVE
,
216 checktime
: Some(checktime
),
217 url
: Some("https://www.proxmox.com/en/proxmox-backup-server/pricing".into()),
219 nextduedate
: Some("2021-09-19".into()),
220 regdate
: Some("2020-09-19 00:00:00".into()),
221 productname
: Some("Proxmox Backup Server Test Subscription -1 year".into()),
226 /// queries the up to date subscription status and parses the response
227 pub fn check_subscription(key
: String
, server_id
: String
) -> Result
<SubscriptionInfo
, Error
> {
229 let now
= proxmox
::tools
::time
::epoch_i64();
231 let (response
, challenge
) = tools
::runtime
::block_on(register_subscription(&key
, &server_id
, now
))
232 .map_err(|err
| format_err
!("Error checking subscription: {}", err
))?
;
234 parse_register_response(&response
, key
, server_id
, now
, &challenge
)
235 .map_err(|err
| format_err
!("Error parsing subscription check response: {}", err
))
238 /// reads in subscription information and does a basic integrity verification
239 pub fn read_subscription() -> Result
<Option
<SubscriptionInfo
>, Error
> {
241 let cfg
= proxmox
::tools
::fs
::file_read_optional_string(&SUBSCRIPTION_FN
)?
;
242 let cfg
= if let Some(cfg
) = cfg { cfg }
else { return Ok(None); }
;
244 let mut cfg
= cfg
.lines();
246 // first line is key in plain
247 let _key
= if let Some(key
) = cfg
.next() { key }
else { return Ok(None) }
;
248 // second line is checksum of encoded data
249 let checksum
= if let Some(csum
) = cfg
.next() { csum }
else { return Ok(None) }
;
251 let encoded
: String
= cfg
.collect
::<String
>();
252 let decoded
= base64
::decode(encoded
.to_owned())?
;
253 let decoded
= std
::str::from_utf8(&decoded
)?
;
255 let info
: SubscriptionInfo
= serde_json
::from_str(decoded
)?
;
257 let new_checksum
= format
!("{}{}{}", info
.checktime
.unwrap_or(0), encoded
, SHARED_KEY_DATA
);
258 let new_checksum
= base64
::encode(tools
::md5sum(new_checksum
.as_bytes())?
);
260 if checksum
!= new_checksum
{
261 bail
!("stored checksum doesn't matches computed one '{}' != '{}'", checksum
, new_checksum
);
264 let age
= proxmox
::tools
::time
::epoch_i64() - info
.checktime
.unwrap_or(0);
265 if age
< -5400 { // allow some delta for DST changes or time syncs, 1.5h
266 bail
!("Last check time to far in the future.");
267 } else if age
> MAX_LOCAL_KEY_AGE
+ MAX_KEY_CHECK_FAILURE_AGE
{
268 if let SubscriptionStatus
::ACTIVE
= info
.status
{
269 bail
!("subscription information too old");
276 /// writes out subscription status
277 pub fn write_subscription(info
: SubscriptionInfo
) -> Result
<(), Error
> {
278 let key
= info
.key
.to_owned();
279 let server_id
= info
.serverid
.to_owned();
281 let raw
= if info
.key
== None
|| info
.checktime
== None
{
283 } else if let SubscriptionStatus
::NEW
= info
.status
{
284 format
!("{}\n", info
.key
.unwrap())
286 let encoded
= base64
::encode(serde_json
::to_string(&info
)?
);
287 let csum
= format
!("{}{}{}", info
.checktime
.unwrap_or(0), encoded
, SHARED_KEY_DATA
);
288 let csum
= base64
::encode(tools
::md5sum(csum
.as_bytes())?
);
289 format
!("{}\n{}\n{}\n", info
.key
.unwrap(), csum
, encoded
)
292 let backup_user
= crate::backup
::backup_user()?
;
293 let mode
= nix
::sys
::stat
::Mode
::from_bits_truncate(0o0640);
294 let file_opts
= CreateOptions
::new()
296 .owner(nix
::unistd
::ROOT
)
297 .group(backup_user
.gid
);
299 let subscription_file
= std
::path
::Path
::new(SUBSCRIPTION_FN
);
300 replace_file(subscription_file
, raw
.as_bytes(), file_opts
)?
;
302 update_apt_auth(key
, server_id
)?
;
307 /// deletes subscription from server
308 pub fn delete_subscription() -> Result
<(), Error
> {
309 let subscription_file
= std
::path
::Path
::new(SUBSCRIPTION_FN
);
310 nix
::unistd
::unlink(subscription_file
)?
;
311 update_apt_auth(None
, None
)?
;
315 /// updates apt authentication for repo access
316 pub fn update_apt_auth(key
: Option
<String
>, password
: Option
<String
>) -> Result
<(), Error
> {
317 let auth_conf
= std
::path
::Path
::new(APT_AUTH_FN
);
318 match (key
, password
) {
319 (Some(key
), Some(password
)) => {
321 "machine enterprise.proxmox.com/debian/pbs\n login {}\n password {}\n",
325 let mode
= nix
::sys
::stat
::Mode
::from_bits_truncate(0o0640);
326 let file_opts
= CreateOptions
::new()
328 .owner(nix
::unistd
::ROOT
);
330 // we use a namespaced .conf file, so just overwrite..
331 replace_file(auth_conf
, conf
.as_bytes(), file_opts
)
332 .map_err(|e
| format_err
!("Error saving apt auth config - {}", e
))?
;
334 _
=> match nix
::unistd
::unlink(auth_conf
) {
336 Err(nix
::Error
::Sys(nix
::errno
::Errno
::ENOENT
)) => Ok(()), // ignore not existing
337 Err(err
) => Err(err
),
338 }.map_err(|e
| format_err
!("Error clearing apt auth config - {}", e
))?
,