itsi-server 0.1.1 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of itsi-server might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +7 -0
- data/Cargo.lock +4417 -0
- data/Cargo.toml +7 -0
- data/README.md +4 -0
- data/Rakefile +8 -1
- data/_index.md +6 -0
- data/exe/itsi +94 -45
- data/ext/itsi_error/Cargo.toml +2 -0
- data/ext/itsi_error/src/from.rs +68 -0
- data/ext/itsi_error/src/lib.rs +18 -34
- data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
- data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
- data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
- data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
- data/ext/itsi_instrument_entry/Cargo.toml +15 -0
- data/ext/itsi_instrument_entry/src/lib.rs +31 -0
- data/ext/itsi_rb_helpers/Cargo.toml +3 -0
- data/ext/itsi_rb_helpers/src/heap_value.rs +139 -0
- data/ext/itsi_rb_helpers/src/lib.rs +140 -10
- data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
- data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
- data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
- 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
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
- data/ext/itsi_scheduler/Cargo.toml +24 -0
- data/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
- data/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
- data/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
- data/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
- data/ext/itsi_scheduler/src/lib.rs +38 -0
- data/ext/itsi_server/Cargo.lock +2956 -0
- data/ext/itsi_server/Cargo.toml +73 -13
- data/ext/itsi_server/extconf.rb +1 -1
- data/ext/itsi_server/src/env.rs +43 -0
- data/ext/itsi_server/src/lib.rs +100 -40
- data/ext/itsi_server/src/ruby_types/itsi_body_proxy/big_bytes.rs +109 -0
- data/ext/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +141 -0
- data/ext/itsi_server/src/ruby_types/itsi_grpc_request.rs +147 -0
- data/ext/itsi_server/src/ruby_types/itsi_grpc_response.rs +19 -0
- data/ext/itsi_server/src/ruby_types/itsi_grpc_stream/mod.rs +216 -0
- data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +282 -0
- data/ext/itsi_server/src/ruby_types/itsi_http_response.rs +388 -0
- data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +225 -0
- data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +355 -0
- data/ext/itsi_server/src/ruby_types/itsi_server.rs +82 -0
- data/ext/itsi_server/src/ruby_types/mod.rs +55 -0
- data/ext/itsi_server/src/server/bind.rs +75 -31
- data/ext/itsi_server/src/server/bind_protocol.rs +37 -0
- data/ext/itsi_server/src/server/byte_frame.rs +32 -0
- data/ext/itsi_server/src/server/cache_store.rs +74 -0
- data/ext/itsi_server/src/server/io_stream.rs +104 -0
- data/ext/itsi_server/src/server/itsi_service.rs +172 -0
- data/ext/itsi_server/src/server/lifecycle_event.rs +12 -0
- data/ext/itsi_server/src/server/listener.rs +332 -132
- data/ext/itsi_server/src/server/middleware_stack/middleware.rs +153 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +47 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +58 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +82 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +321 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +139 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +300 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +287 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +48 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +127 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +191 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/grpc_service.rs +72 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +85 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +195 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +82 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +216 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +124 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +76 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +43 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +34 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +93 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +162 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +158 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +12 -0
- data/ext/itsi_server/src/server/middleware_stack/mod.rs +315 -0
- data/ext/itsi_server/src/server/mod.rs +15 -2
- data/ext/itsi_server/src/server/process_worker.rs +229 -0
- data/ext/itsi_server/src/server/rate_limiter.rs +565 -0
- data/ext/itsi_server/src/server/request_job.rs +11 -0
- data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +337 -0
- data/ext/itsi_server/src/server/serve_strategy/mod.rs +30 -0
- data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +421 -0
- data/ext/itsi_server/src/server/signal.rs +93 -0
- data/ext/itsi_server/src/server/static_file_server.rs +984 -0
- data/ext/itsi_server/src/server/thread_worker.rs +444 -0
- data/ext/itsi_server/src/server/tls/locked_dir_cache.rs +132 -0
- data/ext/itsi_server/src/server/tls.rs +187 -60
- data/ext/itsi_server/src/server/types.rs +43 -0
- data/ext/itsi_tracing/Cargo.toml +5 -0
- data/ext/itsi_tracing/src/lib.rs +225 -7
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
- data/lib/itsi/http_request.rb +87 -0
- data/lib/itsi/http_response.rb +39 -0
- data/lib/itsi/server/Itsi.rb +119 -0
- data/lib/itsi/server/config/dsl.rb +506 -0
- data/lib/itsi/server/config.rb +131 -0
- data/lib/itsi/server/default_app/default_app.rb +38 -0
- data/lib/itsi/server/default_app/index.html +91 -0
- data/lib/itsi/server/grpc_interface.rb +213 -0
- data/lib/itsi/server/rack/handler/itsi.rb +27 -0
- data/lib/itsi/server/rack_interface.rb +94 -0
- data/lib/itsi/server/scheduler_interface.rb +21 -0
- data/lib/itsi/server/scheduler_mode.rb +10 -0
- data/lib/itsi/server/signal_trap.rb +29 -0
- data/lib/itsi/server/version.rb +1 -1
- data/lib/itsi/server.rb +90 -9
- data/lib/itsi/standard_headers.rb +86 -0
- metadata +122 -31
- data/ext/itsi_server/src/request/itsi_request.rs +0 -143
- data/ext/itsi_server/src/request/mod.rs +0 -1
- data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -32
- data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -52
- data/ext/itsi_server/src/server/itsi_server.rs +0 -182
- data/ext/itsi_server/src/server/transfer_protocol.rs +0 -23
- data/ext/itsi_server/src/stream_writer/mod.rs +0 -21
- data/lib/itsi/request.rb +0 -39
@@ -0,0 +1,287 @@
|
|
1
|
+
use super::{FromValue, MiddlewareLayer};
|
2
|
+
use crate::server::{
|
3
|
+
itsi_service::RequestContext,
|
4
|
+
types::{HttpRequest, HttpResponse, RequestExt},
|
5
|
+
};
|
6
|
+
use async_trait::async_trait;
|
7
|
+
use http::{HeaderMap, Method, Response};
|
8
|
+
use http_body_util::{combinators::BoxBody, Empty};
|
9
|
+
use itsi_error::ItsiError;
|
10
|
+
use magnus::error::Result;
|
11
|
+
use serde::Deserialize;
|
12
|
+
|
13
|
+
#[derive(Debug, Clone, Deserialize)]
|
14
|
+
pub struct Cors {
|
15
|
+
pub allowed_origins: Vec<String>,
|
16
|
+
pub allowed_methods: Vec<HttpMethod>,
|
17
|
+
pub allowed_headers: Vec<String>,
|
18
|
+
pub exposed_headers: Vec<String>,
|
19
|
+
pub allow_credentials: bool,
|
20
|
+
pub max_age: Option<u64>,
|
21
|
+
}
|
22
|
+
|
23
|
+
#[derive(Debug, Clone, Deserialize)]
|
24
|
+
pub enum HttpMethod {
|
25
|
+
#[serde(rename(deserialize = "GET"))]
|
26
|
+
Get,
|
27
|
+
#[serde(rename(deserialize = "POST"))]
|
28
|
+
Post,
|
29
|
+
#[serde(rename(deserialize = "PUT"))]
|
30
|
+
Put,
|
31
|
+
#[serde(rename(deserialize = "DELETE"))]
|
32
|
+
Delete,
|
33
|
+
#[serde(rename(deserialize = "OPTIONS"))]
|
34
|
+
Options,
|
35
|
+
#[serde(rename(deserialize = "HEAD"))]
|
36
|
+
Head,
|
37
|
+
#[serde(rename(deserialize = "PATCH"))]
|
38
|
+
Patch,
|
39
|
+
}
|
40
|
+
|
41
|
+
impl HttpMethod {
|
42
|
+
pub fn matches(&self, other: &str) -> bool {
|
43
|
+
match self {
|
44
|
+
HttpMethod::Get => other.eq_ignore_ascii_case("GET"),
|
45
|
+
HttpMethod::Post => other.eq_ignore_ascii_case("POST"),
|
46
|
+
HttpMethod::Put => other.eq_ignore_ascii_case("PUT"),
|
47
|
+
HttpMethod::Delete => other.eq_ignore_ascii_case("DELETE"),
|
48
|
+
HttpMethod::Options => other.eq_ignore_ascii_case("OPTIONS"),
|
49
|
+
HttpMethod::Head => other.eq_ignore_ascii_case("HEAD"),
|
50
|
+
HttpMethod::Patch => other.eq_ignore_ascii_case("PATCH"),
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
pub fn to_str(&self) -> &str {
|
55
|
+
match self {
|
56
|
+
HttpMethod::Get => "GET",
|
57
|
+
HttpMethod::Post => "POST",
|
58
|
+
HttpMethod::Put => "PUT",
|
59
|
+
HttpMethod::Delete => "DELETE",
|
60
|
+
HttpMethod::Options => "OPTIONS",
|
61
|
+
HttpMethod::Head => "HEAD",
|
62
|
+
HttpMethod::Patch => "PATCH",
|
63
|
+
}
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
impl Cors {
|
68
|
+
/// Generate the simple CORS headers (used in normal responses)
|
69
|
+
fn cors_headers(&self, origin: &str) -> Result<HeaderMap> {
|
70
|
+
let mut headers = HeaderMap::new();
|
71
|
+
|
72
|
+
headers.insert("Vary", "Origin".parse().map_err(ItsiError::default)?);
|
73
|
+
|
74
|
+
if origin.is_empty() {
|
75
|
+
// When credentials are allowed, you cannot return "*".
|
76
|
+
if !self.allow_credentials {
|
77
|
+
headers.insert(
|
78
|
+
"Access-Control-Allow-Origin",
|
79
|
+
"*".parse().map_err(ItsiError::default)?,
|
80
|
+
);
|
81
|
+
}
|
82
|
+
return Ok(headers);
|
83
|
+
}
|
84
|
+
|
85
|
+
// Only return a header if the origin is allowed.
|
86
|
+
if self.allowed_origins.iter().any(|o| o == origin || o == "*") {
|
87
|
+
// If credentials are allowed, we must echo back the exact origin.
|
88
|
+
let value = if self.allow_credentials {
|
89
|
+
origin
|
90
|
+
} else {
|
91
|
+
// If not, and if "*" is allowed, you can still use "*".
|
92
|
+
if self.allowed_origins.iter().any(|o| o == "*") {
|
93
|
+
"*"
|
94
|
+
} else {
|
95
|
+
origin
|
96
|
+
}
|
97
|
+
};
|
98
|
+
headers.insert(
|
99
|
+
"Access-Control-Allow-Origin",
|
100
|
+
value.parse().map_err(ItsiError::default)?,
|
101
|
+
);
|
102
|
+
}
|
103
|
+
|
104
|
+
if !self.allowed_methods.is_empty() {
|
105
|
+
headers.insert(
|
106
|
+
"Access-Control-Allow-Methods",
|
107
|
+
self.allowed_methods
|
108
|
+
.iter()
|
109
|
+
.map(HttpMethod::to_str)
|
110
|
+
.collect::<Vec<&str>>()
|
111
|
+
.join(", ")
|
112
|
+
.parse()
|
113
|
+
.map_err(ItsiError::default)?,
|
114
|
+
);
|
115
|
+
}
|
116
|
+
if !self.allowed_headers.is_empty() {
|
117
|
+
headers.insert(
|
118
|
+
"Access-Control-Allow-Headers",
|
119
|
+
self.allowed_headers
|
120
|
+
.join(", ")
|
121
|
+
.parse()
|
122
|
+
.map_err(ItsiError::default)?,
|
123
|
+
);
|
124
|
+
}
|
125
|
+
if self.allow_credentials {
|
126
|
+
headers.insert(
|
127
|
+
"Access-Control-Allow-Credentials",
|
128
|
+
"true".parse().map_err(ItsiError::default)?,
|
129
|
+
);
|
130
|
+
}
|
131
|
+
if let Some(max_age) = self.max_age {
|
132
|
+
headers.insert(
|
133
|
+
"Access-Control-Max-Age",
|
134
|
+
max_age.to_string().parse().map_err(ItsiError::default)?,
|
135
|
+
);
|
136
|
+
}
|
137
|
+
if !self.exposed_headers.is_empty() {
|
138
|
+
headers.insert(
|
139
|
+
"Access-Control-Expose-Headers",
|
140
|
+
self.exposed_headers
|
141
|
+
.join(", ")
|
142
|
+
.parse()
|
143
|
+
.map_err(ItsiError::default)?,
|
144
|
+
);
|
145
|
+
}
|
146
|
+
Ok(headers)
|
147
|
+
}
|
148
|
+
|
149
|
+
fn preflight_headers(
|
150
|
+
&self,
|
151
|
+
origin: Option<&str>,
|
152
|
+
req_method: Option<&str>,
|
153
|
+
req_headers: Option<&str>,
|
154
|
+
) -> Result<HeaderMap> {
|
155
|
+
let mut headers = HeaderMap::new();
|
156
|
+
|
157
|
+
headers.insert("Vary", "Origin".parse().map_err(ItsiError::default)?);
|
158
|
+
|
159
|
+
let origin = match origin {
|
160
|
+
Some(o) if !o.is_empty() => o,
|
161
|
+
_ => return Ok(headers), // Missing Origin – preflight fails
|
162
|
+
};
|
163
|
+
|
164
|
+
if !self
|
165
|
+
.allowed_origins
|
166
|
+
.iter()
|
167
|
+
.any(|allowed| allowed == "*" || allowed == origin)
|
168
|
+
{
|
169
|
+
return Ok(headers);
|
170
|
+
}
|
171
|
+
|
172
|
+
let request_method = match req_method {
|
173
|
+
Some(m) if !m.is_empty() => m,
|
174
|
+
_ => return Ok(headers), // Missing request method – preflight fails
|
175
|
+
};
|
176
|
+
|
177
|
+
if !self
|
178
|
+
.allowed_methods
|
179
|
+
.iter()
|
180
|
+
.any(|m| m.matches(request_method))
|
181
|
+
{
|
182
|
+
return Ok(headers);
|
183
|
+
}
|
184
|
+
|
185
|
+
if let Some(request_headers) = req_headers {
|
186
|
+
let req_headers_list: Vec<&str> = request_headers
|
187
|
+
.split(',')
|
188
|
+
.map(|s| s.trim())
|
189
|
+
.filter(|s| !s.is_empty())
|
190
|
+
.collect();
|
191
|
+
for header in req_headers_list {
|
192
|
+
if !self
|
193
|
+
.allowed_headers
|
194
|
+
.iter()
|
195
|
+
.any(|allowed| allowed.eq_ignore_ascii_case(header))
|
196
|
+
{
|
197
|
+
return Ok(headers);
|
198
|
+
}
|
199
|
+
}
|
200
|
+
}
|
201
|
+
|
202
|
+
headers.insert("Access-Control-Allow-Origin", origin.parse().unwrap());
|
203
|
+
headers.insert(
|
204
|
+
"Access-Control-Allow-Methods",
|
205
|
+
self.allowed_methods
|
206
|
+
.iter()
|
207
|
+
.map(HttpMethod::to_str)
|
208
|
+
.collect::<Vec<&str>>()
|
209
|
+
.join(", ")
|
210
|
+
.parse()
|
211
|
+
.map_err(ItsiError::default)?,
|
212
|
+
);
|
213
|
+
headers.insert(
|
214
|
+
"Access-Control-Allow-Headers",
|
215
|
+
self.allowed_headers
|
216
|
+
.join(", ")
|
217
|
+
.parse()
|
218
|
+
.map_err(ItsiError::default)?,
|
219
|
+
);
|
220
|
+
if self.allow_credentials {
|
221
|
+
headers.insert(
|
222
|
+
"Access-Control-Allow-Credentials",
|
223
|
+
"true".parse().map_err(ItsiError::default)?,
|
224
|
+
);
|
225
|
+
}
|
226
|
+
if let Some(max_age) = self.max_age {
|
227
|
+
headers.insert(
|
228
|
+
"Access-Control-Max-Age",
|
229
|
+
max_age.to_string().parse().map_err(ItsiError::default)?,
|
230
|
+
);
|
231
|
+
}
|
232
|
+
if !self.exposed_headers.is_empty() {
|
233
|
+
headers.insert(
|
234
|
+
"Access-Control-Expose-Headers",
|
235
|
+
self.exposed_headers
|
236
|
+
.join(", ")
|
237
|
+
.parse()
|
238
|
+
.map_err(ItsiError::default)?,
|
239
|
+
);
|
240
|
+
}
|
241
|
+
|
242
|
+
Ok(headers)
|
243
|
+
}
|
244
|
+
}
|
245
|
+
|
246
|
+
#[async_trait]
|
247
|
+
impl MiddlewareLayer for Cors {
|
248
|
+
// For OPTIONS (preflight) requests we:
|
249
|
+
// 1. Extract Origin, Access-Control-Request-Method, and Access-Control-Request-Headers.
|
250
|
+
// 2. Validate them using our hardened preflight_headers function.
|
251
|
+
// 3. If validations pass (i.e. headers is non-empty), return a 204 response with those headers.
|
252
|
+
// Otherwise, the absence of headers indicates the request doesn’t meet the CORS policy.
|
253
|
+
async fn before(
|
254
|
+
&self,
|
255
|
+
req: HttpRequest,
|
256
|
+
context: &mut RequestContext,
|
257
|
+
) -> Result<either::Either<HttpRequest, HttpResponse>> {
|
258
|
+
let origin = req.header("Origin");
|
259
|
+
if req.method() == Method::OPTIONS {
|
260
|
+
let ac_request_method = req.header("Access-Control-Request-Method");
|
261
|
+
let ac_request_headers = req.header("Access-Control-Request-Headers");
|
262
|
+
let headers = self.preflight_headers(origin, ac_request_method, ac_request_headers)?;
|
263
|
+
|
264
|
+
let mut response_builder = Response::builder().status(204);
|
265
|
+
*response_builder.headers_mut().unwrap() = headers;
|
266
|
+
let response = response_builder
|
267
|
+
.body(BoxBody::new(Empty::new()))
|
268
|
+
.map_err(ItsiError::default)?;
|
269
|
+
return Ok(either::Either::Right(response));
|
270
|
+
}
|
271
|
+
context.set_origin(origin.map(|s| s.to_string()));
|
272
|
+
Ok(either::Either::Left(req))
|
273
|
+
}
|
274
|
+
|
275
|
+
// The after hook can be used to inject CORS headers into non-preflight responses.
|
276
|
+
async fn after(&self, mut resp: HttpResponse, context: &mut RequestContext) -> HttpResponse {
|
277
|
+
if let Some(Some(origin)) = context.origin.get() {
|
278
|
+
if let Ok(cors_headers) = self.cors_headers(origin) {
|
279
|
+
for (key, value) in cors_headers.iter() {
|
280
|
+
resp.headers_mut().insert(key.clone(), value.clone());
|
281
|
+
}
|
282
|
+
}
|
283
|
+
}
|
284
|
+
resp
|
285
|
+
}
|
286
|
+
}
|
287
|
+
impl FromValue for Cors {}
|
@@ -0,0 +1,48 @@
|
|
1
|
+
use crate::server::{
|
2
|
+
itsi_service::RequestContext,
|
3
|
+
types::{HttpRequest, HttpResponse},
|
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
|
+
pub error_response: ErrorResponse,
|
21
|
+
}
|
22
|
+
|
23
|
+
#[async_trait]
|
24
|
+
impl MiddlewareLayer for DenyList {
|
25
|
+
async fn initialize(&self) -> Result<()> {
|
26
|
+
let denied_ips = RegexSet::new(&self.denied_patterns).map_err(ItsiError::default)?;
|
27
|
+
self.denied_ips
|
28
|
+
.set(denied_ips)
|
29
|
+
.map_err(|e| ItsiError::default(format!("Failed to set allowed IPs: {:?}", e)))?;
|
30
|
+
Ok(())
|
31
|
+
}
|
32
|
+
|
33
|
+
async fn before(
|
34
|
+
&self,
|
35
|
+
req: HttpRequest,
|
36
|
+
context: &mut RequestContext,
|
37
|
+
) -> Result<Either<HttpRequest, HttpResponse>> {
|
38
|
+
if let Some(denied_ips) = self.denied_ips.get() {
|
39
|
+
if denied_ips.is_match(&context.addr) {
|
40
|
+
return Ok(Either::Right(
|
41
|
+
self.error_response.to_http_response(&req).await,
|
42
|
+
));
|
43
|
+
}
|
44
|
+
}
|
45
|
+
Ok(Either::Left(req))
|
46
|
+
}
|
47
|
+
}
|
48
|
+
impl FromValue for DenyList {}
|
@@ -0,0 +1,127 @@
|
|
1
|
+
use crate::server::static_file_server::ROOT_STATIC_FILE_SERVER;
|
2
|
+
use crate::server::types::RequestExt;
|
3
|
+
use crate::server::{
|
4
|
+
itsi_service::RequestContext,
|
5
|
+
types::{HttpRequest, HttpResponse},
|
6
|
+
};
|
7
|
+
|
8
|
+
use bytes::Bytes;
|
9
|
+
use either::Either;
|
10
|
+
use http::Response;
|
11
|
+
use http_body_util::{combinators::BoxBody, Full};
|
12
|
+
use itsi_error::ItsiError;
|
13
|
+
use serde::Deserialize;
|
14
|
+
use std::path::{Path, PathBuf};
|
15
|
+
|
16
|
+
#[derive(Debug, Clone, Deserialize)]
|
17
|
+
/// Filters can each have a customizable error response.
|
18
|
+
/// They can:
|
19
|
+
/// * Return Plain-text
|
20
|
+
/// * Return HTML
|
21
|
+
/// * Return JSON
|
22
|
+
pub struct ErrorResponse {
|
23
|
+
code: u16,
|
24
|
+
plaintext: Option<String>,
|
25
|
+
html: Option<PathBuf>,
|
26
|
+
json: Option<serde_json::Value>,
|
27
|
+
default: ErrorFormat,
|
28
|
+
}
|
29
|
+
|
30
|
+
#[derive(Debug, Clone, Deserialize, Default)]
|
31
|
+
enum ErrorFormat {
|
32
|
+
#[default]
|
33
|
+
#[serde(rename(deserialize = "plaintext"))]
|
34
|
+
Plaintext,
|
35
|
+
#[serde(rename(deserialize = "html"))]
|
36
|
+
Html,
|
37
|
+
#[serde(rename(deserialize = "json"))]
|
38
|
+
Json,
|
39
|
+
}
|
40
|
+
|
41
|
+
impl Default for ErrorResponse {
|
42
|
+
fn default() -> Self {
|
43
|
+
ErrorResponse {
|
44
|
+
code: 500,
|
45
|
+
plaintext: Some("Error".to_owned()),
|
46
|
+
html: None,
|
47
|
+
json: None,
|
48
|
+
default: ErrorFormat::Plaintext,
|
49
|
+
}
|
50
|
+
}
|
51
|
+
}
|
52
|
+
|
53
|
+
impl ErrorResponse {
|
54
|
+
pub(crate) async fn to_http_response(&self, request: &HttpRequest) -> HttpResponse {
|
55
|
+
let accept = request.accept();
|
56
|
+
let body = match accept {
|
57
|
+
Some(accept) if accept.contains("text/plain") => BoxBody::new(Full::new(Bytes::from(
|
58
|
+
self.plaintext.clone().unwrap_or_else(|| "Error".to_owned()),
|
59
|
+
))),
|
60
|
+
Some(accept) if accept.contains("text/html") => {
|
61
|
+
if let Some(path) = &self.html {
|
62
|
+
let path = path.to_str().unwrap();
|
63
|
+
let response = ROOT_STATIC_FILE_SERVER.serve_single(path).await;
|
64
|
+
|
65
|
+
if response.status().is_success() {
|
66
|
+
response.into_body()
|
67
|
+
} else {
|
68
|
+
BoxBody::new(Full::new(Bytes::from("Error")))
|
69
|
+
}
|
70
|
+
} else {
|
71
|
+
BoxBody::new(Full::new(Bytes::from("Error")))
|
72
|
+
}
|
73
|
+
}
|
74
|
+
Some(accept) if accept.contains("application/json") => {
|
75
|
+
BoxBody::new(Full::new(Bytes::from(
|
76
|
+
self.json
|
77
|
+
.as_ref()
|
78
|
+
.map(|json| json.to_string())
|
79
|
+
.unwrap_or_else(|| "Error".to_owned()),
|
80
|
+
)))
|
81
|
+
}
|
82
|
+
_ => match self.default {
|
83
|
+
ErrorFormat::Plaintext => BoxBody::new(Full::new(Bytes::from(
|
84
|
+
self.plaintext.clone().unwrap_or_else(|| "Error".to_owned()),
|
85
|
+
))),
|
86
|
+
ErrorFormat::Html => {
|
87
|
+
if let Some(path) = &self.html {
|
88
|
+
let path = path.to_str().unwrap();
|
89
|
+
let response = ROOT_STATIC_FILE_SERVER.serve_single(path).await;
|
90
|
+
|
91
|
+
if response.status().is_success() {
|
92
|
+
response.into_body()
|
93
|
+
} else {
|
94
|
+
BoxBody::new(Full::new(Bytes::from("Error")))
|
95
|
+
}
|
96
|
+
} else {
|
97
|
+
BoxBody::new(Full::new(Bytes::from("Error")))
|
98
|
+
}
|
99
|
+
}
|
100
|
+
ErrorFormat::Json => BoxBody::new(Full::new(Bytes::from(
|
101
|
+
self.json
|
102
|
+
.as_ref()
|
103
|
+
.map(|json| json.to_string())
|
104
|
+
.unwrap_or_else(|| "Error".to_owned()),
|
105
|
+
))),
|
106
|
+
},
|
107
|
+
};
|
108
|
+
|
109
|
+
Response::builder().status(self.code).body(body).unwrap()
|
110
|
+
}
|
111
|
+
|
112
|
+
pub async fn before(
|
113
|
+
&self,
|
114
|
+
req: HttpRequest,
|
115
|
+
_context: &mut RequestContext,
|
116
|
+
) -> Result<Either<HttpRequest, HttpResponse>, ItsiError> {
|
117
|
+
if let Some(path) = req.uri().path().strip_prefix("/error/") {
|
118
|
+
let path = Path::new(path);
|
119
|
+
if path.exists() {
|
120
|
+
let path = path.to_str().unwrap();
|
121
|
+
let response = ROOT_STATIC_FILE_SERVER.serve_single(path).await;
|
122
|
+
return Ok(Either::Right(response));
|
123
|
+
}
|
124
|
+
}
|
125
|
+
Ok(Either::Left(req))
|
126
|
+
}
|
127
|
+
}
|
@@ -0,0 +1,191 @@
|
|
1
|
+
use super::{FromValue, MiddlewareLayer};
|
2
|
+
use crate::server::{itsi_service::RequestContext, types::HttpResponse};
|
3
|
+
use async_trait::async_trait;
|
4
|
+
use base64::{engine::general_purpose, Engine as _};
|
5
|
+
use bytes::{Bytes, BytesMut};
|
6
|
+
use either::Either;
|
7
|
+
use futures::TryStreamExt;
|
8
|
+
use http::{header, HeaderValue, Response, StatusCode};
|
9
|
+
use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full};
|
10
|
+
use hyper::body::Body;
|
11
|
+
use magnus::error::Result;
|
12
|
+
use serde::Deserialize;
|
13
|
+
use sha2::{Digest, Sha256};
|
14
|
+
|
15
|
+
#[derive(Debug, Clone, Copy, Deserialize, Default)]
|
16
|
+
pub enum ETagType {
|
17
|
+
#[serde(rename = "strong")]
|
18
|
+
#[default]
|
19
|
+
Strong,
|
20
|
+
#[serde(rename = "weak")]
|
21
|
+
Weak,
|
22
|
+
}
|
23
|
+
|
24
|
+
#[derive(Debug, Clone, Copy, Deserialize, Default)]
|
25
|
+
pub enum HashAlgorithm {
|
26
|
+
#[serde(rename = "sha256")]
|
27
|
+
#[default]
|
28
|
+
Sha256,
|
29
|
+
#[serde(rename = "md5")]
|
30
|
+
Md5,
|
31
|
+
}
|
32
|
+
|
33
|
+
#[derive(Debug, Clone, Deserialize)]
|
34
|
+
pub struct ETag {
|
35
|
+
#[serde(default)]
|
36
|
+
pub r#type: ETagType,
|
37
|
+
#[serde(default)]
|
38
|
+
pub algorithm: HashAlgorithm,
|
39
|
+
#[serde(default)]
|
40
|
+
pub min_body_size: usize,
|
41
|
+
#[serde(default = "default_true")]
|
42
|
+
pub handle_if_none_match: bool,
|
43
|
+
}
|
44
|
+
|
45
|
+
fn default_true() -> bool {
|
46
|
+
true
|
47
|
+
}
|
48
|
+
|
49
|
+
#[async_trait]
|
50
|
+
impl MiddlewareLayer for ETag {
|
51
|
+
async fn before(
|
52
|
+
&self,
|
53
|
+
req: crate::server::types::HttpRequest,
|
54
|
+
context: &mut RequestContext,
|
55
|
+
) -> Result<Either<crate::server::types::HttpRequest, HttpResponse>> {
|
56
|
+
// Store if-none-match header in context if present for later use in after hook
|
57
|
+
if self.handle_if_none_match {
|
58
|
+
if let Some(if_none_match) = req.headers().get(header::IF_NONE_MATCH) {
|
59
|
+
if let Ok(etag_value) = if_none_match.to_str() {
|
60
|
+
context.set_if_none_match(Some(etag_value.to_string()));
|
61
|
+
}
|
62
|
+
}
|
63
|
+
}
|
64
|
+
Ok(Either::Left(req))
|
65
|
+
}
|
66
|
+
|
67
|
+
async fn after(&self, resp: HttpResponse, context: &mut RequestContext) -> HttpResponse {
|
68
|
+
// Skip for error responses or responses that shouldn't have ETags
|
69
|
+
match resp.status() {
|
70
|
+
StatusCode::OK
|
71
|
+
| StatusCode::CREATED
|
72
|
+
| StatusCode::ACCEPTED
|
73
|
+
| StatusCode::NON_AUTHORITATIVE_INFORMATION
|
74
|
+
| StatusCode::NO_CONTENT
|
75
|
+
| StatusCode::PARTIAL_CONTENT => {}
|
76
|
+
_ => return resp,
|
77
|
+
}
|
78
|
+
|
79
|
+
// Skip if already has an ETag
|
80
|
+
if resp.headers().contains_key(header::ETAG) {
|
81
|
+
return resp;
|
82
|
+
}
|
83
|
+
|
84
|
+
// Skip if Cache-Control: no-store is present
|
85
|
+
if let Some(cache_control) = resp.headers().get(header::CACHE_CONTROL) {
|
86
|
+
if let Ok(cache_control_str) = cache_control.to_str() {
|
87
|
+
if cache_control_str.contains("no-store") {
|
88
|
+
return resp;
|
89
|
+
}
|
90
|
+
}
|
91
|
+
}
|
92
|
+
|
93
|
+
// Check if body is a stream or fixed size using size_hint (similar to compression.rs)
|
94
|
+
let body_size = resp.size_hint().exact();
|
95
|
+
|
96
|
+
// Skip streaming bodies
|
97
|
+
if body_size.is_none() {
|
98
|
+
return resp;
|
99
|
+
}
|
100
|
+
|
101
|
+
// Skip if body is too small
|
102
|
+
if body_size.unwrap_or(0) < self.min_body_size as u64 {
|
103
|
+
return resp;
|
104
|
+
}
|
105
|
+
|
106
|
+
let (mut parts, mut body) = resp.into_parts();
|
107
|
+
let etag_value = if let Some(existing_etag) = parts.headers.get(header::ETAG) {
|
108
|
+
existing_etag.to_str().unwrap_or("").to_string()
|
109
|
+
} else {
|
110
|
+
// Get the full bytes from the body
|
111
|
+
let full_bytes: Bytes = match body
|
112
|
+
.into_data_stream()
|
113
|
+
.try_fold(BytesMut::new(), |mut acc, chunk| async move {
|
114
|
+
acc.extend_from_slice(&chunk);
|
115
|
+
Ok(acc)
|
116
|
+
})
|
117
|
+
.await
|
118
|
+
{
|
119
|
+
Ok(bytes_mut) => bytes_mut.freeze(),
|
120
|
+
Err(_) => return Response::from_parts(parts, BoxBody::new(Empty::new())),
|
121
|
+
};
|
122
|
+
|
123
|
+
let computed_etag = match self.algorithm {
|
124
|
+
HashAlgorithm::Sha256 => {
|
125
|
+
let mut hasher = Sha256::new();
|
126
|
+
hasher.update(&full_bytes);
|
127
|
+
let result = hasher.finalize();
|
128
|
+
general_purpose::STANDARD.encode(result)
|
129
|
+
}
|
130
|
+
HashAlgorithm::Md5 => {
|
131
|
+
let digest = md5::compute(&full_bytes);
|
132
|
+
format!("{:x}", digest)
|
133
|
+
}
|
134
|
+
};
|
135
|
+
|
136
|
+
let formatted_etag = match self.r#type {
|
137
|
+
ETagType::Strong => format!("\"{}\"", computed_etag),
|
138
|
+
ETagType::Weak => format!("W/\"{}\"", computed_etag),
|
139
|
+
};
|
140
|
+
|
141
|
+
if let Ok(value) = HeaderValue::from_str(&formatted_etag) {
|
142
|
+
parts.headers.insert(header::ETAG, value);
|
143
|
+
}
|
144
|
+
|
145
|
+
body = Full::new(full_bytes).boxed();
|
146
|
+
formatted_etag
|
147
|
+
};
|
148
|
+
|
149
|
+
// Handle 304 Not Modified if we have an If-None-Match header and it matches
|
150
|
+
if self.handle_if_none_match {
|
151
|
+
if let Some(if_none_match) = context.get_if_none_match() {
|
152
|
+
if if_none_match == etag_value || if_none_match == "*" {
|
153
|
+
// Return 304 Not Modified without the body
|
154
|
+
let mut not_modified = Response::new(BoxBody::new(Empty::new()));
|
155
|
+
*not_modified.status_mut() = StatusCode::NOT_MODIFIED;
|
156
|
+
// Copy headers we want to preserve
|
157
|
+
for (name, value) in parts.headers.iter() {
|
158
|
+
if matches!(
|
159
|
+
name,
|
160
|
+
&header::CACHE_CONTROL
|
161
|
+
| &header::CONTENT_LOCATION
|
162
|
+
| &header::DATE
|
163
|
+
| &header::ETAG
|
164
|
+
| &header::EXPIRES
|
165
|
+
| &header::VARY
|
166
|
+
) {
|
167
|
+
not_modified.headers_mut().insert(name, value.clone());
|
168
|
+
}
|
169
|
+
}
|
170
|
+
return not_modified;
|
171
|
+
}
|
172
|
+
}
|
173
|
+
}
|
174
|
+
|
175
|
+
// Recreate response with the original body and the ETag header
|
176
|
+
Response::from_parts(parts, body)
|
177
|
+
}
|
178
|
+
}
|
179
|
+
|
180
|
+
impl Default for ETag {
|
181
|
+
fn default() -> Self {
|
182
|
+
Self {
|
183
|
+
r#type: ETagType::Strong,
|
184
|
+
algorithm: HashAlgorithm::Sha256,
|
185
|
+
min_body_size: 0,
|
186
|
+
handle_if_none_match: true,
|
187
|
+
}
|
188
|
+
}
|
189
|
+
}
|
190
|
+
|
191
|
+
impl FromValue for ETag {}
|