itsi 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e3af10f03274d8f16b8ef964b4c97825c85f89898c136c9d105405b46581cfdb
4
- data.tar.gz: 3254b73a71c5c56e6f1cf30249b1edbd865d9ee22dc9ae350e4e93adea778c81
3
+ metadata.gz: 43c8c15592c8e8e749938e5c5c4b5e481ba3ea88d82bac63059a612e35bf2213
4
+ data.tar.gz: e57ab7eacc32f7a0ac702be2e15ac9a36d48aff27aaa3705f77c428df2acf0f8
5
5
  SHA512:
6
- metadata.gz: e7c3e0561e32e4dfcf5b06e7a0a860f715b0cc9faf8f0f52da73e7051141617d4945cabaa2acce7fed74ae110dc1591bd9b38d035d0db75d538e063254478948
7
- data.tar.gz: 7065897feb5a41ffe7cb5e2e0d7ca38790ed26498e2a0ed203a2575c95895ca6c9d22a9f2a45ba66ed599399ad6f0b4b50f229f34b074a1994ae560ab5774e2a
6
+ metadata.gz: 1ea20d0e8d9d82cb4b095674192ead9837334c5472761cce1dc093d43d829c2d71ecb74d06a667f4c8274607791bd1cd1ad12f005bf1bc8a3fa5179499a61d8f
7
+ data.tar.gz: c1aa8a84ebd694f3f39d287fd67e320c69f89dd35315af3203e09259c500404825790e925abd8b9a6ee26d5a97638a1d7442a9bb369653398922a42a08742498
data/Cargo.lock CHANGED
@@ -434,7 +434,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
434
434
  checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
435
435
  dependencies = [
436
436
  "libc",
437
- "windows-sys 0.52.0",
437
+ "windows-sys 0.59.0",
438
438
  ]
439
439
 
440
440
  [[package]]
@@ -479,6 +479,16 @@ dependencies = [
479
479
  "percent-encoding",
480
480
  ]
481
481
 
482
+ [[package]]
483
+ name = "fs2"
484
+ version = "0.4.3"
485
+ source = "registry+https://github.com/rust-lang/crates.io-index"
486
+ checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
487
+ dependencies = [
488
+ "libc",
489
+ "winapi",
490
+ ]
491
+
482
492
  [[package]]
483
493
  name = "fs_extra"
484
494
  version = "1.3.0"
@@ -974,10 +984,12 @@ name = "itsi-server"
974
984
  version = "0.1.0"
975
985
  dependencies = [
976
986
  "async-channel",
987
+ "async-trait",
977
988
  "base64",
978
989
  "bytes",
979
990
  "crossbeam",
980
991
  "derive_more",
992
+ "fs2",
981
993
  "futures",
982
994
  "http",
983
995
  "http-body-util",
@@ -993,6 +1005,7 @@ dependencies = [
993
1005
  "pin-project",
994
1006
  "rb-sys",
995
1007
  "rcgen",
1008
+ "ring",
996
1009
  "rustls",
997
1010
  "rustls-pemfile",
998
1011
  "socket2",
@@ -1480,7 +1493,7 @@ dependencies = [
1480
1493
  "once_cell",
1481
1494
  "socket2",
1482
1495
  "tracing",
1483
- "windows-sys 0.52.0",
1496
+ "windows-sys 0.59.0",
1484
1497
  ]
1485
1498
 
1486
1499
  [[package]]
@@ -1684,9 +1697,9 @@ dependencies = [
1684
1697
 
1685
1698
  [[package]]
1686
1699
  name = "ring"
1687
- version = "0.17.11"
1700
+ version = "0.17.14"
1688
1701
  source = "registry+https://github.com/rust-lang/crates.io-index"
1689
- checksum = "da5349ae27d3887ca812fb375b45a4fbb36d8d12d2df394968cd86e35683fe73"
1702
+ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
1690
1703
  dependencies = [
1691
1704
  "cc",
1692
1705
  "cfg-if",
@@ -1733,7 +1746,7 @@ dependencies = [
1733
1746
  "errno",
1734
1747
  "libc",
1735
1748
  "linux-raw-sys 0.4.15",
1736
- "windows-sys 0.52.0",
1749
+ "windows-sys 0.59.0",
1737
1750
  ]
1738
1751
 
1739
1752
  [[package]]
@@ -1746,7 +1759,7 @@ dependencies = [
1746
1759
  "errno",
1747
1760
  "libc",
1748
1761
  "linux-raw-sys 0.9.2",
1749
- "windows-sys 0.52.0",
1762
+ "windows-sys 0.59.0",
1750
1763
  ]
1751
1764
 
1752
1765
  [[package]]
@@ -1997,7 +2010,7 @@ dependencies = [
1997
2010
  "getrandom 0.3.1",
1998
2011
  "once_cell",
1999
2012
  "rustix 1.0.1",
2000
- "windows-sys 0.52.0",
2013
+ "windows-sys 0.59.0",
2001
2014
  ]
2002
2015
 
2003
2016
  [[package]]
@@ -41,3 +41,6 @@ tempfile = "3.18.0"
41
41
  sysinfo = "0.33.1"
42
42
  tokio-rustls-acme = "0.6.0"
43
43
  rustls = "0.23.23"
44
+ fs2 = "0.4.3"
45
+ ring = "0.17.14"
46
+ async-trait = "0.1.87"
@@ -78,8 +78,9 @@ impl TokioListener {
78
78
  {
79
79
  let mut state = state.lock().await;
80
80
  loop {
81
- if let Some(event) = StreamExt::next(&mut *state).await {
82
- info!("Received acme event: {:?}", event)
81
+ match StreamExt::next(&mut *state).await {
82
+ Some(event) => info!("Received acme event: {:?}", event),
83
+ None => error!("Received no acme event"),
83
84
  }
84
85
  }
85
86
  }
@@ -96,19 +97,19 @@ impl TokioListener {
96
97
  }
97
98
  ItsiTlsAcceptor::Automatic(acme_acceptor, _, rustls_config) => {
98
99
  let accept_future = acme_acceptor.accept(tcp_stream.0);
99
- match accept_future.await.unwrap() {
100
- None => Err(ItsiError::Pass()),
101
- Some(start_handshake) => {
102
- let tls_stream = start_handshake
103
- .into_stream(rustls_config.clone())
104
- .await
105
- .unwrap();
106
- // Wrap in your IoStream::TcpTls variant
100
+ match accept_future.await {
101
+ Ok(None) => Err(ItsiError::Pass()),
102
+ Ok(Some(start_handshake)) => {
103
+ let tls_stream = start_handshake.into_stream(rustls_config.clone()).await?;
107
104
  Ok(IoStream::TcpTls {
108
105
  stream: tls_stream,
109
106
  addr: SockAddr::Tcp(Arc::new(tcp_stream.1)),
110
107
  })
111
108
  }
109
+ Err(error) => {
110
+ error!(error = format!("{:?}", error));
111
+ Err(ItsiError::Pass())
112
+ }
112
113
  }
113
114
  }
114
115
  }
@@ -83,7 +83,7 @@ impl SingleMode {
83
83
  Ok(())
84
84
  }
85
85
 
86
- #[instrument(parent=None, skip(self))]
86
+ #[instrument(parent=None, skip(self), fields(pid=format!("{}", Pid::this())))]
87
87
  pub fn run(self: Arc<Self>) -> Result<()> {
88
88
  let mut listener_task_set = JoinSet::new();
89
89
  let self_ref = Arc::new(self);
@@ -0,0 +1,132 @@
1
+ use async_trait::async_trait;
2
+ use fs2::FileExt;
3
+ use parking_lot::Mutex;
4
+ use std::env;
5
+ use std::fs::{self, OpenOptions};
6
+ use std::io::Error as IoError;
7
+ use std::path::{Path, PathBuf};
8
+ use tokio_rustls_acme::caches::DirCache;
9
+ use tokio_rustls_acme::{AccountCache, CertCache};
10
+
11
+ /// A wrapper around DirCache that locks a file before writing cert/account data.
12
+ pub struct LockedDirCache<P: AsRef<Path> + Send + Sync> {
13
+ inner: DirCache<P>,
14
+ lock_path: PathBuf,
15
+ current_lock: Mutex<Option<std::fs::File>>,
16
+ }
17
+
18
+ impl<P: AsRef<Path> + Send + Sync> LockedDirCache<P> {
19
+ pub fn new(dir: P) -> Self {
20
+ let dir_path = dir.as_ref().to_path_buf();
21
+ std::fs::create_dir_all(&dir_path).unwrap();
22
+ let lock_path =
23
+ dir_path.join(env::var("ITSI_ACME_LOCK_FILE_NAME").unwrap_or(".acme.lock".to_string()));
24
+ Self::touch_file(&lock_path).expect("Failed to create lock file");
25
+
26
+ Self {
27
+ inner: DirCache::new(dir),
28
+ lock_path,
29
+ current_lock: Mutex::new(None),
30
+ }
31
+ }
32
+
33
+ fn touch_file(path: &PathBuf) -> std::io::Result<()> {
34
+ if let Some(parent) = path.parent() {
35
+ fs::create_dir_all(parent)?;
36
+ }
37
+ fs::OpenOptions::new()
38
+ .create(true)
39
+ .write(true)
40
+ .truncate(true)
41
+ .open(path)?;
42
+ Ok(())
43
+ }
44
+
45
+ fn lock_exclusive(&self) -> Result<(), IoError> {
46
+ if self.current_lock.lock().is_some() {
47
+ return Ok(());
48
+ }
49
+
50
+ if let Some(parent) = self.lock_path.parent() {
51
+ std::fs::create_dir_all(parent)?;
52
+ }
53
+ let lockfile = OpenOptions::new()
54
+ .create(true)
55
+ .write(true)
56
+ .truncate(true)
57
+ .open(&self.lock_path)?;
58
+ lockfile.lock_exclusive()?;
59
+ *self.current_lock.lock() = Some(lockfile);
60
+ Ok(())
61
+ }
62
+
63
+ fn unlock(&self) -> Result<(), IoError> {
64
+ self.current_lock.lock().take();
65
+ Ok(())
66
+ }
67
+ }
68
+
69
+ #[async_trait]
70
+ impl<P: AsRef<Path> + Send + Sync> CertCache for LockedDirCache<P> {
71
+ type EC = IoError;
72
+
73
+ async fn load_cert(
74
+ &self,
75
+ domains: &[String],
76
+ directory_url: &str,
77
+ ) -> Result<Option<Vec<u8>>, Self::EC> {
78
+ self.lock_exclusive()?;
79
+ let result = self.inner.load_cert(domains, directory_url).await;
80
+
81
+ if let Ok(Some(_)) = result {
82
+ self.unlock()?;
83
+ }
84
+
85
+ result
86
+ }
87
+
88
+ async fn store_cert(
89
+ &self,
90
+ domains: &[String],
91
+ directory_url: &str,
92
+ cert: &[u8],
93
+ ) -> Result<(), Self::EC> {
94
+ // Acquire the lock before storing
95
+ self.lock_exclusive()?;
96
+
97
+ // Perform the store operation
98
+ let result = self.inner.store_cert(domains, directory_url, cert).await;
99
+
100
+ if let Ok(()) = result {
101
+ self.unlock()?;
102
+ }
103
+ result
104
+ }
105
+ }
106
+
107
+ #[async_trait]
108
+ impl<P: AsRef<Path> + Send + Sync> AccountCache for LockedDirCache<P> {
109
+ type EA = IoError;
110
+
111
+ async fn load_account(
112
+ &self,
113
+ contact: &[String],
114
+ directory_url: &str,
115
+ ) -> Result<Option<Vec<u8>>, Self::EA> {
116
+ self.lock_exclusive()?;
117
+ self.inner.load_account(contact, directory_url).await
118
+ }
119
+
120
+ async fn store_account(
121
+ &self,
122
+ contact: &[String],
123
+ directory_url: &str,
124
+ account: &[u8],
125
+ ) -> Result<(), Self::EA> {
126
+ self.lock_exclusive()?;
127
+
128
+ self.inner
129
+ .store_account(contact, directory_url, account)
130
+ .await
131
+ }
132
+ }
@@ -1,8 +1,12 @@
1
1
  use base64::{engine::general_purpose, Engine as _};
2
2
  use itsi_error::Result;
3
3
  use itsi_tracing::info;
4
+ use locked_dir_cache::LockedDirCache;
4
5
  use rcgen::{CertificateParams, DnType, KeyPair, SanType};
5
- use rustls::pki_types::{CertificateDer, PrivateKeyDer};
6
+ use rustls::{
7
+ pki_types::{CertificateDer, PrivateKeyDer},
8
+ ClientConfig, RootCertStore,
9
+ };
6
10
  use rustls_pemfile::{certs, pkcs8_private_keys};
7
11
  use std::{
8
12
  collections::HashMap,
@@ -12,8 +16,8 @@ use std::{
12
16
  };
13
17
  use tokio::sync::Mutex;
14
18
  use tokio_rustls::{rustls::ServerConfig, TlsAcceptor};
15
- use tokio_rustls_acme::{caches::DirCache, AcmeAcceptor, AcmeConfig, AcmeState};
16
-
19
+ use tokio_rustls_acme::{AcmeAcceptor, AcmeConfig, AcmeState};
20
+ mod locked_dir_cache;
17
21
  const ITS_CA_CERT: &str = include_str!("./itsi_ca/itsi_ca.crt");
18
22
  const ITS_CA_KEY: &str = include_str!("./itsi_ca/itsi_ca.key");
19
23
 
@@ -48,10 +52,41 @@ pub fn configure_tls(
48
52
  domains = format!("{:?}", domains),
49
53
  directory_url, "Requesting acme cert"
50
54
  );
55
+
56
+ let mut root_cert_store = RootCertStore::empty();
57
+ if let Ok(ca_pem_path) = env::var("ITSI_ACME_CA_PEM_PATH") {
58
+ let ca_pem = fs::read(ca_pem_path).expect("failed to read CA pem file");
59
+ let mut ca_reader = BufReader::new(&ca_pem[..]);
60
+ let der_certs: Vec<CertificateDer> = certs(&mut ca_reader)
61
+ .collect::<std::result::Result<Vec<CertificateDer>, _>>()
62
+ .map_err(|e| {
63
+ itsi_error::ItsiError::ArgumentError(format!(
64
+ "Invalid ACME CA Pem path {:?}",
65
+ e
66
+ ))
67
+ })?;
68
+ root_cert_store.add_parsable_certificates(der_certs);
69
+ }
70
+
71
+ let client_config = ClientConfig::builder()
72
+ .with_root_certificates(root_cert_store)
73
+ .with_no_client_auth();
74
+
75
+ let contact_email = env::var("ITSI_ACME_CONTACT_EMAIL").map_err(|_| {
76
+ itsi_error::ItsiError::ArgumentError(
77
+ "ITSI_ACME_CONTACT_EMAIL must be set before you can auto-generate production certificates"
78
+ .to_string(),
79
+ )
80
+ })?;
81
+
82
+ let cache_dir = env::var("ITSI_ACME_CACHE_DIR")
83
+ .unwrap_or_else(|_| "./.rustls_acme_cache".to_string());
84
+
51
85
  let acme_state = AcmeConfig::new(domains)
52
- .contact(["mailto:wc@pico.net.nz"])
53
- .cache(DirCache::new("./rustls_acme_cache"))
86
+ .contact([format!("mailto:{}", contact_email)])
87
+ .cache(LockedDirCache::new(cache_dir))
54
88
  .directory(directory_url)
89
+ .client_tls_config(Arc::new(client_config))
55
90
  .state();
56
91
  let rustls_config = ServerConfig::builder()
57
92
  .with_no_client_auth()
@@ -41,3 +41,6 @@ tempfile = "3.18.0"
41
41
  sysinfo = "0.33.1"
42
42
  tokio-rustls-acme = "0.6.0"
43
43
  rustls = "0.23.23"
44
+ fs2 = "0.4.3"
45
+ ring = "0.17.14"
46
+ async-trait = "0.1.87"
@@ -78,8 +78,9 @@ impl TokioListener {
78
78
  {
79
79
  let mut state = state.lock().await;
80
80
  loop {
81
- if let Some(event) = StreamExt::next(&mut *state).await {
82
- info!("Received acme event: {:?}", event)
81
+ match StreamExt::next(&mut *state).await {
82
+ Some(event) => info!("Received acme event: {:?}", event),
83
+ None => error!("Received no acme event"),
83
84
  }
84
85
  }
85
86
  }
@@ -96,19 +97,19 @@ impl TokioListener {
96
97
  }
97
98
  ItsiTlsAcceptor::Automatic(acme_acceptor, _, rustls_config) => {
98
99
  let accept_future = acme_acceptor.accept(tcp_stream.0);
99
- match accept_future.await.unwrap() {
100
- None => Err(ItsiError::Pass()),
101
- Some(start_handshake) => {
102
- let tls_stream = start_handshake
103
- .into_stream(rustls_config.clone())
104
- .await
105
- .unwrap();
106
- // Wrap in your IoStream::TcpTls variant
100
+ match accept_future.await {
101
+ Ok(None) => Err(ItsiError::Pass()),
102
+ Ok(Some(start_handshake)) => {
103
+ let tls_stream = start_handshake.into_stream(rustls_config.clone()).await?;
107
104
  Ok(IoStream::TcpTls {
108
105
  stream: tls_stream,
109
106
  addr: SockAddr::Tcp(Arc::new(tcp_stream.1)),
110
107
  })
111
108
  }
109
+ Err(error) => {
110
+ error!(error = format!("{:?}", error));
111
+ Err(ItsiError::Pass())
112
+ }
112
113
  }
113
114
  }
114
115
  }
@@ -83,7 +83,7 @@ impl SingleMode {
83
83
  Ok(())
84
84
  }
85
85
 
86
- #[instrument(parent=None, skip(self))]
86
+ #[instrument(parent=None, skip(self), fields(pid=format!("{}", Pid::this())))]
87
87
  pub fn run(self: Arc<Self>) -> Result<()> {
88
88
  let mut listener_task_set = JoinSet::new();
89
89
  let self_ref = Arc::new(self);
@@ -0,0 +1,132 @@
1
+ use async_trait::async_trait;
2
+ use fs2::FileExt;
3
+ use parking_lot::Mutex;
4
+ use std::env;
5
+ use std::fs::{self, OpenOptions};
6
+ use std::io::Error as IoError;
7
+ use std::path::{Path, PathBuf};
8
+ use tokio_rustls_acme::caches::DirCache;
9
+ use tokio_rustls_acme::{AccountCache, CertCache};
10
+
11
+ /// A wrapper around DirCache that locks a file before writing cert/account data.
12
+ pub struct LockedDirCache<P: AsRef<Path> + Send + Sync> {
13
+ inner: DirCache<P>,
14
+ lock_path: PathBuf,
15
+ current_lock: Mutex<Option<std::fs::File>>,
16
+ }
17
+
18
+ impl<P: AsRef<Path> + Send + Sync> LockedDirCache<P> {
19
+ pub fn new(dir: P) -> Self {
20
+ let dir_path = dir.as_ref().to_path_buf();
21
+ std::fs::create_dir_all(&dir_path).unwrap();
22
+ let lock_path =
23
+ dir_path.join(env::var("ITSI_ACME_LOCK_FILE_NAME").unwrap_or(".acme.lock".to_string()));
24
+ Self::touch_file(&lock_path).expect("Failed to create lock file");
25
+
26
+ Self {
27
+ inner: DirCache::new(dir),
28
+ lock_path,
29
+ current_lock: Mutex::new(None),
30
+ }
31
+ }
32
+
33
+ fn touch_file(path: &PathBuf) -> std::io::Result<()> {
34
+ if let Some(parent) = path.parent() {
35
+ fs::create_dir_all(parent)?;
36
+ }
37
+ fs::OpenOptions::new()
38
+ .create(true)
39
+ .write(true)
40
+ .truncate(true)
41
+ .open(path)?;
42
+ Ok(())
43
+ }
44
+
45
+ fn lock_exclusive(&self) -> Result<(), IoError> {
46
+ if self.current_lock.lock().is_some() {
47
+ return Ok(());
48
+ }
49
+
50
+ if let Some(parent) = self.lock_path.parent() {
51
+ std::fs::create_dir_all(parent)?;
52
+ }
53
+ let lockfile = OpenOptions::new()
54
+ .create(true)
55
+ .write(true)
56
+ .truncate(true)
57
+ .open(&self.lock_path)?;
58
+ lockfile.lock_exclusive()?;
59
+ *self.current_lock.lock() = Some(lockfile);
60
+ Ok(())
61
+ }
62
+
63
+ fn unlock(&self) -> Result<(), IoError> {
64
+ self.current_lock.lock().take();
65
+ Ok(())
66
+ }
67
+ }
68
+
69
+ #[async_trait]
70
+ impl<P: AsRef<Path> + Send + Sync> CertCache for LockedDirCache<P> {
71
+ type EC = IoError;
72
+
73
+ async fn load_cert(
74
+ &self,
75
+ domains: &[String],
76
+ directory_url: &str,
77
+ ) -> Result<Option<Vec<u8>>, Self::EC> {
78
+ self.lock_exclusive()?;
79
+ let result = self.inner.load_cert(domains, directory_url).await;
80
+
81
+ if let Ok(Some(_)) = result {
82
+ self.unlock()?;
83
+ }
84
+
85
+ result
86
+ }
87
+
88
+ async fn store_cert(
89
+ &self,
90
+ domains: &[String],
91
+ directory_url: &str,
92
+ cert: &[u8],
93
+ ) -> Result<(), Self::EC> {
94
+ // Acquire the lock before storing
95
+ self.lock_exclusive()?;
96
+
97
+ // Perform the store operation
98
+ let result = self.inner.store_cert(domains, directory_url, cert).await;
99
+
100
+ if let Ok(()) = result {
101
+ self.unlock()?;
102
+ }
103
+ result
104
+ }
105
+ }
106
+
107
+ #[async_trait]
108
+ impl<P: AsRef<Path> + Send + Sync> AccountCache for LockedDirCache<P> {
109
+ type EA = IoError;
110
+
111
+ async fn load_account(
112
+ &self,
113
+ contact: &[String],
114
+ directory_url: &str,
115
+ ) -> Result<Option<Vec<u8>>, Self::EA> {
116
+ self.lock_exclusive()?;
117
+ self.inner.load_account(contact, directory_url).await
118
+ }
119
+
120
+ async fn store_account(
121
+ &self,
122
+ contact: &[String],
123
+ directory_url: &str,
124
+ account: &[u8],
125
+ ) -> Result<(), Self::EA> {
126
+ self.lock_exclusive()?;
127
+
128
+ self.inner
129
+ .store_account(contact, directory_url, account)
130
+ .await
131
+ }
132
+ }
@@ -1,8 +1,12 @@
1
1
  use base64::{engine::general_purpose, Engine as _};
2
2
  use itsi_error::Result;
3
3
  use itsi_tracing::info;
4
+ use locked_dir_cache::LockedDirCache;
4
5
  use rcgen::{CertificateParams, DnType, KeyPair, SanType};
5
- use rustls::pki_types::{CertificateDer, PrivateKeyDer};
6
+ use rustls::{
7
+ pki_types::{CertificateDer, PrivateKeyDer},
8
+ ClientConfig, RootCertStore,
9
+ };
6
10
  use rustls_pemfile::{certs, pkcs8_private_keys};
7
11
  use std::{
8
12
  collections::HashMap,
@@ -12,8 +16,8 @@ use std::{
12
16
  };
13
17
  use tokio::sync::Mutex;
14
18
  use tokio_rustls::{rustls::ServerConfig, TlsAcceptor};
15
- use tokio_rustls_acme::{caches::DirCache, AcmeAcceptor, AcmeConfig, AcmeState};
16
-
19
+ use tokio_rustls_acme::{AcmeAcceptor, AcmeConfig, AcmeState};
20
+ mod locked_dir_cache;
17
21
  const ITS_CA_CERT: &str = include_str!("./itsi_ca/itsi_ca.crt");
18
22
  const ITS_CA_KEY: &str = include_str!("./itsi_ca/itsi_ca.key");
19
23
 
@@ -48,10 +52,41 @@ pub fn configure_tls(
48
52
  domains = format!("{:?}", domains),
49
53
  directory_url, "Requesting acme cert"
50
54
  );
55
+
56
+ let mut root_cert_store = RootCertStore::empty();
57
+ if let Ok(ca_pem_path) = env::var("ITSI_ACME_CA_PEM_PATH") {
58
+ let ca_pem = fs::read(ca_pem_path).expect("failed to read CA pem file");
59
+ let mut ca_reader = BufReader::new(&ca_pem[..]);
60
+ let der_certs: Vec<CertificateDer> = certs(&mut ca_reader)
61
+ .collect::<std::result::Result<Vec<CertificateDer>, _>>()
62
+ .map_err(|e| {
63
+ itsi_error::ItsiError::ArgumentError(format!(
64
+ "Invalid ACME CA Pem path {:?}",
65
+ e
66
+ ))
67
+ })?;
68
+ root_cert_store.add_parsable_certificates(der_certs);
69
+ }
70
+
71
+ let client_config = ClientConfig::builder()
72
+ .with_root_certificates(root_cert_store)
73
+ .with_no_client_auth();
74
+
75
+ let contact_email = env::var("ITSI_ACME_CONTACT_EMAIL").map_err(|_| {
76
+ itsi_error::ItsiError::ArgumentError(
77
+ "ITSI_ACME_CONTACT_EMAIL must be set before you can auto-generate production certificates"
78
+ .to_string(),
79
+ )
80
+ })?;
81
+
82
+ let cache_dir = env::var("ITSI_ACME_CACHE_DIR")
83
+ .unwrap_or_else(|_| "./.rustls_acme_cache".to_string());
84
+
51
85
  let acme_state = AcmeConfig::new(domains)
52
- .contact(["mailto:wc@pico.net.nz"])
53
- .cache(DirCache::new("./rustls_acme_cache"))
86
+ .contact([format!("mailto:{}", contact_email)])
87
+ .cache(LockedDirCache::new(cache_dir))
54
88
  .directory(directory_url)
89
+ .client_tls_config(Arc::new(client_config))
55
90
  .state();
56
91
  let rustls_config = ServerConfig::builder()
57
92
  .with_no_client_auth()
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Scheduler
5
- VERSION = "0.1.4"
5
+ VERSION = "0.1.6"
6
6
  end
7
7
  end
@@ -41,3 +41,6 @@ tempfile = "3.18.0"
41
41
  sysinfo = "0.33.1"
42
42
  tokio-rustls-acme = "0.6.0"
43
43
  rustls = "0.23.23"
44
+ fs2 = "0.4.3"
45
+ ring = "0.17.14"
46
+ async-trait = "0.1.87"
@@ -78,8 +78,9 @@ impl TokioListener {
78
78
  {
79
79
  let mut state = state.lock().await;
80
80
  loop {
81
- if let Some(event) = StreamExt::next(&mut *state).await {
82
- info!("Received acme event: {:?}", event)
81
+ match StreamExt::next(&mut *state).await {
82
+ Some(event) => info!("Received acme event: {:?}", event),
83
+ None => error!("Received no acme event"),
83
84
  }
84
85
  }
85
86
  }
@@ -96,19 +97,19 @@ impl TokioListener {
96
97
  }
97
98
  ItsiTlsAcceptor::Automatic(acme_acceptor, _, rustls_config) => {
98
99
  let accept_future = acme_acceptor.accept(tcp_stream.0);
99
- match accept_future.await.unwrap() {
100
- None => Err(ItsiError::Pass()),
101
- Some(start_handshake) => {
102
- let tls_stream = start_handshake
103
- .into_stream(rustls_config.clone())
104
- .await
105
- .unwrap();
106
- // Wrap in your IoStream::TcpTls variant
100
+ match accept_future.await {
101
+ Ok(None) => Err(ItsiError::Pass()),
102
+ Ok(Some(start_handshake)) => {
103
+ let tls_stream = start_handshake.into_stream(rustls_config.clone()).await?;
107
104
  Ok(IoStream::TcpTls {
108
105
  stream: tls_stream,
109
106
  addr: SockAddr::Tcp(Arc::new(tcp_stream.1)),
110
107
  })
111
108
  }
109
+ Err(error) => {
110
+ error!(error = format!("{:?}", error));
111
+ Err(ItsiError::Pass())
112
+ }
112
113
  }
113
114
  }
114
115
  }
@@ -83,7 +83,7 @@ impl SingleMode {
83
83
  Ok(())
84
84
  }
85
85
 
86
- #[instrument(parent=None, skip(self))]
86
+ #[instrument(parent=None, skip(self), fields(pid=format!("{}", Pid::this())))]
87
87
  pub fn run(self: Arc<Self>) -> Result<()> {
88
88
  let mut listener_task_set = JoinSet::new();
89
89
  let self_ref = Arc::new(self);
@@ -0,0 +1,132 @@
1
+ use async_trait::async_trait;
2
+ use fs2::FileExt;
3
+ use parking_lot::Mutex;
4
+ use std::env;
5
+ use std::fs::{self, OpenOptions};
6
+ use std::io::Error as IoError;
7
+ use std::path::{Path, PathBuf};
8
+ use tokio_rustls_acme::caches::DirCache;
9
+ use tokio_rustls_acme::{AccountCache, CertCache};
10
+
11
+ /// A wrapper around DirCache that locks a file before writing cert/account data.
12
+ pub struct LockedDirCache<P: AsRef<Path> + Send + Sync> {
13
+ inner: DirCache<P>,
14
+ lock_path: PathBuf,
15
+ current_lock: Mutex<Option<std::fs::File>>,
16
+ }
17
+
18
+ impl<P: AsRef<Path> + Send + Sync> LockedDirCache<P> {
19
+ pub fn new(dir: P) -> Self {
20
+ let dir_path = dir.as_ref().to_path_buf();
21
+ std::fs::create_dir_all(&dir_path).unwrap();
22
+ let lock_path =
23
+ dir_path.join(env::var("ITSI_ACME_LOCK_FILE_NAME").unwrap_or(".acme.lock".to_string()));
24
+ Self::touch_file(&lock_path).expect("Failed to create lock file");
25
+
26
+ Self {
27
+ inner: DirCache::new(dir),
28
+ lock_path,
29
+ current_lock: Mutex::new(None),
30
+ }
31
+ }
32
+
33
+ fn touch_file(path: &PathBuf) -> std::io::Result<()> {
34
+ if let Some(parent) = path.parent() {
35
+ fs::create_dir_all(parent)?;
36
+ }
37
+ fs::OpenOptions::new()
38
+ .create(true)
39
+ .write(true)
40
+ .truncate(true)
41
+ .open(path)?;
42
+ Ok(())
43
+ }
44
+
45
+ fn lock_exclusive(&self) -> Result<(), IoError> {
46
+ if self.current_lock.lock().is_some() {
47
+ return Ok(());
48
+ }
49
+
50
+ if let Some(parent) = self.lock_path.parent() {
51
+ std::fs::create_dir_all(parent)?;
52
+ }
53
+ let lockfile = OpenOptions::new()
54
+ .create(true)
55
+ .write(true)
56
+ .truncate(true)
57
+ .open(&self.lock_path)?;
58
+ lockfile.lock_exclusive()?;
59
+ *self.current_lock.lock() = Some(lockfile);
60
+ Ok(())
61
+ }
62
+
63
+ fn unlock(&self) -> Result<(), IoError> {
64
+ self.current_lock.lock().take();
65
+ Ok(())
66
+ }
67
+ }
68
+
69
+ #[async_trait]
70
+ impl<P: AsRef<Path> + Send + Sync> CertCache for LockedDirCache<P> {
71
+ type EC = IoError;
72
+
73
+ async fn load_cert(
74
+ &self,
75
+ domains: &[String],
76
+ directory_url: &str,
77
+ ) -> Result<Option<Vec<u8>>, Self::EC> {
78
+ self.lock_exclusive()?;
79
+ let result = self.inner.load_cert(domains, directory_url).await;
80
+
81
+ if let Ok(Some(_)) = result {
82
+ self.unlock()?;
83
+ }
84
+
85
+ result
86
+ }
87
+
88
+ async fn store_cert(
89
+ &self,
90
+ domains: &[String],
91
+ directory_url: &str,
92
+ cert: &[u8],
93
+ ) -> Result<(), Self::EC> {
94
+ // Acquire the lock before storing
95
+ self.lock_exclusive()?;
96
+
97
+ // Perform the store operation
98
+ let result = self.inner.store_cert(domains, directory_url, cert).await;
99
+
100
+ if let Ok(()) = result {
101
+ self.unlock()?;
102
+ }
103
+ result
104
+ }
105
+ }
106
+
107
+ #[async_trait]
108
+ impl<P: AsRef<Path> + Send + Sync> AccountCache for LockedDirCache<P> {
109
+ type EA = IoError;
110
+
111
+ async fn load_account(
112
+ &self,
113
+ contact: &[String],
114
+ directory_url: &str,
115
+ ) -> Result<Option<Vec<u8>>, Self::EA> {
116
+ self.lock_exclusive()?;
117
+ self.inner.load_account(contact, directory_url).await
118
+ }
119
+
120
+ async fn store_account(
121
+ &self,
122
+ contact: &[String],
123
+ directory_url: &str,
124
+ account: &[u8],
125
+ ) -> Result<(), Self::EA> {
126
+ self.lock_exclusive()?;
127
+
128
+ self.inner
129
+ .store_account(contact, directory_url, account)
130
+ .await
131
+ }
132
+ }
@@ -1,8 +1,12 @@
1
1
  use base64::{engine::general_purpose, Engine as _};
2
2
  use itsi_error::Result;
3
3
  use itsi_tracing::info;
4
+ use locked_dir_cache::LockedDirCache;
4
5
  use rcgen::{CertificateParams, DnType, KeyPair, SanType};
5
- use rustls::pki_types::{CertificateDer, PrivateKeyDer};
6
+ use rustls::{
7
+ pki_types::{CertificateDer, PrivateKeyDer},
8
+ ClientConfig, RootCertStore,
9
+ };
6
10
  use rustls_pemfile::{certs, pkcs8_private_keys};
7
11
  use std::{
8
12
  collections::HashMap,
@@ -12,8 +16,8 @@ use std::{
12
16
  };
13
17
  use tokio::sync::Mutex;
14
18
  use tokio_rustls::{rustls::ServerConfig, TlsAcceptor};
15
- use tokio_rustls_acme::{caches::DirCache, AcmeAcceptor, AcmeConfig, AcmeState};
16
-
19
+ use tokio_rustls_acme::{AcmeAcceptor, AcmeConfig, AcmeState};
20
+ mod locked_dir_cache;
17
21
  const ITS_CA_CERT: &str = include_str!("./itsi_ca/itsi_ca.crt");
18
22
  const ITS_CA_KEY: &str = include_str!("./itsi_ca/itsi_ca.key");
19
23
 
@@ -48,10 +52,41 @@ pub fn configure_tls(
48
52
  domains = format!("{:?}", domains),
49
53
  directory_url, "Requesting acme cert"
50
54
  );
55
+
56
+ let mut root_cert_store = RootCertStore::empty();
57
+ if let Ok(ca_pem_path) = env::var("ITSI_ACME_CA_PEM_PATH") {
58
+ let ca_pem = fs::read(ca_pem_path).expect("failed to read CA pem file");
59
+ let mut ca_reader = BufReader::new(&ca_pem[..]);
60
+ let der_certs: Vec<CertificateDer> = certs(&mut ca_reader)
61
+ .collect::<std::result::Result<Vec<CertificateDer>, _>>()
62
+ .map_err(|e| {
63
+ itsi_error::ItsiError::ArgumentError(format!(
64
+ "Invalid ACME CA Pem path {:?}",
65
+ e
66
+ ))
67
+ })?;
68
+ root_cert_store.add_parsable_certificates(der_certs);
69
+ }
70
+
71
+ let client_config = ClientConfig::builder()
72
+ .with_root_certificates(root_cert_store)
73
+ .with_no_client_auth();
74
+
75
+ let contact_email = env::var("ITSI_ACME_CONTACT_EMAIL").map_err(|_| {
76
+ itsi_error::ItsiError::ArgumentError(
77
+ "ITSI_ACME_CONTACT_EMAIL must be set before you can auto-generate production certificates"
78
+ .to_string(),
79
+ )
80
+ })?;
81
+
82
+ let cache_dir = env::var("ITSI_ACME_CACHE_DIR")
83
+ .unwrap_or_else(|_| "./.rustls_acme_cache".to_string());
84
+
51
85
  let acme_state = AcmeConfig::new(domains)
52
- .contact(["mailto:wc@pico.net.nz"])
53
- .cache(DirCache::new("./rustls_acme_cache"))
86
+ .contact([format!("mailto:{}", contact_email)])
87
+ .cache(LockedDirCache::new(cache_dir))
54
88
  .directory(directory_url)
89
+ .client_tls_config(Arc::new(client_config))
55
90
  .state();
56
91
  let rustls_config = ServerConfig::builder()
57
92
  .with_no_client_auth()
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Server
5
- VERSION = "0.1.4"
5
+ VERSION = "0.1.6"
6
6
  end
7
7
  end
data/lib/itsi/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Itsi
2
- VERSION = "0.1.4"
2
+ VERSION = "0.1.6"
3
3
  end
data/tasks.txt CHANGED
@@ -1,5 +1,3 @@
1
- - Release v0.1.3
2
- - Autocert (Hard. And dev certs use local CA)
3
1
  - Test on MOD
4
2
  - Filters (Medium)
5
3
  - Static File Serve (Medium).error pages
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: itsi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wouter Coppieters
@@ -15,28 +15,28 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 0.1.4
18
+ version: 0.1.6
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: 0.1.4
25
+ version: 0.1.6
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: itsi-scheduler
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: 0.1.4
32
+ version: 0.1.6
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: 0.1.4
39
+ version: 0.1.6
40
40
  description: Wrapper Gem for both the Itsi server and it's Fiber scheduler
41
41
  email:
42
42
  - wc@pico.net.nz
@@ -91,6 +91,7 @@ files:
91
91
  - crates/itsi_server/src/server/signal.rs
92
92
  - crates/itsi_server/src/server/thread_worker.rs
93
93
  - crates/itsi_server/src/server/tls.rs
94
+ - crates/itsi_server/src/server/tls/locked_dir_cache.rs
94
95
  - crates/itsi_tracing/Cargo.lock
95
96
  - crates/itsi_tracing/Cargo.toml
96
97
  - crates/itsi_tracing/src/lib.rs
@@ -148,6 +149,7 @@ files:
148
149
  - gems/scheduler/ext/itsi_server/src/server/signal.rs
149
150
  - gems/scheduler/ext/itsi_server/src/server/thread_worker.rs
150
151
  - gems/scheduler/ext/itsi_server/src/server/tls.rs
152
+ - gems/scheduler/ext/itsi_server/src/server/tls/locked_dir_cache.rs
151
153
  - gems/scheduler/ext/itsi_tracing/Cargo.lock
152
154
  - gems/scheduler/ext/itsi_tracing/Cargo.toml
153
155
  - gems/scheduler/ext/itsi_tracing/src/lib.rs
@@ -218,6 +220,7 @@ files:
218
220
  - gems/server/ext/itsi_server/src/server/signal.rs
219
221
  - gems/server/ext/itsi_server/src/server/thread_worker.rs
220
222
  - gems/server/ext/itsi_server/src/server/tls.rs
223
+ - gems/server/ext/itsi_server/src/server/tls/locked_dir_cache.rs
221
224
  - gems/server/ext/itsi_tracing/Cargo.lock
222
225
  - gems/server/ext/itsi_tracing/Cargo.toml
223
226
  - gems/server/ext/itsi_tracing/src/lib.rs