Compare commits
2 Commits
a7e74ac5dd
...
6d8799ce8c
Author | SHA1 | Date | |
---|---|---|---|
|
6d8799ce8c | ||
|
0f42327a57 |
2
.env.dev
2
.env.dev
@ -3,3 +3,5 @@ export DOMIPLY_PASSPHRASE=foobar
|
|||||||
export DOMIPLY_ORIGIN_STORE_GIT_DIR_PATH=/tmp/domiply_dev_env/origin/git
|
export DOMIPLY_ORIGIN_STORE_GIT_DIR_PATH=/tmp/domiply_dev_env/origin/git
|
||||||
export DOMIPLY_DOMAIN_CHECKER_TARGET_A=127.0.0.1
|
export DOMIPLY_DOMAIN_CHECKER_TARGET_A=127.0.0.1
|
||||||
export DOMIPLY_DOMAIN_CONFIG_STORE_DIR_PATH=/tmp/domiply_dev_env/domain/config
|
export DOMIPLY_DOMAIN_CONFIG_STORE_DIR_PATH=/tmp/domiply_dev_env/domain/config
|
||||||
|
export DOMIPLY_DOMAIN_ACME_STORE_DIR_PATH=/tmp/domiply_dev_env/domain/acme
|
||||||
|
export DOMIPLY_DOMAIN_ACME_CONTACT_EMAIL=domiply@example.com
|
||||||
|
127
Cargo.lock
generated
127
Cargo.lock
generated
@ -2,6 +2,24 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "acme2"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "453e534d4f46dcdddd7aa8619e9a664e153f34383d14710db0b0d76c2964db89"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.13.1",
|
||||||
|
"hyper",
|
||||||
|
"openssl",
|
||||||
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"tracing-futures",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "adler"
|
name = "adler"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@ -107,6 +125,12 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.21.0"
|
version = "0.21.0"
|
||||||
@ -360,6 +384,7 @@ dependencies = [
|
|||||||
name = "domiply"
|
name = "domiply"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"acme2",
|
||||||
"clap",
|
"clap",
|
||||||
"futures",
|
"futures",
|
||||||
"gix",
|
"gix",
|
||||||
@ -369,6 +394,7 @@ dependencies = [
|
|||||||
"hyper",
|
"hyper",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"mockall",
|
"mockall",
|
||||||
|
"openssl",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -495,6 +521,21 @@ version = "1.0.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foreign-types"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||||
|
dependencies = [
|
||||||
|
"foreign-types-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foreign-types-shared"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@ -1147,7 +1188,7 @@ version = "0.31.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f01c2bf7b989c679695ef635fc7d9e80072e08101be4b53193c8e8b649900102"
|
checksum = "f01c2bf7b989c679695ef635fc7d9e80072e08101be4b53193c8e8b649900102"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.21.0",
|
||||||
"bstr",
|
"bstr",
|
||||||
"gix-command",
|
"gix-command",
|
||||||
"gix-credentials",
|
"gix-credentials",
|
||||||
@ -1768,6 +1809,44 @@ version = "1.17.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
|
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl"
|
||||||
|
version = "0.10.52"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"cfg-if",
|
||||||
|
"foreign-types",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"openssl-macros",
|
||||||
|
"openssl-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-macros"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.15",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-sys"
|
||||||
|
version = "0.9.87"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@ -1841,6 +1920,26 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead"
|
||||||
|
dependencies = [
|
||||||
|
"pin-project-internal",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-internal"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.15",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.9"
|
version = "0.2.9"
|
||||||
@ -1853,6 +1952,12 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkg-config"
|
||||||
|
version = "0.3.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@ -2078,7 +2183,7 @@ version = "0.11.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13293b639a097af28fc8a90f22add145a9c954e49d77da06263d58cf44d5fb91"
|
checksum = "13293b639a097af28fc8a90f22add145a9c954e49d77da06263d58cf44d5fb91"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.21.0",
|
||||||
"bytes",
|
"bytes",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@ -2203,7 +2308,7 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b"
|
checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.21.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2579,6 +2684,16 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-futures"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
|
||||||
|
dependencies = [
|
||||||
|
"pin-project",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "trust-dns-client"
|
name = "trust-dns-client"
|
||||||
version = "0.22.0"
|
version = "0.22.0"
|
||||||
@ -2730,6 +2845,12 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
|
@ -32,3 +32,5 @@ hyper = { version = "0.14.26", features = [ "server" ]}
|
|||||||
http = "0.2.9"
|
http = "0.2.9"
|
||||||
serde_urlencoded = "0.7.1"
|
serde_urlencoded = "0.7.1"
|
||||||
tokio-util = "0.7.8"
|
tokio-util = "0.7.8"
|
||||||
|
acme2 = "0.5.1"
|
||||||
|
openssl = "0.10.52"
|
||||||
|
16
flake.nix
16
flake.nix
@ -23,24 +23,32 @@
|
|||||||
rustc = toolchain;
|
rustc = toolchain;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
opensslEnv = {
|
||||||
|
OPENSSL_STATIC = "1";
|
||||||
|
OPENSSL_LIB_DIR = "${pkgs.pkgsStatic.openssl.out}/lib";
|
||||||
|
OPENSSL_INCLUDE_DIR = "${pkgs.pkgsStatic.openssl.dev}/include";
|
||||||
|
};
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
defaultPackage = naersk-lib.buildPackage {
|
defaultPackage = naersk-lib.buildPackage ({
|
||||||
src = ./.;
|
src = ./.;
|
||||||
doCheck = false;
|
doCheck = false;
|
||||||
nativeBuildInputs = [ pkgs.pkgsStatic.stdenv.cc ];
|
nativeBuildInputs = [ pkgs.pkgsStatic.stdenv.cc ];
|
||||||
CARGO_BUILD_TARGET = "x86_64-unknown-linux-musl";
|
CARGO_BUILD_TARGET = "x86_64-unknown-linux-musl";
|
||||||
CARGO_BUILD_RUSTFLAGS = "-C target-feature=+crt-static";
|
CARGO_BUILD_RUSTFLAGS = "-C target-feature=+crt-static";
|
||||||
};
|
} // opensslEnv);
|
||||||
|
|
||||||
devShell = pkgs.mkShell {
|
devShell = pkgs.mkShell ({
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
|
pkgs.stdenv.cc
|
||||||
|
pkgs.openssl
|
||||||
toolchain
|
toolchain
|
||||||
];
|
];
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
source $(pwd)/.env.dev
|
source $(pwd)/.env.dev
|
||||||
export CARGO_HOME=$(pwd)/.cargo
|
export CARGO_HOME=$(pwd)/.cargo
|
||||||
'';
|
'';
|
||||||
};
|
} // opensslEnv);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
pub mod acme;
|
||||||
pub mod checker;
|
pub mod checker;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod manager;
|
pub mod manager;
|
||||||
@ -15,7 +16,7 @@ pub struct Name {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Name {
|
impl Name {
|
||||||
fn as_str(&self) -> &str {
|
pub fn as_str(&self) -> &str {
|
||||||
self.utf8_str.as_str()
|
self.utf8_str.as_str()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
5
src/domain/acme.rs
Normal file
5
src/domain/acme.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
pub mod manager;
|
||||||
|
pub mod store;
|
||||||
|
|
||||||
|
pub type AccountKey = openssl::pkey::PKey<openssl::pkey::Private>;
|
||||||
|
pub type Certificate = openssl::x509::X509;
|
225
src/domain/acme/manager.rs
Normal file
225
src/domain/acme/manager.rs
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
use std::{future, pin, sync, time};
|
||||||
|
|
||||||
|
use crate::domain::{self, acme};
|
||||||
|
use crate::error;
|
||||||
|
use crate::error::{MapUnexpected, ToUnexpected};
|
||||||
|
|
||||||
|
const LETS_ENCRYPT_URL: &'static str = "https://acme-v02.api.letsencrypt.org/directory";
|
||||||
|
|
||||||
|
pub type GetHttp01ChallengeKeyError = acme::store::GetHttp01ChallengeKeyError;
|
||||||
|
|
||||||
|
#[mockall::automock(
|
||||||
|
type SyncDomainFuture=future::Ready<Result<(), error::Unexpected>>;
|
||||||
|
)]
|
||||||
|
pub trait Manager {
|
||||||
|
type SyncDomainFuture<'mgr>: future::Future<Output = Result<(), error::Unexpected>>
|
||||||
|
+ Send
|
||||||
|
+ Unpin
|
||||||
|
+ 'mgr
|
||||||
|
where
|
||||||
|
Self: 'mgr;
|
||||||
|
|
||||||
|
fn sync_domain(&self, domain: domain::Name) -> Self::SyncDomainFuture<'_>;
|
||||||
|
|
||||||
|
fn get_http01_challenge_key(&self, token: &str) -> Result<String, GetHttp01ChallengeKeyError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait BoxedManager: Manager + Send + Sync + Clone + 'static {}
|
||||||
|
|
||||||
|
struct ManagerImpl<Store>
|
||||||
|
where
|
||||||
|
Store: acme::store::BoxedStore,
|
||||||
|
{
|
||||||
|
store: Store,
|
||||||
|
account: sync::Arc<acme2::Account>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn new<Store>(
|
||||||
|
store: Store,
|
||||||
|
contact_email: &str,
|
||||||
|
) -> Result<impl BoxedManager, error::Unexpected>
|
||||||
|
where
|
||||||
|
Store: acme::store::BoxedStore,
|
||||||
|
{
|
||||||
|
let dir = acme2::DirectoryBuilder::new(LETS_ENCRYPT_URL.to_string())
|
||||||
|
.build()
|
||||||
|
.await
|
||||||
|
.map_unexpected()?;
|
||||||
|
|
||||||
|
let mut builder = acme2::AccountBuilder::new(dir);
|
||||||
|
builder.contact(vec![contact_email.to_string()]);
|
||||||
|
builder.terms_of_service_agreed(true);
|
||||||
|
|
||||||
|
match store.get_account_key() {
|
||||||
|
Ok(account_key) => {
|
||||||
|
builder.private_key(account_key);
|
||||||
|
}
|
||||||
|
Err(acme::store::GetAccountKeyError::NotFound) => (),
|
||||||
|
Err(acme::store::GetAccountKeyError::Unexpected(err)) => return Err(err.to_unexpected()),
|
||||||
|
}
|
||||||
|
|
||||||
|
let account = builder.build().await.map_unexpected()?;
|
||||||
|
|
||||||
|
store
|
||||||
|
.set_account_key(&account.private_key())
|
||||||
|
.map_unexpected()?;
|
||||||
|
|
||||||
|
Ok(sync::Arc::new(ManagerImpl { store, account }))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Store> BoxedManager for sync::Arc<ManagerImpl<Store>> where Store: acme::store::BoxedStore {}
|
||||||
|
|
||||||
|
impl<Store> Manager for sync::Arc<ManagerImpl<Store>>
|
||||||
|
where
|
||||||
|
Store: acme::store::BoxedStore,
|
||||||
|
{
|
||||||
|
type SyncDomainFuture<'mgr> = pin::Pin<Box<dyn future::Future<Output = Result<(), error::Unexpected>> + Send + 'mgr>>
|
||||||
|
where Self: 'mgr;
|
||||||
|
|
||||||
|
fn sync_domain(&self, domain: domain::Name) -> Self::SyncDomainFuture<'_> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let mut builder = acme2::OrderBuilder::new(self.account.clone());
|
||||||
|
builder.add_dns_identifier(domain.as_str().to_string());
|
||||||
|
let order = builder.build().await.map_unexpected()?;
|
||||||
|
|
||||||
|
let authorizations = order.authorizations().await.map_unexpected()?;
|
||||||
|
|
||||||
|
for auth in authorizations {
|
||||||
|
let challenge = auth
|
||||||
|
.get_challenge("http-01")
|
||||||
|
.ok_or(error::Unexpected::from("expected http-01 challenge"))?;
|
||||||
|
|
||||||
|
let challenge_token = challenge
|
||||||
|
.token
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(error::Unexpected::from("expected challenge to have token"))?;
|
||||||
|
|
||||||
|
let challenge_key = challenge
|
||||||
|
.key_authorization()
|
||||||
|
.map_unexpected()?
|
||||||
|
.ok_or(error::Unexpected::from("expected challenge to have key"))?;
|
||||||
|
|
||||||
|
self.store
|
||||||
|
.set_http01_challenge_key(challenge_token, &challenge_key)
|
||||||
|
.map_unexpected()?;
|
||||||
|
|
||||||
|
// At this point the manager is prepared to serve the challenge key via the
|
||||||
|
// `get_http01_challenge_key` method. It is expected that there is some http
|
||||||
|
// server, with this domain pointing at it, which is prepared to serve that
|
||||||
|
// challenge token/key under the `/.well-known/` path. The `validate()` call below
|
||||||
|
// will instigate the acme server to make this check, and block until it succeeds.
|
||||||
|
|
||||||
|
let challenge = challenge.validate().await.map_unexpected()?;
|
||||||
|
|
||||||
|
// Poll the challenge every 5 seconds until it is in either the
|
||||||
|
// `valid` or `invalid` state.
|
||||||
|
let challenge = challenge
|
||||||
|
.wait_done(time::Duration::from_secs(5), 3)
|
||||||
|
.await
|
||||||
|
.map_unexpected()?;
|
||||||
|
|
||||||
|
if challenge.status != acme2::ChallengeStatus::Valid {
|
||||||
|
return Err(error::Unexpected::from(
|
||||||
|
format!(
|
||||||
|
"expected challenge status to be valid, instead it was {:?}",
|
||||||
|
challenge.status
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.store
|
||||||
|
.del_http01_challenge_key(&challenge_token)
|
||||||
|
.map_unexpected()?;
|
||||||
|
|
||||||
|
// Poll the authorization every 5 seconds until it is in either the
|
||||||
|
// `valid` or `invalid` state.
|
||||||
|
let authorization = auth
|
||||||
|
.wait_done(time::Duration::from_secs(5), 3)
|
||||||
|
.await
|
||||||
|
.map_unexpected()?;
|
||||||
|
|
||||||
|
if authorization.status != acme2::AuthorizationStatus::Valid {
|
||||||
|
return Err(error::Unexpected::from(
|
||||||
|
format!(
|
||||||
|
"expected authorization status to be valid, instead it was {:?}",
|
||||||
|
authorization.status,
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll the order every 5 seconds until it is in either the `ready` or `invalid` state.
|
||||||
|
// Ready means that it is now ready for finalization (certificate creation).
|
||||||
|
let order = order
|
||||||
|
.wait_ready(time::Duration::from_secs(5), 3)
|
||||||
|
.await
|
||||||
|
.map_unexpected()?;
|
||||||
|
|
||||||
|
if order.status != acme2::OrderStatus::Ready {
|
||||||
|
return Err(error::Unexpected::from(
|
||||||
|
format!(
|
||||||
|
"expected order status to be ready, instead it was {:?}",
|
||||||
|
order.status,
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate an RSA private key for the certificate.
|
||||||
|
let pkey = acme2::gen_rsa_private_key(4096).map_unexpected()?;
|
||||||
|
|
||||||
|
// Create a certificate signing request for the order, and request
|
||||||
|
// the certificate.
|
||||||
|
let order = order
|
||||||
|
.finalize(acme2::Csr::Automatic(pkey))
|
||||||
|
.await
|
||||||
|
.map_unexpected()?;
|
||||||
|
|
||||||
|
// Poll the order every 5 seconds until it is in either the
|
||||||
|
// `valid` or `invalid` state. Valid means that the certificate
|
||||||
|
// has been provisioned, and is now ready for download.
|
||||||
|
let order = order
|
||||||
|
.wait_done(time::Duration::from_secs(5), 3)
|
||||||
|
.await
|
||||||
|
.map_unexpected()?;
|
||||||
|
|
||||||
|
if order.status != acme2::OrderStatus::Valid {
|
||||||
|
return Err(error::Unexpected::from(
|
||||||
|
format!(
|
||||||
|
"expected order status to be valid, instead it was {:?}",
|
||||||
|
order.status,
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download the certificate, and panic if it doesn't exist.
|
||||||
|
let cert =
|
||||||
|
order
|
||||||
|
.certificate()
|
||||||
|
.await
|
||||||
|
.map_unexpected()?
|
||||||
|
.ok_or(error::Unexpected::from(
|
||||||
|
"expected the order to return a certificate",
|
||||||
|
))?;
|
||||||
|
|
||||||
|
if cert.len() <= 1 {
|
||||||
|
return Err(error::Unexpected::from(
|
||||||
|
format!(
|
||||||
|
"expected more than one certificate to be returned, instead got {}",
|
||||||
|
cert.len(),
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_http01_challenge_key(&self, token: &str) -> Result<String, GetHttp01ChallengeKeyError> {
|
||||||
|
self.store.get_http01_challenge_key(token)
|
||||||
|
}
|
||||||
|
}
|
240
src/domain/acme/store.rs
Normal file
240
src/domain/acme/store.rs
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::{fs, io, path, sync};
|
||||||
|
|
||||||
|
use crate::domain::acme::{AccountKey, Certificate};
|
||||||
|
use crate::error;
|
||||||
|
use crate::error::{MapUnexpected, ToUnexpected};
|
||||||
|
|
||||||
|
use hex::ToHex;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum GetAccountKeyError {
|
||||||
|
#[error("not found")]
|
||||||
|
NotFound,
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Unexpected(#[from] error::Unexpected),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum GetHttp01ChallengeKeyError {
|
||||||
|
#[error("not found")]
|
||||||
|
NotFound,
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Unexpected(#[from] error::Unexpected),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum GetCertificateError {
|
||||||
|
#[error("not found")]
|
||||||
|
NotFound,
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Unexpected(#[from] error::Unexpected),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[mockall::automock]
|
||||||
|
pub trait Store {
|
||||||
|
fn set_account_key(&self, k: &AccountKey) -> Result<(), error::Unexpected>;
|
||||||
|
fn get_account_key(&self) -> Result<AccountKey, GetAccountKeyError>;
|
||||||
|
|
||||||
|
fn set_http01_challenge_key(&self, token: &str, key: &str) -> Result<(), error::Unexpected>;
|
||||||
|
fn get_http01_challenge_key(&self, token: &str) -> Result<String, GetHttp01ChallengeKeyError>;
|
||||||
|
fn del_http01_challenge_key(&self, token: &str) -> Result<(), error::Unexpected>;
|
||||||
|
|
||||||
|
fn set_certificate(
|
||||||
|
&self,
|
||||||
|
domain: &str,
|
||||||
|
cert: Vec<Certificate>,
|
||||||
|
) -> Result<(), error::Unexpected>;
|
||||||
|
|
||||||
|
/// Returned vec is guaranteed to have len > 0
|
||||||
|
fn get_certificate(&self, domain: &str) -> Result<Vec<Certificate>, GetCertificateError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait BoxedStore: Store + Send + Sync + Clone + 'static {}
|
||||||
|
|
||||||
|
struct FSStore {
|
||||||
|
dir_path: path::PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(dir_path: &path::Path) -> Result<impl BoxedStore, 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 {
|
||||||
|
dir_path: dir_path.into(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FSStore {
|
||||||
|
fn account_key_path(&self) -> path::PathBuf {
|
||||||
|
self.dir_path.join("account.key")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn http01_challenge_key_path(&self, token: &str) -> path::PathBuf {
|
||||||
|
// hash it for safety
|
||||||
|
let mut h = Sha256::new();
|
||||||
|
h.write_all(token.as_bytes())
|
||||||
|
.expect("token successfully hashed");
|
||||||
|
let n = h.finalize().encode_hex::<String>();
|
||||||
|
|
||||||
|
self.dir_path.join("http01_challenge_keys").join(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn certificate_path(&self, domain: &str) -> path::PathBuf {
|
||||||
|
self.dir_path.join("certificates").join(domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoxedStore for sync::Arc<FSStore> {}
|
||||||
|
|
||||||
|
impl Store for sync::Arc<FSStore> {
|
||||||
|
fn set_account_key(&self, k: &AccountKey) -> 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()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_account_key(&self) -> Result<AccountKey, GetAccountKeyError> {
|
||||||
|
let mut file = fs::File::open(self.account_key_path()).map_err(|e| match e.kind() {
|
||||||
|
io::ErrorKind::NotFound => GetAccountKeyError::NotFound,
|
||||||
|
_ => e.to_unexpected().into(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut pem = Vec::<u8>::new();
|
||||||
|
file.read_to_end(&mut pem).map_unexpected()?;
|
||||||
|
|
||||||
|
let k = AccountKey::private_key_from_pem(&pem).map_unexpected()?;
|
||||||
|
Ok(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_http01_challenge_key(&self, token: &str, key: &str) -> Result<(), error::Unexpected> {
|
||||||
|
let mut file = fs::File::create(self.http01_challenge_key_path(token)).map_unexpected()?;
|
||||||
|
file.write_all(key.as_bytes()).map_unexpected()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_http01_challenge_key(&self, token: &str) -> Result<String, GetHttp01ChallengeKeyError> {
|
||||||
|
let mut file =
|
||||||
|
fs::File::open(self.http01_challenge_key_path(token)).map_err(|e| match e.kind() {
|
||||||
|
io::ErrorKind::NotFound => GetHttp01ChallengeKeyError::NotFound,
|
||||||
|
_ => e.to_unexpected().into(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut key = String::new();
|
||||||
|
file.read_to_string(&mut key).map_unexpected()?;
|
||||||
|
|
||||||
|
Ok(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn del_http01_challenge_key(&self, token: &str) -> Result<(), error::Unexpected> {
|
||||||
|
fs::remove_file(self.http01_challenge_key_path(token)).map_unexpected()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_certificate(
|
||||||
|
&self,
|
||||||
|
domain: &str,
|
||||||
|
cert: Vec<Certificate>,
|
||||||
|
) -> Result<(), error::Unexpected> {
|
||||||
|
let cert: Vec<String> = 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()?;
|
||||||
|
|
||||||
|
let cert_file = fs::File::create(self.certificate_path(domain)).map_unexpected()?;
|
||||||
|
|
||||||
|
serde_json::to_writer(cert_file, &cert).map_unexpected()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_certificate(&self, domain: &str) -> Result<Vec<Certificate>, GetCertificateError> {
|
||||||
|
let file = fs::File::open(self.certificate_path(domain)).map_err(|e| match e.kind() {
|
||||||
|
io::ErrorKind::NotFound => GetCertificateError::NotFound,
|
||||||
|
_ => e.to_unexpected().into(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let cert: Vec<String> = serde_json::from_reader(file).map_unexpected()?;
|
||||||
|
|
||||||
|
let cert: Vec<Certificate> = cert
|
||||||
|
.into_iter()
|
||||||
|
.map(|cert| openssl::x509::X509::from_pem(cert.as_bytes()).map_unexpected())
|
||||||
|
.try_collect()?;
|
||||||
|
|
||||||
|
Ok(cert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempdir::TempDir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn account_key() {
|
||||||
|
let tmp_dir = TempDir::new("domain_acme_store_account_key").unwrap();
|
||||||
|
let store = new(tmp_dir.path()).expect("store created");
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
store.get_account_key(),
|
||||||
|
Err::<AccountKey, GetAccountKeyError>(GetAccountKeyError::NotFound)
|
||||||
|
));
|
||||||
|
|
||||||
|
let k = acme2::gen_rsa_private_key(4096).expect("private key generated");
|
||||||
|
|
||||||
|
store.set_account_key(&k).expect("account private key set");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
k.private_key_to_pem_pkcs8().unwrap(),
|
||||||
|
store
|
||||||
|
.get_account_key()
|
||||||
|
.expect("account private key retrieved")
|
||||||
|
.private_key_to_pem_pkcs8()
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn http01_challenge_key() {
|
||||||
|
let tmp_dir = TempDir::new("domain_acme_store_http01_challenge_key").unwrap();
|
||||||
|
let store = new(tmp_dir.path()).expect("store created");
|
||||||
|
|
||||||
|
let token = "foo".to_string();
|
||||||
|
let key = "bar".to_string();
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
store.get_http01_challenge_key(&token),
|
||||||
|
Err::<String, GetHttp01ChallengeKeyError>(GetHttp01ChallengeKeyError::NotFound)
|
||||||
|
));
|
||||||
|
|
||||||
|
store
|
||||||
|
.set_http01_challenge_key(&token, &key)
|
||||||
|
.expect("http01 challenge set");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
key,
|
||||||
|
store
|
||||||
|
.get_http01_challenge_key(&token)
|
||||||
|
.expect("retrieved http01 challenge"),
|
||||||
|
);
|
||||||
|
|
||||||
|
store
|
||||||
|
.del_http01_challenge_key(&token)
|
||||||
|
.expect("deleted http01 challenge");
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
store.get_http01_challenge_key(&token),
|
||||||
|
Err::<String, GetHttp01ChallengeKeyError>(GetHttp01ChallengeKeyError::NotFound)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,8 @@
|
|||||||
use crate::domain::{self, checker, config};
|
use crate::domain::{self, acme, checker, config};
|
||||||
use crate::error::{MapUnexpected, ToUnexpected};
|
use crate::error::{MapUnexpected, ToUnexpected};
|
||||||
use crate::{error, origin};
|
use crate::{error, origin};
|
||||||
|
|
||||||
use std::future;
|
use std::{future, pin, sync};
|
||||||
use std::{pin, sync};
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum GetConfigError {
|
pub enum GetConfigError {
|
||||||
@ -114,6 +113,8 @@ impl From<config::SetError> for SyncWithConfigError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type GetAcmeHttp01ChallengeKeyError = acme::manager::GetHttp01ChallengeKeyError;
|
||||||
|
|
||||||
#[mockall::automock(
|
#[mockall::automock(
|
||||||
type Origin=origin::MockOrigin;
|
type Origin=origin::MockOrigin;
|
||||||
type SyncWithConfigFuture=future::Ready<Result<(), SyncWithConfigError>>;
|
type SyncWithConfigFuture=future::Ready<Result<(), SyncWithConfigError>>;
|
||||||
@ -147,49 +148,61 @@ pub trait Manager {
|
|||||||
) -> Self::SyncWithConfigFuture<'_>;
|
) -> Self::SyncWithConfigFuture<'_>;
|
||||||
|
|
||||||
fn sync_all_origins(&self) -> Result<Self::SyncAllOriginsErrorsIter<'_>, error::Unexpected>;
|
fn sync_all_origins(&self) -> Result<Self::SyncAllOriginsErrorsIter<'_>, error::Unexpected>;
|
||||||
|
|
||||||
|
fn get_acme_http01_challenge_key(
|
||||||
|
&self,
|
||||||
|
token: &str,
|
||||||
|
) -> Result<String, GetAcmeHttp01ChallengeKeyError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait BoxedManager: Manager + Send + Sync + Clone {}
|
pub trait BoxedManager: Manager + Send + Sync + Clone {}
|
||||||
|
|
||||||
struct ManagerImpl<OriginStore, DomainConfigStore>
|
struct ManagerImpl<OriginStore, DomainConfigStore, AcmeManager>
|
||||||
where
|
where
|
||||||
OriginStore: origin::store::BoxedStore,
|
OriginStore: origin::store::BoxedStore,
|
||||||
DomainConfigStore: config::BoxedStore,
|
DomainConfigStore: config::BoxedStore,
|
||||||
|
AcmeManager: acme::manager::BoxedManager,
|
||||||
{
|
{
|
||||||
origin_store: OriginStore,
|
origin_store: OriginStore,
|
||||||
domain_config_store: DomainConfigStore,
|
domain_config_store: DomainConfigStore,
|
||||||
domain_checker: checker::DNSChecker,
|
domain_checker: checker::DNSChecker,
|
||||||
|
acme_manager: Option<AcmeManager>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new<OriginStore, DomainConfigStore>(
|
pub fn new<OriginStore, DomainConfigStore, AcmeManager>(
|
||||||
origin_store: OriginStore,
|
origin_store: OriginStore,
|
||||||
domain_config_store: DomainConfigStore,
|
domain_config_store: DomainConfigStore,
|
||||||
domain_checker: checker::DNSChecker,
|
domain_checker: checker::DNSChecker,
|
||||||
|
acme_manager: Option<AcmeManager>,
|
||||||
) -> impl BoxedManager
|
) -> impl BoxedManager
|
||||||
where
|
where
|
||||||
OriginStore: origin::store::BoxedStore,
|
OriginStore: origin::store::BoxedStore,
|
||||||
DomainConfigStore: config::BoxedStore,
|
DomainConfigStore: config::BoxedStore,
|
||||||
|
AcmeManager: acme::manager::BoxedManager,
|
||||||
{
|
{
|
||||||
sync::Arc::new(ManagerImpl {
|
sync::Arc::new(ManagerImpl {
|
||||||
origin_store,
|
origin_store,
|
||||||
domain_config_store,
|
domain_config_store,
|
||||||
domain_checker,
|
domain_checker,
|
||||||
|
acme_manager: acme_manager,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<OriginStore, DomainConfigStore> BoxedManager
|
impl<OriginStore, DomainConfigStore, AcmeManager> BoxedManager
|
||||||
for sync::Arc<ManagerImpl<OriginStore, DomainConfigStore>>
|
for sync::Arc<ManagerImpl<OriginStore, DomainConfigStore, AcmeManager>>
|
||||||
where
|
where
|
||||||
OriginStore: origin::store::BoxedStore,
|
OriginStore: origin::store::BoxedStore,
|
||||||
DomainConfigStore: config::BoxedStore,
|
DomainConfigStore: config::BoxedStore,
|
||||||
|
AcmeManager: acme::manager::BoxedManager,
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<OriginStore, DomainConfigStore> Manager
|
impl<OriginStore, DomainConfigStore, AcmeManager> Manager
|
||||||
for sync::Arc<ManagerImpl<OriginStore, DomainConfigStore>>
|
for sync::Arc<ManagerImpl<OriginStore, DomainConfigStore, AcmeManager>>
|
||||||
where
|
where
|
||||||
OriginStore: origin::store::BoxedStore,
|
OriginStore: origin::store::BoxedStore,
|
||||||
DomainConfigStore: config::BoxedStore,
|
DomainConfigStore: config::BoxedStore,
|
||||||
|
AcmeManager: acme::manager::BoxedManager,
|
||||||
{
|
{
|
||||||
type Origin<'mgr> = OriginStore::Origin<'mgr>
|
type Origin<'mgr> = OriginStore::Origin<'mgr>
|
||||||
where Self: 'mgr;
|
where Self: 'mgr;
|
||||||
@ -231,6 +244,10 @@ where
|
|||||||
|
|
||||||
self.domain_config_store.set(&domain, &config)?;
|
self.domain_config_store.set(&domain, &config)?;
|
||||||
|
|
||||||
|
if let Some(ref acme_manager) = self.acme_manager {
|
||||||
|
acme_manager.sync_domain(domain.clone()).await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -255,4 +272,15 @@ where
|
|||||||
None
|
None
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_acme_http01_challenge_key(
|
||||||
|
&self,
|
||||||
|
token: &str,
|
||||||
|
) -> Result<String, GetAcmeHttp01ChallengeKeyError> {
|
||||||
|
if let Some(ref acme_manager) = self.acme_manager {
|
||||||
|
return acme_manager.get_http01_challenge_key(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(GetAcmeHttp01ChallengeKeyError::NotFound)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,12 @@ use std::fmt;
|
|||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
/// Unexpected is a String which implements the Error trait. It is intended to be used in
|
||||||
|
/// situations where the caller is being given an error they can't really handle, except to pass it
|
||||||
|
/// along or log it.
|
||||||
|
///
|
||||||
|
/// The error is intended to also implement Send + Sync + Clone, such that it is easy to use in
|
||||||
|
/// async situations.
|
||||||
pub struct Unexpected(String);
|
pub struct Unexpected(String);
|
||||||
|
|
||||||
impl fmt::Display for Unexpected {
|
impl fmt::Display for Unexpected {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#![feature(iterator_try_collect)]
|
||||||
|
|
||||||
pub mod domain;
|
pub mod domain;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod origin;
|
pub mod origin;
|
||||||
|
64
src/main.rs
64
src/main.rs
@ -1,3 +1,5 @@
|
|||||||
|
#![feature(result_option_inspect)]
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use futures::stream::StreamExt;
|
use futures::stream::StreamExt;
|
||||||
use signal_hook_tokio::Signals;
|
use signal_hook_tokio::Signals;
|
||||||
@ -10,17 +12,25 @@ use std::path;
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync;
|
use std::sync;
|
||||||
|
|
||||||
|
use domiply::domain::acme::manager::Manager as AcmeManager;
|
||||||
use domiply::domain::manager::Manager;
|
use domiply::domain::manager::Manager;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version)]
|
#[command(version)]
|
||||||
#[command(about = "A domiply to another dimension")]
|
#[command(about = "A domiply to another dimension")]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
|
#[arg(long, required = true, env = "DOMIPLY_HTTP_DOMAIN")]
|
||||||
|
http_domain: String,
|
||||||
|
|
||||||
#[arg(long, default_value_t = SocketAddr::from_str("[::]:3030").unwrap(), env = "DOMIPLY_HTTP_LISTEN_ADDR")]
|
#[arg(long, default_value_t = SocketAddr::from_str("[::]:3030").unwrap(), env = "DOMIPLY_HTTP_LISTEN_ADDR")]
|
||||||
http_listen_addr: SocketAddr,
|
http_listen_addr: SocketAddr,
|
||||||
|
|
||||||
#[arg(long, required = true, env = "DOMIPLY_HTTP_DOMAIN")]
|
#[arg(
|
||||||
http_domain: String,
|
long,
|
||||||
|
help = "E.g. '[::]:443', if given then SSL certs will automatically be retrieved for all domains using LetsEncrypt",
|
||||||
|
env = "DOMIPLY_HTTPS_LISTEN_ADDR"
|
||||||
|
)]
|
||||||
|
https_listen_addr: Option<SocketAddr>,
|
||||||
|
|
||||||
#[arg(long, required = true, env = "DOMIPLY_PASSPHRASE")]
|
#[arg(long, required = true, env = "DOMIPLY_PASSPHRASE")]
|
||||||
passphrase: String,
|
passphrase: String,
|
||||||
@ -36,6 +46,12 @@ struct Cli {
|
|||||||
|
|
||||||
#[arg(long, required = true, env = "DOMIPLY_DOMAIN_CONFIG_STORE_DIR_PATH")]
|
#[arg(long, required = true, env = "DOMIPLY_DOMAIN_CONFIG_STORE_DIR_PATH")]
|
||||||
domain_config_store_dir_path: path::PathBuf,
|
domain_config_store_dir_path: path::PathBuf,
|
||||||
|
|
||||||
|
#[arg(long, required = true, env = "DOMIPLY_DOMAIN_ACME_STORE_DIR_PATH")]
|
||||||
|
domain_acme_store_dir_path: path::PathBuf,
|
||||||
|
|
||||||
|
#[arg(long, required = true, env = "DOMIPLY_DOMAIN_ACME_CONTACT_EMAIL")]
|
||||||
|
domain_acme_contact_email: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@ -81,7 +97,29 @@ fn main() {
|
|||||||
let domain_config_store = domiply::domain::config::new(&config.domain_config_store_dir_path)
|
let domain_config_store = domiply::domain::config::new(&config.domain_config_store_dir_path)
|
||||||
.expect("domain config store initialized");
|
.expect("domain config store initialized");
|
||||||
|
|
||||||
let manager = domiply::domain::manager::new(origin_store, domain_config_store, domain_checker);
|
let domain_acme_manager = config.https_listen_addr.and_then(|_addr| {
|
||||||
|
let domain_acme_store =
|
||||||
|
domiply::domain::acme::store::new(&config.domain_acme_store_dir_path)
|
||||||
|
.expect("domain acme store initialized");
|
||||||
|
|
||||||
|
let domain_acme_manager = tokio_runtime.block_on(async {
|
||||||
|
domiply::domain::acme::manager::new(
|
||||||
|
domain_acme_store,
|
||||||
|
&config.domain_acme_contact_email,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("domain acme manager initialized")
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(domain_acme_manager)
|
||||||
|
});
|
||||||
|
|
||||||
|
let manager = domiply::domain::manager::new(
|
||||||
|
origin_store,
|
||||||
|
domain_config_store,
|
||||||
|
domain_checker,
|
||||||
|
domain_acme_manager.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
let origin_syncer_handler = {
|
let origin_syncer_handler = {
|
||||||
let manager = manager.clone();
|
let manager = manager.clone();
|
||||||
@ -137,10 +175,12 @@ fn main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let server_handler = {
|
let server_handler = {
|
||||||
|
let http_domain = config.http_domain.clone();
|
||||||
|
|
||||||
tokio_runtime.spawn(async move {
|
tokio_runtime.spawn(async move {
|
||||||
let addr = config.http_listen_addr;
|
let addr = config.http_listen_addr;
|
||||||
|
|
||||||
println!("Listening on http://{}:{}", config.http_domain, addr.port());
|
println!("Listening on http://{}:{}", http_domain, addr.port());
|
||||||
let server = hyper::Server::bind(&addr).serve(make_service);
|
let server = hyper::Server::bind(&addr).serve(make_service);
|
||||||
|
|
||||||
let graceful = server.with_graceful_shutdown(async {
|
let graceful = server.with_graceful_shutdown(async {
|
||||||
@ -153,6 +193,22 @@ 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 domain = domiply::domain::Name::from_str(&config.http_domain)
|
||||||
|
.expect("--http-domain parses as a domain");
|
||||||
|
|
||||||
|
tokio_runtime.spawn(async move {
|
||||||
|
_ = domain_acme_manager
|
||||||
|
.sync_domain(domain.clone())
|
||||||
|
.await
|
||||||
|
.inspect_err(|err| {
|
||||||
|
println!("Error while getting cert for {}: {err}", domain.as_str())
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
tokio_runtime
|
tokio_runtime
|
||||||
.block_on(async { futures::try_join!(origin_syncer_handler, server_handler) })
|
.block_on(async { futures::try_join!(origin_syncer_handler, server_handler) })
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -315,7 +315,7 @@ mod tests {
|
|||||||
|
|
||||||
let descr = origin::Descr::Git {
|
let descr = origin::Descr::Git {
|
||||||
url: curr_dir.clone(),
|
url: curr_dir.clone(),
|
||||||
branch_name: String::from("master"),
|
branch_name: String::from("main"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let other_descr = origin::Descr::Git {
|
let other_descr = origin::Descr::Git {
|
||||||
|
@ -341,6 +341,21 @@ where
|
|||||||
return svc.render(200, path, ());
|
return svc.render(200, path, ());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if method == Method::GET && path.starts_with("/.well-known/") {
|
||||||
|
let token = path.trim_start_matches("/.well-known/");
|
||||||
|
|
||||||
|
if let Ok(key) = svc.domain_manager.get_acme_http01_challenge_key(token) {
|
||||||
|
let body: hyper::Body = key.into();
|
||||||
|
return match Response::builder().status(200).body(body) {
|
||||||
|
Ok(res) => Ok(res),
|
||||||
|
Err(err) => Err(format!(
|
||||||
|
"failed to write acme http-01 challenge key: {}",
|
||||||
|
err
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match (method, path) {
|
match (method, path) {
|
||||||
(&Method::GET, "/") | (&Method::GET, "/index.html") => svc.render_page("/index.html", ()),
|
(&Method::GET, "/") | (&Method::GET, "/index.html") => svc.render_page("/index.html", ()),
|
||||||
(&Method::GET, "/domain.html") => {
|
(&Method::GET, "/domain.html") => {
|
||||||
|
Loading…
Reference in New Issue
Block a user