itsi-scheduler 0.1.5 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Cargo.lock +120 -52
- data/README.md +57 -24
- data/Rakefile +0 -4
- data/ext/itsi_acme/Cargo.toml +86 -0
- data/ext/itsi_acme/examples/high_level.rs +63 -0
- data/ext/itsi_acme/examples/high_level_warp.rs +52 -0
- data/ext/itsi_acme/examples/low_level.rs +87 -0
- data/ext/itsi_acme/examples/low_level_axum.rs +66 -0
- data/ext/itsi_acme/src/acceptor.rs +81 -0
- data/ext/itsi_acme/src/acme.rs +354 -0
- data/ext/itsi_acme/src/axum.rs +86 -0
- data/ext/itsi_acme/src/cache.rs +39 -0
- data/ext/itsi_acme/src/caches/boxed.rs +80 -0
- data/ext/itsi_acme/src/caches/composite.rs +69 -0
- data/ext/itsi_acme/src/caches/dir.rs +106 -0
- data/ext/itsi_acme/src/caches/mod.rs +11 -0
- data/ext/itsi_acme/src/caches/no.rs +78 -0
- data/ext/itsi_acme/src/caches/test.rs +136 -0
- data/ext/itsi_acme/src/config.rs +172 -0
- data/ext/itsi_acme/src/https_helper.rs +69 -0
- data/ext/itsi_acme/src/incoming.rs +142 -0
- data/ext/itsi_acme/src/jose.rs +161 -0
- data/ext/itsi_acme/src/lib.rs +142 -0
- data/ext/itsi_acme/src/resolver.rs +59 -0
- data/ext/itsi_acme/src/state.rs +424 -0
- data/ext/itsi_error/Cargo.toml +1 -0
- data/ext/itsi_error/src/lib.rs +106 -7
- data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
- data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
- data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
- data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
- data/ext/itsi_rb_helpers/Cargo.toml +1 -0
- data/ext/itsi_rb_helpers/src/heap_value.rs +18 -0
- data/ext/itsi_rb_helpers/src/lib.rs +63 -12
- data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
- data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
- data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
- data/ext/itsi_rb_helpers/target/debug/build/rb-sys-eb9ed4ff3a60f995/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
- data/ext/itsi_scheduler/Cargo.toml +1 -1
- data/ext/itsi_scheduler/src/itsi_scheduler.rs +9 -3
- data/ext/itsi_scheduler/src/lib.rs +1 -0
- data/ext/itsi_server/Cargo.lock +2956 -0
- data/ext/itsi_server/Cargo.toml +73 -29
- data/ext/itsi_server/src/default_responses/mod.rs +11 -0
- data/ext/itsi_server/src/env.rs +43 -0
- data/ext/itsi_server/src/lib.rs +114 -75
- data/ext/itsi_server/src/prelude.rs +2 -0
- data/ext/itsi_server/src/{body_proxy → ruby_types/itsi_body_proxy}/big_bytes.rs +10 -5
- data/ext/itsi_server/src/{body_proxy/itsi_body_proxy.rs → ruby_types/itsi_body_proxy/mod.rs} +29 -8
- data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
- data/ext/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +264 -0
- data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +362 -0
- data/ext/itsi_server/src/{response/itsi_response.rs → ruby_types/itsi_http_response.rs} +84 -40
- data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +233 -0
- data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +565 -0
- data/ext/itsi_server/src/ruby_types/itsi_server.rs +86 -0
- data/ext/itsi_server/src/ruby_types/mod.rs +48 -0
- data/ext/itsi_server/src/server/{bind.rs → binds/bind.rs} +59 -24
- data/ext/itsi_server/src/server/binds/listener.rs +444 -0
- data/ext/itsi_server/src/server/binds/mod.rs +4 -0
- data/ext/itsi_server/src/server/{tls → binds/tls}/locked_dir_cache.rs +57 -19
- data/ext/itsi_server/src/server/{tls.rs → binds/tls.rs} +120 -31
- data/ext/itsi_server/src/server/byte_frame.rs +32 -0
- data/ext/itsi_server/src/server/http_message_types.rs +97 -0
- data/ext/itsi_server/src/server/io_stream.rs +2 -1
- data/ext/itsi_server/src/server/lifecycle_event.rs +3 -0
- data/ext/itsi_server/src/server/middleware_stack/middleware.rs +170 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +63 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +94 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +94 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +343 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +151 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +316 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +301 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/csp.rs +193 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +64 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +192 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +171 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +198 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +82 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +209 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +116 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +411 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +142 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +55 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +54 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +51 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +126 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +187 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +173 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +31 -0
- data/ext/itsi_server/src/server/middleware_stack/mod.rs +381 -0
- data/ext/itsi_server/src/server/mod.rs +7 -5
- data/ext/itsi_server/src/server/process_worker.rs +65 -14
- data/ext/itsi_server/src/server/redirect_type.rs +26 -0
- data/ext/itsi_server/src/server/request_job.rs +11 -0
- data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +150 -50
- data/ext/itsi_server/src/server/serve_strategy/mod.rs +9 -6
- data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +399 -165
- data/ext/itsi_server/src/server/signal.rs +33 -26
- data/ext/itsi_server/src/server/size_limited_incoming.rs +107 -0
- data/ext/itsi_server/src/server/thread_worker.rs +218 -107
- data/ext/itsi_server/src/services/cache_store.rs +74 -0
- data/ext/itsi_server/src/services/itsi_http_service.rs +257 -0
- data/ext/itsi_server/src/services/mime_types.rs +1416 -0
- data/ext/itsi_server/src/services/mod.rs +6 -0
- data/ext/itsi_server/src/services/password_hasher.rs +83 -0
- data/ext/itsi_server/src/services/rate_limiter.rs +580 -0
- data/ext/itsi_server/src/services/static_file_server.rs +1340 -0
- data/ext/itsi_tracing/Cargo.toml +1 -0
- data/ext/itsi_tracing/src/lib.rs +362 -33
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
- data/itsi-scheduler-100.png +0 -0
- data/lib/itsi/scheduler/version.rb +1 -1
- data/lib/itsi/scheduler.rb +11 -6
- metadata +117 -24
- data/CHANGELOG.md +0 -5
- data/CODE_OF_CONDUCT.md +0 -132
- data/LICENSE.txt +0 -21
- data/ext/itsi_error/src/from.rs +0 -71
- data/ext/itsi_server/extconf.rb +0 -6
- data/ext/itsi_server/src/body_proxy/mod.rs +0 -2
- data/ext/itsi_server/src/request/itsi_request.rs +0 -277
- data/ext/itsi_server/src/request/mod.rs +0 -1
- data/ext/itsi_server/src/response/mod.rs +0 -1
- data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -13
- data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -5
- data/ext/itsi_server/src/server/itsi_server.rs +0 -244
- data/ext/itsi_server/src/server/listener.rs +0 -327
- /data/ext/itsi_server/src/server/{bind_protocol.rs → binds/bind_protocol.rs} +0 -0
@@ -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 {}
|