itsi 0.2.25 → 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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -3
  3. data/Cargo.lock +940 -988
  4. data/Cargo.toml +0 -1
  5. data/Dockerfile +2 -2
  6. data/Rakefile +24 -7
  7. data/crates/itsi_acme/Cargo.toml +2 -1
  8. data/crates/itsi_acme/src/acceptor.rs +1 -1
  9. data/crates/itsi_acme/src/acme.rs +31 -3
  10. data/crates/itsi_acme/src/http_challenge.rs +81 -0
  11. data/crates/itsi_acme/src/https_helper.rs +3 -1
  12. data/crates/itsi_acme/src/jose.rs +6 -2
  13. data/crates/itsi_acme/src/lib.rs +2 -0
  14. data/crates/itsi_acme/src/resolver.rs +27 -4
  15. data/crates/itsi_acme/src/state.rs +183 -22
  16. data/crates/itsi_scheduler/Cargo.toml +1 -1
  17. data/crates/itsi_scheduler/src/itsi_scheduler.rs +115 -64
  18. data/crates/itsi_scheduler/src/lib.rs +2 -1
  19. data/crates/itsi_server/Cargo.lock +2 -2
  20. data/crates/itsi_server/Cargo.toml +2 -1
  21. data/crates/itsi_server/src/lib.rs +15 -0
  22. data/crates/itsi_server/src/ruby_types/itsi_http_request.rs +9 -0
  23. data/crates/itsi_server/src/ruby_types/itsi_http_response.rs +95 -0
  24. data/crates/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +22 -1
  25. data/crates/itsi_server/src/ruby_types/itsi_server.rs +100 -0
  26. data/crates/itsi_server/src/server/binds/listener.rs +9 -24
  27. data/crates/itsi_server/src/server/binds/tls.rs +372 -67
  28. data/crates/itsi_server/src/services/itsi_http_service.rs +46 -2
  29. data/gems/scheduler/Cargo.lock +940 -988
  30. data/gems/scheduler/Cargo.toml +0 -1
  31. data/gems/scheduler/Gemfile +8 -2
  32. data/gems/scheduler/Gemfile.lock +107 -0
  33. data/gems/scheduler/Rakefile +33 -9
  34. data/gems/scheduler/itsi-scheduler.gemspec +0 -2
  35. data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
  36. data/gems/scheduler/lib/itsi/scheduler.rb +121 -6
  37. data/gems/scheduler/test/helpers/test_helper.rb +2 -0
  38. data/gems/scheduler/test/test_address_resolve.rb +8 -2
  39. data/gems/scheduler/test/test_itsi_scheduler.rb +80 -0
  40. data/gems/scheduler/test/test_timeout_after.rb +102 -0
  41. data/gems/server/Cargo.lock +940 -988
  42. data/gems/server/Cargo.toml +0 -1
  43. data/gems/server/Gemfile +2 -0
  44. data/gems/server/Gemfile.lock +123 -0
  45. data/gems/server/Rakefile +18 -5
  46. data/gems/server/itsi-server.gemspec +0 -2
  47. data/gems/server/lib/itsi/http_request.rb +10 -0
  48. data/gems/server/lib/itsi/server/rack_interface.rb +45 -2
  49. data/gems/server/lib/itsi/server/version.rb +1 -1
  50. data/gems/server/lib/itsi/server.rb +24 -0
  51. data/gems/server/test/acme/local_acme_challenges.rb +190 -0
  52. data/gems/server/test/helpers/local_acme.rb +218 -0
  53. data/gems/server/test/helpers/test_helper.rb +7 -11
  54. data/gems/server/test/middleware/endpoint.rb +9 -6
  55. data/gems/server/test/rack/test_rack_server.rb +79 -0
  56. data/lib/itsi/version.rb +1 -1
  57. metadata +11 -59
  58. data/gems/scheduler/vendor/rb-sys-build/.cargo-ok +0 -1
  59. data/gems/scheduler/vendor/rb-sys-build/.cargo_vcs_info.json +0 -6
  60. data/gems/scheduler/vendor/rb-sys-build/Cargo.lock +0 -294
  61. data/gems/scheduler/vendor/rb-sys-build/Cargo.toml +0 -71
  62. data/gems/scheduler/vendor/rb-sys-build/Cargo.toml.orig +0 -32
  63. data/gems/scheduler/vendor/rb-sys-build/LICENSE-APACHE +0 -190
  64. data/gems/scheduler/vendor/rb-sys-build/LICENSE-MIT +0 -21
  65. data/gems/scheduler/vendor/rb-sys-build/src/bindings/sanitizer.rs +0 -185
  66. data/gems/scheduler/vendor/rb-sys-build/src/bindings/stable_api.rs +0 -247
  67. data/gems/scheduler/vendor/rb-sys-build/src/bindings/wrapper.h +0 -71
  68. data/gems/scheduler/vendor/rb-sys-build/src/bindings.rs +0 -280
  69. data/gems/scheduler/vendor/rb-sys-build/src/cc.rs +0 -421
  70. data/gems/scheduler/vendor/rb-sys-build/src/lib.rs +0 -12
  71. data/gems/scheduler/vendor/rb-sys-build/src/rb_config/flags.rs +0 -101
  72. data/gems/scheduler/vendor/rb-sys-build/src/rb_config/library.rs +0 -132
  73. data/gems/scheduler/vendor/rb-sys-build/src/rb_config/search_path.rs +0 -57
  74. data/gems/scheduler/vendor/rb-sys-build/src/rb_config.rs +0 -906
  75. data/gems/scheduler/vendor/rb-sys-build/src/utils.rs +0 -53
  76. data/gems/server/vendor/rb-sys-build/.cargo-ok +0 -1
  77. data/gems/server/vendor/rb-sys-build/.cargo_vcs_info.json +0 -6
  78. data/gems/server/vendor/rb-sys-build/Cargo.lock +0 -294
  79. data/gems/server/vendor/rb-sys-build/Cargo.toml +0 -71
  80. data/gems/server/vendor/rb-sys-build/Cargo.toml.orig +0 -32
  81. data/gems/server/vendor/rb-sys-build/LICENSE-APACHE +0 -190
  82. data/gems/server/vendor/rb-sys-build/LICENSE-MIT +0 -21
  83. data/gems/server/vendor/rb-sys-build/src/bindings/sanitizer.rs +0 -185
  84. data/gems/server/vendor/rb-sys-build/src/bindings/stable_api.rs +0 -247
  85. data/gems/server/vendor/rb-sys-build/src/bindings/wrapper.h +0 -71
  86. data/gems/server/vendor/rb-sys-build/src/bindings.rs +0 -280
  87. data/gems/server/vendor/rb-sys-build/src/cc.rs +0 -421
  88. data/gems/server/vendor/rb-sys-build/src/lib.rs +0 -12
  89. data/gems/server/vendor/rb-sys-build/src/rb_config/flags.rs +0 -101
  90. data/gems/server/vendor/rb-sys-build/src/rb_config/library.rs +0 -132
  91. data/gems/server/vendor/rb-sys-build/src/rb_config/search_path.rs +0 -57
  92. data/gems/server/vendor/rb-sys-build/src/rb_config.rs +0 -906
  93. data/gems/server/vendor/rb-sys-build/src/utils.rs +0 -53
  94. data/vendor/rb-sys-build/.cargo-ok +0 -1
  95. data/vendor/rb-sys-build/.cargo_vcs_info.json +0 -6
  96. data/vendor/rb-sys-build/Cargo.lock +0 -294
  97. data/vendor/rb-sys-build/Cargo.toml +0 -71
  98. data/vendor/rb-sys-build/Cargo.toml.orig +0 -32
  99. data/vendor/rb-sys-build/LICENSE-APACHE +0 -190
  100. data/vendor/rb-sys-build/LICENSE-MIT +0 -21
  101. data/vendor/rb-sys-build/src/bindings/sanitizer.rs +0 -185
  102. data/vendor/rb-sys-build/src/bindings/stable_api.rs +0 -247
  103. data/vendor/rb-sys-build/src/bindings/wrapper.h +0 -71
  104. data/vendor/rb-sys-build/src/bindings.rs +0 -280
  105. data/vendor/rb-sys-build/src/cc.rs +0 -421
  106. data/vendor/rb-sys-build/src/lib.rs +0 -12
  107. data/vendor/rb-sys-build/src/rb_config/flags.rs +0 -101
  108. data/vendor/rb-sys-build/src/rb_config/library.rs +0 -132
  109. data/vendor/rb-sys-build/src/rb_config/search_path.rs +0 -57
  110. data/vendor/rb-sys-build/src/rb_config.rs +0 -906
  111. data/vendor/rb-sys-build/src/utils.rs +0 -53
data/Cargo.toml CHANGED
@@ -14,7 +14,6 @@ resolver = "2"
14
14
 
15
15
  [patch.crates-io]
16
16
  magnus = { git = "https://github.com/matsadler/magnus.git", rev = "1ed232edb2b75a2eed9b1def34ad57e55c411a5c" }
17
- rb-sys-build = { path = "vendor/rb-sys-build" }
18
17
 
19
18
 
20
19
  [profile.release]
data/Dockerfile CHANGED
@@ -3,7 +3,7 @@ FROM ruby:3.4
3
3
  RUN apt-get update && apt-get install build-essential libclang-dev -y && apt-get clean && rm -rf /var/lib/apt/lists/*
4
4
  RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
5
5
 
6
- COPY pkg/itsi-server-0.2.25.gem .
7
- RUN gem install itsi-server-0.2.25.gem
6
+ COPY pkg/itsi-server-0.2.23.gem .
7
+ RUN gem install itsi-server-0.2.23.gem
8
8
 
9
9
  CMD ["itsi", "serve"]
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
@@ -57,9 +78,7 @@ task :sync_crates do
57
78
  require 'fileutils'
58
79
  GEMS.each do |gem_info|
59
80
  ext_dir = File.join(gem_info[:dir], 'ext')
60
- vendor_dir = File.join(gem_info[:dir], 'vendor')
61
81
  FileUtils.mkdir_p(ext_dir)
62
- FileUtils.mkdir_p(vendor_dir)
63
82
 
64
83
  Dir.chdir('crates') do
65
84
  Dir['*'].each do |to_sync|
@@ -70,8 +89,6 @@ task :sync_crates do
70
89
  system("cp ../Cargo.lock ../#{gem_info[:dir]}/Cargo.lock")
71
90
  end
72
91
  end
73
-
74
- system("rsync -q -av vendor/rb-sys-build/ #{vendor_dir}/rb-sys-build --delete")
75
92
  end
76
93
  end
77
94
 
@@ -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
  }
@@ -20,8 +20,9 @@ use x509_parser::parse_x509_certificate;
20
20
 
21
21
  use crate::acceptor::AcmeAcceptor;
22
22
  use crate::acme::{
23
- Account, AcmeError, Auth, AuthStatus, Directory, Identifier, Order, OrderStatus,
23
+ Account, AcmeError, Auth, AuthStatus, Challenge, Directory, Identifier, Order, OrderStatus,
24
24
  };
25
+ use crate::http_challenge::Http01Handler;
25
26
  use crate::{AcmeConfig, Incoming, ResolvesServerCertAcme};
26
27
 
27
28
  type Timer = std::pin::Pin<Box<Sleep>>;
@@ -36,6 +37,9 @@ pub struct AcmeState<EC: Debug = Infallible, EA: Debug = EC> {
36
37
  config: Arc<AcmeConfig<EC, EA>>,
37
38
  resolver: Arc<ResolvesServerCertAcme>,
38
39
  account_key: Option<Vec<u8>>,
40
+ http01_handler: Arc<Http01Handler>,
41
+ http01_enabled: bool,
42
+ managed_domain: Option<String>,
39
43
 
40
44
  early_action: Option<BoxFuture<Event<EC, EA>>>,
41
45
  load_cert: Option<BoxFuture<Result<Option<Vec<u8>>, EC>>>,
@@ -128,12 +132,29 @@ impl<EC: 'static + Debug, EA: 'static + Debug> AcmeState<EC, EA> {
128
132
  pub fn resolver(&self) -> Arc<ResolvesServerCertAcme> {
129
133
  self.resolver.clone()
130
134
  }
135
+ pub fn http01_handler(&self) -> Arc<Http01Handler> {
136
+ self.http01_handler.clone()
137
+ }
138
+ pub fn set_http01_enabled(&mut self, enabled: bool) {
139
+ self.http01_enabled = enabled;
140
+ }
131
141
  pub fn new(config: AcmeConfig<EC, EA>) -> Self {
142
+ Self::new_with_resolver(config, ResolvesServerCertAcme::new(), Arc::new(Http01Handler::new()), None)
143
+ }
144
+ pub fn new_with_resolver(
145
+ config: AcmeConfig<EC, EA>,
146
+ resolver: Arc<ResolvesServerCertAcme>,
147
+ http01_handler: Arc<Http01Handler>,
148
+ managed_domain: Option<String>,
149
+ ) -> Self {
132
150
  let config = Arc::new(config);
133
151
  Self {
134
152
  config: config.clone(),
135
- resolver: ResolvesServerCertAcme::new(),
153
+ resolver,
136
154
  account_key: None,
155
+ http01_handler,
156
+ http01_enabled: false,
157
+ managed_domain,
137
158
  early_action: None,
138
159
  load_cert: Some(Box::pin({
139
160
  let config = config.clone();
@@ -195,7 +216,11 @@ impl<EC: 'static + Debug, EA: 'static + Debug> AcmeState<EC, EA> {
195
216
  }
196
217
  }
197
218
  };
198
- self.resolver.set_cert(Arc::new(cert));
219
+ let cert = Arc::new(cert);
220
+ match self.managed_domain.as_ref() {
221
+ Some(domain) => self.resolver.set_cert_for_domain(domain.clone(), cert),
222
+ None => self.resolver.set_cert(cert),
223
+ }
199
224
  let wait_duration = (validity[1] - (validity[1] - validity[0]) / 3 - Utc::now())
200
225
  .max(chrono::Duration::zero())
201
226
  .to_std()
@@ -220,6 +245,8 @@ impl<EC: 'static + Debug, EA: 'static + Debug> AcmeState<EC, EA> {
220
245
  async fn order(
221
246
  config: Arc<AcmeConfig<EC, EA>>,
222
247
  resolver: Arc<ResolvesServerCertAcme>,
248
+ http01_handler: Arc<Http01Handler>,
249
+ http01_enabled: bool,
223
250
  key_pair: Vec<u8>,
224
251
  ) -> Result<Vec<u8>, OrderError> {
225
252
  let directory = Directory::discover(&config.client_config, &config.directory_url).await?;
@@ -242,10 +269,16 @@ impl<EC: 'static + Debug, EA: 'static + Debug> AcmeState<EC, EA> {
242
269
  loop {
243
270
  match order.status {
244
271
  OrderStatus::Pending => {
245
- let auth_futures = order
246
- .authorizations
247
- .iter()
248
- .map(|url| Self::authorize(&config, &resolver, &account, url));
272
+ let auth_futures = order.authorizations.iter().map(|url| {
273
+ Self::authorize(
274
+ &config,
275
+ &resolver,
276
+ &http01_handler,
277
+ http01_enabled,
278
+ &account,
279
+ url,
280
+ )
281
+ });
249
282
  try_join_all(auth_futures).await?;
250
283
  log::info!("completed all authorizations");
251
284
  order = account.order(&config.client_config, &order_url).await?;
@@ -289,40 +322,147 @@ impl<EC: 'static + Debug, EA: 'static + Debug> AcmeState<EC, EA> {
289
322
  async fn authorize(
290
323
  config: &AcmeConfig<EC, EA>,
291
324
  resolver: &ResolvesServerCertAcme,
325
+ http01_handler: &Http01Handler,
326
+ http01_enabled: bool,
292
327
  account: &Account,
293
328
  url: &String,
294
329
  ) -> Result<(), OrderError> {
295
330
  let auth = account.auth(&config.client_config, url).await?;
296
- let (domain, challenge_url) = match auth.status {
331
+ let (domain, challenges) = match auth.status {
297
332
  AuthStatus::Pending => {
298
333
  let Identifier::Dns(domain) = auth.identifier;
299
- log::info!("trigger challenge for {}", &domain);
334
+ (domain, auth.challenges)
335
+ }
336
+ AuthStatus::Valid => return Ok(()),
337
+ _ => return Err(OrderError::BadAuth(auth)),
338
+ };
339
+
340
+ log::info!("trigger challenge for {}", &domain);
341
+
342
+ let primary = if http01_enabled {
343
+ ChallengeKind::Http01
344
+ } else {
345
+ ChallengeKind::TlsAlpn01
346
+ };
347
+ let secondary = match primary {
348
+ ChallengeKind::Http01 => ChallengeKind::TlsAlpn01,
349
+ ChallengeKind::TlsAlpn01 => ChallengeKind::Http01,
350
+ };
351
+
352
+ match Self::attempt_authorization(
353
+ config,
354
+ resolver,
355
+ http01_handler,
356
+ account,
357
+ &domain,
358
+ url,
359
+ &challenges,
360
+ primary,
361
+ )
362
+ .await?
363
+ {
364
+ AttemptOutcome::Validated => return Ok(()),
365
+ AttemptOutcome::Unavailable | AttemptOutcome::RetryableFailure => {}
366
+ }
367
+
368
+ match Self::attempt_authorization(
369
+ config,
370
+ resolver,
371
+ http01_handler,
372
+ account,
373
+ &domain,
374
+ url,
375
+ &challenges,
376
+ secondary,
377
+ )
378
+ .await?
379
+ {
380
+ AttemptOutcome::Validated => Ok(()),
381
+ AttemptOutcome::Unavailable | AttemptOutcome::RetryableFailure => {
382
+ Err(OrderError::TooManyAttemptsAuth(domain))
383
+ }
384
+ }
385
+ }
386
+
387
+ async fn attempt_authorization(
388
+ config: &AcmeConfig<EC, EA>,
389
+ resolver: &ResolvesServerCertAcme,
390
+ http01_handler: &Http01Handler,
391
+ account: &Account,
392
+ domain: &str,
393
+ auth_url: &str,
394
+ challenges: &[Challenge],
395
+ kind: ChallengeKind,
396
+ ) -> Result<AttemptOutcome, OrderError> {
397
+ match kind {
398
+ ChallengeKind::TlsAlpn01 => {
300
399
  let (challenge, auth_key) =
301
- account.tls_alpn_01(&auth.challenges, domain.clone())?;
302
- resolver.set_auth_key(domain.clone(), Arc::new(auth_key));
400
+ match account.tls_alpn_01(challenges, domain.to_string()) {
401
+ Ok(value) => value,
402
+ Err(AcmeError::NoTlsAlpn01Challenge) => {
403
+ return Ok(AttemptOutcome::Unavailable);
404
+ }
405
+ Err(error) => return Err(OrderError::Acme(error)),
406
+ };
407
+
408
+ resolver.set_auth_key(domain.to_string(), Arc::new(auth_key));
303
409
  account
304
410
  .challenge(&config.client_config, &challenge.url)
305
411
  .await?;
306
- (domain, challenge.url.clone())
412
+ let outcome =
413
+ Self::poll_authorization(config, account, domain, auth_url, &challenge.url)
414
+ .await?;
415
+ resolver.remove_auth_key(domain);
416
+ Ok(outcome)
307
417
  }
308
- AuthStatus::Valid => return Ok(()),
309
- _ => return Err(OrderError::BadAuth(auth)),
310
- };
418
+ ChallengeKind::Http01 => {
419
+ let (challenge, key_authorization) = match account.http_01(challenges) {
420
+ Ok(value) => value,
421
+ Err(AcmeError::NoHttp01Challenge) => return Ok(AttemptOutcome::Unavailable),
422
+ Err(error) => return Err(OrderError::Acme(error)),
423
+ };
424
+ let token = challenge
425
+ .token
426
+ .clone()
427
+ .ok_or(OrderError::Acme(AcmeError::MissingChallengeToken))?;
428
+
429
+ http01_handler.add_challenge(token.clone(), key_authorization);
430
+ account
431
+ .challenge(&config.client_config, &challenge.url)
432
+ .await?;
433
+ let outcome =
434
+ Self::poll_authorization(config, account, domain, auth_url, &challenge.url)
435
+ .await?;
436
+ http01_handler.remove_challenge(&token);
437
+ Ok(outcome)
438
+ }
439
+ }
440
+ }
441
+
442
+ async fn poll_authorization(
443
+ config: &AcmeConfig<EC, EA>,
444
+ account: &Account,
445
+ domain: &str,
446
+ auth_url: &str,
447
+ challenge_url: &str,
448
+ ) -> Result<AttemptOutcome, OrderError> {
311
449
  for i in 0u64..5 {
312
450
  after(Duration::from_secs(1u64 << i)).await;
313
- let auth = account.auth(&config.client_config, url).await?;
451
+ let auth = account.auth(&config.client_config, auth_url).await?;
314
452
  match auth.status {
315
453
  AuthStatus::Pending => {
316
- log::info!("authorization for {} still pending", &domain);
454
+ log::info!("authorization for {} still pending", domain);
317
455
  account
318
- .challenge(&config.client_config, &challenge_url)
319
- .await?
456
+ .challenge(&config.client_config, challenge_url)
457
+ .await?;
320
458
  }
321
- AuthStatus::Valid => return Ok(()),
459
+ AuthStatus::Valid => return Ok(AttemptOutcome::Validated),
460
+ AuthStatus::Invalid => return Ok(AttemptOutcome::RetryableFailure),
322
461
  _ => return Err(OrderError::BadAuth(auth)),
323
462
  }
324
463
  }
325
- Err(OrderError::TooManyAttemptsAuth(domain))
464
+
465
+ Ok(AttemptOutcome::RetryableFailure)
326
466
  }
327
467
  fn poll_next_infinite(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Event<EC, EA>> {
328
468
  loop {
@@ -408,13 +548,34 @@ impl<EC: 'static + Debug, EA: 'static + Debug> AcmeState<EC, EA> {
408
548
  };
409
549
  let config = self.config.clone();
410
550
  let resolver = self.resolver.clone();
551
+ let http01_handler = self.http01_handler.clone();
552
+ let http01_enabled = self.http01_enabled;
411
553
  self.order = Some(Box::pin({
412
- Self::order(config.clone(), resolver.clone(), account_key)
554
+ Self::order(
555
+ config.clone(),
556
+ resolver.clone(),
557
+ http01_handler,
558
+ http01_enabled,
559
+ account_key,
560
+ )
413
561
  }));
414
562
  }
415
563
  }
416
564
  }
417
565
 
566
+ #[derive(Clone, Copy, Debug)]
567
+ enum ChallengeKind {
568
+ Http01,
569
+ TlsAlpn01,
570
+ }
571
+
572
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
573
+ enum AttemptOutcome {
574
+ Validated,
575
+ RetryableFailure,
576
+ Unavailable,
577
+ }
578
+
418
579
  impl<EC: 'static + Debug, EA: 'static + Debug> Stream for AcmeState<EC, EA> {
419
580
  type Item = Event<EC, EA>;
420
581
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "itsi-scheduler"
3
- version = "0.2.25"
3
+ version = "0.2.27-rc1"
4
4
  edition = "2021"
5
5
  authors = ["Wouter Coppieters <wc@pico.net.nz>"]
6
6
  license = "MIT"