//! Tools for automatically generating certificates //! //! Really, the only thing you will probably ever need from this module if you aren't //! developing the project is the [`CertGenMode`] enum, which can be passed to //! [`Server::set_certificate_generation_mode()`]. You won't even need to call any //! methods on it or anything. //! //! [`Server::set_certificate_generation_mode()`]: crate::Server::set_certificate_generation_mode() use anyhow::{bail, Context, Result}; use rustls::ServerConfig; use std::fs; use std::io::{stdin, stdout, Write}; use std::path::Path; #[derive(Clone, Debug)] /// The mode to use for determining the domains to use for a new certificate. Only /// applies to [`CertGenMode::gencert()`]. pub enum CertGenMode { /// Do not generate any certificates. Error if not available. None, /// Use a provided set of domains Preset(Vec), /// Prompt the user using stdin/stdout to enter domains. Interactive, } impl CertGenMode { /// Generate a new self-signed certificate /// /// Assumes that certificates do not already exist, and will overwrite anything at the /// provided paths. The paths provided should be paths to non-existant files which /// the program has access to write to. /// /// ## Errors /// /// Returns an error if [`CertGenMode::None`], or if there is an error generating the /// certificate, or writing to either of the provided files. pub fn gencert(self, cert: impl AsRef, key: impl AsRef) -> Result { let (domains, interactive) = match self { Self::None => bail!("Automatic certificate generation disabled"), Self::Preset(domains) => (domains, false), Self::Interactive => (prompt_domains(), true), }; let certificate = rcgen::generate_simple_self_signed(domains) .context("Could not generate a certificate with the given domains")?; fs::create_dir_all( cert.as_ref() .parent() .expect("Received directory as certificate path, should be a file.") )?; fs::create_dir_all( key.as_ref() .parent() .expect("Received directory as key path, should be a file.") )?; fs::write(cert.as_ref(), &certificate.serialize_pem()?.as_bytes()) .context("Failed to write newly generated certificate to file")?; fs::write(key.as_ref(), &certificate.serialize_private_key_pem().as_bytes()) .context("Failed to write newly generated private key to file")?; if interactive { println!("Certificate generated successfully!"); } Ok(certificate) } /// Attempts to load a certificate/key from a file, or generate it if not found /// /// The produced certificate & key is immediately fed to a [`rustls::ServerConfig`] /// /// See [`CertGenMode::gencert()`] for more info. /// /// ## Errors /// /// Returns an error if a certificate is not found **and** cannot be generated. pub fn load_or_generate(self, to: &mut ServerConfig, cert: impl AsRef, key: impl AsRef) -> Result<()> { match (crate::load_cert_chain(&cert.as_ref().into()), crate::load_key(&key.as_ref().into())) { (Ok(cert_chain), Ok(key)) => { to.set_single_cert(cert_chain, key) .context("Failed to use loaded TLS certificate")?; }, (Err(e), _) | (_, Err(e)) => { warn!("Failed to load certificate from file: {}, now trying automatic generation", e); let cert = self.gencert(cert, key).context("Could not generate certificate")?; to.set_single_cert( vec![rustls::Certificate(cert.serialize_der()?)], rustls::PrivateKey(cert.serialize_private_key_der()) )?; } } Ok(()) } } /// Attempt to get domains by prompting the user /// /// Guaranteed to return at least one domain. The user is provided `localhost` as a /// default. /// /// ## Panics /// Panics if reading from stdin or writing to stdout returns an error. pub fn prompt_domains() -> Vec { let mut domains = Vec::with_capacity(1); let mut input = String::with_capacity(8); println!("Now generating self-signed certificate..."); print!("Please enter a domain (CN) for your certificate [localhost]: "); stdout().flush().unwrap(); loop { stdin().read_line(&mut input).unwrap(); let domain = input.trim(); if domain.is_empty() { if domains.is_empty() { println!("Using `localhost` as domain."); domains.push("localhost".to_string()); } return domains; } else { domains.push(domain.to_owned()); input.clear(); print!("Add another domain, or finish with a blank input: "); stdout().flush().unwrap(); } } }