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,454 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quicsilver
4
+ module Protocol
5
+ # HTTP/3 Frame Types (RFC 9114)
6
+ FRAME_DATA = 0x00
7
+ FRAME_HEADERS = 0x01
8
+ FRAME_CANCEL_PUSH = 0x03
9
+ FRAME_SETTINGS = 0x04
10
+ FRAME_PUSH_PROMISE = 0x05
11
+ FRAME_GOAWAY = 0x07
12
+ FRAME_MAX_PUSH_ID = 0x0d
13
+ FRAME_PRIORITY_UPDATE = 0xF0700 # RFC 9218 — request stream priority update
14
+
15
+ # Settings identifiers
16
+ SETTINGS_QPACK_MAX_TABLE_CAPACITY = 0x01
17
+ SETTINGS_QPACK_BLOCKED_STREAMS = 0x07
18
+ SETTINGS_ENABLE_CONNECT_PROTOCOL = 0x08 # RFC 9220
19
+ SETTINGS_H3_DATAGRAM = 0x33 # RFC 9297
20
+ SETTINGS_ENABLE_WEBTRANSPORT = 0x2b603742 # WebTransport draft-06
21
+ SETTINGS_WT_ENABLED = 0x2c7cf000 # WebTransport over HTTP/3
22
+ SETTINGS_WT_MAX_SESSIONS = 0x14e9cd29 # Legacy WebTransport session limit
23
+ SETTINGS_WT_INITIAL_MAX_DATA = 0x2b61
24
+ SETTINGS_WT_INITIAL_MAX_STREAMS_UNI = 0x2b64
25
+ SETTINGS_WT_INITIAL_MAX_STREAMS_BIDI = 0x2b65
26
+
27
+ # Declaring a non-zero value for any of these is how an endpoint opts into
28
+ # WebTransport session flow control (draft-16 §5.1). It is a promise to
29
+ # implement the whole of §5, so do not advertise one until the capsules
30
+ # and accounting exist.
31
+ # The session flow control window we advertise. 16 MiB leaves room for
32
+ # many children above the 1 MiB each queues, and 100 streams per direction
33
+ # bounds concurrency without being a limit an application trips over.
34
+ # Both are our choice; the draft only defines the mechanism.
35
+ WT_INITIAL_MAX_DATA = 16 * 1024 * 1024
36
+ WT_INITIAL_MAX_STREAMS = 100
37
+
38
+ WT_FLOW_CONTROL_SETTINGS = [
39
+ SETTINGS_WT_INITIAL_MAX_DATA,
40
+ SETTINGS_WT_INITIAL_MAX_STREAMS_UNI,
41
+ SETTINGS_WT_INITIAL_MAX_STREAMS_BIDI
42
+ ].freeze
43
+ SETTINGS_MAX_FIELD_SECTION_SIZE = 0x06 # RFC 9114 §7.2.4.1
44
+
45
+ # Frame types forbidden on request streams (RFC 9114 Section 7.2.4, 7.2.6, 7.2.7)
46
+ CONTROL_ONLY_FRAMES = [FRAME_CANCEL_PUSH, FRAME_SETTINGS, FRAME_GOAWAY, FRAME_MAX_PUSH_ID].freeze
47
+ CONTROL_ONLY_FRAME_SET = CONTROL_ONLY_FRAMES.to_h { |type| [type, true] }.freeze
48
+
49
+ # HTTP/2 frame types, reserved in HTTP/3 so that an HTTP/2 frame arriving
50
+ # here is rejected rather than mistaken for an extension (RFC 9114 7.2.8)
51
+ HTTP2_RESERVED_FRAME_SET = {0x02 => true, 0x06 => true, 0x08 => true, 0x09 => true}.freeze
52
+
53
+ # WT_STREAM is registered as a frame type to reserve the code point, but it
54
+ # carries no length and is only legal as the very first bytes of a request
55
+ # stream, where the server classifies it before any frame parsing. Anywhere
56
+ # else it is a connection error (draft-ietf-webtrans-http3-16 §4.3).
57
+ FRAME_WT_STREAM = 0x41
58
+ # HTTP/3 Datagram Error Code (RFC 9297 Section 5.2)
59
+ H3_DATAGRAM_ERROR = 0x33
60
+
61
+ # HTTP/3 Error Codes (RFC 9114 Section 8.1)
62
+ H3_NO_ERROR = 0x100
63
+ H3_GENERAL_PROTOCOL_ERROR = 0x101
64
+ H3_INTERNAL_ERROR = 0x102
65
+ H3_STREAM_CREATION_ERROR = 0x103
66
+ H3_CLOSED_CRITICAL_STREAM = 0x104
67
+ H3_FRAME_UNEXPECTED = 0x105
68
+ H3_FRAME_ERROR = 0x106
69
+ H3_EXCESSIVE_LOAD = 0x107
70
+ H3_ID_ERROR = 0x108
71
+ H3_SETTINGS_ERROR = 0x109
72
+ H3_MISSING_SETTINGS = 0x10a
73
+ H3_REQUEST_REJECTED = 0x10b
74
+ H3_REQUEST_CANCELLED = 0x10c
75
+ H3_REQUEST_INCOMPLETE = 0x10d
76
+ H3_MESSAGE_ERROR = 0x10e
77
+ H3_CONNECT_ERROR = 0x10f
78
+ H3_VERSION_FALLBACK = 0x110
79
+
80
+ # QPACK Error Codes (RFC 9204 Section 6)
81
+ QPACK_DECOMPRESSION_FAILED = 0x200
82
+ QPACK_ENCODER_STREAM_ERROR = 0x201
83
+ QPACK_DECODER_STREAM_ERROR = 0x202
84
+
85
+ # Protocol errors that carry an HTTP/3 error code for CONNECTION_CLOSE / RESET_STREAM.
86
+ # FrameError → connection error (CONNECTION_CLOSE)
87
+ # MessageError → stream error (RESET_STREAM) on request streams
88
+ class FrameError < Quicsilver::Error
89
+ attr_reader :error_code
90
+ # The default suits a frame that is valid but arrived somewhere it is not
91
+ # allowed (RFC 9114 4.1, 7.2.4), which is the common case. Anything else
92
+ # must pass its own code: callers that leave this implicit are asserting
93
+ # H3_FRAME_UNEXPECTED is correct, and the handler turns that into a
94
+ # connection error.
95
+ def initialize(msg = nil, error_code: H3_FRAME_UNEXPECTED)
96
+ @error_code = error_code
97
+ super(msg)
98
+ end
99
+ end
100
+
101
+ class MessageError < Quicsilver::Error
102
+ attr_reader :error_code
103
+ def initialize(msg = nil, error_code: H3_MESSAGE_ERROR)
104
+ @error_code = error_code
105
+ super(msg)
106
+ end
107
+ end
108
+
109
+ # QPACK Static Table Indices (RFC 9204 Appendix A)
110
+ STATIC_TABLE = [
111
+ [':authority', ''], # 0
112
+ [':path', '/'], # 1
113
+ ['age', '0'], # 2
114
+ ['content-disposition', ''], # 3
115
+ ['content-length', '0'], # 4
116
+ ['cookie', ''], # 5
117
+ ['date', ''], # 6
118
+ ['etag', ''], # 7
119
+ ['if-modified-since', ''], # 8
120
+ ['if-none-match', ''], # 9
121
+ ['last-modified', ''], # 10
122
+ ['link', ''], # 11
123
+ ['location', ''], # 12
124
+ ['referer', ''], # 13
125
+ ['set-cookie', ''], # 14
126
+ [':method', 'CONNECT'], # 15
127
+ [':method', 'DELETE'], # 16
128
+ [':method', 'GET'], # 17
129
+ [':method', 'HEAD'], # 18
130
+ [':method', 'OPTIONS'], # 19
131
+ [':method', 'POST'], # 20
132
+ [':method', 'PUT'], # 21
133
+ [':scheme', 'http'], # 22
134
+ [':scheme', 'https'], # 23
135
+ [':status', '103'], # 24
136
+ [':status', '200'], # 25
137
+ [':status', '304'], # 26
138
+ [':status', '404'], # 27
139
+ [':status', '503'], # 28
140
+ ['accept', '*/*'], # 29
141
+ ['accept', 'application/dns-message'], # 30
142
+ ['accept-encoding', 'gzip, deflate, br'], # 31
143
+ ['accept-ranges', 'bytes'], # 32
144
+ ['access-control-allow-headers', 'cache-control'], # 33
145
+ ['access-control-allow-headers', 'content-type'], # 34
146
+ ['access-control-allow-origin', '*'], # 35
147
+ ['cache-control', 'max-age=0'], # 36
148
+ ['cache-control', 'max-age=2592000'], # 37
149
+ ['cache-control', 'max-age=604800'], # 38
150
+ ['cache-control', 'no-cache'], # 39
151
+ ['cache-control', 'no-store'], # 40
152
+ ['cache-control', 'public, max-age=31536000'], # 41
153
+ ['content-encoding', 'br'], # 42
154
+ ['content-encoding', 'gzip'], # 43
155
+ ['content-type', 'application/dns-message'], # 44
156
+ ['content-type', 'application/javascript'], # 45
157
+ ['content-type', 'application/json'], # 46
158
+ ['content-type', 'application/x-www-form-urlencoded'], # 47
159
+ ['content-type', 'image/gif'], # 48
160
+ ['content-type', 'image/jpeg'], # 49
161
+ ['content-type', 'image/png'], # 50
162
+ ['content-type', 'text/css'], # 51
163
+ ['content-type', 'text/html; charset=utf-8'], # 52
164
+ ['content-type', 'text/plain'], # 53
165
+ ['content-type', 'text/plain;charset=utf-8'], # 54
166
+ ['range', 'bytes=0-'], # 55
167
+ ['strict-transport-security', 'max-age=31536000'], # 56
168
+ ['strict-transport-security', 'max-age=31536000; includeSubDomains'], # 57
169
+ ['strict-transport-security', 'max-age=31536000; includeSubDomains; preload'], # 58
170
+ ['vary', 'accept-encoding'], # 59
171
+ ['vary', 'origin'], # 60
172
+ ['x-content-type-options', 'nosniff'], # 61
173
+ ['x-xss-protection', '1; mode=block'], # 62
174
+ [':status', '100'], # 63
175
+ [':status', '204'], # 64
176
+ [':status', '206'], # 65
177
+ [':status', '302'], # 66
178
+ [':status', '400'], # 67
179
+ [':status', '403'], # 68
180
+ [':status', '421'], # 69
181
+ [':status', '425'], # 70
182
+ [':status', '500'], # 71
183
+ ['accept-language', ''], # 72
184
+ ['access-control-allow-credentials', 'FALSE'], # 73
185
+ ['access-control-allow-credentials', 'TRUE'], # 74
186
+ ['access-control-allow-headers', '*'], # 75
187
+ ['access-control-allow-methods', 'get'], # 76
188
+ ['access-control-allow-methods', 'get, post, options'], # 77
189
+ ['access-control-allow-methods', 'options'], # 78
190
+ ['access-control-expose-headers', 'content-length'], # 79
191
+ ['access-control-request-headers', 'content-type'], # 80
192
+ ['access-control-request-method', 'get'], # 81
193
+ ['access-control-request-method', 'post'], # 82
194
+ ['alt-svc', 'clear'], # 83
195
+ ['authorization', ''], # 84
196
+ ['content-security-policy', "script-src 'none'; object-src 'none'; base-uri 'none'"], # 85
197
+ ['early-data', '1'], # 86
198
+ ['expect-ct', ''], # 87
199
+ ['forwarded', ''], # 88
200
+ ['if-range', ''], # 89
201
+ ['origin', ''], # 90
202
+ ['purpose', 'prefetch'], # 91
203
+ ['server', ''], # 92
204
+ ['timing-allow-origin', '*'], # 93
205
+ ['upgrade-insecure-requests', '1'], # 94
206
+ ['user-agent', ''], # 95
207
+ ['x-forwarded-for', ''], # 96
208
+ ['x-frame-options', 'deny'], # 97
209
+ ['x-frame-options', 'sameorigin'] # 98
210
+ ].freeze
211
+
212
+ # Commonly used indices
213
+ QPACK_AUTHORITY = 0
214
+ QPACK_PATH = 1
215
+ QPACK_CONTENT_LENGTH = 4
216
+ QPACK_METHOD_CONNECT = 15
217
+ QPACK_METHOD_DELETE = 16
218
+ QPACK_METHOD_GET = 17
219
+ QPACK_METHOD_HEAD = 18
220
+ QPACK_METHOD_OPTIONS = 19
221
+ QPACK_METHOD_POST = 20
222
+ QPACK_METHOD_PUT = 21
223
+ QPACK_SCHEME_HTTP = 22
224
+ QPACK_SCHEME_HTTPS = 23
225
+ QPACK_STATUS_200 = 25
226
+ QPACK_STATUS_404 = 27
227
+ QPACK_STATUS_500 = 71
228
+ QPACK_STATUS_400 = 67
229
+ QPACK_CONTENT_TYPE_JSON = 46
230
+ QPACK_CONTENT_TYPE_PLAIN = 53
231
+
232
+ # Maximum stream ID for initial GOAWAY (2^62 - 4, per RFC 9114)
233
+ MAX_STREAM_ID = (2**62) - 4
234
+
235
+ class << self
236
+ # Frame types a request stream may carry. Unknown types are extensions and
237
+ # must be ignored (RFC 9114 9), so only the two reserved sets raise.
238
+ # Both the buffered parser and the streaming body reader validate here.
239
+ def reject_unexpected_request_frame!(type)
240
+ if CONTROL_ONLY_FRAME_SET.key?(type)
241
+ raise FrameError, "Frame type 0x#{type.to_s(16)} not allowed on request streams"
242
+ end
243
+
244
+ if HTTP2_RESERVED_FRAME_SET.key?(type)
245
+ raise FrameError.new("Reserved HTTP/2 frame type 0x#{type.to_s(16)} not allowed in HTTP/3",
246
+ error_code: H3_FRAME_UNEXPECTED)
247
+ end
248
+
249
+ # Reaching here means the stream already parsed as HTTP/3, so this
250
+ # WT_STREAM is not at the front of the stream (draft-16 §4.3).
251
+ if type == FRAME_WT_STREAM
252
+ raise FrameError.new("WT_STREAM is only allowed as the first bytes of a request stream",
253
+ error_code: H3_FRAME_ERROR)
254
+ end
255
+ end
256
+
257
+ # Precomputed varint encodings for single-byte values (0-63)
258
+ VARINT_SMALL = Array.new(64) { |v| [v].pack('C').freeze }.freeze
259
+ VARINT_MED = Array.new(16384 - 64) { |i| v = i + 64; [0x40 | (v >> 8), v & 0xFF].pack('C*').freeze }.freeze
260
+
261
+ # Cache for large varint encodings (values >= 16384)
262
+ VARINT_LARGE_CACHE = {}
263
+ VARINT_LARGE_CACHE_MAX = 256
264
+
265
+ # Encode variable-length integer
266
+ def encode_varint(value)
267
+ if value < 64
268
+ VARINT_SMALL[value]
269
+ elsif value < 16384
270
+ VARINT_MED[value - 64]
271
+ else
272
+ cached = VARINT_LARGE_CACHE[value]
273
+ return cached if cached
274
+
275
+ result = if value < 1073741824
276
+ [0x80 | (value >> 24), (value >> 16) & 0xFF,
277
+ (value >> 8) & 0xFF, value & 0xFF].pack('C*').freeze
278
+ else
279
+ [0xC0 | (value >> 56), (value >> 48) & 0xFF,
280
+ (value >> 40) & 0xFF, (value >> 32) & 0xFF,
281
+ (value >> 24) & 0xFF, (value >> 16) & 0xFF,
282
+ (value >> 8) & 0xFF, value & 0xFF].pack('C*').freeze
283
+ end
284
+
285
+ VARINT_LARGE_CACHE[value] = result if VARINT_LARGE_CACHE.size < VARINT_LARGE_CACHE_MAX
286
+ result
287
+ end
288
+ end
289
+
290
+ def build_settings_frame(settings = {})
291
+ payload = ""
292
+ settings.each do |id, value|
293
+ payload += encode_varint(id)
294
+ payload += encode_varint(value)
295
+ end
296
+
297
+ frame_type = encode_varint(FRAME_SETTINGS)
298
+ frame_length = encode_varint(payload.bytesize)
299
+
300
+ frame_type + frame_length + payload
301
+ end
302
+
303
+ # Generate a random GREASE ID (RFC 9114 §7.2.4.1): 0x1f * N + 0x21
304
+ def grease_id
305
+ 31 * rand(0..20) + 33
306
+ end
307
+
308
+ # Has this endpoint declared intent to use session flow control? Both
309
+ # endpoints must, for it to be enabled.
310
+ def wt_flow_control_opt_in?(settings)
311
+ WT_FLOW_CONTROL_SETTINGS.any? { |id| settings[id].to_i > 0 }
312
+ end
313
+
314
+ # The SETTINGS we advertise. Kept separate from the frame so a connection
315
+ # can ask what it promised the peer without re-parsing the wire.
316
+ #
317
+ # @param max_field_section_size [Integer, nil] Advertise SETTINGS_MAX_FIELD_SECTION_SIZE (0x06)
318
+ # to the peer (RFC 9114 §4.2.2 / §7.2.4.1). nil = don't advertise.
319
+ def control_stream_settings(max_field_section_size: nil, datagram_receive_enabled: true)
320
+ settings = {
321
+ SETTINGS_QPACK_MAX_TABLE_CAPACITY => 0,
322
+ SETTINGS_QPACK_BLOCKED_STREAMS => 0,
323
+ SETTINGS_ENABLE_CONNECT_PROTOCOL => 1,
324
+ SETTINGS_H3_DATAGRAM => datagram_receive_enabled ? 1 : 0,
325
+ SETTINGS_ENABLE_WEBTRANSPORT => 1,
326
+ SETTINGS_WT_ENABLED => 1,
327
+ # Declaring these enables session flow control when the peer also
328
+ # does (§5.1): the initial limits a session grants its peer, raised
329
+ # by capsules as we consume data and close streams.
330
+ #
331
+ # The data window sits well above the 1 MiB a single child stream
332
+ # queues, so one stream cannot exhaust a session's credit, in the
333
+ # same spirit as QUIC keeping its connection window above any one
334
+ # stream's. Both values are ours to choose; the draft defines only
335
+ # the mechanism.
336
+ SETTINGS_WT_INITIAL_MAX_DATA => WT_INITIAL_MAX_DATA,
337
+ SETTINGS_WT_INITIAL_MAX_STREAMS_BIDI => WT_INITIAL_MAX_STREAMS,
338
+ SETTINGS_WT_INITIAL_MAX_STREAMS_UNI => WT_INITIAL_MAX_STREAMS,
339
+ # Still one session per connection. Flow control now permits more
340
+ # (§5.1), but sharing a connection between sessions is a separate
341
+ # change.
342
+ SETTINGS_WT_MAX_SESSIONS => 1
343
+ }
344
+ settings[SETTINGS_MAX_FIELD_SECTION_SIZE] = max_field_section_size if max_field_section_size
345
+ settings
346
+ end
347
+
348
+ # Build control stream data.
349
+ def build_control_stream(max_field_section_size: nil, datagram_receive_enabled: true)
350
+ stream_type = [0x00].pack("C") # Control stream type
351
+ settings_hash = control_stream_settings(
352
+ max_field_section_size: max_field_section_size,
353
+ datagram_receive_enabled: datagram_receive_enabled
354
+ )
355
+ settings_hash[grease_id] = grease_id # GREASE setting (RFC 9114 §7.2.4.1)
356
+
357
+ stream_type + build_settings_frame(settings_hash)
358
+ end
359
+
360
+ # Build GOAWAY frame (RFC 9114 Section 7.2.6)
361
+ # stream_id: The last client-initiated bidirectional stream ID the server will process
362
+ # Build an HTTP/3 frame: varint type + varint length + payload.
363
+ def build_frame(type, payload)
364
+ payload = payload.to_s.b
365
+ encode_varint(type) + encode_varint(payload.bytesize) + payload
366
+ end
367
+
368
+ # Build a HEADERS frame from key-value pairs via QPACK.
369
+ def build_headers_frame(pairs, encoder: Qpack::Encoder.new)
370
+ build_frame(FRAME_HEADERS, encoder.encode(pairs))
371
+ end
372
+
373
+ def build_goaway_frame(stream_id)
374
+ frame_type = encode_varint(FRAME_GOAWAY)
375
+ payload = encode_varint(stream_id)
376
+ frame_length = encode_varint(payload.bytesize)
377
+
378
+ frame_type + frame_length + payload
379
+ end
380
+
381
+ # Cache for decode_varint_str: (object_id << 16 | offset) → [value, consumed]
382
+ VARINT_STR_CACHE = {}
383
+ VARINT_STR_CACHE_MAX = 256
384
+
385
+ # Decode variable-length integer from a String using getbyte (no array needed)
386
+ # Returns [value, bytes_consumed]
387
+ def decode_varint_str(str, offset = 0)
388
+ cache_key = (str.object_id << 16) | offset
389
+ cached = VARINT_STR_CACHE[cache_key]
390
+ return cached if cached
391
+
392
+ first = str.getbyte(offset)
393
+ return [0, 0] unless first
394
+
395
+ if first < 0x40
396
+ result = VARINT_DECODE_SMALL[first]
397
+ else
398
+ prefix = (first & 0xC0) >> 6
399
+ length = 1 << prefix
400
+
401
+ return [0, 0] if offset + length > str.bytesize
402
+
403
+ result = case prefix
404
+ when 0
405
+ [first & 0x3F, 1]
406
+ when 1
407
+ [(first & 0x3F) << 8 | str.getbyte(offset + 1), 2]
408
+ when 2
409
+ [(first & 0x3F) << 24 | str.getbyte(offset + 1) << 16 |
410
+ str.getbyte(offset + 2) << 8 | str.getbyte(offset + 3), 4]
411
+ else
412
+ [(first & 0x3F) << 56 | str.getbyte(offset + 1) << 48 |
413
+ str.getbyte(offset + 2) << 40 | str.getbyte(offset + 3) << 32 |
414
+ str.getbyte(offset + 4) << 24 | str.getbyte(offset + 5) << 16 |
415
+ str.getbyte(offset + 6) << 8 | str.getbyte(offset + 7), 8]
416
+ end
417
+ end
418
+
419
+ VARINT_STR_CACHE[cache_key] = result if VARINT_STR_CACHE.size < VARINT_STR_CACHE_MAX
420
+ result
421
+ end
422
+
423
+ # Precomputed decode results for single-byte varints (0-63)
424
+ VARINT_DECODE_SMALL = Array.new(64) { |v| [v, 1].freeze }.freeze
425
+
426
+ # Decode variable-length integer (RFC 9000)
427
+ # Returns [value, bytes_consumed]
428
+ def decode_varint(bytes, offset = 0)
429
+ first = bytes[offset]
430
+ return [0, 0] unless first
431
+
432
+ # Fast path for single-byte varints (most common: frame types, small lengths)
433
+ return VARINT_DECODE_SMALL[first] if first < 0x40
434
+
435
+ prefix = (first & 0xC0) >> 6
436
+ length = 1 << prefix
437
+ return [0, 0] if offset + length > bytes.size
438
+
439
+ case prefix
440
+ when 1
441
+ [(first & 0x3F) << 8 | bytes[offset + 1], 2]
442
+ when 2
443
+ [(first & 0x3F) << 24 | bytes[offset + 1] << 16 |
444
+ bytes[offset + 2] << 8 | bytes[offset + 3], 4]
445
+ else # 3
446
+ [(first & 0x3F) << 56 | bytes[offset + 1] << 48 |
447
+ bytes[offset + 2] << 40 | bytes[offset + 3] << 32 |
448
+ bytes[offset + 4] << 24 | bytes[offset + 5] << 16 |
449
+ bytes[offset + 6] << 8 | bytes[offset + 7], 8]
450
+ end
451
+ end
452
+ end
453
+ end
454
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quicsilver
4
+ module Protocol
5
+ # HTTP Extensible Priorities (RFC 9218).
6
+ #
7
+ # Parses the `priority` header from HTTP requests. Urgency ranges from
8
+ # 0 (highest) to 7 (lowest), defaulting to 3. Incremental indicates
9
+ # whether partial data is useful to the client (e.g. progressive images).
10
+ #
11
+ # Usage:
12
+ # priority = Priority.parse("u=0, i")
13
+ # priority.urgency # => 0
14
+ # priority.incremental # => true
15
+ #
16
+ # priority = Priority.parse(nil) # default
17
+ # priority.urgency # => 3
18
+ # priority.incremental # => false
19
+ class Priority
20
+ DEFAULT_URGENCY = 3
21
+ MIN_URGENCY = 0
22
+ MAX_URGENCY = 7
23
+
24
+ attr_reader :urgency, :incremental
25
+
26
+ def initialize(urgency: DEFAULT_URGENCY, incremental: false)
27
+ @urgency = urgency.clamp(MIN_URGENCY, MAX_URGENCY)
28
+ @incremental = incremental
29
+ end
30
+
31
+ # Parse a priority header value (RFC 9218 §4, Structured Field Values).
32
+ # Returns a Priority with defaults for missing or invalid values.
33
+ # RFC 9218 §4: Encode as Structured Field Value.
34
+ # e.g. "u=0, i" or "u=5" or "u=3" (default)
35
+ def to_s
36
+ parts = []
37
+ parts << "u=#{@urgency}" if @urgency != DEFAULT_URGENCY
38
+ parts << "i" if @incremental
39
+ parts.empty? ? "u=#{DEFAULT_URGENCY}" : parts.join(", ")
40
+ end
41
+
42
+ def self.parse(value)
43
+ return new unless value && !value.empty?
44
+
45
+ urgency = DEFAULT_URGENCY
46
+ incremental = false
47
+
48
+ value.split(",").each do |param|
49
+ param = param.strip
50
+ if param.start_with?("u=")
51
+ urgency = param[2..].to_i
52
+ elsif param == "i"
53
+ incremental = true
54
+ elsif param == "i=?0"
55
+ incremental = false
56
+ elsif param == "i=?1"
57
+ incremental = true
58
+ end
59
+ end
60
+
61
+ new(urgency: urgency, incremental: incremental)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "huffman"
4
+
5
+ module Quicsilver
6
+ module Protocol
7
+ module Qpack
8
+ module Decoder
9
+ # Decode a QPACK string literal (RFC 9204 Section 4.1.2)
10
+ # Returns [string, bytes_consumed]
11
+ # String-based variant: accepts a binary String instead of byte array
12
+ def decode_qpack_string_from_str(data, offset)
13
+ first = data.getbyte(offset)
14
+ huffman = (first & 0x80) != 0
15
+
16
+ length = first & 0x7F
17
+ len_bytes = 1
18
+ if length == 0x7F
19
+ multiplier = 1
20
+ while offset + len_bytes < data.bytesize
21
+ next_byte = data.getbyte(offset + len_bytes)
22
+ len_bytes += 1
23
+ length += (next_byte & 0x7F) * multiplier
24
+ break if (next_byte & 0x80) == 0
25
+ multiplier *= 128
26
+ end
27
+ end
28
+
29
+ data_offset = offset + len_bytes
30
+ raw = data.byteslice(data_offset, length)
31
+
32
+ str = if huffman
33
+ Huffman.decode(raw) || raw
34
+ else
35
+ raw
36
+ end
37
+
38
+ [str, len_bytes + length]
39
+ end
40
+
41
+ # Byte-array variant. Results are not cached: the input is mutable, so
42
+ # keying on identity or object_id returns a stale decode after an
43
+ # in-place edit. Production decoding uses the String variant above.
44
+ def decode_qpack_string(bytes, offset)
45
+ return decode_qpack_string_from_str(bytes, offset) if bytes.is_a?(String)
46
+
47
+ first = bytes[offset]
48
+ huffman = (first & 0x80) != 0
49
+
50
+ # Inline 7-bit prefix integer decode to avoid method call
51
+ length = first & 0x7F
52
+ len_bytes = 1
53
+ if length == 0x7F
54
+ multiplier = 1
55
+ while offset + len_bytes < bytes.size
56
+ next_byte = bytes[offset + len_bytes]
57
+ len_bytes += 1
58
+ length += (next_byte & 0x7F) * multiplier
59
+ break if (next_byte & 0x80) == 0
60
+ multiplier *= 128
61
+ end
62
+ end
63
+
64
+ data_offset = offset + len_bytes
65
+ raw = bytes[data_offset, length].pack("C*")
66
+
67
+ str = if huffman
68
+ Huffman.decode(raw) || raw
69
+ else
70
+ raw
71
+ end
72
+
73
+ [str, len_bytes + length].freeze
74
+ end
75
+
76
+ # String-based prefix integer decoding
77
+ def decode_prefix_integer_str(data, offset, prefix_bits, pattern_mask)
78
+ max_prefix = (1 << prefix_bits) - 1
79
+ first_byte = data.getbyte(offset)
80
+ value = first_byte & max_prefix
81
+ bytes_consumed = 1
82
+
83
+ if value == max_prefix
84
+ multiplier = 1
85
+ loop do
86
+ return [value, bytes_consumed] if offset + bytes_consumed >= data.bytesize
87
+ next_byte = data.getbyte(offset + bytes_consumed)
88
+ bytes_consumed += 1
89
+ value += (next_byte & 0x7F) * multiplier
90
+ break if (next_byte & 0x80) == 0
91
+ multiplier *= 128
92
+ end
93
+ end
94
+
95
+ [value, bytes_consumed]
96
+ end
97
+
98
+ # RFC 7541 prefix integer decoding
99
+ # Returns [value, bytes_consumed]
100
+ def decode_prefix_integer(bytes, offset, prefix_bits, pattern_mask)
101
+ max_prefix = (1 << prefix_bits) - 1
102
+
103
+ first_byte = bytes[offset]
104
+ value = first_byte & max_prefix
105
+ bytes_consumed = 1
106
+
107
+ if value == max_prefix
108
+ multiplier = 1
109
+ loop do
110
+ return [value, bytes_consumed] if offset + bytes_consumed >= bytes.size
111
+
112
+ next_byte = bytes[offset + bytes_consumed]
113
+ bytes_consumed += 1
114
+
115
+ value += (next_byte & 0x7F) * multiplier
116
+ break if (next_byte & 0x80) == 0
117
+
118
+ multiplier *= 128
119
+ end
120
+ end
121
+
122
+ [value, bytes_consumed]
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end