#![cfg(feature = "quic")]
use std::net::{SocketAddr, ToSocketAddrs};
use std::path::PathBuf;
use ikigai_quic::Identity;
use crate::Certs;
fn dir() -> Result<PathBuf, String> {
let config = ikigai_engine::config::path()
.ok_or("no config directory (set $XDG_CONFIG_HOME or $HOME)")?;
let parent = config
.parent()
.ok_or("config path has no parent directory")?;
Ok(parent.join("quic"))
}
fn base_dir(certs: &Certs) -> Result<PathBuf, String> {
match &certs.cert_dir {
Some(d) => Ok(PathBuf::from(d)),
None => dir(),
}
}
fn default_path(name: &str, certs: &Certs) -> Result<PathBuf, String> {
Ok(base_dir(certs)?.join(name))
}
pub fn generate(force: bool, dir_override: Option<PathBuf>) -> Result<PathBuf, String> {
let dir = match dir_override {
Some(d) => d,
None => dir()?,
};
std::fs::create_dir_all(&dir).map_err(|e| format!("create {}: {e}", dir.display()))?;
restrict(&dir, 0o700)?;
if !force {
let existing: Vec<String> = ["server.crt", "server.key", "client.crt", "client.key"]
.iter()
.map(|name| dir.join(name))
.filter(|path| path.exists())
.map(|path| path.display().to_string())
.collect();
if !existing.is_empty() {
return Err(format!(
"{} already exist(s) — nothing was written (use `--force` to replace the whole set)",
existing.join(", ")
));
}
}
for (name, identity) in [
("server", ikigai_quic::generate()),
("client", ikigai_quic::generate()),
] {
write(
&dir.join(format!("{name}.crt")),
&identity.cert_pem,
0o600,
force,
)?;
write(
&dir.join(format!("{name}.key")),
&identity.key_pem,
0o600,
force,
)?;
}
Ok(dir)
}
fn write(path: &std::path::Path, contents: &str, mode: u32, force: bool) -> Result<(), String> {
if path.exists() && !force {
return Err(format!(
"{} already exists (use `--force` to overwrite)",
path.display()
));
}
std::fs::write(path, contents).map_err(|e| format!("write {}: {e}", path.display()))?;
restrict(path, mode)?;
Ok(())
}
pub fn server_identity(certs: &Certs) -> Result<Identity, String> {
Ok(Identity {
cert_pem: read(certs.server_cert.clone(), "server.crt", certs)?,
key_pem: read(certs.server_key.clone(), "server.key", certs)?,
})
}
pub fn client_identity(certs: &Certs) -> Result<Identity, String> {
Ok(Identity {
cert_pem: read(certs.client_cert.clone(), "client.crt", certs)?,
key_pem: read(certs.client_key.clone(), "client.key", certs)?,
})
}
pub fn trusted_client_cert(certs: &Certs) -> Result<String, String> {
read(certs.client_cert.clone(), "client.crt", certs)
}
pub fn add_client(name: &str, certs: &Certs, force: bool) -> Result<PathBuf, String> {
if name.is_empty() || name.contains('/') || name.contains("..") {
return Err(format!("invalid client name: {name:?}"));
}
let clients = base_dir(certs)?.join("clients");
std::fs::create_dir_all(&clients).map_err(|e| format!("create {}: {e}", clients.display()))?;
restrict(&clients, 0o700)?;
let identity = ikigai_quic::generate();
let crt = clients.join(format!("{name}.crt"));
write(&crt, &identity.cert_pem, 0o600, force)?;
write(
&clients.join(format!("{name}.key")),
&identity.key_pem,
0o600,
force,
)?;
Ok(crt)
}
pub fn trusted_client_certs(certs: &Certs) -> Result<Vec<String>, String> {
let mut pems = vec![trusted_client_cert(certs)?];
if let Ok(entries) = base_dir(certs)
.map(|d| d.join("clients"))
.and_then(|clients| std::fs::read_dir(&clients).map_err(|e| e.to_string()))
{
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "crt") {
if let Ok(pem) = std::fs::read_to_string(&path) {
pems.push(pem);
}
}
}
}
Ok(pems)
}
pub fn trusted_server_cert(certs: &Certs) -> Result<String, String> {
read(certs.server_cert.clone(), "server.crt", certs)
}
fn read(explicit: Option<String>, default_name: &str, certs: &Certs) -> Result<String, String> {
let path = match explicit {
Some(path) => PathBuf::from(path),
None => default_path(default_name, certs)?,
};
std::fs::read_to_string(&path).map_err(|e| {
format!(
"read {}: {e} — run `ikigai cert generate` first?",
path.display()
)
})
}
pub fn parse_addr(target: &str) -> Result<SocketAddr, String> {
let hostport = target
.strip_prefix("quic://")
.ok_or_else(|| format!("not a quic:// target: {target}"))?;
let mut addrs: Vec<SocketAddr> = hostport
.to_socket_addrs()
.map_err(|e| format!("resolve {hostport}: {e}"))?
.collect();
addrs.sort_by_key(|a| a.is_ipv6());
addrs
.into_iter()
.next()
.ok_or_else(|| format!("{hostport} resolved to no address"))
}
#[cfg(unix)]
fn restrict(path: &std::path::Path, mode: u32) -> Result<(), String> {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
.map_err(|e| format!("set permissions on {}: {e}", path.display()))
}
#[cfg(not(unix))]
fn restrict(_path: &std::path::Path, _mode: u32) -> Result<(), String> {
Ok(())
}