itsi-server 0.1.1 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +7 -0
- data/Cargo.lock +3937 -0
- data/Cargo.toml +7 -0
- data/README.md +4 -0
- data/Rakefile +8 -1
- data/_index.md +6 -0
- data/exe/itsi +141 -46
- data/ext/itsi_error/Cargo.toml +3 -0
- data/ext/itsi_error/src/lib.rs +98 -24
- data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
- data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
- data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
- data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
- data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
- data/ext/itsi_instrument_entry/Cargo.toml +15 -0
- data/ext/itsi_instrument_entry/src/lib.rs +31 -0
- data/ext/itsi_rb_helpers/Cargo.toml +3 -0
- data/ext/itsi_rb_helpers/src/heap_value.rs +139 -0
- data/ext/itsi_rb_helpers/src/lib.rs +140 -10
- data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
- data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
- data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
- data/ext/itsi_rb_helpers/target/debug/build/rb-sys-eb9ed4ff3a60f995/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
- data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
- data/ext/itsi_scheduler/Cargo.toml +24 -0
- data/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
- data/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
- data/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
- data/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
- data/ext/itsi_scheduler/src/lib.rs +38 -0
- data/ext/itsi_server/Cargo.lock +2956 -0
- data/ext/itsi_server/Cargo.toml +72 -14
- data/ext/itsi_server/extconf.rb +1 -1
- data/ext/itsi_server/src/default_responses/html/401.html +68 -0
- data/ext/itsi_server/src/default_responses/html/403.html +68 -0
- data/ext/itsi_server/src/default_responses/html/404.html +68 -0
- data/ext/itsi_server/src/default_responses/html/413.html +71 -0
- data/ext/itsi_server/src/default_responses/html/429.html +68 -0
- data/ext/itsi_server/src/default_responses/html/500.html +71 -0
- data/ext/itsi_server/src/default_responses/html/502.html +71 -0
- data/ext/itsi_server/src/default_responses/html/503.html +68 -0
- data/ext/itsi_server/src/default_responses/html/504.html +69 -0
- data/ext/itsi_server/src/default_responses/html/index.html +238 -0
- data/ext/itsi_server/src/default_responses/json/401.json +6 -0
- data/ext/itsi_server/src/default_responses/json/403.json +6 -0
- data/ext/itsi_server/src/default_responses/json/404.json +6 -0
- data/ext/itsi_server/src/default_responses/json/413.json +6 -0
- data/ext/itsi_server/src/default_responses/json/429.json +6 -0
- data/ext/itsi_server/src/default_responses/json/500.json +6 -0
- data/ext/itsi_server/src/default_responses/json/502.json +6 -0
- data/ext/itsi_server/src/default_responses/json/503.json +6 -0
- data/ext/itsi_server/src/default_responses/json/504.json +6 -0
- data/ext/itsi_server/src/default_responses/mod.rs +11 -0
- data/ext/itsi_server/src/env.rs +43 -0
- data/ext/itsi_server/src/lib.rs +132 -40
- data/ext/itsi_server/src/prelude.rs +2 -0
- data/ext/itsi_server/src/ruby_types/itsi_body_proxy/big_bytes.rs +109 -0
- data/ext/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +143 -0
- data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
- data/ext/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +264 -0
- data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +345 -0
- data/ext/itsi_server/src/ruby_types/itsi_http_response.rs +391 -0
- data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +225 -0
- data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +375 -0
- data/ext/itsi_server/src/ruby_types/itsi_server.rs +83 -0
- data/ext/itsi_server/src/ruby_types/mod.rs +48 -0
- data/ext/itsi_server/src/server/binds/bind.rs +201 -0
- data/ext/itsi_server/src/server/binds/bind_protocol.rs +37 -0
- data/ext/itsi_server/src/server/binds/listener.rs +432 -0
- data/ext/itsi_server/src/server/binds/mod.rs +4 -0
- data/ext/itsi_server/src/server/binds/tls/locked_dir_cache.rs +132 -0
- data/ext/itsi_server/src/server/binds/tls.rs +270 -0
- data/ext/itsi_server/src/server/byte_frame.rs +32 -0
- data/ext/itsi_server/src/server/http_message_types.rs +97 -0
- data/ext/itsi_server/src/server/io_stream.rs +105 -0
- data/ext/itsi_server/src/server/lifecycle_event.rs +12 -0
- data/ext/itsi_server/src/server/middleware_stack/middleware.rs +165 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +56 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +87 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +86 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +285 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +142 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +289 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +292 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +55 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +190 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +157 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +195 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +82 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +201 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +87 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +414 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +131 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +76 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +44 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +36 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +126 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +180 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +163 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +12 -0
- data/ext/itsi_server/src/server/middleware_stack/mod.rs +347 -0
- data/ext/itsi_server/src/server/mod.rs +12 -5
- data/ext/itsi_server/src/server/process_worker.rs +247 -0
- data/ext/itsi_server/src/server/request_job.rs +11 -0
- data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +342 -0
- data/ext/itsi_server/src/server/serve_strategy/mod.rs +30 -0
- data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +421 -0
- data/ext/itsi_server/src/server/signal.rs +76 -0
- data/ext/itsi_server/src/server/size_limited_incoming.rs +101 -0
- data/ext/itsi_server/src/server/thread_worker.rs +475 -0
- data/ext/itsi_server/src/services/cache_store.rs +74 -0
- data/ext/itsi_server/src/services/itsi_http_service.rs +239 -0
- data/ext/itsi_server/src/services/mime_types.rs +1416 -0
- data/ext/itsi_server/src/services/mod.rs +6 -0
- data/ext/itsi_server/src/services/password_hasher.rs +83 -0
- data/ext/itsi_server/src/services/rate_limiter.rs +569 -0
- data/ext/itsi_server/src/services/static_file_server.rs +1324 -0
- data/ext/itsi_tracing/Cargo.toml +5 -0
- data/ext/itsi_tracing/src/lib.rs +315 -7
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
- data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
- data/lib/itsi/http_request/response_status_shortcodes.rb +74 -0
- data/lib/itsi/http_request.rb +186 -0
- data/lib/itsi/http_response.rb +41 -0
- data/lib/itsi/passfile.rb +109 -0
- data/lib/itsi/server/config/dsl.rb +565 -0
- data/lib/itsi/server/config.rb +166 -0
- data/lib/itsi/server/default_app/default_app.rb +34 -0
- data/lib/itsi/server/default_app/index.html +115 -0
- data/lib/itsi/server/default_config/Itsi-rackup.rb +119 -0
- data/lib/itsi/server/default_config/Itsi.rb +107 -0
- data/lib/itsi/server/grpc/grpc_call.rb +246 -0
- data/lib/itsi/server/grpc/grpc_interface.rb +100 -0
- data/lib/itsi/server/grpc/reflection/v1/reflection_pb.rb +26 -0
- data/lib/itsi/server/grpc/reflection/v1/reflection_services_pb.rb +122 -0
- data/lib/itsi/server/rack/handler/itsi.rb +27 -0
- data/lib/itsi/server/rack_interface.rb +94 -0
- data/lib/itsi/server/route_tester.rb +107 -0
- data/lib/itsi/server/scheduler_interface.rb +21 -0
- data/lib/itsi/server/scheduler_mode.rb +10 -0
- data/lib/itsi/server/signal_trap.rb +29 -0
- data/lib/itsi/server/typed_handlers/param_parser.rb +200 -0
- data/lib/itsi/server/typed_handlers/source_parser.rb +55 -0
- data/lib/itsi/server/typed_handlers.rb +17 -0
- data/lib/itsi/server/version.rb +1 -1
- data/lib/itsi/server.rb +160 -9
- data/lib/itsi/standard_headers.rb +86 -0
- data/lib/ruby_lsp/itsi/addon.rb +111 -0
- data/lib/shell_completions/completions.rb +26 -0
- metadata +182 -25
- data/ext/itsi_server/src/request/itsi_request.rs +0 -143
- data/ext/itsi_server/src/request/mod.rs +0 -1
- data/ext/itsi_server/src/server/bind.rs +0 -138
- data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -32
- data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -52
- data/ext/itsi_server/src/server/itsi_server.rs +0 -182
- data/ext/itsi_server/src/server/listener.rs +0 -218
- data/ext/itsi_server/src/server/tls.rs +0 -138
- data/ext/itsi_server/src/server/transfer_protocol.rs +0 -23
- data/ext/itsi_server/src/stream_writer/mod.rs +0 -21
- data/lib/itsi/request.rb +0 -39
@@ -0,0 +1,109 @@
|
|
1
|
+
use bytes::Bytes;
|
2
|
+
use magnus::{IntoValue, Ruby, Value};
|
3
|
+
use std::io::{Result as IoResult, Write};
|
4
|
+
use std::path::PathBuf;
|
5
|
+
use tempfile::NamedTempFile;
|
6
|
+
|
7
|
+
const THRESHOLD: usize = 1024 * 1024; // 1 MB
|
8
|
+
|
9
|
+
/// A container that will hold data in memory if it’s small, or in a temporary file on disk if it exceeds THRESHOLD.
|
10
|
+
/// Used for providing Rack input data.
|
11
|
+
pub enum BigBytes {
|
12
|
+
InMemory(Vec<u8>),
|
13
|
+
OnDisk(NamedTempFile),
|
14
|
+
}
|
15
|
+
|
16
|
+
/// The result type for reading the contents of a `BigBytes` value.
|
17
|
+
pub enum BigBytesReadResult {
|
18
|
+
/// When the data is stored in memory, returns the cached bytes.
|
19
|
+
InMemory(Vec<u8>),
|
20
|
+
/// When the data is stored on disk, returns the path to the temporary file.
|
21
|
+
OnDisk(PathBuf),
|
22
|
+
}
|
23
|
+
|
24
|
+
impl BigBytes {
|
25
|
+
/// Creates a new, empty BigBytes instance (initially in memory).
|
26
|
+
pub fn new() -> Self {
|
27
|
+
BigBytes::InMemory(Vec::new())
|
28
|
+
}
|
29
|
+
|
30
|
+
/// Returns either the raw bytes, or the file path of the BigBytes
|
31
|
+
///
|
32
|
+
/// - If stored in memory, returns a clone of the bytes.
|
33
|
+
/// - If stored on disk, returns the file path of the temporary file.
|
34
|
+
pub fn read(&self) -> IoResult<BigBytesReadResult> {
|
35
|
+
match self {
|
36
|
+
BigBytes::InMemory(vec) => Ok(BigBytesReadResult::InMemory(vec.clone())),
|
37
|
+
BigBytes::OnDisk(temp_file) => {
|
38
|
+
// Flush to be safe, then return the file path.
|
39
|
+
temp_file.as_file().sync_all()?;
|
40
|
+
Ok(BigBytesReadResult::OnDisk(temp_file.path().to_path_buf()))
|
41
|
+
}
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
/// Turn this into a value that can be used in Ruby.
|
46
|
+
pub fn as_value(&self) -> Option<Value> {
|
47
|
+
match self {
|
48
|
+
BigBytes::InMemory(bytes) => {
|
49
|
+
let bytes = Bytes::from(bytes.to_owned());
|
50
|
+
if bytes.is_empty() {
|
51
|
+
None
|
52
|
+
} else {
|
53
|
+
Some(bytes.into_value())
|
54
|
+
}
|
55
|
+
}
|
56
|
+
BigBytes::OnDisk(path) => {
|
57
|
+
let ruby = Ruby::get().unwrap();
|
58
|
+
let rarray = ruby.ary_new();
|
59
|
+
rarray.push(path.path().to_str().unwrap().into_value()).ok();
|
60
|
+
Some(rarray.into_value())
|
61
|
+
}
|
62
|
+
}
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
impl Drop for BigBytes {
|
67
|
+
fn drop(&mut self) {
|
68
|
+
match self {
|
69
|
+
BigBytes::InMemory(_) => {}
|
70
|
+
BigBytes::OnDisk(path) => {
|
71
|
+
let _ = std::fs::remove_file(path);
|
72
|
+
}
|
73
|
+
}
|
74
|
+
}
|
75
|
+
}
|
76
|
+
|
77
|
+
impl Default for BigBytes {
|
78
|
+
fn default() -> Self {
|
79
|
+
Self::new()
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
impl Write for BigBytes {
|
84
|
+
fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
|
85
|
+
match self {
|
86
|
+
BigBytes::InMemory(vec) => {
|
87
|
+
// Check if writing the new bytes would exceed the threshold.
|
88
|
+
if vec.len() + buf.len() > THRESHOLD {
|
89
|
+
let mut tmp = NamedTempFile::new()?;
|
90
|
+
tmp.write_all(vec)?;
|
91
|
+
tmp.write_all(buf)?;
|
92
|
+
*self = BigBytes::OnDisk(tmp);
|
93
|
+
Ok(buf.len())
|
94
|
+
} else {
|
95
|
+
vec.extend_from_slice(buf);
|
96
|
+
Ok(buf.len())
|
97
|
+
}
|
98
|
+
}
|
99
|
+
BigBytes::OnDisk(tmp_file) => tmp_file.write(buf),
|
100
|
+
}
|
101
|
+
}
|
102
|
+
|
103
|
+
fn flush(&mut self) -> IoResult<()> {
|
104
|
+
match self {
|
105
|
+
BigBytes::InMemory(_) => Ok(()),
|
106
|
+
BigBytes::OnDisk(tmp_file) => tmp_file.flush(),
|
107
|
+
}
|
108
|
+
}
|
109
|
+
}
|
@@ -0,0 +1,143 @@
|
|
1
|
+
pub mod big_bytes;
|
2
|
+
use big_bytes::BigBytes;
|
3
|
+
use bytes::Bytes;
|
4
|
+
use futures::executor::block_on;
|
5
|
+
use http_body_util::{BodyDataStream, BodyExt};
|
6
|
+
use hyper::body::Incoming;
|
7
|
+
use magnus::{error::Result as MagnusResult, scan_args, IntoValue, RString, Ruby, Value};
|
8
|
+
use parking_lot::Mutex;
|
9
|
+
use std::sync::{
|
10
|
+
atomic::{self, AtomicBool},
|
11
|
+
Arc,
|
12
|
+
};
|
13
|
+
use tokio_stream::StreamExt;
|
14
|
+
|
15
|
+
use crate::server::size_limited_incoming::SizeLimitedIncoming;
|
16
|
+
|
17
|
+
#[magnus::wrap(class = "Itsi::BodyProxy", free_immediately, size)]
|
18
|
+
#[derive(Debug, Clone)]
|
19
|
+
pub struct ItsiBodyProxy {
|
20
|
+
pub incoming: Arc<Mutex<BodyDataStream<SizeLimitedIncoming<Incoming>>>>,
|
21
|
+
pub closed: Arc<AtomicBool>,
|
22
|
+
pub buf: Arc<Mutex<Vec<u8>>>,
|
23
|
+
}
|
24
|
+
|
25
|
+
pub enum ItsiBody {
|
26
|
+
Buffered(BigBytes),
|
27
|
+
Stream(ItsiBodyProxy),
|
28
|
+
}
|
29
|
+
|
30
|
+
impl ItsiBody {
|
31
|
+
pub fn into_value(&self) -> Option<Value> {
|
32
|
+
match self {
|
33
|
+
ItsiBody::Buffered(bytes) => bytes.as_value(),
|
34
|
+
ItsiBody::Stream(proxy) => Some(proxy.clone().into_value()),
|
35
|
+
}
|
36
|
+
}
|
37
|
+
}
|
38
|
+
impl ItsiBodyProxy {
|
39
|
+
pub fn new(incoming: SizeLimitedIncoming<Incoming>) -> Self {
|
40
|
+
ItsiBodyProxy {
|
41
|
+
incoming: Arc::new(Mutex::new(incoming.into_data_stream())),
|
42
|
+
closed: Arc::new(AtomicBool::new(false)),
|
43
|
+
buf: Arc::new(Mutex::new(vec![])),
|
44
|
+
}
|
45
|
+
}
|
46
|
+
/// Read up to the next line-break OR EOF
|
47
|
+
pub fn gets(&self) -> MagnusResult<Option<Bytes>> {
|
48
|
+
self.verify_open()?;
|
49
|
+
let mut stream = self.incoming.lock();
|
50
|
+
let mut buf = self.buf.lock();
|
51
|
+
while !buf.contains(&b'\n') {
|
52
|
+
if let Some(chunk) = block_on(stream.next()) {
|
53
|
+
let chunk = chunk.map_err(|err| {
|
54
|
+
magnus::Error::new(
|
55
|
+
magnus::exception::standard_error(),
|
56
|
+
format!("Error reading body {:?}", err),
|
57
|
+
)
|
58
|
+
})?;
|
59
|
+
buf.extend_from_slice(&chunk);
|
60
|
+
} else {
|
61
|
+
break;
|
62
|
+
}
|
63
|
+
}
|
64
|
+
if let Some(pos) = buf.iter().position(|&x| x == b'\n') {
|
65
|
+
let line = buf.drain(..=pos).collect::<Vec<u8>>();
|
66
|
+
Ok(Some(line.into()))
|
67
|
+
} else if !buf.is_empty() {
|
68
|
+
let line = buf.drain(..).collect::<Vec<u8>>();
|
69
|
+
Ok(Some(line.into()))
|
70
|
+
} else {
|
71
|
+
Ok(None)
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
pub fn read(&self, args: &[Value]) -> MagnusResult<Option<RString>> {
|
76
|
+
self.verify_open()?;
|
77
|
+
let scanned =
|
78
|
+
scan_args::scan_args::<(), (Option<usize>, Option<RString>), (), (), (), ()>(args)?;
|
79
|
+
let (length, mut buffer) = scanned.optional;
|
80
|
+
let mut stream = self.incoming.lock();
|
81
|
+
let mut buf = self.buf.lock();
|
82
|
+
|
83
|
+
while length.is_none_or(|target_length| buf.len() < target_length) {
|
84
|
+
if let Some(chunk) = block_on(stream.next()) {
|
85
|
+
let chunk = chunk.map_err(|err| {
|
86
|
+
magnus::Error::new(
|
87
|
+
magnus::exception::standard_error(),
|
88
|
+
format!("Error reading body {:?}", err),
|
89
|
+
)
|
90
|
+
})?;
|
91
|
+
buf.extend_from_slice(&chunk);
|
92
|
+
} else if length.is_some() {
|
93
|
+
return Ok(None);
|
94
|
+
} else {
|
95
|
+
break;
|
96
|
+
}
|
97
|
+
}
|
98
|
+
let output_string = buffer.take().unwrap_or(RString::buf_new(buf.len()));
|
99
|
+
output_string.cat(buf.clone());
|
100
|
+
buf.clear();
|
101
|
+
Ok(Some(output_string))
|
102
|
+
}
|
103
|
+
|
104
|
+
pub fn to_bytes(&self) -> MagnusResult<Vec<u8>> {
|
105
|
+
self.verify_open()?;
|
106
|
+
let mut stream = self.incoming.lock();
|
107
|
+
let mut buf = self.buf.lock();
|
108
|
+
|
109
|
+
while let Some(chunk) = block_on(stream.next()) {
|
110
|
+
let chunk = chunk.map_err(|err| {
|
111
|
+
magnus::Error::new(
|
112
|
+
magnus::exception::standard_error(),
|
113
|
+
format!("Error reading body {:?}", err),
|
114
|
+
)
|
115
|
+
})?;
|
116
|
+
buf.extend_from_slice(&chunk);
|
117
|
+
}
|
118
|
+
|
119
|
+
Ok(buf.clone())
|
120
|
+
}
|
121
|
+
|
122
|
+
/// Equivalent to calling gets and yielding it, until we reach EOF
|
123
|
+
pub fn each(ruby: &Ruby, rbself: &Self) -> MagnusResult<()> {
|
124
|
+
let proc = ruby.block_proc()?;
|
125
|
+
while let Some(str) = rbself.gets()? {
|
126
|
+
proc.call::<_, Value>((str,))?;
|
127
|
+
}
|
128
|
+
Ok(())
|
129
|
+
}
|
130
|
+
|
131
|
+
fn verify_open(&self) -> MagnusResult<()> {
|
132
|
+
if self.closed.load(atomic::Ordering::SeqCst) {
|
133
|
+
return Err(magnus::Error::new(
|
134
|
+
magnus::exception::standard_error(),
|
135
|
+
"Body stream is closed",
|
136
|
+
));
|
137
|
+
}
|
138
|
+
Ok(())
|
139
|
+
}
|
140
|
+
pub fn close(&self) {
|
141
|
+
self.closed.store(true, atomic::Ordering::SeqCst);
|
142
|
+
}
|
143
|
+
}
|
@@ -0,0 +1,344 @@
|
|
1
|
+
use super::itsi_grpc_response_stream::ItsiGrpcResponseStream;
|
2
|
+
use crate::prelude::*;
|
3
|
+
use crate::server::http_message_types::{HttpRequest, HttpResponse};
|
4
|
+
use crate::server::{byte_frame::ByteFrame, request_job::RequestJob};
|
5
|
+
use crate::services::itsi_http_service::HttpRequestContext;
|
6
|
+
use async_compression::futures::bufread::{GzipDecoder, GzipEncoder, ZlibDecoder, ZlibEncoder};
|
7
|
+
use bytes::Bytes;
|
8
|
+
use derive_more::Debug;
|
9
|
+
use futures::{executor::block_on, io::Cursor, AsyncReadExt};
|
10
|
+
use http::{request::Parts, Response, StatusCode};
|
11
|
+
use http_body_util::{combinators::BoxBody, BodyExt, Empty};
|
12
|
+
use itsi_error::CLIENT_CONNECTION_CLOSED;
|
13
|
+
use itsi_rb_helpers::{print_rb_backtrace, HeapValue};
|
14
|
+
use itsi_tracing::debug;
|
15
|
+
use magnus::{
|
16
|
+
block::Proc,
|
17
|
+
error::{ErrorType, Result as MagnusResult},
|
18
|
+
Error, Symbol,
|
19
|
+
};
|
20
|
+
use magnus::{
|
21
|
+
value::{LazyId, ReprValue},
|
22
|
+
Ruby, Value,
|
23
|
+
};
|
24
|
+
use regex::Regex;
|
25
|
+
use std::sync::LazyLock;
|
26
|
+
use std::{collections::HashMap, sync::Arc, time::Instant};
|
27
|
+
use tokio::sync::mpsc::{self};
|
28
|
+
|
29
|
+
static ID_MESSAGE: LazyId = LazyId::new("message");
|
30
|
+
static MIN_GZIP_SIZE: u32 = 128;
|
31
|
+
static MIN_DEFLATE_SIZE: u32 = 128;
|
32
|
+
static METHOD_NAME_REGEX: LazyLock<Regex> =
|
33
|
+
LazyLock::new(|| Regex::new(r"([a-z])([A-Z])").expect("Failed to compile regex"));
|
34
|
+
|
35
|
+
#[derive(Debug)]
|
36
|
+
#[magnus::wrap(class = "Itsi::GrpcCall", free_immediately, size)]
|
37
|
+
pub struct ItsiGrpcCall {
|
38
|
+
pub parts: Parts,
|
39
|
+
pub start: Instant,
|
40
|
+
pub compression_in: CompressionAlgorithm,
|
41
|
+
pub compression_out: CompressionAlgorithm,
|
42
|
+
#[debug(skip)]
|
43
|
+
pub context: HttpRequestContext,
|
44
|
+
#[debug(skip)]
|
45
|
+
pub stream: ItsiGrpcResponseStream,
|
46
|
+
}
|
47
|
+
|
48
|
+
#[derive(Debug, Clone)]
|
49
|
+
pub enum CompressionAlgorithm {
|
50
|
+
None,
|
51
|
+
Deflate,
|
52
|
+
Gzip,
|
53
|
+
}
|
54
|
+
|
55
|
+
impl ItsiGrpcCall {
|
56
|
+
pub fn service_name(&self) -> MagnusResult<String> {
|
57
|
+
let path = self.parts.uri.path();
|
58
|
+
Ok(path.split('/').nth_back(1).unwrap().to_string())
|
59
|
+
}
|
60
|
+
|
61
|
+
pub fn method_name(&self) -> MagnusResult<Symbol> {
|
62
|
+
let path = self.parts.uri.path();
|
63
|
+
let method_name = path.split('/').nth_back(0).unwrap();
|
64
|
+
let snake_case_method_name = METHOD_NAME_REGEX
|
65
|
+
.replace_all(method_name, "${1}_${2}")
|
66
|
+
.to_lowercase();
|
67
|
+
Ok(Symbol::new(snake_case_method_name))
|
68
|
+
}
|
69
|
+
|
70
|
+
pub fn stream(&self) -> MagnusResult<ItsiGrpcResponseStream> {
|
71
|
+
Ok(self.stream.clone())
|
72
|
+
}
|
73
|
+
|
74
|
+
pub fn timeout(&self) -> MagnusResult<Option<f64>> {
|
75
|
+
let timeout_str = self
|
76
|
+
.parts
|
77
|
+
.headers
|
78
|
+
.get("grpc-timeout")
|
79
|
+
.and_then(|hv| hv.to_str().ok())
|
80
|
+
.unwrap_or("");
|
81
|
+
Ok(parse_grpc_timeout(timeout_str).ok())
|
82
|
+
}
|
83
|
+
|
84
|
+
pub fn is_cancelled(&self) -> MagnusResult<bool> {
|
85
|
+
self.stream.is_cancelled()
|
86
|
+
}
|
87
|
+
|
88
|
+
pub fn add_headers(&self, headers: HashMap<Bytes, Vec<Bytes>>) -> MagnusResult<()> {
|
89
|
+
self.stream.add_headers(headers)
|
90
|
+
}
|
91
|
+
|
92
|
+
pub fn content_type_str(&self) -> &str {
|
93
|
+
self.parts
|
94
|
+
.headers
|
95
|
+
.get("Content-Type")
|
96
|
+
.and_then(|hv| hv.to_str().ok())
|
97
|
+
.unwrap_or("application/x-www-form-urlencoded")
|
98
|
+
}
|
99
|
+
|
100
|
+
pub fn is_json(&self) -> bool {
|
101
|
+
self.content_type_str() == "application/json"
|
102
|
+
}
|
103
|
+
|
104
|
+
pub fn process(self, ruby: &Ruby, app_proc: Arc<HeapValue<Proc>>) -> magnus::error::Result<()> {
|
105
|
+
let response = self.stream.clone();
|
106
|
+
let result = app_proc.call::<_, Value>((self,));
|
107
|
+
if let Err(err) = result {
|
108
|
+
Self::internal_error(ruby, response, err);
|
109
|
+
}
|
110
|
+
Ok(())
|
111
|
+
}
|
112
|
+
|
113
|
+
pub fn internal_error(_ruby: &Ruby, stream: ItsiGrpcResponseStream, err: Error) {
|
114
|
+
if let Some(rb_err) = err.value() {
|
115
|
+
print_rb_backtrace(rb_err);
|
116
|
+
stream.internal_server_error(err.to_string());
|
117
|
+
} else {
|
118
|
+
stream.internal_server_error(err.to_string());
|
119
|
+
}
|
120
|
+
}
|
121
|
+
|
122
|
+
pub(crate) async fn process_request(
|
123
|
+
app: Arc<HeapValue<Proc>>,
|
124
|
+
hyper_request: HttpRequest,
|
125
|
+
context: &HttpRequestContext,
|
126
|
+
nonblocking: bool,
|
127
|
+
) -> itsi_error::Result<HttpResponse> {
|
128
|
+
let (request, mut receiver) = ItsiGrpcCall::new(hyper_request, context).await;
|
129
|
+
let shutdown_channel = context.service.shutdown_channel.clone();
|
130
|
+
let response_stream = request.stream.clone();
|
131
|
+
let sender = if nonblocking {
|
132
|
+
&context.nonblocking_sender
|
133
|
+
} else {
|
134
|
+
&context.sender
|
135
|
+
};
|
136
|
+
match sender
|
137
|
+
.send(RequestJob::ProcessGrpcRequest(request, app))
|
138
|
+
.await
|
139
|
+
{
|
140
|
+
Err(err) => {
|
141
|
+
error!("Error occurred: {}", err);
|
142
|
+
let mut response = Response::new(BoxBody::new(Empty::new()));
|
143
|
+
*response.status_mut() = StatusCode::BAD_REQUEST;
|
144
|
+
Ok(response)
|
145
|
+
}
|
146
|
+
_ => match receiver.recv().await {
|
147
|
+
Some(first_frame) => Ok(response_stream
|
148
|
+
.build_response(first_frame, receiver, shutdown_channel)
|
149
|
+
.await),
|
150
|
+
None => Ok(Response::new(BoxBody::new(Empty::new()))),
|
151
|
+
},
|
152
|
+
}
|
153
|
+
}
|
154
|
+
|
155
|
+
pub fn is_connection_closed_err(ruby: &Ruby, err: &Error) -> bool {
|
156
|
+
match err.error_type() {
|
157
|
+
ErrorType::Jump(_) => false,
|
158
|
+
ErrorType::Error(_, _) => false,
|
159
|
+
ErrorType::Exception(exception) => {
|
160
|
+
exception.is_kind_of(ruby.exception_eof_error())
|
161
|
+
&& err
|
162
|
+
.value()
|
163
|
+
.map(|v| {
|
164
|
+
v.funcall::<_, _, String>(*ID_MESSAGE, ())
|
165
|
+
.unwrap_or("".to_string())
|
166
|
+
.eq(CLIENT_CONNECTION_CLOSED)
|
167
|
+
})
|
168
|
+
.unwrap_or(false)
|
169
|
+
}
|
170
|
+
}
|
171
|
+
}
|
172
|
+
|
173
|
+
pub fn should_compress_output(&self, message_size: u32) -> bool {
|
174
|
+
match self.compression_out {
|
175
|
+
CompressionAlgorithm::Gzip => message_size > MIN_GZIP_SIZE,
|
176
|
+
CompressionAlgorithm::Deflate => message_size > MIN_DEFLATE_SIZE,
|
177
|
+
CompressionAlgorithm::None => false,
|
178
|
+
}
|
179
|
+
}
|
180
|
+
|
181
|
+
pub fn compress_output(&self, bytes: Bytes) -> MagnusResult<Bytes> {
|
182
|
+
match self.compression_out {
|
183
|
+
CompressionAlgorithm::Gzip => Self::compress_gzip(bytes),
|
184
|
+
CompressionAlgorithm::Deflate => Self::compress_deflate(bytes),
|
185
|
+
CompressionAlgorithm::None => Ok(bytes),
|
186
|
+
}
|
187
|
+
}
|
188
|
+
|
189
|
+
pub fn decompress_input(&self, bytes: Bytes) -> MagnusResult<Bytes> {
|
190
|
+
match self.compression_in {
|
191
|
+
CompressionAlgorithm::Gzip => Self::decompress_gzip(bytes),
|
192
|
+
CompressionAlgorithm::Deflate => Self::decompress_deflate(bytes),
|
193
|
+
CompressionAlgorithm::None => Ok(bytes),
|
194
|
+
}
|
195
|
+
}
|
196
|
+
|
197
|
+
fn decompress_deflate(input: Bytes) -> MagnusResult<Bytes> {
|
198
|
+
let cursor = Cursor::new(input);
|
199
|
+
let mut decoder = ZlibDecoder::new(cursor);
|
200
|
+
|
201
|
+
let result = block_on(async {
|
202
|
+
let mut output = Vec::new();
|
203
|
+
decoder.read_to_end(&mut output).await?;
|
204
|
+
Ok(Bytes::from(output))
|
205
|
+
})
|
206
|
+
.map_err(|e: std::io::Error| {
|
207
|
+
Error::new(
|
208
|
+
magnus::exception::standard_error(),
|
209
|
+
format!("deflate decompression failed: {}", e),
|
210
|
+
)
|
211
|
+
})?;
|
212
|
+
|
213
|
+
Ok(result)
|
214
|
+
}
|
215
|
+
|
216
|
+
fn decompress_gzip(input: Bytes) -> MagnusResult<Bytes> {
|
217
|
+
let cursor = Cursor::new(input);
|
218
|
+
let mut decoder = GzipDecoder::new(cursor);
|
219
|
+
|
220
|
+
let result = block_on(async {
|
221
|
+
let mut output = Vec::new();
|
222
|
+
decoder.read_to_end(&mut output).await?;
|
223
|
+
Ok(Bytes::from(output))
|
224
|
+
})
|
225
|
+
.map_err(|e: std::io::Error| {
|
226
|
+
Error::new(
|
227
|
+
magnus::exception::standard_error(),
|
228
|
+
format!("gzip decompression failed: {}", e),
|
229
|
+
)
|
230
|
+
})?;
|
231
|
+
|
232
|
+
Ok(result)
|
233
|
+
}
|
234
|
+
|
235
|
+
fn compress_gzip(input: Bytes) -> MagnusResult<Bytes> {
|
236
|
+
let mut output = Vec::with_capacity(input.len() / 2);
|
237
|
+
let cursor = Cursor::new(input);
|
238
|
+
let mut encoder = GzipEncoder::new(cursor);
|
239
|
+
|
240
|
+
let result = block_on(async {
|
241
|
+
encoder.read_to_end(&mut output).await?;
|
242
|
+
Ok::<Bytes, std::io::Error>(output.into())
|
243
|
+
})
|
244
|
+
.map_err(|e| {
|
245
|
+
Error::new(
|
246
|
+
magnus::exception::standard_error(),
|
247
|
+
format!("gzip compression failed: {e}"),
|
248
|
+
)
|
249
|
+
})?;
|
250
|
+
|
251
|
+
Ok(result)
|
252
|
+
}
|
253
|
+
|
254
|
+
fn compress_deflate(input: Bytes) -> MagnusResult<Bytes> {
|
255
|
+
let mut output = Vec::with_capacity(input.len() / 2);
|
256
|
+
let cursor = Cursor::new(input);
|
257
|
+
let mut encoder = ZlibEncoder::new(cursor);
|
258
|
+
|
259
|
+
let result = block_on(async {
|
260
|
+
encoder.read_to_end(&mut output).await?;
|
261
|
+
Ok::<Bytes, std::io::Error>(output.into())
|
262
|
+
})
|
263
|
+
.map_err(|e| {
|
264
|
+
Error::new(
|
265
|
+
magnus::exception::standard_error(),
|
266
|
+
format!("deflate compression failed: {e}"),
|
267
|
+
)
|
268
|
+
})?;
|
269
|
+
|
270
|
+
Ok(result)
|
271
|
+
}
|
272
|
+
|
273
|
+
pub(crate) async fn new(
|
274
|
+
request: HttpRequest,
|
275
|
+
context: &HttpRequestContext,
|
276
|
+
) -> (ItsiGrpcCall, mpsc::Receiver<ByteFrame>) {
|
277
|
+
let (parts, body) = request.into_parts();
|
278
|
+
let response_channel = mpsc::channel::<ByteFrame>(100);
|
279
|
+
let compression_in: CompressionAlgorithm = match parts.headers.get("grpc-encoding") {
|
280
|
+
Some(encoding) => match encoding.to_str() {
|
281
|
+
Ok(encoding) => match encoding {
|
282
|
+
"gzip" => CompressionAlgorithm::Gzip,
|
283
|
+
"deflate" => CompressionAlgorithm::Deflate,
|
284
|
+
_ => CompressionAlgorithm::None,
|
285
|
+
},
|
286
|
+
Err(_) => CompressionAlgorithm::None,
|
287
|
+
},
|
288
|
+
None => CompressionAlgorithm::None,
|
289
|
+
};
|
290
|
+
let compression_out: CompressionAlgorithm = match parts.headers.get("grpc-accept-encoding")
|
291
|
+
{
|
292
|
+
Some(accept_encoding) => match accept_encoding.to_str() {
|
293
|
+
Ok(accept_encoding) => {
|
294
|
+
let encodings: Vec<&str> =
|
295
|
+
accept_encoding.split(',').map(|s| s.trim()).collect();
|
296
|
+
if encodings.contains(&"gzip") {
|
297
|
+
CompressionAlgorithm::Gzip
|
298
|
+
} else if encodings.contains(&"deflate") {
|
299
|
+
CompressionAlgorithm::Deflate
|
300
|
+
} else {
|
301
|
+
CompressionAlgorithm::None
|
302
|
+
}
|
303
|
+
}
|
304
|
+
Err(_) => CompressionAlgorithm::None,
|
305
|
+
},
|
306
|
+
None => CompressionAlgorithm::None,
|
307
|
+
};
|
308
|
+
(
|
309
|
+
Self {
|
310
|
+
context: context.clone(),
|
311
|
+
start: Instant::now(),
|
312
|
+
compression_out: compression_out.clone(),
|
313
|
+
compression_in,
|
314
|
+
parts,
|
315
|
+
stream: ItsiGrpcResponseStream::new(
|
316
|
+
compression_out,
|
317
|
+
response_channel.0,
|
318
|
+
body.into_data_stream(),
|
319
|
+
)
|
320
|
+
.await,
|
321
|
+
},
|
322
|
+
response_channel.1,
|
323
|
+
)
|
324
|
+
}
|
325
|
+
}
|
326
|
+
|
327
|
+
fn parse_grpc_timeout(timeout_str: &str) -> Result<f64> {
|
328
|
+
if timeout_str.len() < 2 {
|
329
|
+
return Err("Timeout string too short".into());
|
330
|
+
}
|
331
|
+
let (value_str, unit) = timeout_str.split_at(timeout_str.len() - 1);
|
332
|
+
let value: u64 = value_str.parse().map_err(|_| "Invalid timeout value")?;
|
333
|
+
let duration_secs = match unit {
|
334
|
+
"n" => value as f64 / 1_000_000_000.0, // nanoseconds
|
335
|
+
"u" => value as f64 / 1_000_000.0, // microseconds
|
336
|
+
"m" => value as f64 / 1_000.0, // milliseconds
|
337
|
+
"S" => value as f64, // seconds
|
338
|
+
"M" => value as f64 * 60.0, // minutes
|
339
|
+
"H" => value as f64 * 3600.0, // hours
|
340
|
+
_ => return Err("Invalid timeout unit".into()),
|
341
|
+
};
|
342
|
+
|
343
|
+
Ok(duration_secs)
|
344
|
+
}
|