use anyhow::{Result, anyhow};
use chrono::{DateTime, Duration, Utc};
use mac_address::get_mac_address;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::fs;
use std::path::Path;
use std::process::Command;
const LICENSE_FILE: &str = ".opensearch-api-license";
const TRIAL_DAYS: i64 = 7;
#[derive(Debug, Serialize, Deserialize)]
pub struct License {
pub key: String,
pub fingerprint: String,
pub ip: String,
pub product: String,
pub issued_at: DateTime<Utc>,
pub expires_at: Option<DateTime<Utc>>,
pub is_trial: bool,
pub validated_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SystemInfo {
pub fingerprint: String,
pub ip: String,
pub product: String,
pub hostname: String,
pub mac_address: String,
pub cpu_info: String,
}
impl SystemInfo {
pub fn collect() -> Result<Self> {
let hostname = hostname::get()?.to_string_lossy().to_string();
let mac_address = get_mac_address()?
.map(|mac| mac.to_string())
.unwrap_or_else(|| "unknown".to_string());
let cpu_info = if cfg!(target_os = "linux") {
fs::read_to_string("/proc/cpuinfo")
.ok()
.and_then(|content| {
content
.lines()
.find(|line| line.starts_with("model name"))
.map(|line| line.split(':').nth(1).unwrap_or("").trim().to_string())
})
.unwrap_or_else(|| "unknown".to_string())
} else {
"unknown".to_string()
};
let ip = get_local_ip().unwrap_or_else(|| "127.0.0.1".to_string());
let fingerprint = Self::generate_fingerprint(&hostname, &mac_address, &cpu_info);
Ok(Self {
fingerprint,
ip,
product: "opensearch-api".to_string(),
hostname,
mac_address,
cpu_info,
})
}
fn generate_fingerprint(hostname: &str, mac: &str, cpu: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(hostname.as_bytes());
hasher.update(mac.as_bytes());
hasher.update(cpu.as_bytes());
let result = hasher.finalize();
format!("{:x}", result)
}
}
fn get_local_ip() -> Option<String> {
let output = Command::new("hostname").arg("-I").output().ok()?;
if output.status.success() {
let ips = String::from_utf8_lossy(&output.stdout);
ips.split_whitespace().next().map(|ip| ip.to_string())
} else {
None
}
}
pub struct LicenseManager {
license_path: String,
}
impl LicenseManager {
pub fn new() -> Self {
let license_path = LICENSE_FILE.to_string();
Self { license_path }
}
pub fn check_license(&self) -> Result<License> {
if !Path::new(&self.license_path).exists() {
return self.check_trial();
}
let license_data = fs::read_to_string(&self.license_path)?;
let license: License = serde_json::from_str(&license_data)?;
let system_info = SystemInfo::collect()?;
if license.fingerprint != system_info.fingerprint {
return Err(anyhow!("License is not valid for this system"));
}
if let Some(expires_at) = license.expires_at {
if Utc::now() > expires_at {
return Err(anyhow!("License has expired"));
}
}
Ok(license)
}
fn check_trial(&self) -> Result<License> {
let trial_file = format!("{}.trial", self.license_path);
if !Path::new(&trial_file).exists() {
let system_info = SystemInfo::collect()?;
let trial = License {
key: "TRIAL".to_string(),
fingerprint: system_info.fingerprint.clone(),
ip: system_info.ip.clone(),
product: system_info.product.clone(),
issued_at: Utc::now(),
expires_at: Some(Utc::now() + Duration::days(TRIAL_DAYS)),
is_trial: true,
validated_at: Utc::now(),
};
let trial_data = serde_json::to_string_pretty(&trial)?;
fs::write(&trial_file, trial_data)?;
println!("🔔 Trial license created (valid for {} days)", TRIAL_DAYS);
println!(" To get a full license, run: opensearch-api --license");
return Ok(trial);
}
let trial_data = fs::read_to_string(&trial_file)?;
let trial: License = serde_json::from_str(&trial_data)?;
if let Some(expires_at) = trial.expires_at {
if Utc::now() > expires_at {
return Err(anyhow!(
"Trial period has expired. Please obtain a license by running: opensearch-api --license"
));
}
let days_left = (expires_at - Utc::now()).num_days();
if days_left <= 2 {
println!(
"⚠️ Trial expires in {} days. Run 'opensearch-api --license' to get a full license",
days_left
);
}
}
Ok(trial)
}
pub fn save_license(&self, license_key: &str) -> Result<()> {
let system_info = SystemInfo::collect()?;
let license = License {
key: license_key.to_string(),
fingerprint: system_info.fingerprint,
ip: system_info.ip,
product: system_info.product,
issued_at: Utc::now(),
expires_at: None, is_trial: false,
validated_at: Utc::now(),
};
let license_data = serde_json::to_string_pretty(&license)?;
fs::write(&self.license_path, license_data)?;
let trial_file = format!("{}.trial", self.license_path);
if Path::new(&trial_file).exists() {
fs::remove_file(&trial_file).ok();
}
println!("✅ License saved successfully");
Ok(())
}
pub fn show_license_request() -> Result<()> {
let system_info = SystemInfo::collect()?;
println!("\n{}", "=".repeat(70));
println!("LICENSE REQUEST INFORMATION");
println!("{}", "=".repeat(70));
println!("Please provide this information to obtain a license:\n");
println!("Product: {}", system_info.product);
println!("Hostname: {}", system_info.hostname);
println!("IP Address: {}", system_info.ip);
println!("Fingerprint: {}", system_info.fingerprint);
println!("\nSystem Details:");
println!(" MAC Address: {}", system_info.mac_address);
println!(" CPU Info: {}", system_info.cpu_info);
println!("{}", "=".repeat(70));
println!("\nTo activate your license:");
println!("1. Send the above information to your license provider");
println!("2. Once you receive your license key, run:");
println!(" opensearch-api --activate-license YOUR_LICENSE_KEY");
println!("{}", "=".repeat(70));
Ok(())
}
}