itsi 0.1.6 → 0.1.7

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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +49 -0
  3. data/crates/itsi_error/src/lib.rs +1 -1
  4. data/crates/itsi_server/Cargo.toml +1 -0
  5. data/crates/itsi_server/src/env.rs +43 -0
  6. data/crates/itsi_server/src/lib.rs +1 -0
  7. data/crates/itsi_server/src/server/tls/locked_dir_cache.rs +3 -3
  8. data/crates/itsi_server/src/server/tls.rs +74 -39
  9. data/gems/scheduler/ext/itsi_error/src/lib.rs +1 -1
  10. data/gems/scheduler/ext/itsi_server/Cargo.toml +1 -0
  11. data/gems/scheduler/ext/itsi_server/src/env.rs +43 -0
  12. data/gems/scheduler/ext/itsi_server/src/lib.rs +1 -0
  13. data/gems/scheduler/ext/itsi_server/src/server/tls/locked_dir_cache.rs +3 -3
  14. data/gems/scheduler/ext/itsi_server/src/server/tls.rs +74 -39
  15. data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
  16. data/gems/server/ext/itsi_error/src/lib.rs +1 -1
  17. data/gems/server/ext/itsi_server/Cargo.toml +1 -0
  18. data/gems/server/ext/itsi_server/src/env.rs +43 -0
  19. data/gems/server/ext/itsi_server/src/lib.rs +1 -0
  20. data/gems/server/ext/itsi_server/src/server/tls/locked_dir_cache.rs +3 -3
  21. data/gems/server/ext/itsi_server/src/server/tls.rs +74 -39
  22. data/gems/server/lib/itsi/server/version.rb +1 -1
  23. data/lib/itsi/version.rb +1 -1
  24. metadata +9 -12
  25. data/crates/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -13
  26. data/crates/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -5
  27. data/gems/scheduler/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -13
  28. data/gems/scheduler/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -5
  29. data/gems/server/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -13
  30. data/gems/server/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 43c8c15592c8e8e749938e5c5c4b5e481ba3ea88d82bac63059a612e35bf2213
4
- data.tar.gz: e57ab7eacc32f7a0ac702be2e15ac9a36d48aff27aaa3705f77c428df2acf0f8
3
+ metadata.gz: 88879d13925e0738ba948eb707096da6e8b039cb317ce0975333295550725c70
4
+ data.tar.gz: 0eaf0dc170f15efb83428313c7b2554f1e19fe8c9dc81a8c5c4d553a03b586cd
5
5
  SHA512:
6
- metadata.gz: 1ea20d0e8d9d82cb4b095674192ead9837334c5472761cce1dc093d43d829c2d71ecb74d06a667f4c8274607791bd1cd1ad12f005bf1bc8a3fa5179499a61d8f
7
- data.tar.gz: c1aa8a84ebd694f3f39d287fd67e320c69f89dd35315af3203e09259c500404825790e925abd8b9a6ee26d5a97638a1d7442a9bb369653398922a42a08742498
6
+ metadata.gz: e37c041ee042a7d7e69f4f577864e08890aa3bb9fe855c292fc95950c9fc682d5afa0e2e74fc4bd5fb9c5cb7f60733e6e5fcf250f205742c6caec32a23399c68
7
+ data.tar.gz: d7c9cdd9bcde2a58192798e601a8b5a3099a76b9e924d4b58e206c11a3f015b094a0d975f4f87fd5338e3cfd05f46cd3f4684f59fb140643be85e8452be84195
data/Cargo.lock CHANGED
@@ -398,6 +398,27 @@ dependencies = [
398
398
  "unicode-xid",
399
399
  ]
400
400
 
401
+ [[package]]
402
+ name = "dirs"
403
+ version = "6.0.0"
404
+ source = "registry+https://github.com/rust-lang/crates.io-index"
405
+ checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
406
+ dependencies = [
407
+ "dirs-sys",
408
+ ]
409
+
410
+ [[package]]
411
+ name = "dirs-sys"
412
+ version = "0.5.0"
413
+ source = "registry+https://github.com/rust-lang/crates.io-index"
414
+ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
415
+ dependencies = [
416
+ "libc",
417
+ "option-ext",
418
+ "redox_users",
419
+ "windows-sys 0.59.0",
420
+ ]
421
+
401
422
  [[package]]
402
423
  name = "displaydoc"
403
424
  version = "0.2.5"
@@ -989,6 +1010,7 @@ dependencies = [
989
1010
  "bytes",
990
1011
  "crossbeam",
991
1012
  "derive_more",
1013
+ "dirs",
992
1014
  "fs2",
993
1015
  "futures",
994
1016
  "http",
@@ -1106,6 +1128,16 @@ dependencies = [
1106
1128
  "windows-targets 0.52.6",
1107
1129
  ]
1108
1130
 
1131
+ [[package]]
1132
+ name = "libredox"
1133
+ version = "0.1.3"
1134
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1135
+ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
1136
+ dependencies = [
1137
+ "bitflags",
1138
+ "libc",
1139
+ ]
1140
+
1109
1141
  [[package]]
1110
1142
  name = "linux-raw-sys"
1111
1143
  version = "0.4.15"
@@ -1321,6 +1353,12 @@ version = "1.20.3"
1321
1353
  source = "registry+https://github.com/rust-lang/crates.io-index"
1322
1354
  checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
1323
1355
 
1356
+ [[package]]
1357
+ name = "option-ext"
1358
+ version = "0.2.0"
1359
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1360
+ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
1361
+
1324
1362
  [[package]]
1325
1363
  name = "overload"
1326
1364
  version = "0.1.1"
@@ -1608,6 +1646,17 @@ dependencies = [
1608
1646
  "bitflags",
1609
1647
  ]
1610
1648
 
1649
+ [[package]]
1650
+ name = "redox_users"
1651
+ version = "0.5.0"
1652
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1653
+ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
1654
+ dependencies = [
1655
+ "getrandom 0.2.15",
1656
+ "libredox",
1657
+ "thiserror 2.0.11",
1658
+ ]
1659
+
1611
1660
  [[package]]
1612
1661
  name = "regex"
1613
1662
  version = "1.11.1"
@@ -3,7 +3,7 @@ use thiserror::Error;
3
3
 
4
4
  pub type Result<T> = std::result::Result<T, ItsiError>;
5
5
 
6
- #[derive(Error, Debug)]
6
+ #[derive(Error, Debug, Clone)]
7
7
  pub enum ItsiError {
8
8
  #[error("Invalid input {0}")]
9
9
  InvalidInput(String),
@@ -44,3 +44,4 @@ rustls = "0.23.23"
44
44
  fs2 = "0.4.3"
45
45
  ring = "0.17.14"
46
46
  async-trait = "0.1.87"
47
+ dirs = "6.0.0"
@@ -0,0 +1,43 @@
1
+ use std::{
2
+ env::{var, VarError},
3
+ path::PathBuf,
4
+ sync::LazyLock,
5
+ };
6
+
7
+ type StringVar = LazyLock<String>;
8
+ type MaybeStringVar = LazyLock<Result<String, VarError>>;
9
+ type PathVar = LazyLock<PathBuf>;
10
+
11
+ /// ACME Configuration for auto-generating production certificates
12
+ /// *ITSI_ACME_CACHE_DIR* - Directory to store cached certificates
13
+ /// so that these are not regenerated every time the server starts
14
+ pub static ITSI_ACME_CACHE_DIR: StringVar = LazyLock::new(|| {
15
+ var("ITSI_ACME_CACHE_DIR").unwrap_or_else(|_| "./.rustls_acme_cache".to_string())
16
+ });
17
+
18
+ /// *ITSI_ACME_CONTACT_EMAIL* - Contact Email address to provide to ACME server during certificate renewal
19
+ pub static ITSI_ACME_CONTACT_EMAIL: MaybeStringVar =
20
+ LazyLock::new(|| var("ITSI_ACME_CONTACT_EMAIL"));
21
+
22
+ /// *ITSI_ACME_CA_PEM_PATH* - Optional CA Pem path, used for testing with non-trusted CAs for certifcate generation.
23
+ pub static ITSI_ACME_CA_PEM_PATH: MaybeStringVar = LazyLock::new(|| var("ITSI_ACME_CA_PEM_PATH"));
24
+
25
+ /// *ITSI_ACME_DIRECTORY_URL* - Directory URL to use for ACME certificate generation.
26
+ pub static ITSI_ACME_DIRECTORY_URL: StringVar = LazyLock::new(|| {
27
+ var("ITSI_ACME_DIRECTORY_URL")
28
+ .unwrap_or_else(|_| "https://acme-v02.api.letsencrypt.org/directory".to_string())
29
+ });
30
+
31
+ /// *ITSI_ACME_LOCK_FILE_NAME* - Name of the lock file used to prevent concurrent certificate generation.
32
+ pub static ITSI_ACME_LOCK_FILE_NAME: StringVar =
33
+ LazyLock::new(|| var("ITSI_ACME_LOCK_FILE_NAME").unwrap_or(".acme.lock".to_string()));
34
+
35
+ pub static ITSI_LOCAL_CA_DIR: PathVar = LazyLock::new(|| {
36
+ var("ITSI_LOCAL_CA_DIR")
37
+ .map(PathBuf::from)
38
+ .unwrap_or_else(|_| {
39
+ dirs::home_dir()
40
+ .expect("Failed to find HOME directory when initializing ITSI_LOCAL_CA_DIR")
41
+ .join(".itsi")
42
+ })
43
+ });
@@ -6,6 +6,7 @@ use server::{itsi_server::Server, signal::reset_signal_handlers};
6
6
  use tracing::*;
7
7
 
8
8
  pub mod body_proxy;
9
+ pub mod env;
9
10
  pub mod request;
10
11
  pub mod response;
11
12
  pub mod server;
@@ -1,13 +1,14 @@
1
1
  use async_trait::async_trait;
2
2
  use fs2::FileExt;
3
3
  use parking_lot::Mutex;
4
- use std::env;
5
4
  use std::fs::{self, OpenOptions};
6
5
  use std::io::Error as IoError;
7
6
  use std::path::{Path, PathBuf};
8
7
  use tokio_rustls_acme::caches::DirCache;
9
8
  use tokio_rustls_acme::{AccountCache, CertCache};
10
9
 
10
+ use crate::env::ITSI_ACME_LOCK_FILE_NAME;
11
+
11
12
  /// A wrapper around DirCache that locks a file before writing cert/account data.
12
13
  pub struct LockedDirCache<P: AsRef<Path> + Send + Sync> {
13
14
  inner: DirCache<P>,
@@ -19,8 +20,7 @@ impl<P: AsRef<Path> + Send + Sync> LockedDirCache<P> {
19
20
  pub fn new(dir: P) -> Self {
20
21
  let dir_path = dir.as_ref().to_path_buf();
21
22
  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()));
23
+ let lock_path = dir_path.join(&*ITSI_ACME_LOCK_FILE_NAME);
24
24
  Self::touch_file(&lock_path).expect("Failed to create lock file");
25
25
 
26
26
  Self {
@@ -2,7 +2,9 @@ use base64::{engine::general_purpose, Engine as _};
2
2
  use itsi_error::Result;
3
3
  use itsi_tracing::info;
4
4
  use locked_dir_cache::LockedDirCache;
5
- use rcgen::{CertificateParams, DnType, KeyPair, SanType};
5
+ use rcgen::{
6
+ generate_simple_self_signed, CertificateParams, CertifiedKey, DnType, KeyPair, SanType,
7
+ };
6
8
  use rustls::{
7
9
  pki_types::{CertificateDer, PrivateKeyDer},
8
10
  ClientConfig, RootCertStore,
@@ -10,16 +12,19 @@ use rustls::{
10
12
  use rustls_pemfile::{certs, pkcs8_private_keys};
11
13
  use std::{
12
14
  collections::HashMap,
13
- env, fs,
15
+ fs,
14
16
  io::{BufReader, Error},
15
17
  sync::Arc,
16
18
  };
17
19
  use tokio::sync::Mutex;
18
20
  use tokio_rustls::{rustls::ServerConfig, TlsAcceptor};
19
21
  use tokio_rustls_acme::{AcmeAcceptor, AcmeConfig, AcmeState};
22
+
23
+ use crate::env::{
24
+ ITSI_ACME_CACHE_DIR, ITSI_ACME_CA_PEM_PATH, ITSI_ACME_CONTACT_EMAIL, ITSI_ACME_DIRECTORY_URL,
25
+ ITSI_LOCAL_CA_DIR,
26
+ };
20
27
  mod locked_dir_cache;
21
- const ITS_CA_CERT: &str = include_str!("./itsi_ca/itsi_ca.crt");
22
- const ITS_CA_KEY: &str = include_str!("./itsi_ca/itsi_ca.key");
23
28
 
24
29
  #[derive(Clone)]
25
30
  pub enum ItsiTlsAcceptor {
@@ -31,11 +36,12 @@ pub enum ItsiTlsAcceptor {
31
36
  ),
32
37
  }
33
38
 
34
- // Generates a TLS configuration based on either :
35
- // * Input "cert" and "key" options (either paths or Base64-encoded strings) or
36
- // * Performs automatic certificate generation/retrieval. Generated certs use an internal self-signed Isti CA.
37
- // If a non-local host or optional domain parameter is provided,
38
- // an automated certificate will attempt to be fetched using let's encrypt.
39
+ /// Generates a TLS configuration based on either :
40
+ /// * Input "cert" and "key" options (either paths or Base64-encoded strings) or
41
+ /// * Performs automatic certificate generation/retrieval. Generated certs use an internal self-signed Isti CA.
42
+ ///
43
+ /// If a non-local host or optional domain parameter is provided,
44
+ /// an automated certificate will attempt to be fetched using let's encrypt.
39
45
  pub fn configure_tls(
40
46
  host: &str,
41
47
  query_params: &HashMap<String, String>,
@@ -44,17 +50,27 @@ pub fn configure_tls(
44
50
  .get("domains")
45
51
  .map(|v| v.split(',').map(String::from).collect::<Vec<_>>());
46
52
 
47
- if query_params.get("cert").is_none() || query_params.get("key").is_none() {
53
+ if query_params.get("cert").is_some_and(|c| c == "auto") {
48
54
  if let Some(domains) = domains {
49
- let directory_url = env::var("ACME_DIRECTORY_URL")
50
- .unwrap_or_else(|_| "https://acme-v02.api.letsencrypt.org/directory".to_string());
55
+ let directory_url = &*ITSI_ACME_DIRECTORY_URL;
51
56
  info!(
52
57
  domains = format!("{:?}", domains),
53
58
  directory_url, "Requesting acme cert"
54
59
  );
55
60
 
56
- let mut root_cert_store = RootCertStore::empty();
57
- if let Ok(ca_pem_path) = env::var("ITSI_ACME_CA_PEM_PATH") {
61
+ let acme_config = AcmeConfig::new(domains)
62
+ .contact([format!("mailto:{}", (*ITSI_ACME_CONTACT_EMAIL).as_ref().map_err(|_| {
63
+ itsi_error::ItsiError::ArgumentError(
64
+ "ITSI_ACME_CONTACT_EMAIL must be set before you can auto-generate production certificates"
65
+ .to_string(),
66
+ )
67
+ })?)])
68
+ .cache(LockedDirCache::new(&*ITSI_ACME_CACHE_DIR))
69
+ .directory(directory_url);
70
+
71
+ let acme_state = if let Ok(ca_pem_path) = &*ITSI_ACME_CA_PEM_PATH {
72
+ let mut root_cert_store = RootCertStore::empty();
73
+
58
74
  let ca_pem = fs::read(ca_pem_path).expect("failed to read CA pem file");
59
75
  let mut ca_reader = BufReader::new(&ca_pem[..]);
60
76
  let der_certs: Vec<CertificateDer> = certs(&mut ca_reader)
@@ -66,31 +82,23 @@ pub fn configure_tls(
66
82
  ))
67
83
  })?;
68
84
  root_cert_store.add_parsable_certificates(der_certs);
69
- }
70
85
 
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
-
85
- let acme_state = AcmeConfig::new(domains)
86
- .contact([format!("mailto:{}", contact_email)])
87
- .cache(LockedDirCache::new(cache_dir))
88
- .directory(directory_url)
89
- .client_tls_config(Arc::new(client_config))
90
- .state();
91
- let rustls_config = ServerConfig::builder()
86
+ let client_config = ClientConfig::builder()
87
+ .with_root_certificates(root_cert_store)
88
+ .with_no_client_auth();
89
+ acme_config
90
+ .client_tls_config(Arc::new(client_config))
91
+ .state()
92
+ } else {
93
+ acme_config.state()
94
+ };
95
+
96
+ let mut rustls_config = ServerConfig::builder()
92
97
  .with_no_client_auth()
93
98
  .with_cert_resolver(acme_state.resolver());
99
+
100
+ rustls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
101
+
94
102
  let acceptor = acme_state.acceptor();
95
103
  return Ok(ItsiTlsAcceptor::Automatic(
96
104
  acceptor,
@@ -107,7 +115,7 @@ pub fn configure_tls(
107
115
  let key = load_private_key(key_path);
108
116
  (certs, key)
109
117
  } else {
110
- generate_ca_signed_cert(vec![host.to_owned()])?
118
+ generate_ca_signed_cert(domains.unwrap_or(vec![host.to_owned()]))?
111
119
  };
112
120
 
113
121
  let mut config = ServerConfig::builder()
@@ -179,9 +187,10 @@ pub fn generate_ca_signed_cert(
179
187
  domains: Vec<String>,
180
188
  ) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)> {
181
189
  info!("Generating New Itsi CA - Self signed Certificate. Use `itsi ca export` to export the CA certificate for import into your local trust store.");
190
+ let (ca_key_pem, ca_cert_pem) = get_or_create_local_dev_ca()?;
182
191
 
183
- let ca_kp = KeyPair::from_pem(ITS_CA_KEY).expect("Failed to load embedded CA key");
184
- let ca_cert = CertificateParams::from_ca_cert_pem(ITS_CA_CERT)
192
+ let ca_kp = KeyPair::from_pem(&ca_key_pem).expect("Failed to load CA key");
193
+ let ca_cert = CertificateParams::from_ca_cert_pem(&ca_cert_pem)
185
194
  .expect("Failed to parse embedded CA certificate")
186
195
  .self_signed(&ca_kp)
187
196
  .expect("Failed to self-sign embedded CA cert");
@@ -221,3 +230,29 @@ pub fn generate_ca_signed_cert(
221
230
  PrivateKeyDer::try_from(ee_key.serialize_der()).unwrap(),
222
231
  ))
223
232
  }
233
+
234
+ fn get_or_create_local_dev_ca() -> Result<(String, String)> {
235
+ let ca_dir = &*ITSI_LOCAL_CA_DIR;
236
+ fs::create_dir_all(ca_dir)?;
237
+
238
+ let key_path = ca_dir.join("itsi_dev_ca.key");
239
+ let cert_path = ca_dir.join("itsi_dev_ca.crt");
240
+
241
+ if key_path.exists() && cert_path.exists() {
242
+ // Already have a local CA
243
+ let key_pem = fs::read_to_string(&key_path)?;
244
+ let cert_pem = fs::read_to_string(&cert_path)?;
245
+
246
+ Ok((key_pem, cert_pem))
247
+ } else {
248
+ let subject_alt_names = vec!["dev.itsi.fyi".to_string(), "localhost".to_string()];
249
+
250
+ let CertifiedKey { cert, key_pair } =
251
+ generate_simple_self_signed(subject_alt_names).unwrap();
252
+
253
+ fs::write(&key_path, key_pair.serialize_pem())?;
254
+ fs::write(&cert_path, cert.pem())?;
255
+
256
+ Ok((key_pair.serialize_pem(), cert.pem()))
257
+ }
258
+ }
@@ -3,7 +3,7 @@ use thiserror::Error;
3
3
 
4
4
  pub type Result<T> = std::result::Result<T, ItsiError>;
5
5
 
6
- #[derive(Error, Debug)]
6
+ #[derive(Error, Debug, Clone)]
7
7
  pub enum ItsiError {
8
8
  #[error("Invalid input {0}")]
9
9
  InvalidInput(String),
@@ -44,3 +44,4 @@ rustls = "0.23.23"
44
44
  fs2 = "0.4.3"
45
45
  ring = "0.17.14"
46
46
  async-trait = "0.1.87"
47
+ dirs = "6.0.0"
@@ -0,0 +1,43 @@
1
+ use std::{
2
+ env::{var, VarError},
3
+ path::PathBuf,
4
+ sync::LazyLock,
5
+ };
6
+
7
+ type StringVar = LazyLock<String>;
8
+ type MaybeStringVar = LazyLock<Result<String, VarError>>;
9
+ type PathVar = LazyLock<PathBuf>;
10
+
11
+ /// ACME Configuration for auto-generating production certificates
12
+ /// *ITSI_ACME_CACHE_DIR* - Directory to store cached certificates
13
+ /// so that these are not regenerated every time the server starts
14
+ pub static ITSI_ACME_CACHE_DIR: StringVar = LazyLock::new(|| {
15
+ var("ITSI_ACME_CACHE_DIR").unwrap_or_else(|_| "./.rustls_acme_cache".to_string())
16
+ });
17
+
18
+ /// *ITSI_ACME_CONTACT_EMAIL* - Contact Email address to provide to ACME server during certificate renewal
19
+ pub static ITSI_ACME_CONTACT_EMAIL: MaybeStringVar =
20
+ LazyLock::new(|| var("ITSI_ACME_CONTACT_EMAIL"));
21
+
22
+ /// *ITSI_ACME_CA_PEM_PATH* - Optional CA Pem path, used for testing with non-trusted CAs for certifcate generation.
23
+ pub static ITSI_ACME_CA_PEM_PATH: MaybeStringVar = LazyLock::new(|| var("ITSI_ACME_CA_PEM_PATH"));
24
+
25
+ /// *ITSI_ACME_DIRECTORY_URL* - Directory URL to use for ACME certificate generation.
26
+ pub static ITSI_ACME_DIRECTORY_URL: StringVar = LazyLock::new(|| {
27
+ var("ITSI_ACME_DIRECTORY_URL")
28
+ .unwrap_or_else(|_| "https://acme-v02.api.letsencrypt.org/directory".to_string())
29
+ });
30
+
31
+ /// *ITSI_ACME_LOCK_FILE_NAME* - Name of the lock file used to prevent concurrent certificate generation.
32
+ pub static ITSI_ACME_LOCK_FILE_NAME: StringVar =
33
+ LazyLock::new(|| var("ITSI_ACME_LOCK_FILE_NAME").unwrap_or(".acme.lock".to_string()));
34
+
35
+ pub static ITSI_LOCAL_CA_DIR: PathVar = LazyLock::new(|| {
36
+ var("ITSI_LOCAL_CA_DIR")
37
+ .map(PathBuf::from)
38
+ .unwrap_or_else(|_| {
39
+ dirs::home_dir()
40
+ .expect("Failed to find HOME directory when initializing ITSI_LOCAL_CA_DIR")
41
+ .join(".itsi")
42
+ })
43
+ });
@@ -6,6 +6,7 @@ use server::{itsi_server::Server, signal::reset_signal_handlers};
6
6
  use tracing::*;
7
7
 
8
8
  pub mod body_proxy;
9
+ pub mod env;
9
10
  pub mod request;
10
11
  pub mod response;
11
12
  pub mod server;
@@ -1,13 +1,14 @@
1
1
  use async_trait::async_trait;
2
2
  use fs2::FileExt;
3
3
  use parking_lot::Mutex;
4
- use std::env;
5
4
  use std::fs::{self, OpenOptions};
6
5
  use std::io::Error as IoError;
7
6
  use std::path::{Path, PathBuf};
8
7
  use tokio_rustls_acme::caches::DirCache;
9
8
  use tokio_rustls_acme::{AccountCache, CertCache};
10
9
 
10
+ use crate::env::ITSI_ACME_LOCK_FILE_NAME;
11
+
11
12
  /// A wrapper around DirCache that locks a file before writing cert/account data.
12
13
  pub struct LockedDirCache<P: AsRef<Path> + Send + Sync> {
13
14
  inner: DirCache<P>,
@@ -19,8 +20,7 @@ impl<P: AsRef<Path> + Send + Sync> LockedDirCache<P> {
19
20
  pub fn new(dir: P) -> Self {
20
21
  let dir_path = dir.as_ref().to_path_buf();
21
22
  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()));
23
+ let lock_path = dir_path.join(&*ITSI_ACME_LOCK_FILE_NAME);
24
24
  Self::touch_file(&lock_path).expect("Failed to create lock file");
25
25
 
26
26
  Self {
@@ -2,7 +2,9 @@ use base64::{engine::general_purpose, Engine as _};
2
2
  use itsi_error::Result;
3
3
  use itsi_tracing::info;
4
4
  use locked_dir_cache::LockedDirCache;
5
- use rcgen::{CertificateParams, DnType, KeyPair, SanType};
5
+ use rcgen::{
6
+ generate_simple_self_signed, CertificateParams, CertifiedKey, DnType, KeyPair, SanType,
7
+ };
6
8
  use rustls::{
7
9
  pki_types::{CertificateDer, PrivateKeyDer},
8
10
  ClientConfig, RootCertStore,
@@ -10,16 +12,19 @@ use rustls::{
10
12
  use rustls_pemfile::{certs, pkcs8_private_keys};
11
13
  use std::{
12
14
  collections::HashMap,
13
- env, fs,
15
+ fs,
14
16
  io::{BufReader, Error},
15
17
  sync::Arc,
16
18
  };
17
19
  use tokio::sync::Mutex;
18
20
  use tokio_rustls::{rustls::ServerConfig, TlsAcceptor};
19
21
  use tokio_rustls_acme::{AcmeAcceptor, AcmeConfig, AcmeState};
22
+
23
+ use crate::env::{
24
+ ITSI_ACME_CACHE_DIR, ITSI_ACME_CA_PEM_PATH, ITSI_ACME_CONTACT_EMAIL, ITSI_ACME_DIRECTORY_URL,
25
+ ITSI_LOCAL_CA_DIR,
26
+ };
20
27
  mod locked_dir_cache;
21
- const ITS_CA_CERT: &str = include_str!("./itsi_ca/itsi_ca.crt");
22
- const ITS_CA_KEY: &str = include_str!("./itsi_ca/itsi_ca.key");
23
28
 
24
29
  #[derive(Clone)]
25
30
  pub enum ItsiTlsAcceptor {
@@ -31,11 +36,12 @@ pub enum ItsiTlsAcceptor {
31
36
  ),
32
37
  }
33
38
 
34
- // Generates a TLS configuration based on either :
35
- // * Input "cert" and "key" options (either paths or Base64-encoded strings) or
36
- // * Performs automatic certificate generation/retrieval. Generated certs use an internal self-signed Isti CA.
37
- // If a non-local host or optional domain parameter is provided,
38
- // an automated certificate will attempt to be fetched using let's encrypt.
39
+ /// Generates a TLS configuration based on either :
40
+ /// * Input "cert" and "key" options (either paths or Base64-encoded strings) or
41
+ /// * Performs automatic certificate generation/retrieval. Generated certs use an internal self-signed Isti CA.
42
+ ///
43
+ /// If a non-local host or optional domain parameter is provided,
44
+ /// an automated certificate will attempt to be fetched using let's encrypt.
39
45
  pub fn configure_tls(
40
46
  host: &str,
41
47
  query_params: &HashMap<String, String>,
@@ -44,17 +50,27 @@ pub fn configure_tls(
44
50
  .get("domains")
45
51
  .map(|v| v.split(',').map(String::from).collect::<Vec<_>>());
46
52
 
47
- if query_params.get("cert").is_none() || query_params.get("key").is_none() {
53
+ if query_params.get("cert").is_some_and(|c| c == "auto") {
48
54
  if let Some(domains) = domains {
49
- let directory_url = env::var("ACME_DIRECTORY_URL")
50
- .unwrap_or_else(|_| "https://acme-v02.api.letsencrypt.org/directory".to_string());
55
+ let directory_url = &*ITSI_ACME_DIRECTORY_URL;
51
56
  info!(
52
57
  domains = format!("{:?}", domains),
53
58
  directory_url, "Requesting acme cert"
54
59
  );
55
60
 
56
- let mut root_cert_store = RootCertStore::empty();
57
- if let Ok(ca_pem_path) = env::var("ITSI_ACME_CA_PEM_PATH") {
61
+ let acme_config = AcmeConfig::new(domains)
62
+ .contact([format!("mailto:{}", (*ITSI_ACME_CONTACT_EMAIL).as_ref().map_err(|_| {
63
+ itsi_error::ItsiError::ArgumentError(
64
+ "ITSI_ACME_CONTACT_EMAIL must be set before you can auto-generate production certificates"
65
+ .to_string(),
66
+ )
67
+ })?)])
68
+ .cache(LockedDirCache::new(&*ITSI_ACME_CACHE_DIR))
69
+ .directory(directory_url);
70
+
71
+ let acme_state = if let Ok(ca_pem_path) = &*ITSI_ACME_CA_PEM_PATH {
72
+ let mut root_cert_store = RootCertStore::empty();
73
+
58
74
  let ca_pem = fs::read(ca_pem_path).expect("failed to read CA pem file");
59
75
  let mut ca_reader = BufReader::new(&ca_pem[..]);
60
76
  let der_certs: Vec<CertificateDer> = certs(&mut ca_reader)
@@ -66,31 +82,23 @@ pub fn configure_tls(
66
82
  ))
67
83
  })?;
68
84
  root_cert_store.add_parsable_certificates(der_certs);
69
- }
70
85
 
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
-
85
- let acme_state = AcmeConfig::new(domains)
86
- .contact([format!("mailto:{}", contact_email)])
87
- .cache(LockedDirCache::new(cache_dir))
88
- .directory(directory_url)
89
- .client_tls_config(Arc::new(client_config))
90
- .state();
91
- let rustls_config = ServerConfig::builder()
86
+ let client_config = ClientConfig::builder()
87
+ .with_root_certificates(root_cert_store)
88
+ .with_no_client_auth();
89
+ acme_config
90
+ .client_tls_config(Arc::new(client_config))
91
+ .state()
92
+ } else {
93
+ acme_config.state()
94
+ };
95
+
96
+ let mut rustls_config = ServerConfig::builder()
92
97
  .with_no_client_auth()
93
98
  .with_cert_resolver(acme_state.resolver());
99
+
100
+ rustls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
101
+
94
102
  let acceptor = acme_state.acceptor();
95
103
  return Ok(ItsiTlsAcceptor::Automatic(
96
104
  acceptor,
@@ -107,7 +115,7 @@ pub fn configure_tls(
107
115
  let key = load_private_key(key_path);
108
116
  (certs, key)
109
117
  } else {
110
- generate_ca_signed_cert(vec![host.to_owned()])?
118
+ generate_ca_signed_cert(domains.unwrap_or(vec![host.to_owned()]))?
111
119
  };
112
120
 
113
121
  let mut config = ServerConfig::builder()
@@ -179,9 +187,10 @@ pub fn generate_ca_signed_cert(
179
187
  domains: Vec<String>,
180
188
  ) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)> {
181
189
  info!("Generating New Itsi CA - Self signed Certificate. Use `itsi ca export` to export the CA certificate for import into your local trust store.");
190
+ let (ca_key_pem, ca_cert_pem) = get_or_create_local_dev_ca()?;
182
191
 
183
- let ca_kp = KeyPair::from_pem(ITS_CA_KEY).expect("Failed to load embedded CA key");
184
- let ca_cert = CertificateParams::from_ca_cert_pem(ITS_CA_CERT)
192
+ let ca_kp = KeyPair::from_pem(&ca_key_pem).expect("Failed to load CA key");
193
+ let ca_cert = CertificateParams::from_ca_cert_pem(&ca_cert_pem)
185
194
  .expect("Failed to parse embedded CA certificate")
186
195
  .self_signed(&ca_kp)
187
196
  .expect("Failed to self-sign embedded CA cert");
@@ -221,3 +230,29 @@ pub fn generate_ca_signed_cert(
221
230
  PrivateKeyDer::try_from(ee_key.serialize_der()).unwrap(),
222
231
  ))
223
232
  }
233
+
234
+ fn get_or_create_local_dev_ca() -> Result<(String, String)> {
235
+ let ca_dir = &*ITSI_LOCAL_CA_DIR;
236
+ fs::create_dir_all(ca_dir)?;
237
+
238
+ let key_path = ca_dir.join("itsi_dev_ca.key");
239
+ let cert_path = ca_dir.join("itsi_dev_ca.crt");
240
+
241
+ if key_path.exists() && cert_path.exists() {
242
+ // Already have a local CA
243
+ let key_pem = fs::read_to_string(&key_path)?;
244
+ let cert_pem = fs::read_to_string(&cert_path)?;
245
+
246
+ Ok((key_pem, cert_pem))
247
+ } else {
248
+ let subject_alt_names = vec!["dev.itsi.fyi".to_string(), "localhost".to_string()];
249
+
250
+ let CertifiedKey { cert, key_pair } =
251
+ generate_simple_self_signed(subject_alt_names).unwrap();
252
+
253
+ fs::write(&key_path, key_pair.serialize_pem())?;
254
+ fs::write(&cert_path, cert.pem())?;
255
+
256
+ Ok((key_pair.serialize_pem(), cert.pem()))
257
+ }
258
+ }
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Scheduler
5
- VERSION = "0.1.6"
5
+ VERSION = "0.1.7"
6
6
  end
7
7
  end
@@ -3,7 +3,7 @@ use thiserror::Error;
3
3
 
4
4
  pub type Result<T> = std::result::Result<T, ItsiError>;
5
5
 
6
- #[derive(Error, Debug)]
6
+ #[derive(Error, Debug, Clone)]
7
7
  pub enum ItsiError {
8
8
  #[error("Invalid input {0}")]
9
9
  InvalidInput(String),
@@ -44,3 +44,4 @@ rustls = "0.23.23"
44
44
  fs2 = "0.4.3"
45
45
  ring = "0.17.14"
46
46
  async-trait = "0.1.87"
47
+ dirs = "6.0.0"
@@ -0,0 +1,43 @@
1
+ use std::{
2
+ env::{var, VarError},
3
+ path::PathBuf,
4
+ sync::LazyLock,
5
+ };
6
+
7
+ type StringVar = LazyLock<String>;
8
+ type MaybeStringVar = LazyLock<Result<String, VarError>>;
9
+ type PathVar = LazyLock<PathBuf>;
10
+
11
+ /// ACME Configuration for auto-generating production certificates
12
+ /// *ITSI_ACME_CACHE_DIR* - Directory to store cached certificates
13
+ /// so that these are not regenerated every time the server starts
14
+ pub static ITSI_ACME_CACHE_DIR: StringVar = LazyLock::new(|| {
15
+ var("ITSI_ACME_CACHE_DIR").unwrap_or_else(|_| "./.rustls_acme_cache".to_string())
16
+ });
17
+
18
+ /// *ITSI_ACME_CONTACT_EMAIL* - Contact Email address to provide to ACME server during certificate renewal
19
+ pub static ITSI_ACME_CONTACT_EMAIL: MaybeStringVar =
20
+ LazyLock::new(|| var("ITSI_ACME_CONTACT_EMAIL"));
21
+
22
+ /// *ITSI_ACME_CA_PEM_PATH* - Optional CA Pem path, used for testing with non-trusted CAs for certifcate generation.
23
+ pub static ITSI_ACME_CA_PEM_PATH: MaybeStringVar = LazyLock::new(|| var("ITSI_ACME_CA_PEM_PATH"));
24
+
25
+ /// *ITSI_ACME_DIRECTORY_URL* - Directory URL to use for ACME certificate generation.
26
+ pub static ITSI_ACME_DIRECTORY_URL: StringVar = LazyLock::new(|| {
27
+ var("ITSI_ACME_DIRECTORY_URL")
28
+ .unwrap_or_else(|_| "https://acme-v02.api.letsencrypt.org/directory".to_string())
29
+ });
30
+
31
+ /// *ITSI_ACME_LOCK_FILE_NAME* - Name of the lock file used to prevent concurrent certificate generation.
32
+ pub static ITSI_ACME_LOCK_FILE_NAME: StringVar =
33
+ LazyLock::new(|| var("ITSI_ACME_LOCK_FILE_NAME").unwrap_or(".acme.lock".to_string()));
34
+
35
+ pub static ITSI_LOCAL_CA_DIR: PathVar = LazyLock::new(|| {
36
+ var("ITSI_LOCAL_CA_DIR")
37
+ .map(PathBuf::from)
38
+ .unwrap_or_else(|_| {
39
+ dirs::home_dir()
40
+ .expect("Failed to find HOME directory when initializing ITSI_LOCAL_CA_DIR")
41
+ .join(".itsi")
42
+ })
43
+ });
@@ -6,6 +6,7 @@ use server::{itsi_server::Server, signal::reset_signal_handlers};
6
6
  use tracing::*;
7
7
 
8
8
  pub mod body_proxy;
9
+ pub mod env;
9
10
  pub mod request;
10
11
  pub mod response;
11
12
  pub mod server;
@@ -1,13 +1,14 @@
1
1
  use async_trait::async_trait;
2
2
  use fs2::FileExt;
3
3
  use parking_lot::Mutex;
4
- use std::env;
5
4
  use std::fs::{self, OpenOptions};
6
5
  use std::io::Error as IoError;
7
6
  use std::path::{Path, PathBuf};
8
7
  use tokio_rustls_acme::caches::DirCache;
9
8
  use tokio_rustls_acme::{AccountCache, CertCache};
10
9
 
10
+ use crate::env::ITSI_ACME_LOCK_FILE_NAME;
11
+
11
12
  /// A wrapper around DirCache that locks a file before writing cert/account data.
12
13
  pub struct LockedDirCache<P: AsRef<Path> + Send + Sync> {
13
14
  inner: DirCache<P>,
@@ -19,8 +20,7 @@ impl<P: AsRef<Path> + Send + Sync> LockedDirCache<P> {
19
20
  pub fn new(dir: P) -> Self {
20
21
  let dir_path = dir.as_ref().to_path_buf();
21
22
  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()));
23
+ let lock_path = dir_path.join(&*ITSI_ACME_LOCK_FILE_NAME);
24
24
  Self::touch_file(&lock_path).expect("Failed to create lock file");
25
25
 
26
26
  Self {
@@ -2,7 +2,9 @@ use base64::{engine::general_purpose, Engine as _};
2
2
  use itsi_error::Result;
3
3
  use itsi_tracing::info;
4
4
  use locked_dir_cache::LockedDirCache;
5
- use rcgen::{CertificateParams, DnType, KeyPair, SanType};
5
+ use rcgen::{
6
+ generate_simple_self_signed, CertificateParams, CertifiedKey, DnType, KeyPair, SanType,
7
+ };
6
8
  use rustls::{
7
9
  pki_types::{CertificateDer, PrivateKeyDer},
8
10
  ClientConfig, RootCertStore,
@@ -10,16 +12,19 @@ use rustls::{
10
12
  use rustls_pemfile::{certs, pkcs8_private_keys};
11
13
  use std::{
12
14
  collections::HashMap,
13
- env, fs,
15
+ fs,
14
16
  io::{BufReader, Error},
15
17
  sync::Arc,
16
18
  };
17
19
  use tokio::sync::Mutex;
18
20
  use tokio_rustls::{rustls::ServerConfig, TlsAcceptor};
19
21
  use tokio_rustls_acme::{AcmeAcceptor, AcmeConfig, AcmeState};
22
+
23
+ use crate::env::{
24
+ ITSI_ACME_CACHE_DIR, ITSI_ACME_CA_PEM_PATH, ITSI_ACME_CONTACT_EMAIL, ITSI_ACME_DIRECTORY_URL,
25
+ ITSI_LOCAL_CA_DIR,
26
+ };
20
27
  mod locked_dir_cache;
21
- const ITS_CA_CERT: &str = include_str!("./itsi_ca/itsi_ca.crt");
22
- const ITS_CA_KEY: &str = include_str!("./itsi_ca/itsi_ca.key");
23
28
 
24
29
  #[derive(Clone)]
25
30
  pub enum ItsiTlsAcceptor {
@@ -31,11 +36,12 @@ pub enum ItsiTlsAcceptor {
31
36
  ),
32
37
  }
33
38
 
34
- // Generates a TLS configuration based on either :
35
- // * Input "cert" and "key" options (either paths or Base64-encoded strings) or
36
- // * Performs automatic certificate generation/retrieval. Generated certs use an internal self-signed Isti CA.
37
- // If a non-local host or optional domain parameter is provided,
38
- // an automated certificate will attempt to be fetched using let's encrypt.
39
+ /// Generates a TLS configuration based on either :
40
+ /// * Input "cert" and "key" options (either paths or Base64-encoded strings) or
41
+ /// * Performs automatic certificate generation/retrieval. Generated certs use an internal self-signed Isti CA.
42
+ ///
43
+ /// If a non-local host or optional domain parameter is provided,
44
+ /// an automated certificate will attempt to be fetched using let's encrypt.
39
45
  pub fn configure_tls(
40
46
  host: &str,
41
47
  query_params: &HashMap<String, String>,
@@ -44,17 +50,27 @@ pub fn configure_tls(
44
50
  .get("domains")
45
51
  .map(|v| v.split(',').map(String::from).collect::<Vec<_>>());
46
52
 
47
- if query_params.get("cert").is_none() || query_params.get("key").is_none() {
53
+ if query_params.get("cert").is_some_and(|c| c == "auto") {
48
54
  if let Some(domains) = domains {
49
- let directory_url = env::var("ACME_DIRECTORY_URL")
50
- .unwrap_or_else(|_| "https://acme-v02.api.letsencrypt.org/directory".to_string());
55
+ let directory_url = &*ITSI_ACME_DIRECTORY_URL;
51
56
  info!(
52
57
  domains = format!("{:?}", domains),
53
58
  directory_url, "Requesting acme cert"
54
59
  );
55
60
 
56
- let mut root_cert_store = RootCertStore::empty();
57
- if let Ok(ca_pem_path) = env::var("ITSI_ACME_CA_PEM_PATH") {
61
+ let acme_config = AcmeConfig::new(domains)
62
+ .contact([format!("mailto:{}", (*ITSI_ACME_CONTACT_EMAIL).as_ref().map_err(|_| {
63
+ itsi_error::ItsiError::ArgumentError(
64
+ "ITSI_ACME_CONTACT_EMAIL must be set before you can auto-generate production certificates"
65
+ .to_string(),
66
+ )
67
+ })?)])
68
+ .cache(LockedDirCache::new(&*ITSI_ACME_CACHE_DIR))
69
+ .directory(directory_url);
70
+
71
+ let acme_state = if let Ok(ca_pem_path) = &*ITSI_ACME_CA_PEM_PATH {
72
+ let mut root_cert_store = RootCertStore::empty();
73
+
58
74
  let ca_pem = fs::read(ca_pem_path).expect("failed to read CA pem file");
59
75
  let mut ca_reader = BufReader::new(&ca_pem[..]);
60
76
  let der_certs: Vec<CertificateDer> = certs(&mut ca_reader)
@@ -66,31 +82,23 @@ pub fn configure_tls(
66
82
  ))
67
83
  })?;
68
84
  root_cert_store.add_parsable_certificates(der_certs);
69
- }
70
85
 
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
-
85
- let acme_state = AcmeConfig::new(domains)
86
- .contact([format!("mailto:{}", contact_email)])
87
- .cache(LockedDirCache::new(cache_dir))
88
- .directory(directory_url)
89
- .client_tls_config(Arc::new(client_config))
90
- .state();
91
- let rustls_config = ServerConfig::builder()
86
+ let client_config = ClientConfig::builder()
87
+ .with_root_certificates(root_cert_store)
88
+ .with_no_client_auth();
89
+ acme_config
90
+ .client_tls_config(Arc::new(client_config))
91
+ .state()
92
+ } else {
93
+ acme_config.state()
94
+ };
95
+
96
+ let mut rustls_config = ServerConfig::builder()
92
97
  .with_no_client_auth()
93
98
  .with_cert_resolver(acme_state.resolver());
99
+
100
+ rustls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
101
+
94
102
  let acceptor = acme_state.acceptor();
95
103
  return Ok(ItsiTlsAcceptor::Automatic(
96
104
  acceptor,
@@ -107,7 +115,7 @@ pub fn configure_tls(
107
115
  let key = load_private_key(key_path);
108
116
  (certs, key)
109
117
  } else {
110
- generate_ca_signed_cert(vec![host.to_owned()])?
118
+ generate_ca_signed_cert(domains.unwrap_or(vec![host.to_owned()]))?
111
119
  };
112
120
 
113
121
  let mut config = ServerConfig::builder()
@@ -179,9 +187,10 @@ pub fn generate_ca_signed_cert(
179
187
  domains: Vec<String>,
180
188
  ) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)> {
181
189
  info!("Generating New Itsi CA - Self signed Certificate. Use `itsi ca export` to export the CA certificate for import into your local trust store.");
190
+ let (ca_key_pem, ca_cert_pem) = get_or_create_local_dev_ca()?;
182
191
 
183
- let ca_kp = KeyPair::from_pem(ITS_CA_KEY).expect("Failed to load embedded CA key");
184
- let ca_cert = CertificateParams::from_ca_cert_pem(ITS_CA_CERT)
192
+ let ca_kp = KeyPair::from_pem(&ca_key_pem).expect("Failed to load CA key");
193
+ let ca_cert = CertificateParams::from_ca_cert_pem(&ca_cert_pem)
185
194
  .expect("Failed to parse embedded CA certificate")
186
195
  .self_signed(&ca_kp)
187
196
  .expect("Failed to self-sign embedded CA cert");
@@ -221,3 +230,29 @@ pub fn generate_ca_signed_cert(
221
230
  PrivateKeyDer::try_from(ee_key.serialize_der()).unwrap(),
222
231
  ))
223
232
  }
233
+
234
+ fn get_or_create_local_dev_ca() -> Result<(String, String)> {
235
+ let ca_dir = &*ITSI_LOCAL_CA_DIR;
236
+ fs::create_dir_all(ca_dir)?;
237
+
238
+ let key_path = ca_dir.join("itsi_dev_ca.key");
239
+ let cert_path = ca_dir.join("itsi_dev_ca.crt");
240
+
241
+ if key_path.exists() && cert_path.exists() {
242
+ // Already have a local CA
243
+ let key_pem = fs::read_to_string(&key_path)?;
244
+ let cert_pem = fs::read_to_string(&cert_path)?;
245
+
246
+ Ok((key_pem, cert_pem))
247
+ } else {
248
+ let subject_alt_names = vec!["dev.itsi.fyi".to_string(), "localhost".to_string()];
249
+
250
+ let CertifiedKey { cert, key_pair } =
251
+ generate_simple_self_signed(subject_alt_names).unwrap();
252
+
253
+ fs::write(&key_path, key_pair.serialize_pem())?;
254
+ fs::write(&cert_path, cert.pem())?;
255
+
256
+ Ok((key_pair.serialize_pem(), cert.pem()))
257
+ }
258
+ }
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Server
5
- VERSION = "0.1.6"
5
+ VERSION = "0.1.7"
6
6
  end
7
7
  end
data/lib/itsi/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Itsi
2
- VERSION = "0.1.6"
2
+ VERSION = "0.1.7"
3
3
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: itsi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wouter Coppieters
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-03-14 00:00:00.000000000 Z
10
+ date: 2025-03-15 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: itsi-server
@@ -15,28 +15,28 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 0.1.6
18
+ version: 0.1.7
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.6
25
+ version: 0.1.7
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.6
32
+ version: 0.1.7
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.6
39
+ version: 0.1.7
40
40
  description: Wrapper Gem for both the Itsi server and it's Fiber scheduler
41
41
  email:
42
42
  - wc@pico.net.nz
@@ -70,6 +70,7 @@ files:
70
70
  - crates/itsi_server/src/body_proxy/big_bytes.rs
71
71
  - crates/itsi_server/src/body_proxy/itsi_body_proxy.rs
72
72
  - crates/itsi_server/src/body_proxy/mod.rs
73
+ - crates/itsi_server/src/env.rs
73
74
  - crates/itsi_server/src/lib.rs
74
75
  - crates/itsi_server/src/request/itsi_request.rs
75
76
  - crates/itsi_server/src/request/mod.rs
@@ -78,8 +79,6 @@ files:
78
79
  - crates/itsi_server/src/server/bind.rs
79
80
  - crates/itsi_server/src/server/bind_protocol.rs
80
81
  - crates/itsi_server/src/server/io_stream.rs
81
- - crates/itsi_server/src/server/itsi_ca/itsi_ca.crt
82
- - crates/itsi_server/src/server/itsi_ca/itsi_ca.key
83
82
  - crates/itsi_server/src/server/itsi_server.rs
84
83
  - crates/itsi_server/src/server/lifecycle_event.rs
85
84
  - crates/itsi_server/src/server/listener.rs
@@ -128,6 +127,7 @@ files:
128
127
  - gems/scheduler/ext/itsi_server/src/body_proxy/big_bytes.rs
129
128
  - gems/scheduler/ext/itsi_server/src/body_proxy/itsi_body_proxy.rs
130
129
  - gems/scheduler/ext/itsi_server/src/body_proxy/mod.rs
130
+ - gems/scheduler/ext/itsi_server/src/env.rs
131
131
  - gems/scheduler/ext/itsi_server/src/lib.rs
132
132
  - gems/scheduler/ext/itsi_server/src/request/itsi_request.rs
133
133
  - gems/scheduler/ext/itsi_server/src/request/mod.rs
@@ -136,8 +136,6 @@ files:
136
136
  - gems/scheduler/ext/itsi_server/src/server/bind.rs
137
137
  - gems/scheduler/ext/itsi_server/src/server/bind_protocol.rs
138
138
  - gems/scheduler/ext/itsi_server/src/server/io_stream.rs
139
- - gems/scheduler/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt
140
- - gems/scheduler/ext/itsi_server/src/server/itsi_ca/itsi_ca.key
141
139
  - gems/scheduler/ext/itsi_server/src/server/itsi_server.rs
142
140
  - gems/scheduler/ext/itsi_server/src/server/lifecycle_event.rs
143
141
  - gems/scheduler/ext/itsi_server/src/server/listener.rs
@@ -199,6 +197,7 @@ files:
199
197
  - gems/server/ext/itsi_server/src/body_proxy/big_bytes.rs
200
198
  - gems/server/ext/itsi_server/src/body_proxy/itsi_body_proxy.rs
201
199
  - gems/server/ext/itsi_server/src/body_proxy/mod.rs
200
+ - gems/server/ext/itsi_server/src/env.rs
202
201
  - gems/server/ext/itsi_server/src/lib.rs
203
202
  - gems/server/ext/itsi_server/src/request/itsi_request.rs
204
203
  - gems/server/ext/itsi_server/src/request/mod.rs
@@ -207,8 +206,6 @@ files:
207
206
  - gems/server/ext/itsi_server/src/server/bind.rs
208
207
  - gems/server/ext/itsi_server/src/server/bind_protocol.rs
209
208
  - gems/server/ext/itsi_server/src/server/io_stream.rs
210
- - gems/server/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt
211
- - gems/server/ext/itsi_server/src/server/itsi_ca/itsi_ca.key
212
209
  - gems/server/ext/itsi_server/src/server/itsi_server.rs
213
210
  - gems/server/ext/itsi_server/src/server/lifecycle_event.rs
214
211
  - gems/server/ext/itsi_server/src/server/listener.rs
@@ -1,13 +0,0 @@
1
- -----BEGIN CERTIFICATE-----
2
- MIIB9TCCAZugAwIBAgIUMpQtAScU2Ow9c1Xy/0b/kS/BuwcwCgYIKoZIzj0EAwIw
3
- UDELMAkGA1UEBhMCVVMxDTALBgNVBAgMBEl0c2kxDTALBgNVBAcMBEl0c2kxEDAO
4
- BgNVBAoMB0l0c2kgQ0ExETAPBgNVBAMMCGl0c2kuZnlpMB4XDTI1MDMwMzIwMjg1
5
- N1oXDTM1MDMwMTIwMjg1N1owUDELMAkGA1UEBhMCVVMxDTALBgNVBAgMBEl0c2kx
6
- DTALBgNVBAcMBEl0c2kxEDAOBgNVBAoMB0l0c2kgQ0ExETAPBgNVBAMMCGl0c2ku
7
- ZnlpMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqGdC9Vi1r7ARvqSkPXkAgiV5
8
- gn2MMTeEafagrWT7G1onSh/G+Qstxl61kfFNLOTiy6NSgAtKG+gfveCTo0Pcz6NT
9
- MFEwHQYDVR0OBBYEFN7zzDodmiK2VAzLDydDvb6Er+U+MB8GA1UdIwQYMBaAFN7z
10
- zDodmiK2VAzLDydDvb6Er+U+MA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwID
11
- SAAwRQIhAP8q3PiwqTwCbRvYvvetxH39mAce1mfQMosb33ns228VAiBXdb+p9s0o
12
- 5ug5g9/MTvrIPI7GgolXCWZunkouy0LSrw==
13
- -----END CERTIFICATE-----
@@ -1,5 +0,0 @@
1
- -----BEGIN PRIVATE KEY-----
2
- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgC7WOxDmO7pBvDvYn
3
- YI8+z2/2c0ChxBsuJkQq/dXi1RyhRANCAASoZ0L1WLWvsBG+pKQ9eQCCJXmCfYwx
4
- N4Rp9qCtZPsbWidKH8b5Cy3GXrWR8U0s5OLLo1KAC0ob6B+94JOjQ9zP
5
- -----END PRIVATE KEY-----
@@ -1,13 +0,0 @@
1
- -----BEGIN CERTIFICATE-----
2
- MIIB9TCCAZugAwIBAgIUMpQtAScU2Ow9c1Xy/0b/kS/BuwcwCgYIKoZIzj0EAwIw
3
- UDELMAkGA1UEBhMCVVMxDTALBgNVBAgMBEl0c2kxDTALBgNVBAcMBEl0c2kxEDAO
4
- BgNVBAoMB0l0c2kgQ0ExETAPBgNVBAMMCGl0c2kuZnlpMB4XDTI1MDMwMzIwMjg1
5
- N1oXDTM1MDMwMTIwMjg1N1owUDELMAkGA1UEBhMCVVMxDTALBgNVBAgMBEl0c2kx
6
- DTALBgNVBAcMBEl0c2kxEDAOBgNVBAoMB0l0c2kgQ0ExETAPBgNVBAMMCGl0c2ku
7
- ZnlpMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqGdC9Vi1r7ARvqSkPXkAgiV5
8
- gn2MMTeEafagrWT7G1onSh/G+Qstxl61kfFNLOTiy6NSgAtKG+gfveCTo0Pcz6NT
9
- MFEwHQYDVR0OBBYEFN7zzDodmiK2VAzLDydDvb6Er+U+MB8GA1UdIwQYMBaAFN7z
10
- zDodmiK2VAzLDydDvb6Er+U+MA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwID
11
- SAAwRQIhAP8q3PiwqTwCbRvYvvetxH39mAce1mfQMosb33ns228VAiBXdb+p9s0o
12
- 5ug5g9/MTvrIPI7GgolXCWZunkouy0LSrw==
13
- -----END CERTIFICATE-----
@@ -1,5 +0,0 @@
1
- -----BEGIN PRIVATE KEY-----
2
- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgC7WOxDmO7pBvDvYn
3
- YI8+z2/2c0ChxBsuJkQq/dXi1RyhRANCAASoZ0L1WLWvsBG+pKQ9eQCCJXmCfYwx
4
- N4Rp9qCtZPsbWidKH8b5Cy3GXrWR8U0s5OLLo1KAC0ob6B+94JOjQ9zP
5
- -----END PRIVATE KEY-----
@@ -1,13 +0,0 @@
1
- -----BEGIN CERTIFICATE-----
2
- MIIB9TCCAZugAwIBAgIUMpQtAScU2Ow9c1Xy/0b/kS/BuwcwCgYIKoZIzj0EAwIw
3
- UDELMAkGA1UEBhMCVVMxDTALBgNVBAgMBEl0c2kxDTALBgNVBAcMBEl0c2kxEDAO
4
- BgNVBAoMB0l0c2kgQ0ExETAPBgNVBAMMCGl0c2kuZnlpMB4XDTI1MDMwMzIwMjg1
5
- N1oXDTM1MDMwMTIwMjg1N1owUDELMAkGA1UEBhMCVVMxDTALBgNVBAgMBEl0c2kx
6
- DTALBgNVBAcMBEl0c2kxEDAOBgNVBAoMB0l0c2kgQ0ExETAPBgNVBAMMCGl0c2ku
7
- ZnlpMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqGdC9Vi1r7ARvqSkPXkAgiV5
8
- gn2MMTeEafagrWT7G1onSh/G+Qstxl61kfFNLOTiy6NSgAtKG+gfveCTo0Pcz6NT
9
- MFEwHQYDVR0OBBYEFN7zzDodmiK2VAzLDydDvb6Er+U+MB8GA1UdIwQYMBaAFN7z
10
- zDodmiK2VAzLDydDvb6Er+U+MA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwID
11
- SAAwRQIhAP8q3PiwqTwCbRvYvvetxH39mAce1mfQMosb33ns228VAiBXdb+p9s0o
12
- 5ug5g9/MTvrIPI7GgolXCWZunkouy0LSrw==
13
- -----END CERTIFICATE-----
@@ -1,5 +0,0 @@
1
- -----BEGIN PRIVATE KEY-----
2
- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgC7WOxDmO7pBvDvYn
3
- YI8+z2/2c0ChxBsuJkQq/dXi1RyhRANCAASoZ0L1WLWvsBG+pKQ9eQCCJXmCfYwx
4
- N4Rp9qCtZPsbWidKH8b5Cy3GXrWR8U0s5OLLo1KAC0ob6B+94JOjQ9zP
5
- -----END PRIVATE KEY-----