-
-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
1c1fdc1
commit 9b9622e
Showing
6 changed files
with
235 additions
and
96 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
use crate::constants::ACME_ACCOUNT_SUBDIR; | ||
use async_trait::async_trait; | ||
use aws_lc_rs as crypto; | ||
use base64::prelude::*; | ||
use blocking::unblock; | ||
use crypto::digest::{Context, SHA256}; | ||
use rustls_acme::{AccountCache, CertCache}; | ||
use std::{ | ||
io::ErrorKind, | ||
path::{Path, PathBuf}, | ||
}; | ||
|
||
enum FileType { | ||
Account, | ||
Cert, | ||
} | ||
|
||
#[derive(Debug)] | ||
pub struct DirCache { | ||
account_dir: PathBuf, | ||
cert_dir: PathBuf, | ||
} | ||
|
||
impl DirCache { | ||
pub fn new<P>(dir: P, server_name: impl AsRef<Path>) -> Self | ||
where | ||
P: AsRef<Path>, | ||
{ | ||
Self { | ||
account_dir: dir.as_ref().join(ACME_ACCOUNT_SUBDIR), | ||
cert_dir: dir.as_ref().join(server_name), | ||
} | ||
} | ||
async fn read_if_exist(&self, file: impl AsRef<Path>, file_type: FileType) -> Result<Option<Vec<u8>>, std::io::Error> { | ||
let subdir = match file_type { | ||
FileType::Account => &self.account_dir, | ||
FileType::Cert => &self.cert_dir, | ||
}; | ||
let file_path = subdir.join(file); | ||
match unblock(move || std::fs::read(file_path)).await { | ||
Ok(content) => Ok(Some(content)), | ||
Err(err) => match err.kind() { | ||
ErrorKind::NotFound => Ok(None), | ||
_ => Err(err), | ||
}, | ||
} | ||
} | ||
async fn write(&self, file: impl AsRef<Path>, contents: impl AsRef<[u8]>, file_type: FileType) -> Result<(), std::io::Error> { | ||
let subdir = match file_type { | ||
FileType::Account => &self.account_dir, | ||
FileType::Cert => &self.cert_dir, | ||
} | ||
.clone(); | ||
let subdir_clone = subdir.clone(); | ||
unblock(move || std::fs::create_dir_all(subdir_clone)).await?; | ||
let file_path = subdir.join(file); | ||
let contents = contents.as_ref().to_owned(); | ||
unblock(move || std::fs::write(file_path, contents)).await | ||
} | ||
pub fn cached_account_file_name(contact: &[String], directory_url: impl AsRef<str>) -> String { | ||
let mut ctx = Context::new(&SHA256); | ||
for el in contact { | ||
ctx.update(el.as_ref()); | ||
ctx.update(&[0]) | ||
} | ||
ctx.update(directory_url.as_ref().as_bytes()); | ||
let hash = BASE64_URL_SAFE_NO_PAD.encode(ctx.finish()); | ||
format!("cached_account_{}", hash) | ||
} | ||
pub fn cached_cert_file_name(domains: &[String], directory_url: impl AsRef<str>) -> String { | ||
let mut ctx = Context::new(&SHA256); | ||
for domain in domains { | ||
ctx.update(domain.as_ref()); | ||
ctx.update(&[0]) | ||
} | ||
ctx.update(directory_url.as_ref().as_bytes()); | ||
let hash = BASE64_URL_SAFE_NO_PAD.encode(ctx.finish()); | ||
format!("cached_cert_{}", hash) | ||
} | ||
} | ||
|
||
#[async_trait] | ||
impl CertCache for DirCache { | ||
type EC = std::io::Error; | ||
async fn load_cert(&self, domains: &[String], directory_url: &str) -> Result<Option<Vec<u8>>, Self::EC> { | ||
let file_name = Self::cached_cert_file_name(domains, directory_url); | ||
self.read_if_exist(file_name, FileType::Cert).await | ||
} | ||
async fn store_cert(&self, domains: &[String], directory_url: &str, cert: &[u8]) -> Result<(), Self::EC> { | ||
let file_name = Self::cached_cert_file_name(domains, directory_url); | ||
self.write(file_name, cert, FileType::Cert).await | ||
} | ||
} | ||
|
||
#[async_trait] | ||
impl AccountCache for DirCache { | ||
type EA = std::io::Error; | ||
async fn load_account(&self, contact: &[String], directory_url: &str) -> Result<Option<Vec<u8>>, Self::EA> { | ||
let file_name = Self::cached_account_file_name(contact, directory_url); | ||
self.read_if_exist(file_name, FileType::Account).await | ||
} | ||
|
||
async fn store_account(&self, contact: &[String], directory_url: &str, account: &[u8]) -> Result<(), Self::EA> { | ||
let file_name = Self::cached_account_file_name(contact, directory_url); | ||
self.write(file_name, account, FileType::Account).await | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,83 +1,96 @@ | ||
use rustc_hash::FxHashMap as HashMap; | ||
use std::path::PathBuf; | ||
use url::Url; | ||
|
||
use crate::dir_cache::DirCache; | ||
use crate::{ | ||
constants::{ACME_ACCOUNT_SUBDIR, ACME_CERTIFICATE_FILE_NAME, ACME_DIR_URL, ACME_PRIVATE_KEY_FILE_NAME, ACME_REGISTRY_PATH}, | ||
constants::{ACME_DIR_URL, ACME_REGISTRY_PATH}, | ||
error::RpxyAcmeError, | ||
log::*, | ||
}; | ||
use rustc_hash::FxHashMap as HashMap; | ||
use rustls_acme::AcmeConfig; | ||
use std::{fmt::Debug, path::PathBuf, sync::Arc}; | ||
use url::Url; | ||
|
||
#[derive(Debug)] | ||
/// ACME settings | ||
pub struct AcmeTargets { | ||
/// ACME account email | ||
pub email: String, | ||
pub struct AcmeContexts<EC, EA = EC> | ||
where | ||
EC: Debug + 'static, | ||
EA: Debug + 'static, | ||
{ | ||
/// ACME directory url | ||
pub acme_dir_url: Url, | ||
/// ACME registry path that stores account key and certificate | ||
pub acme_registry_path: PathBuf, | ||
/// ACME accounts directory, subdirectory of ACME_REGISTRY_PATH | ||
pub acme_accounts_dir: PathBuf, | ||
/// ACME target info map | ||
pub acme_targets: HashMap<String, AcmeTargetInfo>, | ||
} | ||
|
||
#[derive(Debug)] | ||
/// ACME settings for each server name | ||
pub struct AcmeTargetInfo { | ||
/// Server name | ||
pub server_name: String, | ||
/// private key path | ||
pub private_key_path: PathBuf, | ||
/// certificate path | ||
pub certificate_path: PathBuf, | ||
acme_dir_url: Url, | ||
/// ACME registry directory | ||
acme_registry_dir: PathBuf, | ||
/// ACME contacts | ||
contacts: Vec<String>, | ||
/// ACME config | ||
inner: HashMap<String, Box<AcmeConfig<EC, EA>>>, | ||
} | ||
|
||
impl AcmeTargets { | ||
/// Create a new instance | ||
pub fn try_new(email: &str, acme_dir_url: Option<&str>, acme_registry_path: Option<&str>) -> Result<Self, RpxyAcmeError> { | ||
let acme_dir_url = Url::parse(acme_dir_url.unwrap_or(ACME_DIR_URL))?; | ||
let acme_registry_path = acme_registry_path.map_or_else(|| PathBuf::from(ACME_REGISTRY_PATH), PathBuf::from); | ||
if acme_registry_path.exists() && !acme_registry_path.is_dir() { | ||
return Err(RpxyAcmeError::InvalidAcmeRegistryPath); | ||
} | ||
let acme_account_dir = acme_registry_path.join(ACME_ACCOUNT_SUBDIR); | ||
if acme_account_dir.exists() && !acme_account_dir.is_dir() { | ||
impl AcmeContexts<std::io::Error> { | ||
/// Create a new instance. Note that for each domain, a new AcmeConfig is created. | ||
/// This means that for each domain, a distinct operation will be dispatched and separated certificates will be generated. | ||
pub fn try_new( | ||
acme_dir_url: Option<&str>, | ||
acme_registry_dir: Option<&str>, | ||
contacts: &[String], | ||
domains: &[String], | ||
) -> Result<Self, RpxyAcmeError> { | ||
let acme_registry_dir = acme_registry_dir | ||
.map(|v| v.to_ascii_lowercase()) | ||
.map_or_else(|| PathBuf::from(ACME_REGISTRY_PATH), PathBuf::from); | ||
if acme_registry_dir.exists() && !acme_registry_dir.is_dir() { | ||
return Err(RpxyAcmeError::InvalidAcmeRegistryPath); | ||
} | ||
std::fs::create_dir_all(&acme_account_dir)?; | ||
let acme_dir_url = acme_dir_url | ||
.map(|v| v.to_ascii_lowercase()) | ||
.as_deref() | ||
.map_or_else(|| Url::parse(ACME_DIR_URL), Url::parse)?; | ||
let contacts = contacts.iter().map(|email| format!("mailto:{email}")).collect::<Vec<_>>(); | ||
let rustls_client_config = rustls::ClientConfig::builder() | ||
.dangerous() // The `Verifier` we're using is actually safe | ||
.with_custom_certificate_verifier(std::sync::Arc::new(rustls_platform_verifier::Verifier::new())) | ||
.with_no_client_auth(); | ||
let rustls_client_config = Arc::new(rustls_client_config); | ||
|
||
let inner = domains | ||
.iter() | ||
.map(|domain| { | ||
let dir_cache = DirCache::new(&acme_registry_dir, domain); | ||
let config = AcmeConfig::new([domain]) | ||
.contact(&contacts) | ||
.cache(dir_cache) | ||
.directory(acme_dir_url.as_str()) | ||
.client_tls_config(rustls_client_config.clone()); | ||
let config = Box::new(config); | ||
(domain.to_ascii_lowercase(), config) | ||
}) | ||
.collect::<HashMap<_, _>>(); | ||
|
||
Ok(Self { | ||
email: email.to_owned(), | ||
acme_dir_url, | ||
acme_registry_path, | ||
acme_accounts_dir: acme_account_dir, | ||
acme_targets: HashMap::default(), | ||
acme_registry_dir, | ||
contacts, | ||
inner, | ||
}) | ||
} | ||
} | ||
|
||
/// Add a new target | ||
/// Write dummy cert and key files if not exists | ||
pub fn add_target(&mut self, server_name: &str) -> Result<(), RpxyAcmeError> { | ||
info!("Adding ACME target: {}", server_name); | ||
let parent_dir = self.acme_registry_path.join(server_name); | ||
let private_key_path = parent_dir.join(ACME_PRIVATE_KEY_FILE_NAME); | ||
let certificate_path = parent_dir.join(ACME_CERTIFICATE_FILE_NAME); | ||
|
||
if !parent_dir.exists() { | ||
warn!("Creating ACME target directory: {}", parent_dir.display()); | ||
std::fs::create_dir_all(parent_dir)?; | ||
} | ||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
self.acme_targets.insert( | ||
server_name.to_owned(), | ||
AcmeTargetInfo { | ||
server_name: server_name.to_owned(), | ||
private_key_path, | ||
certificate_path, | ||
}, | ||
); | ||
Ok(()) | ||
#[test] | ||
fn test_try_new() { | ||
let acme_dir_url = "https://acme.example.com/directory"; | ||
let acme_registry_dir = "/tmp/acme"; | ||
let contacts = vec!["[email protected]".to_string()]; | ||
let acme_contexts: AcmeContexts<std::io::Error> = AcmeContexts::try_new( | ||
Some(acme_dir_url), | ||
Some(acme_registry_dir), | ||
&contacts, | ||
&["example.com".to_string(), "example.org".to_string()], | ||
) | ||
.unwrap(); | ||
println!("{:#?}", acme_contexts); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters