Compare commits
2 Commits
209daacf1b
...
e29de0d29c
Author | SHA1 | Date | |
---|---|---|---|
|
e29de0d29c | ||
|
4f98a9a244 |
202
Cargo.lock
generated
202
Cargo.lock
generated
@ -47,6 +47,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.3.2"
|
||||
@ -209,6 +218,19 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.2.7"
|
||||
@ -263,6 +285,12 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.7"
|
||||
@ -358,6 +386,41 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0558d22a7b463ed0241e993f76f09f30b126687447751a8638587b864e4b3944"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab8bfa2e259f8ee1ce5e97824a3c55ec4404a0d772ca7fa96bf19f0752a046eb"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.3.3"
|
||||
@ -395,16 +458,21 @@ dependencies = [
|
||||
"mime_guess",
|
||||
"mockall",
|
||||
"openssl",
|
||||
"pem",
|
||||
"rust-embed",
|
||||
"rustls 0.21.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"serde_with",
|
||||
"sha2",
|
||||
"signal-hook",
|
||||
"signal-hook-tokio",
|
||||
"tempdir",
|
||||
"thiserror",
|
||||
"tls-listener",
|
||||
"tokio",
|
||||
"tokio-rustls 0.24.0",
|
||||
"tokio-util",
|
||||
"trust-dns-client",
|
||||
]
|
||||
@ -1432,11 +1500,40 @@ checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c"
|
||||
dependencies = [
|
||||
"http",
|
||||
"hyper",
|
||||
"rustls",
|
||||
"rustls 0.20.8",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.23.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.56"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.2.3"
|
||||
@ -1476,6 +1573,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown 0.12.3",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1775,6 +1873,16 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.15"
|
||||
@ -1870,6 +1978,16 @@ dependencies = [
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b13fe415cdf3c8e44518e18a7c95a13431d9bdf6d15367d82b23c377fdd441a"
|
||||
dependencies = [
|
||||
"base64 0.21.0",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.2.0"
|
||||
@ -2200,13 +2318,13 @@ dependencies = [
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls",
|
||||
"rustls 0.20.8",
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.23.4",
|
||||
"tower-service",
|
||||
"trust-dns-resolver",
|
||||
"url",
|
||||
@ -2302,6 +2420,18 @@ dependencies = [
|
||||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c911ba11bc8433e811ce56fde130ccf32f5127cab0e0194e9c68c5a5b671791e"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring",
|
||||
"rustls-webpki",
|
||||
"sct",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "1.0.2"
|
||||
@ -2311,6 +2441,16 @@ dependencies = [
|
||||
"base64 0.21.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.100.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.13"
|
||||
@ -2385,6 +2525,34 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f02d8aa6e3c385bf084924f660ce2a3a6bd333ba55b35e8590b321f35d88513"
|
||||
dependencies = [
|
||||
"base64 0.21.0",
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with_macros",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edc7d5d3932fb12ce722ee5e64dd38c504efba37567f0c402f6ca728c3b8b070"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1_smol"
|
||||
version = "1.0.0"
|
||||
@ -2591,6 +2759,20 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tls-listener"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81294c017957a1a69794f506723519255879e15a870507faf45dfed288b763dd"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"hyper",
|
||||
"pin-project-lite",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-rustls 0.24.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.28.1"
|
||||
@ -2627,11 +2809,21 @@ version = "0.23.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"rustls 0.20.8",
|
||||
"tokio",
|
||||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0d409377ff5b1e3ca6437aa86c1eb7d40c134bfec254e44c830defa92669db5"
|
||||
dependencies = [
|
||||
"rustls 0.21.1",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.8"
|
||||
|
@ -28,9 +28,14 @@ clap = { version = "4.2.7", features = ["derive", "env"] }
|
||||
handlebars = { version = "4.3.7", features = [ "rust-embed" ]}
|
||||
rust-embed = "6.6.1"
|
||||
mime_guess = "2.0.4"
|
||||
hyper = { version = "0.14.26", features = [ "server" ]}
|
||||
hyper = { version = "0.14.26", features = [ "server", "stream" ]}
|
||||
http = "0.2.9"
|
||||
serde_urlencoded = "0.7.1"
|
||||
tokio-util = "0.7.8"
|
||||
acme2 = "0.5.1"
|
||||
openssl = "0.10.52"
|
||||
rustls = "0.21.1"
|
||||
pem = "2.0.1"
|
||||
serde_with = "3.0.0"
|
||||
tls-listener = { version = "0.7.0", features = [ "rustls", "hyper-h1" ]}
|
||||
tokio-rustls = "0.24.0"
|
||||
|
12
TODO
12
TODO
@ -1,11 +1,3 @@
|
||||
- make acme store implement https://docs.rs/rustls/latest/rustls/server/trait.ResolvesServerCert.html
|
||||
|
||||
- pass that into https://docs.rs/rustls/latest/rustls/struct.ConfigBuilder.html#
|
||||
|
||||
- turn that into a TlsAcceptor (From is implemented here:
|
||||
https://docs.rs/tokio-rustls/latest/tokio_rustls/struct.TlsAcceptor.html#impl-From%3CArc%3CServerConfig%3E%3E-for-TlsAcceptor)
|
||||
|
||||
- use tls-listener crate to wrap hyper accepter: https://github.com/tmccombs/tls-listener/blob/main/examples/http.rs#L24
|
||||
- https://github.com/tmccombs/tls-listener/blob/main/examples/tls_config/mod.rs
|
||||
|
||||
- logging
|
||||
- expect statements (pretend it's "expected", not "expect")
|
||||
- map_unexpected annotation string
|
||||
|
@ -1,6 +1,8 @@
|
||||
pub mod manager;
|
||||
pub mod store;
|
||||
|
||||
pub type PrivateKey = openssl::pkey::PKey<openssl::pkey::Private>;
|
||||
mod private_key;
|
||||
pub use self::private_key::PrivateKey;
|
||||
|
||||
pub type Certificate = openssl::x509::X509;
|
||||
mod certificate;
|
||||
pub use self::certificate::Certificate;
|
||||
|
45
src/domain/acme/certificate.rs
Normal file
45
src/domain/acme/certificate.rs
Normal file
@ -0,0 +1,45 @@
|
||||
use std::convert::{From, TryFrom};
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde_with::{DeserializeFromStr, SerializeDisplay};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DeserializeFromStr, SerializeDisplay)]
|
||||
/// DER-encoded X.509, like rustls::Certificate.
|
||||
pub struct Certificate(Vec<u8>);
|
||||
|
||||
impl FromStr for Certificate {
|
||||
type Err = pem::PemError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Certificate(pem::parse(s)?.into_contents()))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Certificate {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
pem::Pem::new("CERTIFICATE", self.0.clone()).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&openssl::x509::X509Ref> for Certificate {
|
||||
type Error = openssl::error::ErrorStack;
|
||||
|
||||
fn try_from(c: &openssl::x509::X509Ref) -> Result<Self, Self::Error> {
|
||||
Ok(Certificate(c.to_der()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Certificate> for openssl::x509::X509 {
|
||||
type Error = openssl::error::ErrorStack;
|
||||
|
||||
fn try_from(c: &Certificate) -> Result<Self, Self::Error> {
|
||||
Ok(openssl::x509::X509::from_der(&c.0)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Certificate> for rustls::Certificate {
|
||||
fn from(c: Certificate) -> Self {
|
||||
rustls::Certificate(c.0)
|
||||
}
|
||||
}
|
@ -55,17 +55,17 @@ where
|
||||
|
||||
match store.get_account_key() {
|
||||
Ok(account_key) => {
|
||||
builder.private_key(account_key);
|
||||
builder.private_key((&account_key).try_into().map_unexpected()?);
|
||||
}
|
||||
Err(acme::store::GetAccountKeyError::NotFound) => (),
|
||||
Err(acme::store::GetAccountKeyError::Unexpected(err)) => return Err(err.to_unexpected()),
|
||||
}
|
||||
|
||||
let account = builder.build().await.map_unexpected()?;
|
||||
let account_key: acme::PrivateKey =
|
||||
account.private_key().as_ref().try_into().map_unexpected()?;
|
||||
|
||||
store
|
||||
.set_account_key(&account.private_key())
|
||||
.map_unexpected()?;
|
||||
store.set_account_key(&account_key).map_unexpected()?;
|
||||
|
||||
Ok(sync::Arc::new(ManagerImpl { store, account }))
|
||||
}
|
||||
@ -89,6 +89,10 @@ where
|
||||
.expect("parsed thirty days from now as Asn1Time");
|
||||
|
||||
let cert_with_soonest_not_after = cert
|
||||
.into_iter()
|
||||
.map(|cert| openssl::x509::X509::try_from(&cert))
|
||||
.try_collect::<Vec<openssl::x509::X509>>()
|
||||
.map_unexpected()?
|
||||
.into_iter()
|
||||
.reduce(|a, b| if a.not_after() < b.not_after() { a } else { b })
|
||||
.ok_or(error::Unexpected::from(
|
||||
@ -206,12 +210,14 @@ where
|
||||
}
|
||||
|
||||
// Generate an RSA private key for the certificate.
|
||||
let pkey = acme2::gen_rsa_private_key(4096).map_unexpected()?;
|
||||
let pkey = acme::PrivateKey::new();
|
||||
|
||||
let acme2_pkey = (&pkey).try_into().map_unexpected()?;
|
||||
|
||||
// Create a certificate signing request for the order, and request
|
||||
// the certificate.
|
||||
let order = order
|
||||
.finalize(acme2::Csr::Automatic(pkey.clone()))
|
||||
.finalize(acme2::Csr::Automatic(acme2_pkey))
|
||||
.await
|
||||
.map_unexpected()?;
|
||||
|
||||
@ -239,14 +245,17 @@ where
|
||||
|
||||
// Download the certificate, and panic if it doesn't exist.
|
||||
println!("fetching certificate for domain {}", domain.as_str());
|
||||
let cert =
|
||||
order
|
||||
.certificate()
|
||||
.await
|
||||
.map_unexpected()?
|
||||
.ok_or(error::Unexpected::from(
|
||||
"expected the order to return a certificate",
|
||||
))?;
|
||||
let cert = order
|
||||
.certificate()
|
||||
.await
|
||||
.map_unexpected()?
|
||||
.ok_or(error::Unexpected::from(
|
||||
"expected the order to return a certificate",
|
||||
))?
|
||||
.into_iter()
|
||||
.map(|cert| acme::Certificate::try_from(cert.as_ref()))
|
||||
.try_collect::<Vec<acme::Certificate>>()
|
||||
.map_unexpected()?;
|
||||
|
||||
if cert.len() <= 1 {
|
||||
return Err(error::Unexpected::from(
|
||||
@ -260,7 +269,7 @@ where
|
||||
|
||||
println!("certificate for {} successfully retrieved", domain.as_str());
|
||||
self.store
|
||||
.set_certificate(domain.as_str(), &pkey, cert)
|
||||
.set_certificate(domain.as_str(), pkey, cert)
|
||||
.map_unexpected()?;
|
||||
|
||||
Ok(())
|
||||
|
55
src/domain/acme/private_key.rs
Normal file
55
src/domain/acme/private_key.rs
Normal file
@ -0,0 +1,55 @@
|
||||
use std::convert::{From, TryFrom};
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde_with::{DeserializeFromStr, SerializeDisplay};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DeserializeFromStr, SerializeDisplay)]
|
||||
/// DER-encoded ASN.1 in either PKCS#8, PKCS#1, or Sec1 format, like rustls::PrivateKey.
|
||||
pub struct PrivateKey(Vec<u8>);
|
||||
|
||||
impl PrivateKey {
|
||||
pub fn new() -> PrivateKey {
|
||||
acme2::gen_rsa_private_key(4096)
|
||||
.expect("RSA private key generated")
|
||||
.as_ref()
|
||||
.try_into()
|
||||
.expect("RSA private key converted to internal representation")
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PrivateKey {
|
||||
type Err = pem::PemError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(PrivateKey(pem::parse(s)?.into_contents()))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PrivateKey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
pem::Pem::new("PRIVATE KEY", self.0.clone()).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&openssl::pkey::PKeyRef<openssl::pkey::Private>> for PrivateKey {
|
||||
type Error = openssl::error::ErrorStack;
|
||||
|
||||
fn try_from(k: &openssl::pkey::PKeyRef<openssl::pkey::Private>) -> Result<Self, Self::Error> {
|
||||
Ok(PrivateKey(k.private_key_to_der()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&PrivateKey> for openssl::pkey::PKey<openssl::pkey::Private> {
|
||||
type Error = openssl::error::ErrorStack;
|
||||
|
||||
fn try_from(k: &PrivateKey) -> Result<Self, Self::Error> {
|
||||
Ok(openssl::pkey::PKey::private_key_from_der(&k.0)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PrivateKey> for rustls::PrivateKey {
|
||||
fn from(k: PrivateKey) -> Self {
|
||||
rustls::PrivateKey(k.0)
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
use std::io::{Read, Write};
|
||||
use std::str::FromStr;
|
||||
use std::{fs, io, path, sync};
|
||||
|
||||
use crate::domain::acme::{Certificate, PrivateKey};
|
||||
@ -48,7 +49,7 @@ pub trait Store {
|
||||
fn set_certificate(
|
||||
&self,
|
||||
domain: &str,
|
||||
key: &PrivateKey,
|
||||
key: PrivateKey,
|
||||
cert: Vec<Certificate>,
|
||||
) -> Result<(), error::Unexpected>;
|
||||
|
||||
@ -63,27 +64,32 @@ pub trait BoxedStore: Store + Send + Sync + Clone + 'static {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct StoredPKeyCert {
|
||||
private_key_pem: String,
|
||||
cert_pems: Vec<String>,
|
||||
private_key: PrivateKey,
|
||||
cert: Vec<Certificate>,
|
||||
}
|
||||
|
||||
struct FSStore {
|
||||
dir_path: path::PathBuf,
|
||||
}
|
||||
|
||||
pub fn new(dir_path: &path::Path) -> Result<impl BoxedStore, error::Unexpected> {
|
||||
#[derive(Clone)]
|
||||
struct BoxedFSStore(sync::Arc<FSStore>);
|
||||
|
||||
pub fn new(
|
||||
dir_path: &path::Path,
|
||||
) -> Result<impl BoxedStore + rustls::server::ResolvesServerCert, error::Unexpected> {
|
||||
fs::create_dir_all(dir_path).map_unexpected()?;
|
||||
fs::create_dir_all(dir_path.join("http01_challenge_keys")).map_unexpected()?;
|
||||
fs::create_dir_all(dir_path.join("certificates")).map_unexpected()?;
|
||||
|
||||
Ok(sync::Arc::new(FSStore {
|
||||
Ok(BoxedFSStore(sync::Arc::new(FSStore {
|
||||
dir_path: dir_path.into(),
|
||||
}))
|
||||
})))
|
||||
}
|
||||
|
||||
impl FSStore {
|
||||
impl BoxedFSStore {
|
||||
fn account_key_path(&self) -> path::PathBuf {
|
||||
self.dir_path.join("account.key")
|
||||
self.0.dir_path.join("account.key")
|
||||
}
|
||||
|
||||
fn http01_challenge_key_path(&self, token: &str) -> path::PathBuf {
|
||||
@ -93,24 +99,23 @@ impl FSStore {
|
||||
.expect("token successfully hashed");
|
||||
let n = h.finalize().encode_hex::<String>();
|
||||
|
||||
self.dir_path.join("http01_challenge_keys").join(n)
|
||||
self.0.dir_path.join("http01_challenge_keys").join(n)
|
||||
}
|
||||
|
||||
fn certificate_path(&self, domain: &str) -> path::PathBuf {
|
||||
self.dir_path
|
||||
.join("certificates")
|
||||
.join(domain)
|
||||
.with_extension("json")
|
||||
let mut domain = domain.to_string();
|
||||
domain.push_str(".json");
|
||||
|
||||
self.0.dir_path.join("certificates").join(domain)
|
||||
}
|
||||
}
|
||||
|
||||
impl BoxedStore for sync::Arc<FSStore> {}
|
||||
impl BoxedStore for BoxedFSStore {}
|
||||
|
||||
impl Store for sync::Arc<FSStore> {
|
||||
impl Store for BoxedFSStore {
|
||||
fn set_account_key(&self, k: &PrivateKey) -> Result<(), error::Unexpected> {
|
||||
let mut file = fs::File::create(self.account_key_path()).map_unexpected()?;
|
||||
let pem = k.private_key_to_pem_pkcs8().map_unexpected()?;
|
||||
file.write_all(&pem).map_unexpected()?;
|
||||
file.write_all(k.to_string().as_bytes()).map_unexpected()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -120,11 +125,11 @@ impl Store for sync::Arc<FSStore> {
|
||||
_ => e.to_unexpected().into(),
|
||||
})?;
|
||||
|
||||
let mut pem = Vec::<u8>::new();
|
||||
file.read_to_end(&mut pem).map_unexpected()?;
|
||||
let mut key = String::new();
|
||||
file.read_to_string(&mut key).map_unexpected()?;
|
||||
|
||||
let k = PrivateKey::private_key_from_pem(&pem).map_unexpected()?;
|
||||
Ok(k)
|
||||
let key = PrivateKey::from_str(&key).map_unexpected()?;
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
fn set_http01_challenge_key(&self, token: &str, key: &str) -> Result<(), error::Unexpected> {
|
||||
@ -154,20 +159,12 @@ impl Store for sync::Arc<FSStore> {
|
||||
fn set_certificate(
|
||||
&self,
|
||||
domain: &str,
|
||||
key: &PrivateKey,
|
||||
key: PrivateKey,
|
||||
cert: Vec<Certificate>,
|
||||
) -> Result<(), error::Unexpected> {
|
||||
let to_store = StoredPKeyCert {
|
||||
private_key_pem: String::from_utf8(key.private_key_to_pem_pkcs8().map_unexpected()?)
|
||||
.map_unexpected()?,
|
||||
cert_pems: cert
|
||||
.into_iter()
|
||||
.map(|cert| {
|
||||
let cert_pem = cert.to_pem().map_unexpected()?;
|
||||
let cert_pem = String::from_utf8(cert_pem).map_unexpected()?;
|
||||
Ok::<String, error::Unexpected>(cert_pem)
|
||||
})
|
||||
.try_collect()?,
|
||||
private_key: key,
|
||||
cert: cert,
|
||||
};
|
||||
|
||||
let cert_file = fs::File::create(self.certificate_path(domain)).map_unexpected()?;
|
||||
@ -188,22 +185,47 @@ impl Store for sync::Arc<FSStore> {
|
||||
|
||||
let stored: StoredPKeyCert = serde_json::from_reader(file).map_unexpected()?;
|
||||
|
||||
let key =
|
||||
PrivateKey::private_key_from_pem(stored.private_key_pem.as_bytes()).map_unexpected()?;
|
||||
Ok((stored.private_key, stored.cert))
|
||||
}
|
||||
}
|
||||
|
||||
let cert: Vec<Certificate> = stored
|
||||
.cert_pems
|
||||
.into_iter()
|
||||
.map(|cert| openssl::x509::X509::from_pem(cert.as_bytes()).map_unexpected())
|
||||
.try_collect()?;
|
||||
impl rustls::server::ResolvesServerCert for BoxedFSStore {
|
||||
fn resolve(
|
||||
&self,
|
||||
client_hello: rustls::server::ClientHello<'_>,
|
||||
) -> Option<sync::Arc<rustls::sign::CertifiedKey>> {
|
||||
let domain = if let Some(domain) = client_hello.server_name() {
|
||||
domain
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Ok((key, cert))
|
||||
match self.get_certificate(domain) {
|
||||
Err(GetCertificateError::NotFound) => Ok(None),
|
||||
Err(GetCertificateError::Unexpected(err)) => Err(err),
|
||||
Ok((key, cert)) => {
|
||||
match rustls::sign::any_supported_type(&key.into()).map_unexpected() {
|
||||
Err(err) => Err(err),
|
||||
Ok(key) => Ok(Some(sync::Arc::new(rustls::sign::CertifiedKey {
|
||||
cert: cert.into_iter().map(|cert| cert.into()).collect(),
|
||||
key: key,
|
||||
ocsp: None,
|
||||
sct_list: None,
|
||||
}))),
|
||||
}
|
||||
}
|
||||
}
|
||||
.unwrap_or_else(|err| {
|
||||
println!("Unexpected error getting cert for domain {domain}: {err}");
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::domain::acme;
|
||||
use tempdir::TempDir;
|
||||
|
||||
#[test]
|
||||
@ -216,17 +238,15 @@ mod tests {
|
||||
Err::<PrivateKey, GetAccountKeyError>(GetAccountKeyError::NotFound)
|
||||
));
|
||||
|
||||
let k = acme2::gen_rsa_private_key(4096).expect("private key generated");
|
||||
let k = acme::PrivateKey::new();
|
||||
|
||||
store.set_account_key(&k).expect("account private key set");
|
||||
|
||||
assert_eq!(
|
||||
k.private_key_to_pem_pkcs8().unwrap(),
|
||||
k,
|
||||
store
|
||||
.get_account_key()
|
||||
.expect("account private key retrieved")
|
||||
.private_key_to_pem_pkcs8()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
use std::net;
|
||||
use std::str::FromStr;
|
||||
use std::sync;
|
||||
|
||||
use crate::error::MapUnexpected;
|
||||
use crate::{domain, error};
|
||||
@ -37,8 +36,7 @@ pub struct DNSChecker {
|
||||
client: tokio::sync::Mutex<AsyncClient>,
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
tokio_runtime: sync::Arc<tokio::runtime::Runtime>,
|
||||
pub async fn new(
|
||||
target_a: net::Ipv4Addr,
|
||||
resolver_addr: &str,
|
||||
) -> Result<DNSChecker, NewDNSCheckerError> {
|
||||
@ -48,11 +46,9 @@ pub fn new(
|
||||
|
||||
let stream = udp::UdpClientStream::<tokio::net::UdpSocket>::new(resolver_addr);
|
||||
|
||||
let (client, bg) = tokio_runtime
|
||||
.block_on(async { AsyncClient::connect(stream).await })
|
||||
.map_unexpected()?;
|
||||
let (client, bg) = AsyncClient::connect(stream).await.map_unexpected()?;
|
||||
|
||||
tokio_runtime.spawn(bg);
|
||||
tokio::spawn(bg);
|
||||
|
||||
Ok(DNSChecker {
|
||||
target_a,
|
||||
|
203
src/main.rs
203
src/main.rs
@ -56,23 +56,17 @@ struct Cli {
|
||||
domain_acme_contact_email: Option<String>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let config = Cli::parse();
|
||||
|
||||
let mut wait_group = FuturesUnordered::new();
|
||||
|
||||
let tokio_runtime = std::sync::Arc::new(
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let canceller = tokio_runtime.block_on(async { tokio_util::sync::CancellationToken::new() });
|
||||
let canceller = tokio_util::sync::CancellationToken::new();
|
||||
|
||||
{
|
||||
let canceller = canceller.clone();
|
||||
tokio_runtime.spawn(async move {
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut signals =
|
||||
Signals::new(signal_hook::consts::TERM_SIGNALS).expect("initialized signals");
|
||||
|
||||
@ -92,16 +86,16 @@ fn main() {
|
||||
.expect("git origin store initialized");
|
||||
|
||||
let domain_checker = domiply::domain::checker::new(
|
||||
tokio_runtime.clone(),
|
||||
config.domain_checker_target_a,
|
||||
&config.domain_checker_resolver_addr,
|
||||
)
|
||||
.await
|
||||
.expect("domain checker initialized");
|
||||
|
||||
let domain_config_store = domiply::domain::config::new(&config.domain_config_store_dir_path)
|
||||
.expect("domain config store initialized");
|
||||
|
||||
let domain_acme_manager = config.https_listen_addr.and_then(|_addr| {
|
||||
let (domain_acme_store, domain_acme_manager) = if config.https_listen_addr.is_some() {
|
||||
let domain_acme_store =
|
||||
domiply::domain::acme::store::new(&config.domain_acme_store_dir_path)
|
||||
.expect("domain acme store initialized");
|
||||
@ -110,14 +104,17 @@ fn main() {
|
||||
// settings.
|
||||
let domain_acme_contact_email = config.domain_acme_contact_email.unwrap();
|
||||
|
||||
let domain_acme_manager = tokio_runtime.block_on(async {
|
||||
domiply::domain::acme::manager::new(domain_acme_store, &domain_acme_contact_email)
|
||||
.await
|
||||
.expect("domain acme manager initialized")
|
||||
});
|
||||
let domain_acme_manager = domiply::domain::acme::manager::new(
|
||||
domain_acme_store.clone(),
|
||||
&domain_acme_contact_email,
|
||||
)
|
||||
.await
|
||||
.expect("domain acme manager initialized");
|
||||
|
||||
Some(domain_acme_manager)
|
||||
});
|
||||
(Some(domain_acme_store), Some(domain_acme_manager))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let manager = domiply::domain::manager::new(
|
||||
origin_store,
|
||||
@ -130,7 +127,7 @@ fn main() {
|
||||
let manager = manager.clone();
|
||||
let canceller = canceller.clone();
|
||||
|
||||
tokio_runtime.spawn(async move {
|
||||
tokio::spawn(async move {
|
||||
let mut interval = time::interval(time::Duration::from_secs(20 * 60));
|
||||
|
||||
loop {
|
||||
@ -166,8 +163,12 @@ fn main() {
|
||||
|
||||
let service = sync::Arc::new(service);
|
||||
|
||||
let make_service =
|
||||
hyper::service::make_service_fn(move |_conn: &hyper::server::conn::AddrStream| {
|
||||
wait_group.push({
|
||||
let http_domain = config.http_domain.clone();
|
||||
let canceller = canceller.clone();
|
||||
let service = service.clone();
|
||||
|
||||
let make_service = hyper::service::make_service_fn(move |_| {
|
||||
let service = service.clone();
|
||||
|
||||
// Create a `Service` for responding to the request.
|
||||
@ -179,11 +180,7 @@ fn main() {
|
||||
async move { Ok::<_, Infallible>(service) }
|
||||
});
|
||||
|
||||
wait_group.push({
|
||||
let http_domain = config.http_domain.clone();
|
||||
let canceller = canceller.clone();
|
||||
|
||||
tokio_runtime.spawn(async move {
|
||||
tokio::spawn(async move {
|
||||
let addr = config.http_listen_addr;
|
||||
|
||||
println!(
|
||||
@ -203,61 +200,119 @@ fn main() {
|
||||
})
|
||||
});
|
||||
|
||||
// if there's an acme manager then it means that https is enabled, and we should ensure that
|
||||
// the http domain for domiply itself has a valid certificate.
|
||||
if let Some(domain_acme_manager) = domain_acme_manager {
|
||||
let manager = manager.clone();
|
||||
let canceller = canceller.clone();
|
||||
let http_domain = config.http_domain.clone();
|
||||
// if there's an acme manager then it means that https is enabled
|
||||
if let (Some(domain_acme_store), Some(domain_acme_manager)) =
|
||||
(domain_acme_store, domain_acme_manager)
|
||||
{
|
||||
// Periodically refresh all domain certs, including the http_domain passed in the Cli opts
|
||||
wait_group.push({
|
||||
let manager = manager.clone();
|
||||
let http_domain = config.http_domain.clone();
|
||||
let canceller = canceller.clone();
|
||||
|
||||
// Periodically refresh all domain certs
|
||||
wait_group.push(tokio_runtime.spawn(async move {
|
||||
let mut interval = time::interval(time::Duration::from_secs(60 * 60));
|
||||
tokio::spawn(async move {
|
||||
let mut interval = time::interval(time::Duration::from_secs(60 * 60));
|
||||
|
||||
loop {
|
||||
select! {
|
||||
_ = interval.tick() => (),
|
||||
_ = canceller.cancelled() => return,
|
||||
loop {
|
||||
select! {
|
||||
_ = interval.tick() => (),
|
||||
_ = canceller.cancelled() => return,
|
||||
}
|
||||
|
||||
_ = domain_acme_manager
|
||||
.sync_domain(http_domain.clone())
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
println!(
|
||||
"Error while getting cert for {}: {err}",
|
||||
http_domain.as_str()
|
||||
)
|
||||
});
|
||||
|
||||
let domains_iter = manager.all_domains();
|
||||
|
||||
if let Err(err) = domains_iter {
|
||||
println!("Got error calling all_domains: {err}");
|
||||
continue;
|
||||
}
|
||||
|
||||
for domain in domains_iter.unwrap().into_iter() {
|
||||
match domain {
|
||||
Ok(domain) => {
|
||||
let _ = domain_acme_manager
|
||||
.sync_domain(domain.clone())
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
println!(
|
||||
"Error while getting cert for {}: {err}",
|
||||
domain.as_str(),
|
||||
)
|
||||
});
|
||||
}
|
||||
Err(err) => println!("Error iterating through domains: {err}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
_ = domain_acme_manager
|
||||
.sync_domain(http_domain.clone())
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
println!(
|
||||
"Error while getting cert for {}: {err}",
|
||||
http_domain.as_str()
|
||||
)
|
||||
});
|
||||
// HTTPS server
|
||||
wait_group.push({
|
||||
let http_domain = config.http_domain.clone();
|
||||
let canceller = canceller.clone();
|
||||
let service = service.clone();
|
||||
|
||||
let domains_iter = manager.all_domains();
|
||||
let make_service = hyper::service::make_service_fn(move |_| {
|
||||
let service = service.clone();
|
||||
|
||||
if let Err(err) = domains_iter {
|
||||
println!("Got error calling all_domains: {err}");
|
||||
continue;
|
||||
}
|
||||
// Create a `Service` for responding to the request.
|
||||
let service = hyper::service::service_fn(move |req| {
|
||||
domiply::service::handle_request(service.clone(), req)
|
||||
});
|
||||
|
||||
for domain in domains_iter.unwrap().into_iter() {
|
||||
match domain {
|
||||
Ok(domain) => {
|
||||
let _ = domain_acme_manager
|
||||
.sync_domain(domain.clone())
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
println!(
|
||||
"Error while getting cert for {}: {err}",
|
||||
domain.as_str(),
|
||||
)
|
||||
});
|
||||
}
|
||||
Err(err) => println!("Error iterating through domains: {err}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
}));
|
||||
// Return the service to hyper.
|
||||
async move { Ok::<_, Infallible>(service) }
|
||||
});
|
||||
|
||||
tokio::spawn(async move {
|
||||
let canceller = canceller.clone();
|
||||
let server_config: tokio_rustls::TlsAcceptor = sync::Arc::new(
|
||||
rustls::server::ServerConfig::builder()
|
||||
.with_safe_default_cipher_suites()
|
||||
.with_safe_default_kx_groups()
|
||||
.with_safe_default_protocol_versions()
|
||||
.unwrap()
|
||||
.with_no_client_auth()
|
||||
.with_cert_resolver(sync::Arc::from(domain_acme_store)),
|
||||
)
|
||||
.into();
|
||||
|
||||
let addr = config.https_listen_addr.unwrap();
|
||||
let addr_incoming = hyper::server::conn::AddrIncoming::bind(&addr)
|
||||
.expect("https listen socket created");
|
||||
|
||||
let incoming = tls_listener::TlsListener::new(server_config, addr_incoming);
|
||||
|
||||
println!(
|
||||
"Listening on https://{}:{}",
|
||||
http_domain.as_str(),
|
||||
addr.port()
|
||||
);
|
||||
|
||||
let server = hyper::Server::builder(incoming).serve(make_service);
|
||||
|
||||
let graceful = server.with_graceful_shutdown(async {
|
||||
canceller.cancelled().await;
|
||||
});
|
||||
|
||||
if let Err(e) = graceful.await {
|
||||
panic!("server error: {}", e);
|
||||
};
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
tokio_runtime.block_on(async { while let Some(_) = wait_group.next().await {} });
|
||||
while let Some(_) = wait_group.next().await {}
|
||||
|
||||
println!("Graceful shutdown complete");
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user