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,270 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quicsilver
|
|
4
|
+
class Server
|
|
5
|
+
# The limits a peer has granted this session (draft-ietf-webtrans-http3-16
|
|
6
|
+
# §5). Session flow control sits above QUIC's own: QUIC bounds the
|
|
7
|
+
# connection and each stream, this bounds one session across all of its
|
|
8
|
+
# streams.
|
|
9
|
+
#
|
|
10
|
+
# This is what we are permitted to send. WebTransportReceiveLimits below
|
|
11
|
+
# is the mirror: what the peer may send us, against limits we advertised.
|
|
12
|
+
# The two are kept apart because the rules differ. A granted limit must
|
|
13
|
+
# increase and is never spent here; an advertised one is spent and never
|
|
14
|
+
# moves on its own.
|
|
15
|
+
#
|
|
16
|
+
# Limits start at the peer's WT_INITIAL_MAX_* settings, default 0, which
|
|
17
|
+
# means nothing can be sent until a capsule raises them (§5.5).
|
|
18
|
+
class WebTransportFlowControl
|
|
19
|
+
WT = Protocol::WebTransport
|
|
20
|
+
|
|
21
|
+
attr_reader :max_data, :max_streams_bidi, :max_streams_uni
|
|
22
|
+
attr_reader :data_sent
|
|
23
|
+
|
|
24
|
+
def self.from_settings(settings)
|
|
25
|
+
new(
|
|
26
|
+
max_data: settings[Protocol::SETTINGS_WT_INITIAL_MAX_DATA].to_i,
|
|
27
|
+
max_streams_bidi: settings[Protocol::SETTINGS_WT_INITIAL_MAX_STREAMS_BIDI].to_i,
|
|
28
|
+
max_streams_uni: settings[Protocol::SETTINGS_WT_INITIAL_MAX_STREAMS_UNI].to_i
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize(max_data: 0, max_streams_bidi: 0, max_streams_uni: 0)
|
|
33
|
+
@max_data = max_data
|
|
34
|
+
@max_streams_bidi = max_streams_bidi
|
|
35
|
+
@max_streams_uni = max_streams_uni
|
|
36
|
+
@data_sent = 0
|
|
37
|
+
@streams_opened_bidi = 0
|
|
38
|
+
@streams_opened_uni = 0
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Apply a limit the peer sent. Capsules travel on the CONNECT stream and
|
|
42
|
+
# so arrive in order, which is why a limit that does not increase is an
|
|
43
|
+
# error here where the equivalent QUIC frame would simply be stale
|
|
44
|
+
# (§5.6.2, §5.6.4).
|
|
45
|
+
#
|
|
46
|
+
# The BLOCKED capsules carry a limit too, but it is the sender telling us
|
|
47
|
+
# what it was stuck behind, not a grant, so it changes nothing.
|
|
48
|
+
def apply(limit)
|
|
49
|
+
case limit.kind
|
|
50
|
+
when :max_data then @max_data = increase!(:max_data, @max_data, limit.limit)
|
|
51
|
+
when :max_streams then apply_max_streams(limit)
|
|
52
|
+
when :data_blocked, :streams_blocked then nil
|
|
53
|
+
else raise ArgumentError, "Unknown flow control capsule kind #{limit.kind}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def max_streams(direction)
|
|
58
|
+
(direction == :bidi) ? @max_streams_bidi : @max_streams_uni
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def streams_opened(direction)
|
|
62
|
+
(direction == :bidi) ? @streams_opened_bidi : @streams_opened_uni
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# "An endpoint MUST NOT open more streams than permitted by the current
|
|
66
|
+
# stream limit set by its peer" (§5.6.2). The limit counts closed
|
|
67
|
+
# streams too, so this only ever rises.
|
|
68
|
+
def open_stream!(direction)
|
|
69
|
+
opened = streams_opened(direction)
|
|
70
|
+
limit = max_streams(direction)
|
|
71
|
+
if opened + 1 > limit
|
|
72
|
+
raise WT::SendBlocked.new(
|
|
73
|
+
"cannot open a #{direction} stream: #{opened} of #{limit} used",
|
|
74
|
+
limit: limit, needed: 1
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if direction == :bidi
|
|
79
|
+
@streams_opened_bidi += 1
|
|
80
|
+
else
|
|
81
|
+
@streams_opened_uni += 1
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# "The sum of the lengths of Stream Body data sent on all streams
|
|
86
|
+
# associated with this session MUST NOT exceed the Maximum Data value
|
|
87
|
+
# advertised by a receiver" (§5.6.4). Stream bodies only: the prefix
|
|
88
|
+
# linking a stream to its session is excluded (§5.4).
|
|
89
|
+
def send_data!(bytes)
|
|
90
|
+
return if bytes.zero?
|
|
91
|
+
|
|
92
|
+
if @data_sent + bytes > @max_data
|
|
93
|
+
raise WT::SendBlocked.new(
|
|
94
|
+
"cannot send #{bytes} bytes: #{@data_sent} of #{@max_data} used",
|
|
95
|
+
limit: @max_data, needed: bytes
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
@data_sent += bytes
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def data_remaining = @max_data - @data_sent
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def apply_max_streams(limit)
|
|
107
|
+
if limit.direction == :bidi
|
|
108
|
+
@max_streams_bidi = increase!(:max_streams_bidi, @max_streams_bidi, limit.limit)
|
|
109
|
+
else
|
|
110
|
+
@max_streams_uni = increase!(:max_streams_uni, @max_streams_uni, limit.limit)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# "If an endpoint receives a WT_MAX_DATA capsule with a Maximum Data
|
|
115
|
+
# value that does not increase the Maximum Data value previously
|
|
116
|
+
# received, it MUST close the WebTransport session with a
|
|
117
|
+
# WT_FLOW_CONTROL_ERROR error code." Equal is not an increase.
|
|
118
|
+
def increase!(name, current, value)
|
|
119
|
+
unless value > current
|
|
120
|
+
raise WT::FlowControlError,
|
|
121
|
+
"#{name} of #{value} does not increase the current limit of #{current}"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
value
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# What a peer is permitted to send us in one session, and what it has used
|
|
129
|
+
# (draft-ietf-webtrans-http3-16 §5.3, §5.4).
|
|
130
|
+
#
|
|
131
|
+
# The mirror of WebTransportFlowControl: that holds limits the peer grants
|
|
132
|
+
# us, this holds limits we granted the peer and enforces them.
|
|
133
|
+
#
|
|
134
|
+
# Limits start at the WT_INITIAL_MAX_* values we advertised and rise as we
|
|
135
|
+
# issue capsules. Counting is deliberately plain integer arithmetic: this
|
|
136
|
+
# runs on every received chunk.
|
|
137
|
+
class WebTransportReceiveLimits
|
|
138
|
+
WT = Protocol::WebTransport
|
|
139
|
+
|
|
140
|
+
attr_reader :data_received, :streams_opened_bidi, :streams_opened_uni
|
|
141
|
+
|
|
142
|
+
private def increment_closed(direction)
|
|
143
|
+
if direction == :bidi
|
|
144
|
+
@streams_closed_bidi += 1
|
|
145
|
+
else
|
|
146
|
+
@streams_closed_uni += 1
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private def set_max_streams(direction, value)
|
|
151
|
+
if direction == :bidi
|
|
152
|
+
@max_streams_bidi = value
|
|
153
|
+
else
|
|
154
|
+
@max_streams_uni = value
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def self.from_settings(settings)
|
|
159
|
+
new(
|
|
160
|
+
max_data: settings[Protocol::SETTINGS_WT_INITIAL_MAX_DATA].to_i,
|
|
161
|
+
max_streams_bidi: settings[Protocol::SETTINGS_WT_INITIAL_MAX_STREAMS_BIDI].to_i,
|
|
162
|
+
max_streams_uni: settings[Protocol::SETTINGS_WT_INITIAL_MAX_STREAMS_UNI].to_i
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Grant more once half the window has been used up, the way QUIC paces
|
|
167
|
+
# MAX_DATA (§5.6 refers to RFC 9000 §4.2). Waiting until it is exhausted
|
|
168
|
+
# would stall the peer for a round trip.
|
|
169
|
+
GRANT_THRESHOLD = 2
|
|
170
|
+
|
|
171
|
+
def initialize(max_data: 0, max_streams_bidi: 0, max_streams_uni: 0)
|
|
172
|
+
@max_data = max_data
|
|
173
|
+
@max_streams_bidi = max_streams_bidi
|
|
174
|
+
@max_streams_uni = max_streams_uni
|
|
175
|
+
@data_received = 0
|
|
176
|
+
@streams_opened_bidi = 0
|
|
177
|
+
@streams_opened_uni = 0
|
|
178
|
+
@data_consumed = 0
|
|
179
|
+
@streams_closed_bidi = 0
|
|
180
|
+
@streams_closed_uni = 0
|
|
181
|
+
# The windows we advertised are the increment we extend by.
|
|
182
|
+
@data_window = max_data
|
|
183
|
+
@stream_window_bidi = max_streams_bidi
|
|
184
|
+
@stream_window_uni = max_streams_uni
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
attr_reader :data_consumed, :max_data, :max_streams_bidi, :max_streams_uni
|
|
188
|
+
|
|
189
|
+
# The application read `bytes`, so that much of the window is free
|
|
190
|
+
# again. Returns the new limit to advertise, or nil when it is not yet
|
|
191
|
+
# worth a capsule. "Endpoints SHOULD send WT_MAX_DATA... as they consume
|
|
192
|
+
# data" (§5.6).
|
|
193
|
+
def consume_data(bytes)
|
|
194
|
+
return if bytes.zero? || @data_window.zero?
|
|
195
|
+
|
|
196
|
+
@data_consumed += bytes
|
|
197
|
+
extended = @data_consumed + @data_window
|
|
198
|
+
return unless extended - @max_data >= @data_window / GRANT_THRESHOLD
|
|
199
|
+
|
|
200
|
+
@max_data = extended
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# A stream closed, so the session can afford another. Returns the new
|
|
204
|
+
# limit to advertise, or nil.
|
|
205
|
+
def close_stream(direction)
|
|
206
|
+
window = (direction == :bidi) ? @stream_window_bidi : @stream_window_uni
|
|
207
|
+
return if window.zero?
|
|
208
|
+
|
|
209
|
+
closed = increment_closed(direction)
|
|
210
|
+
extended = closed + window
|
|
211
|
+
current = max_streams(direction)
|
|
212
|
+
return unless extended - current >= [window / GRANT_THRESHOLD, 1].max
|
|
213
|
+
|
|
214
|
+
set_max_streams(direction, extended)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def max_streams(direction)
|
|
218
|
+
(direction == :bidi) ? @max_streams_bidi : @max_streams_uni
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# "If an endpoint receives an incoming stream for a session that would
|
|
222
|
+
# exceed the advertised Maximum Streams value, it MUST close the
|
|
223
|
+
# WebTransport session with a WT_FLOW_CONTROL_ERROR error code" (§5.6.2).
|
|
224
|
+
#
|
|
225
|
+
# The limit counts closed streams as well as open ones, so this only ever
|
|
226
|
+
# goes up. The CONNECT stream itself is not included (§5.3).
|
|
227
|
+
def open_stream!(direction)
|
|
228
|
+
if direction == :bidi
|
|
229
|
+
@streams_opened_bidi += 1
|
|
230
|
+
exceeded!(:max_streams_bidi, @streams_opened_bidi, @max_streams_bidi)
|
|
231
|
+
else
|
|
232
|
+
@streams_opened_uni += 1
|
|
233
|
+
exceeded!(:max_streams_uni, @streams_opened_uni, @max_streams_uni)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# "The sum of the lengths of Stream Body data sent on all streams
|
|
238
|
+
# associated with this session MUST NOT exceed the Maximum Data value
|
|
239
|
+
# advertised by a receiver... If an endpoint receives Stream Body data in
|
|
240
|
+
# excess of this limit, it MUST close the WebTransport session with a
|
|
241
|
+
# WT_FLOW_CONTROL_ERROR error code" (§5.6.4).
|
|
242
|
+
#
|
|
243
|
+
# Body only: the signal value, stream type and session ID are excluded so
|
|
244
|
+
# that linking a stream to its session never costs credit (§5.4).
|
|
245
|
+
def receive_data!(bytes)
|
|
246
|
+
return if bytes.zero?
|
|
247
|
+
|
|
248
|
+
@data_received += bytes
|
|
249
|
+
exceeded!(:max_data, @data_received, @max_data)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# A reset stream consumed credit for everything its sender counted, not
|
|
253
|
+
# just what reached us. Without charging the difference the two endpoints
|
|
254
|
+
# disagree about the session total (§5.4, RFC 9000 §4.5).
|
|
255
|
+
def reset_stream!(final_size, delivered)
|
|
256
|
+
return if final_size.nil? || final_size <= delivered
|
|
257
|
+
|
|
258
|
+
receive_data!(final_size - delivered)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
private
|
|
262
|
+
|
|
263
|
+
def exceeded!(name, used, limit)
|
|
264
|
+
return if used <= limit
|
|
265
|
+
|
|
266
|
+
raise WT::FlowControlError, "#{name} exceeded: #{used} of #{limit}"
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quicsilver
|
|
4
|
+
class Server
|
|
5
|
+
# Tracks WebTransport sessions and routes connection-level stream events.
|
|
6
|
+
#
|
|
7
|
+
# WebTransport child streams arrive at the QUIC/HTTP3 connection before they
|
|
8
|
+
# can be associated with a session. The stream prefix carries the session ID,
|
|
9
|
+
# and that prefix can be split across receives. This object owns that
|
|
10
|
+
# cross-session routing state so Server can stay focused on HTTP/3 dispatch.
|
|
11
|
+
#
|
|
12
|
+
# Scoped to a single connection — see WebTransportRegistry.
|
|
13
|
+
class WebTransportManager
|
|
14
|
+
MAX_PENDING_CONNECT_BYTES = 65_536
|
|
15
|
+
ConnectRequest = Struct.new(:stream_id, :stream_handle, :headers, :data, :early_data, :fin, keyword_init: true)
|
|
16
|
+
|
|
17
|
+
def initialize
|
|
18
|
+
@sessions = {}
|
|
19
|
+
@pending_streams = {}
|
|
20
|
+
@stream_states = {}
|
|
21
|
+
@starting_streams = {}
|
|
22
|
+
@pending_uni_streams = {}
|
|
23
|
+
@pending_connect = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def sessions
|
|
27
|
+
@sessions.values
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def register(session)
|
|
31
|
+
session.stream_manager = self
|
|
32
|
+
@sessions[session.stream_id] = session
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def unregister(stream_id)
|
|
36
|
+
@pending_connect = nil if @pending_connect&.stream_id == stream_id
|
|
37
|
+
@sessions.delete(stream_id)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def defer_connect(request)
|
|
41
|
+
if @pending_connect || routable_sessions.any?
|
|
42
|
+
return reject_stream(request.stream_id, request.stream_handle, error_code: Protocol::H3_REQUEST_REJECTED)
|
|
43
|
+
end
|
|
44
|
+
@pending_connect = request
|
|
45
|
+
@stream_states[request.stream_id] = :pending_connect
|
|
46
|
+
receive_pending_connect("", fin: request.fin)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def take_pending_connect
|
|
50
|
+
request = @pending_connect
|
|
51
|
+
@pending_connect = nil
|
|
52
|
+
@stream_states.delete(request.stream_id) if request
|
|
53
|
+
request
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def cancel_pending_connect(stream_id, stream_handle)
|
|
57
|
+
return false unless @pending_connect&.stream_id == stream_id
|
|
58
|
+
|
|
59
|
+
reject_stream(stream_id, stream_handle, error_code: Protocol::H3_REQUEST_CANCELLED)
|
|
60
|
+
true
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def session(stream_id)
|
|
64
|
+
session = @sessions[stream_id]
|
|
65
|
+
return session if session&.routable?
|
|
66
|
+
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def routable_sessions
|
|
71
|
+
@sessions.each_value.select(&:routable?)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def open_session
|
|
75
|
+
@sessions.each_value.find(&:open?)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def active_stream(stream_id)
|
|
79
|
+
owning_session(stream_id)&.stream(stream_id)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def session_for_stream(stream_id)
|
|
83
|
+
session = owning_session(stream_id)
|
|
84
|
+
session if session&.stream(stream_id)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def route_owned_stream(stream_id, stream_handle, payload, fin: false)
|
|
88
|
+
if @pending_connect&.stream_id == stream_id
|
|
89
|
+
receive_pending_connect(payload, fin: fin)
|
|
90
|
+
elsif (session = @sessions[stream_id])
|
|
91
|
+
session.receive_connect_stream_data(payload, fin: fin)
|
|
92
|
+
elsif known_stream?(stream_id)
|
|
93
|
+
if (stream = active_stream(stream_id))
|
|
94
|
+
stream.replace_stream_handle(stream_handle) if fin && stream_handle
|
|
95
|
+
fin ? stream.receive_fin(payload) : stream.receive_data(payload)
|
|
96
|
+
end
|
|
97
|
+
else
|
|
98
|
+
return false
|
|
99
|
+
end
|
|
100
|
+
true
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def register_starting_stream(stream)
|
|
104
|
+
@starting_streams[stream.stream_handle] = stream
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def discard_starting_stream(handle)
|
|
108
|
+
@starting_streams.delete(handle)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def stream_started(stream_id, handle)
|
|
112
|
+
return unless stream_id && stream_id.between?(0, (1 << 62) - 1)
|
|
113
|
+
return unless (stream = @starting_streams.delete(handle))
|
|
114
|
+
|
|
115
|
+
stream.session.stream_started(stream, stream_id)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def register_stream(stream_id, session_id)
|
|
119
|
+
@stream_states[stream_id] = session_id
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def known_stream?(stream_id)
|
|
123
|
+
@stream_states.key?(stream_id)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def rejected_stream?(stream_id)
|
|
127
|
+
@stream_states[stream_id] == :rejected
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def reject_session(session, status)
|
|
131
|
+
unregister(session.stream_id)
|
|
132
|
+
# Keep routing late data and FIN here until the CONNECT stream shuts down.
|
|
133
|
+
@stream_states[session.stream_id] = :rejected
|
|
134
|
+
session.reject!(status)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def reject_stream(stream_id, stream_handle, error_code: Protocol::WebTransport::BUFFERED_STREAM_REJECTED)
|
|
138
|
+
@pending_connect = nil if @pending_connect&.stream_id == stream_id
|
|
139
|
+
@pending_streams.delete(stream_id)
|
|
140
|
+
@pending_uni_streams.delete(stream_id)
|
|
141
|
+
@stream_states[stream_id] = :rejected
|
|
142
|
+
Transport::Stream.new(stream_handle).abort(error_code)
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def stream_shutdown_complete(stream_id, handle: nil)
|
|
147
|
+
@pending_connect = nil if @pending_connect&.stream_id == stream_id
|
|
148
|
+
starting = @starting_streams.delete(handle)
|
|
149
|
+
starting&.session&.remove_starting_stream(handle)
|
|
150
|
+
@pending_uni_streams.delete(stream_id)
|
|
151
|
+
@pending_streams.delete(stream_id)
|
|
152
|
+
owner = session_for_stream(stream_id)
|
|
153
|
+
known = @stream_states.delete(stream_id) || starting
|
|
154
|
+
if (session = @sessions.delete(stream_id))
|
|
155
|
+
session.notify_close
|
|
156
|
+
return true
|
|
157
|
+
end
|
|
158
|
+
return !!known unless owner
|
|
159
|
+
|
|
160
|
+
owner.remove_stream(stream_id)
|
|
161
|
+
true
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def pending_stream?(stream_id)
|
|
165
|
+
@pending_streams.key?(stream_id)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def pending_payload(stream_id, stream_handle, payload)
|
|
169
|
+
pending = @pending_streams[stream_id]
|
|
170
|
+
if pending
|
|
171
|
+
pending[:buffer] << payload
|
|
172
|
+
payload = pending[:buffer]
|
|
173
|
+
stream_handle = pending[:handle]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
case bidi_prefix_state(payload)
|
|
177
|
+
when :matched
|
|
178
|
+
@pending_streams.delete(stream_id)
|
|
179
|
+
payload
|
|
180
|
+
when :incomplete
|
|
181
|
+
@pending_streams[stream_id] ||= { handle: stream_handle, buffer: "".b }
|
|
182
|
+
@pending_streams[stream_id][:buffer] << payload unless pending
|
|
183
|
+
nil
|
|
184
|
+
else
|
|
185
|
+
@pending_streams.delete(stream_id)
|
|
186
|
+
nil
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def bidi_stream?(payload)
|
|
191
|
+
bidi_prefix_state(payload) == :matched
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def accept_bidi_stream(stream_id, stream_handle, payload, fin: false)
|
|
195
|
+
session_id, = WebTransportSession.parse_stream_prefix(payload)
|
|
196
|
+
return reject_stream(stream_id, stream_handle) unless session_id && @sessions[session_id]&.accepts_new_streams?
|
|
197
|
+
|
|
198
|
+
WebTransportSession.accept_stream(@sessions, stream_id, stream_handle, payload, fin: fin)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def route_unidirectional_stream(stream_id, stream_handle, payload, fin: false)
|
|
202
|
+
if (stream = active_stream(stream_id))
|
|
203
|
+
fin ? stream.receive_fin(payload) : stream.receive_data(payload)
|
|
204
|
+
return stream
|
|
205
|
+
end
|
|
206
|
+
return if known_stream?(stream_id)
|
|
207
|
+
|
|
208
|
+
payload = @pending_uni_streams.delete(stream_id).to_s.b + payload
|
|
209
|
+
_, length = Protocol.decode_varint_str(payload, 0)
|
|
210
|
+
if length == 0
|
|
211
|
+
return reject_stream(stream_id, stream_handle) if fin
|
|
212
|
+
|
|
213
|
+
@pending_uni_streams[stream_id] = payload
|
|
214
|
+
return
|
|
215
|
+
end
|
|
216
|
+
session_id, initial_data = WebTransportSession.parse_uni_stream_data(payload)
|
|
217
|
+
session = @sessions[session_id]
|
|
218
|
+
return reject_stream(stream_id, stream_handle) unless session&.accepts_new_streams?
|
|
219
|
+
|
|
220
|
+
stream = session.add_uni_stream(stream_handle, stream_id)
|
|
221
|
+
fin ? stream.receive_fin(initial_data) : stream.receive_data(initial_data)
|
|
222
|
+
stream
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def receive_datagram(datagram)
|
|
226
|
+
stream_id, payload = Protocol::Datagram.decode(datagram)
|
|
227
|
+
return false unless (session = @sessions[stream_id])
|
|
228
|
+
return false unless session.open?
|
|
229
|
+
|
|
230
|
+
session.receive_datagram(payload)
|
|
231
|
+
true
|
|
232
|
+
rescue
|
|
233
|
+
false
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def build_datagram(session, payload)
|
|
237
|
+
Protocol::Datagram.encode(session.stream_id, payload)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
private
|
|
241
|
+
|
|
242
|
+
def receive_pending_connect(payload, fin:)
|
|
243
|
+
request = @pending_connect
|
|
244
|
+
if request.data.bytesize + payload.bytesize > MAX_PENDING_CONNECT_BYTES
|
|
245
|
+
return reject_stream(request.stream_id, request.stream_handle, error_code: Protocol::H3_EXCESSIVE_LOAD)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
request.data << payload
|
|
249
|
+
request.fin ||= fin
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def owning_session(stream_id)
|
|
253
|
+
session_id = @stream_states[stream_id]
|
|
254
|
+
return unless session_id.is_a?(Integer)
|
|
255
|
+
|
|
256
|
+
session(session_id)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def bidi_prefix_state(data)
|
|
260
|
+
type, type_len = Protocol.decode_varint_str(data, 0)
|
|
261
|
+
return :incomplete if type_len == 0 && incomplete_varint?(data, 0)
|
|
262
|
+
return :no_match unless type == WebTransportSession::WT_STREAM_BIDI && type_len > 0
|
|
263
|
+
|
|
264
|
+
_, sid_len = Protocol.decode_varint_str(data, type_len)
|
|
265
|
+
return :incomplete if sid_len == 0
|
|
266
|
+
|
|
267
|
+
:matched
|
|
268
|
+
rescue
|
|
269
|
+
:no_match
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def incomplete_varint?(data, offset)
|
|
273
|
+
first = data.getbyte(offset)
|
|
274
|
+
return false unless first
|
|
275
|
+
|
|
276
|
+
length = 1 << ((first & 0xC0) >> 6)
|
|
277
|
+
data.bytesize - offset < length
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "web_transport_manager"
|
|
4
|
+
|
|
5
|
+
module Quicsilver
|
|
6
|
+
class Server
|
|
7
|
+
# One WebTransportManager per QUIC connection.
|
|
8
|
+
#
|
|
9
|
+
# A session is identified by its CONNECT stream id, and QUIC stream ids
|
|
10
|
+
# restart on every connection, so two clients routinely both have a session
|
|
11
|
+
# on stream 0. Sharing one manager across connections routed streams into
|
|
12
|
+
# the wrong client's session.
|
|
13
|
+
class WebTransportRegistry
|
|
14
|
+
def initialize
|
|
15
|
+
@managers = {}
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# The manager owning this connection's sessions, created on first use.
|
|
20
|
+
def for(connection_handle)
|
|
21
|
+
@mutex.synchronize do
|
|
22
|
+
@managers[connection_handle] ||= WebTransportManager.new
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Forget a connection, returning its sessions so the caller can notify
|
|
27
|
+
# them.
|
|
28
|
+
def drop(connection_handle)
|
|
29
|
+
manager = @mutex.synchronize { @managers.delete(connection_handle) }
|
|
30
|
+
return [] unless manager
|
|
31
|
+
|
|
32
|
+
manager.sessions
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def connection_count
|
|
36
|
+
@mutex.synchronize { @managers.size }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|