itsi-scheduler 0.1.5 → 0.2.2
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 +120 -52
- data/README.md +57 -24
- data/Rakefile +0 -4
- data/ext/itsi_acme/Cargo.toml +86 -0
- data/ext/itsi_acme/examples/high_level.rs +63 -0
- data/ext/itsi_acme/examples/high_level_warp.rs +52 -0
- data/ext/itsi_acme/examples/low_level.rs +87 -0
- data/ext/itsi_acme/examples/low_level_axum.rs +66 -0
- data/ext/itsi_acme/src/acceptor.rs +81 -0
- data/ext/itsi_acme/src/acme.rs +354 -0
- data/ext/itsi_acme/src/axum.rs +86 -0
- data/ext/itsi_acme/src/cache.rs +39 -0
- data/ext/itsi_acme/src/caches/boxed.rs +80 -0
- data/ext/itsi_acme/src/caches/composite.rs +69 -0
- data/ext/itsi_acme/src/caches/dir.rs +106 -0
- data/ext/itsi_acme/src/caches/mod.rs +11 -0
- data/ext/itsi_acme/src/caches/no.rs +78 -0
- data/ext/itsi_acme/src/caches/test.rs +136 -0
- data/ext/itsi_acme/src/config.rs +172 -0
- data/ext/itsi_acme/src/https_helper.rs +69 -0
- data/ext/itsi_acme/src/incoming.rs +142 -0
- data/ext/itsi_acme/src/jose.rs +161 -0
- data/ext/itsi_acme/src/lib.rs +142 -0
- data/ext/itsi_acme/src/resolver.rs +59 -0
- data/ext/itsi_acme/src/state.rs +424 -0
- data/ext/itsi_error/Cargo.toml +1 -0
- data/ext/itsi_error/src/lib.rs +106 -7
- data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
- data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
- data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
- data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
- data/ext/itsi_rb_helpers/Cargo.toml +1 -0
- data/ext/itsi_rb_helpers/src/heap_value.rs +18 -0
- data/ext/itsi_rb_helpers/src/lib.rs +63 -12
- data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
- data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
- data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
- data/ext/itsi_rb_helpers/target/debug/build/rb-sys-eb9ed4ff3a60f995/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
- data/ext/itsi_scheduler/Cargo.toml +1 -1
- data/ext/itsi_scheduler/src/itsi_scheduler.rs +9 -3
- data/ext/itsi_scheduler/src/lib.rs +1 -0
- data/ext/itsi_server/Cargo.lock +2956 -0
- data/ext/itsi_server/Cargo.toml +73 -29
- data/ext/itsi_server/src/default_responses/mod.rs +11 -0
- data/ext/itsi_server/src/env.rs +43 -0
- data/ext/itsi_server/src/lib.rs +114 -75
- data/ext/itsi_server/src/prelude.rs +2 -0
- data/ext/itsi_server/src/{body_proxy → ruby_types/itsi_body_proxy}/big_bytes.rs +10 -5
- data/ext/itsi_server/src/{body_proxy/itsi_body_proxy.rs → ruby_types/itsi_body_proxy/mod.rs} +29 -8
- data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
- data/ext/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +264 -0
- data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +362 -0
- data/ext/itsi_server/src/{response/itsi_response.rs → ruby_types/itsi_http_response.rs} +84 -40
- data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +233 -0
- data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +565 -0
- data/ext/itsi_server/src/ruby_types/itsi_server.rs +86 -0
- data/ext/itsi_server/src/ruby_types/mod.rs +48 -0
- data/ext/itsi_server/src/server/{bind.rs → binds/bind.rs} +59 -24
- data/ext/itsi_server/src/server/binds/listener.rs +444 -0
- data/ext/itsi_server/src/server/binds/mod.rs +4 -0
- data/ext/itsi_server/src/server/{tls → binds/tls}/locked_dir_cache.rs +57 -19
- data/ext/itsi_server/src/server/{tls.rs → binds/tls.rs} +120 -31
- data/ext/itsi_server/src/server/byte_frame.rs +32 -0
- data/ext/itsi_server/src/server/http_message_types.rs +97 -0
- data/ext/itsi_server/src/server/io_stream.rs +2 -1
- data/ext/itsi_server/src/server/lifecycle_event.rs +3 -0
- data/ext/itsi_server/src/server/middleware_stack/middleware.rs +170 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +63 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +94 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +94 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +343 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +151 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +316 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +301 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/csp.rs +193 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +64 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +192 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +171 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +198 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +82 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +209 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +116 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +411 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +142 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +55 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +54 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +51 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +126 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +187 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +173 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +31 -0
- data/ext/itsi_server/src/server/middleware_stack/mod.rs +381 -0
- data/ext/itsi_server/src/server/mod.rs +7 -5
- data/ext/itsi_server/src/server/process_worker.rs +65 -14
- data/ext/itsi_server/src/server/redirect_type.rs +26 -0
- data/ext/itsi_server/src/server/request_job.rs +11 -0
- data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +150 -50
- data/ext/itsi_server/src/server/serve_strategy/mod.rs +9 -6
- data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +399 -165
- data/ext/itsi_server/src/server/signal.rs +33 -26
- data/ext/itsi_server/src/server/size_limited_incoming.rs +107 -0
- data/ext/itsi_server/src/server/thread_worker.rs +218 -107
- data/ext/itsi_server/src/services/cache_store.rs +74 -0
- data/ext/itsi_server/src/services/itsi_http_service.rs +257 -0
- data/ext/itsi_server/src/services/mime_types.rs +1416 -0
- data/ext/itsi_server/src/services/mod.rs +6 -0
- data/ext/itsi_server/src/services/password_hasher.rs +83 -0
- data/ext/itsi_server/src/services/rate_limiter.rs +580 -0
- data/ext/itsi_server/src/services/static_file_server.rs +1340 -0
- data/ext/itsi_tracing/Cargo.toml +1 -0
- data/ext/itsi_tracing/src/lib.rs +362 -33
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
- data/itsi-scheduler-100.png +0 -0
- data/lib/itsi/scheduler/version.rb +1 -1
- data/lib/itsi/scheduler.rb +11 -6
- metadata +117 -24
- data/CHANGELOG.md +0 -5
- data/CODE_OF_CONDUCT.md +0 -132
- data/LICENSE.txt +0 -21
- data/ext/itsi_error/src/from.rs +0 -71
- data/ext/itsi_server/extconf.rb +0 -6
- data/ext/itsi_server/src/body_proxy/mod.rs +0 -2
- data/ext/itsi_server/src/request/itsi_request.rs +0 -277
- data/ext/itsi_server/src/request/mod.rs +0 -1
- data/ext/itsi_server/src/response/mod.rs +0 -1
- data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -13
- data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -5
- data/ext/itsi_server/src/server/itsi_server.rs +0 -244
- data/ext/itsi_server/src/server/listener.rs +0 -327
- /data/ext/itsi_server/src/server/{bind_protocol.rs → binds/bind_protocol.rs} +0 -0
data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs
ADDED
@@ -0,0 +1,192 @@
|
|
1
|
+
use super::{ContentSource, DefaultFormat, ErrorResponse};
|
2
|
+
use crate::server::http_message_types::ResponseFormat;
|
3
|
+
use bytes::Bytes;
|
4
|
+
use http_body_util::{combinators::BoxBody, Full};
|
5
|
+
use std::{convert::Infallible, sync::Arc};
|
6
|
+
|
7
|
+
impl DefaultFormat {
|
8
|
+
pub fn response_for_code(&self, code: u16) -> ContentSource {
|
9
|
+
match self {
|
10
|
+
DefaultFormat::Plaintext => match code {
|
11
|
+
500 => ContentSource::Static(Arc::new("500 Internal Error".into())),
|
12
|
+
404 => ContentSource::Static(Arc::new("404 Not Found".into())),
|
13
|
+
401 => ContentSource::Static(Arc::new("401 Unauthorized".into())),
|
14
|
+
403 => ContentSource::Static(Arc::new("403 Forbidden".into())),
|
15
|
+
413 => ContentSource::Static(Arc::new("413 Payload Too Large".into())),
|
16
|
+
429 => ContentSource::Static(Arc::new("429 Too Many Requests".into())),
|
17
|
+
502 => ContentSource::Static(Arc::new("502 Bad Gateway".into())),
|
18
|
+
503 => ContentSource::Static(Arc::new("503 Service Unavailable".into())),
|
19
|
+
504 => ContentSource::Static(Arc::new("504 Gateway Timeout".into())),
|
20
|
+
_ => ContentSource::Static(Arc::new("Unexpected Error".into())),
|
21
|
+
},
|
22
|
+
DefaultFormat::Html => match code {
|
23
|
+
500 => ContentSource::Static(Arc::new(
|
24
|
+
include_str!("../../../../default_responses/html/500.html").into(),
|
25
|
+
)),
|
26
|
+
404 => ContentSource::Static(Arc::new(
|
27
|
+
include_str!("../../../../default_responses/html/404.html").into(),
|
28
|
+
)),
|
29
|
+
401 => ContentSource::Static(Arc::new(
|
30
|
+
include_str!("../../../../default_responses/html/401.html").into(),
|
31
|
+
)),
|
32
|
+
403 => ContentSource::Static(Arc::new(
|
33
|
+
include_str!("../../../../default_responses/html/403.html").into(),
|
34
|
+
)),
|
35
|
+
413 => ContentSource::Static(Arc::new(
|
36
|
+
include_str!("../../../../default_responses/html/413.html").into(),
|
37
|
+
)),
|
38
|
+
429 => ContentSource::Static(Arc::new(
|
39
|
+
include_str!("../../../../default_responses/html/429.html").into(),
|
40
|
+
)),
|
41
|
+
502 => ContentSource::Static(Arc::new(
|
42
|
+
include_str!("../../../../default_responses/html/502.html").into(),
|
43
|
+
)),
|
44
|
+
503 => ContentSource::Static(Arc::new(
|
45
|
+
include_str!("../../../../default_responses/html/503.html").into(),
|
46
|
+
)),
|
47
|
+
504 => ContentSource::Static(Arc::new(
|
48
|
+
include_str!("../../../../default_responses/html/504.html").into(),
|
49
|
+
)),
|
50
|
+
_ => ContentSource::Static(Arc::new(
|
51
|
+
include_str!("../../../../default_responses/html/500.html").into(),
|
52
|
+
)),
|
53
|
+
},
|
54
|
+
DefaultFormat::Json => match code {
|
55
|
+
500 => ContentSource::Static(Arc::new(
|
56
|
+
include_str!("../../../../default_responses/json/500.json").into(),
|
57
|
+
)),
|
58
|
+
404 => ContentSource::Static(Arc::new(
|
59
|
+
include_str!("../../../../default_responses/json/404.json").into(),
|
60
|
+
)),
|
61
|
+
401 => ContentSource::Static(Arc::new(
|
62
|
+
include_str!("../../../../default_responses/json/401.json").into(),
|
63
|
+
)),
|
64
|
+
403 => ContentSource::Static(Arc::new(
|
65
|
+
include_str!("../../../../default_responses/json/403.json").into(),
|
66
|
+
)),
|
67
|
+
413 => ContentSource::Static(Arc::new(
|
68
|
+
include_str!("../../../../default_responses/json/413.json").into(),
|
69
|
+
)),
|
70
|
+
429 => ContentSource::Static(Arc::new(
|
71
|
+
include_str!("../../../../default_responses/json/429.json").into(),
|
72
|
+
)),
|
73
|
+
502 => ContentSource::Static(Arc::new(
|
74
|
+
include_str!("../../../../default_responses/json/502.json").into(),
|
75
|
+
)),
|
76
|
+
503 => ContentSource::Static(Arc::new(
|
77
|
+
include_str!("../../../../default_responses/json/503.json").into(),
|
78
|
+
)),
|
79
|
+
504 => ContentSource::Static(Arc::new(
|
80
|
+
include_str!("../../../../default_responses/json/504.json").into(),
|
81
|
+
)),
|
82
|
+
_ => ContentSource::Static(Arc::new("Unexpected Error".into())),
|
83
|
+
},
|
84
|
+
}
|
85
|
+
}
|
86
|
+
}
|
87
|
+
impl ErrorResponse {
|
88
|
+
pub fn fallback_body_for(code: u16, accept: ResponseFormat) -> BoxBody<Bytes, Infallible> {
|
89
|
+
let source = match accept {
|
90
|
+
ResponseFormat::TEXT => DefaultFormat::Plaintext.response_for_code(code),
|
91
|
+
ResponseFormat::HTML => DefaultFormat::Html.response_for_code(code),
|
92
|
+
ResponseFormat::JSON => DefaultFormat::Json.response_for_code(code),
|
93
|
+
ResponseFormat::UNKNOWN => ContentSource::Inline("Unexpected Error".to_owned()),
|
94
|
+
};
|
95
|
+
match source {
|
96
|
+
ContentSource::Inline(bytes) => BoxBody::new(Full::new(Bytes::from(bytes))),
|
97
|
+
ContentSource::Static(text) => {
|
98
|
+
BoxBody::new(Full::new(Bytes::from(String::from(text.as_str()))))
|
99
|
+
}
|
100
|
+
ContentSource::File(_) => BoxBody::new(Full::new(Bytes::from("Unexpected error"))),
|
101
|
+
}
|
102
|
+
}
|
103
|
+
pub fn internal_server_error() -> Self {
|
104
|
+
ErrorResponse {
|
105
|
+
code: 500,
|
106
|
+
plaintext: Some(DefaultFormat::Plaintext.response_for_code(500)),
|
107
|
+
html: Some(DefaultFormat::Html.response_for_code(500)),
|
108
|
+
json: Some(DefaultFormat::Json.response_for_code(500)),
|
109
|
+
default: DefaultFormat::Html,
|
110
|
+
}
|
111
|
+
}
|
112
|
+
|
113
|
+
pub fn not_found() -> Self {
|
114
|
+
ErrorResponse {
|
115
|
+
code: 404,
|
116
|
+
plaintext: Some(DefaultFormat::Plaintext.response_for_code(404)),
|
117
|
+
html: Some(DefaultFormat::Html.response_for_code(404)),
|
118
|
+
json: Some(DefaultFormat::Json.response_for_code(404)),
|
119
|
+
default: DefaultFormat::Html,
|
120
|
+
}
|
121
|
+
}
|
122
|
+
|
123
|
+
pub fn unauthorized() -> Self {
|
124
|
+
ErrorResponse {
|
125
|
+
code: 401,
|
126
|
+
plaintext: Some(DefaultFormat::Plaintext.response_for_code(401)),
|
127
|
+
html: Some(DefaultFormat::Html.response_for_code(401)),
|
128
|
+
json: Some(DefaultFormat::Json.response_for_code(401)),
|
129
|
+
default: DefaultFormat::Html,
|
130
|
+
}
|
131
|
+
}
|
132
|
+
|
133
|
+
pub fn forbidden() -> Self {
|
134
|
+
ErrorResponse {
|
135
|
+
code: 403,
|
136
|
+
plaintext: Some(DefaultFormat::Plaintext.response_for_code(403)),
|
137
|
+
html: Some(DefaultFormat::Html.response_for_code(403)),
|
138
|
+
json: Some(DefaultFormat::Json.response_for_code(403)),
|
139
|
+
default: DefaultFormat::Html,
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
143
|
+
pub fn payload_too_large() -> Self {
|
144
|
+
ErrorResponse {
|
145
|
+
code: 413,
|
146
|
+
plaintext: Some(DefaultFormat::Plaintext.response_for_code(413)),
|
147
|
+
html: Some(DefaultFormat::Html.response_for_code(413)),
|
148
|
+
json: Some(DefaultFormat::Json.response_for_code(413)),
|
149
|
+
default: DefaultFormat::Html,
|
150
|
+
}
|
151
|
+
}
|
152
|
+
|
153
|
+
pub fn too_many_requests() -> Self {
|
154
|
+
ErrorResponse {
|
155
|
+
code: 429,
|
156
|
+
plaintext: Some(DefaultFormat::Plaintext.response_for_code(429)),
|
157
|
+
html: Some(DefaultFormat::Html.response_for_code(429)),
|
158
|
+
json: Some(DefaultFormat::Json.response_for_code(429)),
|
159
|
+
default: DefaultFormat::Html,
|
160
|
+
}
|
161
|
+
}
|
162
|
+
|
163
|
+
pub fn bad_gateway() -> Self {
|
164
|
+
ErrorResponse {
|
165
|
+
code: 502,
|
166
|
+
plaintext: Some(DefaultFormat::Plaintext.response_for_code(502)),
|
167
|
+
html: Some(DefaultFormat::Html.response_for_code(502)),
|
168
|
+
json: Some(DefaultFormat::Json.response_for_code(502)),
|
169
|
+
default: DefaultFormat::Html,
|
170
|
+
}
|
171
|
+
}
|
172
|
+
|
173
|
+
pub fn service_unavailable() -> Self {
|
174
|
+
ErrorResponse {
|
175
|
+
code: 503,
|
176
|
+
plaintext: Some(DefaultFormat::Plaintext.response_for_code(503)),
|
177
|
+
html: Some(DefaultFormat::Html.response_for_code(503)),
|
178
|
+
json: Some(DefaultFormat::Json.response_for_code(503)),
|
179
|
+
default: DefaultFormat::Html,
|
180
|
+
}
|
181
|
+
}
|
182
|
+
|
183
|
+
pub fn gateway_timeout() -> Self {
|
184
|
+
ErrorResponse {
|
185
|
+
code: 504,
|
186
|
+
plaintext: Some(DefaultFormat::Plaintext.response_for_code(504)),
|
187
|
+
html: Some(DefaultFormat::Html.response_for_code(504)),
|
188
|
+
json: Some(DefaultFormat::Json.response_for_code(504)),
|
189
|
+
default: DefaultFormat::Html,
|
190
|
+
}
|
191
|
+
}
|
192
|
+
}
|
@@ -0,0 +1,171 @@
|
|
1
|
+
use bytes::Bytes;
|
2
|
+
use http::header::CONTENT_TYPE;
|
3
|
+
use http::Response;
|
4
|
+
use http_body_util::{combinators::BoxBody, Full};
|
5
|
+
use serde::{Deserialize, Deserializer};
|
6
|
+
use std::convert::Infallible;
|
7
|
+
use std::path::PathBuf;
|
8
|
+
use std::sync::Arc;
|
9
|
+
use tracing::warn;
|
10
|
+
|
11
|
+
use crate::server::http_message_types::{HttpResponse, ResponseFormat};
|
12
|
+
use crate::services::static_file_server::ROOT_STATIC_FILE_SERVER;
|
13
|
+
mod default_responses;
|
14
|
+
|
15
|
+
#[derive(Debug, Clone, Deserialize)]
|
16
|
+
pub enum ContentSource {
|
17
|
+
#[serde(rename(deserialize = "inline"))]
|
18
|
+
Inline(String),
|
19
|
+
#[serde(rename(deserialize = "file"))]
|
20
|
+
File(PathBuf),
|
21
|
+
#[serde(rename(deserialize = "static"))]
|
22
|
+
#[serde(skip_deserializing)]
|
23
|
+
Static(Arc<String>),
|
24
|
+
}
|
25
|
+
|
26
|
+
#[derive(Debug, Clone, Deserialize, Default)]
|
27
|
+
pub enum DefaultFormat {
|
28
|
+
#[serde(rename(deserialize = "plaintext"))]
|
29
|
+
Plaintext,
|
30
|
+
#[default]
|
31
|
+
#[serde(rename(deserialize = "html"))]
|
32
|
+
Html,
|
33
|
+
#[serde(rename(deserialize = "json"))]
|
34
|
+
Json,
|
35
|
+
}
|
36
|
+
|
37
|
+
#[derive(Debug, Clone)]
|
38
|
+
pub struct ErrorResponse {
|
39
|
+
pub code: u16,
|
40
|
+
pub plaintext: Option<ContentSource>,
|
41
|
+
pub html: Option<ContentSource>,
|
42
|
+
pub json: Option<ContentSource>,
|
43
|
+
pub default: DefaultFormat, // must match one of the provided fields
|
44
|
+
}
|
45
|
+
|
46
|
+
impl<'de> Deserialize<'de> for ErrorResponse {
|
47
|
+
fn deserialize<D>(deserializer: D) -> Result<ErrorResponse, D::Error>
|
48
|
+
where
|
49
|
+
D: Deserializer<'de>,
|
50
|
+
{
|
51
|
+
let def = ErrorResponseDef::deserialize(deserializer)?;
|
52
|
+
Ok(def.into())
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
/// An untagged enum to support two input formats:
|
57
|
+
/// - A detailed struct with all fields.
|
58
|
+
/// - A string with the name of a default error response.
|
59
|
+
#[derive(Debug, Clone, Deserialize)]
|
60
|
+
#[serde(untagged)]
|
61
|
+
pub enum ErrorResponseDef {
|
62
|
+
Detailed {
|
63
|
+
code: u16,
|
64
|
+
plaintext: Option<ContentSource>,
|
65
|
+
html: Option<ContentSource>,
|
66
|
+
json: Option<ContentSource>,
|
67
|
+
default: DefaultFormat,
|
68
|
+
},
|
69
|
+
Named(String),
|
70
|
+
}
|
71
|
+
|
72
|
+
impl From<ErrorResponseDef> for ErrorResponse {
|
73
|
+
fn from(def: ErrorResponseDef) -> Self {
|
74
|
+
match def {
|
75
|
+
ErrorResponseDef::Detailed {
|
76
|
+
code,
|
77
|
+
plaintext,
|
78
|
+
html,
|
79
|
+
json,
|
80
|
+
default,
|
81
|
+
} => ErrorResponse {
|
82
|
+
code,
|
83
|
+
plaintext,
|
84
|
+
html,
|
85
|
+
json,
|
86
|
+
default,
|
87
|
+
},
|
88
|
+
ErrorResponseDef::Named(name) => match name.as_str() {
|
89
|
+
"internal_server_error" => ErrorResponse::internal_server_error(),
|
90
|
+
"not_found" => ErrorResponse::not_found(),
|
91
|
+
"unauthorized" => ErrorResponse::unauthorized(),
|
92
|
+
"forbidden" => ErrorResponse::forbidden(),
|
93
|
+
"payload_too_large" => ErrorResponse::payload_too_large(),
|
94
|
+
"too_many_requests" => ErrorResponse::too_many_requests(),
|
95
|
+
"bad_gateway" => ErrorResponse::bad_gateway(),
|
96
|
+
"service_unavailable" => ErrorResponse::service_unavailable(),
|
97
|
+
"gateway_timeout" => ErrorResponse::gateway_timeout(),
|
98
|
+
_ => {
|
99
|
+
warn!(
|
100
|
+
"Unknown error response name: {}. Using internal server error.",
|
101
|
+
name
|
102
|
+
);
|
103
|
+
ErrorResponse::internal_server_error()
|
104
|
+
}
|
105
|
+
},
|
106
|
+
}
|
107
|
+
}
|
108
|
+
}
|
109
|
+
|
110
|
+
impl ErrorResponse {
|
111
|
+
pub(crate) async fn to_http_response(&self, accept: ResponseFormat) -> HttpResponse {
|
112
|
+
let mut resp = Response::builder().status(self.code);
|
113
|
+
let response = match accept {
|
114
|
+
ResponseFormat::TEXT => {
|
115
|
+
resp = resp.header(CONTENT_TYPE, "text/plain");
|
116
|
+
resp.body(Self::get_response_body(self.code, &self.plaintext, accept).await)
|
117
|
+
}
|
118
|
+
ResponseFormat::HTML => {
|
119
|
+
resp = resp.header(CONTENT_TYPE, "text/html");
|
120
|
+
resp.body(Self::get_response_body(self.code, &self.html, accept).await)
|
121
|
+
}
|
122
|
+
ResponseFormat::JSON => {
|
123
|
+
resp = resp.header(CONTENT_TYPE, "application/json");
|
124
|
+
resp.body(Self::get_response_body(self.code, &self.json, accept).await)
|
125
|
+
}
|
126
|
+
ResponseFormat::UNKNOWN => match self.default {
|
127
|
+
DefaultFormat::Plaintext => {
|
128
|
+
resp = resp.header(CONTENT_TYPE, "text/plain");
|
129
|
+
resp.body(Self::get_response_body(self.code, &self.plaintext, accept).await)
|
130
|
+
}
|
131
|
+
DefaultFormat::Html => {
|
132
|
+
resp = resp.header(CONTENT_TYPE, "text/html");
|
133
|
+
resp.body(Self::get_response_body(self.code, &self.html, accept).await)
|
134
|
+
}
|
135
|
+
DefaultFormat::Json => {
|
136
|
+
resp = resp.header(CONTENT_TYPE, "application/json");
|
137
|
+
resp.body(Self::get_response_body(self.code, &self.json, accept).await)
|
138
|
+
}
|
139
|
+
},
|
140
|
+
};
|
141
|
+
response.unwrap()
|
142
|
+
}
|
143
|
+
|
144
|
+
async fn get_response_body(
|
145
|
+
code: u16,
|
146
|
+
source: &Option<ContentSource>,
|
147
|
+
accept: ResponseFormat,
|
148
|
+
) -> BoxBody<Bytes, Infallible> {
|
149
|
+
match source {
|
150
|
+
Some(ContentSource::Inline(text)) => {
|
151
|
+
return BoxBody::new(Full::new(Bytes::from(text.clone())));
|
152
|
+
}
|
153
|
+
Some(ContentSource::Static(text)) => {
|
154
|
+
return BoxBody::new(Full::new(Bytes::from(String::from(text.as_str()))));
|
155
|
+
}
|
156
|
+
Some(ContentSource::File(path)) => {
|
157
|
+
// Convert the PathBuf to a &str (assumes valid UTF-8).
|
158
|
+
if let Some(path_str) = path.to_str() {
|
159
|
+
let response = ROOT_STATIC_FILE_SERVER
|
160
|
+
.serve_single(path_str, accept.clone(), &[])
|
161
|
+
.await;
|
162
|
+
if response.status().is_success() {
|
163
|
+
return response.into_body();
|
164
|
+
}
|
165
|
+
}
|
166
|
+
}
|
167
|
+
None => {}
|
168
|
+
}
|
169
|
+
ErrorResponse::fallback_body_for(code, accept)
|
170
|
+
}
|
171
|
+
}
|
@@ -0,0 +1,198 @@
|
|
1
|
+
use crate::{
|
2
|
+
server::http_message_types::{HttpRequest, HttpResponse},
|
3
|
+
services::itsi_http_service::HttpRequestContext,
|
4
|
+
};
|
5
|
+
|
6
|
+
use super::{FromValue, MiddlewareLayer};
|
7
|
+
use async_trait::async_trait;
|
8
|
+
use base64::{engine::general_purpose, Engine as _};
|
9
|
+
use bytes::{Bytes, BytesMut};
|
10
|
+
use either::Either;
|
11
|
+
use futures::TryStreamExt;
|
12
|
+
use http::{header, HeaderValue, Response, StatusCode};
|
13
|
+
use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full};
|
14
|
+
use hyper::body::Body;
|
15
|
+
use magnus::error::Result;
|
16
|
+
use serde::Deserialize;
|
17
|
+
use sha2::{Digest, Sha256};
|
18
|
+
use tracing::debug;
|
19
|
+
|
20
|
+
#[derive(Debug, Clone, Copy, Deserialize, Default)]
|
21
|
+
pub enum ETagType {
|
22
|
+
#[serde(rename = "strong")]
|
23
|
+
#[default]
|
24
|
+
Strong,
|
25
|
+
#[serde(rename = "weak")]
|
26
|
+
Weak,
|
27
|
+
}
|
28
|
+
|
29
|
+
#[derive(Debug, Clone, Copy, Deserialize, Default)]
|
30
|
+
pub enum HashAlgorithm {
|
31
|
+
#[serde(rename = "sha256")]
|
32
|
+
#[default]
|
33
|
+
Sha256,
|
34
|
+
#[serde(rename = "md5")]
|
35
|
+
Md5,
|
36
|
+
}
|
37
|
+
|
38
|
+
#[derive(Debug, Clone, Deserialize)]
|
39
|
+
pub struct ETag {
|
40
|
+
#[serde(default)]
|
41
|
+
pub r#type: ETagType,
|
42
|
+
#[serde(default)]
|
43
|
+
pub algorithm: HashAlgorithm,
|
44
|
+
#[serde(default)]
|
45
|
+
pub min_body_size: usize,
|
46
|
+
#[serde(default = "default_true")]
|
47
|
+
pub handle_if_none_match: bool,
|
48
|
+
}
|
49
|
+
|
50
|
+
fn default_true() -> bool {
|
51
|
+
true
|
52
|
+
}
|
53
|
+
|
54
|
+
#[async_trait]
|
55
|
+
impl MiddlewareLayer for ETag {
|
56
|
+
async fn before(
|
57
|
+
&self,
|
58
|
+
req: HttpRequest,
|
59
|
+
context: &mut HttpRequestContext,
|
60
|
+
) -> Result<Either<HttpRequest, HttpResponse>> {
|
61
|
+
// Store if-none-match header in context if present for later use in after hook
|
62
|
+
if self.handle_if_none_match {
|
63
|
+
if let Some(if_none_match) = req.headers().get(header::IF_NONE_MATCH) {
|
64
|
+
debug!(target: "middleware::etag", "Received If-None-Match header: {:?}", if_none_match);
|
65
|
+
if let Ok(etag_value) = if_none_match.to_str() {
|
66
|
+
context.set_if_none_match(Some(etag_value.to_string()));
|
67
|
+
}
|
68
|
+
}
|
69
|
+
}
|
70
|
+
Ok(Either::Left(req))
|
71
|
+
}
|
72
|
+
|
73
|
+
async fn after(&self, resp: HttpResponse, context: &mut HttpRequestContext) -> HttpResponse {
|
74
|
+
// Skip for error responses or responses that shouldn't have ETags
|
75
|
+
match resp.status() {
|
76
|
+
StatusCode::OK
|
77
|
+
| StatusCode::CREATED
|
78
|
+
| StatusCode::ACCEPTED
|
79
|
+
| StatusCode::NON_AUTHORITATIVE_INFORMATION
|
80
|
+
| StatusCode::NO_CONTENT
|
81
|
+
| StatusCode::PARTIAL_CONTENT => {}
|
82
|
+
_ => {
|
83
|
+
debug!(target: "middleware::etag", "Skipping ETag middleware for ineligible response");
|
84
|
+
return resp;
|
85
|
+
}
|
86
|
+
}
|
87
|
+
|
88
|
+
if resp.headers().contains_key(header::ETAG) {
|
89
|
+
debug!(target: "middleware::etag", "Forwarding response with existing ETag");
|
90
|
+
return resp;
|
91
|
+
}
|
92
|
+
|
93
|
+
if let Some(cache_control) = resp.headers().get(header::CACHE_CONTROL) {
|
94
|
+
if let Ok(cache_control_str) = cache_control.to_str() {
|
95
|
+
if cache_control_str.contains("no-store") {
|
96
|
+
debug!(target: "middleware::etag", "Skipping ETag for no-store response");
|
97
|
+
return resp;
|
98
|
+
}
|
99
|
+
}
|
100
|
+
}
|
101
|
+
|
102
|
+
let body_size = resp.size_hint().exact();
|
103
|
+
|
104
|
+
if body_size.is_none() {
|
105
|
+
debug!(target: "middleware::etag", "Skipping ETag for streaming response");
|
106
|
+
return resp;
|
107
|
+
}
|
108
|
+
|
109
|
+
if body_size.unwrap_or(0) < self.min_body_size as u64 {
|
110
|
+
debug!(target: "middleware::etag", "Skipping ETag for small response");
|
111
|
+
return resp;
|
112
|
+
}
|
113
|
+
|
114
|
+
let (mut parts, mut body) = resp.into_parts();
|
115
|
+
let etag_value = if let Some(existing_etag) = parts.headers.get(header::ETAG) {
|
116
|
+
existing_etag.to_str().unwrap_or("").to_string()
|
117
|
+
} else {
|
118
|
+
// Get the full bytes from the body
|
119
|
+
let full_bytes: Bytes = match body
|
120
|
+
.into_data_stream()
|
121
|
+
.try_fold(BytesMut::new(), |mut acc, chunk| async move {
|
122
|
+
acc.extend_from_slice(&chunk);
|
123
|
+
Ok(acc)
|
124
|
+
})
|
125
|
+
.await
|
126
|
+
{
|
127
|
+
Ok(bytes_mut) => bytes_mut.freeze(),
|
128
|
+
Err(_) => return Response::from_parts(parts, BoxBody::new(Empty::new())),
|
129
|
+
};
|
130
|
+
|
131
|
+
let computed_etag = match self.algorithm {
|
132
|
+
HashAlgorithm::Sha256 => {
|
133
|
+
let mut hasher = Sha256::new();
|
134
|
+
hasher.update(&full_bytes);
|
135
|
+
let result = hasher.finalize();
|
136
|
+
general_purpose::STANDARD.encode(result)
|
137
|
+
}
|
138
|
+
HashAlgorithm::Md5 => {
|
139
|
+
let digest = md5::compute(&full_bytes);
|
140
|
+
format!("{:x}", digest)
|
141
|
+
}
|
142
|
+
};
|
143
|
+
|
144
|
+
let formatted_etag = match self.r#type {
|
145
|
+
ETagType::Strong => format!("\"{}\"", computed_etag),
|
146
|
+
ETagType::Weak => format!("W/\"{}\"", computed_etag),
|
147
|
+
};
|
148
|
+
|
149
|
+
debug!(target: "middleware::etag", "Computed ETag for response {}", formatted_etag);
|
150
|
+
if let Ok(value) = HeaderValue::from_str(&formatted_etag) {
|
151
|
+
parts.headers.insert(header::ETAG, value);
|
152
|
+
}
|
153
|
+
|
154
|
+
body = Full::new(full_bytes).boxed();
|
155
|
+
formatted_etag
|
156
|
+
};
|
157
|
+
|
158
|
+
if self.handle_if_none_match {
|
159
|
+
if let Some(if_none_match) = context.get_if_none_match() {
|
160
|
+
if if_none_match == etag_value || if_none_match == "*" {
|
161
|
+
// Return 304 Not Modified without the body
|
162
|
+
let mut not_modified = Response::new(BoxBody::new(Empty::new()));
|
163
|
+
*not_modified.status_mut() = StatusCode::NOT_MODIFIED;
|
164
|
+
// Copy headers we want to preserve
|
165
|
+
for (name, value) in parts.headers.iter() {
|
166
|
+
if matches!(
|
167
|
+
name,
|
168
|
+
&header::CACHE_CONTROL
|
169
|
+
| &header::CONTENT_LOCATION
|
170
|
+
| &header::DATE
|
171
|
+
| &header::ETAG
|
172
|
+
| &header::EXPIRES
|
173
|
+
| &header::VARY
|
174
|
+
) {
|
175
|
+
not_modified.headers_mut().insert(name, value.clone());
|
176
|
+
}
|
177
|
+
}
|
178
|
+
return not_modified;
|
179
|
+
}
|
180
|
+
}
|
181
|
+
}
|
182
|
+
|
183
|
+
Response::from_parts(parts, body)
|
184
|
+
}
|
185
|
+
}
|
186
|
+
|
187
|
+
impl Default for ETag {
|
188
|
+
fn default() -> Self {
|
189
|
+
Self {
|
190
|
+
r#type: ETagType::Strong,
|
191
|
+
algorithm: HashAlgorithm::Sha256,
|
192
|
+
min_body_size: 0,
|
193
|
+
handle_if_none_match: true,
|
194
|
+
}
|
195
|
+
}
|
196
|
+
}
|
197
|
+
|
198
|
+
impl FromValue for ETag {}
|
@@ -0,0 +1,82 @@
|
|
1
|
+
use http::{header::GetAll, HeaderValue};
|
2
|
+
|
3
|
+
/// Given a list of header values (which may be comma-separated and may have quality parameters)
|
4
|
+
/// and a list of supported items (each supported item is a full value or a prefix ending with '*'),
|
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>(header_values: &[HeaderValue], supported: I) -> Option<&'a str>
|
7
|
+
where
|
8
|
+
I: IntoIterator<Item = &'a str> + Clone,
|
9
|
+
{
|
10
|
+
// best candidate: (quality, supported_index, candidate)
|
11
|
+
let mut best: Option<(f32, usize, &'a str)> = None;
|
12
|
+
|
13
|
+
for value in header_values.iter() {
|
14
|
+
if let Ok(s) = value.to_str() {
|
15
|
+
for token in s.split(',') {
|
16
|
+
let token = token.trim();
|
17
|
+
if token.is_empty() {
|
18
|
+
continue;
|
19
|
+
}
|
20
|
+
let mut parts = token.split(';');
|
21
|
+
let enc = parts.next()?.trim();
|
22
|
+
if enc.is_empty() {
|
23
|
+
continue;
|
24
|
+
}
|
25
|
+
let quality = parts
|
26
|
+
.find_map(|p| {
|
27
|
+
let p = p.trim();
|
28
|
+
if let Some(q_str) = p.strip_prefix("q=") {
|
29
|
+
q_str.parse::<f32>().ok()
|
30
|
+
} else {
|
31
|
+
None
|
32
|
+
}
|
33
|
+
})
|
34
|
+
.unwrap_or(1.0);
|
35
|
+
|
36
|
+
// For each supported encoding, iterate over a clone of the iterable.
|
37
|
+
for (i, supp) in supported.clone().into_iter().enumerate() {
|
38
|
+
let is_match = if supp == "*" {
|
39
|
+
true
|
40
|
+
} else if let Some(prefix) = supp.strip_suffix('*') {
|
41
|
+
enc.starts_with(prefix)
|
42
|
+
} else {
|
43
|
+
enc.eq_ignore_ascii_case(supp)
|
44
|
+
};
|
45
|
+
|
46
|
+
if is_match {
|
47
|
+
best = match best {
|
48
|
+
Some((best_q, best_idx, _))
|
49
|
+
if quality > best_q || (quality == best_q && i < best_idx) =>
|
50
|
+
{
|
51
|
+
Some((quality, i, supp))
|
52
|
+
}
|
53
|
+
None => Some((quality, i, supp)),
|
54
|
+
_ => best,
|
55
|
+
};
|
56
|
+
}
|
57
|
+
}
|
58
|
+
}
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
best.map(|(_, _, candidate)| candidate)
|
63
|
+
}
|
64
|
+
|
65
|
+
pub fn header_contains(header_values: &GetAll<HeaderValue>, needle: &str) -> bool {
|
66
|
+
if needle == "*" {
|
67
|
+
return true;
|
68
|
+
}
|
69
|
+
let mut headers = header_values
|
70
|
+
.iter()
|
71
|
+
.flat_map(|value| value.to_str().unwrap_or("").split(','))
|
72
|
+
.map(|s| s.trim().split(';').next().unwrap_or(""))
|
73
|
+
.filter(|s| !s.is_empty());
|
74
|
+
|
75
|
+
let needle_lower = needle;
|
76
|
+
if needle.ends_with('*') {
|
77
|
+
let prefix = &needle_lower[..needle_lower.len() - 1];
|
78
|
+
headers.any(|h| h.starts_with(prefix))
|
79
|
+
} else {
|
80
|
+
headers.any(|h| h == needle_lower)
|
81
|
+
}
|
82
|
+
}
|