quicsilver 0.3.0 → 0.5.0
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/.github/workflows/ci.yml +9 -1
- data/.github/workflows/cibuildgem.yaml +117 -0
- data/.gitignore +8 -1
- data/CHANGELOG.md +82 -0
- data/Gemfile.lock +27 -2
- data/README.md +205 -29
- data/Rakefile +75 -3
- data/benchmarks/baseline.rb +175 -0
- data/benchmarks/compare.rb +398 -0
- data/benchmarks/helpers.rb +138 -64
- data/benchmarks/latency.rb +128 -0
- data/benchmarks/loss.rb +193 -0
- data/benchmarks/mobile.rb +125 -0
- data/docs/connection_identity.md +141 -0
- data/examples/README.md +44 -91
- 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 +6 -18
- data/examples/priorities.rb +60 -0
- data/examples/protocol_http_server.rb +31 -0
- data/examples/rack_http3_server.rb +8 -20
- data/examples/rails_feature_test.rb +280 -0
- data/examples/server_stats.rb +117 -0
- data/examples/simple_client_test.rb +5 -5
- 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 +111 -12
- 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 +1646 -189
- data/lib/quicsilver/client/client.rb +546 -88
- data/lib/quicsilver/client/connection_pool.rb +202 -0
- data/lib/quicsilver/client/request.rb +106 -14
- data/lib/quicsilver/errors.rb +39 -0
- data/lib/quicsilver/libmsquic.2.dylib +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 +137 -10
- data/lib/quicsilver/protocol/priority.rb +65 -0
- data/lib/quicsilver/protocol/qpack/decoder.rb +4 -42
- data/lib/quicsilver/protocol/qpack/encoder.rb +49 -21
- data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +16 -1
- data/lib/quicsilver/protocol/qpack/huffman.rb +4 -4
- data/lib/quicsilver/protocol/request_encoder.rb +6 -3
- data/lib/quicsilver/protocol/request_parser.rb +43 -141
- data/lib/quicsilver/protocol/response_encoder.rb +40 -5
- data/lib/quicsilver/protocol/response_parser.rb +26 -128
- 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/rack_adapter.rb +77 -0
- data/lib/quicsilver/server/receive_queue.rb +106 -0
- data/lib/quicsilver/server/request_handler.rb +111 -48
- data/lib/quicsilver/server/request_registry.rb +18 -5
- 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 +848 -104
- 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 +113 -11
- data/lib/quicsilver/transport/connection.rb +376 -132
- data/lib/quicsilver/transport/connection_stats.rb +53 -0
- data/lib/quicsilver/transport/event_loop.rb +36 -0
- data/lib/quicsilver/transport/inbound_stream.rb +21 -1
- data/lib/quicsilver/transport/stream.rb +18 -3
- data/lib/quicsilver/transport/stream_event.rb +25 -2
- data/lib/quicsilver/transport/stream_id.rb +26 -0
- data/lib/quicsilver/version.rb +1 -1
- data/lib/quicsilver.rb +45 -11
- data/lib/rackup/handler/quicsilver.rb +57 -38
- data/quicsilver.gemspec +17 -2
- metadata +120 -10
- data/benchmarks/concurrent.rb +0 -110
- data/benchmarks/quicsilver_server.rb +0 -46
- data/benchmarks/rails.rb +0 -170
- data/benchmarks/throughput.rb +0 -113
- data/examples/setup_certs.sh +0 -57
|
@@ -3,13 +3,66 @@
|
|
|
3
3
|
module Quicsilver
|
|
4
4
|
module Transport
|
|
5
5
|
class Connection
|
|
6
|
+
include Protocol::ControlStreamParser
|
|
7
|
+
|
|
8
|
+
# MsQuic QUIC_STATUS_INVALID_STATE (POSIX errno ETOOMANYREFS = 0x59).
|
|
9
|
+
# Stream already shut down by peer — raised by StreamSend when the
|
|
10
|
+
# client has reset or closed the stream.
|
|
11
|
+
MSQUIC_INVALID_STATE = "0x59"
|
|
12
|
+
# The peer tore the stream down before we finished responding.
|
|
13
|
+
STREAM_CLOSED = "QUIC stream is closed"
|
|
14
|
+
|
|
6
15
|
attr_reader :handle, :data, :streams
|
|
7
16
|
attr_reader :control_stream_id, :qpack_encoder_stream_id, :qpack_decoder_stream_id
|
|
8
17
|
attr_reader :server_control_stream
|
|
18
|
+
attr_reader :peer_goaway_id, :local_goaway_id
|
|
19
|
+
attr_reader :stream_priorities
|
|
20
|
+
attr_reader :remote_address, :remote_port, :session_resumed
|
|
21
|
+
|
|
22
|
+
def settings_received?
|
|
23
|
+
@settings_received
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def datagram_send_enabled?
|
|
27
|
+
Quicsilver.connection_datagram_send_enabled?(@handle)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def reliable_reset_enabled?
|
|
31
|
+
Quicsilver.connection_reliable_reset_enabled?(@handle)
|
|
32
|
+
end
|
|
9
33
|
|
|
10
|
-
|
|
34
|
+
# WebTransport session flow control is on only when both endpoints have
|
|
35
|
+
# declared intent by advertising a non-zero WT_INITIAL_MAX_* setting
|
|
36
|
+
# (draft-16 §5.1). Until it is, at most one session may share the
|
|
37
|
+
# connection and flow control capsules must be ignored, because the peer
|
|
38
|
+
# may have sent them before seeing our SETTINGS.
|
|
39
|
+
def wt_flow_control_enabled?
|
|
40
|
+
return false unless settings_received?
|
|
41
|
+
|
|
42
|
+
Protocol.wt_flow_control_opt_in?(local_settings) &&
|
|
43
|
+
Protocol.wt_flow_control_opt_in?(@settings)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# What we advertise to the peer. Derived rather than captured when the
|
|
47
|
+
# control stream opens, so there is one source for the promise we make.
|
|
48
|
+
def local_settings
|
|
49
|
+
Protocol.control_stream_settings(max_field_section_size: @max_header_size)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def webtransport_settings_valid?(protocol)
|
|
53
|
+
return false unless settings_received? && @settings[Protocol::SETTINGS_H3_DATAGRAM] == 1
|
|
54
|
+
|
|
55
|
+
# Compatibility path: the legacy token does not require the newer flag.
|
|
56
|
+
protocol == Protocol::WebTransport::PROTOCOL_LEGACY ||
|
|
57
|
+
(@settings[Protocol::SETTINGS_WT_ENABLED] == 1 && reliable_reset_enabled?)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def initialize(handle, data, max_header_size: nil, connection_id: nil, transport_server_id: nil)
|
|
11
61
|
@handle = handle
|
|
12
62
|
@data = data
|
|
63
|
+
@max_header_size = max_header_size
|
|
64
|
+
@connection_id = hex_string(connection_id)
|
|
65
|
+
@transport_server_id = transport_server_id
|
|
13
66
|
@streams = {}
|
|
14
67
|
@response_buffers = {}
|
|
15
68
|
@mutex = Mutex.new
|
|
@@ -24,6 +77,58 @@ module Quicsilver
|
|
|
24
77
|
|
|
25
78
|
@settings = {}
|
|
26
79
|
@settings_received = false
|
|
80
|
+
@peer_goaway_id = nil
|
|
81
|
+
@local_goaway_id = nil
|
|
82
|
+
@stream_priorities = {}
|
|
83
|
+
@session_resumed = @data[2] == true
|
|
84
|
+
@remote_address = nil
|
|
85
|
+
@remote_port = 0
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Resolve peer address from MsQuic. Must be called before the connection closes.
|
|
89
|
+
def resolve_remote_address!
|
|
90
|
+
result = Quicsilver.connection_remote_address(@handle)
|
|
91
|
+
@remote_address = result&.first
|
|
92
|
+
@remote_port = result&.last&.to_i || 0
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Stable opaque connection identifier exposed to apps. The current MsQuic
|
|
96
|
+
# backing value is the original destination CID; consumers must not parse
|
|
97
|
+
# it as the server-generated routing CID.
|
|
98
|
+
attr_reader :connection_id
|
|
99
|
+
|
|
100
|
+
def active_streams
|
|
101
|
+
@streams.size
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# HTTP/3 request streams are client-initiated bidirectional QUIC streams.
|
|
105
|
+
def active_request_streams
|
|
106
|
+
@streams.keys.count { |stream_id| StreamId.request?(stream_id) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def request_context(stream_id: nil)
|
|
110
|
+
connection = connection_metadata
|
|
111
|
+
if stream_id
|
|
112
|
+
connection["stream_id"] = stream_id
|
|
113
|
+
connection["request_id"] = request_id_for(stream_id)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
{
|
|
117
|
+
"connection" => connection
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def to_h
|
|
122
|
+
connection_metadata.merge(
|
|
123
|
+
"remote_address" => @remote_address,
|
|
124
|
+
"remote_port" => @remote_port,
|
|
125
|
+
"session_resumed" => @session_resumed,
|
|
126
|
+
"streams" => {
|
|
127
|
+
"active" => active_streams,
|
|
128
|
+
"active_requests" => active_request_streams
|
|
129
|
+
},
|
|
130
|
+
"transport" => stats.to_h
|
|
131
|
+
)
|
|
27
132
|
end
|
|
28
133
|
|
|
29
134
|
# === Setup (called after connection established) ===
|
|
@@ -31,13 +136,17 @@ module Quicsilver
|
|
|
31
136
|
def setup_http3_streams
|
|
32
137
|
# Control stream (required)
|
|
33
138
|
@server_control_stream = open_stream(unidirectional: true)
|
|
34
|
-
@server_control_stream.send(Protocol.build_control_stream)
|
|
139
|
+
@server_control_stream.send(Protocol.build_control_stream(max_field_section_size: @max_header_size))
|
|
35
140
|
|
|
36
141
|
# QPACK encoder/decoder streams
|
|
37
142
|
[0x02, 0x03].each do |type|
|
|
38
143
|
stream = open_stream(unidirectional: true)
|
|
39
144
|
stream.send([type].pack("C"))
|
|
40
145
|
end
|
|
146
|
+
|
|
147
|
+
# GREASE unidirectional stream (RFC 9114 §6.2)
|
|
148
|
+
stream = open_stream(unidirectional: true)
|
|
149
|
+
stream.send(Protocol.encode_varint(Protocol.grease_id) + "GREASE".b)
|
|
41
150
|
end
|
|
42
151
|
|
|
43
152
|
# === Stream Management ===
|
|
@@ -62,14 +171,14 @@ module Quicsilver
|
|
|
62
171
|
|
|
63
172
|
def buffer_data(stream_id, data)
|
|
64
173
|
@mutex.synchronize do
|
|
65
|
-
(@response_buffers[stream_id] ||=
|
|
174
|
+
(@response_buffers[stream_id] ||= "".b) << data
|
|
66
175
|
end
|
|
67
176
|
end
|
|
68
177
|
|
|
69
178
|
def complete_stream(stream_id, final_data)
|
|
70
179
|
@mutex.synchronize do
|
|
71
180
|
buffer = @response_buffers.delete(stream_id)
|
|
72
|
-
(buffer
|
|
181
|
+
(buffer || "".b) + (final_data || "".b)
|
|
73
182
|
end
|
|
74
183
|
end
|
|
75
184
|
|
|
@@ -78,36 +187,60 @@ module Quicsilver
|
|
|
78
187
|
def send_goaway(stream_id = nil)
|
|
79
188
|
return unless @server_control_stream
|
|
80
189
|
|
|
81
|
-
stream_id ||=
|
|
190
|
+
stream_id ||= last_request_stream_id
|
|
191
|
+
validate_goaway_id!(stream_id)
|
|
192
|
+
|
|
82
193
|
@server_control_stream.send(Protocol.build_goaway_frame(stream_id))
|
|
194
|
+
@local_goaway_id = stream_id
|
|
195
|
+
rescue ArgumentError
|
|
196
|
+
raise # Re-raise validation errors
|
|
83
197
|
rescue => e
|
|
84
198
|
Quicsilver.logger.error("Failed to send GOAWAY: #{e.message}")
|
|
85
199
|
end
|
|
86
200
|
|
|
87
|
-
|
|
88
|
-
|
|
201
|
+
# RFC 9114 §5.2: GOAWAY IDs MUST NOT increase from a previous value.
|
|
202
|
+
def validate_goaway_id!(stream_id)
|
|
203
|
+
if @local_goaway_id && stream_id > @local_goaway_id
|
|
204
|
+
raise ArgumentError, "GOAWAY stream ID #{stream_id} exceeds previous #{@local_goaway_id}"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Send an informational (1xx) response before the final response.
|
|
209
|
+
# RFC 9114 §4.1: encoded as a HEADERS frame, no FIN.
|
|
210
|
+
def send_informational(stream, status, headers)
|
|
211
|
+
data = Protocol::ResponseEncoder.encode_informational(status, headers)
|
|
212
|
+
stream.send(data, fin: false)
|
|
213
|
+
rescue RuntimeError, IOError => e
|
|
214
|
+
raise unless stream_send_error?(e)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def send_response(stream, status, headers, body, head_request: false, trailers: nil)
|
|
218
|
+
body = [] if body.nil?
|
|
219
|
+
encoder = Protocol::ResponseEncoder.new(status, headers, body, head_request: head_request, trailers: trailers)
|
|
89
220
|
|
|
90
221
|
if body.respond_to?(:to_ary)
|
|
91
|
-
|
|
222
|
+
stream.send(encoder.encode, fin: true)
|
|
92
223
|
else
|
|
93
224
|
encoder.stream_encode do |frame_data, fin|
|
|
94
|
-
|
|
225
|
+
stream.send(frame_data, fin: fin) unless frame_data.empty? && !fin
|
|
95
226
|
end
|
|
96
227
|
end
|
|
97
|
-
rescue RuntimeError => e
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
228
|
+
rescue RuntimeError, IOError => e
|
|
229
|
+
raise unless stream_send_error?(e)
|
|
230
|
+
ensure
|
|
231
|
+
# RFC 9110: Always close the body to release resources (file handles, fibers, etc.)
|
|
232
|
+
body.close if body.respond_to?(:close)
|
|
101
233
|
end
|
|
102
234
|
|
|
103
235
|
def send_error(stream, status, message)
|
|
104
236
|
body = ["#{status} #{message}"]
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
237
|
+
headers = { "content-type" => "text/plain" }
|
|
238
|
+
# RFC 9110 §15.6.4: 503 responses SHOULD include Retry-After
|
|
239
|
+
headers["retry-after"] = "1" if status == 503
|
|
240
|
+
encoder = Protocol::ResponseEncoder.new(status, headers, body)
|
|
241
|
+
stream.send(encoder.encode, fin: true)
|
|
242
|
+
rescue RuntimeError, IOError => e
|
|
243
|
+
raise unless stream_send_error?(e)
|
|
111
244
|
end
|
|
112
245
|
|
|
113
246
|
# === Control Stream Handling ===
|
|
@@ -116,61 +249,22 @@ module Quicsilver
|
|
|
116
249
|
# Called on each RECEIVE event — control streams never send FIN.
|
|
117
250
|
def receive_unidirectional_data(stream_id, data)
|
|
118
251
|
@mutex.synchronize do
|
|
119
|
-
(@response_buffers[stream_id] ||=
|
|
252
|
+
(@response_buffers[stream_id] ||= "".b) << data
|
|
120
253
|
end
|
|
121
254
|
|
|
122
|
-
buf = @mutex.synchronize { @response_buffers[stream_id]
|
|
255
|
+
buf = @mutex.synchronize { @response_buffers[stream_id] || "".b }
|
|
123
256
|
return if buf.empty?
|
|
124
257
|
|
|
125
|
-
# First time seeing this stream: identify stream type
|
|
126
258
|
unless @uni_stream_types&.key?(stream_id)
|
|
127
259
|
@uni_stream_types ||= {}
|
|
128
|
-
stream_type,
|
|
129
|
-
return
|
|
130
|
-
|
|
131
|
-
case stream_type
|
|
132
|
-
when 0x00 # Control stream
|
|
133
|
-
raise Protocol::FrameError, "Duplicate control stream" if @control_stream_id
|
|
134
|
-
@control_stream_id = stream_id
|
|
135
|
-
@uni_stream_types[stream_id] = :control
|
|
136
|
-
# Remove the stream type byte from the buffer
|
|
137
|
-
@mutex.synchronize { @response_buffers[stream_id] = StringIO.new(buf[type_len..] || "".b) }
|
|
138
|
-
when 0x01
|
|
139
|
-
raise Protocol::FrameError, "Client must not send push streams"
|
|
140
|
-
when 0x02 # QPACK encoder stream
|
|
141
|
-
raise Protocol::FrameError, "Duplicate QPACK encoder stream" if @qpack_encoder_stream_id
|
|
142
|
-
@qpack_encoder_stream_id = stream_id
|
|
143
|
-
@uni_stream_types[stream_id] = :qpack_encoder
|
|
144
|
-
@mutex.synchronize { @response_buffers[stream_id] = StringIO.new(buf[type_len..] || "".b) }
|
|
145
|
-
when 0x03 # QPACK decoder stream
|
|
146
|
-
raise Protocol::FrameError, "Duplicate QPACK decoder stream" if @qpack_decoder_stream_id
|
|
147
|
-
@qpack_decoder_stream_id = stream_id
|
|
148
|
-
@uni_stream_types[stream_id] = :qpack_decoder
|
|
149
|
-
@mutex.synchronize { @response_buffers[stream_id] = StringIO.new(buf[type_len..] || "".b) }
|
|
150
|
-
else
|
|
151
|
-
# Unknown unidirectional stream types MUST be ignored (RFC 9114 §6.2)
|
|
152
|
-
@uni_stream_types[stream_id] = :unknown
|
|
153
|
-
return
|
|
154
|
-
end
|
|
260
|
+
stream_type, payload = decode_unidirectional_stream(buf)
|
|
261
|
+
return unless stream_type
|
|
155
262
|
|
|
156
|
-
|
|
263
|
+
register_unidirectional_stream(stream_id, stream_type, payload)
|
|
264
|
+
buf = @mutex.synchronize { @response_buffers[stream_id] || "".b }
|
|
157
265
|
end
|
|
158
266
|
|
|
159
|
-
|
|
160
|
-
return if buf.empty?
|
|
161
|
-
|
|
162
|
-
case stream_type
|
|
163
|
-
when :control
|
|
164
|
-
parse_control_frames(buf)
|
|
165
|
-
# Clear parsed data from buffer
|
|
166
|
-
@mutex.synchronize { @response_buffers[stream_id] = StringIO.new("".b) }
|
|
167
|
-
when :qpack_encoder
|
|
168
|
-
validate_qpack_encoder_data(buf)
|
|
169
|
-
@mutex.synchronize { @response_buffers[stream_id] = StringIO.new("".b) }
|
|
170
|
-
when :qpack_decoder
|
|
171
|
-
validate_qpack_decoder_data(buf)
|
|
172
|
-
@mutex.synchronize { @response_buffers[stream_id] = StringIO.new("".b) }
|
|
173
|
-
end
|
|
267
|
+
process_unidirectional_payload(stream_id, buf)
|
|
174
268
|
end
|
|
175
269
|
|
|
176
270
|
def handle_unidirectional_stream(stream, fin: true)
|
|
@@ -184,35 +278,120 @@ module Quicsilver
|
|
|
184
278
|
data = stream.data
|
|
185
279
|
return if data.empty?
|
|
186
280
|
|
|
187
|
-
stream_type,
|
|
188
|
-
return
|
|
189
|
-
payload = data[type_len..-1]
|
|
281
|
+
stream_type, payload = decode_unidirectional_stream(data)
|
|
282
|
+
return unless stream_type
|
|
190
283
|
|
|
191
284
|
case stream_type
|
|
192
|
-
when
|
|
285
|
+
when Protocol::UnidirectionalStream::CONTROL
|
|
193
286
|
set_control_stream(stream_id, payload)
|
|
194
287
|
if fin
|
|
195
288
|
raise Protocol::FrameError.new("Closure of critical stream", error_code: Protocol::H3_CLOSED_CRITICAL_STREAM)
|
|
196
289
|
end
|
|
197
|
-
when
|
|
198
|
-
raise Protocol::FrameError
|
|
199
|
-
|
|
200
|
-
|
|
290
|
+
when Protocol::UnidirectionalStream::PUSH
|
|
291
|
+
raise Protocol::FrameError.new("Client must not send push streams",
|
|
292
|
+
error_code: Protocol::H3_STREAM_CREATION_ERROR)
|
|
293
|
+
when Protocol::UnidirectionalStream::QPACK_ENCODER
|
|
294
|
+
raise Protocol::FrameError.new("Duplicate QPACK encoder stream",
|
|
295
|
+
error_code: Protocol::H3_STREAM_CREATION_ERROR) if @qpack_encoder_stream_id
|
|
201
296
|
@qpack_encoder_stream_id = stream_id
|
|
202
297
|
if fin
|
|
203
298
|
raise Protocol::FrameError.new("Closure of critical stream", error_code: Protocol::H3_CLOSED_CRITICAL_STREAM)
|
|
204
299
|
end
|
|
205
|
-
when
|
|
206
|
-
raise Protocol::FrameError
|
|
300
|
+
when Protocol::UnidirectionalStream::QPACK_DECODER
|
|
301
|
+
raise Protocol::FrameError.new("Duplicate QPACK decoder stream",
|
|
302
|
+
error_code: Protocol::H3_STREAM_CREATION_ERROR) if @qpack_decoder_stream_id
|
|
207
303
|
@qpack_decoder_stream_id = stream_id
|
|
208
304
|
if fin
|
|
209
305
|
raise Protocol::FrameError.new("Closure of critical stream", error_code: Protocol::H3_CLOSED_CRITICAL_STREAM)
|
|
210
306
|
end
|
|
307
|
+
when Protocol::WebTransport::UNI_STREAM_TYPE
|
|
308
|
+
@uni_stream_types ||= {}
|
|
309
|
+
@uni_stream_types[stream_id] = :webtransport_uni
|
|
310
|
+
[:webtransport_uni, payload || "".b]
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def decode_unidirectional_stream(data)
|
|
315
|
+
stream_type, type_len = Protocol.decode_varint_str(data, 0)
|
|
316
|
+
return if type_len == 0
|
|
317
|
+
|
|
318
|
+
[stream_type, data[type_len..] || "".b]
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def register_unidirectional_stream(stream_id, stream_type, payload)
|
|
322
|
+
case stream_type
|
|
323
|
+
when Protocol::UnidirectionalStream::CONTROL
|
|
324
|
+
raise Protocol::FrameError.new("Duplicate control stream",
|
|
325
|
+
error_code: Protocol::H3_STREAM_CREATION_ERROR) if @control_stream_id
|
|
326
|
+
@control_stream_id = stream_id
|
|
327
|
+
@uni_stream_types[stream_id] = :control
|
|
328
|
+
@mutex.synchronize { @response_buffers[stream_id] = payload }
|
|
329
|
+
when Protocol::UnidirectionalStream::PUSH
|
|
330
|
+
raise Protocol::FrameError.new("Client must not send push streams",
|
|
331
|
+
error_code: Protocol::H3_STREAM_CREATION_ERROR)
|
|
332
|
+
when Protocol::UnidirectionalStream::QPACK_ENCODER
|
|
333
|
+
raise Protocol::FrameError.new("Duplicate QPACK encoder stream",
|
|
334
|
+
error_code: Protocol::H3_STREAM_CREATION_ERROR) if @qpack_encoder_stream_id
|
|
335
|
+
@qpack_encoder_stream_id = stream_id
|
|
336
|
+
@uni_stream_types[stream_id] = :qpack_encoder
|
|
337
|
+
@mutex.synchronize { @response_buffers[stream_id] = payload }
|
|
338
|
+
when Protocol::UnidirectionalStream::QPACK_DECODER
|
|
339
|
+
raise Protocol::FrameError.new("Duplicate QPACK decoder stream",
|
|
340
|
+
error_code: Protocol::H3_STREAM_CREATION_ERROR) if @qpack_decoder_stream_id
|
|
341
|
+
@qpack_decoder_stream_id = stream_id
|
|
342
|
+
@uni_stream_types[stream_id] = :qpack_decoder
|
|
343
|
+
@mutex.synchronize { @response_buffers[stream_id] = payload }
|
|
344
|
+
when Protocol::WebTransport::UNI_STREAM_TYPE
|
|
345
|
+
@uni_stream_types[stream_id] = :webtransport_uni
|
|
346
|
+
@mutex.synchronize { @response_buffers[stream_id] = payload }
|
|
347
|
+
else
|
|
348
|
+
@uni_stream_types[stream_id] = :unknown
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def process_unidirectional_payload(stream_id, payload)
|
|
353
|
+
return if payload.empty?
|
|
354
|
+
|
|
355
|
+
case @uni_stream_types[stream_id]
|
|
356
|
+
when :webtransport_uni
|
|
357
|
+
@mutex.synchronize { @response_buffers[stream_id] = "".b }
|
|
358
|
+
[:webtransport_uni, payload]
|
|
359
|
+
when :control
|
|
360
|
+
# Keep whatever did not form a whole frame. Clearing the buffer
|
|
361
|
+
# discarded the front of a frame that arrived split across two
|
|
362
|
+
# receives; its tail then parsed as a new frame, which fails the
|
|
363
|
+
# connection. Control streams never send FIN, so nothing recovers
|
|
364
|
+
# it later.
|
|
365
|
+
consume_unidirectional(stream_id, payload, parse_control_frames(payload))
|
|
366
|
+
when :qpack_encoder
|
|
367
|
+
# Unlike :control this validates the leading instruction byte rather
|
|
368
|
+
# than reassembling frames, so clearing the buffer loses nothing.
|
|
369
|
+
# An instruction split across receives could still be misread; not
|
|
370
|
+
# observed, and it would need incremental QPACK decoding to fix.
|
|
371
|
+
validate_qpack_encoder_data(payload)
|
|
372
|
+
@mutex.synchronize { @response_buffers[stream_id] = "".b }
|
|
373
|
+
when :qpack_decoder
|
|
374
|
+
validate_qpack_decoder_data(payload)
|
|
375
|
+
@mutex.synchronize { @response_buffers[stream_id] = "".b }
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Drop the bytes a parser consumed, keeping any partial frame. The buffer
|
|
380
|
+
# may have grown while we parsed, so only what we read is removed.
|
|
381
|
+
def consume_unidirectional(stream_id, payload, consumed)
|
|
382
|
+
@mutex.synchronize do
|
|
383
|
+
buffer = @response_buffers[stream_id]
|
|
384
|
+
next if buffer.nil?
|
|
385
|
+
|
|
386
|
+
remainder = (consumed >= payload.bytesize) ? "".b : payload.byteslice(consumed..)
|
|
387
|
+
@response_buffers[stream_id] =
|
|
388
|
+
(buffer.bytesize > payload.bytesize) ? remainder + buffer.byteslice(payload.bytesize..) : remainder
|
|
211
389
|
end
|
|
212
390
|
end
|
|
213
391
|
|
|
214
392
|
def set_control_stream(stream_id, payload = nil)
|
|
215
|
-
raise Protocol::FrameError
|
|
393
|
+
raise Protocol::FrameError.new("Duplicate control stream",
|
|
394
|
+
error_code: Protocol::H3_STREAM_CREATION_ERROR) if @control_stream_id
|
|
216
395
|
@control_stream_id = stream_id
|
|
217
396
|
parse_control_frames(payload) if payload && !payload.empty?
|
|
218
397
|
end
|
|
@@ -221,6 +400,29 @@ module Quicsilver
|
|
|
221
400
|
@settings
|
|
222
401
|
end
|
|
223
402
|
|
|
403
|
+
# Get the priority for a stream. Returns default Priority if not set.
|
|
404
|
+
def stream_priority(stream_id)
|
|
405
|
+
@stream_priorities[stream_id] || Protocol::Priority.new
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Apply priority to a QUIC stream via MsQuic.
|
|
409
|
+
# MsQuic: 0 = lowest, 0xFFFF = highest.
|
|
410
|
+
# HTTP urgency: 0 = highest, 7 = lowest.
|
|
411
|
+
# Maps urgency into evenly spaced bands across the uint16 range.
|
|
412
|
+
# The priority is queued and applied on the MsQuic event thread.
|
|
413
|
+
def apply_stream_priority(stream, priority)
|
|
414
|
+
handle = stream.respond_to?(:stream_handle) ? stream.stream_handle : nil
|
|
415
|
+
return unless handle
|
|
416
|
+
quic_priority = (7 - priority.urgency) * 0x2000
|
|
417
|
+
Quicsilver.set_stream_priority(handle, quic_priority)
|
|
418
|
+
rescue => e
|
|
419
|
+
Quicsilver.logger.debug("Failed to set stream priority: #{e.message}")
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def uni_stream_type(stream_id)
|
|
423
|
+
@uni_stream_types&.dig(stream_id)
|
|
424
|
+
end
|
|
425
|
+
|
|
224
426
|
def critical_stream?(stream_id)
|
|
225
427
|
stream_id == @control_stream_id ||
|
|
226
428
|
stream_id == @qpack_encoder_stream_id ||
|
|
@@ -234,83 +436,105 @@ module Quicsilver
|
|
|
234
436
|
Quicsilver.connection_shutdown(@handle, error_code, false)
|
|
235
437
|
end
|
|
236
438
|
|
|
237
|
-
|
|
439
|
+
# Returns QUIC transport statistics for this connection.
|
|
440
|
+
def stats
|
|
441
|
+
ConnectionStats.from_hash(Quicsilver.connection_statistics(@handle))
|
|
442
|
+
end
|
|
238
443
|
|
|
239
444
|
def open_stream(unidirectional: false)
|
|
240
445
|
handle = Quicsilver.open_stream(@data, unidirectional)
|
|
241
446
|
Stream.new(handle)
|
|
242
447
|
end
|
|
243
448
|
|
|
244
|
-
|
|
245
|
-
|
|
449
|
+
private
|
|
450
|
+
|
|
451
|
+
def hex_string(value)
|
|
452
|
+
value.unpack1("H*") if value
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def connection_metadata
|
|
456
|
+
metadata = {}
|
|
457
|
+
metadata["connection_id"] = connection_id if connection_id
|
|
458
|
+
metadata
|
|
246
459
|
end
|
|
247
460
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
HTTP2_SETTINGS = [0x00, 0x02, 0x03, 0x04, 0x05].freeze
|
|
461
|
+
def request_id_for(stream_id)
|
|
462
|
+
[@transport_server_id, connection_id, stream_id].compact.join(":")
|
|
463
|
+
end
|
|
252
464
|
|
|
253
|
-
#
|
|
465
|
+
# Stream may have been reset by client — expected during normal operation.
|
|
466
|
+
def stream_send_error?(error)
|
|
467
|
+
unless error.message.include?(MSQUIC_INVALID_STATE) ||
|
|
468
|
+
error.message.include?("StreamSend failed") ||
|
|
469
|
+
error.message.include?(STREAM_CLOSED)
|
|
470
|
+
return false
|
|
471
|
+
end
|
|
472
|
+
Quicsilver.logger.debug("Stream send failed (client likely reset): #{error.message}")
|
|
473
|
+
true
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def last_request_stream_id
|
|
477
|
+
@streams.keys.select { |stream_id| StreamId.request?(stream_id) }.max || 0
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# Frame types forbidden on the control stream (RFC 9114 §7.2.4)
|
|
481
|
+
# Note: CANCEL_PUSH (0x03) and MAX_PUSH_ID (0x0d) ARE allowed on control stream.
|
|
254
482
|
FORBIDDEN_ON_CONTROL = [
|
|
255
483
|
0x00, # DATA — request streams only
|
|
256
484
|
0x01, # HEADERS — request streams only
|
|
257
|
-
0x02, # HTTP/2 PRIORITY (reserved)
|
|
485
|
+
0x02, # HTTP/2 PRIORITY (reserved, §7.2.8)
|
|
258
486
|
0x05, # PUSH_PROMISE — request streams only
|
|
259
|
-
0x06, # HTTP/2 PING (reserved)
|
|
260
|
-
0x08, # HTTP/2 WINDOW_UPDATE (reserved)
|
|
261
|
-
0x09, # HTTP/2 CONTINUATION (reserved)
|
|
487
|
+
0x06, # HTTP/2 PING (reserved, §7.2.8)
|
|
488
|
+
0x08, # HTTP/2 WINDOW_UPDATE (reserved, §7.2.8)
|
|
489
|
+
0x09, # HTTP/2 CONTINUATION (reserved, §7.2.8)
|
|
262
490
|
].freeze
|
|
263
491
|
|
|
264
|
-
def
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
while offset < data.bytesize
|
|
269
|
-
frame_type, type_len = Protocol.decode_varint(data.bytes, offset)
|
|
270
|
-
frame_length, length_len = Protocol.decode_varint(data.bytes, offset + type_len)
|
|
271
|
-
break if type_len == 0 || length_len == 0
|
|
272
|
-
|
|
273
|
-
if first_frame && frame_type != Protocol::FRAME_SETTINGS
|
|
274
|
-
raise Protocol::FrameError.new("First frame on control stream must be SETTINGS", error_code: Protocol::H3_MISSING_SETTINGS)
|
|
275
|
-
end
|
|
276
|
-
first_frame = false
|
|
492
|
+
def on_settings_received(settings)
|
|
493
|
+
@settings.merge!(settings)
|
|
494
|
+
end
|
|
277
495
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
496
|
+
def handle_control_frame(type, payload)
|
|
497
|
+
# WT_STREAM belongs at the front of a request stream and nowhere else;
|
|
498
|
+
# on the control stream it is H3_FRAME_ERROR, not H3_FRAME_UNEXPECTED
|
|
499
|
+
# (draft-ietf-webtrans-http3-16 §4.3).
|
|
500
|
+
if type == Protocol::FRAME_WT_STREAM
|
|
501
|
+
raise Protocol::FrameError.new(
|
|
502
|
+
"WT_STREAM is not allowed on the control stream",
|
|
503
|
+
error_code: Protocol::H3_FRAME_ERROR
|
|
504
|
+
)
|
|
505
|
+
end
|
|
281
506
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
507
|
+
if FORBIDDEN_ON_CONTROL.include?(type)
|
|
508
|
+
raise Protocol::FrameError.new(
|
|
509
|
+
"Frame type 0x#{type.to_s(16)} not allowed on control stream",
|
|
510
|
+
error_code: Protocol::H3_FRAME_UNEXPECTED
|
|
511
|
+
)
|
|
512
|
+
end
|
|
287
513
|
|
|
288
|
-
|
|
514
|
+
case type
|
|
515
|
+
when Protocol::FRAME_PRIORITY_UPDATE
|
|
516
|
+
parse_priority_update(payload)
|
|
517
|
+
when Protocol::FRAME_CANCEL_PUSH, Protocol::FRAME_MAX_PUSH_ID
|
|
518
|
+
# No-op: we don't implement server push (RFC 9114 §7.2.3, §7.2.7).
|
|
519
|
+
# These are valid on the control stream — accept and ignore.
|
|
289
520
|
end
|
|
290
521
|
end
|
|
291
522
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
break if id_len == 0 || value_len == 0
|
|
299
|
-
|
|
300
|
-
if HTTP2_SETTINGS.include?(id)
|
|
301
|
-
raise Protocol::FrameError.new("HTTP/2 setting identifier 0x#{id.to_s(16)} not allowed in HTTP/3", error_code: Protocol::H3_SETTINGS_ERROR)
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
raise Protocol::FrameError, "Duplicate setting identifier 0x#{id.to_s(16)}" if seen.include?(id)
|
|
305
|
-
seen.add(id)
|
|
306
|
-
|
|
307
|
-
@settings[id] = value
|
|
308
|
-
offset += id_len + value_len
|
|
309
|
-
end
|
|
523
|
+
# RFC 9218 §7: Parse PRIORITY_UPDATE frame.
|
|
524
|
+
# Payload is a stream ID varint followed by a Priority Field Value string.
|
|
525
|
+
def parse_priority_update(payload)
|
|
526
|
+
stream_id, consumed = Protocol.decode_varint(payload.bytes, 0)
|
|
527
|
+
priority_value = payload[consumed..]
|
|
528
|
+
@stream_priorities[stream_id] = Protocol::Priority.parse(priority_value)
|
|
310
529
|
end
|
|
530
|
+
|
|
311
531
|
# RFC 9204 §4.1.3: Validate QPACK encoder stream instructions.
|
|
312
|
-
# We advertise QPACK_MAX_TABLE_CAPACITY = 0, so
|
|
313
|
-
#
|
|
532
|
+
# We advertise QPACK_MAX_TABLE_CAPACITY = 0, so:
|
|
533
|
+
# - Set Dynamic Table Capacity to 0 is valid (RFC 9204 §3.2.2)
|
|
534
|
+
# - Set Dynamic Table Capacity > 0 is an error
|
|
535
|
+
# - Insert With Name Reference (1xxxxxxx) is an error (no table to insert into)
|
|
536
|
+
# - Insert With Literal Name (01xxxxxx) is an error (capacity is 0)
|
|
537
|
+
# - Duplicate (000xxxxx) is an error (table is empty)
|
|
314
538
|
def validate_qpack_encoder_data(data)
|
|
315
539
|
return if data.empty?
|
|
316
540
|
byte = data.bytes[0]
|
|
@@ -319,9 +543,29 @@ module Quicsilver
|
|
|
319
543
|
if (byte & 0xE0) == 0x20
|
|
320
544
|
capacity, _ = Protocol.decode_varint(data.bytes, 0)
|
|
321
545
|
capacity &= 0x1F # mask off the instruction prefix
|
|
322
|
-
#
|
|
546
|
+
# Setting to 0 is valid; non-zero exceeds our advertised maximum.
|
|
547
|
+
if capacity > 0
|
|
548
|
+
raise Protocol::FrameError.new(
|
|
549
|
+
"Dynamic table capacity exceeds advertised maximum",
|
|
550
|
+
error_code: Protocol::QPACK_ENCODER_STREAM_ERROR
|
|
551
|
+
)
|
|
552
|
+
end
|
|
553
|
+
elsif (byte & 0x80) == 0x80
|
|
554
|
+
# Insert With Name Reference — cannot insert when capacity=0
|
|
555
|
+
raise Protocol::FrameError.new(
|
|
556
|
+
"Insert instruction received but dynamic table capacity is 0",
|
|
557
|
+
error_code: Protocol::QPACK_ENCODER_STREAM_ERROR
|
|
558
|
+
)
|
|
559
|
+
elsif (byte & 0xC0) == 0x40
|
|
560
|
+
# Insert With Literal Name — cannot insert when capacity=0
|
|
561
|
+
raise Protocol::FrameError.new(
|
|
562
|
+
"Insert instruction received but dynamic table capacity is 0",
|
|
563
|
+
error_code: Protocol::QPACK_ENCODER_STREAM_ERROR
|
|
564
|
+
)
|
|
565
|
+
elsif (byte & 0xE0) == 0x00
|
|
566
|
+
# Duplicate — table is empty, nothing to duplicate
|
|
323
567
|
raise Protocol::FrameError.new(
|
|
324
|
-
"
|
|
568
|
+
"Duplicate instruction received but dynamic table is empty",
|
|
325
569
|
error_code: Protocol::QPACK_ENCODER_STREAM_ERROR
|
|
326
570
|
)
|
|
327
571
|
end
|