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,54 @@
1
+ use super::{string_rewrite::StringRewrite, FromValue, MiddlewareLayer};
2
+ use crate::{
3
+ server::{
4
+ http_message_types::{HttpBody, HttpRequest, HttpResponse},
5
+ redirect_type::RedirectType,
6
+ },
7
+ services::itsi_http_service::HttpRequestContext,
8
+ };
9
+ use async_trait::async_trait;
10
+ use either::Either;
11
+ use http::Response;
12
+ use magnus::error::Result;
13
+ use serde::Deserialize;
14
+ use tracing::debug;
15
+
16
+ #[derive(Debug, Clone, Deserialize)]
17
+ pub struct Redirect {
18
+ pub to: StringRewrite,
19
+ #[serde(default)]
20
+ #[serde(rename(deserialize = "type"))]
21
+ pub redirect_type: RedirectType,
22
+ }
23
+
24
+ #[async_trait]
25
+ impl MiddlewareLayer for Redirect {
26
+ async fn before(
27
+ &self,
28
+ req: HttpRequest,
29
+ context: &mut HttpRequestContext,
30
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
31
+ Ok(Either::Right(self.redirect_response(&req, context)?))
32
+ }
33
+ }
34
+
35
+ impl Redirect {
36
+ pub fn redirect_response(
37
+ &self,
38
+ req: &HttpRequest,
39
+ context: &mut HttpRequestContext,
40
+ ) -> Result<HttpResponse> {
41
+ let mut response = Response::new(HttpBody::empty());
42
+ *response.status_mut() = self.redirect_type.status_code();
43
+ let destination = self.to.rewrite_request(req, context).parse().map_err(|e| {
44
+ magnus::Error::new(
45
+ magnus::Ruby::get().unwrap().exception_standard_error(),
46
+ format!("Invalid Rewrite String: {:?}: {}", self.to, e),
47
+ )
48
+ })?;
49
+ debug!(target: "middleware::redirect", "Redirecting to {:?}", destination);
50
+ response.headers_mut().append("Location", destination);
51
+ Ok(response)
52
+ }
53
+ }
54
+ impl FromValue for Redirect {}
@@ -0,0 +1,54 @@
1
+ use std::collections::HashMap;
2
+
3
+ use crate::{
4
+ server::http_message_types::{HttpRequest, HttpResponse},
5
+ services::itsi_http_service::HttpRequestContext,
6
+ };
7
+
8
+ use super::{FromValue, MiddlewareLayer, StringRewrite};
9
+ use async_trait::async_trait;
10
+ use either::Either;
11
+ use http::HeaderName;
12
+ use magnus::error::Result;
13
+ use serde::Deserialize;
14
+
15
+ #[derive(Debug, Clone, Deserialize)]
16
+ pub struct RequestHeaders {
17
+ pub additions: HashMap<String, Vec<StringRewrite>>,
18
+ pub removals: Vec<String>,
19
+ }
20
+ #[async_trait]
21
+ impl MiddlewareLayer for RequestHeaders {
22
+ async fn before(
23
+ &self,
24
+ mut req: HttpRequest,
25
+ context: &mut HttpRequestContext,
26
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
27
+ let mut headers_to_add = Vec::new();
28
+
29
+ for (header_name, header_values) in &self.additions {
30
+ if let Ok(parsed_header_name) = header_name.parse::<HeaderName>() {
31
+ for header_value in header_values {
32
+ if let Ok(parsed_header_value) =
33
+ header_value.rewrite_request(&req, context).parse()
34
+ {
35
+ headers_to_add.push((parsed_header_name.clone(), parsed_header_value));
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ let headers = req.headers_mut();
42
+
43
+ for removal in &self.removals {
44
+ headers.remove(removal);
45
+ }
46
+
47
+ for (name, value) in headers_to_add {
48
+ headers.append(name, value);
49
+ }
50
+
51
+ Ok(Either::Left(req))
52
+ }
53
+ }
54
+ impl FromValue for RequestHeaders {}
@@ -0,0 +1,51 @@
1
+ use std::collections::HashMap;
2
+
3
+ use super::{FromValue, MiddlewareLayer, StringRewrite};
4
+ use crate::{
5
+ server::http_message_types::HttpResponse, services::itsi_http_service::HttpRequestContext,
6
+ };
7
+ use async_trait::async_trait;
8
+ use http::HeaderName;
9
+ use serde::Deserialize;
10
+
11
+ #[derive(Debug, Clone, Deserialize)]
12
+ pub struct ResponseHeaders {
13
+ pub additions: HashMap<String, Vec<StringRewrite>>,
14
+ pub removals: Vec<String>,
15
+ }
16
+
17
+ #[async_trait]
18
+ impl MiddlewareLayer for ResponseHeaders {
19
+ async fn after(
20
+ &self,
21
+ mut resp: HttpResponse,
22
+ context: &mut HttpRequestContext,
23
+ ) -> HttpResponse {
24
+ let mut headers_to_add = Vec::new();
25
+
26
+ for (header_name, header_values) in &self.additions {
27
+ if let Ok(parsed_header_name) = header_name.parse::<HeaderName>() {
28
+ for header_value in header_values {
29
+ if let Ok(parsed_header_value) =
30
+ header_value.rewrite_response(&resp, context).parse()
31
+ {
32
+ headers_to_add.push((parsed_header_name.clone(), parsed_header_value));
33
+ }
34
+ }
35
+ }
36
+ }
37
+
38
+ let headers = resp.headers_mut();
39
+
40
+ for removal in &self.removals {
41
+ headers.remove(removal);
42
+ }
43
+
44
+ for (name, value) in headers_to_add {
45
+ headers.append(name, value);
46
+ }
47
+
48
+ resp
49
+ }
50
+ }
51
+ impl FromValue for ResponseHeaders {}
@@ -0,0 +1,138 @@
1
+ use super::MiddlewareLayer;
2
+ use crate::ruby_types::itsi_grpc_call::ItsiGrpcCall;
3
+ use crate::ruby_types::itsi_http_request::ItsiHttpRequest;
4
+ use crate::server::http_message_types::{HttpRequest, HttpResponse};
5
+ use crate::services::itsi_http_service::HttpRequestContext;
6
+ use crate::services::static_file_server::ROOT_STATIC_FILE_SERVER;
7
+ use async_trait::async_trait;
8
+ use derive_more::Debug;
9
+ use either::Either;
10
+ use itsi_rb_helpers::{HeapVal, HeapValue};
11
+ use magnus::{block::Proc, error::Result, value::ReprValue};
12
+ use regex::Regex;
13
+ use std::str::FromStr;
14
+ use std::sync::atomic::Ordering;
15
+ use std::sync::Arc;
16
+
17
+ #[derive(Debug)]
18
+ pub struct RubyApp {
19
+ app: Arc<HeapValue<Proc>>,
20
+ request_type: RequestType,
21
+ script_name: Option<String>,
22
+ sendfile: bool,
23
+ nonblocking: bool,
24
+ base_path: Regex,
25
+ }
26
+
27
+ #[derive(Debug)]
28
+ pub enum RequestType {
29
+ Http,
30
+ Grpc,
31
+ }
32
+
33
+ impl FromStr for RequestType {
34
+ type Err = &'static str;
35
+
36
+ fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
37
+ match s {
38
+ "http" => Ok(RequestType::Http),
39
+ "grpc" => Ok(RequestType::Grpc),
40
+ _ => Err("Invalid request type"),
41
+ }
42
+ }
43
+ }
44
+
45
+ impl RubyApp {
46
+ pub fn from_value(params: HeapVal) -> magnus::error::Result<Arc<Self>> {
47
+ let app = params
48
+ .funcall::<_, _, Proc>(magnus::Ruby::get().unwrap().to_symbol("[]"), ("app_proc",))?;
49
+ let sendfile = params
50
+ .funcall::<_, _, bool>(magnus::Ruby::get().unwrap().to_symbol("[]"), ("sendfile",))
51
+ .unwrap_or(true);
52
+ let nonblocking = params
53
+ .funcall::<_, _, bool>(
54
+ magnus::Ruby::get().unwrap().to_symbol("[]"),
55
+ ("nonblocking",),
56
+ )
57
+ .unwrap_or(false);
58
+ let base_path_src = params
59
+ .funcall::<_, _, String>(magnus::Ruby::get().unwrap().to_symbol("[]"), ("base_path",))
60
+ .unwrap_or("".to_owned());
61
+ let script_name = params
62
+ .funcall::<_, _, Option<String>>(
63
+ magnus::Ruby::get().unwrap().to_symbol("[]"),
64
+ ("script_name",),
65
+ )
66
+ .unwrap_or(None);
67
+ let base_path = Regex::new(&base_path_src).unwrap();
68
+
69
+ let request_type: RequestType = params
70
+ .funcall::<_, _, String>(
71
+ magnus::Ruby::get().unwrap().to_symbol("[]"),
72
+ ("request_type",),
73
+ )
74
+ .unwrap_or("http".to_string())
75
+ .parse()
76
+ .unwrap_or(RequestType::Http);
77
+
78
+ Ok(Arc::new(RubyApp {
79
+ app: Arc::new(app.into()),
80
+ sendfile,
81
+ nonblocking,
82
+ script_name,
83
+ request_type,
84
+ base_path,
85
+ }))
86
+ }
87
+ }
88
+
89
+ #[async_trait]
90
+ impl MiddlewareLayer for RubyApp {
91
+ async fn before(
92
+ &self,
93
+ req: HttpRequest,
94
+ context: &mut HttpRequestContext,
95
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
96
+ context.is_ruby_request.store(true, Ordering::SeqCst);
97
+ match self.request_type {
98
+ RequestType::Http => {
99
+ let uri = req.uri().path();
100
+ let script_name = self.script_name.clone().unwrap_or_else(|| {
101
+ self.base_path
102
+ .captures(uri)
103
+ .and_then(|caps| caps.name("base_path"))
104
+ .map(|m| m.as_str())
105
+ .unwrap_or("/")
106
+ .to_owned()
107
+ });
108
+ ItsiHttpRequest::process_request(
109
+ self.app.clone(),
110
+ req,
111
+ context,
112
+ script_name,
113
+ self.nonblocking,
114
+ )
115
+ .await
116
+ .map_err(|e| e.into())
117
+ .map(Either::Right)
118
+ }
119
+ RequestType::Grpc => {
120
+ ItsiGrpcCall::process_request(self.app.clone(), req, context, self.nonblocking)
121
+ .await
122
+ .map_err(|e| e.into())
123
+ .map(Either::Right)
124
+ }
125
+ }
126
+ }
127
+
128
+ async fn after(&self, resp: HttpResponse, context: &mut HttpRequestContext) -> HttpResponse {
129
+ if self.sendfile {
130
+ if let Some(sendfile_header) = resp.headers().get("X-Sendfile") {
131
+ return ROOT_STATIC_FILE_SERVER
132
+ .serve_single_abs(sendfile_header.to_str().unwrap(), context.accept, &[])
133
+ .await;
134
+ }
135
+ }
136
+ resp
137
+ }
138
+ }
@@ -0,0 +1,269 @@
1
+ use super::{FromValue, MiddlewareLayer};
2
+ use crate::{
3
+ server::http_message_types::{HttpRequest, HttpResponse},
4
+ services::{
5
+ itsi_http_service::HttpRequestContext,
6
+ static_file_server::{
7
+ NotFoundBehavior, ServeRange, StaticFileServer, StaticFileServerConfig,
8
+ },
9
+ },
10
+ };
11
+ use async_trait::async_trait;
12
+ use either::Either;
13
+ use http::{
14
+ header::{IF_MODIFIED_SINCE, RANGE},
15
+ HeaderMap, HeaderValue, Method,
16
+ };
17
+ use itsi_error::ItsiError;
18
+ use magnus::error::Result;
19
+ use quick_cache::sync::Cache;
20
+ use regex::Regex;
21
+ use serde::Deserialize;
22
+ use std::{
23
+ collections::HashMap,
24
+ path::PathBuf,
25
+ sync::{Arc, OnceLock},
26
+ time::Duration,
27
+ };
28
+ use tracing::debug;
29
+
30
+ /// Compact representation of the client's Accept-Encoding preferences.
31
+ /// Priority order is determined by the bit checks in `pick_encoding`.
32
+ #[derive(Clone, Copy, Debug, Default)]
33
+ struct AcceptEncodingMask(u8);
34
+
35
+ impl AcceptEncodingMask {
36
+ const BR: u8 = 1 << 0;
37
+ const GZIP: u8 = 1 << 1;
38
+ const ZSTD: u8 = 1 << 2;
39
+ const DEFLATE: u8 = 1 << 3;
40
+
41
+ fn from_headers(headers: &[HeaderValue]) -> Self {
42
+ let mut mask = 0u8;
43
+
44
+ for hv in headers {
45
+ let Ok(s) = hv.to_str() else { continue };
46
+
47
+ // We intentionally ignore q-values and treat any mention as "acceptable".
48
+ // This is a fast-path optimization for common benchmark/client headers.
49
+ for part in s.split(',') {
50
+ let token = part.split(';').next().unwrap_or("").trim();
51
+ match token {
52
+ "br" => mask |= Self::BR,
53
+ "gzip" => mask |= Self::GZIP,
54
+ "zstd" => mask |= Self::ZSTD,
55
+ "deflate" => mask |= Self::DEFLATE,
56
+ _ => {}
57
+ }
58
+ }
59
+ }
60
+
61
+ Self(mask)
62
+ }
63
+
64
+ fn pick_encoding(self) -> Option<&'static str> {
65
+ // Prefer stronger/faster compression if available.
66
+ // (Actual availability is checked by the file server.)
67
+ if (self.0 & Self::ZSTD) != 0 {
68
+ return Some("zstd");
69
+ }
70
+ if (self.0 & Self::BR) != 0 {
71
+ return Some("br");
72
+ }
73
+ if (self.0 & Self::GZIP) != 0 {
74
+ return Some("gzip");
75
+ }
76
+ if (self.0 & Self::DEFLATE) != 0 {
77
+ return Some("deflate");
78
+ }
79
+ None
80
+ }
81
+ }
82
+
83
+ #[derive(Debug, Deserialize)]
84
+ pub struct StaticAssets {
85
+ pub root_dir: PathBuf,
86
+ pub not_found_behavior: NotFoundBehavior,
87
+ pub auto_index: bool,
88
+ pub try_html_extension: bool,
89
+ pub max_file_size_in_memory: u64,
90
+ pub max_files_in_memory: u64,
91
+ pub file_check_interval: u64,
92
+ pub headers: Option<HashMap<String, String>>,
93
+ pub allowed_extensions: Vec<String>,
94
+ pub relative_path: bool,
95
+ pub serve_hidden_files: bool,
96
+ pub base_path: String,
97
+ #[serde(skip)]
98
+ pub base_path_regex: OnceLock<Regex>,
99
+ #[serde(skip)]
100
+ file_server: OnceLock<StaticFileServer>,
101
+ }
102
+
103
+ #[async_trait]
104
+ impl MiddlewareLayer for StaticAssets {
105
+ async fn initialize(&self) -> Result<()> {
106
+ if let Ok(metadata) = tokio::fs::metadata(&self.root_dir).await {
107
+ if metadata.is_dir() {
108
+ Ok(())
109
+ } else {
110
+ Err(ItsiError::InvalidInput(
111
+ "Root directory exists but is not a directory".to_string(),
112
+ ))
113
+ }
114
+ } else {
115
+ Err(ItsiError::InvalidInput(
116
+ "Root directory exists but is not a directory".to_string(),
117
+ ))
118
+ }?;
119
+ self.base_path_regex
120
+ .set(Regex::new(&self.base_path).map_err(ItsiError::new)?)
121
+ .map_err(ItsiError::new)?;
122
+
123
+ debug!(target: "middleware::static_assets", "Base path regexp: {}", self.base_path);
124
+
125
+ self.file_server
126
+ .set(StaticFileServer::new(StaticFileServerConfig {
127
+ root_dir: self.root_dir.clone(),
128
+ not_found_behavior: self.not_found_behavior.clone(),
129
+ auto_index: self.auto_index,
130
+ max_entries: self.max_files_in_memory,
131
+ try_html_extension: self.try_html_extension,
132
+ max_file_size: self.max_file_size_in_memory,
133
+ headers: self.headers.clone(),
134
+ recheck_interval: Duration::from_secs(self.file_check_interval),
135
+ serve_hidden_files: self.serve_hidden_files,
136
+ allowed_extensions: self.allowed_extensions.clone(),
137
+ miss_cache: Arc::new(Cache::new(self.max_files_in_memory as usize)),
138
+ })?)
139
+ .map_err(ItsiError::new)?;
140
+ Ok(())
141
+ }
142
+
143
+ async fn before(
144
+ &self,
145
+ req: HttpRequest,
146
+ context: &mut HttpRequestContext,
147
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
148
+ // Only handle GET and HEAD requests
149
+ if req.method() != Method::GET && req.method() != Method::HEAD {
150
+ debug!(target: "middleware::static_assets", "Refusing to handle non-GET/HEAD request");
151
+ return Ok(Either::Left(req));
152
+ }
153
+
154
+ // We still populate the context cache for any other middleware that might want it,
155
+ // but we avoid re-parsing Accept-Encoding later by computing a compact mask here.
156
+ context.set_supported_encoding_set(&req);
157
+
158
+ let abs_path = req.uri().path();
159
+ let rel_path = if !self.relative_path {
160
+ abs_path.trim_start_matches("/")
161
+ } else {
162
+ let base_path = self
163
+ .base_path_regex
164
+ .get()
165
+ .unwrap()
166
+ .captures(abs_path)
167
+ .and_then(|caps| caps.name("base_path"))
168
+ .map(|m| m.as_str())
169
+ .unwrap_or("/");
170
+
171
+ match abs_path.strip_prefix(base_path) {
172
+ Some(suffix) => suffix,
173
+ None => return Ok(Either::Left(req)),
174
+ }
175
+ };
176
+
177
+ debug!(target: "middleware::static_assets", "Asset path is {}", rel_path);
178
+ let is_head_request = req.method() == Method::HEAD;
179
+
180
+ // Extract range and if-modified-since headers
181
+ let serve_range = parse_range_header(req.headers());
182
+ let if_modified_since = req
183
+ .headers()
184
+ .get(IF_MODIFIED_SINCE)
185
+ .and_then(|ims| ims.to_str().ok())
186
+ .and_then(|ims_str| httpdate::parse_http_date(ims_str).ok());
187
+
188
+ // Let the file server handle everything
189
+ let file_server = self.file_server.get().unwrap();
190
+ let encodings: &[HeaderValue] = context
191
+ .supported_encoding_set()
192
+ .map_or(&[], |set| set.as_slice());
193
+
194
+ // Compute a fast encoding preference and narrow the encoding list we hand to the server.
195
+ // This avoids repeated per-request string splitting/trim in the static file server.
196
+ let mask = AcceptEncodingMask::from_headers(encodings);
197
+ let preferred = mask.pick_encoding();
198
+
199
+ let narrowed: [HeaderValue; 1];
200
+ let encodings_for_server: &[HeaderValue] = if let Some(token) = preferred {
201
+ // Safe: these are valid header values and the file server only needs to see
202
+ // a minimal representation to pick a cached variant.
203
+ narrowed = [HeaderValue::from_static(token)];
204
+ &narrowed
205
+ } else {
206
+ &[]
207
+ };
208
+
209
+ let response = file_server
210
+ .serve(
211
+ &req,
212
+ rel_path,
213
+ abs_path,
214
+ serve_range,
215
+ if_modified_since,
216
+ is_head_request,
217
+ encodings_for_server,
218
+ )
219
+ .await;
220
+
221
+ if response.is_none() {
222
+ Ok(Either::Left(req))
223
+ } else {
224
+ Ok(Either::Right(response.unwrap()))
225
+ }
226
+ }
227
+ }
228
+
229
+ fn parse_range_header(headers: &HeaderMap) -> ServeRange {
230
+ let Some(range_header) = headers.get(RANGE) else {
231
+ return ServeRange::Full;
232
+ };
233
+
234
+ let range_header = range_header.to_str().unwrap_or("");
235
+ let bytes_prefix = "bytes=";
236
+ if !range_header.starts_with(bytes_prefix) {
237
+ return ServeRange::Full;
238
+ }
239
+
240
+ // Only consider the first range specifier, ignore multi-range requests.
241
+ let range_str = range_header[bytes_prefix.len()..]
242
+ .split(',')
243
+ .next()
244
+ .unwrap_or("");
245
+
246
+ let Some((start_str, end_str)) = range_str.split_once('-') else {
247
+ return ServeRange::Full;
248
+ };
249
+
250
+ let start = if start_str.is_empty() {
251
+ end_str.parse::<u64>().unwrap_or(0)
252
+ } else if let Ok(start) = start_str.parse::<u64>() {
253
+ start
254
+ } else {
255
+ return ServeRange::Full;
256
+ };
257
+
258
+ let end = if end_str.is_empty() {
259
+ u64::MAX // sentinel for open-ended ranges
260
+ } else if let Ok(end) = end_str.parse::<u64>() {
261
+ end
262
+ } else {
263
+ return ServeRange::Full;
264
+ };
265
+
266
+ ServeRange::Range(start, end)
267
+ }
268
+
269
+ impl FromValue for StaticAssets {}
@@ -0,0 +1,62 @@
1
+ use std::sync::OnceLock;
2
+
3
+ use super::{FromValue, MiddlewareLayer};
4
+ use crate::server::http_message_types::{HttpBody, HttpRequest, HttpResponse};
5
+ use crate::services::itsi_http_service::HttpRequestContext;
6
+ use async_trait::async_trait;
7
+ use bytes::Bytes;
8
+ use derive_more::Debug;
9
+ use either::Either;
10
+ use http::{HeaderMap, HeaderName, HeaderValue, Response, StatusCode};
11
+ use itsi_error::ItsiError;
12
+ use magnus::error::Result;
13
+ use serde::Deserialize;
14
+
15
+ #[derive(Debug, Deserialize)]
16
+ pub struct StaticResponse {
17
+ code: u16,
18
+ headers: Vec<(String, String)>,
19
+ body: Vec<u8>,
20
+ #[serde(skip)]
21
+ header_map: OnceLock<HeaderMap>,
22
+ #[serde(skip)]
23
+ body_bytes: OnceLock<Bytes>,
24
+ #[serde(skip)]
25
+ status_code: OnceLock<StatusCode>,
26
+ }
27
+
28
+ #[async_trait]
29
+ impl MiddlewareLayer for StaticResponse {
30
+ async fn initialize(&self) -> Result<()> {
31
+ let mut header_map = HeaderMap::new();
32
+ for (key, value) in self.headers.iter() {
33
+ if let (Ok(hn), Ok(hv)) = (key.parse::<HeaderName>(), value.parse::<HeaderValue>()) {
34
+ header_map.insert(hn, hv);
35
+ }
36
+ }
37
+ self.header_map
38
+ .set(header_map)
39
+ .map_err(|_| ItsiError::new("Failed to set headers"))?;
40
+ self.body_bytes
41
+ .set(Bytes::from(self.body.clone()))
42
+ .map_err(|_| ItsiError::new("Failed to set body bytes"))?;
43
+ self.status_code
44
+ .set(StatusCode::from_u16(self.code).unwrap_or(StatusCode::OK))
45
+ .map_err(|_| ItsiError::new("Failed to set status code"))?;
46
+ Ok(())
47
+ }
48
+
49
+ async fn before(
50
+ &self,
51
+ _req: HttpRequest,
52
+ _context: &mut HttpRequestContext,
53
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
54
+ let mut resp = Response::new(HttpBody::full(self.body_bytes.get().unwrap().clone()));
55
+ *resp.status_mut() = *self.status_code.get().unwrap();
56
+ *resp.headers_mut() = self.header_map.get().unwrap().clone();
57
+
58
+ Ok(Either::Right(resp))
59
+ }
60
+ }
61
+
62
+ impl FromValue for StaticResponse {}