itsi-scheduler 0.1.5 → 0.1.19
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.
Potentially problematic release.
This version of itsi-scheduler might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CODE_OF_CONDUCT.md +7 -0
- data/Cargo.lock +90 -22
- data/README.md +5 -0
- data/_index.md +7 -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 +59 -9
- 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/src/itsi_scheduler.rs +1 -1
- data/ext/itsi_server/Cargo.lock +2956 -0
- data/ext/itsi_server/Cargo.toml +72 -28
- 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 +113 -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 +345 -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 +225 -0
- data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +375 -0
- data/ext/itsi_server/src/ruby_types/itsi_server.rs +83 -0
- data/ext/itsi_server/src/ruby_types/mod.rs +48 -0
- data/ext/itsi_server/src/server/{bind.rs → binds/bind.rs} +56 -24
- data/ext/itsi_server/src/server/{listener.rs → binds/listener.rs} +218 -113
- data/ext/itsi_server/src/server/binds/mod.rs +4 -0
- data/ext/itsi_server/src/server/{tls → binds/tls}/locked_dir_cache.rs +55 -17
- data/ext/itsi_server/src/server/{tls.rs → binds/tls.rs} +109 -28
- 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 +165 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +56 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +87 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +86 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +285 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +142 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +289 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +292 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +55 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +190 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +157 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +195 -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 +201 -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 +87 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +414 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +131 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +76 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +44 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +36 -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 +180 -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 +163 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +12 -0
- data/ext/itsi_server/src/server/middleware_stack/mod.rs +347 -0
- data/ext/itsi_server/src/server/mod.rs +6 -5
- data/ext/itsi_server/src/server/process_worker.rs +65 -14
- data/ext/itsi_server/src/server/request_job.rs +11 -0
- data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +137 -49
- data/ext/itsi_server/src/server/serve_strategy/mod.rs +9 -6
- data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +338 -164
- data/ext/itsi_server/src/server/signal.rs +32 -26
- data/ext/itsi_server/src/server/size_limited_incoming.rs +101 -0
- data/ext/itsi_server/src/server/thread_worker.rs +214 -107
- data/ext/itsi_server/src/services/cache_store.rs +74 -0
- data/ext/itsi_server/src/services/itsi_http_service.rs +239 -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 +569 -0
- data/ext/itsi_server/src/services/static_file_server.rs +1324 -0
- data/ext/itsi_tracing/Cargo.toml +1 -0
- data/ext/itsi_tracing/src/lib.rs +312 -34
- 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/lib/itsi/scheduler/version.rb +1 -1
- data/lib/itsi/scheduler.rb +2 -2
- metadata +93 -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/{bind_protocol.rs → binds/bind_protocol.rs} +0 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
use crate::{
|
2
|
+
server::http_message_types::{HttpRequest, HttpResponse, RequestExt},
|
3
|
+
services::itsi_http_service::HttpRequestContext,
|
4
|
+
};
|
5
|
+
|
6
|
+
use super::{ErrorResponse, FromValue, MiddlewareLayer};
|
7
|
+
|
8
|
+
use async_trait::async_trait;
|
9
|
+
use either::Either;
|
10
|
+
use itsi_error::ItsiError;
|
11
|
+
use magnus::error::Result;
|
12
|
+
use regex::RegexSet;
|
13
|
+
use serde::Deserialize;
|
14
|
+
use std::sync::OnceLock;
|
15
|
+
|
16
|
+
#[derive(Debug, Clone, Deserialize)]
|
17
|
+
pub struct AllowList {
|
18
|
+
#[serde(skip_deserializing)]
|
19
|
+
pub allowed_ips: OnceLock<RegexSet>,
|
20
|
+
pub allowed_patterns: Vec<String>,
|
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
|
+
if !allowed_ips.is_match(&context.addr) {
|
46
|
+
return Ok(Either::Right(
|
47
|
+
self.error_response
|
48
|
+
.to_http_response(req.accept().into())
|
49
|
+
.await,
|
50
|
+
));
|
51
|
+
}
|
52
|
+
}
|
53
|
+
Ok(Either::Left(req))
|
54
|
+
}
|
55
|
+
}
|
56
|
+
impl FromValue for AllowList {}
|
@@ -0,0 +1,87 @@
|
|
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
|
+
|
15
|
+
type PasswordHash = String;
|
16
|
+
|
17
|
+
/// A simple API key filter.
|
18
|
+
/// The API key can be given inside the header or a query string
|
19
|
+
/// Keys are validated against a list of allowed key values (Changing these requires a restart)
|
20
|
+
#[derive(Debug, Clone, Deserialize)]
|
21
|
+
pub struct AuthAPIKey {
|
22
|
+
pub valid_keys: HashMap<String, PasswordHash>,
|
23
|
+
pub key_id_source: Option<TokenSource>,
|
24
|
+
pub token_source: TokenSource,
|
25
|
+
#[serde(default = "unauthorized_error_response")]
|
26
|
+
pub error_response: ErrorResponse,
|
27
|
+
}
|
28
|
+
|
29
|
+
fn unauthorized_error_response() -> ErrorResponse {
|
30
|
+
ErrorResponse::unauthorized()
|
31
|
+
}
|
32
|
+
|
33
|
+
#[async_trait]
|
34
|
+
impl MiddlewareLayer for AuthAPIKey {
|
35
|
+
async fn before(
|
36
|
+
&self,
|
37
|
+
req: HttpRequest,
|
38
|
+
_context: &mut HttpRequestContext,
|
39
|
+
) -> Result<Either<HttpRequest, HttpResponse>> {
|
40
|
+
if let Some(submitted_key) = match &self.token_source {
|
41
|
+
TokenSource::Header { name, prefix } => {
|
42
|
+
if let Some(header) = req.header(name) {
|
43
|
+
if let Some(prefix) = prefix {
|
44
|
+
Some(header.strip_prefix(prefix).unwrap_or("").trim_ascii())
|
45
|
+
} else {
|
46
|
+
Some(header.trim_ascii())
|
47
|
+
}
|
48
|
+
} else {
|
49
|
+
None
|
50
|
+
}
|
51
|
+
}
|
52
|
+
TokenSource::Query(query_name) => req.query_param(query_name),
|
53
|
+
} {
|
54
|
+
if let Some(key_id) = self.key_id_source.as_ref() {
|
55
|
+
let key_id = match &key_id {
|
56
|
+
TokenSource::Header { name, prefix } => {
|
57
|
+
if let Some(header) = req.header(name) {
|
58
|
+
if let Some(prefix) = prefix {
|
59
|
+
Some(header.strip_prefix(prefix).unwrap_or("").trim_ascii())
|
60
|
+
} else {
|
61
|
+
Some(header.trim_ascii())
|
62
|
+
}
|
63
|
+
} else {
|
64
|
+
None
|
65
|
+
}
|
66
|
+
}
|
67
|
+
TokenSource::Query(query_name) => req.query_param(query_name),
|
68
|
+
};
|
69
|
+
if let Some(hash) = key_id.and_then(|kid| self.valid_keys.get(kid)) {
|
70
|
+
if password_hasher::verify_password_hash(submitted_key, hash).is_ok_and(|v| v) {
|
71
|
+
return Ok(Either::Left(req));
|
72
|
+
}
|
73
|
+
}
|
74
|
+
} else if self.valid_keys.iter().any(|(_key_id, key)| {
|
75
|
+
password_hasher::verify_password_hash(submitted_key, key).is_ok_and(|v| v)
|
76
|
+
}) {
|
77
|
+
return Ok(Either::Left(req));
|
78
|
+
}
|
79
|
+
}
|
80
|
+
Ok(Either::Right(
|
81
|
+
self.error_response
|
82
|
+
.to_http_response(req.accept().into())
|
83
|
+
.await,
|
84
|
+
))
|
85
|
+
}
|
86
|
+
}
|
87
|
+
impl FromValue for AuthAPIKey {}
|
@@ -0,0 +1,86 @@
|
|
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
|
+
|
12
|
+
use crate::{
|
13
|
+
server::http_message_types::{HttpRequest, HttpResponse, RequestExt},
|
14
|
+
services::{itsi_http_service::HttpRequestContext, password_hasher::verify_password_hash},
|
15
|
+
};
|
16
|
+
|
17
|
+
use super::{FromValue, MiddlewareLayer};
|
18
|
+
|
19
|
+
type PasswordHash = String;
|
20
|
+
|
21
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
22
|
+
pub struct AuthBasic {
|
23
|
+
pub realm: String,
|
24
|
+
/// Maps usernames to passwords.
|
25
|
+
pub credential_pairs: HashMap<String, PasswordHash>,
|
26
|
+
}
|
27
|
+
|
28
|
+
impl AuthBasic {
|
29
|
+
fn basic_auth_failed_response(&self) -> HttpResponse {
|
30
|
+
Response::builder()
|
31
|
+
.status(StatusCode::UNAUTHORIZED)
|
32
|
+
.header(
|
33
|
+
"WWW-Authenticate",
|
34
|
+
format!("Basic realm=\"{}\"", self.realm),
|
35
|
+
)
|
36
|
+
.body(BoxBody::new(Full::new(Bytes::from("Unauthorized"))))
|
37
|
+
.unwrap()
|
38
|
+
}
|
39
|
+
}
|
40
|
+
#[async_trait]
|
41
|
+
impl MiddlewareLayer for AuthBasic {
|
42
|
+
async fn before(
|
43
|
+
&self,
|
44
|
+
req: HttpRequest,
|
45
|
+
_context: &mut HttpRequestContext,
|
46
|
+
) -> Result<Either<HttpRequest, HttpResponse>> {
|
47
|
+
// Retrieve the Authorization header.
|
48
|
+
let auth_header = req.header("Authorization");
|
49
|
+
|
50
|
+
if !auth_header.is_some_and(|header| header.starts_with("Basic ")) {
|
51
|
+
return Ok(Either::Right(self.basic_auth_failed_response()));
|
52
|
+
}
|
53
|
+
|
54
|
+
let auth_header = auth_header.unwrap();
|
55
|
+
|
56
|
+
let encoded_credentials = &auth_header["Basic ".len()..];
|
57
|
+
let decoded = match general_purpose::STANDARD.decode(encoded_credentials) {
|
58
|
+
Ok(bytes) => bytes,
|
59
|
+
Err(_) => {
|
60
|
+
return Ok(Either::Right(self.basic_auth_failed_response()));
|
61
|
+
}
|
62
|
+
};
|
63
|
+
|
64
|
+
let decoded_str = match str::from_utf8(&decoded) {
|
65
|
+
Ok(s) => s,
|
66
|
+
Err(_) => {
|
67
|
+
return Ok(Either::Right(self.basic_auth_failed_response()));
|
68
|
+
}
|
69
|
+
};
|
70
|
+
|
71
|
+
let mut parts = decoded_str.splitn(2, ':');
|
72
|
+
let username = parts.next().unwrap_or("");
|
73
|
+
let password = parts.next().unwrap_or("");
|
74
|
+
match self.credential_pairs.get(username) {
|
75
|
+
Some(expected_password_hash) => {
|
76
|
+
match verify_password_hash(password, expected_password_hash) {
|
77
|
+
Ok(true) => Ok(Either::Left(req)),
|
78
|
+
_ => Ok(Either::Right(self.basic_auth_failed_response())),
|
79
|
+
}
|
80
|
+
}
|
81
|
+
None => Ok(Either::Right(self.basic_auth_failed_response())),
|
82
|
+
}
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
86
|
+
impl FromValue for AuthBasic {}
|
@@ -0,0 +1,285 @@
|
|
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::error;
|
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
|
+
pub leeway: Option<u64>,
|
36
|
+
#[serde(default = "unauthorized_error_response")]
|
37
|
+
pub error_response: ErrorResponse,
|
38
|
+
}
|
39
|
+
|
40
|
+
fn unauthorized_error_response() -> ErrorResponse {
|
41
|
+
ErrorResponse::unauthorized()
|
42
|
+
}
|
43
|
+
|
44
|
+
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Hash)]
|
45
|
+
pub enum JwtAlgorithm {
|
46
|
+
#[serde(rename(deserialize = "hs256"))]
|
47
|
+
Hs256,
|
48
|
+
#[serde(rename(deserialize = "hs384"))]
|
49
|
+
Hs384,
|
50
|
+
#[serde(rename(deserialize = "hs512"))]
|
51
|
+
Hs512,
|
52
|
+
#[serde(rename(deserialize = "rs256"))]
|
53
|
+
Rs256,
|
54
|
+
#[serde(rename(deserialize = "rs384"))]
|
55
|
+
Rs384,
|
56
|
+
#[serde(rename(deserialize = "rs512"))]
|
57
|
+
Rs512,
|
58
|
+
#[serde(rename(deserialize = "es256"))]
|
59
|
+
Es256,
|
60
|
+
#[serde(rename(deserialize = "es384"))]
|
61
|
+
Es384,
|
62
|
+
#[serde(rename(deserialize = "ps256"))]
|
63
|
+
Ps256,
|
64
|
+
#[serde(rename(deserialize = "ps384"))]
|
65
|
+
Ps384,
|
66
|
+
#[serde(rename(deserialize = "ps512"))]
|
67
|
+
Ps512,
|
68
|
+
}
|
69
|
+
|
70
|
+
// Allow conversion from jsonwebtoken’s Algorithm to our JwtAlgorithm.
|
71
|
+
impl From<JwtAlg> for JwtAlgorithm {
|
72
|
+
fn from(alg: JwtAlg) -> Self {
|
73
|
+
match alg {
|
74
|
+
JwtAlg::HS256 => JwtAlgorithm::Hs256,
|
75
|
+
JwtAlg::HS384 => JwtAlgorithm::Hs384,
|
76
|
+
JwtAlg::HS512 => JwtAlgorithm::Hs512,
|
77
|
+
JwtAlg::RS256 => JwtAlgorithm::Rs256,
|
78
|
+
JwtAlg::RS384 => JwtAlgorithm::Rs384,
|
79
|
+
JwtAlg::RS512 => JwtAlgorithm::Rs512,
|
80
|
+
JwtAlg::ES256 => JwtAlgorithm::Es256,
|
81
|
+
JwtAlg::ES384 => JwtAlgorithm::Es384,
|
82
|
+
JwtAlg::PS256 => JwtAlgorithm::Ps256,
|
83
|
+
JwtAlg::PS384 => JwtAlgorithm::Ps384,
|
84
|
+
JwtAlg::PS512 => JwtAlgorithm::Ps512,
|
85
|
+
_ => panic!("Unsupported algorithm"),
|
86
|
+
}
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
impl JwtAlgorithm {
|
91
|
+
/// Given a base64-encoded key string, decode and construct a jsonwebtoken::DecodingKey.
|
92
|
+
pub fn key_from(&self, base64: &str) -> itsi_error::Result<DecodingKey> {
|
93
|
+
match self {
|
94
|
+
// For HMAC algorithms, use the secret directly.
|
95
|
+
JwtAlgorithm::Hs256 | JwtAlgorithm::Hs384 | JwtAlgorithm::Hs512 => {
|
96
|
+
Ok(DecodingKey::from_secret(
|
97
|
+
&general_purpose::STANDARD
|
98
|
+
.decode(base64)
|
99
|
+
.map_err(ItsiError::new)?,
|
100
|
+
))
|
101
|
+
}
|
102
|
+
// For RSA (and PS) algorithms, expect a PEM-formatted key.
|
103
|
+
JwtAlgorithm::Rs256
|
104
|
+
| JwtAlgorithm::Rs384
|
105
|
+
| JwtAlgorithm::Rs512
|
106
|
+
| JwtAlgorithm::Ps256
|
107
|
+
| JwtAlgorithm::Ps384
|
108
|
+
| JwtAlgorithm::Ps512 => DecodingKey::from_rsa_pem(base64.trim_ascii().as_bytes())
|
109
|
+
.map_err(|e| ItsiError::new(e.to_string())),
|
110
|
+
// For ECDSA algorithms, expect a PEM-formatted key.
|
111
|
+
JwtAlgorithm::Es256 | JwtAlgorithm::Es384 => {
|
112
|
+
DecodingKey::from_ec_pem(base64.trim_ascii().as_bytes())
|
113
|
+
.map_err(|e| ItsiError::new(e.to_string()))
|
114
|
+
}
|
115
|
+
}
|
116
|
+
}
|
117
|
+
}
|
118
|
+
|
119
|
+
#[derive(Debug, Deserialize)]
|
120
|
+
#[serde(untagged)]
|
121
|
+
enum Audience {
|
122
|
+
Single(String),
|
123
|
+
Multiple(Vec<String>),
|
124
|
+
}
|
125
|
+
|
126
|
+
#[derive(Debug, Deserialize)]
|
127
|
+
struct Claims {
|
128
|
+
// Here we assume the token includes an expiration.
|
129
|
+
#[allow(dead_code)]
|
130
|
+
exp: usize,
|
131
|
+
// The audience claim may be a single string or an array.
|
132
|
+
aud: Option<Audience>,
|
133
|
+
sub: Option<String>,
|
134
|
+
iss: Option<String>,
|
135
|
+
}
|
136
|
+
|
137
|
+
#[async_trait]
|
138
|
+
impl MiddlewareLayer for AuthJwt {
|
139
|
+
async fn initialize(&self) -> Result<()> {
|
140
|
+
let keys: HashMap<JwtAlgorithm, Vec<DecodingKey>> = self
|
141
|
+
.verifiers
|
142
|
+
.iter()
|
143
|
+
.map(|(algorithm, key_strings)| {
|
144
|
+
let algo = algorithm.clone();
|
145
|
+
let keys: itsi_error::Result<Vec<DecodingKey>> = key_strings
|
146
|
+
.iter()
|
147
|
+
.map(|key_string| algorithm.key_from(key_string))
|
148
|
+
.collect();
|
149
|
+
keys.map(|keys| (algo, keys))
|
150
|
+
})
|
151
|
+
.collect::<itsi_error::Result<HashMap<JwtAlgorithm, Vec<DecodingKey>>>>()?;
|
152
|
+
self.keys
|
153
|
+
.set(keys)
|
154
|
+
.map_err(|_| ItsiError::new("Failed to set keys"))?;
|
155
|
+
Ok(())
|
156
|
+
}
|
157
|
+
|
158
|
+
async fn before(
|
159
|
+
&self,
|
160
|
+
req: HttpRequest,
|
161
|
+
_context: &mut HttpRequestContext,
|
162
|
+
) -> Result<Either<HttpRequest, HttpResponse>> {
|
163
|
+
// Retrieve the JWT token from either a header or a query parameter.
|
164
|
+
let token_str = match &self.token_source {
|
165
|
+
TokenSource::Header { name, prefix } => {
|
166
|
+
if let Some(header) = req.header(name) {
|
167
|
+
if let Some(prefix) = prefix {
|
168
|
+
Some(header.strip_prefix(prefix).unwrap_or("").trim_ascii())
|
169
|
+
} else {
|
170
|
+
Some(header.trim_ascii())
|
171
|
+
}
|
172
|
+
} else {
|
173
|
+
None
|
174
|
+
}
|
175
|
+
}
|
176
|
+
TokenSource::Query(query_name) => req.query_param(query_name),
|
177
|
+
};
|
178
|
+
|
179
|
+
if token_str.is_none() {
|
180
|
+
return Ok(Either::Right(
|
181
|
+
self.error_response
|
182
|
+
.to_http_response(req.accept().into())
|
183
|
+
.await,
|
184
|
+
));
|
185
|
+
}
|
186
|
+
let token_str = token_str.unwrap();
|
187
|
+
let header =
|
188
|
+
decode_header(token_str).map_err(|_| ItsiError::new("Invalid token header"))?;
|
189
|
+
let alg: JwtAlgorithm = header.alg.into();
|
190
|
+
|
191
|
+
if !self.verifiers.contains_key(&alg) {
|
192
|
+
return Ok(Either::Right(
|
193
|
+
self.error_response
|
194
|
+
.to_http_response(req.accept().into())
|
195
|
+
.await,
|
196
|
+
));
|
197
|
+
}
|
198
|
+
let keys = self.keys.get().unwrap().get(&alg).unwrap();
|
199
|
+
|
200
|
+
// Build validation based on the algorithm and optional leeway.
|
201
|
+
let mut validation = Validation::new(match alg {
|
202
|
+
JwtAlgorithm::Hs256 => JwtAlg::HS256,
|
203
|
+
JwtAlgorithm::Hs384 => JwtAlg::HS384,
|
204
|
+
JwtAlgorithm::Hs512 => JwtAlg::HS512,
|
205
|
+
JwtAlgorithm::Rs256 => JwtAlg::RS256,
|
206
|
+
JwtAlgorithm::Rs384 => JwtAlg::RS384,
|
207
|
+
JwtAlgorithm::Rs512 => JwtAlg::RS512,
|
208
|
+
JwtAlgorithm::Es256 => JwtAlg::ES256,
|
209
|
+
JwtAlgorithm::Es384 => JwtAlg::ES384,
|
210
|
+
JwtAlgorithm::Ps256 => JwtAlg::PS256,
|
211
|
+
JwtAlgorithm::Ps384 => JwtAlg::PS384,
|
212
|
+
JwtAlgorithm::Ps512 => JwtAlg::PS512,
|
213
|
+
});
|
214
|
+
|
215
|
+
if let Some(leeway) = self.leeway {
|
216
|
+
validation.leeway = leeway;
|
217
|
+
}
|
218
|
+
|
219
|
+
let token_data: Option<TokenData<Claims>> =
|
220
|
+
keys.iter()
|
221
|
+
.find_map(|key| match decode::<Claims>(token_str, key, &validation) {
|
222
|
+
Ok(data) => Some(data),
|
223
|
+
Err(e) => {
|
224
|
+
error!("Token validation failed: {:?}", e);
|
225
|
+
None
|
226
|
+
}
|
227
|
+
});
|
228
|
+
let token_data = if let Some(data) = token_data {
|
229
|
+
data
|
230
|
+
} else {
|
231
|
+
return Ok(Either::Right(
|
232
|
+
self.error_response
|
233
|
+
.to_http_response(req.accept().into())
|
234
|
+
.await,
|
235
|
+
));
|
236
|
+
};
|
237
|
+
|
238
|
+
let claims = token_data.claims;
|
239
|
+
|
240
|
+
if let Some(expected_audiences) = &self.audiences {
|
241
|
+
if let Some(aud) = &claims.aud {
|
242
|
+
let token_auds: HashSet<String> = match aud {
|
243
|
+
Audience::Single(s) => [s.clone()].into_iter().collect(),
|
244
|
+
Audience::Multiple(v) => v.iter().cloned().collect(),
|
245
|
+
};
|
246
|
+
if expected_audiences.is_disjoint(&token_auds) {
|
247
|
+
return Ok(Either::Right(
|
248
|
+
self.error_response
|
249
|
+
.to_http_response(req.accept().into())
|
250
|
+
.await,
|
251
|
+
));
|
252
|
+
}
|
253
|
+
}
|
254
|
+
}
|
255
|
+
|
256
|
+
if let Some(expected_subjects) = &self.subjects {
|
257
|
+
if let Some(sub) = &claims.sub {
|
258
|
+
if !expected_subjects.contains(sub) {
|
259
|
+
return Ok(Either::Right(
|
260
|
+
self.error_response
|
261
|
+
.to_http_response(req.accept().into())
|
262
|
+
.await,
|
263
|
+
));
|
264
|
+
}
|
265
|
+
}
|
266
|
+
}
|
267
|
+
|
268
|
+
// Verify expected issuer.
|
269
|
+
if let Some(expected_issuers) = &self.issuers {
|
270
|
+
if let Some(iss) = &claims.iss {
|
271
|
+
if !expected_issuers.contains(iss) {
|
272
|
+
return Ok(Either::Right(
|
273
|
+
self.error_response
|
274
|
+
.to_http_response(req.accept().into())
|
275
|
+
.await,
|
276
|
+
));
|
277
|
+
}
|
278
|
+
}
|
279
|
+
}
|
280
|
+
|
281
|
+
Ok(Either::Left(req))
|
282
|
+
}
|
283
|
+
}
|
284
|
+
|
285
|
+
impl FromValue for AuthJwt {}
|
@@ -0,0 +1,142 @@
|
|
1
|
+
use crate::{
|
2
|
+
server::http_message_types::HttpResponse, services::itsi_http_service::HttpRequestContext,
|
3
|
+
};
|
4
|
+
|
5
|
+
use super::{FromValue, MiddlewareLayer};
|
6
|
+
use async_trait::async_trait;
|
7
|
+
use http::{HeaderName, HeaderValue};
|
8
|
+
use magnus::error::Result;
|
9
|
+
use serde::Deserialize;
|
10
|
+
use std::{collections::HashMap, sync::OnceLock};
|
11
|
+
|
12
|
+
#[derive(Debug, Clone, Deserialize)]
|
13
|
+
pub struct CacheControl {
|
14
|
+
#[serde(default)]
|
15
|
+
pub max_age: Option<u64>,
|
16
|
+
#[serde(default)]
|
17
|
+
pub s_max_age: Option<u64>,
|
18
|
+
#[serde(default)]
|
19
|
+
pub stale_while_revalidate: Option<u64>,
|
20
|
+
#[serde(default)]
|
21
|
+
pub stale_if_error: Option<u64>,
|
22
|
+
#[serde(default)]
|
23
|
+
pub public: bool,
|
24
|
+
#[serde(default)]
|
25
|
+
pub private: bool,
|
26
|
+
#[serde(default)]
|
27
|
+
pub no_cache: bool,
|
28
|
+
#[serde(default)]
|
29
|
+
pub no_store: bool,
|
30
|
+
#[serde(default)]
|
31
|
+
pub must_revalidate: bool,
|
32
|
+
#[serde(default)]
|
33
|
+
pub proxy_revalidate: bool,
|
34
|
+
#[serde(default)]
|
35
|
+
pub immutable: bool,
|
36
|
+
#[serde(default)]
|
37
|
+
pub vary: Vec<String>,
|
38
|
+
#[serde(default)]
|
39
|
+
pub additional_headers: HashMap<String, String>,
|
40
|
+
#[serde(skip_deserializing)]
|
41
|
+
pub cache_control_str: OnceLock<String>,
|
42
|
+
}
|
43
|
+
|
44
|
+
#[async_trait]
|
45
|
+
impl MiddlewareLayer for CacheControl {
|
46
|
+
async fn initialize(&self) -> Result<()> {
|
47
|
+
let mut directives = Vec::new();
|
48
|
+
|
49
|
+
if self.public && !self.private {
|
50
|
+
directives.push("public".to_owned());
|
51
|
+
} else if self.private && !self.public {
|
52
|
+
directives.push("private".to_owned());
|
53
|
+
}
|
54
|
+
if self.no_cache {
|
55
|
+
directives.push("no-cache".to_owned());
|
56
|
+
}
|
57
|
+
if self.no_store {
|
58
|
+
directives.push("no-store".to_owned());
|
59
|
+
}
|
60
|
+
if self.must_revalidate {
|
61
|
+
directives.push("must-revalidate".to_owned());
|
62
|
+
}
|
63
|
+
if self.proxy_revalidate {
|
64
|
+
directives.push("proxy-revalidate".to_owned());
|
65
|
+
}
|
66
|
+
if self.immutable {
|
67
|
+
directives.push("immutable".to_owned());
|
68
|
+
}
|
69
|
+
|
70
|
+
// Add age parameters
|
71
|
+
if let Some(max_age) = self.max_age {
|
72
|
+
directives.push(format!("max-age={}", max_age));
|
73
|
+
}
|
74
|
+
|
75
|
+
if let Some(s_max_age) = self.s_max_age {
|
76
|
+
directives.push(format!("s-maxage={}", s_max_age));
|
77
|
+
}
|
78
|
+
|
79
|
+
if let Some(stale_while_revalidate) = self.stale_while_revalidate {
|
80
|
+
directives.push(format!("stale-while-revalidate={}", stale_while_revalidate));
|
81
|
+
}
|
82
|
+
|
83
|
+
if let Some(stale_if_error) = self.stale_if_error {
|
84
|
+
directives.push(format!("stale-if-error={}", stale_if_error));
|
85
|
+
}
|
86
|
+
|
87
|
+
// Set the Cache-Control header if we have directives
|
88
|
+
if !directives.is_empty() {
|
89
|
+
let cache_control_value = directives.join(", ");
|
90
|
+
self.cache_control_str.set(cache_control_value).unwrap();
|
91
|
+
}
|
92
|
+
|
93
|
+
Ok(())
|
94
|
+
}
|
95
|
+
|
96
|
+
async fn after(&self, mut resp: HttpResponse, _: &mut HttpRequestContext) -> HttpResponse {
|
97
|
+
// Skip for statuses where caching doesn't make sense
|
98
|
+
let status = resp.status().as_u16();
|
99
|
+
if matches!(status, 401 | 403 | 500..=599) {
|
100
|
+
return resp;
|
101
|
+
}
|
102
|
+
|
103
|
+
// Set the Cache-Control header if we have directives
|
104
|
+
if let Some(cache_control_value) = self.cache_control_str.get() {
|
105
|
+
if let Ok(value) = HeaderValue::from_str(cache_control_value) {
|
106
|
+
resp.headers_mut().insert("Cache-Control", value);
|
107
|
+
}
|
108
|
+
}
|
109
|
+
|
110
|
+
// Set Expires header based on max-age if present
|
111
|
+
if let Some(max_age) = self.max_age {
|
112
|
+
// Set the Expires header based on max-age
|
113
|
+
// Use a helper to format the HTTP date correctly
|
114
|
+
let expires = chrono::Utc::now() + chrono::Duration::seconds(max_age as i64);
|
115
|
+
let expires_str = expires.format("%a, %d %b %Y %H:%M:%S GMT").to_string();
|
116
|
+
if let Ok(value) = HeaderValue::from_str(&expires_str) {
|
117
|
+
resp.headers_mut().insert("Expires", value);
|
118
|
+
}
|
119
|
+
}
|
120
|
+
|
121
|
+
// Set Vary header
|
122
|
+
if !self.vary.is_empty() {
|
123
|
+
let vary_value = self.vary.join(", ");
|
124
|
+
if let Ok(value) = HeaderValue::from_str(&vary_value) {
|
125
|
+
resp.headers_mut().insert("Vary", value);
|
126
|
+
}
|
127
|
+
}
|
128
|
+
|
129
|
+
// Set additional custom headers
|
130
|
+
for (name, value) in &self.additional_headers {
|
131
|
+
if let Ok(header_value) = HeaderValue::from_str(value) {
|
132
|
+
if let Ok(header_name) = name.parse::<HeaderName>() {
|
133
|
+
resp.headers_mut().insert(header_name, header_value);
|
134
|
+
}
|
135
|
+
}
|
136
|
+
}
|
137
|
+
|
138
|
+
resp
|
139
|
+
}
|
140
|
+
}
|
141
|
+
|
142
|
+
impl FromValue for CacheControl {}
|