Skip to content

Commit f8a147d

Browse files
committed
Expose ACME support in payjoin directory binary
Currently the certificate cache directory is mandatory. In principle it doesn't have to be.
1 parent 2a69d33 commit f8a147d

3 files changed

Lines changed: 81 additions & 2 deletions

File tree

payjoin-directory/src/cli.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ pub struct Cli {
1616
#[arg(long, env = "PJ_METRIC_PORT", help = "The port to bind for prometheus metrics export")]
1717
pub metrics_port: Option<u16>, // TODO tokio_listener::ListenerAddressLFlag
1818

19+
#[cfg(feature = "acme")]
20+
#[clap(flatten)]
21+
pub acme: AcmeCli,
22+
1923
#[arg(
2024
long,
2125
env = "PJ_DIR_TIMEOUT_SECS",
@@ -37,3 +41,22 @@ pub struct Cli {
3741
)]
3842
pub ohttp_keys: Option<PathBuf>,
3943
}
44+
45+
#[cfg(feature = "acme")]
46+
#[derive(Debug, Parser)]
47+
pub struct AcmeCli {
48+
#[arg(long = "acme-domain", help = "The domain for which to request a certificate using ACME")]
49+
pub domain: Option<String>,
50+
51+
#[arg(
52+
long = "acme-contact",
53+
help = "Contact information for ACME usage (e.g. 'mailto:admin@example.com')"
54+
)]
55+
pub contact: Option<String>,
56+
57+
#[arg(long, help = "Whether to use the staging environment [default: production]")]
58+
pub lets_encrypt_staging: Option<bool>,
59+
60+
#[arg(long = "acme-cache-dir", help = "What directory to use for the ACME cache")]
61+
pub cache_dir: Option<PathBuf>,
62+
}

payjoin-directory/src/config.rs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,27 @@ pub struct Config {
1717
pub timeout: Duration,
1818
pub storage_dir: PathBuf,
1919
pub ohttp_keys: PathBuf, // TODO OhttpConfig struct with rotation params, etc
20+
#[cfg(feature = "acme")]
21+
pub acme: Option<AcmeConfig>,
22+
}
23+
24+
#[cfg(feature = "acme")]
25+
#[derive(Debug, Clone, Deserialize)]
26+
pub struct AcmeConfig {
27+
pub domain: String,
28+
pub contact: String,
29+
pub lets_encrypt_staging: bool,
30+
pub cache_dir: PathBuf,
31+
}
32+
33+
#[cfg(feature = "acme")]
34+
impl From<AcmeConfig> for tokio_rustls_acme::AcmeConfig<std::io::Error, std::io::Error> {
35+
fn from(acme_config: AcmeConfig) -> Self {
36+
tokio_rustls_acme::AcmeConfig::new([acme_config.domain])
37+
.contact_push(acme_config.contact)
38+
.cache(tokio_rustls_acme::caches::DirCache::new(acme_config.cache_dir))
39+
.directory_lets_encrypt(!acme_config.lets_encrypt_staging)
40+
}
2041
}
2142

2243
impl Config {
@@ -35,12 +56,23 @@ impl Config {
3556
timeout: Duration::from_secs(built_config.get("timeout")?),
3657
storage_dir: built_config.get("storage_dir")?,
3758
ohttp_keys: built_config.get("ohttp_keys")?,
59+
#[cfg(feature = "acme")]
60+
acme: if built_config.get_table("acme").is_ok() {
61+
Some(AcmeConfig {
62+
domain: built_config.get("acme.domain")?,
63+
contact: built_config.get("acme.contact")?,
64+
lets_encrypt_staging: built_config.get("acme.lets_encrypt_staging")?,
65+
cache_dir: built_config.get("acme.cache_dir")?,
66+
})
67+
} else {
68+
None
69+
},
3870
})
3971
}
4072
}
4173

4274
fn add_defaults(config: Builder, cli: &Cli) -> Result<Builder, ConfigError> {
43-
config
75+
let config = config
4476
.set_default("listen_addr", "[::]:8080")?
4577
.set_override_option("listen_addr", cli.port.map(|port| format!("[::]:{}", port)))?
4678
.set_default("metrics_listen_addr", Option::<String>::None)?
@@ -58,5 +90,21 @@ fn add_defaults(config: Builder, cli: &Cli) -> Result<Builder, ConfigError> {
5890
.set_override_option(
5991
"storage_dir",
6092
cli.storage_dir.clone().map(|s| s.to_string_lossy().into_owned()),
61-
)
93+
)?;
94+
95+
#[cfg(feature = "acme")]
96+
let config = if cli.acme.domain.is_some() {
97+
config
98+
.set_override_option("acme.domain", cli.acme.domain.clone())?
99+
.set_override_option("acme.contact", cli.acme.contact.clone())?
100+
.set_override_option("acme.lets_encrypt_staging", cli.acme.lets_encrypt_staging)?
101+
.set_override_option(
102+
"acme.cache_dir",
103+
cli.acme.cache_dir.clone().map(|s| s.to_string_lossy().into_owned()),
104+
)?
105+
} else {
106+
config
107+
};
108+
109+
Ok(config)
62110
}

payjoin-directory/src/main.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,15 @@ async fn main() -> Result<(), BoxError> {
3838
}
3939

4040
let listener = TcpListener::bind(config.listen_addr).await?;
41+
42+
#[cfg(feature = "acme")]
43+
if let Some(acme_config) = config.acme {
44+
service.serve_acme(listener, acme_config.into()).await;
45+
return Ok(());
46+
}
47+
4148
service.serve_tcp(listener).await;
49+
4250
Ok(())
4351
}
4452

0 commit comments

Comments
 (0)