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,162 @@
|
|
1
|
+
use super::{FromValue, MiddlewareLayer};
|
2
|
+
use crate::server::{
|
3
|
+
itsi_service::RequestContext,
|
4
|
+
static_file_server::{NotFoundBehavior, ServeRange, StaticFileServer, StaticFileServerConfig},
|
5
|
+
types::{HttpRequest, HttpResponse},
|
6
|
+
};
|
7
|
+
use async_trait::async_trait;
|
8
|
+
use either::Either;
|
9
|
+
use http::{
|
10
|
+
header::{IF_MODIFIED_SINCE, RANGE},
|
11
|
+
HeaderMap, Method,
|
12
|
+
};
|
13
|
+
use itsi_error::ItsiError;
|
14
|
+
use magnus::error::Result;
|
15
|
+
use serde::Deserialize;
|
16
|
+
use std::{collections::HashMap, path::PathBuf, sync::OnceLock, time::Duration};
|
17
|
+
|
18
|
+
#[derive(Debug, Deserialize)]
|
19
|
+
pub struct StaticAssets {
|
20
|
+
pub root_dir: PathBuf,
|
21
|
+
pub not_found_behavior: NotFoundBehavior,
|
22
|
+
pub auto_index: bool,
|
23
|
+
pub try_html_extension: bool,
|
24
|
+
pub max_file_size_in_memory: u64,
|
25
|
+
pub max_files_in_memory: u64,
|
26
|
+
pub file_check_interval: u64,
|
27
|
+
pub headers: Option<HashMap<String, String>>,
|
28
|
+
pub relative_path: bool,
|
29
|
+
#[serde(skip)]
|
30
|
+
file_server: OnceLock<StaticFileServer>,
|
31
|
+
}
|
32
|
+
|
33
|
+
#[async_trait]
|
34
|
+
impl MiddlewareLayer for StaticAssets {
|
35
|
+
async fn initialize(&self) -> Result<()> {
|
36
|
+
if let Ok(metadata) = tokio::fs::metadata(&self.root_dir).await {
|
37
|
+
if metadata.is_dir() {
|
38
|
+
Ok(())
|
39
|
+
} else {
|
40
|
+
Err(ItsiError::InvalidInput(
|
41
|
+
"Root directory exists but is not a directory".to_string(),
|
42
|
+
))
|
43
|
+
}
|
44
|
+
} else {
|
45
|
+
Err(ItsiError::InvalidInput(
|
46
|
+
"Root directory exists but is not a directory".to_string(),
|
47
|
+
))
|
48
|
+
}?;
|
49
|
+
self.file_server
|
50
|
+
.set(StaticFileServer::new(StaticFileServerConfig {
|
51
|
+
root_dir: self.root_dir.clone(),
|
52
|
+
not_found_behavior: self.not_found_behavior.clone(),
|
53
|
+
auto_index: self.auto_index,
|
54
|
+
max_entries: self.max_files_in_memory,
|
55
|
+
try_html_extension: self.try_html_extension,
|
56
|
+
max_file_size: self.max_file_size_in_memory,
|
57
|
+
recheck_interval: Duration::from_secs(self.file_check_interval),
|
58
|
+
}))
|
59
|
+
.map_err(ItsiError::default)?;
|
60
|
+
Ok(())
|
61
|
+
}
|
62
|
+
|
63
|
+
async fn before(
|
64
|
+
&self,
|
65
|
+
req: HttpRequest,
|
66
|
+
context: &mut RequestContext,
|
67
|
+
) -> Result<Either<HttpRequest, HttpResponse>> {
|
68
|
+
// Only handle GET and HEAD requests
|
69
|
+
if req.method() != Method::GET && req.method() != Method::HEAD {
|
70
|
+
return Ok(Either::Left(req));
|
71
|
+
}
|
72
|
+
let abs_path = req.uri().path();
|
73
|
+
let rel_path = if !self.relative_path {
|
74
|
+
abs_path
|
75
|
+
} else {
|
76
|
+
match context
|
77
|
+
.matching_pattern
|
78
|
+
.as_ref()
|
79
|
+
.and_then(|pattern| pattern.captures(req.uri().path()))
|
80
|
+
.and_then(|captures| captures.name("path_suffix"))
|
81
|
+
.map(|m| m.as_str())
|
82
|
+
{
|
83
|
+
Some(suffix) => suffix,
|
84
|
+
None => return Ok(Either::Left(req)),
|
85
|
+
}
|
86
|
+
};
|
87
|
+
|
88
|
+
// Determine if this is a HEAD request
|
89
|
+
let is_head_request = req.method() == Method::HEAD;
|
90
|
+
|
91
|
+
// Extract range and if-modified-since headers
|
92
|
+
let serve_range = parse_range_header(req.headers());
|
93
|
+
let if_modified_since = req
|
94
|
+
.headers()
|
95
|
+
.get(IF_MODIFIED_SINCE)
|
96
|
+
.and_then(|ims| ims.to_str().ok())
|
97
|
+
.and_then(|ims_str| httpdate::parse_http_date(ims_str).ok());
|
98
|
+
|
99
|
+
// Let the file server handle everything
|
100
|
+
let file_server = self.file_server.get().unwrap();
|
101
|
+
let response = file_server
|
102
|
+
.serve(
|
103
|
+
&req,
|
104
|
+
rel_path,
|
105
|
+
abs_path,
|
106
|
+
serve_range,
|
107
|
+
if_modified_since,
|
108
|
+
is_head_request,
|
109
|
+
)
|
110
|
+
.await;
|
111
|
+
|
112
|
+
if response.is_none() {
|
113
|
+
Ok(Either::Left(req))
|
114
|
+
} else {
|
115
|
+
Ok(Either::Right(response.unwrap()))
|
116
|
+
}
|
117
|
+
}
|
118
|
+
}
|
119
|
+
|
120
|
+
fn parse_range_header(headers: &HeaderMap) -> ServeRange {
|
121
|
+
let range_header = headers.get(RANGE);
|
122
|
+
if range_header.is_none() {
|
123
|
+
return ServeRange::Full;
|
124
|
+
}
|
125
|
+
let range_header = range_header.unwrap().to_str().unwrap_or("");
|
126
|
+
let bytes_prefix = "bytes=";
|
127
|
+
if !range_header.starts_with(bytes_prefix) {
|
128
|
+
return ServeRange::Full;
|
129
|
+
}
|
130
|
+
|
131
|
+
let range_str = &range_header[bytes_prefix.len()..];
|
132
|
+
|
133
|
+
let range_parts: Vec<&str> = range_str
|
134
|
+
.split(',')
|
135
|
+
.next()
|
136
|
+
.unwrap_or("")
|
137
|
+
.split('-')
|
138
|
+
.collect();
|
139
|
+
if range_parts.len() != 2 {
|
140
|
+
return ServeRange::Full;
|
141
|
+
}
|
142
|
+
|
143
|
+
let start = if range_parts[0].is_empty() {
|
144
|
+
range_parts[1].parse::<u64>().unwrap_or(0)
|
145
|
+
} else if let Ok(start) = range_parts[0].parse::<u64>() {
|
146
|
+
start
|
147
|
+
} else {
|
148
|
+
return ServeRange::Full;
|
149
|
+
};
|
150
|
+
|
151
|
+
let end = if range_parts[1].is_empty() {
|
152
|
+
u64::MAX // Use u64::MAX as sentinel for open-ended ranges
|
153
|
+
} else if let Ok(end) = range_parts[1].parse::<u64>() {
|
154
|
+
end // No conversion needed, already u64
|
155
|
+
} else {
|
156
|
+
return ServeRange::Full;
|
157
|
+
};
|
158
|
+
|
159
|
+
ServeRange::Range(start, end)
|
160
|
+
}
|
161
|
+
|
162
|
+
impl FromValue for StaticAssets {}
|
@@ -0,0 +1,158 @@
|
|
1
|
+
use crate::server::{
|
2
|
+
itsi_service::RequestContext,
|
3
|
+
types::{HttpRequest, HttpResponse},
|
4
|
+
};
|
5
|
+
use serde::Deserialize;
|
6
|
+
use std::sync::OnceLock;
|
7
|
+
|
8
|
+
#[derive(Debug, Clone, Deserialize)]
|
9
|
+
#[serde(transparent)]
|
10
|
+
pub struct StringRewrite {
|
11
|
+
pub template_string: String,
|
12
|
+
#[serde(default)]
|
13
|
+
pub segments: OnceLock<Vec<Segment>>,
|
14
|
+
}
|
15
|
+
|
16
|
+
#[derive(Debug, Clone, Deserialize)]
|
17
|
+
pub enum Segment {
|
18
|
+
Literal(String),
|
19
|
+
Placeholder(String),
|
20
|
+
}
|
21
|
+
|
22
|
+
pub fn parse_template(template: &str) -> Vec<Segment> {
|
23
|
+
let mut segments = Vec::new();
|
24
|
+
let mut last_index = 0;
|
25
|
+
while let Some(start_index) = template[last_index..].find('{') {
|
26
|
+
let start_index = last_index + start_index;
|
27
|
+
// Add the literal text before the placeholder.
|
28
|
+
if start_index > last_index {
|
29
|
+
segments.push(Segment::Literal(
|
30
|
+
template[last_index..start_index].to_string(),
|
31
|
+
));
|
32
|
+
}
|
33
|
+
// Find the corresponding closing brace.
|
34
|
+
if let Some(end_index) = template[start_index..].find('}') {
|
35
|
+
let end_index = start_index + end_index;
|
36
|
+
let placeholder = &template[start_index + 1..end_index];
|
37
|
+
segments.push(Segment::Placeholder(placeholder.to_string()));
|
38
|
+
last_index = end_index + 1;
|
39
|
+
} else {
|
40
|
+
// No closing brace found; treat the rest as literal.
|
41
|
+
segments.push(Segment::Literal(template[start_index..].to_string()));
|
42
|
+
break;
|
43
|
+
}
|
44
|
+
}
|
45
|
+
if last_index < template.len() {
|
46
|
+
segments.push(Segment::Literal(template[last_index..].to_string()));
|
47
|
+
}
|
48
|
+
segments
|
49
|
+
}
|
50
|
+
|
51
|
+
impl StringRewrite {
|
52
|
+
pub fn rewrite_request(&self, req: &HttpRequest, context: &RequestContext) -> String {
|
53
|
+
let segments = self
|
54
|
+
.segments
|
55
|
+
.get_or_init(|| parse_template(&self.template_string));
|
56
|
+
let captures = context
|
57
|
+
.matching_pattern
|
58
|
+
.as_ref()
|
59
|
+
.and_then(|re| re.captures(req.uri().path()));
|
60
|
+
|
61
|
+
let mut result = String::with_capacity(self.template_string.len());
|
62
|
+
|
63
|
+
for segment in segments {
|
64
|
+
match segment {
|
65
|
+
Segment::Literal(text) => result.push_str(text),
|
66
|
+
Segment::Placeholder(placeholder) => {
|
67
|
+
let replacement = match placeholder.as_str() {
|
68
|
+
"request_id" => context.request_id(),
|
69
|
+
"method" => req.method().as_str().to_string(),
|
70
|
+
"path" => req.uri().path().to_string(),
|
71
|
+
"host" => req.uri().host().unwrap_or("localhost").to_string(),
|
72
|
+
"path_and_query" => req
|
73
|
+
.uri()
|
74
|
+
.path_and_query()
|
75
|
+
.map(|pq| pq.to_string())
|
76
|
+
.unwrap_or("".to_string()),
|
77
|
+
"query" => {
|
78
|
+
let query = req.uri().query().unwrap_or("").to_string();
|
79
|
+
if query.is_empty() {
|
80
|
+
query
|
81
|
+
} else {
|
82
|
+
format!("?{}", query)
|
83
|
+
}
|
84
|
+
}
|
85
|
+
"port" => req
|
86
|
+
.uri()
|
87
|
+
.port()
|
88
|
+
.map(|p| p.to_string())
|
89
|
+
.unwrap_or_else(|| "80".to_string()),
|
90
|
+
"start_time" => {
|
91
|
+
if let Some(start_time) = context.start_time() {
|
92
|
+
start_time.format("%Y-%m-%d:%H:%M:%S:%3f").to_string()
|
93
|
+
} else {
|
94
|
+
"N/A".to_string()
|
95
|
+
}
|
96
|
+
}
|
97
|
+
other => {
|
98
|
+
// Try using the context's matching regex if available.
|
99
|
+
if let Some(caps) = &captures {
|
100
|
+
if let Some(m) = caps.name(other) {
|
101
|
+
m.as_str().to_string()
|
102
|
+
} else {
|
103
|
+
// Fallback: leave the placeholder as is.
|
104
|
+
format!("{{{}}}", other)
|
105
|
+
}
|
106
|
+
} else {
|
107
|
+
format!("{{{}}}", other)
|
108
|
+
}
|
109
|
+
}
|
110
|
+
};
|
111
|
+
result.push_str(&replacement);
|
112
|
+
}
|
113
|
+
}
|
114
|
+
}
|
115
|
+
|
116
|
+
result
|
117
|
+
}
|
118
|
+
|
119
|
+
pub fn rewrite_response(&self, resp: &HttpResponse, context: &RequestContext) -> String {
|
120
|
+
let segments = self
|
121
|
+
.segments
|
122
|
+
.get_or_init(|| parse_template(&self.template_string));
|
123
|
+
|
124
|
+
let mut result = String::with_capacity(self.template_string.len());
|
125
|
+
for segment in segments {
|
126
|
+
match segment {
|
127
|
+
Segment::Literal(text) => result.push_str(text),
|
128
|
+
Segment::Placeholder(placeholder) => {
|
129
|
+
let replacement = match placeholder.as_str() {
|
130
|
+
"request_id" => context.request_id(),
|
131
|
+
"status" => resp.status().as_str().to_string(),
|
132
|
+
"response_time" => {
|
133
|
+
if let Some(response_time) = context.get_response_time() {
|
134
|
+
if let Some(microseconds) = response_time.num_microseconds() {
|
135
|
+
format!("{:.3}ms", microseconds as f64 / 1000.0)
|
136
|
+
} else {
|
137
|
+
format!("{}ms", response_time.num_milliseconds())
|
138
|
+
}
|
139
|
+
} else {
|
140
|
+
"-".to_string()
|
141
|
+
}
|
142
|
+
}
|
143
|
+
other => {
|
144
|
+
if let Some(header_value) = resp.headers().get(other) {
|
145
|
+
format!("{:?}", header_value)
|
146
|
+
} else {
|
147
|
+
format!("{{{}}}", other)
|
148
|
+
}
|
149
|
+
}
|
150
|
+
};
|
151
|
+
result.push_str(&replacement);
|
152
|
+
}
|
153
|
+
}
|
154
|
+
}
|
155
|
+
|
156
|
+
result
|
157
|
+
}
|
158
|
+
}
|
@@ -0,0 +1,12 @@
|
|
1
|
+
use serde::{Deserialize, Serialize};
|
2
|
+
|
3
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
4
|
+
pub enum TokenSource {
|
5
|
+
#[serde(rename(deserialize = "header"))]
|
6
|
+
Header {
|
7
|
+
name: String,
|
8
|
+
prefix: Option<String>,
|
9
|
+
},
|
10
|
+
#[serde(rename(deserialize = "query"))]
|
11
|
+
Query(String),
|
12
|
+
}
|
@@ -0,0 +1,315 @@
|
|
1
|
+
mod middleware;
|
2
|
+
mod middlewares;
|
3
|
+
use super::types::HttpRequest;
|
4
|
+
use http::header::{ACCEPT, CONTENT_TYPE, HOST};
|
5
|
+
use itsi_rb_helpers::HeapVal;
|
6
|
+
use magnus::{error::Result, value::ReprValue, RArray, RHash, Ruby, TryConvert, Value};
|
7
|
+
pub use middleware::Middleware;
|
8
|
+
pub use middlewares::*;
|
9
|
+
use regex::{Regex, RegexSet};
|
10
|
+
use std::{collections::HashMap, sync::Arc};
|
11
|
+
use tracing::info;
|
12
|
+
|
13
|
+
#[derive(Debug)]
|
14
|
+
pub struct MiddlewareSet {
|
15
|
+
pub route_set: RegexSet,
|
16
|
+
pub patterns: Vec<Arc<Regex>>,
|
17
|
+
pub stacks: HashMap<usize, MiddlewareStack>,
|
18
|
+
pub default_stack: Vec<Middleware>,
|
19
|
+
}
|
20
|
+
|
21
|
+
#[derive(Debug)]
|
22
|
+
pub struct MiddlewareStack {
|
23
|
+
layers: Vec<Middleware>,
|
24
|
+
methods: Option<Vec<StringMatch>>,
|
25
|
+
protocols: Option<Vec<StringMatch>>,
|
26
|
+
hosts: Option<Vec<StringMatch>>,
|
27
|
+
extensions: Option<Vec<StringMatch>>,
|
28
|
+
ports: Option<Vec<StringMatch>>,
|
29
|
+
content_types: Option<Vec<StringMatch>>,
|
30
|
+
accepts: Option<Vec<StringMatch>>,
|
31
|
+
}
|
32
|
+
|
33
|
+
#[derive(Debug)]
|
34
|
+
enum StringMatch {
|
35
|
+
Exact(String),
|
36
|
+
Wildcard(Regex),
|
37
|
+
}
|
38
|
+
|
39
|
+
impl StringMatch {
|
40
|
+
fn from_value(value: Value) -> Result<Self> {
|
41
|
+
let ruby = Ruby::get().unwrap();
|
42
|
+
if value.is_kind_of(ruby.class_regexp()) {
|
43
|
+
let src_str = value.funcall::<_, _, String>("source", ())?;
|
44
|
+
let regex = Regex::new(&src_str).map_err(|e| {
|
45
|
+
magnus::Error::new(
|
46
|
+
magnus::exception::exception(),
|
47
|
+
format!("Invalid regexp: {}", e),
|
48
|
+
)
|
49
|
+
})?;
|
50
|
+
Ok(StringMatch::Wildcard(regex))
|
51
|
+
} else {
|
52
|
+
Ok(StringMatch::Exact(value.to_string()))
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
fn matches(&self, value: &str) -> bool {
|
57
|
+
match self {
|
58
|
+
StringMatch::Exact(s) => s.eq_ignore_ascii_case(value),
|
59
|
+
StringMatch::Wildcard(re) => re.is_match(value),
|
60
|
+
}
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
impl MiddlewareStack {
|
65
|
+
pub fn matches(&self, request: &HttpRequest) -> bool {
|
66
|
+
if let Some(methods) = &self.methods {
|
67
|
+
let method = request.method().as_str();
|
68
|
+
if !methods.iter().any(|m| m.matches(method)) {
|
69
|
+
return false;
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
if let (Some(protocols), Some(protocol)) = (&self.protocols, request.uri().scheme()) {
|
74
|
+
if !protocols.iter().any(|p| p.matches(protocol.as_str())) {
|
75
|
+
return false;
|
76
|
+
}
|
77
|
+
}
|
78
|
+
|
79
|
+
if let (Some(hosts), Some(host)) = (&self.hosts, request.headers().get(HOST)) {
|
80
|
+
if let Ok(host) = host.to_str() {
|
81
|
+
if !hosts.iter().any(|d| d.matches(host)) {
|
82
|
+
return false;
|
83
|
+
}
|
84
|
+
}
|
85
|
+
}
|
86
|
+
|
87
|
+
if let (Some(ports), Some(port)) = (&self.ports, request.uri().port()) {
|
88
|
+
if !ports.iter().any(|d| d.matches(port.as_str())) {
|
89
|
+
info!("No match between port {} and {:?}", port, ports);
|
90
|
+
return false;
|
91
|
+
}
|
92
|
+
}
|
93
|
+
|
94
|
+
if let Some(extensions) = &self.extensions {
|
95
|
+
let extension = request.uri().path().split('.').next_back().unwrap_or("");
|
96
|
+
if !extensions.iter().any(|e| e.matches(extension)) {
|
97
|
+
return false;
|
98
|
+
}
|
99
|
+
}
|
100
|
+
|
101
|
+
if let Some(content_types) = &self.content_types {
|
102
|
+
if let Some(content_type) = request.headers().get(CONTENT_TYPE) {
|
103
|
+
if !content_types
|
104
|
+
.iter()
|
105
|
+
.any(|ct| ct.matches(content_type.to_str().unwrap_or("")))
|
106
|
+
{
|
107
|
+
return false;
|
108
|
+
}
|
109
|
+
}
|
110
|
+
}
|
111
|
+
|
112
|
+
if let Some(accepts) = &self.accepts {
|
113
|
+
if let Some(accept) = request.headers().get(ACCEPT) {
|
114
|
+
if !accepts
|
115
|
+
.iter()
|
116
|
+
.any(|a| a.matches(accept.to_str().unwrap_or("")))
|
117
|
+
{
|
118
|
+
return false;
|
119
|
+
}
|
120
|
+
}
|
121
|
+
}
|
122
|
+
|
123
|
+
true
|
124
|
+
}
|
125
|
+
}
|
126
|
+
|
127
|
+
impl MiddlewareSet {
|
128
|
+
pub fn new(routes_raw: Option<HeapVal>, default_app: HeapVal) -> Result<Self> {
|
129
|
+
if let Some(routes_raw) = routes_raw {
|
130
|
+
let mut stacks = HashMap::new();
|
131
|
+
let mut routes = vec![];
|
132
|
+
for (index, route) in RArray::from_value(*routes_raw)
|
133
|
+
.ok_or(magnus::Error::new(
|
134
|
+
magnus::exception::exception(),
|
135
|
+
format!("Routes must be an array. Got {:?}", routes_raw),
|
136
|
+
))?
|
137
|
+
.into_iter()
|
138
|
+
.enumerate()
|
139
|
+
{
|
140
|
+
let route_hash: RHash = RHash::try_convert(route)?;
|
141
|
+
let route_raw = route_hash
|
142
|
+
.get("route")
|
143
|
+
.ok_or(magnus::Error::new(
|
144
|
+
magnus::exception::exception(),
|
145
|
+
"Route is missing :route key",
|
146
|
+
))?
|
147
|
+
.funcall::<_, _, String>("source", ())?;
|
148
|
+
let middleware =
|
149
|
+
RArray::from_value(route_hash.get("middleware").ok_or(magnus::Error::new(
|
150
|
+
magnus::exception::exception(),
|
151
|
+
"Route is missing middleware key",
|
152
|
+
))?)
|
153
|
+
.ok_or(magnus::Error::new(
|
154
|
+
magnus::exception::exception(),
|
155
|
+
format!("middleware must be an array. Got {:?}", routes_raw),
|
156
|
+
))?;
|
157
|
+
|
158
|
+
let mut layers = middleware
|
159
|
+
.into_iter()
|
160
|
+
.map(MiddlewareSet::parse_middleware)
|
161
|
+
.collect::<Result<Vec<_>>>()?;
|
162
|
+
layers.push(Middleware::RubyApp(RubyApp::from_value(
|
163
|
+
default_app.clone(),
|
164
|
+
)?));
|
165
|
+
routes.push(route_raw);
|
166
|
+
layers.sort();
|
167
|
+
stacks.insert(
|
168
|
+
index,
|
169
|
+
MiddlewareStack {
|
170
|
+
layers,
|
171
|
+
methods: extract_optional_match_array(route_hash, "methods")?,
|
172
|
+
protocols: extract_optional_match_array(route_hash, "protocols")?,
|
173
|
+
hosts: extract_optional_match_array(route_hash, "hosts")?,
|
174
|
+
extensions: extract_optional_match_array(route_hash, "extensions")?,
|
175
|
+
ports: extract_optional_match_array(route_hash, "ports")?,
|
176
|
+
content_types: extract_optional_match_array(route_hash, "content_types")?,
|
177
|
+
accepts: extract_optional_match_array(route_hash, "accepts")?,
|
178
|
+
},
|
179
|
+
);
|
180
|
+
}
|
181
|
+
info!("Routes are {:?}", routes);
|
182
|
+
Ok(Self {
|
183
|
+
route_set: RegexSet::new(&routes).map_err(|e| {
|
184
|
+
magnus::Error::new(
|
185
|
+
magnus::exception::exception(),
|
186
|
+
format!("Failed to create route set: {}", e),
|
187
|
+
)
|
188
|
+
})?,
|
189
|
+
patterns: routes
|
190
|
+
.into_iter()
|
191
|
+
.map(|r| Regex::new(&r))
|
192
|
+
.collect::<std::result::Result<Vec<Regex>, regex::Error>>()
|
193
|
+
.map_err(|e| {
|
194
|
+
magnus::Error::new(
|
195
|
+
magnus::exception::exception(),
|
196
|
+
format!("Failed to create route set: {}", e),
|
197
|
+
)
|
198
|
+
})?
|
199
|
+
.into_iter()
|
200
|
+
.map(Arc::new)
|
201
|
+
.collect(),
|
202
|
+
stacks,
|
203
|
+
default_stack: vec![Middleware::RubyApp(RubyApp::from_value(default_app)?)],
|
204
|
+
})
|
205
|
+
} else {
|
206
|
+
Ok(Self {
|
207
|
+
route_set: RegexSet::empty(),
|
208
|
+
patterns: Vec::new(),
|
209
|
+
stacks: HashMap::new(),
|
210
|
+
default_stack: vec![Middleware::RubyApp(RubyApp::from_value(default_app)?)],
|
211
|
+
})
|
212
|
+
}
|
213
|
+
}
|
214
|
+
|
215
|
+
pub fn stack_for(&self, request: &HttpRequest) -> (&Vec<Middleware>, Option<Arc<Regex>>) {
|
216
|
+
let binding = self.route_set.matches(request.uri().path());
|
217
|
+
let matches = binding.iter();
|
218
|
+
for index in matches {
|
219
|
+
let matching_pattern = self.patterns.get(index).cloned();
|
220
|
+
if let Some(stack) = self.stacks.get(&index) {
|
221
|
+
if stack.matches(request) {
|
222
|
+
return (&stack.layers, matching_pattern);
|
223
|
+
}
|
224
|
+
}
|
225
|
+
}
|
226
|
+
(self.default_stack(), None)
|
227
|
+
}
|
228
|
+
|
229
|
+
pub fn parse_middleware(middleware: Value) -> Result<Middleware> {
|
230
|
+
let middleware_hash = RHash::from_value(middleware).ok_or(magnus::Error::new(
|
231
|
+
magnus::exception::exception(),
|
232
|
+
format!("Filter must be a hash. Got {:?}", middleware),
|
233
|
+
))?;
|
234
|
+
let middleware_type: String = middleware_hash
|
235
|
+
.get("type")
|
236
|
+
.ok_or(magnus::Error::new(
|
237
|
+
magnus::exception::exception(),
|
238
|
+
format!("Filter must have a :type key. Got {:?}", middleware_hash),
|
239
|
+
))?
|
240
|
+
.to_string();
|
241
|
+
|
242
|
+
let parameters: Value = middleware_hash.get("parameters").ok_or(magnus::Error::new(
|
243
|
+
magnus::exception::exception(),
|
244
|
+
format!(
|
245
|
+
"Filter must have a :parameters key. Got {:?}",
|
246
|
+
middleware_hash
|
247
|
+
),
|
248
|
+
))?;
|
249
|
+
|
250
|
+
let result = match middleware_type.as_str() {
|
251
|
+
"allow_list" => Middleware::AllowList(AllowList::from_value(parameters)?),
|
252
|
+
"auth_basic" => Middleware::AuthBasic(AuthBasic::from_value(parameters)?),
|
253
|
+
"auth_jwt" => Middleware::AuthJwt(Box::new(AuthJwt::from_value(parameters)?)),
|
254
|
+
"auth_api_key" => Middleware::AuthAPIKey(AuthAPIKey::from_value(parameters)?),
|
255
|
+
"cache_control" => Middleware::CacheControl(CacheControl::from_value(parameters)?),
|
256
|
+
"deny_list" => Middleware::DenyList(DenyList::from_value(parameters)?),
|
257
|
+
"etag" => Middleware::ETag(ETag::from_value(parameters)?),
|
258
|
+
"intrusion_protection" => {
|
259
|
+
Middleware::IntrusionProtection(IntrusionProtection::from_value(parameters)?)
|
260
|
+
}
|
261
|
+
"rate_limit" => Middleware::RateLimit(RateLimit::from_value(parameters)?),
|
262
|
+
"cors" => Middleware::Cors(Box::new(Cors::from_value(parameters)?)),
|
263
|
+
"request_headers" => {
|
264
|
+
Middleware::RequestHeaders(RequestHeaders::from_value(parameters)?)
|
265
|
+
}
|
266
|
+
"response_headers" => {
|
267
|
+
Middleware::ResponseHeaders(ResponseHeaders::from_value(parameters)?)
|
268
|
+
}
|
269
|
+
"static_assets" => Middleware::StaticAssets(StaticAssets::from_value(parameters)?),
|
270
|
+
"compression" => Middleware::Compression(Compression::from_value(parameters)?),
|
271
|
+
"log_requests" => Middleware::LogRequests(LogRequests::from_value(parameters)?),
|
272
|
+
"redirect" => Middleware::Redirect(Redirect::from_value(parameters)?),
|
273
|
+
"app" => Middleware::RubyApp(RubyApp::from_value(parameters.into())?),
|
274
|
+
"proxy" => Middleware::Proxy(Proxy::from_value(parameters)?),
|
275
|
+
_ => {
|
276
|
+
return Err(magnus::Error::new(
|
277
|
+
magnus::exception::exception(),
|
278
|
+
format!("Unknown filter type: {}", middleware_type),
|
279
|
+
))
|
280
|
+
}
|
281
|
+
};
|
282
|
+
|
283
|
+
Ok(result)
|
284
|
+
}
|
285
|
+
|
286
|
+
fn default_stack(&self) -> &Vec<Middleware> {
|
287
|
+
&self.default_stack
|
288
|
+
}
|
289
|
+
|
290
|
+
pub async fn initialize_layers(&self) -> Result<()> {
|
291
|
+
for middleware in &self.default_stack {
|
292
|
+
middleware.initialize().await?;
|
293
|
+
}
|
294
|
+
for stack in self.stacks.values() {
|
295
|
+
for middleware in &stack.layers {
|
296
|
+
middleware.initialize().await?;
|
297
|
+
}
|
298
|
+
}
|
299
|
+
Ok(())
|
300
|
+
}
|
301
|
+
}
|
302
|
+
|
303
|
+
fn extract_optional_match_array(route_hash: RHash, arg: &str) -> Result<Option<Vec<StringMatch>>> {
|
304
|
+
let rarray = route_hash.aref::<_, Option<RArray>>(arg)?;
|
305
|
+
if let Some(array) = rarray {
|
306
|
+
Ok(Some(
|
307
|
+
array
|
308
|
+
.into_iter()
|
309
|
+
.map(StringMatch::from_value)
|
310
|
+
.collect::<Result<Vec<StringMatch>>>()?,
|
311
|
+
))
|
312
|
+
} else {
|
313
|
+
Ok(None)
|
314
|
+
}
|
315
|
+
}
|
@@ -1,5 +1,18 @@
|
|
1
1
|
pub mod bind;
|
2
|
-
pub mod
|
2
|
+
pub mod bind_protocol;
|
3
|
+
pub mod byte_frame;
|
4
|
+
pub mod cache_store;
|
5
|
+
pub mod io_stream;
|
6
|
+
pub mod itsi_service;
|
7
|
+
pub mod lifecycle_event;
|
3
8
|
pub mod listener;
|
9
|
+
pub mod middleware_stack;
|
10
|
+
pub mod process_worker;
|
11
|
+
pub mod rate_limiter;
|
12
|
+
pub mod request_job;
|
13
|
+
pub mod serve_strategy;
|
14
|
+
pub mod signal;
|
15
|
+
pub mod static_file_server;
|
16
|
+
pub mod thread_worker;
|
4
17
|
pub mod tls;
|
5
|
-
pub mod
|
18
|
+
pub mod types;
|