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,6 @@
1
+ pub mod cache_store;
2
+ pub mod itsi_http_service;
3
+ pub mod mime_types;
4
+ pub mod password_hasher;
5
+ pub mod rate_limiter;
6
+ pub mod static_file_server;
@@ -0,0 +1,83 @@
1
+ use argon2::{
2
+ password_hash::{rand_core::OsRng, PasswordHasher, PasswordVerifier, SaltString},
3
+ Argon2, PasswordHash,
4
+ };
5
+
6
+ use itsi_error::ItsiError;
7
+ use magnus::{error::Result, Value};
8
+ use serde::Deserialize;
9
+ use serde_magnus::deserialize;
10
+ use sha_crypt::{
11
+ sha256_check, sha256_simple, sha512_check, sha512_simple, Sha256Params, Sha512Params,
12
+ };
13
+
14
+ #[derive(Debug, Deserialize)]
15
+ pub enum HashAlgorithm {
16
+ #[serde(rename(deserialize = "bcrypt"))]
17
+ Bcrypt,
18
+ #[serde(rename(deserialize = "sha256"))]
19
+ Sha256Crypt,
20
+ #[serde(rename(deserialize = "sha512"))]
21
+ Sha512Crypt,
22
+ #[serde(rename(deserialize = "argon2"))]
23
+ Argon2,
24
+ #[serde(rename(deserialize = "none"))]
25
+ None,
26
+ }
27
+
28
+ pub fn create_password_hash(password: String, algo: Value) -> Result<String> {
29
+ let hash_algorithm: HashAlgorithm = deserialize(algo)?;
30
+ match hash_algorithm {
31
+ HashAlgorithm::Bcrypt => {
32
+ // Use the bcrypt crate for password hashing.
33
+ bcrypt::hash(&password, bcrypt::DEFAULT_COST)
34
+ .map_err(ItsiError::new)
35
+ .map(Ok)?
36
+ }
37
+ HashAlgorithm::Sha256Crypt => {
38
+ let params = Sha256Params::new(1000).unwrap();
39
+ let hash = sha256_simple(&password, &params)
40
+ .map_err(|_| ItsiError::new("SHA256 hashing failed"))?;
41
+ Ok(hash)
42
+ }
43
+ HashAlgorithm::Sha512Crypt => {
44
+ let params = Sha512Params::new(1000).unwrap();
45
+ let hash = sha512_simple(&password, &params)
46
+ .map_err(|_| ItsiError::new("SHA512 hashing failed"))?;
47
+ Ok(hash)
48
+ }
49
+ HashAlgorithm::Argon2 => {
50
+ let salt = SaltString::generate(&mut OsRng);
51
+ let argon2 = Argon2::default();
52
+ let password_hash = argon2
53
+ .hash_password(password.as_bytes(), &salt)
54
+ .map_err(|_| ItsiError::new("Argon2 hashing failed"))?
55
+ .to_string();
56
+ Ok(password_hash)
57
+ }
58
+ HashAlgorithm::None => Ok(format!("$none${}", password)),
59
+ }
60
+ }
61
+
62
+ pub fn verify_password_hash(password: &str, hash: &str) -> Result<bool> {
63
+ if hash.starts_with("$2a$") || hash.starts_with("$2b$") || hash.starts_with("$2y$") {
64
+ Ok(bcrypt::verify(password, hash).map_err(ItsiError::new)?)
65
+ } else if hash.starts_with("$5$") {
66
+ Ok(sha256_check(password, hash).is_ok())
67
+ } else if hash.starts_with("$6$") {
68
+ Ok(sha512_check(password, hash).is_ok())
69
+ } else if hash.starts_with("$argon2") {
70
+ let parsed_hash =
71
+ PasswordHash::new(hash).map_err(|_| ItsiError::new("Argon2 hash parsing failed"))?;
72
+ Ok(Argon2::default()
73
+ .verify_password(password.as_bytes(), &parsed_hash)
74
+ .is_ok())
75
+ } else if hash
76
+ .strip_prefix("$none$")
77
+ .is_some_and(|stripped| stripped == password)
78
+ {
79
+ Ok(true)
80
+ } else {
81
+ Err(ItsiError::new("Unsupported hash algorithm").into())
82
+ }
83
+ }
@@ -14,10 +14,9 @@ use url::Url;
14
14
  #[derive(Debug)]
15
15
  pub enum RateLimitError {
16
16
  RedisError(RedisError),
17
- RateLimitExceeded { limit: u64, count: u64 },
17
+ RateLimitExceeded { limit: u64, count: u64, ttl: u64 },
18
18
  LockError,
19
19
  ConnectionTimeout,
20
- // Other error variants as needed.
21
20
  }
22
21
 
23
22
  impl From<RedisError> for RateLimitError {
@@ -30,8 +29,8 @@ impl std::fmt::Display for RateLimitError {
30
29
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31
30
  match self {
32
31
  RateLimitError::RedisError(e) => write!(f, "Redis error: {}", e),
33
- RateLimitError::RateLimitExceeded { limit, count } => {
34
- write!(f, "Rate limit exceeded: {}/{}", count, limit)
32
+ RateLimitError::RateLimitExceeded { limit, count, ttl } => {
33
+ write!(f, "Rate limit exceeded: {}/{} (ttl: {})", count, limit, ttl)
35
34
  }
36
35
  RateLimitError::LockError => write!(f, "Failed to acquire lock"),
37
36
  RateLimitError::ConnectionTimeout => write!(f, "Connection timeout"),
@@ -46,7 +45,7 @@ pub trait RateLimiter: Send + Sync + std::fmt::Debug {
46
45
  /// Returns the new counter value.
47
46
  ///
48
47
  /// If the operation fails, returns Ok(0) to fail open.
49
- async fn increment(&self, key: &str, timeout: Duration) -> Result<u64, RateLimitError>;
48
+ async fn increment(&self, key: &str, timeout: Duration) -> Result<(u64, u64), RateLimitError>;
50
49
 
51
50
  /// Checks if the rate limit is exceeded for the given key.
52
51
  /// Returns Ok(current_count) if not exceeded, or Err(RateLimitExceeded) if exceeded.
@@ -58,7 +57,7 @@ pub trait RateLimiter: Send + Sync + std::fmt::Debug {
58
57
  key: &str,
59
58
  limit: u64,
60
59
  timeout: Duration,
61
- ) -> Result<u64, RateLimitError>;
60
+ ) -> Result<(u64, u64), RateLimitError>;
62
61
 
63
62
  /// Returns self as Any for downcasting
64
63
  fn as_any(&self) -> &dyn Any;
@@ -99,15 +98,9 @@ impl RedisRateLimiter {
99
98
  "Invalid Redis URL format",
100
99
  ))));
101
100
  }
102
-
103
- // Create a Redis client
104
101
  let client = Client::open(connection_url).map_err(RateLimitError::RedisError)?;
105
-
106
- // Use tokio timeout to prevent hanging on connection attempt
107
102
  let connection_manager_result =
108
103
  timeout(CONNECTION_TIMEOUT, ConnectionManager::new(client)).await;
109
-
110
- // Handle timeout and connection errors
111
104
  let connection_manager = match connection_manager_result {
112
105
  Ok(result) => result.map_err(RateLimitError::RedisError)?,
113
106
  Err(_) => return Err(RateLimitError::ConnectionTimeout),
@@ -120,7 +113,7 @@ impl RedisRateLimiter {
120
113
  if redis.call('TTL', KEYS[1]) < 0 then
121
114
  redis.call('EXPIRE', KEYS[1], ARGV[1])
122
115
  end
123
- return current
116
+ return { current, ttl }
124
117
  "#,
125
118
  );
126
119
 
@@ -172,7 +165,7 @@ impl RedisRateLimiter {
172
165
 
173
166
  #[async_trait]
174
167
  impl RateLimiter for RedisRateLimiter {
175
- async fn increment(&self, key: &str, timeout: Duration) -> Result<u64, RateLimitError> {
168
+ async fn increment(&self, key: &str, timeout: Duration) -> Result<(u64, u64), RateLimitError> {
176
169
  let timeout_secs = timeout.as_secs();
177
170
  let mut connection = (*self.connection).clone();
178
171
 
@@ -184,11 +177,11 @@ impl RateLimiter for RedisRateLimiter {
184
177
  .invoke_async(&mut connection)
185
178
  .await
186
179
  {
187
- Ok(value) => Ok(value),
180
+ Ok((count, ttl)) => Ok((count, ttl)),
188
181
  Err(err) => {
189
182
  // Log the error but return 0 to fail open
190
183
  tracing::warn!("Redis rate limit error: {}", err);
191
- Ok(0)
184
+ Ok((0, timeout_secs))
192
185
  }
193
186
  }
194
187
  }
@@ -198,12 +191,14 @@ impl RateLimiter for RedisRateLimiter {
198
191
  key: &str,
199
192
  limit: u64,
200
193
  timeout: Duration,
201
- ) -> Result<u64, RateLimitError> {
194
+ ) -> Result<(u64, u64), RateLimitError> {
202
195
  match self.increment(key, timeout).await {
203
- Ok(count) if count <= limit => Ok(count),
204
- Ok(count) if count > limit => Err(RateLimitError::RateLimitExceeded { limit, count }),
196
+ Ok((count, ttl)) if count <= limit => Ok((count, ttl)),
197
+ Ok((count, ttl)) if count > limit => {
198
+ Err(RateLimitError::RateLimitExceeded { limit, count, ttl })
199
+ }
205
200
  // For any error or other case, fail open
206
- _ => Ok(0),
201
+ _ => Ok((0, timeout.as_secs())),
207
202
  }
208
203
  }
209
204
 
@@ -297,10 +292,8 @@ impl Default for InMemoryRateLimiter {
297
292
 
298
293
  #[async_trait]
299
294
  impl RateLimiter for InMemoryRateLimiter {
300
- async fn increment(&self, key: &str, timeout: Duration) -> Result<u64, RateLimitError> {
301
- // Periodically clean up expired entries
295
+ async fn increment(&self, key: &str, timeout: Duration) -> Result<(u64, u64), RateLimitError> {
302
296
  if rand::rng().random_bool(0.01) {
303
- // 1% chance on each call
304
297
  self.cleanup().await;
305
298
  }
306
299
 
@@ -315,11 +308,20 @@ impl RateLimiter for InMemoryRateLimiter {
315
308
  expires_at: now + timeout,
316
309
  });
317
310
 
318
- // Update expiry time if it's an existing entry
319
- entry.expires_at = now + timeout;
320
- entry.count += 1;
311
+ if entry.expires_at < now {
312
+ entry.expires_at = now + timeout;
313
+ entry.count = 1;
314
+ } else {
315
+ entry.count += 1;
316
+ }
317
+
318
+ let ttl = if entry.expires_at > now {
319
+ entry.expires_at.duration_since(now).as_secs()
320
+ } else {
321
+ 0
322
+ };
321
323
 
322
- Ok(entry.count)
324
+ Ok((entry.count, ttl))
323
325
  }
324
326
 
325
327
  async fn check_limit(
@@ -327,12 +329,14 @@ impl RateLimiter for InMemoryRateLimiter {
327
329
  key: &str,
328
330
  limit: u64,
329
331
  timeout: Duration,
330
- ) -> Result<u64, RateLimitError> {
332
+ ) -> Result<(u64, u64), RateLimitError> {
331
333
  match self.increment(key, timeout).await {
332
- Ok(count) if count <= limit => Ok(count),
333
- Ok(count) if count > limit => Err(RateLimitError::RateLimitExceeded { limit, count }),
334
+ Ok((count, ttl)) if count <= limit => Ok((count, ttl)),
335
+ Ok((count, ttl)) if count > limit => {
336
+ Err(RateLimitError::RateLimitExceeded { limit, count, ttl })
337
+ }
334
338
  // For any error or other case, fail open
335
- _ => Ok(0),
339
+ _ => Ok((0, timeout.as_secs())),
336
340
  }
337
341
  }
338
342