quicsilver 0.4.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 +29 -5
- data/.gitignore +5 -0
- data/CHANGELOG.md +50 -0
- data/Gemfile.lock +8 -1
- data/README.md +128 -15
- data/Rakefile +10 -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/benchmark.rb +80 -0
- data/examples/connection_pool_demo.rb +1 -1
- data/examples/example_helper.rb +51 -0
- data/examples/feature_demo.rb +7 -7
- data/examples/grpc_style.rb +2 -2
- data/examples/latency_aware_routing.rb +382 -0
- data/examples/lifecycle_drain.rb +264 -0
- data/examples/priorities.rb +2 -2
- data/examples/rails_feature_test.rb +92 -72
- data/examples/server_stats.rb +117 -0
- data/examples/simple_client_test.rb +3 -3
- data/examples/trailers.rb +23 -23
- data/examples/webtransport_smoke.rb +546 -0
- data/examples/worker_load_balancing.rb +425 -0
- data/ext/quicsilver/extconf.rb +100 -15
- 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 +1640 -222
- data/lib/quicsilver/client/client.rb +419 -60
- data/lib/quicsilver/client/connection_pool.rb +122 -26
- 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 +41 -21
- 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 +12 -1
- data/lib/quicsilver/protocol/datagram.rb +40 -0
- data/lib/quicsilver/protocol/frame_parser.rb +5 -6
- data/lib/quicsilver/protocol/frame_reader.rb +31 -2
- data/lib/quicsilver/protocol/frames.rb +128 -12
- data/lib/quicsilver/protocol/priority.rb +9 -0
- data/lib/quicsilver/protocol/qpack/decoder.rb +4 -42
- data/lib/quicsilver/protocol/qpack/encoder.rb +10 -20
- 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 +16 -2
- data/lib/quicsilver/protocol/response_encoder.rb +18 -8
- data/lib/quicsilver/protocol/response_parser.rb +6 -0
- data/lib/quicsilver/protocol/stream_input.rb +3 -3
- 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 +24 -13
- 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 +655 -185
- 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 +103 -10
- data/lib/quicsilver/transport/connection.rb +302 -87
- 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 +22 -11
- data/lib/rackup/handler/quicsilver.rb +57 -38
- data/quicsilver.gemspec +10 -3
- metadata +54 -8
- 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/lib/quicsilver/quicsilver.bundle +0 -0
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "scheduler"
|
|
4
|
+
require_relative "schedulers/thread_scheduler"
|
|
5
|
+
require_relative "web_transport_registry"
|
|
6
|
+
require_relative "web_transport_session"
|
|
7
|
+
require_relative "web_transport_stream"
|
|
8
|
+
|
|
3
9
|
module Quicsilver
|
|
4
10
|
class Server
|
|
5
|
-
attr_reader :address, :port, :server_configuration, :running, :connections, :request_registry, :shutting_down, :max_queue_size, :max_connections
|
|
11
|
+
attr_reader :address, :port, :server_configuration, :running, :connections, :request_registry, :shutting_down, :max_queue_size, :max_connections, :scheduler
|
|
6
12
|
|
|
13
|
+
DEFAULT_THREAD_POOL_SIZE = 5
|
|
14
|
+
DEFAULT_QUEUE_MULTIPLIER = 4
|
|
15
|
+
DEFAULT_MAX_CONNECTIONS = 100
|
|
7
16
|
STREAM_EVENT_RECEIVE = "RECEIVE"
|
|
8
17
|
STREAM_EVENT_RECEIVE_FIN = "RECEIVE_FIN"
|
|
9
18
|
STREAM_EVENT_CONNECTION_ESTABLISHED = "CONNECTION_ESTABLISHED"
|
|
@@ -11,9 +20,12 @@ module Quicsilver
|
|
|
11
20
|
STREAM_EVENT_CONNECTION_CLOSED = "CONNECTION_CLOSED"
|
|
12
21
|
STREAM_EVENT_STREAM_RESET = "STREAM_RESET"
|
|
13
22
|
STREAM_EVENT_STOP_SENDING = "STOP_SENDING"
|
|
23
|
+
STREAM_EVENT_START_COMPLETE = "STREAM_START_COMPLETE"
|
|
24
|
+
STREAM_EVENT_PEER_ACCEPTED = "STREAM_PEER_ACCEPTED"
|
|
25
|
+
STREAM_EVENT_SHUTDOWN_COMPLETE = "STREAM_SHUTDOWN_COMPLETE"
|
|
14
26
|
|
|
15
|
-
ServerStopError = Class.new(
|
|
16
|
-
DrainTimeoutError = Class.new(
|
|
27
|
+
ServerStopError = Class.new(Quicsilver::Error)
|
|
28
|
+
DrainTimeoutError = Class.new(Quicsilver::Error)
|
|
17
29
|
|
|
18
30
|
# Tracks an in-flight streaming request between RECEIVE and RECEIVE_FIN.
|
|
19
31
|
# The stream handle arrives at RECEIVE_FIN; the worker thread waits for it.
|
|
@@ -46,11 +58,23 @@ module Quicsilver
|
|
|
46
58
|
end
|
|
47
59
|
end
|
|
48
60
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
# Default bind address is 0.0.0.0 (IPv4).
|
|
62
|
+
#
|
|
63
|
+
# ⚠️ IPv4 vs IPv6 matters for QUIC!
|
|
64
|
+
# MsQuic creates either an AF_INET or AF_INET6 socket based on the address.
|
|
65
|
+
# On macOS, MsQuic sets IPV6_V6ONLY=1 by default (no dual-stack), so:
|
|
66
|
+
# - address: "::" → IPv6 only (drops IPv4 UDP packets silently)
|
|
67
|
+
# - address: "0.0.0.0" → IPv4 only (most /etc/hosts map to 127.0.0.1)
|
|
68
|
+
#
|
|
69
|
+
# For HTTP/3 Alt-Svc upgrades to work, the browser must be able to reach
|
|
70
|
+
# the QUIC server at the same IP family it used for the TCP connection.
|
|
71
|
+
# Since most local setups map hostnames to 127.0.0.1 in /etc/hosts,
|
|
72
|
+
# defaulting to 0.0.0.0 (IPv4) is the safest choice.
|
|
73
|
+
#
|
|
74
|
+
# If you need IPv6, either:
|
|
75
|
+
# 1. Add "::1 your-hostname" to /etc/hosts, OR
|
|
76
|
+
# 2. Run two server instances (one IPv4, one IPv6) like Caddy/ngtcp2
|
|
77
|
+
def initialize(port = 4433, address: "0.0.0.0", app: nil, server_configuration: nil, threads: DEFAULT_THREAD_POOL_SIZE, max_queue_size: nil, max_connections: DEFAULT_MAX_CONNECTIONS, scheduler: nil)
|
|
54
78
|
@port = port
|
|
55
79
|
@address = address
|
|
56
80
|
@app = app || default_rack_app
|
|
@@ -61,16 +85,25 @@ module Quicsilver
|
|
|
61
85
|
@config_handle = nil
|
|
62
86
|
@connections = {}
|
|
63
87
|
@request_registry = RequestRegistry.new
|
|
64
|
-
@handler_threads = []
|
|
65
|
-
@handler_mutex = Mutex.new
|
|
66
88
|
@thread_pool_size = threads
|
|
67
89
|
@max_queue_size = max_queue_size || threads * DEFAULT_QUEUE_MULTIPLIER
|
|
68
|
-
@
|
|
90
|
+
@yield_threshold = [@max_queue_size / 2, 1].max
|
|
91
|
+
# Tell peers how many streams we can serve, not how many we would like
|
|
92
|
+
# to. In flight plus queued is the honest ceiling; past that a request
|
|
93
|
+
# would be shed, and QUIC can decline the stream instead.
|
|
94
|
+
@server_configuration.limit_concurrent_requests!(@thread_pool_size + @max_queue_size)
|
|
95
|
+
@scheduler = build_scheduler(scheduler)
|
|
69
96
|
@max_connections = max_connections
|
|
70
97
|
@cancelled_streams = Set.new
|
|
71
98
|
@cancelled_mutex = Mutex.new
|
|
72
|
-
@pending_streams = {}
|
|
99
|
+
@pending_streams = {}
|
|
73
100
|
@pending_mutex = Mutex.new
|
|
101
|
+
@datagram_callback = nil
|
|
102
|
+
@connection_callback = nil
|
|
103
|
+
@connection_closed_callback = nil
|
|
104
|
+
@connection_migrated_callback = nil
|
|
105
|
+
@connection_error_callback = nil
|
|
106
|
+
@webtransport = WebTransportRegistry.new
|
|
74
107
|
|
|
75
108
|
protocol_app = wrap_app(@app, @server_configuration.mode)
|
|
76
109
|
|
|
@@ -85,28 +118,85 @@ module Quicsilver
|
|
|
85
118
|
self.class.instance = self
|
|
86
119
|
end
|
|
87
120
|
|
|
121
|
+
# Send an unreliable datagram to a connection (RFC 9297 / QUIC RFC 9221).
|
|
122
|
+
# Must fit in a single QUIC packet (~1200 bytes).
|
|
123
|
+
# Requires peer to have advertised SETTINGS_H3_DATAGRAM=1.
|
|
124
|
+
#
|
|
125
|
+
# server.datagram_send(connection, "real-time-update")
|
|
126
|
+
#
|
|
127
|
+
def datagram_send(connection, data)
|
|
128
|
+
unless connection.settings[Protocol::SETTINGS_H3_DATAGRAM]
|
|
129
|
+
raise Error, "Peer did not advertise SETTINGS_H3_DATAGRAM support"
|
|
130
|
+
end
|
|
131
|
+
Quicsilver.datagram_send(connection.data, data.to_s.b)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Register a callback for received datagrams.
|
|
135
|
+
#
|
|
136
|
+
# server.on_datagram { |connection, data| puts "Got: #{data}" }
|
|
137
|
+
#
|
|
138
|
+
def on_datagram(&block)
|
|
139
|
+
@datagram_callback = block
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Register a callback for new QUIC connections.
|
|
143
|
+
#
|
|
144
|
+
# server.on_connection { |conn|
|
|
145
|
+
# puts "New connection from #{conn.remote_address} (resumed: #{conn.session_resumed})"
|
|
146
|
+
# }
|
|
147
|
+
#
|
|
148
|
+
def on_connection(&block)
|
|
149
|
+
@connection_callback = block
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Register a callback for closed connections.
|
|
153
|
+
#
|
|
154
|
+
# server.on_connection_closed { |conn| StatsD.decrement("quic.connections") }
|
|
155
|
+
#
|
|
156
|
+
def on_connection_closed(&block)
|
|
157
|
+
@connection_closed_callback = block
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Register a callback for connection migration (client IP changed).
|
|
161
|
+
# This event only exists in QUIC — TCP connections die on IP change.
|
|
162
|
+
#
|
|
163
|
+
# server.on_connection_migrated { |conn, old_address, new_address|
|
|
164
|
+
# Rails.logger.info("Client migrated #{old_address} → #{new_address}")
|
|
165
|
+
# }
|
|
166
|
+
#
|
|
167
|
+
def on_connection_migrated(&block)
|
|
168
|
+
@connection_migrated_callback = block
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Register a callback for connection errors (transport failure or peer shutdown).
|
|
172
|
+
#
|
|
173
|
+
# server.on_connection_error { |conn, error_code, reason|
|
|
174
|
+
# # reason: :transport — network/protocol failure
|
|
175
|
+
# # reason: :peer — client sent CONNECTION_CLOSE
|
|
176
|
+
# # error_code: QUIC/HTTP/3 error code (e.g. H3_NO_ERROR = 0x100)
|
|
177
|
+
# Envoy.drain(conn) if reason == :transport
|
|
178
|
+
# }
|
|
179
|
+
#
|
|
180
|
+
def on_connection_error(&block)
|
|
181
|
+
@connection_error_callback = block
|
|
182
|
+
end
|
|
183
|
+
|
|
88
184
|
def start
|
|
89
185
|
raise ServerIsRunningError, "Server is already running" if @running
|
|
90
186
|
|
|
187
|
+
configure_transport_server_id
|
|
91
188
|
Quicsilver.open_connection
|
|
92
189
|
@config_handle = Quicsilver.create_server_configuration(@server_configuration.to_h)
|
|
93
190
|
raise ServerConfigurationError, "Failed to create server configuration" unless @config_handle
|
|
94
191
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
unless Quicsilver.start_listener(@listener_data.listener_handle, @address, @port, @server_configuration.alpn)
|
|
100
|
-
Quicsilver.close_configuration(@config_handle)
|
|
101
|
-
@config_handle = nil
|
|
102
|
-
cleanup_failed_server
|
|
103
|
-
raise ServerListenerError, "Failed to start listener on #{@address}:#{@port}"
|
|
104
|
-
end
|
|
192
|
+
create_listener
|
|
193
|
+
configure_listener
|
|
194
|
+
start_listener
|
|
105
195
|
|
|
106
196
|
@running = true
|
|
107
197
|
|
|
108
198
|
setup_signal_handlers
|
|
109
|
-
|
|
199
|
+
@scheduler.start
|
|
110
200
|
Quicsilver.event_loop.start
|
|
111
201
|
Quicsilver.event_loop.join # Block until shutdown
|
|
112
202
|
rescue ServerConfigurationError, ServerListenerError => e
|
|
@@ -157,23 +247,63 @@ module Quicsilver
|
|
|
157
247
|
@running
|
|
158
248
|
end
|
|
159
249
|
|
|
160
|
-
def
|
|
161
|
-
@
|
|
250
|
+
def draining?
|
|
251
|
+
@shutting_down
|
|
162
252
|
end
|
|
163
253
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
254
|
+
def ready?
|
|
255
|
+
@running && !draining? && !@scheduler.full?
|
|
256
|
+
end
|
|
167
257
|
|
|
168
|
-
|
|
258
|
+
# Return a point-in-time snapshot of app-server and transport state.
|
|
259
|
+
#
|
|
260
|
+
# The top-level server values are Quicsilver-owned facts: listener state,
|
|
261
|
+
# Ruby connection/request counts, and scheduler queue pressure. The
|
|
262
|
+
# "transport" entry exposes process-wide QUIC transport counters, so it may
|
|
263
|
+
# include activity from other Quicsilver servers/clients in the same process.
|
|
264
|
+
def stats
|
|
265
|
+
{
|
|
266
|
+
"cibir" => configured_cibir,
|
|
267
|
+
"running" => @running,
|
|
268
|
+
"ready" => ready?,
|
|
269
|
+
"draining" => draining?,
|
|
270
|
+
"shutting_down" => @shutting_down,
|
|
271
|
+
"connections" => {
|
|
272
|
+
"active" => @connections.size,
|
|
273
|
+
"max" => @max_connections
|
|
274
|
+
},
|
|
275
|
+
"requests" => {
|
|
276
|
+
"active" => @request_registry.active_count
|
|
277
|
+
},
|
|
278
|
+
"scheduler" => {
|
|
279
|
+
"threads" => @thread_pool_size,
|
|
280
|
+
"pending" => @scheduler.pending,
|
|
281
|
+
"max_queue_size" => @max_queue_size,
|
|
282
|
+
"full" => @scheduler.full?
|
|
283
|
+
},
|
|
284
|
+
"transport" => transport_counters,
|
|
285
|
+
"datagrams" => {
|
|
286
|
+
# Dropped before Ruby saw them because the event queue's datagram
|
|
287
|
+
# bound was reached. Datagrams are best-effort (RFC 9221); this is
|
|
288
|
+
# how you find out the dispatcher fell behind.
|
|
289
|
+
"dropped" => Quicsilver.dropped_datagrams
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
end
|
|
169
293
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
294
|
+
def connection_snapshots
|
|
295
|
+
@connections.values.map(&:to_h)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def cancelled_stream?(stream_id, connection_handle)
|
|
299
|
+
@cancelled_mutex.synchronize { @cancelled_streams.include?([connection_handle, stream_id]) }
|
|
300
|
+
end
|
|
174
301
|
|
|
175
|
-
|
|
176
|
-
|
|
302
|
+
# Wait for work queue to drain, then shut down the scheduler
|
|
303
|
+
def drain(timeout: 5)
|
|
304
|
+
Quicsilver.logger.debug("Draining work queue (#{@scheduler.pending} pending)")
|
|
305
|
+
@scheduler.drain(timeout: timeout)
|
|
306
|
+
@scheduler.stop
|
|
177
307
|
end
|
|
178
308
|
|
|
179
309
|
# Graceful shutdown: send GOAWAY, drain requests, then stop
|
|
@@ -202,7 +332,8 @@ module Quicsilver
|
|
|
202
332
|
|
|
203
333
|
# Log any requests that didn't complete
|
|
204
334
|
unless @request_registry.empty?
|
|
205
|
-
@request_registry.active_requests.each do |
|
|
335
|
+
@request_registry.active_requests.each do |request_id, req|
|
|
336
|
+
stream_id = req[:stream_id] || request_id
|
|
206
337
|
elapsed = Time.now - req[:started_at]
|
|
207
338
|
Quicsilver.logger.warn("Force-closing request: #{req[:method]} #{req[:path]} (stream: #{stream_id}, elapsed: #{elapsed.round(2)}s)")
|
|
208
339
|
end
|
|
@@ -230,17 +361,26 @@ module Quicsilver
|
|
|
230
361
|
return
|
|
231
362
|
end
|
|
232
363
|
|
|
233
|
-
|
|
234
|
-
|
|
364
|
+
quic_connection_ids = Quicsilver.connection_ids(connection_handle)
|
|
365
|
+
connection_id = quic_connection_ids["original_destination_connection_id"]
|
|
366
|
+
connection = Transport::Connection.new(
|
|
367
|
+
connection_handle,
|
|
368
|
+
connection_data,
|
|
369
|
+
max_header_size: @server_configuration.max_header_size,
|
|
370
|
+
connection_id: connection_id,
|
|
371
|
+
transport_server_id: @server_configuration.transport_server_id
|
|
372
|
+
)
|
|
373
|
+
connection.resolve_remote_address!
|
|
235
374
|
@connections[connection_handle] = connection
|
|
236
375
|
connection.setup_http3_streams
|
|
237
|
-
|
|
376
|
+
@connection_callback&.call(connection)
|
|
238
377
|
when STREAM_EVENT_CONNECTION_CLOSED
|
|
239
|
-
|
|
240
|
-
Quicsilver.close_server_connection(connection_handle)
|
|
241
|
-
|
|
378
|
+
handle_connection_closed(connection_handle)
|
|
242
379
|
when STREAM_EVENT_SEND_COMPLETE
|
|
243
380
|
# Buffer cleanup handled in C extension
|
|
381
|
+
when STREAM_EVENT_SHUTDOWN_COMPLETE
|
|
382
|
+
return unless (connection = @connections[connection_handle])
|
|
383
|
+
handle_stream_shutdown(connection, stream_id, Transport::StreamEvent.new(data, event).handle)
|
|
244
384
|
when STREAM_EVENT_RECEIVE
|
|
245
385
|
return unless (connection = @connections[connection_handle])
|
|
246
386
|
handle_receive(connection, connection_handle, stream_id, data, early_data: early_data)
|
|
@@ -256,48 +396,166 @@ module Quicsilver
|
|
|
256
396
|
if connection.critical_stream?(stream_id)
|
|
257
397
|
Quicsilver.logger.error("Critical stream #{stream_id} reset by peer")
|
|
258
398
|
Quicsilver.connection_shutdown(connection_handle, Protocol::H3_CLOSED_CRITICAL_STREAM, false) rescue nil
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
399
|
+
elsif (wt = @webtransport.for(connection_handle).unregister(stream_id))
|
|
400
|
+
wt.notify_close
|
|
401
|
+
connection.remove_stream(stream_id)
|
|
402
|
+
elsif (wt_session = @webtransport.for(connection_handle).session_for_stream(stream_id))
|
|
403
|
+
wt_session.stream(stream_id)&.notify_peer_reset(event.error_code, event.final_size)
|
|
404
|
+
elsif !@webtransport.for(connection_handle).known_stream?(stream_id)
|
|
405
|
+
cancel_stream(connection, stream_id)
|
|
264
406
|
end
|
|
265
407
|
when STREAM_EVENT_STOP_SENDING
|
|
266
|
-
return unless @connections[connection_handle]
|
|
408
|
+
return unless (connection = @connections[connection_handle])
|
|
267
409
|
event = Transport::StreamEvent.new(data, "STOP_SENDING")
|
|
268
410
|
Quicsilver.logger.debug("Stream #{stream_id} stop sending requested with error code: 0x#{event.error_code.to_s(16)}")
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
411
|
+
return if @webtransport.for(connection_handle).cancel_pending_connect(stream_id, event.handle)
|
|
412
|
+
|
|
413
|
+
# §4.4: STOP_SENDING on a WebTransport stream is delivered to the
|
|
414
|
+
# application, like RESET_STREAM. Without this it falls through to the
|
|
415
|
+
# HTTP/3 cancel below, which answers with the wrong code and runs
|
|
416
|
+
# request bookkeeping for a stream that is not a request.
|
|
417
|
+
if (wt_session = @webtransport.for(connection_handle).session_for_stream(stream_id))
|
|
418
|
+
wt_session.stream(stream_id)&.notify_peer_stop_sending(event.error_code)
|
|
419
|
+
elsif !@webtransport.for(connection_handle).known_stream?(stream_id)
|
|
420
|
+
Quicsilver.stream_reset(event.handle, Protocol::H3_REQUEST_CANCELLED)
|
|
421
|
+
cancel_stream(connection, stream_id)
|
|
422
|
+
end
|
|
423
|
+
when STREAM_EVENT_START_COMPLETE
|
|
424
|
+
return unless @connections.key?(connection_handle)
|
|
425
|
+
@webtransport.for(connection_handle).stream_started(stream_id, Transport::StreamEvent.new(data, event).handle)
|
|
426
|
+
# peer_accepted=true: stream is flowing. false: queued at peer's limit.
|
|
427
|
+
# Server-side: this fires for outbound streams (control, QPACK).
|
|
428
|
+
# Frequent false here means the client's stream limit is too low.
|
|
429
|
+
accepted = data.getbyte(8) == 1
|
|
430
|
+
Quicsilver.logger.debug("Stream #{stream_id} start complete (peer_accepted=#{accepted})")
|
|
431
|
+
when STREAM_EVENT_PEER_ACCEPTED
|
|
432
|
+
# Queued stream now accepted — client sent MAX_STREAMS.
|
|
433
|
+
Quicsilver.logger.debug("Stream #{stream_id} accepted by peer (MAX_STREAMS raised)")
|
|
434
|
+
when "CONNECTION_ERROR"
|
|
435
|
+
return unless (connection = @connections[connection_handle])
|
|
436
|
+
if @connection_error_callback && data.bytesize >= 13
|
|
437
|
+
error_code = data.unpack1("Q<")
|
|
438
|
+
reason = data.getbyte(12) == 0 ? :transport : :peer
|
|
439
|
+
@connection_error_callback.call(connection, error_code, reason)
|
|
440
|
+
end
|
|
441
|
+
when "CONNECTION_MIGRATED"
|
|
442
|
+
return unless (connection = @connections[connection_handle])
|
|
443
|
+
old_address = connection.remote_address
|
|
444
|
+
new_address = data.force_encoding(Encoding::UTF_8)
|
|
445
|
+
connection.instance_variable_set(:@remote_address, new_address)
|
|
446
|
+
@connection_migrated_callback&.call(connection, old_address, new_address)
|
|
447
|
+
when "DATAGRAM_RECEIVED"
|
|
448
|
+
return unless (connection = @connections[connection_handle])
|
|
449
|
+
unless @webtransport.for(connection_handle).receive_datagram(data)
|
|
450
|
+
@datagram_callback&.call(connection, data)
|
|
451
|
+
end
|
|
272
452
|
end
|
|
273
453
|
end
|
|
274
454
|
|
|
275
455
|
private
|
|
276
456
|
|
|
457
|
+
def configure_transport_server_id
|
|
458
|
+
return unless @server_configuration.transport_server_id
|
|
459
|
+
|
|
460
|
+
Quicsilver.apply_msquic_server_id([@server_configuration.transport_server_id].pack("H*"))
|
|
461
|
+
rescue RuntimeError => e
|
|
462
|
+
raise TransportError.new(e.message, status: TransportError.parse_status(e.message))
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def create_listener
|
|
466
|
+
result = Quicsilver.create_listener(@config_handle)
|
|
467
|
+
@listener_data = ListenerData.new(result[0], result[1])
|
|
468
|
+
raise ServerListenerError, "Failed to create listener #{@address}:#{@port}" unless @listener_data
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Configure listener-level CIBIR routing bytes before starting the listener.
|
|
472
|
+
def configure_listener
|
|
473
|
+
if @server_configuration.cibir_id
|
|
474
|
+
Quicsilver.configure_listener_cibir(
|
|
475
|
+
@listener_data.listener_handle,
|
|
476
|
+
@server_configuration.cibir_bytes
|
|
477
|
+
)
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def handle_stream_shutdown(connection, stream_id, handle)
|
|
482
|
+
return unless @webtransport.for(connection.handle).stream_shutdown_complete(stream_id, handle: handle)
|
|
483
|
+
|
|
484
|
+
connection.remove_stream(stream_id)
|
|
485
|
+
rescue StandardError
|
|
486
|
+
connection.remove_stream(stream_id)
|
|
487
|
+
raise
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def handle_connection_closed(connection_handle)
|
|
491
|
+
connection = @connections.delete(connection_handle)
|
|
492
|
+
begin
|
|
493
|
+
close_webtransport_sessions(connection_handle)
|
|
494
|
+
@connection_closed_callback&.call(connection) if connection
|
|
495
|
+
ensure
|
|
496
|
+
connection&.streams&.clear
|
|
497
|
+
Quicsilver.close_server_connection(connection_handle)
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def close_webtransport_sessions(connection_handle)
|
|
502
|
+
@webtransport.drop(connection_handle).each do |session|
|
|
503
|
+
session.notify_close
|
|
504
|
+
rescue StandardError => error
|
|
505
|
+
Quicsilver.logger.error("WebTransport session #{session.stream_id} close failed: #{error.class}: #{error.message}")
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def start_listener
|
|
510
|
+
return if Quicsilver.start_listener(@listener_data.listener_handle, @address, @port, @server_configuration.alpn)
|
|
511
|
+
|
|
512
|
+
Quicsilver.close_configuration(@config_handle)
|
|
513
|
+
@config_handle = nil
|
|
514
|
+
cleanup_failed_server
|
|
515
|
+
raise ServerListenerError, "Failed to start listener on #{@address}:#{@port}"
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def configured_cibir
|
|
519
|
+
if @server_configuration.cibir_id
|
|
520
|
+
{
|
|
521
|
+
"id" => @server_configuration.cibir_id,
|
|
522
|
+
"offset" => 0
|
|
523
|
+
}
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def transport_counters
|
|
528
|
+
Quicsilver.transport_counters
|
|
529
|
+
rescue RuntimeError => error
|
|
530
|
+
raise unless error.message.include?("QUIC transport not initialized")
|
|
531
|
+
|
|
532
|
+
nil
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# A request answered without reading its body. Later data and the FIN are
|
|
536
|
+
# dropped rather than treated as a new request.
|
|
537
|
+
def answer_without_body(connection, stream_id)
|
|
538
|
+
@cancelled_mutex.synchronize { @cancelled_streams.add([connection.handle, stream_id]) }
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def cancel_stream(connection, stream_id)
|
|
542
|
+
@cancelled_mutex.synchronize { @cancelled_streams.add([connection.handle, stream_id]) }
|
|
543
|
+
pending = @pending_mutex.synchronize { @pending_streams.delete([connection.handle, stream_id]) }
|
|
544
|
+
pending&.body&.close(RuntimeError.new("Stream #{stream_id} cancelled"))
|
|
545
|
+
@request_registry.complete(stream_id, connection.handle)
|
|
546
|
+
connection.remove_stream(stream_id)
|
|
547
|
+
end
|
|
548
|
+
|
|
277
549
|
# Wrap the user's app for the configured mode.
|
|
278
550
|
# Rack mode: inject rack.early_hints support, then wrap with protocol-rack.
|
|
279
551
|
# Falcon mode: pass through as-is (native protocol-http app).
|
|
280
552
|
def wrap_app(app, mode)
|
|
281
553
|
case mode
|
|
282
554
|
when :falcon then app
|
|
283
|
-
else ::
|
|
555
|
+
else Server::RackAdapter.new(app)
|
|
284
556
|
end
|
|
285
557
|
end
|
|
286
558
|
|
|
287
|
-
# Bridges protocol-http's interim_response to Rack's rack.early_hints.
|
|
288
|
-
# In a Rails controller: send_early_hints("link" => '</style.css>; rel=preload')
|
|
289
|
-
def with_early_hints(app)
|
|
290
|
-
->(env) {
|
|
291
|
-
request = env["protocol.http.request"]
|
|
292
|
-
if request&.respond_to?(:interim_response) && request.interim_response
|
|
293
|
-
env["rack.early_hints"] = ->(headers) {
|
|
294
|
-
request.send_interim_response(103, ::Protocol::HTTP::Headers[headers.map { |k, v| [k, v] }])
|
|
295
|
-
}
|
|
296
|
-
end
|
|
297
|
-
app.call(env)
|
|
298
|
-
}
|
|
299
|
-
end
|
|
300
|
-
|
|
301
559
|
def setup_signal_handlers
|
|
302
560
|
%w[INT TERM].each do |signal|
|
|
303
561
|
trap(signal) { Thread.new { shutdown } }
|
|
@@ -327,41 +585,76 @@ module Quicsilver
|
|
|
327
585
|
|
|
328
586
|
attr_reader :work_queue
|
|
329
587
|
|
|
588
|
+
# RECEIVE data is always [stream_handle(8)][payload...] from C.
|
|
589
|
+
HANDLE_SIZE = 8
|
|
590
|
+
|
|
330
591
|
def handle_receive(connection, connection_handle, stream_id, data, early_data: false)
|
|
592
|
+
stream_handle = data.byteslice(0, HANDLE_SIZE)&.unpack1("Q<")
|
|
593
|
+
payload = data.byteslice(HANDLE_SIZE..-1) || "".b
|
|
594
|
+
return if @webtransport.for(connection_handle).route_owned_stream(stream_id, stream_handle, payload)
|
|
595
|
+
|
|
331
596
|
# Unidirectional streams (control, QPACK) must be processed incrementally —
|
|
332
597
|
# they never send FIN, so waiting for RECEIVE_FIN would mean never parsing.
|
|
333
|
-
if (stream_id
|
|
598
|
+
if Transport::StreamId.unidirectional?(stream_id)
|
|
334
599
|
begin
|
|
335
|
-
connection.receive_unidirectional_data(stream_id,
|
|
600
|
+
stream_type, stream_payload = connection.receive_unidirectional_data(stream_id, payload)
|
|
601
|
+
if stream_type == :webtransport_uni
|
|
602
|
+
route_wt_uni_stream(connection_handle, stream_id, stream_handle, stream_payload)
|
|
603
|
+
end
|
|
604
|
+
if connection.settings_received? && (request = @webtransport.for(connection_handle).take_pending_connect)
|
|
605
|
+
establish_webtransport(connection, request)
|
|
606
|
+
end
|
|
336
607
|
rescue Protocol::FrameError => e
|
|
337
608
|
Quicsilver.logger.error("Control stream error: #{e.message} (0x#{e.error_code.to_s(16)})")
|
|
338
609
|
Quicsilver.connection_shutdown(connection_handle, e.error_code, false) rescue nil
|
|
339
610
|
end
|
|
340
611
|
else
|
|
341
|
-
handle_bidi_receive(connection, connection_handle, stream_id,
|
|
612
|
+
handle_bidi_receive(connection, connection_handle, stream_id, stream_handle, payload, early_data: early_data)
|
|
342
613
|
end
|
|
343
614
|
end
|
|
344
615
|
|
|
345
|
-
def handle_bidi_receive(connection, connection_handle, stream_id,
|
|
346
|
-
|
|
616
|
+
def handle_bidi_receive(connection, connection_handle, stream_id, stream_handle, payload, early_data: false)
|
|
617
|
+
return if cancelled_stream?(stream_id, connection_handle)
|
|
618
|
+
|
|
619
|
+
manager = @webtransport.for(connection_handle)
|
|
620
|
+
|
|
621
|
+
pending = @pending_mutex.synchronize { @pending_streams[[connection_handle, stream_id]] }
|
|
622
|
+
payload = connection.complete_stream(stream_id, payload) unless pending
|
|
347
623
|
if pending
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
elsif
|
|
353
|
-
|
|
624
|
+
pending.frame_buffer << payload
|
|
625
|
+
drain_pending_frames(pending)
|
|
626
|
+
elsif (wt_payload = manager.pending_payload(stream_id, stream_handle, payload))
|
|
627
|
+
accept_webtransport_stream(connection_handle, stream_id, stream_handle, wt_payload)
|
|
628
|
+
elsif manager.pending_stream?(stream_id)
|
|
629
|
+
elsif contains_headers_frame?(payload)
|
|
630
|
+
dispatch_streaming(connection, connection_handle, stream_id, payload, stream_handle: stream_handle, early_data: early_data)
|
|
354
631
|
else
|
|
355
|
-
connection.buffer_data(stream_id,
|
|
632
|
+
connection.buffer_data(stream_id, payload)
|
|
356
633
|
end
|
|
357
634
|
end
|
|
358
635
|
|
|
359
636
|
def handle_receive_fin(connection, connection_handle, stream_id, data, early_data: false)
|
|
637
|
+
return if cancelled_stream?(stream_id, connection_handle)
|
|
638
|
+
|
|
360
639
|
event = Transport::StreamEvent.new(data, "RECEIVE_FIN")
|
|
640
|
+
manager = @webtransport.for(connection_handle)
|
|
641
|
+
return if manager.route_owned_stream(stream_id, event.handle, event.data, fin: true)
|
|
642
|
+
|
|
643
|
+
if connection.uni_stream_type(stream_id) == :webtransport_uni
|
|
644
|
+
# receive_fin closes the read side only once the FIN's data is fully
|
|
645
|
+
# accepted; under backpressure a deferred suffix is still outstanding.
|
|
646
|
+
route_wt_uni_stream(connection_handle, stream_id, event.handle, event.data, fin: true)
|
|
647
|
+
return
|
|
648
|
+
end
|
|
361
649
|
|
|
362
|
-
pending = @pending_mutex.synchronize { @pending_streams[stream_id] }
|
|
650
|
+
pending = @pending_mutex.synchronize { @pending_streams[[connection_handle, stream_id]] }
|
|
363
651
|
if pending
|
|
364
652
|
complete_streaming_request(pending, event)
|
|
653
|
+
elsif Transport::StreamId.bidirectional?(stream_id) &&
|
|
654
|
+
(wt_payload = manager.pending_payload(stream_id, event.handle, event.data))
|
|
655
|
+
accept_webtransport_stream(connection_handle, stream_id, event.handle, wt_payload, fin: true)
|
|
656
|
+
elsif manager.pending_stream?(stream_id)
|
|
657
|
+
manager.reject_stream(stream_id, event.handle)
|
|
365
658
|
else
|
|
366
659
|
complete_buffered_request(connection, connection_handle, stream_id, event, early_data: early_data)
|
|
367
660
|
end
|
|
@@ -370,7 +663,7 @@ module Quicsilver
|
|
|
370
663
|
def complete_streaming_request(pending, event)
|
|
371
664
|
if event.data && !event.data.empty?
|
|
372
665
|
pending.frame_buffer << event.data
|
|
373
|
-
|
|
666
|
+
drain_pending_frames(pending)
|
|
374
667
|
end
|
|
375
668
|
pending.body.close_write
|
|
376
669
|
pending.complete(event.handle)
|
|
@@ -384,10 +677,14 @@ module Quicsilver
|
|
|
384
677
|
|
|
385
678
|
if stream.bidirectional?
|
|
386
679
|
connection.track_client_stream(stream_id)
|
|
387
|
-
|
|
680
|
+
dispatch_streaming(connection, connection_handle, stream_id, full_data,
|
|
681
|
+
stream_handle: event.handle, early_data: early_data, completed_stream: stream)
|
|
388
682
|
else
|
|
389
683
|
begin
|
|
390
|
-
connection.handle_unidirectional_stream(stream)
|
|
684
|
+
stream_type, payload = connection.handle_unidirectional_stream(stream)
|
|
685
|
+
if stream_type == :webtransport_uni
|
|
686
|
+
route_wt_uni_stream(connection_handle, stream_id, stream.stream_handle, payload, fin: true)
|
|
687
|
+
end
|
|
391
688
|
rescue Protocol::FrameError => e
|
|
392
689
|
Quicsilver.logger.error("Control stream error: #{e.message} (0x#{e.error_code.to_s(16)})")
|
|
393
690
|
Quicsilver.connection_shutdown(connection_handle, e.error_code, false) rescue nil
|
|
@@ -395,36 +692,69 @@ module Quicsilver
|
|
|
395
692
|
end
|
|
396
693
|
end
|
|
397
694
|
|
|
695
|
+
# Replayable early data may only carry safe methods when the policy is
|
|
696
|
+
# :reject. There is no stream handle here to answer 425 with, so the
|
|
697
|
+
# request is dropped.
|
|
698
|
+
def reject_early_data?(method, early_data, stream_id)
|
|
699
|
+
return false unless @server_configuration.early_data_policy == :reject
|
|
700
|
+
return false unless early_data
|
|
701
|
+
return false if RequestHandler::SAFE_METHODS.include?(method)
|
|
702
|
+
|
|
703
|
+
Quicsilver.logger.debug("Rejected 0-RTT #{method} on stream #{stream_id} (no stream handle to send 425)")
|
|
704
|
+
true
|
|
705
|
+
end
|
|
706
|
+
|
|
398
707
|
def dispatch_request(connection, stream, early_data: false)
|
|
399
|
-
if @
|
|
708
|
+
if @scheduler.full?
|
|
400
709
|
Quicsilver.logger.warn("Work queue full (#{@max_queue_size}), rejecting request")
|
|
401
710
|
connection.send_error(stream, 503, "Service Unavailable") if stream.writable?
|
|
402
711
|
else
|
|
403
|
-
|
|
712
|
+
enqueue_work([connection, stream, early_data])
|
|
404
713
|
end
|
|
405
714
|
end
|
|
406
715
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
716
|
+
# The dispatcher thread holds the GVL while it delivers a batch of events,
|
|
717
|
+
# and the workers cannot drain the scheduler queue until it lets go. A
|
|
718
|
+
# burst larger than the queue's headroom would then be shed by admission
|
|
719
|
+
# control even though the workers are idle. So the dispatcher yields once
|
|
720
|
+
# the queue is half full. The trigger is pressure on the admission budget,
|
|
721
|
+
# which is the only reason to interrupt a batch; a fixed batch size would
|
|
722
|
+
# have to be smaller than every possible max_queue_size to be safe.
|
|
723
|
+
def enqueue_work(work)
|
|
724
|
+
@scheduler.enqueue(work)
|
|
725
|
+
Thread.pass if @scheduler.pending >= @yield_threshold
|
|
726
|
+
end
|
|
412
727
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
728
|
+
# Send an error response on a stream we only hold a raw handle for.
|
|
729
|
+
# Without a handle the peer has not given us anywhere to write yet, so the
|
|
730
|
+
# stream is left for the normal reset/close path.
|
|
731
|
+
def send_stream_error(connection, stream_id, stream_handle, status, message)
|
|
732
|
+
return unless stream_handle
|
|
733
|
+
|
|
734
|
+
stream = Transport::InboundStream.new(stream_id)
|
|
735
|
+
stream.stream_handle = stream_handle
|
|
736
|
+
connection.send_error(stream, status, message) if stream.writable?
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
def build_scheduler(scheduler_class)
|
|
740
|
+
klass = scheduler_class || Schedulers::ThreadScheduler
|
|
741
|
+
|
|
742
|
+
klass.new(
|
|
743
|
+
concurrency: @thread_pool_size,
|
|
744
|
+
max_queue_size: @max_queue_size
|
|
745
|
+
) do |work|
|
|
746
|
+
if work.is_a?(Array) && work[0] == :streaming
|
|
747
|
+
handle_streaming_request(work[1])
|
|
748
|
+
else
|
|
749
|
+
connection, stream, early_data = work
|
|
750
|
+
@request_handler.call(connection, stream, early_data: early_data)
|
|
420
751
|
end
|
|
421
|
-
@handler_mutex.synchronize { @handler_threads << thread }
|
|
422
752
|
end
|
|
423
753
|
end
|
|
424
754
|
|
|
425
|
-
#
|
|
426
|
-
#
|
|
427
|
-
def dispatch_streaming(connection, connection_handle, stream_id, data, early_data: false)
|
|
755
|
+
# Admit requests when headers arrive, whether or not FIN accompanies them.
|
|
756
|
+
# Completed HTTP requests keep their normal worker dispatch.
|
|
757
|
+
def dispatch_streaming(connection, connection_handle, stream_id, data, stream_handle: nil, early_data: false, completed_stream: nil)
|
|
428
758
|
parser = Protocol::RequestParser.new(
|
|
429
759
|
data,
|
|
430
760
|
max_header_size: @server_configuration.max_header_size,
|
|
@@ -435,28 +765,119 @@ module Quicsilver
|
|
|
435
765
|
parser.validate_headers!
|
|
436
766
|
|
|
437
767
|
headers = parser.headers
|
|
438
|
-
|
|
768
|
+
if headers.empty?
|
|
769
|
+
if completed_stream
|
|
770
|
+
dispatch_request(connection, completed_stream, early_data: early_data)
|
|
771
|
+
else
|
|
772
|
+
connection.buffer_data(stream_id, data)
|
|
773
|
+
end
|
|
774
|
+
return
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
# RFC 9114 §5.2: Reject requests on streams at or above the GOAWAY stream ID
|
|
778
|
+
if connection.local_goaway_id && stream_id >= connection.local_goaway_id
|
|
779
|
+
Quicsilver.logger.debug("Rejecting stream #{stream_id} after GOAWAY (#{connection.local_goaway_id})")
|
|
780
|
+
return
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
method = headers[":method"]
|
|
784
|
+
|
|
785
|
+
Quicsilver.logger.debug(
|
|
786
|
+
"HTTP/3 request stream=#{stream_id} method=#{method.inspect} " \
|
|
787
|
+
"path=#{headers[":path"].inspect} protocol=#{headers[":protocol"].inspect} " \
|
|
788
|
+
"authority=#{headers[":authority"].inspect} headers=#{headers.inspect}"
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
if method == "CONNECT" && Protocol::WebTransport.protocol?(headers[":protocol"])
|
|
792
|
+
# The 0-RTT policy has to be applied here too. A WebTransport CONNECT
|
|
793
|
+
# is not a safe method, and this branch runs before the check in
|
|
794
|
+
# enqueue_streaming_request, so replayable session setup was reaching
|
|
795
|
+
# the application while an ordinary CONNECT was refused.
|
|
796
|
+
return if reject_early_data?(method, early_data, stream_id)
|
|
797
|
+
|
|
798
|
+
dispatch_webtransport_connect(connection, stream_id, headers, data,
|
|
799
|
+
stream_handle: stream_handle, early_data: early_data, fin: !completed_stream.nil?)
|
|
800
|
+
elsif completed_stream
|
|
801
|
+
dispatch_request(connection, completed_stream, early_data: early_data)
|
|
802
|
+
else
|
|
803
|
+
enqueue_streaming_request(connection, stream_id, parser, data,
|
|
804
|
+
stream_handle: stream_handle, early_data: early_data)
|
|
805
|
+
end
|
|
806
|
+
rescue Protocol::FrameError => e
|
|
807
|
+
Quicsilver.logger.error("Frame error: #{e.message}")
|
|
808
|
+
Quicsilver.connection_shutdown(connection_handle, e.error_code, false) rescue nil
|
|
809
|
+
rescue Protocol::MessageError => e
|
|
810
|
+
if completed_stream
|
|
811
|
+
dispatch_request(connection, completed_stream, early_data: early_data)
|
|
812
|
+
else
|
|
813
|
+
Quicsilver.logger.error("Message error on stream #{stream_id}: #{e.message}")
|
|
814
|
+
end
|
|
815
|
+
rescue => e
|
|
816
|
+
Quicsilver.logger.error("Error in streaming dispatch: #{e.class} - #{e.message}")
|
|
817
|
+
end
|
|
439
818
|
|
|
819
|
+
def dispatch_webtransport_connect(connection, stream_id, headers, data, stream_handle:, early_data:, fin: false)
|
|
820
|
+
# Validated HEADERS exist. Preserve following bytes, including partial
|
|
821
|
+
# frames, for the session's incremental CONNECT decoder.
|
|
822
|
+
header_end = Protocol::FrameReader.each(data) do |type, _payload, offset|
|
|
823
|
+
break offset if type == Protocol::FRAME_HEADERS
|
|
824
|
+
end
|
|
825
|
+
request = WebTransportManager::ConnectRequest.new(
|
|
826
|
+
stream_id: stream_id, stream_handle: stream_handle, headers: headers,
|
|
827
|
+
data: data.byteslice(header_end..), early_data: early_data, fin: fin
|
|
828
|
+
)
|
|
829
|
+
if connection.settings_received?
|
|
830
|
+
establish_webtransport(connection, request)
|
|
831
|
+
else
|
|
832
|
+
@webtransport.for(connection.handle).defer_connect(request)
|
|
833
|
+
end
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
def establish_webtransport(connection, request)
|
|
837
|
+
unless request.headers[":scheme"] == "https" &&
|
|
838
|
+
connection.webtransport_settings_valid?(request.headers[":protocol"]) && connection.datagram_send_enabled?
|
|
839
|
+
@webtransport.for(connection.handle).reject_stream(
|
|
840
|
+
request.stream_id, request.stream_handle, error_code: Protocol::H3_MESSAGE_ERROR
|
|
841
|
+
)
|
|
842
|
+
return
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
session = accept_webtransport(connection, connection.handle, request.stream_id,
|
|
846
|
+
request.stream_handle, request.headers, early_data: request.early_data)
|
|
847
|
+
session&.receive_connect_stream_data(request.data, fin: request.fin)
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
def enqueue_streaming_request(connection, stream_id, parser, data, stream_handle:, early_data:)
|
|
851
|
+
connection_handle = connection.handle
|
|
852
|
+
headers = parser.headers
|
|
440
853
|
method = headers[":method"]
|
|
441
854
|
|
|
442
|
-
if
|
|
443
|
-
|
|
444
|
-
|
|
855
|
+
return if reject_early_data?(method, early_data, stream_id)
|
|
856
|
+
|
|
857
|
+
# Reject before tracking the request so overload cannot leave orphaned state.
|
|
858
|
+
if @scheduler.full?
|
|
859
|
+
Quicsilver.logger.warn("Work queue full (#{@max_queue_size}), shedding stream #{stream_id}")
|
|
860
|
+
# The response is already complete, so the rest of the request body is
|
|
861
|
+
# of no interest. Record that before sending, so body that arrives
|
|
862
|
+
# while we answer is discarded rather than buffered: its HEADERS have
|
|
863
|
+
# been consumed, so parsing what is left would report "DATA frame
|
|
864
|
+
# before HEADERS" and take down the whole connection.
|
|
865
|
+
answer_without_body(connection, stream_id)
|
|
866
|
+
send_stream_error(connection, stream_id, stream_handle, 503, "Service Unavailable")
|
|
445
867
|
return
|
|
446
868
|
end
|
|
447
869
|
|
|
448
|
-
request, body = @request_handler.adapter.build_request(
|
|
870
|
+
request, body = @request_handler.adapter.build_request(
|
|
871
|
+
headers,
|
|
872
|
+
remote_address: connection.remote_address,
|
|
873
|
+
remote_port: connection.remote_port,
|
|
874
|
+
transport_context: connection.request_context(stream_id: stream_id)
|
|
875
|
+
)
|
|
449
876
|
request.headers.add("quicsilver-early-data", early_data.to_s)
|
|
450
877
|
|
|
451
|
-
#
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
# Complete DATA frames the parser extracted
|
|
455
|
-
if parser.body && parser.body.size > 0
|
|
456
|
-
parser.body.rewind
|
|
457
|
-
body_data = parser.body.read
|
|
458
|
-
body.write(body_data) unless body_data.empty?
|
|
459
|
-
end
|
|
878
|
+
# Copy complete DATA payloads; partial frames stay in the pending buffer.
|
|
879
|
+
if body && parser.body.size > 0
|
|
880
|
+
body.write(parser.body.read)
|
|
460
881
|
end
|
|
461
882
|
|
|
462
883
|
pending = PendingStream.new(
|
|
@@ -471,28 +892,15 @@ module Quicsilver
|
|
|
471
892
|
remainder = data.byteslice(parser.bytes_consumed..-1)
|
|
472
893
|
if remainder && remainder.bytesize > 0
|
|
473
894
|
pending.frame_buffer << remainder
|
|
474
|
-
|
|
895
|
+
drain_pending_frames(pending)
|
|
475
896
|
end
|
|
476
|
-
@pending_mutex.synchronize { @pending_streams[stream_id] = pending }
|
|
897
|
+
@pending_mutex.synchronize { @pending_streams[[connection_handle, stream_id]] = pending }
|
|
477
898
|
|
|
478
899
|
connection.track_client_stream(stream_id)
|
|
479
900
|
@request_registry.track(stream_id, connection_handle,
|
|
480
901
|
path: headers[":path"] || "/", method: method || "GET")
|
|
481
902
|
|
|
482
|
-
|
|
483
|
-
Quicsilver.logger.warn("Work queue full (#{@max_queue_size}), rejecting request")
|
|
484
|
-
body&.close
|
|
485
|
-
@pending_mutex.synchronize { @pending_streams.delete(stream_id) }
|
|
486
|
-
else
|
|
487
|
-
@work_queue.push([:streaming, pending])
|
|
488
|
-
end
|
|
489
|
-
rescue Protocol::FrameError => e
|
|
490
|
-
Quicsilver.logger.error("Frame error: #{e.message}")
|
|
491
|
-
Quicsilver.connection_shutdown(connection_handle, e.error_code, false) rescue nil
|
|
492
|
-
rescue Protocol::MessageError => e
|
|
493
|
-
Quicsilver.logger.error("Message error on stream #{stream_id}: #{e.message}")
|
|
494
|
-
rescue => e
|
|
495
|
-
Quicsilver.logger.error("Error in streaming dispatch: #{e.class} - #{e.message}")
|
|
903
|
+
@scheduler.enqueue([:streaming, pending])
|
|
496
904
|
end
|
|
497
905
|
|
|
498
906
|
def handle_streaming_request(pending)
|
|
@@ -505,7 +913,7 @@ module Quicsilver
|
|
|
505
913
|
return
|
|
506
914
|
end
|
|
507
915
|
|
|
508
|
-
return if cancelled_stream?(pending.stream_id)
|
|
916
|
+
return if cancelled_stream?(pending.stream_id, pending.connection.handle)
|
|
509
917
|
|
|
510
918
|
headers = response.headers
|
|
511
919
|
|
|
@@ -515,15 +923,16 @@ module Quicsilver
|
|
|
515
923
|
trailer_hash
|
|
516
924
|
end
|
|
517
925
|
|
|
518
|
-
|
|
926
|
+
# Pairs rather than a Hash, for the set-cookie reason in RequestHandler#send_response.
|
|
927
|
+
response_headers = []
|
|
519
928
|
if headers.respond_to?(:header)
|
|
520
|
-
headers.header.each { |name, value| response_headers[name
|
|
929
|
+
headers.header.each { |name, value| response_headers << [ name, value ] }
|
|
521
930
|
else
|
|
522
|
-
headers&.each { |name, value| response_headers[name
|
|
931
|
+
headers&.each { |name, value| response_headers << [ name, value ] }
|
|
523
932
|
end
|
|
524
933
|
|
|
525
|
-
|
|
526
|
-
response_headers["content-length"
|
|
934
|
+
unless response_headers.any? { |name, _| name == "content-length" }
|
|
935
|
+
response_headers << [ "content-length", response.body.length.to_s ] if response.body&.length
|
|
527
936
|
end
|
|
528
937
|
|
|
529
938
|
stream = Transport::InboundStream.new(pending.stream_id)
|
|
@@ -532,7 +941,7 @@ module Quicsilver
|
|
|
532
941
|
pending.connection.apply_stream_priority(stream, pending.priority)
|
|
533
942
|
pending.connection.send_response(stream, response.status, response_headers, response.body,
|
|
534
943
|
head_request: pending.request.method == "HEAD", trailers: trailers)
|
|
535
|
-
@request_registry.complete(pending.stream_id)
|
|
944
|
+
@request_registry.complete(pending.stream_id, pending.connection.handle)
|
|
536
945
|
rescue => e
|
|
537
946
|
Quicsilver.logger.error("Streaming request error: #{e.class} - #{e.message}")
|
|
538
947
|
if pending.stream_handle
|
|
@@ -541,70 +950,131 @@ module Quicsilver
|
|
|
541
950
|
pending.connection.send_error(stream, 500, "Internal Server Error") if stream.writable?
|
|
542
951
|
end
|
|
543
952
|
ensure
|
|
544
|
-
@pending_mutex.synchronize { @pending_streams.delete(pending.stream_id) }
|
|
545
|
-
@
|
|
546
|
-
@
|
|
953
|
+
@pending_mutex.synchronize { @pending_streams.delete([pending.connection.handle, pending.stream_id]) }
|
|
954
|
+
@cancelled_mutex.synchronize { @cancelled_streams.delete([pending.connection.handle, pending.stream_id]) }
|
|
955
|
+
@request_registry.complete(pending.stream_id, pending.connection.handle)
|
|
956
|
+
pending.connection.remove_stream(pending.stream_id)
|
|
547
957
|
end
|
|
548
958
|
|
|
549
|
-
#
|
|
550
|
-
#
|
|
551
|
-
|
|
959
|
+
# Drain buffered body frames, failing the connection if the peer sends a
|
|
960
|
+
# frame type that is not allowed on a request stream.
|
|
961
|
+
def drain_pending_frames(pending)
|
|
962
|
+
drain_data_frames(pending)
|
|
963
|
+
rescue Protocol::FrameError => e
|
|
964
|
+
handle = pending.connection.handle
|
|
965
|
+
Quicsilver.logger.error("Request stream error: #{e.message} (0x#{e.error_code.to_s(16)})")
|
|
966
|
+
Quicsilver.connection_shutdown(handle, e.error_code, false) rescue nil
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
# Drain complete frames from the buffer, keeping any partial trailing frame
|
|
970
|
+
# for the next RECEIVE callback. DATA payloads go to the request body;
|
|
971
|
+
# anything else is an extension frame, which RFC 9114 9 says to ignore.
|
|
552
972
|
def drain_data_frames(pending)
|
|
553
|
-
|
|
973
|
+
buffer = pending.frame_buffer
|
|
554
974
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
if
|
|
558
|
-
|
|
559
|
-
type_len = 1
|
|
560
|
-
else
|
|
561
|
-
type, type_len = Protocol.decode_varint_str(buf, 0)
|
|
562
|
-
break if type_len == 0
|
|
563
|
-
end
|
|
975
|
+
consumed = Protocol::FrameReader.each(buffer) do |type, payload|
|
|
976
|
+
Protocol.reject_unexpected_request_frame!(type)
|
|
977
|
+
pending.body.write(payload) if type == Protocol::FRAME_DATA
|
|
978
|
+
end
|
|
564
979
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
980
|
+
pending.frame_buffer = consumed.zero? ? buffer : (buffer.byteslice(consumed..) || "".b)
|
|
981
|
+
end
|
|
982
|
+
|
|
983
|
+
def accept_webtransport(connection, connection_handle, stream_id, stream_handle, headers, early_data: false)
|
|
984
|
+
# Draft-16 §5.1 forbids concurrent sessions without session flow control.
|
|
985
|
+
manager = @webtransport.for(connection_handle)
|
|
986
|
+
if manager.routable_sessions.any?
|
|
987
|
+
manager.reject_stream(stream_id, stream_handle, error_code: Protocol::H3_REQUEST_REJECTED)
|
|
988
|
+
return
|
|
989
|
+
end
|
|
574
990
|
|
|
575
|
-
|
|
576
|
-
|
|
991
|
+
Quicsilver.logger.debug(
|
|
992
|
+
"WebTransport CONNECT stream=#{stream_id} path=#{headers[":path"].inspect} " \
|
|
993
|
+
"authority=#{headers[":authority"].inspect} headers=#{headers.inspect}"
|
|
994
|
+
)
|
|
577
995
|
|
|
578
|
-
|
|
579
|
-
|
|
996
|
+
stream = Transport::InboundStream.new(stream_id)
|
|
997
|
+
stream.stream_handle = stream_handle
|
|
580
998
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
999
|
+
session = WebTransportSession.new(
|
|
1000
|
+
connection: connection,
|
|
1001
|
+
stream: stream,
|
|
1002
|
+
headers: headers
|
|
1003
|
+
)
|
|
585
1004
|
|
|
586
|
-
|
|
1005
|
+
dispatch_webtransport_to_rack(connection, connection_handle, stream_id, headers, session, early_data: early_data)
|
|
1006
|
+
session if session.accepted?
|
|
1007
|
+
rescue => e
|
|
1008
|
+
Quicsilver.logger.error("WebTransport error: #{e.class} - #{e.message}")
|
|
1009
|
+
nil
|
|
1010
|
+
end
|
|
1011
|
+
|
|
1012
|
+
def dispatch_webtransport_to_rack(connection, connection_handle, stream_id, headers, session, early_data: false)
|
|
1013
|
+
request_context = connection.request_context(stream_id: stream_id)
|
|
1014
|
+
rack_context = Rack::Context.new(
|
|
1015
|
+
stream_id: stream_id,
|
|
1016
|
+
early_data: early_data,
|
|
1017
|
+
webtransport: session,
|
|
1018
|
+
metadata: request_context
|
|
1019
|
+
)
|
|
1020
|
+
|
|
1021
|
+
request, _body = @request_handler.adapter.build_request(
|
|
1022
|
+
headers,
|
|
1023
|
+
remote_address: connection.remote_address,
|
|
1024
|
+
remote_port: connection.remote_port,
|
|
1025
|
+
transport_context: request_context,
|
|
1026
|
+
rack_context: rack_context
|
|
1027
|
+
)
|
|
1028
|
+
|
|
1029
|
+
Quicsilver.logger.debug(
|
|
1030
|
+
"Dispatching WebTransport to Rack stream=#{stream_id} " \
|
|
1031
|
+
"method=#{request.method.inspect} path=#{request.path.inspect} early_data=#{early_data.inspect}"
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
@webtransport.for(connection_handle).register(session)
|
|
1035
|
+
response = @request_handler.adapter.call(request)
|
|
1036
|
+
|
|
1037
|
+
Quicsilver.logger.debug(
|
|
1038
|
+
"WebTransport Rack response stream=#{stream_id} status=#{response.status.inspect} " \
|
|
1039
|
+
"accepted=#{session.accepted?}"
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
if session.accepted?
|
|
1043
|
+
connection.track_client_stream(stream_id)
|
|
1044
|
+
else
|
|
1045
|
+
@webtransport.for(connection_handle).reject_session(session, response.status)
|
|
587
1046
|
end
|
|
1047
|
+
end
|
|
1048
|
+
|
|
1049
|
+
def route_wt_uni_stream(connection_handle, stream_id, stream_handle, payload, fin: false)
|
|
1050
|
+
# After Connection strips the 0x54 stream type, payload is:
|
|
1051
|
+
# [session_id varint][data...]
|
|
1052
|
+
# Reuse the same prefix parser — format is identical minus the type byte.
|
|
1053
|
+
@webtransport.for(connection_handle).route_unidirectional_stream(stream_id, stream_handle, payload, fin: fin)
|
|
1054
|
+
rescue Protocol::FrameError => e
|
|
1055
|
+
Quicsilver.logger.error("WebTransport stream error: #{e.message} (0x#{e.error_code.to_s(16)})")
|
|
1056
|
+
Quicsilver.connection_shutdown(connection_handle, e.error_code, false)
|
|
1057
|
+
nil
|
|
1058
|
+
rescue => e
|
|
1059
|
+
Quicsilver.logger.error("WebTransport uni stream error: #{e.class} - #{e.message}")
|
|
1060
|
+
raise
|
|
1061
|
+
end
|
|
588
1062
|
|
|
589
|
-
|
|
1063
|
+
# Accept an incoming WebTransport stream — parse prefix and route to session
|
|
1064
|
+
def accept_webtransport_stream(connection_handle, stream_id, stream_handle, payload, fin: false)
|
|
1065
|
+
@webtransport.for(connection_handle).accept_bidi_stream(stream_id, stream_handle, payload, fin: fin)
|
|
1066
|
+
rescue Protocol::FrameError => e
|
|
1067
|
+
Quicsilver.logger.error("WebTransport stream error: #{e.message} (0x#{e.error_code.to_s(16)})")
|
|
1068
|
+
Quicsilver.connection_shutdown(connection_handle, e.error_code, false)
|
|
1069
|
+
nil
|
|
1070
|
+
rescue => e
|
|
1071
|
+
Quicsilver.logger.error("WebTransport stream error: #{e.class} - #{e.message}")
|
|
1072
|
+
raise
|
|
590
1073
|
end
|
|
591
1074
|
|
|
592
|
-
# Heuristic: check if raw data starts with an HTTP/3 HEADERS frame (type 0x01).
|
|
593
|
-
# QUIC typically delivers complete frames, but if this misidentifies data,
|
|
594
|
-
# the parser will fail safely in dispatch_streaming's rescue handlers.
|
|
595
1075
|
def contains_headers_frame?(data)
|
|
596
1076
|
return false if data.nil? || data.bytesize < 2
|
|
597
1077
|
data.getbyte(0) == Protocol::FRAME_HEADERS
|
|
598
1078
|
end
|
|
599
|
-
|
|
600
|
-
def stop_worker_pool
|
|
601
|
-
@thread_pool_size.times { @work_queue.push(:shutdown) }
|
|
602
|
-
@handler_mutex.synchronize do
|
|
603
|
-
@handler_threads.each { |t| t.join(2) }
|
|
604
|
-
# Raise into any stuck workers
|
|
605
|
-
@handler_threads.each { |t| t.raise(DrainTimeoutError, "drain timeout") if t.alive? }
|
|
606
|
-
@handler_threads.clear
|
|
607
|
-
end
|
|
608
|
-
end
|
|
609
1079
|
end
|
|
610
1080
|
end
|