itsi-server 0.1.1 → 0.1.13

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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -0
  3. data/CODE_OF_CONDUCT.md +7 -0
  4. data/Cargo.lock +4417 -0
  5. data/Cargo.toml +7 -0
  6. data/README.md +4 -0
  7. data/Rakefile +8 -1
  8. data/_index.md +6 -0
  9. data/exe/itsi +94 -45
  10. data/ext/itsi_error/Cargo.toml +2 -0
  11. data/ext/itsi_error/src/from.rs +68 -0
  12. data/ext/itsi_error/src/lib.rs +18 -34
  13. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  14. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  15. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  16. data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  17. data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
  18. data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
  19. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
  20. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
  21. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
  22. data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
  23. data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
  24. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
  25. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
  26. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
  27. data/ext/itsi_instrument_entry/Cargo.toml +15 -0
  28. data/ext/itsi_instrument_entry/src/lib.rs +31 -0
  29. data/ext/itsi_rb_helpers/Cargo.toml +3 -0
  30. data/ext/itsi_rb_helpers/src/heap_value.rs +139 -0
  31. data/ext/itsi_rb_helpers/src/lib.rs +140 -10
  32. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  33. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  34. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  35. data/ext/itsi_rb_helpers/target/debug/build/rb-sys-eb9ed4ff3a60f995/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  36. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
  37. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
  38. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
  39. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
  40. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
  41. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
  42. data/ext/itsi_scheduler/Cargo.toml +24 -0
  43. data/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
  44. data/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
  45. data/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
  46. data/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
  47. data/ext/itsi_scheduler/src/lib.rs +38 -0
  48. data/ext/itsi_server/Cargo.lock +2956 -0
  49. data/ext/itsi_server/Cargo.toml +73 -13
  50. data/ext/itsi_server/extconf.rb +1 -1
  51. data/ext/itsi_server/src/env.rs +43 -0
  52. data/ext/itsi_server/src/lib.rs +100 -40
  53. data/ext/itsi_server/src/ruby_types/itsi_body_proxy/big_bytes.rs +109 -0
  54. data/ext/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +141 -0
  55. data/ext/itsi_server/src/ruby_types/itsi_grpc_request.rs +147 -0
  56. data/ext/itsi_server/src/ruby_types/itsi_grpc_response.rs +19 -0
  57. data/ext/itsi_server/src/ruby_types/itsi_grpc_stream/mod.rs +216 -0
  58. data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +282 -0
  59. data/ext/itsi_server/src/ruby_types/itsi_http_response.rs +388 -0
  60. data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +225 -0
  61. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +355 -0
  62. data/ext/itsi_server/src/ruby_types/itsi_server.rs +82 -0
  63. data/ext/itsi_server/src/ruby_types/mod.rs +55 -0
  64. data/ext/itsi_server/src/server/bind.rs +75 -31
  65. data/ext/itsi_server/src/server/bind_protocol.rs +37 -0
  66. data/ext/itsi_server/src/server/byte_frame.rs +32 -0
  67. data/ext/itsi_server/src/server/cache_store.rs +74 -0
  68. data/ext/itsi_server/src/server/io_stream.rs +104 -0
  69. data/ext/itsi_server/src/server/itsi_service.rs +172 -0
  70. data/ext/itsi_server/src/server/lifecycle_event.rs +12 -0
  71. data/ext/itsi_server/src/server/listener.rs +332 -132
  72. data/ext/itsi_server/src/server/middleware_stack/middleware.rs +153 -0
  73. data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +47 -0
  74. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +58 -0
  75. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +82 -0
  76. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +321 -0
  77. data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +139 -0
  78. data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +300 -0
  79. data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +287 -0
  80. data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +48 -0
  81. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +127 -0
  82. data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +191 -0
  83. data/ext/itsi_server/src/server/middleware_stack/middlewares/grpc_service.rs +72 -0
  84. data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +85 -0
  85. data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +195 -0
  86. data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
  87. data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +82 -0
  88. data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +216 -0
  89. data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +124 -0
  90. data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +76 -0
  91. data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +43 -0
  92. data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +34 -0
  93. data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +93 -0
  94. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +162 -0
  95. data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +158 -0
  96. data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +12 -0
  97. data/ext/itsi_server/src/server/middleware_stack/mod.rs +315 -0
  98. data/ext/itsi_server/src/server/mod.rs +15 -2
  99. data/ext/itsi_server/src/server/process_worker.rs +229 -0
  100. data/ext/itsi_server/src/server/rate_limiter.rs +565 -0
  101. data/ext/itsi_server/src/server/request_job.rs +11 -0
  102. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +337 -0
  103. data/ext/itsi_server/src/server/serve_strategy/mod.rs +30 -0
  104. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +421 -0
  105. data/ext/itsi_server/src/server/signal.rs +93 -0
  106. data/ext/itsi_server/src/server/static_file_server.rs +984 -0
  107. data/ext/itsi_server/src/server/thread_worker.rs +444 -0
  108. data/ext/itsi_server/src/server/tls/locked_dir_cache.rs +132 -0
  109. data/ext/itsi_server/src/server/tls.rs +187 -60
  110. data/ext/itsi_server/src/server/types.rs +43 -0
  111. data/ext/itsi_tracing/Cargo.toml +5 -0
  112. data/ext/itsi_tracing/src/lib.rs +225 -7
  113. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
  114. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
  115. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
  116. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
  117. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
  118. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
  119. data/lib/itsi/http_request.rb +87 -0
  120. data/lib/itsi/http_response.rb +39 -0
  121. data/lib/itsi/server/Itsi.rb +119 -0
  122. data/lib/itsi/server/config/dsl.rb +506 -0
  123. data/lib/itsi/server/config.rb +131 -0
  124. data/lib/itsi/server/default_app/default_app.rb +38 -0
  125. data/lib/itsi/server/default_app/index.html +91 -0
  126. data/lib/itsi/server/grpc_interface.rb +213 -0
  127. data/lib/itsi/server/rack/handler/itsi.rb +27 -0
  128. data/lib/itsi/server/rack_interface.rb +94 -0
  129. data/lib/itsi/server/scheduler_interface.rb +21 -0
  130. data/lib/itsi/server/scheduler_mode.rb +10 -0
  131. data/lib/itsi/server/signal_trap.rb +29 -0
  132. data/lib/itsi/server/version.rb +1 -1
  133. data/lib/itsi/server.rb +90 -9
  134. data/lib/itsi/standard_headers.rb +86 -0
  135. metadata +122 -31
  136. data/ext/itsi_server/src/request/itsi_request.rs +0 -143
  137. data/ext/itsi_server/src/request/mod.rs +0 -1
  138. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -32
  139. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -52
  140. data/ext/itsi_server/src/server/itsi_server.rs +0 -182
  141. data/ext/itsi_server/src/server/transfer_protocol.rs +0 -23
  142. data/ext/itsi_server/src/stream_writer/mod.rs +0 -21
  143. data/lib/itsi/request.rb +0 -39
@@ -1,20 +1,115 @@
1
1
  use base64::{engine::general_purpose, Engine as _};
2
2
  use itsi_error::Result;
3
- use itsi_tracing::{info, warn};
4
- use rcgen::{CertificateParams, DnType, KeyPair, SanType};
3
+ use itsi_tracing::info;
4
+ use locked_dir_cache::LockedDirCache;
5
+ use rcgen::{
6
+ generate_simple_self_signed, CertificateParams, CertifiedKey, DnType, KeyPair, SanType,
7
+ };
8
+ use rustls::{
9
+ pki_types::{CertificateDer, PrivateKeyDer},
10
+ ClientConfig, RootCertStore,
11
+ };
5
12
  use rustls_pemfile::{certs, pkcs8_private_keys};
6
- use std::{collections::HashMap, fs, io::BufReader};
7
- use tokio_rustls::rustls::{Certificate, PrivateKey, ServerConfig};
8
-
9
- const ITS_CA_CERT: &str = include_str!("./itsi_ca/itsi_ca.crt");
10
- const ITS_CA_KEY: &str = include_str!("./itsi_ca/itsi_ca.key");
11
-
12
- // Generates a TLS configuration based on either :
13
- // * Input "cert" and "key" options (either paths or Base64-encoded strings) or
14
- // * Performs automatic certificate generation/retrieval. Generated certs use an internal self-signed Isti CA.
15
- // If a non-local host or optional domain parameter is provided,
16
- // 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> {
13
+ use std::{
14
+ collections::HashMap,
15
+ fs,
16
+ io::{BufReader, Error},
17
+ sync::Arc,
18
+ };
19
+ use tokio::sync::Mutex;
20
+ use tokio_rustls::{rustls::ServerConfig, TlsAcceptor};
21
+ use tokio_rustls_acme::{AcmeAcceptor, AcmeConfig, AcmeState};
22
+
23
+ use crate::env::{
24
+ ITSI_ACME_CACHE_DIR, ITSI_ACME_CA_PEM_PATH, ITSI_ACME_CONTACT_EMAIL, ITSI_ACME_DIRECTORY_URL,
25
+ ITSI_LOCAL_CA_DIR,
26
+ };
27
+ mod locked_dir_cache;
28
+
29
+ #[derive(Clone)]
30
+ pub enum ItsiTlsAcceptor {
31
+ Manual(TlsAcceptor),
32
+ Automatic(
33
+ AcmeAcceptor,
34
+ Arc<Mutex<AcmeState<Error>>>,
35
+ Arc<ServerConfig>,
36
+ ),
37
+ }
38
+
39
+ /// Generates a TLS configuration based on either :
40
+ /// * Input "cert" and "key" options (either paths or Base64-encoded strings) or
41
+ /// * Performs automatic certificate generation/retrieval. Generated certs use an internal self-signed Isti CA.
42
+ ///
43
+ /// If a non-local host or optional domain parameter is provided,
44
+ /// an automated certificate will attempt to be fetched using let's encrypt.
45
+ pub fn configure_tls(
46
+ host: &str,
47
+ query_params: &HashMap<String, String>,
48
+ ) -> Result<ItsiTlsAcceptor> {
49
+ let domains = query_params
50
+ .get("domains")
51
+ .map(|v| v.split(',').map(String::from).collect::<Vec<_>>())
52
+ .or_else(|| query_params.get("domain").map(|v| vec![v.to_string()]));
53
+
54
+ if query_params.get("cert").is_some_and(|c| c == "acme") {
55
+ if let Some(domains) = domains {
56
+ let directory_url = &*ITSI_ACME_DIRECTORY_URL;
57
+ info!(
58
+ domains = format!("{:?}", domains),
59
+ directory_url, "Requesting acme cert"
60
+ );
61
+ let acme_contact_email = query_params
62
+ .get("acme_email")
63
+ .map(|s| s.to_string())
64
+ .or_else(|| (*ITSI_ACME_CONTACT_EMAIL).as_ref().ok().map(|s| s.to_string()))
65
+ .ok_or_else(|| itsi_error::ItsiError::ArgumentError(
66
+ "acme_email query param or ITSI_ACME_CONTACT_EMAIL must be set before you can auto-generate let's encrypt certificates".to_string(),
67
+ ))?;
68
+
69
+ let acme_config = AcmeConfig::new(domains)
70
+ .contact([format!("mailto:{}", acme_contact_email)])
71
+ .cache(LockedDirCache::new(&*ITSI_ACME_CACHE_DIR))
72
+ .directory(directory_url);
73
+
74
+ let acme_state = if let Ok(ca_pem_path) = &*ITSI_ACME_CA_PEM_PATH {
75
+ let mut root_cert_store = RootCertStore::empty();
76
+
77
+ let ca_pem = fs::read(ca_pem_path).expect("failed to read CA pem file");
78
+ let mut ca_reader = BufReader::new(&ca_pem[..]);
79
+ let der_certs: Vec<CertificateDer> = certs(&mut ca_reader)
80
+ .collect::<std::result::Result<Vec<CertificateDer>, _>>()
81
+ .map_err(|e| {
82
+ itsi_error::ItsiError::ArgumentError(format!(
83
+ "Invalid ACME CA Pem path {:?}",
84
+ e
85
+ ))
86
+ })?;
87
+ root_cert_store.add_parsable_certificates(der_certs);
88
+
89
+ let client_config = ClientConfig::builder()
90
+ .with_root_certificates(root_cert_store)
91
+ .with_no_client_auth();
92
+ acme_config
93
+ .client_tls_config(Arc::new(client_config))
94
+ .state()
95
+ } else {
96
+ acme_config.state()
97
+ };
98
+
99
+ let mut rustls_config = ServerConfig::builder()
100
+ .with_no_client_auth()
101
+ .with_cert_resolver(acme_state.resolver());
102
+
103
+ rustls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
104
+
105
+ let acceptor = acme_state.acceptor();
106
+ return Ok(ItsiTlsAcceptor::Automatic(
107
+ acceptor,
108
+ Arc::new(Mutex::new(acme_state)),
109
+ Arc::new(rustls_config),
110
+ ));
111
+ }
112
+ }
18
113
  let (certs, key) = if let (Some(cert_path), Some(key_path)) =
19
114
  (query_params.get("cert"), query_params.get("key"))
20
115
  {
@@ -22,41 +117,20 @@ pub fn configure_tls(host: &str, query_params: &HashMap<String, String>) -> Resu
22
117
  let certs = load_certs(cert_path);
23
118
  let key = load_private_key(key_path);
24
119
  (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
120
  } else {
46
- generate_ca_signed_cert(host)?
121
+ generate_ca_signed_cert(domains.unwrap_or(vec![host.to_owned()]))?
47
122
  };
48
123
 
49
124
  let mut config = ServerConfig::builder()
50
- .with_safe_defaults()
51
125
  .with_no_client_auth()
52
126
  .with_single_cert(certs, key)
53
127
  .expect("Failed to build TLS config");
54
128
 
55
129
  config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
56
- Ok(config)
130
+ Ok(ItsiTlsAcceptor::Manual(TlsAcceptor::from(Arc::new(config))))
57
131
  }
58
132
 
59
- pub fn load_certs(path: &str) -> Vec<Certificate> {
133
+ pub fn load_certs(path: &str) -> Vec<CertificateDer<'static>> {
60
134
  let data = if let Some(stripped) = path.strip_prefix("base64:") {
61
135
  general_purpose::STANDARD
62
136
  .decode(stripped)
@@ -74,14 +148,20 @@ pub fn load_certs(path: &str) -> Vec<Certificate> {
74
148
  })
75
149
  .collect::<Result<_>>()
76
150
  .expect("Failed to parse certificate file");
77
- certs_der.into_iter().map(Certificate).collect()
151
+ certs_der
152
+ .into_iter()
153
+ .map(|vec| {
154
+ // Convert the owned Vec<u8> into a CertificateDer and force 'static.
155
+ unsafe { std::mem::transmute(CertificateDer::from(vec)) }
156
+ })
157
+ .collect()
78
158
  } else {
79
- vec![Certificate(data)]
159
+ vec![CertificateDer::from(data)]
80
160
  }
81
161
  }
82
162
 
83
163
  /// Loads a private key from a file or Base64.
84
- pub fn load_private_key(path: &str) -> PrivateKey {
164
+ pub fn load_private_key(path: &str) -> PrivateKeyDer<'static> {
85
165
  let key_data = if let Some(stripped) = path.strip_prefix("base64:") {
86
166
  general_purpose::STANDARD
87
167
  .decode(stripped)
@@ -100,39 +180,86 @@ pub fn load_private_key(path: &str) -> PrivateKey {
100
180
  .collect::<Result<_>>()
101
181
  .expect("Failed to parse private key");
102
182
  if !keys.is_empty() {
103
- return PrivateKey(keys[0].clone());
183
+ return PrivateKeyDer::try_from(keys[0].clone()).unwrap();
104
184
  }
105
185
  }
106
- PrivateKey(key_data)
186
+ PrivateKeyDer::try_from(key_data).unwrap()
107
187
  }
108
188
 
109
- pub fn generate_ca_signed_cert(domain: &str) -> Result<(Vec<Certificate>, PrivateKey)> {
110
- info!("Generating New Itsi CA - Self signed Certificate. Use `itsi ca export` to export the CA certificate for import into your local trust store.");
189
+ pub fn generate_ca_signed_cert(
190
+ domains: Vec<String>,
191
+ ) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)> {
192
+ info!(
193
+ domains = format!("{}", domains.join(", ")),
194
+ "Self signed cert",
195
+ );
196
+ info!(
197
+ "Add {} to your system's trusted cert store to resolve certificate errors.",
198
+ format!("{}/itsi_dev_ca.crt", ITSI_LOCAL_CA_DIR.to_str().unwrap())
199
+ );
200
+ info!("Dev CA path can be overridden by setting env var: `ITSI_LOCAL_CA_DIR`.");
201
+ let (ca_key_pem, ca_cert_pem) = get_or_create_local_dev_ca()?;
111
202
 
112
- let ca_kp = KeyPair::from_pem(ITS_CA_KEY).unwrap();
113
- let params = CertificateParams::from_ca_cert_pem(ITS_CA_CERT).unwrap();
203
+ let ca_kp = KeyPair::from_pem(&ca_key_pem).expect("Failed to load CA key");
204
+ let ca_cert = CertificateParams::from_ca_cert_pem(&ca_cert_pem)
205
+ .expect("Failed to parse embedded CA certificate")
206
+ .self_signed(&ca_kp)
207
+ .expect("Failed to self-sign embedded CA cert");
114
208
 
115
- let ca_cert = params.self_signed(&ca_kp).unwrap();
116
- let ee_key = KeyPair::generate().unwrap();
209
+ let ee_key = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap();
117
210
  let mut ee_params = CertificateParams::default();
118
211
 
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:
212
+ use std::net::IpAddr;
213
+
214
+ ee_params.subject_alt_names = domains
215
+ .iter()
216
+ .map(|domain| {
217
+ if let Ok(ip) = domain.parse::<IpAddr>() {
218
+ SanType::IpAddress(ip)
219
+ } else {
220
+ SanType::DnsName(domain.clone().try_into().unwrap())
221
+ }
222
+ })
223
+ .collect();
224
+
122
225
  ee_params
123
226
  .distinguished_name
124
- .push(DnType::CommonName, domain);
227
+ .push(DnType::CommonName, domains[0].clone());
125
228
 
126
229
  ee_params.use_authority_key_identifier_extension = true;
127
230
 
128
- let ee_cert = ee_params.signed_by(&ee_key, &ca_cert, &ee_key).unwrap();
231
+ let ee_cert = ee_params.signed_by(&ee_key, &ca_cert, &ca_kp).unwrap();
129
232
  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())))
233
+ let ee_cert = CertificateDer::from(ee_cert_der);
234
+ let ca_cert = CertificateDer::from(ca_cert.der().to_vec());
235
+ Ok((
236
+ vec![ee_cert, ca_cert],
237
+ PrivateKeyDer::try_from(ee_key.serialize_der()).unwrap(),
238
+ ))
132
239
  }
133
240
 
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)
241
+ fn get_or_create_local_dev_ca() -> Result<(String, String)> {
242
+ let ca_dir = &*ITSI_LOCAL_CA_DIR;
243
+ fs::create_dir_all(ca_dir)?;
244
+
245
+ let key_path = ca_dir.join("itsi_dev_ca.key");
246
+ let cert_path = ca_dir.join("itsi_dev_ca.crt");
247
+
248
+ if key_path.exists() && cert_path.exists() {
249
+ // Already have a local CA
250
+ let key_pem = fs::read_to_string(&key_path)?;
251
+ let cert_pem = fs::read_to_string(&cert_path)?;
252
+
253
+ Ok((key_pem, cert_pem))
254
+ } else {
255
+ let subject_alt_names = vec!["dev.itsi.fyi".to_string(), "localhost".to_string()];
256
+
257
+ let CertifiedKey { cert, key_pair } =
258
+ generate_simple_self_signed(subject_alt_names).unwrap();
259
+
260
+ fs::write(&key_path, key_pair.serialize_pem())?;
261
+ fs::write(&cert_path, cert.pem())?;
262
+
263
+ Ok((key_pair.serialize_pem(), cert.pem()))
264
+ }
138
265
  }
@@ -0,0 +1,43 @@
1
+ use std::convert::Infallible;
2
+
3
+ use bytes::Bytes;
4
+ use http::{Request, Response};
5
+ use http_body_util::combinators::BoxBody;
6
+ use hyper::body::Incoming;
7
+
8
+ pub type HttpResponse = Response<BoxBody<Bytes, Infallible>>;
9
+ pub type HttpRequest = Request<Incoming>;
10
+
11
+ pub trait RequestExt {
12
+ fn content_type(&self) -> Option<&str>;
13
+ fn accept(&self) -> Option<&str>;
14
+ fn header(&self, header_name: &str) -> Option<&str>;
15
+ fn query_param(&self, query_name: &str) -> Option<&str>;
16
+ }
17
+
18
+ impl RequestExt for HttpRequest {
19
+ fn content_type(&self) -> Option<&str> {
20
+ self.headers()
21
+ .get("content-type")
22
+ .map(|hv| hv.to_str().unwrap_or(""))
23
+ }
24
+
25
+ fn accept(&self) -> Option<&str> {
26
+ self.headers()
27
+ .get("accept")
28
+ .map(|hv| hv.to_str().unwrap_or(""))
29
+ }
30
+
31
+ fn header(&self, header_name: &str) -> Option<&str> {
32
+ self.headers()
33
+ .get(header_name)
34
+ .map(|hv| hv.to_str().unwrap_or(""))
35
+ }
36
+
37
+ fn query_param(&self, query_name: &str) -> Option<&str> {
38
+ self.uri()
39
+ .query()
40
+ .and_then(|query| query.split('&').find(|param| param.starts_with(query_name)))
41
+ .map(|param| param.split('=').nth(1).unwrap_or(""))
42
+ }
43
+ }
@@ -9,4 +9,9 @@ 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"
17
+ tracing-appender = "0.2.3"
@@ -1,11 +1,229 @@
1
+ use atty::{Stream, is};
2
+ use std::{
3
+ env,
4
+ sync::{Mutex, OnceLock},
5
+ };
1
6
  pub use tracing::{debug, error, info, trace, warn};
2
- use tracing_subscriber::{EnvFilter, fmt};
7
+ use tracing_appender::rolling;
8
+ use tracing_subscriber::Layer;
9
+ use tracing_subscriber::fmt::writer::BoxMakeWriter;
10
+ use tracing_subscriber::{EnvFilter, fmt, prelude::*, reload};
3
11
 
12
+ // Global reload handle for changing the level at runtime.
13
+ static RELOAD_HANDLE: OnceLock<
14
+ Mutex<Option<reload::Handle<EnvFilter, tracing_subscriber::Registry>>>,
15
+ > = OnceLock::new();
16
+
17
+ /// Log format: Plain or JSON.
18
+ #[derive(Debug, Clone)]
19
+ pub enum LogFormat {
20
+ Plain,
21
+ Json,
22
+ }
23
+
24
+ /// Log target: STDOUT, File, or Both.
25
+ #[derive(Debug, Clone)]
26
+ pub enum LogTarget {
27
+ Stdout,
28
+ File(String), // file name (rotated daily)
29
+ Both(String), // file name (rotated daily) plus STDOUT
30
+ }
31
+
32
+ /// Logger configuration.
33
+ #[derive(Debug, Clone)]
34
+ pub struct LogConfig {
35
+ /// Log level as a string (e.g. "info", "debug").
36
+ pub level: String,
37
+ /// Format: Plain (with optional ANSI) or JSON.
38
+ pub format: LogFormat,
39
+ /// Target: STDOUT, File, or Both.
40
+ pub target: LogTarget,
41
+ /// Whether to enable ANSI coloring (for plain text).
42
+ pub use_ansi: bool,
43
+ }
44
+
45
+ impl Default for LogConfig {
46
+ fn default() -> Self {
47
+ let level = env::var("ITSI_LOG").unwrap_or_else(|_| "info".into());
48
+ let format = match env::var("ITSI_LOG_FORMAT").as_deref() {
49
+ Ok("json") => LogFormat::Json,
50
+ _ => LogFormat::Plain,
51
+ };
52
+ let target = match env::var("ITSI_LOG_TARGET").as_deref() {
53
+ Ok("file") => {
54
+ let file = env::var("ITSI_LOG_FILE").unwrap_or_else(|_| "app.log".into());
55
+ LogTarget::File(file)
56
+ }
57
+ Ok("both") => {
58
+ let file = env::var("ITSI_LOG_FILE").unwrap_or_else(|_| "app.log".into());
59
+ LogTarget::Both(file)
60
+ }
61
+ _ => LogTarget::Stdout,
62
+ };
63
+ // If ITSI_LOG_ANSI is set, use that; otherwise, use ANSI if stdout is a TTY.
64
+ let use_ansi = env::var("ITSI_LOG_ANSI")
65
+ .map(|s| s == "true")
66
+ .unwrap_or_else(|_| is(Stream::Stdout));
67
+ Self {
68
+ level,
69
+ format,
70
+ target,
71
+ use_ansi,
72
+ }
73
+ }
74
+ }
75
+
76
+ /// Initialize the global tracing subscriber with the default configuration.
4
77
  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)
9
- .event_format(format)
10
- .init();
78
+ init_with_config(LogConfig::default());
79
+ }
80
+
81
+ /// Initialize the global tracing subscriber with a given configuration.
82
+ pub fn init_with_config(config: LogConfig) {
83
+ // Build an EnvFilter from the configured level.
84
+ let env_filter = EnvFilter::new(config.level);
85
+
86
+ // Build the formatting layer based on target and format.
87
+ let fmt_layer = match config.target {
88
+ LogTarget::Stdout => match config.format {
89
+ LogFormat::Plain => fmt::layer()
90
+ .compact()
91
+ .with_file(false)
92
+ .with_line_number(false)
93
+ .with_target(false)
94
+ .with_thread_ids(false)
95
+ .with_writer(BoxMakeWriter::new(std::io::stdout))
96
+ .with_ansi(config.use_ansi)
97
+ .boxed(),
98
+ LogFormat::Json => fmt::layer()
99
+ .compact()
100
+ .with_file(false)
101
+ .with_line_number(false)
102
+ .with_target(false)
103
+ .with_thread_ids(false)
104
+ .with_writer(BoxMakeWriter::new(std::io::stdout))
105
+ .with_ansi(config.use_ansi)
106
+ .json()
107
+ .boxed(),
108
+ },
109
+ LogTarget::File(file) => match config.format {
110
+ LogFormat::Plain => fmt::layer()
111
+ .compact()
112
+ .with_file(false)
113
+ .with_line_number(false)
114
+ .with_target(false)
115
+ .with_thread_ids(false)
116
+ .with_writer(BoxMakeWriter::new({
117
+ let file = file.clone();
118
+ move || rolling::daily(".", file.clone())
119
+ }))
120
+ .with_ansi(false)
121
+ .boxed(),
122
+ LogFormat::Json => fmt::layer()
123
+ .compact()
124
+ .with_file(false)
125
+ .with_line_number(false)
126
+ .with_target(false)
127
+ .with_thread_ids(false)
128
+ .with_writer(BoxMakeWriter::new({
129
+ let file = file.clone();
130
+ move || rolling::daily(".", file.clone())
131
+ }))
132
+ .with_ansi(false)
133
+ .json()
134
+ .boxed(),
135
+ },
136
+ LogTarget::Both(file) => {
137
+ // For "Both" target, handle each format separately to avoid type mismatches
138
+ match config.format {
139
+ LogFormat::Plain => {
140
+ let stdout_layer = fmt::layer()
141
+ .compact()
142
+ .with_file(false)
143
+ .with_line_number(false)
144
+ .with_target(false)
145
+ .with_thread_ids(false)
146
+ .with_writer(BoxMakeWriter::new(std::io::stdout))
147
+ .with_ansi(config.use_ansi);
148
+
149
+ let file_layer = fmt::layer()
150
+ .compact()
151
+ .with_file(false)
152
+ .with_line_number(false)
153
+ .with_target(false)
154
+ .with_thread_ids(false)
155
+ .with_writer(BoxMakeWriter::new({
156
+ let file = file.clone();
157
+ move || rolling::daily(".", file.clone())
158
+ }))
159
+ .with_ansi(false);
160
+
161
+ stdout_layer.and_then(file_layer).boxed()
162
+ }
163
+ LogFormat::Json => {
164
+ let stdout_layer = fmt::layer()
165
+ .compact()
166
+ .with_file(false)
167
+ .with_line_number(false)
168
+ .with_target(false)
169
+ .with_thread_ids(false)
170
+ .with_writer(BoxMakeWriter::new(std::io::stdout))
171
+ .with_ansi(config.use_ansi)
172
+ .json();
173
+
174
+ let file_layer = fmt::layer()
175
+ .compact()
176
+ .with_file(false)
177
+ .with_line_number(false)
178
+ .with_target(false)
179
+ .with_thread_ids(false)
180
+ .with_writer(BoxMakeWriter::new({
181
+ let file = file.clone();
182
+ move || rolling::daily(".", file.clone())
183
+ }))
184
+ .with_ansi(false)
185
+ .json();
186
+
187
+ stdout_layer.and_then(file_layer).boxed()
188
+ }
189
+ }
190
+ }
191
+ };
192
+
193
+ // Create a reloadable filter layer so we can update the level at runtime.
194
+ let (filter_layer, handle) = reload::Layer::new(env_filter);
195
+
196
+ // Build the subscriber registry
197
+ let subscriber = tracing_subscriber::registry()
198
+ .with(filter_layer)
199
+ .with(fmt_layer);
200
+
201
+ tracing::subscriber::set_global_default(subscriber)
202
+ .expect("Unable to set global tracing subscriber");
203
+
204
+ RELOAD_HANDLE.set(Mutex::new(Some(handle))).unwrap();
205
+ }
206
+
207
+ /// Change the log level at runtime.
208
+ pub fn set_level(new_level: &str) {
209
+ if let Some(handle) = RELOAD_HANDLE.get().unwrap().lock().unwrap().as_ref() {
210
+ handle
211
+ .modify(|filter| *filter = EnvFilter::new(new_level))
212
+ .expect("Failed to update log level");
213
+ } else {
214
+ eprintln!("Reload handle not initialized; call init() first.");
215
+ }
216
+ }
217
+
218
+ /// Run a function silently by temporarily setting a no-op subscriber.
219
+ pub fn run_silently<F, R>(f: F) -> R
220
+ where
221
+ F: FnOnce() -> R,
222
+ {
223
+ let no_op_subscriber = tracing_subscriber::fmt()
224
+ .with_writer(std::io::sink)
225
+ .with_max_level(tracing_subscriber::filter::LevelFilter::OFF)
226
+ .finish();
227
+ let dispatch = tracing::Dispatch::new(no_op_subscriber);
228
+ tracing::dispatcher::with_default(&dispatch, f)
11
229
  }