use std::net::IpAddr;
use std::path::{Path, PathBuf};
use std::sync::Once;
static INSTALL_PROVIDER: Once = Once::new();
pub fn ensure_crypto_provider_installed() {
INSTALL_PROVIDER.call_once(|| {
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
});
}
pub fn cert_path() -> PathBuf {
std::env::var("GHOSTLINK_TLS_CERT_PATH")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("tls_cert.pem"))
}
pub fn key_path() -> PathBuf {
std::env::var("GHOSTLINK_TLS_KEY_PATH")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("tls_key.pem"))
}
pub fn ensure_self_signed_cert(
cert_path: &Path,
key_path: &Path,
san_names: Vec<String>,
) -> Result<(), String> {
if cert_path.exists() && key_path.exists() {
return Ok(());
}
let certified_key = rcgen::generate_simple_self_signed(san_names)
.map_err(|e| format!("failed to generate self-signed certificate: {e}"))?;
std::fs::write(cert_path, certified_key.cert.pem())
.map_err(|e| format!("failed to write TLS cert to {}: {e}", cert_path.display()))?;
std::fs::write(key_path, certified_key.signing_key.serialize_pem())
.map_err(|e| format!("failed to write TLS key to {}: {e}", key_path.display()))?;
Ok(())
}
pub fn is_loopback_host(host: &str) -> bool {
if host.eq_ignore_ascii_case("localhost") {
return true;
}
host.parse::<IpAddr>()
.map(|ip| ip.is_loopback())
.unwrap_or(false)
}
pub async fn build_rustls_config(
host: &str,
) -> Result<axum_server::tls_rustls::RustlsConfig, String> {
ensure_crypto_provider_installed();
let cert = cert_path();
let key = key_path();
let mut san_names = vec!["localhost".to_string(), "127.0.0.1".to_string()];
if !is_loopback_host(host) {
san_names.push(host.to_string());
}
ensure_self_signed_cert(&cert, &key, san_names)?;
axum_server::tls_rustls::RustlsConfig::from_pem_file(&cert, &key)
.await
.map_err(|e| format!("failed to load TLS cert/key into rustls config: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_loopback_host_recognizes_common_local_forms() {
assert!(is_loopback_host("localhost"));
assert!(is_loopback_host("LOCALHOST"));
assert!(is_loopback_host("127.0.0.1"));
assert!(is_loopback_host("::1"));
}
#[test]
fn is_loopback_host_rejects_lan_and_remote_addresses() {
assert!(!is_loopback_host("0.0.0.0"));
assert!(!is_loopback_host("192.168.1.50"));
assert!(!is_loopback_host("example.com"));
assert!(!is_loopback_host(""));
}
#[test]
fn ensure_self_signed_cert_generates_once_and_is_idempotent() {
let dir = std::env::temp_dir().join(format!("ghostlink-tls-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create temp dir");
let cert_path = dir.join("cert.pem");
let key_path = dir.join("key.pem");
let _ = std::fs::remove_file(&cert_path);
let _ = std::fs::remove_file(&key_path);
ensure_self_signed_cert(&cert_path, &key_path, vec!["localhost".to_string()])
.expect("first generation should succeed");
assert!(cert_path.exists());
assert!(key_path.exists());
let cert_bytes_before = std::fs::read(&cert_path).expect("read cert");
ensure_self_signed_cert(&cert_path, &key_path, vec!["localhost".to_string()])
.expect("idempotent call should succeed");
let cert_bytes_after = std::fs::read(&cert_path).expect("read cert again");
assert_eq!(cert_bytes_before, cert_bytes_after);
let _ = std::fs::remove_file(&cert_path);
let _ = std::fs::remove_file(&key_path);
let _ = std::fs::remove_dir(&dir);
}
}