itsi-server 0.1.1 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/exe/itsi +88 -28
  3. data/ext/itsi_error/Cargo.toml +2 -0
  4. data/ext/itsi_error/src/from.rs +71 -0
  5. data/ext/itsi_error/src/lib.rs +12 -37
  6. data/ext/itsi_instrument_entry/Cargo.toml +15 -0
  7. data/ext/itsi_instrument_entry/src/lib.rs +31 -0
  8. data/ext/itsi_rb_helpers/Cargo.toml +2 -0
  9. data/ext/itsi_rb_helpers/src/heap_value.rs +121 -0
  10. data/ext/itsi_rb_helpers/src/lib.rs +90 -10
  11. data/ext/itsi_scheduler/Cargo.toml +24 -0
  12. data/ext/itsi_scheduler/extconf.rb +6 -0
  13. data/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
  14. data/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
  15. data/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
  16. data/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
  17. data/ext/itsi_scheduler/src/lib.rs +38 -0
  18. data/ext/itsi_server/Cargo.toml +17 -3
  19. data/ext/itsi_server/extconf.rb +1 -1
  20. data/ext/itsi_server/src/body_proxy/big_bytes.rs +104 -0
  21. data/ext/itsi_server/src/body_proxy/itsi_body_proxy.rs +122 -0
  22. data/ext/itsi_server/src/body_proxy/mod.rs +2 -0
  23. data/ext/itsi_server/src/lib.rs +61 -7
  24. data/ext/itsi_server/src/request/itsi_request.rs +238 -104
  25. data/ext/itsi_server/src/response/itsi_response.rs +347 -0
  26. data/ext/itsi_server/src/response/mod.rs +1 -0
  27. data/ext/itsi_server/src/server/bind.rs +54 -23
  28. data/ext/itsi_server/src/server/bind_protocol.rs +37 -0
  29. data/ext/itsi_server/src/server/io_stream.rs +104 -0
  30. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +11 -30
  31. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +3 -50
  32. data/ext/itsi_server/src/server/itsi_server.rs +196 -134
  33. data/ext/itsi_server/src/server/lifecycle_event.rs +9 -0
  34. data/ext/itsi_server/src/server/listener.rs +240 -132
  35. data/ext/itsi_server/src/server/mod.rs +7 -1
  36. data/ext/itsi_server/src/server/process_worker.rs +196 -0
  37. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +254 -0
  38. data/ext/itsi_server/src/server/serve_strategy/mod.rs +27 -0
  39. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +247 -0
  40. data/ext/itsi_server/src/server/signal.rs +70 -0
  41. data/ext/itsi_server/src/server/thread_worker.rs +368 -0
  42. data/ext/itsi_server/src/server/tls.rs +101 -51
  43. data/ext/itsi_tracing/Cargo.toml +4 -0
  44. data/ext/itsi_tracing/src/lib.rs +36 -6
  45. data/lib/itsi/request.rb +30 -14
  46. data/lib/itsi/server/rack/handler/itsi.rb +25 -0
  47. data/lib/itsi/server/scheduler_mode.rb +6 -0
  48. data/lib/itsi/server/version.rb +1 -1
  49. data/lib/itsi/server.rb +82 -2
  50. data/lib/itsi/signals.rb +23 -0
  51. data/lib/itsi/stream_io.rb +38 -0
  52. metadata +38 -25
  53. data/ext/itsi_server/src/server/transfer_protocol.rs +0 -23
  54. data/ext/itsi_server/src/stream_writer/mod.rs +0 -21
@@ -0,0 +1,368 @@
1
+ use super::itsi_server::RequestJob;
2
+ use crate::{request::itsi_request::ItsiRequest, ITSI_SERVER};
3
+ use itsi_rb_helpers::{
4
+ call_with_gvl, call_without_gvl, create_ruby_thread, kill_threads, HeapValue,
5
+ };
6
+ use itsi_tracing::{debug, error, info, warn};
7
+ use magnus::{
8
+ error::Result,
9
+ value::{InnerValue, Lazy, LazyId, Opaque, ReprValue},
10
+ Module, RClass, Ruby, Thread, Value,
11
+ };
12
+ use nix::unistd::Pid;
13
+ use parking_lot::{Mutex, RwLock};
14
+ use std::{
15
+ num::NonZeroU8,
16
+ ops::Deref,
17
+ sync::{
18
+ atomic::{AtomicBool, Ordering},
19
+ Arc,
20
+ },
21
+ thread,
22
+ time::{Duration, Instant},
23
+ };
24
+ use tokio::{runtime::Builder as RuntimeBuilder, sync::watch};
25
+ use tracing::instrument;
26
+ pub struct ThreadWorker {
27
+ pub id: String,
28
+ pub app: Opaque<Value>,
29
+ pub receiver: Arc<async_channel::Receiver<RequestJob>>,
30
+ pub sender: async_channel::Sender<RequestJob>,
31
+ pub thread: RwLock<Option<HeapValue<Thread>>>,
32
+ pub terminated: Arc<AtomicBool>,
33
+ pub scheduler_class: Option<Opaque<Value>>,
34
+ }
35
+
36
+ static ID_CALL: LazyId = LazyId::new("call");
37
+ static ID_ALIVE: LazyId = LazyId::new("alive?");
38
+ static ID_SCHEDULER: LazyId = LazyId::new("scheduler");
39
+ static ID_SCHEDULE: LazyId = LazyId::new("schedule");
40
+ static ID_BLOCK: LazyId = LazyId::new("block");
41
+ static ID_YIELD: LazyId = LazyId::new("yield");
42
+ static ID_CONST_GET: LazyId = LazyId::new("const_get");
43
+ static CLASS_FIBER: Lazy<RClass> = Lazy::new(|ruby| {
44
+ ruby.module_kernel()
45
+ .const_get::<_, RClass>("Fiber")
46
+ .unwrap()
47
+ });
48
+
49
+ pub struct TerminateWakerSignal(bool);
50
+
51
+ #[instrument(name = "Boot", parent=None, skip(threads, app, pid, scheduler_class))]
52
+ pub fn build_thread_workers(
53
+ pid: Pid,
54
+ threads: NonZeroU8,
55
+ app: Opaque<Value>,
56
+ scheduler_class: Option<String>,
57
+ ) -> Result<(Arc<Vec<ThreadWorker>>, async_channel::Sender<RequestJob>)> {
58
+ let (sender, receiver) = async_channel::bounded(20);
59
+ let receiver_ref = Arc::new(receiver);
60
+ let sender_ref = sender;
61
+ let (app, scheduler_class) = load_app(app, scheduler_class)?;
62
+ Ok((
63
+ Arc::new(
64
+ (1..=u8::from(threads))
65
+ .map(|id| {
66
+ info!(pid = pid.as_raw(), id, "Thread");
67
+ ThreadWorker::new(
68
+ format!("{:?}#{:?}", pid, id),
69
+ app,
70
+ receiver_ref.clone(),
71
+ sender_ref.clone(),
72
+ scheduler_class,
73
+ )
74
+ })
75
+ .collect::<Result<Vec<_>>>()?,
76
+ ),
77
+ sender_ref,
78
+ ))
79
+ }
80
+
81
+ pub fn load_app(
82
+ app: Opaque<Value>,
83
+ scheduler_class: Option<String>,
84
+ ) -> Result<(Opaque<Value>, Option<Opaque<Value>>)> {
85
+ call_with_gvl(|ruby| {
86
+ let app = app.get_inner_with(&ruby);
87
+ let app = Opaque::from(
88
+ app.funcall::<_, _, Value>(*ID_CALL, ())
89
+ .expect("Couldn't load app"),
90
+ );
91
+ let scheduler_class = if let Some(scheduler_class) = scheduler_class {
92
+ Some(Opaque::from(
93
+ ruby.module_kernel()
94
+ .funcall::<_, _, Value>(*ID_CONST_GET, (scheduler_class,))?,
95
+ ))
96
+ } else {
97
+ None
98
+ };
99
+ Ok((app, scheduler_class))
100
+ })
101
+ }
102
+ impl ThreadWorker {
103
+ pub fn new(
104
+ id: String,
105
+ app: Opaque<Value>,
106
+ receiver: Arc<async_channel::Receiver<RequestJob>>,
107
+ sender: async_channel::Sender<RequestJob>,
108
+ scheduler_class: Option<Opaque<Value>>,
109
+ ) -> Result<Self> {
110
+ let mut worker = Self {
111
+ id,
112
+ app,
113
+ receiver,
114
+ sender,
115
+ thread: RwLock::new(None),
116
+ terminated: Arc::new(AtomicBool::new(false)),
117
+ scheduler_class,
118
+ };
119
+ worker.run()?;
120
+ Ok(worker)
121
+ }
122
+
123
+ #[instrument(skip(self), fields(id = self.id))]
124
+ pub async fn request_shutdown(&self) {
125
+ match self.sender.send(RequestJob::Shutdown).await {
126
+ Ok(_) => {}
127
+ Err(err) => error!("Failed to send shutdown request: {}", err),
128
+ };
129
+ info!("Requesting shutdown");
130
+ }
131
+
132
+ #[instrument(skip(self, deadline), fields(id = self.id))]
133
+ pub fn poll_shutdown(&self, deadline: Instant) -> bool {
134
+ call_with_gvl(|_ruby| {
135
+ if let Some(thread) = self.thread.read().deref() {
136
+ if Instant::now() > deadline {
137
+ warn!("Worker shutdown timed out. Killing thread");
138
+ self.terminated.store(true, Ordering::SeqCst);
139
+ kill_threads(vec![thread.as_value()]);
140
+ }
141
+ if thread.funcall::<_, _, bool>(*ID_ALIVE, ()).unwrap_or(false) {
142
+ return true;
143
+ }
144
+ info!("Thread has shut down");
145
+ }
146
+ self.thread.write().take();
147
+
148
+ false
149
+ })
150
+ }
151
+
152
+ pub fn run(&mut self) -> Result<()> {
153
+ let id = self.id.clone();
154
+ let app = self.app;
155
+ let receiver = self.receiver.clone();
156
+ let terminated = self.terminated.clone();
157
+ let scheduler_class = self.scheduler_class;
158
+ call_with_gvl(|_| {
159
+ *self.thread.write() = Some(
160
+ create_ruby_thread(move || {
161
+ if let Some(scheduler_class) = scheduler_class {
162
+ if let Err(err) =
163
+ Self::fiber_accept_loop(id, app, receiver, scheduler_class, terminated)
164
+ {
165
+ error!("Error in fiber_accept_loop: {:?}", err);
166
+ }
167
+ } else {
168
+ Self::accept_loop(id, app, receiver, terminated);
169
+ }
170
+ })
171
+ .into(),
172
+ );
173
+ Ok::<(), magnus::Error>(())
174
+ })?;
175
+ Ok(())
176
+ }
177
+
178
+ pub fn build_scheduler_proc(
179
+ app: Opaque<Value>,
180
+ leader: &Arc<Mutex<Option<RequestJob>>>,
181
+ receiver: &Arc<async_channel::Receiver<RequestJob>>,
182
+ terminated: &Arc<AtomicBool>,
183
+ waker_sender: &watch::Sender<TerminateWakerSignal>,
184
+ ) -> magnus::block::Proc {
185
+ let leader = leader.clone();
186
+ let receiver = receiver.clone();
187
+ let terminated = terminated.clone();
188
+ let waker_sender = waker_sender.clone();
189
+ Ruby::get().unwrap().proc_from_fn(move |ruby, _args, _blk| {
190
+ let scheduler = ruby
191
+ .get_inner(&CLASS_FIBER)
192
+ .funcall::<_, _, Value>(*ID_SCHEDULER, ())
193
+ .unwrap();
194
+ let server = ruby.get_inner(&ITSI_SERVER);
195
+ let thread_current = ruby.thread_current();
196
+ let leader_clone = leader.clone();
197
+ let receiver = receiver.clone();
198
+ let terminated = terminated.clone();
199
+ let waker_sender = waker_sender.clone();
200
+ let mut batch = Vec::with_capacity(MAX_BATCH_SIZE as usize);
201
+
202
+ static MAX_BATCH_SIZE: i32 = 25;
203
+ call_without_gvl(move || loop {
204
+ let mut idle_counter = 0;
205
+ if let Some(v) = leader_clone.lock().take() {
206
+ match v {
207
+ RequestJob::ProcessRequest(itsi_request) => {
208
+ batch.push(RequestJob::ProcessRequest(itsi_request))
209
+ }
210
+ RequestJob::Shutdown => {
211
+ waker_sender.send(TerminateWakerSignal(true)).unwrap();
212
+ break;
213
+ }
214
+ }
215
+ }
216
+ for _ in 0..MAX_BATCH_SIZE {
217
+ if let Ok(req) = receiver.try_recv() {
218
+ batch.push(req);
219
+ } else {
220
+ break;
221
+ }
222
+ }
223
+
224
+ let shutdown_requested = call_with_gvl(|_| {
225
+ for req in batch.drain(..) {
226
+ match req {
227
+ RequestJob::ProcessRequest(request) => {
228
+ let response = request.response.clone();
229
+ if let Err(err) =
230
+ server.funcall::<_, _, Value>(*ID_SCHEDULE, (app, request))
231
+ {
232
+ ItsiRequest::internal_error(ruby, response, err)
233
+ }
234
+ }
235
+ RequestJob::Shutdown => return true,
236
+ }
237
+ }
238
+ false
239
+ });
240
+
241
+ if shutdown_requested || terminated.load(Ordering::Relaxed) {
242
+ waker_sender.send(TerminateWakerSignal(true)).unwrap();
243
+ break;
244
+ }
245
+
246
+ let yield_result = if receiver.is_empty() {
247
+ waker_sender.send(TerminateWakerSignal(false)).unwrap();
248
+ idle_counter = (idle_counter + 1) % 100;
249
+ call_with_gvl(|ruby| {
250
+ if idle_counter == 0 {
251
+ ruby.gc_start();
252
+ }
253
+ scheduler.funcall::<_, _, Value>(*ID_BLOCK, (thread_current, None::<u8>))
254
+ })
255
+ } else {
256
+ call_with_gvl(|_| scheduler.funcall::<_, _, Value>(*ID_YIELD, ()))
257
+ };
258
+
259
+ if yield_result.is_err() {
260
+ break;
261
+ }
262
+ })
263
+ })
264
+ }
265
+
266
+ #[instrument(skip_all, fields(thread_worker=id))]
267
+ pub fn fiber_accept_loop(
268
+ id: String,
269
+ app: Opaque<Value>,
270
+ receiver: Arc<async_channel::Receiver<RequestJob>>,
271
+ scheduler_class: Opaque<Value>,
272
+ terminated: Arc<AtomicBool>,
273
+ ) -> Result<()> {
274
+ let ruby = Ruby::get().unwrap();
275
+ let (waker_sender, waker_receiver) = watch::channel(TerminateWakerSignal(false));
276
+ let leader: Arc<Mutex<Option<RequestJob>>> = Arc::new(Mutex::new(None));
277
+ let server = ruby.get_inner(&ITSI_SERVER);
278
+ let scheduler_proc =
279
+ Self::build_scheduler_proc(app, &leader, &receiver, &terminated, &waker_sender);
280
+ let (scheduler, scheduler_fiber) = server.funcall::<_, _, (Value, Value)>(
281
+ "start_scheduler_loop",
282
+ (scheduler_class, scheduler_proc),
283
+ )?;
284
+ Self::start_waker_thread(
285
+ scheduler.into(),
286
+ scheduler_fiber.into(),
287
+ leader,
288
+ receiver,
289
+ waker_receiver,
290
+ );
291
+ Ok(())
292
+ }
293
+
294
+ #[allow(clippy::await_holding_lock)]
295
+ pub fn start_waker_thread(
296
+ scheduler: Opaque<Value>,
297
+ scheduler_fiber: Opaque<Value>,
298
+ leader: Arc<Mutex<Option<RequestJob>>>,
299
+ receiver: Arc<async_channel::Receiver<RequestJob>>,
300
+ mut waker_receiver: watch::Receiver<TerminateWakerSignal>,
301
+ ) {
302
+ create_ruby_thread(move || {
303
+ let scheduler = scheduler.get_inner_with(&Ruby::get().unwrap());
304
+ let leader = leader.clone();
305
+ call_without_gvl(|| {
306
+ RuntimeBuilder::new_current_thread()
307
+ .build()
308
+ .expect("Failed to build Tokio runtime")
309
+ .block_on(async {
310
+ loop {
311
+ waker_receiver.changed().await.ok();
312
+ if waker_receiver.borrow().0 {
313
+ break;
314
+ }
315
+ tokio::select! {
316
+ _ = waker_receiver.changed() => {
317
+ if waker_receiver.borrow().0 {
318
+ break;
319
+ }
320
+ },
321
+ next_msg = receiver.recv() => {
322
+ *leader.lock() = next_msg.ok();
323
+ call_with_gvl(|_| {
324
+ scheduler
325
+ .funcall::<_, _, Value>(
326
+ "unblock",
327
+ (None::<u8>, scheduler_fiber),
328
+ )
329
+ .ok();
330
+ });
331
+ }
332
+ }
333
+ }
334
+ })
335
+ });
336
+ });
337
+ }
338
+
339
+ #[instrument(skip_all, fields(thread_worker=id))]
340
+ pub fn accept_loop(
341
+ id: String,
342
+ app: Opaque<Value>,
343
+ receiver: Arc<async_channel::Receiver<RequestJob>>,
344
+ terminated: Arc<AtomicBool>,
345
+ ) {
346
+ let ruby = Ruby::get().unwrap();
347
+ let server = ruby.get_inner(&ITSI_SERVER);
348
+ call_without_gvl(|| loop {
349
+ match receiver.recv_blocking() {
350
+ Ok(RequestJob::ProcessRequest(request)) => {
351
+ if terminated.load(Ordering::Relaxed) {
352
+ break;
353
+ }
354
+ call_with_gvl(|_ruby| {
355
+ request.process(&ruby, server, app).ok();
356
+ })
357
+ }
358
+ Ok(RequestJob::Shutdown) => {
359
+ debug!("Shutting down thread worker");
360
+ break;
361
+ }
362
+ Err(_) => {
363
+ thread::sleep(Duration::from_micros(1));
364
+ }
365
+ }
366
+ });
367
+ }
368
+ }
@@ -1,20 +1,69 @@
1
1
  use base64::{engine::general_purpose, Engine as _};
2
2
  use itsi_error::Result;
3
- use itsi_tracing::{info, warn};
3
+ use itsi_tracing::info;
4
4
  use rcgen::{CertificateParams, DnType, KeyPair, SanType};
5
+ use rustls::pki_types::{CertificateDer, PrivateKeyDer};
5
6
  use rustls_pemfile::{certs, pkcs8_private_keys};
6
- use std::{collections::HashMap, fs, io::BufReader};
7
- use tokio_rustls::rustls::{Certificate, PrivateKey, ServerConfig};
7
+ use std::{
8
+ collections::HashMap,
9
+ env, fs,
10
+ io::{BufReader, Error},
11
+ sync::Arc,
12
+ };
13
+ use tokio::sync::Mutex;
14
+ use tokio_rustls::{rustls::ServerConfig, TlsAcceptor};
15
+ use tokio_rustls_acme::{caches::DirCache, AcmeAcceptor, AcmeConfig, AcmeState};
8
16
 
9
17
  const ITS_CA_CERT: &str = include_str!("./itsi_ca/itsi_ca.crt");
10
18
  const ITS_CA_KEY: &str = include_str!("./itsi_ca/itsi_ca.key");
11
19
 
20
+ #[derive(Clone)]
21
+ pub enum ItsiTlsAcceptor {
22
+ Manual(TlsAcceptor),
23
+ Automatic(
24
+ AcmeAcceptor,
25
+ Arc<Mutex<AcmeState<Error>>>,
26
+ Arc<ServerConfig>,
27
+ ),
28
+ }
29
+
12
30
  // Generates a TLS configuration based on either :
13
31
  // * Input "cert" and "key" options (either paths or Base64-encoded strings) or
14
32
  // * Performs automatic certificate generation/retrieval. Generated certs use an internal self-signed Isti CA.
15
33
  // If a non-local host or optional domain parameter is provided,
16
34
  // an automated certificate will attempt to be fetched using let's encrypt.
17
- pub fn configure_tls(host: &str, query_params: &HashMap<String, String>) -> Result<ServerConfig> {
35
+ pub fn configure_tls(
36
+ host: &str,
37
+ query_params: &HashMap<String, String>,
38
+ ) -> Result<ItsiTlsAcceptor> {
39
+ let domains = query_params
40
+ .get("domains")
41
+ .map(|v| v.split(',').map(String::from).collect::<Vec<_>>());
42
+
43
+ if query_params.get("cert").is_none() || query_params.get("key").is_none() {
44
+ if let Some(domains) = domains {
45
+ let directory_url = env::var("ACME_DIRECTORY_URL")
46
+ .unwrap_or_else(|_| "https://acme-v02.api.letsencrypt.org/directory".to_string());
47
+ info!(
48
+ domains = format!("{:?}", domains),
49
+ directory_url, "Requesting acme cert"
50
+ );
51
+ let acme_state = AcmeConfig::new(domains)
52
+ .contact(["mailto:wc@pico.net.nz"])
53
+ .cache(DirCache::new("./rustls_acme_cache"))
54
+ .directory(directory_url)
55
+ .state();
56
+ let rustls_config = ServerConfig::builder()
57
+ .with_no_client_auth()
58
+ .with_cert_resolver(acme_state.resolver());
59
+ let acceptor = acme_state.acceptor();
60
+ return Ok(ItsiTlsAcceptor::Automatic(
61
+ acceptor,
62
+ Arc::new(Mutex::new(acme_state)),
63
+ Arc::new(rustls_config),
64
+ ));
65
+ }
66
+ }
18
67
  let (certs, key) = if let (Some(cert_path), Some(key_path)) =
19
68
  (query_params.get("cert"), query_params.get("key"))
20
69
  {
@@ -22,41 +71,20 @@ pub fn configure_tls(host: &str, query_params: &HashMap<String, String>) -> Resu
22
71
  let certs = load_certs(cert_path);
23
72
  let key = load_private_key(key_path);
24
73
  (certs, key)
25
- } else if query_params
26
- .get("cert")
27
- .map(|v| v == "auto")
28
- .unwrap_or(false)
29
- {
30
- let domain_param = query_params.get("domain");
31
- let host_string = host.to_string();
32
- let domain = domain_param.or_else(|| {
33
- if host_string != "localhost" {
34
- Some(&host_string)
35
- } else {
36
- None
37
- }
38
- });
39
-
40
- if let Some(domain) = domain {
41
- retrieve_acme_cert(domain)?
42
- } else {
43
- generate_ca_signed_cert(host)?
44
- }
45
74
  } else {
46
- generate_ca_signed_cert(host)?
75
+ generate_ca_signed_cert(vec![host.to_owned()])?
47
76
  };
48
77
 
49
78
  let mut config = ServerConfig::builder()
50
- .with_safe_defaults()
51
79
  .with_no_client_auth()
52
80
  .with_single_cert(certs, key)
53
81
  .expect("Failed to build TLS config");
54
82
 
55
83
  config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
56
- Ok(config)
84
+ Ok(ItsiTlsAcceptor::Manual(TlsAcceptor::from(Arc::new(config))))
57
85
  }
58
86
 
59
- pub fn load_certs(path: &str) -> Vec<Certificate> {
87
+ pub fn load_certs(path: &str) -> Vec<CertificateDer<'static>> {
60
88
  let data = if let Some(stripped) = path.strip_prefix("base64:") {
61
89
  general_purpose::STANDARD
62
90
  .decode(stripped)
@@ -74,14 +102,20 @@ pub fn load_certs(path: &str) -> Vec<Certificate> {
74
102
  })
75
103
  .collect::<Result<_>>()
76
104
  .expect("Failed to parse certificate file");
77
- certs_der.into_iter().map(Certificate).collect()
105
+ certs_der
106
+ .into_iter()
107
+ .map(|vec| {
108
+ // Convert the owned Vec<u8> into a CertificateDer and force 'static.
109
+ unsafe { std::mem::transmute(CertificateDer::from(vec)) }
110
+ })
111
+ .collect()
78
112
  } else {
79
- vec![Certificate(data)]
113
+ vec![CertificateDer::from(data)]
80
114
  }
81
115
  }
82
116
 
83
117
  /// Loads a private key from a file or Base64.
84
- pub fn load_private_key(path: &str) -> PrivateKey {
118
+ pub fn load_private_key(path: &str) -> PrivateKeyDer<'static> {
85
119
  let key_data = if let Some(stripped) = path.strip_prefix("base64:") {
86
120
  general_purpose::STANDARD
87
121
  .decode(stripped)
@@ -100,39 +134,55 @@ pub fn load_private_key(path: &str) -> PrivateKey {
100
134
  .collect::<Result<_>>()
101
135
  .expect("Failed to parse private key");
102
136
  if !keys.is_empty() {
103
- return PrivateKey(keys[0].clone());
137
+ return PrivateKeyDer::try_from(keys[0].clone()).unwrap();
104
138
  }
105
139
  }
106
- PrivateKey(key_data)
140
+ PrivateKeyDer::try_from(key_data).unwrap()
107
141
  }
108
142
 
109
- pub fn generate_ca_signed_cert(domain: &str) -> Result<(Vec<Certificate>, PrivateKey)> {
143
+ pub fn generate_ca_signed_cert(
144
+ domains: Vec<String>,
145
+ ) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)> {
110
146
  info!("Generating New Itsi CA - Self signed Certificate. Use `itsi ca export` to export the CA certificate for import into your local trust store.");
111
147
 
112
- let ca_kp = KeyPair::from_pem(ITS_CA_KEY).unwrap();
113
- let params = CertificateParams::from_ca_cert_pem(ITS_CA_CERT).unwrap();
148
+ let ca_kp = KeyPair::from_pem(ITS_CA_KEY).expect("Failed to load embedded CA key");
149
+ let ca_cert = CertificateParams::from_ca_cert_pem(ITS_CA_CERT)
150
+ .expect("Failed to parse embedded CA certificate")
151
+ .self_signed(&ca_kp)
152
+ .expect("Failed to self-sign embedded CA cert");
114
153
 
115
- let ca_cert = params.self_signed(&ca_kp).unwrap();
116
- let ee_key = KeyPair::generate().unwrap();
154
+ let ee_key = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap();
117
155
  let mut ee_params = CertificateParams::default();
118
156
 
119
- // Set the domain in the subject alternative names (SAN)
120
- ee_params.subject_alt_names = vec![SanType::DnsName(domain.try_into()?)];
121
- // Optionally, set the Common Name (CN) in the distinguished name:
157
+ info!(
158
+ "Generated certificate will be valid for domains {:?}",
159
+ domains
160
+ );
161
+ use std::net::IpAddr;
162
+
163
+ ee_params.subject_alt_names = domains
164
+ .iter()
165
+ .map(|domain| {
166
+ if let Ok(ip) = domain.parse::<IpAddr>() {
167
+ SanType::IpAddress(ip)
168
+ } else {
169
+ SanType::DnsName(domain.clone().try_into().unwrap())
170
+ }
171
+ })
172
+ .collect();
173
+
122
174
  ee_params
123
175
  .distinguished_name
124
- .push(DnType::CommonName, domain);
176
+ .push(DnType::CommonName, domains[0].clone());
125
177
 
126
178
  ee_params.use_authority_key_identifier_extension = true;
127
179
 
128
- let ee_cert = ee_params.signed_by(&ee_key, &ca_cert, &ee_key).unwrap();
180
+ let ee_cert = ee_params.signed_by(&ee_key, &ca_cert, &ca_kp).unwrap();
129
181
  let ee_cert_der = ee_cert.der().to_vec();
130
- let ee_cert = Certificate(ee_cert_der);
131
- Ok((vec![ee_cert], PrivateKey(ee_key.serialize_der())))
132
- }
133
-
134
- /// Retrieves an ACME certificate for a given domain.
135
- pub fn retrieve_acme_cert(domain: &str) -> Result<(Vec<Certificate>, PrivateKey)> {
136
- warn!("Retrieving ACME cert for {}", domain);
137
- generate_ca_signed_cert(domain)
182
+ let ee_cert = CertificateDer::from(ee_cert_der);
183
+ let ca_cert = CertificateDer::from(ca_cert.der().to_vec());
184
+ Ok((
185
+ vec![ee_cert, ca_cert],
186
+ PrivateKeyDer::try_from(ee_key.serialize_der()).unwrap(),
187
+ ))
138
188
  }
@@ -9,4 +9,8 @@ tracing-subscriber = { version = "0.3.19", features = [
9
9
  "env-filter",
10
10
  "std",
11
11
  "fmt",
12
+ "json",
13
+ "ansi",
12
14
  ] }
15
+ tracing-attributes = "0.1"
16
+ atty = "0.2.14"
@@ -1,11 +1,41 @@
1
+ use std::env;
2
+
3
+ use atty::{Stream, is};
1
4
  pub use tracing::{debug, error, info, trace, warn};
2
- use tracing_subscriber::{EnvFilter, fmt};
5
+ pub use tracing_attributes::instrument; // Explicitly export from tracing-attributes
6
+ use tracing_subscriber::{
7
+ EnvFilter,
8
+ fmt::{self, format},
9
+ };
3
10
 
11
+ #[instrument]
4
12
  pub fn init() {
5
- let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
6
- let format = fmt::format().with_level(true).with_target(false).compact();
7
- tracing_subscriber::fmt()
8
- .with_env_filter(env_filter)
13
+ let env_filter = EnvFilter::builder()
14
+ .with_env_var("ITSI_LOG")
15
+ .try_from_env()
16
+ .unwrap_or_else(|_| EnvFilter::new("info"));
17
+
18
+ let format = fmt::format()
19
+ .compact()
20
+ .with_file(false)
21
+ .with_level(true)
22
+ .with_line_number(false)
23
+ .with_source_location(false)
24
+ .with_target(false)
25
+ .with_thread_ids(false);
26
+
27
+ let is_tty = is(Stream::Stdout);
28
+
29
+ let subscriber = tracing_subscriber::fmt()
9
30
  .event_format(format)
10
- .init();
31
+ .with_env_filter(env_filter);
32
+
33
+ if (is_tty && env::var("ITSI_LOG_PLAIN").is_err()) || env::var("ITSI_LOG_ANSI").is_ok() {
34
+ subscriber.with_ansi(true).init();
35
+ } else {
36
+ subscriber
37
+ .fmt_fields(format::JsonFields::default())
38
+ .event_format(fmt::format().json())
39
+ .init();
40
+ }
11
41
  }