itsi 0.2.13 → 0.2.14
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 +9 -0
- data/Cargo.lock +3 -2
- data/crates/itsi_scheduler/Cargo.toml +1 -1
- data/crates/itsi_server/Cargo.toml +2 -1
- data/crates/itsi_server/src/ruby_types/itsi_http_response.rs +46 -5
- data/gems/scheduler/Cargo.lock +1 -1
- data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
- data/gems/server/Cargo.lock +2 -1
- data/gems/server/lib/itsi/server/config/middleware/static_assets.rb +3 -2
- data/gems/server/lib/itsi/server/config/middleware/static_response.rb +7 -6
- data/gems/server/lib/itsi/server/version.rb +1 -1
- data/gems/server/test/rack/test_rack_server.rb +32 -1
- data/lib/itsi/version.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 239b868f5edfaff5a1c7dd9e47b9f6023f284ae7c06a43438ec127ab83b5f1c1
|
4
|
+
data.tar.gz: 8f0e2154a4e05281eeabb9e8eaded247be04ea17efa66e72cccbad7df356dcfb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: db430b1ba0736844a54eb9ebe00a2f12b51342b27544bdd58ccce2440fb5eb1e6e961cf95dc3a5665def3384ba1f8cd2cfa457f93e62d01022be3419a1f3b00c
|
7
|
+
data.tar.gz: e626d98db8d34bddca498e8086ff8b4dfe41091b1363f7c3f0899b061ecab0ff79b628f2393f82b54dca073c78f85801fb07af3d5c0880f334103fd3cf7a7b86
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
## [0.2.14] - 2025-04-30
|
2
|
+
- Support new-line separated headers for Rack 2 backward compatibility.
|
3
|
+
|
4
|
+
## [0.2.12] - 2025-04-29
|
5
|
+
- Max Rust edition is now "2021"
|
6
|
+
- Removed invalid rbs files causing RI doc generation failure
|
7
|
+
- Fixed header clobbering in Rack
|
8
|
+
- Added new `ruby_thread_request_backlog_size` option
|
9
|
+
|
1
10
|
## [0.2.3] - 2025-04-22
|
2
11
|
|
3
12
|
- Public release!
|
data/Cargo.lock
CHANGED
@@ -1644,7 +1644,7 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
|
1644
1644
|
|
1645
1645
|
[[package]]
|
1646
1646
|
name = "itsi-scheduler"
|
1647
|
-
version = "0.2.
|
1647
|
+
version = "0.2.14"
|
1648
1648
|
dependencies = [
|
1649
1649
|
"bytes",
|
1650
1650
|
"derive_more",
|
@@ -1662,7 +1662,7 @@ dependencies = [
|
|
1662
1662
|
|
1663
1663
|
[[package]]
|
1664
1664
|
name = "itsi-server"
|
1665
|
-
version = "0.2.
|
1665
|
+
version = "0.2.14"
|
1666
1666
|
dependencies = [
|
1667
1667
|
"argon2",
|
1668
1668
|
"async-channel",
|
@@ -1692,6 +1692,7 @@ dependencies = [
|
|
1692
1692
|
"jsonwebtoken",
|
1693
1693
|
"magnus",
|
1694
1694
|
"md5",
|
1695
|
+
"memchr",
|
1695
1696
|
"moka",
|
1696
1697
|
"nix",
|
1697
1698
|
"notify",
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[package]
|
2
2
|
name = "itsi-server"
|
3
|
-
version = "0.2.
|
3
|
+
version = "0.2.14"
|
4
4
|
edition = "2021"
|
5
5
|
authors = ["Wouter Coppieters <wc@pico.net.nz>"]
|
6
6
|
license = "MIT"
|
@@ -88,3 +88,4 @@ percent-encoding = "2.3.1"
|
|
88
88
|
sha-crypt = "0.5.0"
|
89
89
|
argon2 = "0.5.3"
|
90
90
|
core_affinity = "0.8.3"
|
91
|
+
memchr = "2.7.4"
|
@@ -1,4 +1,4 @@
|
|
1
|
-
use bytes::{Bytes, BytesMut};
|
1
|
+
use bytes::{Buf, Bytes, BytesMut};
|
2
2
|
use derive_more::Debug;
|
3
3
|
use futures::stream::{unfold, StreamExt};
|
4
4
|
use http::{
|
@@ -12,6 +12,7 @@ use hyper_util::rt::TokioIo;
|
|
12
12
|
use itsi_error::Result;
|
13
13
|
use itsi_tracing::error;
|
14
14
|
use magnus::error::Result as MagnusResult;
|
15
|
+
use memchr::{memchr, memchr_iter};
|
15
16
|
use parking_lot::RwLock;
|
16
17
|
use std::{
|
17
18
|
collections::HashMap,
|
@@ -345,13 +346,54 @@ impl ItsiHttpResponse {
|
|
345
346
|
let header_name: HeaderName = HeaderName::from_bytes(&name).map_err(|e| {
|
346
347
|
itsi_error::ItsiError::InvalidInput(format!("Invalid header name {:?}: {:?}", name, e))
|
347
348
|
})?;
|
348
|
-
let header_value = unsafe { HeaderValue::from_maybe_shared_unchecked(value) };
|
349
349
|
if let Some(ref mut resp) = *self.data.response.write() {
|
350
|
-
resp.headers_mut()
|
350
|
+
let headers_mut = resp.headers_mut();
|
351
|
+
self.insert_header(headers_mut, &header_name, value);
|
351
352
|
}
|
352
353
|
Ok(())
|
353
354
|
}
|
354
355
|
|
356
|
+
pub fn insert_header(
|
357
|
+
&self,
|
358
|
+
headers_mut: &mut HeaderMap,
|
359
|
+
header_name: &HeaderName,
|
360
|
+
value: Bytes,
|
361
|
+
) {
|
362
|
+
static MAX_SPLIT_HEADERS: usize = 100;
|
363
|
+
|
364
|
+
let mut start = 0usize;
|
365
|
+
let mut emitted = 0usize;
|
366
|
+
|
367
|
+
for idx in memchr_iter(b'\n', &value).chain(std::iter::once(value.len())) {
|
368
|
+
if idx == start {
|
369
|
+
start += 1;
|
370
|
+
continue;
|
371
|
+
}
|
372
|
+
|
373
|
+
let mut part = value.slice(start..idx);
|
374
|
+
if part.ends_with(b"\r") {
|
375
|
+
part.truncate(part.len() - 1);
|
376
|
+
}
|
377
|
+
if let Some(&(b' ' | b'\t')) = part.first() {
|
378
|
+
part.advance(1);
|
379
|
+
}
|
380
|
+
if memchr(0, &part).is_some() || part.iter().any(|&b| b < 0x20) {
|
381
|
+
warn!("stripped control char from header {:?}", header_name);
|
382
|
+
start = idx + 1;
|
383
|
+
continue;
|
384
|
+
}
|
385
|
+
|
386
|
+
emitted += 1;
|
387
|
+
if emitted > MAX_SPLIT_HEADERS {
|
388
|
+
break;
|
389
|
+
}
|
390
|
+
|
391
|
+
let hv = unsafe { HeaderValue::from_maybe_shared_unchecked(part) };
|
392
|
+
headers_mut.append(header_name, hv);
|
393
|
+
start = idx + 1;
|
394
|
+
}
|
395
|
+
}
|
396
|
+
|
355
397
|
pub fn add_headers(&self, headers: HashMap<Bytes, Vec<Bytes>>) -> MagnusResult<()> {
|
356
398
|
if let Some(ref mut resp) = *self.data.response.write() {
|
357
399
|
let headers_mut = resp.headers_mut();
|
@@ -363,8 +405,7 @@ impl ItsiHttpResponse {
|
|
363
405
|
))
|
364
406
|
})?;
|
365
407
|
for value in values {
|
366
|
-
|
367
|
-
headers_mut.append(&header_name, header_value);
|
408
|
+
self.insert_header(headers_mut, &header_name, value);
|
368
409
|
}
|
369
410
|
}
|
370
411
|
}
|
data/gems/scheduler/Cargo.lock
CHANGED
data/gems/server/Cargo.lock
CHANGED
@@ -1644,7 +1644,7 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
|
1644
1644
|
|
1645
1645
|
[[package]]
|
1646
1646
|
name = "itsi-server"
|
1647
|
-
version = "0.2.
|
1647
|
+
version = "0.2.14"
|
1648
1648
|
dependencies = [
|
1649
1649
|
"argon2",
|
1650
1650
|
"async-channel",
|
@@ -1674,6 +1674,7 @@ dependencies = [
|
|
1674
1674
|
"jsonwebtoken",
|
1675
1675
|
"magnus",
|
1676
1676
|
"md5",
|
1677
|
+
"memchr",
|
1677
1678
|
"moka",
|
1678
1679
|
"nix",
|
1679
1680
|
"notify",
|
@@ -18,7 +18,8 @@ module Itsi
|
|
18
18
|
serve_hidden_files: ${11|true,false|}
|
19
19
|
SNIPPET
|
20
20
|
|
21
|
-
detail "Serves static files from a designated directory with options for auto indexing, in-memory caching,
|
21
|
+
detail "Serves static files from a designated directory with options for auto indexing, in-memory caching, "\
|
22
|
+
"and custom header support. Supports relative path rewriting and file range requests."
|
22
23
|
|
23
24
|
ErrorResponse = TypedStruct.new do
|
24
25
|
{
|
@@ -42,7 +43,7 @@ module Itsi
|
|
42
43
|
{
|
43
44
|
root_dir: (Type(String) & Required()).default("./"),
|
44
45
|
not_found_behavior: Or(
|
45
|
-
Enum([
|
46
|
+
Enum(%w[fallthrough index redirect internal_server_error]),
|
46
47
|
Type(IndexResponse),
|
47
48
|
Type(RedirectResponse),
|
48
49
|
Type(ErrorResponse)
|
@@ -2,20 +2,21 @@ module Itsi
|
|
2
2
|
class Server
|
3
3
|
module Config
|
4
4
|
class StaticResponse < Middleware
|
5
|
+
|
5
6
|
insert_text <<~SNIPPET
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
static_response \\
|
8
|
+
code: ${1|200,404,500|},
|
9
|
+
headers: [${2|%w[content-type text/plain],%w[cache-control max-age=60]|}],
|
10
|
+
body: ${3|"OK", "Not Found"|}
|
10
11
|
SNIPPET
|
11
12
|
|
12
13
|
detail "Immediately return a fixed HTTP response with code, headers, and body."
|
13
14
|
|
14
15
|
schema do
|
15
16
|
{
|
16
|
-
code:
|
17
|
+
code: (Type(Integer) & Required()),
|
17
18
|
headers: Array(Array(Type(String), Type(String))).default([]),
|
18
|
-
body:
|
19
|
+
body: Type(String).default("")
|
19
20
|
}
|
20
21
|
end
|
21
22
|
|
@@ -356,7 +356,7 @@ class TestRackServer < Minitest::Test
|
|
356
356
|
end
|
357
357
|
|
358
358
|
def test_multi_field_headers
|
359
|
-
server(app_with_lint: lambda do |
|
359
|
+
server(app_with_lint: lambda do |_|
|
360
360
|
[200, { "content-type" => "text/plain", "x-example" => ["one, two, three", "four, five"] }, ["Multiple Field Headers"]]
|
361
361
|
end) do
|
362
362
|
response = get_resp("/")
|
@@ -365,4 +365,35 @@ class TestRackServer < Minitest::Test
|
|
365
365
|
assert_equal "Multiple Field Headers", response.body
|
366
366
|
end
|
367
367
|
end
|
368
|
+
|
369
|
+
# For backwards compatibility with Rack-2, which uses "\n" for multiple values in headers.
|
370
|
+
# https://github.com/rack/rack/blob/df6c47357f6c6bec2d585f45f417285d813d9b3a/lib/rack/utils.rb#L271
|
371
|
+
#
|
372
|
+
# In Rack 3, the behavior has changed to using Arrays of Strings exclusively.
|
373
|
+
# Note we don't use Rack lint here, because it'll complain about the invalid header value.
|
374
|
+
def test_multiline_headers_legacy_cookie
|
375
|
+
cookies = "one\r\ntwo\n three\n\tfour\nfive\n\n"
|
376
|
+
server(app: ->(_) { [200, { "set-cookie" => cookies }, ["OK"]] }) do
|
377
|
+
resp = get_resp("/")
|
378
|
+
assert_equal "200", resp.code
|
379
|
+
# folded lines should coalesce; empty lines disappear
|
380
|
+
assert_equal %w[one two three four five], resp.get_fields("set-cookie")
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
def test_control_chars_are_stripped
|
385
|
+
evil = "good\nbad\x01bad\ngood"
|
386
|
+
server(app: ->(_) { [200, { "x-evil" => evil }, ["body"]] }) do
|
387
|
+
resp = get_resp("/")
|
388
|
+
assert_equal %w[good good], resp.get_fields("x-evil")
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
def test_rack3_array_is_untouched
|
393
|
+
server(app: ->(_) { [200,
|
394
|
+
{ "set-cookie" => ["a=b", "c=d"] }, ["OK"] ] }) do
|
395
|
+
resp = get_resp("/")
|
396
|
+
assert_equal %w[a=b c=d], resp.get_fields("set-cookie")
|
397
|
+
end
|
398
|
+
end
|
368
399
|
end
|
data/lib/itsi/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: itsi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.14
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wouter Coppieters
|
@@ -15,28 +15,28 @@ dependencies:
|
|
15
15
|
requirements:
|
16
16
|
- - "~>"
|
17
17
|
- !ruby/object:Gem::Version
|
18
|
-
version: 0.2.
|
18
|
+
version: 0.2.14
|
19
19
|
type: :runtime
|
20
20
|
prerelease: false
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
22
22
|
requirements:
|
23
23
|
- - "~>"
|
24
24
|
- !ruby/object:Gem::Version
|
25
|
-
version: 0.2.
|
25
|
+
version: 0.2.14
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: itsi-server
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
29
29
|
requirements:
|
30
30
|
- - "~>"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: 0.2.
|
32
|
+
version: 0.2.14
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
36
36
|
requirements:
|
37
37
|
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
|
-
version: 0.2.
|
39
|
+
version: 0.2.14
|
40
40
|
description: Wrapper Gem for both the Itsi server and the Itsi Fiber scheduler
|
41
41
|
email:
|
42
42
|
- wc@pico.net.nz
|