itsi-scheduler 0.2.22-aarch64-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +8 -0
- data/Cargo.lock +997 -0
- data/Cargo.toml +7 -0
- data/Rakefile +39 -0
- 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.lock +368 -0
- data/ext/itsi_error/Cargo.toml +12 -0
- data/ext/itsi_error/src/lib.rs +140 -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.lock +355 -0
- data/ext/itsi_rb_helpers/Cargo.toml +11 -0
- data/ext/itsi_rb_helpers/src/heap_value.rs +139 -0
- data/ext/itsi_rb_helpers/src/lib.rs +232 -0
- data/ext/itsi_scheduler/Cargo.toml +24 -0
- data/ext/itsi_scheduler/extconf.rb +11 -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 +320 -0
- data/ext/itsi_scheduler/src/lib.rs +39 -0
- data/ext/itsi_server/Cargo.lock +2956 -0
- data/ext/itsi_server/Cargo.toml +94 -0
- data/ext/itsi_server/src/default_responses/mod.rs +14 -0
- data/ext/itsi_server/src/env.rs +43 -0
- data/ext/itsi_server/src/lib.rs +154 -0
- data/ext/itsi_server/src/prelude.rs +2 -0
- data/ext/itsi_server/src/ruby_types/itsi_body_proxy/big_bytes.rs +116 -0
- data/ext/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +149 -0
- data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +346 -0
- data/ext/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +265 -0
- data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +399 -0
- data/ext/itsi_server/src/ruby_types/itsi_http_response.rs +447 -0
- data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +545 -0
- data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +650 -0
- data/ext/itsi_server/src/ruby_types/itsi_server.rs +102 -0
- data/ext/itsi_server/src/ruby_types/mod.rs +48 -0
- data/ext/itsi_server/src/server/binds/bind.rs +204 -0
- data/ext/itsi_server/src/server/binds/bind_protocol.rs +37 -0
- data/ext/itsi_server/src/server/binds/listener.rs +485 -0
- data/ext/itsi_server/src/server/binds/mod.rs +4 -0
- data/ext/itsi_server/src/server/binds/tls/locked_dir_cache.rs +132 -0
- data/ext/itsi_server/src/server/binds/tls.rs +278 -0
- data/ext/itsi_server/src/server/byte_frame.rs +32 -0
- data/ext/itsi_server/src/server/frame_stream.rs +143 -0
- data/ext/itsi_server/src/server/http_message_types.rs +230 -0
- data/ext/itsi_server/src/server/io_stream.rs +128 -0
- data/ext/itsi_server/src/server/lifecycle_event.rs +12 -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 +93 -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 +329 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +300 -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 +188 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +168 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +183 -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 +133 -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 +122 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +407 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +155 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +54 -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 +138 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +269 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +62 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +218 -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 +14 -0
- data/ext/itsi_server/src/server/process_worker.rs +247 -0
- 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/acceptor.rs +100 -0
- data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +411 -0
- data/ext/itsi_server/src/server/serve_strategy/mod.rs +31 -0
- data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +449 -0
- data/ext/itsi_server/src/server/signal.rs +129 -0
- data/ext/itsi_server/src/server/size_limited_incoming.rs +107 -0
- data/ext/itsi_server/src/server/thread_worker.rs +504 -0
- data/ext/itsi_server/src/services/cache_store.rs +74 -0
- data/ext/itsi_server/src/services/itsi_http_service.rs +270 -0
- data/ext/itsi_server/src/services/mime_types.rs +2896 -0
- data/ext/itsi_server/src/services/mod.rs +6 -0
- data/ext/itsi_server/src/services/password_hasher.rs +89 -0
- data/ext/itsi_server/src/services/rate_limiter.rs +609 -0
- data/ext/itsi_server/src/services/static_file_server.rs +1400 -0
- data/ext/itsi_tracing/Cargo.lock +274 -0
- data/ext/itsi_tracing/Cargo.toml +17 -0
- data/ext/itsi_tracing/src/lib.rs +370 -0
- data/itsi-scheduler-100.png +0 -0
- data/lib/itsi/schedule_refinement.rb +96 -0
- data/lib/itsi/scheduler/3.1/itsi_scheduler.so +0 -0
- data/lib/itsi/scheduler/3.2/itsi_scheduler.so +0 -0
- data/lib/itsi/scheduler/3.3/itsi_scheduler.so +0 -0
- data/lib/itsi/scheduler/3.4/itsi_scheduler.so +0 -0
- data/lib/itsi/scheduler/4.0/itsi_scheduler.so +0 -0
- data/lib/itsi/scheduler/native_extension.rb +34 -0
- data/lib/itsi/scheduler/version.rb +7 -0
- data/lib/itsi/scheduler.rb +153 -0
- data/vendor/rb-sys-build/.cargo-ok +1 -0
- data/vendor/rb-sys-build/.cargo_vcs_info.json +6 -0
- data/vendor/rb-sys-build/Cargo.lock +294 -0
- data/vendor/rb-sys-build/Cargo.toml +71 -0
- data/vendor/rb-sys-build/Cargo.toml.orig +32 -0
- data/vendor/rb-sys-build/LICENSE-APACHE +190 -0
- data/vendor/rb-sys-build/LICENSE-MIT +21 -0
- data/vendor/rb-sys-build/src/bindings/sanitizer.rs +185 -0
- data/vendor/rb-sys-build/src/bindings/stable_api.rs +247 -0
- data/vendor/rb-sys-build/src/bindings/wrapper.h +71 -0
- data/vendor/rb-sys-build/src/bindings.rs +280 -0
- data/vendor/rb-sys-build/src/cc.rs +421 -0
- data/vendor/rb-sys-build/src/lib.rs +12 -0
- data/vendor/rb-sys-build/src/rb_config/flags.rs +101 -0
- data/vendor/rb-sys-build/src/rb_config/library.rs +132 -0
- data/vendor/rb-sys-build/src/rb_config/search_path.rs +57 -0
- data/vendor/rb-sys-build/src/rb_config.rs +906 -0
- data/vendor/rb-sys-build/src/utils.rs +53 -0
- metadata +210 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
use crate::{
|
|
2
|
+
server::http_message_types::{HttpBody, HttpRequest, HttpResponse},
|
|
3
|
+
services::itsi_http_service::HttpRequestContext,
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
use super::{FromValue, MiddlewareLayer};
|
|
7
|
+
use async_trait::async_trait;
|
|
8
|
+
use base64::{engine::general_purpose, Engine as _};
|
|
9
|
+
use bytes::{Bytes, BytesMut};
|
|
10
|
+
use either::Either;
|
|
11
|
+
use futures::TryStreamExt;
|
|
12
|
+
use http::{header, HeaderValue, Response, StatusCode};
|
|
13
|
+
use http_body_util::BodyExt;
|
|
14
|
+
use hyper::body::Body;
|
|
15
|
+
use magnus::error::Result;
|
|
16
|
+
use serde::Deserialize;
|
|
17
|
+
use sha2::{Digest, Sha256};
|
|
18
|
+
use tracing::debug;
|
|
19
|
+
|
|
20
|
+
#[derive(Debug, Clone, Copy, Deserialize, Default)]
|
|
21
|
+
pub enum ETagType {
|
|
22
|
+
#[serde(rename = "strong")]
|
|
23
|
+
#[default]
|
|
24
|
+
Strong,
|
|
25
|
+
#[serde(rename = "weak")]
|
|
26
|
+
Weak,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#[derive(Debug, Clone, Copy, Deserialize, Default)]
|
|
30
|
+
pub enum HashAlgorithm {
|
|
31
|
+
#[serde(rename = "sha256")]
|
|
32
|
+
#[default]
|
|
33
|
+
Sha256,
|
|
34
|
+
#[serde(rename = "md5")]
|
|
35
|
+
Md5,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
39
|
+
pub struct ETag {
|
|
40
|
+
#[serde(default)]
|
|
41
|
+
pub r#type: ETagType,
|
|
42
|
+
#[serde(default)]
|
|
43
|
+
pub algorithm: HashAlgorithm,
|
|
44
|
+
#[serde(default)]
|
|
45
|
+
pub min_body_size: usize,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#[async_trait]
|
|
49
|
+
impl MiddlewareLayer for ETag {
|
|
50
|
+
async fn before(
|
|
51
|
+
&self,
|
|
52
|
+
req: HttpRequest,
|
|
53
|
+
context: &mut HttpRequestContext,
|
|
54
|
+
) -> Result<Either<HttpRequest, HttpResponse>> {
|
|
55
|
+
// Store if-none-match header in context if present for later use in after hook
|
|
56
|
+
if let Some(if_none_match) = req.headers().get(header::IF_NONE_MATCH) {
|
|
57
|
+
debug!(target: "middleware::etag", "Received If-None-Match header: {:?}", if_none_match);
|
|
58
|
+
if let Ok(etag_value) = if_none_match.to_str() {
|
|
59
|
+
context.set_if_none_match(Some(etag_value.to_string()));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
Ok(Either::Left(req))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async fn after(&self, resp: HttpResponse, context: &mut HttpRequestContext) -> HttpResponse {
|
|
67
|
+
// Skip for error responses or responses that shouldn't have ETags
|
|
68
|
+
match resp.status() {
|
|
69
|
+
StatusCode::OK
|
|
70
|
+
| StatusCode::CREATED
|
|
71
|
+
| StatusCode::ACCEPTED
|
|
72
|
+
| StatusCode::NON_AUTHORITATIVE_INFORMATION
|
|
73
|
+
| StatusCode::NO_CONTENT
|
|
74
|
+
| StatusCode::PARTIAL_CONTENT => {}
|
|
75
|
+
_ => {
|
|
76
|
+
debug!(target: "middleware::etag", "Skipping ETag middleware for ineligible response");
|
|
77
|
+
return resp;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if let Some(cache_control) = resp.headers().get(header::CACHE_CONTROL) {
|
|
82
|
+
if let Ok(cache_control_str) = cache_control.to_str() {
|
|
83
|
+
if cache_control_str.contains("no-store") {
|
|
84
|
+
debug!(target: "middleware::etag", "Skipping ETag for no-store response");
|
|
85
|
+
return resp;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let body_size = resp.size_hint().exact();
|
|
91
|
+
|
|
92
|
+
if body_size.is_none() {
|
|
93
|
+
debug!(target: "middleware::etag", "Skipping ETag for streaming response");
|
|
94
|
+
return resp;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if body_size.unwrap_or(0) < self.min_body_size as u64 {
|
|
98
|
+
debug!(target: "middleware::etag", "Skipping ETag for small response");
|
|
99
|
+
return resp;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let (mut parts, mut body) = resp.into_parts();
|
|
103
|
+
let etag_value = if let Some(existing_etag) = parts.headers.get(header::ETAG) {
|
|
104
|
+
existing_etag.to_str().unwrap_or("").to_string()
|
|
105
|
+
} else {
|
|
106
|
+
// Get the full bytes from the body
|
|
107
|
+
let full_bytes: Bytes = match body
|
|
108
|
+
.into_data_stream()
|
|
109
|
+
.try_fold(BytesMut::new(), |mut acc, chunk| async move {
|
|
110
|
+
acc.extend_from_slice(&chunk);
|
|
111
|
+
Ok(acc)
|
|
112
|
+
})
|
|
113
|
+
.await
|
|
114
|
+
{
|
|
115
|
+
Ok(bytes_mut) => bytes_mut.freeze(),
|
|
116
|
+
Err(_) => return Response::from_parts(parts, HttpBody::empty()),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
let computed_etag = match self.algorithm {
|
|
120
|
+
HashAlgorithm::Sha256 => {
|
|
121
|
+
let mut hasher = Sha256::new();
|
|
122
|
+
hasher.update(&full_bytes);
|
|
123
|
+
let result = hasher.finalize();
|
|
124
|
+
general_purpose::STANDARD.encode(result)
|
|
125
|
+
}
|
|
126
|
+
HashAlgorithm::Md5 => {
|
|
127
|
+
let digest = md5::compute(&full_bytes);
|
|
128
|
+
format!("{:x}", digest)
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
let formatted_etag = match self.r#type {
|
|
133
|
+
ETagType::Strong => format!("\"{}\"", computed_etag),
|
|
134
|
+
ETagType::Weak => format!("W/\"{}\"", computed_etag),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
debug!(target: "middleware::etag", "Computed ETag for response {}", formatted_etag);
|
|
138
|
+
if let Ok(value) = HeaderValue::from_str(&formatted_etag) {
|
|
139
|
+
parts.headers.insert(header::ETAG, value);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
body = HttpBody::full(full_bytes);
|
|
143
|
+
formatted_etag
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if let Some(if_none_match) = context.get_if_none_match() {
|
|
147
|
+
if if_none_match == etag_value || if_none_match == "*" {
|
|
148
|
+
// Return 304 Not Modified without the body
|
|
149
|
+
let mut not_modified = Response::new(HttpBody::empty());
|
|
150
|
+
*not_modified.status_mut() = StatusCode::NOT_MODIFIED;
|
|
151
|
+
// Copy headers we want to preserve
|
|
152
|
+
for (name, value) in parts.headers.iter() {
|
|
153
|
+
if matches!(
|
|
154
|
+
name,
|
|
155
|
+
&header::CACHE_CONTROL
|
|
156
|
+
| &header::CONTENT_LOCATION
|
|
157
|
+
| &header::DATE
|
|
158
|
+
| &header::ETAG
|
|
159
|
+
| &header::EXPIRES
|
|
160
|
+
| &header::VARY
|
|
161
|
+
) {
|
|
162
|
+
not_modified.headers_mut().insert(name, value.clone());
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return not_modified;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
Response::from_parts(parts, body)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
impl Default for ETag {
|
|
174
|
+
fn default() -> Self {
|
|
175
|
+
Self {
|
|
176
|
+
r#type: ETagType::Strong,
|
|
177
|
+
algorithm: HashAlgorithm::Sha256,
|
|
178
|
+
min_body_size: 0,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
impl FromValue for ETag {}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
use http::{header::GetAll, HeaderValue};
|
|
2
|
+
|
|
3
|
+
/// Given a list of header values (which may be comma-separated and may have quality parameters)
|
|
4
|
+
/// and a list of supported items (each supported item is a full value or a prefix ending with '*'),
|
|
5
|
+
/// return Some(supported_item) for the first supported item that matches any header value, or None.
|
|
6
|
+
pub fn find_first_supported<'a, I>(header_values: &[HeaderValue], supported: I) -> Option<&'a str>
|
|
7
|
+
where
|
|
8
|
+
I: IntoIterator<Item = &'a str> + Clone,
|
|
9
|
+
{
|
|
10
|
+
// best candidate: (quality, supported_index, candidate)
|
|
11
|
+
let mut best: Option<(f32, usize, &'a str)> = None;
|
|
12
|
+
|
|
13
|
+
for value in header_values.iter() {
|
|
14
|
+
if let Ok(s) = value.to_str() {
|
|
15
|
+
for token in s.split(',') {
|
|
16
|
+
let token = token.trim();
|
|
17
|
+
if token.is_empty() {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
let mut parts = token.split(';');
|
|
21
|
+
let enc = parts.next()?.trim();
|
|
22
|
+
if enc.is_empty() {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
let quality = parts
|
|
26
|
+
.find_map(|p| {
|
|
27
|
+
let p = p.trim();
|
|
28
|
+
if let Some(q_str) = p.strip_prefix("q=") {
|
|
29
|
+
q_str.parse::<f32>().ok()
|
|
30
|
+
} else {
|
|
31
|
+
None
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
.unwrap_or(1.0);
|
|
35
|
+
|
|
36
|
+
// For each supported encoding, iterate over a clone of the iterable.
|
|
37
|
+
for (i, supp) in supported.clone().into_iter().enumerate() {
|
|
38
|
+
let is_match = if supp == "*" {
|
|
39
|
+
true
|
|
40
|
+
} else if let Some(prefix) = supp.strip_suffix('*') {
|
|
41
|
+
enc.starts_with(prefix)
|
|
42
|
+
} else {
|
|
43
|
+
enc.eq_ignore_ascii_case(supp)
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if is_match {
|
|
47
|
+
best = match best {
|
|
48
|
+
Some((best_q, best_idx, _))
|
|
49
|
+
if quality > best_q || (quality == best_q && i < best_idx) =>
|
|
50
|
+
{
|
|
51
|
+
Some((quality, i, supp))
|
|
52
|
+
}
|
|
53
|
+
None => Some((quality, i, supp)),
|
|
54
|
+
_ => best,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
best.map(|(_, _, candidate)| candidate)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
pub fn header_contains(header_values: &GetAll<HeaderValue>, needle: &str) -> bool {
|
|
66
|
+
if needle == "*" {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
let mut headers = header_values
|
|
70
|
+
.iter()
|
|
71
|
+
.flat_map(|value| value.to_str().unwrap_or("").split(','))
|
|
72
|
+
.map(|s| s.trim().split(';').next().unwrap_or(""))
|
|
73
|
+
.filter(|s| !s.is_empty());
|
|
74
|
+
|
|
75
|
+
let needle_lower = needle;
|
|
76
|
+
if needle.ends_with('*') {
|
|
77
|
+
let prefix = &needle_lower[..needle_lower.len() - 1];
|
|
78
|
+
headers.any(|h| h.starts_with(prefix))
|
|
79
|
+
} else {
|
|
80
|
+
headers.any(|h| h == needle_lower)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
use crate::server::http_message_types::{HttpRequest, HttpResponse, RequestExt};
|
|
2
|
+
use crate::services::itsi_http_service::HttpRequestContext;
|
|
3
|
+
use crate::services::rate_limiter::{
|
|
4
|
+
get_ban_manager, get_rate_limiter, BanManager, RateLimiter, RateLimiterConfig,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
use super::token_source::TokenSource;
|
|
8
|
+
use super::{ErrorResponse, FromValue, MiddlewareLayer};
|
|
9
|
+
|
|
10
|
+
use async_trait::async_trait;
|
|
11
|
+
use either::Either;
|
|
12
|
+
use itsi_tracing::*;
|
|
13
|
+
use magnus::error::Result;
|
|
14
|
+
use regex::RegexSet;
|
|
15
|
+
use serde::Deserialize;
|
|
16
|
+
use std::time::Duration;
|
|
17
|
+
use std::{
|
|
18
|
+
collections::HashMap,
|
|
19
|
+
sync::{Arc, OnceLock},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
23
|
+
pub struct IntrusionProtection {
|
|
24
|
+
#[serde(skip_deserializing)]
|
|
25
|
+
pub banned_url_pattern_matcher: OnceLock<RegexSet>,
|
|
26
|
+
#[serde(default)]
|
|
27
|
+
pub banned_url_patterns: Vec<String>,
|
|
28
|
+
#[serde(skip_deserializing)]
|
|
29
|
+
pub banned_header_pattern_matchers: OnceLock<HashMap<String, RegexSet>>,
|
|
30
|
+
#[serde(default)]
|
|
31
|
+
pub banned_header_patterns: HashMap<String, Vec<String>>,
|
|
32
|
+
pub banned_time_seconds: f64,
|
|
33
|
+
#[serde(skip_deserializing)]
|
|
34
|
+
pub rate_limiter: OnceLock<Arc<dyn RateLimiter>>,
|
|
35
|
+
#[serde(skip_deserializing)]
|
|
36
|
+
pub ban_manager: OnceLock<BanManager>,
|
|
37
|
+
pub store_config: RateLimiterConfig,
|
|
38
|
+
pub trusted_proxies: HashMap<String, TokenSource>,
|
|
39
|
+
#[serde(default = "forbidden_error_response")]
|
|
40
|
+
pub error_response: ErrorResponse,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fn forbidden_error_response() -> ErrorResponse {
|
|
44
|
+
ErrorResponse::forbidden()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[async_trait]
|
|
48
|
+
impl MiddlewareLayer for IntrusionProtection {
|
|
49
|
+
async fn initialize(&self) -> Result<()> {
|
|
50
|
+
// Initialize regex matchers for URL patterns
|
|
51
|
+
if !self.banned_url_patterns.is_empty() {
|
|
52
|
+
match RegexSet::new(&self.banned_url_patterns) {
|
|
53
|
+
Ok(regex_set) => {
|
|
54
|
+
debug!(target: "middleware::intrusion_protection", "Compiled URL regex patterns: {} items.", regex_set.len());
|
|
55
|
+
let _ = self.banned_url_pattern_matcher.set(regex_set);
|
|
56
|
+
}
|
|
57
|
+
Err(e) => {
|
|
58
|
+
error!("Failed to compile URL regex patterns: {:?}", e);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Initialize regex matchers for header patterns
|
|
64
|
+
if !self.banned_header_patterns.is_empty() {
|
|
65
|
+
let mut header_matchers = HashMap::new();
|
|
66
|
+
for (header_name, patterns) in &self.banned_header_patterns {
|
|
67
|
+
if !patterns.is_empty() {
|
|
68
|
+
match RegexSet::new(patterns) {
|
|
69
|
+
Ok(regex_set) => {
|
|
70
|
+
debug!(target: "middleware::intrusion_protection", "Compiled header regex patterns for {}: {} items.", header_name, regex_set.len());
|
|
71
|
+
header_matchers.insert(header_name.clone(), regex_set);
|
|
72
|
+
}
|
|
73
|
+
Err(e) => {
|
|
74
|
+
error!(
|
|
75
|
+
"Failed to compile header regex patterns for {}: {:?}",
|
|
76
|
+
header_name, e
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
let _ = self.banned_header_pattern_matchers.set(header_matchers);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Initialize rate limiter (used for tracking bans)
|
|
86
|
+
// This will automatically fall back to in-memory if Redis fails
|
|
87
|
+
if let Ok(limiter) = get_rate_limiter(&self.store_config).await {
|
|
88
|
+
debug!(target: "middleware::intrusion_protection", "Initialized rate limiter.");
|
|
89
|
+
let _ = self.rate_limiter.set(limiter);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Initialize ban manager
|
|
93
|
+
// This will automatically fall back to in-memory if Redis fails
|
|
94
|
+
if let Ok(manager) = get_ban_manager(&self.store_config).await {
|
|
95
|
+
debug!(target: "middleware::intrusion_protection", "Initialized ban manager.");
|
|
96
|
+
let _ = self.ban_manager.set(manager);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
Ok(())
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async fn before(
|
|
103
|
+
&self,
|
|
104
|
+
req: HttpRequest,
|
|
105
|
+
context: &mut HttpRequestContext,
|
|
106
|
+
) -> Result<Either<HttpRequest, HttpResponse>> {
|
|
107
|
+
// Get client IP address from context's service
|
|
108
|
+
let client_ip = if self.trusted_proxies.contains_key(&context.addr) {
|
|
109
|
+
let source = self.trusted_proxies.get(&context.addr).unwrap();
|
|
110
|
+
source.extract_token(&req).unwrap_or(&context.addr)
|
|
111
|
+
} else {
|
|
112
|
+
&context.addr
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Check if the IP is already banned
|
|
116
|
+
if let Some(ban_manager) = self.ban_manager.get() {
|
|
117
|
+
match ban_manager.is_banned(client_ip).await {
|
|
118
|
+
Ok(Some(_)) => {
|
|
119
|
+
debug!(target: "middleware::intrusion_protection", "IP {} is banned.", client_ip);
|
|
120
|
+
return Ok(Either::Right(
|
|
121
|
+
self.error_response
|
|
122
|
+
.to_http_response(req.accept().into())
|
|
123
|
+
.await,
|
|
124
|
+
));
|
|
125
|
+
}
|
|
126
|
+
Err(e) => {
|
|
127
|
+
error!("Error checking IP ban status: {:?}", e);
|
|
128
|
+
// Continue processing - fail open
|
|
129
|
+
}
|
|
130
|
+
_ => {}
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
warn!("No ban manager available for intrusion protection");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check for banned URL patterns
|
|
137
|
+
if let Some(url_matcher) = self.banned_url_pattern_matcher.get() {
|
|
138
|
+
let path = req.uri().path_and_query().map(|p| p.as_str()).unwrap_or("");
|
|
139
|
+
|
|
140
|
+
if url_matcher.is_match(path) {
|
|
141
|
+
debug!(target: "middleware::intrusion_protection", "Banned URL pattern detected: {}", path);
|
|
142
|
+
if let Some(ban_manager) = self.ban_manager.get() {
|
|
143
|
+
match ban_manager
|
|
144
|
+
.ban_ip(
|
|
145
|
+
client_ip,
|
|
146
|
+
&format!("Banned URL pattern detected: {}", path),
|
|
147
|
+
Duration::from_secs_f64(self.banned_time_seconds),
|
|
148
|
+
)
|
|
149
|
+
.await
|
|
150
|
+
{
|
|
151
|
+
Ok(_) => {}
|
|
152
|
+
Err(e) => error!("Failed to ban IP {}: {:?}", client_ip, e),
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Always return the error response even if banning failed
|
|
157
|
+
return Ok(Either::Right(
|
|
158
|
+
self.error_response
|
|
159
|
+
.to_http_response(req.accept().into())
|
|
160
|
+
.await,
|
|
161
|
+
));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check for banned header patterns
|
|
166
|
+
if let Some(header_matchers) = self.banned_header_pattern_matchers.get() {
|
|
167
|
+
for (header_name, pattern_set) in header_matchers {
|
|
168
|
+
if let Some(header_value) = req.header(header_name) {
|
|
169
|
+
if pattern_set.is_match(header_value) {
|
|
170
|
+
debug!(target: "middleware::intrusion_protection", "Banned header pattern detected: {} in {}", header_value, header_name);
|
|
171
|
+
|
|
172
|
+
// Ban the IP address if possible
|
|
173
|
+
if let Some(ban_manager) = self.ban_manager.get() {
|
|
174
|
+
match ban_manager
|
|
175
|
+
.ban_ip(
|
|
176
|
+
client_ip,
|
|
177
|
+
&format!(
|
|
178
|
+
"Banned header pattern detected: {} in {}",
|
|
179
|
+
header_value, header_name
|
|
180
|
+
),
|
|
181
|
+
Duration::from_secs_f64(self.banned_time_seconds),
|
|
182
|
+
)
|
|
183
|
+
.await
|
|
184
|
+
{
|
|
185
|
+
Ok(_) => info!(
|
|
186
|
+
"Successfully banned IP {} for {} seconds",
|
|
187
|
+
client_ip, self.banned_time_seconds
|
|
188
|
+
),
|
|
189
|
+
Err(e) => error!("Failed to ban IP {}: {:?}", client_ip, e),
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Always return the error response even if banning failed
|
|
194
|
+
return Ok(Either::Right(
|
|
195
|
+
self.error_response
|
|
196
|
+
.to_http_response(req.accept().into())
|
|
197
|
+
.await,
|
|
198
|
+
));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// No intrusion detected
|
|
205
|
+
Ok(Either::Left(req))
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
impl FromValue for IntrusionProtection {}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
use async_trait::async_trait;
|
|
2
|
+
use either::Either;
|
|
3
|
+
use itsi_tracing::*;
|
|
4
|
+
use magnus::error::Result;
|
|
5
|
+
use serde::Deserialize;
|
|
6
|
+
use tracing::enabled;
|
|
7
|
+
|
|
8
|
+
use crate::server::http_message_types::{HttpRequest, HttpResponse};
|
|
9
|
+
use crate::services::itsi_http_service::HttpRequestContext;
|
|
10
|
+
|
|
11
|
+
use super::string_rewrite::StringRewrite;
|
|
12
|
+
use super::{FromValue, MiddlewareLayer};
|
|
13
|
+
|
|
14
|
+
/// Logging middleware for HTTP requests and responses
|
|
15
|
+
///
|
|
16
|
+
/// Supports customizable log formats with placeholders
|
|
17
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
18
|
+
pub struct LogRequests {
|
|
19
|
+
pub before: Option<LogConfig>,
|
|
20
|
+
pub after: Option<LogConfig>,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
24
|
+
pub struct LogConfig {
|
|
25
|
+
level: LogMiddlewareLevel,
|
|
26
|
+
format: StringRewrite,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
30
|
+
pub enum LogMiddlewareLevel {
|
|
31
|
+
#[serde(rename(deserialize = "INFO"))]
|
|
32
|
+
Info,
|
|
33
|
+
#[serde(rename(deserialize = "TRACE"))]
|
|
34
|
+
Trace,
|
|
35
|
+
#[serde(rename(deserialize = "DEBUG"))]
|
|
36
|
+
Debug,
|
|
37
|
+
#[serde(rename(deserialize = "WARN"))]
|
|
38
|
+
Warn,
|
|
39
|
+
#[serde(rename(deserialize = "ERROR"))]
|
|
40
|
+
Error,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[async_trait]
|
|
44
|
+
impl MiddlewareLayer for LogRequests {
|
|
45
|
+
async fn initialize(&self) -> Result<()> {
|
|
46
|
+
Ok(())
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async fn before(
|
|
50
|
+
&self,
|
|
51
|
+
req: HttpRequest,
|
|
52
|
+
context: &mut HttpRequestContext,
|
|
53
|
+
) -> Result<Either<HttpRequest, HttpResponse>> {
|
|
54
|
+
context.init_logging_params();
|
|
55
|
+
if let Some(LogConfig { level, format }) = self.before.as_ref() {
|
|
56
|
+
match level {
|
|
57
|
+
LogMiddlewareLevel::Trace => {
|
|
58
|
+
if enabled!(target: "middleware::log_requests", tracing::Level::TRACE) {
|
|
59
|
+
let message = format.rewrite_request(&req, context);
|
|
60
|
+
trace!(target: "middleware::log_requests", message);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
LogMiddlewareLevel::Debug => {
|
|
64
|
+
if enabled!(target: "middleware::log_requests", tracing::Level::DEBUG) {
|
|
65
|
+
let message = format.rewrite_request(&req, context);
|
|
66
|
+
debug!(target: "middleware::log_requests", message);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
LogMiddlewareLevel::Info => {
|
|
70
|
+
if enabled!(target: "middleware::log_requests", tracing::Level::INFO) {
|
|
71
|
+
let message = format.rewrite_request(&req, context);
|
|
72
|
+
info!(target: "middleware::log_requests", message);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
LogMiddlewareLevel::Warn => {
|
|
76
|
+
if enabled!(target: "middleware::log_requests", tracing::Level::WARN) {
|
|
77
|
+
let message = format.rewrite_request(&req, context);
|
|
78
|
+
warn!(target: "middleware::log_requests", message);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
LogMiddlewareLevel::Error => {
|
|
82
|
+
if enabled!(target: "middleware::log_requests", tracing::Level::ERROR) {
|
|
83
|
+
let message = format.rewrite_request(&req, context);
|
|
84
|
+
error!(target: "middleware::log_requests", message);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
Ok(Either::Left(req))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async fn after(&self, resp: HttpResponse, context: &mut HttpRequestContext) -> HttpResponse {
|
|
94
|
+
if let Some(LogConfig { level, format }) = self.after.as_ref() {
|
|
95
|
+
match level {
|
|
96
|
+
LogMiddlewareLevel::Trace => {
|
|
97
|
+
if enabled!(target: "middleware::log_requests", tracing::Level::TRACE) {
|
|
98
|
+
let message = format.rewrite_response(&resp, context);
|
|
99
|
+
trace!(target: "middleware::log_requests", message);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
LogMiddlewareLevel::Debug => {
|
|
103
|
+
if enabled!(target: "middleware::log_requests", tracing::Level::DEBUG) {
|
|
104
|
+
let message = format.rewrite_response(&resp, context);
|
|
105
|
+
debug!(target: "middleware::log_requests", message);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
LogMiddlewareLevel::Info => {
|
|
109
|
+
if enabled!(target: "middleware::log_requests", tracing::Level::INFO) {
|
|
110
|
+
let message = format.rewrite_response(&resp, context);
|
|
111
|
+
info!(target: "middleware::log_requests", message);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
LogMiddlewareLevel::Warn => {
|
|
115
|
+
if enabled!(target: "middleware::log_requests", tracing::Level::WARN) {
|
|
116
|
+
let message = format.rewrite_response(&resp, context);
|
|
117
|
+
warn!(target: "middleware::log_requests", message);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
LogMiddlewareLevel::Error => {
|
|
121
|
+
if enabled!(target: "middleware::log_requests", tracing::Level::ERROR) {
|
|
122
|
+
let message = format.rewrite_response(&resp, context);
|
|
123
|
+
error!(target: "middleware::log_requests", message);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
resp
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
impl FromValue for LogRequests {}
|
|
@@ -0,0 +1,47 @@
|
|
|
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 http::StatusCode;
|
|
10
|
+
use magnus::error::Result;
|
|
11
|
+
use serde::Deserialize;
|
|
12
|
+
use std::sync::atomic::Ordering;
|
|
13
|
+
|
|
14
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
15
|
+
pub struct MaxBody {
|
|
16
|
+
pub limit_bytes: usize,
|
|
17
|
+
#[serde(default = "payload_too_large_error_response")]
|
|
18
|
+
pub error_response: ErrorResponse,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fn payload_too_large_error_response() -> ErrorResponse {
|
|
22
|
+
ErrorResponse::payload_too_large()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#[async_trait]
|
|
26
|
+
impl MiddlewareLayer for MaxBody {
|
|
27
|
+
async fn before(
|
|
28
|
+
&self,
|
|
29
|
+
req: HttpRequest,
|
|
30
|
+
context: &mut HttpRequestContext,
|
|
31
|
+
) -> Result<Either<HttpRequest, HttpResponse>> {
|
|
32
|
+
req.body().limit.store(self.limit_bytes, Ordering::Relaxed);
|
|
33
|
+
context.set_response_format(req.accept().into());
|
|
34
|
+
Ok(Either::Left(req))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async fn after(&self, resp: HttpResponse, context: &mut HttpRequestContext) -> HttpResponse {
|
|
38
|
+
if resp.status() == StatusCode::PAYLOAD_TOO_LARGE {
|
|
39
|
+
self.error_response
|
|
40
|
+
.to_http_response(*context.response_format())
|
|
41
|
+
.await
|
|
42
|
+
} else {
|
|
43
|
+
resp
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
impl FromValue for MaxBody {}
|