itsi-scheduler 0.2.22-aarch64-linux

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 (149) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +8 -0
  3. data/Cargo.lock +997 -0
  4. data/Cargo.toml +7 -0
  5. data/Rakefile +39 -0
  6. data/ext/itsi_acme/Cargo.toml +86 -0
  7. data/ext/itsi_acme/examples/high_level.rs +63 -0
  8. data/ext/itsi_acme/examples/high_level_warp.rs +52 -0
  9. data/ext/itsi_acme/examples/low_level.rs +87 -0
  10. data/ext/itsi_acme/examples/low_level_axum.rs +66 -0
  11. data/ext/itsi_acme/src/acceptor.rs +81 -0
  12. data/ext/itsi_acme/src/acme.rs +354 -0
  13. data/ext/itsi_acme/src/axum.rs +86 -0
  14. data/ext/itsi_acme/src/cache.rs +39 -0
  15. data/ext/itsi_acme/src/caches/boxed.rs +80 -0
  16. data/ext/itsi_acme/src/caches/composite.rs +69 -0
  17. data/ext/itsi_acme/src/caches/dir.rs +106 -0
  18. data/ext/itsi_acme/src/caches/mod.rs +11 -0
  19. data/ext/itsi_acme/src/caches/no.rs +78 -0
  20. data/ext/itsi_acme/src/caches/test.rs +136 -0
  21. data/ext/itsi_acme/src/config.rs +172 -0
  22. data/ext/itsi_acme/src/https_helper.rs +69 -0
  23. data/ext/itsi_acme/src/incoming.rs +142 -0
  24. data/ext/itsi_acme/src/jose.rs +161 -0
  25. data/ext/itsi_acme/src/lib.rs +142 -0
  26. data/ext/itsi_acme/src/resolver.rs +59 -0
  27. data/ext/itsi_acme/src/state.rs +424 -0
  28. data/ext/itsi_error/Cargo.lock +368 -0
  29. data/ext/itsi_error/Cargo.toml +12 -0
  30. data/ext/itsi_error/src/lib.rs +140 -0
  31. data/ext/itsi_instrument_entry/Cargo.toml +15 -0
  32. data/ext/itsi_instrument_entry/src/lib.rs +31 -0
  33. data/ext/itsi_rb_helpers/Cargo.lock +355 -0
  34. data/ext/itsi_rb_helpers/Cargo.toml +11 -0
  35. data/ext/itsi_rb_helpers/src/heap_value.rs +139 -0
  36. data/ext/itsi_rb_helpers/src/lib.rs +232 -0
  37. data/ext/itsi_scheduler/Cargo.toml +24 -0
  38. data/ext/itsi_scheduler/extconf.rb +11 -0
  39. data/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
  40. data/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
  41. data/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
  42. data/ext/itsi_scheduler/src/itsi_scheduler.rs +320 -0
  43. data/ext/itsi_scheduler/src/lib.rs +39 -0
  44. data/ext/itsi_server/Cargo.lock +2956 -0
  45. data/ext/itsi_server/Cargo.toml +94 -0
  46. data/ext/itsi_server/src/default_responses/mod.rs +14 -0
  47. data/ext/itsi_server/src/env.rs +43 -0
  48. data/ext/itsi_server/src/lib.rs +154 -0
  49. data/ext/itsi_server/src/prelude.rs +2 -0
  50. data/ext/itsi_server/src/ruby_types/itsi_body_proxy/big_bytes.rs +116 -0
  51. data/ext/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +149 -0
  52. data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +346 -0
  53. data/ext/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +265 -0
  54. data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +399 -0
  55. data/ext/itsi_server/src/ruby_types/itsi_http_response.rs +447 -0
  56. data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +545 -0
  57. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +650 -0
  58. data/ext/itsi_server/src/ruby_types/itsi_server.rs +102 -0
  59. data/ext/itsi_server/src/ruby_types/mod.rs +48 -0
  60. data/ext/itsi_server/src/server/binds/bind.rs +204 -0
  61. data/ext/itsi_server/src/server/binds/bind_protocol.rs +37 -0
  62. data/ext/itsi_server/src/server/binds/listener.rs +485 -0
  63. data/ext/itsi_server/src/server/binds/mod.rs +4 -0
  64. data/ext/itsi_server/src/server/binds/tls/locked_dir_cache.rs +132 -0
  65. data/ext/itsi_server/src/server/binds/tls.rs +278 -0
  66. data/ext/itsi_server/src/server/byte_frame.rs +32 -0
  67. data/ext/itsi_server/src/server/frame_stream.rs +143 -0
  68. data/ext/itsi_server/src/server/http_message_types.rs +230 -0
  69. data/ext/itsi_server/src/server/io_stream.rs +128 -0
  70. data/ext/itsi_server/src/server/lifecycle_event.rs +12 -0
  71. data/ext/itsi_server/src/server/middleware_stack/middleware.rs +170 -0
  72. data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +63 -0
  73. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +94 -0
  74. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +93 -0
  75. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +343 -0
  76. data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +151 -0
  77. data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +329 -0
  78. data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +300 -0
  79. data/ext/itsi_server/src/server/middleware_stack/middlewares/csp.rs +193 -0
  80. data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +64 -0
  81. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +188 -0
  82. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +168 -0
  83. data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +183 -0
  84. data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +82 -0
  85. data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +209 -0
  86. data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +133 -0
  87. data/ext/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
  88. data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +122 -0
  89. data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +407 -0
  90. data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +155 -0
  91. data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +54 -0
  92. data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +54 -0
  93. data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +51 -0
  94. data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +138 -0
  95. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +269 -0
  96. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +62 -0
  97. data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +218 -0
  98. data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +31 -0
  99. data/ext/itsi_server/src/server/middleware_stack/mod.rs +381 -0
  100. data/ext/itsi_server/src/server/mod.rs +14 -0
  101. data/ext/itsi_server/src/server/process_worker.rs +247 -0
  102. data/ext/itsi_server/src/server/redirect_type.rs +26 -0
  103. data/ext/itsi_server/src/server/request_job.rs +11 -0
  104. data/ext/itsi_server/src/server/serve_strategy/acceptor.rs +100 -0
  105. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +411 -0
  106. data/ext/itsi_server/src/server/serve_strategy/mod.rs +31 -0
  107. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +449 -0
  108. data/ext/itsi_server/src/server/signal.rs +129 -0
  109. data/ext/itsi_server/src/server/size_limited_incoming.rs +107 -0
  110. data/ext/itsi_server/src/server/thread_worker.rs +504 -0
  111. data/ext/itsi_server/src/services/cache_store.rs +74 -0
  112. data/ext/itsi_server/src/services/itsi_http_service.rs +270 -0
  113. data/ext/itsi_server/src/services/mime_types.rs +2896 -0
  114. data/ext/itsi_server/src/services/mod.rs +6 -0
  115. data/ext/itsi_server/src/services/password_hasher.rs +89 -0
  116. data/ext/itsi_server/src/services/rate_limiter.rs +609 -0
  117. data/ext/itsi_server/src/services/static_file_server.rs +1400 -0
  118. data/ext/itsi_tracing/Cargo.lock +274 -0
  119. data/ext/itsi_tracing/Cargo.toml +17 -0
  120. data/ext/itsi_tracing/src/lib.rs +370 -0
  121. data/itsi-scheduler-100.png +0 -0
  122. data/lib/itsi/schedule_refinement.rb +96 -0
  123. data/lib/itsi/scheduler/3.1/itsi_scheduler.so +0 -0
  124. data/lib/itsi/scheduler/3.2/itsi_scheduler.so +0 -0
  125. data/lib/itsi/scheduler/3.3/itsi_scheduler.so +0 -0
  126. data/lib/itsi/scheduler/3.4/itsi_scheduler.so +0 -0
  127. data/lib/itsi/scheduler/4.0/itsi_scheduler.so +0 -0
  128. data/lib/itsi/scheduler/native_extension.rb +34 -0
  129. data/lib/itsi/scheduler/version.rb +7 -0
  130. data/lib/itsi/scheduler.rb +153 -0
  131. data/vendor/rb-sys-build/.cargo-ok +1 -0
  132. data/vendor/rb-sys-build/.cargo_vcs_info.json +6 -0
  133. data/vendor/rb-sys-build/Cargo.lock +294 -0
  134. data/vendor/rb-sys-build/Cargo.toml +71 -0
  135. data/vendor/rb-sys-build/Cargo.toml.orig +32 -0
  136. data/vendor/rb-sys-build/LICENSE-APACHE +190 -0
  137. data/vendor/rb-sys-build/LICENSE-MIT +21 -0
  138. data/vendor/rb-sys-build/src/bindings/sanitizer.rs +185 -0
  139. data/vendor/rb-sys-build/src/bindings/stable_api.rs +247 -0
  140. data/vendor/rb-sys-build/src/bindings/wrapper.h +71 -0
  141. data/vendor/rb-sys-build/src/bindings.rs +280 -0
  142. data/vendor/rb-sys-build/src/cc.rs +421 -0
  143. data/vendor/rb-sys-build/src/lib.rs +12 -0
  144. data/vendor/rb-sys-build/src/rb_config/flags.rs +101 -0
  145. data/vendor/rb-sys-build/src/rb_config/library.rs +132 -0
  146. data/vendor/rb-sys-build/src/rb_config/search_path.rs +57 -0
  147. data/vendor/rb-sys-build/src/rb_config.rs +906 -0
  148. data/vendor/rb-sys-build/src/utils.rs +53 -0
  149. metadata +210 -0
@@ -0,0 +1,329 @@
1
+ use crate::{
2
+ server::http_message_types::{HttpBody, HttpRequest, HttpResponse},
3
+ services::itsi_http_service::HttpRequestContext,
4
+ };
5
+
6
+ use super::{
7
+ header_interpretation::{find_first_supported, header_contains},
8
+ FromValue, MiddlewareLayer,
9
+ };
10
+
11
+ use async_compression::{
12
+ tokio::bufread::{BrotliEncoder, DeflateEncoder, GzipEncoder, ZstdEncoder},
13
+ Level,
14
+ };
15
+ use async_trait::async_trait;
16
+ use bytes::{Bytes, BytesMut};
17
+ use either::Either;
18
+ use futures::TryStreamExt;
19
+ use http::{
20
+ header::{GetAll, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE},
21
+ HeaderValue, Response,
22
+ };
23
+ use http_body_util::{BodyExt, StreamBody};
24
+ use hyper::body::Body;
25
+ use magnus::error::Result;
26
+ use serde::{Deserialize, Serialize};
27
+ use std::convert::Infallible;
28
+ use tokio::io::{AsyncRead, AsyncReadExt, BufReader};
29
+ use tokio_stream::StreamExt;
30
+ use tokio_util::io::{ReaderStream, StreamReader};
31
+ use tracing::debug;
32
+ #[derive(Debug, Clone, Serialize, Deserialize)]
33
+ pub struct Compression {
34
+ min_size: usize,
35
+ algorithms: Vec<CompressionAlgorithm>,
36
+ compress_streams: bool,
37
+ mime_types: Vec<MimeType>,
38
+ level: CompressionLevel,
39
+ }
40
+
41
+ #[derive(Debug, Clone, Serialize, Deserialize)]
42
+ enum CompressionLevel {
43
+ #[serde(rename(deserialize = "fastest"))]
44
+ Fastest,
45
+ #[serde(rename(deserialize = "best"))]
46
+ Best,
47
+ #[serde(rename(deserialize = "balanced"))]
48
+ Balanced,
49
+ #[serde(rename(deserialize = "precise"))]
50
+ Precise(i32),
51
+ }
52
+
53
+ impl CompressionLevel {
54
+ fn to_async_compression_level(&self) -> Level {
55
+ match self {
56
+ CompressionLevel::Fastest => Level::Fastest,
57
+ CompressionLevel::Best => Level::Best,
58
+ CompressionLevel::Balanced => Level::Default,
59
+ CompressionLevel::Precise(level) => Level::Precise(*level),
60
+ }
61
+ }
62
+ }
63
+
64
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord)]
65
+ pub enum CompressionAlgorithm {
66
+ #[serde(rename(deserialize = "gzip"))]
67
+ Gzip,
68
+ #[serde(rename(deserialize = "br"))]
69
+ Brotli,
70
+ #[serde(rename(deserialize = "deflate"))]
71
+ Deflate,
72
+ #[serde(rename(deserialize = "zstd"))]
73
+ Zstd,
74
+ #[serde(rename(deserialize = "identity"))]
75
+ Identity,
76
+ }
77
+
78
+ impl CompressionAlgorithm {
79
+ pub fn as_str(&self) -> &'static str {
80
+ match self {
81
+ CompressionAlgorithm::Gzip => "gzip",
82
+ CompressionAlgorithm::Brotli => "br",
83
+ CompressionAlgorithm::Deflate => "deflate",
84
+ CompressionAlgorithm::Zstd => "zstd",
85
+ CompressionAlgorithm::Identity => "identity",
86
+ }
87
+ }
88
+
89
+ pub fn header_value(&self) -> HeaderValue {
90
+ HeaderValue::from_str(self.as_str()).unwrap()
91
+ }
92
+ }
93
+
94
+ #[derive(Debug, Clone, Serialize, Deserialize)]
95
+ enum MimeType {
96
+ #[serde(rename(deserialize = "text"))]
97
+ Text,
98
+ #[serde(rename(deserialize = "image"))]
99
+ Image,
100
+ #[serde(rename(deserialize = "application"))]
101
+ Application,
102
+ #[serde(rename(deserialize = "audio"))]
103
+ Audio,
104
+ #[serde(rename(deserialize = "video"))]
105
+ Video,
106
+ #[serde(rename(deserialize = "font"))]
107
+ Font,
108
+ #[serde(rename(deserialize = "other"))]
109
+ Other(String),
110
+ #[serde(rename(deserialize = "all"))]
111
+ All,
112
+ }
113
+
114
+ impl MimeType {
115
+ pub fn matches(&self, content_encodings: &GetAll<HeaderValue>) -> bool {
116
+ match self {
117
+ MimeType::Text => header_contains(content_encodings, "text/*"),
118
+ MimeType::Image => header_contains(content_encodings, "image/*"),
119
+ MimeType::Application => header_contains(content_encodings, "application/*"),
120
+ MimeType::Audio => header_contains(content_encodings, "audio/*"),
121
+ MimeType::Video => header_contains(content_encodings, "video/*"),
122
+ MimeType::Font => header_contains(content_encodings, "font/*"),
123
+ MimeType::Other(v) => header_contains(content_encodings, v),
124
+ MimeType::All => header_contains(content_encodings, "*"),
125
+ }
126
+ }
127
+ }
128
+
129
+ fn stream_encode<R>(encoder: R) -> HttpBody
130
+ where
131
+ R: AsyncRead + Unpin + Sync + Send + 'static,
132
+ {
133
+ let encoded_stream = ReaderStream::new(encoder)
134
+ .map(|res| res.map_err(|_| -> Infallible { unreachable!("We handle IO errors above") }));
135
+ HttpBody::stream(StreamBody::new(encoded_stream))
136
+ }
137
+
138
+ fn update_content_encoding(parts: &mut http::response::Parts, new_encoding: HeaderValue) {
139
+ if let Some(existing) = parts.headers.get(CONTENT_ENCODING) {
140
+ let mut encodings = existing.to_str().unwrap_or("").to_owned();
141
+ if !encodings.is_empty() {
142
+ encodings.push_str(", ");
143
+ }
144
+ encodings.push_str(new_encoding.to_str().unwrap());
145
+ parts
146
+ .headers
147
+ .insert(CONTENT_ENCODING, HeaderValue::from_str(&encodings).unwrap());
148
+ } else {
149
+ parts.headers.insert(CONTENT_ENCODING, new_encoding);
150
+ }
151
+ }
152
+
153
+ #[async_trait]
154
+ impl MiddlewareLayer for Compression {
155
+ async fn before(
156
+ &self,
157
+ req: HttpRequest,
158
+ context: &mut HttpRequestContext,
159
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
160
+ context.set_supported_encoding_set(&req);
161
+ Ok(Either::Left(req))
162
+ }
163
+
164
+ /// We'll apply compression on the response, where appropriate.
165
+ /// This is if:
166
+ /// * The response body is larger than the minimum size.
167
+ /// * The response content type is supported.
168
+ /// * The client supports the compression algorithm.
169
+ async fn after(&self, resp: HttpResponse, context: &mut HttpRequestContext) -> HttpResponse {
170
+ let body_size = resp.size_hint().exact();
171
+ let resp = resp;
172
+
173
+ // Don't compress if it's not an explicitly listed compressable type
174
+ if !self
175
+ .mime_types
176
+ .iter()
177
+ .any(|mt| mt.matches(&resp.headers().get_all(CONTENT_TYPE)))
178
+ {
179
+ debug!(
180
+ target: "middleware::compress",
181
+ "Mime type not supported for compression"
182
+ );
183
+ return resp;
184
+ }
185
+
186
+ // Don't compress streams unless compress streams is enabled.
187
+ if body_size.is_none() && !self.compress_streams {
188
+ debug!(
189
+ target: "middleware::compress",
190
+ "Stream compression disabled"
191
+ );
192
+ return resp;
193
+ }
194
+
195
+ // Don't compress too small bodies
196
+ if body_size.is_some_and(|s| s < self.min_size as u64) {
197
+ debug!(
198
+ target: "middleware::compress",
199
+ "Body size too small for compression"
200
+ );
201
+ return resp;
202
+ }
203
+
204
+ // Don't recompress if we're already compressed in a supported format
205
+ for existing_encoding in resp.headers().get_all(CONTENT_ENCODING) {
206
+ if let Ok(encodings) = existing_encoding.to_str() {
207
+ for encoding in encodings.split(',').map(str::trim) {
208
+ let encoding = encoding.split(';').next().unwrap_or(encoding).trim();
209
+ if self.algorithms.iter().any(|algo| algo.as_str() == encoding) {
210
+ debug!(
211
+ target: "middleware::compress",
212
+ "Body already compressed with supported algorithm"
213
+ );
214
+ return resp;
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ let compression_method =
221
+ if let Some(supported_encoding_set) = context.supported_encoding_set() {
222
+ match find_first_supported(
223
+ supported_encoding_set,
224
+ self.algorithms.iter().map(|algo| algo.as_str()),
225
+ ) {
226
+ Some("gzip") => CompressionAlgorithm::Gzip,
227
+ Some("br") => CompressionAlgorithm::Brotli,
228
+ Some("deflate") => CompressionAlgorithm::Deflate,
229
+ Some("zstd") => CompressionAlgorithm::Zstd,
230
+ _ => CompressionAlgorithm::Identity,
231
+ }
232
+ } else {
233
+ CompressionAlgorithm::Identity
234
+ };
235
+
236
+ debug!(
237
+ target: "middleware::compress",
238
+ "Selected compression method: {:?}", compression_method
239
+ );
240
+ if matches!(compression_method, CompressionAlgorithm::Identity) {
241
+ return resp;
242
+ }
243
+
244
+ let (mut parts, body) = resp.into_parts();
245
+
246
+ let new_body = if let Some(_size) = body_size {
247
+ let full_bytes: Bytes = body
248
+ .into_data_stream()
249
+ .try_fold(BytesMut::new(), |mut acc, chunk| async move {
250
+ acc.extend_from_slice(&chunk);
251
+ Ok(acc)
252
+ })
253
+ .await
254
+ .unwrap()
255
+ .freeze();
256
+
257
+ let cursor = std::io::Cursor::new(full_bytes);
258
+ let reader = BufReader::new(cursor);
259
+ let compressed_bytes = match compression_method {
260
+ CompressionAlgorithm::Gzip => {
261
+ let mut encoder =
262
+ GzipEncoder::with_quality(reader, self.level.to_async_compression_level());
263
+ let mut buf = Vec::new();
264
+ encoder.read_to_end(&mut buf).await.unwrap();
265
+ buf
266
+ }
267
+ CompressionAlgorithm::Brotli => {
268
+ let mut encoder = BrotliEncoder::with_quality(
269
+ reader,
270
+ self.level.to_async_compression_level(),
271
+ );
272
+ let mut buf = Vec::new();
273
+ encoder.read_to_end(&mut buf).await.unwrap();
274
+ buf
275
+ }
276
+ CompressionAlgorithm::Deflate => {
277
+ let mut encoder = DeflateEncoder::with_quality(
278
+ reader,
279
+ self.level.to_async_compression_level(),
280
+ );
281
+ let mut buf = Vec::new();
282
+ encoder.read_to_end(&mut buf).await.unwrap();
283
+ buf
284
+ }
285
+ CompressionAlgorithm::Zstd => {
286
+ let mut encoder =
287
+ ZstdEncoder::with_quality(reader, self.level.to_async_compression_level());
288
+ let mut buf = Vec::new();
289
+ encoder.read_to_end(&mut buf).await.unwrap();
290
+ buf
291
+ }
292
+ CompressionAlgorithm::Identity => unreachable!(),
293
+ };
294
+ HttpBody::full(Bytes::from(compressed_bytes))
295
+ } else {
296
+ let stream = body.into_data_stream().map_err(std::io::Error::other);
297
+ let async_read_fut = StreamReader::new(stream);
298
+ let reader = BufReader::new(async_read_fut);
299
+ match compression_method {
300
+ CompressionAlgorithm::Gzip => stream_encode(GzipEncoder::with_quality(
301
+ reader,
302
+ self.level.to_async_compression_level(),
303
+ )),
304
+ CompressionAlgorithm::Brotli => stream_encode(BrotliEncoder::with_quality(
305
+ reader,
306
+ self.level.to_async_compression_level(),
307
+ )),
308
+ CompressionAlgorithm::Deflate => stream_encode(DeflateEncoder::with_quality(
309
+ reader,
310
+ self.level.to_async_compression_level(),
311
+ )),
312
+ CompressionAlgorithm::Zstd => stream_encode(ZstdEncoder::with_quality(
313
+ reader,
314
+ self.level.to_async_compression_level(),
315
+ )),
316
+ CompressionAlgorithm::Identity => unreachable!(),
317
+ }
318
+ };
319
+
320
+ update_content_encoding(&mut parts, compression_method.header_value());
321
+ parts.headers.remove(CONTENT_LENGTH);
322
+ debug!(
323
+ target: "middleware::compress",
324
+ "Response compressed"
325
+ );
326
+ Response::from_parts(parts, new_body)
327
+ }
328
+ }
329
+ impl FromValue for Compression {}
@@ -0,0 +1,300 @@
1
+ use super::{FromValue, MiddlewareLayer};
2
+ use crate::{
3
+ server::http_message_types::{HttpBody, HttpRequest, HttpResponse, RequestExt},
4
+ services::itsi_http_service::HttpRequestContext,
5
+ };
6
+
7
+ use async_trait::async_trait;
8
+ use http::{HeaderMap, Method, Response};
9
+ use itsi_error::ItsiError;
10
+ use magnus::error::Result;
11
+ use serde::Deserialize;
12
+ use tracing::debug;
13
+
14
+ #[derive(Debug, Clone, Deserialize)]
15
+ pub struct Cors {
16
+ pub allow_origins: Vec<String>,
17
+ pub allow_methods: Vec<HttpMethod>,
18
+ pub allow_headers: Vec<String>,
19
+ pub allow_credentials: bool,
20
+ pub expose_headers: Vec<String>,
21
+ pub max_age: Option<u64>,
22
+ }
23
+
24
+ #[derive(Debug, Clone, Deserialize)]
25
+ pub enum HttpMethod {
26
+ #[serde(rename(deserialize = "GET"))]
27
+ Get,
28
+ #[serde(rename(deserialize = "POST"))]
29
+ Post,
30
+ #[serde(rename(deserialize = "PUT"))]
31
+ Put,
32
+ #[serde(rename(deserialize = "DELETE"))]
33
+ Delete,
34
+ #[serde(rename(deserialize = "OPTIONS"))]
35
+ Options,
36
+ #[serde(rename(deserialize = "HEAD"))]
37
+ Head,
38
+ #[serde(rename(deserialize = "PATCH"))]
39
+ Patch,
40
+ }
41
+
42
+ impl HttpMethod {
43
+ pub fn matches(&self, other: &str) -> bool {
44
+ match self {
45
+ HttpMethod::Get => other.eq_ignore_ascii_case("GET"),
46
+ HttpMethod::Post => other.eq_ignore_ascii_case("POST"),
47
+ HttpMethod::Put => other.eq_ignore_ascii_case("PUT"),
48
+ HttpMethod::Delete => other.eq_ignore_ascii_case("DELETE"),
49
+ HttpMethod::Options => other.eq_ignore_ascii_case("OPTIONS"),
50
+ HttpMethod::Head => other.eq_ignore_ascii_case("HEAD"),
51
+ HttpMethod::Patch => other.eq_ignore_ascii_case("PATCH"),
52
+ }
53
+ }
54
+
55
+ pub fn to_str(&self) -> &str {
56
+ match self {
57
+ HttpMethod::Get => "GET",
58
+ HttpMethod::Post => "POST",
59
+ HttpMethod::Put => "PUT",
60
+ HttpMethod::Delete => "DELETE",
61
+ HttpMethod::Options => "OPTIONS",
62
+ HttpMethod::Head => "HEAD",
63
+ HttpMethod::Patch => "PATCH",
64
+ }
65
+ }
66
+ }
67
+
68
+ impl Cors {
69
+ /// Generate the simple CORS headers (used in normal responses)
70
+ fn cors_headers(&self, origin: &str) -> Result<HeaderMap> {
71
+ let mut headers = HeaderMap::new();
72
+
73
+ headers.insert("Vary", "Origin".parse().map_err(ItsiError::new)?);
74
+
75
+ if origin.is_empty() {
76
+ // When credentials are allowed, you cannot return "*".
77
+ debug!(target: "middleware::cors", "Origin empty {}", origin);
78
+ if !self.allow_credentials {
79
+ headers.insert(
80
+ "Access-Control-Allow-Origin",
81
+ "*".parse().map_err(ItsiError::new)?,
82
+ );
83
+ }
84
+ return Ok(headers);
85
+ }
86
+
87
+ // Only return a header if the origin is allowed.
88
+ if self.allow_origins.iter().any(|o| o == origin || o == "*") {
89
+ // If credentials are allowed, we must echo back the exact origin.
90
+ let value = if self.allow_credentials {
91
+ origin
92
+ } else {
93
+ // If not, and if "*" is allowed, you can still use "*".
94
+ if self.allow_origins.iter().any(|o| o == "*") {
95
+ "*"
96
+ } else {
97
+ origin
98
+ }
99
+ };
100
+ headers.insert(
101
+ "Access-Control-Allow-Origin",
102
+ value.parse().map_err(ItsiError::new)?,
103
+ );
104
+ }
105
+
106
+ if !self.allow_methods.is_empty() {
107
+ headers.insert(
108
+ "Access-Control-Allow-Methods",
109
+ self.allow_methods
110
+ .iter()
111
+ .map(HttpMethod::to_str)
112
+ .collect::<Vec<&str>>()
113
+ .join(", ")
114
+ .parse()
115
+ .map_err(ItsiError::new)?,
116
+ );
117
+ }
118
+ if !self.allow_headers.is_empty() {
119
+ headers.insert(
120
+ "Access-Control-Allow-Headers",
121
+ self.allow_headers
122
+ .join(", ")
123
+ .parse()
124
+ .map_err(ItsiError::new)?,
125
+ );
126
+ }
127
+ if self.allow_credentials {
128
+ headers.insert(
129
+ "Access-Control-Allow-Credentials",
130
+ "true".parse().map_err(ItsiError::new)?,
131
+ );
132
+ }
133
+ if let Some(max_age) = self.max_age {
134
+ headers.insert(
135
+ "Access-Control-Max-Age",
136
+ max_age.to_string().parse().map_err(ItsiError::new)?,
137
+ );
138
+ }
139
+ if !self.expose_headers.is_empty() {
140
+ headers.insert(
141
+ "Access-Control-Expose-Headers",
142
+ self.expose_headers
143
+ .join(", ")
144
+ .parse()
145
+ .map_err(ItsiError::new)?,
146
+ );
147
+ }
148
+ Ok(headers)
149
+ }
150
+
151
+ fn preflight_headers(
152
+ &self,
153
+ origin: Option<&str>,
154
+ req_method: Option<&str>,
155
+ req_headers: Option<&str>,
156
+ ) -> Result<HeaderMap> {
157
+ let mut headers = HeaderMap::new();
158
+
159
+ headers.insert("Vary", "Origin".parse().map_err(ItsiError::new)?);
160
+
161
+ let origin = match origin {
162
+ Some(o) if !o.is_empty() => o,
163
+ _ => {
164
+ debug!(target: "middleware::cors", "Missing Origin – preflight fails");
165
+ return Ok(headers);
166
+ }
167
+ };
168
+
169
+ if !self
170
+ .allow_origins
171
+ .iter()
172
+ .any(|allowed| allowed == "*" || allowed == origin)
173
+ {
174
+ debug!(target: "middleware::cors", "Origin not allowed");
175
+ return Ok(headers);
176
+ }
177
+
178
+ let request_method = match req_method {
179
+ Some(m) if !m.is_empty() => m,
180
+ _ => {
181
+ debug!(target: "middleware::cors", "Missing request method – preflight fails");
182
+ return Ok(headers);
183
+ }
184
+ };
185
+
186
+ if !self.allow_methods.iter().any(|m| m.matches(request_method)) {
187
+ debug!(target: "middleware::cors", "Method not allowed");
188
+ return Ok(headers);
189
+ }
190
+
191
+ if let Some(request_headers) = req_headers {
192
+ let req_headers_list: Vec<&str> = request_headers
193
+ .split(',')
194
+ .map(|s| s.trim())
195
+ .filter(|s| !s.is_empty())
196
+ .collect();
197
+ for header in req_headers_list {
198
+ if !self
199
+ .allow_headers
200
+ .iter()
201
+ .any(|allowed| allowed.eq_ignore_ascii_case(header))
202
+ {
203
+ debug!(target: "middleware::cors", "Header not allowed {}", header);
204
+ return Ok(headers);
205
+ }
206
+ }
207
+ }
208
+
209
+ headers.insert("Access-Control-Allow-Origin", origin.parse().unwrap());
210
+ headers.insert(
211
+ "Access-Control-Allow-Methods",
212
+ self.allow_methods
213
+ .iter()
214
+ .map(HttpMethod::to_str)
215
+ .collect::<Vec<&str>>()
216
+ .join(", ")
217
+ .parse()
218
+ .map_err(ItsiError::new)?,
219
+ );
220
+ headers.insert(
221
+ "Access-Control-Allow-Headers",
222
+ self.allow_headers
223
+ .join(", ")
224
+ .parse()
225
+ .map_err(ItsiError::new)?,
226
+ );
227
+ if self.allow_credentials {
228
+ headers.insert(
229
+ "Access-Control-Allow-Credentials",
230
+ "true".parse().map_err(ItsiError::new)?,
231
+ );
232
+ }
233
+ if let Some(max_age) = self.max_age {
234
+ headers.insert(
235
+ "Access-Control-Max-Age",
236
+ max_age.to_string().parse().map_err(ItsiError::new)?,
237
+ );
238
+ }
239
+ if !self.expose_headers.is_empty() {
240
+ headers.insert(
241
+ "Access-Control-Expose-Headers",
242
+ self.expose_headers
243
+ .join(", ")
244
+ .parse()
245
+ .map_err(ItsiError::new)?,
246
+ );
247
+ }
248
+
249
+ Ok(headers)
250
+ }
251
+ }
252
+
253
+ #[async_trait]
254
+ impl MiddlewareLayer for Cors {
255
+ // For OPTIONS (preflight) requests we:
256
+ // 1. Extract Origin, Access-Control-Request-Method, and Access-Control-Request-Headers.
257
+ // 2. Validate them using our hardened preflight_headers function.
258
+ // 3. If validations pass (i.e. headers is non-empty), return a 204 response with those headers.
259
+ // Otherwise, the absence of headers indicates the request doesn’t meet the CORS policy.
260
+ async fn before(
261
+ &self,
262
+ req: HttpRequest,
263
+ context: &mut HttpRequestContext,
264
+ ) -> Result<either::Either<HttpRequest, HttpResponse>> {
265
+ let origin = req.header("Origin");
266
+ debug!(target: "middleware::cors", "Origin: {:?}", origin);
267
+ if req.method() == Method::OPTIONS {
268
+ let ac_request_method = req.header("Access-Control-Request-Method");
269
+ let ac_request_headers = req.header("Access-Control-Request-Headers");
270
+ let headers = self.preflight_headers(origin, ac_request_method, ac_request_headers)?;
271
+ debug!(target: "middleware::cors", "Preflight Headers: {:?}", headers);
272
+ let mut response_builder = Response::builder().status(204);
273
+ *response_builder.headers_mut().unwrap() = headers;
274
+ let response = response_builder
275
+ .body(HttpBody::empty())
276
+ .map_err(ItsiError::new)?;
277
+ return Ok(either::Either::Right(response));
278
+ }
279
+ context.set_origin(origin.map(|s| s.to_string()));
280
+ Ok(either::Either::Left(req))
281
+ }
282
+
283
+ async fn after(
284
+ &self,
285
+ mut resp: HttpResponse,
286
+ context: &mut HttpRequestContext,
287
+ ) -> HttpResponse {
288
+ if let Some(Some(origin)) = context.origin.get() {
289
+ debug!(target: "middleware::cors", "fetching cors headers for origin {}", origin);
290
+ if let Ok(cors_headers) = self.cors_headers(origin) {
291
+ debug!(target: "middleware::cors", "Cors Headers: {:?}", cors_headers);
292
+ for (key, value) in cors_headers.iter() {
293
+ resp.headers_mut().insert(key.clone(), value.clone());
294
+ }
295
+ }
296
+ }
297
+ resp
298
+ }
299
+ }
300
+ impl FromValue for Cors {}