itsi-scheduler 0.1.5 → 0.1.19

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.

Potentially problematic release.


This version of itsi-scheduler might be problematic. Click here for more details.

Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/CODE_OF_CONDUCT.md +7 -0
  3. data/Cargo.lock +90 -22
  4. data/README.md +5 -0
  5. data/_index.md +7 -0
  6. data/ext/itsi_error/Cargo.toml +1 -0
  7. data/ext/itsi_error/src/lib.rs +106 -7
  8. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  9. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  10. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  11. data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  12. data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
  13. data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
  14. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
  15. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
  16. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
  17. data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
  18. data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
  19. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
  20. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
  21. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
  22. data/ext/itsi_rb_helpers/Cargo.toml +1 -0
  23. data/ext/itsi_rb_helpers/src/heap_value.rs +18 -0
  24. data/ext/itsi_rb_helpers/src/lib.rs +59 -9
  25. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  26. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  27. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  28. 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
  29. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
  30. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
  31. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
  32. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
  33. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
  34. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
  35. data/ext/itsi_scheduler/src/itsi_scheduler.rs +1 -1
  36. data/ext/itsi_server/Cargo.lock +2956 -0
  37. data/ext/itsi_server/Cargo.toml +72 -28
  38. data/ext/itsi_server/src/default_responses/mod.rs +11 -0
  39. data/ext/itsi_server/src/env.rs +43 -0
  40. data/ext/itsi_server/src/lib.rs +113 -75
  41. data/ext/itsi_server/src/prelude.rs +2 -0
  42. data/ext/itsi_server/src/{body_proxy → ruby_types/itsi_body_proxy}/big_bytes.rs +10 -5
  43. data/ext/itsi_server/src/{body_proxy/itsi_body_proxy.rs → ruby_types/itsi_body_proxy/mod.rs} +29 -8
  44. data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
  45. data/ext/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +264 -0
  46. data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +345 -0
  47. data/ext/itsi_server/src/{response/itsi_response.rs → ruby_types/itsi_http_response.rs} +84 -40
  48. data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +225 -0
  49. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +375 -0
  50. data/ext/itsi_server/src/ruby_types/itsi_server.rs +83 -0
  51. data/ext/itsi_server/src/ruby_types/mod.rs +48 -0
  52. data/ext/itsi_server/src/server/{bind.rs → binds/bind.rs} +56 -24
  53. data/ext/itsi_server/src/server/{listener.rs → binds/listener.rs} +218 -113
  54. data/ext/itsi_server/src/server/binds/mod.rs +4 -0
  55. data/ext/itsi_server/src/server/{tls → binds/tls}/locked_dir_cache.rs +55 -17
  56. data/ext/itsi_server/src/server/{tls.rs → binds/tls.rs} +109 -28
  57. data/ext/itsi_server/src/server/byte_frame.rs +32 -0
  58. data/ext/itsi_server/src/server/http_message_types.rs +97 -0
  59. data/ext/itsi_server/src/server/io_stream.rs +2 -1
  60. data/ext/itsi_server/src/server/lifecycle_event.rs +3 -0
  61. data/ext/itsi_server/src/server/middleware_stack/middleware.rs +165 -0
  62. data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +56 -0
  63. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +87 -0
  64. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +86 -0
  65. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +285 -0
  66. data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +142 -0
  67. data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +289 -0
  68. data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +292 -0
  69. data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +55 -0
  70. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +190 -0
  71. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +157 -0
  72. data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +195 -0
  73. data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +82 -0
  74. data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +201 -0
  75. data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
  76. data/ext/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
  77. data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +87 -0
  78. data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +414 -0
  79. data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +131 -0
  80. data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +76 -0
  81. data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +44 -0
  82. data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +36 -0
  83. data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +126 -0
  84. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +180 -0
  85. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
  86. data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +163 -0
  87. data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +12 -0
  88. data/ext/itsi_server/src/server/middleware_stack/mod.rs +347 -0
  89. data/ext/itsi_server/src/server/mod.rs +6 -5
  90. data/ext/itsi_server/src/server/process_worker.rs +65 -14
  91. data/ext/itsi_server/src/server/request_job.rs +11 -0
  92. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +137 -49
  93. data/ext/itsi_server/src/server/serve_strategy/mod.rs +9 -6
  94. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +338 -164
  95. data/ext/itsi_server/src/server/signal.rs +32 -26
  96. data/ext/itsi_server/src/server/size_limited_incoming.rs +101 -0
  97. data/ext/itsi_server/src/server/thread_worker.rs +214 -107
  98. data/ext/itsi_server/src/services/cache_store.rs +74 -0
  99. data/ext/itsi_server/src/services/itsi_http_service.rs +239 -0
  100. data/ext/itsi_server/src/services/mime_types.rs +1416 -0
  101. data/ext/itsi_server/src/services/mod.rs +6 -0
  102. data/ext/itsi_server/src/services/password_hasher.rs +83 -0
  103. data/ext/itsi_server/src/services/rate_limiter.rs +569 -0
  104. data/ext/itsi_server/src/services/static_file_server.rs +1324 -0
  105. data/ext/itsi_tracing/Cargo.toml +1 -0
  106. data/ext/itsi_tracing/src/lib.rs +312 -34
  107. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
  108. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
  109. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
  110. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
  111. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
  112. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
  113. data/lib/itsi/scheduler/version.rb +1 -1
  114. data/lib/itsi/scheduler.rb +2 -2
  115. metadata +93 -21
  116. data/ext/itsi_error/src/from.rs +0 -71
  117. data/ext/itsi_server/extconf.rb +0 -6
  118. data/ext/itsi_server/src/body_proxy/mod.rs +0 -2
  119. data/ext/itsi_server/src/request/itsi_request.rs +0 -277
  120. data/ext/itsi_server/src/request/mod.rs +0 -1
  121. data/ext/itsi_server/src/response/mod.rs +0 -1
  122. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -13
  123. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -5
  124. data/ext/itsi_server/src/server/itsi_server.rs +0 -244
  125. /data/ext/itsi_server/src/server/{bind_protocol.rs → binds/bind_protocol.rs} +0 -0
@@ -0,0 +1,345 @@
1
+ use derive_more::Debug;
2
+ use futures::StreamExt;
3
+ use http::{header::CONTENT_LENGTH, request::Parts, Response, StatusCode, Version};
4
+ use http_body_util::{combinators::BoxBody, BodyExt, Empty};
5
+ use itsi_error::CLIENT_CONNECTION_CLOSED;
6
+ use itsi_rb_helpers::{print_rb_backtrace, HeapValue};
7
+ use itsi_tracing::{debug, error};
8
+ use magnus::{
9
+ block::Proc,
10
+ error::{ErrorType, Result as MagnusResult},
11
+ Error, RHash, Symbol,
12
+ };
13
+ use magnus::{
14
+ value::{LazyId, ReprValue},
15
+ Ruby, Value,
16
+ };
17
+ use std::{fmt, io::Write, sync::Arc, time::Instant};
18
+ use tokio::sync::mpsc::{self};
19
+
20
+ use super::{
21
+ itsi_body_proxy::{big_bytes::BigBytes, ItsiBody, ItsiBodyProxy},
22
+ itsi_http_response::ItsiHttpResponse,
23
+ };
24
+ use crate::{
25
+ server::{
26
+ byte_frame::ByteFrame,
27
+ http_message_types::{HttpRequest, HttpResponse},
28
+ request_job::RequestJob,
29
+ size_limited_incoming::MaxBodySizeReached,
30
+ },
31
+ services::itsi_http_service::HttpRequestContext,
32
+ };
33
+
34
+ static ID_MESSAGE: LazyId = LazyId::new("message");
35
+
36
+ #[derive(Debug)]
37
+ #[magnus::wrap(class = "Itsi::HttpRequest", free_immediately, size)]
38
+ pub struct ItsiHttpRequest {
39
+ pub parts: Parts,
40
+ #[debug(skip)]
41
+ pub body: ItsiBody,
42
+ pub version: Version,
43
+ pub response: ItsiHttpResponse,
44
+ pub start: Instant,
45
+ #[debug(skip)]
46
+ pub context: HttpRequestContext,
47
+ pub script_name: String,
48
+ }
49
+
50
+ impl fmt::Display for ItsiHttpRequest {
51
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52
+ write!(
53
+ f,
54
+ "{} {} {}",
55
+ self.version().unwrap(),
56
+ self.method().unwrap(),
57
+ self.path().unwrap()
58
+ )
59
+ }
60
+ }
61
+
62
+ impl ItsiHttpRequest {
63
+ pub fn is_connection_closed_err(ruby: &Ruby, err: &Error) -> bool {
64
+ match err.error_type() {
65
+ ErrorType::Jump(_) => false,
66
+ ErrorType::Error(_, _) => false,
67
+ ErrorType::Exception(exception) => {
68
+ exception.is_kind_of(ruby.exception_eof_error())
69
+ && err
70
+ .value()
71
+ .map(|v| {
72
+ v.funcall::<_, _, String>(*ID_MESSAGE, ())
73
+ .unwrap_or("".to_string())
74
+ .eq(CLIENT_CONNECTION_CLOSED)
75
+ })
76
+ .unwrap_or(false)
77
+ }
78
+ }
79
+ }
80
+ pub fn content_type_str(&self) -> &str {
81
+ self.parts
82
+ .headers
83
+ .get("Content-Type")
84
+ .and_then(|hv| hv.to_str().ok())
85
+ .unwrap_or("application/x-www-form-urlencoded")
86
+ }
87
+
88
+ pub fn is_json(&self) -> bool {
89
+ self.content_type_str() == "application/json"
90
+ }
91
+
92
+ pub fn url_params(&self) -> magnus::error::Result<RHash> {
93
+ let captures = self
94
+ .context
95
+ .matching_pattern
96
+ .as_ref()
97
+ .and_then(|re| re.captures(self.parts.uri.path()));
98
+ if let Some(caps) = &captures {
99
+ let re = self.context.matching_pattern.as_ref().unwrap();
100
+ let params = RHash::with_capacity(caps.len());
101
+ for (i, group_name) in re.capture_names().enumerate().skip(1) {
102
+ if let Some(name) = group_name {
103
+ if let Some(m) = caps.get(i) {
104
+ // Insert into the hash: key is the group name, value is the match.
105
+ params.aset(Symbol::new(name), m.as_str())?;
106
+ }
107
+ }
108
+ }
109
+ Ok(params)
110
+ } else {
111
+ Ok(RHash::new())
112
+ }
113
+ }
114
+
115
+ pub fn is_html(&self) -> bool {
116
+ self.content_type_str() == "text/html"
117
+ }
118
+
119
+ pub fn is_url_encoded(&self) -> bool {
120
+ self.content_type_str() == "application/x-www-form-urlencoded"
121
+ }
122
+
123
+ pub fn is_multipart(&self) -> bool {
124
+ self.content_type_str().starts_with("multipart/form-data")
125
+ }
126
+
127
+ pub fn content_length(&self) -> Option<u64> {
128
+ self.parts
129
+ .headers
130
+ .get(CONTENT_LENGTH)
131
+ .and_then(|hv| hv.to_str().ok())
132
+ .and_then(|s| s.parse().ok())
133
+ }
134
+
135
+ pub fn process(self, ruby: &Ruby, app_proc: Arc<HeapValue<Proc>>) -> magnus::error::Result<()> {
136
+ let response = self.response.clone();
137
+ let result = app_proc.call::<_, Value>((self,));
138
+ if let Err(err) = result {
139
+ Self::internal_error(ruby, response, err);
140
+ }
141
+ Ok(())
142
+ }
143
+
144
+ pub fn internal_error(ruby: &Ruby, response: ItsiHttpResponse, err: Error) {
145
+ if Self::is_connection_closed_err(ruby, &err) {
146
+ debug!("Connection closed by client");
147
+ response.close();
148
+ } else if let Some(rb_err) = err.value() {
149
+ print_rb_backtrace(rb_err);
150
+ response.internal_server_error(err.to_string());
151
+ } else {
152
+ response.internal_server_error(err.to_string());
153
+ }
154
+ }
155
+
156
+ pub fn error(self, message: String) {
157
+ self.response.internal_server_error(message);
158
+ }
159
+
160
+ pub(crate) async fn process_request(
161
+ app: Arc<HeapValue<Proc>>,
162
+ hyper_request: HttpRequest,
163
+ context: &HttpRequestContext,
164
+ script_name: String,
165
+ nonblocking: bool,
166
+ ) -> itsi_error::Result<HttpResponse> {
167
+ match ItsiHttpRequest::new(hyper_request, context, script_name).await {
168
+ Ok((request, mut receiver)) => {
169
+ let shutdown_channel = context.service.shutdown_channel.clone();
170
+ let response = request.response.clone();
171
+ let sender = if nonblocking {
172
+ &context.nonblocking_sender
173
+ } else {
174
+ &context.sender
175
+ };
176
+ match sender
177
+ .send(RequestJob::ProcessHttpRequest(request, app))
178
+ .await
179
+ {
180
+ Err(err) => {
181
+ error!("Error occurred: {}", err);
182
+ let mut response = Response::new(BoxBody::new(Empty::new()));
183
+ *response.status_mut() = StatusCode::BAD_REQUEST;
184
+ Ok(response)
185
+ }
186
+ _ => match receiver.recv().await {
187
+ Some(first_frame) => Ok(response
188
+ .build(first_frame, receiver, shutdown_channel)
189
+ .await),
190
+ None => Ok(response
191
+ .build(ByteFrame::Empty, receiver, shutdown_channel)
192
+ .await),
193
+ },
194
+ }
195
+ }
196
+ Err(err_resp) => Ok(err_resp),
197
+ }
198
+ }
199
+
200
+ pub(crate) async fn new(
201
+ request: HttpRequest,
202
+ context: &HttpRequestContext,
203
+ script_name: String,
204
+ ) -> Result<(ItsiHttpRequest, mpsc::Receiver<ByteFrame>), HttpResponse> {
205
+ let (parts, body) = request.into_parts();
206
+ let body = if context.server_params.streamable_body {
207
+ ItsiBody::Stream(ItsiBodyProxy::new(body))
208
+ } else {
209
+ let mut body_bytes = BigBytes::new();
210
+ let mut stream = body.into_data_stream();
211
+ while let Some(chunk) = stream.next().await {
212
+ match chunk {
213
+ Ok(byte_array) => body_bytes.write_all(&byte_array).unwrap(),
214
+ Err(e) => {
215
+ let mut err_resp = Response::new(BoxBody::new(Empty::new()));
216
+ if e.downcast_ref::<MaxBodySizeReached>().is_some() {
217
+ *err_resp.status_mut() = StatusCode::PAYLOAD_TOO_LARGE;
218
+ }
219
+ return Err(err_resp);
220
+ }
221
+ }
222
+ }
223
+ ItsiBody::Buffered(body_bytes)
224
+ };
225
+ let response_channel = mpsc::channel::<ByteFrame>(100);
226
+ Ok((
227
+ Self {
228
+ context: context.clone(),
229
+ version: parts.version,
230
+ response: ItsiHttpResponse::new(parts.clone(), response_channel.0),
231
+ start: Instant::now(),
232
+ script_name,
233
+ body,
234
+ parts,
235
+ },
236
+ response_channel.1,
237
+ ))
238
+ }
239
+
240
+ pub(crate) fn path(&self) -> MagnusResult<&str> {
241
+ Ok(self
242
+ .parts
243
+ .uri
244
+ .path()
245
+ .strip_prefix(&self.script_name)
246
+ .unwrap_or(self.parts.uri.path()))
247
+ }
248
+
249
+ pub(crate) fn script_name(&self) -> MagnusResult<&str> {
250
+ Ok(&self.script_name)
251
+ }
252
+
253
+ pub(crate) fn query_string(&self) -> MagnusResult<&str> {
254
+ Ok(self.parts.uri.query().unwrap_or(""))
255
+ }
256
+
257
+ pub(crate) fn method(&self) -> MagnusResult<&str> {
258
+ Ok(self.parts.method.as_str())
259
+ }
260
+
261
+ pub(crate) fn version(&self) -> MagnusResult<&str> {
262
+ Ok(match self.version {
263
+ Version::HTTP_09 => "HTTP/0.9",
264
+ Version::HTTP_10 => "HTTP/1.0",
265
+ Version::HTTP_11 => "HTTP/1.1",
266
+ Version::HTTP_2 => "HTTP/2.0",
267
+ Version::HTTP_3 => "HTTP/3.0",
268
+ _ => "HTTP/Unknown",
269
+ })
270
+ }
271
+
272
+ pub(crate) fn rack_protocol(&self) -> MagnusResult<Vec<&str>> {
273
+ Ok(self
274
+ .parts
275
+ .headers
276
+ .get("upgrade")
277
+ .or_else(|| self.parts.headers.get("protocol"))
278
+ .map(|value| {
279
+ value
280
+ .to_str()
281
+ .unwrap_or("")
282
+ .split(',')
283
+ .map(|s| s.trim())
284
+ .collect::<Vec<&str>>()
285
+ })
286
+ .unwrap_or_else(|| vec!["http"]))
287
+ }
288
+
289
+ pub(crate) fn host(&self) -> MagnusResult<&str> {
290
+ Ok(self
291
+ .parts
292
+ .uri
293
+ .host()
294
+ .unwrap_or_else(|| &self.context.listener.host))
295
+ }
296
+
297
+ pub(crate) fn scheme(&self) -> MagnusResult<&str> {
298
+ Ok(self
299
+ .parts
300
+ .uri
301
+ .scheme()
302
+ .map(|scheme| scheme.as_str())
303
+ .unwrap_or_else(|| &self.context.listener.scheme))
304
+ }
305
+
306
+ pub(crate) fn headers(&self) -> MagnusResult<Vec<(&str, &str)>> {
307
+ Ok(self
308
+ .parts
309
+ .headers
310
+ .iter()
311
+ .map(|(hn, hv)| (hn.as_str(), hv.to_str().unwrap_or("")))
312
+ .collect::<Vec<(&str, &str)>>())
313
+ }
314
+
315
+ pub fn header(&self, name: String) -> MagnusResult<Option<Vec<&str>>> {
316
+ let result: Vec<&str> = self
317
+ .parts
318
+ .headers
319
+ .get_all(&name)
320
+ .iter()
321
+ .filter_map(|value| value.to_str().ok())
322
+ .collect();
323
+ Ok(Some(result))
324
+ }
325
+
326
+ pub(crate) fn remote_addr(&self) -> MagnusResult<&str> {
327
+ Ok(&self.context.addr)
328
+ }
329
+
330
+ pub(crate) fn port(&self) -> MagnusResult<u16> {
331
+ Ok(self
332
+ .parts
333
+ .uri
334
+ .port_u16()
335
+ .unwrap_or(self.context.listener.port))
336
+ }
337
+
338
+ pub(crate) fn body(&self) -> MagnusResult<Option<Value>> {
339
+ Ok(self.body.into_value())
340
+ }
341
+
342
+ pub(crate) fn response(&self) -> MagnusResult<ItsiHttpResponse> {
343
+ Ok(self.response.clone())
344
+ }
345
+ }
@@ -2,8 +2,9 @@ use bytes::{Bytes, BytesMut};
2
2
  use derive_more::Debug;
3
3
  use futures::stream::{unfold, StreamExt};
4
4
  use http::{
5
- header::TRANSFER_ENCODING, request::Parts, HeaderMap, HeaderName, HeaderValue, Request,
6
- Response, StatusCode,
5
+ header::{ACCEPT, TRANSFER_ENCODING},
6
+ request::Parts,
7
+ HeaderMap, HeaderName, HeaderValue, Request, Response, StatusCode,
7
8
  };
8
9
  use http_body_util::{combinators::BoxBody, Empty, Full, StreamBody};
9
10
  use hyper::{body::Frame, upgrade::Upgraded};
@@ -13,7 +14,7 @@ use itsi_tracing::error;
13
14
  use magnus::error::Result as MagnusResult;
14
15
  use parking_lot::RwLock;
15
16
  use std::{
16
- convert::Infallible,
17
+ collections::HashMap,
17
18
  io,
18
19
  os::{fd::FromRawFd, unix::net::UnixStream},
19
20
  str::FromStr,
@@ -31,30 +32,33 @@ use tokio_stream::wrappers::ReceiverStream;
31
32
  use tokio_util::io::ReaderStream;
32
33
  use tracing::warn;
33
34
 
34
- use crate::server::serve_strategy::single_mode::RunningPhase;
35
+ use crate::server::{
36
+ byte_frame::ByteFrame, http_message_types::HttpResponse,
37
+ serve_strategy::single_mode::RunningPhase,
38
+ };
35
39
 
36
- #[magnus::wrap(class = "Itsi::Response", free_immediately, size)]
40
+ #[magnus::wrap(class = "Itsi::HttpResponse", free_immediately, size)]
37
41
  #[derive(Debug, Clone)]
38
- pub struct ItsiResponse {
42
+ pub struct ItsiHttpResponse {
39
43
  pub data: Arc<ResponseData>,
40
44
  }
41
45
 
42
46
  #[derive(Debug)]
43
47
  pub struct ResponseData {
44
- pub response: RwLock<Option<Response<BoxBody<Bytes, Infallible>>>>,
45
- pub response_writer: RwLock<Option<mpsc::Sender<Option<Bytes>>>>,
48
+ pub response: RwLock<Option<HttpResponse>>,
49
+ pub response_writer: RwLock<Option<mpsc::Sender<ByteFrame>>>,
46
50
  pub response_buffer: RwLock<BytesMut>,
47
51
  pub hijacked_socket: RwLock<Option<UnixStream>>,
48
52
  pub parts: Parts,
49
53
  }
50
54
 
51
- impl ItsiResponse {
55
+ impl ItsiHttpResponse {
52
56
  pub async fn build(
53
57
  &self,
54
- first_frame: Option<Bytes>,
55
- receiver: mpsc::Receiver<Option<Bytes>>,
58
+ first_frame: ByteFrame,
59
+ receiver: mpsc::Receiver<ByteFrame>,
56
60
  shutdown_rx: watch::Receiver<RunningPhase>,
57
- ) -> Response<BoxBody<Bytes, Infallible>> {
61
+ ) -> HttpResponse {
58
62
  if self.is_hijacked() {
59
63
  return match self.process_hijacked_response().await {
60
64
  Ok(result) => result,
@@ -64,31 +68,30 @@ impl ItsiResponse {
64
68
  }
65
69
  };
66
70
  }
67
-
68
71
  let mut response = self.data.response.write().take().unwrap();
69
- *response.body_mut() = if first_frame.is_none() {
72
+ *response.body_mut() = if matches!(first_frame, ByteFrame::Empty) {
70
73
  BoxBody::new(Empty::new())
71
- } else if receiver.is_closed() && receiver.is_empty() {
72
- BoxBody::new(Full::new(first_frame.unwrap()))
74
+ } else if matches!(first_frame, ByteFrame::End(_)) {
75
+ BoxBody::new(Full::new(first_frame.into()))
73
76
  } else {
74
- let initial_frame = tokio_stream::once(Ok(Frame::data(first_frame.unwrap())));
77
+ let initial_frame = tokio_stream::once(Ok(Frame::data(Bytes::from(first_frame))));
75
78
  let frame_stream = unfold(
76
79
  (ReceiverStream::new(receiver), shutdown_rx),
77
80
  |(mut receiver, mut shutdown_rx)| async move {
78
81
  if let RunningPhase::ShutdownPending = *shutdown_rx.borrow() {
79
- warn!("Disconnecting streaming client.");
80
82
  return None;
81
83
  }
82
84
  loop {
83
85
  tokio::select! {
84
86
  maybe_bytes = receiver.next() => {
85
- if let Some(bytes) = maybe_bytes {
86
- // We assume `bytes` is Some(Bytes) here.
87
- return Some((Ok(Frame::data(bytes.unwrap())), (receiver, shutdown_rx)));
88
- } else {
89
- // Receiver closed, end the stream.
90
- return None;
87
+ match maybe_bytes {
88
+ Some(ByteFrame::Data(bytes)) | Some(ByteFrame::End(bytes)) => {
89
+ return Some((Ok(Frame::data(bytes)), (receiver, shutdown_rx)));
91
90
  }
91
+ _ => {
92
+ return None;
93
+ }
94
+ }
92
95
  },
93
96
  _ = shutdown_rx.changed() => {
94
97
  match *shutdown_rx.borrow() {
@@ -184,7 +187,7 @@ impl ItsiResponse {
184
187
  Ok((headers, status, requires_upgrade, reader))
185
188
  }
186
189
 
187
- pub async fn process_hijacked_response(&self) -> Result<Response<BoxBody<Bytes, Infallible>>> {
190
+ pub async fn process_hijacked_response(&self) -> Result<HttpResponse> {
188
191
  let (headers, status, requires_upgrade, reader) = self.read_hijacked_headers().await?;
189
192
  let mut response = if requires_upgrade {
190
193
  let parts = self.data.parts.clone();
@@ -261,27 +264,31 @@ impl ItsiResponse {
261
264
  }
262
265
  }
263
266
 
264
- pub fn send_frame(&self, frame: Bytes) -> MagnusResult<usize> {
265
- self.send_frame_into(frame, &self.data.response_writer)
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
266
273
  }
267
274
 
268
- pub fn send_and_close(&self, frame: Bytes) -> MagnusResult<usize> {
269
- let result = self.send_frame_into(frame, &self.data.response_writer);
275
+ pub fn send_and_close(&self, frame: Bytes) -> MagnusResult<()> {
276
+ let result = self.send_frame_into(ByteFrame::End(frame), &self.data.response_writer);
270
277
  self.data.response_writer.write().take();
271
278
  result
272
279
  }
273
280
 
274
281
  pub fn send_frame_into(
275
282
  &self,
276
- frame: Bytes,
277
- writer: &RwLock<Option<mpsc::Sender<Option<Bytes>>>>,
278
- ) -> MagnusResult<usize> {
283
+ frame: ByteFrame,
284
+ writer: &RwLock<Option<mpsc::Sender<ByteFrame>>>,
285
+ ) -> MagnusResult<()> {
279
286
  if let Some(writer) = writer.write().as_ref() {
280
- writer
281
- .blocking_send(Some(frame))
282
- .map_err(|_| itsi_error::ItsiError::ClientConnectionClosed)?;
287
+ return Ok(writer
288
+ .blocking_send(frame)
289
+ .map_err(|_| itsi_error::ItsiError::ClientConnectionClosed)?);
283
290
  }
284
- Ok(0)
291
+ Ok(())
285
292
  }
286
293
 
287
294
  pub fn is_hijacked(&self) -> bool {
@@ -293,11 +300,28 @@ impl ItsiResponse {
293
300
  Ok(true)
294
301
  }
295
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
+
296
320
  pub fn close_read(&self) -> MagnusResult<bool> {
297
- todo!();
321
+ Ok(true)
298
322
  }
299
323
 
300
- pub fn new(parts: Parts, response_writer: mpsc::Sender<Option<Bytes>>) -> Self {
324
+ pub fn new(parts: Parts, response_writer: mpsc::Sender<ByteFrame>) -> Self {
301
325
  Self {
302
326
  data: Arc::new(ResponseData {
303
327
  response: RwLock::new(Some(Response::new(BoxBody::new(Empty::new())))),
@@ -320,6 +344,26 @@ impl ItsiResponse {
320
344
  Ok(())
321
345
  }
322
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
+
323
367
  pub fn set_status(&self, status: u16) -> MagnusResult<()> {
324
368
  if let Some(ref mut resp) = *self.data.response.write() {
325
369
  *resp.status_mut() = StatusCode::from_u16(status).map_err(|e| {
@@ -338,8 +382,8 @@ impl ItsiResponse {
338
382
  *self.data.hijacked_socket.write() = Some(stream);
339
383
  if let Some(writer) = self.data.response_writer.write().as_ref() {
340
384
  writer
341
- .blocking_send(None)
342
- .map_err(|_| itsi_error::ItsiError::ClientConnectionClosed)?;
385
+ .blocking_send(ByteFrame::Empty)
386
+ .map_err(|_| itsi_error::ItsiError::ClientConnectionClosed)?
343
387
  }
344
388
  self.close();
345
389
  Ok(())