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.
- checksums.yaml +4 -4
- data/Cargo.lock +124 -109
- 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 +8 -10
- 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 +124 -109
- 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 +116 -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 +160 -101
- 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 +326 -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
@@ -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, ¶ms)
|
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, ¶ms)
|
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(
|
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 =>
|
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
|
-
|
319
|
-
|
320
|
-
|
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 =>
|
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
|
|