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
@@ -4,22 +4,19 @@ mod timer;
4
4
  use io_helpers::{build_interest, poll_readiness, set_nonblocking};
5
5
  use io_waiter::IoWaiter;
6
6
  use itsi_error::ItsiError;
7
- use itsi_rb_helpers::{call_without_gvl, create_ruby_thread};
8
- use magnus::{
9
- error::Result as MagnusResult,
10
- value::{InnerValue, Lazy, LazyId, Opaque, ReprValue},
11
- Module, RClass, Ruby, Value,
12
- };
7
+ use itsi_rb_helpers::call_without_gvl;
8
+ use magnus::{error::Result as MagnusResult, Ruby};
13
9
  use mio::{Events, Poll, Token, Waker};
14
- use parking_lot::{Mutex, RwLock};
10
+ use parking_lot::Mutex;
15
11
  use std::{
16
12
  collections::{BinaryHeap, HashMap, VecDeque},
13
+ ffi::CString,
17
14
  os::fd::RawFd,
18
- sync::Arc,
15
+ ptr,
19
16
  time::Duration,
20
17
  };
21
18
  use timer::Timer;
22
- use tracing::{debug, error, info, warn};
19
+ use tracing::{debug, info, warn};
23
20
 
24
21
  #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
25
22
  pub(crate) struct Readiness(i16);
@@ -31,9 +28,6 @@ impl std::fmt::Debug for ItsiScheduler {
31
28
  }
32
29
 
33
30
  const WAKE_TOKEN: Token = Token(0);
34
- static ID_CURRENT: LazyId = LazyId::new("current");
35
- static CLASS_FIBER: Lazy<RClass> =
36
- Lazy::new(|ruby| ruby.module_kernel().const_get("Fiber").unwrap());
37
31
 
38
32
  #[magnus::wrap(class = "Itsi::Scheduler", free_immediately, size)]
39
33
  pub(crate) struct ItsiScheduler {
@@ -123,6 +117,57 @@ impl ItsiScheduler {
123
117
  self.timers.lock().retain(|timer| timer.token.0 != token);
124
118
  }
125
119
 
120
+ pub fn cancel_wait(&self, token: usize) -> MagnusResult<()> {
121
+ let token = Token(token);
122
+
123
+ self.timers.lock().retain(|timer| timer.token != token);
124
+
125
+ let mut io_waiters = self.io_waiters.lock();
126
+ let Some(mut waiter) = io_waiters.remove(&token) else {
127
+ return Ok(());
128
+ };
129
+
130
+ let mut registry = self.registry.lock();
131
+ let Some(queue) = registry.get_mut(&waiter.fd) else {
132
+ return Ok(());
133
+ };
134
+
135
+ let Some(position) = queue.iter().position(|entry| entry.token == token) else {
136
+ return Ok(());
137
+ };
138
+
139
+ if position == 0 {
140
+ self.poll
141
+ .lock()
142
+ .registry()
143
+ .deregister(&mut waiter)
144
+ .map_err(|_| {
145
+ ItsiError::ArgumentError("Failed to deregister".to_string())
146
+ })?;
147
+ }
148
+
149
+ queue.remove(position);
150
+
151
+ if position == 0 {
152
+ if let Some(head) = queue.get_mut(0) {
153
+ let interest = build_interest(head.readiness)?;
154
+ self.poll
155
+ .lock()
156
+ .registry()
157
+ .register(head, head.token, interest)
158
+ .map_err(|_| {
159
+ ItsiError::ArgumentError("Failed to register".to_string())
160
+ })?;
161
+ }
162
+ }
163
+
164
+ if queue.is_empty() {
165
+ registry.remove(&waiter.fd);
166
+ }
167
+
168
+ Ok(())
169
+ }
170
+
126
171
  pub fn has_pending_io(&self) -> bool {
127
172
  !self.timers.lock().is_empty() || !self.io_waiters.lock().is_empty()
128
173
  }
@@ -220,63 +265,69 @@ impl ItsiScheduler {
220
265
  })
221
266
  }
222
267
 
223
- pub fn run_blocking_in_thread<T, F>(&self, ruby: &Ruby, work: F) -> MagnusResult<Option<T>>
224
- where
225
- T: Send + Sync + std::fmt::Debug + 'static,
226
- F: FnOnce() -> Option<T> + Send + 'static,
227
- {
228
- let result: Arc<RwLock<Option<T>>> = Arc::new(RwLock::new(None));
229
- let result_clone = Arc::clone(&result);
230
-
231
- let class_fiber = ruby.get_inner(&CLASS_FIBER);
232
- let current_fiber = ruby
233
- .get_inner(&CLASS_FIBER)
234
- .funcall::<_, _, Value>(*ID_CURRENT, ());
235
-
236
- if current_fiber.is_err() {
237
- error!("Failed to get current fiber");
238
- return Err(ItsiError::ArgumentError("Failed to get current fiber".to_string()).into());
239
- }
240
- let current_fiber = Opaque::from(current_fiber.unwrap());
241
- let scheduler = Opaque::from(class_fiber.funcall::<_, _, Value>("scheduler", ()).unwrap());
242
-
243
- create_ruby_thread(move || {
244
- call_without_gvl(|| {
245
- let outcome = work();
246
- *result_clone.write() = outcome;
247
- });
248
-
249
- let ruby = Ruby::get().unwrap();
250
- scheduler
251
- .get_inner_with(&ruby)
252
- .funcall::<_, _, Value>("unblock", (None::<String>, current_fiber))
253
- .unwrap();
254
- });
255
-
256
- scheduler
257
- .get_inner_with(ruby)
258
- .funcall::<_, _, Value>("block", (None::<Value>, None::<u64>))?;
259
-
260
- let result_opt = Arc::try_unwrap(result).unwrap().write().take();
261
- Ok(result_opt)
262
- }
263
-
264
268
  pub fn address_resolve(
265
- ruby: &Ruby,
266
- rself: &Self,
269
+ _ruby: &Ruby,
270
+ _rself: &Self,
267
271
  hostname: String,
268
272
  ) -> MagnusResult<Option<Vec<String>>> {
269
- let result: Option<Vec<String>> = rself.run_blocking_in_thread(ruby, move || {
270
- use std::net::ToSocketAddrs;
271
- let addrs_res = (hostname.as_str(), 0).to_socket_addrs();
272
- match addrs_res {
273
- Ok(addrs) => {
274
- let ips: Vec<String> = addrs.map(|s| s.ip().to_string()).collect();
275
- Some(ips)
273
+ let result: Option<Vec<String>> = call_without_gvl(move || {
274
+ let hostname = CString::new(hostname).ok()?;
275
+ let hints = nix::libc::addrinfo {
276
+ ai_flags: 0,
277
+ ai_family: nix::libc::AF_UNSPEC,
278
+ ai_socktype: nix::libc::SOCK_STREAM,
279
+ ai_protocol: 0,
280
+ ai_addrlen: 0,
281
+ ai_addr: ptr::null_mut(),
282
+ ai_canonname: ptr::null_mut(),
283
+ ai_next: ptr::null_mut(),
284
+ };
285
+ let mut res: *mut nix::libc::addrinfo = ptr::null_mut();
286
+ let rc = unsafe {
287
+ nix::libc::getaddrinfo(hostname.as_ptr(), ptr::null(), &hints, &mut res)
288
+ };
289
+ if rc != 0 || res.is_null() {
290
+ return None;
291
+ }
292
+
293
+ let mut ips = Vec::new();
294
+ let mut current = res;
295
+ while !current.is_null() {
296
+ let ai = unsafe { &*current };
297
+ if !ai.ai_addr.is_null() {
298
+ match ai.ai_family {
299
+ nix::libc::AF_INET => {
300
+ let addr = unsafe {
301
+ &*(ai.ai_addr as *const nix::libc::sockaddr_in)
302
+ };
303
+ let ip = std::net::Ipv4Addr::from(u32::from_be(addr.sin_addr.s_addr));
304
+ ips.push(ip.to_string());
305
+ }
306
+ nix::libc::AF_INET6 => {
307
+ let addr = unsafe {
308
+ &*(ai.ai_addr as *const nix::libc::sockaddr_in6)
309
+ };
310
+ let ip = std::net::Ipv6Addr::from(addr.sin6_addr.s6_addr);
311
+ ips.push(ip.to_string());
312
+ }
313
+ _ => {}
314
+ }
276
315
  }
277
- Err(_) => None,
316
+ current = ai.ai_next;
278
317
  }
279
- })?;
318
+
319
+ unsafe {
320
+ nix::libc::freeaddrinfo(res);
321
+ }
322
+
323
+ if ips.is_empty() {
324
+ None
325
+ } else {
326
+ ips.sort();
327
+ ips.dedup();
328
+ Some(ips)
329
+ }
330
+ });
280
331
  Ok(result)
281
332
  }
282
333
 
@@ -20,8 +20,9 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
20
20
  scheduler.define_method("warn", method!(ItsiScheduler::warn, 1))?;
21
21
  scheduler.define_method("start_timer", method!(ItsiScheduler::start_timer, 2))?;
22
22
  scheduler.define_method("clear_timer", method!(ItsiScheduler::clear_timer, 1))?;
23
+ scheduler.define_method("cancel_wait", method!(ItsiScheduler::cancel_wait, 1))?;
23
24
  scheduler.define_method(
24
- "address_resolve",
25
+ "native_address_resolve",
25
26
  method!(ItsiScheduler::address_resolve, 1),
26
27
  )?;
27
28
  scheduler.define_method("has_pending_io?", method!(ItsiScheduler::has_pending_io, 0))?;
@@ -984,7 +984,7 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
984
984
 
985
985
  [[package]]
986
986
  name = "itsi-scheduler"
987
- version = "0.2.25"
987
+ version = "0.2.23"
988
988
  dependencies = [
989
989
  "bytes",
990
990
  "derive_more",
@@ -1002,7 +1002,7 @@ dependencies = [
1002
1002
 
1003
1003
  [[package]]
1004
1004
  name = "itsi-server"
1005
- version = "0.2.25"
1005
+ version = "0.2.23"
1006
1006
  dependencies = [
1007
1007
  "async-channel",
1008
1008
  "async-trait",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "itsi-server"
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"
@@ -70,6 +70,7 @@ redis = { version = "0.29.2", features = [
70
70
  ] }
71
71
  rustls = "0.23.23"
72
72
  rustls-pemfile = "2.2.0"
73
+ webpki-roots = "0.26"
73
74
  serde = "1.0.219"
74
75
  serde_json = "1.0.140"
75
76
  serde_magnus = "0.11.0"
@@ -41,6 +41,20 @@ fn init(ruby: &Ruby) -> Result<()> {
41
41
  server.define_singleton_method("reset_signal_handlers", function!(reset_signal_handlers, 0))?;
42
42
  server.define_method("start", method!(ItsiServer::start, 0))?;
43
43
  server.define_method("stop", method!(ItsiServer::stop, 0))?;
44
+ server.define_method("tls_bindings", method!(ItsiServer::tls_bindings, 0))?;
45
+ server.define_method("tls_domains", method!(ItsiServer::tls_domains, 1))?;
46
+ server.define_method(
47
+ "tls_domain_statuses",
48
+ method!(ItsiServer::tls_domain_statuses, 1),
49
+ )?;
50
+ server.define_method(
51
+ "register_tls_domain",
52
+ method!(ItsiServer::register_tls_domain, 2),
53
+ )?;
54
+ server.define_method(
55
+ "unregister_tls_domain",
56
+ method!(ItsiServer::unregister_tls_domain, 2),
57
+ )?;
44
58
 
45
59
  let request = ruby.get_inner(&ITSI_REQUEST);
46
60
  request.define_method("path", method!(ItsiHttpRequest::path, 0))?;
@@ -94,6 +108,7 @@ fn init(ruby: &Ruby) -> Result<()> {
94
108
  response.define_method("<<", method!(ItsiHttpResponse::send_frame, 1))?;
95
109
  response.define_method("write", method!(ItsiHttpResponse::send_frame, 1))?;
96
110
  response.define_method("read", method!(ItsiHttpResponse::recv_frame, 0))?;
111
+ response.define_method("partial_hijack", method!(ItsiHttpResponse::partial_hijack, 1))?;
97
112
  response.define_method("closed?", method!(ItsiHttpResponse::is_closed, 0))?;
98
113
  response.define_method(
99
114
  "send_and_close",
@@ -211,6 +211,15 @@ impl ItsiHttpRequest {
211
211
  }
212
212
  }
213
213
  }
214
+ Ok(ResponseFrame::PartialHijackedResponse(response)) => {
215
+ match response.process_partial_hijacked_response().await {
216
+ Ok(result) => Ok(result),
217
+ Err(e) => {
218
+ error!("Error processing partial hijacked response: {}", e);
219
+ Ok(Response::new(HttpBody::empty()))
220
+ }
221
+ }
222
+ }
214
223
  Err(_) => {
215
224
  error!("Failed to receive response from receiver");
216
225
  Ok(INTERNAL_SERVER_ERROR_RESPONSE
@@ -23,10 +23,12 @@ use parking_lot::RwLock;
23
23
  use std::{
24
24
  collections::HashMap,
25
25
  io,
26
+ io::Read,
26
27
  ops::Deref,
27
28
  os::{fd::FromRawFd, unix::net::UnixStream},
28
29
  str::FromStr,
29
30
  sync::Arc,
31
+ thread,
30
32
  time::Duration,
31
33
  };
32
34
  use tokio::{
@@ -56,6 +58,7 @@ pub struct ResponseInner {
56
58
  pub frame_writer: RwLock<Option<Sender<Bytes>>>,
57
59
  pub response: RwLock<Option<HttpResponse>>,
58
60
  pub hijacked_socket: RwLock<Option<UnixStream>>,
61
+ pub partial_hijacked_socket: RwLock<Option<UnixStream>>,
59
62
  pub response_sender: RwLock<Option<OneshotSender<ResponseFrame>>>,
60
63
  pub shutdown_rx: watch::Receiver<RunningPhase>,
61
64
  pub parts: Arc<Parts>,
@@ -65,6 +68,7 @@ pub struct ResponseInner {
65
68
  pub enum ResponseFrame {
66
69
  HttpResponse(Box<HttpResponse>),
67
70
  HijackedResponse(ItsiHttpResponse),
71
+ PartialHijackedResponse(ItsiHttpResponse),
68
72
  }
69
73
 
70
74
  impl ItsiHttpResponse {
@@ -81,6 +85,7 @@ impl ItsiHttpResponse {
81
85
  frame_writer: RwLock::new(None),
82
86
  response: RwLock::new(Some(Response::new(HttpBody::empty()))),
83
87
  hijacked_socket: RwLock::new(None),
88
+ partial_hijacked_socket: RwLock::new(None),
84
89
  }),
85
90
  }
86
91
  }
@@ -219,6 +224,42 @@ impl ItsiHttpResponse {
219
224
  Ok(response)
220
225
  }
221
226
 
227
+ pub async fn process_partial_hijacked_response(&self) -> Result<HttpResponse> {
228
+ let stream =
229
+ self.partial_hijacked_socket
230
+ .write()
231
+ .take()
232
+ .ok_or(itsi_error::ItsiError::InvalidInput(
233
+ "Couldn't partial hijack stream".to_owned(),
234
+ ))?;
235
+ let stream = TokioUnixStream::from_std(stream).map_err(|err| {
236
+ itsi_error::ItsiError::InvalidInput(format!(
237
+ "Couldn't prepare partial hijack stream: {:?}",
238
+ err
239
+ ))
240
+ })?;
241
+
242
+ let mut response = self.response.write().take().ok_or(
243
+ itsi_error::ItsiError::InvalidInput("Missing partial hijack response".to_owned()),
244
+ )?;
245
+ let parts = self.parts.clone();
246
+
247
+ tokio::spawn(async move {
248
+ let mut req = Request::from_parts((*parts).clone(), Empty::<Bytes>::new());
249
+ match hyper::upgrade::on(&mut req).await {
250
+ Ok(upgraded) => {
251
+ if let Err(err) = Self::two_way_bridge(upgraded, stream).await {
252
+ warn!("partial hijack bridge error: {:?}", err);
253
+ }
254
+ }
255
+ Err(err) => warn!("partial hijack upgrade error: {:?}", err),
256
+ }
257
+ });
258
+
259
+ *response.body_mut() = HttpBody::empty();
260
+ Ok(response)
261
+ }
262
+
222
263
  pub fn internal_server_error(&self, message: String) {
223
264
  error!(message);
224
265
  self.close_write().ok();
@@ -304,6 +345,60 @@ impl ItsiHttpResponse {
304
345
  // not implemented
305
346
  }
306
347
 
348
+ pub fn partial_hijack(&self, fd: i32) -> MagnusResult<()> {
349
+ let mut stream = unsafe { UnixStream::from_raw_fd(fd) };
350
+ let status = self
351
+ .response
352
+ .read()
353
+ .as_ref()
354
+ .map(|response| response.status())
355
+ .unwrap_or(StatusCode::OK);
356
+
357
+ if status == StatusCode::SWITCHING_PROTOCOLS {
358
+ *self.partial_hijacked_socket.write() = Some(stream);
359
+ if let Some(sender) = self.response_sender.write().take() {
360
+ sender
361
+ .send(ResponseFrame::PartialHijackedResponse(self.clone()))
362
+ .ok();
363
+ }
364
+ } else if let Some(mut response) = self.response.write().take() {
365
+ let (writer, reader) = tokio::sync::mpsc::channel::<Bytes>(256);
366
+ let shutdown_rx = self.shutdown_rx.clone();
367
+ let frame_stream = FrameStream::new(reader, shutdown_rx);
368
+ let buffered =
369
+ BufferedStream::new(frame_stream, 32 * 1024, Duration::from_millis(10));
370
+ *response.body_mut() = HttpBody::stream(buffered);
371
+ self.frame_writer.write().replace(writer.clone());
372
+
373
+ thread::spawn(move || {
374
+ let mut buf = [0u8; 16 * 1024];
375
+ loop {
376
+ match stream.read(&mut buf) {
377
+ Ok(0) => break,
378
+ Ok(n) => {
379
+ if writer
380
+ .blocking_send(Bytes::copy_from_slice(&buf[..n]))
381
+ .is_err()
382
+ {
383
+ break;
384
+ }
385
+ }
386
+ Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,
387
+ Err(_) => break,
388
+ }
389
+ }
390
+ });
391
+
392
+ if let Some(sender) = self.response_sender.write().take() {
393
+ sender
394
+ .send(ResponseFrame::HttpResponse(Box::new(response)))
395
+ .ok();
396
+ }
397
+ }
398
+
399
+ Ok(())
400
+ }
401
+
307
402
  pub fn is_closed(&self) -> bool {
308
403
  self.response.read().is_none() && self.frame_writer.read().is_none()
309
404
  }
@@ -2,7 +2,7 @@ use super::file_watcher::{self, WatcherCommand};
2
2
  use crate::{
3
3
  ruby_types::ITSI_SERVER_CONFIG,
4
4
  server::{
5
- binds::{bind::Bind, listener::Listener},
5
+ binds::{bind::Bind, listener::Listener, tls::DynamicAcmeManager},
6
6
  middleware_stack::MiddlewareSet,
7
7
  },
8
8
  };
@@ -81,6 +81,8 @@ pub struct ServerParams {
81
81
  pub binds: Vec<Bind>,
82
82
  #[debug(skip)]
83
83
  pub(crate) listeners: Mutex<Vec<Listener>>,
84
+ #[debug(skip)]
85
+ pub acme_managers: RwLock<Vec<(String, DynamicAcmeManager)>>,
84
86
  listener_info: Mutex<HashMap<String, i32>>,
85
87
  pub itsi_server_token_preference: ItsiServerTokenPreference,
86
88
  pub preloaded: AtomicBool,
@@ -348,6 +350,7 @@ impl ServerParams {
348
350
  preexisting_listeners,
349
351
  listener_info: Mutex::new(HashMap::new()),
350
352
  listeners: Mutex::new(Vec::new()),
353
+ acme_managers: RwLock::new(Vec::new()),
351
354
  middleware_loader: middleware_loader.into(),
352
355
  middleware: OnceLock::new(),
353
356
  preloaded: AtomicBool::new(false),
@@ -401,8 +404,26 @@ impl ServerParams {
401
404
  })
402
405
  .collect::<Result<HashMap<String, i32>>>()?;
403
406
 
407
+ let has_http_listener = listeners
408
+ .iter()
409
+ .any(|listener| matches!(listener, Listener::Tcp(_)));
410
+ let mut acme_managers = Vec::new();
411
+ for listener in &listeners {
412
+ match listener {
413
+ Listener::TcpTls((_, acceptor)) | Listener::UnixTls((_, acceptor)) => {
414
+ acceptor.set_http01_enabled(has_http_listener);
415
+ acceptor.initialize_domains();
416
+ if let Some(manager) = acceptor.manager() {
417
+ acme_managers.push((listener.handover()?.0, manager));
418
+ }
419
+ }
420
+ Listener::Tcp(_) | Listener::Unix(_) => {}
421
+ }
422
+ }
423
+
404
424
  *self.listener_info.lock() = listener_info;
405
425
  *self.listeners.lock() = listeners;
426
+ *self.acme_managers.write() = acme_managers;
406
427
  Ok(())
407
428
  }
408
429
  }
@@ -8,6 +8,7 @@ use itsi_server_config::ItsiServerConfig;
8
8
  use itsi_tracing::{error, run_silently};
9
9
  use magnus::{block::Proc, error::Result, RHash, Ruby};
10
10
  use parking_lot::Mutex;
11
+ use std::collections::HashMap;
11
12
  use std::{path::PathBuf, sync::Arc};
12
13
  use tracing::{info, instrument};
13
14
  mod file_watcher;
@@ -46,6 +47,105 @@ impl ItsiServer {
46
47
  Ok(())
47
48
  }
48
49
 
50
+ fn selected_acme_manager(
51
+ &self,
52
+ listener_id: Option<String>,
53
+ ) -> Result<crate::server::binds::tls::DynamicAcmeManager> {
54
+ let config = self.config()?;
55
+ let server_params = config.server_params.read();
56
+ if server_params.workers > 1 {
57
+ return Err(magnus::Error::new(
58
+ magnus::Ruby::get().unwrap().exception_runtime_error(),
59
+ "Dynamic TLS domain management currently only supports single-worker mode",
60
+ ));
61
+ }
62
+
63
+ let managers = server_params.acme_managers.read();
64
+ if managers.is_empty() {
65
+ return Err(magnus::Error::new(
66
+ magnus::Ruby::get().unwrap().exception_runtime_error(),
67
+ "No ACME-managed TLS bindings are configured",
68
+ ));
69
+ }
70
+
71
+ if let Some(listener_id) = listener_id {
72
+ managers
73
+ .iter()
74
+ .find(|(id, _)| id == &listener_id)
75
+ .map(|(_, manager)| manager.clone())
76
+ .ok_or_else(|| {
77
+ magnus::Error::new(
78
+ magnus::Ruby::get().unwrap().exception_runtime_error(),
79
+ format!("Unknown ACME TLS binding: {}", listener_id),
80
+ )
81
+ })
82
+ } else if managers.len() == 1 {
83
+ Ok(managers[0].1.clone())
84
+ } else {
85
+ Err(magnus::Error::new(
86
+ magnus::Ruby::get().unwrap().exception_runtime_error(),
87
+ "Multiple ACME TLS bindings are configured; specify a listener_id",
88
+ ))
89
+ }
90
+ }
91
+
92
+ pub fn tls_bindings(&self) -> Result<Vec<String>> {
93
+ let config = self.config()?;
94
+ let server_params = config.server_params.read();
95
+ let bindings = server_params
96
+ .acme_managers
97
+ .read()
98
+ .iter()
99
+ .map(|(listener_id, _)| listener_id.clone())
100
+ .collect();
101
+ Ok(bindings)
102
+ }
103
+
104
+ pub fn tls_domains(&self, listener_id: Option<String>) -> Result<Vec<String>> {
105
+ let manager = self.selected_acme_manager(listener_id)?;
106
+ Ok(manager
107
+ .statuses()
108
+ .into_iter()
109
+ .map(|status| status.domain)
110
+ .collect())
111
+ }
112
+
113
+ pub fn tls_domain_statuses(
114
+ &self,
115
+ listener_id: Option<String>,
116
+ ) -> Result<Vec<HashMap<String, String>>> {
117
+ let manager = self.selected_acme_manager(listener_id)?;
118
+ Ok(manager
119
+ .statuses()
120
+ .into_iter()
121
+ .map(|status| {
122
+ let mut out = HashMap::new();
123
+ out.insert("domain".to_string(), status.domain);
124
+ out.insert("status".to_string(), status.status);
125
+ if let Some(last_error) = status.last_error {
126
+ out.insert("last_error".to_string(), last_error);
127
+ }
128
+ out
129
+ })
130
+ .collect())
131
+ }
132
+
133
+ pub fn register_tls_domain(&self, domain: String, listener_id: Option<String>) -> Result<()> {
134
+ let manager = self.selected_acme_manager(listener_id)?;
135
+ manager.register_domain(domain);
136
+ Ok(())
137
+ }
138
+
139
+ pub fn unregister_tls_domain(
140
+ &self,
141
+ domain: String,
142
+ listener_id: Option<String>,
143
+ ) -> Result<()> {
144
+ let manager = self.selected_acme_manager(listener_id)?;
145
+ manager.unregister_domain(&domain);
146
+ Ok(())
147
+ }
148
+
49
149
  fn config(&self) -> Result<Arc<ItsiServerConfig>> {
50
150
  self.config.lock().as_ref().cloned().ok_or_else(|| {
51
151
  magnus::Error::new(
@@ -8,7 +8,6 @@ use super::bind_protocol::BindProtocol;
8
8
 
9
9
  use super::tls::ItsiTlsAcceptor;
10
10
  use itsi_error::{ItsiError, Result};
11
- use itsi_tracing::info;
12
11
  use socket2::{Domain, Protocol, SockRef, Socket, Type};
13
12
  use std::fmt::Display;
14
13
  use std::net::{IpAddr, SocketAddr, TcpListener};
@@ -21,7 +20,6 @@ use tokio::net::UnixListener as TokioUnixListener;
21
20
  use tokio::net::{unix, TcpStream, UnixStream};
22
21
  use tokio::sync::watch::Receiver;
23
22
  use tokio::time::timeout;
24
- use tokio_stream::StreamExt;
25
23
 
26
24
  pub(crate) enum Listener {
27
25
  Tcp(TcpListener),
@@ -115,25 +113,8 @@ impl TokioListener {
115
113
  }
116
114
 
117
115
  pub async fn spawn_acme_event_task(&self, mut shutdown_receiver: Receiver<RunningPhase>) {
118
- if let TokioListener::TcpTls(
119
- _,
120
- ItsiTlsAcceptor::Automatic(_acme_acceptor, state, _server_config),
121
- ) = self
122
- {
123
- let mut state = state.lock().await;
124
- loop {
125
- tokio::select! {
126
- stream_event = StreamExt::next(&mut *state) => {
127
- match stream_event {
128
- Some(event) => info!("ACME Event: {:?}", event),
129
- None => error!("Received no acme event"),
130
- }
131
- },
132
- _ = shutdown_receiver.changed() => {
133
- break;
134
- }
135
- }
136
- }
116
+ if let TokioListener::TcpTls(_, ItsiTlsAcceptor::Automatic { .. }) = self {
117
+ let _ = shutdown_receiver.changed().await;
137
118
  }
138
119
  }
139
120
 
@@ -215,7 +196,11 @@ impl AcceptedStream {
215
196
  Err(_) => Err(ItsiError::Pass),
216
197
  }
217
198
  }
218
- ItsiTlsAcceptor::Automatic(acme_acceptor, _, rustls_config) => {
199
+ ItsiTlsAcceptor::Automatic {
200
+ acme_acceptor,
201
+ server_config,
202
+ ..
203
+ } => {
219
204
  let accept_future = acme_acceptor.accept(stream);
220
205
  match timeout(handshake_timeout, accept_future).await {
221
206
  Err(_) => Err(ItsiError::Pass),
@@ -224,7 +209,7 @@ impl AcceptedStream {
224
209
  Ok(Some(start_handshake)) => {
225
210
  match timeout(
226
211
  handshake_timeout,
227
- start_handshake.into_stream(rustls_config.clone()),
212
+ start_handshake.into_stream(server_config.clone()),
228
213
  )
229
214
  .await
230
215
  {
@@ -259,7 +244,7 @@ impl AcceptedStream {
259
244
  Err(_) => Err(ItsiError::Pass),
260
245
  }
261
246
  }
262
- ItsiTlsAcceptor::Automatic(_, _, _) => {
247
+ ItsiTlsAcceptor::Automatic { .. } => {
263
248
  error!("Automatic TLS not supported on Unix sockets");
264
249
  Err(ItsiError::UnsupportedProtocol(
265
250
  "Automatic TLS on Unix Sockets".to_owned(),