itsi 0.1.13 → 0.1.18

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 +113 -593
  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 +9 -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 +145 -181
  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 +113 -593
  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 +116 -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 +160 -101
  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 +326 -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 -32
  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,8 +1,9 @@
1
1
  use super::{FromValue, MiddlewareLayer};
2
- use crate::server::{
3
- itsi_service::RequestContext,
4
- types::{HttpRequest, HttpResponse, RequestExt},
2
+ use crate::{
3
+ server::http_message_types::{HttpRequest, HttpResponse, RequestExt},
4
+ services::itsi_http_service::HttpRequestContext,
5
5
  };
6
+
6
7
  use async_trait::async_trait;
7
8
  use http::{HeaderMap, Method, Response};
8
9
  use http_body_util::{combinators::BoxBody, Empty};
@@ -69,14 +70,14 @@ impl Cors {
69
70
  fn cors_headers(&self, origin: &str) -> Result<HeaderMap> {
70
71
  let mut headers = HeaderMap::new();
71
72
 
72
- headers.insert("Vary", "Origin".parse().map_err(ItsiError::default)?);
73
+ headers.insert("Vary", "Origin".parse().map_err(ItsiError::new)?);
73
74
 
74
75
  if origin.is_empty() {
75
76
  // When credentials are allowed, you cannot return "*".
76
77
  if !self.allow_credentials {
77
78
  headers.insert(
78
79
  "Access-Control-Allow-Origin",
79
- "*".parse().map_err(ItsiError::default)?,
80
+ "*".parse().map_err(ItsiError::new)?,
80
81
  );
81
82
  }
82
83
  return Ok(headers);
@@ -97,7 +98,7 @@ impl Cors {
97
98
  };
98
99
  headers.insert(
99
100
  "Access-Control-Allow-Origin",
100
- value.parse().map_err(ItsiError::default)?,
101
+ value.parse().map_err(ItsiError::new)?,
101
102
  );
102
103
  }
103
104
 
@@ -110,7 +111,7 @@ impl Cors {
110
111
  .collect::<Vec<&str>>()
111
112
  .join(", ")
112
113
  .parse()
113
- .map_err(ItsiError::default)?,
114
+ .map_err(ItsiError::new)?,
114
115
  );
115
116
  }
116
117
  if !self.allowed_headers.is_empty() {
@@ -119,19 +120,19 @@ impl Cors {
119
120
  self.allowed_headers
120
121
  .join(", ")
121
122
  .parse()
122
- .map_err(ItsiError::default)?,
123
+ .map_err(ItsiError::new)?,
123
124
  );
124
125
  }
125
126
  if self.allow_credentials {
126
127
  headers.insert(
127
128
  "Access-Control-Allow-Credentials",
128
- "true".parse().map_err(ItsiError::default)?,
129
+ "true".parse().map_err(ItsiError::new)?,
129
130
  );
130
131
  }
131
132
  if let Some(max_age) = self.max_age {
132
133
  headers.insert(
133
134
  "Access-Control-Max-Age",
134
- max_age.to_string().parse().map_err(ItsiError::default)?,
135
+ max_age.to_string().parse().map_err(ItsiError::new)?,
135
136
  );
136
137
  }
137
138
  if !self.exposed_headers.is_empty() {
@@ -140,7 +141,7 @@ impl Cors {
140
141
  self.exposed_headers
141
142
  .join(", ")
142
143
  .parse()
143
- .map_err(ItsiError::default)?,
144
+ .map_err(ItsiError::new)?,
144
145
  );
145
146
  }
146
147
  Ok(headers)
@@ -154,7 +155,7 @@ impl Cors {
154
155
  ) -> Result<HeaderMap> {
155
156
  let mut headers = HeaderMap::new();
156
157
 
157
- headers.insert("Vary", "Origin".parse().map_err(ItsiError::default)?);
158
+ headers.insert("Vary", "Origin".parse().map_err(ItsiError::new)?);
158
159
 
159
160
  let origin = match origin {
160
161
  Some(o) if !o.is_empty() => o,
@@ -208,25 +209,25 @@ impl Cors {
208
209
  .collect::<Vec<&str>>()
209
210
  .join(", ")
210
211
  .parse()
211
- .map_err(ItsiError::default)?,
212
+ .map_err(ItsiError::new)?,
212
213
  );
213
214
  headers.insert(
214
215
  "Access-Control-Allow-Headers",
215
216
  self.allowed_headers
216
217
  .join(", ")
217
218
  .parse()
218
- .map_err(ItsiError::default)?,
219
+ .map_err(ItsiError::new)?,
219
220
  );
220
221
  if self.allow_credentials {
221
222
  headers.insert(
222
223
  "Access-Control-Allow-Credentials",
223
- "true".parse().map_err(ItsiError::default)?,
224
+ "true".parse().map_err(ItsiError::new)?,
224
225
  );
225
226
  }
226
227
  if let Some(max_age) = self.max_age {
227
228
  headers.insert(
228
229
  "Access-Control-Max-Age",
229
- max_age.to_string().parse().map_err(ItsiError::default)?,
230
+ max_age.to_string().parse().map_err(ItsiError::new)?,
230
231
  );
231
232
  }
232
233
  if !self.exposed_headers.is_empty() {
@@ -235,7 +236,7 @@ impl Cors {
235
236
  self.exposed_headers
236
237
  .join(", ")
237
238
  .parse()
238
- .map_err(ItsiError::default)?,
239
+ .map_err(ItsiError::new)?,
239
240
  );
240
241
  }
241
242
 
@@ -253,7 +254,7 @@ impl MiddlewareLayer for Cors {
253
254
  async fn before(
254
255
  &self,
255
256
  req: HttpRequest,
256
- context: &mut RequestContext,
257
+ context: &mut HttpRequestContext,
257
258
  ) -> Result<either::Either<HttpRequest, HttpResponse>> {
258
259
  let origin = req.header("Origin");
259
260
  if req.method() == Method::OPTIONS {
@@ -265,7 +266,7 @@ impl MiddlewareLayer for Cors {
265
266
  *response_builder.headers_mut().unwrap() = headers;
266
267
  let response = response_builder
267
268
  .body(BoxBody::new(Empty::new()))
268
- .map_err(ItsiError::default)?;
269
+ .map_err(ItsiError::new)?;
269
270
  return Ok(either::Either::Right(response));
270
271
  }
271
272
  context.set_origin(origin.map(|s| s.to_string()));
@@ -273,7 +274,11 @@ impl MiddlewareLayer for Cors {
273
274
  }
274
275
 
275
276
  // The after hook can be used to inject CORS headers into non-preflight responses.
276
- async fn after(&self, mut resp: HttpResponse, context: &mut RequestContext) -> HttpResponse {
277
+ async fn after(
278
+ &self,
279
+ mut resp: HttpResponse,
280
+ context: &mut HttpRequestContext,
281
+ ) -> HttpResponse {
277
282
  if let Some(Some(origin)) = context.origin.get() {
278
283
  if let Ok(cors_headers) = self.cors_headers(origin) {
279
284
  for (key, value) in cors_headers.iter() {
@@ -1,6 +1,6 @@
1
- use crate::server::{
2
- itsi_service::RequestContext,
3
- types::{HttpRequest, HttpResponse},
1
+ use crate::{
2
+ server::http_message_types::{HttpRequest, HttpResponse, RequestExt},
3
+ services::itsi_http_service::HttpRequestContext,
4
4
  };
5
5
 
6
6
  use super::{ErrorResponse, FromValue, MiddlewareLayer};
@@ -17,28 +17,35 @@ pub struct DenyList {
17
17
  #[serde(skip_deserializing)]
18
18
  pub denied_ips: OnceLock<RegexSet>,
19
19
  pub denied_patterns: Vec<String>,
20
+ #[serde(default = "forbidden_error_response")]
20
21
  pub error_response: ErrorResponse,
21
22
  }
22
23
 
24
+ fn forbidden_error_response() -> ErrorResponse {
25
+ ErrorResponse::forbidden()
26
+ }
27
+
23
28
  #[async_trait]
24
29
  impl MiddlewareLayer for DenyList {
25
30
  async fn initialize(&self) -> Result<()> {
26
- let denied_ips = RegexSet::new(&self.denied_patterns).map_err(ItsiError::default)?;
31
+ let denied_ips = RegexSet::new(&self.denied_patterns).map_err(ItsiError::new)?;
27
32
  self.denied_ips
28
33
  .set(denied_ips)
29
- .map_err(|e| ItsiError::default(format!("Failed to set allowed IPs: {:?}", e)))?;
34
+ .map_err(|e| ItsiError::new(format!("Failed to set allowed IPs: {:?}", e)))?;
30
35
  Ok(())
31
36
  }
32
37
 
33
38
  async fn before(
34
39
  &self,
35
40
  req: HttpRequest,
36
- context: &mut RequestContext,
41
+ context: &mut HttpRequestContext,
37
42
  ) -> Result<Either<HttpRequest, HttpResponse>> {
38
43
  if let Some(denied_ips) = self.denied_ips.get() {
39
44
  if denied_ips.is_match(&context.addr) {
40
45
  return Ok(Either::Right(
41
- self.error_response.to_http_response(&req).await,
46
+ self.error_response
47
+ .to_http_response(req.accept().into())
48
+ .await,
42
49
  ));
43
50
  }
44
51
  }
@@ -0,0 +1,190 @@
1
+ use std::convert::Infallible;
2
+
3
+ use bytes::Bytes;
4
+ use http_body_util::{combinators::BoxBody, Full};
5
+
6
+ use crate::server::http_message_types::ResponseFormat;
7
+
8
+ use super::{ContentSource, DefaultFormat, ErrorResponse};
9
+
10
+ impl DefaultFormat {
11
+ pub fn response_for_code(&self, code: u16) -> ContentSource {
12
+ match self {
13
+ DefaultFormat::Plaintext => match code {
14
+ 500 => ContentSource::Inline("500 Internal Error".to_owned()),
15
+ 404 => ContentSource::Inline("404 Not Found".to_owned()),
16
+ 401 => ContentSource::Inline("401 Unauthorized".to_owned()),
17
+ 403 => ContentSource::Inline("403 Forbidden".to_owned()),
18
+ 413 => ContentSource::Inline("413 Payload Too Large".to_owned()),
19
+ 429 => ContentSource::Inline("429 Too Many Requests".to_owned()),
20
+ 502 => ContentSource::Inline("502 Bad Gateway".to_owned()),
21
+ 503 => ContentSource::Inline("503 Service Unavailable".to_owned()),
22
+ 504 => ContentSource::Inline("504 Gateway Timeout".to_owned()),
23
+ _ => ContentSource::Inline("Unexpected Error".to_owned()),
24
+ },
25
+ DefaultFormat::Html => match code {
26
+ 500 => ContentSource::Inline(
27
+ include_str!("../../../../default_responses/html/500.html").to_owned(),
28
+ ),
29
+ 404 => ContentSource::Inline(
30
+ include_str!("../../../../default_responses/html/404.html").to_owned(),
31
+ ),
32
+ 401 => ContentSource::Inline(
33
+ include_str!("../../../../default_responses/html/401.html").to_owned(),
34
+ ),
35
+ 403 => ContentSource::Inline(
36
+ include_str!("../../../../default_responses/html/403.html").to_owned(),
37
+ ),
38
+ 413 => ContentSource::Inline(
39
+ include_str!("../../../../default_responses/html/413.html").to_owned(),
40
+ ),
41
+ 429 => ContentSource::Inline(
42
+ include_str!("../../../../default_responses/html/429.html").to_owned(),
43
+ ),
44
+ 502 => ContentSource::Inline(
45
+ include_str!("../../../../default_responses/html/502.html").to_owned(),
46
+ ),
47
+ 503 => ContentSource::Inline(
48
+ include_str!("../../../../default_responses/html/503.html").to_owned(),
49
+ ),
50
+ 504 => ContentSource::Inline(
51
+ include_str!("../../../../default_responses/html/504.html").to_owned(),
52
+ ),
53
+ _ => ContentSource::Inline("Unexpected Error".to_owned()),
54
+ },
55
+ DefaultFormat::Json => match code {
56
+ 500 => ContentSource::Inline(
57
+ include_str!("../../../../default_responses/json/500.json").to_owned(),
58
+ ),
59
+ 404 => ContentSource::Inline(
60
+ include_str!("../../../../default_responses/json/404.json").to_owned(),
61
+ ),
62
+ 401 => ContentSource::Inline(
63
+ include_str!("../../../../default_responses/json/401.json").to_owned(),
64
+ ),
65
+ 403 => ContentSource::Inline(
66
+ include_str!("../../../../default_responses/json/403.json").to_owned(),
67
+ ),
68
+ 413 => ContentSource::Inline(
69
+ include_str!("../../../../default_responses/json/413.json").to_owned(),
70
+ ),
71
+ 429 => ContentSource::Inline(
72
+ include_str!("../../../../default_responses/json/429.json").to_owned(),
73
+ ),
74
+ 502 => ContentSource::Inline(
75
+ include_str!("../../../../default_responses/json/502.json").to_owned(),
76
+ ),
77
+ 503 => ContentSource::Inline(
78
+ include_str!("../../../../default_responses/json/503.json").to_owned(),
79
+ ),
80
+ 504 => ContentSource::Inline(
81
+ include_str!("../../../../default_responses/json/504.json").to_owned(),
82
+ ),
83
+ _ => ContentSource::Inline("Unexpected Error".to_owned()),
84
+ },
85
+ }
86
+ }
87
+ }
88
+ impl ErrorResponse {
89
+ pub fn fallback_body_for(code: u16, accept: ResponseFormat) -> BoxBody<Bytes, Infallible> {
90
+ let source = match accept {
91
+ ResponseFormat::TEXT => DefaultFormat::Plaintext.response_for_code(code),
92
+ ResponseFormat::HTML => DefaultFormat::Html.response_for_code(code),
93
+ ResponseFormat::JSON => DefaultFormat::Json.response_for_code(code),
94
+ ResponseFormat::UNKNOWN => ContentSource::Inline("Unexpected Error".to_owned()),
95
+ };
96
+ match source {
97
+ ContentSource::Inline(bytes) => BoxBody::new(Full::new(Bytes::from(bytes))),
98
+ ContentSource::File(_) => BoxBody::new(Full::new(Bytes::from("Unexpected error"))),
99
+ }
100
+ }
101
+ pub fn internal_server_error() -> Self {
102
+ ErrorResponse {
103
+ code: 500,
104
+ plaintext: Some(DefaultFormat::Plaintext.response_for_code(500)),
105
+ html: Some(DefaultFormat::Html.response_for_code(500)),
106
+ json: Some(DefaultFormat::Json.response_for_code(500)),
107
+ default: DefaultFormat::Html,
108
+ }
109
+ }
110
+
111
+ pub fn not_found() -> Self {
112
+ ErrorResponse {
113
+ code: 404,
114
+ plaintext: Some(DefaultFormat::Plaintext.response_for_code(404)),
115
+ html: Some(DefaultFormat::Html.response_for_code(404)),
116
+ json: Some(DefaultFormat::Json.response_for_code(404)),
117
+ default: DefaultFormat::Html,
118
+ }
119
+ }
120
+
121
+ pub fn unauthorized() -> Self {
122
+ ErrorResponse {
123
+ code: 401,
124
+ plaintext: Some(DefaultFormat::Plaintext.response_for_code(401)),
125
+ html: Some(DefaultFormat::Html.response_for_code(401)),
126
+ json: Some(DefaultFormat::Json.response_for_code(401)),
127
+ default: DefaultFormat::Html,
128
+ }
129
+ }
130
+
131
+ pub fn forbidden() -> Self {
132
+ ErrorResponse {
133
+ code: 403,
134
+ plaintext: Some(DefaultFormat::Plaintext.response_for_code(403)),
135
+ html: Some(DefaultFormat::Html.response_for_code(403)),
136
+ json: Some(DefaultFormat::Json.response_for_code(403)),
137
+ default: DefaultFormat::Html,
138
+ }
139
+ }
140
+
141
+ pub fn payload_too_large() -> Self {
142
+ ErrorResponse {
143
+ code: 413,
144
+ plaintext: Some(DefaultFormat::Plaintext.response_for_code(413)),
145
+ html: Some(DefaultFormat::Html.response_for_code(413)),
146
+ json: Some(DefaultFormat::Json.response_for_code(413)),
147
+ default: DefaultFormat::Html,
148
+ }
149
+ }
150
+
151
+ pub fn too_many_requests() -> Self {
152
+ ErrorResponse {
153
+ code: 429,
154
+ plaintext: Some(DefaultFormat::Plaintext.response_for_code(429)),
155
+ html: Some(DefaultFormat::Html.response_for_code(429)),
156
+ json: Some(DefaultFormat::Json.response_for_code(429)),
157
+ default: DefaultFormat::Html,
158
+ }
159
+ }
160
+
161
+ pub fn bad_gateway() -> Self {
162
+ ErrorResponse {
163
+ code: 502,
164
+ plaintext: Some(DefaultFormat::Plaintext.response_for_code(502)),
165
+ html: Some(DefaultFormat::Html.response_for_code(502)),
166
+ json: Some(DefaultFormat::Json.response_for_code(502)),
167
+ default: DefaultFormat::Html,
168
+ }
169
+ }
170
+
171
+ pub fn service_unavailable() -> Self {
172
+ ErrorResponse {
173
+ code: 503,
174
+ plaintext: Some(DefaultFormat::Plaintext.response_for_code(503)),
175
+ html: Some(DefaultFormat::Html.response_for_code(503)),
176
+ json: Some(DefaultFormat::Json.response_for_code(503)),
177
+ default: DefaultFormat::Html,
178
+ }
179
+ }
180
+
181
+ pub fn gateway_timeout() -> Self {
182
+ ErrorResponse {
183
+ code: 504,
184
+ plaintext: Some(DefaultFormat::Plaintext.response_for_code(504)),
185
+ html: Some(DefaultFormat::Html.response_for_code(504)),
186
+ json: Some(DefaultFormat::Json.response_for_code(504)),
187
+ default: DefaultFormat::Html,
188
+ }
189
+ }
190
+ }
@@ -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
  }