itsi-server 0.2.15 → 0.2.16

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +73 -73
  3. data/ext/itsi_scheduler/Cargo.toml +1 -1
  4. data/ext/itsi_server/Cargo.lock +1 -1
  5. data/ext/itsi_server/Cargo.toml +1 -1
  6. data/ext/itsi_server/extconf.rb +3 -1
  7. data/ext/itsi_server/src/lib.rs +1 -0
  8. data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +2 -2
  9. data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +9 -11
  10. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +6 -1
  11. data/ext/itsi_server/src/server/binds/listener.rs +4 -1
  12. data/ext/itsi_server/src/server/http_message_types.rs +1 -1
  13. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +32 -34
  14. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +3 -4
  15. data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +23 -38
  16. data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +65 -14
  17. data/ext/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +1 -1
  18. data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +1 -1
  19. data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +21 -8
  20. data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +1 -5
  21. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +12 -3
  22. data/ext/itsi_server/src/server/process_worker.rs +2 -1
  23. data/ext/itsi_server/src/server/serve_strategy/acceptor.rs +96 -0
  24. data/ext/itsi_server/src/server/serve_strategy/mod.rs +1 -0
  25. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +80 -136
  26. data/ext/itsi_server/src/server/thread_worker.rs +10 -3
  27. data/ext/itsi_server/src/services/itsi_http_service.rs +26 -21
  28. data/ext/itsi_server/src/services/mime_types.rs +185 -183
  29. data/ext/itsi_server/src/services/rate_limiter.rs +16 -34
  30. data/ext/itsi_server/src/services/static_file_server.rs +7 -13
  31. data/lib/itsi/server/config/config_helpers.rb +1 -2
  32. data/lib/itsi/server/config/middleware/etag.md +3 -7
  33. data/lib/itsi/server/config/middleware/etag.rb +2 -4
  34. data/lib/itsi/server/config/options/listen_backlog.rb +1 -1
  35. data/lib/itsi/server/config/options/send_buffer_size.md +15 -0
  36. data/lib/itsi/server/config/options/send_buffer_size.rb +19 -0
  37. data/lib/itsi/server/config.rb +24 -25
  38. data/lib/itsi/server/route_tester.rb +1 -1
  39. data/lib/itsi/server/version.rb +1 -1
  40. metadata +4 -1
@@ -1,4 +1,5 @@
1
1
  use async_trait::async_trait;
2
+ use parking_lot::{Mutex, RwLock};
2
3
  use rand::Rng;
3
4
  use redis::aio::ConnectionManager;
4
5
  use redis::{Client, RedisError, Script};
@@ -6,9 +7,9 @@ use serde::Deserialize;
6
7
  use std::any::Any;
7
8
  use std::collections::{HashMap, HashSet};
8
9
  use std::result::Result;
9
- use std::sync::{Arc, LazyLock, Mutex};
10
+ use std::sync::{Arc, LazyLock};
10
11
  use std::time::{Duration, Instant};
11
- use tokio::sync::{Mutex as AsyncMutex, RwLock};
12
+ use tokio::sync::Mutex as AsyncMutex;
12
13
  use tokio::time::timeout;
13
14
  use tracing::warn;
14
15
  use url::Url;
@@ -242,10 +243,10 @@ impl InMemoryRateLimiter {
242
243
  /// Cleans up expired entries
243
244
  async fn cleanup(&self) {
244
245
  // Try to get the write lock, but fail open if we can't
245
- if let Ok(mut entries) = self.entries.try_write() {
246
- let now = Instant::now();
247
- entries.retain(|_, entry| entry.expires_at > now);
248
- }
246
+ let now = Instant::now();
247
+ self.entries
248
+ .write()
249
+ .retain(|_, entry| entry.expires_at > now);
249
250
  }
250
251
 
251
252
  /// Bans an IP address for the specified duration
@@ -258,12 +259,7 @@ impl InMemoryRateLimiter {
258
259
  let now = Instant::now();
259
260
  let ban_key = format!("ban:ip:{}", ip);
260
261
 
261
- let mut entries = self.entries.try_write().map_err(|e| {
262
- tracing::error!("Failed to acquire write lock: {}", e);
263
- RateLimitError::LockError
264
- })?;
265
-
266
- entries.insert(
262
+ self.entries.write().insert(
267
263
  ban_key,
268
264
  RateLimitEntry {
269
265
  count: 1, // Use count=1 to indicate banned
@@ -279,12 +275,7 @@ impl InMemoryRateLimiter {
279
275
  let now = Instant::now();
280
276
  let ban_key = format!("ban:ip:{}", ip);
281
277
 
282
- let entries = self.entries.try_read().map_err(|e| {
283
- tracing::error!("Failed to acquire read lock: {}", e);
284
- RateLimitError::LockError
285
- })?;
286
-
287
- if let Some(entry) = entries.get(&ban_key) {
278
+ if let Some(entry) = self.entries.read().get(&ban_key) {
288
279
  if entry.expires_at > now {
289
280
  // IP is banned, return a generic reason since we don't store reasons
290
281
  return Ok(Some("IP address banned".to_string()));
@@ -310,7 +301,7 @@ impl RateLimiter for InMemoryRateLimiter {
310
301
 
311
302
  let now = Instant::now();
312
303
 
313
- let mut entries = self.entries.write().await;
304
+ let mut entries = self.entries.write();
314
305
 
315
306
  let entry = entries
316
307
  .entry(key.to_string())
@@ -436,7 +427,7 @@ impl RateLimiterStore {
436
427
  ) -> Result<Arc<RedisRateLimiter>, RateLimitError> {
437
428
  // First check if this URL is known to fail
438
429
  {
439
- let failed_urls = self.failed_urls.lock().unwrap_or_else(|e| e.into_inner());
430
+ let failed_urls = self.failed_urls.lock();
440
431
  if failed_urls.contains(connection_url) {
441
432
  return Err(RateLimitError::ConnectionTimeout);
442
433
  }
@@ -444,10 +435,7 @@ impl RateLimiterStore {
444
435
 
445
436
  // Then check if we already have a limiter for this URL
446
437
  {
447
- let limiters = self
448
- .redis_limiters
449
- .lock()
450
- .unwrap_or_else(|e| e.into_inner());
438
+ let limiters = self.redis_limiters.lock();
451
439
  if let Some(limiter) = limiters.get(connection_url) {
452
440
  return Ok(limiter.clone());
453
441
  }
@@ -455,7 +443,7 @@ impl RateLimiterStore {
455
443
 
456
444
  // Get a dedicated mutex for this URL or create a new one if it doesn't exist
457
445
  let url_mutex = {
458
- let mut locks = CONNECTION_LOCKS.lock().unwrap_or_else(|e| e.into_inner());
446
+ let mut locks = CONNECTION_LOCKS.lock();
459
447
 
460
448
  // Get or create the mutex for this URL
461
449
  locks
@@ -476,10 +464,7 @@ impl RateLimiterStore {
476
464
 
477
465
  // Check again if another thread created the limiter while we were waiting
478
466
  {
479
- let limiters = self
480
- .redis_limiters
481
- .lock()
482
- .unwrap_or_else(|e| e.into_inner());
467
+ let limiters = self.redis_limiters.lock();
483
468
  if let Some(limiter) = limiters.get(connection_url) {
484
469
  return Ok(limiter.clone());
485
470
  }
@@ -492,10 +477,7 @@ impl RateLimiterStore {
492
477
  let limiter = Arc::new(limiter);
493
478
 
494
479
  // Store it for future use
495
- let mut limiters = self
496
- .redis_limiters
497
- .lock()
498
- .unwrap_or_else(|e| e.into_inner());
480
+ let mut limiters = self.redis_limiters.lock();
499
481
  limiters.insert(connection_url.to_string(), limiter.clone());
500
482
 
501
483
  Ok(limiter)
@@ -503,7 +485,7 @@ impl RateLimiterStore {
503
485
  Err(e) => {
504
486
  tracing::error!("Failed to initialize Redis rate limiter: {}", e);
505
487
  // Cache the failure
506
- let mut failed_urls = self.failed_urls.lock().unwrap_or_else(|e| e.into_inner());
488
+ let mut failed_urls = self.failed_urls.lock();
507
489
  failed_urls.insert(connection_url.to_string());
508
490
  Err(e)
509
491
  }
@@ -19,7 +19,7 @@ use http::{
19
19
  use http_body_util::{combinators::BoxBody, Full};
20
20
  use itsi_error::Result;
21
21
  use parking_lot::{Mutex, RwLock};
22
- use percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC};
22
+ use percent_encoding::percent_decode_str;
23
23
  use quick_cache::sync::Cache;
24
24
  use serde::Deserialize;
25
25
  use serde_json::json;
@@ -175,7 +175,7 @@ impl CacheEntry {
175
175
  let mut hasher = Sha256::new();
176
176
  hasher.update(&bytes);
177
177
  let result = hasher.finalize();
178
- general_purpose::STANDARD.encode(result)
178
+ general_purpose::STANDARD.encode(&result[..16])
179
179
  };
180
180
  let headers_ct = get_mime_type(&path);
181
181
  let headers_etag = format!(r#"W/"{etag}""#).parse().unwrap();
@@ -279,7 +279,7 @@ impl StaticFileServer {
279
279
  supported_encodings: &[HeaderValue],
280
280
  ) -> Option<HttpResponse> {
281
281
  let accept: ResponseFormat = request.accept().into();
282
- let resolved = self.resolve(path, abs_path, accept.clone()).await;
282
+ let resolved = self.resolve(path, abs_path, accept).await;
283
283
 
284
284
  Some(match resolved {
285
285
  Ok(ResolvedAsset {
@@ -1188,7 +1188,6 @@ async fn generate_directory_listing(
1188
1188
 
1189
1189
  // Generate JSON entries for directories.
1190
1190
  for (name, metadata) in dirs {
1191
- let encoded = utf8_percent_encode(&name, NON_ALPHANUMERIC).to_string();
1192
1191
  let modified = metadata
1193
1192
  .modified()
1194
1193
  .ok()
@@ -1201,7 +1200,7 @@ async fn generate_directory_listing(
1201
1200
 
1202
1201
  items.push(json!({
1203
1202
  "name": format!("{}/", name),
1204
- "path": format!("{}/", encoded),
1203
+ "path": format!("{}/", name),
1205
1204
  "is_dir": true,
1206
1205
  "size": null,
1207
1206
  "modified": modified,
@@ -1210,7 +1209,6 @@ async fn generate_directory_listing(
1210
1209
 
1211
1210
  // Generate JSON entries for files.
1212
1211
  for (name, metadata) in files {
1213
- let encoded = utf8_percent_encode(&name, NON_ALPHANUMERIC).to_string();
1214
1212
  let file_size = metadata.len();
1215
1213
  let formatted_size = if file_size < 1024 {
1216
1214
  format!("{} B", file_size)
@@ -1234,7 +1232,7 @@ async fn generate_directory_listing(
1234
1232
 
1235
1233
  items.push(json!({
1236
1234
  "name": name,
1237
- "path": encoded,
1235
+ "path": name,
1238
1236
  "is_dir": false,
1239
1237
  "size": formatted_size,
1240
1238
  "modified": modified_str,
@@ -1341,11 +1339,9 @@ async fn generate_directory_listing(
1341
1339
 
1342
1340
  // Generate rows for directories.
1343
1341
  for (name, metadata) in dirs {
1344
- let encoded = utf8_percent_encode(&name, NON_ALPHANUMERIC).to_string();
1345
-
1346
1342
  rows.push_str(&format!(
1347
1343
  r#"<tr><td><a href="{0}/">{1}/</a></td><td class="size">-</td><td class="date">{2}</td></tr>"#,
1348
- encoded,
1344
+ name,
1349
1345
  name,
1350
1346
  metadata.modified().ok().map(|m| DateTime::<Utc>::from(m).format("%Y-%m-%d %H:%M:%S").to_string())
1351
1347
  .unwrap_or_else(|| "-".to_string())
@@ -1355,8 +1351,6 @@ async fn generate_directory_listing(
1355
1351
 
1356
1352
  // Generate rows for files.
1357
1353
  for (name, metadata) in files {
1358
- let encoded = utf8_percent_encode(&name, NON_ALPHANUMERIC).to_string();
1359
-
1360
1354
  let file_size = metadata.len();
1361
1355
  let formatted_size = if file_size < 1024 {
1362
1356
  format!("{} B", file_size)
@@ -1380,7 +1374,7 @@ async fn generate_directory_listing(
1380
1374
 
1381
1375
  rows.push_str(&format!(
1382
1376
  r#"<tr><td><a href="{0}">{1}</a></td><td class="size">{2}</td><td class="date">{3}</td></tr>"#,
1383
- encoded, name, formatted_size, modified_str
1377
+ name, name, formatted_size, modified_str
1384
1378
  ));
1385
1379
  rows.push('\n');
1386
1380
  }
@@ -97,14 +97,13 @@ module Itsi
97
97
  if !self.class.ancestors.include?(Middleware) && !location.parent.nil?
98
98
  raise "#{opt_name} must be set at the top level"
99
99
  end
100
-
101
100
  @location = location
102
101
  @params = case schema
103
102
  when TypedStruct::Validation
104
103
  schema.validate!(params)
105
104
  when Array
106
105
  default, validation = schema
107
- params ? validation.validate!(params) : default
106
+ !params.nil? ? validation.validate!(params) : default
108
107
  when nil
109
108
  nil
110
109
  else
@@ -13,8 +13,7 @@ ETags are useful for optimizing client-side caching, conditional GETs, and reduc
13
13
  etag \
14
14
  type: "strong",
15
15
  algorithm: "sha256",
16
- min_body_size: 0,
17
- handle_if_none_match: true
16
+ min_body_size: 0
18
17
  ```
19
18
 
20
19
  ## ETag Applied to a sub-location
@@ -23,8 +22,7 @@ location "/assets" do
23
22
  etag \
24
23
  type: "weak",
25
24
  algorithm: "md5",
26
- min_body_size: 1024,
27
- handle_if_none_match: true
25
+ min_body_size: 1024
28
26
  end
29
27
  ```
30
28
 
@@ -40,12 +38,10 @@ end
40
38
 
41
39
  - **min_body_size**: Minimum response body size (in bytes) required before an ETag is generated. Use this to skip ETags for small or trivial responses.
42
40
 
43
- - **handle_if_none_match**: When `true`, incoming requests with a matching `If-None-Match` header will receive a `304 Not Modified` response (instead of a full body), if the ETag matches the computed value.
44
-
45
41
  ## How It Works
46
42
 
47
43
  ### Before the Response
48
- If `handle_if_none_match` is enabled and the request includes an `If-None-Match` header, the value is stored in the request context for comparison later.
44
+ If the request includes an `If-None-Match` header, the value is stored in the request context for comparison later.
49
45
 
50
46
  ### After the Response
51
47
 
@@ -7,8 +7,7 @@ module Itsi
7
7
  etag \\
8
8
  type: ${1|"strong","weak"|},
9
9
  algorithm: ${2|"sha256","md5"|},
10
- min_body_size: ${3|0,1024|},
11
- handle_if_none_match: ${4|true,false|}
10
+ min_body_size: ${3|0,1024|}
12
11
  SNIPPET
13
12
 
14
13
  detail "Enables ETag generation for the server."
@@ -17,8 +16,7 @@ module Itsi
17
16
  {
18
17
  type: (Enum(["strong", "weak"]) & Required()).default("strong"),
19
18
  algorithm: (Enum(["sha256", "md5"]) & Required()).default("sha256"),
20
- min_body_size: Range(0...1024 ** 3).default(0),
21
- handle_if_none_match: Bool().default(true)
19
+ min_body_size: Range(0...1024 ** 3).default(0)
22
20
  }
23
21
  end
24
22
  end
@@ -4,7 +4,7 @@ module Itsi
4
4
  class ListenBacklog < Option
5
5
 
6
6
  insert_text <<~SNIPPET
7
- listen_backlog ${1|262_144,1_048_576|}
7
+ listen_backlog ${1|1024,2048,4096|}
8
8
  SNIPPET
9
9
 
10
10
  detail "Specifies the size of the listen backlog for the socket. Larger backlog sizes can improve performance for high-throughput applications by allowing more pending connections to queue, but may increase memory usage. The default value is 1024."
@@ -0,0 +1,15 @@
1
+ ---
2
+ title: Send Buffer Size
3
+ url: /options/send_buffer_size
4
+ ---
5
+
6
+ Configures the size of the send buffer for the socket. Larger buffer sizes can improve performance for high-throughput applications but may increase memory usage. The default value is 262,144 bytes.
7
+
8
+ ## Configuration
9
+ ```ruby {filename=Itsi.rb}
10
+ send_buffer_size 262_144
11
+ ```
12
+
13
+ ```ruby {filename=Itsi.rb}
14
+ send_buffer_size 1_048_576
15
+ ```
@@ -0,0 +1,19 @@
1
+ module Itsi
2
+ class Server
3
+ module Config
4
+ class SendBufferSize < Option
5
+
6
+ insert_text <<~SNIPPET
7
+ send_buffer_size ${1|262_144,1_048_576|}
8
+ SNIPPET
9
+
10
+ detail "Specifies the size of the send buffer for the socket. Larger buffer sizes can improve performance for high-throughput applications but may increase memory usage. The default value is 262,144 bytes."
11
+
12
+ schema do
13
+ (Type(Integer) & Range(1..Float::INFINITY) & Required()).default(262_144)
14
+ end
15
+
16
+ end
17
+ end
18
+ end
19
+ end
@@ -41,29 +41,28 @@ module Itsi
41
41
  DSL.evaluate(&builder_proc)
42
42
  elsif args[:static]
43
43
  DSL.evaluate do
44
- location "*" do
45
- rate_limit key: "address", store_config: "in_memory", requests: 2, seconds: 5
46
- etag type: "strong", algorithm: "md5", min_body_size: 1024 * 1024
47
- compress min_size: 1024 * 1024, level: "fastest", algorithms: %w[zstd gzip br deflate],
48
- mime_types: %w[all], compress_streams: true
49
- log_requests before: { level: "INFO", format: "[{request_id}] {method} {path_and_query} - {addr} " },
50
- after: { level: "INFO",
51
- format: "[{request_id}] └─ {status} in {response_time}" }
52
- static_assets \
53
- relative_path: true,
54
- allowed_extensions: [],
55
- root_dir: ".",
56
- not_found_behavior: { error: "not_found" },
57
- auto_index: true,
58
- try_html_extension: true,
59
- max_file_size_in_memory: 1024 * 1024, # 1MB
60
- max_files_in_memory: 1000,
61
- file_check_interval: 1,
62
- serve_hidden_files: false,
63
- headers: {
64
- "X-Content-Type-Options" => "nosniff"
65
- }
66
- end
44
+ rate_limit key: "address", store_config: "in_memory", requests: 5, seconds: 10
45
+ etag type: "strong", algorithm: "md5", min_body_size: 1024 * 1024
46
+ compress min_size: 1024 * 1024, level: "fastest", algorithms: %w[zstd gzip br deflate],
47
+ mime_types: %w[all], compress_streams: true
48
+ log_requests before: { level: "DEBUG", format: "[{request_id}] {method} {path_and_query} - {addr} " },
49
+ after: { level: "DEBUG",
50
+ format: "[{request_id}] └─ {status} in {response_time}" }
51
+ nodelay false
52
+ static_assets \
53
+ relative_path: true,
54
+ allowed_extensions: [],
55
+ root_dir: ".",
56
+ not_found_behavior: { error: "not_found" },
57
+ auto_index: true,
58
+ try_html_extension: true,
59
+ max_file_size_in_memory: 1024 * 1024, # 1MB
60
+ max_files_in_memory: 1000,
61
+ file_check_interval: 1,
62
+ serve_hidden_files: false,
63
+ headers: {
64
+ "X-Content-Type-Options" => "nosniff"
65
+ }
67
66
  end
68
67
  elsif File.exist?(config_file_path.to_s)
69
68
  DSL.evaluate(config_file_path)
@@ -106,7 +105,6 @@ module Itsi
106
105
  Server.write_pid
107
106
  end
108
107
 
109
-
110
108
  srv_config = {
111
109
  workers: args.fetch(:workers) { itsifile_config.fetch(:workers, 1) },
112
110
  worker_memory_limit: args.fetch(:worker_memory_limit) { itsifile_config.fetch(:worker_memory_limit, nil) },
@@ -148,7 +146,8 @@ module Itsi
148
146
  reuse_port: itsifile_config.fetch(:reuse_port, true),
149
147
  listen_backlog: itsifile_config.fetch(:listen_backlog, 1024),
150
148
  nodelay: itsifile_config.fetch(:nodelay, true),
151
- recv_buffer_size: itsifile_config.fetch(:recv_buffer_size, 262_144)
149
+ recv_buffer_size: itsifile_config.fetch(:recv_buffer_size, 262_144),
150
+ send_buffer_size: itsifile_config.fetch(:send_buffer_size, 262_144)
152
151
  }.transform_keys(&:to_s)
153
152
 
154
153
  [srv_config, errors_to_error_lines(errors)]
@@ -23,7 +23,7 @@ module Itsi
23
23
  when "cors"
24
24
  "\e[33mcors\e[0m(#{mw_args["allow_origins"].join(" ")}, #{mw_args["allow_methods"].join(" ")})"
25
25
  when "etag"
26
- "\e[33metag\e[0m(#{mw_args["type"]}/#{mw_args["algorithm"]}, #{mw_args["handle_if_none_match"] ? "if_none_match" : ""})"
26
+ "\e[33metag\e[0m(#{mw_args["type"]}/#{mw_args["algorithm"]})"
27
27
  when "cache_control"
28
28
  "\e[33mcache_control\e[0m(max_age: #{mw_args["max_age"]}, #{mw_args.select do |_, v|
29
29
  v == true
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Server
5
- VERSION = "0.2.15"
5
+ VERSION = "0.2.16"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: itsi-server
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.15
4
+ version: 0.2.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wouter Coppieters
@@ -233,6 +233,7 @@ files:
233
233
  - ext/itsi_server/src/server/process_worker.rs
234
234
  - ext/itsi_server/src/server/redirect_type.rs
235
235
  - ext/itsi_server/src/server/request_job.rs
236
+ - ext/itsi_server/src/server/serve_strategy/acceptor.rs
236
237
  - ext/itsi_server/src/server/serve_strategy/cluster_mode.rs
237
238
  - ext/itsi_server/src/server/serve_strategy/mod.rs
238
239
  - ext/itsi_server/src/server/serve_strategy/single_mode.rs
@@ -501,6 +502,8 @@ files:
501
502
  - lib/itsi/server/config/options/ruby_thread_request_backlog_size.rb
502
503
  - lib/itsi/server/config/options/scheduler_threads.md
503
504
  - lib/itsi/server/config/options/scheduler_threads.rb
505
+ - lib/itsi/server/config/options/send_buffer_size.md
506
+ - lib/itsi/server/config/options/send_buffer_size.rb
504
507
  - lib/itsi/server/config/options/shutdown_timeout.md
505
508
  - lib/itsi/server/config/options/shutdown_timeout.rb
506
509
  - lib/itsi/server/config/options/stream_body.md