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.
Files changed (107) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +49 -0
  3. data/.github/workflows/cibuildgem.yaml +117 -0
  4. data/.gitignore +21 -0
  5. data/.gitmodules +3 -0
  6. data/.ruby-version +1 -0
  7. data/CHANGELOG.md +175 -0
  8. data/Gemfile +6 -0
  9. data/Gemfile.lock +67 -0
  10. data/LICENSE +21 -0
  11. data/README.md +261 -0
  12. data/Rakefile +130 -0
  13. data/benchmarks/baseline.rb +175 -0
  14. data/benchmarks/compare.rb +398 -0
  15. data/benchmarks/components.rb +191 -0
  16. data/benchmarks/helpers.rb +162 -0
  17. data/benchmarks/latency.rb +128 -0
  18. data/benchmarks/loss.rb +193 -0
  19. data/benchmarks/mobile.rb +125 -0
  20. data/bin/console +14 -0
  21. data/bin/setup +8 -0
  22. data/docs/connection_identity.md +141 -0
  23. data/examples/README.md +58 -0
  24. data/examples/benchmark.rb +191 -0
  25. data/examples/connection_pool_demo.rb +47 -0
  26. data/examples/example_helper.rb +69 -0
  27. data/examples/falcon_middleware.rb +44 -0
  28. data/examples/feature_demo.rb +125 -0
  29. data/examples/grpc_style.rb +97 -0
  30. data/examples/latency_aware_routing.rb +382 -0
  31. data/examples/lifecycle_drain.rb +264 -0
  32. data/examples/minimal_http3_server.rb +14 -0
  33. data/examples/priorities.rb +60 -0
  34. data/examples/protocol_http_server.rb +31 -0
  35. data/examples/rack_http3_server.rb +42 -0
  36. data/examples/rails_feature_test.rb +280 -0
  37. data/examples/server_stats.rb +117 -0
  38. data/examples/simple_client_test.rb +26 -0
  39. data/examples/streaming_sse.rb +33 -0
  40. data/examples/trailers.rb +69 -0
  41. data/examples/webtransport_smoke.rb +546 -0
  42. data/examples/worker_load_balancing.rb +425 -0
  43. data/ext/quicsilver/extconf.rb +124 -0
  44. data/ext/quicsilver/include/msquic.h +2226 -0
  45. data/ext/quicsilver/include/msquic_posix.h +696 -0
  46. data/ext/quicsilver/include/quic_sal_stub.h +314 -0
  47. data/ext/quicsilver/msquic_patch.rb +68 -0
  48. data/ext/quicsilver/patches/reliable-reset.patch +723 -0
  49. data/ext/quicsilver/patches/stream-final-size.patch +47 -0
  50. data/ext/quicsilver/quicsilver.c +2741 -0
  51. data/lib/quicsilver/3.4/quicsilver.so +0 -0
  52. data/lib/quicsilver/4.0/quicsilver.so +0 -0
  53. data/lib/quicsilver/client/client.rb +708 -0
  54. data/lib/quicsilver/client/connection_pool.rb +202 -0
  55. data/lib/quicsilver/client/request.rb +190 -0
  56. data/lib/quicsilver/errors.rb +39 -0
  57. data/lib/quicsilver/libmsquic.so.2 +0 -0
  58. data/lib/quicsilver/protocol/adapter.rb +196 -0
  59. data/lib/quicsilver/protocol/capsule.rb +38 -0
  60. data/lib/quicsilver/protocol/connect_stream_decoder.rb +75 -0
  61. data/lib/quicsilver/protocol/control_stream_parser.rb +117 -0
  62. data/lib/quicsilver/protocol/datagram.rb +40 -0
  63. data/lib/quicsilver/protocol/frame_parser.rb +141 -0
  64. data/lib/quicsilver/protocol/frame_reader.rb +84 -0
  65. data/lib/quicsilver/protocol/frames.rb +454 -0
  66. data/lib/quicsilver/protocol/priority.rb +65 -0
  67. data/lib/quicsilver/protocol/qpack/decoder.rb +127 -0
  68. data/lib/quicsilver/protocol/qpack/encoder.rb +217 -0
  69. data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +140 -0
  70. data/lib/quicsilver/protocol/qpack/huffman.rb +459 -0
  71. data/lib/quicsilver/protocol/request_encoder.rb +50 -0
  72. data/lib/quicsilver/protocol/request_parser.rb +289 -0
  73. data/lib/quicsilver/protocol/response_encoder.rb +107 -0
  74. data/lib/quicsilver/protocol/response_parser.rb +147 -0
  75. data/lib/quicsilver/protocol/stream_input.rb +98 -0
  76. data/lib/quicsilver/protocol/stream_output.rb +59 -0
  77. data/lib/quicsilver/protocol/unidirectional_stream.rb +12 -0
  78. data/lib/quicsilver/protocol/web_transport.rb +214 -0
  79. data/lib/quicsilver/rack/context.rb +33 -0
  80. data/lib/quicsilver/response.rb +40 -0
  81. data/lib/quicsilver/server/listener_data.rb +14 -0
  82. data/lib/quicsilver/server/rack_adapter.rb +77 -0
  83. data/lib/quicsilver/server/receive_queue.rb +106 -0
  84. data/lib/quicsilver/server/request_handler.rb +149 -0
  85. data/lib/quicsilver/server/request_registry.rb +63 -0
  86. data/lib/quicsilver/server/scheduler.rb +50 -0
  87. data/lib/quicsilver/server/schedulers/fiber_scheduler.rb +79 -0
  88. data/lib/quicsilver/server/schedulers/thread_scheduler.rb +76 -0
  89. data/lib/quicsilver/server/server.rb +1080 -0
  90. data/lib/quicsilver/server/web_transport_flow_control.rb +270 -0
  91. data/lib/quicsilver/server/web_transport_manager.rb +281 -0
  92. data/lib/quicsilver/server/web_transport_registry.rb +40 -0
  93. data/lib/quicsilver/server/web_transport_session.rb +812 -0
  94. data/lib/quicsilver/server/web_transport_stream.rb +326 -0
  95. data/lib/quicsilver/transport/configuration.rb +234 -0
  96. data/lib/quicsilver/transport/connection.rb +594 -0
  97. data/lib/quicsilver/transport/connection_stats.rb +53 -0
  98. data/lib/quicsilver/transport/event_loop.rb +74 -0
  99. data/lib/quicsilver/transport/inbound_stream.rb +53 -0
  100. data/lib/quicsilver/transport/stream.rb +43 -0
  101. data/lib/quicsilver/transport/stream_event.rb +49 -0
  102. data/lib/quicsilver/transport/stream_id.rb +26 -0
  103. data/lib/quicsilver/version.rb +3 -0
  104. data/lib/quicsilver.rb +96 -0
  105. data/lib/rackup/handler/quicsilver.rb +96 -0
  106. data/quicsilver.gemspec +66 -0
  107. 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