itsi-scheduler 0.1.5 → 0.1.19

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.

Potentially problematic release.


This version of itsi-scheduler might be problematic. Click here for more details.

Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/CODE_OF_CONDUCT.md +7 -0
  3. data/Cargo.lock +90 -22
  4. data/README.md +5 -0
  5. data/_index.md +7 -0
  6. data/ext/itsi_error/Cargo.toml +1 -0
  7. data/ext/itsi_error/src/lib.rs +106 -7
  8. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  9. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  10. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  11. data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  12. data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
  13. data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
  14. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
  15. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
  16. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
  17. data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
  18. data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
  19. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
  20. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
  21. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
  22. data/ext/itsi_rb_helpers/Cargo.toml +1 -0
  23. data/ext/itsi_rb_helpers/src/heap_value.rs +18 -0
  24. data/ext/itsi_rb_helpers/src/lib.rs +59 -9
  25. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  26. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  27. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  28. data/ext/itsi_rb_helpers/target/debug/build/rb-sys-eb9ed4ff3a60f995/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  29. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
  30. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
  31. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
  32. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
  33. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
  34. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
  35. data/ext/itsi_scheduler/src/itsi_scheduler.rs +1 -1
  36. data/ext/itsi_server/Cargo.lock +2956 -0
  37. data/ext/itsi_server/Cargo.toml +72 -28
  38. data/ext/itsi_server/src/default_responses/mod.rs +11 -0
  39. data/ext/itsi_server/src/env.rs +43 -0
  40. data/ext/itsi_server/src/lib.rs +113 -75
  41. data/ext/itsi_server/src/prelude.rs +2 -0
  42. data/ext/itsi_server/src/{body_proxy → ruby_types/itsi_body_proxy}/big_bytes.rs +10 -5
  43. data/ext/itsi_server/src/{body_proxy/itsi_body_proxy.rs → ruby_types/itsi_body_proxy/mod.rs} +29 -8
  44. data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
  45. data/ext/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +264 -0
  46. data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +345 -0
  47. data/ext/itsi_server/src/{response/itsi_response.rs → ruby_types/itsi_http_response.rs} +84 -40
  48. data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +225 -0
  49. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +375 -0
  50. data/ext/itsi_server/src/ruby_types/itsi_server.rs +83 -0
  51. data/ext/itsi_server/src/ruby_types/mod.rs +48 -0
  52. data/ext/itsi_server/src/server/{bind.rs → binds/bind.rs} +56 -24
  53. data/ext/itsi_server/src/server/{listener.rs → binds/listener.rs} +218 -113
  54. data/ext/itsi_server/src/server/binds/mod.rs +4 -0
  55. data/ext/itsi_server/src/server/{tls → binds/tls}/locked_dir_cache.rs +55 -17
  56. data/ext/itsi_server/src/server/{tls.rs → binds/tls.rs} +109 -28
  57. data/ext/itsi_server/src/server/byte_frame.rs +32 -0
  58. data/ext/itsi_server/src/server/http_message_types.rs +97 -0
  59. data/ext/itsi_server/src/server/io_stream.rs +2 -1
  60. data/ext/itsi_server/src/server/lifecycle_event.rs +3 -0
  61. data/ext/itsi_server/src/server/middleware_stack/middleware.rs +165 -0
  62. data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +56 -0
  63. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +87 -0
  64. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +86 -0
  65. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +285 -0
  66. data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +142 -0
  67. data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +289 -0
  68. data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +292 -0
  69. data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +55 -0
  70. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +190 -0
  71. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +157 -0
  72. data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +195 -0
  73. data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +82 -0
  74. data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +201 -0
  75. data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
  76. data/ext/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
  77. data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +87 -0
  78. data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +414 -0
  79. data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +131 -0
  80. data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +76 -0
  81. data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +44 -0
  82. data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +36 -0
  83. data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +126 -0
  84. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +180 -0
  85. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
  86. data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +163 -0
  87. data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +12 -0
  88. data/ext/itsi_server/src/server/middleware_stack/mod.rs +347 -0
  89. data/ext/itsi_server/src/server/mod.rs +6 -5
  90. data/ext/itsi_server/src/server/process_worker.rs +65 -14
  91. data/ext/itsi_server/src/server/request_job.rs +11 -0
  92. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +137 -49
  93. data/ext/itsi_server/src/server/serve_strategy/mod.rs +9 -6
  94. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +338 -164
  95. data/ext/itsi_server/src/server/signal.rs +32 -26
  96. data/ext/itsi_server/src/server/size_limited_incoming.rs +101 -0
  97. data/ext/itsi_server/src/server/thread_worker.rs +214 -107
  98. data/ext/itsi_server/src/services/cache_store.rs +74 -0
  99. data/ext/itsi_server/src/services/itsi_http_service.rs +239 -0
  100. data/ext/itsi_server/src/services/mime_types.rs +1416 -0
  101. data/ext/itsi_server/src/services/mod.rs +6 -0
  102. data/ext/itsi_server/src/services/password_hasher.rs +83 -0
  103. data/ext/itsi_server/src/services/rate_limiter.rs +569 -0
  104. data/ext/itsi_server/src/services/static_file_server.rs +1324 -0
  105. data/ext/itsi_tracing/Cargo.toml +1 -0
  106. data/ext/itsi_tracing/src/lib.rs +312 -34
  107. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
  108. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
  109. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
  110. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
  111. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
  112. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
  113. data/lib/itsi/scheduler/version.rb +1 -1
  114. data/lib/itsi/scheduler.rb +2 -2
  115. metadata +93 -21
  116. data/ext/itsi_error/src/from.rs +0 -71
  117. data/ext/itsi_server/extconf.rb +0 -6
  118. data/ext/itsi_server/src/body_proxy/mod.rs +0 -2
  119. data/ext/itsi_server/src/request/itsi_request.rs +0 -277
  120. data/ext/itsi_server/src/request/mod.rs +0 -1
  121. data/ext/itsi_server/src/response/mod.rs +0 -1
  122. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -13
  123. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -5
  124. data/ext/itsi_server/src/server/itsi_server.rs +0 -244
  125. /data/ext/itsi_server/src/server/{bind_protocol.rs → binds/bind_protocol.rs} +0 -0
@@ -1,34 +1,68 @@
1
1
  use async_trait::async_trait;
2
- use fs2::FileExt; // for lock_exclusive, unlock
3
- use std::fs::OpenOptions;
2
+ use fs2::FileExt;
3
+ use parking_lot::Mutex;
4
+ use std::fs::{self, OpenOptions};
4
5
  use std::io::Error as IoError;
5
6
  use std::path::{Path, PathBuf};
6
7
  use tokio_rustls_acme::caches::DirCache;
7
8
  use tokio_rustls_acme::{AccountCache, CertCache};
8
9
 
10
+ use crate::env::ITSI_ACME_LOCK_FILE_NAME;
11
+
9
12
  /// A wrapper around DirCache that locks a file before writing cert/account data.
10
13
  pub struct LockedDirCache<P: AsRef<Path> + Send + Sync> {
11
14
  inner: DirCache<P>,
12
15
  lock_path: PathBuf,
16
+ current_lock: Mutex<Option<std::fs::File>>,
13
17
  }
14
18
 
15
19
  impl<P: AsRef<Path> + Send + Sync> LockedDirCache<P> {
16
20
  pub fn new(dir: P) -> Self {
17
21
  let dir_path = dir.as_ref().to_path_buf();
18
- let lock_path = dir_path.join(".acme.lock");
22
+ std::fs::create_dir_all(&dir_path).unwrap();
23
+ let lock_path = dir_path.join(&*ITSI_ACME_LOCK_FILE_NAME);
24
+ Self::touch_file(&lock_path).expect("Failed to create lock file");
25
+
19
26
  Self {
20
27
  inner: DirCache::new(dir),
21
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)?;
22
36
  }
37
+ fs::OpenOptions::new()
38
+ .create(true)
39
+ .write(true)
40
+ .truncate(true)
41
+ .open(path)?;
42
+ Ok(())
23
43
  }
24
44
 
25
- fn lock_exclusive(&self) -> Result<std::fs::File, IoError> {
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
+ }
26
53
  let lockfile = OpenOptions::new()
54
+ .create(true)
27
55
  .write(true)
28
56
  .truncate(true)
29
57
  .open(&self.lock_path)?;
30
58
  lockfile.lock_exclusive()?;
31
- Ok(lockfile)
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(())
32
66
  }
33
67
  }
34
68
 
@@ -41,8 +75,14 @@ impl<P: AsRef<Path> + Send + Sync> CertCache for LockedDirCache<P> {
41
75
  domains: &[String],
42
76
  directory_url: &str,
43
77
  ) -> Result<Option<Vec<u8>>, Self::EC> {
44
- // Just delegate to the inner DirCache
45
- self.inner.load_cert(domains, directory_url).await
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
46
86
  }
47
87
 
48
88
  async fn store_cert(
@@ -52,13 +92,14 @@ impl<P: AsRef<Path> + Send + Sync> CertCache for LockedDirCache<P> {
52
92
  cert: &[u8],
53
93
  ) -> Result<(), Self::EC> {
54
94
  // Acquire the lock before storing
55
- let lockfile = self.lock_exclusive()?;
95
+ self.lock_exclusive()?;
56
96
 
57
97
  // Perform the store operation
58
98
  let result = self.inner.store_cert(domains, directory_url, cert).await;
59
99
 
60
- // Unlock and return
61
- let _ = fs2::FileExt::unlock(&lockfile);
100
+ if let Ok(()) = result {
101
+ self.unlock()?;
102
+ }
62
103
  result
63
104
  }
64
105
  }
@@ -72,6 +113,7 @@ impl<P: AsRef<Path> + Send + Sync> AccountCache for LockedDirCache<P> {
72
113
  contact: &[String],
73
114
  directory_url: &str,
74
115
  ) -> Result<Option<Vec<u8>>, Self::EA> {
116
+ self.lock_exclusive()?;
75
117
  self.inner.load_account(contact, directory_url).await
76
118
  }
77
119
 
@@ -81,14 +123,10 @@ impl<P: AsRef<Path> + Send + Sync> AccountCache for LockedDirCache<P> {
81
123
  directory_url: &str,
82
124
  account: &[u8],
83
125
  ) -> Result<(), Self::EA> {
84
- let lockfile = self.lock_exclusive()?;
126
+ self.lock_exclusive()?;
85
127
 
86
- let result = self
87
- .inner
128
+ self.inner
88
129
  .store_account(contact, directory_url, account)
89
- .await;
90
-
91
- let _ = fs2::FileExt::unlock(&lockfile);
92
- result
130
+ .await
93
131
  }
94
132
  }
@@ -2,21 +2,30 @@ 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};
6
- use rustls::pki_types::{CertificateDer, PrivateKeyDer};
5
+ use rcgen::{
6
+ BasicConstraints, CertificateParams, DistinguishedName, DnType, IsCa, KeyPair, SanType,
7
+ };
8
+ use rustls::{
9
+ pki_types::{CertificateDer, PrivateKeyDer},
10
+ ClientConfig, RootCertStore,
11
+ };
7
12
  use rustls_pemfile::{certs, pkcs8_private_keys};
8
13
  use std::{
9
14
  collections::HashMap,
10
- env, fs,
15
+ fs,
11
16
  io::{BufReader, Error},
12
17
  sync::Arc,
13
18
  };
14
19
  use tokio::sync::Mutex;
15
20
  use tokio_rustls::{rustls::ServerConfig, TlsAcceptor};
16
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
+ };
27
+
17
28
  mod locked_dir_cache;
18
- const ITS_CA_CERT: &str = include_str!("./itsi_ca/itsi_ca.crt");
19
- const ITS_CA_KEY: &str = include_str!("./itsi_ca/itsi_ca.key");
20
29
 
21
30
  #[derive(Clone)]
22
31
  pub enum ItsiTlsAcceptor {
@@ -28,35 +37,72 @@ pub enum ItsiTlsAcceptor {
28
37
  ),
29
38
  }
30
39
 
31
- // Generates a TLS configuration based on either :
32
- // * Input "cert" and "key" options (either paths or Base64-encoded strings) or
33
- // * Performs automatic certificate generation/retrieval. Generated certs use an internal self-signed Isti CA.
34
- // If a non-local host or optional domain parameter is provided,
35
- // an automated certificate will attempt to be fetched using let's encrypt.
40
+ /// Generates a TLS configuration based on either :
41
+ /// * Input "cert" and "key" options (either paths or Base64-encoded strings) or
42
+ /// * Performs automatic certificate generation/retrieval. Generated certs use an internal self-signed Isti CA.
43
+ ///
44
+ /// If a non-local host or optional domain parameter is provided,
45
+ /// an automated certificate will attempt to be fetched using let's encrypt.
36
46
  pub fn configure_tls(
37
47
  host: &str,
38
48
  query_params: &HashMap<String, String>,
39
49
  ) -> Result<ItsiTlsAcceptor> {
40
50
  let domains = query_params
41
51
  .get("domains")
42
- .map(|v| v.split(',').map(String::from).collect::<Vec<_>>());
52
+ .map(|v| v.split(',').map(String::from).collect::<Vec<_>>())
53
+ .or_else(|| query_params.get("domain").map(|v| vec![v.to_string()]));
43
54
 
44
- if query_params.get("cert").is_none() || query_params.get("key").is_none() {
55
+ if query_params.get("cert").is_some_and(|c| c == "acme") {
45
56
  if let Some(domains) = domains {
46
- let directory_url = env::var("ACME_DIRECTORY_URL")
47
- .unwrap_or_else(|_| "https://acme-v02.api.letsencrypt.org/directory".to_string());
57
+ let directory_url = &*ITSI_ACME_DIRECTORY_URL;
48
58
  info!(
49
59
  domains = format!("{:?}", domains),
50
60
  directory_url, "Requesting acme cert"
51
61
  );
52
- let acme_state = AcmeConfig::new(domains)
53
- .contact(["mailto:wc@pico.net.nz"])
54
- .cache(LockedDirCache::new("./rustls_acme_cache"))
55
- .directory(directory_url)
56
- .state();
57
- let rustls_config = ServerConfig::builder()
62
+ let acme_contact_email = query_params
63
+ .get("acme_email")
64
+ .map(|s| s.to_string())
65
+ .or_else(|| (*ITSI_ACME_CONTACT_EMAIL).as_ref().ok().map(|s| s.to_string()))
66
+ .ok_or_else(|| itsi_error::ItsiError::ArgumentError(
67
+ "acme_email query param or ITSI_ACME_CONTACT_EMAIL must be set before you can auto-generate let's encrypt certificates".to_string(),
68
+ ))?;
69
+
70
+ let acme_config = AcmeConfig::new(domains)
71
+ .contact([format!("mailto:{}", acme_contact_email)])
72
+ .cache(LockedDirCache::new(&*ITSI_ACME_CACHE_DIR))
73
+ .directory(directory_url);
74
+
75
+ let acme_state = if let Ok(ca_pem_path) = &*ITSI_ACME_CA_PEM_PATH {
76
+ let mut root_cert_store = RootCertStore::empty();
77
+
78
+ let ca_pem = fs::read(ca_pem_path).expect("failed to read CA pem file");
79
+ let mut ca_reader = BufReader::new(&ca_pem[..]);
80
+ let der_certs: Vec<CertificateDer> = certs(&mut ca_reader)
81
+ .collect::<std::result::Result<Vec<CertificateDer>, _>>()
82
+ .map_err(|e| {
83
+ itsi_error::ItsiError::ArgumentError(format!(
84
+ "Invalid ACME CA Pem path {:?}",
85
+ e
86
+ ))
87
+ })?;
88
+ root_cert_store.add_parsable_certificates(der_certs);
89
+
90
+ let client_config = ClientConfig::builder()
91
+ .with_root_certificates(root_cert_store)
92
+ .with_no_client_auth();
93
+ acme_config
94
+ .client_tls_config(Arc::new(client_config))
95
+ .state()
96
+ } else {
97
+ acme_config.state()
98
+ };
99
+
100
+ let mut rustls_config = ServerConfig::builder()
58
101
  .with_no_client_auth()
59
102
  .with_cert_resolver(acme_state.resolver());
103
+
104
+ rustls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
105
+
60
106
  let acceptor = acme_state.acceptor();
61
107
  return Ok(ItsiTlsAcceptor::Automatic(
62
108
  acceptor,
@@ -73,7 +119,7 @@ pub fn configure_tls(
73
119
  let key = load_private_key(key_path);
74
120
  (certs, key)
75
121
  } else {
76
- generate_ca_signed_cert(vec![host.to_owned()])?
122
+ generate_ca_signed_cert(domains.unwrap_or(vec![host.to_owned()]))?
77
123
  };
78
124
 
79
125
  let mut config = ServerConfig::builder()
@@ -144,10 +190,19 @@ pub fn load_private_key(path: &str) -> PrivateKeyDer<'static> {
144
190
  pub fn generate_ca_signed_cert(
145
191
  domains: Vec<String>,
146
192
  ) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)> {
147
- info!("Generating New Itsi CA - Self signed Certificate. Use `itsi ca export` to export the CA certificate for import into your local trust store.");
193
+ info!(
194
+ domains = format!("{}", domains.join(", ")),
195
+ "Self signed cert",
196
+ );
197
+ info!(
198
+ "Add {} to your system's trusted cert store to resolve certificate errors.",
199
+ format!("{}/itsi_dev_ca.crt", ITSI_LOCAL_CA_DIR.to_str().unwrap())
200
+ );
201
+ info!("Dev CA path can be overridden by setting env var: `ITSI_LOCAL_CA_DIR`.");
202
+ let (ca_key_pem, ca_cert_pem) = get_or_create_local_dev_ca()?;
148
203
 
149
- let ca_kp = KeyPair::from_pem(ITS_CA_KEY).expect("Failed to load embedded CA key");
150
- let ca_cert = CertificateParams::from_ca_cert_pem(ITS_CA_CERT)
204
+ let ca_kp = KeyPair::from_pem(&ca_key_pem).expect("Failed to load CA key");
205
+ let ca_cert = CertificateParams::from_ca_cert_pem(&ca_cert_pem)
151
206
  .expect("Failed to parse embedded CA certificate")
152
207
  .self_signed(&ca_kp)
153
208
  .expect("Failed to self-sign embedded CA cert");
@@ -155,10 +210,6 @@ pub fn generate_ca_signed_cert(
155
210
  let ee_key = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap();
156
211
  let mut ee_params = CertificateParams::default();
157
212
 
158
- info!(
159
- "Generated certificate will be valid for domains {:?}",
160
- domains
161
- );
162
213
  use std::net::IpAddr;
163
214
 
164
215
  ee_params.subject_alt_names = domains
@@ -187,3 +238,33 @@ pub fn generate_ca_signed_cert(
187
238
  PrivateKeyDer::try_from(ee_key.serialize_der()).unwrap(),
188
239
  ))
189
240
  }
241
+
242
+ fn get_or_create_local_dev_ca() -> Result<(String, String)> {
243
+ let ca_dir = &*ITSI_LOCAL_CA_DIR;
244
+ fs::create_dir_all(ca_dir)?;
245
+
246
+ let key_path = ca_dir.join("itsi_dev_ca.key");
247
+ let cert_path = ca_dir.join("itsi_dev_ca.crt");
248
+
249
+ if key_path.exists() && cert_path.exists() {
250
+ // Already have a local CA
251
+ let key_pem = fs::read_to_string(&key_path)?;
252
+ let cert_pem = fs::read_to_string(&cert_path)?;
253
+
254
+ Ok((key_pem, cert_pem))
255
+ } else {
256
+ let subject_alt_names = vec!["dev.itsi.fyi".to_string(), "localhost".to_string()];
257
+ let mut params = CertificateParams::new(subject_alt_names)?;
258
+ let mut distinguished_name = DistinguishedName::new();
259
+ distinguished_name.push(DnType::CommonName, "Itsi Development CA");
260
+ params.distinguished_name = distinguished_name;
261
+ params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
262
+ let key_pair = KeyPair::generate()?;
263
+ let cert = params.self_signed(&key_pair)?;
264
+
265
+ fs::write(&key_path, key_pair.serialize_pem())?;
266
+ fs::write(&cert_path, cert.pem())?;
267
+
268
+ Ok((key_pair.serialize_pem(), cert.pem()))
269
+ }
270
+ }
@@ -0,0 +1,32 @@
1
+ use std::ops::Deref;
2
+
3
+ use bytes::Bytes;
4
+
5
+ #[derive(Debug)]
6
+ pub enum ByteFrame {
7
+ Data(Bytes),
8
+ End(Bytes),
9
+ Empty,
10
+ }
11
+
12
+ impl Deref for ByteFrame {
13
+ type Target = Bytes;
14
+
15
+ fn deref(&self) -> &Self::Target {
16
+ match self {
17
+ ByteFrame::Data(data) => data,
18
+ ByteFrame::End(data) => data,
19
+ ByteFrame::Empty => unreachable!(),
20
+ }
21
+ }
22
+ }
23
+
24
+ impl From<ByteFrame> for Bytes {
25
+ fn from(frame: ByteFrame) -> Self {
26
+ match frame {
27
+ ByteFrame::Data(data) => data,
28
+ ByteFrame::End(data) => data,
29
+ ByteFrame::Empty => unreachable!(),
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,97 @@
1
+ use std::convert::Infallible;
2
+
3
+ use bytes::Bytes;
4
+ use http::{Request, Response};
5
+ use http_body_util::combinators::BoxBody;
6
+ use hyper::body::Incoming;
7
+
8
+ use super::size_limited_incoming::SizeLimitedIncoming;
9
+
10
+ pub type HttpResponse = Response<BoxBody<Bytes, Infallible>>;
11
+ pub type HttpRequest = Request<SizeLimitedIncoming<Incoming>>;
12
+
13
+ pub trait ConversionExt {
14
+ fn limit(self) -> HttpRequest;
15
+ }
16
+
17
+ impl ConversionExt for Request<Incoming> {
18
+ fn limit(self) -> HttpRequest {
19
+ let (parts, body) = self.into_parts();
20
+ Request::from_parts(parts, SizeLimitedIncoming::new(body))
21
+ }
22
+ }
23
+
24
+ pub trait RequestExt {
25
+ fn content_type(&self) -> Option<&str>;
26
+ fn accept(&self) -> Option<&str>;
27
+ fn header(&self, header_name: &str) -> Option<&str>;
28
+ fn query_param(&self, query_name: &str) -> Option<&str>;
29
+ }
30
+
31
+ pub trait PathExt {
32
+ fn no_trailing_slash(&self) -> &str;
33
+ }
34
+
35
+ #[derive(Debug, Clone)]
36
+ pub enum ResponseFormat {
37
+ JSON,
38
+ HTML,
39
+ TEXT,
40
+ UNKNOWN,
41
+ }
42
+
43
+ #[derive(Debug, Clone, Default)]
44
+ pub struct SupportedEncodingSet {
45
+ pub zstd: bool,
46
+ pub br: bool,
47
+ pub deflate: bool,
48
+ pub gzip: bool,
49
+ }
50
+
51
+ impl From<Option<&str>> for ResponseFormat {
52
+ fn from(value: Option<&str>) -> Self {
53
+ match value {
54
+ Some("application/json") => ResponseFormat::JSON,
55
+ Some("text/html") => ResponseFormat::HTML,
56
+ Some("text/plain") => ResponseFormat::TEXT,
57
+ _ => ResponseFormat::UNKNOWN,
58
+ }
59
+ }
60
+ }
61
+
62
+ impl PathExt for str {
63
+ fn no_trailing_slash(&self) -> &str {
64
+ if self == "/" {
65
+ self
66
+ } else {
67
+ self.trim_end_matches("/")
68
+ }
69
+ }
70
+ }
71
+
72
+ impl RequestExt for HttpRequest {
73
+ fn content_type(&self) -> Option<&str> {
74
+ self.headers()
75
+ .get("content-type")
76
+ .map(|hv| hv.to_str().unwrap_or(""))
77
+ }
78
+
79
+ fn accept(&self) -> Option<&str> {
80
+ self.headers()
81
+ .get("accept")
82
+ .map(|hv| hv.to_str().unwrap_or(""))
83
+ }
84
+
85
+ fn header(&self, header_name: &str) -> Option<&str> {
86
+ self.headers()
87
+ .get(header_name)
88
+ .map(|hv| hv.to_str().unwrap_or(""))
89
+ }
90
+
91
+ fn query_param(&self, query_name: &str) -> Option<&str> {
92
+ self.uri()
93
+ .query()
94
+ .and_then(|query| query.split('&').find(|param| param.starts_with(query_name)))
95
+ .map(|param| param.split('=').nth(1).unwrap_or(""))
96
+ }
97
+ }
@@ -1,4 +1,3 @@
1
- use super::listener::SockAddr;
2
1
  use pin_project::pin_project;
3
2
  use tokio::net::{TcpStream, UnixStream};
4
3
  use tokio_rustls::server::TlsStream;
@@ -8,6 +7,8 @@ use std::pin::Pin;
8
7
  use std::task::{Context, Poll};
9
8
  use tokio::io::{AsyncRead, AsyncWrite};
10
9
 
10
+ use super::binds::listener::SockAddr;
11
+
11
12
  #[pin_project(project = IoStreamEnumProj)]
12
13
  pub enum IoStream {
13
14
  Tcp {
@@ -3,7 +3,10 @@ pub enum LifecycleEvent {
3
3
  Start,
4
4
  Shutdown,
5
5
  Restart,
6
+ Reload,
6
7
  IncreaseWorkers,
7
8
  DecreaseWorkers,
8
9
  ForceShutdown,
10
+ PrintInfo,
11
+ ChildTerminated,
9
12
  }
@@ -0,0 +1,165 @@
1
+ use crate::{
2
+ server::http_message_types::{HttpRequest, HttpResponse},
3
+ services::itsi_http_service::HttpRequestContext,
4
+ };
5
+
6
+ use super::middlewares::*;
7
+
8
+ use async_trait::async_trait;
9
+ use either::Either;
10
+ use magnus::error::Result;
11
+ use std::cmp::Ordering;
12
+
13
+ #[derive(Debug)]
14
+ pub enum Middleware {
15
+ AllowList(AllowList),
16
+ AuthAPIKey(AuthAPIKey),
17
+ AuthBasic(AuthBasic),
18
+ AuthJwt(Box<AuthJwt>),
19
+ CacheControl(CacheControl),
20
+ Compression(Compression),
21
+ Cors(Box<Cors>),
22
+ DenyList(DenyList),
23
+ ETag(ETag),
24
+ IntrusionProtection(IntrusionProtection),
25
+ LogRequests(LogRequests),
26
+ MaxBody(MaxBody),
27
+ Proxy(Proxy),
28
+ RateLimit(RateLimit),
29
+ Redirect(Redirect),
30
+ RequestHeaders(RequestHeaders),
31
+ ResponseHeaders(ResponseHeaders),
32
+ RubyApp(RubyApp),
33
+ StaticAssets(StaticAssets),
34
+ StaticResponse(StaticResponse),
35
+ }
36
+
37
+ #[async_trait]
38
+ impl MiddlewareLayer for Middleware {
39
+ /// Called just once, to initialize the middleware state.
40
+ async fn initialize(&self) -> Result<()> {
41
+ match self {
42
+ Middleware::DenyList(filter) => filter.initialize().await,
43
+ Middleware::AllowList(filter) => filter.initialize().await,
44
+ Middleware::AuthBasic(filter) => filter.initialize().await,
45
+ Middleware::AuthJwt(filter) => filter.initialize().await,
46
+ Middleware::AuthAPIKey(filter) => filter.initialize().await,
47
+ Middleware::IntrusionProtection(filter) => filter.initialize().await,
48
+ Middleware::MaxBody(filter) => filter.initialize().await,
49
+ Middleware::RateLimit(filter) => filter.initialize().await,
50
+ Middleware::RequestHeaders(filter) => filter.initialize().await,
51
+ Middleware::ResponseHeaders(filter) => filter.initialize().await,
52
+ Middleware::CacheControl(filter) => filter.initialize().await,
53
+ Middleware::Cors(filter) => filter.initialize().await,
54
+ Middleware::ETag(filter) => filter.initialize().await,
55
+ Middleware::StaticAssets(filter) => filter.initialize().await,
56
+ Middleware::StaticResponse(filter) => filter.initialize().await,
57
+ Middleware::Compression(filter) => filter.initialize().await,
58
+ Middleware::LogRequests(filter) => filter.initialize().await,
59
+ Middleware::Redirect(filter) => filter.initialize().await,
60
+ Middleware::Proxy(filter) => filter.initialize().await,
61
+ Middleware::RubyApp(filter) => filter.initialize().await,
62
+ }
63
+ }
64
+
65
+ async fn before(
66
+ &self,
67
+ req: HttpRequest,
68
+ context: &mut HttpRequestContext,
69
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
70
+ match self {
71
+ Middleware::DenyList(filter) => filter.before(req, context).await,
72
+ Middleware::AllowList(filter) => filter.before(req, context).await,
73
+ Middleware::AuthBasic(filter) => filter.before(req, context).await,
74
+ Middleware::AuthJwt(filter) => filter.before(req, context).await,
75
+ Middleware::AuthAPIKey(filter) => filter.before(req, context).await,
76
+ Middleware::IntrusionProtection(filter) => filter.before(req, context).await,
77
+ Middleware::MaxBody(filter) => filter.before(req, context).await,
78
+ Middleware::RequestHeaders(filter) => filter.before(req, context).await,
79
+ Middleware::ResponseHeaders(filter) => filter.before(req, context).await,
80
+ Middleware::RateLimit(filter) => filter.before(req, context).await,
81
+ Middleware::CacheControl(filter) => filter.before(req, context).await,
82
+ Middleware::Cors(filter) => filter.before(req, context).await,
83
+ Middleware::ETag(filter) => filter.before(req, context).await,
84
+ Middleware::StaticAssets(filter) => filter.before(req, context).await,
85
+ Middleware::StaticResponse(filter) => filter.before(req, context).await,
86
+ Middleware::Compression(filter) => filter.before(req, context).await,
87
+ Middleware::LogRequests(filter) => filter.before(req, context).await,
88
+ Middleware::Redirect(filter) => filter.before(req, context).await,
89
+ Middleware::Proxy(filter) => filter.before(req, context).await,
90
+ Middleware::RubyApp(filter) => filter.before(req, context).await,
91
+ }
92
+ }
93
+
94
+ async fn after(&self, res: HttpResponse, context: &mut HttpRequestContext) -> HttpResponse {
95
+ match self {
96
+ Middleware::DenyList(filter) => filter.after(res, context).await,
97
+ Middleware::AllowList(filter) => filter.after(res, context).await,
98
+ Middleware::AuthBasic(filter) => filter.after(res, context).await,
99
+ Middleware::AuthJwt(filter) => filter.after(res, context).await,
100
+ Middleware::AuthAPIKey(filter) => filter.after(res, context).await,
101
+ Middleware::IntrusionProtection(filter) => filter.after(res, context).await,
102
+ Middleware::MaxBody(filter) => filter.after(res, context).await,
103
+ Middleware::RateLimit(filter) => filter.after(res, context).await,
104
+ Middleware::RequestHeaders(filter) => filter.after(res, context).await,
105
+ Middleware::ResponseHeaders(filter) => filter.after(res, context).await,
106
+ Middleware::CacheControl(filter) => filter.after(res, context).await,
107
+ Middleware::Cors(filter) => filter.after(res, context).await,
108
+ Middleware::ETag(filter) => filter.after(res, context).await,
109
+ Middleware::StaticAssets(filter) => filter.after(res, context).await,
110
+ Middleware::StaticResponse(filter) => filter.after(res, context).await,
111
+ Middleware::Compression(filter) => filter.after(res, context).await,
112
+ Middleware::LogRequests(filter) => filter.after(res, context).await,
113
+ Middleware::Redirect(filter) => filter.after(res, context).await,
114
+ Middleware::Proxy(filter) => filter.after(res, context).await,
115
+ Middleware::RubyApp(filter) => filter.after(res, context).await,
116
+ }
117
+ }
118
+ }
119
+
120
+ impl Middleware {
121
+ fn variant_order(&self) -> usize {
122
+ match self {
123
+ Middleware::DenyList(_) => 0,
124
+ Middleware::AllowList(_) => 1,
125
+ Middleware::IntrusionProtection(_) => 2,
126
+ Middleware::Redirect(_) => 3,
127
+ Middleware::LogRequests(_) => 4,
128
+ Middleware::CacheControl(_) => 5,
129
+ Middleware::RequestHeaders(_) => 6,
130
+ Middleware::ResponseHeaders(_) => 7,
131
+ Middleware::MaxBody(_) => 8,
132
+ Middleware::AuthBasic(_) => 9,
133
+ Middleware::AuthJwt(_) => 10,
134
+ Middleware::AuthAPIKey(_) => 11,
135
+ Middleware::RateLimit(_) => 12,
136
+ Middleware::ETag(_) => 13,
137
+ Middleware::Compression(_) => 14,
138
+ Middleware::Proxy(_) => 15,
139
+ Middleware::Cors(_) => 16,
140
+ Middleware::StaticResponse(_) => 17,
141
+ Middleware::StaticAssets(_) => 18,
142
+ Middleware::RubyApp(_) => 19,
143
+ }
144
+ }
145
+ }
146
+
147
+ impl PartialEq for Middleware {
148
+ fn eq(&self, other: &Self) -> bool {
149
+ self.variant_order() == other.variant_order()
150
+ }
151
+ }
152
+
153
+ impl Eq for Middleware {}
154
+
155
+ impl PartialOrd for Middleware {
156
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
157
+ Some(self.variant_order().cmp(&other.variant_order()))
158
+ }
159
+ }
160
+
161
+ impl Ord for Middleware {
162
+ fn cmp(&self, other: &Self) -> Ordering {
163
+ self.variant_order().cmp(&other.variant_order())
164
+ }
165
+ }