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.
- checksums.yaml +4 -4
- data/Cargo.lock +113 -593
- data/Cargo.toml +6 -0
- data/crates/itsi_error/Cargo.toml +1 -0
- data/crates/itsi_error/src/lib.rs +100 -10
- data/crates/itsi_scheduler/src/itsi_scheduler.rs +1 -1
- data/crates/itsi_server/Cargo.toml +9 -11
- data/crates/itsi_server/src/default_responses/html/401.html +68 -0
- data/crates/itsi_server/src/default_responses/html/403.html +68 -0
- data/crates/itsi_server/src/default_responses/html/404.html +68 -0
- data/crates/itsi_server/src/default_responses/html/413.html +71 -0
- data/crates/itsi_server/src/default_responses/html/429.html +68 -0
- data/crates/itsi_server/src/default_responses/html/500.html +71 -0
- data/crates/itsi_server/src/default_responses/html/502.html +71 -0
- data/crates/itsi_server/src/default_responses/html/503.html +68 -0
- data/crates/itsi_server/src/default_responses/html/504.html +69 -0
- data/crates/itsi_server/src/default_responses/html/index.html +238 -0
- data/crates/itsi_server/src/default_responses/json/401.json +6 -0
- data/crates/itsi_server/src/default_responses/json/403.json +6 -0
- data/crates/itsi_server/src/default_responses/json/404.json +6 -0
- data/crates/itsi_server/src/default_responses/json/413.json +6 -0
- data/crates/itsi_server/src/default_responses/json/429.json +6 -0
- data/crates/itsi_server/src/default_responses/json/500.json +6 -0
- data/crates/itsi_server/src/default_responses/json/502.json +6 -0
- data/crates/itsi_server/src/default_responses/json/503.json +6 -0
- data/crates/itsi_server/src/default_responses/json/504.json +6 -0
- data/crates/itsi_server/src/default_responses/mod.rs +11 -0
- data/crates/itsi_server/src/lib.rs +58 -26
- data/crates/itsi_server/src/prelude.rs +2 -0
- data/crates/itsi_server/src/ruby_types/README.md +21 -0
- data/crates/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +8 -6
- data/crates/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
- data/crates/itsi_server/src/ruby_types/{itsi_grpc_stream → itsi_grpc_response_stream}/mod.rs +121 -73
- data/crates/itsi_server/src/ruby_types/itsi_http_request.rs +103 -40
- data/crates/itsi_server/src/ruby_types/itsi_http_response.rs +8 -5
- data/crates/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +4 -4
- data/crates/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +37 -17
- data/crates/itsi_server/src/ruby_types/itsi_server.rs +4 -3
- data/crates/itsi_server/src/ruby_types/mod.rs +6 -13
- data/crates/itsi_server/src/server/{bind.rs → binds/bind.rs} +23 -4
- data/crates/itsi_server/src/server/{listener.rs → binds/listener.rs} +24 -10
- data/crates/itsi_server/src/server/binds/mod.rs +4 -0
- data/crates/itsi_server/src/server/{tls.rs → binds/tls.rs} +9 -4
- data/crates/itsi_server/src/server/http_message_types.rs +97 -0
- data/crates/itsi_server/src/server/io_stream.rs +2 -1
- data/crates/itsi_server/src/server/middleware_stack/middleware.rs +28 -16
- data/crates/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +17 -8
- data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +47 -18
- data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +13 -9
- data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +145 -181
- data/crates/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +5 -2
- data/crates/itsi_server/src/server/middleware_stack/middlewares/compression.rs +37 -48
- data/crates/itsi_server/src/server/middleware_stack/middlewares/cors.rs +25 -20
- data/crates/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +14 -7
- data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +190 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +125 -95
- data/crates/itsi_server/src/server/middleware_stack/middlewares/etag.rs +9 -5
- data/crates/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +1 -4
- data/crates/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +25 -19
- data/crates/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +4 -4
- data/crates/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/mod.rs +9 -4
- data/crates/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +260 -62
- data/crates/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +29 -22
- data/crates/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +6 -6
- data/crates/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +6 -5
- data/crates/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +4 -2
- data/crates/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +51 -18
- data/crates/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +31 -13
- data/crates/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +13 -8
- data/crates/itsi_server/src/server/middleware_stack/mod.rs +101 -69
- data/crates/itsi_server/src/server/mod.rs +3 -9
- data/crates/itsi_server/src/server/process_worker.rs +21 -3
- data/crates/itsi_server/src/server/request_job.rs +2 -2
- data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +8 -3
- data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +26 -26
- data/crates/itsi_server/src/server/signal.rs +24 -41
- data/crates/itsi_server/src/server/size_limited_incoming.rs +101 -0
- data/crates/itsi_server/src/server/thread_worker.rs +59 -28
- data/crates/itsi_server/src/services/itsi_http_service.rs +239 -0
- data/crates/itsi_server/src/services/mime_types.rs +1416 -0
- data/crates/itsi_server/src/services/mod.rs +6 -0
- data/crates/itsi_server/src/services/password_hasher.rs +83 -0
- data/crates/itsi_server/src/{server → services}/rate_limiter.rs +35 -31
- data/crates/itsi_server/src/{server → services}/static_file_server.rs +521 -181
- data/crates/itsi_tracing/src/lib.rs +145 -55
- data/{Itsi.rb → foo/Itsi.rb} +6 -9
- data/gems/scheduler/Cargo.lock +7 -0
- data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
- data/gems/scheduler/test/helpers/test_helper.rb +0 -1
- data/gems/scheduler/test/test_address_resolve.rb +0 -1
- data/gems/scheduler/test/test_network_io.rb +1 -1
- data/gems/scheduler/test/test_process_wait.rb +0 -1
- data/gems/server/Cargo.lock +113 -593
- data/gems/server/exe/itsi +65 -19
- data/gems/server/itsi-server.gemspec +4 -3
- data/gems/server/lib/itsi/http_request/response_status_shortcodes.rb +74 -0
- data/gems/server/lib/itsi/http_request.rb +116 -17
- data/gems/server/lib/itsi/http_response.rb +2 -0
- data/gems/server/lib/itsi/passfile.rb +109 -0
- data/gems/server/lib/itsi/server/config/dsl.rb +160 -101
- data/gems/server/lib/itsi/server/config.rb +58 -23
- data/gems/server/lib/itsi/server/default_app/default_app.rb +25 -29
- data/gems/server/lib/itsi/server/default_app/index.html +113 -89
- data/gems/server/lib/itsi/server/{Itsi.rb → default_config/Itsi-rackup.rb} +1 -1
- data/gems/server/lib/itsi/server/default_config/Itsi.rb +107 -0
- data/gems/server/lib/itsi/server/grpc/grpc_call.rb +246 -0
- data/gems/server/lib/itsi/server/grpc/grpc_interface.rb +100 -0
- data/gems/server/lib/itsi/server/grpc/reflection/v1/reflection_pb.rb +26 -0
- data/gems/server/lib/itsi/server/grpc/reflection/v1/reflection_services_pb.rb +122 -0
- data/gems/server/lib/itsi/server/route_tester.rb +107 -0
- data/gems/server/lib/itsi/server/typed_handlers/param_parser.rb +200 -0
- data/gems/server/lib/itsi/server/typed_handlers/source_parser.rb +55 -0
- data/gems/server/lib/itsi/server/typed_handlers.rb +17 -0
- data/gems/server/lib/itsi/server/version.rb +1 -1
- data/gems/server/lib/itsi/server.rb +82 -12
- data/gems/server/lib/ruby_lsp/itsi/addon.rb +111 -0
- data/gems/server/lib/shell_completions/completions.rb +26 -0
- data/gems/server/test/helpers/test_helper.rb +2 -1
- data/lib/itsi/version.rb +1 -1
- data/sandbox/README.md +5 -0
- data/sandbox/itsi_file/Gemfile +4 -2
- data/sandbox/itsi_file/Gemfile.lock +48 -6
- data/sandbox/itsi_file/Itsi.rb +326 -129
- data/sandbox/itsi_file/call.json +1 -0
- data/sandbox/itsi_file/echo_client/Gemfile +10 -0
- data/sandbox/itsi_file/echo_client/Gemfile.lock +27 -0
- data/sandbox/itsi_file/echo_client/README.md +95 -0
- data/sandbox/itsi_file/echo_client/echo_client.rb +164 -0
- data/sandbox/itsi_file/echo_client/gen_proto.sh +17 -0
- data/sandbox/itsi_file/echo_client/lib/echo_pb.rb +16 -0
- data/sandbox/itsi_file/echo_client/lib/echo_services_pb.rb +29 -0
- data/sandbox/itsi_file/echo_client/run_client.rb +64 -0
- data/sandbox/itsi_file/echo_client/test_compressions.sh +20 -0
- data/sandbox/itsi_file/echo_service_nonitsi/Gemfile +10 -0
- data/sandbox/itsi_file/echo_service_nonitsi/Gemfile.lock +79 -0
- data/sandbox/itsi_file/echo_service_nonitsi/echo.proto +26 -0
- data/sandbox/itsi_file/echo_service_nonitsi/echo_pb.rb +16 -0
- data/sandbox/itsi_file/echo_service_nonitsi/echo_services_pb.rb +29 -0
- data/sandbox/itsi_file/echo_service_nonitsi/server.rb +52 -0
- data/sandbox/itsi_sandbox_async/config.ru +0 -1
- data/sandbox/itsi_sandbox_rack/Gemfile.lock +2 -2
- data/sandbox/itsi_sandbox_rails/Gemfile +2 -2
- data/sandbox/itsi_sandbox_rails/Gemfile.lock +76 -2
- data/sandbox/itsi_sandbox_rails/app/controllers/home_controller.rb +15 -0
- data/sandbox/itsi_sandbox_rails/config/environments/development.rb +1 -0
- data/sandbox/itsi_sandbox_rails/config/environments/production.rb +1 -0
- data/sandbox/itsi_sandbox_rails/config/routes.rb +2 -0
- data/sandbox/itsi_sinatra/app.rb +0 -1
- data/sandbox/static_files/.env +1 -0
- data/sandbox/static_files/404.html +25 -0
- data/sandbox/static_files/_DSC0102.NEF.jpg +0 -0
- data/sandbox/static_files/about.html +68 -0
- data/sandbox/static_files/tiny.html +1 -0
- data/sandbox/static_files/writebook.zip +0 -0
- data/tasks.txt +28 -32
- metadata +87 -26
- data/crates/itsi_error/src/from.rs +0 -68
- data/crates/itsi_server/src/ruby_types/itsi_grpc_request.rs +0 -147
- data/crates/itsi_server/src/ruby_types/itsi_grpc_response.rs +0 -19
- data/crates/itsi_server/src/server/itsi_service.rs +0 -172
- data/crates/itsi_server/src/server/middleware_stack/middlewares/grpc_service.rs +0 -72
- data/crates/itsi_server/src/server/types.rs +0 -43
- data/gems/server/lib/itsi/server/grpc_interface.rb +0 -213
- data/sandbox/itsi_file/public/assets/index.html +0 -1
- /data/crates/itsi_server/src/server/{bind_protocol.rs → binds/bind_protocol.rs} +0 -0
- /data/crates/itsi_server/src/server/{tls → binds/tls}/locked_dir_cache.rs +0 -0
- /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::
|
3
|
-
|
4
|
-
|
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::
|
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::
|
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::
|
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::
|
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::
|
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::
|
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::
|
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::
|
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::
|
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::
|
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::
|
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::
|
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::
|
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::
|
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
|
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::
|
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(
|
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::
|
2
|
-
|
3
|
-
|
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::
|
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::
|
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
|
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
|
46
|
+
self.error_response
|
47
|
+
.to_http_response(req.accept().into())
|
48
|
+
.await,
|
42
49
|
));
|
43
50
|
}
|
44
51
|
}
|
data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs
ADDED
@@ -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
|
2
|
+
use http::header::CONTENT_TYPE;
|
10
3
|
use http::Response;
|
11
4
|
use http_body_util::{combinators::BoxBody, Full};
|
12
|
-
use
|
13
|
-
use
|
14
|
-
use std::path::
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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,
|
55
|
-
let
|
56
|
-
let
|
57
|
-
|
58
|
-
|
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
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
) ->
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
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
|
-
|
155
|
+
ErrorResponse::fallback_body_for(code, accept)
|
126
156
|
}
|
127
157
|
}
|