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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3c1c81086711872c2ceaa5042078e05f4da5b6f4246bf6d5bc24ff607581557
4
- data.tar.gz: 551e582ba837c8a4fe19a09ced2f38866493be737e6ac5188d206fad4835a510
3
+ metadata.gz: 239b868f5edfaff5a1c7dd9e47b9f6023f284ae7c06a43438ec127ab83b5f1c1
4
+ data.tar.gz: 8f0e2154a4e05281eeabb9e8eaded247be04ea17efa66e72cccbad7df356dcfb
5
5
  SHA512:
6
- metadata.gz: 5e858281765fdc2c698d2af7ec7e5c05efbda0430cc49ca0319acd23bdc7f35b12d72a5496fbac6c3cbd1d594f46dfb16bf5daac2fded466be9ede98d3cecca5
7
- data.tar.gz: e28a5fa4d4c3c6b2b2169005f0d686d90f2bf9c1b98f02bc7dd283189e26ad09698d519a7879280724b49e2794c994872b7c3cba5927dcdcf974d6b762f24164
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.13"
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.13"
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-scheduler"
3
- version = "0.2.13"
3
+ version = "0.2.14"
4
4
  edition = "2021"
5
5
  authors = ["Wouter Coppieters <wc@pico.net.nz>"]
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "itsi-server"
3
- version = "0.2.13"
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().append(header_name, header_value);
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
- let header_value = unsafe { HeaderValue::from_maybe_shared_unchecked(value) };
367
- headers_mut.append(&header_name, header_value);
408
+ self.insert_header(headers_mut, &header_name, value);
368
409
  }
369
410
  }
370
411
  }
@@ -213,7 +213,7 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
213
213
 
214
214
  [[package]]
215
215
  name = "itsi-scheduler"
216
- version = "0.2.13"
216
+ version = "0.2.14"
217
217
  dependencies = [
218
218
  "bytes",
219
219
  "derive_more",
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Scheduler
5
- VERSION = "0.2.13"
5
+ VERSION = "0.2.14"
6
6
  end
7
7
  end
@@ -1644,7 +1644,7 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
1644
1644
 
1645
1645
  [[package]]
1646
1646
  name = "itsi-server"
1647
- version = "0.2.13"
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, and custom header support. Supports relative path rewriting and file range requests."
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(["fallthrough", "index", "redirect", "internal_server_error"]),
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
- static_response \\
7
- code: ${1|200,404,500|}, \\
8
- headers: [${2|["Content-Type","text/plain"],["Cache-Control","max-age=60"]|}], \\
9
- body: ${3|"OK".bytes, "Not Found".bytes|}
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: (Type(Integer) & Required()),
17
+ code: (Type(Integer) & Required()),
17
18
  headers: Array(Array(Type(String), Type(String))).default([]),
18
- body: Type(String).default("")
19
+ body: Type(String).default("")
19
20
  }
20
21
  end
21
22
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Server
5
- VERSION = "0.2.13"
5
+ VERSION = "0.2.14"
6
6
  end
7
7
  end
@@ -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 |env|
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
@@ -1,3 +1,3 @@
1
1
  module Itsi
2
- VERSION = '0.2.13'
2
+ VERSION = '0.2.14'
3
3
  end
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.13
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.13
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.13
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.13
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.13
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