fluffer 4.0.2

🦊 Fluffer is a fun and experimental gemini server framework.
Documentation
use rcgen::{CertificateParams, DistinguishedName, DnType, DnValue};
use std::{
	fs::{File, Permissions},
	io::{stdin, stdout, Write},
	os::unix::prelude::PermissionsExt,
	path::PathBuf,
};

use crate::err::AppErr;

const OK: &str = "\x1b[0m\x1b[33m[fluffer]\x1b[0m";
const FAIL: &str = "\x1b[31m[fluffer]\x1b[0m";
const PROMPT: &str = "\x1b[33;3m→ ";

/// Interactively generate a certificate if one doesn't exist.
pub fn gen_cert(cert: &PathBuf, key: &PathBuf) -> Result<(), AppErr> {
	let cert_display = cert.display();
	let key_display = key.display();

	match (File::open(cert).is_ok(), File::open(key).is_ok()) {
		(false, true) => {
			eprintln!("{FAIL} Missing certificate: [{cert_display}].");
			return Err(AppErr::RcGenStop);
		}
		(true, false) => {
			eprintln!("{FAIL} Missing private key: [{key_display}].");
			return Err(AppErr::RcGenStop);
		}
		(true, true) => return Ok(()),
		(false, false) => (),
	}

	let stdin = stdin();
	let mut stdout = stdout();

	// Y/N
	print!(
		"\n{OK} Missing certificate files!
Expected two files: [{cert_display}] and [{key_display}].

\x1b[1mDo you want to generate a keypair interactively?\x1b[0m [y/n]
{PROMPT}"
	);
	stdout.flush()?;
	let mut yorn = String::new();
	stdin.read_line(&mut yorn)?;

	if yorn != "y\n" {
		return Err(AppErr::RcGenStop);
	}

	// Prompt domain(s)
	print!(
		"\n{OK} Enter a comma-separated list of domain name(s) you will be using.
e.g. localhost, *.localhost, sub.example.com
{PROMPT}"
	);
	stdout.flush()?;

	let mut domains = String::new();
	stdin.read_line(&mut domains)?;

	let domains: Vec<String> = domains.split(',').map(|d| d.trim().to_string()).collect();

	if domains.iter().all(|x| x.is_empty()) {
		return Err(AppErr::RcGenNoDomains);
	}

	// Preview domains
	println!("\x1b[3m{domains:#?}\x1b[0m");

	// Use the first domain as the subject name
	let subject_name = domains.first().ok_or(AppErr::RcGenNoDomains)?.clone();
	let subject_alt_names = domains;

	// Create params
	let mut params = CertificateParams::new(subject_alt_names);
	params.distinguished_name = DistinguishedName::new();
	params
		.distinguished_name
		.push(DnType::CommonName, DnValue::Utf8String(subject_name));

	// Generate keypair
	let gen_pair = rcgen::Certificate::from_params(params)?;

	// Write cert
	let pem = gen_pair.serialize_pem()?;
	let mut file = File::create(cert)?;
	write!(file, "{pem}")?;
	println!("{OK} 📜 Wrote cert.pem");

	// Write key
	let mut file = File::create("key.pem")?;
	file.set_permissions(Permissions::from_mode(0o600))?;
	write!(file, "{}", gen_pair.serialize_private_key_pem())?;
	println!("{OK} 🔑 Wrote {key_display}");

	Ok(())
}