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.
Files changed (184) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -0
  3. data/CODE_OF_CONDUCT.md +7 -0
  4. data/Cargo.lock +3937 -0
  5. data/Cargo.toml +7 -0
  6. data/README.md +4 -0
  7. data/Rakefile +8 -1
  8. data/_index.md +6 -0
  9. data/exe/itsi +141 -46
  10. data/ext/itsi_error/Cargo.toml +3 -0
  11. data/ext/itsi_error/src/lib.rs +98 -24
  12. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  13. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  14. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  15. data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  16. data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
  17. data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
  18. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
  19. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
  20. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
  21. data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
  22. data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
  23. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
  24. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
  25. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
  26. data/ext/itsi_instrument_entry/Cargo.toml +15 -0
  27. data/ext/itsi_instrument_entry/src/lib.rs +31 -0
  28. data/ext/itsi_rb_helpers/Cargo.toml +3 -0
  29. data/ext/itsi_rb_helpers/src/heap_value.rs +139 -0
  30. data/ext/itsi_rb_helpers/src/lib.rs +140 -10
  31. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  32. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  33. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  34. 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
  35. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
  36. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
  37. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
  38. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
  39. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
  40. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
  41. data/ext/itsi_scheduler/Cargo.toml +24 -0
  42. data/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
  43. data/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
  44. data/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
  45. data/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
  46. data/ext/itsi_scheduler/src/lib.rs +38 -0
  47. data/ext/itsi_server/Cargo.lock +2956 -0
  48. data/ext/itsi_server/Cargo.toml +72 -14
  49. data/ext/itsi_server/extconf.rb +1 -1
  50. data/ext/itsi_server/src/default_responses/html/401.html +68 -0
  51. data/ext/itsi_server/src/default_responses/html/403.html +68 -0
  52. data/ext/itsi_server/src/default_responses/html/404.html +68 -0
  53. data/ext/itsi_server/src/default_responses/html/413.html +71 -0
  54. data/ext/itsi_server/src/default_responses/html/429.html +68 -0
  55. data/ext/itsi_server/src/default_responses/html/500.html +71 -0
  56. data/ext/itsi_server/src/default_responses/html/502.html +71 -0
  57. data/ext/itsi_server/src/default_responses/html/503.html +68 -0
  58. data/ext/itsi_server/src/default_responses/html/504.html +69 -0
  59. data/ext/itsi_server/src/default_responses/html/index.html +238 -0
  60. data/ext/itsi_server/src/default_responses/json/401.json +6 -0
  61. data/ext/itsi_server/src/default_responses/json/403.json +6 -0
  62. data/ext/itsi_server/src/default_responses/json/404.json +6 -0
  63. data/ext/itsi_server/src/default_responses/json/413.json +6 -0
  64. data/ext/itsi_server/src/default_responses/json/429.json +6 -0
  65. data/ext/itsi_server/src/default_responses/json/500.json +6 -0
  66. data/ext/itsi_server/src/default_responses/json/502.json +6 -0
  67. data/ext/itsi_server/src/default_responses/json/503.json +6 -0
  68. data/ext/itsi_server/src/default_responses/json/504.json +6 -0
  69. data/ext/itsi_server/src/default_responses/mod.rs +11 -0
  70. data/ext/itsi_server/src/env.rs +43 -0
  71. data/ext/itsi_server/src/lib.rs +132 -40
  72. data/ext/itsi_server/src/prelude.rs +2 -0
  73. data/ext/itsi_server/src/ruby_types/itsi_body_proxy/big_bytes.rs +109 -0
  74. data/ext/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +143 -0
  75. data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
  76. data/ext/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +264 -0
  77. data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +345 -0
  78. data/ext/itsi_server/src/ruby_types/itsi_http_response.rs +391 -0
  79. data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +225 -0
  80. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +375 -0
  81. data/ext/itsi_server/src/ruby_types/itsi_server.rs +83 -0
  82. data/ext/itsi_server/src/ruby_types/mod.rs +48 -0
  83. data/ext/itsi_server/src/server/binds/bind.rs +201 -0
  84. data/ext/itsi_server/src/server/binds/bind_protocol.rs +37 -0
  85. data/ext/itsi_server/src/server/binds/listener.rs +432 -0
  86. data/ext/itsi_server/src/server/binds/mod.rs +4 -0
  87. data/ext/itsi_server/src/server/binds/tls/locked_dir_cache.rs +132 -0
  88. data/ext/itsi_server/src/server/binds/tls.rs +270 -0
  89. data/ext/itsi_server/src/server/byte_frame.rs +32 -0
  90. data/ext/itsi_server/src/server/http_message_types.rs +97 -0
  91. data/ext/itsi_server/src/server/io_stream.rs +105 -0
  92. data/ext/itsi_server/src/server/lifecycle_event.rs +12 -0
  93. data/ext/itsi_server/src/server/middleware_stack/middleware.rs +165 -0
  94. data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +56 -0
  95. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +87 -0
  96. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +86 -0
  97. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +285 -0
  98. data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +142 -0
  99. data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +289 -0
  100. data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +292 -0
  101. data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +55 -0
  102. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +190 -0
  103. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +157 -0
  104. data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +195 -0
  105. data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +82 -0
  106. data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +201 -0
  107. data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
  108. data/ext/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
  109. data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +87 -0
  110. data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +414 -0
  111. data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +131 -0
  112. data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +76 -0
  113. data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +44 -0
  114. data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +36 -0
  115. data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +126 -0
  116. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +180 -0
  117. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
  118. data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +163 -0
  119. data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +12 -0
  120. data/ext/itsi_server/src/server/middleware_stack/mod.rs +347 -0
  121. data/ext/itsi_server/src/server/mod.rs +12 -5
  122. data/ext/itsi_server/src/server/process_worker.rs +247 -0
  123. data/ext/itsi_server/src/server/request_job.rs +11 -0
  124. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +342 -0
  125. data/ext/itsi_server/src/server/serve_strategy/mod.rs +30 -0
  126. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +421 -0
  127. data/ext/itsi_server/src/server/signal.rs +76 -0
  128. data/ext/itsi_server/src/server/size_limited_incoming.rs +101 -0
  129. data/ext/itsi_server/src/server/thread_worker.rs +475 -0
  130. data/ext/itsi_server/src/services/cache_store.rs +74 -0
  131. data/ext/itsi_server/src/services/itsi_http_service.rs +239 -0
  132. data/ext/itsi_server/src/services/mime_types.rs +1416 -0
  133. data/ext/itsi_server/src/services/mod.rs +6 -0
  134. data/ext/itsi_server/src/services/password_hasher.rs +83 -0
  135. data/ext/itsi_server/src/services/rate_limiter.rs +569 -0
  136. data/ext/itsi_server/src/services/static_file_server.rs +1324 -0
  137. data/ext/itsi_tracing/Cargo.toml +5 -0
  138. data/ext/itsi_tracing/src/lib.rs +315 -7
  139. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
  140. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
  141. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
  142. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
  143. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
  144. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
  145. data/lib/itsi/http_request/response_status_shortcodes.rb +74 -0
  146. data/lib/itsi/http_request.rb +186 -0
  147. data/lib/itsi/http_response.rb +41 -0
  148. data/lib/itsi/passfile.rb +109 -0
  149. data/lib/itsi/server/config/dsl.rb +565 -0
  150. data/lib/itsi/server/config.rb +166 -0
  151. data/lib/itsi/server/default_app/default_app.rb +34 -0
  152. data/lib/itsi/server/default_app/index.html +115 -0
  153. data/lib/itsi/server/default_config/Itsi-rackup.rb +119 -0
  154. data/lib/itsi/server/default_config/Itsi.rb +107 -0
  155. data/lib/itsi/server/grpc/grpc_call.rb +246 -0
  156. data/lib/itsi/server/grpc/grpc_interface.rb +100 -0
  157. data/lib/itsi/server/grpc/reflection/v1/reflection_pb.rb +26 -0
  158. data/lib/itsi/server/grpc/reflection/v1/reflection_services_pb.rb +122 -0
  159. data/lib/itsi/server/rack/handler/itsi.rb +27 -0
  160. data/lib/itsi/server/rack_interface.rb +94 -0
  161. data/lib/itsi/server/route_tester.rb +107 -0
  162. data/lib/itsi/server/scheduler_interface.rb +21 -0
  163. data/lib/itsi/server/scheduler_mode.rb +10 -0
  164. data/lib/itsi/server/signal_trap.rb +29 -0
  165. data/lib/itsi/server/typed_handlers/param_parser.rb +200 -0
  166. data/lib/itsi/server/typed_handlers/source_parser.rb +55 -0
  167. data/lib/itsi/server/typed_handlers.rb +17 -0
  168. data/lib/itsi/server/version.rb +1 -1
  169. data/lib/itsi/server.rb +160 -9
  170. data/lib/itsi/standard_headers.rb +86 -0
  171. data/lib/ruby_lsp/itsi/addon.rb +111 -0
  172. data/lib/shell_completions/completions.rb +26 -0
  173. metadata +182 -25
  174. data/ext/itsi_server/src/request/itsi_request.rs +0 -143
  175. data/ext/itsi_server/src/request/mod.rs +0 -1
  176. data/ext/itsi_server/src/server/bind.rs +0 -138
  177. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -32
  178. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -52
  179. data/ext/itsi_server/src/server/itsi_server.rs +0 -182
  180. data/ext/itsi_server/src/server/listener.rs +0 -218
  181. data/ext/itsi_server/src/server/tls.rs +0 -138
  182. data/ext/itsi_server/src/server/transfer_protocol.rs +0 -23
  183. data/ext/itsi_server/src/stream_writer/mod.rs +0 -21
  184. data/lib/itsi/request.rb +0 -39
@@ -0,0 +1,391 @@
1
+ use bytes::{Bytes, BytesMut};
2
+ use derive_more::Debug;
3
+ use futures::stream::{unfold, StreamExt};
4
+ use http::{
5
+ header::{ACCEPT, TRANSFER_ENCODING},
6
+ request::Parts,
7
+ HeaderMap, HeaderName, HeaderValue, Request, Response, StatusCode,
8
+ };
9
+ use http_body_util::{combinators::BoxBody, Empty, Full, StreamBody};
10
+ use hyper::{body::Frame, upgrade::Upgraded};
11
+ use hyper_util::rt::TokioIo;
12
+ use itsi_error::Result;
13
+ use itsi_tracing::error;
14
+ use magnus::error::Result as MagnusResult;
15
+ use parking_lot::RwLock;
16
+ use std::{
17
+ collections::HashMap,
18
+ io,
19
+ os::{fd::FromRawFd, unix::net::UnixStream},
20
+ str::FromStr,
21
+ sync::Arc,
22
+ };
23
+ use tokio::{
24
+ io::AsyncReadExt,
25
+ net::UnixStream as TokioUnixStream,
26
+ sync::{
27
+ mpsc::{self},
28
+ watch,
29
+ },
30
+ };
31
+ use tokio_stream::wrappers::ReceiverStream;
32
+ use tokio_util::io::ReaderStream;
33
+ use tracing::warn;
34
+
35
+ use crate::server::{
36
+ byte_frame::ByteFrame, http_message_types::HttpResponse,
37
+ serve_strategy::single_mode::RunningPhase,
38
+ };
39
+
40
+ #[magnus::wrap(class = "Itsi::HttpResponse", free_immediately, size)]
41
+ #[derive(Debug, Clone)]
42
+ pub struct ItsiHttpResponse {
43
+ pub data: Arc<ResponseData>,
44
+ }
45
+
46
+ #[derive(Debug)]
47
+ pub struct ResponseData {
48
+ pub response: RwLock<Option<HttpResponse>>,
49
+ pub response_writer: RwLock<Option<mpsc::Sender<ByteFrame>>>,
50
+ pub response_buffer: RwLock<BytesMut>,
51
+ pub hijacked_socket: RwLock<Option<UnixStream>>,
52
+ pub parts: Parts,
53
+ }
54
+
55
+ impl ItsiHttpResponse {
56
+ pub async fn build(
57
+ &self,
58
+ first_frame: ByteFrame,
59
+ receiver: mpsc::Receiver<ByteFrame>,
60
+ shutdown_rx: watch::Receiver<RunningPhase>,
61
+ ) -> HttpResponse {
62
+ if self.is_hijacked() {
63
+ return match self.process_hijacked_response().await {
64
+ Ok(result) => result,
65
+ Err(e) => {
66
+ error!("Error processing hijacked response: {}", e);
67
+ Response::new(BoxBody::new(Empty::new()))
68
+ }
69
+ };
70
+ }
71
+ let mut response = self.data.response.write().take().unwrap();
72
+ *response.body_mut() = if matches!(first_frame, ByteFrame::Empty) {
73
+ BoxBody::new(Empty::new())
74
+ } else if matches!(first_frame, ByteFrame::End(_)) {
75
+ BoxBody::new(Full::new(first_frame.into()))
76
+ } else {
77
+ let initial_frame = tokio_stream::once(Ok(Frame::data(Bytes::from(first_frame))));
78
+ let frame_stream = unfold(
79
+ (ReceiverStream::new(receiver), shutdown_rx),
80
+ |(mut receiver, mut shutdown_rx)| async move {
81
+ if let RunningPhase::ShutdownPending = *shutdown_rx.borrow() {
82
+ return None;
83
+ }
84
+ loop {
85
+ tokio::select! {
86
+ maybe_bytes = receiver.next() => {
87
+ match maybe_bytes {
88
+ Some(ByteFrame::Data(bytes)) | Some(ByteFrame::End(bytes)) => {
89
+ return Some((Ok(Frame::data(bytes)), (receiver, shutdown_rx)));
90
+ }
91
+ _ => {
92
+ return None;
93
+ }
94
+ }
95
+ },
96
+ _ = shutdown_rx.changed() => {
97
+ match *shutdown_rx.borrow() {
98
+ RunningPhase::ShutdownPending => {
99
+ warn!("Disconnecting streaming client.");
100
+ return None;
101
+ },
102
+ _ => continue,
103
+ }
104
+ }
105
+ }
106
+ }
107
+ },
108
+ );
109
+
110
+ let combined_stream = initial_frame.chain(frame_stream);
111
+ BoxBody::new(StreamBody::new(combined_stream))
112
+ };
113
+ response
114
+ }
115
+
116
+ pub fn close(&self) {
117
+ self.data.response_writer.write().take();
118
+ }
119
+
120
+ async fn two_way_bridge(upgraded: Upgraded, local: TokioUnixStream) -> io::Result<()> {
121
+ let client_io = TokioIo::new(upgraded);
122
+
123
+ // Split each side
124
+ let (mut lr, mut lw) = tokio::io::split(local);
125
+ let (mut cr, mut cw) = tokio::io::split(client_io);
126
+
127
+ let to_ruby = tokio::spawn(async move {
128
+ if let Err(e) = tokio::io::copy(&mut cr, &mut lw).await {
129
+ eprintln!("Error copying upgraded->local: {:?}", e);
130
+ }
131
+ });
132
+ let from_ruby = tokio::spawn(async move {
133
+ if let Err(e) = tokio::io::copy(&mut lr, &mut cw).await {
134
+ eprintln!("Error copying upgraded->local: {:?}", e);
135
+ }
136
+ });
137
+
138
+ let _ = to_ruby.await;
139
+ let _ = from_ruby.await;
140
+ Ok(())
141
+ }
142
+
143
+ async fn read_response_headers(&self, reader: &mut TokioUnixStream) -> Result<Vec<u8>> {
144
+ let mut buf = [0u8; 1];
145
+ let mut collected = Vec::new();
146
+ loop {
147
+ let n = reader.read(&mut buf).await?;
148
+ if n == 0 {
149
+ // EOF reached unexpectedly
150
+ break;
151
+ }
152
+ collected.push(buf[0]);
153
+ if collected.ends_with(b"\r\n\r\n") {
154
+ break;
155
+ }
156
+ }
157
+
158
+ Ok(collected)
159
+ }
160
+
161
+ pub async fn read_hijacked_headers(
162
+ &self,
163
+ ) -> Result<(HeaderMap, StatusCode, bool, TokioUnixStream)> {
164
+ let hijacked_socket =
165
+ self.data
166
+ .hijacked_socket
167
+ .write()
168
+ .take()
169
+ .ok_or(itsi_error::ItsiError::InvalidInput(
170
+ "Couldn't hijack stream".to_owned(),
171
+ ))?;
172
+ let mut reader = TokioUnixStream::from_std(hijacked_socket).unwrap();
173
+ let response_headers = self.read_response_headers(&mut reader).await?;
174
+ let mut headers = [httparse::EMPTY_HEADER; 64];
175
+ let mut resp = httparse::Response::new(&mut headers);
176
+ resp.parse(&response_headers)?;
177
+
178
+ let status = StatusCode::from_u16(resp.code.unwrap_or(200)).unwrap_or(StatusCode::OK);
179
+ let mut headers = HeaderMap::new();
180
+ for header in resp.headers.iter() {
181
+ headers.insert(
182
+ HeaderName::from_str(header.name).unwrap(),
183
+ HeaderValue::from_bytes(header.value).unwrap(),
184
+ );
185
+ }
186
+ let requires_upgrade = status == StatusCode::SWITCHING_PROTOCOLS;
187
+ Ok((headers, status, requires_upgrade, reader))
188
+ }
189
+
190
+ pub async fn process_hijacked_response(&self) -> Result<HttpResponse> {
191
+ let (headers, status, requires_upgrade, reader) = self.read_hijacked_headers().await?;
192
+ let mut response = if requires_upgrade {
193
+ let parts = self.data.parts.clone();
194
+ tokio::spawn(async move {
195
+ let mut req = Request::from_parts(parts, Empty::<Bytes>::new());
196
+ match hyper::upgrade::on(&mut req).await {
197
+ Ok(upgraded) => {
198
+ Self::two_way_bridge(upgraded, reader)
199
+ .await
200
+ .expect("Error in creating two way bridge");
201
+ }
202
+ Err(e) => eprintln!("upgrade error: {:?}", e),
203
+ }
204
+ });
205
+ Response::new(BoxBody::new(Empty::new()))
206
+ } else {
207
+ let stream = ReaderStream::new(reader);
208
+ let boxed_body = if headers
209
+ .get(TRANSFER_ENCODING)
210
+ .is_some_and(|h| h == "chunked")
211
+ {
212
+ BoxBody::new(StreamBody::new(unfold(
213
+ (stream, Vec::new()),
214
+ |(mut stream, mut buf)| async move {
215
+ loop {
216
+ if let Some(pos) = buf.iter().position(|&b| b == b'\n') {
217
+ let line = buf.drain(..=pos).collect::<Vec<u8>>();
218
+ let line = std::str::from_utf8(&line).ok()?.trim();
219
+ let chunk_size = usize::from_str_radix(line, 16).ok()?;
220
+ if chunk_size == 0 {
221
+ return None;
222
+ }
223
+ while buf.len() < chunk_size {
224
+ match stream.next().await {
225
+ Some(Ok(chunk)) => buf.extend_from_slice(&chunk),
226
+ _ => return None,
227
+ }
228
+ }
229
+ let data = buf.drain(..chunk_size).collect::<Vec<u8>>();
230
+ if buf.starts_with(b"\r\n") {
231
+ buf.drain(..2);
232
+ }
233
+ return Some((Ok(Frame::data(Bytes::from(data))), (stream, buf)));
234
+ }
235
+ match stream.next().await {
236
+ Some(Ok(chunk)) => buf.extend_from_slice(&chunk),
237
+ _ => return None,
238
+ }
239
+ }
240
+ },
241
+ )))
242
+ } else {
243
+ BoxBody::new(StreamBody::new(stream.map(
244
+ |result: std::result::Result<Bytes, io::Error>| {
245
+ result
246
+ .map(Frame::data)
247
+ .map_err(|e| unreachable!("unexpected io error: {:?}", e))
248
+ },
249
+ )))
250
+ };
251
+ Response::new(boxed_body)
252
+ };
253
+
254
+ *response.status_mut() = status;
255
+ *response.headers_mut() = headers;
256
+ Ok(response)
257
+ }
258
+
259
+ pub fn internal_server_error(&self, message: String) {
260
+ error!(message);
261
+ self.data.response_writer.write().take();
262
+ if let Some(ref mut response) = *self.data.response.write() {
263
+ *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
264
+ }
265
+ }
266
+
267
+ pub fn send_frame(&self, frame: Bytes) -> MagnusResult<()> {
268
+ self.send_frame_into(ByteFrame::Data(frame), &self.data.response_writer)
269
+ }
270
+
271
+ pub fn recv_frame(&self) {
272
+ // not implemented
273
+ }
274
+
275
+ pub fn send_and_close(&self, frame: Bytes) -> MagnusResult<()> {
276
+ let result = self.send_frame_into(ByteFrame::End(frame), &self.data.response_writer);
277
+ self.data.response_writer.write().take();
278
+ result
279
+ }
280
+
281
+ pub fn send_frame_into(
282
+ &self,
283
+ frame: ByteFrame,
284
+ writer: &RwLock<Option<mpsc::Sender<ByteFrame>>>,
285
+ ) -> MagnusResult<()> {
286
+ if let Some(writer) = writer.write().as_ref() {
287
+ return Ok(writer
288
+ .blocking_send(frame)
289
+ .map_err(|_| itsi_error::ItsiError::ClientConnectionClosed)?);
290
+ }
291
+ Ok(())
292
+ }
293
+
294
+ pub fn is_hijacked(&self) -> bool {
295
+ self.data.hijacked_socket.read().is_some()
296
+ }
297
+
298
+ pub fn close_write(&self) -> MagnusResult<bool> {
299
+ self.data.response_writer.write().take();
300
+ Ok(true)
301
+ }
302
+
303
+ pub fn accept_str(&self) -> &str {
304
+ self.data
305
+ .parts
306
+ .headers
307
+ .get(ACCEPT)
308
+ .and_then(|hv| hv.to_str().ok()) // handle invalid utf-8
309
+ .unwrap_or("application/x-www-form-urlencoded")
310
+ }
311
+
312
+ pub fn is_html(&self) -> bool {
313
+ self.accept_str().starts_with("text/html")
314
+ }
315
+
316
+ pub fn is_json(&self) -> bool {
317
+ self.accept_str().starts_with("application/json")
318
+ }
319
+
320
+ pub fn close_read(&self) -> MagnusResult<bool> {
321
+ Ok(true)
322
+ }
323
+
324
+ pub fn new(parts: Parts, response_writer: mpsc::Sender<ByteFrame>) -> Self {
325
+ Self {
326
+ data: Arc::new(ResponseData {
327
+ response: RwLock::new(Some(Response::new(BoxBody::new(Empty::new())))),
328
+ response_writer: RwLock::new(Some(response_writer)),
329
+ response_buffer: RwLock::new(BytesMut::new()),
330
+ hijacked_socket: RwLock::new(None),
331
+ parts,
332
+ }),
333
+ }
334
+ }
335
+
336
+ pub fn add_header(&self, name: Bytes, value: Bytes) -> MagnusResult<()> {
337
+ let header_name: HeaderName = HeaderName::from_bytes(&name).map_err(|e| {
338
+ itsi_error::ItsiError::InvalidInput(format!("Invalid header name {:?}: {:?}", name, e))
339
+ })?;
340
+ let header_value = unsafe { HeaderValue::from_maybe_shared_unchecked(value) };
341
+ if let Some(ref mut resp) = *self.data.response.write() {
342
+ resp.headers_mut().insert(header_name, header_value);
343
+ }
344
+ Ok(())
345
+ }
346
+
347
+ pub fn add_headers(&self, headers: HashMap<Bytes, Vec<Bytes>>) -> MagnusResult<()> {
348
+ if let Some(ref mut resp) = *self.data.response.write() {
349
+ let headers_mut = resp.headers_mut();
350
+ for (name, values) in headers {
351
+ let header_name = HeaderName::from_bytes(&name).map_err(|e| {
352
+ itsi_error::ItsiError::InvalidInput(format!(
353
+ "Invalid header name {:?}: {:?}",
354
+ name, e
355
+ ))
356
+ })?;
357
+ for value in values {
358
+ let header_value = unsafe { HeaderValue::from_maybe_shared_unchecked(value) };
359
+ headers_mut.insert(&header_name, header_value);
360
+ }
361
+ }
362
+ }
363
+
364
+ Ok(())
365
+ }
366
+
367
+ pub fn set_status(&self, status: u16) -> MagnusResult<()> {
368
+ if let Some(ref mut resp) = *self.data.response.write() {
369
+ *resp.status_mut() = StatusCode::from_u16(status).map_err(|e| {
370
+ itsi_error::ItsiError::InvalidInput(format!(
371
+ "Invalid status code {:?}: {:?}",
372
+ status, e
373
+ ))
374
+ })?;
375
+ }
376
+ Ok(())
377
+ }
378
+
379
+ pub fn hijack(&self, fd: i32) -> MagnusResult<()> {
380
+ let stream = unsafe { UnixStream::from_raw_fd(fd) };
381
+
382
+ *self.data.hijacked_socket.write() = Some(stream);
383
+ if let Some(writer) = self.data.response_writer.write().as_ref() {
384
+ writer
385
+ .blocking_send(ByteFrame::Empty)
386
+ .map_err(|_| itsi_error::ItsiError::ClientConnectionClosed)?
387
+ }
388
+ self.close();
389
+ Ok(())
390
+ }
391
+ }
@@ -0,0 +1,225 @@
1
+ use derive_more::Debug;
2
+ use globset::{Glob, GlobSet, GlobSetBuilder};
3
+ use magnus::error::Result;
4
+ use nix::unistd::{close, fork, pipe, read};
5
+ use notify::{event::ModifyKind, EventKind, RecommendedWatcher};
6
+ use notify::{Event, RecursiveMode, Watcher};
7
+ use std::path::Path;
8
+ use std::sync::mpsc::Sender;
9
+ use std::time::{Duration, Instant};
10
+ use std::{collections::HashSet, fs};
11
+ use std::{
12
+ os::fd::{AsRawFd, IntoRawFd, OwnedFd},
13
+ path::PathBuf,
14
+ process::Command,
15
+ sync::mpsc,
16
+ thread::{self},
17
+ };
18
+
19
+ /// Represents a set of patterns and commands.
20
+ #[derive(Debug, Clone)]
21
+ struct PatternGroup {
22
+ base_dir: PathBuf,
23
+ glob_set: GlobSet,
24
+ pattern: String,
25
+ commands: Vec<Vec<String>>,
26
+ last_triggered: Option<Instant>,
27
+ }
28
+
29
+ /// Extracts the base directory from a wildcard pattern by taking the portion up to the first
30
+ /// component that contains a wildcard character.
31
+ fn extract_and_canonicalize_base_dir(pattern: &str) -> PathBuf {
32
+ if !(pattern.contains("*") || pattern.contains("?") || pattern.contains('[')) {
33
+ if let Ok(metadata) = fs::metadata(pattern) {
34
+ if metadata.is_dir() {
35
+ return fs::canonicalize(pattern).unwrap();
36
+ }
37
+ if metadata.is_file() {
38
+ return fs::canonicalize(pattern)
39
+ .unwrap()
40
+ .parent()
41
+ .unwrap()
42
+ .to_path_buf();
43
+ }
44
+ }
45
+ }
46
+
47
+ let path = Path::new(pattern);
48
+ let mut base = PathBuf::new();
49
+ for comp in path.components() {
50
+ let comp_str = comp.as_os_str().to_string_lossy();
51
+ if comp_str.contains('*') || comp_str.contains('?') || comp_str.contains('[') {
52
+ break;
53
+ } else {
54
+ base.push(comp);
55
+ }
56
+ }
57
+ // If no base was built, default to "."
58
+ let base = if base.as_os_str().is_empty() || !base.exists() {
59
+ PathBuf::from(".")
60
+ } else {
61
+ base
62
+ };
63
+
64
+ // Canonicalize to get the absolute path.
65
+ fs::canonicalize(&base).unwrap_or(base)
66
+ }
67
+
68
+ /// Minimum time between triggering the same pattern group (debounce time)
69
+ const DEBOUNCE_DURATION: Duration = Duration::from_millis(500);
70
+
71
+ pub fn watch_groups(pattern_groups: Vec<(String, Vec<Vec<String>>)>) -> Result<Option<OwnedFd>> {
72
+ let (r_fd, w_fd): (OwnedFd, OwnedFd) = pipe().map_err(|e| {
73
+ magnus::Error::new(
74
+ magnus::exception::standard_error(),
75
+ format!("Failed to create watcher pipe: {}", e),
76
+ )
77
+ })?;
78
+
79
+ let fork_result = unsafe {
80
+ fork().map_err(|e| {
81
+ magnus::Error::new(
82
+ magnus::exception::standard_error(),
83
+ format!("Failed to fork file watcher: {}", e),
84
+ )
85
+ })
86
+ }?;
87
+
88
+ if fork_result.is_child() {
89
+ let _ = close(w_fd.into_raw_fd());
90
+ thread::spawn(move || {
91
+ let mut buf = [0u8; 1];
92
+ loop {
93
+ match read(r_fd.as_raw_fd(), &mut buf) {
94
+ Ok(0) => {
95
+ std::process::exit(0);
96
+ }
97
+ Ok(_) => {}
98
+ Err(_) => {
99
+ std::process::exit(0);
100
+ }
101
+ }
102
+ }
103
+ });
104
+
105
+ let mut groups = Vec::new();
106
+ for (pattern, commands) in pattern_groups.into_iter() {
107
+ let base_dir = extract_and_canonicalize_base_dir(&pattern);
108
+ let glob = Glob::new(&pattern).map_err(|e| {
109
+ magnus::Error::new(
110
+ magnus::exception::standard_error(),
111
+ format!("Failed to create watch glob: {}", e),
112
+ )
113
+ })?;
114
+ let glob_set = GlobSetBuilder::new().add(glob).build().map_err(|e| {
115
+ magnus::Error::new(
116
+ magnus::exception::standard_error(),
117
+ format!("Failed to create watch glob set: {}", e),
118
+ )
119
+ })?;
120
+ groups.push(PatternGroup {
121
+ base_dir,
122
+ glob_set,
123
+ pattern,
124
+ commands,
125
+ last_triggered: None,
126
+ });
127
+ }
128
+
129
+ // Create a channel and a watcher.
130
+ let (tx, rx) = mpsc::channel::<notify::Result<Event>>();
131
+ let sender = tx.clone();
132
+ fn event_fn(sender: Sender<notify::Result<Event>>) -> impl Fn(notify::Result<Event>) {
133
+ move |res| match res {
134
+ Ok(event) => {
135
+ sender.send(Ok(event)).unwrap();
136
+ }
137
+ Err(e) => println!("watch error: {:?}", e),
138
+ }
139
+ }
140
+
141
+ let mut watched_dirs = HashSet::new();
142
+ let mut watcher: RecommendedWatcher =
143
+ notify::recommended_watcher(event_fn(sender)).expect("Failed to create watcher");
144
+ for group in &groups {
145
+ if watched_dirs.insert(group.base_dir.clone()) {
146
+ watcher
147
+ .watch(&group.base_dir, RecursiveMode::Recursive)
148
+ .expect("Failed to add watch");
149
+ }
150
+ }
151
+
152
+ // Main event loop.
153
+ for res in rx {
154
+ match res {
155
+ Ok(event) => {
156
+ if !matches!(event.kind, EventKind::Modify(ModifyKind::Metadata(_))) {
157
+ continue;
158
+ }
159
+ let now = Instant::now();
160
+ for group in &mut groups {
161
+ for path in event.paths.iter() {
162
+ if let Ok(rel_path) = path.strip_prefix(&group.base_dir) {
163
+ if group.glob_set.is_match(rel_path)
164
+ || rel_path.to_str().is_some_and(|s| s == group.pattern)
165
+ {
166
+ // Check if we should debounce this event
167
+ if let Some(last_triggered) = group.last_triggered {
168
+ if now.duration_since(last_triggered) < DEBOUNCE_DURATION {
169
+ // Skip this event as we've recently triggered for this pattern
170
+ continue;
171
+ }
172
+ }
173
+
174
+ // Update the last triggered time
175
+ group.last_triggered = Some(now);
176
+
177
+ // Execute the commands for this group.
178
+ for command in &group.commands {
179
+ if command.is_empty() {
180
+ continue;
181
+ }
182
+ let mut cmd = Command::new(&command[0]);
183
+ if command.len() > 1 {
184
+ cmd.args(&command[1..]);
185
+ }
186
+ match cmd.spawn() {
187
+ Ok(mut child) => {
188
+ if let Err(e) = child.wait() {
189
+ eprintln!(
190
+ "Command {:?} failed: {:?}",
191
+ command, e
192
+ );
193
+ }
194
+ }
195
+ Err(e) => {
196
+ eprintln!(
197
+ "Failed to execute command {:?}: {:?}",
198
+ command, e
199
+ );
200
+ }
201
+ }
202
+ }
203
+ break;
204
+ }
205
+ }
206
+ }
207
+ }
208
+ }
209
+ Err(e) => println!("Watch error: {:?}", e),
210
+ }
211
+ }
212
+
213
+ // Clean up the watches.
214
+ for group in &groups {
215
+ watcher
216
+ .unwatch(&group.base_dir)
217
+ .expect("Failed to remove watch");
218
+ }
219
+ drop(watcher);
220
+ std::process::exit(0);
221
+ } else {
222
+ let _ = close(r_fd.into_raw_fd());
223
+ Ok(Some(w_fd))
224
+ }
225
+ }