itsi-scheduler 0.1.5 → 0.2.2
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.
- checksums.yaml +4 -4
- data/Cargo.lock +120 -52
- data/README.md +57 -24
- data/Rakefile +0 -4
- data/ext/itsi_acme/Cargo.toml +86 -0
- data/ext/itsi_acme/examples/high_level.rs +63 -0
- data/ext/itsi_acme/examples/high_level_warp.rs +52 -0
- data/ext/itsi_acme/examples/low_level.rs +87 -0
- data/ext/itsi_acme/examples/low_level_axum.rs +66 -0
- data/ext/itsi_acme/src/acceptor.rs +81 -0
- data/ext/itsi_acme/src/acme.rs +354 -0
- data/ext/itsi_acme/src/axum.rs +86 -0
- data/ext/itsi_acme/src/cache.rs +39 -0
- data/ext/itsi_acme/src/caches/boxed.rs +80 -0
- data/ext/itsi_acme/src/caches/composite.rs +69 -0
- data/ext/itsi_acme/src/caches/dir.rs +106 -0
- data/ext/itsi_acme/src/caches/mod.rs +11 -0
- data/ext/itsi_acme/src/caches/no.rs +78 -0
- data/ext/itsi_acme/src/caches/test.rs +136 -0
- data/ext/itsi_acme/src/config.rs +172 -0
- data/ext/itsi_acme/src/https_helper.rs +69 -0
- data/ext/itsi_acme/src/incoming.rs +142 -0
- data/ext/itsi_acme/src/jose.rs +161 -0
- data/ext/itsi_acme/src/lib.rs +142 -0
- data/ext/itsi_acme/src/resolver.rs +59 -0
- data/ext/itsi_acme/src/state.rs +424 -0
- data/ext/itsi_error/Cargo.toml +1 -0
- data/ext/itsi_error/src/lib.rs +106 -7
- 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_rb_helpers/Cargo.toml +1 -0
- data/ext/itsi_rb_helpers/src/heap_value.rs +18 -0
- data/ext/itsi_rb_helpers/src/lib.rs +63 -12
- 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 +1 -1
- data/ext/itsi_scheduler/src/itsi_scheduler.rs +9 -3
- data/ext/itsi_scheduler/src/lib.rs +1 -0
- data/ext/itsi_server/Cargo.lock +2956 -0
- data/ext/itsi_server/Cargo.toml +73 -29
- data/ext/itsi_server/src/default_responses/mod.rs +11 -0
- data/ext/itsi_server/src/env.rs +43 -0
- data/ext/itsi_server/src/lib.rs +114 -75
- data/ext/itsi_server/src/prelude.rs +2 -0
- data/ext/itsi_server/src/{body_proxy → ruby_types/itsi_body_proxy}/big_bytes.rs +10 -5
- data/ext/itsi_server/src/{body_proxy/itsi_body_proxy.rs → ruby_types/itsi_body_proxy/mod.rs} +29 -8
- data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
- data/ext/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +264 -0
- data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +362 -0
- data/ext/itsi_server/src/{response/itsi_response.rs → ruby_types/itsi_http_response.rs} +84 -40
- data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +233 -0
- data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +565 -0
- data/ext/itsi_server/src/ruby_types/itsi_server.rs +86 -0
- data/ext/itsi_server/src/ruby_types/mod.rs +48 -0
- data/ext/itsi_server/src/server/{bind.rs → binds/bind.rs} +59 -24
- data/ext/itsi_server/src/server/binds/listener.rs +444 -0
- data/ext/itsi_server/src/server/binds/mod.rs +4 -0
- data/ext/itsi_server/src/server/{tls → binds/tls}/locked_dir_cache.rs +57 -19
- data/ext/itsi_server/src/server/{tls.rs → binds/tls.rs} +120 -31
- data/ext/itsi_server/src/server/byte_frame.rs +32 -0
- data/ext/itsi_server/src/server/http_message_types.rs +97 -0
- data/ext/itsi_server/src/server/io_stream.rs +2 -1
- data/ext/itsi_server/src/server/lifecycle_event.rs +3 -0
- data/ext/itsi_server/src/server/middleware_stack/middleware.rs +170 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +63 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +94 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +94 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +343 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +151 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +316 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +301 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/csp.rs +193 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +64 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +192 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +171 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +198 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +82 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +209 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +116 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +411 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +142 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +55 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +54 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +51 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +126 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +187 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +173 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +31 -0
- data/ext/itsi_server/src/server/middleware_stack/mod.rs +381 -0
- data/ext/itsi_server/src/server/mod.rs +7 -5
- data/ext/itsi_server/src/server/process_worker.rs +65 -14
- data/ext/itsi_server/src/server/redirect_type.rs +26 -0
- data/ext/itsi_server/src/server/request_job.rs +11 -0
- data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +150 -50
- data/ext/itsi_server/src/server/serve_strategy/mod.rs +9 -6
- data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +399 -165
- data/ext/itsi_server/src/server/signal.rs +33 -26
- data/ext/itsi_server/src/server/size_limited_incoming.rs +107 -0
- data/ext/itsi_server/src/server/thread_worker.rs +218 -107
- data/ext/itsi_server/src/services/cache_store.rs +74 -0
- data/ext/itsi_server/src/services/itsi_http_service.rs +257 -0
- data/ext/itsi_server/src/services/mime_types.rs +1416 -0
- data/ext/itsi_server/src/services/mod.rs +6 -0
- data/ext/itsi_server/src/services/password_hasher.rs +83 -0
- data/ext/itsi_server/src/services/rate_limiter.rs +580 -0
- data/ext/itsi_server/src/services/static_file_server.rs +1340 -0
- data/ext/itsi_tracing/Cargo.toml +1 -0
- data/ext/itsi_tracing/src/lib.rs +362 -33
- 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/itsi-scheduler-100.png +0 -0
- data/lib/itsi/scheduler/version.rb +1 -1
- data/lib/itsi/scheduler.rb +11 -6
- metadata +117 -24
- data/CHANGELOG.md +0 -5
- data/CODE_OF_CONDUCT.md +0 -132
- data/LICENSE.txt +0 -21
- data/ext/itsi_error/src/from.rs +0 -71
- data/ext/itsi_server/extconf.rb +0 -6
- data/ext/itsi_server/src/body_proxy/mod.rs +0 -2
- data/ext/itsi_server/src/request/itsi_request.rs +0 -277
- data/ext/itsi_server/src/request/mod.rs +0 -1
- data/ext/itsi_server/src/response/mod.rs +0 -1
- data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -13
- data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -5
- data/ext/itsi_server/src/server/itsi_server.rs +0 -244
- data/ext/itsi_server/src/server/listener.rs +0 -327
- /data/ext/itsi_server/src/server/{bind_protocol.rs → binds/bind_protocol.rs} +0 -0
@@ -0,0 +1,301 @@
|
|
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
|
+
use tracing::debug;
|
14
|
+
|
15
|
+
#[derive(Debug, Clone, Deserialize)]
|
16
|
+
pub struct Cors {
|
17
|
+
pub allow_origins: Vec<String>,
|
18
|
+
pub allow_methods: Vec<HttpMethod>,
|
19
|
+
pub allow_headers: Vec<String>,
|
20
|
+
pub allow_credentials: bool,
|
21
|
+
pub expose_headers: Vec<String>,
|
22
|
+
pub max_age: Option<u64>,
|
23
|
+
}
|
24
|
+
|
25
|
+
#[derive(Debug, Clone, Deserialize)]
|
26
|
+
pub enum HttpMethod {
|
27
|
+
#[serde(rename(deserialize = "GET"))]
|
28
|
+
Get,
|
29
|
+
#[serde(rename(deserialize = "POST"))]
|
30
|
+
Post,
|
31
|
+
#[serde(rename(deserialize = "PUT"))]
|
32
|
+
Put,
|
33
|
+
#[serde(rename(deserialize = "DELETE"))]
|
34
|
+
Delete,
|
35
|
+
#[serde(rename(deserialize = "OPTIONS"))]
|
36
|
+
Options,
|
37
|
+
#[serde(rename(deserialize = "HEAD"))]
|
38
|
+
Head,
|
39
|
+
#[serde(rename(deserialize = "PATCH"))]
|
40
|
+
Patch,
|
41
|
+
}
|
42
|
+
|
43
|
+
impl HttpMethod {
|
44
|
+
pub fn matches(&self, other: &str) -> bool {
|
45
|
+
match self {
|
46
|
+
HttpMethod::Get => other.eq_ignore_ascii_case("GET"),
|
47
|
+
HttpMethod::Post => other.eq_ignore_ascii_case("POST"),
|
48
|
+
HttpMethod::Put => other.eq_ignore_ascii_case("PUT"),
|
49
|
+
HttpMethod::Delete => other.eq_ignore_ascii_case("DELETE"),
|
50
|
+
HttpMethod::Options => other.eq_ignore_ascii_case("OPTIONS"),
|
51
|
+
HttpMethod::Head => other.eq_ignore_ascii_case("HEAD"),
|
52
|
+
HttpMethod::Patch => other.eq_ignore_ascii_case("PATCH"),
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
pub fn to_str(&self) -> &str {
|
57
|
+
match self {
|
58
|
+
HttpMethod::Get => "GET",
|
59
|
+
HttpMethod::Post => "POST",
|
60
|
+
HttpMethod::Put => "PUT",
|
61
|
+
HttpMethod::Delete => "DELETE",
|
62
|
+
HttpMethod::Options => "OPTIONS",
|
63
|
+
HttpMethod::Head => "HEAD",
|
64
|
+
HttpMethod::Patch => "PATCH",
|
65
|
+
}
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
impl Cors {
|
70
|
+
/// Generate the simple CORS headers (used in normal responses)
|
71
|
+
fn cors_headers(&self, origin: &str) -> Result<HeaderMap> {
|
72
|
+
let mut headers = HeaderMap::new();
|
73
|
+
|
74
|
+
headers.insert("Vary", "Origin".parse().map_err(ItsiError::new)?);
|
75
|
+
|
76
|
+
if origin.is_empty() {
|
77
|
+
// When credentials are allowed, you cannot return "*".
|
78
|
+
debug!(target: "middleware::cors", "Origin empty {}", origin);
|
79
|
+
if !self.allow_credentials {
|
80
|
+
headers.insert(
|
81
|
+
"Access-Control-Allow-Origin",
|
82
|
+
"*".parse().map_err(ItsiError::new)?,
|
83
|
+
);
|
84
|
+
}
|
85
|
+
return Ok(headers);
|
86
|
+
}
|
87
|
+
|
88
|
+
// Only return a header if the origin is allowed.
|
89
|
+
if self.allow_origins.iter().any(|o| o == origin || o == "*") {
|
90
|
+
// If credentials are allowed, we must echo back the exact origin.
|
91
|
+
let value = if self.allow_credentials {
|
92
|
+
origin
|
93
|
+
} else {
|
94
|
+
// If not, and if "*" is allowed, you can still use "*".
|
95
|
+
if self.allow_origins.iter().any(|o| o == "*") {
|
96
|
+
"*"
|
97
|
+
} else {
|
98
|
+
origin
|
99
|
+
}
|
100
|
+
};
|
101
|
+
headers.insert(
|
102
|
+
"Access-Control-Allow-Origin",
|
103
|
+
value.parse().map_err(ItsiError::new)?,
|
104
|
+
);
|
105
|
+
}
|
106
|
+
|
107
|
+
if !self.allow_methods.is_empty() {
|
108
|
+
headers.insert(
|
109
|
+
"Access-Control-Allow-Methods",
|
110
|
+
self.allow_methods
|
111
|
+
.iter()
|
112
|
+
.map(HttpMethod::to_str)
|
113
|
+
.collect::<Vec<&str>>()
|
114
|
+
.join(", ")
|
115
|
+
.parse()
|
116
|
+
.map_err(ItsiError::new)?,
|
117
|
+
);
|
118
|
+
}
|
119
|
+
if !self.allow_headers.is_empty() {
|
120
|
+
headers.insert(
|
121
|
+
"Access-Control-Allow-Headers",
|
122
|
+
self.allow_headers
|
123
|
+
.join(", ")
|
124
|
+
.parse()
|
125
|
+
.map_err(ItsiError::new)?,
|
126
|
+
);
|
127
|
+
}
|
128
|
+
if self.allow_credentials {
|
129
|
+
headers.insert(
|
130
|
+
"Access-Control-Allow-Credentials",
|
131
|
+
"true".parse().map_err(ItsiError::new)?,
|
132
|
+
);
|
133
|
+
}
|
134
|
+
if let Some(max_age) = self.max_age {
|
135
|
+
headers.insert(
|
136
|
+
"Access-Control-Max-Age",
|
137
|
+
max_age.to_string().parse().map_err(ItsiError::new)?,
|
138
|
+
);
|
139
|
+
}
|
140
|
+
if !self.expose_headers.is_empty() {
|
141
|
+
headers.insert(
|
142
|
+
"Access-Control-Expose-Headers",
|
143
|
+
self.expose_headers
|
144
|
+
.join(", ")
|
145
|
+
.parse()
|
146
|
+
.map_err(ItsiError::new)?,
|
147
|
+
);
|
148
|
+
}
|
149
|
+
Ok(headers)
|
150
|
+
}
|
151
|
+
|
152
|
+
fn preflight_headers(
|
153
|
+
&self,
|
154
|
+
origin: Option<&str>,
|
155
|
+
req_method: Option<&str>,
|
156
|
+
req_headers: Option<&str>,
|
157
|
+
) -> Result<HeaderMap> {
|
158
|
+
let mut headers = HeaderMap::new();
|
159
|
+
|
160
|
+
headers.insert("Vary", "Origin".parse().map_err(ItsiError::new)?);
|
161
|
+
|
162
|
+
let origin = match origin {
|
163
|
+
Some(o) if !o.is_empty() => o,
|
164
|
+
_ => {
|
165
|
+
debug!(target: "middleware::cors", "Missing Origin – preflight fails");
|
166
|
+
return Ok(headers);
|
167
|
+
}
|
168
|
+
};
|
169
|
+
|
170
|
+
if !self
|
171
|
+
.allow_origins
|
172
|
+
.iter()
|
173
|
+
.any(|allowed| allowed == "*" || allowed == origin)
|
174
|
+
{
|
175
|
+
debug!(target: "middleware::cors", "Origin not allowed");
|
176
|
+
return Ok(headers);
|
177
|
+
}
|
178
|
+
|
179
|
+
let request_method = match req_method {
|
180
|
+
Some(m) if !m.is_empty() => m,
|
181
|
+
_ => {
|
182
|
+
debug!(target: "middleware::cors", "Missing request method – preflight fails");
|
183
|
+
return Ok(headers);
|
184
|
+
}
|
185
|
+
};
|
186
|
+
|
187
|
+
if !self.allow_methods.iter().any(|m| m.matches(request_method)) {
|
188
|
+
debug!(target: "middleware::cors", "Method not allowed");
|
189
|
+
return Ok(headers);
|
190
|
+
}
|
191
|
+
|
192
|
+
if let Some(request_headers) = req_headers {
|
193
|
+
let req_headers_list: Vec<&str> = request_headers
|
194
|
+
.split(',')
|
195
|
+
.map(|s| s.trim())
|
196
|
+
.filter(|s| !s.is_empty())
|
197
|
+
.collect();
|
198
|
+
for header in req_headers_list {
|
199
|
+
if !self
|
200
|
+
.allow_headers
|
201
|
+
.iter()
|
202
|
+
.any(|allowed| allowed.eq_ignore_ascii_case(header))
|
203
|
+
{
|
204
|
+
debug!(target: "middleware::cors", "Header not allowed {}", header);
|
205
|
+
return Ok(headers);
|
206
|
+
}
|
207
|
+
}
|
208
|
+
}
|
209
|
+
|
210
|
+
headers.insert("Access-Control-Allow-Origin", origin.parse().unwrap());
|
211
|
+
headers.insert(
|
212
|
+
"Access-Control-Allow-Methods",
|
213
|
+
self.allow_methods
|
214
|
+
.iter()
|
215
|
+
.map(HttpMethod::to_str)
|
216
|
+
.collect::<Vec<&str>>()
|
217
|
+
.join(", ")
|
218
|
+
.parse()
|
219
|
+
.map_err(ItsiError::new)?,
|
220
|
+
);
|
221
|
+
headers.insert(
|
222
|
+
"Access-Control-Allow-Headers",
|
223
|
+
self.allow_headers
|
224
|
+
.join(", ")
|
225
|
+
.parse()
|
226
|
+
.map_err(ItsiError::new)?,
|
227
|
+
);
|
228
|
+
if self.allow_credentials {
|
229
|
+
headers.insert(
|
230
|
+
"Access-Control-Allow-Credentials",
|
231
|
+
"true".parse().map_err(ItsiError::new)?,
|
232
|
+
);
|
233
|
+
}
|
234
|
+
if let Some(max_age) = self.max_age {
|
235
|
+
headers.insert(
|
236
|
+
"Access-Control-Max-Age",
|
237
|
+
max_age.to_string().parse().map_err(ItsiError::new)?,
|
238
|
+
);
|
239
|
+
}
|
240
|
+
if !self.expose_headers.is_empty() {
|
241
|
+
headers.insert(
|
242
|
+
"Access-Control-Expose-Headers",
|
243
|
+
self.expose_headers
|
244
|
+
.join(", ")
|
245
|
+
.parse()
|
246
|
+
.map_err(ItsiError::new)?,
|
247
|
+
);
|
248
|
+
}
|
249
|
+
|
250
|
+
Ok(headers)
|
251
|
+
}
|
252
|
+
}
|
253
|
+
|
254
|
+
#[async_trait]
|
255
|
+
impl MiddlewareLayer for Cors {
|
256
|
+
// For OPTIONS (preflight) requests we:
|
257
|
+
// 1. Extract Origin, Access-Control-Request-Method, and Access-Control-Request-Headers.
|
258
|
+
// 2. Validate them using our hardened preflight_headers function.
|
259
|
+
// 3. If validations pass (i.e. headers is non-empty), return a 204 response with those headers.
|
260
|
+
// Otherwise, the absence of headers indicates the request doesn’t meet the CORS policy.
|
261
|
+
async fn before(
|
262
|
+
&self,
|
263
|
+
req: HttpRequest,
|
264
|
+
context: &mut HttpRequestContext,
|
265
|
+
) -> Result<either::Either<HttpRequest, HttpResponse>> {
|
266
|
+
let origin = req.header("Origin");
|
267
|
+
debug!(target: "middleware::cors", "Origin: {:?}", origin);
|
268
|
+
if req.method() == Method::OPTIONS {
|
269
|
+
let ac_request_method = req.header("Access-Control-Request-Method");
|
270
|
+
let ac_request_headers = req.header("Access-Control-Request-Headers");
|
271
|
+
let headers = self.preflight_headers(origin, ac_request_method, ac_request_headers)?;
|
272
|
+
debug!(target: "middleware::cors", "Preflight Headers: {:?}", headers);
|
273
|
+
let mut response_builder = Response::builder().status(204);
|
274
|
+
*response_builder.headers_mut().unwrap() = headers;
|
275
|
+
let response = response_builder
|
276
|
+
.body(BoxBody::new(Empty::new()))
|
277
|
+
.map_err(ItsiError::new)?;
|
278
|
+
return Ok(either::Either::Right(response));
|
279
|
+
}
|
280
|
+
context.set_origin(origin.map(|s| s.to_string()));
|
281
|
+
Ok(either::Either::Left(req))
|
282
|
+
}
|
283
|
+
|
284
|
+
async fn after(
|
285
|
+
&self,
|
286
|
+
mut resp: HttpResponse,
|
287
|
+
context: &mut HttpRequestContext,
|
288
|
+
) -> HttpResponse {
|
289
|
+
if let Some(Some(origin)) = context.origin.get() {
|
290
|
+
debug!(target: "middleware::cors", "fetching cors headers for origin {}", origin);
|
291
|
+
if let Ok(cors_headers) = self.cors_headers(origin) {
|
292
|
+
debug!(target: "middleware::cors", "Cors Headers: {:?}", cors_headers);
|
293
|
+
for (key, value) in cors_headers.iter() {
|
294
|
+
resp.headers_mut().insert(key.clone(), value.clone());
|
295
|
+
}
|
296
|
+
}
|
297
|
+
}
|
298
|
+
resp
|
299
|
+
}
|
300
|
+
}
|
301
|
+
impl FromValue for Cors {}
|
@@ -0,0 +1,193 @@
|
|
1
|
+
use super::FromValue;
|
2
|
+
use crate::{
|
3
|
+
server::http_message_types::{HttpRequest, HttpResponse},
|
4
|
+
services::itsi_http_service::HttpRequestContext,
|
5
|
+
};
|
6
|
+
use async_trait::async_trait;
|
7
|
+
use bytes::{Bytes, BytesMut};
|
8
|
+
use either::Either;
|
9
|
+
use futures::TryStreamExt;
|
10
|
+
use http::{HeaderValue, StatusCode};
|
11
|
+
use http_body_util::{combinators::BoxBody, BodyExt, Empty};
|
12
|
+
use itsi_error::ItsiError;
|
13
|
+
use serde::{Deserialize, Serialize};
|
14
|
+
use std::sync::Arc;
|
15
|
+
use std::{path::PathBuf, sync::OnceLock};
|
16
|
+
use tokio::sync::Mutex;
|
17
|
+
use tokio::time::{self, Duration};
|
18
|
+
use tracing::debug;
|
19
|
+
|
20
|
+
#[derive(Debug, Serialize, Deserialize)]
|
21
|
+
pub struct CspReport {
|
22
|
+
#[serde(rename = "csp-report")]
|
23
|
+
pub report: ReportDetails,
|
24
|
+
}
|
25
|
+
|
26
|
+
#[derive(Debug, Serialize, Deserialize)]
|
27
|
+
pub struct ReportDetails {
|
28
|
+
#[serde(rename = "document-uri")]
|
29
|
+
pub document_uri: String,
|
30
|
+
#[serde(rename = "referrer")]
|
31
|
+
pub referrer: Option<String>,
|
32
|
+
#[serde(rename = "violated-directive")]
|
33
|
+
pub violated_directive: String,
|
34
|
+
#[serde(rename = "original-policy")]
|
35
|
+
pub original_policy: String,
|
36
|
+
#[serde(rename = "blocked-uri")]
|
37
|
+
pub blocked_uri: String,
|
38
|
+
}
|
39
|
+
|
40
|
+
#[derive(Debug, Deserialize)]
|
41
|
+
pub struct CspConfig {
|
42
|
+
pub default_src: Vec<String>,
|
43
|
+
pub script_src: Vec<String>,
|
44
|
+
pub style_src: Vec<String>,
|
45
|
+
pub report_uri: Vec<String>,
|
46
|
+
}
|
47
|
+
|
48
|
+
#[derive(Debug, Deserialize)]
|
49
|
+
pub struct Csp {
|
50
|
+
pub policy: Option<CspConfig>,
|
51
|
+
pub reporting_enabled: bool,
|
52
|
+
pub report_file: Option<PathBuf>,
|
53
|
+
pub report_endpoint: String,
|
54
|
+
pub flush_interval: f64,
|
55
|
+
|
56
|
+
#[serde(skip)]
|
57
|
+
pub computed_policy: OnceLock<String>,
|
58
|
+
#[serde(skip)]
|
59
|
+
pub pending_reports: Arc<Mutex<Vec<CspReport>>>,
|
60
|
+
#[serde(skip)]
|
61
|
+
pub flush_task: OnceLock<tokio::task::JoinHandle<()>>,
|
62
|
+
}
|
63
|
+
|
64
|
+
#[async_trait]
|
65
|
+
impl super::MiddlewareLayer for Csp {
|
66
|
+
async fn initialize(&self) -> Result<(), magnus::error::Error> {
|
67
|
+
if let Some(policy_config) = &self.policy {
|
68
|
+
let mut parts = Vec::new();
|
69
|
+
if !policy_config.default_src.is_empty() {
|
70
|
+
parts.push(format!(
|
71
|
+
"default-src {}",
|
72
|
+
policy_config.default_src.join(" ")
|
73
|
+
));
|
74
|
+
}
|
75
|
+
if !policy_config.script_src.is_empty() {
|
76
|
+
parts.push(format!("script-src {}", policy_config.script_src.join(" ")));
|
77
|
+
}
|
78
|
+
if !policy_config.style_src.is_empty() {
|
79
|
+
parts.push(format!("style-src {}", policy_config.style_src.join(" ")));
|
80
|
+
}
|
81
|
+
if !policy_config.report_uri.is_empty() {
|
82
|
+
parts.push(format!("report-uri {}", policy_config.report_uri.join(" ")));
|
83
|
+
}
|
84
|
+
let policy = parts.join("; ");
|
85
|
+
debug!(target: "middleware::csp", "Computed CSP policy: {}", policy);
|
86
|
+
self.computed_policy
|
87
|
+
.set(policy)
|
88
|
+
.map_err(|_| ItsiError::new("Failed to set computed CSP policy"))?;
|
89
|
+
}
|
90
|
+
|
91
|
+
if self.reporting_enabled {
|
92
|
+
if let Some(ref report_file) = self.report_file {
|
93
|
+
let flush_interval = self.flush_interval;
|
94
|
+
let report_path = report_file.clone();
|
95
|
+
let pending_reports = Arc::clone(&self.pending_reports);
|
96
|
+
let handle = tokio::spawn(async move {
|
97
|
+
let mut interval = time::interval(Duration::from_secs_f64(flush_interval));
|
98
|
+
loop {
|
99
|
+
interval.tick().await;
|
100
|
+
|
101
|
+
let mut reports = pending_reports.lock().await;
|
102
|
+
if !reports.is_empty() {
|
103
|
+
let mut lines = String::new();
|
104
|
+
for report in reports.iter() {
|
105
|
+
if let Ok(line) = serde_json::to_string(report) {
|
106
|
+
lines.push_str(&line);
|
107
|
+
lines.push('\n');
|
108
|
+
}
|
109
|
+
}
|
110
|
+
reports.clear();
|
111
|
+
|
112
|
+
debug!("Flushing CSP report to file {:?}", &report_path.display());
|
113
|
+
|
114
|
+
use tokio::io::AsyncWriteExt;
|
115
|
+
|
116
|
+
match tokio::fs::OpenOptions::new()
|
117
|
+
.append(true)
|
118
|
+
.create(true)
|
119
|
+
.open(&report_path)
|
120
|
+
.await
|
121
|
+
{
|
122
|
+
Ok(mut file) => {
|
123
|
+
if let Err(e) = file.write_all(lines.as_bytes()).await {
|
124
|
+
eprintln!("Error writing CSP reports: {:?}", e);
|
125
|
+
}
|
126
|
+
}
|
127
|
+
Err(e) => {
|
128
|
+
eprintln!("Error opening CSP report file: {:?}", e);
|
129
|
+
}
|
130
|
+
}
|
131
|
+
}
|
132
|
+
}
|
133
|
+
});
|
134
|
+
self.flush_task
|
135
|
+
.set(handle)
|
136
|
+
.map_err(|_| ItsiError::new("Failed to set flush task handle"))?;
|
137
|
+
}
|
138
|
+
}
|
139
|
+
Ok(())
|
140
|
+
}
|
141
|
+
|
142
|
+
async fn before(
|
143
|
+
&self,
|
144
|
+
req: HttpRequest,
|
145
|
+
_context: &mut HttpRequestContext,
|
146
|
+
) -> Result<Either<HttpRequest, HttpResponse>, magnus::error::Error> {
|
147
|
+
if self.reporting_enabled && req.uri().path() == self.report_endpoint {
|
148
|
+
debug!(target: "middleware::csp", "Received CSP report");
|
149
|
+
let full_bytes: Result<Bytes, _> = req
|
150
|
+
.into_body()
|
151
|
+
.into_data_stream()
|
152
|
+
.try_fold(BytesMut::new(), |mut acc, chunk| async move {
|
153
|
+
acc.extend_from_slice(&chunk);
|
154
|
+
Ok(acc)
|
155
|
+
})
|
156
|
+
.await
|
157
|
+
.map(|b| b.freeze());
|
158
|
+
|
159
|
+
if let Ok(body_bytes) = full_bytes {
|
160
|
+
if let Ok(report) = serde_json::from_slice::<CspReport>(&body_bytes) {
|
161
|
+
debug!(target: "middleware::csp", "Report: {:?}", report);
|
162
|
+
let mut pending = self.pending_reports.lock().await;
|
163
|
+
pending.push(report);
|
164
|
+
}
|
165
|
+
}
|
166
|
+
|
167
|
+
let mut resp = HttpResponse::new(BoxBody::new(Empty::new()));
|
168
|
+
*resp.status_mut() = StatusCode::NO_CONTENT;
|
169
|
+
return Ok(Either::Right(resp));
|
170
|
+
}
|
171
|
+
Ok(Either::Left(req))
|
172
|
+
}
|
173
|
+
|
174
|
+
async fn after(&self, resp: HttpResponse, _context: &mut HttpRequestContext) -> HttpResponse {
|
175
|
+
if let Some(policy) = self.computed_policy.get() {
|
176
|
+
if !resp.headers().contains_key("Content-Security-Policy") {
|
177
|
+
debug!(target: "middleware::csp", "Adding CSP header");
|
178
|
+
let (mut parts, body) = resp.into_parts();
|
179
|
+
if let Ok(header_value) = HeaderValue::from_str(policy) {
|
180
|
+
parts
|
181
|
+
.headers
|
182
|
+
.insert("Content-Security-Policy", header_value);
|
183
|
+
}
|
184
|
+
return HttpResponse::from_parts(parts, body);
|
185
|
+
} else {
|
186
|
+
debug!(target: "middleware::csp", "CSP header already present");
|
187
|
+
}
|
188
|
+
}
|
189
|
+
resp
|
190
|
+
}
|
191
|
+
}
|
192
|
+
|
193
|
+
impl FromValue for Csp {}
|
@@ -0,0 +1,64 @@
|
|
1
|
+
use crate::{
|
2
|
+
server::http_message_types::{HttpRequest, HttpResponse, RequestExt},
|
3
|
+
services::itsi_http_service::HttpRequestContext,
|
4
|
+
};
|
5
|
+
|
6
|
+
use super::{token_source::TokenSource, 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::{collections::HashMap, sync::OnceLock};
|
14
|
+
use tracing::debug;
|
15
|
+
|
16
|
+
#[derive(Debug, Clone, Deserialize)]
|
17
|
+
pub struct DenyList {
|
18
|
+
#[serde(skip_deserializing)]
|
19
|
+
pub denied_ips: OnceLock<RegexSet>,
|
20
|
+
pub denied_patterns: Vec<String>,
|
21
|
+
pub trusted_proxies: HashMap<String, TokenSource>,
|
22
|
+
#[serde(default = "forbidden_error_response")]
|
23
|
+
pub error_response: ErrorResponse,
|
24
|
+
}
|
25
|
+
|
26
|
+
fn forbidden_error_response() -> ErrorResponse {
|
27
|
+
ErrorResponse::forbidden()
|
28
|
+
}
|
29
|
+
|
30
|
+
#[async_trait]
|
31
|
+
impl MiddlewareLayer for DenyList {
|
32
|
+
async fn initialize(&self) -> Result<()> {
|
33
|
+
let denied_ips = RegexSet::new(&self.denied_patterns).map_err(ItsiError::new)?;
|
34
|
+
self.denied_ips
|
35
|
+
.set(denied_ips)
|
36
|
+
.map_err(|e| ItsiError::new(format!("Failed to set allowed IPs: {:?}", e)))?;
|
37
|
+
Ok(())
|
38
|
+
}
|
39
|
+
|
40
|
+
async fn before(
|
41
|
+
&self,
|
42
|
+
req: HttpRequest,
|
43
|
+
context: &mut HttpRequestContext,
|
44
|
+
) -> Result<Either<HttpRequest, HttpResponse>> {
|
45
|
+
let addr = if self.trusted_proxies.contains_key(&context.addr) {
|
46
|
+
let source = self.trusted_proxies.get(&context.addr).unwrap();
|
47
|
+
source.extract_token(&req).unwrap_or(&context.addr)
|
48
|
+
} else {
|
49
|
+
&context.addr
|
50
|
+
};
|
51
|
+
if let Some(denied_ips) = self.denied_ips.get() {
|
52
|
+
if denied_ips.is_match(addr) {
|
53
|
+
debug!(target: "middleware::deny_list", "IP address {} is not allowed", addr);
|
54
|
+
return Ok(Either::Right(
|
55
|
+
self.error_response
|
56
|
+
.to_http_response(req.accept().into())
|
57
|
+
.await,
|
58
|
+
));
|
59
|
+
}
|
60
|
+
}
|
61
|
+
Ok(Either::Left(req))
|
62
|
+
}
|
63
|
+
}
|
64
|
+
impl FromValue for DenyList {}
|