1use crate::claims::{ActivationMethod, LicenseTokenClaims};
2use crate::device_token::DeviceToken;
3use backon::{BlockingRetryable, ExponentialBuilder};
4use chrono::Utc;
5use jsonwebtoken::errors::ErrorKind;
6use jsonwebtoken::{get_current_timestamp, Algorithm, DecodingKey, Validation};
7use serde::{Deserialize, Serialize};
8use std::path::PathBuf;
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::sync::mpsc::{Receiver, Sender};
11use std::sync::Arc;
12use std::thread::{sleep, JoinHandle};
13use std::time::Duration;
14use std::{fs, io, thread};
15use thiserror::Error;
16use ureq::http::StatusCode;
17
18pub enum ActivationState {
20 NeedsActivation(Option<String>),
27
28 Activated(LicenseTokenClaims),
30}
31
32#[derive(Error, Debug)]
34pub enum ActivationError {
35 #[error("Could not validate cached token: {0}")]
37 LoadCachedToken(#[from] CachedTokenError),
38
39 #[error("Could not save license token to disk: {0}")]
41 SaveCachedToken(#[from] io::Error),
42
43 #[error("Could not fetch online activation url: {0}")]
45 FetchActivationUrl(MoonbaseApiError),
46
47 #[error("Could not fetch activation state of online token: {0}")]
49 FetchActivationState(MoonbaseApiError),
50
51 #[error("Could not validate offline token: {0}")]
53 OfflineToken(#[from] OfflineTokenValidationError),
54}
55
56#[derive(Error, Debug)]
57pub enum OfflineTokenValidationError {
58 #[error("the license token is invalid: {0}")]
59 Invalid(#[from] jsonwebtoken::errors::Error),
60 #[error("inapplicable token: {0}")]
61 Inapplicable(#[from] InapplicableTokenError),
62 #[error("the license token is not an offline token")]
63 NoOfflineToken,
64}
65
66#[derive(Error, Debug)]
67pub enum CachedTokenError {
68 #[error("error loading cached token file: {0}")]
70 Io(#[from] io::Error),
71
72 #[error("invalid JWT payload: {0}")]
74 Invalid(#[from] jsonwebtoken::errors::Error),
75
76 #[error("inapplicable token: {0}")]
78 Inapplicable(#[from] InapplicableTokenError),
79
80 #[error("token has been revoked")]
82 Revoked,
83
84 #[error("token could not be refreshed")]
87 RefreshFailed(#[from] MoonbaseApiError),
88}
89
90#[derive(Error, Debug)]
91pub enum InapplicableTokenError {
92 #[error("the license token is not valid for this device")]
93 InvalidDeviceSignature,
94}
95
96#[derive(Error, Debug)]
97pub enum MoonbaseApiError {
98 #[error("issues contacting API: {0}")]
100 Io(#[from] ureq::Error),
101
102 #[error("unexpected response with status code {0} and body {1}")]
104 UnexpectedResponse(StatusCode, String),
105
106 #[error("invalid token: {0}")]
108 InvalidToken(#[from] jsonwebtoken::errors::Error),
109}
110
111#[derive(Clone)]
113pub struct LicenseActivationConfig {
114 pub vendor_id: String,
118 pub product_id: String,
120 pub jwt_pubkey: String,
122
123 pub cached_token_path: PathBuf,
125
126 pub device_name: String,
129 pub device_signature: String,
131
132 pub online_token_refresh_threshold: Duration,
135 pub online_token_expiration_threshold: Duration,
138}
139
140pub struct LicenseActivator {
142 cfg: LicenseActivationConfig,
143
144 pub state_recv: Receiver<ActivationState>,
154 state_send: Sender<ActivationState>,
155
156 pub error_recv: Receiver<ActivationError>,
162 error_send: Sender<ActivationError>,
163
164 pub poll_online_activation: Arc<AtomicBool>,
170
171 running: Arc<AtomicBool>,
173 join: Option<JoinHandle<()>>,
175}
176
177impl Drop for LicenseActivator {
178 fn drop(&mut self) {
179 self.running.store(false, Ordering::Relaxed);
180 self.join.take().unwrap().join().unwrap();
181 }
182}
183
184impl LicenseActivator {
185 pub fn spawn(cfg: LicenseActivationConfig) -> Self {
191 let (state_send, state_recv) = std::sync::mpsc::channel();
193 let (error_send, error_recv) = std::sync::mpsc::channel();
194
195 let running = Arc::new(AtomicBool::new(true));
197 let running_clone = running.clone();
198
199 let poll_online_activation = Arc::new(AtomicBool::new(false));
200 let poll_online_activation_clone = poll_online_activation.clone();
201
202 let state_send_clone = state_send.clone();
203 let error_send_clone = error_send.clone();
204 let cfg_clone = cfg.clone();
205
206 let join = thread::spawn(|| {
207 worker_thread(
208 running_clone,
209 state_send_clone,
210 error_send_clone,
211 poll_online_activation_clone,
212 cfg_clone,
213 );
214 });
215
216 Self {
217 cfg,
218
219 state_recv,
220 state_send,
221
222 error_recv,
223 error_send,
224
225 poll_online_activation,
226
227 running,
228 join: Some(join),
229 }
230 }
231
232 pub fn machine_file_contents(&self) -> String {
234 DeviceToken::new(
235 self.cfg.device_signature.clone(),
236 self.cfg.device_name.clone(),
237 self.cfg.product_id.clone(),
238 )
239 .serialize()
240 }
241
242 pub fn submit_offline_activation_token(&mut self, token: &str) {
248 match self.check_offline_activation_token(token) {
249 Ok(claims) => {
250 _ = self.state_send.send(ActivationState::Activated(claims));
251
252 self.running.store(false, Ordering::Relaxed);
254
255 if let Err(e) = fs::write(&self.cfg.cached_token_path, token) {
257 _ = self.error_send.send(ActivationError::SaveCachedToken(e));
258 }
259 }
260 Err(e) => _ = self.error_send.send(ActivationError::OfflineToken(e)),
261 }
262 }
263
264 fn check_offline_activation_token(
265 &mut self,
266 token: &str,
267 ) -> Result<LicenseTokenClaims, OfflineTokenValidationError> {
268 let claims = parse_token(&self.cfg, token)?;
269
270 if claims.method != ActivationMethod::Offline {
271 return Err(OfflineTokenValidationError::NoOfflineToken);
272 }
273
274 validate_token_applicable(&self.cfg, &claims)?;
275
276 Ok(claims)
277 }
278}
279
280impl LicenseActivationConfig {
281 fn moonbase_api_base_url(&self) -> String {
283 format!("https://{}.moonbase.sh", self.vendor_id)
284 }
285}
286
287fn worker_thread(
288 running: Arc<AtomicBool>,
289 state_send: Sender<ActivationState>,
290 error_send: Sender<ActivationError>,
291 poll_online_activation: Arc<AtomicBool>,
292 cfg: LicenseActivationConfig,
293) {
294 match check_cached_token(&cfg, running.clone()) {
296 Ok(Some(result)) => {
297 _ = state_send.send(ActivationState::Activated(result.claims));
298
299 if let Some(token) = result.new_token {
300 if let Err(e) = fs::write(&cfg.cached_token_path, token) {
302 _ = error_send.send(ActivationError::SaveCachedToken(e));
303 }
304 }
305
306 return;
307 }
308 Ok(None) => {
309 }
311 Err(e) => {
312 _ = error_send.send(ActivationError::LoadCachedToken(e));
314 }
315 }
316
317 _ = state_send.send(ActivationState::NeedsActivation(None));
323
324 let activation_urls = match (|| moonbase_request_online_activation(&cfg))
326 .retry(
327 &ExponentialBuilder::default()
328 .with_max_delay(Duration::from_secs(10))
329 .with_max_times(10),
330 )
331 .when(|_| running.load(Ordering::Relaxed))
332 .call()
333 {
334 Ok(activation_urls) => Some(activation_urls),
335 Err(e) => {
336 _ = error_send.send(ActivationError::FetchActivationUrl(e));
338 None
339 }
340 };
341
342 if let Some(activation_urls) = activation_urls.as_ref() {
343 _ = state_send.send(ActivationState::NeedsActivation(Some(
346 activation_urls.browser.clone(),
347 )));
348 }
349
350 while running.load(Ordering::Relaxed) {
353 sleep(Duration::from_secs(5));
354
355 match activation_urls.as_ref() {
356 Some(activation_urls) if poll_online_activation.load(Ordering::Relaxed) => {
357 match moonbase_check_online_activation(&cfg, &activation_urls.request) {
361 Ok(TokenValidationResponse::Valid(token, claims)) => {
362 _ = state_send.send(ActivationState::Activated(claims));
364
365 if let Err(e) = fs::write(&cfg.cached_token_path, token) {
367 _ = error_send.send(ActivationError::SaveCachedToken(e));
368 }
369
370 return;
371 }
372 Ok(TokenValidationResponse::Invalid) => {
373 }
375 Err(e) => {
376 _ = error_send.send(ActivationError::FetchActivationState(e));
377 }
378 }
379 }
380 _ => {
381 if let Ok(Some(result)) = check_cached_token(&cfg, running.clone()) {
384 _ = state_send.send(ActivationState::Activated(result.claims));
385
386 if let Some(token) = result.new_token {
387 if let Err(e) = fs::write(&cfg.cached_token_path, token) {
389 _ = error_send.send(ActivationError::SaveCachedToken(e));
390 }
391 }
392
393 return;
394 }
395 }
396 }
397 }
398}
399
400struct CachedTokenCheckResult {
401 claims: LicenseTokenClaims,
403 new_token: Option<String>,
405}
406
407fn check_cached_token(
415 cfg: &LicenseActivationConfig,
416 running: Arc<AtomicBool>,
417) -> Result<Option<CachedTokenCheckResult>, CachedTokenError> {
418 match load_cached_token(cfg) {
419 Ok(Some((token, claims))) => {
420 match claims.method {
421 ActivationMethod::Offline => {
422 Ok(Some(CachedTokenCheckResult {
426 claims,
427 new_token: None,
428 }))
429 }
430 ActivationMethod::Online => {
431 let token_validation_age = Utc::now() - claims.last_validated;
435
436 let token_validation_age = token_validation_age.to_std().ok();
441
442 if let Some(token_validation_age) = token_validation_age {
443 if token_validation_age < cfg.online_token_refresh_threshold {
444 return Ok(Some(CachedTokenCheckResult {
448 claims,
449 new_token: None,
450 }));
451 }
452 }
453
454 match (|| moonbase_refresh_token(cfg, &token))
456 .retry(
457 &ExponentialBuilder::default()
458 .with_max_delay(Duration::from_secs(5))
459 .with_max_times(5),
460 )
461 .when(|_| running.load(Ordering::Relaxed))
462 .call()
463 {
464 Ok(TokenValidationResponse::Valid(new_token, claims)) => {
465 Ok(Some(CachedTokenCheckResult {
467 claims,
468 new_token: Some(new_token),
469 }))
470 }
471 Ok(TokenValidationResponse::Invalid) => {
472 Err(CachedTokenError::Revoked)
474 }
475 Err(e) => {
476 if let Some(token_validation_age) = token_validation_age {
479 if token_validation_age < cfg.online_token_expiration_threshold {
480 return Ok(Some(CachedTokenCheckResult {
484 claims,
485 new_token: None,
486 }));
487 }
488 }
489
490 Err(e.into())
491 }
492 }
493 }
494 }
495 }
496 Ok(None) => Ok(None),
497 Err(e) => Err(e),
498 }
499}
500
501fn load_cached_token(
508 cfg: &LicenseActivationConfig,
509) -> Result<Option<(String, LicenseTokenClaims)>, CachedTokenError> {
510 if !fs::exists(&cfg.cached_token_path)? {
511 return Ok(None);
512 }
513
514 let token = fs::read_to_string(&cfg.cached_token_path)?;
515
516 let claims = parse_token(cfg, &token)?;
518
519 validate_token_applicable(cfg, &claims)?;
521
522 Ok(Some((token, claims)))
523}
524
525fn parse_token(
531 cfg: &LicenseActivationConfig,
532 token: &str,
533) -> Result<LicenseTokenClaims, jsonwebtoken::errors::Error> {
534 let mut validation = Validation::new(Algorithm::RS256);
535 validation.set_audience(&[&cfg.product_id]);
536
537 validation.required_spec_claims.clear();
539 validation.validate_exp = false;
540
541 let claims = jsonwebtoken::decode::<LicenseTokenClaims>(
542 token,
543 &DecodingKey::from_rsa_pem(cfg.jwt_pubkey.as_bytes()).unwrap(),
544 &validation,
545 )?
546 .claims;
547
548 if let Some(expires_at) = claims.expires_at {
551 if expires_at.timestamp() as u64 - validation.reject_tokens_expiring_in_less_than
552 < get_current_timestamp() - validation.leeway
553 {
554 return Err(ErrorKind::ExpiredSignature.into());
555 }
556 }
557 Ok(claims)
558}
559
560fn validate_token_applicable(
561 cfg: &LicenseActivationConfig,
562 claims: &LicenseTokenClaims,
563) -> Result<(), InapplicableTokenError> {
564 if claims.device_signature != cfg.device_signature {
565 return Err(InapplicableTokenError::InvalidDeviceSignature);
566 }
567
568 Ok(())
569}
570
571enum TokenValidationResponse {
572 Valid(String, LicenseTokenClaims),
574 Invalid,
576}
577
578fn moonbase_refresh_token(
581 cfg: &LicenseActivationConfig,
582 token: &str,
583) -> Result<TokenValidationResponse, MoonbaseApiError> {
584 let response = ureq::post(format!(
585 "{}/api/client/licenses/{}/validate",
586 cfg.moonbase_api_base_url(),
587 cfg.product_id
588 ))
589 .config()
590 .http_status_as_error(false)
591 .timeout_global(Some(Duration::from_secs(10)))
592 .build()
593 .content_type("text/plain")
594 .send(token)?;
595
596 let status = response.status();
597
598 if status == StatusCode::OK {
599 let token = response.into_body().read_to_string()?;
602
603 return match parse_token(cfg, &token) {
605 Ok(claims) => Ok(TokenValidationResponse::Valid(token, claims)),
606 Err(_) => Err(MoonbaseApiError::UnexpectedResponse(status, token)),
607 };
608 }
609
610 if status == StatusCode::BAD_REQUEST {
612 return Ok(TokenValidationResponse::Invalid);
613 }
614
615 Err(MoonbaseApiError::UnexpectedResponse(
617 status,
618 response
619 .into_body()
620 .read_to_string()
621 .unwrap_or("".to_string()),
624 ))
625}
626
627#[derive(Serialize)]
628struct ActivationUrlsRequestPayload {
629 #[serde(rename = "deviceName")]
630 device_name: String,
631 #[serde(rename = "deviceSignature")]
632 device_signature: String,
633}
634
635#[derive(Deserialize)]
636struct ActivationUrls {
637 request: String,
640 browser: String,
643}
644
645fn moonbase_request_online_activation(
647 cfg: &LicenseActivationConfig,
648) -> Result<ActivationUrls, MoonbaseApiError> {
649 let response = ureq::post(format!(
650 "{}/api/client/activations/{}/request",
651 cfg.moonbase_api_base_url(),
652 cfg.product_id
653 ))
654 .config()
655 .timeout_global(Some(Duration::from_secs(10)))
656 .build()
657 .send_json(ActivationUrlsRequestPayload {
658 device_name: cfg.device_name.clone(),
659 device_signature: cfg.device_signature.clone(),
660 })?;
661
662 let status = response.status();
663 if status == StatusCode::OK {
664 let mut body = response.into_body();
666 return match body.read_json::<ActivationUrls>() {
667 Ok(response) => Ok(response),
668 Err(_) => Err(MoonbaseApiError::UnexpectedResponse(
669 status,
670 body.read_to_string().unwrap_or("".into()),
671 )),
672 };
673 }
674
675 Err(MoonbaseApiError::UnexpectedResponse(
677 status,
678 response
679 .into_body()
680 .read_to_string()
681 .unwrap_or("".into()),
684 ))
685}
686
687fn moonbase_check_online_activation(
690 cfg: &LicenseActivationConfig,
691 url: &str,
692) -> Result<TokenValidationResponse, MoonbaseApiError> {
693 let response = ureq::get(url)
694 .config()
695 .timeout_global(Some(Duration::from_secs(10)))
696 .build()
697 .call()?;
698
699 let status = response.status();
700
701 if status == StatusCode::NO_CONTENT {
702 return Ok(TokenValidationResponse::Invalid);
704 }
705
706 if status == StatusCode::OK {
707 let token = response.into_body().read_to_string()?;
710
711 let claims = parse_token(cfg, &token)?;
713 return Ok(TokenValidationResponse::Valid(token, claims));
714 }
715
716 Err(MoonbaseApiError::UnexpectedResponse(
718 status,
719 response
720 .into_body()
721 .read_to_string()
722 .unwrap_or("".to_string()),
725 ))
726}