use crate::claims::{ActivationMethod, LicenseTokenClaims};
use crate::device_token::DeviceToken;
use backon::{BlockingRetryable, ExponentialBuilder};
use chrono::Utc;
use jsonwebtoken::errors::ErrorKind;
use jsonwebtoken::{get_current_timestamp, Algorithm, DecodingKey, Validation};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{Receiver, Sender};
use std::sync::Arc;
use std::thread::{sleep, JoinHandle};
use std::time::Duration;
use std::{fs, io, thread};
use thiserror::Error;
use ureq::http::StatusCode;
pub enum ActivationState {
NeedsActivation(Option<String>),
Activated(LicenseTokenClaims),
}
#[derive(Error, Debug)]
pub enum ActivationError {
#[error("Could not validate cached token: {0}")]
LoadCachedToken(#[from] CachedTokenError),
#[error("Could not save license token to disk: {0}")]
SaveCachedToken(#[from] io::Error),
#[error("Could not fetch online activation url: {0}")]
FetchActivationUrl(MoonbaseApiError),
#[error("Could not fetch activation state of online token: {0}")]
FetchActivationState(MoonbaseApiError),
#[error("Could not validate offline token: {0}")]
OfflineToken(#[from] OfflineTokenValidationError),
}
#[derive(Error, Debug)]
pub enum OfflineTokenValidationError {
#[error("the license token is invalid: {0}")]
Invalid(#[from] jsonwebtoken::errors::Error),
#[error("inapplicable token: {0}")]
Inapplicable(#[from] InapplicableTokenError),
#[error("the license token is not an offline token")]
NoOfflineToken,
}
#[derive(Error, Debug)]
pub enum CachedTokenError {
#[error("error loading cached token file: {0}")]
Io(#[from] io::Error),
#[error("invalid JWT payload: {0}")]
Invalid(#[from] jsonwebtoken::errors::Error),
#[error("inapplicable token: {0}")]
Inapplicable(#[from] InapplicableTokenError),
#[error("online validation failed: {1}")]
ValidationFailed(ValidationFailedType, String),
#[error("token could not be refreshed")]
RefreshFailed(#[from] MoonbaseApiError),
}
#[derive(Error, Debug)]
pub enum InapplicableTokenError {
#[error("the license token is not valid for this device")]
InvalidDeviceSignature,
}
#[derive(Error, Debug)]
pub enum MoonbaseApiError {
#[error("issues contacting API: {0}")]
Io(#[from] ureq::Error),
#[error("unexpected response with status code {0} and body {1}")]
UnexpectedResponse(StatusCode, String),
#[error("invalid token: {0}")]
InvalidToken(#[from] jsonwebtoken::errors::Error),
}
#[derive(Debug)]
pub enum ValidationFailedType {
LicenseRevoked,
LicenseActivationRevoked,
LicenseExpired,
NoEligibleLicense,
Unknown,
}
#[derive(Clone)]
pub struct LicenseActivationConfig {
pub vendor_id: String,
pub product_id: String,
pub jwt_pubkey: String,
pub cached_token_path: PathBuf,
pub device_name: String,
pub device_signature: String,
pub online_token_refresh_threshold: Duration,
pub online_token_expiration_threshold: Duration,
}
pub struct LicenseActivator {
cfg: LicenseActivationConfig,
pub state_recv: Receiver<ActivationState>,
state_send: Sender<ActivationState>,
pub error_recv: Receiver<ActivationError>,
error_send: Sender<ActivationError>,
pub poll_online_activation: Arc<AtomicBool>,
running: Arc<AtomicBool>,
join: Option<JoinHandle<()>>,
}
impl Drop for LicenseActivator {
fn drop(&mut self) {
self.running.store(false, Ordering::Relaxed);
self.join.take().unwrap().join().unwrap();
}
}
impl LicenseActivator {
pub fn spawn(cfg: LicenseActivationConfig) -> Self {
let (state_send, state_recv) = std::sync::mpsc::channel();
let (error_send, error_recv) = std::sync::mpsc::channel();
let running = Arc::new(AtomicBool::new(true));
let running_clone = running.clone();
let poll_online_activation = Arc::new(AtomicBool::new(false));
let poll_online_activation_clone = poll_online_activation.clone();
let state_send_clone = state_send.clone();
let error_send_clone = error_send.clone();
let cfg_clone = cfg.clone();
let join = thread::spawn(|| {
worker_thread(
running_clone,
state_send_clone,
error_send_clone,
poll_online_activation_clone,
cfg_clone,
);
});
Self {
cfg,
state_recv,
state_send,
error_recv,
error_send,
poll_online_activation,
running,
join: Some(join),
}
}
pub fn machine_file_contents(&self) -> String {
DeviceToken::new(
self.cfg.device_signature.clone(),
self.cfg.device_name.clone(),
self.cfg.product_id.clone(),
)
.serialize()
}
pub fn submit_offline_activation_token(&mut self, token: &str) {
match self.check_offline_activation_token(token) {
Ok(claims) => {
_ = self.state_send.send(ActivationState::Activated(claims));
self.running.store(false, Ordering::Relaxed);
if let Err(e) = fs::write(&self.cfg.cached_token_path, token) {
_ = self.error_send.send(ActivationError::SaveCachedToken(e));
}
}
Err(e) => _ = self.error_send.send(ActivationError::OfflineToken(e)),
}
}
fn check_offline_activation_token(
&mut self,
token: &str,
) -> Result<LicenseTokenClaims, OfflineTokenValidationError> {
let claims = parse_token(&self.cfg, token)?;
if claims.method != ActivationMethod::Offline {
return Err(OfflineTokenValidationError::NoOfflineToken);
}
validate_token_applicable(&self.cfg, &claims)?;
Ok(claims)
}
}
impl LicenseActivationConfig {
fn moonbase_api_base_url(&self) -> String {
format!("https://{}.moonbase.sh", self.vendor_id)
}
}
fn worker_thread(
running: Arc<AtomicBool>,
state_send: Sender<ActivationState>,
error_send: Sender<ActivationError>,
poll_online_activation: Arc<AtomicBool>,
cfg: LicenseActivationConfig,
) {
match check_cached_token(&cfg, running.clone()) {
Ok(Some(result)) => {
_ = state_send.send(ActivationState::Activated(result.claims));
if let Some(token) = result.new_token {
if let Err(e) = fs::write(&cfg.cached_token_path, token) {
_ = error_send.send(ActivationError::SaveCachedToken(e));
}
}
return;
}
Ok(None) => {
}
Err(e) => {
_ = error_send.send(ActivationError::LoadCachedToken(e));
}
}
_ = state_send.send(ActivationState::NeedsActivation(None));
let activation_urls = match (|| moonbase_request_online_activation(&cfg))
.retry(
&ExponentialBuilder::default()
.with_max_delay(Duration::from_secs(10))
.with_max_times(10),
)
.when(|_| running.load(Ordering::Relaxed))
.call()
{
Ok(activation_urls) => Some(activation_urls),
Err(e) => {
_ = error_send.send(ActivationError::FetchActivationUrl(e));
None
}
};
if let Some(activation_urls) = activation_urls.as_ref() {
_ = state_send.send(ActivationState::NeedsActivation(Some(
activation_urls.browser.clone(),
)));
}
while running.load(Ordering::Relaxed) {
sleep(Duration::from_secs(5));
match activation_urls.as_ref() {
Some(activation_urls) if poll_online_activation.load(Ordering::Relaxed) => {
match moonbase_check_online_activation(&cfg, &activation_urls.request) {
Ok(Some((token, claims))) => {
_ = state_send.send(ActivationState::Activated(claims));
if let Err(e) = fs::write(&cfg.cached_token_path, token) {
_ = error_send.send(ActivationError::SaveCachedToken(e));
}
return;
}
Ok(None) => {
}
Err(e) => {
_ = error_send.send(ActivationError::FetchActivationState(e));
}
}
}
_ => {
if let Ok(Some(result)) = check_cached_token(&cfg, running.clone()) {
_ = state_send.send(ActivationState::Activated(result.claims));
if let Some(token) = result.new_token {
if let Err(e) = fs::write(&cfg.cached_token_path, token) {
_ = error_send.send(ActivationError::SaveCachedToken(e));
}
}
return;
}
}
}
}
}
struct CachedTokenCheckResult {
claims: LicenseTokenClaims,
new_token: Option<String>,
}
fn check_cached_token(
cfg: &LicenseActivationConfig,
running: Arc<AtomicBool>,
) -> Result<Option<CachedTokenCheckResult>, CachedTokenError> {
match load_cached_token(cfg) {
Ok(Some((token, claims))) => {
match claims.method {
ActivationMethod::Offline => {
Ok(Some(CachedTokenCheckResult {
claims,
new_token: None,
}))
}
ActivationMethod::Online => {
let token_validation_age = Utc::now() - claims.last_validated;
let token_validation_age = token_validation_age.to_std().ok();
if let Some(token_validation_age) = token_validation_age {
if token_validation_age < cfg.online_token_refresh_threshold {
return Ok(Some(CachedTokenCheckResult {
claims,
new_token: None,
}));
}
}
match (|| moonbase_refresh_token(cfg, &token))
.retry(
&ExponentialBuilder::default()
.with_max_delay(Duration::from_secs(5))
.with_max_times(5),
)
.when(|_| running.load(Ordering::Relaxed))
.call()
{
Ok(TokenValidationResponse::Valid(new_token, claims)) => {
Ok(Some(CachedTokenCheckResult {
claims,
new_token: Some(new_token),
}))
}
Ok(TokenValidationResponse::ValidationFailed(failure_type, detail)) => {
Err(CachedTokenError::ValidationFailed(failure_type, detail))
}
Err(e) => {
if let Some(token_validation_age) = token_validation_age {
if token_validation_age < cfg.online_token_expiration_threshold {
return Ok(Some(CachedTokenCheckResult {
claims,
new_token: None,
}));
}
}
Err(e.into())
}
}
}
}
}
Ok(None) => Ok(None),
Err(e) => Err(e),
}
}
fn load_cached_token(
cfg: &LicenseActivationConfig,
) -> Result<Option<(String, LicenseTokenClaims)>, CachedTokenError> {
if !fs::exists(&cfg.cached_token_path)? {
return Ok(None);
}
let token = fs::read_to_string(&cfg.cached_token_path)?;
let claims = parse_token(cfg, &token)?;
validate_token_applicable(cfg, &claims)?;
Ok(Some((token, claims)))
}
fn parse_token(
cfg: &LicenseActivationConfig,
token: &str,
) -> Result<LicenseTokenClaims, jsonwebtoken::errors::Error> {
let mut validation = Validation::new(Algorithm::RS256);
validation.set_audience(&[&cfg.product_id]);
validation.required_spec_claims.clear();
validation.validate_exp = false;
let claims = jsonwebtoken::decode::<LicenseTokenClaims>(
token,
&DecodingKey::from_rsa_pem(cfg.jwt_pubkey.as_bytes()).unwrap(),
&validation,
)?
.claims;
if let Some(expires_at) = claims.expires_at {
if expires_at.timestamp() as u64 - validation.reject_tokens_expiring_in_less_than
< get_current_timestamp() - validation.leeway
{
return Err(ErrorKind::ExpiredSignature.into());
}
}
Ok(claims)
}
fn validate_token_applicable(
cfg: &LicenseActivationConfig,
claims: &LicenseTokenClaims,
) -> Result<(), InapplicableTokenError> {
if claims.device_signature != cfg.device_signature {
return Err(InapplicableTokenError::InvalidDeviceSignature);
}
Ok(())
}
enum TokenValidationResponse {
Valid(String, LicenseTokenClaims),
ValidationFailed(ValidationFailedType, String),
}
fn moonbase_refresh_token(
cfg: &LicenseActivationConfig,
token: &str,
) -> Result<TokenValidationResponse, MoonbaseApiError> {
let response = ureq::post(format!(
"{}/api/client/licenses/{}/validate",
cfg.moonbase_api_base_url(),
cfg.product_id
))
.config()
.http_status_as_error(false)
.timeout_global(Some(Duration::from_secs(10)))
.build()
.content_type("text/plain")
.send(token)?;
let status = response.status();
if status == StatusCode::OK {
let token = response.into_body().read_to_string()?;
return match parse_token(cfg, &token) {
Ok(claims) => Ok(TokenValidationResponse::Valid(token, claims)),
Err(_) => Err(MoonbaseApiError::UnexpectedResponse(status, token)),
};
}
if status == StatusCode::BAD_REQUEST {
let body = response.into_body().read_to_string()?;
let problem: ProblemDetails = serde_json::from_str(&body)
.map_err(|_| MoonbaseApiError::UnexpectedResponse(status, body.clone()))?;
let failure_type = match problem.error_type.as_str() {
"LicenseRevoked" => ValidationFailedType::LicenseRevoked,
"LicenseActivationRevoked" => ValidationFailedType::LicenseActivationRevoked,
"LicenseExpired" => ValidationFailedType::LicenseExpired,
"NoEligibleLicense" => ValidationFailedType::NoEligibleLicense,
_ => ValidationFailedType::Unknown,
};
return Ok(TokenValidationResponse::ValidationFailed(
failure_type,
problem.detail,
));
}
Err(MoonbaseApiError::UnexpectedResponse(
status,
response
.into_body()
.read_to_string()
.unwrap_or("".to_string()),
))
}
#[derive(Deserialize)]
struct ProblemDetails {
#[serde(rename = "errorType")]
error_type: String,
detail: String,
}
#[derive(Serialize)]
struct ActivationUrlsRequestPayload {
#[serde(rename = "deviceName")]
device_name: String,
#[serde(rename = "deviceSignature")]
device_signature: String,
}
#[derive(Deserialize)]
struct ActivationUrls {
request: String,
browser: String,
}
fn moonbase_request_online_activation(
cfg: &LicenseActivationConfig,
) -> Result<ActivationUrls, MoonbaseApiError> {
let response = ureq::post(format!(
"{}/api/client/activations/{}/request",
cfg.moonbase_api_base_url(),
cfg.product_id
))
.config()
.timeout_global(Some(Duration::from_secs(10)))
.build()
.send_json(ActivationUrlsRequestPayload {
device_name: cfg.device_name.clone(),
device_signature: cfg.device_signature.clone(),
})?;
let status = response.status();
if status == StatusCode::OK {
let mut body = response.into_body();
return match body.read_json::<ActivationUrls>() {
Ok(response) => Ok(response),
Err(_) => Err(MoonbaseApiError::UnexpectedResponse(
status,
body.read_to_string().unwrap_or("".into()),
)),
};
}
Err(MoonbaseApiError::UnexpectedResponse(
status,
response
.into_body()
.read_to_string()
.unwrap_or("".into()),
))
}
fn moonbase_check_online_activation(
cfg: &LicenseActivationConfig,
url: &str,
) -> Result<Option<(String, LicenseTokenClaims)>, MoonbaseApiError> {
let response = ureq::get(url)
.config()
.timeout_global(Some(Duration::from_secs(10)))
.build()
.call()?;
let status = response.status();
if status == StatusCode::NO_CONTENT {
return Ok(None);
}
if status == StatusCode::OK {
let token = response.into_body().read_to_string()?;
let claims = parse_token(cfg, &token)?;
return Ok(Some((token, claims)));
}
Err(MoonbaseApiError::UnexpectedResponse(
status,
response
.into_body()
.read_to_string()
.unwrap_or("".to_string()),
))
}