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,72 @@
|
|
1
|
+
use super::MiddlewareLayer;
|
2
|
+
use crate::{
|
3
|
+
ruby_types::itsi_grpc_request::ItsiGrpcRequest,
|
4
|
+
server::{
|
5
|
+
itsi_service::RequestContext,
|
6
|
+
types::{HttpRequest, HttpResponse},
|
7
|
+
},
|
8
|
+
};
|
9
|
+
use async_trait::async_trait;
|
10
|
+
use derive_more::Debug;
|
11
|
+
use either::Either;
|
12
|
+
use http::StatusCode;
|
13
|
+
use itsi_rb_helpers::{HeapVal, HeapValue};
|
14
|
+
use magnus::{block::Proc, error::Result, value::ReprValue, Symbol, Value};
|
15
|
+
use std::sync::Arc;
|
16
|
+
|
17
|
+
#[derive(Debug)]
|
18
|
+
pub struct GrpcService {
|
19
|
+
service: Arc<HeapValue<Proc>>,
|
20
|
+
adapter: Value, // Ruby CustomGrpcAdapter object
|
21
|
+
}
|
22
|
+
|
23
|
+
impl GrpcService {
|
24
|
+
pub fn from_value(params: HeapVal) -> magnus::error::Result<Self> {
|
25
|
+
let service = params.funcall::<_, _, Proc>(Symbol::new("[]"), ("service_proc",))?;
|
26
|
+
let adapter = params.funcall::<_, _, Value>(Symbol::new("[]"), ("adapter",))?;
|
27
|
+
Ok(GrpcService {
|
28
|
+
service: Arc::new(service.into()),
|
29
|
+
adapter,
|
30
|
+
})
|
31
|
+
}
|
32
|
+
}
|
33
|
+
|
34
|
+
#[async_trait]
|
35
|
+
impl MiddlewareLayer for GrpcService {
|
36
|
+
async fn before(
|
37
|
+
&self,
|
38
|
+
req: HttpRequest,
|
39
|
+
context: &mut RequestContext,
|
40
|
+
) -> Result<Either<HttpRequest, HttpResponse>> {
|
41
|
+
// Extract gRPC method and service names from the path
|
42
|
+
let path = req.uri().path();
|
43
|
+
let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
|
44
|
+
|
45
|
+
if parts.len() < 2 {
|
46
|
+
return Ok(Either::Right(HttpResponse::new(StatusCode::BAD_REQUEST)));
|
47
|
+
}
|
48
|
+
|
49
|
+
let service_name = parts[0].to_string();
|
50
|
+
let method_name = parts[1].to_string();
|
51
|
+
|
52
|
+
// Get RPC descriptor from the adapter
|
53
|
+
let rpc_desc = self.adapter.funcall::<_, _, Option<Value>>(
|
54
|
+
"get_rpc_desc",
|
55
|
+
(service_name.clone(), method_name.clone()),
|
56
|
+
)?;
|
57
|
+
|
58
|
+
// Create gRPC request and process it
|
59
|
+
let (grpc_req, _) = ItsiGrpcRequest::new(
|
60
|
+
req,
|
61
|
+
context,
|
62
|
+
method_name,
|
63
|
+
service_name,
|
64
|
+
rpc_desc,
|
65
|
+
).await;
|
66
|
+
|
67
|
+
grpc_req
|
68
|
+
.process(context.ruby(), self.service.clone())
|
69
|
+
.map_err(|e| e.into())
|
70
|
+
.map(|_| Either::Right(HttpResponse::new(StatusCode::OK)))
|
71
|
+
}
|
72
|
+
}
|
@@ -0,0 +1,85 @@
|
|
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>(
|
7
|
+
header_values: &http::header::GetAll<http::HeaderValue>,
|
8
|
+
supported: I,
|
9
|
+
) -> Option<&'a str>
|
10
|
+
where
|
11
|
+
I: IntoIterator<Item = &'a str> + Clone,
|
12
|
+
{
|
13
|
+
// best candidate: (quality, supported_index, candidate)
|
14
|
+
let mut best: Option<(f32, usize, &'a str)> = None;
|
15
|
+
|
16
|
+
for value in header_values.iter() {
|
17
|
+
if let Ok(s) = value.to_str() {
|
18
|
+
for token in s.split(',') {
|
19
|
+
let token = token.trim();
|
20
|
+
if token.is_empty() {
|
21
|
+
continue;
|
22
|
+
}
|
23
|
+
let mut parts = token.split(';');
|
24
|
+
let enc = parts.next()?.trim();
|
25
|
+
if enc.is_empty() {
|
26
|
+
continue;
|
27
|
+
}
|
28
|
+
let quality = parts
|
29
|
+
.find_map(|p| {
|
30
|
+
let p = p.trim();
|
31
|
+
if let Some(q_str) = p.strip_prefix("q=") {
|
32
|
+
q_str.parse::<f32>().ok()
|
33
|
+
} else {
|
34
|
+
None
|
35
|
+
}
|
36
|
+
})
|
37
|
+
.unwrap_or(1.0);
|
38
|
+
|
39
|
+
// For each supported encoding, iterate over a clone of the iterable.
|
40
|
+
for (i, supp) in supported.clone().into_iter().enumerate() {
|
41
|
+
let is_match = if supp == "*" {
|
42
|
+
true
|
43
|
+
} else if let Some(prefix) = supp.strip_suffix('*') {
|
44
|
+
enc.starts_with(prefix)
|
45
|
+
} else {
|
46
|
+
enc.eq_ignore_ascii_case(supp)
|
47
|
+
};
|
48
|
+
|
49
|
+
if is_match {
|
50
|
+
best = match best {
|
51
|
+
Some((best_q, best_idx, _))
|
52
|
+
if quality > best_q || (quality == best_q && i < best_idx) =>
|
53
|
+
{
|
54
|
+
Some((quality, i, supp))
|
55
|
+
}
|
56
|
+
None => Some((quality, i, supp)),
|
57
|
+
_ => best,
|
58
|
+
};
|
59
|
+
}
|
60
|
+
}
|
61
|
+
}
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
best.map(|(_, _, candidate)| candidate)
|
66
|
+
}
|
67
|
+
|
68
|
+
pub fn header_contains(header_values: &GetAll<HeaderValue>, needle: &str) -> bool {
|
69
|
+
if needle == "*" {
|
70
|
+
return true;
|
71
|
+
}
|
72
|
+
let mut headers = header_values
|
73
|
+
.iter()
|
74
|
+
.flat_map(|value| value.to_str().unwrap_or("").split(','))
|
75
|
+
.map(|s| s.trim().split(';').next().unwrap_or(""))
|
76
|
+
.filter(|s| !s.is_empty());
|
77
|
+
|
78
|
+
let needle_lower = needle;
|
79
|
+
if needle.ends_with('*') {
|
80
|
+
let prefix = &needle_lower[..needle_lower.len() - 1];
|
81
|
+
headers.any(|h| h.starts_with(prefix))
|
82
|
+
} else {
|
83
|
+
headers.any(|h| h == needle_lower)
|
84
|
+
}
|
85
|
+
}
|
@@ -0,0 +1,195 @@
|
|
1
|
+
use super::{ErrorResponse, FromValue, MiddlewareLayer};
|
2
|
+
use crate::server::{
|
3
|
+
itsi_service::RequestContext,
|
4
|
+
rate_limiter::{get_ban_manager, get_rate_limiter, BanManager, RateLimiter, RateLimiterConfig},
|
5
|
+
types::{HttpRequest, HttpResponse, RequestExt},
|
6
|
+
};
|
7
|
+
use async_trait::async_trait;
|
8
|
+
use either::Either;
|
9
|
+
use itsi_tracing::*;
|
10
|
+
use magnus::error::Result;
|
11
|
+
use regex::RegexSet;
|
12
|
+
use serde::Deserialize;
|
13
|
+
use std::time::Duration;
|
14
|
+
use std::{
|
15
|
+
collections::HashMap,
|
16
|
+
sync::{Arc, OnceLock},
|
17
|
+
};
|
18
|
+
|
19
|
+
#[derive(Debug, Clone, Deserialize)]
|
20
|
+
pub struct IntrusionProtection {
|
21
|
+
#[serde(skip_deserializing)]
|
22
|
+
pub banned_url_pattern_matcher: OnceLock<RegexSet>,
|
23
|
+
#[serde(default)]
|
24
|
+
pub banned_url_patterns: Vec<String>,
|
25
|
+
#[serde(skip_deserializing)]
|
26
|
+
pub banned_header_pattern_matchers: OnceLock<HashMap<String, RegexSet>>,
|
27
|
+
#[serde(default)]
|
28
|
+
pub banned_header_patterns: HashMap<String, Vec<String>>,
|
29
|
+
pub banned_time_seconds: u64,
|
30
|
+
#[serde(skip_deserializing)]
|
31
|
+
pub rate_limiter: OnceLock<Arc<dyn RateLimiter>>,
|
32
|
+
#[serde(skip_deserializing)]
|
33
|
+
pub ban_manager: OnceLock<BanManager>,
|
34
|
+
pub store_config: RateLimiterConfig,
|
35
|
+
pub error_response: ErrorResponse,
|
36
|
+
}
|
37
|
+
|
38
|
+
#[async_trait]
|
39
|
+
impl MiddlewareLayer for IntrusionProtection {
|
40
|
+
async fn initialize(&self) -> Result<()> {
|
41
|
+
// Initialize regex matchers for URL patterns
|
42
|
+
if !self.banned_url_patterns.is_empty() {
|
43
|
+
match RegexSet::new(&self.banned_url_patterns) {
|
44
|
+
Ok(regex_set) => {
|
45
|
+
let _ = self.banned_url_pattern_matcher.set(regex_set);
|
46
|
+
}
|
47
|
+
Err(e) => {
|
48
|
+
error!("Failed to compile URL regex patterns: {:?}", e);
|
49
|
+
}
|
50
|
+
}
|
51
|
+
}
|
52
|
+
|
53
|
+
// Initialize regex matchers for header patterns
|
54
|
+
if !self.banned_header_patterns.is_empty() {
|
55
|
+
let mut header_matchers = HashMap::new();
|
56
|
+
for (header_name, patterns) in &self.banned_header_patterns {
|
57
|
+
if !patterns.is_empty() {
|
58
|
+
match RegexSet::new(patterns) {
|
59
|
+
Ok(regex_set) => {
|
60
|
+
header_matchers.insert(header_name.clone(), regex_set);
|
61
|
+
}
|
62
|
+
Err(e) => {
|
63
|
+
error!(
|
64
|
+
"Failed to compile header regex patterns for {}: {:?}",
|
65
|
+
header_name, e
|
66
|
+
);
|
67
|
+
}
|
68
|
+
}
|
69
|
+
}
|
70
|
+
}
|
71
|
+
let _ = self.banned_header_pattern_matchers.set(header_matchers);
|
72
|
+
}
|
73
|
+
|
74
|
+
// Initialize rate limiter (used for tracking bans)
|
75
|
+
// This will automatically fall back to in-memory if Redis fails
|
76
|
+
if let Ok(limiter) = get_rate_limiter(&self.store_config).await {
|
77
|
+
let _ = self.rate_limiter.set(limiter);
|
78
|
+
}
|
79
|
+
|
80
|
+
// Initialize ban manager
|
81
|
+
// This will automatically fall back to in-memory if Redis fails
|
82
|
+
if let Ok(manager) = get_ban_manager(&self.store_config).await {
|
83
|
+
let _ = self.ban_manager.set(manager);
|
84
|
+
}
|
85
|
+
|
86
|
+
Ok(())
|
87
|
+
}
|
88
|
+
|
89
|
+
async fn before(
|
90
|
+
&self,
|
91
|
+
req: HttpRequest,
|
92
|
+
context: &mut RequestContext,
|
93
|
+
) -> Result<Either<HttpRequest, HttpResponse>> {
|
94
|
+
// Get client IP address from context's service
|
95
|
+
let client_ip = &context.addr;
|
96
|
+
|
97
|
+
// Check if the IP is already banned
|
98
|
+
if let Some(ban_manager) = self.ban_manager.get() {
|
99
|
+
match ban_manager.is_banned(client_ip).await {
|
100
|
+
Ok(Some(reason)) => {
|
101
|
+
info!("Request from banned IP {}: {}", client_ip, reason);
|
102
|
+
return Ok(Either::Right(
|
103
|
+
self.error_response.to_http_response(&req).await,
|
104
|
+
));
|
105
|
+
}
|
106
|
+
Err(e) => {
|
107
|
+
error!("Error checking IP ban status: {:?}", e);
|
108
|
+
// Continue processing - fail open
|
109
|
+
}
|
110
|
+
_ => {
|
111
|
+
// Not banned, continue with intrusion checks
|
112
|
+
}
|
113
|
+
}
|
114
|
+
} else {
|
115
|
+
warn!("No ban manager available for intrusion protection");
|
116
|
+
}
|
117
|
+
|
118
|
+
// Check for banned URL patterns
|
119
|
+
if let Some(url_matcher) = self.banned_url_pattern_matcher.get() {
|
120
|
+
let path = req.uri().path_and_query().map(|p| p.as_str()).unwrap_or("");
|
121
|
+
info!("Checking URL pattern match for {}", path);
|
122
|
+
if url_matcher.is_match(path) {
|
123
|
+
info!("Intrusion detected: URL pattern match for {}", path);
|
124
|
+
|
125
|
+
// Ban the IP address if possible
|
126
|
+
if let Some(ban_manager) = self.ban_manager.get() {
|
127
|
+
match ban_manager
|
128
|
+
.ban_ip(
|
129
|
+
client_ip,
|
130
|
+
&format!("Banned URL pattern detected: {}", path),
|
131
|
+
Duration::from_secs(self.banned_time_seconds),
|
132
|
+
)
|
133
|
+
.await
|
134
|
+
{
|
135
|
+
Ok(_) => info!(
|
136
|
+
"Successfully banned IP {} for {} seconds",
|
137
|
+
client_ip, self.banned_time_seconds
|
138
|
+
),
|
139
|
+
Err(e) => error!("Failed to ban IP {}: {:?}", client_ip, e),
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
143
|
+
// Always return the error response even if banning failed
|
144
|
+
return Ok(Either::Right(
|
145
|
+
self.error_response.to_http_response(&req).await,
|
146
|
+
));
|
147
|
+
}
|
148
|
+
}
|
149
|
+
|
150
|
+
// Check for banned header patterns
|
151
|
+
if let Some(header_matchers) = self.banned_header_pattern_matchers.get() {
|
152
|
+
for (header_name, pattern_set) in header_matchers {
|
153
|
+
if let Some(header_value) = req.header(header_name) {
|
154
|
+
if pattern_set.is_match(header_value) {
|
155
|
+
info!(
|
156
|
+
"Intrusion detected: Header pattern match for {} in header {}",
|
157
|
+
header_value, header_name
|
158
|
+
);
|
159
|
+
|
160
|
+
// Ban the IP address if possible
|
161
|
+
if let Some(ban_manager) = self.ban_manager.get() {
|
162
|
+
match ban_manager
|
163
|
+
.ban_ip(
|
164
|
+
client_ip,
|
165
|
+
&format!(
|
166
|
+
"Banned header pattern detected: {} in {}",
|
167
|
+
header_value, header_name
|
168
|
+
),
|
169
|
+
Duration::from_secs(self.banned_time_seconds),
|
170
|
+
)
|
171
|
+
.await
|
172
|
+
{
|
173
|
+
Ok(_) => info!(
|
174
|
+
"Successfully banned IP {} for {} seconds",
|
175
|
+
client_ip, self.banned_time_seconds
|
176
|
+
),
|
177
|
+
Err(e) => error!("Failed to ban IP {}: {:?}", client_ip, e),
|
178
|
+
}
|
179
|
+
}
|
180
|
+
|
181
|
+
// Always return the error response even if banning failed
|
182
|
+
return Ok(Either::Right(
|
183
|
+
self.error_response.to_http_response(&req).await,
|
184
|
+
));
|
185
|
+
}
|
186
|
+
}
|
187
|
+
}
|
188
|
+
}
|
189
|
+
|
190
|
+
// No intrusion detected
|
191
|
+
Ok(Either::Left(req))
|
192
|
+
}
|
193
|
+
}
|
194
|
+
|
195
|
+
impl FromValue for IntrusionProtection {}
|
@@ -0,0 +1,82 @@
|
|
1
|
+
use async_trait::async_trait;
|
2
|
+
use either::Either;
|
3
|
+
use itsi_tracing::*;
|
4
|
+
use magnus::error::Result;
|
5
|
+
use serde::Deserialize;
|
6
|
+
|
7
|
+
use crate::server::itsi_service::RequestContext;
|
8
|
+
use crate::server::types::{HttpRequest, HttpResponse};
|
9
|
+
|
10
|
+
use super::string_rewrite::StringRewrite;
|
11
|
+
use super::{FromValue, MiddlewareLayer};
|
12
|
+
|
13
|
+
/// Logging middleware for HTTP requests and responses
|
14
|
+
///
|
15
|
+
/// Supports customizable log formats with placeholders
|
16
|
+
#[derive(Debug, Clone, Deserialize)]
|
17
|
+
pub struct LogRequests {
|
18
|
+
pub before: Option<LogConfig>,
|
19
|
+
pub after: Option<LogConfig>,
|
20
|
+
}
|
21
|
+
|
22
|
+
#[derive(Debug, Clone, Deserialize)]
|
23
|
+
pub struct LogConfig {
|
24
|
+
level: LogMiddlewareLevel,
|
25
|
+
format: StringRewrite,
|
26
|
+
}
|
27
|
+
|
28
|
+
#[derive(Debug, Clone, Deserialize)]
|
29
|
+
pub enum LogMiddlewareLevel {
|
30
|
+
#[serde(rename(deserialize = "INFO"))]
|
31
|
+
Info,
|
32
|
+
#[serde(rename(deserialize = "TRACE"))]
|
33
|
+
Trace,
|
34
|
+
#[serde(rename(deserialize = "DEBUG"))]
|
35
|
+
Debug,
|
36
|
+
#[serde(rename(deserialize = "WARN"))]
|
37
|
+
Warn,
|
38
|
+
#[serde(rename(deserialize = "ERROR"))]
|
39
|
+
Error,
|
40
|
+
}
|
41
|
+
|
42
|
+
impl LogMiddlewareLevel {
|
43
|
+
pub fn log(&self, message: String) {
|
44
|
+
match self {
|
45
|
+
LogMiddlewareLevel::Trace => trace!(message),
|
46
|
+
LogMiddlewareLevel::Debug => debug!(message),
|
47
|
+
LogMiddlewareLevel::Info => info!(message),
|
48
|
+
LogMiddlewareLevel::Warn => warn!(message),
|
49
|
+
LogMiddlewareLevel::Error => error!(message),
|
50
|
+
}
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
#[async_trait]
|
55
|
+
impl MiddlewareLayer for LogRequests {
|
56
|
+
async fn initialize(&self) -> Result<()> {
|
57
|
+
Ok(())
|
58
|
+
}
|
59
|
+
|
60
|
+
async fn before(
|
61
|
+
&self,
|
62
|
+
req: HttpRequest,
|
63
|
+
context: &mut RequestContext,
|
64
|
+
) -> Result<Either<HttpRequest, HttpResponse>> {
|
65
|
+
context.track_start_time();
|
66
|
+
if let Some(LogConfig { level, format }) = self.before.as_ref() {
|
67
|
+
level.log(format.rewrite_request(&req, context));
|
68
|
+
}
|
69
|
+
|
70
|
+
Ok(Either::Left(req))
|
71
|
+
}
|
72
|
+
|
73
|
+
async fn after(&self, resp: HttpResponse, context: &mut RequestContext) -> HttpResponse {
|
74
|
+
if let Some(LogConfig { level, format }) = self.after.as_ref() {
|
75
|
+
level.log(format.rewrite_response(&resp, context));
|
76
|
+
}
|
77
|
+
|
78
|
+
resp
|
79
|
+
}
|
80
|
+
}
|
81
|
+
|
82
|
+
impl FromValue for LogRequests {}
|
@@ -0,0 +1,82 @@
|
|
1
|
+
mod allow_list;
|
2
|
+
mod auth_api_key;
|
3
|
+
mod auth_basic;
|
4
|
+
mod auth_jwt;
|
5
|
+
mod cache_control;
|
6
|
+
mod compression;
|
7
|
+
mod cors;
|
8
|
+
mod deny_list;
|
9
|
+
mod error_response;
|
10
|
+
mod etag;
|
11
|
+
mod header_interpretation;
|
12
|
+
mod intrusion_protection;
|
13
|
+
mod log_requests;
|
14
|
+
mod proxy;
|
15
|
+
mod rate_limit;
|
16
|
+
mod redirect;
|
17
|
+
mod request_headers;
|
18
|
+
mod response_headers;
|
19
|
+
mod ruby_app;
|
20
|
+
mod static_assets;
|
21
|
+
mod string_rewrite;
|
22
|
+
mod token_source;
|
23
|
+
|
24
|
+
pub use allow_list::AllowList;
|
25
|
+
use async_trait::async_trait;
|
26
|
+
pub use auth_api_key::AuthAPIKey;
|
27
|
+
pub use auth_basic::AuthBasic;
|
28
|
+
pub use auth_jwt::AuthJwt;
|
29
|
+
pub use cache_control::CacheControl;
|
30
|
+
pub use compression::Compression;
|
31
|
+
pub use compression::CompressionAlgorithm;
|
32
|
+
pub use cors::Cors;
|
33
|
+
pub use deny_list::DenyList;
|
34
|
+
use either::Either;
|
35
|
+
pub use error_response::ErrorResponse;
|
36
|
+
pub use etag::ETag;
|
37
|
+
pub use intrusion_protection::IntrusionProtection;
|
38
|
+
pub use log_requests::LogRequests;
|
39
|
+
use magnus::error::Result;
|
40
|
+
use magnus::Value;
|
41
|
+
pub use proxy::Proxy;
|
42
|
+
pub use rate_limit::RateLimit;
|
43
|
+
pub use redirect::Redirect;
|
44
|
+
pub use request_headers::RequestHeaders;
|
45
|
+
pub use response_headers::ResponseHeaders;
|
46
|
+
pub use ruby_app::RubyApp;
|
47
|
+
use serde::Deserialize;
|
48
|
+
use serde_magnus::deserialize;
|
49
|
+
pub use static_assets::StaticAssets;
|
50
|
+
|
51
|
+
use crate::server::itsi_service::RequestContext;
|
52
|
+
use crate::server::types::{HttpRequest, HttpResponse};
|
53
|
+
|
54
|
+
pub trait FromValue: Sized + Send + Sync + 'static {
|
55
|
+
fn from_value(value: Value) -> Result<Self>
|
56
|
+
where
|
57
|
+
Self: Deserialize<'static>,
|
58
|
+
{
|
59
|
+
deserialize(value)
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
#[async_trait]
|
64
|
+
pub trait MiddlewareLayer: Sized + Send + Sync + 'static {
|
65
|
+
/// Called just once, to initialize the middleware state.
|
66
|
+
async fn initialize(&self) -> Result<()> {
|
67
|
+
Ok(())
|
68
|
+
}
|
69
|
+
/// The "before" hook. By default, it passes through the request.
|
70
|
+
async fn before(
|
71
|
+
&self,
|
72
|
+
req: HttpRequest,
|
73
|
+
_context: &mut RequestContext,
|
74
|
+
) -> Result<Either<HttpRequest, HttpResponse>> {
|
75
|
+
Ok(Either::Left(req))
|
76
|
+
}
|
77
|
+
|
78
|
+
/// The "after" hook. By default, it passes through the response.
|
79
|
+
async fn after(&self, resp: HttpResponse, _context: &mut RequestContext) -> HttpResponse {
|
80
|
+
resp
|
81
|
+
}
|
82
|
+
}
|