itsi 0.1.14 → 0.1.19

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 (169) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +126 -272
  3. data/Cargo.toml +6 -0
  4. data/crates/itsi_error/Cargo.toml +1 -0
  5. data/crates/itsi_error/src/lib.rs +100 -10
  6. data/crates/itsi_scheduler/src/itsi_scheduler.rs +1 -1
  7. data/crates/itsi_server/Cargo.toml +12 -11
  8. data/crates/itsi_server/src/default_responses/html/401.html +68 -0
  9. data/crates/itsi_server/src/default_responses/html/403.html +68 -0
  10. data/crates/itsi_server/src/default_responses/html/404.html +68 -0
  11. data/crates/itsi_server/src/default_responses/html/413.html +71 -0
  12. data/crates/itsi_server/src/default_responses/html/429.html +68 -0
  13. data/crates/itsi_server/src/default_responses/html/500.html +71 -0
  14. data/crates/itsi_server/src/default_responses/html/502.html +71 -0
  15. data/crates/itsi_server/src/default_responses/html/503.html +68 -0
  16. data/crates/itsi_server/src/default_responses/html/504.html +69 -0
  17. data/crates/itsi_server/src/default_responses/html/index.html +238 -0
  18. data/crates/itsi_server/src/default_responses/json/401.json +6 -0
  19. data/crates/itsi_server/src/default_responses/json/403.json +6 -0
  20. data/crates/itsi_server/src/default_responses/json/404.json +6 -0
  21. data/crates/itsi_server/src/default_responses/json/413.json +6 -0
  22. data/crates/itsi_server/src/default_responses/json/429.json +6 -0
  23. data/crates/itsi_server/src/default_responses/json/500.json +6 -0
  24. data/crates/itsi_server/src/default_responses/json/502.json +6 -0
  25. data/crates/itsi_server/src/default_responses/json/503.json +6 -0
  26. data/crates/itsi_server/src/default_responses/json/504.json +6 -0
  27. data/crates/itsi_server/src/default_responses/mod.rs +11 -0
  28. data/crates/itsi_server/src/lib.rs +58 -26
  29. data/crates/itsi_server/src/prelude.rs +2 -0
  30. data/crates/itsi_server/src/ruby_types/README.md +21 -0
  31. data/crates/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +8 -6
  32. data/crates/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
  33. data/crates/itsi_server/src/ruby_types/{itsi_grpc_stream → itsi_grpc_response_stream}/mod.rs +121 -73
  34. data/crates/itsi_server/src/ruby_types/itsi_http_request.rs +103 -40
  35. data/crates/itsi_server/src/ruby_types/itsi_http_response.rs +8 -5
  36. data/crates/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +4 -4
  37. data/crates/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +37 -17
  38. data/crates/itsi_server/src/ruby_types/itsi_server.rs +4 -3
  39. data/crates/itsi_server/src/ruby_types/mod.rs +6 -13
  40. data/crates/itsi_server/src/server/{bind.rs → binds/bind.rs} +23 -4
  41. data/crates/itsi_server/src/server/{listener.rs → binds/listener.rs} +24 -10
  42. data/crates/itsi_server/src/server/binds/mod.rs +4 -0
  43. data/crates/itsi_server/src/server/{tls.rs → binds/tls.rs} +9 -4
  44. data/crates/itsi_server/src/server/http_message_types.rs +97 -0
  45. data/crates/itsi_server/src/server/io_stream.rs +2 -1
  46. data/crates/itsi_server/src/server/middleware_stack/middleware.rs +28 -16
  47. data/crates/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +17 -8
  48. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +47 -18
  49. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +13 -9
  50. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +50 -29
  51. data/crates/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +5 -2
  52. data/crates/itsi_server/src/server/middleware_stack/middlewares/compression.rs +37 -48
  53. data/crates/itsi_server/src/server/middleware_stack/middlewares/cors.rs +25 -20
  54. data/crates/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +14 -7
  55. data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +190 -0
  56. data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +125 -95
  57. data/crates/itsi_server/src/server/middleware_stack/middlewares/etag.rs +9 -5
  58. data/crates/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +1 -4
  59. data/crates/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +25 -19
  60. data/crates/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +4 -4
  61. data/crates/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
  62. data/crates/itsi_server/src/server/middleware_stack/middlewares/mod.rs +9 -4
  63. data/crates/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +260 -62
  64. data/crates/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +29 -22
  65. data/crates/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +6 -6
  66. data/crates/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +6 -5
  67. data/crates/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +4 -2
  68. data/crates/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +51 -18
  69. data/crates/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +31 -13
  70. data/crates/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
  71. data/crates/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +13 -8
  72. data/crates/itsi_server/src/server/middleware_stack/mod.rs +101 -69
  73. data/crates/itsi_server/src/server/mod.rs +3 -9
  74. data/crates/itsi_server/src/server/process_worker.rs +21 -3
  75. data/crates/itsi_server/src/server/request_job.rs +2 -2
  76. data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +8 -3
  77. data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +26 -26
  78. data/crates/itsi_server/src/server/signal.rs +24 -41
  79. data/crates/itsi_server/src/server/size_limited_incoming.rs +101 -0
  80. data/crates/itsi_server/src/server/thread_worker.rs +59 -28
  81. data/crates/itsi_server/src/services/itsi_http_service.rs +239 -0
  82. data/crates/itsi_server/src/services/mime_types.rs +1416 -0
  83. data/crates/itsi_server/src/services/mod.rs +6 -0
  84. data/crates/itsi_server/src/services/password_hasher.rs +83 -0
  85. data/crates/itsi_server/src/{server → services}/rate_limiter.rs +35 -31
  86. data/crates/itsi_server/src/{server → services}/static_file_server.rs +521 -181
  87. data/crates/itsi_tracing/src/lib.rs +145 -55
  88. data/{Itsi.rb → foo/Itsi.rb} +6 -9
  89. data/gems/scheduler/Cargo.lock +7 -0
  90. data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
  91. data/gems/scheduler/test/helpers/test_helper.rb +0 -1
  92. data/gems/scheduler/test/test_address_resolve.rb +0 -1
  93. data/gems/scheduler/test/test_network_io.rb +1 -1
  94. data/gems/scheduler/test/test_process_wait.rb +0 -1
  95. data/gems/server/Cargo.lock +126 -272
  96. data/gems/server/exe/itsi +65 -19
  97. data/gems/server/itsi-server.gemspec +4 -3
  98. data/gems/server/lib/itsi/http_request/response_status_shortcodes.rb +74 -0
  99. data/gems/server/lib/itsi/http_request.rb +117 -17
  100. data/gems/server/lib/itsi/http_response.rb +2 -0
  101. data/gems/server/lib/itsi/passfile.rb +109 -0
  102. data/gems/server/lib/itsi/server/config/dsl.rb +171 -99
  103. data/gems/server/lib/itsi/server/config.rb +58 -23
  104. data/gems/server/lib/itsi/server/default_app/default_app.rb +25 -29
  105. data/gems/server/lib/itsi/server/default_app/index.html +113 -89
  106. data/gems/server/lib/itsi/server/{Itsi.rb → default_config/Itsi-rackup.rb} +1 -1
  107. data/gems/server/lib/itsi/server/default_config/Itsi.rb +107 -0
  108. data/gems/server/lib/itsi/server/grpc/grpc_call.rb +246 -0
  109. data/gems/server/lib/itsi/server/grpc/grpc_interface.rb +100 -0
  110. data/gems/server/lib/itsi/server/grpc/reflection/v1/reflection_pb.rb +26 -0
  111. data/gems/server/lib/itsi/server/grpc/reflection/v1/reflection_services_pb.rb +122 -0
  112. data/gems/server/lib/itsi/server/route_tester.rb +107 -0
  113. data/gems/server/lib/itsi/server/typed_handlers/param_parser.rb +200 -0
  114. data/gems/server/lib/itsi/server/typed_handlers/source_parser.rb +55 -0
  115. data/gems/server/lib/itsi/server/typed_handlers.rb +17 -0
  116. data/gems/server/lib/itsi/server/version.rb +1 -1
  117. data/gems/server/lib/itsi/server.rb +82 -12
  118. data/gems/server/lib/ruby_lsp/itsi/addon.rb +111 -0
  119. data/gems/server/lib/shell_completions/completions.rb +26 -0
  120. data/gems/server/test/helpers/test_helper.rb +2 -1
  121. data/lib/itsi/version.rb +1 -1
  122. data/sandbox/README.md +5 -0
  123. data/sandbox/itsi_file/Gemfile +4 -2
  124. data/sandbox/itsi_file/Gemfile.lock +48 -6
  125. data/sandbox/itsi_file/Itsi.rb +327 -129
  126. data/sandbox/itsi_file/call.json +1 -0
  127. data/sandbox/itsi_file/echo_client/Gemfile +10 -0
  128. data/sandbox/itsi_file/echo_client/Gemfile.lock +27 -0
  129. data/sandbox/itsi_file/echo_client/README.md +95 -0
  130. data/sandbox/itsi_file/echo_client/echo_client.rb +164 -0
  131. data/sandbox/itsi_file/echo_client/gen_proto.sh +17 -0
  132. data/sandbox/itsi_file/echo_client/lib/echo_pb.rb +16 -0
  133. data/sandbox/itsi_file/echo_client/lib/echo_services_pb.rb +29 -0
  134. data/sandbox/itsi_file/echo_client/run_client.rb +64 -0
  135. data/sandbox/itsi_file/echo_client/test_compressions.sh +20 -0
  136. data/sandbox/itsi_file/echo_service_nonitsi/Gemfile +10 -0
  137. data/sandbox/itsi_file/echo_service_nonitsi/Gemfile.lock +79 -0
  138. data/sandbox/itsi_file/echo_service_nonitsi/echo.proto +26 -0
  139. data/sandbox/itsi_file/echo_service_nonitsi/echo_pb.rb +16 -0
  140. data/sandbox/itsi_file/echo_service_nonitsi/echo_services_pb.rb +29 -0
  141. data/sandbox/itsi_file/echo_service_nonitsi/server.rb +52 -0
  142. data/sandbox/itsi_sandbox_async/config.ru +0 -1
  143. data/sandbox/itsi_sandbox_rack/Gemfile.lock +2 -2
  144. data/sandbox/itsi_sandbox_rails/Gemfile +2 -2
  145. data/sandbox/itsi_sandbox_rails/Gemfile.lock +76 -2
  146. data/sandbox/itsi_sandbox_rails/app/controllers/home_controller.rb +15 -0
  147. data/sandbox/itsi_sandbox_rails/config/environments/development.rb +1 -0
  148. data/sandbox/itsi_sandbox_rails/config/environments/production.rb +1 -0
  149. data/sandbox/itsi_sandbox_rails/config/routes.rb +2 -0
  150. data/sandbox/itsi_sinatra/app.rb +0 -1
  151. data/sandbox/static_files/.env +1 -0
  152. data/sandbox/static_files/404.html +25 -0
  153. data/sandbox/static_files/_DSC0102.NEF.jpg +0 -0
  154. data/sandbox/static_files/about.html +68 -0
  155. data/sandbox/static_files/tiny.html +1 -0
  156. data/sandbox/static_files/writebook.zip +0 -0
  157. data/tasks.txt +28 -33
  158. metadata +87 -26
  159. data/crates/itsi_error/src/from.rs +0 -68
  160. data/crates/itsi_server/src/ruby_types/itsi_grpc_request.rs +0 -147
  161. data/crates/itsi_server/src/ruby_types/itsi_grpc_response.rs +0 -19
  162. data/crates/itsi_server/src/server/itsi_service.rs +0 -172
  163. data/crates/itsi_server/src/server/middleware_stack/middlewares/grpc_service.rs +0 -72
  164. data/crates/itsi_server/src/server/types.rs +0 -43
  165. data/gems/server/lib/itsi/server/grpc_interface.rb +0 -213
  166. data/sandbox/itsi_file/public/assets/index.html +0 -1
  167. /data/crates/itsi_server/src/server/{bind_protocol.rs → binds/bind_protocol.rs} +0 -0
  168. /data/crates/itsi_server/src/server/{tls → binds/tls}/locked_dir_cache.rs +0 -0
  169. /data/crates/itsi_server/src/{server → services}/cache_store.rs +0 -0
@@ -1,127 +1,157 @@
1
- use crate::server::static_file_server::ROOT_STATIC_FILE_SERVER;
2
- use crate::server::types::RequestExt;
3
- use crate::server::{
4
- itsi_service::RequestContext,
5
- types::{HttpRequest, HttpResponse},
6
- };
7
-
8
1
  use bytes::Bytes;
9
- use either::Either;
2
+ use http::header::CONTENT_TYPE;
10
3
  use http::Response;
11
4
  use http_body_util::{combinators::BoxBody, Full};
12
- use itsi_error::ItsiError;
13
- use serde::Deserialize;
14
- use std::path::{Path, PathBuf};
5
+ use serde::{Deserialize, Deserializer};
6
+ use std::convert::Infallible;
7
+ use std::path::PathBuf;
8
+
9
+ use crate::server::http_message_types::{HttpResponse, ResponseFormat};
10
+ use crate::services::static_file_server::ROOT_STATIC_FILE_SERVER;
11
+ mod default_responses;
15
12
 
16
13
  #[derive(Debug, Clone, Deserialize)]
17
- /// Filters can each have a customizable error response.
18
- /// They can:
19
- /// * Return Plain-text
20
- /// * Return HTML
21
- /// * Return JSON
22
- pub struct ErrorResponse {
23
- code: u16,
24
- plaintext: Option<String>,
25
- html: Option<PathBuf>,
26
- json: Option<serde_json::Value>,
27
- default: ErrorFormat,
14
+ pub enum ContentSource {
15
+ #[serde(rename(deserialize = "inline"))]
16
+ Inline(String),
17
+ #[serde(rename(deserialize = "file"))]
18
+ File(PathBuf),
28
19
  }
29
20
 
30
21
  #[derive(Debug, Clone, Deserialize, Default)]
31
- enum ErrorFormat {
32
- #[default]
22
+ pub enum DefaultFormat {
33
23
  #[serde(rename(deserialize = "plaintext"))]
34
24
  Plaintext,
25
+ #[default]
35
26
  #[serde(rename(deserialize = "html"))]
36
27
  Html,
37
28
  #[serde(rename(deserialize = "json"))]
38
29
  Json,
39
30
  }
40
31
 
41
- impl Default for ErrorResponse {
42
- fn default() -> Self {
43
- ErrorResponse {
44
- code: 500,
45
- plaintext: Some("Error".to_owned()),
46
- html: None,
47
- json: None,
48
- default: ErrorFormat::Plaintext,
32
+ #[derive(Debug, Clone)]
33
+ pub struct ErrorResponse {
34
+ pub code: u16,
35
+ pub plaintext: Option<ContentSource>,
36
+ pub html: Option<ContentSource>,
37
+ pub json: Option<ContentSource>,
38
+ pub default: DefaultFormat, // must match one of the provided fields
39
+ }
40
+
41
+ impl<'de> Deserialize<'de> for ErrorResponse {
42
+ fn deserialize<D>(deserializer: D) -> Result<ErrorResponse, D::Error>
43
+ where
44
+ D: Deserializer<'de>,
45
+ {
46
+ let def = ErrorResponseDef::deserialize(deserializer)?;
47
+ Ok(def.into())
48
+ }
49
+ }
50
+
51
+ /// An untagged enum to support two input formats:
52
+ /// - A detailed struct with all fields.
53
+ /// - A string with the name of a default error response.
54
+ #[derive(Debug, Clone, Deserialize)]
55
+ #[serde(untagged)]
56
+ pub enum ErrorResponseDef {
57
+ Detailed {
58
+ code: u16,
59
+ plaintext: Option<ContentSource>,
60
+ html: Option<ContentSource>,
61
+ json: Option<ContentSource>,
62
+ default: DefaultFormat,
63
+ },
64
+ Named(String),
65
+ }
66
+
67
+ impl From<ErrorResponseDef> for ErrorResponse {
68
+ fn from(def: ErrorResponseDef) -> Self {
69
+ match def {
70
+ ErrorResponseDef::Detailed {
71
+ code,
72
+ plaintext,
73
+ html,
74
+ json,
75
+ default,
76
+ } => ErrorResponse {
77
+ code,
78
+ plaintext,
79
+ html,
80
+ json,
81
+ default,
82
+ },
83
+ ErrorResponseDef::Named(name) => match name.as_str() {
84
+ "internal_server_error" => ErrorResponse::internal_server_error(),
85
+ "not_found" => ErrorResponse::not_found(),
86
+ "unauthorized" => ErrorResponse::unauthorized(),
87
+ "forbidden" => ErrorResponse::forbidden(),
88
+ "payload_too_large" => ErrorResponse::payload_too_large(),
89
+ "too_many_requests" => ErrorResponse::too_many_requests(),
90
+ "bad_gateway" => ErrorResponse::bad_gateway(),
91
+ "service_unavailable" => ErrorResponse::service_unavailable(),
92
+ "gateway_timeout" => ErrorResponse::gateway_timeout(),
93
+ _ => panic!("Unknown error response name: {}", name),
94
+ },
49
95
  }
50
96
  }
51
97
  }
52
98
 
53
99
  impl ErrorResponse {
54
- pub(crate) async fn to_http_response(&self, request: &HttpRequest) -> HttpResponse {
55
- let accept = request.accept();
56
- let body = match accept {
57
- Some(accept) if accept.contains("text/plain") => BoxBody::new(Full::new(Bytes::from(
58
- self.plaintext.clone().unwrap_or_else(|| "Error".to_owned()),
59
- ))),
60
- Some(accept) if accept.contains("text/html") => {
61
- if let Some(path) = &self.html {
62
- let path = path.to_str().unwrap();
63
- let response = ROOT_STATIC_FILE_SERVER.serve_single(path).await;
64
-
65
- if response.status().is_success() {
66
- response.into_body()
67
- } else {
68
- BoxBody::new(Full::new(Bytes::from("Error")))
69
- }
70
- } else {
71
- BoxBody::new(Full::new(Bytes::from("Error")))
72
- }
100
+ pub(crate) async fn to_http_response(&self, accept: ResponseFormat) -> HttpResponse {
101
+ let mut resp = Response::builder().status(self.code);
102
+ let response = match accept {
103
+ ResponseFormat::TEXT => {
104
+ resp = resp.header(CONTENT_TYPE, "text/plain");
105
+ resp.body(Self::get_response_body(self.code, &self.plaintext, accept).await)
73
106
  }
74
- Some(accept) if accept.contains("application/json") => {
75
- BoxBody::new(Full::new(Bytes::from(
76
- self.json
77
- .as_ref()
78
- .map(|json| json.to_string())
79
- .unwrap_or_else(|| "Error".to_owned()),
80
- )))
107
+ ResponseFormat::HTML => {
108
+ resp = resp.header(CONTENT_TYPE, "text/html");
109
+ resp.body(Self::get_response_body(self.code, &self.html, accept).await)
81
110
  }
82
- _ => match self.default {
83
- ErrorFormat::Plaintext => BoxBody::new(Full::new(Bytes::from(
84
- self.plaintext.clone().unwrap_or_else(|| "Error".to_owned()),
85
- ))),
86
- ErrorFormat::Html => {
87
- if let Some(path) = &self.html {
88
- let path = path.to_str().unwrap();
89
- let response = ROOT_STATIC_FILE_SERVER.serve_single(path).await;
90
-
91
- if response.status().is_success() {
92
- response.into_body()
93
- } else {
94
- BoxBody::new(Full::new(Bytes::from("Error")))
95
- }
96
- } else {
97
- BoxBody::new(Full::new(Bytes::from("Error")))
98
- }
111
+ ResponseFormat::JSON => {
112
+ resp = resp.header(CONTENT_TYPE, "application/json");
113
+ resp.body(Self::get_response_body(self.code, &self.json, accept).await)
114
+ }
115
+ ResponseFormat::UNKNOWN => match self.default {
116
+ DefaultFormat::Plaintext => {
117
+ resp = resp.header(CONTENT_TYPE, "text/plain");
118
+ resp.body(Self::get_response_body(self.code, &self.plaintext, accept).await)
119
+ }
120
+ DefaultFormat::Html => {
121
+ resp = resp.header(CONTENT_TYPE, "text/html");
122
+ resp.body(Self::get_response_body(self.code, &self.html, accept).await)
123
+ }
124
+ DefaultFormat::Json => {
125
+ resp = resp.header(CONTENT_TYPE, "application/json");
126
+ resp.body(Self::get_response_body(self.code, &self.json, accept).await)
99
127
  }
100
- ErrorFormat::Json => BoxBody::new(Full::new(Bytes::from(
101
- self.json
102
- .as_ref()
103
- .map(|json| json.to_string())
104
- .unwrap_or_else(|| "Error".to_owned()),
105
- ))),
106
128
  },
107
129
  };
108
-
109
- Response::builder().status(self.code).body(body).unwrap()
130
+ response.unwrap()
110
131
  }
111
132
 
112
- pub async fn before(
113
- &self,
114
- req: HttpRequest,
115
- _context: &mut RequestContext,
116
- ) -> Result<Either<HttpRequest, HttpResponse>, ItsiError> {
117
- if let Some(path) = req.uri().path().strip_prefix("/error/") {
118
- let path = Path::new(path);
119
- if path.exists() {
120
- let path = path.to_str().unwrap();
121
- let response = ROOT_STATIC_FILE_SERVER.serve_single(path).await;
122
- return Ok(Either::Right(response));
133
+ async fn get_response_body(
134
+ code: u16,
135
+ source: &Option<ContentSource>,
136
+ accept: ResponseFormat,
137
+ ) -> BoxBody<Bytes, Infallible> {
138
+ match source {
139
+ Some(ContentSource::Inline(text)) => {
140
+ return BoxBody::new(Full::new(Bytes::from(text.clone())));
141
+ }
142
+ Some(ContentSource::File(path)) => {
143
+ // Convert the PathBuf to a &str (assumes valid UTF-8).
144
+ if let Some(path_str) = path.to_str() {
145
+ let response = ROOT_STATIC_FILE_SERVER
146
+ .serve_single(path_str, accept.clone(), &[])
147
+ .await;
148
+ if response.status().is_success() {
149
+ return response.into_body();
150
+ }
151
+ }
123
152
  }
153
+ None => {}
124
154
  }
125
- Ok(Either::Left(req))
155
+ ErrorResponse::fallback_body_for(code, accept)
126
156
  }
127
157
  }
@@ -1,5 +1,9 @@
1
+ use crate::{
2
+ server::http_message_types::{HttpRequest, HttpResponse},
3
+ services::itsi_http_service::HttpRequestContext,
4
+ };
5
+
1
6
  use super::{FromValue, MiddlewareLayer};
2
- use crate::server::{itsi_service::RequestContext, types::HttpResponse};
3
7
  use async_trait::async_trait;
4
8
  use base64::{engine::general_purpose, Engine as _};
5
9
  use bytes::{Bytes, BytesMut};
@@ -50,9 +54,9 @@ fn default_true() -> bool {
50
54
  impl MiddlewareLayer for ETag {
51
55
  async fn before(
52
56
  &self,
53
- req: crate::server::types::HttpRequest,
54
- context: &mut RequestContext,
55
- ) -> Result<Either<crate::server::types::HttpRequest, HttpResponse>> {
57
+ req: HttpRequest,
58
+ context: &mut HttpRequestContext,
59
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
56
60
  // Store if-none-match header in context if present for later use in after hook
57
61
  if self.handle_if_none_match {
58
62
  if let Some(if_none_match) = req.headers().get(header::IF_NONE_MATCH) {
@@ -64,7 +68,7 @@ impl MiddlewareLayer for ETag {
64
68
  Ok(Either::Left(req))
65
69
  }
66
70
 
67
- async fn after(&self, resp: HttpResponse, context: &mut RequestContext) -> HttpResponse {
71
+ async fn after(&self, resp: HttpResponse, context: &mut HttpRequestContext) -> HttpResponse {
68
72
  // Skip for error responses or responses that shouldn't have ETags
69
73
  match resp.status() {
70
74
  StatusCode::OK
@@ -3,10 +3,7 @@ use http::{header::GetAll, HeaderValue};
3
3
  /// Given a list of header values (which may be comma-separated and may have quality parameters)
4
4
  /// and a list of supported items (each supported item is a full value or a prefix ending with '*'),
5
5
  /// return Some(supported_item) for the first supported item that matches any header value, or None.
6
- pub fn find_first_supported<'a, I>(
7
- header_values: &http::header::GetAll<http::HeaderValue>,
8
- supported: I,
9
- ) -> Option<&'a str>
6
+ pub fn find_first_supported<'a, I>(header_values: &[HeaderValue], supported: I) -> Option<&'a str>
10
7
  where
11
8
  I: IntoIterator<Item = &'a str> + Clone,
12
9
  {
@@ -1,9 +1,11 @@
1
- use super::{ErrorResponse, FromValue, MiddlewareLayer};
2
- use crate::server::{
3
- itsi_service::RequestContext,
4
- rate_limiter::{get_ban_manager, get_rate_limiter, BanManager, RateLimiter, RateLimiterConfig},
5
- types::{HttpRequest, HttpResponse, RequestExt},
1
+ use crate::server::http_message_types::{HttpRequest, HttpResponse, RequestExt};
2
+ use crate::services::itsi_http_service::HttpRequestContext;
3
+ use crate::services::rate_limiter::{
4
+ get_ban_manager, get_rate_limiter, BanManager, RateLimiter, RateLimiterConfig,
6
5
  };
6
+
7
+ use super::{ErrorResponse, FromValue, MiddlewareLayer};
8
+
7
9
  use async_trait::async_trait;
8
10
  use either::Either;
9
11
  use itsi_tracing::*;
@@ -32,9 +34,14 @@ pub struct IntrusionProtection {
32
34
  #[serde(skip_deserializing)]
33
35
  pub ban_manager: OnceLock<BanManager>,
34
36
  pub store_config: RateLimiterConfig,
37
+ #[serde(default = "forbidden_error_response")]
35
38
  pub error_response: ErrorResponse,
36
39
  }
37
40
 
41
+ fn forbidden_error_response() -> ErrorResponse {
42
+ ErrorResponse::forbidden()
43
+ }
44
+
38
45
  #[async_trait]
39
46
  impl MiddlewareLayer for IntrusionProtection {
40
47
  async fn initialize(&self) -> Result<()> {
@@ -89,7 +96,7 @@ impl MiddlewareLayer for IntrusionProtection {
89
96
  async fn before(
90
97
  &self,
91
98
  req: HttpRequest,
92
- context: &mut RequestContext,
99
+ context: &mut HttpRequestContext,
93
100
  ) -> Result<Either<HttpRequest, HttpResponse>> {
94
101
  // Get client IP address from context's service
95
102
  let client_ip = &context.addr;
@@ -97,10 +104,11 @@ impl MiddlewareLayer for IntrusionProtection {
97
104
  // Check if the IP is already banned
98
105
  if let Some(ban_manager) = self.ban_manager.get() {
99
106
  match ban_manager.is_banned(client_ip).await {
100
- Ok(Some(reason)) => {
101
- info!("Request from banned IP {}: {}", client_ip, reason);
107
+ Ok(Some(_)) => {
102
108
  return Ok(Either::Right(
103
- self.error_response.to_http_response(&req).await,
109
+ self.error_response
110
+ .to_http_response(req.accept().into())
111
+ .await,
104
112
  ));
105
113
  }
106
114
  Err(e) => {
@@ -118,11 +126,8 @@ impl MiddlewareLayer for IntrusionProtection {
118
126
  // Check for banned URL patterns
119
127
  if let Some(url_matcher) = self.banned_url_pattern_matcher.get() {
120
128
  let path = req.uri().path_and_query().map(|p| p.as_str()).unwrap_or("");
121
- info!("Checking URL pattern match for {}", path);
122
- if url_matcher.is_match(path) {
123
- info!("Intrusion detected: URL pattern match for {}", path);
124
129
 
125
- // Ban the IP address if possible
130
+ if url_matcher.is_match(path) {
126
131
  if let Some(ban_manager) = self.ban_manager.get() {
127
132
  match ban_manager
128
133
  .ban_ip(
@@ -132,17 +137,16 @@ impl MiddlewareLayer for IntrusionProtection {
132
137
  )
133
138
  .await
134
139
  {
135
- Ok(_) => info!(
136
- "Successfully banned IP {} for {} seconds",
137
- client_ip, self.banned_time_seconds
138
- ),
140
+ Ok(_) => {}
139
141
  Err(e) => error!("Failed to ban IP {}: {:?}", client_ip, e),
140
142
  }
141
143
  }
142
144
 
143
145
  // Always return the error response even if banning failed
144
146
  return Ok(Either::Right(
145
- self.error_response.to_http_response(&req).await,
147
+ self.error_response
148
+ .to_http_response(req.accept().into())
149
+ .await,
146
150
  ));
147
151
  }
148
152
  }
@@ -180,7 +184,9 @@ impl MiddlewareLayer for IntrusionProtection {
180
184
 
181
185
  // Always return the error response even if banning failed
182
186
  return Ok(Either::Right(
183
- self.error_response.to_http_response(&req).await,
187
+ self.error_response
188
+ .to_http_response(req.accept().into())
189
+ .await,
184
190
  ));
185
191
  }
186
192
  }
@@ -4,8 +4,8 @@ use itsi_tracing::*;
4
4
  use magnus::error::Result;
5
5
  use serde::Deserialize;
6
6
 
7
- use crate::server::itsi_service::RequestContext;
8
- use crate::server::types::{HttpRequest, HttpResponse};
7
+ use crate::server::http_message_types::{HttpRequest, HttpResponse};
8
+ use crate::services::itsi_http_service::HttpRequestContext;
9
9
 
10
10
  use super::string_rewrite::StringRewrite;
11
11
  use super::{FromValue, MiddlewareLayer};
@@ -60,7 +60,7 @@ impl MiddlewareLayer for LogRequests {
60
60
  async fn before(
61
61
  &self,
62
62
  req: HttpRequest,
63
- context: &mut RequestContext,
63
+ context: &mut HttpRequestContext,
64
64
  ) -> Result<Either<HttpRequest, HttpResponse>> {
65
65
  context.track_start_time();
66
66
  if let Some(LogConfig { level, format }) = self.before.as_ref() {
@@ -70,7 +70,7 @@ impl MiddlewareLayer for LogRequests {
70
70
  Ok(Either::Left(req))
71
71
  }
72
72
 
73
- async fn after(&self, resp: HttpResponse, context: &mut RequestContext) -> HttpResponse {
73
+ async fn after(&self, resp: HttpResponse, context: &mut HttpRequestContext) -> HttpResponse {
74
74
  if let Some(LogConfig { level, format }) = self.after.as_ref() {
75
75
  level.log(format.rewrite_response(&resp, context));
76
76
  }
@@ -0,0 +1,47 @@
1
+ use crate::{
2
+ server::http_message_types::{HttpRequest, HttpResponse, RequestExt},
3
+ services::itsi_http_service::HttpRequestContext,
4
+ };
5
+
6
+ use super::{ErrorResponse, FromValue, MiddlewareLayer};
7
+ use async_trait::async_trait;
8
+ use either::Either;
9
+ use http::StatusCode;
10
+ use magnus::error::Result;
11
+ use serde::Deserialize;
12
+ use std::sync::atomic::Ordering;
13
+
14
+ #[derive(Debug, Clone, Deserialize)]
15
+ pub struct MaxBody {
16
+ pub max_size: usize,
17
+ #[serde(default = "payload_too_large_error_response")]
18
+ pub error_response: ErrorResponse,
19
+ }
20
+
21
+ fn payload_too_large_error_response() -> ErrorResponse {
22
+ ErrorResponse::payload_too_large()
23
+ }
24
+
25
+ #[async_trait]
26
+ impl MiddlewareLayer for MaxBody {
27
+ async fn before(
28
+ &self,
29
+ req: HttpRequest,
30
+ context: &mut HttpRequestContext,
31
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
32
+ req.body().limit.store(self.max_size, Ordering::Relaxed);
33
+ context.set_response_format(req.accept().into());
34
+ Ok(Either::Left(req))
35
+ }
36
+
37
+ async fn after(&self, resp: HttpResponse, context: &mut HttpRequestContext) -> HttpResponse {
38
+ if resp.status() == StatusCode::PAYLOAD_TOO_LARGE {
39
+ self.error_response
40
+ .to_http_response(context.response_format().clone())
41
+ .await
42
+ } else {
43
+ resp
44
+ }
45
+ }
46
+ }
47
+ impl FromValue for MaxBody {}
@@ -11,6 +11,7 @@ mod etag;
11
11
  mod header_interpretation;
12
12
  mod intrusion_protection;
13
13
  mod log_requests;
14
+ mod max_body;
14
15
  mod proxy;
15
16
  mod rate_limit;
16
17
  mod redirect;
@@ -18,6 +19,7 @@ mod request_headers;
18
19
  mod response_headers;
19
20
  mod ruby_app;
20
21
  mod static_assets;
22
+ mod static_response;
21
23
  mod string_rewrite;
22
24
  mod token_source;
23
25
 
@@ -38,6 +40,7 @@ pub use intrusion_protection::IntrusionProtection;
38
40
  pub use log_requests::LogRequests;
39
41
  use magnus::error::Result;
40
42
  use magnus::Value;
43
+ pub use max_body::MaxBody;
41
44
  pub use proxy::Proxy;
42
45
  pub use rate_limit::RateLimit;
43
46
  pub use redirect::Redirect;
@@ -47,9 +50,11 @@ pub use ruby_app::RubyApp;
47
50
  use serde::Deserialize;
48
51
  use serde_magnus::deserialize;
49
52
  pub use static_assets::StaticAssets;
53
+ pub use static_response::StaticResponse;
50
54
 
51
- use crate::server::itsi_service::RequestContext;
52
- use crate::server::types::{HttpRequest, HttpResponse};
55
+ use crate::server::http_message_types::HttpRequest;
56
+ use crate::server::http_message_types::HttpResponse;
57
+ use crate::services::itsi_http_service::HttpRequestContext;
53
58
 
54
59
  pub trait FromValue: Sized + Send + Sync + 'static {
55
60
  fn from_value(value: Value) -> Result<Self>
@@ -70,13 +75,13 @@ pub trait MiddlewareLayer: Sized + Send + Sync + 'static {
70
75
  async fn before(
71
76
  &self,
72
77
  req: HttpRequest,
73
- _context: &mut RequestContext,
78
+ _context: &mut HttpRequestContext,
74
79
  ) -> Result<Either<HttpRequest, HttpResponse>> {
75
80
  Ok(Either::Left(req))
76
81
  }
77
82
 
78
83
  /// The "after" hook. By default, it passes through the response.
79
- async fn after(&self, resp: HttpResponse, _context: &mut RequestContext) -> HttpResponse {
84
+ async fn after(&self, resp: HttpResponse, _context: &mut HttpRequestContext) -> HttpResponse {
80
85
  resp
81
86
  }
82
87
  }