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,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
|