itsi-server 0.2.21.rc1 → 0.2.21.rc2

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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +12 -12
  3. data/ext/itsi_error/Cargo.toml +1 -1
  4. data/ext/itsi_rb_helpers/Cargo.toml +2 -2
  5. data/ext/itsi_rb_helpers/src/heap_value.rs +2 -2
  6. data/ext/itsi_rb_helpers/src/lib.rs +9 -5
  7. data/ext/itsi_scheduler/Cargo.toml +2 -2
  8. data/ext/itsi_server/Cargo.toml +2 -2
  9. data/ext/itsi_server/src/lib.rs +3 -2
  10. data/ext/itsi_server/src/ruby_types/itsi_server.rs +34 -18
  11. data/ext/itsi_server/src/server/frame_stream.rs +2 -1
  12. data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +8 -2
  13. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +90 -21
  14. data/ext/itsi_server/src/services/password_hasher.rs +8 -2
  15. data/ext/itsi_server/src/services/rate_limiter.rs +72 -25
  16. data/ext/itsi_server/src/services/static_file_server.rs +35 -9
  17. data/ext/itsi_server/target/release/build/clang-sys-0dae18670e690c25/out/common.rs +355 -0
  18. data/ext/itsi_server/target/release/build/clang-sys-0dae18670e690c25/out/dynamic.rs +276 -0
  19. data/ext/itsi_server/target/release/build/clang-sys-0dae18670e690c25/out/macros.rs +49 -0
  20. data/ext/itsi_server/target/release/build/oid-registry-71b994a322b296ec/out/oid_db.rs +537 -0
  21. data/ext/itsi_server/target/release/build/rb-sys-9f9831ab50fb86db/out/bindings-0.9.124-mri-arm64-darwin24-2.7.8.rs +6234 -0
  22. data/ext/itsi_server/target/release/build/rb-sys-9f9831ab50fb86db/out/bindings-0.9.124-mri-arm64-darwin24-3.4.5.rs +8936 -0
  23. data/ext/itsi_server/target/release/build/rb-sys-9f9831ab50fb86db/out/bindings-0.9.124-mri-arm64-darwin24-4.0.1.rs +9060 -0
  24. data/ext/itsi_server/target/release/build/typenum-11265e44e46de3b7/out/tests.rs +20563 -0
  25. data/ext/itsi_tracing/src/lib.rs +3 -3
  26. data/lib/itsi/http_request.rb +22 -17
  27. data/lib/itsi/rack_env_pool.rb +7 -17
  28. data/lib/itsi/server/config/middleware/static_assets.rb +8 -1
  29. data/lib/itsi/server/rack_interface.rb +12 -0
  30. data/lib/itsi/server/version.rb +1 -1
  31. metadata +9 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 874e6827c0fa031b9efcaeeb024a83b457367e9e6e78138307ab1dfb1dc5f342
4
- data.tar.gz: b13b6138c4a7c630bd88c907ae8edc9bca0cbe7626d96e47c24dfa4bf020e470
3
+ metadata.gz: 4b429e81e1fd3091d28a61a2db0c55ea8d38965077c4da330470c248f8a7f39c
4
+ data.tar.gz: c6bb3a7a3ac05b5d1a8b75d97774b6576717e900dae56e33bae11b70cf6ea782
5
5
  SHA512:
6
- metadata.gz: 180ccbafb120b1bde651f5cf574c97afc4214a199b6f273f656b17fae1fb8daa42fe7376ea6fe78d545882e049d3de01bb301ebac6da389906f59ccb3c28c0b4
7
- data.tar.gz: bb31862f6755a22c799e2292ef603f1ae9c1cdc82ce3f78583a5571f54f077b432dec33d9e7dbf0459bf4ca823738ffc9db0500caea27fe24dd8a99899829e8c
6
+ metadata.gz: cfc08df98fb6faad6c36e240f96e10e9c49f6ffc1f87c4fd874317ea382cf4a27dd06136844d8a3831781c308cac0c49f60ad870e66ad88e09659fd4e10b4a93
7
+ data.tar.gz: 2b09c90b8079d16998a213f6fdb49cb655463ea6d0c487fafd9c590b0a54c1a64fc3ed360a3ffb7bfbc3135cc5c7e2310f21894de4c3b08fa38d3a7bf1142613
data/Cargo.lock CHANGED
@@ -1906,9 +1906,9 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
1906
1906
 
1907
1907
  [[package]]
1908
1908
  name = "magnus"
1909
- version = "0.7.1"
1909
+ version = "0.8.2"
1910
1910
  source = "registry+https://github.com/rust-lang/crates.io-index"
1911
- checksum = "3d87ae53030f3a22e83879e666cb94e58a7bdf31706878a0ba48752994146dab"
1911
+ checksum = "3b36a5b126bbe97eb0d02d07acfeb327036c6319fd816139a49824a83b7f9012"
1912
1912
  dependencies = [
1913
1913
  "bytes",
1914
1914
  "magnus-macros",
@@ -1919,9 +1919,9 @@ dependencies = [
1919
1919
 
1920
1920
  [[package]]
1921
1921
  name = "magnus-macros"
1922
- version = "0.6.0"
1922
+ version = "0.8.0"
1923
1923
  source = "registry+https://github.com/rust-lang/crates.io-index"
1924
- checksum = "5968c820e2960565f647819f5928a42d6e874551cab9d88d75e3e0660d7f71e3"
1924
+ checksum = "47607461fd8e1513cb4f2076c197d8092d921a1ea75bd08af97398f593751892"
1925
1925
  dependencies = [
1926
1926
  "proc-macro2",
1927
1927
  "quote",
@@ -2554,18 +2554,18 @@ dependencies = [
2554
2554
 
2555
2555
  [[package]]
2556
2556
  name = "rb-sys"
2557
- version = "0.9.111"
2557
+ version = "0.9.124"
2558
2558
  source = "registry+https://github.com/rust-lang/crates.io-index"
2559
- checksum = "becea799ce051c16fb140be80f5e7cf781070f99ca099332383c2b17861249af"
2559
+ checksum = "c85c4188462601e2aa1469def389c17228566f82ea72f137ed096f21591bc489"
2560
2560
  dependencies = [
2561
2561
  "rb-sys-build",
2562
2562
  ]
2563
2563
 
2564
2564
  [[package]]
2565
2565
  name = "rb-sys-build"
2566
- version = "0.9.111"
2566
+ version = "0.9.124"
2567
2567
  source = "registry+https://github.com/rust-lang/crates.io-index"
2568
- checksum = "64691175abc704862f60a9ca8ef06174080cc50615f2bf1d4759f46db18b4d29"
2568
+ checksum = "568068db4102230882e6d4ae8de6632e224ca75fe5970f6e026a04e91ed635d3"
2569
2569
  dependencies = [
2570
2570
  "bindgen",
2571
2571
  "lazy_static",
@@ -2578,9 +2578,9 @@ dependencies = [
2578
2578
 
2579
2579
  [[package]]
2580
2580
  name = "rb-sys-env"
2581
- version = "0.1.2"
2581
+ version = "0.2.3"
2582
2582
  source = "registry+https://github.com/rust-lang/crates.io-index"
2583
- checksum = "a35802679f07360454b418a5d1735c89716bde01d35b1560fc953c1415a0b3bb"
2583
+ checksum = "cca7ad6a7e21e72151d56fe2495a259b5670e204c3adac41ee7ef676ea08117a"
2584
2584
 
2585
2585
  [[package]]
2586
2586
  name = "rcgen"
@@ -2972,9 +2972,9 @@ dependencies = [
2972
2972
 
2973
2973
  [[package]]
2974
2974
  name = "serde_magnus"
2975
- version = "0.9.0"
2975
+ version = "0.11.0"
2976
2976
  source = "registry+https://github.com/rust-lang/crates.io-index"
2977
- checksum = "51b8b945a2dadb221f1c5490cfb411cab6c3821446b8eca50ee07e5a3893ec51"
2977
+ checksum = "8ff64c88ddd26acdcad5a501f18bcc339927b77b69f4a03bfaf2a6fc5ba2ac4b"
2978
2978
  dependencies = [
2979
2979
  "magnus",
2980
2980
  "serde",
@@ -5,7 +5,7 @@ edition = "2021"
5
5
 
6
6
  [dependencies]
7
7
  thiserror = "2.0.11"
8
- magnus = { version = "0.7.1" }
8
+ magnus = { version = "0.8.2" }
9
9
  rcgen = "0.13.2"
10
10
  nix = "0.29.0"
11
11
  httparse = "1.10.1"
@@ -5,7 +5,7 @@ edition = "2021"
5
5
 
6
6
  [dependencies]
7
7
  cfg-if = "1.0.0"
8
- magnus = { version = "0.7.1", features = ["rb-sys", "bytes"] }
8
+ magnus = { version = "0.8.2", features = ["rb-sys", "bytes"] }
9
9
  nix = "0.29.0"
10
- rb-sys = "0.9.105"
10
+ rb-sys = "0.9.117"
11
11
  serde = "1.0.219"
@@ -1,7 +1,7 @@
1
- use magnus::IntoValue;
2
1
  use magnus::rb_sys::AsRawValue;
3
2
  use magnus::value::BoxValue;
4
- use magnus::{Ruby, Value, value::ReprValue};
3
+ use magnus::IntoValue;
4
+ use magnus::{value::ReprValue, Ruby, Value};
5
5
  use std::fmt::{self, Debug, Formatter};
6
6
  use std::ops::Deref;
7
7
 
@@ -1,14 +1,14 @@
1
1
  use std::{ffi::c_int, os::raw::c_void, ptr::null_mut};
2
2
 
3
3
  use magnus::{
4
- ArgList, RArray, Ruby, Thread, Value,
5
4
  block::Proc,
6
- rb_sys::{AsRawId, FromRawValue, protect},
5
+ rb_sys::{protect, AsRawId, FromRawValue},
7
6
  value::{IntoId, LazyId, ReprValue},
7
+ ArgList, RArray, Ruby, Thread, Value,
8
8
  };
9
9
  use rb_sys::{
10
- VALUE, rb_funcallv, rb_thread_call_with_gvl, rb_thread_call_without_gvl, rb_thread_create,
11
- rb_thread_schedule, rb_thread_wakeup,
10
+ rb_funcallv, rb_thread_call_with_gvl, rb_thread_call_without_gvl, rb_thread_create,
11
+ rb_thread_schedule, rb_thread_wakeup, VALUE,
12
12
  };
13
13
 
14
14
  mod heap_value;
@@ -182,7 +182,11 @@ pub fn terminate_non_fork_safe_threads() {
182
182
  && !v_thread
183
183
  .funcall::<_, _, bool>(*ID_THREAD_VARIABLE_GET, (ruby.sym_new("fork_safe"),))
184
184
  .unwrap_or(false);
185
- if non_fork_safe { Some(v_thread) } else { None }
185
+ if non_fork_safe {
186
+ Some(v_thread)
187
+ } else {
188
+ None
189
+ }
186
190
  })
187
191
  .collect::<Vec<_>>();
188
192
 
@@ -10,7 +10,7 @@ publish = false
10
10
  crate-type = ["cdylib"]
11
11
 
12
12
  [dependencies]
13
- magnus = { version = "0.7.1", features = ["rb-sys", "bytes"] }
13
+ magnus = { version = "0.8.2", features = ["rb-sys", "bytes"] }
14
14
  derive_more = { version = "2.0.1", features = ["debug"] }
15
15
  itsi_tracing = { path = "../itsi_tracing" }
16
16
  itsi_rb_helpers = { path = "../itsi_rb_helpers" }
@@ -18,7 +18,7 @@ itsi_error = { path = "../itsi_error" }
18
18
  itsi_instrument_entry = { path = "../itsi_instrument_entry" }
19
19
  parking_lot = "0.12.3"
20
20
  mio = { version = "1.0.3", features = ["os-poll", "os-ext"] }
21
- rb-sys = "0.9.105"
21
+ rb-sys = "0.9.117"
22
22
  bytes = "1.10.1"
23
23
  nix = "0.29.0"
24
24
  tracing = "0.1.41"
@@ -42,7 +42,7 @@ itsi_rb_helpers = { path = "../itsi_rb_helpers" }
42
42
  itsi_tracing = { path = "../itsi_tracing" }
43
43
  itsi_acme = { path = "../itsi_acme" }
44
44
  jsonwebtoken = "9.3.1"
45
- magnus = { version = "0.7.1", features = ["bytes", "rb-sys"] }
45
+ magnus = { version = "0.8.2", features = ["bytes", "rb-sys"] }
46
46
  notify = { version = "8.0.0" }
47
47
  nix = { version = "0.29.0", features = [
48
48
  "socket",
@@ -72,7 +72,7 @@ rustls = "0.23.23"
72
72
  rustls-pemfile = "2.2.0"
73
73
  serde = "1.0.219"
74
74
  serde_json = "1.0.140"
75
- serde_magnus = "0.9.0"
75
+ serde_magnus = "0.11.0"
76
76
  sha2 = "0.10.8"
77
77
  socket2 = "0.5.8"
78
78
  sysinfo = "0.33.1"
@@ -7,7 +7,7 @@ pub mod ruby_types;
7
7
  pub mod server;
8
8
  pub mod services;
9
9
 
10
- use magnus::{error::Result, function, method, Module, Object, Ruby};
10
+ use magnus::{error::Result, function, method, Class, Module, Object, Ruby};
11
11
  use prelude::*;
12
12
  use ruby_types::{
13
13
  itsi_body_proxy::ItsiBodyProxy, itsi_grpc_call::ItsiGrpcCall,
@@ -36,7 +36,8 @@ fn init(ruby: &Ruby) -> Result<()> {
36
36
  )?;
37
37
 
38
38
  let server = ruby.get_inner(&ITSI_SERVER);
39
- server.define_singleton_method("new", function!(ItsiServer::new, 3))?;
39
+ server.define_alloc_func::<ItsiServer>();
40
+ server.define_method("initialize", method!(ItsiServer::initialize, 3))?;
40
41
  server.define_singleton_method("reset_signal_handlers", function!(reset_signal_handlers, 0))?;
41
42
  server.define_method("start", method!(ItsiServer::start, 0))?;
42
43
  server.define_method("stop", method!(ItsiServer::stop, 0))?;
@@ -13,26 +13,32 @@ use tracing::{info, instrument};
13
13
  mod file_watcher;
14
14
  pub mod itsi_server_config;
15
15
  #[magnus::wrap(class = "Itsi::Server", free_immediately, size)]
16
- #[derive(Clone)]
16
+ #[derive(Clone, Default)]
17
17
  pub struct ItsiServer {
18
- pub config: Arc<Mutex<Arc<ItsiServerConfig>>>,
18
+ pub config: Arc<Mutex<Option<Arc<ItsiServerConfig>>>>,
19
19
  }
20
20
 
21
21
  impl ItsiServer {
22
- pub fn new(
23
- ruby: &Ruby,
22
+ pub fn initialize(
23
+ &self,
24
24
  cli_params: RHash,
25
25
  itsifile_path: Option<PathBuf>,
26
26
  itsi_config_proc: Option<Proc>,
27
- ) -> Result<Self> {
28
- Ok(Self {
29
- config: Arc::new(Mutex::new(Arc::new(ItsiServerConfig::new(
30
- ruby,
31
- cli_params,
32
- itsifile_path,
33
- itsi_config_proc,
34
- )?))),
35
- })
27
+ ) -> Result<()> {
28
+ let ruby = Ruby::get().map_err(|_| {
29
+ magnus::Error::new(
30
+ magnus::exception::runtime_error(),
31
+ "Failed to acquire Ruby VM handle",
32
+ )
33
+ })?;
34
+ let config = Arc::new(ItsiServerConfig::new(
35
+ &ruby,
36
+ cli_params,
37
+ itsifile_path,
38
+ itsi_config_proc,
39
+ )?);
40
+ *self.config.lock() = Some(config);
41
+ Ok(())
36
42
  }
37
43
 
38
44
  pub fn stop(&self) -> Result<()> {
@@ -40,10 +46,20 @@ impl ItsiServer {
40
46
  Ok(())
41
47
  }
42
48
 
49
+ fn config(&self) -> Result<Arc<ItsiServerConfig>> {
50
+ self.config.lock().as_ref().cloned().ok_or_else(|| {
51
+ magnus::Error::new(
52
+ magnus::exception::runtime_error(),
53
+ "Itsi::Server not initialized",
54
+ )
55
+ })
56
+ }
57
+
43
58
  #[instrument(skip(self))]
44
59
  pub fn start(&self) -> Result<()> {
45
- self.config.lock().server_params.read().setup_listeners()?;
46
- let result = if self.config.lock().server_params.read().silence {
60
+ let server_config = self.config()?;
61
+ server_config.server_params.read().setup_listeners()?;
62
+ let result = if server_config.server_params.read().silence {
47
63
  run_silently(|| self.build_and_run_strategy())
48
64
  } else {
49
65
  info!("Itsi - Rolling into action. ⚪💨");
@@ -60,11 +76,11 @@ impl ItsiServer {
60
76
  }
61
77
 
62
78
  pub(crate) fn build_strategy(&self) -> Result<ServeStrategy> {
63
- let server_config = self.config.lock();
79
+ let server_config = self.config()?;
64
80
  Ok(if server_config.server_params.read().workers > 1 {
65
- ServeStrategy::Cluster(Arc::new(ClusterMode::new(server_config.clone())))
81
+ ServeStrategy::Cluster(Arc::new(ClusterMode::new(server_config)))
66
82
  } else {
67
- ServeStrategy::Single(Arc::new(SingleMode::new(server_config.clone(), 0)?))
83
+ ServeStrategy::Single(Arc::new(SingleMode::new(server_config, 0)?))
68
84
  })
69
85
  }
70
86
 
@@ -48,9 +48,10 @@ impl Stream for FrameStream {
48
48
  if this.shutdown_rx.has_changed().unwrap_or(false)
49
49
  && *this.shutdown_rx.borrow() == RunningPhase::ShutdownPending
50
50
  {
51
- while let Ok(bytes) = this.receiver.try_recv() {
51
+ if let Ok(bytes) = this.receiver.try_recv() {
52
52
  return Poll::Ready(Some(Ok(bytes)));
53
53
  }
54
+
54
55
  this.drained = true;
55
56
  return Poll::Ready(None);
56
57
  }
@@ -43,7 +43,7 @@ pub use error_response::ErrorResponse;
43
43
  pub use etag::ETag;
44
44
  pub use intrusion_protection::IntrusionProtection;
45
45
  pub use log_requests::LogRequests;
46
- use magnus::error::Result;
46
+ use magnus::{error::Result, Ruby};
47
47
  use magnus::rb_sys::AsRawValue;
48
48
  use magnus::Value;
49
49
  pub use max_body::MaxBody;
@@ -88,7 +88,13 @@ pub trait FromValue: Sized + Send + Sync + 'static {
88
88
  }
89
89
  }
90
90
 
91
- let deserialized: Arc<Self> = Arc::new(deserialize(value)?);
91
+ let ruby = Ruby::get().map_err(|_| {
92
+ magnus::Error::new(
93
+ magnus::exception::runtime_error(),
94
+ "Failed to acquire Ruby VM handle",
95
+ )
96
+ })?;
97
+ let deserialized: Arc<Self> = Arc::new(deserialize(&ruby, value)?);
92
98
  cache.insert(raw, deserialized.clone());
93
99
  Ok(deserialized)
94
100
  }
@@ -27,6 +27,59 @@ use std::{
27
27
  };
28
28
  use tracing::debug;
29
29
 
30
+ /// Compact representation of the client's Accept-Encoding preferences.
31
+ /// Priority order is determined by the bit checks in `pick_encoding`.
32
+ #[derive(Clone, Copy, Debug, Default)]
33
+ struct AcceptEncodingMask(u8);
34
+
35
+ impl AcceptEncodingMask {
36
+ const BR: u8 = 1 << 0;
37
+ const GZIP: u8 = 1 << 1;
38
+ const ZSTD: u8 = 1 << 2;
39
+ const DEFLATE: u8 = 1 << 3;
40
+
41
+ fn from_headers(headers: &[HeaderValue]) -> Self {
42
+ let mut mask = 0u8;
43
+
44
+ for hv in headers {
45
+ let Ok(s) = hv.to_str() else { continue };
46
+
47
+ // We intentionally ignore q-values and treat any mention as "acceptable".
48
+ // This is a fast-path optimization for common benchmark/client headers.
49
+ for part in s.split(',') {
50
+ let token = part.split(';').next().unwrap_or("").trim();
51
+ match token {
52
+ "br" => mask |= Self::BR,
53
+ "gzip" => mask |= Self::GZIP,
54
+ "zstd" => mask |= Self::ZSTD,
55
+ "deflate" => mask |= Self::DEFLATE,
56
+ _ => {}
57
+ }
58
+ }
59
+ }
60
+
61
+ Self(mask)
62
+ }
63
+
64
+ fn pick_encoding(self) -> Option<&'static str> {
65
+ // Prefer stronger/faster compression if available.
66
+ // (Actual availability is checked by the file server.)
67
+ if (self.0 & Self::ZSTD) != 0 {
68
+ return Some("zstd");
69
+ }
70
+ if (self.0 & Self::BR) != 0 {
71
+ return Some("br");
72
+ }
73
+ if (self.0 & Self::GZIP) != 0 {
74
+ return Some("gzip");
75
+ }
76
+ if (self.0 & Self::DEFLATE) != 0 {
77
+ return Some("deflate");
78
+ }
79
+ None
80
+ }
81
+ }
82
+
30
83
  #[derive(Debug, Deserialize)]
31
84
  pub struct StaticAssets {
32
85
  pub root_dir: PathBuf,
@@ -98,7 +151,10 @@ impl MiddlewareLayer for StaticAssets {
98
151
  return Ok(Either::Left(req));
99
152
  }
100
153
 
154
+ // We still populate the context cache for any other middleware that might want it,
155
+ // but we avoid re-parsing Accept-Encoding later by computing a compact mask here.
101
156
  context.set_supported_encoding_set(&req);
157
+
102
158
  let abs_path = req.uri().path();
103
159
  let rel_path = if !self.relative_path {
104
160
  abs_path.trim_start_matches("/")
@@ -119,7 +175,6 @@ impl MiddlewareLayer for StaticAssets {
119
175
  };
120
176
 
121
177
  debug!(target: "middleware::static_assets", "Asset path is {}", rel_path);
122
- // Determine if this is a HEAD request
123
178
  let is_head_request = req.method() == Method::HEAD;
124
179
 
125
180
  // Extract range and if-modified-since headers
@@ -135,6 +190,22 @@ impl MiddlewareLayer for StaticAssets {
135
190
  let encodings: &[HeaderValue] = context
136
191
  .supported_encoding_set()
137
192
  .map_or(&[], |set| set.as_slice());
193
+
194
+ // Compute a fast encoding preference and narrow the encoding list we hand to the server.
195
+ // This avoids repeated per-request string splitting/trim in the static file server.
196
+ let mask = AcceptEncodingMask::from_headers(encodings);
197
+ let preferred = mask.pick_encoding();
198
+
199
+ let narrowed: [HeaderValue; 1];
200
+ let encodings_for_server: &[HeaderValue] = if let Some(token) = preferred {
201
+ // Safe: these are valid header values and the file server only needs to see
202
+ // a minimal representation to pick a cached variant.
203
+ narrowed = [HeaderValue::from_static(token)];
204
+ &narrowed
205
+ } else {
206
+ &[]
207
+ };
208
+
138
209
  let response = file_server
139
210
  .serve(
140
211
  &req,
@@ -143,7 +214,7 @@ impl MiddlewareLayer for StaticAssets {
143
214
  serve_range,
144
215
  if_modified_since,
145
216
  is_head_request,
146
- encodings,
217
+ encodings_for_server,
147
218
  )
148
219
  .await;
149
220
 
@@ -156,40 +227,38 @@ impl MiddlewareLayer for StaticAssets {
156
227
  }
157
228
 
158
229
  fn parse_range_header(headers: &HeaderMap) -> ServeRange {
159
- let range_header = headers.get(RANGE);
160
- if range_header.is_none() {
230
+ let Some(range_header) = headers.get(RANGE) else {
161
231
  return ServeRange::Full;
162
- }
163
- let range_header = range_header.unwrap().to_str().unwrap_or("");
232
+ };
233
+
234
+ let range_header = range_header.to_str().unwrap_or("");
164
235
  let bytes_prefix = "bytes=";
165
236
  if !range_header.starts_with(bytes_prefix) {
166
237
  return ServeRange::Full;
167
238
  }
168
239
 
169
- let range_str = &range_header[bytes_prefix.len()..];
170
-
171
- let range_parts: Vec<&str> = range_str
240
+ // Only consider the first range specifier, ignore multi-range requests.
241
+ let range_str = range_header[bytes_prefix.len()..]
172
242
  .split(',')
173
243
  .next()
174
- .unwrap_or("")
175
- .split('-')
176
- .collect();
177
- if range_parts.len() != 2 {
244
+ .unwrap_or("");
245
+
246
+ let Some((start_str, end_str)) = range_str.split_once('-') else {
178
247
  return ServeRange::Full;
179
- }
248
+ };
180
249
 
181
- let start = if range_parts[0].is_empty() {
182
- range_parts[1].parse::<u64>().unwrap_or(0)
183
- } else if let Ok(start) = range_parts[0].parse::<u64>() {
250
+ let start = if start_str.is_empty() {
251
+ end_str.parse::<u64>().unwrap_or(0)
252
+ } else if let Ok(start) = start_str.parse::<u64>() {
184
253
  start
185
254
  } else {
186
255
  return ServeRange::Full;
187
256
  };
188
257
 
189
- let end = if range_parts[1].is_empty() {
190
- u64::MAX // Use u64::MAX as sentinel for open-ended ranges
191
- } else if let Ok(end) = range_parts[1].parse::<u64>() {
192
- end // No conversion needed, already u64
258
+ let end = if end_str.is_empty() {
259
+ u64::MAX // sentinel for open-ended ranges
260
+ } else if let Ok(end) = end_str.parse::<u64>() {
261
+ end
193
262
  } else {
194
263
  return ServeRange::Full;
195
264
  };
@@ -4,7 +4,7 @@ use argon2::{
4
4
  };
5
5
 
6
6
  use itsi_error::ItsiError;
7
- use magnus::{error::Result, Value};
7
+ use magnus::{error::Result, Ruby, Value};
8
8
  use serde::Deserialize;
9
9
  use serde_magnus::deserialize;
10
10
  use sha_crypt::{
@@ -26,7 +26,13 @@ pub enum HashAlgorithm {
26
26
  }
27
27
 
28
28
  pub fn create_password_hash(password: String, algo: Value) -> Result<String> {
29
- let hash_algorithm: HashAlgorithm = deserialize(algo)?;
29
+ let ruby = Ruby::get().map_err(|_| {
30
+ magnus::Error::new(
31
+ magnus::exception::runtime_error(),
32
+ "Failed to acquire Ruby VM handle",
33
+ )
34
+ })?;
35
+ let hash_algorithm: HashAlgorithm = deserialize(&ruby, algo)?;
30
36
  match hash_algorithm {
31
37
  HashAlgorithm::Bcrypt => {
32
38
  // Use the bcrypt crate for password hashing.
@@ -6,9 +6,11 @@ use redis::{Client, RedisError, Script};
6
6
  use serde::Deserialize;
7
7
  use std::any::Any;
8
8
  use std::collections::{HashMap, HashSet};
9
+ use std::fmt::Write as _;
9
10
  use std::result::Result;
11
+ use std::sync::atomic::{AtomicU64, Ordering};
10
12
  use std::sync::{Arc, LazyLock};
11
- use std::time::{Duration, Instant};
13
+ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
12
14
  use tokio::sync::Mutex as AsyncMutex;
13
15
  use tokio::time::timeout;
14
16
  use tracing::warn;
@@ -303,27 +305,37 @@ impl RateLimiter for InMemoryRateLimiter {
303
305
 
304
306
  let mut entries = self.entries.write();
305
307
 
306
- let entry = entries
307
- .entry(key.to_string())
308
- .or_insert_with(|| RateLimitEntry {
309
- count: 0,
310
- expires_at: now + timeout,
311
- });
308
+ // Avoid per-request allocation: only allocate a String when inserting a new key.
309
+ //
310
+ // NOTE: we use `get_mut` first because `HashMap::entry` for `HashMap<String, _>`
311
+ // requires an owned `String`, which would allocate on every request.
312
+ if let Some(entry) = entries.get_mut(key) {
313
+ if entry.expires_at < now {
314
+ entry.expires_at = now + timeout;
315
+ entry.count = 1;
316
+ } else {
317
+ entry.count += 1;
318
+ }
319
+
320
+ let ttl = if entry.expires_at > now {
321
+ entry.expires_at.duration_since(now).as_secs()
322
+ } else {
323
+ 0
324
+ };
312
325
 
313
- if entry.expires_at < now {
314
- entry.expires_at = now + timeout;
315
- entry.count = 1;
316
- } else {
317
- entry.count += 1;
326
+ return Ok((entry.count, ttl));
318
327
  }
319
328
 
320
- let ttl = if entry.expires_at > now {
321
- entry.expires_at.duration_since(now).as_secs()
322
- } else {
323
- 0
324
- };
329
+ // Insert path: allocate once for the new key.
330
+ entries.insert(
331
+ key.to_owned(),
332
+ RateLimitEntry {
333
+ count: 1,
334
+ expires_at: now + timeout,
335
+ },
336
+ );
325
337
 
326
- Ok((entry.count, ttl))
338
+ Ok((1, timeout.as_secs()))
327
339
  }
328
340
 
329
341
  async fn check_limit(
@@ -378,14 +390,49 @@ impl BanManager {
378
390
  }
379
391
 
380
392
  /// Utility function to create a rate limit key for a specific minute
381
- pub fn create_rate_limit_key(api_key: &str, resource: &str) -> String {
382
- // Get the current minute number (0-59)
383
- let now = std::time::SystemTime::now()
384
- .duration_since(std::time::UNIX_EPOCH)
385
- .unwrap_or_default();
393
+ static CACHED_MINUTE_BUCKET_SECS: AtomicU64 = AtomicU64::new(0);
394
+ static CACHED_MINUTE_BUCKET: AtomicU64 = AtomicU64::new(0);
395
+
396
+ #[inline]
397
+ fn cached_minute_bucket() -> u64 {
398
+ // Cache the computed minute bucket and only refresh at most once per second.
399
+ // This avoids a syscall and divisions/mods on every request, while preserving
400
+ // the exact same value as the previous implementation.
401
+ let now_secs = SystemTime::now()
402
+ .duration_since(UNIX_EPOCH)
403
+ .unwrap_or_default()
404
+ .as_secs();
405
+
406
+ let last = CACHED_MINUTE_BUCKET_SECS.load(Ordering::Relaxed);
407
+ if last != now_secs {
408
+ let minutes = (now_secs / 60) % 60;
409
+ CACHED_MINUTE_BUCKET.store(minutes, Ordering::Relaxed);
410
+ CACHED_MINUTE_BUCKET_SECS.store(now_secs, Ordering::Relaxed);
411
+ minutes
412
+ } else {
413
+ CACHED_MINUTE_BUCKET.load(Ordering::Relaxed)
414
+ }
415
+ }
386
416
 
387
- let minutes = now.as_secs() / 60 % 60;
388
- format!("ratelimit:{}:{}:{}", api_key, resource, minutes)
417
+ pub fn create_rate_limit_key(api_key: &str, resource: &str) -> String {
418
+ let minutes = cached_minute_bucket();
419
+
420
+ // Build the exact same string as:
421
+ // format!("ratelimit:{}:{}:{}", api_key, resource, minutes)
422
+ // but avoid `format!` machinery and minimize reallocations.
423
+ let mut s = String::with_capacity(
424
+ "ratelimit:".len() + api_key.len() + 1 + resource.len() + 1 + 2, // minutes is 0-59
425
+ );
426
+
427
+ s.push_str("ratelimit:");
428
+ s.push_str(api_key);
429
+ s.push(':');
430
+ s.push_str(resource);
431
+ s.push(':');
432
+ // u64->decimal without intermediate allocation
433
+ let _ = write!(&mut s, "{}", minutes);
434
+
435
+ s
389
436
  }
390
437
 
391
438
  /// Utility function to create a ban key for an IP address