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.
Files changed (155) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +120 -52
  3. data/README.md +57 -24
  4. data/Rakefile +0 -4
  5. data/ext/itsi_acme/Cargo.toml +86 -0
  6. data/ext/itsi_acme/examples/high_level.rs +63 -0
  7. data/ext/itsi_acme/examples/high_level_warp.rs +52 -0
  8. data/ext/itsi_acme/examples/low_level.rs +87 -0
  9. data/ext/itsi_acme/examples/low_level_axum.rs +66 -0
  10. data/ext/itsi_acme/src/acceptor.rs +81 -0
  11. data/ext/itsi_acme/src/acme.rs +354 -0
  12. data/ext/itsi_acme/src/axum.rs +86 -0
  13. data/ext/itsi_acme/src/cache.rs +39 -0
  14. data/ext/itsi_acme/src/caches/boxed.rs +80 -0
  15. data/ext/itsi_acme/src/caches/composite.rs +69 -0
  16. data/ext/itsi_acme/src/caches/dir.rs +106 -0
  17. data/ext/itsi_acme/src/caches/mod.rs +11 -0
  18. data/ext/itsi_acme/src/caches/no.rs +78 -0
  19. data/ext/itsi_acme/src/caches/test.rs +136 -0
  20. data/ext/itsi_acme/src/config.rs +172 -0
  21. data/ext/itsi_acme/src/https_helper.rs +69 -0
  22. data/ext/itsi_acme/src/incoming.rs +142 -0
  23. data/ext/itsi_acme/src/jose.rs +161 -0
  24. data/ext/itsi_acme/src/lib.rs +142 -0
  25. data/ext/itsi_acme/src/resolver.rs +59 -0
  26. data/ext/itsi_acme/src/state.rs +424 -0
  27. data/ext/itsi_error/Cargo.toml +1 -0
  28. data/ext/itsi_error/src/lib.rs +106 -7
  29. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  30. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  31. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  32. data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  33. data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
  34. data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
  35. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
  36. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
  37. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
  38. data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
  39. data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
  40. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
  41. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
  42. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
  43. data/ext/itsi_rb_helpers/Cargo.toml +1 -0
  44. data/ext/itsi_rb_helpers/src/heap_value.rs +18 -0
  45. data/ext/itsi_rb_helpers/src/lib.rs +63 -12
  46. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  47. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  48. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  49. 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
  50. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
  51. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
  52. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
  53. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
  54. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
  55. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
  56. data/ext/itsi_scheduler/Cargo.toml +1 -1
  57. data/ext/itsi_scheduler/src/itsi_scheduler.rs +9 -3
  58. data/ext/itsi_scheduler/src/lib.rs +1 -0
  59. data/ext/itsi_server/Cargo.lock +2956 -0
  60. data/ext/itsi_server/Cargo.toml +73 -29
  61. data/ext/itsi_server/src/default_responses/mod.rs +11 -0
  62. data/ext/itsi_server/src/env.rs +43 -0
  63. data/ext/itsi_server/src/lib.rs +114 -75
  64. data/ext/itsi_server/src/prelude.rs +2 -0
  65. data/ext/itsi_server/src/{body_proxy → ruby_types/itsi_body_proxy}/big_bytes.rs +10 -5
  66. data/ext/itsi_server/src/{body_proxy/itsi_body_proxy.rs → ruby_types/itsi_body_proxy/mod.rs} +29 -8
  67. data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
  68. data/ext/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +264 -0
  69. data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +362 -0
  70. data/ext/itsi_server/src/{response/itsi_response.rs → ruby_types/itsi_http_response.rs} +84 -40
  71. data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +233 -0
  72. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +565 -0
  73. data/ext/itsi_server/src/ruby_types/itsi_server.rs +86 -0
  74. data/ext/itsi_server/src/ruby_types/mod.rs +48 -0
  75. data/ext/itsi_server/src/server/{bind.rs → binds/bind.rs} +59 -24
  76. data/ext/itsi_server/src/server/binds/listener.rs +444 -0
  77. data/ext/itsi_server/src/server/binds/mod.rs +4 -0
  78. data/ext/itsi_server/src/server/{tls → binds/tls}/locked_dir_cache.rs +57 -19
  79. data/ext/itsi_server/src/server/{tls.rs → binds/tls.rs} +120 -31
  80. data/ext/itsi_server/src/server/byte_frame.rs +32 -0
  81. data/ext/itsi_server/src/server/http_message_types.rs +97 -0
  82. data/ext/itsi_server/src/server/io_stream.rs +2 -1
  83. data/ext/itsi_server/src/server/lifecycle_event.rs +3 -0
  84. data/ext/itsi_server/src/server/middleware_stack/middleware.rs +170 -0
  85. data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +63 -0
  86. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +94 -0
  87. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +94 -0
  88. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +343 -0
  89. data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +151 -0
  90. data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +316 -0
  91. data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +301 -0
  92. data/ext/itsi_server/src/server/middleware_stack/middlewares/csp.rs +193 -0
  93. data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +64 -0
  94. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +192 -0
  95. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +171 -0
  96. data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +198 -0
  97. data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +82 -0
  98. data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +209 -0
  99. data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
  100. data/ext/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
  101. data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +116 -0
  102. data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +411 -0
  103. data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +142 -0
  104. data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +55 -0
  105. data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +54 -0
  106. data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +51 -0
  107. data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +126 -0
  108. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +187 -0
  109. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
  110. data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +173 -0
  111. data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +31 -0
  112. data/ext/itsi_server/src/server/middleware_stack/mod.rs +381 -0
  113. data/ext/itsi_server/src/server/mod.rs +7 -5
  114. data/ext/itsi_server/src/server/process_worker.rs +65 -14
  115. data/ext/itsi_server/src/server/redirect_type.rs +26 -0
  116. data/ext/itsi_server/src/server/request_job.rs +11 -0
  117. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +150 -50
  118. data/ext/itsi_server/src/server/serve_strategy/mod.rs +9 -6
  119. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +399 -165
  120. data/ext/itsi_server/src/server/signal.rs +33 -26
  121. data/ext/itsi_server/src/server/size_limited_incoming.rs +107 -0
  122. data/ext/itsi_server/src/server/thread_worker.rs +218 -107
  123. data/ext/itsi_server/src/services/cache_store.rs +74 -0
  124. data/ext/itsi_server/src/services/itsi_http_service.rs +257 -0
  125. data/ext/itsi_server/src/services/mime_types.rs +1416 -0
  126. data/ext/itsi_server/src/services/mod.rs +6 -0
  127. data/ext/itsi_server/src/services/password_hasher.rs +83 -0
  128. data/ext/itsi_server/src/services/rate_limiter.rs +580 -0
  129. data/ext/itsi_server/src/services/static_file_server.rs +1340 -0
  130. data/ext/itsi_tracing/Cargo.toml +1 -0
  131. data/ext/itsi_tracing/src/lib.rs +362 -33
  132. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
  133. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
  134. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
  135. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
  136. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
  137. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
  138. data/itsi-scheduler-100.png +0 -0
  139. data/lib/itsi/scheduler/version.rb +1 -1
  140. data/lib/itsi/scheduler.rb +11 -6
  141. metadata +117 -24
  142. data/CHANGELOG.md +0 -5
  143. data/CODE_OF_CONDUCT.md +0 -132
  144. data/LICENSE.txt +0 -21
  145. data/ext/itsi_error/src/from.rs +0 -71
  146. data/ext/itsi_server/extconf.rb +0 -6
  147. data/ext/itsi_server/src/body_proxy/mod.rs +0 -2
  148. data/ext/itsi_server/src/request/itsi_request.rs +0 -277
  149. data/ext/itsi_server/src/request/mod.rs +0 -1
  150. data/ext/itsi_server/src/response/mod.rs +0 -1
  151. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -13
  152. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -5
  153. data/ext/itsi_server/src/server/itsi_server.rs +0 -244
  154. data/ext/itsi_server/src/server/listener.rs +0 -327
  155. /data/ext/itsi_server/src/server/{bind_protocol.rs → binds/bind_protocol.rs} +0 -0
@@ -0,0 +1,63 @@
1
+ use super::{token_source::TokenSource, ErrorResponse, FromValue, MiddlewareLayer};
2
+ use crate::{
3
+ server::http_message_types::{HttpRequest, HttpResponse, RequestExt},
4
+ services::itsi_http_service::HttpRequestContext,
5
+ };
6
+ use async_trait::async_trait;
7
+ use either::Either;
8
+ use itsi_error::ItsiError;
9
+ use magnus::error::Result;
10
+ use regex::RegexSet;
11
+ use serde::Deserialize;
12
+ use std::{collections::HashMap, sync::OnceLock};
13
+ use tracing::debug;
14
+
15
+ #[derive(Debug, Clone, Deserialize)]
16
+ pub struct AllowList {
17
+ #[serde(skip_deserializing)]
18
+ pub allowed_ips: OnceLock<RegexSet>,
19
+ pub allowed_patterns: Vec<String>,
20
+ pub trusted_proxies: HashMap<String, TokenSource>,
21
+ #[serde(default = "forbidden_error_response")]
22
+ pub error_response: ErrorResponse,
23
+ }
24
+
25
+ fn forbidden_error_response() -> ErrorResponse {
26
+ ErrorResponse::forbidden()
27
+ }
28
+
29
+ #[async_trait]
30
+ impl MiddlewareLayer for AllowList {
31
+ async fn initialize(&self) -> Result<()> {
32
+ let allowed_ips = RegexSet::new(&self.allowed_patterns).map_err(ItsiError::new)?;
33
+ self.allowed_ips
34
+ .set(allowed_ips)
35
+ .map_err(|e| ItsiError::new(format!("Failed to set allowed IPs: {:?}", e)))?;
36
+ Ok(())
37
+ }
38
+
39
+ async fn before(
40
+ &self,
41
+ req: HttpRequest,
42
+ context: &mut HttpRequestContext,
43
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
44
+ if let Some(allowed_ips) = self.allowed_ips.get() {
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 !allowed_ips.is_match(addr) {
52
+ debug!(target: "middleware::allow_list", "IP address {} is not allowed", addr);
53
+ return Ok(Either::Right(
54
+ self.error_response
55
+ .to_http_response(req.accept().into())
56
+ .await,
57
+ ));
58
+ }
59
+ }
60
+ Ok(Either::Left(req))
61
+ }
62
+ }
63
+ impl FromValue for AllowList {}
@@ -0,0 +1,94 @@
1
+ use std::collections::HashMap;
2
+
3
+ use crate::{
4
+ server::http_message_types::{HttpRequest, HttpResponse, RequestExt},
5
+ services::{itsi_http_service::HttpRequestContext, password_hasher},
6
+ };
7
+
8
+ use super::{error_response::ErrorResponse, token_source::TokenSource, FromValue, MiddlewareLayer};
9
+
10
+ use async_trait::async_trait;
11
+ use either::Either;
12
+ use magnus::error::Result;
13
+ use serde::Deserialize;
14
+ use tracing::debug;
15
+
16
+ type PasswordHash = String;
17
+
18
+ /// A simple API key filter.
19
+ /// The API key can be given inside the header or a query string
20
+ /// Keys are validated against a list of allowed key values (Changing these requires a restart)
21
+ #[derive(Debug, Clone, Deserialize)]
22
+ pub struct AuthAPIKey {
23
+ pub valid_keys: HashMap<String, PasswordHash>,
24
+ pub key_id_source: Option<TokenSource>,
25
+ pub token_source: TokenSource,
26
+ #[serde(default = "unauthorized_error_response")]
27
+ pub error_response: ErrorResponse,
28
+ }
29
+
30
+ fn unauthorized_error_response() -> ErrorResponse {
31
+ ErrorResponse::unauthorized()
32
+ }
33
+
34
+ #[async_trait]
35
+ impl MiddlewareLayer for AuthAPIKey {
36
+ async fn before(
37
+ &self,
38
+ req: HttpRequest,
39
+ _context: &mut HttpRequestContext,
40
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
41
+ if let Some(submitted_key) = match &self.token_source {
42
+ TokenSource::Header { name, prefix } => {
43
+ if let Some(header) = req.header(name) {
44
+ if let Some(prefix) = prefix {
45
+ Some(header.strip_prefix(prefix).unwrap_or("").trim_ascii())
46
+ } else {
47
+ Some(header.trim_ascii())
48
+ }
49
+ } else {
50
+ None
51
+ }
52
+ }
53
+ TokenSource::Query(query_name) => req.query_param(query_name),
54
+ } {
55
+ debug!(target: "middleware::auth_api_key", "API Key Retrieved. Anonymous {}", self.key_id_source.is_none());
56
+
57
+ if let Some(key_id) = self.key_id_source.as_ref() {
58
+ let key_id = match &key_id {
59
+ TokenSource::Header { name, prefix } => {
60
+ if let Some(header) = req.header(name) {
61
+ if let Some(prefix) = prefix {
62
+ Some(header.strip_prefix(prefix).unwrap_or("").trim_ascii())
63
+ } else {
64
+ Some(header.trim_ascii())
65
+ }
66
+ } else {
67
+ None
68
+ }
69
+ }
70
+ TokenSource::Query(query_name) => req.query_param(query_name),
71
+ };
72
+ debug!(target: "middleware::auth_api_key", "Key ID Retrieved");
73
+ if let Some(hash) = key_id.and_then(|kid| self.valid_keys.get(kid)) {
74
+ debug!(target: "middleware::auth_api_key", "Key for ID found");
75
+ if password_hasher::verify_password_hash(submitted_key, hash).is_ok_and(|v| v) {
76
+ return Ok(Either::Left(req));
77
+ }
78
+ }
79
+ } else if self.valid_keys.values().any(|key| {
80
+ password_hasher::verify_password_hash(submitted_key, key).is_ok_and(|v| v)
81
+ }) {
82
+ return Ok(Either::Left(req));
83
+ }
84
+ }
85
+
86
+ debug!(target: "middleware::auth_api_key", "Failed to authenticate API key");
87
+ Ok(Either::Right(
88
+ self.error_response
89
+ .to_http_response(req.accept().into())
90
+ .await,
91
+ ))
92
+ }
93
+ }
94
+ impl FromValue for AuthAPIKey {}
@@ -0,0 +1,94 @@
1
+ use async_trait::async_trait;
2
+ use base64::{engine::general_purpose, Engine};
3
+ use bytes::Bytes;
4
+ use either::Either;
5
+ use http::{Response, StatusCode};
6
+ use http_body_util::{combinators::BoxBody, Full};
7
+ use magnus::error::Result;
8
+ use serde::{Deserialize, Serialize};
9
+ use std::collections::HashMap;
10
+ use std::str;
11
+ use tracing::debug;
12
+
13
+ use crate::{
14
+ server::http_message_types::{HttpRequest, HttpResponse, RequestExt},
15
+ services::{itsi_http_service::HttpRequestContext, password_hasher::verify_password_hash},
16
+ };
17
+
18
+ use super::{FromValue, MiddlewareLayer};
19
+
20
+ type PasswordHash = String;
21
+
22
+ #[derive(Debug, Clone, Serialize, Deserialize)]
23
+ pub struct AuthBasic {
24
+ pub realm: String,
25
+ /// Maps usernames to passwords.
26
+ pub credential_pairs: HashMap<String, PasswordHash>,
27
+ }
28
+
29
+ impl AuthBasic {
30
+ fn basic_auth_failed_response(&self) -> HttpResponse {
31
+ Response::builder()
32
+ .status(StatusCode::UNAUTHORIZED)
33
+ .header(
34
+ "WWW-Authenticate",
35
+ format!("Basic realm=\"{}\"", self.realm),
36
+ )
37
+ .body(BoxBody::new(Full::new(Bytes::from("Unauthorized"))))
38
+ .unwrap()
39
+ }
40
+ }
41
+ #[async_trait]
42
+ impl MiddlewareLayer for AuthBasic {
43
+ async fn before(
44
+ &self,
45
+ req: HttpRequest,
46
+ _context: &mut HttpRequestContext,
47
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
48
+ // Retrieve the Authorization header.
49
+ let auth_header = req.header("Authorization");
50
+
51
+ if !auth_header.is_some_and(|header| header.starts_with("Basic ")) {
52
+ debug!(target: "middleware::auth_basic", "Basic auth failed. Authorization Header doesn't start with 'Basic '");
53
+ return Ok(Either::Right(self.basic_auth_failed_response()));
54
+ }
55
+
56
+ let auth_header = auth_header.unwrap();
57
+
58
+ let encoded_credentials = &auth_header["Basic ".len()..];
59
+ let decoded = match general_purpose::STANDARD.decode(encoded_credentials) {
60
+ Ok(bytes) => bytes,
61
+ Err(_) => {
62
+ debug!(target: "middleware::auth_basic", "Basic auth failed. Decoding failed");
63
+ return Ok(Either::Right(self.basic_auth_failed_response()));
64
+ }
65
+ };
66
+
67
+ let decoded_str = match str::from_utf8(&decoded) {
68
+ Ok(s) => s,
69
+ Err(_) => {
70
+ debug!(target: "middleware::auth_basic", "Basic auth failed. Decoding failed");
71
+ return Ok(Either::Right(self.basic_auth_failed_response()));
72
+ }
73
+ };
74
+
75
+ let mut parts = decoded_str.splitn(2, ':');
76
+ let username = parts.next().unwrap_or("");
77
+ let password = parts.next().unwrap_or("");
78
+
79
+ match self.credential_pairs.get(username) {
80
+ Some(expected_password_hash) => {
81
+ match verify_password_hash(password, expected_password_hash) {
82
+ Ok(true) => Ok(Either::Left(req)),
83
+ _ => Ok(Either::Right(self.basic_auth_failed_response())),
84
+ }
85
+ }
86
+ None => {
87
+ debug!(target: "middleware::auth_basic", "Basic auth failed. Username {} not found", username);
88
+ Ok(Either::Right(self.basic_auth_failed_response()))
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ impl FromValue for AuthBasic {}
@@ -0,0 +1,343 @@
1
+ use super::{error_response::ErrorResponse, token_source::TokenSource, FromValue, MiddlewareLayer};
2
+ use crate::{
3
+ server::http_message_types::{HttpRequest, HttpResponse, RequestExt},
4
+ services::itsi_http_service::HttpRequestContext,
5
+ };
6
+
7
+ use async_trait::async_trait;
8
+ use base64::{engine::general_purpose, Engine};
9
+ use derive_more::Debug;
10
+ use either::Either;
11
+ use itsi_error::ItsiError;
12
+ use jsonwebtoken::{
13
+ decode, decode_header, Algorithm as JwtAlg, DecodingKey, TokenData, Validation,
14
+ };
15
+ use magnus::error::Result;
16
+ use serde::Deserialize;
17
+ use std::{
18
+ collections::{HashMap, HashSet},
19
+ sync::OnceLock,
20
+ };
21
+ use tracing::debug;
22
+
23
+ #[derive(Debug, Clone, Deserialize)]
24
+ pub struct AuthJwt {
25
+ pub token_source: TokenSource,
26
+ // The verifiers map still holds base64-encoded key strings keyed by algorithm.
27
+ pub verifiers: HashMap<JwtAlgorithm, Vec<String>>,
28
+ // We now store jsonwebtoken’s DecodingKey in our OnceLock.
29
+ #[serde(skip_deserializing)]
30
+ #[debug(skip)]
31
+ pub keys: OnceLock<HashMap<JwtAlgorithm, Vec<DecodingKey>>>,
32
+ pub audiences: Option<HashSet<String>>,
33
+ pub subjects: Option<HashSet<String>>,
34
+ pub issuers: Option<HashSet<String>>,
35
+ #[serde(skip_deserializing)]
36
+ pub audience_vec: OnceLock<Option<Vec<String>>>,
37
+ #[serde(skip_deserializing)]
38
+ pub issuer_vec: OnceLock<Option<Vec<String>>>,
39
+ pub leeway: Option<u64>,
40
+ #[serde(default = "unauthorized_error_response")]
41
+ pub error_response: ErrorResponse,
42
+ }
43
+
44
+ fn unauthorized_error_response() -> ErrorResponse {
45
+ ErrorResponse::unauthorized()
46
+ }
47
+
48
+ #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Hash)]
49
+ pub enum JwtAlgorithm {
50
+ #[serde(rename(deserialize = "hs256"))]
51
+ Hs256,
52
+ #[serde(rename(deserialize = "hs384"))]
53
+ Hs384,
54
+ #[serde(rename(deserialize = "hs512"))]
55
+ Hs512,
56
+ #[serde(rename(deserialize = "rs256"))]
57
+ Rs256,
58
+ #[serde(rename(deserialize = "rs384"))]
59
+ Rs384,
60
+ #[serde(rename(deserialize = "rs512"))]
61
+ Rs512,
62
+ #[serde(rename(deserialize = "es256"))]
63
+ Es256,
64
+ #[serde(rename(deserialize = "es384"))]
65
+ Es384,
66
+ #[serde(rename(deserialize = "ps256"))]
67
+ Ps256,
68
+ #[serde(rename(deserialize = "ps384"))]
69
+ Ps384,
70
+ #[serde(rename(deserialize = "ps512"))]
71
+ Ps512,
72
+ }
73
+
74
+ // Allow conversion from jsonwebtoken’s Algorithm to our JwtAlgorithm.
75
+ impl From<JwtAlg> for JwtAlgorithm {
76
+ fn from(alg: JwtAlg) -> Self {
77
+ match alg {
78
+ JwtAlg::HS256 => JwtAlgorithm::Hs256,
79
+ JwtAlg::HS384 => JwtAlgorithm::Hs384,
80
+ JwtAlg::HS512 => JwtAlgorithm::Hs512,
81
+ JwtAlg::RS256 => JwtAlgorithm::Rs256,
82
+ JwtAlg::RS384 => JwtAlgorithm::Rs384,
83
+ JwtAlg::RS512 => JwtAlgorithm::Rs512,
84
+ JwtAlg::ES256 => JwtAlgorithm::Es256,
85
+ JwtAlg::ES384 => JwtAlgorithm::Es384,
86
+ JwtAlg::PS256 => JwtAlgorithm::Ps256,
87
+ JwtAlg::PS384 => JwtAlgorithm::Ps384,
88
+ JwtAlg::PS512 => JwtAlgorithm::Ps512,
89
+ _ => panic!("Unsupported algorithm"),
90
+ }
91
+ }
92
+ }
93
+
94
+ impl JwtAlgorithm {
95
+ /// Given a base64-encoded key string, decode and construct a jsonwebtoken::DecodingKey.
96
+ pub fn key_from(&self, base64: &str) -> itsi_error::Result<DecodingKey> {
97
+ match self {
98
+ // For HMAC algorithms, expect a base64 encoded secret.
99
+ JwtAlgorithm::Hs256 | JwtAlgorithm::Hs384 | JwtAlgorithm::Hs512 => {
100
+ Ok(DecodingKey::from_secret(
101
+ &general_purpose::STANDARD
102
+ .decode(base64)
103
+ .map_err(ItsiError::new)?,
104
+ ))
105
+ }
106
+ // For RSA (and PS) algorithms, expect a PEM-formatted key.
107
+ JwtAlgorithm::Rs256
108
+ | JwtAlgorithm::Rs384
109
+ | JwtAlgorithm::Rs512
110
+ | JwtAlgorithm::Ps256
111
+ | JwtAlgorithm::Ps384
112
+ | JwtAlgorithm::Ps512 => DecodingKey::from_rsa_pem(base64.trim_ascii().as_bytes())
113
+ .map_err(|e| ItsiError::new(e.to_string())),
114
+ // For ECDSA algorithms, expect a PEM-formatted key.
115
+ JwtAlgorithm::Es256 | JwtAlgorithm::Es384 => {
116
+ DecodingKey::from_ec_pem(base64.trim_ascii().as_bytes())
117
+ .map_err(|e| ItsiError::new(e.to_string()))
118
+ }
119
+ }
120
+ }
121
+ }
122
+
123
+ #[derive(Debug, Deserialize)]
124
+ #[serde(untagged)]
125
+ #[allow(dead_code)]
126
+ enum Audience {
127
+ Single(String),
128
+ Multiple(Vec<String>),
129
+ }
130
+
131
+ #[derive(Debug, Deserialize)]
132
+ #[allow(dead_code)]
133
+ struct Claims {
134
+ // Here we assume the token includes an expiration.
135
+ exp: usize,
136
+ // The audience claim may be a single string or an array.
137
+ aud: Option<Audience>,
138
+ sub: Option<String>,
139
+ iss: Option<String>,
140
+ }
141
+
142
+ #[async_trait]
143
+ impl MiddlewareLayer for AuthJwt {
144
+ async fn initialize(&self) -> Result<()> {
145
+ debug!(
146
+ target: "middleware::auth_jwt",
147
+ "Instantiating auth_jwt with {} verifiers", self.verifiers.len()
148
+ );
149
+
150
+ let keys: HashMap<JwtAlgorithm, Vec<DecodingKey>> = self
151
+ .verifiers
152
+ .iter()
153
+ .map(|(algorithm, key_strings)| {
154
+ let algo = algorithm.clone();
155
+ let keys: itsi_error::Result<Vec<DecodingKey>> = key_strings
156
+ .iter()
157
+ .map(|key_string| algorithm.key_from(key_string))
158
+ .inspect(|key_result| {
159
+ if key_result.is_err() {
160
+ debug!(
161
+ target: "middleware::auth_jwt",
162
+ "Failed to load key for algorithm {:?}", algorithm
163
+ )
164
+ } else {
165
+ debug!(
166
+ target: "middleware::auth_jwt",
167
+ "Loaded key for algorithm {:?}", algorithm
168
+ )
169
+ }
170
+ })
171
+ .collect();
172
+ keys.map(|keys| (algo, keys))
173
+ })
174
+ .collect::<itsi_error::Result<HashMap<JwtAlgorithm, Vec<DecodingKey>>>>()?;
175
+
176
+ self.keys
177
+ .set(keys)
178
+ .map_err(|_| ItsiError::new("Failed to set keys"))?;
179
+
180
+ if let Some(audiences) = self.audiences.as_ref() {
181
+ self.audience_vec
182
+ .set(Some(audiences.iter().cloned().collect::<Vec<_>>()))
183
+ .ok();
184
+ }
185
+ if let Some(issuers) = self.issuers.as_ref() {
186
+ self.issuer_vec
187
+ .set(Some(issuers.iter().cloned().collect::<Vec<_>>()))
188
+ .ok();
189
+ }
190
+ Ok(())
191
+ }
192
+
193
+ async fn before(
194
+ &self,
195
+ req: HttpRequest,
196
+ _: &mut HttpRequestContext,
197
+ ) -> Result<Either<HttpRequest, HttpResponse>> {
198
+ // Retrieve the JWT token from either a header or a query parameter.
199
+ let token_str = match &self.token_source {
200
+ TokenSource::Header { name, prefix } => {
201
+ debug!(
202
+ target: "middleware::auth_jwt",
203
+ "Extracting JWT from header: {}, prefix: {:?}",
204
+ name, prefix
205
+ );
206
+ if let Some(header) = req.header(name) {
207
+ if let Some(prefix) = prefix {
208
+ Some(header.strip_prefix(prefix).unwrap_or("").trim_ascii())
209
+ } else {
210
+ Some(header.trim_ascii())
211
+ }
212
+ } else {
213
+ None
214
+ }
215
+ }
216
+ TokenSource::Query(query_name) => {
217
+ debug!(
218
+ target: "middleware::auth_jwt",
219
+ "Extracting JWT from query parameter: {}",
220
+ query_name
221
+ );
222
+ req.query_param(query_name)
223
+ }
224
+ };
225
+
226
+ if token_str.is_none() {
227
+ debug!(
228
+ target: "middleware::auth_jwt",
229
+ "No JWT found in headers or query parameters"
230
+ );
231
+ return Ok(Either::Right(
232
+ self.error_response
233
+ .to_http_response(req.accept().into())
234
+ .await,
235
+ ));
236
+ }
237
+ let token_str = token_str.unwrap();
238
+ let header = match decode_header(token_str) {
239
+ Ok(header) => header,
240
+ Err(_) => {
241
+ debug!(target: "middleware::auth_jwt", "JWT decoding failed");
242
+ return Ok(Either::Right(
243
+ self.error_response
244
+ .to_http_response(req.accept().into())
245
+ .await,
246
+ ));
247
+ }
248
+ };
249
+
250
+ let alg: JwtAlgorithm = header.alg.into();
251
+
252
+ debug!(
253
+ target: "middleware::auth_jwt",
254
+ "Matched algorithm {:?}", alg
255
+ );
256
+ if !self.verifiers.contains_key(&alg) {
257
+ return Ok(Either::Right(
258
+ self.error_response
259
+ .to_http_response(req.accept().into())
260
+ .await,
261
+ ));
262
+ }
263
+ let keys = self.keys.get().unwrap().get(&alg).unwrap();
264
+
265
+ // Build validation based on the algorithm and optional leeway.
266
+ let mut validation = Validation::new(match alg {
267
+ JwtAlgorithm::Hs256 => JwtAlg::HS256,
268
+ JwtAlgorithm::Hs384 => JwtAlg::HS384,
269
+ JwtAlgorithm::Hs512 => JwtAlg::HS512,
270
+ JwtAlgorithm::Rs256 => JwtAlg::RS256,
271
+ JwtAlgorithm::Rs384 => JwtAlg::RS384,
272
+ JwtAlgorithm::Rs512 => JwtAlg::RS512,
273
+ JwtAlgorithm::Es256 => JwtAlg::ES256,
274
+ JwtAlgorithm::Es384 => JwtAlg::ES384,
275
+ JwtAlgorithm::Ps256 => JwtAlg::PS256,
276
+ JwtAlgorithm::Ps384 => JwtAlg::PS384,
277
+ JwtAlgorithm::Ps512 => JwtAlg::PS512,
278
+ });
279
+
280
+ if let Some(leeway) = self.leeway {
281
+ validation.leeway = leeway;
282
+ }
283
+
284
+ if let Some(Some(auds)) = &self.audience_vec.get() {
285
+ validation.set_audience(auds);
286
+ validation.required_spec_claims.insert("aud".to_owned());
287
+ } else {
288
+ validation.validate_aud = false;
289
+ }
290
+
291
+ if let Some(Some(issuers)) = &self.issuer_vec.get() {
292
+ validation.set_issuer(issuers);
293
+ validation.required_spec_claims.insert("iss".to_owned());
294
+ }
295
+
296
+ if self.subjects.is_some() {
297
+ validation.required_spec_claims.insert("sub".to_owned());
298
+ }
299
+
300
+ let token_data: Option<TokenData<Claims>> =
301
+ keys.iter()
302
+ .find_map(|key| match decode::<Claims>(token_str, key, &validation) {
303
+ Ok(data) => Some(data),
304
+ Err(e) => {
305
+ debug!("Token validation failed: {:?}", e);
306
+ None
307
+ }
308
+ });
309
+
310
+ let token_data = if let Some(data) = token_data {
311
+ data
312
+ } else {
313
+ return Ok(Either::Right(
314
+ self.error_response
315
+ .to_http_response(req.accept().into())
316
+ .await,
317
+ ));
318
+ };
319
+
320
+ let claims = token_data.claims;
321
+
322
+ if let Some(expected_subjects) = &self.subjects {
323
+ if let Some(sub) = &claims.sub {
324
+ if !expected_subjects.contains(sub) {
325
+ debug!(
326
+ target: "middleware::auth_jwt",
327
+ "SUB check failed, token_sub: {:?}, expected_subjects: {:?}",
328
+ sub, expected_subjects
329
+ );
330
+ return Ok(Either::Right(
331
+ self.error_response
332
+ .to_http_response(req.accept().into())
333
+ .await,
334
+ ));
335
+ }
336
+ }
337
+ }
338
+
339
+ Ok(Either::Left(req))
340
+ }
341
+ }
342
+
343
+ impl FromValue for AuthJwt {}