use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct HardwareBinding {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub mac_addresses: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub disk_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub hostnames: Vec<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub custom: HashMap<String, Vec<String>>,
}
impl HardwareBinding {
pub fn new() -> Self {
Self::default()
}
pub fn with_mac_address(mut self, mac: impl Into<String>) -> Self {
self.mac_addresses.push(mac.into().to_uppercase());
self
}
pub fn with_mac_addresses(mut self, macs: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.mac_addresses
.extend(macs.into_iter().map(|m| m.into().to_uppercase()));
self
}
pub fn with_disk_id(mut self, disk_id: impl Into<String>) -> Self {
self.disk_ids.push(disk_id.into());
self
}
pub fn with_hostname(mut self, hostname: impl Into<String>) -> Self {
self.hostnames.push(hostname.into().to_lowercase());
self
}
pub fn with_custom(mut self, key: impl Into<String>, values: Vec<String>) -> Self {
self.custom.insert(key.into(), values);
self
}
pub fn is_empty(&self) -> bool {
self.mac_addresses.is_empty()
&& self.disk_ids.is_empty()
&& self.hostnames.is_empty()
&& self.custom.is_empty()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LicenseData {
pub id: String,
pub serial: String,
pub customer_id: String,
pub product_id: String,
#[serde(default = "default_version")]
pub version: u32,
pub valid_from: DateTime<Utc>,
pub valid_until: DateTime<Utc>,
#[serde(default)]
pub features: Vec<String>,
#[serde(default)]
pub hardware_binding: HardwareBinding,
#[serde(default)]
pub max_seats: u32,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, String>,
pub issued_at: DateTime<Utc>,
}
fn default_version() -> u32 {
1
}
impl LicenseData {
pub fn builder() -> LicenseDataBuilder {
LicenseDataBuilder::new()
}
pub fn has_feature(&self, feature: &str) -> bool {
self.features
.iter()
.any(|f| f.eq_ignore_ascii_case(feature))
}
pub fn days_remaining(&self) -> i64 {
let now = Utc::now();
(self.valid_until - now).num_days()
}
pub fn is_time_valid(&self) -> bool {
let now = Utc::now();
now >= self.valid_from && now <= self.valid_until
}
}
#[derive(Default)]
pub struct LicenseDataBuilder {
id: Option<String>,
serial: Option<String>,
customer_id: Option<String>,
product_id: Option<String>,
version: u32,
valid_from: Option<DateTime<Utc>>,
valid_until: Option<DateTime<Utc>>,
features: Vec<String>,
hardware_binding: HardwareBinding,
max_seats: u32,
metadata: HashMap<String, String>,
}
impl LicenseDataBuilder {
pub fn new() -> Self {
Self {
version: 1,
..Default::default()
}
}
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn serial(mut self, serial: impl Into<String>) -> Self {
self.serial = Some(serial.into());
self
}
pub fn customer_id(mut self, customer_id: impl Into<String>) -> Self {
self.customer_id = Some(customer_id.into());
self
}
pub fn product_id(mut self, product_id: impl Into<String>) -> Self {
self.product_id = Some(product_id.into());
self
}
pub fn version(mut self, version: u32) -> Self {
self.version = version;
self
}
pub fn valid_from(mut self, valid_from: DateTime<Utc>) -> Self {
self.valid_from = Some(valid_from);
self
}
pub fn valid_until(mut self, valid_until: DateTime<Utc>) -> Self {
self.valid_until = Some(valid_until);
self
}
pub fn valid_days(mut self, days: i64) -> Self {
let now = Utc::now();
self.valid_from = Some(now);
self.valid_until = Some(now + chrono::Duration::days(days));
self
}
pub fn feature(mut self, feature: impl Into<String>) -> Self {
self.features.push(feature.into());
self
}
pub fn features(mut self, features: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.features.extend(features.into_iter().map(|f| f.into()));
self
}
pub fn hardware_binding(mut self, binding: HardwareBinding) -> Self {
self.hardware_binding = binding;
self
}
pub fn max_seats(mut self, max_seats: u32) -> Self {
self.max_seats = max_seats;
self
}
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn build(self) -> crate::Result<LicenseData> {
use crate::error::LicenseError;
let now = Utc::now();
Ok(LicenseData {
id: self
.id
.ok_or_else(|| LicenseError::MissingField("id".into()))?,
serial: self
.serial
.ok_or_else(|| LicenseError::MissingField("serial".into()))?,
customer_id: self
.customer_id
.ok_or_else(|| LicenseError::MissingField("customer_id".into()))?,
product_id: self
.product_id
.ok_or_else(|| LicenseError::MissingField("product_id".into()))?,
version: self.version,
valid_from: self.valid_from.unwrap_or(now),
valid_until: self
.valid_until
.unwrap_or(now + chrono::Duration::days(365)),
features: self.features,
hardware_binding: self.hardware_binding,
max_seats: self.max_seats,
metadata: self.metadata,
issued_at: now,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedLicense {
pub data: LicenseData,
pub signature: String,
pub algorithm: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LicenseFormat {
Binary,
Json,
}
pub const BINARY_MAGIC: &[u8; 4] = b"FLIC";
pub const BINARY_VERSION: u8 = 1;