itsi-server 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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +939 -987
  3. data/Cargo.toml +0 -1
  4. data/Rakefile +18 -5
  5. data/ext/itsi_acme/Cargo.toml +2 -1
  6. data/ext/itsi_acme/src/acceptor.rs +1 -1
  7. data/ext/itsi_acme/src/acme.rs +31 -3
  8. data/ext/itsi_acme/src/http_challenge.rs +81 -0
  9. data/ext/itsi_acme/src/https_helper.rs +3 -1
  10. data/ext/itsi_acme/src/jose.rs +6 -2
  11. data/ext/itsi_acme/src/lib.rs +2 -0
  12. data/ext/itsi_acme/src/resolver.rs +27 -4
  13. data/ext/itsi_acme/src/state.rs +183 -22
  14. data/ext/itsi_scheduler/Cargo.toml +1 -1
  15. data/ext/itsi_scheduler/src/itsi_scheduler.rs +115 -64
  16. data/ext/itsi_scheduler/src/lib.rs +2 -1
  17. data/ext/itsi_server/Cargo.lock +2 -2
  18. data/ext/itsi_server/Cargo.toml +2 -1
  19. data/ext/itsi_server/src/lib.rs +15 -0
  20. data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +9 -0
  21. data/ext/itsi_server/src/ruby_types/itsi_http_response.rs +95 -0
  22. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +22 -1
  23. data/ext/itsi_server/src/ruby_types/itsi_server.rs +100 -0
  24. data/ext/itsi_server/src/server/binds/listener.rs +9 -24
  25. data/ext/itsi_server/src/server/binds/tls.rs +372 -67
  26. data/ext/itsi_server/src/services/itsi_http_service.rs +46 -2
  27. data/lib/itsi/http_request.rb +10 -0
  28. data/lib/itsi/server/rack_interface.rb +45 -2
  29. data/lib/itsi/server/version.rb +1 -1
  30. data/lib/itsi/server.rb +24 -0
  31. metadata +3 -20
  32. data/vendor/rb-sys-build/.cargo-ok +0 -1
  33. data/vendor/rb-sys-build/.cargo_vcs_info.json +0 -6
  34. data/vendor/rb-sys-build/Cargo.lock +0 -294
  35. data/vendor/rb-sys-build/Cargo.toml +0 -71
  36. data/vendor/rb-sys-build/Cargo.toml.orig +0 -32
  37. data/vendor/rb-sys-build/LICENSE-APACHE +0 -190
  38. data/vendor/rb-sys-build/LICENSE-MIT +0 -21
  39. data/vendor/rb-sys-build/src/bindings/sanitizer.rs +0 -185
  40. data/vendor/rb-sys-build/src/bindings/stable_api.rs +0 -247
  41. data/vendor/rb-sys-build/src/bindings/wrapper.h +0 -71
  42. data/vendor/rb-sys-build/src/bindings.rs +0 -280
  43. data/vendor/rb-sys-build/src/cc.rs +0 -421
  44. data/vendor/rb-sys-build/src/lib.rs +0 -12
  45. data/vendor/rb-sys-build/src/rb_config/flags.rs +0 -101
  46. data/vendor/rb-sys-build/src/rb_config/library.rs +0 -132
  47. data/vendor/rb-sys-build/src/rb_config/search_path.rs +0 -57
  48. data/vendor/rb-sys-build/src/rb_config.rs +0 -906
  49. data/vendor/rb-sys-build/src/utils.rs +0 -53
@@ -1,8 +1,9 @@
1
1
  use base64::{engine::general_purpose, Engine as _};
2
- use itsi_acme::{AcmeAcceptor, AcmeConfig, AcmeState};
2
+ use itsi_acme::{AcmeAcceptor, AcmeConfig, AcmeState, Http01Handler, ResolvesServerCertAcme};
3
3
  use itsi_error::Result;
4
- use itsi_tracing::info;
4
+ use itsi_tracing::{error, info};
5
5
  use locked_dir_cache::LockedDirCache;
6
+ use parking_lot::{Mutex as ParkingMutex, RwLock as ParkingRwLock};
6
7
  use rcgen::ExtendedKeyUsagePurpose;
7
8
  use rcgen::{
8
9
  BasicConstraints, CertificateParams, DistinguishedName, DnType, IsCa, KeyPair, KeyUsagePurpose,
@@ -17,9 +18,14 @@ use std::{
17
18
  collections::HashMap,
18
19
  fs,
19
20
  io::{BufReader, Error},
20
- sync::Arc,
21
+ sync::{
22
+ atomic::{AtomicBool, Ordering},
23
+ Arc,
24
+ },
25
+ thread::JoinHandle,
21
26
  };
22
- use tokio::sync::Mutex;
27
+ use tokio::runtime::Builder as RuntimeBuilder;
28
+ use tokio::sync::{mpsc, watch};
23
29
  use tokio_rustls::{rustls::ServerConfig, TlsAcceptor};
24
30
 
25
31
  use crate::env::{
@@ -29,14 +35,300 @@ use crate::env::{
29
35
 
30
36
  mod locked_dir_cache;
31
37
 
38
+ #[derive(Debug, Clone)]
39
+ pub struct ManagedTlsDomainStatus {
40
+ pub domain: String,
41
+ pub status: String,
42
+ pub last_error: Option<String>,
43
+ }
44
+
45
+ #[derive(Clone)]
46
+ struct DynamicAcmeConfigTemplate {
47
+ client_config: Arc<ClientConfig>,
48
+ directory_url: String,
49
+ contact: Vec<String>,
50
+ cache_dir: String,
51
+ }
52
+
53
+ impl DynamicAcmeConfigTemplate {
54
+ fn state_for_domain(
55
+ &self,
56
+ domain: &str,
57
+ resolver: Arc<ResolvesServerCertAcme>,
58
+ http01_handler: Arc<Http01Handler>,
59
+ http01_enabled: bool,
60
+ ) -> AcmeState<Error> {
61
+ let state = AcmeConfig::new([domain])
62
+ .contact(self.contact.clone())
63
+ .cache(LockedDirCache::new(self.cache_dir.clone()))
64
+ .directory(&self.directory_url)
65
+ .client_tls_config(self.client_config.clone());
66
+ let mut state = AcmeState::new_with_resolver(
67
+ state,
68
+ resolver,
69
+ http01_handler,
70
+ Some(domain.to_string()),
71
+ );
72
+ state.set_http01_enabled(http01_enabled);
73
+ state
74
+ }
75
+ }
76
+
77
+ enum DynamicAcmeCommand {
78
+ Register(String),
79
+ Unregister(String),
80
+ Shutdown,
81
+ }
82
+
83
+ #[derive(Clone)]
84
+ pub struct DynamicAcmeManager {
85
+ inner: Arc<DynamicAcmeManagerInner>,
86
+ }
87
+
88
+ struct DynamicAcmeManagerInner {
89
+ resolver: Arc<ResolvesServerCertAcme>,
90
+ http01_registry: Arc<ParkingRwLock<HashMap<String, Arc<Http01Handler>>>>,
91
+ statuses: Arc<ParkingRwLock<HashMap<String, ManagedTlsDomainStatus>>>,
92
+ http01_enabled: Arc<AtomicBool>,
93
+ initialized: AtomicBool,
94
+ initial_domains: Vec<String>,
95
+ command_tx: mpsc::UnboundedSender<DynamicAcmeCommand>,
96
+ thread_handle: ParkingMutex<Option<JoinHandle<()>>>,
97
+ }
98
+
99
+ impl DynamicAcmeManager {
100
+ fn new(template: DynamicAcmeConfigTemplate, initial_domains: Vec<String>) -> Self {
101
+ let resolver = ResolvesServerCertAcme::new();
102
+ let http01_handler = Arc::new(Http01Handler::new());
103
+ let http01_registry = Arc::new(ParkingRwLock::new(HashMap::new()));
104
+ let statuses = Arc::new(ParkingRwLock::new(HashMap::new()));
105
+ let http01_enabled = Arc::new(AtomicBool::new(false));
106
+ let (command_tx, mut command_rx) = mpsc::unbounded_channel();
107
+
108
+ let resolver_clone = resolver.clone();
109
+ let http01_handler_clone = http01_handler.clone();
110
+ let http01_registry_clone = http01_registry.clone();
111
+ let statuses_clone = statuses.clone();
112
+ let http01_enabled_clone = http01_enabled.clone();
113
+
114
+ let thread_handle = std::thread::spawn(move || {
115
+ let runtime = RuntimeBuilder::new_current_thread()
116
+ .enable_all()
117
+ .build()
118
+ .expect("Failed to build dynamic ACME runtime");
119
+ runtime.block_on(async move {
120
+ let mut cancellations: HashMap<String, watch::Sender<bool>> = HashMap::new();
121
+
122
+ while let Some(command) = command_rx.recv().await {
123
+ match command {
124
+ DynamicAcmeCommand::Register(domain) => {
125
+ let domain = domain.to_ascii_lowercase();
126
+ if cancellations.contains_key(&domain) {
127
+ continue;
128
+ }
129
+
130
+ statuses_clone.write().insert(
131
+ domain.clone(),
132
+ ManagedTlsDomainStatus {
133
+ domain: domain.clone(),
134
+ status: "pending".to_string(),
135
+ last_error: None,
136
+ },
137
+ );
138
+ http01_registry_clone
139
+ .write()
140
+ .insert(domain.clone(), http01_handler_clone.clone());
141
+
142
+ let (cancel_tx, mut cancel_rx) = watch::channel(false);
143
+ cancellations.insert(domain.clone(), cancel_tx);
144
+
145
+ let resolver = resolver_clone.clone();
146
+ let http01_handler = http01_handler_clone.clone();
147
+ let registry = http01_registry_clone.clone();
148
+ let statuses = statuses_clone.clone();
149
+ let template = template.clone();
150
+ let enabled = http01_enabled_clone.clone();
151
+ let task_domain = domain.clone();
152
+
153
+ tokio::spawn(async move {
154
+ statuses.write().insert(
155
+ task_domain.clone(),
156
+ ManagedTlsDomainStatus {
157
+ domain: task_domain.clone(),
158
+ status: "issuing".to_string(),
159
+ last_error: None,
160
+ },
161
+ );
162
+ let mut state = template.state_for_domain(
163
+ &task_domain,
164
+ resolver.clone(),
165
+ http01_handler,
166
+ enabled.load(Ordering::SeqCst),
167
+ );
168
+
169
+ loop {
170
+ tokio::select! {
171
+ changed = cancel_rx.changed() => {
172
+ if changed.is_ok() && *cancel_rx.borrow() {
173
+ resolver.remove_auth_key(&task_domain);
174
+ resolver.remove_cert_for_domain(&task_domain);
175
+ registry.write().remove(&task_domain);
176
+ statuses.write().remove(&task_domain);
177
+ break;
178
+ }
179
+ }
180
+ event = futures::StreamExt::next(&mut state) => {
181
+ match event {
182
+ Some(Ok(_)) => {
183
+ let mut statuses = statuses.write();
184
+ if let Some(status) = statuses.get_mut(&task_domain) {
185
+ status.status = "active".to_string();
186
+ status.last_error = None;
187
+ }
188
+ }
189
+ Some(Err(err)) => {
190
+ let mut statuses = statuses.write();
191
+ if let Some(status) = statuses.get_mut(&task_domain) {
192
+ status.status = "failed".to_string();
193
+ status.last_error = Some(err.to_string());
194
+ }
195
+ }
196
+ None => break,
197
+ }
198
+ }
199
+ }
200
+ }
201
+ });
202
+ }
203
+ DynamicAcmeCommand::Unregister(domain) => {
204
+ let domain = domain.to_ascii_lowercase();
205
+ if let Some(cancel_tx) = cancellations.remove(&domain) {
206
+ let _ = cancel_tx.send(true);
207
+ } else {
208
+ http01_registry_clone.write().remove(&domain);
209
+ resolver_clone.remove_auth_key(&domain);
210
+ resolver_clone.remove_cert_for_domain(&domain);
211
+ statuses_clone.write().remove(&domain);
212
+ }
213
+ }
214
+ DynamicAcmeCommand::Shutdown => {
215
+ for (_, cancel_tx) in cancellations.drain() {
216
+ let _ = cancel_tx.send(true);
217
+ }
218
+ break;
219
+ }
220
+ }
221
+ }
222
+ });
223
+ });
224
+
225
+ Self {
226
+ inner: Arc::new(DynamicAcmeManagerInner {
227
+ resolver,
228
+ http01_registry,
229
+ statuses,
230
+ http01_enabled,
231
+ initialized: AtomicBool::new(false),
232
+ initial_domains,
233
+ command_tx,
234
+ thread_handle: ParkingMutex::new(Some(thread_handle)),
235
+ }),
236
+ }
237
+ }
238
+
239
+ pub fn resolver(&self) -> Arc<ResolvesServerCertAcme> {
240
+ self.inner.resolver.clone()
241
+ }
242
+
243
+ pub fn set_http01_enabled(&self, enabled: bool) {
244
+ self.inner.http01_enabled.store(enabled, Ordering::SeqCst);
245
+ }
246
+
247
+ pub fn initialize_domains(&self) {
248
+ if self.inner.initialized.swap(true, Ordering::SeqCst) {
249
+ return;
250
+ }
251
+
252
+ for domain in &self.inner.initial_domains {
253
+ self.register_domain(domain.clone());
254
+ }
255
+ }
256
+
257
+ pub fn register_domain(&self, domain: String) {
258
+ let _ = self
259
+ .inner
260
+ .command_tx
261
+ .send(DynamicAcmeCommand::Register(domain));
262
+ }
263
+
264
+ pub fn unregister_domain(&self, domain: &str) {
265
+ let _ = self
266
+ .inner
267
+ .command_tx
268
+ .send(DynamicAcmeCommand::Unregister(domain.to_string()));
269
+ }
270
+
271
+ pub fn http01_response(&self, host: &str, path: &str) -> Option<String> {
272
+ self.inner
273
+ .http01_registry
274
+ .read()
275
+ .get(host)
276
+ .and_then(|handler| handler.handle_challenge_request(path))
277
+ }
278
+
279
+ pub fn statuses(&self) -> Vec<ManagedTlsDomainStatus> {
280
+ let mut statuses = self
281
+ .inner
282
+ .statuses
283
+ .read()
284
+ .values()
285
+ .cloned()
286
+ .collect::<Vec<_>>();
287
+ statuses.sort_by(|a, b| a.domain.cmp(&b.domain));
288
+ statuses
289
+ }
290
+ }
291
+
292
+ impl Drop for DynamicAcmeManagerInner {
293
+ fn drop(&mut self) {
294
+ let _ = self.command_tx.send(DynamicAcmeCommand::Shutdown);
295
+ if let Some(handle) = self.thread_handle.lock().take() {
296
+ if let Err(err) = handle.join() {
297
+ error!("Dynamic ACME manager thread join failed: {:?}", err);
298
+ }
299
+ }
300
+ }
301
+ }
302
+
32
303
  #[derive(Clone)]
33
304
  pub enum ItsiTlsAcceptor {
34
305
  Manual(TlsAcceptor),
35
- Automatic(
36
- AcmeAcceptor,
37
- Arc<Mutex<AcmeState<Error>>>,
38
- Arc<ServerConfig>,
39
- ),
306
+ Automatic {
307
+ acme_acceptor: AcmeAcceptor,
308
+ manager: DynamicAcmeManager,
309
+ server_config: Arc<ServerConfig>,
310
+ },
311
+ }
312
+
313
+ impl ItsiTlsAcceptor {
314
+ pub fn manager(&self) -> Option<DynamicAcmeManager> {
315
+ match self {
316
+ ItsiTlsAcceptor::Automatic { manager, .. } => Some(manager.clone()),
317
+ ItsiTlsAcceptor::Manual(_) => None,
318
+ }
319
+ }
320
+
321
+ pub fn set_http01_enabled(&self, enabled: bool) {
322
+ if let ItsiTlsAcceptor::Automatic { manager, .. } = self {
323
+ manager.set_http01_enabled(enabled);
324
+ }
325
+ }
326
+
327
+ pub fn initialize_domains(&self) {
328
+ if let ItsiTlsAcceptor::Automatic { manager, .. } = self {
329
+ manager.initialize_domains();
330
+ }
331
+ }
40
332
  }
41
333
 
42
334
  /// Generates a TLS configuration based on either :
@@ -52,66 +344,79 @@ pub fn configure_tls(
52
344
  let domains = query_params
53
345
  .get("domains")
54
346
  .map(|v| v.split(',').map(String::from).collect::<Vec<_>>())
55
- .or_else(|| query_params.get("domain").map(|v| vec![v.to_string()]));
347
+ .or_else(|| query_params.get("domain").map(|v| vec![v.to_string()]))
348
+ .unwrap_or_default();
56
349
 
57
350
  if query_params.get("cert").is_some_and(|c| c == "acme") {
58
- if let Some(domains) = domains {
59
- let directory_url = &*ITSI_ACME_DIRECTORY_URL;
60
- info!(
61
- domains = format!("{:?}", domains),
62
- directory_url, "Requesting acme cert"
63
- );
64
- let acme_contact_email = query_params
65
- .get("acme_email")
66
- .map(|s| s.to_string())
67
- .or_else(|| (*ITSI_ACME_CONTACT_EMAIL).as_ref().ok().map(|s| s.to_string()))
68
- .ok_or_else(|| itsi_error::ItsiError::ArgumentError(
69
- "acme_email query param or ITSI_ACME_CONTACT_EMAIL must be set before you can auto-generate let's encrypt certificates".to_string(),
70
- ))?;
71
-
72
- let acme_config = AcmeConfig::new(domains)
73
- .contact([format!("mailto:{}", acme_contact_email)])
74
- .cache(LockedDirCache::new(&*ITSI_ACME_CACHE_DIR))
75
- .directory(directory_url);
76
-
77
- let acme_state = if let Ok(ca_pem_path) = &*ITSI_ACME_CA_PEM_PATH {
78
- let mut root_cert_store = RootCertStore::empty();
79
-
80
- let ca_pem = fs::read(ca_pem_path).expect("failed to read CA pem file");
81
- let mut ca_reader = BufReader::new(&ca_pem[..]);
82
- let der_certs: Vec<CertificateDer> = certs(&mut ca_reader)
83
- .collect::<std::result::Result<Vec<CertificateDer>, _>>()
84
- .map_err(|e| {
85
- itsi_error::ItsiError::ArgumentError(format!(
86
- "Invalid ACME CA Pem path {:?}",
87
- e
88
- ))
89
- })?;
90
- root_cert_store.add_parsable_certificates(der_certs);
91
-
92
- let client_config = ClientConfig::builder()
351
+ let directory_url = &*ITSI_ACME_DIRECTORY_URL;
352
+ info!(
353
+ domains = format!("{:?}", domains),
354
+ directory_url, "Requesting acme cert"
355
+ );
356
+ let acme_contact_email = query_params
357
+ .get("acme_email")
358
+ .map(|s| s.to_string())
359
+ .or_else(|| (*ITSI_ACME_CONTACT_EMAIL).as_ref().ok().map(|s| s.to_string()))
360
+ .ok_or_else(|| itsi_error::ItsiError::ArgumentError(
361
+ "acme_email query param or ITSI_ACME_CONTACT_EMAIL must be set before you can auto-generate let's encrypt certificates".to_string(),
362
+ ))?;
363
+
364
+ let client_config = if let Ok(ca_pem_path) = &*ITSI_ACME_CA_PEM_PATH {
365
+ let mut root_cert_store = RootCertStore::empty();
366
+
367
+ let ca_pem = fs::read(ca_pem_path).expect("failed to read CA pem file");
368
+ let mut ca_reader = BufReader::new(&ca_pem[..]);
369
+ let der_certs: Vec<CertificateDer> = certs(&mut ca_reader)
370
+ .collect::<std::result::Result<Vec<CertificateDer>, _>>()
371
+ .map_err(|e| {
372
+ itsi_error::ItsiError::ArgumentError(format!("Invalid ACME CA Pem path {:?}", e))
373
+ })?;
374
+ root_cert_store.add_parsable_certificates(der_certs);
375
+
376
+ Arc::new(
377
+ ClientConfig::builder()
93
378
  .with_root_certificates(root_cert_store)
94
- .with_no_client_auth();
95
- acme_config
96
- .client_tls_config(Arc::new(client_config))
97
- .state()
98
- } else {
99
- acme_config.state()
100
- };
101
-
102
- let mut rustls_config = ServerConfig::builder()
103
- .with_no_client_auth()
104
- .with_cert_resolver(acme_state.resolver());
105
-
106
- rustls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
107
-
108
- let acceptor = acme_state.acceptor();
109
- return Ok(ItsiTlsAcceptor::Automatic(
110
- acceptor,
111
- Arc::new(Mutex::new(acme_state)),
112
- Arc::new(rustls_config),
113
- ));
114
- }
379
+ .with_no_client_auth(),
380
+ )
381
+ } else {
382
+ let mut root_store = RootCertStore::empty();
383
+ root_store.extend(
384
+ webpki_roots::TLS_SERVER_ROOTS
385
+ .iter()
386
+ .map(|ta| rustls::pki_types::TrustAnchor {
387
+ subject: ta.subject.clone(),
388
+ subject_public_key_info: ta.subject_public_key_info.clone(),
389
+ name_constraints: ta.name_constraints.clone(),
390
+ }),
391
+ );
392
+ Arc::new(
393
+ ClientConfig::builder()
394
+ .with_root_certificates(root_store)
395
+ .with_no_client_auth(),
396
+ )
397
+ };
398
+
399
+ let manager = DynamicAcmeManager::new(
400
+ DynamicAcmeConfigTemplate {
401
+ client_config,
402
+ directory_url: directory_url.to_string(),
403
+ contact: vec![format!("mailto:{}", acme_contact_email)],
404
+ cache_dir: ITSI_ACME_CACHE_DIR.to_string(),
405
+ },
406
+ domains.clone(),
407
+ );
408
+
409
+ let mut rustls_config = ServerConfig::builder()
410
+ .with_no_client_auth()
411
+ .with_cert_resolver(manager.resolver());
412
+
413
+ rustls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
414
+
415
+ return Ok(ItsiTlsAcceptor::Automatic {
416
+ acme_acceptor: AcmeAcceptor::new(manager.resolver()),
417
+ manager,
418
+ server_config: Arc::new(rustls_config),
419
+ });
115
420
  }
116
421
  let (certs, key) = if let (Some(cert_path), Some(key_path)) =
117
422
  (query_params.get("cert"), query_params.get("key"))
@@ -121,7 +426,7 @@ pub fn configure_tls(
121
426
  let key = load_private_key(key_path);
122
427
  (certs, key)
123
428
  } else {
124
- generate_ca_signed_cert(domains.unwrap_or(vec![host.to_owned()]))?
429
+ generate_ca_signed_cert(if domains.is_empty() { vec![host.to_owned()] } else { domains })?
125
430
  };
126
431
 
127
432
  let mut config = ServerConfig::builder()
@@ -1,16 +1,17 @@
1
1
  use crate::default_responses::{NOT_FOUND_RESPONSE, TIMEOUT_RESPONSE};
2
2
  use crate::ruby_types::itsi_server::itsi_server_config::ItsiServerTokenPreference;
3
3
  use crate::server::http_message_types::{
4
- ConversionExt, HttpRequest, HttpResponse, RequestExt, ResponseFormat,
4
+ ConversionExt, HttpBody, HttpRequest, HttpResponse, RequestExt, ResponseFormat,
5
5
  };
6
6
  use crate::server::lifecycle_event::LifecycleEvent;
7
7
  use crate::server::middleware_stack::MiddlewareLayer;
8
8
  use crate::server::serve_strategy::acceptor::AcceptorArgs;
9
9
  use crate::server::signal::{send_lifecycle_event, SHUTDOWN_REQUESTED};
10
+ use bytes::Bytes;
10
11
  use chrono::{self, DateTime, Local};
11
12
  use either::Either;
12
13
  use http::header::ACCEPT_ENCODING;
13
- use http::{HeaderValue, Request};
14
+ use http::{HeaderValue, Request, StatusCode};
14
15
  use hyper::body::Incoming;
15
16
  use regex::Regex;
16
17
  use smallvec::SmallVec;
@@ -174,6 +175,7 @@ impl HttpRequestContext {
174
175
  const SERVER_TOKEN_VERSION: HeaderValue =
175
176
  HeaderValue::from_static(concat!("Itsi/", env!("CARGO_PKG_VERSION")));
176
177
  const SERVER_TOKEN_NAME: HeaderValue = HeaderValue::from_static("Itsi");
178
+ const TEXT_PLAIN_UTF8: HeaderValue = HeaderValue::from_static("text/plain; charset=utf-8");
177
179
 
178
180
  impl ItsiHttpService {
179
181
  pub async fn handle_request(&self, req: Request<Incoming>) -> itsi_error::Result<HttpResponse> {
@@ -188,6 +190,10 @@ impl ItsiHttpService {
188
190
  let token_preference = self.server_params.itsi_server_token_preference;
189
191
 
190
192
  let service_future = async move {
193
+ if let Some(acme_response) = self.acme_http01_response(&req) {
194
+ return Ok(acme_response);
195
+ }
196
+
191
197
  let Some((stack, matching_pattern)) =
192
198
  self.server_params.middleware.get().unwrap().stack_for(&req)
193
199
  else {
@@ -264,4 +270,42 @@ impl ItsiHttpService {
264
270
  service_future.await
265
271
  }
266
272
  }
273
+
274
+ fn acme_http01_response(&self, req: &HttpRequest) -> Option<HttpResponse> {
275
+ let host = normalize_host_header(req.header("host")?)?;
276
+ let managers = self.server_params.acme_managers.read();
277
+ let key_authorization = managers
278
+ .iter()
279
+ .find_map(|(_, manager)| manager.http01_response(host, req.uri().path()))?;
280
+
281
+ let mut builder = http::Response::builder()
282
+ .status(StatusCode::OK)
283
+ .header(http::header::CONTENT_TYPE, TEXT_PLAIN_UTF8);
284
+
285
+ if req.method() == http::Method::HEAD {
286
+ builder = builder.header(http::header::CONTENT_LENGTH, "0");
287
+ return builder.body(HttpBody::empty()).ok();
288
+ }
289
+
290
+ builder
291
+ .header(
292
+ http::header::CONTENT_LENGTH,
293
+ key_authorization.len().to_string(),
294
+ )
295
+ .body(HttpBody::full(Bytes::from(key_authorization)))
296
+ .ok()
297
+ }
298
+ }
299
+
300
+ fn normalize_host_header(host: &str) -> Option<&str> {
301
+ let host = host.trim();
302
+ if host.is_empty() {
303
+ return None;
304
+ }
305
+
306
+ if let Some(stripped) = host.strip_prefix('[') {
307
+ return stripped.split(']').next();
308
+ }
309
+
310
+ Some(host.split(':').next().unwrap_or(host))
267
311
  }
@@ -145,6 +145,16 @@ module Itsi
145
145
  end
146
146
  end
147
147
 
148
+ def partial_hijack
149
+ UNIXSocket.pair.yield_self do |(server_sock, app_sock)|
150
+ server_sock.autoclose = false
151
+ response.partial_hijack(server_sock.fileno)
152
+ server_sock.sync = true
153
+ app_sock.sync = true
154
+ app_sock
155
+ end
156
+ end
157
+
148
158
  # Rack expects env["rack.hijack"] to respond to #call.
149
159
  def call
150
160
  hijack
@@ -1,6 +1,45 @@
1
1
  module Itsi
2
2
  class Server
3
3
  module RackInterface
4
+ class PartialHijackStream
5
+ def initialize(response)
6
+ @response = response
7
+ end
8
+
9
+ def write(chunk)
10
+ @response.write(chunk.to_s)
11
+ end
12
+
13
+ def read(*)
14
+ nil
15
+ end
16
+
17
+ def <<(chunk)
18
+ write(chunk)
19
+ self
20
+ end
21
+
22
+ def flush
23
+ self
24
+ end
25
+
26
+ def close_write
27
+ @response.close_write
28
+ end
29
+
30
+ def close_read
31
+ true
32
+ end
33
+
34
+ def close
35
+ @response.close_write
36
+ end
37
+
38
+ def closed?
39
+ @response.closed?
40
+ end
41
+ end
42
+
4
43
  # Builds a handler proc that is compatible with Rack applications.
5
44
  def self.for(app)
6
45
  require "rack"
@@ -38,7 +77,8 @@ module Itsi
38
77
  response.status = status
39
78
 
40
79
  # 2. Set Headers
41
- body_streamer = streaming_body?(body) ? body : headers.delete("rack.hijack")
80
+ hijack_callback = headers.delete("rack.hijack")
81
+ body_streamer = streaming_body?(body) ? body : hijack_callback
42
82
 
43
83
  response.reserve_headers(headers.size)
44
84
 
@@ -57,7 +97,10 @@ module Itsi
57
97
  # the server will begin to stream it to the client.
58
98
 
59
99
 
60
- if body_streamer
100
+ if hijack_callback
101
+ stream = status == 101 ? request.partial_hijack : PartialHijackStream.new(response)
102
+ body_streamer.call(stream)
103
+ elsif body_streamer
61
104
  # If we're partially hijacked or returned a streaming body,
62
105
  # stream this response.
63
106
  body_streamer.call(response)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Server
5
- VERSION = "0.2.25"
5
+ VERSION = "0.2.27.rc1"
6
6
  end
7
7
  end
data/lib/itsi/server.rb CHANGED
@@ -32,6 +32,30 @@ module Itsi
32
32
  @running && !@running.empty?
33
33
  end
34
34
 
35
+ def current_server
36
+ @running&.last || raise("No running Itsi::Server instance")
37
+ end
38
+
39
+ def tls_bindings
40
+ current_server.tls_bindings
41
+ end
42
+
43
+ def tls_domains(listener_id = nil)
44
+ current_server.tls_domains(listener_id)
45
+ end
46
+
47
+ def tls_domain_statuses(listener_id = nil)
48
+ current_server.tls_domain_statuses(listener_id)
49
+ end
50
+
51
+ def register_tls_domain(domain, listener_id = nil)
52
+ current_server.register_tls_domain(domain, listener_id)
53
+ end
54
+
55
+ def unregister_tls_domain(domain, listener_id = nil)
56
+ current_server.unregister_tls_domain(domain, listener_id)
57
+ end
58
+
35
59
  def start_in_background_thread(cli_params = {}, &blk)
36
60
  @background_threads ||= []
37
61
  server, background_thread = start(cli_params, background: true, &blk)