use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{Context, Result};
use rcgen::{
CertificateParams, DnType, IsCa, KeyPair, SanType,
};
use rustls_pki_types::{CertificateDer, PrivateKeyDer};
const CERT_VALIDITY_DAYS: u32 = 30;
pub fn ensure_dev_cert(extra_hosts: &[String]) -> Result<(PathBuf, PathBuf)> {
let cache_dir = cache_dir();
fs::create_dir_all(&cache_dir)
.with_context(|| format!("creating cert cache dir: {}", cache_dir.display()))?;
let cert_path = cache_dir.join("dev-server.crt");
let key_path = cache_dir.join("dev-server.key");
if cert_path.exists() && key_path.exists() && !is_expired(&cert_path) {
eprintln!("[ssl] Using cached certificate from {}", cache_dir.display());
return Ok((cert_path, key_path));
}
generate_cert(&cert_path, &key_path, extra_hosts)?;
Ok((cert_path, key_path))
}
pub fn load_rustls_config(cert_path: &PathBuf, key_path: &PathBuf) -> Result<rustls::ServerConfig> {
let cert_pem = fs::read(cert_path).context("reading cert file")?;
let key_pem = fs::read(key_path).context("reading key file")?;
let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut &cert_pem[..])
.collect::<std::result::Result<Vec<_>, _>>()
.context("parsing cert PEM")?;
let key: PrivateKeyDer<'static> = rustls_pemfile::private_key(&mut &key_pem[..])
.context("parsing key PEM")?
.context("no private key found in PEM")?;
let config = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)
.context("building rustls ServerConfig")?;
Ok(config)
}
fn cache_dir() -> PathBuf {
if let Ok(dir) = std::env::var("MOBUX_CERT_DIR") {
return PathBuf::from(dir);
}
dirs_cache().join("mobux").join("ssl")
}
fn dirs_cache() -> PathBuf {
if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
return PathBuf::from(xdg);
}
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
PathBuf::from(home).join(".local").join("share")
}
fn is_expired(cert_path: &PathBuf) -> bool {
let Ok(meta) = fs::metadata(cert_path) else {
return true;
};
let Ok(modified) = meta.modified() else {
return true;
};
let age = SystemTime::now()
.duration_since(modified)
.unwrap_or_default();
let max_age_secs = ((CERT_VALIDITY_DAYS - 1) as u64) * 24 * 3600;
age.as_secs() > max_age_secs
}
fn generate_cert(cert_path: &PathBuf, key_path: &PathBuf, extra_hosts: &[String]) -> Result<()> {
let mut sans: Vec<SanType> = vec![
SanType::DnsName("localhost".try_into()?),
SanType::IpAddress(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1))),
SanType::IpAddress(std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST)),
SanType::IpAddress(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED)),
];
if let Ok(hn) = hostname::get() {
if let Some(hn_str) = hn.to_str() {
if let Ok(name) = hn_str.try_into() {
sans.push(SanType::DnsName(name));
}
}
}
for host in extra_hosts {
if let Ok(ip) = host.parse::<std::net::IpAddr>() {
sans.push(SanType::IpAddress(ip));
} else if let Ok(name) = host.as_str().try_into() {
sans.push(SanType::DnsName(name));
}
}
let san_display: Vec<String> = sans
.iter()
.map(|s| match s {
SanType::DnsName(n) => n.to_string(),
SanType::IpAddress(ip) => ip.to_string(),
_ => "?".to_string(),
})
.collect();
eprintln!("[ssl] Generating self-signed certificate …");
eprintln!("[ssl] SANs: {}", san_display.join(", "));
eprintln!("[ssl] Valid for {} days", CERT_VALIDITY_DAYS);
eprintln!("[ssl] Cache: {}", cert_path.parent().unwrap().display());
let key_pair = KeyPair::generate()?;
let mut params = CertificateParams::default();
params.distinguished_name.push(DnType::CommonName, "Mobux Dev Server");
params.distinguished_name.push(DnType::OrganizationName, "Local Development");
params.subject_alt_names = sans;
params.not_before = rcgen::date_time_ymd(2024, 1, 1);
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
let end = now.as_secs() + (CERT_VALIDITY_DAYS as u64) * 24 * 3600;
let end_days = end / 86400;
let (y, m, d) = days_to_ymd(end_days);
params.not_after = rcgen::date_time_ymd(y as i32, m as u8, d as u8);
params.is_ca = IsCa::NoCa;
let cert = params.self_signed(&key_pair)?;
fs::write(key_path, key_pair.serialize_pem()).context("writing key file")?;
fs::write(cert_path, cert.pem()).context("writing cert file")?;
eprintln!("[ssl] Certificate generated successfully.");
eprintln!("[ssl] ⚠ Your browser will show a security warning — this is expected.");
eprintln!("[ssl] In Chrome, type 'thisisunsafe' on the warning page to bypass it.");
Ok(())
}
fn days_to_ymd(days: u64) -> (u64, u64, u64) {
let z = days + 719468;
let era = z / 146097;
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y, m, d)
}