itsi-server 0.1.19 → 0.1.20

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 (174) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +950 -239
  3. data/README.md +2 -0
  4. data/exe/itsi +5 -5
  5. data/ext/itsi_acme/Cargo.toml +86 -0
  6. data/ext/itsi_acme/examples/high_level.rs +63 -0
  7. data/ext/itsi_acme/examples/high_level_warp.rs +52 -0
  8. data/ext/itsi_acme/examples/low_level.rs +87 -0
  9. data/ext/itsi_acme/examples/low_level_axum.rs +66 -0
  10. data/ext/itsi_acme/src/acceptor.rs +81 -0
  11. data/ext/itsi_acme/src/acme.rs +354 -0
  12. data/ext/itsi_acme/src/axum.rs +86 -0
  13. data/ext/itsi_acme/src/cache.rs +39 -0
  14. data/ext/itsi_acme/src/caches/boxed.rs +80 -0
  15. data/ext/itsi_acme/src/caches/composite.rs +69 -0
  16. data/ext/itsi_acme/src/caches/dir.rs +106 -0
  17. data/ext/itsi_acme/src/caches/mod.rs +11 -0
  18. data/ext/itsi_acme/src/caches/no.rs +78 -0
  19. data/ext/itsi_acme/src/caches/test.rs +136 -0
  20. data/ext/itsi_acme/src/config.rs +172 -0
  21. data/ext/itsi_acme/src/https_helper.rs +69 -0
  22. data/ext/itsi_acme/src/incoming.rs +142 -0
  23. data/ext/itsi_acme/src/jose.rs +161 -0
  24. data/ext/itsi_acme/src/lib.rs +142 -0
  25. data/ext/itsi_acme/src/resolver.rs +59 -0
  26. data/ext/itsi_acme/src/state.rs +424 -0
  27. data/ext/itsi_server/Cargo.toml +3 -3
  28. data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +2 -2
  29. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +150 -19
  30. data/ext/itsi_server/src/ruby_types/itsi_server.rs +1 -0
  31. data/ext/itsi_server/src/server/binds/listener.rs +34 -29
  32. data/ext/itsi_server/src/server/binds/tls/locked_dir_cache.rs +2 -2
  33. data/ext/itsi_server/src/server/binds/tls.rs +1 -1
  34. data/ext/itsi_server/src/server/middleware_stack/middleware.rs +33 -28
  35. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +56 -3
  36. data/ext/itsi_server/src/server/middleware_stack/middlewares/csp.rs +179 -0
  37. data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +25 -2
  38. data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +3 -3
  39. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +2 -1
  40. data/ext/itsi_server/src/server/middleware_stack/mod.rs +32 -34
  41. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +10 -4
  42. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +30 -7
  43. data/ext/itsi_server/src/server/thread_worker.rs +2 -2
  44. data/ext/itsi_server/src/services/static_file_server.rs +30 -28
  45. data/ext/itsi_tracing/src/lib.rs +39 -8
  46. data/lib/itsi/server/config/config_helpers.rb +93 -0
  47. data/lib/itsi/server/config/dsl.rb +81 -33
  48. data/lib/itsi/server/config/known_paths/KitchensinkDirectories.txt +2346 -0
  49. data/lib/itsi/server/config/known_paths/Randomfiles.txt +24 -0
  50. data/lib/itsi/server/config/known_paths/UnixDotfiles.txt +52 -0
  51. data/lib/itsi/server/config/known_paths/backdoors/ASP_CommonBackdoors.txt +29 -0
  52. data/lib/itsi/server/config/known_paths/backdoors/bot_control_panels.txt +1668 -0
  53. data/lib/itsi/server/config/known_paths/backdoors/shells.txt +1167 -0
  54. data/lib/itsi/server/config/known_paths/cgi/CGI_HTTP_POST.txt +7 -0
  55. data/lib/itsi/server/config/known_paths/cgi/CGI_HTTP_POST_Windows.txt +6 -0
  56. data/lib/itsi/server/config/known_paths/cgi/CGI_Microsoft.txt +79 -0
  57. data/lib/itsi/server/config/known_paths/cgi/CGI_XPlatform.txt +3948 -0
  58. data/lib/itsi/server/config/known_paths/cms/README.md +5 -0
  59. data/lib/itsi/server/config/known_paths/cms/drupal_plugins.txt +6320 -0
  60. data/lib/itsi/server/config/known_paths/cms/drupal_themes.txt +828 -0
  61. data/lib/itsi/server/config/known_paths/cms/joomla_plugins.txt +224 -0
  62. data/lib/itsi/server/config/known_paths/cms/joomla_themes.txt +30 -0
  63. data/lib/itsi/server/config/known_paths/cms/php-nuke.txt +2142 -0
  64. data/lib/itsi/server/config/known_paths/cms/wordpress.txt +1566 -0
  65. data/lib/itsi/server/config/known_paths/cms/wp_common_theme_files.txt +46 -0
  66. data/lib/itsi/server/config/known_paths/cms/wp_plugins.txt +13366 -0
  67. data/lib/itsi/server/config/known_paths/cms/wp_plugins_full.txt +68662 -0
  68. data/lib/itsi/server/config/known_paths/cms/wp_plugins_top225.txt +225 -0
  69. data/lib/itsi/server/config/known_paths/cms/wp_themes.readme +12 -0
  70. data/lib/itsi/server/config/known_paths/cms/wp_themes.txt +7336 -0
  71. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/3CharExtBrute.txt +17576 -0
  72. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/CommonWebExtensions.txt +80 -0
  73. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/Extensions.Backup.txt +14 -0
  74. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/Extensions.Common.txt +865 -0
  75. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/Extensions.Compressed.txt +186 -0
  76. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/Extensions.Mostcommon.txt +30 -0
  77. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/Extensions.Skipfish.txt +93 -0
  78. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/WordlistSkipfish.txt +1918 -0
  79. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/copy_of.txt +8 -0
  80. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-large-directories-lowercase.txt +56180 -0
  81. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-large-directories.txt +62290 -0
  82. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-large-extensions-lowercase.txt +2367 -0
  83. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-large-extensions.txt +2450 -0
  84. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-large-files-lowercase.txt +35323 -0
  85. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-large-files.txt +37037 -0
  86. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-large-words-lowercase.txt +107982 -0
  87. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-large-words.txt +119600 -0
  88. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-medium-directories-lowercase.txt +26593 -0
  89. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-medium-directories.txt +30009 -0
  90. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-medium-extensions-lowercase.txt +1233 -0
  91. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-medium-extensions.txt +1289 -0
  92. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-medium-files-lowercase.txt +16243 -0
  93. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-medium-files.txt +17128 -0
  94. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-medium-words-lowercase.txt +56293 -0
  95. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-medium-words.txt +63087 -0
  96. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-small-directories-lowercase.txt +17776 -0
  97. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-small-directories.txt +20122 -0
  98. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-small-extensions-lowercase.txt +914 -0
  99. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-small-extensions.txt +963 -0
  100. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-small-files-lowercase.txt +10848 -0
  101. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-small-files.txt +11424 -0
  102. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-small-words-lowercase.txt +38267 -0
  103. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/raft-small-words.txt +43003 -0
  104. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/spanish.txt +445 -0
  105. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/test_demo.txt +36 -0
  106. data/lib/itsi/server/config/known_paths/filename-dirname-bruteforce/upload_variants.txt +44 -0
  107. data/lib/itsi/server/config/known_paths/login-file-locations/Logins.txt +71 -0
  108. data/lib/itsi/server/config/known_paths/login-file-locations/cfm.txt +294 -0
  109. data/lib/itsi/server/config/known_paths/login-file-locations/html.txt +295 -0
  110. data/lib/itsi/server/config/known_paths/login-file-locations/jsp.txt +294 -0
  111. data/lib/itsi/server/config/known_paths/login-file-locations/php.txt +294 -0
  112. data/lib/itsi/server/config/known_paths/login-file-locations/windows-asp.txt +294 -0
  113. data/lib/itsi/server/config/known_paths/login-file-locations/windows-aspx.txt +294 -0
  114. data/lib/itsi/server/config/known_paths/password-file-locations/Passwords.txt +47 -0
  115. data/lib/itsi/server/config/known_paths/php/PHP.txt +30 -0
  116. data/lib/itsi/server/config/known_paths/php/PHP_CommonBackdoors.txt +5 -0
  117. data/lib/itsi/server/config/known_paths/proxy-conf.txt +31 -0
  118. data/lib/itsi/server/config/known_paths/tftp.txt +79 -0
  119. data/lib/itsi/server/config/known_paths/webservers-appservers/ADFS.txt +86 -0
  120. data/lib/itsi/server/config/known_paths/webservers-appservers/AdobeXML.txt +16 -0
  121. data/lib/itsi/server/config/known_paths/webservers-appservers/Apache.txt +101 -0
  122. data/lib/itsi/server/config/known_paths/webservers-appservers/ApacheTomcat.txt +47 -0
  123. data/lib/itsi/server/config/known_paths/webservers-appservers/Apache_Axis.txt +16 -0
  124. data/lib/itsi/server/config/known_paths/webservers-appservers/ColdFusion.txt +111 -0
  125. data/lib/itsi/server/config/known_paths/webservers-appservers/FatwireCMS.txt +390 -0
  126. data/lib/itsi/server/config/known_paths/webservers-appservers/Frontpage.txt +38 -0
  127. data/lib/itsi/server/config/known_paths/webservers-appservers/HP_System_Mgmt_Homepage.txt +239 -0
  128. data/lib/itsi/server/config/known_paths/webservers-appservers/HTTP_POST_Microsoft.txt +2 -0
  129. data/lib/itsi/server/config/known_paths/webservers-appservers/Hyperion.txt +578 -0
  130. data/lib/itsi/server/config/known_paths/webservers-appservers/IIS.txt +187 -0
  131. data/lib/itsi/server/config/known_paths/webservers-appservers/JBoss.txt +5 -0
  132. data/lib/itsi/server/config/known_paths/webservers-appservers/JRun.txt +13 -0
  133. data/lib/itsi/server/config/known_paths/webservers-appservers/JavaServlets_Common.txt +3 -0
  134. data/lib/itsi/server/config/known_paths/webservers-appservers/Joomla_exploitable.txt +1937 -0
  135. data/lib/itsi/server/config/known_paths/webservers-appservers/LotusNotes.txt +206 -0
  136. data/lib/itsi/server/config/known_paths/webservers-appservers/Netware.txt +18 -0
  137. data/lib/itsi/server/config/known_paths/webservers-appservers/Oracle9i.txt +60 -0
  138. data/lib/itsi/server/config/known_paths/webservers-appservers/OracleAppServer.txt +192 -0
  139. data/lib/itsi/server/config/known_paths/webservers-appservers/README.md +6 -0
  140. data/lib/itsi/server/config/known_paths/webservers-appservers/Ruby_Rails.txt +121 -0
  141. data/lib/itsi/server/config/known_paths/webservers-appservers/SAP.txt +463 -0
  142. data/lib/itsi/server/config/known_paths/webservers-appservers/Sharepoint.txt +1707 -0
  143. data/lib/itsi/server/config/known_paths/webservers-appservers/SiteMinder.txt +19 -0
  144. data/lib/itsi/server/config/known_paths/webservers-appservers/SunAppServerGlassfish.txt +51 -0
  145. data/lib/itsi/server/config/known_paths/webservers-appservers/SuniPlanet.txt +35 -0
  146. data/lib/itsi/server/config/known_paths/webservers-appservers/Vignette.txt +73 -0
  147. data/lib/itsi/server/config/known_paths/webservers-appservers/Weblogic.txt +160 -0
  148. data/lib/itsi/server/config/known_paths/webservers-appservers/Websphere.txt +366 -0
  149. data/lib/itsi/server/config/known_paths/wellknown-rfc5785.txt +30 -0
  150. data/lib/itsi/server/config/known_paths.rb +17 -0
  151. data/lib/itsi/server/config/middleware/_index.md +54 -0
  152. data/lib/itsi/server/config/middleware/log_requests.md +63 -0
  153. data/lib/itsi/server/config/middleware/log_requests.rb +33 -0
  154. data/lib/itsi/server/config/middleware.rb +9 -0
  155. data/lib/itsi/server/config/option.rb +9 -0
  156. data/lib/itsi/server/config/options/_index.md +36 -0
  157. data/lib/itsi/server/config/options/fiber_scheduler.md +35 -0
  158. data/lib/itsi/server/config/options/fiber_scheduler.rb +18 -0
  159. data/lib/itsi/server/config/options/threads.md +39 -0
  160. data/lib/itsi/server/config/options/threads.rb +17 -0
  161. data/lib/itsi/server/config/options/workers.md +43 -0
  162. data/lib/itsi/server/config/options/workers.rb +17 -0
  163. data/lib/itsi/server/config/typed_struct.rb +203 -0
  164. data/lib/itsi/server/config.rb +124 -30
  165. data/lib/itsi/server/signal_trap.rb +5 -1
  166. data/lib/itsi/server/typed_handlers/source_parser.rb +1 -1
  167. data/lib/itsi/server/version.rb +1 -1
  168. data/lib/itsi/server.rb +27 -6
  169. data/lib/ruby_lsp/itsi/addon.rb +64 -48
  170. metadata +141 -5
  171. data/CHANGELOG.md +0 -10
  172. data/CODE_OF_CONDUCT.md +0 -139
  173. data/LICENSE.txt +0 -21
  174. data/_index.md +0 -6
@@ -18,7 +18,7 @@ use std::{
18
18
  collections::{HashMap, HashSet},
19
19
  sync::OnceLock,
20
20
  };
21
- use tracing::error;
21
+ use tracing::{debug, error};
22
22
 
23
23
  #[derive(Debug, Clone, Deserialize)]
24
24
  pub struct AuthJwt {
@@ -137,6 +137,11 @@ struct Claims {
137
137
  #[async_trait]
138
138
  impl MiddlewareLayer for AuthJwt {
139
139
  async fn initialize(&self) -> Result<()> {
140
+ debug!(
141
+ target: "middleware::auth_jwt",
142
+ "Instantiating auth_jwt with {} verifiers", self.verifiers.len()
143
+ );
144
+
140
145
  let keys: HashMap<JwtAlgorithm, Vec<DecodingKey>> = self
141
146
  .verifiers
142
147
  .iter()
@@ -145,10 +150,24 @@ impl MiddlewareLayer for AuthJwt {
145
150
  let keys: itsi_error::Result<Vec<DecodingKey>> = key_strings
146
151
  .iter()
147
152
  .map(|key_string| algorithm.key_from(key_string))
153
+ .inspect(|key_result| {
154
+ if key_result.is_err() {
155
+ debug!(
156
+ target: "middleware::auth_jwt",
157
+ "Failed to load key for algorithm {:?}", algorithm
158
+ )
159
+ } else {
160
+ debug!(
161
+ target: "middleware::auth_jwt",
162
+ "Loaded key for algorithm {:?}", algorithm
163
+ )
164
+ }
165
+ })
148
166
  .collect();
149
167
  keys.map(|keys| (algo, keys))
150
168
  })
151
169
  .collect::<itsi_error::Result<HashMap<JwtAlgorithm, Vec<DecodingKey>>>>()?;
170
+
152
171
  self.keys
153
172
  .set(keys)
154
173
  .map_err(|_| ItsiError::new("Failed to set keys"))?;
@@ -158,11 +177,16 @@ impl MiddlewareLayer for AuthJwt {
158
177
  async fn before(
159
178
  &self,
160
179
  req: HttpRequest,
161
- _context: &mut HttpRequestContext,
180
+ _: &mut HttpRequestContext,
162
181
  ) -> Result<Either<HttpRequest, HttpResponse>> {
163
182
  // Retrieve the JWT token from either a header or a query parameter.
164
183
  let token_str = match &self.token_source {
165
184
  TokenSource::Header { name, prefix } => {
185
+ debug!(
186
+ target: "middleware::auth_jwt",
187
+ "Extracting JWT from header: {}, prefix: {:?}",
188
+ name, prefix
189
+ );
166
190
  if let Some(header) = req.header(name) {
167
191
  if let Some(prefix) = prefix {
168
192
  Some(header.strip_prefix(prefix).unwrap_or("").trim_ascii())
@@ -173,10 +197,21 @@ impl MiddlewareLayer for AuthJwt {
173
197
  None
174
198
  }
175
199
  }
176
- TokenSource::Query(query_name) => req.query_param(query_name),
200
+ TokenSource::Query(query_name) => {
201
+ debug!(
202
+ target: "middleware::auth_jwt",
203
+ "Extracting JWT from query parameter: {}",
204
+ query_name
205
+ );
206
+ req.query_param(query_name)
207
+ }
177
208
  };
178
209
 
179
210
  if token_str.is_none() {
211
+ debug!(
212
+ target: "middleware::auth_jwt",
213
+ "No JWT found in headers or query parameters"
214
+ );
180
215
  return Ok(Either::Right(
181
216
  self.error_response
182
217
  .to_http_response(req.accept().into())
@@ -186,8 +221,13 @@ impl MiddlewareLayer for AuthJwt {
186
221
  let token_str = token_str.unwrap();
187
222
  let header =
188
223
  decode_header(token_str).map_err(|_| ItsiError::new("Invalid token header"))?;
224
+
189
225
  let alg: JwtAlgorithm = header.alg.into();
190
226
 
227
+ debug!(
228
+ target: "middleware::auth_jwt",
229
+ "Matched algorithm {:?}", alg
230
+ );
191
231
  if !self.verifiers.contains_key(&alg) {
192
232
  return Ok(Either::Right(
193
233
  self.error_response
@@ -225,6 +265,7 @@ impl MiddlewareLayer for AuthJwt {
225
265
  None
226
266
  }
227
267
  });
268
+
228
269
  let token_data = if let Some(data) = token_data {
229
270
  data
230
271
  } else {
@@ -244,6 +285,10 @@ impl MiddlewareLayer for AuthJwt {
244
285
  Audience::Multiple(v) => v.iter().cloned().collect(),
245
286
  };
246
287
  if expected_audiences.is_disjoint(&token_auds) {
288
+ debug!(
289
+ "AUD check failed, token_auds: {:?}, expected_audiences: {:?}",
290
+ token_auds, expected_audiences
291
+ );
247
292
  return Ok(Either::Right(
248
293
  self.error_response
249
294
  .to_http_response(req.accept().into())
@@ -256,6 +301,10 @@ impl MiddlewareLayer for AuthJwt {
256
301
  if let Some(expected_subjects) = &self.subjects {
257
302
  if let Some(sub) = &claims.sub {
258
303
  if !expected_subjects.contains(sub) {
304
+ debug!(
305
+ "SUB check failed, token_sub: {:?}, expected_subjects: {:?}",
306
+ sub, expected_subjects
307
+ );
259
308
  return Ok(Either::Right(
260
309
  self.error_response
261
310
  .to_http_response(req.accept().into())
@@ -269,6 +318,10 @@ impl MiddlewareLayer for AuthJwt {
269
318
  if let Some(expected_issuers) = &self.issuers {
270
319
  if let Some(iss) = &claims.iss {
271
320
  if !expected_issuers.contains(iss) {
321
+ debug!(
322
+ "ISS check failed, token_iss: {:?}, expected_issuers: {:?}",
323
+ iss, expected_issuers
324
+ );
272
325
  return Ok(Either::Right(
273
326
  self.error_response
274
327
  .to_http_response(req.accept().into())
@@ -0,0 +1,179 @@
1
+ use super::FromValue;
2
+ use crate::{
3
+ server::http_message_types::{HttpRequest, HttpResponse},
4
+ services::itsi_http_service::HttpRequestContext,
5
+ };
6
+ use async_trait::async_trait;
7
+ use bytes::{Bytes, BytesMut};
8
+ use either::Either;
9
+ use futures::TryStreamExt;
10
+ use http::{HeaderValue, StatusCode};
11
+ use http_body_util::{combinators::BoxBody, BodyExt, Empty};
12
+ use itsi_error::ItsiError;
13
+ use serde::{Deserialize, Serialize};
14
+ use std::sync::Arc;
15
+ use std::{path::PathBuf, sync::OnceLock};
16
+ use tokio::sync::Mutex;
17
+ use tokio::time::{self, Duration};
18
+
19
+ #[derive(Debug, Serialize, Deserialize)]
20
+ pub struct CspReport {
21
+ #[serde(rename = "csp-report")]
22
+ pub report: ReportDetails,
23
+ }
24
+
25
+ #[derive(Debug, Serialize, Deserialize)]
26
+ pub struct ReportDetails {
27
+ #[serde(rename = "document-uri")]
28
+ pub document_uri: String,
29
+ #[serde(rename = "referrer")]
30
+ pub referrer: Option<String>,
31
+ #[serde(rename = "violated-directive")]
32
+ pub violated_directive: String,
33
+ #[serde(rename = "original-policy")]
34
+ pub original_policy: String,
35
+ #[serde(rename = "blocked-uri")]
36
+ pub blocked_uri: String,
37
+ }
38
+
39
+ #[derive(Debug, Deserialize)]
40
+ pub struct CspConfig {
41
+ pub default_src: Vec<String>,
42
+ pub script_src: Vec<String>,
43
+ pub style_src: Vec<String>,
44
+ pub report_uri: Vec<String>,
45
+ }
46
+
47
+ #[derive(Debug, Deserialize)]
48
+ pub struct Csp {
49
+ pub policy_input: Option<CspConfig>,
50
+ pub reporting_enabled: bool,
51
+ pub report_file: Option<PathBuf>,
52
+ pub report_endpoint: String,
53
+ pub flush_interval: u64,
54
+
55
+ #[serde(skip)]
56
+ pub computed_policy: OnceLock<String>,
57
+ #[serde(skip)]
58
+ pub pending_reports: Arc<Mutex<Vec<CspReport>>>,
59
+ #[serde(skip)]
60
+ pub flush_task: OnceLock<tokio::task::JoinHandle<()>>,
61
+ }
62
+
63
+ #[async_trait]
64
+ impl super::MiddlewareLayer for Csp {
65
+ async fn initialize(&self) -> Result<(), magnus::error::Error> {
66
+ if let Some(policy_config) = &self.policy_input {
67
+ let mut parts = Vec::new();
68
+ if !policy_config.default_src.is_empty() {
69
+ parts.push(format!(
70
+ "default-src {}",
71
+ policy_config.default_src.join(" ")
72
+ ));
73
+ }
74
+ if !policy_config.script_src.is_empty() {
75
+ parts.push(format!("script-src {}", policy_config.script_src.join(" ")));
76
+ }
77
+ if !policy_config.style_src.is_empty() {
78
+ parts.push(format!("style-src {}", policy_config.style_src.join(" ")));
79
+ }
80
+ if !policy_config.report_uri.is_empty() {
81
+ parts.push(format!("report-uri {}", policy_config.report_uri.join(" ")));
82
+ }
83
+ let policy = parts.join("; ");
84
+ self.computed_policy
85
+ .set(policy)
86
+ .map_err(|_| ItsiError::new("Failed to set computed CSP policy"))?;
87
+ }
88
+
89
+ if self.reporting_enabled {
90
+ if let Some(ref report_file) = self.report_file {
91
+ let flush_interval = self.flush_interval;
92
+ let report_path = report_file.clone();
93
+ let pending_reports = Arc::clone(&self.pending_reports);
94
+ let handle = tokio::spawn(async move {
95
+ let mut interval = time::interval(Duration::from_secs(flush_interval));
96
+ loop {
97
+ interval.tick().await;
98
+
99
+ let mut reports = pending_reports.lock().await;
100
+ if !reports.is_empty() {
101
+ let mut lines = String::new();
102
+ for report in reports.iter() {
103
+ if let Ok(line) = serde_json::to_string(report) {
104
+ lines.push_str(&line);
105
+ lines.push('\n');
106
+ }
107
+ }
108
+ reports.clear();
109
+ if let Err(e) = tokio::fs::OpenOptions::new()
110
+ .append(true)
111
+ .create(true)
112
+ .open(&report_path)
113
+ .await
114
+ .map(|mut file| async move {
115
+ use tokio::io::AsyncWriteExt;
116
+ file.write_all(lines.as_bytes()).await
117
+ })
118
+ .map_err(ItsiError::new)
119
+ {
120
+ eprintln!("Error writing CSP reports: {:?}", e);
121
+ }
122
+ }
123
+ }
124
+ });
125
+ self.flush_task
126
+ .set(handle)
127
+ .map_err(|_| ItsiError::new("Failed to set flush task handle"))?;
128
+ }
129
+ }
130
+ Ok(())
131
+ }
132
+
133
+ async fn before(
134
+ &self,
135
+ req: HttpRequest,
136
+ _context: &mut HttpRequestContext,
137
+ ) -> Result<Either<HttpRequest, HttpResponse>, magnus::error::Error> {
138
+ if self.reporting_enabled && req.uri().path() == self.report_endpoint {
139
+ let full_bytes: Result<Bytes, _> = req
140
+ .into_body()
141
+ .into_data_stream()
142
+ .try_fold(BytesMut::new(), |mut acc, chunk| async move {
143
+ acc.extend_from_slice(&chunk);
144
+ Ok(acc)
145
+ })
146
+ .await
147
+ .map(|b| b.freeze());
148
+
149
+ if let Ok(body_bytes) = full_bytes {
150
+ if let Ok(report) = serde_json::from_slice::<CspReport>(&body_bytes) {
151
+ let mut pending = self.pending_reports.lock().await;
152
+ pending.push(report);
153
+ }
154
+ }
155
+
156
+ let mut resp = HttpResponse::new(BoxBody::new(Empty::new()));
157
+ *resp.status_mut() = StatusCode::NO_CONTENT;
158
+ return Ok(Either::Right(resp));
159
+ }
160
+ Ok(Either::Left(req))
161
+ }
162
+
163
+ async fn after(&self, resp: HttpResponse, _context: &mut HttpRequestContext) -> HttpResponse {
164
+ if let Some(policy) = self.computed_policy.get() {
165
+ if !resp.headers().contains_key("Content-Security-Policy") {
166
+ let (mut parts, body) = resp.into_parts();
167
+ if let Ok(header_value) = HeaderValue::from_str(policy) {
168
+ parts
169
+ .headers
170
+ .insert("Content-Security-Policy", header_value);
171
+ }
172
+ return HttpResponse::from_parts(parts, body);
173
+ }
174
+ }
175
+ resp
176
+ }
177
+ }
178
+
179
+ impl FromValue for Csp {}
@@ -5,6 +5,7 @@ mod auth_jwt;
5
5
  mod cache_control;
6
6
  mod compression;
7
7
  mod cors;
8
+ mod csp;
8
9
  mod deny_list;
9
10
  mod error_response;
10
11
  mod etag;
@@ -23,6 +24,9 @@ mod static_response;
23
24
  mod string_rewrite;
24
25
  mod token_source;
25
26
 
27
+ use std::sync::Arc;
28
+ use std::sync::LazyLock;
29
+
26
30
  pub use allow_list::AllowList;
27
31
  use async_trait::async_trait;
28
32
  pub use auth_api_key::AuthAPIKey;
@@ -32,6 +36,7 @@ pub use cache_control::CacheControl;
32
36
  pub use compression::Compression;
33
37
  pub use compression::CompressionAlgorithm;
34
38
  pub use cors::Cors;
39
+ pub use csp::Csp;
35
40
  pub use deny_list::DenyList;
36
41
  use either::Either;
37
42
  pub use error_response::ErrorResponse;
@@ -39,6 +44,7 @@ pub use etag::ETag;
39
44
  pub use intrusion_protection::IntrusionProtection;
40
45
  pub use log_requests::LogRequests;
41
46
  use magnus::error::Result;
47
+ use magnus::rb_sys::AsRawValue;
42
48
  use magnus::Value;
43
49
  pub use max_body::MaxBody;
44
50
  pub use proxy::Proxy;
@@ -57,11 +63,28 @@ use crate::server::http_message_types::HttpResponse;
57
63
  use crate::services::itsi_http_service::HttpRequestContext;
58
64
 
59
65
  pub trait FromValue: Sized + Send + Sync + 'static {
60
- fn from_value(value: Value) -> Result<Self>
66
+ fn from_value(value: Value) -> Result<Arc<Self>>
61
67
  where
62
68
  Self: Deserialize<'static>,
63
69
  {
64
- deserialize(value)
70
+ use std::collections::HashMap;
71
+ use std::sync::Mutex;
72
+
73
+ let raw = value.as_raw();
74
+ static CACHE: LazyLock<Mutex<HashMap<u64, Arc<dyn std::any::Any + Send + Sync>>>> =
75
+ LazyLock::new(|| Mutex::new(HashMap::new()));
76
+
77
+ let mut cache = CACHE.lock().unwrap();
78
+
79
+ if let Some(cached) = cache.get(&raw) {
80
+ if let Some(deserialized) = cached.downcast_ref::<Arc<Self>>() {
81
+ return Ok(deserialized.clone());
82
+ }
83
+ }
84
+
85
+ let deserialized: Arc<Self> = Arc::new(deserialize(value)?);
86
+ cache.insert(raw, deserialized.clone());
87
+ Ok(deserialized)
65
88
  }
66
89
  }
67
90
 
@@ -42,7 +42,7 @@ impl FromStr for RequestType {
42
42
  }
43
43
 
44
44
  impl RubyApp {
45
- pub fn from_value(params: HeapVal) -> magnus::error::Result<Self> {
45
+ pub fn from_value(params: HeapVal) -> magnus::error::Result<Arc<Self>> {
46
46
  let app = params.funcall::<_, _, Proc>(Symbol::new("[]"), ("app_proc",))?;
47
47
  let sendfile = params
48
48
  .funcall::<_, _, bool>(Symbol::new("[]"), ("sendfile",))
@@ -61,13 +61,13 @@ impl RubyApp {
61
61
  .parse()
62
62
  .unwrap_or(RequestType::Http);
63
63
 
64
- Ok(RubyApp {
64
+ Ok(Arc::new(RubyApp {
65
65
  app: Arc::new(app.into()),
66
66
  sendfile,
67
67
  nonblocking,
68
68
  request_type,
69
69
  base_path,
70
- })
70
+ }))
71
71
  }
72
72
  }
73
73
 
@@ -59,6 +59,7 @@ impl MiddlewareLayer for StaticAssets {
59
59
  self.base_path_regex
60
60
  .set(Regex::new(&self.base_path).map_err(ItsiError::new)?)
61
61
  .map_err(ItsiError::new)?;
62
+
62
63
  self.file_server
63
64
  .set(StaticFileServer::new(StaticFileServerConfig {
64
65
  root_dir: self.root_dir.clone(),
@@ -70,7 +71,7 @@ impl MiddlewareLayer for StaticAssets {
70
71
  recheck_interval: Duration::from_secs(self.file_check_interval),
71
72
  serve_hidden_files: self.serve_hidden_files,
72
73
  allowed_extensions: self.allowed_extensions.clone(),
73
- }))
74
+ })?)
74
75
  .map_err(ItsiError::new)?;
75
76
  Ok(())
76
77
  }
@@ -2,12 +2,14 @@ mod middleware;
2
2
  mod middlewares;
3
3
  use http::header::{ACCEPT, CONTENT_TYPE, HOST};
4
4
  use itsi_rb_helpers::HeapVal;
5
- use magnus::{error::Result, value::ReprValue, RArray, RHash, Ruby, TryConvert, Value};
5
+ use magnus::{
6
+ error::Result, rb_sys::AsRawValue, value::ReprValue, RArray, RHash, Ruby, TryConvert, Value,
7
+ };
6
8
  pub use middleware::Middleware;
7
9
  pub use middlewares::*;
8
10
  use regex::{Regex, RegexSet};
9
11
  use std::{collections::HashMap, sync::Arc};
10
- use tracing::debug;
12
+ use tracing::{debug, info};
11
13
 
12
14
  use super::http_message_types::HttpRequest;
13
15
 
@@ -16,6 +18,7 @@ pub struct MiddlewareSet {
16
18
  pub route_set: RegexSet,
17
19
  pub patterns: Vec<Arc<Regex>>,
18
20
  pub stacks: HashMap<usize, MiddlewareStack>,
21
+ unique_middlewares: HashMap<u64, Middleware>,
19
22
  }
20
23
 
21
24
  #[derive(Debug)]
@@ -128,6 +131,7 @@ impl MiddlewareStack {
128
131
 
129
132
  impl MiddlewareSet {
130
133
  pub fn new(routes_raw: Option<HeapVal>) -> Result<Self> {
134
+ let mut unique_middlewares = HashMap::new();
131
135
  if let Some(routes_raw) = routes_raw {
132
136
  let mut stacks = HashMap::new();
133
137
  let mut routes = vec![];
@@ -147,19 +151,30 @@ impl MiddlewareSet {
147
151
  "Route is missing :route key",
148
152
  ))?
149
153
  .funcall::<_, _, String>("source", ())?;
154
+
150
155
  let middleware =
151
- RArray::from_value(route_hash.get("middleware").ok_or(magnus::Error::new(
156
+ RHash::from_value(route_hash.get("middleware").ok_or(magnus::Error::new(
152
157
  magnus::exception::standard_error(),
153
158
  "Route is missing middleware key",
154
159
  ))?)
155
160
  .ok_or(magnus::Error::new(
156
161
  magnus::exception::standard_error(),
157
- format!("middleware must be an array. Got {:?}", routes_raw),
162
+ format!("middleware must be a hash. Got {:?}", routes_raw),
158
163
  ))?;
159
164
 
160
165
  let mut layers = middleware
161
- .into_iter()
162
- .map(MiddlewareSet::parse_middleware)
166
+ .enumeratorize("each", ())
167
+ .map(|pair| {
168
+ let pair = RArray::from_value(pair.unwrap()).unwrap();
169
+ let middleware_type: String = pair.entry(0).unwrap();
170
+ let value: Value = pair.entry(1).unwrap();
171
+ info!("Parsing middleware from value {}", value);
172
+ let middleware = MiddlewareSet::parse_middleware(middleware_type, value);
173
+ if let Ok(middleware) = middleware.as_ref() {
174
+ unique_middlewares.insert(value.as_raw(), middleware.clone());
175
+ };
176
+ middleware
177
+ })
163
178
  .collect::<Result<Vec<_>>>()?;
164
179
  routes.push(route_raw);
165
180
  layers.sort();
@@ -184,6 +199,7 @@ impl MiddlewareSet {
184
199
  format!("Failed to create route set: {}", e),
185
200
  )
186
201
  })?,
202
+ unique_middlewares,
187
203
  patterns: routes
188
204
  .into_iter()
189
205
  .map(|r| Regex::new(&r))
@@ -241,47 +257,27 @@ impl MiddlewareSet {
241
257
  ))
242
258
  }
243
259
 
244
- pub fn parse_middleware(middleware: Value) -> Result<Middleware> {
245
- let middleware_hash = RHash::from_value(middleware).ok_or(magnus::Error::new(
246
- magnus::exception::standard_error(),
247
- format!("Filter must be a hash. Got {:?}", middleware),
248
- ))?;
249
- let middleware_type: String = middleware_hash
250
- .get("type")
251
- .ok_or(magnus::Error::new(
252
- magnus::exception::standard_error(),
253
- format!("Filter must have a :type key. Got {:?}", middleware_hash),
254
- ))?
255
- .to_string();
260
+ pub fn parse_middleware(middleware_type: String, parameters: Value) -> Result<Middleware> {
256
261
  let mw_type = middleware_type.clone();
257
262
 
258
- let parameters: Value = middleware_hash.get("parameters").ok_or(magnus::Error::new(
259
- magnus::exception::standard_error(),
260
- format!(
261
- "Filter must have a :parameters key. Got {:?}",
262
- middleware_hash
263
- ),
264
- ))?;
265
-
266
263
  let result = (move || -> Result<Middleware> {
267
264
  match mw_type.as_str() {
268
265
  "allow_list" => Ok(Middleware::AllowList(AllowList::from_value(parameters)?)),
269
266
  "auth_basic" => Ok(Middleware::AuthBasic(AuthBasic::from_value(parameters)?)),
270
- "auth_jwt" => Ok(Middleware::AuthJwt(Box::new(AuthJwt::from_value(
271
- parameters,
272
- )?))),
267
+ "auth_jwt" => Ok(Middleware::AuthJwt(AuthJwt::from_value(parameters)?)),
273
268
  "auth_api_key" => Ok(Middleware::AuthAPIKey(AuthAPIKey::from_value(parameters)?)),
274
269
  "cache_control" => Ok(Middleware::CacheControl(CacheControl::from_value(
275
270
  parameters,
276
271
  )?)),
277
272
  "deny_list" => Ok(Middleware::DenyList(DenyList::from_value(parameters)?)),
278
273
  "etag" => Ok(Middleware::ETag(ETag::from_value(parameters)?)),
274
+ "csp" => Ok(Middleware::Csp(Csp::from_value(parameters)?)),
279
275
  "intrusion_protection" => Ok({
280
276
  Middleware::IntrusionProtection(IntrusionProtection::from_value(parameters)?)
281
277
  }),
282
278
  "max_body" => Ok(Middleware::MaxBody(MaxBody::from_value(parameters)?)),
283
279
  "rate_limit" => Ok(Middleware::RateLimit(RateLimit::from_value(parameters)?)),
284
- "cors" => Ok(Middleware::Cors(Box::new(Cors::from_value(parameters)?))),
280
+ "cors" => Ok(Middleware::Cors(Cors::from_value(parameters)?)),
285
281
  "request_headers" => Ok(Middleware::RequestHeaders(RequestHeaders::from_value(
286
282
  parameters,
287
283
  )?)),
@@ -323,10 +319,12 @@ impl MiddlewareSet {
323
319
  }
324
320
 
325
321
  pub async fn initialize_layers(&self) -> Result<()> {
326
- for stack in self.stacks.values() {
327
- for middleware in &stack.layers {
328
- middleware.initialize().await?;
329
- }
322
+ info!(
323
+ "Unique middleware keys: {:?}",
324
+ self.unique_middlewares.keys()
325
+ );
326
+ for middleware in self.unique_middlewares.values() {
327
+ middleware.initialize().await?;
330
328
  }
331
329
  Ok(())
332
330
  }
@@ -73,13 +73,18 @@ impl ClusterMode {
73
73
  Ok(())
74
74
  }
75
75
  LifecycleEvent::Restart => {
76
- self.server_config.dup_fds()?;
77
- self.shutdown().await.ok();
78
- info!("Shutdown complete. Calling reload exec");
79
- self.server_config.reload_exec()?;
76
+ if self.server_config.check_config() {
77
+ self.server_config.dup_fds()?;
78
+ self.shutdown().await.ok();
79
+ info!("Shutdown complete. Calling reload exec");
80
+ self.server_config.reload_exec()?;
81
+ }
80
82
  Ok(())
81
83
  }
82
84
  LifecycleEvent::Reload => {
85
+ if !self.server_config.check_config() {
86
+ return Ok(());
87
+ }
83
88
  let should_reexec = self.server_config.clone().reload(true)?;
84
89
  if should_reexec {
85
90
  self.server_config.dup_fds()?;
@@ -286,6 +291,7 @@ impl ClusterMode {
286
291
  self.build_runtime().block_on(async {
287
292
  let self_ref = self_ref.clone();
288
293
  let mut memory_check_interval = time::interval(time::Duration::from_secs(2));
294
+
289
295
  loop {
290
296
  tokio::select! {
291
297
  _ = receiver.changed() => {