quicsilver 0.5.0-x86_64-linux
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 +7 -0
- data/.github/workflows/ci.yml +49 -0
- data/.github/workflows/cibuildgem.yaml +117 -0
- data/.gitignore +21 -0
- data/.gitmodules +3 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +175 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +67 -0
- data/LICENSE +21 -0
- data/README.md +261 -0
- data/Rakefile +130 -0
- data/benchmarks/baseline.rb +175 -0
- data/benchmarks/compare.rb +398 -0
- data/benchmarks/components.rb +191 -0
- data/benchmarks/helpers.rb +162 -0
- data/benchmarks/latency.rb +128 -0
- data/benchmarks/loss.rb +193 -0
- data/benchmarks/mobile.rb +125 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/docs/connection_identity.md +141 -0
- data/examples/README.md +58 -0
- data/examples/benchmark.rb +191 -0
- data/examples/connection_pool_demo.rb +47 -0
- data/examples/example_helper.rb +69 -0
- data/examples/falcon_middleware.rb +44 -0
- data/examples/feature_demo.rb +125 -0
- data/examples/grpc_style.rb +97 -0
- data/examples/latency_aware_routing.rb +382 -0
- data/examples/lifecycle_drain.rb +264 -0
- data/examples/minimal_http3_server.rb +14 -0
- data/examples/priorities.rb +60 -0
- data/examples/protocol_http_server.rb +31 -0
- data/examples/rack_http3_server.rb +42 -0
- data/examples/rails_feature_test.rb +280 -0
- data/examples/server_stats.rb +117 -0
- data/examples/simple_client_test.rb +26 -0
- data/examples/streaming_sse.rb +33 -0
- data/examples/trailers.rb +69 -0
- data/examples/webtransport_smoke.rb +546 -0
- data/examples/worker_load_balancing.rb +425 -0
- data/ext/quicsilver/extconf.rb +124 -0
- data/ext/quicsilver/include/msquic.h +2226 -0
- data/ext/quicsilver/include/msquic_posix.h +696 -0
- data/ext/quicsilver/include/quic_sal_stub.h +314 -0
- data/ext/quicsilver/msquic_patch.rb +68 -0
- data/ext/quicsilver/patches/reliable-reset.patch +723 -0
- data/ext/quicsilver/patches/stream-final-size.patch +47 -0
- data/ext/quicsilver/quicsilver.c +2741 -0
- data/lib/quicsilver/3.4/quicsilver.so +0 -0
- data/lib/quicsilver/4.0/quicsilver.so +0 -0
- data/lib/quicsilver/client/client.rb +708 -0
- data/lib/quicsilver/client/connection_pool.rb +202 -0
- data/lib/quicsilver/client/request.rb +190 -0
- data/lib/quicsilver/errors.rb +39 -0
- data/lib/quicsilver/libmsquic.so.2 +0 -0
- data/lib/quicsilver/protocol/adapter.rb +196 -0
- data/lib/quicsilver/protocol/capsule.rb +38 -0
- data/lib/quicsilver/protocol/connect_stream_decoder.rb +75 -0
- data/lib/quicsilver/protocol/control_stream_parser.rb +117 -0
- data/lib/quicsilver/protocol/datagram.rb +40 -0
- data/lib/quicsilver/protocol/frame_parser.rb +141 -0
- data/lib/quicsilver/protocol/frame_reader.rb +84 -0
- data/lib/quicsilver/protocol/frames.rb +454 -0
- data/lib/quicsilver/protocol/priority.rb +65 -0
- data/lib/quicsilver/protocol/qpack/decoder.rb +127 -0
- data/lib/quicsilver/protocol/qpack/encoder.rb +217 -0
- data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +140 -0
- data/lib/quicsilver/protocol/qpack/huffman.rb +459 -0
- data/lib/quicsilver/protocol/request_encoder.rb +50 -0
- data/lib/quicsilver/protocol/request_parser.rb +289 -0
- data/lib/quicsilver/protocol/response_encoder.rb +107 -0
- data/lib/quicsilver/protocol/response_parser.rb +147 -0
- data/lib/quicsilver/protocol/stream_input.rb +98 -0
- data/lib/quicsilver/protocol/stream_output.rb +59 -0
- data/lib/quicsilver/protocol/unidirectional_stream.rb +12 -0
- data/lib/quicsilver/protocol/web_transport.rb +214 -0
- data/lib/quicsilver/rack/context.rb +33 -0
- data/lib/quicsilver/response.rb +40 -0
- data/lib/quicsilver/server/listener_data.rb +14 -0
- data/lib/quicsilver/server/rack_adapter.rb +77 -0
- data/lib/quicsilver/server/receive_queue.rb +106 -0
- data/lib/quicsilver/server/request_handler.rb +149 -0
- data/lib/quicsilver/server/request_registry.rb +63 -0
- data/lib/quicsilver/server/scheduler.rb +50 -0
- data/lib/quicsilver/server/schedulers/fiber_scheduler.rb +79 -0
- data/lib/quicsilver/server/schedulers/thread_scheduler.rb +76 -0
- data/lib/quicsilver/server/server.rb +1080 -0
- data/lib/quicsilver/server/web_transport_flow_control.rb +270 -0
- data/lib/quicsilver/server/web_transport_manager.rb +281 -0
- data/lib/quicsilver/server/web_transport_registry.rb +40 -0
- data/lib/quicsilver/server/web_transport_session.rb +812 -0
- data/lib/quicsilver/server/web_transport_stream.rb +326 -0
- data/lib/quicsilver/transport/configuration.rb +234 -0
- data/lib/quicsilver/transport/connection.rb +594 -0
- data/lib/quicsilver/transport/connection_stats.rb +53 -0
- data/lib/quicsilver/transport/event_loop.rb +74 -0
- data/lib/quicsilver/transport/inbound_stream.rb +53 -0
- data/lib/quicsilver/transport/stream.rb +43 -0
- data/lib/quicsilver/transport/stream_event.rb +49 -0
- data/lib/quicsilver/transport/stream_id.rb +26 -0
- data/lib/quicsilver/version.rb +3 -0
- data/lib/quicsilver.rb +96 -0
- data/lib/rackup/handler/quicsilver.rb +96 -0
- data/quicsilver.gemspec +66 -0
- metadata +362 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "frame_parser"
|
|
4
|
+
|
|
5
|
+
module Quicsilver
|
|
6
|
+
module Protocol
|
|
7
|
+
# Incrementally yields CONNECT DATA payloads without waiting for whole
|
|
8
|
+
# frames. Unknown frame payloads are skipped as bytes arrive.
|
|
9
|
+
class ConnectStreamDecoder
|
|
10
|
+
def initialize
|
|
11
|
+
@buffer = "".b
|
|
12
|
+
@frame_type = nil
|
|
13
|
+
@frame_remaining = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# A caller may break after a terminal capsule. Unprocessed bytes remain
|
|
17
|
+
# available to buffered? so it can reject data following that capsule.
|
|
18
|
+
def each(data)
|
|
19
|
+
@buffer << data if data
|
|
20
|
+
until @buffer.empty?
|
|
21
|
+
break unless @frame_remaining || read_frame_header
|
|
22
|
+
|
|
23
|
+
chunk = consume_payload
|
|
24
|
+
yield chunk if @frame_type == FRAME_DATA
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def buffered?
|
|
29
|
+
!@buffer.empty?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def finish!
|
|
33
|
+
if buffered? || @frame_remaining
|
|
34
|
+
raise FrameError.new("Truncated HTTP/3 frame", error_code: H3_FRAME_ERROR)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def clear
|
|
39
|
+
@buffer.clear
|
|
40
|
+
@frame_remaining = nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def read_frame_header
|
|
46
|
+
type, type_length = Protocol.decode_varint_str(@buffer, 0)
|
|
47
|
+
return false if type_length == 0
|
|
48
|
+
length, length_length = Protocol.decode_varint_str(@buffer, type_length)
|
|
49
|
+
return false if length_length == 0
|
|
50
|
+
|
|
51
|
+
# RFC 9114 §4.4 forbids known non-DATA frames after CONNECT,
|
|
52
|
+
# including HEADERS trailers; these are connection errors.
|
|
53
|
+
if CONTROL_ONLY_FRAME_SET.key?(type) ||
|
|
54
|
+
HTTP2_RESERVED_FRAME_SET.key?(type) ||
|
|
55
|
+
[FRAME_HEADERS, FRAME_PUSH_PROMISE, FRAME_PRIORITY_UPDATE].include?(type)
|
|
56
|
+
raise FrameError, "Frame not allowed on CONNECT"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
@frame_type = type
|
|
60
|
+
@frame_remaining = length
|
|
61
|
+
@buffer = @buffer.byteslice(type_length + length_length..) || "".b
|
|
62
|
+
true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def consume_payload
|
|
66
|
+
count = [@frame_remaining, @buffer.bytesize].min
|
|
67
|
+
chunk = @buffer.byteslice(0, count)
|
|
68
|
+
@buffer = @buffer.byteslice(count..) || "".b
|
|
69
|
+
@frame_remaining -= count
|
|
70
|
+
@frame_remaining = nil if @frame_remaining.zero?
|
|
71
|
+
chunk
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quicsilver
|
|
4
|
+
module Protocol
|
|
5
|
+
# Shared control stream parsing for both server Connection and Client.
|
|
6
|
+
#
|
|
7
|
+
# RFC 9114 §7.2.4: Both endpoints MUST send and process SETTINGS.
|
|
8
|
+
# RFC 9114 §7.2.6: Both endpoints MUST validate incoming GOAWAY.
|
|
9
|
+
#
|
|
10
|
+
# Includer must provide:
|
|
11
|
+
# @settings_received — boolean, initially false
|
|
12
|
+
# @peer_goaway_id — nil initially
|
|
13
|
+
#
|
|
14
|
+
# Includer may override:
|
|
15
|
+
# on_settings_received(settings_hash) — called after SETTINGS parsed
|
|
16
|
+
# on_goaway_received(stream_id) — called after GOAWAY parsed
|
|
17
|
+
# handle_control_frame(type, payload) — called for non-SETTINGS/GOAWAY frames
|
|
18
|
+
module ControlStreamParser
|
|
19
|
+
# RFC 9114 §7.2.4.1 / §11.2.2: HTTP/2 setting identifiers forbidden in HTTP/3
|
|
20
|
+
# 0x00 = SETTINGS_HEADER_TABLE_SIZE (reserved), 0x02-0x05 = various HTTP/2 settings
|
|
21
|
+
# Note: 0x08 (SETTINGS_ENABLE_CONNECT_PROTOCOL) is valid in HTTP/3 per RFC 9220
|
|
22
|
+
HTTP2_SETTINGS = [0x00, 0x02, 0x03, 0x04, 0x05].freeze
|
|
23
|
+
|
|
24
|
+
# Returns the number of bytes consumed. A frame split across receives
|
|
25
|
+
# leaves its bytes unconsumed, and the caller must keep them buffered:
|
|
26
|
+
# control streams never send FIN, so a partial frame is normal.
|
|
27
|
+
def parse_control_frames(data)
|
|
28
|
+
first_frame = !@settings_received
|
|
29
|
+
|
|
30
|
+
Protocol::FrameReader.each(data) do |type, payload|
|
|
31
|
+
if first_frame && type != Protocol::FRAME_SETTINGS
|
|
32
|
+
raise Protocol::FrameError.new("First frame on control stream must be SETTINGS",
|
|
33
|
+
error_code: Protocol::H3_MISSING_SETTINGS)
|
|
34
|
+
end
|
|
35
|
+
first_frame = false
|
|
36
|
+
|
|
37
|
+
case type
|
|
38
|
+
when Protocol::FRAME_SETTINGS
|
|
39
|
+
raise Protocol::FrameError, "Duplicate SETTINGS frame on control stream" if @settings_received
|
|
40
|
+
parse_peer_settings(payload)
|
|
41
|
+
@settings_received = true
|
|
42
|
+
when Protocol::FRAME_GOAWAY
|
|
43
|
+
parse_peer_goaway(payload)
|
|
44
|
+
else
|
|
45
|
+
handle_control_frame(type, payload)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def parse_peer_settings(payload)
|
|
51
|
+
offset = 0
|
|
52
|
+
seen = Set.new
|
|
53
|
+
settings = {}
|
|
54
|
+
|
|
55
|
+
while offset < payload.bytesize
|
|
56
|
+
id, id_len = Protocol.decode_varint(payload.bytes, offset)
|
|
57
|
+
value, value_len = Protocol.decode_varint(payload.bytes, offset + id_len)
|
|
58
|
+
break if id_len == 0 || value_len == 0
|
|
59
|
+
|
|
60
|
+
if HTTP2_SETTINGS.include?(id)
|
|
61
|
+
raise Protocol::FrameError.new("HTTP/2 setting identifier 0x#{id.to_s(16)} not allowed in HTTP/3",
|
|
62
|
+
error_code: Protocol::H3_SETTINGS_ERROR)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if seen.include?(id)
|
|
66
|
+
raise Protocol::FrameError.new("Duplicate setting identifier 0x#{id.to_s(16)}",
|
|
67
|
+
error_code: Protocol::H3_SETTINGS_ERROR)
|
|
68
|
+
end
|
|
69
|
+
seen.add(id)
|
|
70
|
+
|
|
71
|
+
settings[id] = value
|
|
72
|
+
offset += id_len + value_len
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
on_settings_received(settings)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# RFC 9114 §7.2.6: Validate incoming GOAWAY frame.
|
|
79
|
+
# Stream ID must be a client-initiated bidirectional stream ID (divisible by 4)
|
|
80
|
+
# and must not increase from a previous GOAWAY.
|
|
81
|
+
def parse_peer_goaway(payload)
|
|
82
|
+
stream_id, _ = Protocol.decode_varint(payload.bytes, 0)
|
|
83
|
+
|
|
84
|
+
unless stream_id % 4 == 0
|
|
85
|
+
raise Protocol::FrameError.new(
|
|
86
|
+
"GOAWAY stream ID #{stream_id} is not a client-initiated bidirectional stream ID",
|
|
87
|
+
error_code: Protocol::H3_ID_ERROR)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if @peer_goaway_id && stream_id > @peer_goaway_id
|
|
91
|
+
raise Protocol::FrameError.new(
|
|
92
|
+
"GOAWAY stream ID #{stream_id} exceeds previous #{@peer_goaway_id}",
|
|
93
|
+
error_code: Protocol::H3_ID_ERROR)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
@peer_goaway_id = stream_id
|
|
97
|
+
on_goaway_received(stream_id)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
# Override in includer to store settings. Default: no-op.
|
|
103
|
+
def on_settings_received(settings)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Override in includer to react to GOAWAY. Default: no-op.
|
|
107
|
+
def on_goaway_received(stream_id)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Override in includer to handle additional frame types on the control stream.
|
|
111
|
+
# Server handles FORBIDDEN_ON_CONTROL and PRIORITY_UPDATE here.
|
|
112
|
+
# Client ignores unknown frames.
|
|
113
|
+
def handle_control_frame(type, payload)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quicsilver
|
|
4
|
+
module Protocol
|
|
5
|
+
# HTTP/3 DATAGRAM payload framing (RFC 9297).
|
|
6
|
+
#
|
|
7
|
+
# The DATAGRAM frame payload starts with a varint quarter stream ID, followed
|
|
8
|
+
# by the application payload. The quarter stream ID is the HTTP/3 stream ID
|
|
9
|
+
# divided by four, matching the client-initiated bidirectional request stream
|
|
10
|
+
# numbering pattern (0, 4, 8, ...).
|
|
11
|
+
module Datagram
|
|
12
|
+
STREAM_ID_DIVISOR = 4
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def encode(stream_id, payload)
|
|
16
|
+
Protocol.encode_varint(quarter_stream_id(stream_id)) + payload.to_s.b
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def decode(datagram)
|
|
20
|
+
quarter_stream_id, prefix_length = Protocol.decode_varint_str(datagram, 0)
|
|
21
|
+
return if prefix_length == 0
|
|
22
|
+
|
|
23
|
+
[stream_id(quarter_stream_id), payload(datagram, prefix_length)]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def quarter_stream_id(stream_id)
|
|
27
|
+
stream_id / STREAM_ID_DIVISOR
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def stream_id(quarter_stream_id)
|
|
31
|
+
quarter_stream_id * STREAM_ID_DIVISOR
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def payload(datagram, prefix_length)
|
|
35
|
+
datagram.byteslice(prefix_length..-1) || "".b
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
require_relative "frame_reader"
|
|
5
|
+
require_relative "qpack/header_block_decoder"
|
|
6
|
+
|
|
7
|
+
module Quicsilver
|
|
8
|
+
module Protocol
|
|
9
|
+
# Base class for HTTP/3 request and response frame parsing.
|
|
10
|
+
#
|
|
11
|
+
# Handles the shared frame walking loop, HEADERS→DATA→HEADERS ordering,
|
|
12
|
+
# trailer parsing, body accumulation, and size limit enforcement.
|
|
13
|
+
#
|
|
14
|
+
# Subclasses implement:
|
|
15
|
+
# - parse_headers(payload) — decode the first HEADERS frame
|
|
16
|
+
class FrameParser
|
|
17
|
+
# Frame types forbidden on request streams — use hash for O(1) lookup
|
|
18
|
+
# Static-only QPACK decoder (no dynamic table). Used by default.
|
|
19
|
+
# Inject a custom decoder via decoder: kwarg for dynamic QPACK support.
|
|
20
|
+
DEFAULT_DECODER = Qpack::HeaderBlockDecoder.default
|
|
21
|
+
|
|
22
|
+
EMPTY_BODY = StringIO.new("".b).tap { |io| io.set_encoding(Encoding::ASCII_8BIT) }
|
|
23
|
+
|
|
24
|
+
attr_reader :headers, :trailers, :bytes_consumed
|
|
25
|
+
|
|
26
|
+
def frames
|
|
27
|
+
@frames || []
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(decoder:, max_body_size: nil, max_header_size: nil, max_header_count: nil, max_frame_payload_size: nil)
|
|
31
|
+
@decoder = decoder
|
|
32
|
+
@max_body_size = max_body_size
|
|
33
|
+
@max_header_size = max_header_size
|
|
34
|
+
@max_header_count = max_header_count
|
|
35
|
+
@max_frame_payload_size = max_frame_payload_size
|
|
36
|
+
@headers = {}
|
|
37
|
+
@trailers = {}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def body
|
|
41
|
+
if @body
|
|
42
|
+
@body.rewind
|
|
43
|
+
@body
|
|
44
|
+
elsif @cached_body_str
|
|
45
|
+
@body = StringIO.new(@cached_body_str)
|
|
46
|
+
@body.set_encoding(Encoding::ASCII_8BIT)
|
|
47
|
+
@body
|
|
48
|
+
else
|
|
49
|
+
EMPTY_BODY
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def parse!
|
|
56
|
+
walk_frames(@data)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def walk_frames(buffer)
|
|
60
|
+
@headers = {}
|
|
61
|
+
@trailers = {}
|
|
62
|
+
@body = nil
|
|
63
|
+
@frames = nil
|
|
64
|
+
@bytes_consumed = 0
|
|
65
|
+
headers_received = false
|
|
66
|
+
trailers_received = false
|
|
67
|
+
|
|
68
|
+
@bytes_consumed = FrameReader.each(buffer) do |type, payload|
|
|
69
|
+
if @max_frame_payload_size && payload.bytesize > @max_frame_payload_size
|
|
70
|
+
raise Protocol::FrameError.new(
|
|
71
|
+
"Frame payload #{payload.bytesize} exceeds limit #{@max_frame_payload_size}",
|
|
72
|
+
error_code: Protocol::H3_EXCESSIVE_LOAD
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
(@frames ||= []) << { type: type, length: payload.bytesize, payload: payload }
|
|
77
|
+
|
|
78
|
+
Protocol.reject_unexpected_request_frame!(type)
|
|
79
|
+
|
|
80
|
+
case type
|
|
81
|
+
when 0x01 # HEADERS
|
|
82
|
+
if @max_header_size && payload.bytesize > @max_header_size
|
|
83
|
+
raise Protocol::MessageError, "Header block #{payload.bytesize} exceeds limit #{@max_header_size}"
|
|
84
|
+
end
|
|
85
|
+
if trailers_received
|
|
86
|
+
raise Protocol::FrameError, "HEADERS frame after trailers"
|
|
87
|
+
elsif headers_received
|
|
88
|
+
parse_trailers(payload)
|
|
89
|
+
trailers_received = true
|
|
90
|
+
else
|
|
91
|
+
parse_headers(payload)
|
|
92
|
+
headers_received = true
|
|
93
|
+
end
|
|
94
|
+
when 0x00 # DATA
|
|
95
|
+
raise Protocol::FrameError, "DATA frame before HEADERS" unless headers_received
|
|
96
|
+
raise Protocol::FrameError, "DATA frame after trailers" if trailers_received
|
|
97
|
+
unless @body
|
|
98
|
+
@body = StringIO.new
|
|
99
|
+
@body.set_encoding(Encoding::ASCII_8BIT)
|
|
100
|
+
end
|
|
101
|
+
@body.write(payload)
|
|
102
|
+
if @max_body_size && @body.size > @max_body_size
|
|
103
|
+
raise Protocol::MessageError, "Body size #{@body.size} exceeds limit #{@max_body_size}"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
@body&.rewind
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# RFC 9110 §5.3: Combine duplicate header values.
|
|
112
|
+
# - set-cookie: join with "\n" (Rack convention, MUST NOT combine with comma)
|
|
113
|
+
# - cookie: join with "; " (RFC 9114 §4.2.1)
|
|
114
|
+
# - all others: join with ", "
|
|
115
|
+
def store_header(name, value)
|
|
116
|
+
if @headers.key?(name)
|
|
117
|
+
separator = case name
|
|
118
|
+
when "set-cookie" then "\n"
|
|
119
|
+
when "cookie" then "; "
|
|
120
|
+
else ", "
|
|
121
|
+
end
|
|
122
|
+
@headers[name] = "#{@headers[name]}#{separator}#{value}"
|
|
123
|
+
else
|
|
124
|
+
if @max_header_count && @headers.size >= @max_header_count
|
|
125
|
+
raise Protocol::MessageError, "Header count exceeds limit #{@max_header_count}"
|
|
126
|
+
end
|
|
127
|
+
@headers[name] = value
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def parse_trailers(payload)
|
|
132
|
+
@decoder.decode(payload) do |name, value|
|
|
133
|
+
if name.start_with?(":")
|
|
134
|
+
raise Protocol::MessageError, "Pseudo-header '#{name}' in trailers"
|
|
135
|
+
end
|
|
136
|
+
@trailers[name] = value
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quicsilver
|
|
4
|
+
module Protocol
|
|
5
|
+
# Extracts HTTP/3 frames from a byte buffer.
|
|
6
|
+
# Yields (type, payload, offset) per frame. Returns total bytes consumed.
|
|
7
|
+
# Existing callers using |type, payload| are unaffected — offset is ignored.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# FrameReader.each(buffer) do |type, payload|
|
|
11
|
+
# case type
|
|
12
|
+
# when FRAME_HEADERS then ...
|
|
13
|
+
# when FRAME_DATA then ...
|
|
14
|
+
# end
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# With offset (for early exit via throw/catch):
|
|
18
|
+
# strip = FrameReader.skip_while(data) { |type, payload| ... }
|
|
19
|
+
# data = data.byteslice(strip..) if strip > 0
|
|
20
|
+
module FrameReader
|
|
21
|
+
def self.each(buffer)
|
|
22
|
+
offset = 0
|
|
23
|
+
buf_size = buffer.bytesize
|
|
24
|
+
|
|
25
|
+
while offset < buf_size
|
|
26
|
+
break if buf_size - offset < 2
|
|
27
|
+
|
|
28
|
+
# Inline single-byte varint fast path (covers frame types 0x00-0x3F)
|
|
29
|
+
type_byte = buffer.getbyte(offset)
|
|
30
|
+
if type_byte < 0x40
|
|
31
|
+
type = type_byte
|
|
32
|
+
type_len = 1
|
|
33
|
+
else
|
|
34
|
+
type, type_len = Protocol.decode_varint_str(buffer, offset)
|
|
35
|
+
break if type_len == 0
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
len_byte = buffer.getbyte(offset + type_len)
|
|
39
|
+
break unless len_byte
|
|
40
|
+
if len_byte < 0x40
|
|
41
|
+
length = len_byte
|
|
42
|
+
length_len = 1
|
|
43
|
+
else
|
|
44
|
+
length, length_len = Protocol.decode_varint_str(buffer, offset + type_len)
|
|
45
|
+
break if length_len == 0
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
header_len = type_len + length_len
|
|
49
|
+
break if buf_size < offset + header_len + length
|
|
50
|
+
|
|
51
|
+
payload = buffer.byteslice(offset + header_len, length)
|
|
52
|
+
offset += header_len + length
|
|
53
|
+
|
|
54
|
+
yield type, payload, offset
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
offset
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Skip leading frames while the block returns true.
|
|
61
|
+
# Returns the byte offset of the first frame that returned false (or 0).
|
|
62
|
+
#
|
|
63
|
+
# skip = FrameReader.skip_while(data) do |type, payload|
|
|
64
|
+
# type == FRAME_HEADERS && informational?(payload)
|
|
65
|
+
# end
|
|
66
|
+
# data = data.byteslice(skip..) if skip > 0
|
|
67
|
+
def self.skip_while(buffer)
|
|
68
|
+
skip_to = 0
|
|
69
|
+
|
|
70
|
+
catch(:stop) do
|
|
71
|
+
each(buffer) do |type, payload, offset|
|
|
72
|
+
if yield(type, payload)
|
|
73
|
+
skip_to = offset
|
|
74
|
+
else
|
|
75
|
+
throw :stop
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
skip_to
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|