itsi 0.2.26 → 0.2.27.rc1

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +7 -3
  3. data/Rakefile +24 -3
  4. data/crates/itsi_acme/Cargo.toml +2 -1
  5. data/crates/itsi_acme/src/acceptor.rs +1 -1
  6. data/crates/itsi_acme/src/acme.rs +31 -3
  7. data/crates/itsi_acme/src/http_challenge.rs +81 -0
  8. data/crates/itsi_acme/src/https_helper.rs +3 -1
  9. data/crates/itsi_acme/src/jose.rs +6 -2
  10. data/crates/itsi_acme/src/lib.rs +2 -0
  11. data/crates/itsi_acme/src/resolver.rs +27 -4
  12. data/crates/itsi_acme/src/state.rs +183 -22
  13. data/crates/itsi_scheduler/Cargo.toml +1 -1
  14. data/crates/itsi_scheduler/src/itsi_scheduler.rs +115 -64
  15. data/crates/itsi_scheduler/src/lib.rs +2 -1
  16. data/crates/itsi_server/Cargo.toml +2 -1
  17. data/crates/itsi_server/src/lib.rs +15 -0
  18. data/crates/itsi_server/src/ruby_types/itsi_http_request.rs +9 -0
  19. data/crates/itsi_server/src/ruby_types/itsi_http_response.rs +95 -0
  20. data/crates/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +22 -1
  21. data/crates/itsi_server/src/ruby_types/itsi_server.rs +100 -0
  22. data/crates/itsi_server/src/server/binds/listener.rs +9 -24
  23. data/crates/itsi_server/src/server/binds/tls.rs +372 -67
  24. data/crates/itsi_server/src/services/itsi_http_service.rs +46 -2
  25. data/gems/scheduler/Cargo.lock +4011 -527
  26. data/gems/scheduler/Gemfile +8 -2
  27. data/gems/scheduler/Gemfile.lock +107 -0
  28. data/gems/scheduler/Rakefile +33 -9
  29. data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
  30. data/gems/scheduler/lib/itsi/scheduler.rb +121 -6
  31. data/gems/scheduler/test/helpers/test_helper.rb +2 -0
  32. data/gems/scheduler/test/test_address_resolve.rb +8 -2
  33. data/gems/scheduler/test/test_itsi_scheduler.rb +80 -0
  34. data/gems/scheduler/test/test_timeout_after.rb +102 -0
  35. data/gems/server/Cargo.lock +30 -1
  36. data/gems/server/Gemfile +2 -0
  37. data/gems/server/Gemfile.lock +123 -0
  38. data/gems/server/Rakefile +18 -5
  39. data/gems/server/lib/itsi/http_request.rb +10 -0
  40. data/gems/server/lib/itsi/server/rack_interface.rb +45 -2
  41. data/gems/server/lib/itsi/server/version.rb +1 -1
  42. data/gems/server/lib/itsi/server.rb +24 -0
  43. data/gems/server/test/acme/local_acme_challenges.rb +190 -0
  44. data/gems/server/test/helpers/local_acme.rb +218 -0
  45. data/gems/server/test/helpers/test_helper.rb +7 -9
  46. data/gems/server/test/middleware/endpoint.rb +9 -6
  47. data/gems/server/test/rack/test_rack_server.rb +79 -0
  48. data/lib/itsi/version.rb +1 -1
  49. metadata +12 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ca7f7c888f0f0907a2459612c698fc9cb9abc64c149dca1de68b159593cc1dc
4
- data.tar.gz: 7b4c54ea8118e9042e96a1cf7904cf61408c94b9820b9868cd1d82a65eabcdae
3
+ metadata.gz: 6ea9fdbfbdd35894f2c5977d2624a4b90cb9467b3133d055d5ae87d1b267d5ff
4
+ data.tar.gz: ad82474c56c170163d2fcc7e75c6e2351fbc61348e3fd8e60697acda4ffbe65a
5
5
  SHA512:
6
- metadata.gz: cefa0a3a2b934de6dcc8b9ea17789f472b62c887cf43b2bb28d30f685e236c8e123170ae573c7088fed10a06a5aea16ed37b6105248a0cbf0df6a3768a7f52d6
7
- data.tar.gz: 71c3f0bd0bb201d3a4fa889eeff42a07d101c77948b834706bff1d68ebb200d50cbc7bb60b0262015450752996f1d440add00f868fb7b40e98dab80725a23a94
6
+ metadata.gz: 47615c9e033b2119fe5a1c5780ed9acb4731500667fdc42f75df7a9768f0f4feda2d6e80f04ff85a1a938e8832934704b20d81cea48b7d96a87406bae22511e4
7
+ data.tar.gz: 33ccb5b47c880ab500dc0551fbb853a8e19a5afd95cd3c8a5e4fc64b3895a602a68e8aaaeaa3b1133ad4bf56e968a4821efc71798b2c46b8d62705a790de78cd
data/Cargo.lock CHANGED
@@ -1662,7 +1662,7 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
1662
1662
 
1663
1663
  [[package]]
1664
1664
  name = "itsi-scheduler"
1665
- version = "0.2.26"
1665
+ version = "0.2.27"
1666
1666
  dependencies = [
1667
1667
  "bytes",
1668
1668
  "derive_more",
@@ -1680,7 +1680,7 @@ dependencies = [
1680
1680
 
1681
1681
  [[package]]
1682
1682
  name = "itsi-server"
1683
- version = "0.2.26"
1683
+ version = "0.2.27"
1684
1684
  dependencies = [
1685
1685
  "argon2",
1686
1686
  "async-channel",
@@ -1742,6 +1742,7 @@ dependencies = [
1742
1742
  "tokio-util",
1743
1743
  "tracing",
1744
1744
  "url",
1745
+ "webpki-roots",
1745
1746
  ]
1746
1747
 
1747
1748
  [[package]]
@@ -1757,6 +1758,7 @@ dependencies = [
1757
1758
  "futures",
1758
1759
  "log",
1759
1760
  "num-bigint",
1761
+ "parking_lot",
1760
1762
  "pem",
1761
1763
  "proc-macro2",
1762
1764
  "rcgen",
@@ -2606,7 +2608,9 @@ dependencies = [
2606
2608
 
2607
2609
  [[package]]
2608
2610
  name = "rb-sys-build"
2609
- version = "0.9.124"
2611
+ version = "0.9.126"
2612
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2613
+ checksum = "855fc1ad8943d12c89ef12f9147f1cc531f5bf19fb744112fdd317bb6ee7b5c5"
2610
2614
  dependencies = [
2611
2615
  "bindgen 0.72.1",
2612
2616
  "lazy_static",
data/Rakefile CHANGED
@@ -1,7 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'bundler/gem_tasks'
4
- require 'minitest/test_task'
4
+
5
+ def test_tasks_requested?
6
+ requested = Rake.application.top_level_tasks
7
+ return true if requested.empty?
8
+
9
+ requested.any? do |task_name|
10
+ task_name == "test" ||
11
+ task_name.end_with?(":test") ||
12
+ task_name.start_with?("test:")
13
+ end
14
+ end
15
+
16
+ require 'minitest/test_task' if test_tasks_requested?
5
17
 
6
18
  # Ensure the nested gems' `lib` directories are included in the LOAD_PATH
7
19
  $LOAD_PATH.unshift(File.expand_path('scheduler/lib', __dir__))
@@ -25,17 +37,26 @@ GEMS = [
25
37
  ]
26
38
  SHARED_TASKS = %i[compile compile:dev test]
27
39
 
40
+ def quiet_ruby_env
41
+ rubyopt = [ENV["RUBYOPT"], "-W0"].compact.join(" ").strip
42
+ rubyopt.empty? ? {} : { "RUBYOPT" => rubyopt }
43
+ end
44
+
28
45
  GEMS.each do |gem|
29
46
  namespace gem[:shortname] do
30
47
  desc "Run tasks in the #{gem[:dir]} directory"
31
48
  task :default do
32
- sh "cd #{gem[:dir]} && bundle exec rake"
49
+ Dir.chdir(gem[:dir]) do
50
+ sh quiet_ruby_env, "bundle", "exec", "rake", verbose: false
51
+ end
33
52
  end
34
53
 
35
54
  SHARED_TASKS.each do |task|
36
55
  task task do
37
56
  Rake::Task[:sync_crates].invoke
38
- sh "cd #{gem[:dir]} && bundle exec rake #{task}"
57
+ Dir.chdir(gem[:dir]) do
58
+ sh quiet_ruby_env, "bundle", "exec", "rake", task.to_s, verbose: false
59
+ end
39
60
  end
40
61
  end
41
62
  end
@@ -31,13 +31,14 @@ async-trait = "0.1.53"
31
31
  rustls = { version = "0.23", default-features = false, features = ["ring"] }
32
32
  time = "0.3.36" # force the transitive dependency to a more recent minimal version. The build fails with 0.3.20
33
33
 
34
- tokio = { version = "1.20.1", default-features = false }
34
+ tokio = { version = "1.20.1", default-features = false, features = ["fs", "io-util", "rt", "sync", "time"] }
35
35
  tokio-rustls = { version = "0.26", default-features = false, features = [
36
36
  "tls12",
37
37
  ] }
38
38
  reqwest = { version = "0.12", default-features = false, features = [
39
39
  "rustls-tls",
40
40
  ] }
41
+ parking_lot = "0.12"
41
42
 
42
43
  # Axum
43
44
  axum-server = { version = "0.7", features = ["tokio-rustls"], optional = true }
@@ -16,7 +16,7 @@ pub struct AcmeAcceptor {
16
16
  }
17
17
 
18
18
  impl AcmeAcceptor {
19
- pub(crate) fn new(resolver: Arc<ResolvesServerCertAcme>) -> Self {
19
+ pub fn new(resolver: Arc<ResolvesServerCertAcme>) -> Self {
20
20
  let mut config = ServerConfig::builder()
21
21
  .with_no_client_auth()
22
22
  .with_cert_resolver(resolver);
@@ -1,7 +1,7 @@
1
1
  use std::sync::Arc;
2
2
 
3
3
  use crate::https_helper::{https, HttpsRequestError, Method, Response};
4
- use crate::jose::{key_authorization_sha256, sign, sign_eab, JoseError};
4
+ use crate::jose::{key_authorization, key_authorization_sha256, sign, sign_eab, JoseError};
5
5
  use base64::engine::general_purpose::URL_SAFE_NO_PAD;
6
6
  use base64::Engine;
7
7
  use rcgen::{CustomExtension, Error as RcgenError, PKCS_ECDSA_P256_SHA256};
@@ -191,7 +191,11 @@ impl Account {
191
191
  None => return Err(AcmeError::NoTlsAlpn01Challenge),
192
192
  };
193
193
  let mut params = rcgen::CertificateParams::new(vec![domain])?;
194
- let key_auth = key_authorization_sha256(&self.key_pair, &challenge.token)?;
194
+ let token = challenge
195
+ .token
196
+ .as_deref()
197
+ .ok_or(AcmeError::MissingChallengeToken)?;
198
+ let key_auth = key_authorization_sha256(&self.key_pair, token)?;
195
199
  params.custom_extensions = vec![CustomExtension::new_acme_identifier(key_auth.as_ref())];
196
200
 
197
201
  let key_pair = rcgen::KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?;
@@ -204,6 +208,24 @@ impl Account {
204
208
  let certified_key = CertifiedKey::new(vec![cert.der().clone()], pk);
205
209
  Ok((challenge, certified_key))
206
210
  }
211
+
212
+ pub fn http_01<'a>(
213
+ &self,
214
+ challenges: &'a [Challenge],
215
+ ) -> Result<(&'a Challenge, String), AcmeError> {
216
+ let challenge = challenges.iter().find(|c| c.typ == ChallengeType::Http01);
217
+
218
+ let challenge = match challenge {
219
+ Some(challenge) => challenge,
220
+ None => return Err(AcmeError::NoHttp01Challenge),
221
+ };
222
+ let token = challenge
223
+ .token
224
+ .as_deref()
225
+ .ok_or(AcmeError::MissingChallengeToken)?;
226
+
227
+ Ok((challenge, key_authorization(&self.key_pair, token)?))
228
+ }
207
229
  }
208
230
 
209
231
  #[derive(Debug, Clone, Deserialize)]
@@ -253,6 +275,8 @@ pub enum ChallengeType {
253
275
  Dns01,
254
276
  #[serde(rename = "tls-alpn-01")]
255
277
  TlsAlpn01,
278
+ #[serde(other)]
279
+ Unknown,
256
280
  }
257
281
 
258
282
  #[derive(Debug, Deserialize)]
@@ -305,7 +329,7 @@ pub struct Challenge {
305
329
  #[serde(rename = "type")]
306
330
  pub typ: ChallengeType,
307
331
  pub url: String,
308
- pub token: String,
332
+ pub token: Option<String>,
309
333
  pub error: Option<Problem>,
310
334
  }
311
335
 
@@ -335,6 +359,10 @@ pub enum AcmeError {
335
359
  Crypto(#[from] Unspecified),
336
360
  #[error("acme service response is missing {0} header")]
337
361
  MissingHeader(&'static str),
362
+ #[error("selected challenge is missing token")]
363
+ MissingChallengeToken,
364
+ #[error("no http-01 challenge found")]
365
+ NoHttp01Challenge,
338
366
  #[error("no tls-alpn-01 challenge found")]
339
367
  NoTlsAlpn01Challenge,
340
368
  }
@@ -0,0 +1,81 @@
1
+ use parking_lot::RwLock;
2
+ use std::collections::HashMap;
3
+ use std::sync::Arc;
4
+
5
+ const ACME_CHALLENGE_PREFIX: &str = "/.well-known/acme-challenge/";
6
+
7
+ #[derive(Debug, Clone, Default)]
8
+ pub struct Http01Handler {
9
+ challenges: Arc<RwLock<HashMap<String, String>>>,
10
+ }
11
+
12
+ impl Http01Handler {
13
+ pub fn new() -> Self {
14
+ Self::default()
15
+ }
16
+
17
+ pub fn add_challenge(&self, token: String, key_authorization: String) {
18
+ self.challenges.write().insert(token, key_authorization);
19
+ }
20
+
21
+ pub fn remove_challenge(&self, token: &str) {
22
+ self.challenges.write().remove(token);
23
+ }
24
+
25
+ pub fn handle_challenge_request(&self, path: &str) -> Option<String> {
26
+ let token = path.strip_prefix(ACME_CHALLENGE_PREFIX)?;
27
+ if token.is_empty()
28
+ || !token
29
+ .chars()
30
+ .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
31
+ {
32
+ return None;
33
+ }
34
+
35
+ self.challenges.read().get(token).cloned()
36
+ }
37
+ }
38
+
39
+ #[cfg(test)]
40
+ mod tests {
41
+ use super::Http01Handler;
42
+
43
+ #[test]
44
+ fn serves_registered_key_authorization() {
45
+ let handler = Http01Handler::new();
46
+ handler.add_challenge("token_123".to_string(), "token_123.thumbprint".to_string());
47
+
48
+ assert_eq!(
49
+ handler.handle_challenge_request("/.well-known/acme-challenge/token_123"),
50
+ Some("token_123.thumbprint".to_string())
51
+ );
52
+ }
53
+
54
+ #[test]
55
+ fn rejects_invalid_paths_and_tokens() {
56
+ let handler = Http01Handler::new();
57
+ handler.add_challenge("token_123".to_string(), "token_123.thumbprint".to_string());
58
+
59
+ assert_eq!(handler.handle_challenge_request("/not-acme"), None);
60
+ assert_eq!(
61
+ handler.handle_challenge_request("/.well-known/acme-challenge/"),
62
+ None
63
+ );
64
+ assert_eq!(
65
+ handler.handle_challenge_request("/.well-known/acme-challenge/invalid token"),
66
+ None
67
+ );
68
+ }
69
+
70
+ #[test]
71
+ fn removes_tokens() {
72
+ let handler = Http01Handler::new();
73
+ handler.add_challenge("token_123".to_string(), "token_123.thumbprint".to_string());
74
+ handler.remove_challenge("token_123");
75
+
76
+ assert_eq!(
77
+ handler.handle_challenge_request("/.well-known/acme-challenge/token_123"),
78
+ None
79
+ );
80
+ }
81
+ }
@@ -30,7 +30,9 @@ pub(crate) async fn https(
30
30
  let client = reqwest::ClientBuilder::new()
31
31
  .use_preconfigured_tls(client_config.clone())
32
32
  .build()?;
33
- let mut request = client.request(method, url.as_ref());
33
+ let mut request = client
34
+ .request(method, url.as_ref())
35
+ .header("User-Agent", concat!("itsi-acme/", env!("CARGO_PKG_VERSION")));
34
36
  if let Some(body) = body {
35
37
  request = request
36
38
  .body(body)
@@ -53,11 +53,15 @@ pub(crate) fn key_authorization_sha256(
53
53
  key: &EcdsaKeyPair,
54
54
  token: &str,
55
55
  ) -> Result<Digest, JoseError> {
56
- let jwk = Jwk::new(key);
57
- let key_authorization = format!("{}.{}", token, jwk.thumb_sha256_base64()?);
56
+ let key_authorization = key_authorization(key, token)?;
58
57
  Ok(digest(&SHA256, key_authorization.as_bytes()))
59
58
  }
60
59
 
60
+ pub(crate) fn key_authorization(key: &EcdsaKeyPair, token: &str) -> Result<String, JoseError> {
61
+ let jwk = Jwk::new(key);
62
+ Ok(format!("{}.{}", token, jwk.thumb_sha256_base64()?))
63
+ }
64
+
61
65
  #[derive(Serialize)]
62
66
  pub(crate) struct Body {
63
67
  protected: String,
@@ -126,6 +126,7 @@ pub mod axum;
126
126
  mod cache;
127
127
  pub mod caches;
128
128
  mod config;
129
+ mod http_challenge;
129
130
  mod https_helper;
130
131
  mod incoming;
131
132
  mod jose;
@@ -137,6 +138,7 @@ pub use tokio_rustls;
137
138
  pub use acceptor::*;
138
139
  pub use cache::*;
139
140
  pub use config::*;
141
+ pub use http_challenge::*;
140
142
  pub use incoming::*;
141
143
  pub use resolver::*;
142
144
  pub use state::*;
@@ -13,24 +13,40 @@ pub struct ResolvesServerCertAcme {
13
13
  #[derive(Debug)]
14
14
  struct Inner {
15
15
  cert: Option<Arc<CertifiedKey>>,
16
+ certs: BTreeMap<String, Arc<CertifiedKey>>,
16
17
  auth_keys: BTreeMap<String, Arc<CertifiedKey>>,
17
18
  }
18
19
 
19
20
  impl ResolvesServerCertAcme {
20
- pub(crate) fn new() -> Arc<Self> {
21
+ pub fn new() -> Arc<Self> {
21
22
  Arc::new(Self {
22
23
  inner: Mutex::new(Inner {
23
24
  cert: None,
25
+ certs: Default::default(),
24
26
  auth_keys: Default::default(),
25
27
  }),
26
28
  })
27
29
  }
28
- pub(crate) fn set_cert(&self, cert: Arc<CertifiedKey>) {
30
+ pub fn set_cert(&self, cert: Arc<CertifiedKey>) {
29
31
  self.inner.lock().unwrap().cert = Some(cert);
30
32
  }
31
- pub(crate) fn set_auth_key(&self, domain: String, cert: Arc<CertifiedKey>) {
33
+ pub fn set_cert_for_domain(&self, domain: String, cert: Arc<CertifiedKey>) {
34
+ let mut inner = self.inner.lock().unwrap();
35
+ if inner.cert.is_none() {
36
+ inner.cert = Some(cert.clone());
37
+ }
38
+ inner.certs.insert(domain, cert);
39
+ }
40
+ pub fn remove_cert_for_domain(&self, domain: &str) {
41
+ self.inner.lock().unwrap().certs.remove(domain);
42
+ }
43
+ pub fn set_auth_key(&self, domain: String, cert: Arc<CertifiedKey>) {
32
44
  self.inner.lock().unwrap().auth_keys.insert(domain, cert);
33
45
  }
46
+
47
+ pub fn remove_auth_key(&self, domain: &str) {
48
+ self.inner.lock().unwrap().auth_keys.remove(domain);
49
+ }
34
50
  }
35
51
 
36
52
  impl ResolvesServerCert for ResolvesServerCertAcme {
@@ -53,7 +69,14 @@ impl ResolvesServerCert for ResolvesServerCertAcme {
53
69
  }
54
70
  }
55
71
  } else {
56
- self.inner.lock().unwrap().cert.clone()
72
+ let inner = self.inner.lock().unwrap();
73
+ match client_hello.server_name() {
74
+ Some(domain) => {
75
+ let domain = AsRef::<str>::as_ref(&domain);
76
+ inner.certs.get(domain).cloned().or_else(|| inner.cert.clone())
77
+ }
78
+ None => inner.cert.clone(),
79
+ }
57
80
  }
58
81
  }
59
82
  }