itsi-server 0.1.1 → 0.1.18

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 (184) 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 +3937 -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 +141 -46
  10. data/ext/itsi_error/Cargo.toml +3 -0
  11. data/ext/itsi_error/src/lib.rs +98 -24
  12. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  13. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  14. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  15. data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  16. data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
  17. data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
  18. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
  19. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
  20. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
  21. data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
  22. data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
  23. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
  24. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
  25. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
  26. data/ext/itsi_instrument_entry/Cargo.toml +15 -0
  27. data/ext/itsi_instrument_entry/src/lib.rs +31 -0
  28. data/ext/itsi_rb_helpers/Cargo.toml +3 -0
  29. data/ext/itsi_rb_helpers/src/heap_value.rs +139 -0
  30. data/ext/itsi_rb_helpers/src/lib.rs +140 -10
  31. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  32. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  33. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  34. 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
  35. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
  36. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
  37. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
  38. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
  39. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
  40. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
  41. data/ext/itsi_scheduler/Cargo.toml +24 -0
  42. data/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
  43. data/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
  44. data/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
  45. data/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
  46. data/ext/itsi_scheduler/src/lib.rs +38 -0
  47. data/ext/itsi_server/Cargo.lock +2956 -0
  48. data/ext/itsi_server/Cargo.toml +72 -14
  49. data/ext/itsi_server/extconf.rb +1 -1
  50. data/ext/itsi_server/src/default_responses/html/401.html +68 -0
  51. data/ext/itsi_server/src/default_responses/html/403.html +68 -0
  52. data/ext/itsi_server/src/default_responses/html/404.html +68 -0
  53. data/ext/itsi_server/src/default_responses/html/413.html +71 -0
  54. data/ext/itsi_server/src/default_responses/html/429.html +68 -0
  55. data/ext/itsi_server/src/default_responses/html/500.html +71 -0
  56. data/ext/itsi_server/src/default_responses/html/502.html +71 -0
  57. data/ext/itsi_server/src/default_responses/html/503.html +68 -0
  58. data/ext/itsi_server/src/default_responses/html/504.html +69 -0
  59. data/ext/itsi_server/src/default_responses/html/index.html +238 -0
  60. data/ext/itsi_server/src/default_responses/json/401.json +6 -0
  61. data/ext/itsi_server/src/default_responses/json/403.json +6 -0
  62. data/ext/itsi_server/src/default_responses/json/404.json +6 -0
  63. data/ext/itsi_server/src/default_responses/json/413.json +6 -0
  64. data/ext/itsi_server/src/default_responses/json/429.json +6 -0
  65. data/ext/itsi_server/src/default_responses/json/500.json +6 -0
  66. data/ext/itsi_server/src/default_responses/json/502.json +6 -0
  67. data/ext/itsi_server/src/default_responses/json/503.json +6 -0
  68. data/ext/itsi_server/src/default_responses/json/504.json +6 -0
  69. data/ext/itsi_server/src/default_responses/mod.rs +11 -0
  70. data/ext/itsi_server/src/env.rs +43 -0
  71. data/ext/itsi_server/src/lib.rs +132 -40
  72. data/ext/itsi_server/src/prelude.rs +2 -0
  73. data/ext/itsi_server/src/ruby_types/itsi_body_proxy/big_bytes.rs +109 -0
  74. data/ext/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +143 -0
  75. data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
  76. data/ext/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +264 -0
  77. data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +345 -0
  78. data/ext/itsi_server/src/ruby_types/itsi_http_response.rs +391 -0
  79. data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +225 -0
  80. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +375 -0
  81. data/ext/itsi_server/src/ruby_types/itsi_server.rs +83 -0
  82. data/ext/itsi_server/src/ruby_types/mod.rs +48 -0
  83. data/ext/itsi_server/src/server/binds/bind.rs +201 -0
  84. data/ext/itsi_server/src/server/binds/bind_protocol.rs +37 -0
  85. data/ext/itsi_server/src/server/binds/listener.rs +432 -0
  86. data/ext/itsi_server/src/server/binds/mod.rs +4 -0
  87. data/ext/itsi_server/src/server/binds/tls/locked_dir_cache.rs +132 -0
  88. data/ext/itsi_server/src/server/binds/tls.rs +270 -0
  89. data/ext/itsi_server/src/server/byte_frame.rs +32 -0
  90. data/ext/itsi_server/src/server/http_message_types.rs +97 -0
  91. data/ext/itsi_server/src/server/io_stream.rs +105 -0
  92. data/ext/itsi_server/src/server/lifecycle_event.rs +12 -0
  93. data/ext/itsi_server/src/server/middleware_stack/middleware.rs +165 -0
  94. data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +56 -0
  95. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +87 -0
  96. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +86 -0
  97. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +285 -0
  98. data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +142 -0
  99. data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +289 -0
  100. data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +292 -0
  101. data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +55 -0
  102. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +190 -0
  103. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +157 -0
  104. data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +195 -0
  105. data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +82 -0
  106. data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +201 -0
  107. data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
  108. data/ext/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
  109. data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +87 -0
  110. data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +414 -0
  111. data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +131 -0
  112. data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +76 -0
  113. data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +44 -0
  114. data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +36 -0
  115. data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +126 -0
  116. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +180 -0
  117. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
  118. data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +163 -0
  119. data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +12 -0
  120. data/ext/itsi_server/src/server/middleware_stack/mod.rs +347 -0
  121. data/ext/itsi_server/src/server/mod.rs +12 -5
  122. data/ext/itsi_server/src/server/process_worker.rs +247 -0
  123. data/ext/itsi_server/src/server/request_job.rs +11 -0
  124. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +342 -0
  125. data/ext/itsi_server/src/server/serve_strategy/mod.rs +30 -0
  126. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +421 -0
  127. data/ext/itsi_server/src/server/signal.rs +76 -0
  128. data/ext/itsi_server/src/server/size_limited_incoming.rs +101 -0
  129. data/ext/itsi_server/src/server/thread_worker.rs +475 -0
  130. data/ext/itsi_server/src/services/cache_store.rs +74 -0
  131. data/ext/itsi_server/src/services/itsi_http_service.rs +239 -0
  132. data/ext/itsi_server/src/services/mime_types.rs +1416 -0
  133. data/ext/itsi_server/src/services/mod.rs +6 -0
  134. data/ext/itsi_server/src/services/password_hasher.rs +83 -0
  135. data/ext/itsi_server/src/services/rate_limiter.rs +569 -0
  136. data/ext/itsi_server/src/services/static_file_server.rs +1324 -0
  137. data/ext/itsi_tracing/Cargo.toml +5 -0
  138. data/ext/itsi_tracing/src/lib.rs +315 -7
  139. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
  140. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
  141. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
  142. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
  143. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
  144. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
  145. data/lib/itsi/http_request/response_status_shortcodes.rb +74 -0
  146. data/lib/itsi/http_request.rb +186 -0
  147. data/lib/itsi/http_response.rb +41 -0
  148. data/lib/itsi/passfile.rb +109 -0
  149. data/lib/itsi/server/config/dsl.rb +565 -0
  150. data/lib/itsi/server/config.rb +166 -0
  151. data/lib/itsi/server/default_app/default_app.rb +34 -0
  152. data/lib/itsi/server/default_app/index.html +115 -0
  153. data/lib/itsi/server/default_config/Itsi-rackup.rb +119 -0
  154. data/lib/itsi/server/default_config/Itsi.rb +107 -0
  155. data/lib/itsi/server/grpc/grpc_call.rb +246 -0
  156. data/lib/itsi/server/grpc/grpc_interface.rb +100 -0
  157. data/lib/itsi/server/grpc/reflection/v1/reflection_pb.rb +26 -0
  158. data/lib/itsi/server/grpc/reflection/v1/reflection_services_pb.rb +122 -0
  159. data/lib/itsi/server/rack/handler/itsi.rb +27 -0
  160. data/lib/itsi/server/rack_interface.rb +94 -0
  161. data/lib/itsi/server/route_tester.rb +107 -0
  162. data/lib/itsi/server/scheduler_interface.rb +21 -0
  163. data/lib/itsi/server/scheduler_mode.rb +10 -0
  164. data/lib/itsi/server/signal_trap.rb +29 -0
  165. data/lib/itsi/server/typed_handlers/param_parser.rb +200 -0
  166. data/lib/itsi/server/typed_handlers/source_parser.rb +55 -0
  167. data/lib/itsi/server/typed_handlers.rb +17 -0
  168. data/lib/itsi/server/version.rb +1 -1
  169. data/lib/itsi/server.rb +160 -9
  170. data/lib/itsi/standard_headers.rb +86 -0
  171. data/lib/ruby_lsp/itsi/addon.rb +111 -0
  172. data/lib/shell_completions/completions.rb +26 -0
  173. metadata +182 -25
  174. data/ext/itsi_server/src/request/itsi_request.rs +0 -143
  175. data/ext/itsi_server/src/request/mod.rs +0 -1
  176. data/ext/itsi_server/src/server/bind.rs +0 -138
  177. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -32
  178. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -52
  179. data/ext/itsi_server/src/server/itsi_server.rs +0 -182
  180. data/ext/itsi_server/src/server/listener.rs +0 -218
  181. data/ext/itsi_server/src/server/tls.rs +0 -138
  182. data/ext/itsi_server/src/server/transfer_protocol.rs +0 -23
  183. data/ext/itsi_server/src/stream_writer/mod.rs +0 -21
  184. data/lib/itsi/request.rb +0 -39
@@ -0,0 +1,289 @@
1
+ use crate::{
2
+ server::http_message_types::HttpResponse, services::itsi_http_service::HttpRequestContext,
3
+ };
4
+
5
+ use super::{
6
+ header_interpretation::{find_first_supported, header_contains},
7
+ FromValue, MiddlewareLayer,
8
+ };
9
+
10
+ use async_compression::{
11
+ tokio::bufread::{BrotliEncoder, DeflateEncoder, GzipEncoder, ZstdEncoder},
12
+ Level,
13
+ };
14
+ use async_trait::async_trait;
15
+ use bytes::{Bytes, BytesMut};
16
+ use futures::TryStreamExt;
17
+ use http::{
18
+ header::{GetAll, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE},
19
+ HeaderValue, Response,
20
+ };
21
+ use http_body_util::{combinators::BoxBody, BodyExt, Full, StreamBody};
22
+ use hyper::body::{Body, Frame};
23
+ use serde::{Deserialize, Serialize};
24
+ use std::convert::Infallible;
25
+ use tokio::io::{AsyncRead, AsyncReadExt, BufReader};
26
+ use tokio_stream::StreamExt;
27
+ use tokio_util::io::{ReaderStream, StreamReader};
28
+ #[derive(Debug, Clone, Serialize, Deserialize)]
29
+ pub struct Compression {
30
+ min_size: usize,
31
+ algorithms: Vec<CompressionAlgorithm>,
32
+ compress_streams: bool,
33
+ mime_types: Vec<MimeType>,
34
+ level: CompressionLevel,
35
+ }
36
+
37
+ #[derive(Debug, Clone, Serialize, Deserialize)]
38
+ enum CompressionLevel {
39
+ #[serde(rename(deserialize = "fastest"))]
40
+ Fastest,
41
+ #[serde(rename(deserialize = "best"))]
42
+ Best,
43
+ #[serde(rename(deserialize = "default"))]
44
+ Default,
45
+ #[serde(rename(deserialize = "precise"))]
46
+ Precise(i32),
47
+ }
48
+
49
+ impl CompressionLevel {
50
+ fn to_async_compression_level(&self) -> Level {
51
+ match self {
52
+ CompressionLevel::Fastest => Level::Fastest,
53
+ CompressionLevel::Best => Level::Best,
54
+ CompressionLevel::Default => Level::Default,
55
+ CompressionLevel::Precise(level) => Level::Precise(*level),
56
+ }
57
+ }
58
+ }
59
+
60
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord)]
61
+ pub enum CompressionAlgorithm {
62
+ #[serde(rename(deserialize = "gzip"))]
63
+ Gzip,
64
+ #[serde(rename(deserialize = "brotli"))]
65
+ Brotli,
66
+ #[serde(rename(deserialize = "deflate"))]
67
+ Deflate,
68
+ #[serde(rename(deserialize = "zstd"))]
69
+ Zstd,
70
+ #[serde(rename(deserialize = "none"))]
71
+ None,
72
+ }
73
+
74
+ impl CompressionAlgorithm {
75
+ pub fn as_str(&self) -> &'static str {
76
+ match self {
77
+ CompressionAlgorithm::Gzip => "gzip",
78
+ CompressionAlgorithm::Brotli => "br",
79
+ CompressionAlgorithm::Deflate => "deflate",
80
+ CompressionAlgorithm::Zstd => "zstd",
81
+ CompressionAlgorithm::None => "none",
82
+ }
83
+ }
84
+
85
+ pub fn header_value(&self) -> HeaderValue {
86
+ HeaderValue::from_str(self.as_str()).unwrap()
87
+ }
88
+ }
89
+
90
+ #[derive(Debug, Clone, Serialize, Deserialize)]
91
+ enum MimeType {
92
+ #[serde(rename(deserialize = "text"))]
93
+ Text,
94
+ #[serde(rename(deserialize = "image"))]
95
+ Image,
96
+ #[serde(rename(deserialize = "application"))]
97
+ Application,
98
+ #[serde(rename(deserialize = "audio"))]
99
+ Audio,
100
+ #[serde(rename(deserialize = "video"))]
101
+ Video,
102
+ #[serde(rename(deserialize = "other"))]
103
+ Other(String),
104
+ #[serde(rename(deserialize = "all"))]
105
+ All,
106
+ }
107
+
108
+ impl MimeType {
109
+ pub fn matches(&self, content_encodings: &GetAll<HeaderValue>) -> bool {
110
+ match self {
111
+ MimeType::Text => header_contains(content_encodings, "text/*"),
112
+ MimeType::Image => header_contains(content_encodings, "image/*"),
113
+ MimeType::Application => header_contains(content_encodings, "application/*"),
114
+ MimeType::Audio => header_contains(content_encodings, "audio/*"),
115
+ MimeType::Video => header_contains(content_encodings, "video/*"),
116
+ MimeType::Other(v) => header_contains(content_encodings, v),
117
+ MimeType::All => header_contains(content_encodings, "*"),
118
+ }
119
+ }
120
+ }
121
+
122
+ fn stream_encode<R>(encoder: R) -> BoxBody<Bytes, Infallible>
123
+ where
124
+ R: AsyncRead + Unpin + Sync + Send + 'static,
125
+ {
126
+ let encoded_stream = ReaderStream::new(encoder).map(|res| {
127
+ res.map(Frame::data)
128
+ .map_err(|_| -> Infallible { unreachable!("We handle IO errors above") })
129
+ });
130
+ BoxBody::new(StreamBody::new(encoded_stream))
131
+ }
132
+
133
+ fn update_content_encoding(parts: &mut http::response::Parts, new_encoding: HeaderValue) {
134
+ if let Some(existing) = parts.headers.get(CONTENT_ENCODING) {
135
+ let mut encodings = existing.to_str().unwrap_or("").to_owned();
136
+ if !encodings.is_empty() {
137
+ encodings.push_str(", ");
138
+ }
139
+ encodings.push_str(new_encoding.to_str().unwrap());
140
+ parts
141
+ .headers
142
+ .insert(CONTENT_ENCODING, HeaderValue::from_str(&encodings).unwrap());
143
+ } else {
144
+ parts.headers.insert(CONTENT_ENCODING, new_encoding);
145
+ }
146
+ }
147
+
148
+ #[async_trait]
149
+ impl MiddlewareLayer for Compression {
150
+ /// We'll apply compression on the response, where appropriate.
151
+ /// This is if:
152
+ /// * The response body is larger than the minimum size.
153
+ /// * The response content type is supported.
154
+ /// * The client supports the compression algorithm.
155
+ async fn after(&self, resp: HttpResponse, context: &mut HttpRequestContext) -> HttpResponse {
156
+ let body_size = resp.size_hint().exact();
157
+ let resp = resp;
158
+
159
+ // Don't compress if it's not an explicitly listed compressable type
160
+ if !self
161
+ .mime_types
162
+ .iter()
163
+ .any(|mt| mt.matches(&resp.headers().get_all(CONTENT_TYPE)))
164
+ {
165
+ return resp;
166
+ }
167
+
168
+ // Don't compress streams unless compress streams is enabled.
169
+ if body_size.is_none() && !self.compress_streams {
170
+ return resp;
171
+ }
172
+
173
+ // Don't compress too small bodies
174
+ if body_size.is_some_and(|s| s < self.min_size as u64) {
175
+ return resp;
176
+ }
177
+
178
+ // Don't recompress if we're already compressed in a supported format
179
+ for existing_encoding in resp.headers().get_all(CONTENT_ENCODING) {
180
+ if let Ok(encodings) = existing_encoding.to_str() {
181
+ for encoding in encodings.split(',').map(str::trim) {
182
+ let encoding = encoding.split(';').next().unwrap_or(encoding).trim();
183
+ if self.algorithms.iter().any(|algo| algo.as_str() == encoding) {
184
+ return resp;
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ let compression_method = match find_first_supported(
191
+ &context.supported_encoding_set,
192
+ self.algorithms.iter().map(|algo| algo.as_str()),
193
+ ) {
194
+ Some("gzip") => CompressionAlgorithm::Gzip,
195
+ Some("br") => CompressionAlgorithm::Brotli,
196
+ Some("deflate") => CompressionAlgorithm::Deflate,
197
+ Some("zstd") => CompressionAlgorithm::Zstd,
198
+ _ => CompressionAlgorithm::None,
199
+ };
200
+
201
+ if matches!(compression_method, CompressionAlgorithm::None) {
202
+ return resp;
203
+ }
204
+
205
+ let (mut parts, body) = resp.into_parts();
206
+
207
+ let new_body = if let Some(_size) = body_size {
208
+ let full_bytes: Bytes = body
209
+ .into_data_stream()
210
+ .try_fold(BytesMut::new(), |mut acc, chunk| async move {
211
+ acc.extend_from_slice(&chunk);
212
+ Ok(acc)
213
+ })
214
+ .await
215
+ .unwrap()
216
+ .freeze();
217
+
218
+ let cursor = std::io::Cursor::new(full_bytes);
219
+ let reader = BufReader::new(cursor);
220
+ let compressed_bytes = match compression_method {
221
+ CompressionAlgorithm::Gzip => {
222
+ let mut encoder =
223
+ GzipEncoder::with_quality(reader, self.level.to_async_compression_level());
224
+ let mut buf = Vec::new();
225
+ encoder.read_to_end(&mut buf).await.unwrap();
226
+ buf
227
+ }
228
+ CompressionAlgorithm::Brotli => {
229
+ let mut encoder = BrotliEncoder::with_quality(
230
+ reader,
231
+ self.level.to_async_compression_level(),
232
+ );
233
+ let mut buf = Vec::new();
234
+ encoder.read_to_end(&mut buf).await.unwrap();
235
+ buf
236
+ }
237
+ CompressionAlgorithm::Deflate => {
238
+ let mut encoder = DeflateEncoder::with_quality(
239
+ reader,
240
+ self.level.to_async_compression_level(),
241
+ );
242
+ let mut buf = Vec::new();
243
+ encoder.read_to_end(&mut buf).await.unwrap();
244
+ buf
245
+ }
246
+ CompressionAlgorithm::Zstd => {
247
+ let mut encoder =
248
+ ZstdEncoder::with_quality(reader, self.level.to_async_compression_level());
249
+ let mut buf = Vec::new();
250
+ encoder.read_to_end(&mut buf).await.unwrap();
251
+ buf
252
+ }
253
+ CompressionAlgorithm::None => unreachable!(),
254
+ };
255
+ BoxBody::new(Full::new(Bytes::from(compressed_bytes)))
256
+ } else {
257
+ let stream = body
258
+ .into_data_stream()
259
+ .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e));
260
+ let async_read_fut = StreamReader::new(stream);
261
+ let reader = BufReader::new(async_read_fut);
262
+ match compression_method {
263
+ CompressionAlgorithm::Gzip => stream_encode(GzipEncoder::with_quality(
264
+ reader,
265
+ self.level.to_async_compression_level(),
266
+ )),
267
+ CompressionAlgorithm::Brotli => stream_encode(BrotliEncoder::with_quality(
268
+ reader,
269
+ self.level.to_async_compression_level(),
270
+ )),
271
+ CompressionAlgorithm::Deflate => stream_encode(DeflateEncoder::with_quality(
272
+ reader,
273
+ self.level.to_async_compression_level(),
274
+ )),
275
+ CompressionAlgorithm::Zstd => stream_encode(ZstdEncoder::with_quality(
276
+ reader,
277
+ self.level.to_async_compression_level(),
278
+ )),
279
+ CompressionAlgorithm::None => unreachable!(),
280
+ }
281
+ };
282
+
283
+ update_content_encoding(&mut parts, compression_method.header_value());
284
+ parts.headers.remove(CONTENT_LENGTH);
285
+
286
+ Response::from_parts(parts, new_body)
287
+ }
288
+ }
289
+ impl FromValue for Compression {}
@@ -0,0 +1,292 @@
1
+ use super::{FromValue, MiddlewareLayer};
2
+ use crate::{
3
+ server::http_message_types::{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 http_body_util::{combinators::BoxBody, Empty};
10
+ use itsi_error::ItsiError;
11
+ use magnus::error::Result;
12
+ use serde::Deserialize;
13
+
14
+ #[derive(Debug, Clone, Deserialize)]
15
+ pub struct Cors {
16
+ pub allowed_origins: Vec<String>,
17
+ pub allowed_methods: Vec<HttpMethod>,
18
+ pub allowed_headers: Vec<String>,
19
+ pub exposed_headers: Vec<String>,
20
+ pub allow_credentials: bool,
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
+ if !self.allow_credentials {
78
+ headers.insert(
79
+ "Access-Control-Allow-Origin",
80
+ "*".parse().map_err(ItsiError::new)?,
81
+ );
82
+ }
83
+ return Ok(headers);
84
+ }
85
+
86
+ // Only return a header if the origin is allowed.
87
+ if self.allowed_origins.iter().any(|o| o == origin || o == "*") {
88
+ // If credentials are allowed, we must echo back the exact origin.
89
+ let value = if self.allow_credentials {
90
+ origin
91
+ } else {
92
+ // If not, and if "*" is allowed, you can still use "*".
93
+ if self.allowed_origins.iter().any(|o| o == "*") {
94
+ "*"
95
+ } else {
96
+ origin
97
+ }
98
+ };
99
+ headers.insert(
100
+ "Access-Control-Allow-Origin",
101
+ value.parse().map_err(ItsiError::new)?,
102
+ );
103
+ }
104
+
105
+ if !self.allowed_methods.is_empty() {
106
+ headers.insert(
107
+ "Access-Control-Allow-Methods",
108
+ self.allowed_methods
109
+ .iter()
110
+ .map(HttpMethod::to_str)
111
+ .collect::<Vec<&str>>()
112
+ .join(", ")
113
+ .parse()
114
+ .map_err(ItsiError::new)?,
115
+ );
116
+ }
117
+ if !self.allowed_headers.is_empty() {
118
+ headers.insert(
119
+ "Access-Control-Allow-Headers",
120
+ self.allowed_headers
121
+ .join(", ")
122
+ .parse()
123
+ .map_err(ItsiError::new)?,
124
+ );
125
+ }
126
+ if self.allow_credentials {
127
+ headers.insert(
128
+ "Access-Control-Allow-Credentials",
129
+ "true".parse().map_err(ItsiError::new)?,
130
+ );
131
+ }
132
+ if let Some(max_age) = self.max_age {
133
+ headers.insert(
134
+ "Access-Control-Max-Age",
135
+ max_age.to_string().parse().map_err(ItsiError::new)?,
136
+ );
137
+ }
138
+ if !self.exposed_headers.is_empty() {
139
+ headers.insert(
140
+ "Access-Control-Expose-Headers",
141
+ self.exposed_headers
142
+ .join(", ")
143
+ .parse()
144
+ .map_err(ItsiError::new)?,
145
+ );
146
+ }
147
+ Ok(headers)
148
+ }
149
+
150
+ fn preflight_headers(
151
+ &self,
152
+ origin: Option<&str>,
153
+ req_method: Option<&str>,
154
+ req_headers: Option<&str>,
155
+ ) -> Result<HeaderMap> {
156
+ let mut headers = HeaderMap::new();
157
+
158
+ headers.insert("Vary", "Origin".parse().map_err(ItsiError::new)?);
159
+
160
+ let origin = match origin {
161
+ Some(o) if !o.is_empty() => o,
162
+ _ => return Ok(headers), // Missing Origin – preflight fails
163
+ };
164
+
165
+ if !self
166
+ .allowed_origins
167
+ .iter()
168
+ .any(|allowed| allowed == "*" || allowed == origin)
169
+ {
170
+ return Ok(headers);
171
+ }
172
+
173
+ let request_method = match req_method {
174
+ Some(m) if !m.is_empty() => m,
175
+ _ => return Ok(headers), // Missing request method – preflight fails
176
+ };
177
+
178
+ if !self
179
+ .allowed_methods
180
+ .iter()
181
+ .any(|m| m.matches(request_method))
182
+ {
183
+ return Ok(headers);
184
+ }
185
+
186
+ if let Some(request_headers) = req_headers {
187
+ let req_headers_list: Vec<&str> = request_headers
188
+ .split(',')
189
+ .map(|s| s.trim())
190
+ .filter(|s| !s.is_empty())
191
+ .collect();
192
+ for header in req_headers_list {
193
+ if !self
194
+ .allowed_headers
195
+ .iter()
196
+ .any(|allowed| allowed.eq_ignore_ascii_case(header))
197
+ {
198
+ return Ok(headers);
199
+ }
200
+ }
201
+ }
202
+
203
+ headers.insert("Access-Control-Allow-Origin", origin.parse().unwrap());
204
+ headers.insert(
205
+ "Access-Control-Allow-Methods",
206
+ self.allowed_methods
207
+ .iter()
208
+ .map(HttpMethod::to_str)
209
+ .collect::<Vec<&str>>()
210
+ .join(", ")
211
+ .parse()
212
+ .map_err(ItsiError::new)?,
213
+ );
214
+ headers.insert(
215
+ "Access-Control-Allow-Headers",
216
+ self.allowed_headers
217
+ .join(", ")
218
+ .parse()
219
+ .map_err(ItsiError::new)?,
220
+ );
221
+ if self.allow_credentials {
222
+ headers.insert(
223
+ "Access-Control-Allow-Credentials",
224
+ "true".parse().map_err(ItsiError::new)?,
225
+ );
226
+ }
227
+ if let Some(max_age) = self.max_age {
228
+ headers.insert(
229
+ "Access-Control-Max-Age",
230
+ max_age.to_string().parse().map_err(ItsiError::new)?,
231
+ );
232
+ }
233
+ if !self.exposed_headers.is_empty() {
234
+ headers.insert(
235
+ "Access-Control-Expose-Headers",
236
+ self.exposed_headers
237
+ .join(", ")
238
+ .parse()
239
+ .map_err(ItsiError::new)?,
240
+ );
241
+ }
242
+
243
+ Ok(headers)
244
+ }
245
+ }
246
+
247
+ #[async_trait]
248
+ impl MiddlewareLayer for Cors {
249
+ // For OPTIONS (preflight) requests we:
250
+ // 1. Extract Origin, Access-Control-Request-Method, and Access-Control-Request-Headers.
251
+ // 2. Validate them using our hardened preflight_headers function.
252
+ // 3. If validations pass (i.e. headers is non-empty), return a 204 response with those headers.
253
+ // Otherwise, the absence of headers indicates the request doesn’t meet the CORS policy.
254
+ async fn before(
255
+ &self,
256
+ req: HttpRequest,
257
+ context: &mut HttpRequestContext,
258
+ ) -> Result<either::Either<HttpRequest, HttpResponse>> {
259
+ let origin = req.header("Origin");
260
+ if req.method() == Method::OPTIONS {
261
+ let ac_request_method = req.header("Access-Control-Request-Method");
262
+ let ac_request_headers = req.header("Access-Control-Request-Headers");
263
+ let headers = self.preflight_headers(origin, ac_request_method, ac_request_headers)?;
264
+
265
+ let mut response_builder = Response::builder().status(204);
266
+ *response_builder.headers_mut().unwrap() = headers;
267
+ let response = response_builder
268
+ .body(BoxBody::new(Empty::new()))
269
+ .map_err(ItsiError::new)?;
270
+ return Ok(either::Either::Right(response));
271
+ }
272
+ context.set_origin(origin.map(|s| s.to_string()));
273
+ Ok(either::Either::Left(req))
274
+ }
275
+
276
+ // The after hook can be used to inject CORS headers into non-preflight responses.
277
+ async fn after(
278
+ &self,
279
+ mut resp: HttpResponse,
280
+ context: &mut HttpRequestContext,
281
+ ) -> HttpResponse {
282
+ if let Some(Some(origin)) = context.origin.get() {
283
+ if let Ok(cors_headers) = self.cors_headers(origin) {
284
+ for (key, value) in cors_headers.iter() {
285
+ resp.headers_mut().insert(key.clone(), value.clone());
286
+ }
287
+ }
288
+ }
289
+ resp
290
+ }
291
+ }
292
+ impl FromValue for Cors {}
@@ -0,0 +1,55 @@
1
+ use crate::{
2
+ server::http_message_types::{HttpRequest, HttpResponse, RequestExt},
3
+ services::itsi_http_service::HttpRequestContext,
4
+ };
5
+
6
+ use super::{ErrorResponse, FromValue, MiddlewareLayer};
7
+ use async_trait::async_trait;
8
+ use either::Either;
9
+ use itsi_error::ItsiError;
10
+ use magnus::error::Result;
11
+ use regex::RegexSet;
12
+ use serde::Deserialize;
13
+ use std::sync::OnceLock;
14
+
15
+ #[derive(Debug, Clone, Deserialize)]
16
+ pub struct DenyList {
17
+ #[serde(skip_deserializing)]
18
+ pub denied_ips: OnceLock<RegexSet>,
19
+ pub denied_patterns: Vec<String>,
20
+ #[serde(default = "forbidden_error_response")]
21
+ pub error_response: ErrorResponse,
22
+ }
23
+
24
+ fn forbidden_error_response() -> ErrorResponse {
25
+ ErrorResponse::forbidden()
26
+ }
27
+
28
+ #[async_trait]
29
+ impl MiddlewareLayer for DenyList {
30
+ async fn initialize(&self) -> Result<()> {
31
+ let denied_ips = RegexSet::new(&self.denied_patterns).map_err(ItsiError::new)?;
32
+ self.denied_ips
33
+ .set(denied_ips)
34
+ .map_err(|e| ItsiError::new(format!("Failed to set allowed IPs: {:?}", e)))?;
35
+ Ok(())
36
+ }
37
+
38
+ async fn before(
39
+ &self,
40
+ req: HttpRequest,
41
+ context: &mut HttpRequestContext,
42
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
43
+ if let Some(denied_ips) = self.denied_ips.get() {
44
+ if denied_ips.is_match(&context.addr) {
45
+ return Ok(Either::Right(
46
+ self.error_response
47
+ .to_http_response(req.accept().into())
48
+ .await,
49
+ ));
50
+ }
51
+ }
52
+ Ok(Either::Left(req))
53
+ }
54
+ }
55
+ impl FromValue for DenyList {}