itsi-scheduler 0.2.22-aarch64-linux

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 (149) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +8 -0
  3. data/Cargo.lock +997 -0
  4. data/Cargo.toml +7 -0
  5. data/Rakefile +39 -0
  6. data/ext/itsi_acme/Cargo.toml +86 -0
  7. data/ext/itsi_acme/examples/high_level.rs +63 -0
  8. data/ext/itsi_acme/examples/high_level_warp.rs +52 -0
  9. data/ext/itsi_acme/examples/low_level.rs +87 -0
  10. data/ext/itsi_acme/examples/low_level_axum.rs +66 -0
  11. data/ext/itsi_acme/src/acceptor.rs +81 -0
  12. data/ext/itsi_acme/src/acme.rs +354 -0
  13. data/ext/itsi_acme/src/axum.rs +86 -0
  14. data/ext/itsi_acme/src/cache.rs +39 -0
  15. data/ext/itsi_acme/src/caches/boxed.rs +80 -0
  16. data/ext/itsi_acme/src/caches/composite.rs +69 -0
  17. data/ext/itsi_acme/src/caches/dir.rs +106 -0
  18. data/ext/itsi_acme/src/caches/mod.rs +11 -0
  19. data/ext/itsi_acme/src/caches/no.rs +78 -0
  20. data/ext/itsi_acme/src/caches/test.rs +136 -0
  21. data/ext/itsi_acme/src/config.rs +172 -0
  22. data/ext/itsi_acme/src/https_helper.rs +69 -0
  23. data/ext/itsi_acme/src/incoming.rs +142 -0
  24. data/ext/itsi_acme/src/jose.rs +161 -0
  25. data/ext/itsi_acme/src/lib.rs +142 -0
  26. data/ext/itsi_acme/src/resolver.rs +59 -0
  27. data/ext/itsi_acme/src/state.rs +424 -0
  28. data/ext/itsi_error/Cargo.lock +368 -0
  29. data/ext/itsi_error/Cargo.toml +12 -0
  30. data/ext/itsi_error/src/lib.rs +140 -0
  31. data/ext/itsi_instrument_entry/Cargo.toml +15 -0
  32. data/ext/itsi_instrument_entry/src/lib.rs +31 -0
  33. data/ext/itsi_rb_helpers/Cargo.lock +355 -0
  34. data/ext/itsi_rb_helpers/Cargo.toml +11 -0
  35. data/ext/itsi_rb_helpers/src/heap_value.rs +139 -0
  36. data/ext/itsi_rb_helpers/src/lib.rs +232 -0
  37. data/ext/itsi_scheduler/Cargo.toml +24 -0
  38. data/ext/itsi_scheduler/extconf.rb +11 -0
  39. data/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
  40. data/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
  41. data/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
  42. data/ext/itsi_scheduler/src/itsi_scheduler.rs +320 -0
  43. data/ext/itsi_scheduler/src/lib.rs +39 -0
  44. data/ext/itsi_server/Cargo.lock +2956 -0
  45. data/ext/itsi_server/Cargo.toml +94 -0
  46. data/ext/itsi_server/src/default_responses/mod.rs +14 -0
  47. data/ext/itsi_server/src/env.rs +43 -0
  48. data/ext/itsi_server/src/lib.rs +154 -0
  49. data/ext/itsi_server/src/prelude.rs +2 -0
  50. data/ext/itsi_server/src/ruby_types/itsi_body_proxy/big_bytes.rs +116 -0
  51. data/ext/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +149 -0
  52. data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +346 -0
  53. data/ext/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +265 -0
  54. data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +399 -0
  55. data/ext/itsi_server/src/ruby_types/itsi_http_response.rs +447 -0
  56. data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +545 -0
  57. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +650 -0
  58. data/ext/itsi_server/src/ruby_types/itsi_server.rs +102 -0
  59. data/ext/itsi_server/src/ruby_types/mod.rs +48 -0
  60. data/ext/itsi_server/src/server/binds/bind.rs +204 -0
  61. data/ext/itsi_server/src/server/binds/bind_protocol.rs +37 -0
  62. data/ext/itsi_server/src/server/binds/listener.rs +485 -0
  63. data/ext/itsi_server/src/server/binds/mod.rs +4 -0
  64. data/ext/itsi_server/src/server/binds/tls/locked_dir_cache.rs +132 -0
  65. data/ext/itsi_server/src/server/binds/tls.rs +278 -0
  66. data/ext/itsi_server/src/server/byte_frame.rs +32 -0
  67. data/ext/itsi_server/src/server/frame_stream.rs +143 -0
  68. data/ext/itsi_server/src/server/http_message_types.rs +230 -0
  69. data/ext/itsi_server/src/server/io_stream.rs +128 -0
  70. data/ext/itsi_server/src/server/lifecycle_event.rs +12 -0
  71. data/ext/itsi_server/src/server/middleware_stack/middleware.rs +170 -0
  72. data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +63 -0
  73. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +94 -0
  74. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +93 -0
  75. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +343 -0
  76. data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +151 -0
  77. data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +329 -0
  78. data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +300 -0
  79. data/ext/itsi_server/src/server/middleware_stack/middlewares/csp.rs +193 -0
  80. data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +64 -0
  81. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +188 -0
  82. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +168 -0
  83. data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +183 -0
  84. data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +82 -0
  85. data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +209 -0
  86. data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +133 -0
  87. data/ext/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
  88. data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +122 -0
  89. data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +407 -0
  90. data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +155 -0
  91. data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +54 -0
  92. data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +54 -0
  93. data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +51 -0
  94. data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +138 -0
  95. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +269 -0
  96. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +62 -0
  97. data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +218 -0
  98. data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +31 -0
  99. data/ext/itsi_server/src/server/middleware_stack/mod.rs +381 -0
  100. data/ext/itsi_server/src/server/mod.rs +14 -0
  101. data/ext/itsi_server/src/server/process_worker.rs +247 -0
  102. data/ext/itsi_server/src/server/redirect_type.rs +26 -0
  103. data/ext/itsi_server/src/server/request_job.rs +11 -0
  104. data/ext/itsi_server/src/server/serve_strategy/acceptor.rs +100 -0
  105. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +411 -0
  106. data/ext/itsi_server/src/server/serve_strategy/mod.rs +31 -0
  107. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +449 -0
  108. data/ext/itsi_server/src/server/signal.rs +129 -0
  109. data/ext/itsi_server/src/server/size_limited_incoming.rs +107 -0
  110. data/ext/itsi_server/src/server/thread_worker.rs +504 -0
  111. data/ext/itsi_server/src/services/cache_store.rs +74 -0
  112. data/ext/itsi_server/src/services/itsi_http_service.rs +270 -0
  113. data/ext/itsi_server/src/services/mime_types.rs +2896 -0
  114. data/ext/itsi_server/src/services/mod.rs +6 -0
  115. data/ext/itsi_server/src/services/password_hasher.rs +89 -0
  116. data/ext/itsi_server/src/services/rate_limiter.rs +609 -0
  117. data/ext/itsi_server/src/services/static_file_server.rs +1400 -0
  118. data/ext/itsi_tracing/Cargo.lock +274 -0
  119. data/ext/itsi_tracing/Cargo.toml +17 -0
  120. data/ext/itsi_tracing/src/lib.rs +370 -0
  121. data/itsi-scheduler-100.png +0 -0
  122. data/lib/itsi/schedule_refinement.rb +96 -0
  123. data/lib/itsi/scheduler/3.1/itsi_scheduler.so +0 -0
  124. data/lib/itsi/scheduler/3.2/itsi_scheduler.so +0 -0
  125. data/lib/itsi/scheduler/3.3/itsi_scheduler.so +0 -0
  126. data/lib/itsi/scheduler/3.4/itsi_scheduler.so +0 -0
  127. data/lib/itsi/scheduler/4.0/itsi_scheduler.so +0 -0
  128. data/lib/itsi/scheduler/native_extension.rb +34 -0
  129. data/lib/itsi/scheduler/version.rb +7 -0
  130. data/lib/itsi/scheduler.rb +153 -0
  131. data/vendor/rb-sys-build/.cargo-ok +1 -0
  132. data/vendor/rb-sys-build/.cargo_vcs_info.json +6 -0
  133. data/vendor/rb-sys-build/Cargo.lock +294 -0
  134. data/vendor/rb-sys-build/Cargo.toml +71 -0
  135. data/vendor/rb-sys-build/Cargo.toml.orig +32 -0
  136. data/vendor/rb-sys-build/LICENSE-APACHE +190 -0
  137. data/vendor/rb-sys-build/LICENSE-MIT +21 -0
  138. data/vendor/rb-sys-build/src/bindings/sanitizer.rs +185 -0
  139. data/vendor/rb-sys-build/src/bindings/stable_api.rs +247 -0
  140. data/vendor/rb-sys-build/src/bindings/wrapper.h +71 -0
  141. data/vendor/rb-sys-build/src/bindings.rs +280 -0
  142. data/vendor/rb-sys-build/src/cc.rs +421 -0
  143. data/vendor/rb-sys-build/src/lib.rs +12 -0
  144. data/vendor/rb-sys-build/src/rb_config/flags.rs +101 -0
  145. data/vendor/rb-sys-build/src/rb_config/library.rs +132 -0
  146. data/vendor/rb-sys-build/src/rb_config/search_path.rs +57 -0
  147. data/vendor/rb-sys-build/src/rb_config.rs +906 -0
  148. data/vendor/rb-sys-build/src/utils.rs +53 -0
  149. metadata +210 -0
@@ -0,0 +1,193 @@
1
+ use super::FromValue;
2
+ use crate::{
3
+ server::http_message_types::{HttpBody, HttpRequest, HttpResponse},
4
+ services::itsi_http_service::HttpRequestContext,
5
+ };
6
+ use async_trait::async_trait;
7
+ use bytes::{Bytes, BytesMut};
8
+ use either::Either;
9
+ use futures::TryStreamExt;
10
+ use http::{HeaderValue, StatusCode};
11
+ use http_body_util::BodyExt;
12
+ use itsi_error::ItsiError;
13
+ use serde::{Deserialize, Serialize};
14
+ use std::sync::Arc;
15
+ use std::{path::PathBuf, sync::OnceLock};
16
+ use tokio::sync::Mutex;
17
+ use tokio::time::{self, Duration};
18
+ use tracing::debug;
19
+
20
+ #[derive(Debug, Serialize, Deserialize)]
21
+ pub struct CspReport {
22
+ #[serde(rename = "csp-report")]
23
+ pub report: ReportDetails,
24
+ }
25
+
26
+ #[derive(Debug, Serialize, Deserialize)]
27
+ pub struct ReportDetails {
28
+ #[serde(rename = "document-uri")]
29
+ pub document_uri: String,
30
+ #[serde(rename = "referrer")]
31
+ pub referrer: Option<String>,
32
+ #[serde(rename = "violated-directive")]
33
+ pub violated_directive: String,
34
+ #[serde(rename = "original-policy")]
35
+ pub original_policy: String,
36
+ #[serde(rename = "blocked-uri")]
37
+ pub blocked_uri: String,
38
+ }
39
+
40
+ #[derive(Debug, Deserialize)]
41
+ pub struct CspConfig {
42
+ pub default_src: Vec<String>,
43
+ pub script_src: Vec<String>,
44
+ pub style_src: Vec<String>,
45
+ pub report_uri: Vec<String>,
46
+ }
47
+
48
+ #[derive(Debug, Deserialize)]
49
+ pub struct Csp {
50
+ pub policy: Option<CspConfig>,
51
+ pub reporting_enabled: bool,
52
+ pub report_file: Option<PathBuf>,
53
+ pub report_endpoint: String,
54
+ pub flush_interval: f64,
55
+
56
+ #[serde(skip)]
57
+ pub computed_policy: OnceLock<String>,
58
+ #[serde(skip)]
59
+ pub pending_reports: Arc<Mutex<Vec<CspReport>>>,
60
+ #[serde(skip)]
61
+ pub flush_task: OnceLock<tokio::task::JoinHandle<()>>,
62
+ }
63
+
64
+ #[async_trait]
65
+ impl super::MiddlewareLayer for Csp {
66
+ async fn initialize(&self) -> Result<(), magnus::error::Error> {
67
+ if let Some(policy_config) = &self.policy {
68
+ let mut parts = Vec::new();
69
+ if !policy_config.default_src.is_empty() {
70
+ parts.push(format!(
71
+ "default-src {}",
72
+ policy_config.default_src.join(" ")
73
+ ));
74
+ }
75
+ if !policy_config.script_src.is_empty() {
76
+ parts.push(format!("script-src {}", policy_config.script_src.join(" ")));
77
+ }
78
+ if !policy_config.style_src.is_empty() {
79
+ parts.push(format!("style-src {}", policy_config.style_src.join(" ")));
80
+ }
81
+ if !policy_config.report_uri.is_empty() {
82
+ parts.push(format!("report-uri {}", policy_config.report_uri.join(" ")));
83
+ }
84
+ let policy = parts.join("; ");
85
+ debug!(target: "middleware::csp", "Computed CSP policy: {}", policy);
86
+ self.computed_policy
87
+ .set(policy)
88
+ .map_err(|_| ItsiError::new("Failed to set computed CSP policy"))?;
89
+ }
90
+
91
+ if self.reporting_enabled {
92
+ if let Some(ref report_file) = self.report_file {
93
+ let flush_interval = self.flush_interval;
94
+ let report_path = report_file.clone();
95
+ let pending_reports = Arc::clone(&self.pending_reports);
96
+ let handle = tokio::spawn(async move {
97
+ let mut interval = time::interval(Duration::from_secs_f64(flush_interval));
98
+ loop {
99
+ interval.tick().await;
100
+
101
+ let mut reports = pending_reports.lock().await;
102
+ if !reports.is_empty() {
103
+ let mut lines = String::new();
104
+ for report in reports.iter() {
105
+ if let Ok(line) = serde_json::to_string(report) {
106
+ lines.push_str(&line);
107
+ lines.push('\n');
108
+ }
109
+ }
110
+ reports.clear();
111
+
112
+ debug!("Flushing CSP report to file {:?}", &report_path.display());
113
+
114
+ use tokio::io::AsyncWriteExt;
115
+
116
+ match tokio::fs::OpenOptions::new()
117
+ .append(true)
118
+ .create(true)
119
+ .open(&report_path)
120
+ .await
121
+ {
122
+ Ok(mut file) => {
123
+ if let Err(e) = file.write_all(lines.as_bytes()).await {
124
+ eprintln!("Error writing CSP reports: {:?}", e);
125
+ }
126
+ }
127
+ Err(e) => {
128
+ eprintln!("Error opening CSP report file: {:?}", e);
129
+ }
130
+ }
131
+ }
132
+ }
133
+ });
134
+ self.flush_task
135
+ .set(handle)
136
+ .map_err(|_| ItsiError::new("Failed to set flush task handle"))?;
137
+ }
138
+ }
139
+ Ok(())
140
+ }
141
+
142
+ async fn before(
143
+ &self,
144
+ req: HttpRequest,
145
+ _context: &mut HttpRequestContext,
146
+ ) -> Result<Either<HttpRequest, HttpResponse>, magnus::error::Error> {
147
+ if self.reporting_enabled && req.uri().path() == self.report_endpoint {
148
+ debug!(target: "middleware::csp", "Received CSP report");
149
+ let full_bytes: Result<Bytes, _> = req
150
+ .into_body()
151
+ .into_data_stream()
152
+ .try_fold(BytesMut::new(), |mut acc, chunk| async move {
153
+ acc.extend_from_slice(&chunk);
154
+ Ok(acc)
155
+ })
156
+ .await
157
+ .map(|b| b.freeze());
158
+
159
+ if let Ok(body_bytes) = full_bytes {
160
+ if let Ok(report) = serde_json::from_slice::<CspReport>(&body_bytes) {
161
+ debug!(target: "middleware::csp", "Report: {:?}", report);
162
+ let mut pending = self.pending_reports.lock().await;
163
+ pending.push(report);
164
+ }
165
+ }
166
+
167
+ let mut resp = HttpResponse::new(HttpBody::empty());
168
+ *resp.status_mut() = StatusCode::NO_CONTENT;
169
+ return Ok(Either::Right(resp));
170
+ }
171
+ Ok(Either::Left(req))
172
+ }
173
+
174
+ async fn after(&self, resp: HttpResponse, _context: &mut HttpRequestContext) -> HttpResponse {
175
+ if let Some(policy) = self.computed_policy.get() {
176
+ if !resp.headers().contains_key("Content-Security-Policy") {
177
+ debug!(target: "middleware::csp", "Adding CSP header");
178
+ let (mut parts, body) = resp.into_parts();
179
+ if let Ok(header_value) = HeaderValue::from_str(policy) {
180
+ parts
181
+ .headers
182
+ .insert("Content-Security-Policy", header_value);
183
+ }
184
+ return HttpResponse::from_parts(parts, body);
185
+ } else {
186
+ debug!(target: "middleware::csp", "CSP header already present");
187
+ }
188
+ }
189
+ resp
190
+ }
191
+ }
192
+
193
+ impl FromValue for Csp {}
@@ -0,0 +1,64 @@
1
+ use crate::{
2
+ server::http_message_types::{HttpRequest, HttpResponse, RequestExt},
3
+ services::itsi_http_service::HttpRequestContext,
4
+ };
5
+
6
+ use super::{token_source::TokenSource, ErrorResponse, FromValue, MiddlewareLayer};
7
+ use async_trait::async_trait;
8
+ use either::Either;
9
+ use itsi_error::ItsiError;
10
+ use magnus::error::Result;
11
+ use regex::RegexSet;
12
+ use serde::Deserialize;
13
+ use std::{collections::HashMap, sync::OnceLock};
14
+ use tracing::debug;
15
+
16
+ #[derive(Debug, Clone, Deserialize)]
17
+ pub struct DenyList {
18
+ #[serde(skip_deserializing)]
19
+ pub denied_ips: OnceLock<RegexSet>,
20
+ pub denied_patterns: Vec<String>,
21
+ pub trusted_proxies: HashMap<String, TokenSource>,
22
+ #[serde(default = "forbidden_error_response")]
23
+ pub error_response: ErrorResponse,
24
+ }
25
+
26
+ fn forbidden_error_response() -> ErrorResponse {
27
+ ErrorResponse::forbidden()
28
+ }
29
+
30
+ #[async_trait]
31
+ impl MiddlewareLayer for DenyList {
32
+ async fn initialize(&self) -> Result<()> {
33
+ let denied_ips = RegexSet::new(&self.denied_patterns).map_err(ItsiError::new)?;
34
+ self.denied_ips
35
+ .set(denied_ips)
36
+ .map_err(|e| ItsiError::new(format!("Failed to set allowed IPs: {:?}", e)))?;
37
+ Ok(())
38
+ }
39
+
40
+ async fn before(
41
+ &self,
42
+ req: HttpRequest,
43
+ context: &mut HttpRequestContext,
44
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
45
+ let addr = if self.trusted_proxies.contains_key(&context.addr) {
46
+ let source = self.trusted_proxies.get(&context.addr).unwrap();
47
+ source.extract_token(&req).unwrap_or(&context.addr)
48
+ } else {
49
+ &context.addr
50
+ };
51
+ if let Some(denied_ips) = self.denied_ips.get() {
52
+ if denied_ips.is_match(addr) {
53
+ debug!(target: "middleware::deny_list", "IP address {} is not allowed", addr);
54
+ return Ok(Either::Right(
55
+ self.error_response
56
+ .to_http_response(req.accept().into())
57
+ .await,
58
+ ));
59
+ }
60
+ }
61
+ Ok(Either::Left(req))
62
+ }
63
+ }
64
+ impl FromValue for DenyList {}
@@ -0,0 +1,188 @@
1
+ use super::{ContentSource, DefaultFormat, ErrorResponse};
2
+ use crate::server::http_message_types::{HttpBody, ResponseFormat};
3
+ use bytes::Bytes;
4
+
5
+ impl DefaultFormat {
6
+ pub fn response_for_code(&self, code: u16) -> ContentSource {
7
+ match self {
8
+ DefaultFormat::Plaintext => match code {
9
+ 500 => ContentSource::Static("500 Internal Error".into()),
10
+ 404 => ContentSource::Static("404 Not Found".into()),
11
+ 401 => ContentSource::Static("401 Unauthorized".into()),
12
+ 403 => ContentSource::Static("403 Forbidden".into()),
13
+ 413 => ContentSource::Static("413 Payload Too Large".into()),
14
+ 429 => ContentSource::Static("429 Too Many Requests".into()),
15
+ 502 => ContentSource::Static("502 Bad Gateway".into()),
16
+ 503 => ContentSource::Static("503 Service Unavailable".into()),
17
+ 504 => ContentSource::Static("504 Gateway Timeout".into()),
18
+ _ => ContentSource::Static("Unexpected Error".into()),
19
+ },
20
+ DefaultFormat::Html => match code {
21
+ 500 => ContentSource::Static(
22
+ include_str!("../../../../default_responses/html/500.html").into(),
23
+ ),
24
+ 404 => ContentSource::Static(
25
+ include_str!("../../../../default_responses/html/404.html").into(),
26
+ ),
27
+ 401 => ContentSource::Static(
28
+ include_str!("../../../../default_responses/html/401.html").into(),
29
+ ),
30
+ 403 => ContentSource::Static(
31
+ include_str!("../../../../default_responses/html/403.html").into(),
32
+ ),
33
+ 413 => ContentSource::Static(
34
+ include_str!("../../../../default_responses/html/413.html").into(),
35
+ ),
36
+ 429 => ContentSource::Static(
37
+ include_str!("../../../../default_responses/html/429.html").into(),
38
+ ),
39
+ 502 => ContentSource::Static(
40
+ include_str!("../../../../default_responses/html/502.html").into(),
41
+ ),
42
+ 503 => ContentSource::Static(
43
+ include_str!("../../../../default_responses/html/503.html").into(),
44
+ ),
45
+ 504 => ContentSource::Static(
46
+ include_str!("../../../../default_responses/html/504.html").into(),
47
+ ),
48
+ _ => ContentSource::Static(
49
+ include_str!("../../../../default_responses/html/500.html").into(),
50
+ ),
51
+ },
52
+ DefaultFormat::Json => match code {
53
+ 500 => ContentSource::Static(
54
+ include_str!("../../../../default_responses/json/500.json").into(),
55
+ ),
56
+ 404 => ContentSource::Static(
57
+ include_str!("../../../../default_responses/json/404.json").into(),
58
+ ),
59
+ 401 => ContentSource::Static(
60
+ include_str!("../../../../default_responses/json/401.json").into(),
61
+ ),
62
+ 403 => ContentSource::Static(
63
+ include_str!("../../../../default_responses/json/403.json").into(),
64
+ ),
65
+ 413 => ContentSource::Static(
66
+ include_str!("../../../../default_responses/json/413.json").into(),
67
+ ),
68
+ 429 => ContentSource::Static(
69
+ include_str!("../../../../default_responses/json/429.json").into(),
70
+ ),
71
+ 502 => ContentSource::Static(
72
+ include_str!("../../../../default_responses/json/502.json").into(),
73
+ ),
74
+ 503 => ContentSource::Static(
75
+ include_str!("../../../../default_responses/json/503.json").into(),
76
+ ),
77
+ 504 => ContentSource::Static(
78
+ include_str!("../../../../default_responses/json/504.json").into(),
79
+ ),
80
+ _ => ContentSource::Static("Unexpected Error".into()),
81
+ },
82
+ }
83
+ }
84
+ }
85
+ impl ErrorResponse {
86
+ pub fn fallback_body_for(code: u16, accept: ResponseFormat) -> HttpBody {
87
+ let source = match accept {
88
+ ResponseFormat::TEXT => DefaultFormat::Plaintext.response_for_code(code),
89
+ ResponseFormat::HTML => DefaultFormat::Html.response_for_code(code),
90
+ ResponseFormat::JSON => DefaultFormat::Json.response_for_code(code),
91
+ ResponseFormat::UNKNOWN => ContentSource::Inline("Unexpected Error".to_owned()),
92
+ };
93
+ match source {
94
+ ContentSource::Inline(bytes) => HttpBody::full(Bytes::from(bytes)),
95
+ ContentSource::Static(bytes) => HttpBody::full(bytes),
96
+ ContentSource::File(_) => HttpBody::full(Bytes::from("Unexpected error")),
97
+ }
98
+ }
99
+ pub fn internal_server_error() -> Self {
100
+ ErrorResponse {
101
+ code: 500,
102
+ plaintext: Some(DefaultFormat::Plaintext.response_for_code(500)),
103
+ html: Some(DefaultFormat::Html.response_for_code(500)),
104
+ json: Some(DefaultFormat::Json.response_for_code(500)),
105
+ default: DefaultFormat::Html,
106
+ }
107
+ }
108
+
109
+ pub fn not_found() -> Self {
110
+ ErrorResponse {
111
+ code: 404,
112
+ plaintext: Some(DefaultFormat::Plaintext.response_for_code(404)),
113
+ html: Some(DefaultFormat::Html.response_for_code(404)),
114
+ json: Some(DefaultFormat::Json.response_for_code(404)),
115
+ default: DefaultFormat::Html,
116
+ }
117
+ }
118
+
119
+ pub fn unauthorized() -> Self {
120
+ ErrorResponse {
121
+ code: 401,
122
+ plaintext: Some(DefaultFormat::Plaintext.response_for_code(401)),
123
+ html: Some(DefaultFormat::Html.response_for_code(401)),
124
+ json: Some(DefaultFormat::Json.response_for_code(401)),
125
+ default: DefaultFormat::Html,
126
+ }
127
+ }
128
+
129
+ pub fn forbidden() -> Self {
130
+ ErrorResponse {
131
+ code: 403,
132
+ plaintext: Some(DefaultFormat::Plaintext.response_for_code(403)),
133
+ html: Some(DefaultFormat::Html.response_for_code(403)),
134
+ json: Some(DefaultFormat::Json.response_for_code(403)),
135
+ default: DefaultFormat::Html,
136
+ }
137
+ }
138
+
139
+ pub fn payload_too_large() -> Self {
140
+ ErrorResponse {
141
+ code: 413,
142
+ plaintext: Some(DefaultFormat::Plaintext.response_for_code(413)),
143
+ html: Some(DefaultFormat::Html.response_for_code(413)),
144
+ json: Some(DefaultFormat::Json.response_for_code(413)),
145
+ default: DefaultFormat::Html,
146
+ }
147
+ }
148
+
149
+ pub fn too_many_requests() -> Self {
150
+ ErrorResponse {
151
+ code: 429,
152
+ plaintext: Some(DefaultFormat::Plaintext.response_for_code(429)),
153
+ html: Some(DefaultFormat::Html.response_for_code(429)),
154
+ json: Some(DefaultFormat::Json.response_for_code(429)),
155
+ default: DefaultFormat::Html,
156
+ }
157
+ }
158
+
159
+ pub fn bad_gateway() -> Self {
160
+ ErrorResponse {
161
+ code: 502,
162
+ plaintext: Some(DefaultFormat::Plaintext.response_for_code(502)),
163
+ html: Some(DefaultFormat::Html.response_for_code(502)),
164
+ json: Some(DefaultFormat::Json.response_for_code(502)),
165
+ default: DefaultFormat::Html,
166
+ }
167
+ }
168
+
169
+ pub fn service_unavailable() -> Self {
170
+ ErrorResponse {
171
+ code: 503,
172
+ plaintext: Some(DefaultFormat::Plaintext.response_for_code(503)),
173
+ html: Some(DefaultFormat::Html.response_for_code(503)),
174
+ json: Some(DefaultFormat::Json.response_for_code(503)),
175
+ default: DefaultFormat::Html,
176
+ }
177
+ }
178
+
179
+ pub fn gateway_timeout() -> Self {
180
+ ErrorResponse {
181
+ code: 504,
182
+ plaintext: Some(DefaultFormat::Plaintext.response_for_code(504)),
183
+ html: Some(DefaultFormat::Html.response_for_code(504)),
184
+ json: Some(DefaultFormat::Json.response_for_code(504)),
185
+ default: DefaultFormat::Html,
186
+ }
187
+ }
188
+ }
@@ -0,0 +1,168 @@
1
+ use bytes::Bytes;
2
+ use http::header::CONTENT_TYPE;
3
+ use http::Response;
4
+ use serde::{Deserialize, Deserializer};
5
+ use std::path::PathBuf;
6
+ use tracing::warn;
7
+
8
+ use crate::server::http_message_types::{HttpBody, HttpResponse, ResponseFormat};
9
+ use crate::services::static_file_server::ROOT_STATIC_FILE_SERVER;
10
+ mod default_responses;
11
+
12
+ #[derive(Debug, Clone, Deserialize)]
13
+ pub enum ContentSource {
14
+ #[serde(rename(deserialize = "inline"))]
15
+ Inline(String),
16
+ #[serde(rename(deserialize = "file"))]
17
+ File(PathBuf),
18
+ #[serde(rename(deserialize = "static"))]
19
+ #[serde(skip_deserializing)]
20
+ Static(Bytes),
21
+ }
22
+
23
+ #[derive(Debug, Clone, Deserialize, Default)]
24
+ pub enum DefaultFormat {
25
+ #[serde(rename(deserialize = "plaintext"))]
26
+ Plaintext,
27
+ #[default]
28
+ #[serde(rename(deserialize = "html"))]
29
+ Html,
30
+ #[serde(rename(deserialize = "json"))]
31
+ Json,
32
+ }
33
+
34
+ #[derive(Debug, Clone)]
35
+ pub struct ErrorResponse {
36
+ pub code: u16,
37
+ pub plaintext: Option<ContentSource>,
38
+ pub html: Option<ContentSource>,
39
+ pub json: Option<ContentSource>,
40
+ pub default: DefaultFormat, // must match one of the provided fields
41
+ }
42
+
43
+ impl<'de> Deserialize<'de> for ErrorResponse {
44
+ fn deserialize<D>(deserializer: D) -> Result<ErrorResponse, D::Error>
45
+ where
46
+ D: Deserializer<'de>,
47
+ {
48
+ let def = ErrorResponseDef::deserialize(deserializer)?;
49
+ Ok(def.into())
50
+ }
51
+ }
52
+
53
+ /// An untagged enum to support two input formats:
54
+ /// - A detailed struct with all fields.
55
+ /// - A string with the name of a default error response.
56
+ #[derive(Debug, Clone, Deserialize)]
57
+ #[serde(untagged)]
58
+ pub enum ErrorResponseDef {
59
+ Detailed {
60
+ code: u16,
61
+ plaintext: Option<ContentSource>,
62
+ html: Option<ContentSource>,
63
+ json: Option<ContentSource>,
64
+ default: DefaultFormat,
65
+ },
66
+ Named(String),
67
+ }
68
+
69
+ impl From<ErrorResponseDef> for ErrorResponse {
70
+ fn from(def: ErrorResponseDef) -> Self {
71
+ match def {
72
+ ErrorResponseDef::Detailed {
73
+ code,
74
+ plaintext,
75
+ html,
76
+ json,
77
+ default,
78
+ } => ErrorResponse {
79
+ code,
80
+ plaintext,
81
+ html,
82
+ json,
83
+ default,
84
+ },
85
+ ErrorResponseDef::Named(name) => match name.as_str() {
86
+ "internal_server_error" => ErrorResponse::internal_server_error(),
87
+ "not_found" => ErrorResponse::not_found(),
88
+ "unauthorized" => ErrorResponse::unauthorized(),
89
+ "forbidden" => ErrorResponse::forbidden(),
90
+ "payload_too_large" => ErrorResponse::payload_too_large(),
91
+ "too_many_requests" => ErrorResponse::too_many_requests(),
92
+ "bad_gateway" => ErrorResponse::bad_gateway(),
93
+ "service_unavailable" => ErrorResponse::service_unavailable(),
94
+ "gateway_timeout" => ErrorResponse::gateway_timeout(),
95
+ _ => {
96
+ warn!(
97
+ "Unknown error response name: {}. Using internal server error.",
98
+ name
99
+ );
100
+ ErrorResponse::internal_server_error()
101
+ }
102
+ },
103
+ }
104
+ }
105
+ }
106
+
107
+ impl ErrorResponse {
108
+ pub(crate) async fn to_http_response(&self, accept: ResponseFormat) -> HttpResponse {
109
+ let mut resp = Response::builder().status(self.code);
110
+ let response = match accept {
111
+ ResponseFormat::TEXT => {
112
+ resp = resp.header(CONTENT_TYPE, "text/plain");
113
+ resp.body(Self::get_response_body(self.code, &self.plaintext, accept).await)
114
+ }
115
+ ResponseFormat::HTML => {
116
+ resp = resp.header(CONTENT_TYPE, "text/html");
117
+ resp.body(Self::get_response_body(self.code, &self.html, accept).await)
118
+ }
119
+ ResponseFormat::JSON => {
120
+ resp = resp.header(CONTENT_TYPE, "application/json");
121
+ resp.body(Self::get_response_body(self.code, &self.json, accept).await)
122
+ }
123
+ ResponseFormat::UNKNOWN => match self.default {
124
+ DefaultFormat::Plaintext => {
125
+ resp = resp.header(CONTENT_TYPE, "text/plain");
126
+ resp.body(Self::get_response_body(self.code, &self.plaintext, accept).await)
127
+ }
128
+ DefaultFormat::Html => {
129
+ resp = resp.header(CONTENT_TYPE, "text/html");
130
+ resp.body(Self::get_response_body(self.code, &self.html, accept).await)
131
+ }
132
+ DefaultFormat::Json => {
133
+ resp = resp.header(CONTENT_TYPE, "application/json");
134
+ resp.body(Self::get_response_body(self.code, &self.json, accept).await)
135
+ }
136
+ },
137
+ };
138
+ response.unwrap()
139
+ }
140
+
141
+ async fn get_response_body(
142
+ code: u16,
143
+ source: &Option<ContentSource>,
144
+ accept: ResponseFormat,
145
+ ) -> HttpBody {
146
+ match source {
147
+ Some(ContentSource::Inline(text)) => {
148
+ return HttpBody::full(Bytes::from(text.clone()));
149
+ }
150
+ Some(ContentSource::Static(text)) => {
151
+ return HttpBody::full(text.clone());
152
+ }
153
+ Some(ContentSource::File(path)) => {
154
+ // Convert the PathBuf to a &str (assumes valid UTF-8).
155
+ if let Some(path_str) = path.to_str() {
156
+ let response = ROOT_STATIC_FILE_SERVER
157
+ .serve_single(path_str, accept, &[])
158
+ .await;
159
+ if response.status().is_success() {
160
+ return response.into_body();
161
+ }
162
+ }
163
+ }
164
+ None => {}
165
+ }
166
+ ErrorResponse::fallback_body_for(code, accept)
167
+ }
168
+ }