itsi-scheduler 0.1.5 → 0.1.14
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 +83 -22
- data/README.md +5 -0
- data/_index.md +7 -0
- data/ext/itsi_error/src/from.rs +26 -29
- data/ext/itsi_error/src/lib.rs +10 -1
- 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_server/Cargo.lock +2956 -0
- data/ext/itsi_server/Cargo.toml +69 -26
- data/ext/itsi_server/src/env.rs +43 -0
- data/ext/itsi_server/src/lib.rs +81 -75
- 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} +22 -3
- data/ext/itsi_server/src/ruby_types/itsi_grpc_request.rs +147 -0
- data/ext/itsi_server/src/ruby_types/itsi_grpc_response.rs +19 -0
- data/ext/itsi_server/src/ruby_types/itsi_grpc_stream/mod.rs +216 -0
- data/ext/itsi_server/src/{request/itsi_request.rs → ruby_types/itsi_http_request.rs} +108 -103
- data/ext/itsi_server/src/{response/itsi_response.rs → ruby_types/itsi_http_response.rs} +79 -38
- 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 +355 -0
- data/ext/itsi_server/src/ruby_types/itsi_server.rs +82 -0
- data/ext/itsi_server/src/ruby_types/mod.rs +55 -0
- data/ext/itsi_server/src/server/bind.rs +33 -20
- data/ext/itsi_server/src/server/byte_frame.rs +32 -0
- data/ext/itsi_server/src/server/cache_store.rs +74 -0
- data/ext/itsi_server/src/server/itsi_service.rs +172 -0
- data/ext/itsi_server/src/server/lifecycle_event.rs +3 -0
- data/ext/itsi_server/src/server/listener.rs +197 -106
- data/ext/itsi_server/src/server/middleware_stack/middleware.rs +153 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +47 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +58 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +82 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +264 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +139 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +300 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +287 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +48 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +127 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +191 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/grpc_service.rs +72 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +85 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +195 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +82 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +216 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +124 -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 +43 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +34 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +93 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +162 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +158 -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 +315 -0
- data/ext/itsi_server/src/server/mod.rs +8 -1
- data/ext/itsi_server/src/server/process_worker.rs +44 -11
- data/ext/itsi_server/src/server/rate_limiter.rs +565 -0
- data/ext/itsi_server/src/server/request_job.rs +11 -0
- data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +129 -46
- data/ext/itsi_server/src/server/serve_strategy/mod.rs +9 -6
- data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +337 -163
- data/ext/itsi_server/src/server/signal.rs +25 -2
- data/ext/itsi_server/src/server/static_file_server.rs +984 -0
- data/ext/itsi_server/src/server/thread_worker.rs +164 -88
- data/ext/itsi_server/src/server/tls/locked_dir_cache.rs +55 -17
- data/ext/itsi_server/src/server/tls.rs +104 -28
- data/ext/itsi_server/src/server/types.rs +43 -0
- data/ext/itsi_tracing/Cargo.toml +1 -0
- data/ext/itsi_tracing/src/lib.rs +222 -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 +79 -14
- 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/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
@@ -0,0 +1,153 @@
|
|
1
|
+
use super::middlewares::*;
|
2
|
+
use crate::server::{
|
3
|
+
itsi_service::RequestContext,
|
4
|
+
types::{HttpRequest, HttpResponse},
|
5
|
+
};
|
6
|
+
use async_trait::async_trait;
|
7
|
+
use either::Either;
|
8
|
+
use magnus::error::Result;
|
9
|
+
use std::cmp::Ordering;
|
10
|
+
|
11
|
+
#[derive(Debug)]
|
12
|
+
pub enum Middleware {
|
13
|
+
AllowList(AllowList),
|
14
|
+
AuthAPIKey(AuthAPIKey),
|
15
|
+
AuthBasic(AuthBasic),
|
16
|
+
AuthJwt(Box<AuthJwt>),
|
17
|
+
CacheControl(CacheControl),
|
18
|
+
Compression(Compression),
|
19
|
+
Cors(Box<Cors>),
|
20
|
+
DenyList(DenyList),
|
21
|
+
ETag(ETag),
|
22
|
+
IntrusionProtection(IntrusionProtection),
|
23
|
+
LogRequests(LogRequests),
|
24
|
+
Proxy(Proxy),
|
25
|
+
RateLimit(RateLimit),
|
26
|
+
Redirect(Redirect),
|
27
|
+
RequestHeaders(RequestHeaders),
|
28
|
+
ResponseHeaders(ResponseHeaders),
|
29
|
+
RubyApp(RubyApp),
|
30
|
+
StaticAssets(StaticAssets),
|
31
|
+
}
|
32
|
+
|
33
|
+
#[async_trait]
|
34
|
+
impl MiddlewareLayer for Middleware {
|
35
|
+
/// Called just once, to initialize the middleware state.
|
36
|
+
async fn initialize(&self) -> Result<()> {
|
37
|
+
match self {
|
38
|
+
Middleware::DenyList(filter) => filter.initialize().await,
|
39
|
+
Middleware::AllowList(filter) => filter.initialize().await,
|
40
|
+
Middleware::AuthBasic(filter) => filter.initialize().await,
|
41
|
+
Middleware::AuthJwt(filter) => filter.initialize().await,
|
42
|
+
Middleware::AuthAPIKey(filter) => filter.initialize().await,
|
43
|
+
Middleware::IntrusionProtection(filter) => filter.initialize().await,
|
44
|
+
Middleware::RateLimit(filter) => filter.initialize().await,
|
45
|
+
Middleware::RequestHeaders(filter) => filter.initialize().await,
|
46
|
+
Middleware::ResponseHeaders(filter) => filter.initialize().await,
|
47
|
+
Middleware::CacheControl(filter) => filter.initialize().await,
|
48
|
+
Middleware::Cors(filter) => filter.initialize().await,
|
49
|
+
Middleware::ETag(filter) => filter.initialize().await,
|
50
|
+
Middleware::StaticAssets(filter) => filter.initialize().await,
|
51
|
+
Middleware::Compression(filter) => filter.initialize().await,
|
52
|
+
Middleware::LogRequests(filter) => filter.initialize().await,
|
53
|
+
Middleware::Redirect(filter) => filter.initialize().await,
|
54
|
+
Middleware::Proxy(filter) => filter.initialize().await,
|
55
|
+
Middleware::RubyApp(filter) => filter.initialize().await,
|
56
|
+
}
|
57
|
+
}
|
58
|
+
|
59
|
+
async fn before(
|
60
|
+
&self,
|
61
|
+
req: HttpRequest,
|
62
|
+
context: &mut RequestContext,
|
63
|
+
) -> Result<Either<HttpRequest, HttpResponse>> {
|
64
|
+
match self {
|
65
|
+
Middleware::DenyList(filter) => filter.before(req, context).await,
|
66
|
+
Middleware::AllowList(filter) => filter.before(req, context).await,
|
67
|
+
Middleware::AuthBasic(filter) => filter.before(req, context).await,
|
68
|
+
Middleware::AuthJwt(filter) => filter.before(req, context).await,
|
69
|
+
Middleware::AuthAPIKey(filter) => filter.before(req, context).await,
|
70
|
+
Middleware::IntrusionProtection(filter) => filter.before(req, context).await,
|
71
|
+
Middleware::RequestHeaders(filter) => filter.before(req, context).await,
|
72
|
+
Middleware::ResponseHeaders(filter) => filter.before(req, context).await,
|
73
|
+
Middleware::RateLimit(filter) => filter.before(req, context).await,
|
74
|
+
Middleware::CacheControl(filter) => filter.before(req, context).await,
|
75
|
+
Middleware::Cors(filter) => filter.before(req, context).await,
|
76
|
+
Middleware::ETag(filter) => filter.before(req, context).await,
|
77
|
+
Middleware::StaticAssets(filter) => filter.before(req, context).await,
|
78
|
+
Middleware::Compression(filter) => filter.before(req, context).await,
|
79
|
+
Middleware::LogRequests(filter) => filter.before(req, context).await,
|
80
|
+
Middleware::Redirect(filter) => filter.before(req, context).await,
|
81
|
+
Middleware::Proxy(filter) => filter.before(req, context).await,
|
82
|
+
Middleware::RubyApp(filter) => filter.before(req, context).await,
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
86
|
+
async fn after(&self, res: HttpResponse, context: &mut RequestContext) -> HttpResponse {
|
87
|
+
match self {
|
88
|
+
Middleware::DenyList(filter) => filter.after(res, context).await,
|
89
|
+
Middleware::AllowList(filter) => filter.after(res, context).await,
|
90
|
+
Middleware::AuthBasic(filter) => filter.after(res, context).await,
|
91
|
+
Middleware::AuthJwt(filter) => filter.after(res, context).await,
|
92
|
+
Middleware::AuthAPIKey(filter) => filter.after(res, context).await,
|
93
|
+
Middleware::IntrusionProtection(filter) => filter.after(res, context).await,
|
94
|
+
Middleware::RateLimit(filter) => filter.after(res, context).await,
|
95
|
+
Middleware::RequestHeaders(filter) => filter.after(res, context).await,
|
96
|
+
Middleware::ResponseHeaders(filter) => filter.after(res, context).await,
|
97
|
+
Middleware::CacheControl(filter) => filter.after(res, context).await,
|
98
|
+
Middleware::Cors(filter) => filter.after(res, context).await,
|
99
|
+
Middleware::ETag(filter) => filter.after(res, context).await,
|
100
|
+
Middleware::StaticAssets(filter) => filter.after(res, context).await,
|
101
|
+
Middleware::Compression(filter) => filter.after(res, context).await,
|
102
|
+
Middleware::LogRequests(filter) => filter.after(res, context).await,
|
103
|
+
Middleware::Redirect(filter) => filter.after(res, context).await,
|
104
|
+
Middleware::Proxy(filter) => filter.after(res, context).await,
|
105
|
+
Middleware::RubyApp(filter) => filter.after(res, context).await,
|
106
|
+
}
|
107
|
+
}
|
108
|
+
}
|
109
|
+
|
110
|
+
impl Middleware {
|
111
|
+
fn variant_order(&self) -> usize {
|
112
|
+
match self {
|
113
|
+
Middleware::DenyList(_) => 0,
|
114
|
+
Middleware::AllowList(_) => 1,
|
115
|
+
Middleware::IntrusionProtection(_) => 2,
|
116
|
+
Middleware::Redirect(_) => 3,
|
117
|
+
Middleware::LogRequests(_) => 4,
|
118
|
+
Middleware::CacheControl(_) => 5,
|
119
|
+
Middleware::RequestHeaders(_) => 6,
|
120
|
+
Middleware::ResponseHeaders(_) => 7,
|
121
|
+
Middleware::AuthBasic(_) => 8,
|
122
|
+
Middleware::AuthJwt(_) => 9,
|
123
|
+
Middleware::AuthAPIKey(_) => 10,
|
124
|
+
Middleware::RateLimit(_) => 11,
|
125
|
+
Middleware::ETag(_) => 12,
|
126
|
+
Middleware::Compression(_) => 13,
|
127
|
+
Middleware::Proxy(_) => 14,
|
128
|
+
Middleware::Cors(_) => 15,
|
129
|
+
Middleware::StaticAssets(_) => 16,
|
130
|
+
Middleware::RubyApp(_) => 17,
|
131
|
+
}
|
132
|
+
}
|
133
|
+
}
|
134
|
+
|
135
|
+
impl PartialEq for Middleware {
|
136
|
+
fn eq(&self, other: &Self) -> bool {
|
137
|
+
self.variant_order() == other.variant_order()
|
138
|
+
}
|
139
|
+
}
|
140
|
+
|
141
|
+
impl Eq for Middleware {}
|
142
|
+
|
143
|
+
impl PartialOrd for Middleware {
|
144
|
+
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
145
|
+
Some(self.variant_order().cmp(&other.variant_order()))
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
149
|
+
impl Ord for Middleware {
|
150
|
+
fn cmp(&self, other: &Self) -> Ordering {
|
151
|
+
self.variant_order().cmp(&other.variant_order())
|
152
|
+
}
|
153
|
+
}
|
@@ -0,0 +1,47 @@
|
|
1
|
+
use super::{ErrorResponse, FromValue, MiddlewareLayer};
|
2
|
+
use crate::server::{
|
3
|
+
itsi_service::RequestContext,
|
4
|
+
types::{HttpRequest, HttpResponse},
|
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::sync::OnceLock;
|
13
|
+
|
14
|
+
#[derive(Debug, Clone, Deserialize)]
|
15
|
+
pub struct AllowList {
|
16
|
+
#[serde(skip_deserializing)]
|
17
|
+
pub allowed_ips: OnceLock<RegexSet>,
|
18
|
+
pub allowed_patterns: Vec<String>,
|
19
|
+
pub error_response: ErrorResponse,
|
20
|
+
}
|
21
|
+
|
22
|
+
#[async_trait]
|
23
|
+
impl MiddlewareLayer for AllowList {
|
24
|
+
async fn initialize(&self) -> Result<()> {
|
25
|
+
let allowed_ips = RegexSet::new(&self.allowed_patterns).map_err(ItsiError::default)?;
|
26
|
+
self.allowed_ips
|
27
|
+
.set(allowed_ips)
|
28
|
+
.map_err(|e| ItsiError::default(format!("Failed to set allowed IPs: {:?}", e)))?;
|
29
|
+
Ok(())
|
30
|
+
}
|
31
|
+
|
32
|
+
async fn before(
|
33
|
+
&self,
|
34
|
+
req: HttpRequest,
|
35
|
+
context: &mut RequestContext,
|
36
|
+
) -> Result<Either<HttpRequest, HttpResponse>> {
|
37
|
+
if let Some(allowed_ips) = self.allowed_ips.get() {
|
38
|
+
if !allowed_ips.is_match(&context.addr) {
|
39
|
+
return Ok(Either::Right(
|
40
|
+
self.error_response.to_http_response(&req).await,
|
41
|
+
));
|
42
|
+
}
|
43
|
+
}
|
44
|
+
Ok(Either::Left(req))
|
45
|
+
}
|
46
|
+
}
|
47
|
+
impl FromValue for AllowList {}
|
@@ -0,0 +1,58 @@
|
|
1
|
+
use crate::server::{
|
2
|
+
itsi_service::RequestContext,
|
3
|
+
types::{HttpRequest, HttpResponse, RequestExt},
|
4
|
+
};
|
5
|
+
|
6
|
+
use super::{error_response::ErrorResponse, token_source::TokenSource, FromValue, MiddlewareLayer};
|
7
|
+
|
8
|
+
use async_trait::async_trait;
|
9
|
+
use either::Either;
|
10
|
+
use magnus::error::Result;
|
11
|
+
use serde::Deserialize;
|
12
|
+
|
13
|
+
/// A simple API key filter.
|
14
|
+
/// The API key can be given inside the header or a query string
|
15
|
+
/// Keys are validated against a list of allowed key values (Changing these requires a restart)
|
16
|
+
///
|
17
|
+
#[derive(Debug, Clone, Deserialize)]
|
18
|
+
pub struct AuthAPIKey {
|
19
|
+
pub valid_keys: Vec<String>,
|
20
|
+
pub token_source: TokenSource,
|
21
|
+
pub error_response: ErrorResponse,
|
22
|
+
}
|
23
|
+
|
24
|
+
#[async_trait]
|
25
|
+
impl MiddlewareLayer for AuthAPIKey {
|
26
|
+
async fn before(
|
27
|
+
&self,
|
28
|
+
req: HttpRequest,
|
29
|
+
_context: &mut RequestContext,
|
30
|
+
) -> Result<Either<HttpRequest, HttpResponse>> {
|
31
|
+
let submitted_value = match &self.token_source {
|
32
|
+
TokenSource::Header { name, prefix } => {
|
33
|
+
if let Some(header) = req.header(name) {
|
34
|
+
if let Some(prefix) = prefix {
|
35
|
+
Some(header.strip_prefix(prefix).unwrap_or("").trim_ascii())
|
36
|
+
} else {
|
37
|
+
Some(header.trim_ascii())
|
38
|
+
}
|
39
|
+
} else {
|
40
|
+
None
|
41
|
+
}
|
42
|
+
}
|
43
|
+
TokenSource::Query(query_name) => req.query_param(query_name),
|
44
|
+
};
|
45
|
+
if !self
|
46
|
+
.valid_keys
|
47
|
+
.iter()
|
48
|
+
.any(|key| submitted_value.is_some_and(|sv| sv == key))
|
49
|
+
{
|
50
|
+
Ok(Either::Right(
|
51
|
+
self.error_response.to_http_response(&req).await,
|
52
|
+
))
|
53
|
+
} else {
|
54
|
+
Ok(Either::Left(req))
|
55
|
+
}
|
56
|
+
}
|
57
|
+
}
|
58
|
+
impl FromValue for AuthAPIKey {}
|
@@ -0,0 +1,82 @@
|
|
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::server::{
|
13
|
+
itsi_service::RequestContext,
|
14
|
+
types::{HttpRequest, HttpResponse, RequestExt},
|
15
|
+
};
|
16
|
+
|
17
|
+
use super::{FromValue, MiddlewareLayer};
|
18
|
+
|
19
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
20
|
+
pub struct AuthBasic {
|
21
|
+
pub realm: String,
|
22
|
+
/// Maps usernames to passwords.
|
23
|
+
pub credential_pairs: HashMap<String, String>,
|
24
|
+
}
|
25
|
+
|
26
|
+
impl AuthBasic {
|
27
|
+
fn basic_auth_failed_response(&self) -> HttpResponse {
|
28
|
+
Response::builder()
|
29
|
+
.status(StatusCode::UNAUTHORIZED)
|
30
|
+
.header(
|
31
|
+
"WWW-Authenticate",
|
32
|
+
format!("Basic realm=\"{}\"", self.realm),
|
33
|
+
)
|
34
|
+
.body(BoxBody::new(Full::new(Bytes::from("Unauthorized"))))
|
35
|
+
.unwrap()
|
36
|
+
}
|
37
|
+
}
|
38
|
+
#[async_trait]
|
39
|
+
impl MiddlewareLayer for AuthBasic {
|
40
|
+
async fn before(
|
41
|
+
&self,
|
42
|
+
req: HttpRequest,
|
43
|
+
_context: &mut RequestContext,
|
44
|
+
) -> Result<Either<HttpRequest, HttpResponse>> {
|
45
|
+
// Retrieve the Authorization header.
|
46
|
+
let auth_header = req.header("Authorization");
|
47
|
+
|
48
|
+
if !auth_header.is_some_and(|header| header.starts_with("Basic ")) {
|
49
|
+
return Ok(Either::Right(self.basic_auth_failed_response()));
|
50
|
+
}
|
51
|
+
|
52
|
+
let auth_header = auth_header.unwrap();
|
53
|
+
|
54
|
+
let encoded_credentials = &auth_header["Basic ".len()..];
|
55
|
+
let decoded = match general_purpose::STANDARD.decode(encoded_credentials) {
|
56
|
+
Ok(bytes) => bytes,
|
57
|
+
Err(_) => {
|
58
|
+
return Ok(Either::Right(self.basic_auth_failed_response()));
|
59
|
+
}
|
60
|
+
};
|
61
|
+
|
62
|
+
let decoded_str = match str::from_utf8(&decoded) {
|
63
|
+
Ok(s) => s,
|
64
|
+
Err(_) => {
|
65
|
+
return Ok(Either::Right(self.basic_auth_failed_response()));
|
66
|
+
}
|
67
|
+
};
|
68
|
+
|
69
|
+
let mut parts = decoded_str.splitn(2, ':');
|
70
|
+
let username = parts.next().unwrap_or("");
|
71
|
+
let password = parts.next().unwrap_or("");
|
72
|
+
|
73
|
+
match self.credential_pairs.get(username) {
|
74
|
+
Some(expected_password) if expected_password == password => Ok(Either::Left(req)),
|
75
|
+
_ => {
|
76
|
+
return Ok(Either::Right(self.basic_auth_failed_response()));
|
77
|
+
}
|
78
|
+
}
|
79
|
+
}
|
80
|
+
}
|
81
|
+
|
82
|
+
impl FromValue for AuthBasic {}
|
@@ -0,0 +1,264 @@
|
|
1
|
+
use super::{error_response::ErrorResponse, token_source::TokenSource, FromValue, MiddlewareLayer};
|
2
|
+
use crate::server::{
|
3
|
+
itsi_service::RequestContext,
|
4
|
+
types::{HttpRequest, HttpResponse, RequestExt},
|
5
|
+
};
|
6
|
+
use async_trait::async_trait;
|
7
|
+
use base64::{engine::general_purpose, Engine};
|
8
|
+
use derive_more::Debug;
|
9
|
+
use either::Either;
|
10
|
+
use itsi_error::ItsiError;
|
11
|
+
use jsonwebtoken::{
|
12
|
+
decode, decode_header, Algorithm as JwtAlg, DecodingKey, TokenData, Validation,
|
13
|
+
};
|
14
|
+
use magnus::error::Result;
|
15
|
+
use serde::Deserialize;
|
16
|
+
use std::{
|
17
|
+
collections::{HashMap, HashSet},
|
18
|
+
sync::OnceLock,
|
19
|
+
};
|
20
|
+
|
21
|
+
#[derive(Debug, Clone, Deserialize)]
|
22
|
+
pub struct AuthJwt {
|
23
|
+
pub token_source: TokenSource,
|
24
|
+
// The verifiers map still holds base64-encoded key strings keyed by algorithm.
|
25
|
+
pub verifiers: HashMap<JwtAlgorithm, Vec<String>>,
|
26
|
+
// We now store jsonwebtoken’s DecodingKey in our OnceLock.
|
27
|
+
#[serde(skip_deserializing)]
|
28
|
+
#[debug(skip)]
|
29
|
+
pub keys: OnceLock<HashMap<JwtAlgorithm, Vec<DecodingKey>>>,
|
30
|
+
pub audiences: Option<HashSet<String>>,
|
31
|
+
pub subjects: Option<HashSet<String>>,
|
32
|
+
pub issuers: Option<HashSet<String>>,
|
33
|
+
pub leeway: Option<u64>,
|
34
|
+
pub error_response: ErrorResponse,
|
35
|
+
}
|
36
|
+
|
37
|
+
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Hash)]
|
38
|
+
pub enum JwtAlgorithm {
|
39
|
+
#[serde(rename(deserialize = "hs256"))]
|
40
|
+
Hs256,
|
41
|
+
#[serde(rename(deserialize = "hs384"))]
|
42
|
+
Hs384,
|
43
|
+
#[serde(rename(deserialize = "hs512"))]
|
44
|
+
Hs512,
|
45
|
+
#[serde(rename(deserialize = "rs256"))]
|
46
|
+
Rs256,
|
47
|
+
#[serde(rename(deserialize = "rs384"))]
|
48
|
+
Rs384,
|
49
|
+
#[serde(rename(deserialize = "rs512"))]
|
50
|
+
Rs512,
|
51
|
+
#[serde(rename(deserialize = "es256"))]
|
52
|
+
Es256,
|
53
|
+
#[serde(rename(deserialize = "es384"))]
|
54
|
+
Es384,
|
55
|
+
#[serde(rename(deserialize = "ps256"))]
|
56
|
+
Ps256,
|
57
|
+
#[serde(rename(deserialize = "ps384"))]
|
58
|
+
Ps384,
|
59
|
+
#[serde(rename(deserialize = "ps512"))]
|
60
|
+
Ps512,
|
61
|
+
}
|
62
|
+
|
63
|
+
// Allow conversion from jsonwebtoken’s Algorithm to our JwtAlgorithm.
|
64
|
+
impl From<JwtAlg> for JwtAlgorithm {
|
65
|
+
fn from(alg: JwtAlg) -> Self {
|
66
|
+
match alg {
|
67
|
+
JwtAlg::HS256 => JwtAlgorithm::Hs256,
|
68
|
+
JwtAlg::HS384 => JwtAlgorithm::Hs384,
|
69
|
+
JwtAlg::HS512 => JwtAlgorithm::Hs512,
|
70
|
+
JwtAlg::RS256 => JwtAlgorithm::Rs256,
|
71
|
+
JwtAlg::RS384 => JwtAlgorithm::Rs384,
|
72
|
+
JwtAlg::RS512 => JwtAlgorithm::Rs512,
|
73
|
+
JwtAlg::ES256 => JwtAlgorithm::Es256,
|
74
|
+
JwtAlg::ES384 => JwtAlgorithm::Es384,
|
75
|
+
JwtAlg::PS256 => JwtAlgorithm::Ps256,
|
76
|
+
JwtAlg::PS384 => JwtAlgorithm::Ps384,
|
77
|
+
JwtAlg::PS512 => JwtAlgorithm::Ps512,
|
78
|
+
_ => panic!("Unsupported algorithm"),
|
79
|
+
}
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
impl JwtAlgorithm {
|
84
|
+
/// Given a base64-encoded key string, decode and construct a jsonwebtoken::DecodingKey.
|
85
|
+
pub fn key_from(&self, base64: &str) -> itsi_error::Result<DecodingKey> {
|
86
|
+
let bytes = general_purpose::STANDARD
|
87
|
+
.decode(base64)
|
88
|
+
.map_err(ItsiError::default)?;
|
89
|
+
match self {
|
90
|
+
// For HMAC algorithms, use the secret directly.
|
91
|
+
JwtAlgorithm::Hs256 | JwtAlgorithm::Hs384 | JwtAlgorithm::Hs512 => {
|
92
|
+
Ok(DecodingKey::from_secret(&bytes))
|
93
|
+
}
|
94
|
+
// For RSA (and PS) algorithms, expect a PEM-formatted key.
|
95
|
+
JwtAlgorithm::Rs256
|
96
|
+
| JwtAlgorithm::Rs384
|
97
|
+
| JwtAlgorithm::Rs512
|
98
|
+
| JwtAlgorithm::Ps256
|
99
|
+
| JwtAlgorithm::Ps384
|
100
|
+
| JwtAlgorithm::Ps512 => {
|
101
|
+
DecodingKey::from_rsa_pem(&bytes).map_err(|e| ItsiError::default(e.to_string()))
|
102
|
+
}
|
103
|
+
// For ECDSA algorithms, expect a PEM-formatted key.
|
104
|
+
JwtAlgorithm::Es256 | JwtAlgorithm::Es384 => {
|
105
|
+
DecodingKey::from_ec_pem(&bytes).map_err(|e| ItsiError::default(e.to_string()))
|
106
|
+
}
|
107
|
+
}
|
108
|
+
}
|
109
|
+
}
|
110
|
+
|
111
|
+
#[derive(Debug, Deserialize)]
|
112
|
+
#[serde(untagged)]
|
113
|
+
enum Audience {
|
114
|
+
Single(String),
|
115
|
+
Multiple(Vec<String>),
|
116
|
+
}
|
117
|
+
|
118
|
+
#[derive(Debug, Deserialize)]
|
119
|
+
struct Claims {
|
120
|
+
// Here we assume the token includes an expiration.
|
121
|
+
#[allow(dead_code)]
|
122
|
+
exp: usize,
|
123
|
+
// The audience claim may be a single string or an array.
|
124
|
+
aud: Option<Audience>,
|
125
|
+
sub: Option<String>,
|
126
|
+
iss: Option<String>,
|
127
|
+
}
|
128
|
+
|
129
|
+
#[async_trait]
|
130
|
+
impl MiddlewareLayer for AuthJwt {
|
131
|
+
async fn initialize(&self) -> Result<()> {
|
132
|
+
let keys: HashMap<JwtAlgorithm, Vec<DecodingKey>> = self
|
133
|
+
.verifiers
|
134
|
+
.iter()
|
135
|
+
.map(|(algorithm, key_strings)| {
|
136
|
+
let algo = algorithm.clone();
|
137
|
+
let keys: itsi_error::Result<Vec<DecodingKey>> = key_strings
|
138
|
+
.iter()
|
139
|
+
.map(|key_string| algorithm.key_from(key_string))
|
140
|
+
.collect();
|
141
|
+
keys.map(|keys| (algo, keys))
|
142
|
+
})
|
143
|
+
.collect::<itsi_error::Result<HashMap<JwtAlgorithm, Vec<DecodingKey>>>>()?;
|
144
|
+
self.keys
|
145
|
+
.set(keys)
|
146
|
+
.map_err(|_| ItsiError::default("Failed to set keys".to_string()))?;
|
147
|
+
Ok(())
|
148
|
+
}
|
149
|
+
|
150
|
+
async fn before(
|
151
|
+
&self,
|
152
|
+
req: HttpRequest,
|
153
|
+
_context: &mut RequestContext,
|
154
|
+
) -> Result<Either<HttpRequest, HttpResponse>> {
|
155
|
+
// Retrieve the JWT token from either a header or a query parameter.
|
156
|
+
let token_str = match &self.token_source {
|
157
|
+
TokenSource::Header { name, prefix } => {
|
158
|
+
if let Some(header) = req.header(name) {
|
159
|
+
if let Some(prefix) = prefix {
|
160
|
+
Some(header.strip_prefix(prefix).unwrap_or("").trim_ascii())
|
161
|
+
} else {
|
162
|
+
Some(header.trim_ascii())
|
163
|
+
}
|
164
|
+
} else {
|
165
|
+
None
|
166
|
+
}
|
167
|
+
}
|
168
|
+
TokenSource::Query(query_name) => req.query_param(query_name),
|
169
|
+
};
|
170
|
+
|
171
|
+
if token_str.is_none() {
|
172
|
+
return Ok(Either::Right(
|
173
|
+
self.error_response.to_http_response(&req).await,
|
174
|
+
));
|
175
|
+
}
|
176
|
+
let token_str = token_str.unwrap();
|
177
|
+
|
178
|
+
// Use jsonwebtoken's decode_header to inspect the token and determine its algorithm.
|
179
|
+
let header =
|
180
|
+
decode_header(token_str).map_err(|_| ItsiError::default("Invalid token header"))?;
|
181
|
+
let alg: JwtAlgorithm = header.alg.into();
|
182
|
+
|
183
|
+
if !self.verifiers.contains_key(&alg) {
|
184
|
+
return Ok(Either::Right(
|
185
|
+
self.error_response.to_http_response(&req).await,
|
186
|
+
));
|
187
|
+
}
|
188
|
+
let keys = self.keys.get().unwrap().get(&alg).unwrap();
|
189
|
+
|
190
|
+
// Build validation based on the algorithm and optional leeway.
|
191
|
+
let mut validation = Validation::new(match alg {
|
192
|
+
JwtAlgorithm::Hs256 => JwtAlg::HS256,
|
193
|
+
JwtAlgorithm::Hs384 => JwtAlg::HS384,
|
194
|
+
JwtAlgorithm::Hs512 => JwtAlg::HS512,
|
195
|
+
JwtAlgorithm::Rs256 => JwtAlg::RS256,
|
196
|
+
JwtAlgorithm::Rs384 => JwtAlg::RS384,
|
197
|
+
JwtAlgorithm::Rs512 => JwtAlg::RS512,
|
198
|
+
JwtAlgorithm::Es256 => JwtAlg::ES256,
|
199
|
+
JwtAlgorithm::Es384 => JwtAlg::ES384,
|
200
|
+
JwtAlgorithm::Ps256 => JwtAlg::PS256,
|
201
|
+
JwtAlgorithm::Ps384 => JwtAlg::PS384,
|
202
|
+
JwtAlgorithm::Ps512 => JwtAlg::PS512,
|
203
|
+
});
|
204
|
+
if let Some(leeway) = self.leeway {
|
205
|
+
validation.leeway = leeway;
|
206
|
+
}
|
207
|
+
// (Optional) You could set expected issuer or audience on `validation` here.
|
208
|
+
|
209
|
+
// Try verifying the token using each key until one succeeds.
|
210
|
+
let token_data: Option<TokenData<Claims>> = keys
|
211
|
+
.iter()
|
212
|
+
.find_map(|key| decode::<Claims>(token_str, key, &validation).ok());
|
213
|
+
let token_data = if let Some(data) = token_data {
|
214
|
+
data
|
215
|
+
} else {
|
216
|
+
return Ok(Either::Right(
|
217
|
+
self.error_response.to_http_response(&req).await,
|
218
|
+
));
|
219
|
+
};
|
220
|
+
|
221
|
+
let claims = token_data.claims;
|
222
|
+
|
223
|
+
// Verify expected audiences.
|
224
|
+
if let Some(expected_audiences) = &self.audiences {
|
225
|
+
if let Some(aud) = &claims.aud {
|
226
|
+
let token_auds: HashSet<String> = match aud {
|
227
|
+
Audience::Single(s) => [s.clone()].into_iter().collect(),
|
228
|
+
Audience::Multiple(v) => v.iter().cloned().collect(),
|
229
|
+
};
|
230
|
+
if expected_audiences.is_disjoint(&token_auds) {
|
231
|
+
return Ok(Either::Right(
|
232
|
+
self.error_response.to_http_response(&req).await,
|
233
|
+
));
|
234
|
+
}
|
235
|
+
}
|
236
|
+
}
|
237
|
+
|
238
|
+
// Verify expected subject.
|
239
|
+
if let Some(expected_subjects) = &self.subjects {
|
240
|
+
if let Some(sub) = &claims.sub {
|
241
|
+
if !expected_subjects.contains(sub) {
|
242
|
+
return Ok(Either::Right(
|
243
|
+
self.error_response.to_http_response(&req).await,
|
244
|
+
));
|
245
|
+
}
|
246
|
+
}
|
247
|
+
}
|
248
|
+
|
249
|
+
// Verify expected issuer.
|
250
|
+
if let Some(expected_issuers) = &self.issuers {
|
251
|
+
if let Some(iss) = &claims.iss {
|
252
|
+
if !expected_issuers.contains(iss) {
|
253
|
+
return Ok(Either::Right(
|
254
|
+
self.error_response.to_http_response(&req).await,
|
255
|
+
));
|
256
|
+
}
|
257
|
+
}
|
258
|
+
}
|
259
|
+
|
260
|
+
Ok(Either::Left(req))
|
261
|
+
}
|
262
|
+
}
|
263
|
+
|
264
|
+
impl FromValue for AuthJwt {}
|