quicsilver 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +41 -0
- data/.gitignore +3 -1
- data/CHANGELOG.md +76 -5
- data/Gemfile.lock +18 -4
- data/LICENSE +21 -0
- data/README.md +33 -53
- data/Rakefile +29 -2
- data/benchmarks/components.rb +191 -0
- data/benchmarks/concurrent.rb +110 -0
- data/benchmarks/helpers.rb +88 -0
- data/benchmarks/quicsilver_server.rb +46 -0
- data/benchmarks/rails.rb +170 -0
- data/benchmarks/throughput.rb +113 -0
- data/examples/minimal_http3_server.rb +0 -6
- data/examples/rack_http3_server.rb +0 -6
- data/examples/simple_client_test.rb +26 -0
- data/ext/quicsilver/quicsilver.c +615 -138
- data/lib/quicsilver/client/client.rb +250 -0
- data/lib/quicsilver/client/request.rb +98 -0
- data/lib/quicsilver/protocol/frames.rb +327 -0
- data/lib/quicsilver/protocol/qpack/decoder.rb +165 -0
- data/lib/quicsilver/protocol/qpack/encoder.rb +189 -0
- data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +125 -0
- data/lib/quicsilver/protocol/qpack/huffman.rb +459 -0
- data/lib/quicsilver/protocol/request_encoder.rb +47 -0
- data/lib/quicsilver/protocol/request_parser.rb +387 -0
- data/lib/quicsilver/protocol/response_encoder.rb +72 -0
- data/lib/quicsilver/protocol/response_parser.rb +249 -0
- data/lib/quicsilver/server/listener_data.rb +14 -0
- data/lib/quicsilver/server/request_handler.rb +86 -0
- data/lib/quicsilver/server/request_registry.rb +50 -0
- data/lib/quicsilver/server/server.rb +336 -0
- data/lib/quicsilver/transport/configuration.rb +132 -0
- data/lib/quicsilver/transport/connection.rb +350 -0
- data/lib/quicsilver/transport/event_loop.rb +38 -0
- data/lib/quicsilver/transport/inbound_stream.rb +33 -0
- data/lib/quicsilver/transport/stream.rb +28 -0
- data/lib/quicsilver/transport/stream_event.rb +26 -0
- data/lib/quicsilver/version.rb +1 -1
- data/lib/quicsilver.rb +49 -9
- data/lib/rackup/handler/quicsilver.rb +77 -0
- data/quicsilver.gemspec +10 -3
- metadata +122 -17
- data/examples/minimal_http3_client.rb +0 -89
- data/lib/quicsilver/client.rb +0 -191
- data/lib/quicsilver/http3/request_encoder.rb +0 -112
- data/lib/quicsilver/http3/request_parser.rb +0 -158
- data/lib/quicsilver/http3/response_encoder.rb +0 -73
- data/lib/quicsilver/http3.rb +0 -68
- data/lib/quicsilver/listener_data.rb +0 -29
- data/lib/quicsilver/server.rb +0 -258
- data/lib/quicsilver/server_configuration.rb +0 -49
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quicsilver
|
|
4
|
+
module Transport
|
|
5
|
+
class Connection
|
|
6
|
+
attr_reader :handle, :data, :streams
|
|
7
|
+
attr_reader :control_stream_id, :qpack_encoder_stream_id, :qpack_decoder_stream_id
|
|
8
|
+
attr_reader :server_control_stream
|
|
9
|
+
|
|
10
|
+
def initialize(handle, data)
|
|
11
|
+
@handle = handle
|
|
12
|
+
@data = data
|
|
13
|
+
@streams = {}
|
|
14
|
+
@response_buffers = {}
|
|
15
|
+
@mutex = Mutex.new
|
|
16
|
+
|
|
17
|
+
# Client's control streams (received)
|
|
18
|
+
@control_stream_id = nil
|
|
19
|
+
@qpack_encoder_stream_id = nil
|
|
20
|
+
@qpack_decoder_stream_id = nil
|
|
21
|
+
|
|
22
|
+
# Server's control stream (sent)
|
|
23
|
+
@server_control_stream = nil
|
|
24
|
+
|
|
25
|
+
@settings = {}
|
|
26
|
+
@settings_received = false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# === Setup (called after connection established) ===
|
|
30
|
+
|
|
31
|
+
def setup_http3_streams
|
|
32
|
+
# Control stream (required)
|
|
33
|
+
@server_control_stream = open_stream(unidirectional: true)
|
|
34
|
+
@server_control_stream.send(Protocol.build_control_stream)
|
|
35
|
+
|
|
36
|
+
# QPACK encoder/decoder streams
|
|
37
|
+
[0x02, 0x03].each do |type|
|
|
38
|
+
stream = open_stream(unidirectional: true)
|
|
39
|
+
stream.send([type].pack("C"))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# === Stream Management ===
|
|
44
|
+
|
|
45
|
+
def add_stream(stream)
|
|
46
|
+
@streams[stream.stream_id] = stream
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def get_stream(stream_id)
|
|
50
|
+
@streams[stream_id]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def remove_stream(stream_id)
|
|
54
|
+
@streams.delete(stream_id)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def track_client_stream(stream_id)
|
|
58
|
+
@streams[stream_id] = true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# === Data Handling ===
|
|
62
|
+
|
|
63
|
+
def buffer_data(stream_id, data)
|
|
64
|
+
@mutex.synchronize do
|
|
65
|
+
(@response_buffers[stream_id] ||= StringIO.new("".b)).write(data)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def complete_stream(stream_id, final_data)
|
|
70
|
+
@mutex.synchronize do
|
|
71
|
+
buffer = @response_buffers.delete(stream_id)
|
|
72
|
+
(buffer&.string || "".b) + (final_data || "".b)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# === HTTP/3 Frames ===
|
|
77
|
+
|
|
78
|
+
def send_goaway(stream_id = nil)
|
|
79
|
+
return unless @server_control_stream
|
|
80
|
+
|
|
81
|
+
stream_id ||= last_client_stream_id
|
|
82
|
+
@server_control_stream.send(Protocol.build_goaway_frame(stream_id))
|
|
83
|
+
rescue => e
|
|
84
|
+
Quicsilver.logger.error("Failed to send GOAWAY: #{e.message}")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def send_response(stream, status, headers, body, head_request: false)
|
|
88
|
+
encoder = Protocol::ResponseEncoder.new(status, headers, body, head_request: head_request)
|
|
89
|
+
|
|
90
|
+
if body.respond_to?(:to_ary)
|
|
91
|
+
Quicsilver.send_stream(stream.stream_handle, encoder.encode, true)
|
|
92
|
+
else
|
|
93
|
+
encoder.stream_encode do |frame_data, fin|
|
|
94
|
+
Quicsilver.send_stream(stream.stream_handle, frame_data, fin) unless frame_data.empty? && !fin
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
rescue RuntimeError => e
|
|
98
|
+
# Stream may have been reset by client - this is expected
|
|
99
|
+
raise unless e.message.include?("0x59") || e.message.include?("StreamSend failed")
|
|
100
|
+
Quicsilver.logger.debug("Stream send failed (client likely reset): #{e.message}")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def send_error(stream, status, message)
|
|
104
|
+
body = ["#{status} #{message}"]
|
|
105
|
+
encoder = Protocol::ResponseEncoder.new(status, { "content-type" => "text/plain" }, body)
|
|
106
|
+
Quicsilver.send_stream(stream.stream_handle, encoder.encode, true)
|
|
107
|
+
rescue RuntimeError => e
|
|
108
|
+
# Stream may have been reset by client - this is expected
|
|
109
|
+
raise unless e.message.include?("0x59") || e.message.include?("StreamSend failed")
|
|
110
|
+
Quicsilver.logger.debug("Stream send failed (client likely reset): #{e.message}")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# === Control Stream Handling ===
|
|
114
|
+
|
|
115
|
+
# Process incoming data on a unidirectional stream incrementally.
|
|
116
|
+
# Called on each RECEIVE event — control streams never send FIN.
|
|
117
|
+
def receive_unidirectional_data(stream_id, data)
|
|
118
|
+
@mutex.synchronize do
|
|
119
|
+
(@response_buffers[stream_id] ||= StringIO.new("".b)).write(data)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
buf = @mutex.synchronize { @response_buffers[stream_id]&.string || "".b }
|
|
123
|
+
return if buf.empty?
|
|
124
|
+
|
|
125
|
+
# First time seeing this stream: identify stream type
|
|
126
|
+
unless @uni_stream_types&.key?(stream_id)
|
|
127
|
+
@uni_stream_types ||= {}
|
|
128
|
+
stream_type, type_len = Protocol.decode_varint(buf.bytes, 0)
|
|
129
|
+
return if type_len == 0 # need more data
|
|
130
|
+
|
|
131
|
+
case stream_type
|
|
132
|
+
when 0x00 # Control stream
|
|
133
|
+
raise Protocol::FrameError, "Duplicate control stream" if @control_stream_id
|
|
134
|
+
@control_stream_id = stream_id
|
|
135
|
+
@uni_stream_types[stream_id] = :control
|
|
136
|
+
# Remove the stream type byte from the buffer
|
|
137
|
+
@mutex.synchronize { @response_buffers[stream_id] = StringIO.new(buf[type_len..] || "".b) }
|
|
138
|
+
when 0x01
|
|
139
|
+
raise Protocol::FrameError, "Client must not send push streams"
|
|
140
|
+
when 0x02 # QPACK encoder stream
|
|
141
|
+
raise Protocol::FrameError, "Duplicate QPACK encoder stream" if @qpack_encoder_stream_id
|
|
142
|
+
@qpack_encoder_stream_id = stream_id
|
|
143
|
+
@uni_stream_types[stream_id] = :qpack_encoder
|
|
144
|
+
@mutex.synchronize { @response_buffers[stream_id] = StringIO.new(buf[type_len..] || "".b) }
|
|
145
|
+
when 0x03 # QPACK decoder stream
|
|
146
|
+
raise Protocol::FrameError, "Duplicate QPACK decoder stream" if @qpack_decoder_stream_id
|
|
147
|
+
@qpack_decoder_stream_id = stream_id
|
|
148
|
+
@uni_stream_types[stream_id] = :qpack_decoder
|
|
149
|
+
@mutex.synchronize { @response_buffers[stream_id] = StringIO.new(buf[type_len..] || "".b) }
|
|
150
|
+
else
|
|
151
|
+
# Unknown unidirectional stream types MUST be ignored (RFC 9114 §6.2)
|
|
152
|
+
@uni_stream_types[stream_id] = :unknown
|
|
153
|
+
return
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
buf = @mutex.synchronize { @response_buffers[stream_id]&.string || "".b }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
stream_type = @uni_stream_types[stream_id]
|
|
160
|
+
return if buf.empty?
|
|
161
|
+
|
|
162
|
+
case stream_type
|
|
163
|
+
when :control
|
|
164
|
+
parse_control_frames(buf)
|
|
165
|
+
# Clear parsed data from buffer
|
|
166
|
+
@mutex.synchronize { @response_buffers[stream_id] = StringIO.new("".b) }
|
|
167
|
+
when :qpack_encoder
|
|
168
|
+
validate_qpack_encoder_data(buf)
|
|
169
|
+
@mutex.synchronize { @response_buffers[stream_id] = StringIO.new("".b) }
|
|
170
|
+
when :qpack_decoder
|
|
171
|
+
validate_qpack_decoder_data(buf)
|
|
172
|
+
@mutex.synchronize { @response_buffers[stream_id] = StringIO.new("".b) }
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def handle_unidirectional_stream(stream, fin: true)
|
|
177
|
+
stream_id = stream.stream_id
|
|
178
|
+
|
|
179
|
+
# Already known as critical stream — closure via FIN is an error
|
|
180
|
+
if fin && critical_stream?(stream_id)
|
|
181
|
+
raise Protocol::FrameError.new("Closure of critical stream", error_code: Protocol::H3_CLOSED_CRITICAL_STREAM)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
data = stream.data
|
|
185
|
+
return if data.empty?
|
|
186
|
+
|
|
187
|
+
stream_type, type_len = Protocol.decode_varint(data.bytes, 0)
|
|
188
|
+
return if type_len == 0
|
|
189
|
+
payload = data[type_len..-1]
|
|
190
|
+
|
|
191
|
+
case stream_type
|
|
192
|
+
when 0x00
|
|
193
|
+
set_control_stream(stream_id, payload)
|
|
194
|
+
if fin
|
|
195
|
+
raise Protocol::FrameError.new("Closure of critical stream", error_code: Protocol::H3_CLOSED_CRITICAL_STREAM)
|
|
196
|
+
end
|
|
197
|
+
when 0x01
|
|
198
|
+
raise Protocol::FrameError, "Client must not send push streams"
|
|
199
|
+
when 0x02
|
|
200
|
+
raise Protocol::FrameError, "Duplicate QPACK encoder stream" if @qpack_encoder_stream_id
|
|
201
|
+
@qpack_encoder_stream_id = stream_id
|
|
202
|
+
if fin
|
|
203
|
+
raise Protocol::FrameError.new("Closure of critical stream", error_code: Protocol::H3_CLOSED_CRITICAL_STREAM)
|
|
204
|
+
end
|
|
205
|
+
when 0x03
|
|
206
|
+
raise Protocol::FrameError, "Duplicate QPACK decoder stream" if @qpack_decoder_stream_id
|
|
207
|
+
@qpack_decoder_stream_id = stream_id
|
|
208
|
+
if fin
|
|
209
|
+
raise Protocol::FrameError.new("Closure of critical stream", error_code: Protocol::H3_CLOSED_CRITICAL_STREAM)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def set_control_stream(stream_id, payload = nil)
|
|
215
|
+
raise Protocol::FrameError, "Duplicate control stream" if @control_stream_id
|
|
216
|
+
@control_stream_id = stream_id
|
|
217
|
+
parse_control_frames(payload) if payload && !payload.empty?
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def settings
|
|
221
|
+
@settings
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def critical_stream?(stream_id)
|
|
225
|
+
stream_id == @control_stream_id ||
|
|
226
|
+
stream_id == @qpack_encoder_stream_id ||
|
|
227
|
+
stream_id == @qpack_decoder_stream_id
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# === Shutdown ===
|
|
231
|
+
|
|
232
|
+
def shutdown(error_code = 0)
|
|
233
|
+
send_goaway
|
|
234
|
+
Quicsilver.connection_shutdown(@handle, error_code, false)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
private
|
|
238
|
+
|
|
239
|
+
def open_stream(unidirectional: false)
|
|
240
|
+
handle = Quicsilver.open_stream(@data, unidirectional)
|
|
241
|
+
Stream.new(handle)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def last_client_stream_id
|
|
245
|
+
@streams.keys.select { |id| (id & 0x02) == 0 }.max || 0
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# RFC 9114 §7.2.4.1 / §11.2.2: HTTP/2 setting identifiers forbidden in HTTP/3
|
|
249
|
+
# 0x00 = SETTINGS_HEADER_TABLE_SIZE (reserved), 0x02-0x05 = various HTTP/2 settings
|
|
250
|
+
# Note: 0x08 (SETTINGS_ENABLE_CONNECT_PROTOCOL) is valid in HTTP/3 per RFC 9220
|
|
251
|
+
HTTP2_SETTINGS = [0x00, 0x02, 0x03, 0x04, 0x05].freeze
|
|
252
|
+
|
|
253
|
+
# Frame types forbidden on the control stream
|
|
254
|
+
FORBIDDEN_ON_CONTROL = [
|
|
255
|
+
0x00, # DATA — request streams only
|
|
256
|
+
0x01, # HEADERS — request streams only
|
|
257
|
+
0x02, # HTTP/2 PRIORITY (reserved)
|
|
258
|
+
0x05, # PUSH_PROMISE — request streams only
|
|
259
|
+
0x06, # HTTP/2 PING (reserved)
|
|
260
|
+
0x08, # HTTP/2 WINDOW_UPDATE (reserved)
|
|
261
|
+
0x09, # HTTP/2 CONTINUATION (reserved)
|
|
262
|
+
].freeze
|
|
263
|
+
|
|
264
|
+
def parse_control_frames(data)
|
|
265
|
+
offset = 0
|
|
266
|
+
first_frame = !@settings_received
|
|
267
|
+
|
|
268
|
+
while offset < data.bytesize
|
|
269
|
+
frame_type, type_len = Protocol.decode_varint(data.bytes, offset)
|
|
270
|
+
frame_length, length_len = Protocol.decode_varint(data.bytes, offset + type_len)
|
|
271
|
+
break if type_len == 0 || length_len == 0
|
|
272
|
+
|
|
273
|
+
if first_frame && frame_type != Protocol::FRAME_SETTINGS
|
|
274
|
+
raise Protocol::FrameError.new("First frame on control stream must be SETTINGS", error_code: Protocol::H3_MISSING_SETTINGS)
|
|
275
|
+
end
|
|
276
|
+
first_frame = false
|
|
277
|
+
|
|
278
|
+
if FORBIDDEN_ON_CONTROL.include?(frame_type)
|
|
279
|
+
raise Protocol::FrameError, "Frame type 0x#{frame_type.to_s(16)} not allowed on control stream"
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
if frame_type == Protocol::FRAME_SETTINGS
|
|
283
|
+
raise Protocol::FrameError, "Duplicate SETTINGS frame on control stream" if @settings_received
|
|
284
|
+
parse_settings(data[offset + type_len + length_len, frame_length])
|
|
285
|
+
@settings_received = true
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
offset += type_len + length_len + frame_length
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def parse_settings(payload)
|
|
293
|
+
offset = 0
|
|
294
|
+
seen = Set.new
|
|
295
|
+
while offset < payload.bytesize
|
|
296
|
+
id, id_len = Protocol.decode_varint(payload.bytes, offset)
|
|
297
|
+
value, value_len = Protocol.decode_varint(payload.bytes, offset + id_len)
|
|
298
|
+
break if id_len == 0 || value_len == 0
|
|
299
|
+
|
|
300
|
+
if HTTP2_SETTINGS.include?(id)
|
|
301
|
+
raise Protocol::FrameError.new("HTTP/2 setting identifier 0x#{id.to_s(16)} not allowed in HTTP/3", error_code: Protocol::H3_SETTINGS_ERROR)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
raise Protocol::FrameError, "Duplicate setting identifier 0x#{id.to_s(16)}" if seen.include?(id)
|
|
305
|
+
seen.add(id)
|
|
306
|
+
|
|
307
|
+
@settings[id] = value
|
|
308
|
+
offset += id_len + value_len
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
# RFC 9204 §4.1.3: Validate QPACK encoder stream instructions.
|
|
312
|
+
# We advertise QPACK_MAX_TABLE_CAPACITY = 0, so any Set Dynamic Table Capacity
|
|
313
|
+
# instruction with value > 0 is an error.
|
|
314
|
+
def validate_qpack_encoder_data(data)
|
|
315
|
+
return if data.empty?
|
|
316
|
+
byte = data.bytes[0]
|
|
317
|
+
|
|
318
|
+
# Set Dynamic Table Capacity (001xxxxx)
|
|
319
|
+
if (byte & 0xE0) == 0x20
|
|
320
|
+
capacity, _ = Protocol.decode_varint(data.bytes, 0)
|
|
321
|
+
capacity &= 0x1F # mask off the instruction prefix
|
|
322
|
+
# We advertised capacity 0, any non-zero is an error
|
|
323
|
+
raise Protocol::FrameError.new(
|
|
324
|
+
"Dynamic table capacity exceeds advertised maximum",
|
|
325
|
+
error_code: Protocol::QPACK_ENCODER_STREAM_ERROR
|
|
326
|
+
)
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# RFC 9204 §4.4.3: Validate QPACK decoder stream instructions.
|
|
331
|
+
# Insert Count Increment of 0 is a decoder stream error.
|
|
332
|
+
def validate_qpack_decoder_data(data)
|
|
333
|
+
return if data.empty?
|
|
334
|
+
byte = data.bytes[0]
|
|
335
|
+
|
|
336
|
+
# Insert Count Increment (00xxxxxx)
|
|
337
|
+
if (byte & 0xC0) == 0x00
|
|
338
|
+
increment, _ = Protocol.decode_varint(data.bytes, 0)
|
|
339
|
+
increment &= 0x3F # mask off prefix bits
|
|
340
|
+
if increment == 0
|
|
341
|
+
raise Protocol::FrameError.new(
|
|
342
|
+
"Insert Count Increment of 0 on decoder stream",
|
|
343
|
+
error_code: Protocol::QPACK_DECODER_STREAM_ERROR
|
|
344
|
+
)
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quicsilver
|
|
4
|
+
module Transport
|
|
5
|
+
class EventLoop
|
|
6
|
+
def initialize
|
|
7
|
+
@running = false
|
|
8
|
+
@thread = nil
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def start
|
|
13
|
+
@mutex.synchronize do
|
|
14
|
+
return if @running
|
|
15
|
+
|
|
16
|
+
@running = true
|
|
17
|
+
@thread = Thread.new do
|
|
18
|
+
Quicsilver.poll while @running
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def stop
|
|
24
|
+
@running = false
|
|
25
|
+
Quicsilver.wake
|
|
26
|
+
@thread&.join(2)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def join
|
|
30
|
+
@thread&.join
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.event_loop
|
|
36
|
+
@event_loop ||= Transport::EventLoop.new.tap(&:start)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quicsilver
|
|
4
|
+
module Transport
|
|
5
|
+
class InboundStream
|
|
6
|
+
attr_reader :stream_id, :is_unidirectional, :buffer
|
|
7
|
+
attr_accessor :stream_handle
|
|
8
|
+
|
|
9
|
+
def initialize(stream_id, is_unidirectional: nil)
|
|
10
|
+
@stream_id = stream_id
|
|
11
|
+
@is_unidirectional = is_unidirectional.nil? ? !bidirectional? : is_unidirectional
|
|
12
|
+
@buffer = StringIO.new.tap { |io| io.set_encoding(Encoding::ASCII_8BIT) }
|
|
13
|
+
@stream_handle = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def bidirectional?
|
|
17
|
+
(stream_id & 0x02) == 0
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def writable?
|
|
21
|
+
!stream_handle.nil?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def append_data(data)
|
|
25
|
+
@buffer.write(data)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def data
|
|
29
|
+
@buffer.string
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quicsilver
|
|
4
|
+
module Transport
|
|
5
|
+
# Wraps a QUIC stream opened by Ruby code (client requests, server control streams).
|
|
6
|
+
# Encapsulates the C handle — callers use send/reset/stop_sending methods instead
|
|
7
|
+
# of passing raw pointers to Quicsilver.send_stream etc.
|
|
8
|
+
class Stream
|
|
9
|
+
attr_reader :handle
|
|
10
|
+
|
|
11
|
+
def initialize(handle)
|
|
12
|
+
@handle = handle
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def send(data, fin: false)
|
|
16
|
+
Quicsilver.send_stream(@handle, data, fin)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def reset(error_code = Protocol::H3_REQUEST_CANCELLED)
|
|
20
|
+
Quicsilver.stream_reset(@handle, error_code)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def stop_sending(error_code = Protocol::H3_REQUEST_CANCELLED)
|
|
24
|
+
Quicsilver.stream_stop_sending(@handle, error_code)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quicsilver
|
|
4
|
+
module Transport
|
|
5
|
+
# Parses the binary data packed by the C extension for stream completion events.
|
|
6
|
+
# C packs events as:
|
|
7
|
+
# RECEIVE_FIN: [stream_handle(8)][payload...]
|
|
8
|
+
# STREAM_RESET: [stream_handle(8)][error_code(8)]
|
|
9
|
+
# STOP_SENDING: [stream_handle(8)][error_code(8)]
|
|
10
|
+
class StreamEvent
|
|
11
|
+
attr_reader :handle, :data, :error_code
|
|
12
|
+
|
|
13
|
+
def initialize(raw_data, event_type)
|
|
14
|
+
@handle = raw_data[0, 8].unpack1("Q")
|
|
15
|
+
remaining = raw_data[8..] || "".b
|
|
16
|
+
|
|
17
|
+
case event_type
|
|
18
|
+
when "RECEIVE_FIN"
|
|
19
|
+
@data = remaining
|
|
20
|
+
when "STREAM_RESET", "STOP_SENDING"
|
|
21
|
+
@error_code = remaining.unpack1("Q")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/quicsilver/version.rb
CHANGED
data/lib/quicsilver.rb
CHANGED
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "logger"
|
|
3
4
|
require_relative "quicsilver/version"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
require_relative "quicsilver/
|
|
7
|
-
require_relative "quicsilver/
|
|
8
|
-
require_relative "quicsilver/
|
|
9
|
-
require_relative "quicsilver/
|
|
10
|
-
require_relative "quicsilver/
|
|
11
|
-
require_relative "quicsilver/
|
|
5
|
+
|
|
6
|
+
# Protocol layer (pure HTTP/3 codec)
|
|
7
|
+
require_relative "quicsilver/protocol/frames"
|
|
8
|
+
require_relative "quicsilver/protocol/qpack/encoder"
|
|
9
|
+
require_relative "quicsilver/protocol/request_parser"
|
|
10
|
+
require_relative "quicsilver/protocol/request_encoder"
|
|
11
|
+
require_relative "quicsilver/protocol/response_parser"
|
|
12
|
+
require_relative "quicsilver/protocol/response_encoder"
|
|
13
|
+
|
|
14
|
+
# Transport layer (QUIC primitives)
|
|
15
|
+
require_relative "quicsilver/transport/stream"
|
|
16
|
+
require_relative "quicsilver/transport/stream_event"
|
|
17
|
+
require_relative "quicsilver/transport/inbound_stream"
|
|
18
|
+
require_relative "quicsilver/transport/event_loop"
|
|
19
|
+
require_relative "quicsilver/transport/configuration"
|
|
20
|
+
require_relative "quicsilver/transport/connection"
|
|
21
|
+
|
|
22
|
+
# Server
|
|
23
|
+
require_relative "quicsilver/server/listener_data"
|
|
24
|
+
require_relative "quicsilver/server/request_registry"
|
|
25
|
+
require_relative "quicsilver/server/request_handler"
|
|
26
|
+
require_relative "quicsilver/server/server"
|
|
27
|
+
|
|
28
|
+
# Client
|
|
29
|
+
require_relative "quicsilver/client/request"
|
|
30
|
+
require_relative "quicsilver/client/client"
|
|
31
|
+
|
|
32
|
+
# C extension
|
|
12
33
|
require_relative "quicsilver/quicsilver"
|
|
13
34
|
|
|
35
|
+
# Rackup handler
|
|
36
|
+
require_relative "rackup/handler/quicsilver"
|
|
37
|
+
|
|
14
38
|
module Quicsilver
|
|
15
39
|
class Error < StandardError; end
|
|
16
40
|
class ServerIsRunningError < Error; end
|
|
@@ -19,4 +43,20 @@ module Quicsilver
|
|
|
19
43
|
class ServerError < Error; end
|
|
20
44
|
class ConnectionError < Error; end
|
|
21
45
|
class TimeoutError < Error; end
|
|
22
|
-
|
|
46
|
+
|
|
47
|
+
class << self
|
|
48
|
+
attr_writer :logger
|
|
49
|
+
|
|
50
|
+
def logger
|
|
51
|
+
@logger ||= default_logger
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def default_logger
|
|
57
|
+
Logger.new($stdout, level: Logger::INFO).tap do |log|
|
|
58
|
+
log.progname = "Quicsilver"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rackup/handler"
|
|
4
|
+
require "localhost"
|
|
5
|
+
|
|
6
|
+
module Quicsilver
|
|
7
|
+
module RackHandler
|
|
8
|
+
DEFAULT_OPTIONS = {
|
|
9
|
+
Host: "0.0.0.0",
|
|
10
|
+
Port: 4433,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
def self.run(app, **options)
|
|
14
|
+
normalized_options = {
|
|
15
|
+
host: options[:Host] || options[:host] || DEFAULT_OPTIONS[:Host],
|
|
16
|
+
port: (options[:Port] || options[:port] || DEFAULT_OPTIONS[:Port]).to_i,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
cert_file = options[:cert_file]
|
|
20
|
+
key_file = options[:key_file]
|
|
21
|
+
|
|
22
|
+
if cert_file.nil? && key_file.nil?
|
|
23
|
+
env = options[:environment] || ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
|
|
24
|
+
|
|
25
|
+
if env == 'production'
|
|
26
|
+
raise ArgumentError, "cert_file and key_file are required in production"
|
|
27
|
+
else
|
|
28
|
+
require 'localhost/authority'
|
|
29
|
+
authority = Localhost::Authority.fetch
|
|
30
|
+
cert_file = authority.certificate_path
|
|
31
|
+
key_file = authority.key_path
|
|
32
|
+
Quicsilver.logger.info("Using auto-generated certificates for localhost")
|
|
33
|
+
Quicsilver.logger.info(" Cert: #{cert_file}")
|
|
34
|
+
Quicsilver.logger.info(" Key: #{key_file}")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
config = ::Quicsilver::Transport::Configuration.new(cert_file, key_file)
|
|
39
|
+
|
|
40
|
+
server = ::Quicsilver::Server.new(
|
|
41
|
+
normalized_options[:port],
|
|
42
|
+
address: normalized_options[:host],
|
|
43
|
+
app: app,
|
|
44
|
+
server_configuration: config
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
yield server if block_given?
|
|
48
|
+
|
|
49
|
+
server.start
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.valid_options
|
|
53
|
+
{
|
|
54
|
+
"Host=HOST" => "Hostname to listen on (default: 0.0.0.0)",
|
|
55
|
+
"Port=PORT" => "Port to listen on (default: 4433)",
|
|
56
|
+
"cert_file=PATH" => "Path to TLS certificate file (required)",
|
|
57
|
+
"key_file=PATH" => "Path to TLS key file (required)"
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
module Rackup
|
|
65
|
+
module Handler
|
|
66
|
+
module Quicsilver
|
|
67
|
+
def self.run(app, **options, &block)
|
|
68
|
+
::Quicsilver::RackHandler.run(app, **options, &block)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.valid_options
|
|
72
|
+
::Quicsilver::RackHandler.valid_options
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
register :quicsilver, Quicsilver
|
|
76
|
+
end
|
|
77
|
+
end
|
data/quicsilver.gemspec
CHANGED
|
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.authors = ["Haroon Ahmed"]
|
|
9
9
|
spec.email = ["haroon.ahmed25@gmail.com"]
|
|
10
10
|
|
|
11
|
-
spec.summary = %q{
|
|
12
|
-
spec.description = %q{
|
|
11
|
+
spec.summary = %q{HTTP/3 server implementation for Ruby}
|
|
12
|
+
spec.description = %q{HTTP/3 server implementation for Ruby}
|
|
13
13
|
spec.homepage = "https://github.com/hahmed/quicsilver"
|
|
14
14
|
|
|
15
15
|
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
|
@@ -33,12 +33,19 @@ Gem::Specification.new do |spec|
|
|
|
33
33
|
spec.bindir = "exe"
|
|
34
34
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
35
35
|
spec.require_paths = ["lib"]
|
|
36
|
+
spec.required_ruby_version = ">= 3.2.0"
|
|
36
37
|
|
|
37
38
|
spec.extensions = ['ext/quicsilver/extconf.rb']
|
|
38
39
|
|
|
39
40
|
spec.add_development_dependency "bundler", "~> 2.0"
|
|
40
|
-
spec.add_development_dependency "rake", "~>
|
|
41
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
41
42
|
spec.add_development_dependency 'rake-compiler', '~> 1.2'
|
|
42
43
|
spec.add_development_dependency 'rake-compiler-dock', '~> 1.3'
|
|
43
44
|
spec.add_development_dependency "minitest", "~> 5.0"
|
|
45
|
+
spec.add_development_dependency "minitest-focus", "~> 1.3"
|
|
46
|
+
spec.add_development_dependency "benchmark-ips", "~> 2.12"
|
|
47
|
+
spec.add_dependency "logger"
|
|
48
|
+
spec.add_dependency "localhost", "~> 1.6"
|
|
49
|
+
spec.add_dependency "rack", "~> 3.0"
|
|
50
|
+
spec.add_dependency "rackup", "~> 2.0"
|
|
44
51
|
end
|