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("online validation failed: {1}")]
82 ValidationFailed(ValidationFailedType, String),
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(Debug)]
113pub enum ValidationFailedType {
114 LicenseRevoked,
115 LicenseActivationRevoked,
116 LicenseExpired,
117 NoEligibleLicense,
118 Unknown,
121}
122
123#[derive(Clone)]
125pub struct LicenseActivationConfig {
126 pub vendor_id: String,
130 pub product_id: String,
132 pub jwt_pubkey: String,
134
135 pub cached_token_path: PathBuf,
137
138 pub device_name: String,
141 pub device_signature: String,
143
144 pub online_token_refresh_threshold: Duration,
147 pub online_token_expiration_threshold: Duration,
150}
151
152pub struct LicenseActivator {
154 cfg: LicenseActivationConfig,
155
156 pub state_recv: Receiver<ActivationState>,
166 state_send: Sender<ActivationState>,
167
168 pub error_recv: Receiver<ActivationError>,
174 error_send: Sender<ActivationError>,
175
176 pub poll_online_activation: Arc<AtomicBool>,
182
183 running: Arc<AtomicBool>,
185 join: Option<JoinHandle<()>>,
187}
188
189impl Drop for LicenseActivator {
190 fn drop(&mut self) {
191 self.running.store(false, Ordering::Relaxed);
192 self.join.take().unwrap().join().unwrap();
193 }
194}
195
196impl LicenseActivator {
197 pub fn spawn(cfg: LicenseActivationConfig) -> Self {
203 let (state_send, state_recv) = std::sync::mpsc::channel();
205 let (error_send, error_recv) = std::sync::mpsc::channel();
206
207 let running = Arc::new(AtomicBool::new(true));
209 let running_clone = running.clone();
210
211 let poll_online_activation = Arc::new(AtomicBool::new(false));
212 let poll_online_activation_clone = poll_online_activation.clone();
213
214 let state_send_clone = state_send.clone();
215 let error_send_clone = error_send.clone();
216 let cfg_clone = cfg.clone();
217
218 let join = thread::spawn(|| {
219 worker_thread(
220 running_clone,
221 state_send_clone,
222 error_send_clone,
223 poll_online_activation_clone,
224 cfg_clone,
225 );
226 });
227
228 Self {
229 cfg,
230
231 state_recv,
232 state_send,
233
234 error_recv,
235 error_send,
236
237 poll_online_activation,
238
239 running,
240 join: Some(join),
241 }
242 }
243
244 pub fn machine_file_contents(&self) -> String {
246 DeviceToken::new(
247 self.cfg.device_signature.clone(),
248 self.cfg.device_name.clone(),
249 self.cfg.product_id.clone(),
250 )
251 .serialize()
252 }
253
254 pub fn submit_offline_activation_token(&mut self, token: &str) {
260 match self.check_offline_activation_token(token) {
261 Ok(claims) => {
262 _ = self.state_send.send(ActivationState::Activated(claims));
263
264 self.running.store(false, Ordering::Relaxed);
266
267 if let Err(e) = fs::write(&self.cfg.cached_token_path, token) {
269 _ = self.error_send.send(ActivationError::SaveCachedToken(e));
270 }
271 }
272 Err(e) => _ = self.error_send.send(ActivationError::OfflineToken(e)),
273 }
274 }
275
276 fn check_offline_activation_token(
277 &mut self,
278 token: &str,
279 ) -> Result<LicenseTokenClaims, OfflineTokenValidationError> {
280 let claims = parse_token(&self.cfg, token)?;
281
282 if claims.method != ActivationMethod::Offline {
283 return Err(OfflineTokenValidationError::NoOfflineToken);
284 }
285
286 validate_token_applicable(&self.cfg, &claims)?;
287
288 Ok(claims)
289 }
290}
291
292impl LicenseActivationConfig {
293 fn moonbase_api_base_url(&self) -> String {
295 format!("https://{}.moonbase.sh", self.vendor_id)
296 }
297}
298
299fn worker_thread(
300 running: Arc<AtomicBool>,
301 state_send: Sender<ActivationState>,
302 error_send: Sender<ActivationError>,
303 poll_online_activation: Arc<AtomicBool>,
304 cfg: LicenseActivationConfig,
305) {
306 match check_cached_token(&cfg, running.clone()) {
308 Ok(Some(result)) => {
309 _ = state_send.send(ActivationState::Activated(result.claims));
310
311 if let Some(token) = result.new_token {
312 if let Err(e) = fs::write(&cfg.cached_token_path, token) {
314 _ = error_send.send(ActivationError::SaveCachedToken(e));
315 }
316 }
317
318 return;
319 }
320 Ok(None) => {
321 }
323 Err(e) => {
324 _ = error_send.send(ActivationError::LoadCachedToken(e));
326 }
327 }
328
329 _ = state_send.send(ActivationState::NeedsActivation(None));
335
336 let activation_urls = match (|| moonbase_request_online_activation(&cfg))
338 .retry(
339 &ExponentialBuilder::default()
340 .with_max_delay(Duration::from_secs(10))
341 .with_max_times(10),
342 )
343 .when(|_| running.load(Ordering::Relaxed))
344 .call()
345 {
346 Ok(activation_urls) => Some(activation_urls),
347 Err(e) => {
348 _ = error_send.send(ActivationError::FetchActivationUrl(e));
350 None
351 }
352 };
353
354 if let Some(activation_urls) = activation_urls.as_ref() {
355 _ = state_send.send(ActivationState::NeedsActivation(Some(
358 activation_urls.browser.clone(),
359 )));
360 }
361
362 while running.load(Ordering::Relaxed) {
365 sleep(Duration::from_secs(5));
366
367 match activation_urls.as_ref() {
368 Some(activation_urls) if poll_online_activation.load(Ordering::Relaxed) => {
369 match moonbase_check_online_activation(&cfg, &activation_urls.request) {
373 Ok(Some((token, claims))) => {
374 _ = state_send.send(ActivationState::Activated(claims));
376
377 if let Err(e) = fs::write(&cfg.cached_token_path, token) {
379 _ = error_send.send(ActivationError::SaveCachedToken(e));
380 }
381
382 return;
383 }
384 Ok(None) => {
385 }
387 Err(e) => {
388 _ = error_send.send(ActivationError::FetchActivationState(e));
389 }
390 }
391 }
392 _ => {
393 if let Ok(Some(result)) = check_cached_token(&cfg, running.clone()) {
396 _ = state_send.send(ActivationState::Activated(result.claims));
397
398 if let Some(token) = result.new_token {
399 if let Err(e) = fs::write(&cfg.cached_token_path, token) {
401 _ = error_send.send(ActivationError::SaveCachedToken(e));
402 }
403 }
404
405 return;
406 }
407 }
408 }
409 }
410}
411
412struct CachedTokenCheckResult {
413 claims: LicenseTokenClaims,
415 new_token: Option<String>,
417}
418
419fn check_cached_token(
427 cfg: &LicenseActivationConfig,
428 running: Arc<AtomicBool>,
429) -> Result<Option<CachedTokenCheckResult>, CachedTokenError> {
430 match load_cached_token(cfg) {
431 Ok(Some((token, claims))) => {
432 match claims.method {
433 ActivationMethod::Offline => {
434 Ok(Some(CachedTokenCheckResult {
438 claims,
439 new_token: None,
440 }))
441 }
442 ActivationMethod::Online => {
443 let token_validation_age = Utc::now() - claims.last_validated;
447
448 let token_validation_age = token_validation_age.to_std().ok();
453
454 if let Some(token_validation_age) = token_validation_age {
455 if token_validation_age < cfg.online_token_refresh_threshold {
456 return Ok(Some(CachedTokenCheckResult {
460 claims,
461 new_token: None,
462 }));
463 }
464 }
465
466 match (|| moonbase_refresh_token(cfg, &token))
468 .retry(
469 &ExponentialBuilder::default()
470 .with_max_delay(Duration::from_secs(5))
471 .with_max_times(5),
472 )
473 .when(|_| running.load(Ordering::Relaxed))
474 .call()
475 {
476 Ok(TokenValidationResponse::Valid(new_token, claims)) => {
477 Ok(Some(CachedTokenCheckResult {
479 claims,
480 new_token: Some(new_token),
481 }))
482 }
483 Ok(TokenValidationResponse::ValidationFailed(failure_type, detail)) => {
484 Err(CachedTokenError::ValidationFailed(failure_type, detail))
485 }
486 Err(e) => {
487 if let Some(token_validation_age) = token_validation_age {
490 if token_validation_age < cfg.online_token_expiration_threshold {
491 return Ok(Some(CachedTokenCheckResult {
495 claims,
496 new_token: None,
497 }));
498 }
499 }
500
501 Err(e.into())
502 }
503 }
504 }
505 }
506 }
507 Ok(None) => Ok(None),
508 Err(e) => Err(e),
509 }
510}
511
512fn load_cached_token(
519 cfg: &LicenseActivationConfig,
520) -> Result<Option<(String, LicenseTokenClaims)>, CachedTokenError> {
521 if !fs::exists(&cfg.cached_token_path)? {
522 return Ok(None);
523 }
524
525 let token = fs::read_to_string(&cfg.cached_token_path)?;
526
527 let claims = parse_token(cfg, &token)?;
529
530 validate_token_applicable(cfg, &claims)?;
532
533 Ok(Some((token, claims)))
534}
535
536fn parse_token(
542 cfg: &LicenseActivationConfig,
543 token: &str,
544) -> Result<LicenseTokenClaims, jsonwebtoken::errors::Error> {
545 let mut validation = Validation::new(Algorithm::RS256);
546 validation.set_audience(&[&cfg.product_id]);
547
548 validation.required_spec_claims.clear();
550 validation.validate_exp = false;
551
552 let claims = jsonwebtoken::decode::<LicenseTokenClaims>(
553 token,
554 &DecodingKey::from_rsa_pem(cfg.jwt_pubkey.as_bytes()).unwrap(),
555 &validation,
556 )?
557 .claims;
558
559 if let Some(expires_at) = claims.expires_at {
562 if expires_at.timestamp() as u64 - validation.reject_tokens_expiring_in_less_than
563 < get_current_timestamp() - validation.leeway
564 {
565 return Err(ErrorKind::ExpiredSignature.into());
566 }
567 }
568 Ok(claims)
569}
570
571fn validate_token_applicable(
572 cfg: &LicenseActivationConfig,
573 claims: &LicenseTokenClaims,
574) -> Result<(), InapplicableTokenError> {
575 if claims.device_signature != cfg.device_signature {
576 return Err(InapplicableTokenError::InvalidDeviceSignature);
577 }
578
579 Ok(())
580}
581
582enum TokenValidationResponse {
583 Valid(String, LicenseTokenClaims),
585 ValidationFailed(ValidationFailedType, String),
587}
588
589fn moonbase_refresh_token(
592 cfg: &LicenseActivationConfig,
593 token: &str,
594) -> Result<TokenValidationResponse, MoonbaseApiError> {
595 let response = ureq::post(format!(
596 "{}/api/client/licenses/{}/validate",
597 cfg.moonbase_api_base_url(),
598 cfg.product_id
599 ))
600 .config()
601 .http_status_as_error(false)
602 .timeout_global(Some(Duration::from_secs(10)))
603 .build()
604 .content_type("text/plain")
605 .send(token)?;
606
607 let status = response.status();
608
609 if status == StatusCode::OK {
610 let token = response.into_body().read_to_string()?;
613
614 return match parse_token(cfg, &token) {
616 Ok(claims) => Ok(TokenValidationResponse::Valid(token, claims)),
617 Err(_) => Err(MoonbaseApiError::UnexpectedResponse(status, token)),
618 };
619 }
620
621 if status == StatusCode::BAD_REQUEST {
623 let body = response.into_body().read_to_string()?;
626 let problem: ProblemDetails = serde_json::from_str(&body)
627 .map_err(|_| MoonbaseApiError::UnexpectedResponse(status, body.clone()))?;
628 let failure_type = match problem.error_type.as_str() {
629 "LicenseRevoked" => ValidationFailedType::LicenseRevoked,
630 "LicenseActivationRevoked" => ValidationFailedType::LicenseActivationRevoked,
631 "LicenseExpired" => ValidationFailedType::LicenseExpired,
632 "NoEligibleLicense" => ValidationFailedType::NoEligibleLicense,
633 _ => ValidationFailedType::Unknown,
634 };
635 return Ok(TokenValidationResponse::ValidationFailed(
636 failure_type,
637 problem.detail,
638 ));
639 }
640
641 Err(MoonbaseApiError::UnexpectedResponse(
643 status,
644 response
645 .into_body()
646 .read_to_string()
647 .unwrap_or("".to_string()),
650 ))
651}
652
653#[derive(Deserialize)]
654struct ProblemDetails {
655 #[serde(rename = "errorType")]
656 error_type: String,
657 detail: String,
658}
659
660#[derive(Serialize)]
661struct ActivationUrlsRequestPayload {
662 #[serde(rename = "deviceName")]
663 device_name: String,
664 #[serde(rename = "deviceSignature")]
665 device_signature: String,
666}
667
668#[derive(Deserialize)]
669struct ActivationUrls {
670 request: String,
673 browser: String,
676}
677
678fn moonbase_request_online_activation(
680 cfg: &LicenseActivationConfig,
681) -> Result<ActivationUrls, MoonbaseApiError> {
682 let response = ureq::post(format!(
683 "{}/api/client/activations/{}/request",
684 cfg.moonbase_api_base_url(),
685 cfg.product_id
686 ))
687 .config()
688 .timeout_global(Some(Duration::from_secs(10)))
689 .build()
690 .send_json(ActivationUrlsRequestPayload {
691 device_name: cfg.device_name.clone(),
692 device_signature: cfg.device_signature.clone(),
693 })?;
694
695 let status = response.status();
696 if status == StatusCode::OK {
697 let mut body = response.into_body();
699 return match body.read_json::<ActivationUrls>() {
700 Ok(response) => Ok(response),
701 Err(_) => Err(MoonbaseApiError::UnexpectedResponse(
702 status,
703 body.read_to_string().unwrap_or("".into()),
704 )),
705 };
706 }
707
708 Err(MoonbaseApiError::UnexpectedResponse(
710 status,
711 response
712 .into_body()
713 .read_to_string()
714 .unwrap_or("".into()),
717 ))
718}
719
720fn moonbase_check_online_activation(
725 cfg: &LicenseActivationConfig,
726 url: &str,
727) -> Result<Option<(String, LicenseTokenClaims)>, MoonbaseApiError> {
728 let response = ureq::get(url)
729 .config()
730 .timeout_global(Some(Duration::from_secs(10)))
731 .build()
732 .call()?;
733
734 let status = response.status();
735
736 if status == StatusCode::NO_CONTENT {
737 return Ok(None);
739 }
740
741 if status == StatusCode::OK {
742 let token = response.into_body().read_to_string()?;
745
746 let claims = parse_token(cfg, &token)?;
748 return Ok(Some((token, claims)));
749 }
750
751 Err(MoonbaseApiError::UnexpectedResponse(
753 status,
754 response
755 .into_body()
756 .read_to_string()
757 .unwrap_or("".to_string()),
760 ))
761}