itsi-server 0.2.15 → 0.2.17

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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +75 -73
  3. data/exe/itsi +6 -1
  4. data/ext/itsi_acme/Cargo.toml +1 -1
  5. data/ext/itsi_scheduler/Cargo.toml +1 -1
  6. data/ext/itsi_server/Cargo.lock +1 -1
  7. data/ext/itsi_server/Cargo.toml +3 -1
  8. data/ext/itsi_server/extconf.rb +3 -1
  9. data/ext/itsi_server/src/lib.rs +7 -1
  10. data/ext/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +2 -0
  11. data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +6 -6
  12. data/ext/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +14 -13
  13. data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +71 -42
  14. data/ext/itsi_server/src/ruby_types/itsi_http_response.rs +151 -152
  15. data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +6 -15
  16. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +32 -6
  17. data/ext/itsi_server/src/ruby_types/itsi_server.rs +1 -1
  18. data/ext/itsi_server/src/server/binds/listener.rs +49 -8
  19. data/ext/itsi_server/src/server/frame_stream.rs +142 -0
  20. data/ext/itsi_server/src/server/http_message_types.rs +143 -10
  21. data/ext/itsi_server/src/server/io_stream.rs +28 -5
  22. data/ext/itsi_server/src/server/lifecycle_event.rs +1 -1
  23. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +2 -3
  24. data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +8 -10
  25. data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +2 -3
  26. data/ext/itsi_server/src/server/middleware_stack/middlewares/csp.rs +3 -3
  27. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +54 -58
  28. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +6 -9
  29. data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +27 -42
  30. data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +65 -14
  31. data/ext/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +1 -1
  32. data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +8 -11
  33. data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +21 -8
  34. data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +2 -3
  35. data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +1 -5
  36. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +1 -2
  37. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +13 -6
  38. data/ext/itsi_server/src/server/mod.rs +1 -0
  39. data/ext/itsi_server/src/server/process_worker.rs +5 -5
  40. data/ext/itsi_server/src/server/serve_strategy/acceptor.rs +100 -0
  41. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +87 -31
  42. data/ext/itsi_server/src/server/serve_strategy/mod.rs +1 -0
  43. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +166 -206
  44. data/ext/itsi_server/src/server/signal.rs +37 -9
  45. data/ext/itsi_server/src/server/thread_worker.rs +92 -70
  46. data/ext/itsi_server/src/services/itsi_http_service.rs +67 -62
  47. data/ext/itsi_server/src/services/mime_types.rs +185 -183
  48. data/ext/itsi_server/src/services/rate_limiter.rs +16 -34
  49. data/ext/itsi_server/src/services/static_file_server.rs +35 -60
  50. data/lib/itsi/http_request.rb +31 -39
  51. data/lib/itsi/http_response.rb +5 -0
  52. data/lib/itsi/rack_env_pool.rb +59 -0
  53. data/lib/itsi/server/config/config_helpers.rb +1 -2
  54. data/lib/itsi/server/config/dsl.rb +5 -4
  55. data/lib/itsi/server/config/middleware/etag.md +3 -7
  56. data/lib/itsi/server/config/middleware/etag.rb +2 -4
  57. data/lib/itsi/server/config/middleware/proxy.rb +1 -1
  58. data/lib/itsi/server/config/middleware/rackup_file.rb +2 -2
  59. data/lib/itsi/server/config/options/auto_reload_config.rb +6 -2
  60. data/lib/itsi/server/config/options/include.rb +5 -2
  61. data/lib/itsi/server/config/options/listen_backlog.rb +1 -1
  62. data/lib/itsi/server/config/options/pipeline_flush.md +16 -0
  63. data/lib/itsi/server/config/options/pipeline_flush.rb +19 -0
  64. data/lib/itsi/server/config/options/send_buffer_size.md +15 -0
  65. data/lib/itsi/server/config/options/send_buffer_size.rb +19 -0
  66. data/lib/itsi/server/config/options/writev.md +25 -0
  67. data/lib/itsi/server/config/options/writev.rb +19 -0
  68. data/lib/itsi/server/config.rb +43 -31
  69. data/lib/itsi/server/default_config/Itsi.rb +1 -4
  70. data/lib/itsi/server/grpc/grpc_call.rb +2 -0
  71. data/lib/itsi/server/grpc/grpc_interface.rb +2 -2
  72. data/lib/itsi/server/rack/handler/itsi.rb +3 -1
  73. data/lib/itsi/server/rack_interface.rb +17 -12
  74. data/lib/itsi/server/route_tester.rb +1 -1
  75. data/lib/itsi/server/scheduler_interface.rb +2 -0
  76. data/lib/itsi/server/version.rb +1 -1
  77. data/lib/itsi/server.rb +1 -0
  78. data/lib/ruby_lsp/itsi/addon.rb +12 -13
  79. metadata +10 -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
  }
@@ -2,7 +2,7 @@ use crate::{
2
2
  default_responses::NOT_FOUND_RESPONSE,
3
3
  prelude::*,
4
4
  server::{
5
- http_message_types::{HttpRequest, HttpResponse, RequestExt, ResponseFormat},
5
+ http_message_types::{HttpBody, HttpRequest, HttpResponse, RequestExt, ResponseFormat},
6
6
  middleware_stack::ErrorResponse,
7
7
  redirect_type::RedirectType,
8
8
  },
@@ -16,10 +16,9 @@ use http::{
16
16
  },
17
17
  HeaderName, HeaderValue, Response, StatusCode,
18
18
  };
19
- use http_body_util::{combinators::BoxBody, Full};
20
19
  use itsi_error::Result;
21
20
  use parking_lot::{Mutex, RwLock};
22
- use percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC};
21
+ use percent_encoding::percent_decode_str;
23
22
  use quick_cache::sync::Cache;
24
23
  use serde::Deserialize;
25
24
  use serde_json::json;
@@ -28,7 +27,6 @@ use std::{
28
27
  borrow::Cow,
29
28
  cmp::Ordering,
30
29
  collections::HashMap,
31
- convert::Infallible,
32
30
  fs::Metadata,
33
31
  ops::Deref,
34
32
  path::{Path, PathBuf},
@@ -175,7 +173,7 @@ impl CacheEntry {
175
173
  let mut hasher = Sha256::new();
176
174
  hasher.update(&bytes);
177
175
  let result = hasher.finalize();
178
- general_purpose::STANDARD.encode(result)
176
+ general_purpose::STANDARD.encode(&result[..16])
179
177
  };
180
178
  let headers_ct = get_mime_type(&path);
181
179
  let headers_etag = format!(r#"W/"{etag}""#).parse().unwrap();
@@ -279,7 +277,7 @@ impl StaticFileServer {
279
277
  supported_encodings: &[HeaderValue],
280
278
  ) -> Option<HttpResponse> {
281
279
  let accept: ResponseFormat = request.accept().into();
282
- let resolved = self.resolve(path, abs_path, accept.clone()).await;
280
+ let resolved = self.resolve(path, abs_path, accept).await;
283
281
 
284
282
  Some(match resolved {
285
283
  Ok(ResolvedAsset {
@@ -324,7 +322,7 @@ impl StaticFileServer {
324
322
  }) => Response::builder()
325
323
  .status(StatusCode::MOVED_PERMANENTLY)
326
324
  .header(header::LOCATION, redirect_to)
327
- .body(BoxBody::new(Full::new(Bytes::new())))
325
+ .body(HttpBody::empty())
328
326
  .unwrap(),
329
327
  Err(not_found_behavior) => match not_found_behavior {
330
328
  NotFoundBehavior::Error(error_response) => {
@@ -340,7 +338,7 @@ impl StaticFileServer {
340
338
  NotFoundBehavior::Redirect(redirect) => Response::builder()
341
339
  .status(redirect.r#type.status_code())
342
340
  .header(header::LOCATION, redirect.to)
343
- .body(BoxBody::new(Full::new(Bytes::new())))
341
+ .body(HttpBody::empty())
344
342
  .unwrap(),
345
343
  },
346
344
  })
@@ -407,7 +405,7 @@ impl StaticFileServer {
407
405
 
408
406
  Response::builder()
409
407
  .status(StatusCode::NOT_FOUND)
410
- .body(BoxBody::new(Full::new(Bytes::new())))
408
+ .body(HttpBody::empty())
411
409
  .unwrap()
412
410
  }
413
411
 
@@ -648,15 +646,8 @@ impl StaticFileServer {
648
646
  Err(nf)
649
647
  }
650
648
 
651
- async fn stream_file_range(
652
- &self,
653
- path: PathBuf,
654
- start: u64,
655
- end: u64,
656
- ) -> Option<BoxBody<Bytes, Infallible>> {
649
+ async fn stream_file_range(&self, path: PathBuf, start: u64, end: u64) -> Option<HttpBody> {
657
650
  use futures::TryStreamExt;
658
- use http_body_util::StreamBody;
659
- use hyper::body::Frame;
660
651
  use tokio::io::AsyncSeekExt;
661
652
  use tokio_util::io::ReaderStream;
662
653
 
@@ -687,32 +678,25 @@ impl StaticFileServer {
687
678
  let range_length = end - start + 1;
688
679
  let limited_reader = tokio::io::AsyncReadExt::take(file, range_length);
689
680
  let path_clone = path.clone();
690
- let stream = ReaderStream::with_capacity(limited_reader, 64 * 1024)
691
- .map_ok(Frame::data)
692
- .map_err(move |e| {
693
- warn!("Error streaming file {}: {}", path_clone.display(), e);
694
- unreachable!("We handle IO errors above")
695
- });
696
-
697
- Some(BoxBody::new(StreamBody::new(stream)))
681
+ let stream = ReaderStream::with_capacity(limited_reader, 64 * 1024).map_err(move |e| {
682
+ warn!("Error streaming file {}: {}", path_clone.display(), e);
683
+ unreachable!("We handle IO errors above")
684
+ });
685
+ Some(HttpBody::stream(stream))
698
686
  }
699
687
 
700
- async fn stream_file(&self, path: PathBuf) -> Option<BoxBody<Bytes, Infallible>> {
688
+ async fn stream_file(&self, path: PathBuf) -> Option<HttpBody> {
701
689
  use futures::TryStreamExt;
702
- use http_body_util::StreamBody;
703
- use hyper::body::Frame;
704
690
  use tokio_util::io::ReaderStream;
705
691
 
706
692
  match File::open(&path).await {
707
693
  Ok(file) => {
708
694
  let path_clone = path.clone();
709
- let stream = ReaderStream::with_capacity(file, 64 * 1024)
710
- .map_ok(Frame::data)
711
- .map_err(move |e| {
712
- warn!("Error streaming file {}: {}", path_clone.display(), e);
713
- unreachable!("We handle IO errors above")
714
- });
715
- Some(BoxBody::new(StreamBody::new(stream)))
695
+ let stream = ReaderStream::with_capacity(file, 64 * 1024).map_err(move |e| {
696
+ warn!("Error streaming file {}: {}", path_clone.display(), e);
697
+ unreachable!("We handle IO errors above")
698
+ });
699
+ Some(HttpBody::stream(stream))
716
700
  }
717
701
  Err(e) => {
718
702
  warn!(
@@ -749,7 +733,7 @@ impl StaticFileServer {
749
733
  return Response::builder()
750
734
  .status(StatusCode::RANGE_NOT_SATISFIABLE)
751
735
  .header("Content-Range", format!("bytes */{}", content_length))
752
- .body(BoxBody::new(Full::new(Bytes::new())))
736
+ .body(HttpBody::empty())
753
737
  .unwrap();
754
738
  }
755
739
 
@@ -795,7 +779,7 @@ impl StaticFileServer {
795
779
  builder = builder.header("Content-Range", range);
796
780
  }
797
781
 
798
- return builder.body(BoxBody::new(Full::new(Bytes::new()))).unwrap();
782
+ return builder.body(HttpBody::empty()).unwrap();
799
783
  }
800
784
 
801
785
  // For GET requests, prepare the actual content
@@ -829,10 +813,7 @@ impl StaticFileServer {
829
813
  }
830
814
  }
831
815
 
832
- fn serve_cached_content(
833
- &self,
834
- serve_cache_args: ServeCacheArgs,
835
- ) -> http::Response<BoxBody<Bytes, Infallible>> {
816
+ fn serve_cached_content(&self, serve_cache_args: ServeCacheArgs) -> HttpResponse {
836
817
  let ServeCacheArgs(
837
818
  cache_entry,
838
819
  start,
@@ -855,7 +836,7 @@ impl StaticFileServer {
855
836
  return Response::builder()
856
837
  .status(StatusCode::RANGE_NOT_SATISFIABLE)
857
838
  .header("Content-Range", format!("bytes */{}", content_length))
858
- .body(BoxBody::new(Full::new(Bytes::new())))
839
+ .body(HttpBody::empty())
859
840
  .unwrap();
860
841
  }
861
842
 
@@ -904,7 +885,7 @@ impl StaticFileServer {
904
885
  builder = builder.header("Content-Range", range);
905
886
  }
906
887
 
907
- return builder.body(BoxBody::new(Full::new(Bytes::new()))).unwrap();
888
+ return builder.body(HttpBody::empty()).unwrap();
908
889
  }
909
890
 
910
891
  if is_range_request {
@@ -920,7 +901,7 @@ impl StaticFileServer {
920
901
  cache_entry.last_modified_http_date.clone(),
921
902
  content_range,
922
903
  &self.headers,
923
- BoxBody::new(Full::new(range_bytes)),
904
+ HttpBody::full(range_bytes),
924
905
  )
925
906
  } else {
926
907
  // Return the full content
@@ -987,15 +968,15 @@ fn format_http_date_header(time: SystemTime) -> HeaderValue {
987
968
  .unwrap()
988
969
  }
989
970
 
990
- fn build_ok_body(bytes: Arc<Bytes>) -> BoxBody<Bytes, Infallible> {
991
- BoxBody::new(Full::new(bytes.as_ref().clone()))
971
+ fn build_ok_body(bytes: Arc<Bytes>) -> HttpBody {
972
+ HttpBody::full(bytes.as_ref().clone())
992
973
  }
993
974
 
994
975
  // Helper function to handle not modified responses
995
- fn build_not_modified_response() -> http::Response<BoxBody<Bytes, Infallible>> {
976
+ fn build_not_modified_response() -> HttpResponse {
996
977
  Response::builder()
997
978
  .status(StatusCode::NOT_MODIFIED)
998
- .body(BoxBody::new(Full::new(Bytes::new())))
979
+ .body(HttpBody::empty())
999
980
  .unwrap()
1000
981
  }
1001
982
 
@@ -1009,8 +990,8 @@ fn build_file_response(
1009
990
  last_modified_http_date: HeaderValue,
1010
991
  range_header: Option<String>,
1011
992
  headers: &Option<HashMap<String, String>>,
1012
- body: BoxBody<Bytes, Infallible>,
1013
- ) -> http::Response<BoxBody<Bytes, Infallible>> {
993
+ body: HttpBody,
994
+ ) -> HttpResponse {
1014
995
  let mut response = Response::new(body);
1015
996
 
1016
997
  *response.status_mut() = status;
@@ -1188,7 +1169,6 @@ async fn generate_directory_listing(
1188
1169
 
1189
1170
  // Generate JSON entries for directories.
1190
1171
  for (name, metadata) in dirs {
1191
- let encoded = utf8_percent_encode(&name, NON_ALPHANUMERIC).to_string();
1192
1172
  let modified = metadata
1193
1173
  .modified()
1194
1174
  .ok()
@@ -1201,7 +1181,7 @@ async fn generate_directory_listing(
1201
1181
 
1202
1182
  items.push(json!({
1203
1183
  "name": format!("{}/", name),
1204
- "path": format!("{}/", encoded),
1184
+ "path": format!("{}/", name),
1205
1185
  "is_dir": true,
1206
1186
  "size": null,
1207
1187
  "modified": modified,
@@ -1210,7 +1190,6 @@ async fn generate_directory_listing(
1210
1190
 
1211
1191
  // Generate JSON entries for files.
1212
1192
  for (name, metadata) in files {
1213
- let encoded = utf8_percent_encode(&name, NON_ALPHANUMERIC).to_string();
1214
1193
  let file_size = metadata.len();
1215
1194
  let formatted_size = if file_size < 1024 {
1216
1195
  format!("{} B", file_size)
@@ -1234,7 +1213,7 @@ async fn generate_directory_listing(
1234
1213
 
1235
1214
  items.push(json!({
1236
1215
  "name": name,
1237
- "path": encoded,
1216
+ "path": name,
1238
1217
  "is_dir": false,
1239
1218
  "size": formatted_size,
1240
1219
  "modified": modified_str,
@@ -1341,11 +1320,9 @@ async fn generate_directory_listing(
1341
1320
 
1342
1321
  // Generate rows for directories.
1343
1322
  for (name, metadata) in dirs {
1344
- let encoded = utf8_percent_encode(&name, NON_ALPHANUMERIC).to_string();
1345
-
1346
1323
  rows.push_str(&format!(
1347
1324
  r#"<tr><td><a href="{0}/">{1}/</a></td><td class="size">-</td><td class="date">{2}</td></tr>"#,
1348
- encoded,
1325
+ name,
1349
1326
  name,
1350
1327
  metadata.modified().ok().map(|m| DateTime::<Utc>::from(m).format("%Y-%m-%d %H:%M:%S").to_string())
1351
1328
  .unwrap_or_else(|| "-".to_string())
@@ -1355,8 +1332,6 @@ async fn generate_directory_listing(
1355
1332
 
1356
1333
  // Generate rows for files.
1357
1334
  for (name, metadata) in files {
1358
- let encoded = utf8_percent_encode(&name, NON_ALPHANUMERIC).to_string();
1359
-
1360
1335
  let file_size = metadata.len();
1361
1336
  let formatted_size = if file_size < 1024 {
1362
1337
  format!("{} B", file_size)
@@ -1380,7 +1355,7 @@ async fn generate_directory_listing(
1380
1355
 
1381
1356
  rows.push_str(&format!(
1382
1357
  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
1358
+ name, name, formatted_size, modified_str
1384
1359
  ));
1385
1360
  rows.push('\n');
1386
1361
  }
@@ -14,50 +14,35 @@ module Itsi
14
14
  EMPTY_IO = StringIO.new("").tap { |io| io.set_encoding(Encoding::ASCII_8BIT) }
15
15
 
16
16
  RACK_HEADER_MAP = StandardHeaders::ALL.map do |header|
17
- rack_form = if header == "content-type"
18
- "CONTENT_TYPE"
19
- elsif header == "content-length"
20
- "CONTENT_LENGTH"
21
- else
22
- "HTTP_#{header.upcase.gsub(/-/, "_")}"
23
- end
17
+ rack_form = \
18
+ if header == "content-type"
19
+ "CONTENT_TYPE"
20
+ elsif header == "content-length"
21
+ "CONTENT_LENGTH"
22
+ else
23
+ "HTTP_#{header.upcase.gsub(/-/, "_")}"
24
+ end
24
25
  [header, rack_form]
25
- end.to_h.tap do |hm|
26
- hm.default_proc = proc { |_, key| "HTTP_#{key.upcase.gsub(/-/, "_")}" }
27
- end
26
+ end.to_h
28
27
 
29
- RACK_ENV_TEMPLATE = {
30
- "SERVER_SOFTWARE" => "Itsi",
31
- "rack.errors" => $stderr,
32
- "rack.multithread" => true,
33
- "rack.multiprocess" => true,
34
- "rack.run_once" => false,
35
- "rack.hijack?" => true,
36
- "rack.multipart.buffer_size" => 16_384,
37
- "SCRIPT_NAME" => "",
38
- "REQUEST_METHOD" => "",
39
- "PATH_INFO" => "",
40
- "REQUEST_PATH" => "",
41
- "QUERY_STRING" => "",
42
- "REMOTE_ADDR" => "",
43
- "SERVER_PORT" => "",
44
- "SERVER_NAME" => "",
45
- "SERVER_PROTOCOL" => "",
46
- "HTTP_HOST" => "",
47
- "HTTP_VERSION" => "",
48
- "itsi.request" => "",
49
- "itsi.response" => "",
50
- "rack.version" => nil,
51
- "rack.url_scheme" => "",
52
- "rack.input" => "",
53
- "rack.hijack" => ""
54
- }.freeze
28
+ RACK_HEADER_MAP.default_proc = proc { |_, key| "HTTP_#{key.upcase.gsub(/-/, "_")}" }
29
+
30
+ HTTP_09 = "HTTP/0.9"
31
+ HTTP_09_ARR = ["HTTP/0.9"].freeze
32
+ HTTP_10 = "HTTP/1.0"
33
+ HTTP_10_ARR = ["HTTP/1.0"].freeze
34
+ HTTP_11 = "HTTP/1.1"
35
+ HTTP_11_ARR = ["HTTP/1.1"].freeze
36
+ HTTP_20 = "HTTP/2.0"
37
+ HTTP_20_ARR = ["HTTP/2.0"].freeze
38
+ HTTP_30 = "HTTP/3.0"
39
+ HTTP_30_ARR = ["HTTP/3.0"].freeze
55
40
 
56
41
  def to_rack_env
57
42
  path = self.path
58
43
  host = self.host
59
44
  version = self.version
60
- env = RACK_ENV_TEMPLATE.dup
45
+ env = RackEnvPool.checkout
61
46
  env["SCRIPT_NAME"] = script_name
62
47
  env["REQUEST_METHOD"] = request_method
63
48
  env["REQUEST_PATH"] = env["PATH_INFO"] = path
@@ -68,11 +53,18 @@ module Itsi
68
53
  env["HTTP_VERSION"] = env["SERVER_PROTOCOL"] = version
69
54
  env["itsi.request"] = self
70
55
  env["itsi.response"] = response
71
- env["rack.version"] = [version]
56
+ env["rack.version"] = \
57
+ case version
58
+ when HTTP_09 then HTTP_09_ARR
59
+ when HTTP_10 then HTTP_10_ARR
60
+ when HTTP_11 then HTTP_11_ARR
61
+ when HTTP_20 then HTTP_20_ARR
62
+ when HTTP_30 then HTTP_30_ARR
63
+ end
72
64
  env["rack.url_scheme"] = scheme
73
65
  env["rack.input"] = build_input_io
74
66
  env["rack.hijack"] = method(:hijack)
75
- headers.each do |(k, v)|
67
+ each_header do |k, v|
76
68
  env[case k
77
69
  when "content-type" then "CONTENT_TYPE"
78
70
  when "content-length" then "CONTENT_LENGTH"
@@ -16,6 +16,7 @@ module Itsi
16
16
  body = body.to_s unless body.is_a?(String)
17
17
 
18
18
  if headers
19
+ reserve_headers(headers.size)
19
20
  headers.each do |key, value|
20
21
  if value.is_a?(Array)
21
22
  value.each { |v| add_header(key, v) }
@@ -40,5 +41,9 @@ module Itsi
40
41
  close
41
42
  end
42
43
  end
44
+
45
+ def flush
46
+ # No-op. Our Rust server performs stream coalescing and automatically flushes on a tight interval.
47
+ end
43
48
  end
44
49
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Itsi
4
+ module RackEnvPool
5
+
6
+ RACK_ENV_TEMPLATE = {
7
+ "SERVER_SOFTWARE" => "Itsi",
8
+ "rack.errors" => $stderr,
9
+ "rack.multithread" => true,
10
+ "rack.multiprocess" => true,
11
+ "rack.run_once" => false,
12
+ "rack.hijack?" => true,
13
+ "rack.multipart.buffer_size" => 16_384,
14
+ "SCRIPT_NAME" => "",
15
+ "REQUEST_METHOD" => "",
16
+ "PATH_INFO" => "",
17
+ "REQUEST_PATH" => "",
18
+ "QUERY_STRING" => "",
19
+ "REMOTE_ADDR" => "",
20
+ "SERVER_PORT" => "",
21
+ "SERVER_NAME" => "",
22
+ "SERVER_PROTOCOL" => "",
23
+ "HTTP_HOST" => "",
24
+ "HTTP_VERSION" => "",
25
+ "itsi.request" => "",
26
+ "itsi.response" => "",
27
+ "rack.version" => nil,
28
+ "rack.url_scheme" => "",
29
+ "rack.input" => "",
30
+ "rack.hijack" => ""
31
+ }.freeze
32
+
33
+ POOL = []
34
+
35
+ def self.checkout # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength
36
+ POOL.pop&.tap do |recycled|
37
+ recycled.keys.each do |key|
38
+ case key
39
+ when "SERVER_SOFTWARE" then recycled[key] = "Itsi"
40
+ when "rack.errors" then recycled[key] = $stderr
41
+ when "rack.multithread", "rack.multiprocess", "rack.hijack?" then recycled[key] = true
42
+ when "rack.run_once" then recycled[key] = false
43
+ when "rack.multipart.buffer_size" then recycled[key] = 16_384
44
+ when "SCRIPT_NAME", "REQUEST_METHOD", "PATH_INFO", "REQUEST_PATH", "QUERY_STRING", "REMOTE_ADDR",
45
+ "SERVER_PORT", "SERVER_NAME", "SERVER_PROTOCOL", "HTTP_HOST", "HTTP_VERSION", "itsi.request",
46
+ "itsi.response", "rack.version", "rack.url_scheme", "rack.input", "rack.hijack"
47
+ nil
48
+ else recycled.delete(key)
49
+ end
50
+ end
51
+ end || RACK_ENV_TEMPLATE.dup
52
+ end
53
+
54
+ def self.checkin(env)
55
+ POOL << env
56
+ end
57
+ end
58
+
59
+ end
@@ -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
@@ -55,8 +55,9 @@ module Itsi
55
55
  nested_locations: [],
56
56
  middleware_loader: lambda do
57
57
  @options[:nested_locations].each(&:call)
58
- @middleware[:app] ||= {}
59
- @middleware[:app][:app_proc] = @middleware[:app]&.[](:preloader)&.call || DEFAULT_APP[]
58
+ if !(@middleware[:app] || @middleware[:static_assets])
59
+ @middleware[:app] = { app_proc: DEFAULT_APP[]}
60
+ end
60
61
  [flatten_routes, Config.errors_to_error_lines(errors)]
61
62
  end
62
63
  }
@@ -74,7 +75,7 @@ module Itsi
74
75
  define_method(option_name) do |*args, **kwargs, &blk|
75
76
  option.new(self, *args, **kwargs, &blk).build!
76
77
  rescue Exception => e # rubocop:disable Lint/RescueException
77
- @errors << [e, caller[1]]
78
+ @errors << [e, e.backtrace.find{|r| !(r =~ /server\/config/) }]
78
79
  end
79
80
  end
80
81
 
@@ -85,7 +86,7 @@ module Itsi
85
86
  rescue Config::Endpoint::InvalidHandlerException => e
86
87
  @errors << [e, "#{e.backtrace[0]}:in #{e.message}"]
87
88
  rescue Exception => e # rubocop:disable Lint/RescueException
88
- @errors << [e, caller[1]]
89
+ @errors << [e, e.backtrace.find{|r| !(r =~ /server\/config/) }]
89
90
  end
90
91
  end
91
92
 
@@ -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