itsi-server 0.1.11 → 0.1.12

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.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -0
  3. data/CODE_OF_CONDUCT.md +7 -0
  4. data/Cargo.lock +1536 -45
  5. data/README.md +4 -0
  6. data/_index.md +6 -0
  7. data/exe/itsi +33 -74
  8. data/ext/itsi_error/src/lib.rs +9 -0
  9. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  10. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  11. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  12. data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  13. data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
  14. data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
  15. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
  16. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
  17. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
  18. data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
  19. data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
  20. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
  21. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
  22. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
  23. data/ext/itsi_rb_helpers/Cargo.toml +1 -0
  24. data/ext/itsi_rb_helpers/src/heap_value.rs +18 -0
  25. data/ext/itsi_rb_helpers/src/lib.rs +34 -7
  26. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  27. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  28. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  29. 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
  30. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
  31. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
  32. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
  33. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
  34. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
  35. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
  36. data/ext/itsi_server/Cargo.toml +69 -30
  37. data/ext/itsi_server/src/lib.rs +79 -147
  38. data/ext/itsi_server/src/{body_proxy → ruby_types/itsi_body_proxy}/big_bytes.rs +10 -5
  39. data/ext/itsi_server/src/{body_proxy/itsi_body_proxy.rs → ruby_types/itsi_body_proxy/mod.rs} +22 -3
  40. data/ext/itsi_server/src/ruby_types/itsi_grpc_request.rs +147 -0
  41. data/ext/itsi_server/src/ruby_types/itsi_grpc_response.rs +19 -0
  42. data/ext/itsi_server/src/ruby_types/itsi_grpc_stream/mod.rs +216 -0
  43. data/ext/itsi_server/src/{request/itsi_request.rs → ruby_types/itsi_http_request.rs} +101 -117
  44. data/ext/itsi_server/src/{response/itsi_response.rs → ruby_types/itsi_http_response.rs} +72 -41
  45. data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +225 -0
  46. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +355 -0
  47. data/ext/itsi_server/src/ruby_types/itsi_server.rs +82 -0
  48. data/ext/itsi_server/src/ruby_types/mod.rs +55 -0
  49. data/ext/itsi_server/src/server/bind.rs +13 -5
  50. data/ext/itsi_server/src/server/byte_frame.rs +32 -0
  51. data/ext/itsi_server/src/server/cache_store.rs +74 -0
  52. data/ext/itsi_server/src/server/itsi_service.rs +172 -0
  53. data/ext/itsi_server/src/server/lifecycle_event.rs +3 -0
  54. data/ext/itsi_server/src/server/listener.rs +102 -2
  55. data/ext/itsi_server/src/server/middleware_stack/middleware.rs +153 -0
  56. data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +47 -0
  57. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +58 -0
  58. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +82 -0
  59. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +321 -0
  60. data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +139 -0
  61. data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +300 -0
  62. data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +287 -0
  63. data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +48 -0
  64. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +127 -0
  65. data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +191 -0
  66. data/ext/itsi_server/src/server/middleware_stack/middlewares/grpc_service.rs +72 -0
  67. data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +85 -0
  68. data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +195 -0
  69. data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
  70. data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +82 -0
  71. data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +216 -0
  72. data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +124 -0
  73. data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +76 -0
  74. data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +43 -0
  75. data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +34 -0
  76. data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +93 -0
  77. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +162 -0
  78. data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +158 -0
  79. data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +12 -0
  80. data/ext/itsi_server/src/server/middleware_stack/mod.rs +315 -0
  81. data/ext/itsi_server/src/server/mod.rs +8 -1
  82. data/ext/itsi_server/src/server/process_worker.rs +38 -12
  83. data/ext/itsi_server/src/server/rate_limiter.rs +565 -0
  84. data/ext/itsi_server/src/server/request_job.rs +11 -0
  85. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +119 -42
  86. data/ext/itsi_server/src/server/serve_strategy/mod.rs +9 -6
  87. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +256 -111
  88. data/ext/itsi_server/src/server/signal.rs +19 -0
  89. data/ext/itsi_server/src/server/static_file_server.rs +984 -0
  90. data/ext/itsi_server/src/server/thread_worker.rs +139 -94
  91. data/ext/itsi_server/src/server/types.rs +43 -0
  92. data/ext/itsi_tracing/Cargo.toml +1 -0
  93. data/ext/itsi_tracing/src/lib.rs +216 -45
  94. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
  95. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
  96. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
  97. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
  98. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
  99. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
  100. data/lib/itsi/{request.rb → http_request.rb} +29 -5
  101. data/lib/itsi/http_response.rb +39 -0
  102. data/lib/itsi/server/Itsi.rb +11 -19
  103. data/lib/itsi/server/config/dsl.rb +506 -0
  104. data/lib/itsi/server/config.rb +103 -8
  105. data/lib/itsi/server/default_app/default_app.rb +38 -0
  106. data/lib/itsi/server/grpc_interface.rb +213 -0
  107. data/lib/itsi/server/rack/handler/itsi.rb +8 -17
  108. data/lib/itsi/server/rack_interface.rb +23 -4
  109. data/lib/itsi/server/scheduler_interface.rb +1 -1
  110. data/lib/itsi/server/scheduler_mode.rb +4 -0
  111. data/lib/itsi/server/signal_trap.rb +7 -1
  112. data/lib/itsi/server/version.rb +1 -1
  113. data/lib/itsi/server.rb +74 -63
  114. data/lib/itsi/standard_headers.rb +86 -0
  115. metadata +84 -15
  116. data/ext/itsi_scheduler/extconf.rb +0 -6
  117. data/ext/itsi_server/src/body_proxy/mod.rs +0 -2
  118. data/ext/itsi_server/src/request/mod.rs +0 -1
  119. data/ext/itsi_server/src/response/mod.rs +0 -1
  120. data/ext/itsi_server/src/server/itsi_server.rs +0 -288
  121. data/lib/itsi/server/options_dsl.rb +0 -401
  122. data/lib/itsi/stream_io.rb +0 -38
  123. /data/lib/itsi/{index.html → server/default_app/index.html} +0 -0
@@ -0,0 +1,172 @@
1
+ use super::listener::ListenerInfo;
2
+ use super::middleware_stack::CompressionAlgorithm;
3
+ use super::middleware_stack::MiddlewareLayer;
4
+ use super::request_job::RequestJob;
5
+ use super::serve_strategy::single_mode::RunningPhase;
6
+ use super::types::HttpRequest;
7
+ use super::types::HttpResponse;
8
+ use crate::ruby_types::itsi_server::itsi_server_config::ServerParams;
9
+ use chrono;
10
+ use chrono::Local;
11
+ use either::Either;
12
+ use hyper::service::Service;
13
+ use itsi_error::ItsiError;
14
+ use regex::Regex;
15
+ use std::sync::OnceLock;
16
+ use std::{future::Future, ops::Deref, pin::Pin, sync::Arc};
17
+ use tokio::sync::watch::{self};
18
+
19
+ #[derive(Clone)]
20
+ pub struct ItsiService {
21
+ pub inner: Arc<IstiServiceInner>,
22
+ }
23
+
24
+ impl Deref for ItsiService {
25
+ type Target = Arc<IstiServiceInner>;
26
+
27
+ fn deref(&self) -> &Self::Target {
28
+ &self.inner
29
+ }
30
+ }
31
+
32
+ pub struct IstiServiceInner {
33
+ pub sender: async_channel::Sender<RequestJob>,
34
+ pub server_params: Arc<ServerParams>,
35
+ pub listener: Arc<ListenerInfo>,
36
+ pub addr: String,
37
+ pub shutdown_channel: watch::Receiver<RunningPhase>,
38
+ }
39
+
40
+ #[derive(Clone)]
41
+ pub struct RequestContext {
42
+ inner: Arc<RequestContextInner>,
43
+ }
44
+
45
+ impl Deref for RequestContext {
46
+ type Target = Arc<RequestContextInner>;
47
+
48
+ fn deref(&self) -> &Self::Target {
49
+ &self.inner
50
+ }
51
+ }
52
+
53
+ impl Deref for RequestContextInner {
54
+ type Target = ItsiService;
55
+
56
+ fn deref(&self) -> &Self::Target {
57
+ &self.service
58
+ }
59
+ }
60
+
61
+ pub struct RequestContextInner {
62
+ pub request_id: i128,
63
+ pub service: ItsiService,
64
+ pub matching_pattern: Option<Arc<Regex>>,
65
+ pub compression_method: OnceLock<CompressionAlgorithm>,
66
+ pub origin: OnceLock<Option<String>>,
67
+ pub start_time: chrono::DateTime<chrono::Utc>,
68
+ pub request: Option<Arc<HttpRequest>>,
69
+ pub request_start_time: OnceLock<chrono::DateTime<Local>>,
70
+ pub if_none_match: OnceLock<Option<String>>,
71
+ pub etag_value: OnceLock<Option<String>>,
72
+ }
73
+
74
+ impl RequestContext {
75
+ fn new(service: ItsiService, matching_pattern: Option<Arc<Regex>>) -> Self {
76
+ RequestContext {
77
+ inner: Arc::new(RequestContextInner {
78
+ request_id: rand::random::<i128>(),
79
+ service,
80
+ matching_pattern,
81
+ compression_method: OnceLock::new(),
82
+ origin: OnceLock::new(),
83
+ start_time: chrono::Utc::now(),
84
+ request: None,
85
+ request_start_time: OnceLock::new(),
86
+ if_none_match: OnceLock::new(),
87
+ etag_value: OnceLock::new(),
88
+ }),
89
+ }
90
+ }
91
+
92
+ pub fn set_compression_method(&self, method: CompressionAlgorithm) {
93
+ self.inner.compression_method.set(method).unwrap();
94
+ }
95
+
96
+ pub fn set_origin(&self, origin: Option<String>) {
97
+ self.inner.origin.set(origin).unwrap();
98
+ }
99
+
100
+ pub fn set_if_none_match(&self, value: Option<String>) {
101
+ self.inner.if_none_match.set(value).unwrap();
102
+ }
103
+
104
+ pub fn get_if_none_match(&self) -> Option<String> {
105
+ self.inner.if_none_match.get().cloned().flatten()
106
+ }
107
+
108
+ pub fn request_id(&self) -> String {
109
+ self.inner.request_id.to_string()
110
+ }
111
+
112
+ pub fn track_start_time(&self) {
113
+ self.inner
114
+ .request_start_time
115
+ .get_or_init(chrono::Local::now);
116
+ }
117
+
118
+ pub fn start_time(&self) -> Option<chrono::DateTime<Local>> {
119
+ self.inner.request_start_time.get().cloned()
120
+ }
121
+
122
+ pub fn get_response_time(&self) -> Option<chrono::TimeDelta> {
123
+ self.inner
124
+ .request_start_time
125
+ .get()
126
+ .map(|instant| Local::now() - instant)
127
+ }
128
+ }
129
+
130
+ impl Service<HttpRequest> for ItsiService {
131
+ type Response = HttpResponse;
132
+ type Error = ItsiError;
133
+ type Future = Pin<Box<dyn Future<Output = itsi_error::Result<HttpResponse>> + Send>>;
134
+
135
+ // This is called once per incoming Request.
136
+ fn call(&self, req: HttpRequest) -> Self::Future {
137
+ let params = self.server_params.clone();
138
+ let self_clone = self.clone();
139
+ Box::pin(async move {
140
+ let mut req = req;
141
+ let mut resp: Option<HttpResponse> = None;
142
+ let (stack, matching_pattern) = params.middleware.get().unwrap().stack_for(&req);
143
+ let mut context = RequestContext::new(self_clone, matching_pattern);
144
+ let mut depth = 0;
145
+ for (index, elm) in stack.iter().enumerate() {
146
+ match elm.before(req, &mut context).await {
147
+ Ok(Either::Left(r)) => req = r,
148
+ Ok(Either::Right(r)) => {
149
+ resp = Some(r);
150
+ depth = index;
151
+ break;
152
+ }
153
+ Err(e) => return Err(e.into()),
154
+ }
155
+ }
156
+
157
+ let mut resp = match resp {
158
+ Some(r) => r,
159
+ None => {
160
+ return Err(ItsiError::InternalServerError(
161
+ "No response returned from middleware stack".to_string(),
162
+ ))
163
+ }
164
+ };
165
+ for elm in stack.iter().rev().skip(stack.len() - depth - 1) {
166
+ resp = elm.after(resp, &mut context).await;
167
+ }
168
+
169
+ Ok(resp)
170
+ })
171
+ }
172
+ }
@@ -3,7 +3,10 @@ pub enum LifecycleEvent {
3
3
  Start,
4
4
  Shutdown,
5
5
  Restart,
6
+ Reload,
6
7
  IncreaseWorkers,
7
8
  DecreaseWorkers,
8
9
  ForceShutdown,
10
+ PrintInfo,
11
+ ChildTerminated,
9
12
  }
@@ -6,7 +6,9 @@ use super::tls::ItsiTlsAcceptor;
6
6
  use itsi_error::{ItsiError, Result};
7
7
  use itsi_tracing::info;
8
8
  use socket2::{Domain, Protocol, Socket, Type};
9
+ use std::fmt::Display;
9
10
  use std::net::{IpAddr, SocketAddr, TcpListener};
11
+ use std::os::fd::{AsRawFd, FromRawFd, RawFd};
10
12
  use std::sync::Arc;
11
13
  use std::{os::unix::net::UnixListener, path::PathBuf};
12
14
  use tokio::net::TcpListener as TokioTcpListener;
@@ -241,6 +243,32 @@ impl std::fmt::Display for SockAddr {
241
243
  }
242
244
  }
243
245
  }
246
+ impl Display for Listener {
247
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248
+ match self {
249
+ Listener::Tcp(listener) | Listener::TcpTls((listener, _)) => write!(
250
+ f,
251
+ "{}",
252
+ listener
253
+ .local_addr()
254
+ .map(|addr| addr.to_string())
255
+ .unwrap_or_else(|_| "".to_string())
256
+ ),
257
+
258
+ Listener::Unix(listener) | Listener::UnixTls((listener, _)) => write!(
259
+ f,
260
+ "{}",
261
+ listener
262
+ .local_addr()
263
+ .map(|addr| addr
264
+ .as_pathname()
265
+ .map(|path| path.to_str().unwrap_or("").to_owned())
266
+ .unwrap_or_default())
267
+ .unwrap_or_else(|_| "".to_string())
268
+ ),
269
+ }
270
+ }
271
+ }
244
272
 
245
273
  impl Listener {
246
274
  pub fn into_tokio_listener(self) -> TokioListener {
@@ -261,6 +289,58 @@ impl Listener {
261
289
  ),
262
290
  }
263
291
  }
292
+
293
+ /// Handover information when using exec to hand over the listener to a replacement process.
294
+ pub fn handover(&self) -> Result<(String, i32)> {
295
+ match self {
296
+ Listener::Tcp(listener) => {
297
+ let addr = listener.local_addr()?;
298
+ Ok((
299
+ format!("tcp://{}:{}", addr.ip().to_canonical(), addr.port()),
300
+ listener.as_raw_fd(),
301
+ ))
302
+ }
303
+ Listener::TcpTls((listener, _)) => {
304
+ let addr = listener.local_addr()?;
305
+ Ok((
306
+ format!("tcp://{}:{}", addr.ip().to_canonical(), addr.port()),
307
+ listener.as_raw_fd(),
308
+ ))
309
+ }
310
+ Listener::Unix(listener) => {
311
+ let addr = listener.local_addr()?;
312
+ Ok((
313
+ format!("unix://{}", addr.as_pathname().unwrap().to_str().unwrap()),
314
+ listener.as_raw_fd(),
315
+ ))
316
+ }
317
+ Listener::UnixTls((listener, _)) => {
318
+ let addr = listener.local_addr()?;
319
+ Ok((
320
+ format!("unix://{}", addr.as_pathname().unwrap().to_str().unwrap()),
321
+ listener.as_raw_fd(),
322
+ ))
323
+ }
324
+ }
325
+ }
326
+
327
+ pub fn inherit_fd(bind: Bind, fd: RawFd) -> Result<Self> {
328
+ let bound = match bind.address {
329
+ BindAddress::Ip(_) => match bind.protocol {
330
+ BindProtocol::Http => Listener::Tcp(revive_tcp_socket(fd)?),
331
+ BindProtocol::Https => {
332
+ let tcp_listener = revive_tcp_socket(fd)?;
333
+ Listener::TcpTls((tcp_listener, bind.tls_config.unwrap()))
334
+ }
335
+ _ => unreachable!(),
336
+ },
337
+ BindAddress::UnixSocket(_) => match bind.tls_config {
338
+ Some(tls_config) => Listener::UnixTls((revive_unix_socket(fd)?, tls_config)),
339
+ None => Listener::Unix(revive_unix_socket(fd)?),
340
+ },
341
+ };
342
+ Ok(bound)
343
+ }
264
344
  }
265
345
 
266
346
  impl TryFrom<Bind> for Listener {
@@ -285,6 +365,27 @@ impl TryFrom<Bind> for Listener {
285
365
  }
286
366
  }
287
367
 
368
+ fn revive_tcp_socket(fd: RawFd) -> Result<TcpListener> {
369
+ let socket = unsafe { Socket::from_raw_fd(fd) };
370
+ socket.set_reuse_port(true).ok();
371
+ socket.set_reuse_address(true).ok();
372
+ socket.set_nonblocking(true).ok();
373
+ socket.set_nodelay(true).ok();
374
+ socket.set_recv_buffer_size(262_144).ok();
375
+ socket.set_cloexec(true)?;
376
+ socket.listen(1024)?;
377
+ Ok(socket.into())
378
+ }
379
+
380
+ fn revive_unix_socket(fd: RawFd) -> Result<UnixListener> {
381
+ let socket = unsafe { Socket::from_raw_fd(fd) };
382
+ socket.set_nonblocking(true).ok();
383
+ socket.listen(1024)?;
384
+ socket.set_cloexec(true)?;
385
+
386
+ Ok(socket.into())
387
+ }
388
+
288
389
  fn connect_tcp_socket(addr: IpAddr, port: u16) -> Result<TcpListener> {
289
390
  let domain = match addr {
290
391
  IpAddr::V4(_) => Domain::IPV4,
@@ -297,7 +398,7 @@ fn connect_tcp_socket(addr: IpAddr, port: u16) -> Result<TcpListener> {
297
398
  socket.set_nonblocking(true).ok();
298
399
  socket.set_nodelay(true).ok();
299
400
  socket.set_recv_buffer_size(262_144).ok();
300
- info!("Binding to {:?}", socket_address);
401
+ socket.set_only_v6(false).ok();
301
402
  socket.bind(&socket_address.into())?;
302
403
  socket.listen(1024)?;
303
404
  Ok(socket.into())
@@ -310,7 +411,6 @@ fn connect_unix_socket(path: &PathBuf) -> Result<UnixListener> {
310
411
 
311
412
  let socket_address = socket2::SockAddr::unix(path)?;
312
413
 
313
- info!("Binding to {:?}", path);
314
414
  socket.bind(&socket_address)?;
315
415
  socket.listen(1024)?;
316
416
 
@@ -0,0 +1,153 @@
1
+ use super::middlewares::*;
2
+ use crate::server::{
3
+ itsi_service::RequestContext,
4
+ types::{HttpRequest, HttpResponse},
5
+ };
6
+ use async_trait::async_trait;
7
+ use either::Either;
8
+ use magnus::error::Result;
9
+ use std::cmp::Ordering;
10
+
11
+ #[derive(Debug)]
12
+ pub enum Middleware {
13
+ AllowList(AllowList),
14
+ AuthAPIKey(AuthAPIKey),
15
+ AuthBasic(AuthBasic),
16
+ AuthJwt(Box<AuthJwt>),
17
+ CacheControl(CacheControl),
18
+ Compression(Compression),
19
+ Cors(Box<Cors>),
20
+ DenyList(DenyList),
21
+ ETag(ETag),
22
+ IntrusionProtection(IntrusionProtection),
23
+ LogRequests(LogRequests),
24
+ Proxy(Proxy),
25
+ RateLimit(RateLimit),
26
+ Redirect(Redirect),
27
+ RequestHeaders(RequestHeaders),
28
+ ResponseHeaders(ResponseHeaders),
29
+ RubyApp(RubyApp),
30
+ StaticAssets(StaticAssets),
31
+ }
32
+
33
+ #[async_trait]
34
+ impl MiddlewareLayer for Middleware {
35
+ /// Called just once, to initialize the middleware state.
36
+ async fn initialize(&self) -> Result<()> {
37
+ match self {
38
+ Middleware::DenyList(filter) => filter.initialize().await,
39
+ Middleware::AllowList(filter) => filter.initialize().await,
40
+ Middleware::AuthBasic(filter) => filter.initialize().await,
41
+ Middleware::AuthJwt(filter) => filter.initialize().await,
42
+ Middleware::AuthAPIKey(filter) => filter.initialize().await,
43
+ Middleware::IntrusionProtection(filter) => filter.initialize().await,
44
+ Middleware::RateLimit(filter) => filter.initialize().await,
45
+ Middleware::RequestHeaders(filter) => filter.initialize().await,
46
+ Middleware::ResponseHeaders(filter) => filter.initialize().await,
47
+ Middleware::CacheControl(filter) => filter.initialize().await,
48
+ Middleware::Cors(filter) => filter.initialize().await,
49
+ Middleware::ETag(filter) => filter.initialize().await,
50
+ Middleware::StaticAssets(filter) => filter.initialize().await,
51
+ Middleware::Compression(filter) => filter.initialize().await,
52
+ Middleware::LogRequests(filter) => filter.initialize().await,
53
+ Middleware::Redirect(filter) => filter.initialize().await,
54
+ Middleware::Proxy(filter) => filter.initialize().await,
55
+ Middleware::RubyApp(filter) => filter.initialize().await,
56
+ }
57
+ }
58
+
59
+ async fn before(
60
+ &self,
61
+ req: HttpRequest,
62
+ context: &mut RequestContext,
63
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
64
+ match self {
65
+ Middleware::DenyList(filter) => filter.before(req, context).await,
66
+ Middleware::AllowList(filter) => filter.before(req, context).await,
67
+ Middleware::AuthBasic(filter) => filter.before(req, context).await,
68
+ Middleware::AuthJwt(filter) => filter.before(req, context).await,
69
+ Middleware::AuthAPIKey(filter) => filter.before(req, context).await,
70
+ Middleware::IntrusionProtection(filter) => filter.before(req, context).await,
71
+ Middleware::RequestHeaders(filter) => filter.before(req, context).await,
72
+ Middleware::ResponseHeaders(filter) => filter.before(req, context).await,
73
+ Middleware::RateLimit(filter) => filter.before(req, context).await,
74
+ Middleware::CacheControl(filter) => filter.before(req, context).await,
75
+ Middleware::Cors(filter) => filter.before(req, context).await,
76
+ Middleware::ETag(filter) => filter.before(req, context).await,
77
+ Middleware::StaticAssets(filter) => filter.before(req, context).await,
78
+ Middleware::Compression(filter) => filter.before(req, context).await,
79
+ Middleware::LogRequests(filter) => filter.before(req, context).await,
80
+ Middleware::Redirect(filter) => filter.before(req, context).await,
81
+ Middleware::Proxy(filter) => filter.before(req, context).await,
82
+ Middleware::RubyApp(filter) => filter.before(req, context).await,
83
+ }
84
+ }
85
+
86
+ async fn after(&self, res: HttpResponse, context: &mut RequestContext) -> HttpResponse {
87
+ match self {
88
+ Middleware::DenyList(filter) => filter.after(res, context).await,
89
+ Middleware::AllowList(filter) => filter.after(res, context).await,
90
+ Middleware::AuthBasic(filter) => filter.after(res, context).await,
91
+ Middleware::AuthJwt(filter) => filter.after(res, context).await,
92
+ Middleware::AuthAPIKey(filter) => filter.after(res, context).await,
93
+ Middleware::IntrusionProtection(filter) => filter.after(res, context).await,
94
+ Middleware::RateLimit(filter) => filter.after(res, context).await,
95
+ Middleware::RequestHeaders(filter) => filter.after(res, context).await,
96
+ Middleware::ResponseHeaders(filter) => filter.after(res, context).await,
97
+ Middleware::CacheControl(filter) => filter.after(res, context).await,
98
+ Middleware::Cors(filter) => filter.after(res, context).await,
99
+ Middleware::ETag(filter) => filter.after(res, context).await,
100
+ Middleware::StaticAssets(filter) => filter.after(res, context).await,
101
+ Middleware::Compression(filter) => filter.after(res, context).await,
102
+ Middleware::LogRequests(filter) => filter.after(res, context).await,
103
+ Middleware::Redirect(filter) => filter.after(res, context).await,
104
+ Middleware::Proxy(filter) => filter.after(res, context).await,
105
+ Middleware::RubyApp(filter) => filter.after(res, context).await,
106
+ }
107
+ }
108
+ }
109
+
110
+ impl Middleware {
111
+ fn variant_order(&self) -> usize {
112
+ match self {
113
+ Middleware::DenyList(_) => 0,
114
+ Middleware::AllowList(_) => 1,
115
+ Middleware::IntrusionProtection(_) => 2,
116
+ Middleware::Redirect(_) => 3,
117
+ Middleware::LogRequests(_) => 4,
118
+ Middleware::CacheControl(_) => 5,
119
+ Middleware::RequestHeaders(_) => 6,
120
+ Middleware::ResponseHeaders(_) => 7,
121
+ Middleware::AuthBasic(_) => 8,
122
+ Middleware::AuthJwt(_) => 9,
123
+ Middleware::AuthAPIKey(_) => 10,
124
+ Middleware::RateLimit(_) => 11,
125
+ Middleware::ETag(_) => 12,
126
+ Middleware::Compression(_) => 13,
127
+ Middleware::Proxy(_) => 14,
128
+ Middleware::Cors(_) => 15,
129
+ Middleware::StaticAssets(_) => 16,
130
+ Middleware::RubyApp(_) => 17,
131
+ }
132
+ }
133
+ }
134
+
135
+ impl PartialEq for Middleware {
136
+ fn eq(&self, other: &Self) -> bool {
137
+ self.variant_order() == other.variant_order()
138
+ }
139
+ }
140
+
141
+ impl Eq for Middleware {}
142
+
143
+ impl PartialOrd for Middleware {
144
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
145
+ Some(self.variant_order().cmp(&other.variant_order()))
146
+ }
147
+ }
148
+
149
+ impl Ord for Middleware {
150
+ fn cmp(&self, other: &Self) -> Ordering {
151
+ self.variant_order().cmp(&other.variant_order())
152
+ }
153
+ }
@@ -0,0 +1,47 @@
1
+ use super::{ErrorResponse, FromValue, MiddlewareLayer};
2
+ use crate::server::{
3
+ itsi_service::RequestContext,
4
+ types::{HttpRequest, HttpResponse},
5
+ };
6
+ use async_trait::async_trait;
7
+ use either::Either;
8
+ use itsi_error::ItsiError;
9
+ use magnus::error::Result;
10
+ use regex::RegexSet;
11
+ use serde::Deserialize;
12
+ use std::sync::OnceLock;
13
+
14
+ #[derive(Debug, Clone, Deserialize)]
15
+ pub struct AllowList {
16
+ #[serde(skip_deserializing)]
17
+ pub allowed_ips: OnceLock<RegexSet>,
18
+ pub allowed_patterns: Vec<String>,
19
+ pub error_response: ErrorResponse,
20
+ }
21
+
22
+ #[async_trait]
23
+ impl MiddlewareLayer for AllowList {
24
+ async fn initialize(&self) -> Result<()> {
25
+ let allowed_ips = RegexSet::new(&self.allowed_patterns).map_err(ItsiError::default)?;
26
+ self.allowed_ips
27
+ .set(allowed_ips)
28
+ .map_err(|e| ItsiError::default(format!("Failed to set allowed IPs: {:?}", e)))?;
29
+ Ok(())
30
+ }
31
+
32
+ async fn before(
33
+ &self,
34
+ req: HttpRequest,
35
+ context: &mut RequestContext,
36
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
37
+ if let Some(allowed_ips) = self.allowed_ips.get() {
38
+ if !allowed_ips.is_match(&context.addr) {
39
+ return Ok(Either::Right(
40
+ self.error_response.to_http_response(&req).await,
41
+ ));
42
+ }
43
+ }
44
+ Ok(Either::Left(req))
45
+ }
46
+ }
47
+ impl FromValue for AllowList {}
@@ -0,0 +1,58 @@
1
+ use crate::server::{
2
+ itsi_service::RequestContext,
3
+ types::{HttpRequest, HttpResponse, RequestExt},
4
+ };
5
+
6
+ use super::{error_response::ErrorResponse, token_source::TokenSource, FromValue, MiddlewareLayer};
7
+
8
+ use async_trait::async_trait;
9
+ use either::Either;
10
+ use magnus::error::Result;
11
+ use serde::Deserialize;
12
+
13
+ /// A simple API key filter.
14
+ /// The API key can be given inside the header or a query string
15
+ /// Keys are validated against a list of allowed key values (Changing these requires a restart)
16
+ ///
17
+ #[derive(Debug, Clone, Deserialize)]
18
+ pub struct AuthAPIKey {
19
+ pub valid_keys: Vec<String>,
20
+ pub token_source: TokenSource,
21
+ pub error_response: ErrorResponse,
22
+ }
23
+
24
+ #[async_trait]
25
+ impl MiddlewareLayer for AuthAPIKey {
26
+ async fn before(
27
+ &self,
28
+ req: HttpRequest,
29
+ _context: &mut RequestContext,
30
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
31
+ let submitted_value = match &self.token_source {
32
+ TokenSource::Header { name, prefix } => {
33
+ if let Some(header) = req.header(name) {
34
+ if let Some(prefix) = prefix {
35
+ Some(header.strip_prefix(prefix).unwrap_or("").trim_ascii())
36
+ } else {
37
+ Some(header.trim_ascii())
38
+ }
39
+ } else {
40
+ None
41
+ }
42
+ }
43
+ TokenSource::Query(query_name) => req.query_param(query_name),
44
+ };
45
+ if !self
46
+ .valid_keys
47
+ .iter()
48
+ .any(|key| submitted_value.is_some_and(|sv| sv == key))
49
+ {
50
+ Ok(Either::Right(
51
+ self.error_response.to_http_response(&req).await,
52
+ ))
53
+ } else {
54
+ Ok(Either::Left(req))
55
+ }
56
+ }
57
+ }
58
+ impl FromValue for AuthAPIKey {}
@@ -0,0 +1,82 @@
1
+ use async_trait::async_trait;
2
+ use base64::{engine::general_purpose, Engine};
3
+ use bytes::Bytes;
4
+ use either::Either;
5
+ use http::{Response, StatusCode};
6
+ use http_body_util::{combinators::BoxBody, Full};
7
+ use magnus::error::Result;
8
+ use serde::{Deserialize, Serialize};
9
+ use std::collections::HashMap;
10
+ use std::str;
11
+
12
+ use crate::server::{
13
+ itsi_service::RequestContext,
14
+ types::{HttpRequest, HttpResponse, RequestExt},
15
+ };
16
+
17
+ use super::{FromValue, MiddlewareLayer};
18
+
19
+ #[derive(Debug, Clone, Serialize, Deserialize)]
20
+ pub struct AuthBasic {
21
+ pub realm: String,
22
+ /// Maps usernames to passwords.
23
+ pub credential_pairs: HashMap<String, String>,
24
+ }
25
+
26
+ impl AuthBasic {
27
+ fn basic_auth_failed_response(&self) -> HttpResponse {
28
+ Response::builder()
29
+ .status(StatusCode::UNAUTHORIZED)
30
+ .header(
31
+ "WWW-Authenticate",
32
+ format!("Basic realm=\"{}\"", self.realm),
33
+ )
34
+ .body(BoxBody::new(Full::new(Bytes::from("Unauthorized"))))
35
+ .unwrap()
36
+ }
37
+ }
38
+ #[async_trait]
39
+ impl MiddlewareLayer for AuthBasic {
40
+ async fn before(
41
+ &self,
42
+ req: HttpRequest,
43
+ _context: &mut RequestContext,
44
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
45
+ // Retrieve the Authorization header.
46
+ let auth_header = req.header("Authorization");
47
+
48
+ if !auth_header.is_some_and(|header| header.starts_with("Basic ")) {
49
+ return Ok(Either::Right(self.basic_auth_failed_response()));
50
+ }
51
+
52
+ let auth_header = auth_header.unwrap();
53
+
54
+ let encoded_credentials = &auth_header["Basic ".len()..];
55
+ let decoded = match general_purpose::STANDARD.decode(encoded_credentials) {
56
+ Ok(bytes) => bytes,
57
+ Err(_) => {
58
+ return Ok(Either::Right(self.basic_auth_failed_response()));
59
+ }
60
+ };
61
+
62
+ let decoded_str = match str::from_utf8(&decoded) {
63
+ Ok(s) => s,
64
+ Err(_) => {
65
+ return Ok(Either::Right(self.basic_auth_failed_response()));
66
+ }
67
+ };
68
+
69
+ let mut parts = decoded_str.splitn(2, ':');
70
+ let username = parts.next().unwrap_or("");
71
+ let password = parts.next().unwrap_or("");
72
+
73
+ match self.credential_pairs.get(username) {
74
+ Some(expected_password) if expected_password == password => Ok(Either::Left(req)),
75
+ _ => {
76
+ return Ok(Either::Right(self.basic_auth_failed_response()));
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ impl FromValue for AuthBasic {}