use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError, Weak};
use std::time::Duration;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use futures::future::{BoxFuture, Shared};
use futures::FutureExt;
use tokio::sync::watch;
use tokio::task::AbortHandle;
use tokio_util::sync::CancellationToken;
use super::api::{self, encode_uri_component, ValidateKeyRequest, DEFAULT_API_BASE_URL};
use super::clock::{from_millis, Clock, ClockGuard, SystemClock};
use super::environment;
use super::error::{ConfigError, LatestAllowedVersion, LicenseError, LicenseErrorCode};
use super::fingerprint::{resolve_machine_fingerprint, MachineFingerprint};
use super::options::LicenseManagerOptions;
use super::retry::{with_retry, RetryPolicy, BACKGROUND_RETRY, INTERACTIVE_RETRY};
use super::state::{locked_state, LicenseState, OfflineState};
use super::store::{default_backend, SecureStore};
use super::token::{compute_refresh_at, TokenPayload, TokenVerifier, ValidatedToken};
use super::transport::{ReqwestTransport, Transport};
use super::version::is_version_allowed;
const INSTALL_ID_KEY: &str = "installId";
const LICENSE_KEY_KEY: &str = "licenseKey";
const LICENSE_TOKEN_KEY: &str = "licenseToken";
const PORTAL_BASE_URL: &str = "https://app.licensemanager.dev/portal";
const MINUTE_MS: i64 = 60 * 1000;
const RETRY_INITIAL_MS: i64 = 5 * MINUTE_MS;
const RETRY_MAX_MS: i64 = 60 * MINUTE_MS;
const MAX_TIMEOUT_MS: i64 = 60 * MINUTE_MS;
type Listener = Arc<dyn Fn(LicenseState) + Send + Sync>;
type SharedRequest = Shared<BoxFuture<'static, Result<ValidatedToken, LicenseError>>>;
fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(PoisonError::into_inner)
}
#[derive(Clone)]
pub struct LicenseManager {
inner: Arc<Inner>,
}
struct Inner {
product_id: String,
fingerprint_option: Option<MachineFingerprint>,
version: String,
machine_name: String,
platform: String,
min_offline_ms: i64,
api_base_url: String,
store: SecureStore,
verifier: TokenVerifier,
clock_guard: ClockGuard,
clock: Arc<dyn Clock>,
transport: Arc<dyn Transport>,
runtime: tokio::runtime::Handle,
cancel: CancellationToken,
fingerprint: tokio::sync::OnceCell<String>,
state_tx: watch::Sender<LicenseState>,
listeners: Mutex<Vec<(u64, Listener)>>,
next_id: AtomicU64,
ready: OnceLock<Shared<BoxFuture<'static, LicenseState>>>,
m: Mutex<Mutable>,
}
struct Mutable {
key: Option<String>,
state: LicenseState,
token: Option<TokenPayload>,
online: bool,
latest_allowed_version: LatestAllowedVersion,
retry_delay_ms: i64,
timer: Option<(u64, AbortHandle)>,
inflight: Option<Inflight>,
disposed: bool,
}
struct Inflight {
key: String,
policy: RetryPolicy,
id: u64,
request: SharedRequest,
}
enum Stored {
Active(TokenPayload),
Refused(LicenseState),
}
impl Stored {
fn state(&self) -> LicenseState {
match self {
Self::Active(_) => LicenseState::Active,
Self::Refused(state) => *state,
}
}
}
#[must_use = "the listener is removed when the Subscription is dropped"]
pub struct Subscription {
id: u64,
inner: Weak<Inner>,
}
impl Drop for Subscription {
fn drop(&mut self) {
if let Some(inner) = self.inner.upgrade() {
lock(&inner.listeners).retain(|(id, _)| *id != self.id);
}
}
}
impl std::fmt::Debug for Subscription {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Subscription")
.field("id", &self.id)
.finish()
}
}
impl LicenseManager {
pub(crate) fn new(options: LicenseManagerOptions) -> Result<Self, ConfigError> {
if options.product_id.is_empty() {
return Err(ConfigError::MissingProductId);
}
if options.public_key.is_empty() {
return Err(ConfigError::MissingPublicKey);
}
let hooks = options.testing.unwrap_or_default();
let clock = hooks.clock.unwrap_or_else(|| Arc::new(SystemClock));
let transport = hooks
.transport
.unwrap_or_else(|| Arc::new(ReqwestTransport::new()));
let backend = hooks.secret_backend.unwrap_or_else(default_backend);
let verifier = TokenVerifier::new(&options.public_key, &options.product_id, clock.clone())?;
let store = SecureStore::new(&options.product_id, options.storage_dirs, backend);
let runtime = options.runtime;
let clock_guard =
ClockGuard::new(store.clone(), clock.clone(), transport.clone(), &runtime);
let key = store.get(LICENSE_KEY_KEY);
let (state_tx, _) = watch::channel(LicenseState::Loading);
let inner = Arc::new(Inner {
fingerprint_option: options.fingerprint,
version: options.version,
machine_name: options.machine_name.unwrap_or_else(environment::hostname),
platform: options
.platform
.unwrap_or_else(environment::default_platform),
min_offline_ms: i64::try_from(options.min_offline_duration.as_millis())
.unwrap_or(i64::MAX),
api_base_url: options
.api_base_url
.as_deref()
.unwrap_or(DEFAULT_API_BASE_URL)
.trim_end_matches('/')
.to_owned(),
product_id: options.product_id,
store,
verifier,
clock_guard,
clock,
transport,
runtime,
cancel: CancellationToken::new(),
fingerprint: tokio::sync::OnceCell::new(),
state_tx,
listeners: Mutex::new(Vec::new()),
next_id: AtomicU64::new(0),
ready: OnceLock::new(),
m: Mutex::new(Mutable {
key,
state: LicenseState::Loading,
token: None,
online: true,
latest_allowed_version: LatestAllowedVersion::NotApplicable,
retry_delay_ms: RETRY_INITIAL_MS,
timer: None,
inflight: None,
disposed: false,
}),
});
let ready = {
let inner = inner.clone();
async move { inner.init().await }.boxed().shared()
};
let _ = inner.ready.set(ready.clone());
inner.runtime.spawn(ready);
Ok(Self { inner })
}
pub async fn fingerprint(&self) -> String {
self.inner.fingerprint().await
}
pub fn state(&self) -> LicenseState {
lock(&self.inner.m).state
}
pub async fn ready(&self) -> LicenseState {
self.inner.ready().await
}
pub fn on_state_change<F>(&self, listener: F) -> Subscription
where
F: Fn(LicenseState) + Send + Sync + 'static,
{
let id = self.inner.next_id.fetch_add(1, Ordering::SeqCst);
lock(&self.inner.listeners).push((id, Arc::new(listener)));
Subscription {
id,
inner: Arc::downgrade(&self.inner),
}
}
pub fn subscribe(&self) -> watch::Receiver<LicenseState> {
self.inner.state_tx.subscribe()
}
pub async fn set_license_key(&self, input: &str) -> Result<LicenseState, LicenseError> {
let key = input.trim();
if key.is_empty() {
return Err(LicenseError::new(
LicenseErrorCode::ValidationError,
"The license key is empty",
));
}
let validated = self.inner.request(key, INTERACTIVE_RETRY).await?;
self.inner
.store
.set_async(LICENSE_KEY_KEY, key.to_owned())
.await;
lock(&self.inner.m).key = Some(key.to_owned());
self.inner.activate(validated).await;
Ok(self.state())
}
pub async fn refresh(&self) -> LicenseState {
self.inner.ready().await;
let key = lock(&self.inner.m).key.clone();
match key {
Some(key) => self.inner.revalidate(key, INTERACTIVE_RETRY).await,
None => self.state(),
}
}
pub fn license_key(&self) -> Option<String> {
lock(&self.inner.m).key.clone()
}
pub fn portal_url(&self) -> String {
format!(
"{PORTAL_BASE_URL}/{}",
encode_uri_component(&self.inner.product_id)
)
}
pub fn latest_allowed_version(&self) -> LatestAllowedVersion {
lock(&self.inner.m).latest_allowed_version.clone()
}
pub fn offline_state(&self) -> OfflineState {
let m = lock(&self.inner.m);
if m.online {
return OfflineState::Online;
}
match &m.token {
Some(token) if m.state == LicenseState::Active => {
let expires_at = token.exp.saturating_mul(1000);
let remaining = (expires_at - self.inner.clock.now_ms()).max(0);
OfflineState::Offline {
valid_until: Some(from_millis(expires_at)),
remaining: Duration::from_millis(remaining.unsigned_abs()),
}
}
_ => OfflineState::Offline {
valid_until: None,
remaining: Duration::ZERO,
},
}
}
pub fn is_clock_tampered(&self) -> bool {
self.inner.clock_guard.is_tampered()
}
pub fn dispose(&self) {
self.inner.dispose();
}
#[cfg(test)]
pub(crate) fn is_disposed(&self) -> bool {
lock(&self.inner.m).disposed
}
}
impl std::fmt::Debug for LicenseManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LicenseManager")
.field("product_id", &self.inner.product_id)
.field("state", &self.state())
.finish_non_exhaustive()
}
}
impl Drop for Inner {
fn drop(&mut self) {
self.cancel.cancel();
self.clock_guard.dispose();
}
}
impl Inner {
async fn ready(&self) -> LicenseState {
match self.ready.get() {
Some(ready) => ready.clone().await,
None => lock(&self.m).state,
}
}
fn state(&self) -> LicenseState {
lock(&self.m).state
}
fn key_is(&self, key: &str) -> bool {
lock(&self.m).key.as_deref() == Some(key)
}
fn dispose(&self) {
let mut m = lock(&self.m);
m.disposed = true;
self.cancel.cancel();
clear_timer(&mut m);
drop(m);
self.clock_guard.dispose();
}
async fn fingerprint(&self) -> String {
self.fingerprint
.get_or_init(|| async {
let machine = resolve_machine_fingerprint(self.fingerprint_option.as_ref()).await;
let install_id = self.get_or_create_install_id().await;
format!("{machine}:{install_id}:{}", self.product_id)
})
.await
.clone()
}
async fn get_or_create_install_id(&self) -> String {
if let Some(existing) = self
.store
.get_async(INSTALL_ID_KEY)
.await
.filter(|v| !v.is_empty())
{
return existing;
}
let created = STANDARD.encode(rand::random::<[u8; 32]>());
self.store.set_async(INSTALL_ID_KEY, created.clone()).await;
created
}
async fn init(self: Arc<Self>) -> LicenseState {
let Some(key) = lock(&self.m).key.clone() else {
self.set_state(LicenseState::MissingKey);
return self.state();
};
let stored = self.check_stored_token(&key).await;
if !self.key_is(&key) {
return self.state();
}
let Stored::Active(payload) = stored else {
return self.revalidate(key, INTERACTIVE_RETRY).await;
};
lock(&self.m).token = Some(payload);
self.set_state(LicenseState::Active);
let this = self.clone();
self.runtime.spawn(async move {
this.revalidate(key, BACKGROUND_RETRY).await;
});
self.state()
}
async fn revalidate(self: &Arc<Self>, key: String, policy: RetryPolicy) -> LicenseState {
clear_timer(&mut lock(&self.m));
match self.request(&key, policy).await {
Ok(validated) => {
if !self.key_is(&key) {
return self.state();
}
self.activate(validated).await;
}
Err(error) => {
if !self.key_is(&key) {
return self.state();
}
if error.is_locking() {
self.lock_license(error.code, error.latest_allowed_version)
.await;
} else {
self.validate_offline(&key).await;
}
}
}
self.state()
}
async fn activate(self: &Arc<Self>, ValidatedToken { token, payload }: ValidatedToken) {
self.store.set_async(LICENSE_TOKEN_KEY, token).await;
{
let mut m = lock(&self.m);
m.token = Some(payload);
m.online = true;
m.retry_delay_ms = RETRY_INITIAL_MS;
m.latest_allowed_version = LatestAllowedVersion::NotApplicable;
}
self.set_state(LicenseState::Active);
self.schedule_next();
}
async fn lock_license(
&self,
code: LicenseErrorCode,
latest_allowed_version: LatestAllowedVersion,
) {
self.store.delete_async(LICENSE_TOKEN_KEY).await;
let Some(state) = locked_state(code) else {
return;
};
{
let mut m = lock(&self.m);
m.token = None;
m.online = true;
m.latest_allowed_version = match (code, latest_allowed_version) {
(LicenseErrorCode::VersionNotAllowed, LatestAllowedVersion::NotApplicable) => {
LatestAllowedVersion::NoneAllowed
}
(LicenseErrorCode::VersionNotAllowed, latest) => latest,
_ => LatestAllowedVersion::NotApplicable,
};
}
self.set_state(state);
}
async fn validate_offline(self: &Arc<Self>, key: &str) {
lock(&self.m).online = false;
let stored = self.check_stored_token(key).await;
if !self.key_is(key) {
return;
}
let state = stored.state();
{
let mut m = lock(&self.m);
m.token = match stored {
Stored::Active(payload) => Some(payload),
Stored::Refused(_) => None,
};
if state == LicenseState::VersionNotAllowed {
m.latest_allowed_version = LatestAllowedVersion::NotApplicable;
}
}
self.set_state(state);
self.schedule_next();
let mut m = lock(&self.m);
m.retry_delay_ms = (m.retry_delay_ms * 2).min(RETRY_MAX_MS);
}
async fn check_stored_token(&self, key: &str) -> Stored {
self.clock_guard.ready().await;
if self.clock_guard.is_tampered() {
return Stored::Refused(LicenseState::ClockTampered);
}
let token = self
.store
.get_async(LICENSE_TOKEN_KEY)
.await
.filter(|t| !t.is_empty());
let payload = match token {
Some(token) => {
let fingerprint = self.fingerprint().await;
self.verifier.verify(&token, key, &fingerprint).ok()
}
None => None,
};
let Some(payload) = payload else {
return Stored::Refused(LicenseState::OfflineExpired);
};
if !is_version_allowed(&self.version, payload.mv.as_deref()) {
return Stored::Refused(LicenseState::VersionNotAllowed);
}
Stored::Active(payload)
}
fn schedule_next(self: &Arc<Self>) {
let mut m = lock(&self.m);
clear_timer(&mut m);
let Some(key) = m.key.clone().filter(|_| !m.disposed) else {
return;
};
let now = self.clock.now_ms();
let at = match (&m.token, m.state) {
(Some(token), LicenseState::Active) => {
let mut at = compute_refresh_at(token, self.min_offline_ms);
if !m.online {
at = at.max(now + m.retry_delay_ms);
}
at.min(token.exp.saturating_mul(1000))
}
(_, LicenseState::OfflineExpired | LicenseState::ClockTampered) => {
now + m.retry_delay_ms
}
(_, LicenseState::VersionNotAllowed) if !m.online => now + m.retry_delay_ms,
_ => return,
};
let id = self.next_id.fetch_add(1, Ordering::SeqCst);
let first_step = tokio::time::Instant::now() + step_until(at, now);
let handle = self
.runtime
.spawn(arm(Arc::downgrade(self), id, key, at, first_step));
m.timer = Some((id, handle.abort_handle()));
}
fn request(self: &Arc<Self>, key: &str, policy: RetryPolicy) -> SharedRequest {
let mut m = lock(&self.m);
if let Some(inflight) = &m.inflight {
if inflight.key == key && inflight.policy == policy {
return inflight.request.clone();
}
}
let id = self.next_id.fetch_add(1, Ordering::SeqCst);
let request = {
let this = self.clone();
let key = key.to_owned();
async move {
let result = this.fetch_token(&key, policy).await;
let mut m = lock(&this.m);
if m.inflight.as_ref().is_some_and(|i| i.id == id) {
m.inflight = None;
}
result
}
.boxed()
.shared()
};
m.inflight = Some(Inflight {
key: key.to_owned(),
policy,
id,
request: request.clone(),
});
self.runtime.spawn(request.clone());
request
}
async fn fetch_token(
&self,
key: &str,
policy: RetryPolicy,
) -> Result<ValidatedToken, LicenseError> {
let fingerprint = self.fingerprint().await;
let request = ValidateKeyRequest {
key: key.to_owned(),
fingerprint: fingerprint.clone(),
version: self.version.clone(),
name: self.machine_name.clone(),
platform: self.platform.clone(),
};
let (token, status) = with_retry(
|remaining| {
api::post_validate_key(
&*self.transport,
&self.api_base_url,
&self.product_id,
&request,
remaining,
)
},
api::is_transient,
policy,
&self.cancel,
)
.await?;
match self.verifier.verify(&token, key, &fingerprint) {
Ok(payload) => Ok(ValidatedToken { token, payload }),
Err(_) => Err(LicenseError::new(
LicenseErrorCode::InvalidResponse,
"The license token failed verification",
)
.with_status(status)),
}
}
fn set_state(&self, state: LicenseState) {
{
let mut m = lock(&self.m);
if m.state == state {
return;
}
m.state = state;
}
self.state_tx.send_replace(state);
let listeners: Vec<Listener> = lock(&self.listeners)
.iter()
.map(|(_, listener)| listener.clone())
.collect();
for listener in listeners {
let _ = catch_unwind(AssertUnwindSafe(|| listener(state)));
}
}
}
fn step_until(at: i64, now: i64) -> Duration {
Duration::from_millis((at - now).clamp(0, MAX_TIMEOUT_MS).unsigned_abs())
}
fn clear_timer(m: &mut Mutable) {
if let Some((_, timer)) = m.timer.take() {
timer.abort();
}
}
async fn arm(inner: Weak<Inner>, id: u64, key: String, at: i64, first_step: tokio::time::Instant) {
let (clock, cancel) = match inner.upgrade() {
Some(inner) => (inner.clock.clone(), inner.cancel.clone()),
None => return,
};
let mut step = first_step;
loop {
tokio::select! {
() = tokio::time::sleep_until(step) => {}
() = cancel.cancelled() => return,
}
let now = clock.now_ms();
if now >= at {
break;
}
step = tokio::time::Instant::now() + step_until(at, now);
}
let Some(inner) = inner.upgrade() else {
return;
};
{
let mut m = lock(&inner.m);
if m.timer.as_ref().map(|(timer_id, _)| *timer_id) != Some(id) {
return;
}
m.timer = None;
}
inner.revalidate(key, BACKGROUND_RETRY).await;
}
#[cfg(test)]
mod tests;