From c567b79dadd082aefe6078422507c5726b7c4272 Mon Sep 17 00:00:00 2001 From: Joep Meindertsma Date: Thu, 2 Feb 2023 22:23:07 +0100 Subject: [PATCH] #192 #574 make openssl optional --- server/Cargo.toml | 3 +- server/src/bin.rs | 2 + server/src/errors.rs | 2 +- server/src/https.rs | 193 ++------------------------------------- server/src/https_init.rs | 191 ++++++++++++++++++++++++++++++++++++++ server/src/lib.rs | 2 + server/src/serve.rs | 7 +- 7 files changed, 209 insertions(+), 191 deletions(-) create mode 100644 server/src/https_init.rs diff --git a/server/Cargo.toml b/server/Cargo.toml index e64224226..e9b4d0fe1 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -102,7 +102,8 @@ assert_cmd = "2" [features] default = ["https", "telemetry"] -https = ["acme-lib", "rustls"] +https = ["rustls"] +https_init = ["acme-lib"] process-management = ["sysinfo"] telemetry = ["tracing-opentelemetry", "opentelemetry", "opentelemetry-jaeger"] diff --git a/server/src/bin.rs b/server/src/bin.rs index 835ded185..7297d484d 100644 --- a/server/src/bin.rs +++ b/server/src/bin.rs @@ -11,6 +11,8 @@ mod handlers; mod helpers; #[cfg(feature = "https")] mod https; +#[cfg(feature = "https_init")] +mod https_init; mod jsonerrors; #[cfg(feature = "process-management")] mod process; diff --git a/server/src/errors.rs b/server/src/errors.rs index 535509362..41e3db4a6 100644 --- a/server/src/errors.rs +++ b/server/src/errors.rs @@ -166,7 +166,7 @@ impl From for AtomicServerError { } } -#[cfg(feature = "https")] +#[cfg(feature = "https_init")] impl From for AtomicServerError { fn from(error: acme_lib::Error) -> Self { AtomicServerError { diff --git a/server/src/https.rs b/server/src/https.rs index 3c4a22d0e..0dc614b8b 100644 --- a/server/src/https.rs +++ b/server/src/https.rs @@ -1,11 +1,4 @@ -//! Everything required for setting up HTTPS. - -use acme_lib::create_p384_key; -use acme_lib::persist::FilePersist; -use acme_lib::{Directory, DirectoryUrl, Error}; -use actix_web::{App, HttpServer}; - -use std::sync::mpsc; +//! Everything required for getting HTTPS config from storage. use std::{ fs::{self, File}, io::BufReader, @@ -13,184 +6,10 @@ use std::{ }; use crate::errors::AtomicServerResult; - -/// Starts an HTTP Actix server for HTTPS certificate initialization -pub async fn cert_init_server(config: &crate::config::Config) -> AtomicServerResult<()> { - let address = format!("{}:{}", config.opts.ip, config.opts.port); - tracing::warn!("Server temporarily running in HTTP mode at {}, running Let's Encrypt Certificate initialization...", address); - - let mut well_known_folder = config.static_path.clone(); - well_known_folder.push("well-known"); - fs::create_dir_all(&well_known_folder)?; - - let (tx, rx) = mpsc::channel(); - - let address_clone = address.clone(); - - std::thread::spawn(move || { - actix_web::rt::System::new().block_on(async move { - let init_server = HttpServer::new(move || { - App::new().service( - actix_files::Files::new("/.well-known", well_known_folder.clone()) - .show_files_listing(), - ) - }); - - let running_server = init_server.bind(&address_clone)?.run(); - - tx.send(running_server.handle()) - .expect("Error sending handle during HTTPS init"); - - running_server.await - }) - }); - - let handle = rx.recv().expect("Error receiving handle during HTTPS init"); - - let agent = ureq::builder() - .timeout(std::time::Duration::from_secs(2)) - .build(); - - let well_known_url = format!("http://{}/.well-known/", &config.opts.domain); - tracing::info!("Testing availability of {}", &well_known_url); - let resp = agent - .get(&well_known_url) - .call() - .expect("Unable to send request for Let's Encrypt initialization"); - if resp.status() != 200 { - return Err( - "Server for HTTP initialization not available, returning a non-200 status code".into(), - ); - } else { - tracing::info!("Server for HTTP initialization running correctly"); - } - - crate::https::request_cert(config).map_err(|e| format!("Certification init failed: {}", e))?; - tracing::warn!("HTTPS TLS Cert init sucesful! Stopping HTTP server, starting HTTPS..."); - handle.stop(true).await; - Ok(()) -} - -/// Writes keys to disk using LetsEncrypt -pub fn request_cert(config: &crate::config::Config) -> Result<(), Error> { - // Use DirectoryUrl::LetsEncrypStaging for dev/testing. - let url = if config.opts.development { - DirectoryUrl::LetsEncryptStaging - } else { - DirectoryUrl::LetsEncrypt - }; - - fs::create_dir_all(PathBuf::from(&config.https_path))?; - - // Save/load keys and certificates to current dir. - let persist = FilePersist::new(&config.https_path); - - // Create a directory entrypoint. - let dir = Directory::from_url(persist, url)?; - - // Reads the private account key from persistence, or - // creates a new one before accessing the API to establish - // that it's there. - let email = config - .opts - .email - .clone() - .expect("ATOMIC_EMAIL must be set for HTTPS init"); - tracing::info!("Requesting Let's Encrypt account with {}", email); - let acc = dir.account(&email)?; - - // Order a new TLS certificate for a domain. - let mut ord_new = acc.new_order(&config.opts.domain, &[])?; - - // If the ownership of the domain(s) have already been - // authorized in a previous order, you might be able to - // skip validation. The ACME API provider decides. - let ord_csr = loop { - // are we done? - if let Some(ord_csr) = ord_new.confirm_validations() { - break ord_csr; - } - - // Get the possible authorizations (for a single domain - // this will only be one element). - let auths = ord_new.authorizations()?; - - // For HTTP, the challenge is a text file that needs to - // be placed in your web server's root: - // - // /var/www/.well-known/acme-challenge/ - // - // The important thing is that it's accessible over the - // web for the domain(s) you are trying to get a - // certificate for: - // - // http://mydomain.io/.well-known/acme-challenge/ - let chall = auths[0].http_challenge(); - - // The token is the filename. - let token = chall.http_token(); - - let formatted_path = format!("well-known/acme-challenge/{}", token); - let mut challenge_path = config.static_path.clone(); - challenge_path.push(formatted_path); - - // The proof is the contents of the file - let proof = chall.http_proof(); - - tracing::info!("Writing ACME challange to {:?}", challenge_path); - - fs::create_dir_all( - PathBuf::from(&challenge_path) - .parent() - .expect("Could not find parent folder"), - ) - .expect("Unable to create dirs"); - - fs::write(challenge_path, proof).expect("Unable to write file"); - - // Here you must do "something" to place - // the file/contents in the correct place. - // update_my_web_server(&path, &proof); - - // After the file is accessible from the web, the calls - // this to tell the ACME API to start checking the - // existence of the proof. - // - // The order at ACME will change status to either - // confirm ownership of the domain, or fail due to the - // not finding the proof. To see the change, we poll - // the API with 5000 milliseconds wait between. - chall.validate(5000)?; - - // Update the state against the ACME API. - ord_new.refresh()?; - }; - - // Ownership is proven. Create a private key for - // the certificate. These are provided for convenience, you - // can provide your own keypair instead if you want. - let pkey_pri = create_p384_key(); - - // Submit the CSR. This causes the ACME provider to enter a - // state of "processing" that must be polled until the - // certificate is either issued or rejected. Again we poll - // for the status change. - let ord_cert = ord_csr.finalize_pkey(pkey_pri, 5000)?; - - // Now download the certificate. Also stores the cert in - // the persistence. - tracing::info!("Downloading certificate..."); - let cert = ord_cert.download_and_save_cert()?; - - fs::write(&config.cert_path, cert.certificate()).expect("Unable to write file"); - fs::write(&config.key_path, cert.private_key()).expect("Unable to write file"); - set_certs_created_at_file(config); - tracing::info!("HTTPS init Success!"); - Ok(()) -} - // RUSTLS -pub fn get_https_config(config: &crate::config::Config) -> Result { +pub fn get_https_config( + config: &crate::config::Config, +) -> AtomicServerResult { use rustls_pemfile::{certs, pkcs8_private_keys}; let https_config = rustls::ServerConfig::builder() .with_safe_defaults() @@ -215,7 +34,7 @@ pub fn get_https_config(config: &crate::config::Config) -> Result PathBuf { +pub fn certs_created_at_path(config: &crate::config::Config) -> PathBuf { // ~/.config/atomic/https let mut path = config .cert_path @@ -232,7 +51,7 @@ fn certs_created_at_path(config: &crate::config::Config) -> PathBuf { } /// Adds a file to the .https folder to indicate age of certificates -fn set_certs_created_at_file(config: &crate::config::Config) { +pub fn set_certs_created_at_file(config: &crate::config::Config) { let now_string = chrono::Utc::now(); let path = certs_created_at_path(config); fs::write(&path, now_string.to_string()) diff --git a/server/src/https_init.rs b/server/src/https_init.rs new file mode 100644 index 000000000..3cdcccd05 --- /dev/null +++ b/server/src/https_init.rs @@ -0,0 +1,191 @@ +//! Everything required for setting up HTTPS. +//! Calls the Let's Encrypt API to get a certificate. + +use acme_lib::create_p384_key; +use acme_lib::persist::FilePersist; +use acme_lib::{Directory, DirectoryUrl, Error}; +use actix_web::{App, HttpServer}; + +use std::sync::mpsc; +use std::{ + fs::{self}, + path::PathBuf, +}; + +use crate::errors::AtomicServerResult; +use crate::https::set_certs_created_at_file; + +/// Starts an HTTP Actix server for HTTPS certificate initialization +pub async fn cert_init_server(config: &crate::config::Config) -> AtomicServerResult<()> { + let address = format!("{}:{}", config.opts.ip, config.opts.port); + tracing::warn!("Server temporarily running in HTTP mode at {}, running Let's Encrypt Certificate initialization...", address); + + let mut well_known_folder = config.static_path.clone(); + well_known_folder.push("well-known"); + fs::create_dir_all(&well_known_folder)?; + + let (tx, rx) = mpsc::channel(); + + let address_clone = address.clone(); + + std::thread::spawn(move || { + actix_web::rt::System::new().block_on(async move { + let init_server = HttpServer::new(move || { + App::new().service( + actix_files::Files::new("/.well-known", well_known_folder.clone()) + .show_files_listing(), + ) + }); + + let running_server = init_server.bind(&address_clone)?.run(); + + tx.send(running_server.handle()) + .expect("Error sending handle during HTTPS init"); + + running_server.await + }) + }); + + let handle = rx.recv().expect("Error receiving handle during HTTPS init"); + + let agent = ureq::builder() + .timeout(std::time::Duration::from_secs(2)) + .build(); + + let well_known_url = format!("http://{}/.well-known/", &config.opts.domain); + tracing::info!("Testing availability of {}", &well_known_url); + let resp = agent + .get(&well_known_url) + .call() + .expect("Unable to send request for Let's Encrypt initialization"); + if resp.status() != 200 { + return Err( + "Server for HTTP initialization not available, returning a non-200 status code".into(), + ); + } else { + tracing::info!("Server for HTTP initialization running correctly"); + } + + request_cert(config).map_err(|e| format!("Certification init failed: {}", e))?; + tracing::warn!("HTTPS TLS Cert init sucesful! Stopping HTTP server, starting HTTPS..."); + handle.stop(true).await; + Ok(()) +} + +/// Writes keys to disk using LetsEncrypt +fn request_cert(config: &crate::config::Config) -> Result<(), Error> { + // Use DirectoryUrl::LetsEncrypStaging for dev/testing. + let url = if config.opts.development { + DirectoryUrl::LetsEncryptStaging + } else { + DirectoryUrl::LetsEncrypt + }; + + fs::create_dir_all(PathBuf::from(&config.https_path))?; + + // Save/load keys and certificates to current dir. + let persist = FilePersist::new(&config.https_path); + + // Create a directory entrypoint. + let dir = Directory::from_url(persist, url)?; + + // Reads the private account key from persistence, or + // creates a new one before accessing the API to establish + // that it's there. + let email = config + .opts + .email + .clone() + .expect("ATOMIC_EMAIL must be set for HTTPS init"); + tracing::info!("Requesting Let's Encrypt account with {}", email); + let acc = dir.account(&email)?; + + // Order a new TLS certificate for a domain. + let mut ord_new = acc.new_order(&config.opts.domain, &[])?; + + // If the ownership of the domain(s) have already been + // authorized in a previous order, you might be able to + // skip validation. The ACME API provider decides. + let ord_csr = loop { + // are we done? + if let Some(ord_csr) = ord_new.confirm_validations() { + break ord_csr; + } + + // Get the possible authorizations (for a single domain + // this will only be one element). + let auths = ord_new.authorizations()?; + + // For HTTP, the challenge is a text file that needs to + // be placed in your web server's root: + // + // /var/www/.well-known/acme-challenge/ + // + // The important thing is that it's accessible over the + // web for the domain(s) you are trying to get a + // certificate for: + // + // http://mydomain.io/.well-known/acme-challenge/ + let chall = auths[0].http_challenge(); + + // The token is the filename. + let token = chall.http_token(); + + let formatted_path = format!("well-known/acme-challenge/{}", token); + let mut challenge_path = config.static_path.clone(); + challenge_path.push(formatted_path); + + // The proof is the contents of the file + let proof = chall.http_proof(); + + tracing::info!("Writing ACME challange to {:?}", challenge_path); + + fs::create_dir_all( + PathBuf::from(&challenge_path) + .parent() + .expect("Could not find parent folder"), + ) + .expect("Unable to create dirs"); + + fs::write(challenge_path, proof).expect("Unable to write file"); + + // Here you must do "something" to place + // the file/contents in the correct place. + // update_my_web_server(&path, &proof); + + // After the file is accessible from the web, the calls + // this to tell the ACME API to start checking the + // existence of the proof. + // + // The order at ACME will change status to either + // confirm ownership of the domain, or fail due to the + // not finding the proof. To see the change, we poll + // the API with 5000 milliseconds wait between. + chall.validate(5000)?; + + // Update the state against the ACME API. + ord_new.refresh()?; + }; + + // Ownership is proven. Create a private key for + // the certificate. These are provided for convenience, you + // can provide your own keypair instead if you want. + let pkey_pri = create_p384_key(); + + // Submit the CSR. This causes the ACME provider to enter a + // state of "processing" that must be polled until the + // certificate is either issued or rejected. Again we poll + // for the status change. + let ord_cert = ord_csr.finalize_pkey(pkey_pri, 5000)?; + + // Now download the certificate. Also stores the cert in + // the persistence. + tracing::info!("Downloading certificate..."); + let cert = ord_cert.download_and_save_cert()?; + + fs::write(&config.cert_path, cert.certificate()).expect("Unable to write file"); + fs::write(&config.key_path, cert.private_key()).expect("Unable to write file"); + set_certs_created_at_file(config); + tracing::info!("HTTPS init Success!"); + Ok(()) +} diff --git a/server/src/lib.rs b/server/src/lib.rs index b3c5babfd..fb04dcfe3 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -13,6 +13,8 @@ mod handlers; mod helpers; #[cfg(feature = "https")] mod https; +#[cfg(feature = "https_init")] +mod https_init; mod jsonerrors; #[cfg(feature = "process-management")] mod process; diff --git a/server/src/serve.rs b/server/src/serve.rs index 756347a36..283a9e518 100644 --- a/server/src/serve.rs +++ b/server/src/serve.rs @@ -71,8 +71,11 @@ pub async fn serve(config: crate::config::Config) -> AtomicServerResult<()> { #[cfg(feature = "https")] { // If there is no certificate file, or the certs are too old, start HTTPS initialization - if crate::https::should_renew_certs_check(&config) { - crate::https::cert_init_server(&config).await?; + #[cfg(feature = "https_init")] + { + if crate::https::should_renew_certs_check(&config) { + crate::https_init::cert_init_server(&config).await?; + } } let https_config = crate::https::get_https_config(&config) .expect("HTTPS TLS Configuration with Let's Encrypt failed.");