itsi 0.1.14 → 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.
- checksums.yaml +4 -4
- data/Cargo.lock +126 -272
- data/Cargo.toml +6 -0
- data/crates/itsi_error/Cargo.toml +1 -0
- data/crates/itsi_error/src/lib.rs +100 -10
- data/crates/itsi_scheduler/src/itsi_scheduler.rs +1 -1
- data/crates/itsi_server/Cargo.toml +12 -11
- data/crates/itsi_server/src/default_responses/html/401.html +68 -0
- data/crates/itsi_server/src/default_responses/html/403.html +68 -0
- data/crates/itsi_server/src/default_responses/html/404.html +68 -0
- data/crates/itsi_server/src/default_responses/html/413.html +71 -0
- data/crates/itsi_server/src/default_responses/html/429.html +68 -0
- data/crates/itsi_server/src/default_responses/html/500.html +71 -0
- data/crates/itsi_server/src/default_responses/html/502.html +71 -0
- data/crates/itsi_server/src/default_responses/html/503.html +68 -0
- data/crates/itsi_server/src/default_responses/html/504.html +69 -0
- data/crates/itsi_server/src/default_responses/html/index.html +238 -0
- data/crates/itsi_server/src/default_responses/json/401.json +6 -0
- data/crates/itsi_server/src/default_responses/json/403.json +6 -0
- data/crates/itsi_server/src/default_responses/json/404.json +6 -0
- data/crates/itsi_server/src/default_responses/json/413.json +6 -0
- data/crates/itsi_server/src/default_responses/json/429.json +6 -0
- data/crates/itsi_server/src/default_responses/json/500.json +6 -0
- data/crates/itsi_server/src/default_responses/json/502.json +6 -0
- data/crates/itsi_server/src/default_responses/json/503.json +6 -0
- data/crates/itsi_server/src/default_responses/json/504.json +6 -0
- data/crates/itsi_server/src/default_responses/mod.rs +11 -0
- data/crates/itsi_server/src/lib.rs +58 -26
- data/crates/itsi_server/src/prelude.rs +2 -0
- data/crates/itsi_server/src/ruby_types/README.md +21 -0
- data/crates/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +8 -6
- data/crates/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
- data/crates/itsi_server/src/ruby_types/{itsi_grpc_stream → itsi_grpc_response_stream}/mod.rs +121 -73
- data/crates/itsi_server/src/ruby_types/itsi_http_request.rs +103 -40
- data/crates/itsi_server/src/ruby_types/itsi_http_response.rs +8 -5
- data/crates/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +4 -4
- data/crates/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +37 -17
- data/crates/itsi_server/src/ruby_types/itsi_server.rs +4 -3
- data/crates/itsi_server/src/ruby_types/mod.rs +6 -13
- data/crates/itsi_server/src/server/{bind.rs → binds/bind.rs} +23 -4
- data/crates/itsi_server/src/server/{listener.rs → binds/listener.rs} +24 -10
- data/crates/itsi_server/src/server/binds/mod.rs +4 -0
- data/crates/itsi_server/src/server/{tls.rs → binds/tls.rs} +9 -4
- data/crates/itsi_server/src/server/http_message_types.rs +97 -0
- data/crates/itsi_server/src/server/io_stream.rs +2 -1
- data/crates/itsi_server/src/server/middleware_stack/middleware.rs +28 -16
- data/crates/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +17 -8
- data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +47 -18
- data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +13 -9
- data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +50 -29
- data/crates/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +5 -2
- data/crates/itsi_server/src/server/middleware_stack/middlewares/compression.rs +37 -48
- data/crates/itsi_server/src/server/middleware_stack/middlewares/cors.rs +25 -20
- data/crates/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +14 -7
- data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +190 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +125 -95
- data/crates/itsi_server/src/server/middleware_stack/middlewares/etag.rs +9 -5
- data/crates/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +1 -4
- data/crates/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +25 -19
- data/crates/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +4 -4
- data/crates/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/mod.rs +9 -4
- data/crates/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +260 -62
- data/crates/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +29 -22
- data/crates/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +6 -6
- data/crates/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +6 -5
- data/crates/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +4 -2
- data/crates/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +51 -18
- data/crates/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +31 -13
- data/crates/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +13 -8
- data/crates/itsi_server/src/server/middleware_stack/mod.rs +101 -69
- data/crates/itsi_server/src/server/mod.rs +3 -9
- data/crates/itsi_server/src/server/process_worker.rs +21 -3
- data/crates/itsi_server/src/server/request_job.rs +2 -2
- data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +8 -3
- data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +26 -26
- data/crates/itsi_server/src/server/signal.rs +24 -41
- data/crates/itsi_server/src/server/size_limited_incoming.rs +101 -0
- data/crates/itsi_server/src/server/thread_worker.rs +59 -28
- data/crates/itsi_server/src/services/itsi_http_service.rs +239 -0
- data/crates/itsi_server/src/services/mime_types.rs +1416 -0
- data/crates/itsi_server/src/services/mod.rs +6 -0
- data/crates/itsi_server/src/services/password_hasher.rs +83 -0
- data/crates/itsi_server/src/{server → services}/rate_limiter.rs +35 -31
- data/crates/itsi_server/src/{server → services}/static_file_server.rs +521 -181
- data/crates/itsi_tracing/src/lib.rs +145 -55
- data/{Itsi.rb → foo/Itsi.rb} +6 -9
- data/gems/scheduler/Cargo.lock +7 -0
- data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
- data/gems/scheduler/test/helpers/test_helper.rb +0 -1
- data/gems/scheduler/test/test_address_resolve.rb +0 -1
- data/gems/scheduler/test/test_network_io.rb +1 -1
- data/gems/scheduler/test/test_process_wait.rb +0 -1
- data/gems/server/Cargo.lock +126 -272
- data/gems/server/exe/itsi +65 -19
- data/gems/server/itsi-server.gemspec +4 -3
- data/gems/server/lib/itsi/http_request/response_status_shortcodes.rb +74 -0
- data/gems/server/lib/itsi/http_request.rb +117 -17
- data/gems/server/lib/itsi/http_response.rb +2 -0
- data/gems/server/lib/itsi/passfile.rb +109 -0
- data/gems/server/lib/itsi/server/config/dsl.rb +171 -99
- data/gems/server/lib/itsi/server/config.rb +58 -23
- data/gems/server/lib/itsi/server/default_app/default_app.rb +25 -29
- data/gems/server/lib/itsi/server/default_app/index.html +113 -89
- data/gems/server/lib/itsi/server/{Itsi.rb → default_config/Itsi-rackup.rb} +1 -1
- data/gems/server/lib/itsi/server/default_config/Itsi.rb +107 -0
- data/gems/server/lib/itsi/server/grpc/grpc_call.rb +246 -0
- data/gems/server/lib/itsi/server/grpc/grpc_interface.rb +100 -0
- data/gems/server/lib/itsi/server/grpc/reflection/v1/reflection_pb.rb +26 -0
- data/gems/server/lib/itsi/server/grpc/reflection/v1/reflection_services_pb.rb +122 -0
- data/gems/server/lib/itsi/server/route_tester.rb +107 -0
- data/gems/server/lib/itsi/server/typed_handlers/param_parser.rb +200 -0
- data/gems/server/lib/itsi/server/typed_handlers/source_parser.rb +55 -0
- data/gems/server/lib/itsi/server/typed_handlers.rb +17 -0
- data/gems/server/lib/itsi/server/version.rb +1 -1
- data/gems/server/lib/itsi/server.rb +82 -12
- data/gems/server/lib/ruby_lsp/itsi/addon.rb +111 -0
- data/gems/server/lib/shell_completions/completions.rb +26 -0
- data/gems/server/test/helpers/test_helper.rb +2 -1
- data/lib/itsi/version.rb +1 -1
- data/sandbox/README.md +5 -0
- data/sandbox/itsi_file/Gemfile +4 -2
- data/sandbox/itsi_file/Gemfile.lock +48 -6
- data/sandbox/itsi_file/Itsi.rb +327 -129
- data/sandbox/itsi_file/call.json +1 -0
- data/sandbox/itsi_file/echo_client/Gemfile +10 -0
- data/sandbox/itsi_file/echo_client/Gemfile.lock +27 -0
- data/sandbox/itsi_file/echo_client/README.md +95 -0
- data/sandbox/itsi_file/echo_client/echo_client.rb +164 -0
- data/sandbox/itsi_file/echo_client/gen_proto.sh +17 -0
- data/sandbox/itsi_file/echo_client/lib/echo_pb.rb +16 -0
- data/sandbox/itsi_file/echo_client/lib/echo_services_pb.rb +29 -0
- data/sandbox/itsi_file/echo_client/run_client.rb +64 -0
- data/sandbox/itsi_file/echo_client/test_compressions.sh +20 -0
- data/sandbox/itsi_file/echo_service_nonitsi/Gemfile +10 -0
- data/sandbox/itsi_file/echo_service_nonitsi/Gemfile.lock +79 -0
- data/sandbox/itsi_file/echo_service_nonitsi/echo.proto +26 -0
- data/sandbox/itsi_file/echo_service_nonitsi/echo_pb.rb +16 -0
- data/sandbox/itsi_file/echo_service_nonitsi/echo_services_pb.rb +29 -0
- data/sandbox/itsi_file/echo_service_nonitsi/server.rb +52 -0
- data/sandbox/itsi_sandbox_async/config.ru +0 -1
- data/sandbox/itsi_sandbox_rack/Gemfile.lock +2 -2
- data/sandbox/itsi_sandbox_rails/Gemfile +2 -2
- data/sandbox/itsi_sandbox_rails/Gemfile.lock +76 -2
- data/sandbox/itsi_sandbox_rails/app/controllers/home_controller.rb +15 -0
- data/sandbox/itsi_sandbox_rails/config/environments/development.rb +1 -0
- data/sandbox/itsi_sandbox_rails/config/environments/production.rb +1 -0
- data/sandbox/itsi_sandbox_rails/config/routes.rb +2 -0
- data/sandbox/itsi_sinatra/app.rb +0 -1
- data/sandbox/static_files/.env +1 -0
- data/sandbox/static_files/404.html +25 -0
- data/sandbox/static_files/_DSC0102.NEF.jpg +0 -0
- data/sandbox/static_files/about.html +68 -0
- data/sandbox/static_files/tiny.html +1 -0
- data/sandbox/static_files/writebook.zip +0 -0
- data/tasks.txt +28 -33
- metadata +87 -26
- data/crates/itsi_error/src/from.rs +0 -68
- data/crates/itsi_server/src/ruby_types/itsi_grpc_request.rs +0 -147
- data/crates/itsi_server/src/ruby_types/itsi_grpc_response.rs +0 -19
- data/crates/itsi_server/src/server/itsi_service.rs +0 -172
- data/crates/itsi_server/src/server/middleware_stack/middlewares/grpc_service.rs +0 -72
- data/crates/itsi_server/src/server/types.rs +0 -43
- data/gems/server/lib/itsi/server/grpc_interface.rb +0 -213
- data/sandbox/itsi_file/public/assets/index.html +0 -1
- /data/crates/itsi_server/src/server/{bind_protocol.rs → binds/bind_protocol.rs} +0 -0
- /data/crates/itsi_server/src/server/{tls → binds/tls}/locked_dir_cache.rs +0 -0
- /data/crates/itsi_server/src/{server → services}/cache_store.rs +0 -0
@@ -3,7 +3,7 @@ use itsi_error::Result;
|
|
3
3
|
use itsi_tracing::info;
|
4
4
|
use locked_dir_cache::LockedDirCache;
|
5
5
|
use rcgen::{
|
6
|
-
|
6
|
+
BasicConstraints, CertificateParams, DistinguishedName, DnType, IsCa, KeyPair, SanType,
|
7
7
|
};
|
8
8
|
use rustls::{
|
9
9
|
pki_types::{CertificateDer, PrivateKeyDer},
|
@@ -24,6 +24,7 @@ use crate::env::{
|
|
24
24
|
ITSI_ACME_CACHE_DIR, ITSI_ACME_CA_PEM_PATH, ITSI_ACME_CONTACT_EMAIL, ITSI_ACME_DIRECTORY_URL,
|
25
25
|
ITSI_LOCAL_CA_DIR,
|
26
26
|
};
|
27
|
+
|
27
28
|
mod locked_dir_cache;
|
28
29
|
|
29
30
|
#[derive(Clone)]
|
@@ -253,9 +254,13 @@ fn get_or_create_local_dev_ca() -> Result<(String, String)> {
|
|
253
254
|
Ok((key_pem, cert_pem))
|
254
255
|
} else {
|
255
256
|
let subject_alt_names = vec!["dev.itsi.fyi".to_string(), "localhost".to_string()];
|
256
|
-
|
257
|
-
let
|
258
|
-
|
257
|
+
let mut params = CertificateParams::new(subject_alt_names)?;
|
258
|
+
let mut distinguished_name = DistinguishedName::new();
|
259
|
+
distinguished_name.push(DnType::CommonName, "Itsi Development CA");
|
260
|
+
params.distinguished_name = distinguished_name;
|
261
|
+
params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
|
262
|
+
let key_pair = KeyPair::generate()?;
|
263
|
+
let cert = params.self_signed(&key_pair)?;
|
259
264
|
|
260
265
|
fs::write(&key_path, key_pair.serialize_pem())?;
|
261
266
|
fs::write(&cert_path, cert.pem())?;
|
@@ -0,0 +1,97 @@
|
|
1
|
+
use std::convert::Infallible;
|
2
|
+
|
3
|
+
use bytes::Bytes;
|
4
|
+
use http::{Request, Response};
|
5
|
+
use http_body_util::combinators::BoxBody;
|
6
|
+
use hyper::body::Incoming;
|
7
|
+
|
8
|
+
use super::size_limited_incoming::SizeLimitedIncoming;
|
9
|
+
|
10
|
+
pub type HttpResponse = Response<BoxBody<Bytes, Infallible>>;
|
11
|
+
pub type HttpRequest = Request<SizeLimitedIncoming<Incoming>>;
|
12
|
+
|
13
|
+
pub trait ConversionExt {
|
14
|
+
fn limit(self) -> HttpRequest;
|
15
|
+
}
|
16
|
+
|
17
|
+
impl ConversionExt for Request<Incoming> {
|
18
|
+
fn limit(self) -> HttpRequest {
|
19
|
+
let (parts, body) = self.into_parts();
|
20
|
+
Request::from_parts(parts, SizeLimitedIncoming::new(body))
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
pub trait RequestExt {
|
25
|
+
fn content_type(&self) -> Option<&str>;
|
26
|
+
fn accept(&self) -> Option<&str>;
|
27
|
+
fn header(&self, header_name: &str) -> Option<&str>;
|
28
|
+
fn query_param(&self, query_name: &str) -> Option<&str>;
|
29
|
+
}
|
30
|
+
|
31
|
+
pub trait PathExt {
|
32
|
+
fn no_trailing_slash(&self) -> &str;
|
33
|
+
}
|
34
|
+
|
35
|
+
#[derive(Debug, Clone)]
|
36
|
+
pub enum ResponseFormat {
|
37
|
+
JSON,
|
38
|
+
HTML,
|
39
|
+
TEXT,
|
40
|
+
UNKNOWN,
|
41
|
+
}
|
42
|
+
|
43
|
+
#[derive(Debug, Clone, Default)]
|
44
|
+
pub struct SupportedEncodingSet {
|
45
|
+
pub zstd: bool,
|
46
|
+
pub br: bool,
|
47
|
+
pub deflate: bool,
|
48
|
+
pub gzip: bool,
|
49
|
+
}
|
50
|
+
|
51
|
+
impl From<Option<&str>> for ResponseFormat {
|
52
|
+
fn from(value: Option<&str>) -> Self {
|
53
|
+
match value {
|
54
|
+
Some("application/json") => ResponseFormat::JSON,
|
55
|
+
Some("text/html") => ResponseFormat::HTML,
|
56
|
+
Some("text/plain") => ResponseFormat::TEXT,
|
57
|
+
_ => ResponseFormat::UNKNOWN,
|
58
|
+
}
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
impl PathExt for str {
|
63
|
+
fn no_trailing_slash(&self) -> &str {
|
64
|
+
if self == "/" {
|
65
|
+
self
|
66
|
+
} else {
|
67
|
+
self.trim_end_matches("/")
|
68
|
+
}
|
69
|
+
}
|
70
|
+
}
|
71
|
+
|
72
|
+
impl RequestExt for HttpRequest {
|
73
|
+
fn content_type(&self) -> Option<&str> {
|
74
|
+
self.headers()
|
75
|
+
.get("content-type")
|
76
|
+
.map(|hv| hv.to_str().unwrap_or(""))
|
77
|
+
}
|
78
|
+
|
79
|
+
fn accept(&self) -> Option<&str> {
|
80
|
+
self.headers()
|
81
|
+
.get("accept")
|
82
|
+
.map(|hv| hv.to_str().unwrap_or(""))
|
83
|
+
}
|
84
|
+
|
85
|
+
fn header(&self, header_name: &str) -> Option<&str> {
|
86
|
+
self.headers()
|
87
|
+
.get(header_name)
|
88
|
+
.map(|hv| hv.to_str().unwrap_or(""))
|
89
|
+
}
|
90
|
+
|
91
|
+
fn query_param(&self, query_name: &str) -> Option<&str> {
|
92
|
+
self.uri()
|
93
|
+
.query()
|
94
|
+
.and_then(|query| query.split('&').find(|param| param.starts_with(query_name)))
|
95
|
+
.map(|param| param.split('=').nth(1).unwrap_or(""))
|
96
|
+
}
|
97
|
+
}
|
@@ -1,4 +1,3 @@
|
|
1
|
-
use super::listener::SockAddr;
|
2
1
|
use pin_project::pin_project;
|
3
2
|
use tokio::net::{TcpStream, UnixStream};
|
4
3
|
use tokio_rustls::server::TlsStream;
|
@@ -8,6 +7,8 @@ use std::pin::Pin;
|
|
8
7
|
use std::task::{Context, Poll};
|
9
8
|
use tokio::io::{AsyncRead, AsyncWrite};
|
10
9
|
|
10
|
+
use super::binds::listener::SockAddr;
|
11
|
+
|
11
12
|
#[pin_project(project = IoStreamEnumProj)]
|
12
13
|
pub enum IoStream {
|
13
14
|
Tcp {
|
@@ -1,8 +1,10 @@
|
|
1
|
-
use
|
2
|
-
|
3
|
-
|
4
|
-
types::{HttpRequest, HttpResponse},
|
1
|
+
use crate::{
|
2
|
+
server::http_message_types::{HttpRequest, HttpResponse},
|
3
|
+
services::itsi_http_service::HttpRequestContext,
|
5
4
|
};
|
5
|
+
|
6
|
+
use super::middlewares::*;
|
7
|
+
|
6
8
|
use async_trait::async_trait;
|
7
9
|
use either::Either;
|
8
10
|
use magnus::error::Result;
|
@@ -21,6 +23,7 @@ pub enum Middleware {
|
|
21
23
|
ETag(ETag),
|
22
24
|
IntrusionProtection(IntrusionProtection),
|
23
25
|
LogRequests(LogRequests),
|
26
|
+
MaxBody(MaxBody),
|
24
27
|
Proxy(Proxy),
|
25
28
|
RateLimit(RateLimit),
|
26
29
|
Redirect(Redirect),
|
@@ -28,6 +31,7 @@ pub enum Middleware {
|
|
28
31
|
ResponseHeaders(ResponseHeaders),
|
29
32
|
RubyApp(RubyApp),
|
30
33
|
StaticAssets(StaticAssets),
|
34
|
+
StaticResponse(StaticResponse),
|
31
35
|
}
|
32
36
|
|
33
37
|
#[async_trait]
|
@@ -41,6 +45,7 @@ impl MiddlewareLayer for Middleware {
|
|
41
45
|
Middleware::AuthJwt(filter) => filter.initialize().await,
|
42
46
|
Middleware::AuthAPIKey(filter) => filter.initialize().await,
|
43
47
|
Middleware::IntrusionProtection(filter) => filter.initialize().await,
|
48
|
+
Middleware::MaxBody(filter) => filter.initialize().await,
|
44
49
|
Middleware::RateLimit(filter) => filter.initialize().await,
|
45
50
|
Middleware::RequestHeaders(filter) => filter.initialize().await,
|
46
51
|
Middleware::ResponseHeaders(filter) => filter.initialize().await,
|
@@ -48,6 +53,7 @@ impl MiddlewareLayer for Middleware {
|
|
48
53
|
Middleware::Cors(filter) => filter.initialize().await,
|
49
54
|
Middleware::ETag(filter) => filter.initialize().await,
|
50
55
|
Middleware::StaticAssets(filter) => filter.initialize().await,
|
56
|
+
Middleware::StaticResponse(filter) => filter.initialize().await,
|
51
57
|
Middleware::Compression(filter) => filter.initialize().await,
|
52
58
|
Middleware::LogRequests(filter) => filter.initialize().await,
|
53
59
|
Middleware::Redirect(filter) => filter.initialize().await,
|
@@ -59,7 +65,7 @@ impl MiddlewareLayer for Middleware {
|
|
59
65
|
async fn before(
|
60
66
|
&self,
|
61
67
|
req: HttpRequest,
|
62
|
-
context: &mut
|
68
|
+
context: &mut HttpRequestContext,
|
63
69
|
) -> Result<Either<HttpRequest, HttpResponse>> {
|
64
70
|
match self {
|
65
71
|
Middleware::DenyList(filter) => filter.before(req, context).await,
|
@@ -68,6 +74,7 @@ impl MiddlewareLayer for Middleware {
|
|
68
74
|
Middleware::AuthJwt(filter) => filter.before(req, context).await,
|
69
75
|
Middleware::AuthAPIKey(filter) => filter.before(req, context).await,
|
70
76
|
Middleware::IntrusionProtection(filter) => filter.before(req, context).await,
|
77
|
+
Middleware::MaxBody(filter) => filter.before(req, context).await,
|
71
78
|
Middleware::RequestHeaders(filter) => filter.before(req, context).await,
|
72
79
|
Middleware::ResponseHeaders(filter) => filter.before(req, context).await,
|
73
80
|
Middleware::RateLimit(filter) => filter.before(req, context).await,
|
@@ -75,6 +82,7 @@ impl MiddlewareLayer for Middleware {
|
|
75
82
|
Middleware::Cors(filter) => filter.before(req, context).await,
|
76
83
|
Middleware::ETag(filter) => filter.before(req, context).await,
|
77
84
|
Middleware::StaticAssets(filter) => filter.before(req, context).await,
|
85
|
+
Middleware::StaticResponse(filter) => filter.before(req, context).await,
|
78
86
|
Middleware::Compression(filter) => filter.before(req, context).await,
|
79
87
|
Middleware::LogRequests(filter) => filter.before(req, context).await,
|
80
88
|
Middleware::Redirect(filter) => filter.before(req, context).await,
|
@@ -83,7 +91,7 @@ impl MiddlewareLayer for Middleware {
|
|
83
91
|
}
|
84
92
|
}
|
85
93
|
|
86
|
-
async fn after(&self, res: HttpResponse, context: &mut
|
94
|
+
async fn after(&self, res: HttpResponse, context: &mut HttpRequestContext) -> HttpResponse {
|
87
95
|
match self {
|
88
96
|
Middleware::DenyList(filter) => filter.after(res, context).await,
|
89
97
|
Middleware::AllowList(filter) => filter.after(res, context).await,
|
@@ -91,6 +99,7 @@ impl MiddlewareLayer for Middleware {
|
|
91
99
|
Middleware::AuthJwt(filter) => filter.after(res, context).await,
|
92
100
|
Middleware::AuthAPIKey(filter) => filter.after(res, context).await,
|
93
101
|
Middleware::IntrusionProtection(filter) => filter.after(res, context).await,
|
102
|
+
Middleware::MaxBody(filter) => filter.after(res, context).await,
|
94
103
|
Middleware::RateLimit(filter) => filter.after(res, context).await,
|
95
104
|
Middleware::RequestHeaders(filter) => filter.after(res, context).await,
|
96
105
|
Middleware::ResponseHeaders(filter) => filter.after(res, context).await,
|
@@ -98,6 +107,7 @@ impl MiddlewareLayer for Middleware {
|
|
98
107
|
Middleware::Cors(filter) => filter.after(res, context).await,
|
99
108
|
Middleware::ETag(filter) => filter.after(res, context).await,
|
100
109
|
Middleware::StaticAssets(filter) => filter.after(res, context).await,
|
110
|
+
Middleware::StaticResponse(filter) => filter.after(res, context).await,
|
101
111
|
Middleware::Compression(filter) => filter.after(res, context).await,
|
102
112
|
Middleware::LogRequests(filter) => filter.after(res, context).await,
|
103
113
|
Middleware::Redirect(filter) => filter.after(res, context).await,
|
@@ -118,16 +128,18 @@ impl Middleware {
|
|
118
128
|
Middleware::CacheControl(_) => 5,
|
119
129
|
Middleware::RequestHeaders(_) => 6,
|
120
130
|
Middleware::ResponseHeaders(_) => 7,
|
121
|
-
Middleware::
|
122
|
-
Middleware::
|
123
|
-
Middleware::
|
124
|
-
Middleware::
|
125
|
-
Middleware::
|
126
|
-
Middleware::
|
127
|
-
Middleware::
|
128
|
-
Middleware::
|
129
|
-
Middleware::
|
130
|
-
Middleware::
|
131
|
+
Middleware::MaxBody(_) => 8,
|
132
|
+
Middleware::AuthBasic(_) => 9,
|
133
|
+
Middleware::AuthJwt(_) => 10,
|
134
|
+
Middleware::AuthAPIKey(_) => 11,
|
135
|
+
Middleware::RateLimit(_) => 12,
|
136
|
+
Middleware::ETag(_) => 13,
|
137
|
+
Middleware::Compression(_) => 14,
|
138
|
+
Middleware::Proxy(_) => 15,
|
139
|
+
Middleware::Cors(_) => 16,
|
140
|
+
Middleware::StaticResponse(_) => 17,
|
141
|
+
Middleware::StaticAssets(_) => 18,
|
142
|
+
Middleware::RubyApp(_) => 19,
|
131
143
|
}
|
132
144
|
}
|
133
145
|
}
|
@@ -1,8 +1,10 @@
|
|
1
|
-
use
|
2
|
-
|
3
|
-
|
4
|
-
types::{HttpRequest, HttpResponse},
|
1
|
+
use crate::{
|
2
|
+
server::http_message_types::{HttpRequest, HttpResponse, RequestExt},
|
3
|
+
services::itsi_http_service::HttpRequestContext,
|
5
4
|
};
|
5
|
+
|
6
|
+
use super::{ErrorResponse, FromValue, MiddlewareLayer};
|
7
|
+
|
6
8
|
use async_trait::async_trait;
|
7
9
|
use either::Either;
|
8
10
|
use itsi_error::ItsiError;
|
@@ -16,28 +18,35 @@ pub struct AllowList {
|
|
16
18
|
#[serde(skip_deserializing)]
|
17
19
|
pub allowed_ips: OnceLock<RegexSet>,
|
18
20
|
pub allowed_patterns: Vec<String>,
|
21
|
+
#[serde(default = "forbidden_error_response")]
|
19
22
|
pub error_response: ErrorResponse,
|
20
23
|
}
|
21
24
|
|
25
|
+
fn forbidden_error_response() -> ErrorResponse {
|
26
|
+
ErrorResponse::forbidden()
|
27
|
+
}
|
28
|
+
|
22
29
|
#[async_trait]
|
23
30
|
impl MiddlewareLayer for AllowList {
|
24
31
|
async fn initialize(&self) -> Result<()> {
|
25
|
-
let allowed_ips = RegexSet::new(&self.allowed_patterns).map_err(ItsiError::
|
32
|
+
let allowed_ips = RegexSet::new(&self.allowed_patterns).map_err(ItsiError::new)?;
|
26
33
|
self.allowed_ips
|
27
34
|
.set(allowed_ips)
|
28
|
-
.map_err(|e| ItsiError::
|
35
|
+
.map_err(|e| ItsiError::new(format!("Failed to set allowed IPs: {:?}", e)))?;
|
29
36
|
Ok(())
|
30
37
|
}
|
31
38
|
|
32
39
|
async fn before(
|
33
40
|
&self,
|
34
41
|
req: HttpRequest,
|
35
|
-
context: &mut
|
42
|
+
context: &mut HttpRequestContext,
|
36
43
|
) -> Result<Either<HttpRequest, HttpResponse>> {
|
37
44
|
if let Some(allowed_ips) = self.allowed_ips.get() {
|
38
45
|
if !allowed_ips.is_match(&context.addr) {
|
39
46
|
return Ok(Either::Right(
|
40
|
-
self.error_response
|
47
|
+
self.error_response
|
48
|
+
.to_http_response(req.accept().into())
|
49
|
+
.await,
|
41
50
|
));
|
42
51
|
}
|
43
52
|
}
|
@@ -1,6 +1,8 @@
|
|
1
|
-
use
|
2
|
-
|
3
|
-
|
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},
|
4
6
|
};
|
5
7
|
|
6
8
|
use super::{error_response::ErrorResponse, token_source::TokenSource, FromValue, MiddlewareLayer};
|
@@ -10,25 +12,32 @@ use either::Either;
|
|
10
12
|
use magnus::error::Result;
|
11
13
|
use serde::Deserialize;
|
12
14
|
|
15
|
+
type PasswordHash = String;
|
16
|
+
|
13
17
|
/// A simple API key filter.
|
14
18
|
/// The API key can be given inside the header or a query string
|
15
19
|
/// Keys are validated against a list of allowed key values (Changing these requires a restart)
|
16
|
-
///
|
17
20
|
#[derive(Debug, Clone, Deserialize)]
|
18
21
|
pub struct AuthAPIKey {
|
19
|
-
pub valid_keys:
|
22
|
+
pub valid_keys: HashMap<String, PasswordHash>,
|
23
|
+
pub key_id_source: Option<TokenSource>,
|
20
24
|
pub token_source: TokenSource,
|
25
|
+
#[serde(default = "unauthorized_error_response")]
|
21
26
|
pub error_response: ErrorResponse,
|
22
27
|
}
|
23
28
|
|
29
|
+
fn unauthorized_error_response() -> ErrorResponse {
|
30
|
+
ErrorResponse::unauthorized()
|
31
|
+
}
|
32
|
+
|
24
33
|
#[async_trait]
|
25
34
|
impl MiddlewareLayer for AuthAPIKey {
|
26
35
|
async fn before(
|
27
36
|
&self,
|
28
37
|
req: HttpRequest,
|
29
|
-
_context: &mut
|
38
|
+
_context: &mut HttpRequestContext,
|
30
39
|
) -> Result<Either<HttpRequest, HttpResponse>> {
|
31
|
-
let
|
40
|
+
if let Some(submitted_key) = match &self.token_source {
|
32
41
|
TokenSource::Header { name, prefix } => {
|
33
42
|
if let Some(header) = req.header(name) {
|
34
43
|
if let Some(prefix) = prefix {
|
@@ -41,18 +50,38 @@ impl MiddlewareLayer for AuthAPIKey {
|
|
41
50
|
}
|
42
51
|
}
|
43
52
|
TokenSource::Query(query_name) => req.query_param(query_name),
|
44
|
-
}
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
+
}
|
55
79
|
}
|
80
|
+
Ok(Either::Right(
|
81
|
+
self.error_response
|
82
|
+
.to_http_response(req.accept().into())
|
83
|
+
.await,
|
84
|
+
))
|
56
85
|
}
|
57
86
|
}
|
58
87
|
impl FromValue for AuthAPIKey {}
|
@@ -9,18 +9,20 @@ use serde::{Deserialize, Serialize};
|
|
9
9
|
use std::collections::HashMap;
|
10
10
|
use std::str;
|
11
11
|
|
12
|
-
use crate::
|
13
|
-
|
14
|
-
|
12
|
+
use crate::{
|
13
|
+
server::http_message_types::{HttpRequest, HttpResponse, RequestExt},
|
14
|
+
services::{itsi_http_service::HttpRequestContext, password_hasher::verify_password_hash},
|
15
15
|
};
|
16
16
|
|
17
17
|
use super::{FromValue, MiddlewareLayer};
|
18
18
|
|
19
|
+
type PasswordHash = String;
|
20
|
+
|
19
21
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
20
22
|
pub struct AuthBasic {
|
21
23
|
pub realm: String,
|
22
24
|
/// Maps usernames to passwords.
|
23
|
-
pub credential_pairs: HashMap<String,
|
25
|
+
pub credential_pairs: HashMap<String, PasswordHash>,
|
24
26
|
}
|
25
27
|
|
26
28
|
impl AuthBasic {
|
@@ -40,7 +42,7 @@ impl MiddlewareLayer for AuthBasic {
|
|
40
42
|
async fn before(
|
41
43
|
&self,
|
42
44
|
req: HttpRequest,
|
43
|
-
_context: &mut
|
45
|
+
_context: &mut HttpRequestContext,
|
44
46
|
) -> Result<Either<HttpRequest, HttpResponse>> {
|
45
47
|
// Retrieve the Authorization header.
|
46
48
|
let auth_header = req.header("Authorization");
|
@@ -69,12 +71,14 @@ impl MiddlewareLayer for AuthBasic {
|
|
69
71
|
let mut parts = decoded_str.splitn(2, ':');
|
70
72
|
let username = parts.next().unwrap_or("");
|
71
73
|
let password = parts.next().unwrap_or("");
|
72
|
-
|
73
74
|
match self.credential_pairs.get(username) {
|
74
|
-
Some(
|
75
|
-
|
76
|
-
|
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
|
+
}
|
77
80
|
}
|
81
|
+
None => Ok(Either::Right(self.basic_auth_failed_response())),
|
78
82
|
}
|
79
83
|
}
|
80
84
|
}
|
@@ -1,8 +1,9 @@
|
|
1
1
|
use super::{error_response::ErrorResponse, token_source::TokenSource, FromValue, MiddlewareLayer};
|
2
|
-
use crate::
|
3
|
-
|
4
|
-
|
2
|
+
use crate::{
|
3
|
+
server::http_message_types::{HttpRequest, HttpResponse, RequestExt},
|
4
|
+
services::itsi_http_service::HttpRequestContext,
|
5
5
|
};
|
6
|
+
|
6
7
|
use async_trait::async_trait;
|
7
8
|
use base64::{engine::general_purpose, Engine};
|
8
9
|
use derive_more::Debug;
|
@@ -17,6 +18,7 @@ use std::{
|
|
17
18
|
collections::{HashMap, HashSet},
|
18
19
|
sync::OnceLock,
|
19
20
|
};
|
21
|
+
use tracing::error;
|
20
22
|
|
21
23
|
#[derive(Debug, Clone, Deserialize)]
|
22
24
|
pub struct AuthJwt {
|
@@ -31,9 +33,14 @@ pub struct AuthJwt {
|
|
31
33
|
pub subjects: Option<HashSet<String>>,
|
32
34
|
pub issuers: Option<HashSet<String>>,
|
33
35
|
pub leeway: Option<u64>,
|
36
|
+
#[serde(default = "unauthorized_error_response")]
|
34
37
|
pub error_response: ErrorResponse,
|
35
38
|
}
|
36
39
|
|
40
|
+
fn unauthorized_error_response() -> ErrorResponse {
|
41
|
+
ErrorResponse::unauthorized()
|
42
|
+
}
|
43
|
+
|
37
44
|
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Hash)]
|
38
45
|
pub enum JwtAlgorithm {
|
39
46
|
#[serde(rename(deserialize = "hs256"))]
|
@@ -83,13 +90,14 @@ impl From<JwtAlg> for JwtAlgorithm {
|
|
83
90
|
impl JwtAlgorithm {
|
84
91
|
/// Given a base64-encoded key string, decode and construct a jsonwebtoken::DecodingKey.
|
85
92
|
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
93
|
match self {
|
90
94
|
// For HMAC algorithms, use the secret directly.
|
91
95
|
JwtAlgorithm::Hs256 | JwtAlgorithm::Hs384 | JwtAlgorithm::Hs512 => {
|
92
|
-
Ok(DecodingKey::from_secret(
|
96
|
+
Ok(DecodingKey::from_secret(
|
97
|
+
&general_purpose::STANDARD
|
98
|
+
.decode(base64)
|
99
|
+
.map_err(ItsiError::new)?,
|
100
|
+
))
|
93
101
|
}
|
94
102
|
// For RSA (and PS) algorithms, expect a PEM-formatted key.
|
95
103
|
JwtAlgorithm::Rs256
|
@@ -97,12 +105,12 @@ impl JwtAlgorithm {
|
|
97
105
|
| JwtAlgorithm::Rs512
|
98
106
|
| JwtAlgorithm::Ps256
|
99
107
|
| JwtAlgorithm::Ps384
|
100
|
-
| JwtAlgorithm::Ps512 =>
|
101
|
-
|
102
|
-
}
|
108
|
+
| JwtAlgorithm::Ps512 => DecodingKey::from_rsa_pem(base64.trim_ascii().as_bytes())
|
109
|
+
.map_err(|e| ItsiError::new(e.to_string())),
|
103
110
|
// For ECDSA algorithms, expect a PEM-formatted key.
|
104
111
|
JwtAlgorithm::Es256 | JwtAlgorithm::Es384 => {
|
105
|
-
DecodingKey::from_ec_pem(
|
112
|
+
DecodingKey::from_ec_pem(base64.trim_ascii().as_bytes())
|
113
|
+
.map_err(|e| ItsiError::new(e.to_string()))
|
106
114
|
}
|
107
115
|
}
|
108
116
|
}
|
@@ -143,14 +151,14 @@ impl MiddlewareLayer for AuthJwt {
|
|
143
151
|
.collect::<itsi_error::Result<HashMap<JwtAlgorithm, Vec<DecodingKey>>>>()?;
|
144
152
|
self.keys
|
145
153
|
.set(keys)
|
146
|
-
.map_err(|_| ItsiError::
|
154
|
+
.map_err(|_| ItsiError::new("Failed to set keys"))?;
|
147
155
|
Ok(())
|
148
156
|
}
|
149
157
|
|
150
158
|
async fn before(
|
151
159
|
&self,
|
152
160
|
req: HttpRequest,
|
153
|
-
_context: &mut
|
161
|
+
_context: &mut HttpRequestContext,
|
154
162
|
) -> Result<Either<HttpRequest, HttpResponse>> {
|
155
163
|
// Retrieve the JWT token from either a header or a query parameter.
|
156
164
|
let token_str = match &self.token_source {
|
@@ -170,19 +178,21 @@ impl MiddlewareLayer for AuthJwt {
|
|
170
178
|
|
171
179
|
if token_str.is_none() {
|
172
180
|
return Ok(Either::Right(
|
173
|
-
self.error_response
|
181
|
+
self.error_response
|
182
|
+
.to_http_response(req.accept().into())
|
183
|
+
.await,
|
174
184
|
));
|
175
185
|
}
|
176
186
|
let token_str = token_str.unwrap();
|
177
|
-
|
178
|
-
// Use jsonwebtoken's decode_header to inspect the token and determine its algorithm.
|
179
187
|
let header =
|
180
|
-
decode_header(token_str).map_err(|_| ItsiError::
|
188
|
+
decode_header(token_str).map_err(|_| ItsiError::new("Invalid token header"))?;
|
181
189
|
let alg: JwtAlgorithm = header.alg.into();
|
182
190
|
|
183
191
|
if !self.verifiers.contains_key(&alg) {
|
184
192
|
return Ok(Either::Right(
|
185
|
-
self.error_response
|
193
|
+
self.error_response
|
194
|
+
.to_http_response(req.accept().into())
|
195
|
+
.await,
|
186
196
|
));
|
187
197
|
}
|
188
198
|
let keys = self.keys.get().unwrap().get(&alg).unwrap();
|
@@ -201,26 +211,32 @@ impl MiddlewareLayer for AuthJwt {
|
|
201
211
|
JwtAlgorithm::Ps384 => JwtAlg::PS384,
|
202
212
|
JwtAlgorithm::Ps512 => JwtAlg::PS512,
|
203
213
|
});
|
214
|
+
|
204
215
|
if let Some(leeway) = self.leeway {
|
205
216
|
validation.leeway = leeway;
|
206
217
|
}
|
207
|
-
// (Optional) You could set expected issuer or audience on `validation` here.
|
208
218
|
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
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
|
+
});
|
213
228
|
let token_data = if let Some(data) = token_data {
|
214
229
|
data
|
215
230
|
} else {
|
216
231
|
return Ok(Either::Right(
|
217
|
-
self.error_response
|
232
|
+
self.error_response
|
233
|
+
.to_http_response(req.accept().into())
|
234
|
+
.await,
|
218
235
|
));
|
219
236
|
};
|
220
237
|
|
221
238
|
let claims = token_data.claims;
|
222
239
|
|
223
|
-
// Verify expected audiences.
|
224
240
|
if let Some(expected_audiences) = &self.audiences {
|
225
241
|
if let Some(aud) = &claims.aud {
|
226
242
|
let token_auds: HashSet<String> = match aud {
|
@@ -229,18 +245,21 @@ impl MiddlewareLayer for AuthJwt {
|
|
229
245
|
};
|
230
246
|
if expected_audiences.is_disjoint(&token_auds) {
|
231
247
|
return Ok(Either::Right(
|
232
|
-
self.error_response
|
248
|
+
self.error_response
|
249
|
+
.to_http_response(req.accept().into())
|
250
|
+
.await,
|
233
251
|
));
|
234
252
|
}
|
235
253
|
}
|
236
254
|
}
|
237
255
|
|
238
|
-
// Verify expected subject.
|
239
256
|
if let Some(expected_subjects) = &self.subjects {
|
240
257
|
if let Some(sub) = &claims.sub {
|
241
258
|
if !expected_subjects.contains(sub) {
|
242
259
|
return Ok(Either::Right(
|
243
|
-
self.error_response
|
260
|
+
self.error_response
|
261
|
+
.to_http_response(req.accept().into())
|
262
|
+
.await,
|
244
263
|
));
|
245
264
|
}
|
246
265
|
}
|
@@ -251,7 +270,9 @@ impl MiddlewareLayer for AuthJwt {
|
|
251
270
|
if let Some(iss) = &claims.iss {
|
252
271
|
if !expected_issuers.contains(iss) {
|
253
272
|
return Ok(Either::Right(
|
254
|
-
self.error_response
|
273
|
+
self.error_response
|
274
|
+
.to_http_response(req.accept().into())
|
275
|
+
.await,
|
255
276
|
));
|
256
277
|
}
|
257
278
|
}
|