itsi-scheduler 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 (105) hide show
  1. checksums.yaml +4 -4
  2. data/CODE_OF_CONDUCT.md +7 -0
  3. data/Cargo.lock +75 -14
  4. data/README.md +5 -0
  5. data/_index.md +7 -0
  6. data/ext/itsi_error/src/lib.rs +9 -0
  7. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  8. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  9. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  10. data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  11. data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
  12. data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
  13. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
  14. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
  15. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
  16. data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
  17. data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
  18. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
  19. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
  20. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
  21. data/ext/itsi_rb_helpers/Cargo.toml +1 -0
  22. data/ext/itsi_rb_helpers/src/heap_value.rs +18 -0
  23. data/ext/itsi_rb_helpers/src/lib.rs +34 -7
  24. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  25. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  26. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  27. 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
  28. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
  29. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
  30. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
  31. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
  32. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
  33. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
  34. data/ext/itsi_server/Cargo.toml +69 -30
  35. data/ext/itsi_server/src/lib.rs +79 -147
  36. data/ext/itsi_server/src/{body_proxy → ruby_types/itsi_body_proxy}/big_bytes.rs +10 -5
  37. data/ext/itsi_server/src/{body_proxy/itsi_body_proxy.rs → ruby_types/itsi_body_proxy/mod.rs} +22 -3
  38. data/ext/itsi_server/src/ruby_types/itsi_grpc_request.rs +147 -0
  39. data/ext/itsi_server/src/ruby_types/itsi_grpc_response.rs +19 -0
  40. data/ext/itsi_server/src/ruby_types/itsi_grpc_stream/mod.rs +216 -0
  41. data/ext/itsi_server/src/{request/itsi_request.rs → ruby_types/itsi_http_request.rs} +101 -117
  42. data/ext/itsi_server/src/{response/itsi_response.rs → ruby_types/itsi_http_response.rs} +72 -41
  43. data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +225 -0
  44. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +355 -0
  45. data/ext/itsi_server/src/ruby_types/itsi_server.rs +82 -0
  46. data/ext/itsi_server/src/ruby_types/mod.rs +55 -0
  47. data/ext/itsi_server/src/server/bind.rs +13 -5
  48. data/ext/itsi_server/src/server/byte_frame.rs +32 -0
  49. data/ext/itsi_server/src/server/cache_store.rs +74 -0
  50. data/ext/itsi_server/src/server/itsi_service.rs +172 -0
  51. data/ext/itsi_server/src/server/lifecycle_event.rs +3 -0
  52. data/ext/itsi_server/src/server/listener.rs +102 -2
  53. data/ext/itsi_server/src/server/middleware_stack/middleware.rs +153 -0
  54. data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +47 -0
  55. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +58 -0
  56. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +82 -0
  57. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +321 -0
  58. data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +139 -0
  59. data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +300 -0
  60. data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +287 -0
  61. data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +48 -0
  62. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +127 -0
  63. data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +191 -0
  64. data/ext/itsi_server/src/server/middleware_stack/middlewares/grpc_service.rs +72 -0
  65. data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +85 -0
  66. data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +195 -0
  67. data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
  68. data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +82 -0
  69. data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +216 -0
  70. data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +124 -0
  71. data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +76 -0
  72. data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +43 -0
  73. data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +34 -0
  74. data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +93 -0
  75. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +162 -0
  76. data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +158 -0
  77. data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +12 -0
  78. data/ext/itsi_server/src/server/middleware_stack/mod.rs +315 -0
  79. data/ext/itsi_server/src/server/mod.rs +8 -1
  80. data/ext/itsi_server/src/server/process_worker.rs +38 -12
  81. data/ext/itsi_server/src/server/rate_limiter.rs +565 -0
  82. data/ext/itsi_server/src/server/request_job.rs +11 -0
  83. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +119 -42
  84. data/ext/itsi_server/src/server/serve_strategy/mod.rs +9 -6
  85. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +256 -111
  86. data/ext/itsi_server/src/server/signal.rs +19 -0
  87. data/ext/itsi_server/src/server/static_file_server.rs +984 -0
  88. data/ext/itsi_server/src/server/thread_worker.rs +139 -94
  89. data/ext/itsi_server/src/server/types.rs +43 -0
  90. data/ext/itsi_tracing/Cargo.toml +1 -0
  91. data/ext/itsi_tracing/src/lib.rs +216 -45
  92. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
  93. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
  94. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
  95. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
  96. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
  97. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
  98. data/lib/itsi/scheduler/version.rb +1 -1
  99. data/lib/itsi/scheduler.rb +2 -2
  100. metadata +77 -12
  101. data/ext/itsi_server/extconf.rb +0 -6
  102. data/ext/itsi_server/src/body_proxy/mod.rs +0 -2
  103. data/ext/itsi_server/src/request/mod.rs +0 -1
  104. data/ext/itsi_server/src/response/mod.rs +0 -1
  105. data/ext/itsi_server/src/server/itsi_server.rs +0 -288
@@ -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
+ }
@@ -0,0 +1,216 @@
1
+ use std::{
2
+ collections::HashMap,
3
+ convert::Infallible,
4
+ net::SocketAddr,
5
+ sync::{Arc, OnceLock},
6
+ time::Duration,
7
+ };
8
+
9
+ use crate::server::{
10
+ bind::{Bind, BindAddress},
11
+ itsi_service::RequestContext,
12
+ types::{HttpRequest, HttpResponse},
13
+ };
14
+
15
+ use super::{string_rewrite::StringRewrite, ErrorResponse, FromValue, MiddlewareLayer};
16
+
17
+ use async_trait::async_trait;
18
+ use either::Either;
19
+ use futures::TryStreamExt;
20
+ use http::Response;
21
+ use http_body_util::{combinators::BoxBody, BodyExt, StreamBody};
22
+ use hyper::body::Frame;
23
+ use magnus::error::Result;
24
+ use reqwest::{dns::Resolve, Body, Client, Url};
25
+ use serde::Deserialize;
26
+ use tracing::error;
27
+
28
+ #[derive(Debug, Clone, Deserialize)]
29
+ pub struct Proxy {
30
+ pub to: StringRewrite,
31
+ pub backends: Vec<String>,
32
+ pub headers: HashMap<String, Option<ProxiedHeader>>,
33
+ pub verify_ssl: bool,
34
+ pub timeout: u64,
35
+ pub tls_sni: bool,
36
+ #[serde(skip_deserializing)]
37
+ pub client: OnceLock<Client>,
38
+ pub error_response: ErrorResponse,
39
+ }
40
+
41
+ #[derive(Debug, Clone, Deserialize)]
42
+ pub enum ProxiedHeader {
43
+ #[serde(rename(deserialize = "value"))]
44
+ String(String),
45
+ #[serde(rename(deserialize = "rewrite"))]
46
+ StringRewrite(StringRewrite),
47
+ }
48
+
49
+ impl ProxiedHeader {
50
+ pub fn to_string(&self, req: &HttpRequest, context: &RequestContext) -> String {
51
+ match self {
52
+ ProxiedHeader::String(value) => value.clone(),
53
+ ProxiedHeader::StringRewrite(rewrite) => rewrite.rewrite_request(req, context),
54
+ }
55
+ }
56
+ }
57
+
58
+ #[derive(Debug, Clone)]
59
+ pub struct Resolver {
60
+ backends: Arc<Vec<SocketAddr>>,
61
+ }
62
+
63
+ /// An iterator that owns an Arc to the backend list and iterates over it.
64
+ pub struct ResolverIter {
65
+ backends: Arc<Vec<SocketAddr>>,
66
+ index: usize,
67
+ }
68
+
69
+ impl Iterator for ResolverIter {
70
+ type Item = SocketAddr;
71
+
72
+ fn next(&mut self) -> Option<Self::Item> {
73
+ if self.index < self.backends.len() {
74
+ let addr = self.backends[self.index];
75
+ self.index += 1;
76
+ Some(addr)
77
+ } else {
78
+ None
79
+ }
80
+ }
81
+ }
82
+
83
+ impl Resolve for Resolver {
84
+ fn resolve(&self, _name: reqwest::dns::Name) -> reqwest::dns::Resolving {
85
+ let backends = self.backends.clone();
86
+ let fut = async move {
87
+ let iter = ResolverIter { backends, index: 0 };
88
+ Ok(Box::new(iter) as Box<dyn Iterator<Item = SocketAddr> + Send>)
89
+ };
90
+ Box::pin(fut)
91
+ }
92
+ }
93
+
94
+ #[async_trait]
95
+ impl MiddlewareLayer for Proxy {
96
+ async fn initialize(&self) -> Result<()> {
97
+ let backends = self
98
+ .backends
99
+ .iter()
100
+ .filter_map(|be| {
101
+ let bind: Bind = be.parse().ok()?;
102
+ match (bind.address, bind.port) {
103
+ (BindAddress::Ip(ip_addr), port) => {
104
+ Some(SocketAddr::new(ip_addr, port.unwrap()))
105
+ }
106
+ (BindAddress::UnixSocket(_), _) => None,
107
+ }
108
+ })
109
+ .collect::<Vec<_>>();
110
+
111
+ self.client
112
+ .set(
113
+ Client::builder()
114
+ .timeout(Duration::from_secs(self.timeout))
115
+ .danger_accept_invalid_certs(!self.verify_ssl)
116
+ .danger_accept_invalid_hostnames(!self.verify_ssl)
117
+ .dns_resolver(Arc::new(Resolver {
118
+ backends: Arc::new(backends),
119
+ }))
120
+ .tls_sni(self.tls_sni)
121
+ .build()
122
+ .map_err(|e| {
123
+ magnus::Error::new(
124
+ magnus::exception::runtime_error(),
125
+ format!("Failed to build Reqwest client: {}", e),
126
+ )
127
+ })?,
128
+ )
129
+ .map_err(|_e| {
130
+ magnus::Error::new(
131
+ magnus::exception::exception(),
132
+ "Failed to save resolver backends",
133
+ )
134
+ })?;
135
+ Ok(())
136
+ }
137
+
138
+ async fn before(
139
+ &self,
140
+ req: HttpRequest,
141
+ context: &mut RequestContext,
142
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
143
+ let url = self.to.rewrite_request(&req, context);
144
+ let error_response = self.error_response.to_http_response(&req).await;
145
+
146
+ let destination = match Url::parse(&url) {
147
+ Ok(dest) => dest,
148
+ Err(_) => return Ok(Either::Right(error_response)),
149
+ };
150
+
151
+ let host_str = destination.host_str().unwrap_or_else(|| {
152
+ req.headers()
153
+ .get("Host")
154
+ .and_then(|h| h.to_str().ok())
155
+ .unwrap_or("")
156
+ });
157
+
158
+ let mut reqwest_builder = self
159
+ .client
160
+ .get()
161
+ .unwrap()
162
+ .request(req.method().clone(), url);
163
+
164
+ // Forward incoming headers unless they're in remove_headers or overridden.
165
+ for (name, value) in req.headers().iter() {
166
+ let name_str = name.as_str();
167
+ if self.headers.contains_key(name_str) {
168
+ continue;
169
+ }
170
+ reqwest_builder = reqwest_builder.header(name, value);
171
+ }
172
+
173
+ // Add the host header if it's not overridden and host_str is non-empty.
174
+ if !self.headers.contains_key("host") && !host_str.is_empty() {
175
+ reqwest_builder = reqwest_builder.header("Host", host_str);
176
+ }
177
+
178
+ // Add overriding headers.
179
+ for (name, header_value) in self.headers.iter() {
180
+ if let Some(header_value) = header_value {
181
+ reqwest_builder =
182
+ reqwest_builder.header(name, header_value.to_string(&req, context));
183
+ }
184
+ }
185
+
186
+ let reqwest_builder = reqwest_builder.body(Body::wrap_stream(req.into_data_stream()));
187
+ let reqwest_response = reqwest_builder.send().await;
188
+
189
+ let response = match reqwest_response {
190
+ Ok(response) => {
191
+ let status = response.status();
192
+ let mut builder = Response::builder().status(status);
193
+ for (hn, hv) in response.headers() {
194
+ builder = builder.header(hn, hv);
195
+ }
196
+ let response = builder.body(BoxBody::new(StreamBody::new(
197
+ response
198
+ .bytes_stream()
199
+ .map_ok(Frame::data)
200
+ .map_err(|_| -> Infallible { unreachable!("We handle IO errors above") }),
201
+ )));
202
+ if let Ok(response) = response {
203
+ response
204
+ } else {
205
+ error_response
206
+ }
207
+ }
208
+ Err(e) => {
209
+ error!("Error sending request: {}", e);
210
+ error_response
211
+ }
212
+ };
213
+ Ok(Either::Right(response))
214
+ }
215
+ }
216
+ impl FromValue for Proxy {}
@@ -0,0 +1,124 @@
1
+ use super::{token_source::TokenSource, ErrorResponse, FromValue, MiddlewareLayer};
2
+ use crate::server::{
3
+ itsi_service::RequestContext,
4
+ rate_limiter::{
5
+ create_rate_limit_key, get_rate_limiter, RateLimitError, RateLimiter, RateLimiterConfig,
6
+ },
7
+ types::{HttpRequest, HttpResponse, RequestExt},
8
+ };
9
+ use async_trait::async_trait;
10
+ use either::Either;
11
+ use magnus::error::Result;
12
+ use serde::Deserialize;
13
+ use std::sync::{Arc, OnceLock};
14
+ use std::time::Duration;
15
+
16
+ #[derive(Debug, Clone, Deserialize)]
17
+ pub struct RateLimit {
18
+ pub requests: u64,
19
+ pub seconds: u64,
20
+ pub key: RateLimitKey,
21
+ #[serde(skip_deserializing)]
22
+ pub rate_limiter: OnceLock<Arc<dyn RateLimiter>>,
23
+ pub store_config: RateLimiterConfig,
24
+ pub error_response: ErrorResponse,
25
+ }
26
+
27
+ #[derive(Debug, Clone, Deserialize)]
28
+ pub enum RateLimitKey {
29
+ #[serde(rename(deserialize = "address"))]
30
+ SocketAddress,
31
+ #[serde(rename(deserialize = "parameter"))]
32
+ Parameter(TokenSource),
33
+ }
34
+
35
+ #[async_trait]
36
+ impl MiddlewareLayer for RateLimit {
37
+ async fn initialize(&self) -> Result<()> {
38
+ // Instantiate our rate limiter based on the rate limit config here.
39
+ // This will automatically fall back to in-memory if Redis fails
40
+ if let Ok(limiter) = get_rate_limiter(&self.store_config).await {
41
+ let _ = self.rate_limiter.set(limiter);
42
+ }
43
+ Ok(())
44
+ }
45
+
46
+ async fn before(
47
+ &self,
48
+ req: HttpRequest,
49
+ context: &mut RequestContext,
50
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
51
+ // Get the key to rate limit on
52
+ let key_value = match &self.key {
53
+ RateLimitKey::SocketAddress => {
54
+ // Use the socket address from the context
55
+ &context.addr
56
+ }
57
+ RateLimitKey::Parameter(token_source) => {
58
+ match token_source {
59
+ TokenSource::Header { name, prefix } => {
60
+ if let Some(header) = req.header(name) {
61
+ if let Some(prefix) = prefix {
62
+ header.strip_prefix(prefix).unwrap_or("").trim_ascii()
63
+ } else {
64
+ header.trim_ascii()
65
+ }
66
+ } else {
67
+ // If no token is found, skip rate limiting
68
+ tracing::warn!("No token found in header for rate limiting");
69
+ return Ok(Either::Left(req));
70
+ }
71
+ }
72
+ TokenSource::Query(query_name) => {
73
+ if let Some(value) = req.query_param(query_name) {
74
+ value
75
+ } else {
76
+ // If no token is found, skip rate limiting
77
+ tracing::warn!("No token found in query for rate limiting");
78
+ return Ok(Either::Left(req));
79
+ }
80
+ }
81
+ }
82
+ }
83
+ };
84
+
85
+ // Create a rate limit key
86
+ let rate_limit_key = create_rate_limit_key(key_value, req.uri().path());
87
+
88
+ // Get the rate limiter
89
+ if let Some(limiter) = self.rate_limiter.get() {
90
+ // Check if rate limit is exceeded
91
+ let timeout = Duration::from_secs(self.seconds);
92
+ let limit = self.requests;
93
+
94
+ match limiter.check_limit(&rate_limit_key, limit, timeout).await {
95
+ Ok(_) => {
96
+ // Rate limit not exceeded, allow request
97
+ Ok(Either::Left(req))
98
+ }
99
+ Err(RateLimitError::RateLimitExceeded { limit, count }) => {
100
+ // Rate limit exceeded, return error response
101
+ tracing::info!(
102
+ "Rate limit exceeded for key '{}': {}/{} requests",
103
+ rate_limit_key,
104
+ count,
105
+ limit
106
+ );
107
+ Ok(Either::Right(
108
+ self.error_response.to_http_response(&req).await,
109
+ ))
110
+ }
111
+ Err(e) => {
112
+ // Other error, log and allow request (fail open)
113
+ tracing::error!("Rate limiter error: {:?}", e);
114
+ Ok(Either::Left(req))
115
+ }
116
+ }
117
+ } else {
118
+ // If rate limiter is not initialized, allow request
119
+ tracing::warn!("Rate limiter not initialized");
120
+ Ok(Either::Left(req))
121
+ }
122
+ }
123
+ }
124
+ impl FromValue for RateLimit {}
@@ -0,0 +1,76 @@
1
+ use crate::server::{
2
+ itsi_service::RequestContext,
3
+ types::{HttpRequest, HttpResponse},
4
+ };
5
+
6
+ use super::{string_rewrite::StringRewrite, FromValue, MiddlewareLayer};
7
+
8
+ use async_trait::async_trait;
9
+ use either::Either;
10
+ use http::{Response, StatusCode};
11
+ use http_body_util::{combinators::BoxBody, Empty};
12
+ use magnus::error::Result;
13
+ use serde::Deserialize;
14
+
15
+ /// A simple API key filter.
16
+ /// The API key can be given inside the header or a query string
17
+ /// Keys are validated against a list of allowed key values (Changing these requires a restart)
18
+ ///
19
+ #[derive(Debug, Clone, Deserialize)]
20
+ pub struct Redirect {
21
+ pub to: StringRewrite,
22
+ #[serde(default)]
23
+ #[serde(rename(deserialize = "type"))]
24
+ pub redirect_type: RedirectType,
25
+ }
26
+
27
+ #[derive(Debug, Clone, Deserialize, Default)]
28
+ pub enum RedirectType {
29
+ #[serde(rename(deserialize = "permanent"))]
30
+ #[default]
31
+ Permanent,
32
+ #[serde(rename(deserialize = "temporary"))]
33
+ Temporary,
34
+ #[serde(rename(deserialize = "found"))]
35
+ Found,
36
+ #[serde(rename(deserialize = "moved_permanently"))]
37
+ MovedPermanently,
38
+ }
39
+
40
+ #[async_trait]
41
+ impl MiddlewareLayer for Redirect {
42
+ async fn before(
43
+ &self,
44
+ req: HttpRequest,
45
+ context: &mut RequestContext,
46
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
47
+ Ok(Either::Right(self.redirect_response(&req, context)?))
48
+ }
49
+ }
50
+
51
+ impl Redirect {
52
+ pub fn redirect_response(
53
+ &self,
54
+ req: &HttpRequest,
55
+ context: &mut RequestContext,
56
+ ) -> Result<HttpResponse> {
57
+ let mut response = Response::new(BoxBody::new(Empty::new()));
58
+ *response.status_mut() = match self.redirect_type {
59
+ RedirectType::Permanent => StatusCode::PERMANENT_REDIRECT,
60
+ RedirectType::Temporary => StatusCode::TEMPORARY_REDIRECT,
61
+ RedirectType::MovedPermanently => StatusCode::MOVED_PERMANENTLY,
62
+ RedirectType::Found => StatusCode::FOUND,
63
+ };
64
+ response.headers_mut().append(
65
+ "Location",
66
+ self.to.rewrite_request(req, context).parse().map_err(|e| {
67
+ magnus::Error::new(
68
+ magnus::exception::exception(),
69
+ format!("Invalid Rewrite String: {:?}: {}", self.to, e),
70
+ )
71
+ })?,
72
+ );
73
+ Ok(response)
74
+ }
75
+ }
76
+ impl FromValue for Redirect {}
@@ -0,0 +1,43 @@
1
+ use std::collections::HashMap;
2
+
3
+ use super::{FromValue, MiddlewareLayer};
4
+ use crate::server::{
5
+ itsi_service::RequestContext,
6
+ types::{HttpRequest, HttpResponse},
7
+ };
8
+ use async_trait::async_trait;
9
+ use either::Either;
10
+ use http::HeaderName;
11
+ use magnus::error::Result;
12
+ use serde::Deserialize;
13
+
14
+ #[derive(Debug, Clone, Deserialize)]
15
+ pub struct RequestHeaders {
16
+ pub additions: HashMap<String, Vec<String>>,
17
+ pub removals: Vec<String>,
18
+ }
19
+
20
+ #[async_trait]
21
+ impl MiddlewareLayer for RequestHeaders {
22
+ async fn before(
23
+ &self,
24
+ mut req: HttpRequest,
25
+ _: &mut RequestContext,
26
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
27
+ let headers = req.headers_mut();
28
+ for removal in &self.removals {
29
+ headers.remove(removal);
30
+ }
31
+ for (header_name, header_values) in &self.additions {
32
+ for header_value in header_values {
33
+ if let Ok(parsed_header_name) = header_name.parse::<HeaderName>() {
34
+ if let Ok(parsed_header_value) = header_value.parse() {
35
+ headers.append(parsed_header_name, parsed_header_value);
36
+ }
37
+ }
38
+ }
39
+ }
40
+ Ok(Either::Left(req))
41
+ }
42
+ }
43
+ impl FromValue for RequestHeaders {}
@@ -0,0 +1,34 @@
1
+ use std::collections::HashMap;
2
+
3
+ use super::{FromValue, MiddlewareLayer};
4
+ use crate::server::{itsi_service::RequestContext, types::HttpResponse};
5
+ use async_trait::async_trait;
6
+ use http::HeaderName;
7
+ use serde::Deserialize;
8
+
9
+ #[derive(Debug, Clone, Deserialize)]
10
+ pub struct ResponseHeaders {
11
+ pub additions: HashMap<String, Vec<String>>,
12
+ pub removals: Vec<String>,
13
+ }
14
+
15
+ #[async_trait]
16
+ impl MiddlewareLayer for ResponseHeaders {
17
+ async fn after(&self, mut resp: HttpResponse, _: &mut RequestContext) -> HttpResponse {
18
+ let headers = resp.headers_mut();
19
+ for removal in &self.removals {
20
+ headers.remove(removal);
21
+ }
22
+ for (header_name, header_values) in &self.additions {
23
+ for header_value in header_values {
24
+ if let Ok(parsed_header_name) = header_name.parse::<HeaderName>() {
25
+ if let Ok(parsed_header_value) = header_value.parse() {
26
+ headers.append(parsed_header_name, parsed_header_value);
27
+ }
28
+ }
29
+ }
30
+ }
31
+ resp
32
+ }
33
+ }
34
+ impl FromValue for ResponseHeaders {}