itsi 0.1.14 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +124 -109
  3. data/Cargo.toml +6 -0
  4. data/crates/itsi_error/Cargo.toml +1 -0
  5. data/crates/itsi_error/src/lib.rs +100 -10
  6. data/crates/itsi_scheduler/src/itsi_scheduler.rs +1 -1
  7. data/crates/itsi_server/Cargo.toml +8 -10
  8. data/crates/itsi_server/src/default_responses/html/401.html +68 -0
  9. data/crates/itsi_server/src/default_responses/html/403.html +68 -0
  10. data/crates/itsi_server/src/default_responses/html/404.html +68 -0
  11. data/crates/itsi_server/src/default_responses/html/413.html +71 -0
  12. data/crates/itsi_server/src/default_responses/html/429.html +68 -0
  13. data/crates/itsi_server/src/default_responses/html/500.html +71 -0
  14. data/crates/itsi_server/src/default_responses/html/502.html +71 -0
  15. data/crates/itsi_server/src/default_responses/html/503.html +68 -0
  16. data/crates/itsi_server/src/default_responses/html/504.html +69 -0
  17. data/crates/itsi_server/src/default_responses/html/index.html +238 -0
  18. data/crates/itsi_server/src/default_responses/json/401.json +6 -0
  19. data/crates/itsi_server/src/default_responses/json/403.json +6 -0
  20. data/crates/itsi_server/src/default_responses/json/404.json +6 -0
  21. data/crates/itsi_server/src/default_responses/json/413.json +6 -0
  22. data/crates/itsi_server/src/default_responses/json/429.json +6 -0
  23. data/crates/itsi_server/src/default_responses/json/500.json +6 -0
  24. data/crates/itsi_server/src/default_responses/json/502.json +6 -0
  25. data/crates/itsi_server/src/default_responses/json/503.json +6 -0
  26. data/crates/itsi_server/src/default_responses/json/504.json +6 -0
  27. data/crates/itsi_server/src/default_responses/mod.rs +11 -0
  28. data/crates/itsi_server/src/lib.rs +58 -26
  29. data/crates/itsi_server/src/prelude.rs +2 -0
  30. data/crates/itsi_server/src/ruby_types/README.md +21 -0
  31. data/crates/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +8 -6
  32. data/crates/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
  33. data/crates/itsi_server/src/ruby_types/{itsi_grpc_stream → itsi_grpc_response_stream}/mod.rs +121 -73
  34. data/crates/itsi_server/src/ruby_types/itsi_http_request.rs +103 -40
  35. data/crates/itsi_server/src/ruby_types/itsi_http_response.rs +8 -5
  36. data/crates/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +4 -4
  37. data/crates/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +37 -17
  38. data/crates/itsi_server/src/ruby_types/itsi_server.rs +4 -3
  39. data/crates/itsi_server/src/ruby_types/mod.rs +6 -13
  40. data/crates/itsi_server/src/server/{bind.rs → binds/bind.rs} +23 -4
  41. data/crates/itsi_server/src/server/{listener.rs → binds/listener.rs} +24 -10
  42. data/crates/itsi_server/src/server/binds/mod.rs +4 -0
  43. data/crates/itsi_server/src/server/{tls.rs → binds/tls.rs} +9 -4
  44. data/crates/itsi_server/src/server/http_message_types.rs +97 -0
  45. data/crates/itsi_server/src/server/io_stream.rs +2 -1
  46. data/crates/itsi_server/src/server/middleware_stack/middleware.rs +28 -16
  47. data/crates/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +17 -8
  48. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +47 -18
  49. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +13 -9
  50. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +50 -29
  51. data/crates/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +5 -2
  52. data/crates/itsi_server/src/server/middleware_stack/middlewares/compression.rs +37 -48
  53. data/crates/itsi_server/src/server/middleware_stack/middlewares/cors.rs +25 -20
  54. data/crates/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +14 -7
  55. data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +190 -0
  56. data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +125 -95
  57. data/crates/itsi_server/src/server/middleware_stack/middlewares/etag.rs +9 -5
  58. data/crates/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +1 -4
  59. data/crates/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +25 -19
  60. data/crates/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +4 -4
  61. data/crates/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
  62. data/crates/itsi_server/src/server/middleware_stack/middlewares/mod.rs +9 -4
  63. data/crates/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +260 -62
  64. data/crates/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +29 -22
  65. data/crates/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +6 -6
  66. data/crates/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +6 -5
  67. data/crates/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +4 -2
  68. data/crates/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +51 -18
  69. data/crates/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +31 -13
  70. data/crates/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
  71. data/crates/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +13 -8
  72. data/crates/itsi_server/src/server/middleware_stack/mod.rs +101 -69
  73. data/crates/itsi_server/src/server/mod.rs +3 -9
  74. data/crates/itsi_server/src/server/process_worker.rs +21 -3
  75. data/crates/itsi_server/src/server/request_job.rs +2 -2
  76. data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +8 -3
  77. data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +26 -26
  78. data/crates/itsi_server/src/server/signal.rs +24 -41
  79. data/crates/itsi_server/src/server/size_limited_incoming.rs +101 -0
  80. data/crates/itsi_server/src/server/thread_worker.rs +59 -28
  81. data/crates/itsi_server/src/services/itsi_http_service.rs +239 -0
  82. data/crates/itsi_server/src/services/mime_types.rs +1416 -0
  83. data/crates/itsi_server/src/services/mod.rs +6 -0
  84. data/crates/itsi_server/src/services/password_hasher.rs +83 -0
  85. data/crates/itsi_server/src/{server → services}/rate_limiter.rs +35 -31
  86. data/crates/itsi_server/src/{server → services}/static_file_server.rs +521 -181
  87. data/crates/itsi_tracing/src/lib.rs +145 -55
  88. data/{Itsi.rb → foo/Itsi.rb} +6 -9
  89. data/gems/scheduler/Cargo.lock +7 -0
  90. data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
  91. data/gems/scheduler/test/helpers/test_helper.rb +0 -1
  92. data/gems/scheduler/test/test_address_resolve.rb +0 -1
  93. data/gems/scheduler/test/test_network_io.rb +1 -1
  94. data/gems/scheduler/test/test_process_wait.rb +0 -1
  95. data/gems/server/Cargo.lock +124 -109
  96. data/gems/server/exe/itsi +65 -19
  97. data/gems/server/itsi-server.gemspec +4 -3
  98. data/gems/server/lib/itsi/http_request/response_status_shortcodes.rb +74 -0
  99. data/gems/server/lib/itsi/http_request.rb +116 -17
  100. data/gems/server/lib/itsi/http_response.rb +2 -0
  101. data/gems/server/lib/itsi/passfile.rb +109 -0
  102. data/gems/server/lib/itsi/server/config/dsl.rb +160 -101
  103. data/gems/server/lib/itsi/server/config.rb +58 -23
  104. data/gems/server/lib/itsi/server/default_app/default_app.rb +25 -29
  105. data/gems/server/lib/itsi/server/default_app/index.html +113 -89
  106. data/gems/server/lib/itsi/server/{Itsi.rb → default_config/Itsi-rackup.rb} +1 -1
  107. data/gems/server/lib/itsi/server/default_config/Itsi.rb +107 -0
  108. data/gems/server/lib/itsi/server/grpc/grpc_call.rb +246 -0
  109. data/gems/server/lib/itsi/server/grpc/grpc_interface.rb +100 -0
  110. data/gems/server/lib/itsi/server/grpc/reflection/v1/reflection_pb.rb +26 -0
  111. data/gems/server/lib/itsi/server/grpc/reflection/v1/reflection_services_pb.rb +122 -0
  112. data/gems/server/lib/itsi/server/route_tester.rb +107 -0
  113. data/gems/server/lib/itsi/server/typed_handlers/param_parser.rb +200 -0
  114. data/gems/server/lib/itsi/server/typed_handlers/source_parser.rb +55 -0
  115. data/gems/server/lib/itsi/server/typed_handlers.rb +17 -0
  116. data/gems/server/lib/itsi/server/version.rb +1 -1
  117. data/gems/server/lib/itsi/server.rb +82 -12
  118. data/gems/server/lib/ruby_lsp/itsi/addon.rb +111 -0
  119. data/gems/server/lib/shell_completions/completions.rb +26 -0
  120. data/gems/server/test/helpers/test_helper.rb +2 -1
  121. data/lib/itsi/version.rb +1 -1
  122. data/sandbox/README.md +5 -0
  123. data/sandbox/itsi_file/Gemfile +4 -2
  124. data/sandbox/itsi_file/Gemfile.lock +48 -6
  125. data/sandbox/itsi_file/Itsi.rb +326 -129
  126. data/sandbox/itsi_file/call.json +1 -0
  127. data/sandbox/itsi_file/echo_client/Gemfile +10 -0
  128. data/sandbox/itsi_file/echo_client/Gemfile.lock +27 -0
  129. data/sandbox/itsi_file/echo_client/README.md +95 -0
  130. data/sandbox/itsi_file/echo_client/echo_client.rb +164 -0
  131. data/sandbox/itsi_file/echo_client/gen_proto.sh +17 -0
  132. data/sandbox/itsi_file/echo_client/lib/echo_pb.rb +16 -0
  133. data/sandbox/itsi_file/echo_client/lib/echo_services_pb.rb +29 -0
  134. data/sandbox/itsi_file/echo_client/run_client.rb +64 -0
  135. data/sandbox/itsi_file/echo_client/test_compressions.sh +20 -0
  136. data/sandbox/itsi_file/echo_service_nonitsi/Gemfile +10 -0
  137. data/sandbox/itsi_file/echo_service_nonitsi/Gemfile.lock +79 -0
  138. data/sandbox/itsi_file/echo_service_nonitsi/echo.proto +26 -0
  139. data/sandbox/itsi_file/echo_service_nonitsi/echo_pb.rb +16 -0
  140. data/sandbox/itsi_file/echo_service_nonitsi/echo_services_pb.rb +29 -0
  141. data/sandbox/itsi_file/echo_service_nonitsi/server.rb +52 -0
  142. data/sandbox/itsi_sandbox_async/config.ru +0 -1
  143. data/sandbox/itsi_sandbox_rack/Gemfile.lock +2 -2
  144. data/sandbox/itsi_sandbox_rails/Gemfile +2 -2
  145. data/sandbox/itsi_sandbox_rails/Gemfile.lock +76 -2
  146. data/sandbox/itsi_sandbox_rails/app/controllers/home_controller.rb +15 -0
  147. data/sandbox/itsi_sandbox_rails/config/environments/development.rb +1 -0
  148. data/sandbox/itsi_sandbox_rails/config/environments/production.rb +1 -0
  149. data/sandbox/itsi_sandbox_rails/config/routes.rb +2 -0
  150. data/sandbox/itsi_sinatra/app.rb +0 -1
  151. data/sandbox/static_files/.env +1 -0
  152. data/sandbox/static_files/404.html +25 -0
  153. data/sandbox/static_files/_DSC0102.NEF.jpg +0 -0
  154. data/sandbox/static_files/about.html +68 -0
  155. data/sandbox/static_files/tiny.html +1 -0
  156. data/sandbox/static_files/writebook.zip +0 -0
  157. data/tasks.txt +28 -33
  158. metadata +87 -26
  159. data/crates/itsi_error/src/from.rs +0 -68
  160. data/crates/itsi_server/src/ruby_types/itsi_grpc_request.rs +0 -147
  161. data/crates/itsi_server/src/ruby_types/itsi_grpc_response.rs +0 -19
  162. data/crates/itsi_server/src/server/itsi_service.rs +0 -172
  163. data/crates/itsi_server/src/server/middleware_stack/middlewares/grpc_service.rs +0 -72
  164. data/crates/itsi_server/src/server/types.rs +0 -43
  165. data/gems/server/lib/itsi/server/grpc_interface.rb +0 -213
  166. data/sandbox/itsi_file/public/assets/index.html +0 -1
  167. /data/crates/itsi_server/src/server/{bind_protocol.rs → binds/bind_protocol.rs} +0 -0
  168. /data/crates/itsi_server/src/server/{tls → binds/tls}/locked_dir_cache.rs +0 -0
  169. /data/crates/itsi_server/src/{server → services}/cache_store.rs +0 -0
@@ -0,0 +1,4 @@
1
+ pub mod bind;
2
+ pub mod bind_protocol;
3
+ pub mod listener;
4
+ pub mod tls;
@@ -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
- generate_simple_self_signed, CertificateParams, CertifiedKey, DnType, KeyPair, SanType,
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 CertifiedKey { cert, key_pair } =
258
- generate_simple_self_signed(subject_alt_names).unwrap();
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 super::middlewares::*;
2
- use crate::server::{
3
- itsi_service::RequestContext,
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 RequestContext,
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 RequestContext) -> HttpResponse {
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::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
+ 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 super::{ErrorResponse, FromValue, MiddlewareLayer};
2
- use crate::server::{
3
- itsi_service::RequestContext,
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::default)?;
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::default(format!("Failed to set allowed IPs: {:?}", e)))?;
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 RequestContext,
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.to_http_response(&req).await,
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 crate::server::{
2
- itsi_service::RequestContext,
3
- types::{HttpRequest, HttpResponse, RequestExt},
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: Vec<String>,
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 RequestContext,
38
+ _context: &mut HttpRequestContext,
30
39
  ) -> Result<Either<HttpRequest, HttpResponse>> {
31
- let submitted_value = match &self.token_source {
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
- 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))
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::server::{
13
- itsi_service::RequestContext,
14
- types::{HttpRequest, HttpResponse, RequestExt},
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, 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 RequestContext,
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(expected_password) if expected_password == password => Ok(Either::Left(req)),
75
- _ => {
76
- return Ok(Either::Right(self.basic_auth_failed_response()));
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::server::{
3
- itsi_service::RequestContext,
4
- types::{HttpRequest, HttpResponse, RequestExt},
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(&bytes))
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
- DecodingKey::from_rsa_pem(&bytes).map_err(|e| ItsiError::default(e.to_string()))
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(&bytes).map_err(|e| ItsiError::default(e.to_string()))
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::default("Failed to set keys".to_string()))?;
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 RequestContext,
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.to_http_response(&req).await,
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::default("Invalid token header"))?;
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.to_http_response(&req).await,
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
- // 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());
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.to_http_response(&req).await,
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.to_http_response(&req).await,
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.to_http_response(&req).await,
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.to_http_response(&req).await,
273
+ self.error_response
274
+ .to_http_response(req.accept().into())
275
+ .await,
255
276
  ));
256
277
  }
257
278
  }