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.
- checksums.yaml +4 -4
- data/Cargo.lock +75 -73
- data/exe/itsi +6 -1
- data/ext/itsi_acme/Cargo.toml +1 -1
- data/ext/itsi_scheduler/Cargo.toml +1 -1
- data/ext/itsi_server/Cargo.lock +1 -1
- data/ext/itsi_server/Cargo.toml +3 -1
- data/ext/itsi_server/extconf.rb +3 -1
- data/ext/itsi_server/src/lib.rs +7 -1
- data/ext/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +2 -0
- data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +6 -6
- data/ext/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +14 -13
- data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +71 -42
- data/ext/itsi_server/src/ruby_types/itsi_http_response.rs +151 -152
- data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +6 -15
- data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +32 -6
- data/ext/itsi_server/src/ruby_types/itsi_server.rs +1 -1
- data/ext/itsi_server/src/server/binds/listener.rs +49 -8
- data/ext/itsi_server/src/server/frame_stream.rs +142 -0
- data/ext/itsi_server/src/server/http_message_types.rs +143 -10
- data/ext/itsi_server/src/server/io_stream.rs +28 -5
- data/ext/itsi_server/src/server/lifecycle_event.rs +1 -1
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +2 -3
- data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +8 -10
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +2 -3
- data/ext/itsi_server/src/server/middleware_stack/middlewares/csp.rs +3 -3
- data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +54 -58
- data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +6 -9
- data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +27 -42
- data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +65 -14
- data/ext/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +1 -1
- data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +8 -11
- data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +21 -8
- data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +2 -3
- data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +1 -5
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +1 -2
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +13 -6
- data/ext/itsi_server/src/server/mod.rs +1 -0
- data/ext/itsi_server/src/server/process_worker.rs +5 -5
- data/ext/itsi_server/src/server/serve_strategy/acceptor.rs +100 -0
- data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +87 -31
- data/ext/itsi_server/src/server/serve_strategy/mod.rs +1 -0
- data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +166 -206
- data/ext/itsi_server/src/server/signal.rs +37 -9
- data/ext/itsi_server/src/server/thread_worker.rs +92 -70
- data/ext/itsi_server/src/services/itsi_http_service.rs +67 -62
- data/ext/itsi_server/src/services/mime_types.rs +185 -183
- data/ext/itsi_server/src/services/rate_limiter.rs +16 -34
- data/ext/itsi_server/src/services/static_file_server.rs +35 -60
- data/lib/itsi/http_request.rb +31 -39
- data/lib/itsi/http_response.rb +5 -0
- data/lib/itsi/rack_env_pool.rb +59 -0
- data/lib/itsi/server/config/config_helpers.rb +1 -2
- data/lib/itsi/server/config/dsl.rb +5 -4
- data/lib/itsi/server/config/middleware/etag.md +3 -7
- data/lib/itsi/server/config/middleware/etag.rb +2 -4
- data/lib/itsi/server/config/middleware/proxy.rb +1 -1
- data/lib/itsi/server/config/middleware/rackup_file.rb +2 -2
- data/lib/itsi/server/config/options/auto_reload_config.rb +6 -2
- data/lib/itsi/server/config/options/include.rb +5 -2
- data/lib/itsi/server/config/options/listen_backlog.rb +1 -1
- data/lib/itsi/server/config/options/pipeline_flush.md +16 -0
- data/lib/itsi/server/config/options/pipeline_flush.rb +19 -0
- data/lib/itsi/server/config/options/send_buffer_size.md +15 -0
- data/lib/itsi/server/config/options/send_buffer_size.rb +19 -0
- data/lib/itsi/server/config/options/writev.md +25 -0
- data/lib/itsi/server/config/options/writev.rb +19 -0
- data/lib/itsi/server/config.rb +43 -31
- data/lib/itsi/server/default_config/Itsi.rb +1 -4
- data/lib/itsi/server/grpc/grpc_call.rb +2 -0
- data/lib/itsi/server/grpc/grpc_interface.rb +2 -2
- data/lib/itsi/server/rack/handler/itsi.rb +3 -1
- data/lib/itsi/server/rack_interface.rb +17 -12
- data/lib/itsi/server/route_tester.rb +1 -1
- data/lib/itsi/server/scheduler_interface.rb +2 -0
- data/lib/itsi/server/version.rb +1 -1
- data/lib/itsi/server.rb +1 -0
- data/lib/ruby_lsp/itsi/addon.rb +12 -13
- 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
|
10
|
+
use std::sync::{Arc, LazyLock};
|
10
11
|
use std::time::{Duration, Instant};
|
11
|
-
use tokio::sync::
|
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
|
-
|
246
|
-
|
247
|
-
|
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
|
-
|
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
|
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()
|
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()
|
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()
|
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()
|
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::
|
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
|
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(
|
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(
|
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(
|
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
|
-
.
|
692
|
-
|
693
|
-
|
694
|
-
|
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<
|
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
|
-
.
|
711
|
-
|
712
|
-
|
713
|
-
|
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(
|
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(
|
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(
|
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(
|
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
|
-
|
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>) ->
|
991
|
-
|
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() ->
|
976
|
+
fn build_not_modified_response() -> HttpResponse {
|
996
977
|
Response::builder()
|
997
978
|
.status(StatusCode::NOT_MODIFIED)
|
998
|
-
.body(
|
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:
|
1013
|
-
) ->
|
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!("{}/",
|
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":
|
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
|
-
|
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
|
-
|
1358
|
+
name, name, formatted_size, modified_str
|
1384
1359
|
));
|
1385
1360
|
rows.push('\n');
|
1386
1361
|
}
|
data/lib/itsi/http_request.rb
CHANGED
@@ -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 =
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
26
|
-
hm.default_proc = proc { |_, key| "HTTP_#{key.upcase.gsub(/-/, "_")}" }
|
27
|
-
end
|
26
|
+
end.to_h
|
28
27
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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 =
|
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"] =
|
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
|
-
|
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"
|
data/lib/itsi/http_response.rb
CHANGED
@@ -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
|
-
|
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,
|
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,
|
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
|
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
|
|