quicsilver 0.3.0 → 0.4.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 +1 -1
- data/.github/workflows/cibuildgem.yaml +93 -0
- data/.gitignore +3 -1
- data/CHANGELOG.md +32 -0
- data/Gemfile.lock +20 -2
- data/README.md +92 -29
- data/Rakefile +67 -2
- data/benchmarks/concurrent.rb +2 -2
- data/benchmarks/rails.rb +3 -3
- data/benchmarks/throughput.rb +2 -2
- data/examples/README.md +44 -91
- data/examples/benchmark.rb +111 -0
- data/examples/connection_pool_demo.rb +47 -0
- data/examples/example_helper.rb +18 -0
- data/examples/falcon_middleware.rb +44 -0
- data/examples/feature_demo.rb +125 -0
- data/examples/grpc_style.rb +97 -0
- data/examples/minimal_http3_server.rb +6 -18
- data/examples/priorities.rb +60 -0
- data/examples/protocol_http_server.rb +31 -0
- data/examples/rack_http3_server.rb +8 -20
- data/examples/rails_feature_test.rb +260 -0
- data/examples/simple_client_test.rb +2 -2
- data/examples/streaming_sse.rb +33 -0
- data/examples/trailers.rb +69 -0
- data/ext/quicsilver/extconf.rb +14 -0
- data/ext/quicsilver/quicsilver.c +39 -0
- data/lib/quicsilver/client/client.rb +138 -39
- data/lib/quicsilver/client/connection_pool.rb +106 -0
- data/lib/quicsilver/libmsquic.2.dylib +0 -0
- data/lib/quicsilver/protocol/adapter.rb +176 -0
- data/lib/quicsilver/protocol/control_stream_parser.rb +106 -0
- data/lib/quicsilver/protocol/frame_parser.rb +142 -0
- data/lib/quicsilver/protocol/frame_reader.rb +55 -0
- data/lib/quicsilver/protocol/frames.rb +18 -7
- data/lib/quicsilver/protocol/priority.rb +56 -0
- data/lib/quicsilver/protocol/qpack/encoder.rb +39 -1
- data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +16 -1
- data/lib/quicsilver/protocol/request_parser.rb +28 -140
- data/lib/quicsilver/protocol/response_encoder.rb +27 -2
- data/lib/quicsilver/protocol/response_parser.rb +22 -130
- data/lib/quicsilver/protocol/stream_input.rb +98 -0
- data/lib/quicsilver/protocol/stream_output.rb +59 -0
- data/lib/quicsilver/quicsilver.bundle +0 -0
- data/lib/quicsilver/server/request_handler.rb +96 -44
- data/lib/quicsilver/server/server.rb +316 -42
- data/lib/quicsilver/transport/configuration.rb +10 -1
- data/lib/quicsilver/transport/connection.rb +92 -63
- data/lib/quicsilver/version.rb +1 -1
- data/lib/quicsilver.rb +26 -3
- data/quicsilver.gemspec +10 -2
- metadata +69 -5
- data/examples/setup_certs.sh +0 -57
|
@@ -3,13 +3,23 @@
|
|
|
3
3
|
module Quicsilver
|
|
4
4
|
module Transport
|
|
5
5
|
class Connection
|
|
6
|
+
include Protocol::ControlStreamParser
|
|
7
|
+
|
|
8
|
+
# MsQuic QUIC_STATUS_INVALID_STATE (POSIX errno ETOOMANYREFS = 0x59).
|
|
9
|
+
# Stream already shut down by peer — raised by StreamSend when the
|
|
10
|
+
# client has reset or closed the stream.
|
|
11
|
+
MSQUIC_INVALID_STATE = "0x59"
|
|
12
|
+
|
|
6
13
|
attr_reader :handle, :data, :streams
|
|
7
14
|
attr_reader :control_stream_id, :qpack_encoder_stream_id, :qpack_decoder_stream_id
|
|
8
15
|
attr_reader :server_control_stream
|
|
16
|
+
attr_reader :peer_goaway_id, :local_goaway_id
|
|
17
|
+
attr_reader :stream_priorities
|
|
9
18
|
|
|
10
|
-
def initialize(handle, data)
|
|
19
|
+
def initialize(handle, data, max_header_size: nil)
|
|
11
20
|
@handle = handle
|
|
12
21
|
@data = data
|
|
22
|
+
@max_header_size = max_header_size
|
|
13
23
|
@streams = {}
|
|
14
24
|
@response_buffers = {}
|
|
15
25
|
@mutex = Mutex.new
|
|
@@ -24,6 +34,9 @@ module Quicsilver
|
|
|
24
34
|
|
|
25
35
|
@settings = {}
|
|
26
36
|
@settings_received = false
|
|
37
|
+
@peer_goaway_id = nil
|
|
38
|
+
@local_goaway_id = nil
|
|
39
|
+
@stream_priorities = {}
|
|
27
40
|
end
|
|
28
41
|
|
|
29
42
|
# === Setup (called after connection established) ===
|
|
@@ -31,13 +44,17 @@ module Quicsilver
|
|
|
31
44
|
def setup_http3_streams
|
|
32
45
|
# Control stream (required)
|
|
33
46
|
@server_control_stream = open_stream(unidirectional: true)
|
|
34
|
-
@server_control_stream.send(Protocol.build_control_stream)
|
|
47
|
+
@server_control_stream.send(Protocol.build_control_stream(max_field_section_size: @max_header_size))
|
|
35
48
|
|
|
36
49
|
# QPACK encoder/decoder streams
|
|
37
50
|
[0x02, 0x03].each do |type|
|
|
38
51
|
stream = open_stream(unidirectional: true)
|
|
39
52
|
stream.send([type].pack("C"))
|
|
40
53
|
end
|
|
54
|
+
|
|
55
|
+
# GREASE unidirectional stream (RFC 9297)
|
|
56
|
+
stream = open_stream(unidirectional: true)
|
|
57
|
+
stream.send(Protocol.encode_varint(Protocol.grease_id) + "GREASE".b)
|
|
41
58
|
end
|
|
42
59
|
|
|
43
60
|
# === Stream Management ===
|
|
@@ -62,14 +79,14 @@ module Quicsilver
|
|
|
62
79
|
|
|
63
80
|
def buffer_data(stream_id, data)
|
|
64
81
|
@mutex.synchronize do
|
|
65
|
-
(@response_buffers[stream_id] ||=
|
|
82
|
+
(@response_buffers[stream_id] ||= "".b) << data
|
|
66
83
|
end
|
|
67
84
|
end
|
|
68
85
|
|
|
69
86
|
def complete_stream(stream_id, final_data)
|
|
70
87
|
@mutex.synchronize do
|
|
71
88
|
buffer = @response_buffers.delete(stream_id)
|
|
72
|
-
(buffer
|
|
89
|
+
(buffer || "".b) + (final_data || "".b)
|
|
73
90
|
end
|
|
74
91
|
end
|
|
75
92
|
|
|
@@ -79,13 +96,36 @@ module Quicsilver
|
|
|
79
96
|
return unless @server_control_stream
|
|
80
97
|
|
|
81
98
|
stream_id ||= last_client_stream_id
|
|
99
|
+
validate_goaway_id!(stream_id)
|
|
100
|
+
|
|
82
101
|
@server_control_stream.send(Protocol.build_goaway_frame(stream_id))
|
|
102
|
+
@local_goaway_id = stream_id
|
|
103
|
+
rescue ArgumentError
|
|
104
|
+
raise # Re-raise validation errors
|
|
83
105
|
rescue => e
|
|
84
106
|
Quicsilver.logger.error("Failed to send GOAWAY: #{e.message}")
|
|
85
107
|
end
|
|
86
108
|
|
|
87
|
-
|
|
88
|
-
|
|
109
|
+
# RFC 9114 §5.2: GOAWAY IDs MUST NOT increase from a previous value.
|
|
110
|
+
def validate_goaway_id!(stream_id)
|
|
111
|
+
if @local_goaway_id && stream_id > @local_goaway_id
|
|
112
|
+
raise ArgumentError, "GOAWAY stream ID #{stream_id} exceeds previous #{@local_goaway_id}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Send an informational (1xx) response before the final response.
|
|
117
|
+
# RFC 9114 §4.1: encoded as a HEADERS frame, no FIN.
|
|
118
|
+
def send_informational(stream, status, headers)
|
|
119
|
+
data = Protocol::ResponseEncoder.encode_informational(status, headers)
|
|
120
|
+
Quicsilver.send_stream(stream.stream_handle, data, false)
|
|
121
|
+
rescue RuntimeError => e
|
|
122
|
+
raise unless e.message.include?(MSQUIC_INVALID_STATE) || e.message.include?("StreamSend failed")
|
|
123
|
+
Quicsilver.logger.debug("Stream send failed (client likely reset): #{e.message}")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def send_response(stream, status, headers, body, head_request: false, trailers: nil)
|
|
127
|
+
body = [] if body.nil?
|
|
128
|
+
encoder = Protocol::ResponseEncoder.new(status, headers, body, head_request: head_request, trailers: trailers)
|
|
89
129
|
|
|
90
130
|
if body.respond_to?(:to_ary)
|
|
91
131
|
Quicsilver.send_stream(stream.stream_handle, encoder.encode, true)
|
|
@@ -96,7 +136,7 @@ module Quicsilver
|
|
|
96
136
|
end
|
|
97
137
|
rescue RuntimeError => e
|
|
98
138
|
# Stream may have been reset by client - this is expected
|
|
99
|
-
raise unless e.message.include?(
|
|
139
|
+
raise unless e.message.include?(MSQUIC_INVALID_STATE) || e.message.include?("StreamSend failed")
|
|
100
140
|
Quicsilver.logger.debug("Stream send failed (client likely reset): #{e.message}")
|
|
101
141
|
end
|
|
102
142
|
|
|
@@ -106,7 +146,7 @@ module Quicsilver
|
|
|
106
146
|
Quicsilver.send_stream(stream.stream_handle, encoder.encode, true)
|
|
107
147
|
rescue RuntimeError => e
|
|
108
148
|
# Stream may have been reset by client - this is expected
|
|
109
|
-
raise unless e.message.include?(
|
|
149
|
+
raise unless e.message.include?(MSQUIC_INVALID_STATE) || e.message.include?("StreamSend failed")
|
|
110
150
|
Quicsilver.logger.debug("Stream send failed (client likely reset): #{e.message}")
|
|
111
151
|
end
|
|
112
152
|
|
|
@@ -116,10 +156,10 @@ module Quicsilver
|
|
|
116
156
|
# Called on each RECEIVE event — control streams never send FIN.
|
|
117
157
|
def receive_unidirectional_data(stream_id, data)
|
|
118
158
|
@mutex.synchronize do
|
|
119
|
-
(@response_buffers[stream_id] ||=
|
|
159
|
+
(@response_buffers[stream_id] ||= "".b) << data
|
|
120
160
|
end
|
|
121
161
|
|
|
122
|
-
buf = @mutex.synchronize { @response_buffers[stream_id]
|
|
162
|
+
buf = @mutex.synchronize { @response_buffers[stream_id] || "".b }
|
|
123
163
|
return if buf.empty?
|
|
124
164
|
|
|
125
165
|
# First time seeing this stream: identify stream type
|
|
@@ -134,26 +174,26 @@ module Quicsilver
|
|
|
134
174
|
@control_stream_id = stream_id
|
|
135
175
|
@uni_stream_types[stream_id] = :control
|
|
136
176
|
# Remove the stream type byte from the buffer
|
|
137
|
-
@mutex.synchronize { @response_buffers[stream_id] =
|
|
177
|
+
@mutex.synchronize { @response_buffers[stream_id] = (buf[type_len..] || "".b) }
|
|
138
178
|
when 0x01
|
|
139
179
|
raise Protocol::FrameError, "Client must not send push streams"
|
|
140
180
|
when 0x02 # QPACK encoder stream
|
|
141
181
|
raise Protocol::FrameError, "Duplicate QPACK encoder stream" if @qpack_encoder_stream_id
|
|
142
182
|
@qpack_encoder_stream_id = stream_id
|
|
143
183
|
@uni_stream_types[stream_id] = :qpack_encoder
|
|
144
|
-
@mutex.synchronize { @response_buffers[stream_id] =
|
|
184
|
+
@mutex.synchronize { @response_buffers[stream_id] = (buf[type_len..] || "".b) }
|
|
145
185
|
when 0x03 # QPACK decoder stream
|
|
146
186
|
raise Protocol::FrameError, "Duplicate QPACK decoder stream" if @qpack_decoder_stream_id
|
|
147
187
|
@qpack_decoder_stream_id = stream_id
|
|
148
188
|
@uni_stream_types[stream_id] = :qpack_decoder
|
|
149
|
-
@mutex.synchronize { @response_buffers[stream_id] =
|
|
189
|
+
@mutex.synchronize { @response_buffers[stream_id] = (buf[type_len..] || "".b) }
|
|
150
190
|
else
|
|
151
191
|
# Unknown unidirectional stream types MUST be ignored (RFC 9114 §6.2)
|
|
152
192
|
@uni_stream_types[stream_id] = :unknown
|
|
153
193
|
return
|
|
154
194
|
end
|
|
155
195
|
|
|
156
|
-
buf = @mutex.synchronize { @response_buffers[stream_id]
|
|
196
|
+
buf = @mutex.synchronize { @response_buffers[stream_id] || "".b }
|
|
157
197
|
end
|
|
158
198
|
|
|
159
199
|
stream_type = @uni_stream_types[stream_id]
|
|
@@ -163,13 +203,13 @@ module Quicsilver
|
|
|
163
203
|
when :control
|
|
164
204
|
parse_control_frames(buf)
|
|
165
205
|
# Clear parsed data from buffer
|
|
166
|
-
@mutex.synchronize { @response_buffers[stream_id] =
|
|
206
|
+
@mutex.synchronize { @response_buffers[stream_id] = "".b }
|
|
167
207
|
when :qpack_encoder
|
|
168
208
|
validate_qpack_encoder_data(buf)
|
|
169
|
-
@mutex.synchronize { @response_buffers[stream_id] =
|
|
209
|
+
@mutex.synchronize { @response_buffers[stream_id] = "".b }
|
|
170
210
|
when :qpack_decoder
|
|
171
211
|
validate_qpack_decoder_data(buf)
|
|
172
|
-
@mutex.synchronize { @response_buffers[stream_id] =
|
|
212
|
+
@mutex.synchronize { @response_buffers[stream_id] = "".b }
|
|
173
213
|
end
|
|
174
214
|
end
|
|
175
215
|
|
|
@@ -221,6 +261,25 @@ module Quicsilver
|
|
|
221
261
|
@settings
|
|
222
262
|
end
|
|
223
263
|
|
|
264
|
+
# Get the priority for a stream. Returns default Priority if not set.
|
|
265
|
+
def stream_priority(stream_id)
|
|
266
|
+
@stream_priorities[stream_id] || Protocol::Priority.new
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Apply priority to a QUIC stream via MsQuic.
|
|
270
|
+
# MsQuic: 0 = lowest, 0xFFFF = highest.
|
|
271
|
+
# HTTP urgency: 0 = highest, 7 = lowest.
|
|
272
|
+
# Maps urgency into evenly spaced bands across the uint16 range.
|
|
273
|
+
# The priority is queued and applied on the MsQuic event thread.
|
|
274
|
+
def apply_stream_priority(stream, priority)
|
|
275
|
+
handle = stream.respond_to?(:stream_handle) ? stream.stream_handle : nil
|
|
276
|
+
return unless handle
|
|
277
|
+
quic_priority = (7 - priority.urgency) * 0x2000
|
|
278
|
+
Quicsilver.set_stream_priority(handle, quic_priority)
|
|
279
|
+
rescue => e
|
|
280
|
+
Quicsilver.logger.debug("Failed to set stream priority: #{e.message}")
|
|
281
|
+
end
|
|
282
|
+
|
|
224
283
|
def critical_stream?(stream_id)
|
|
225
284
|
stream_id == @control_stream_id ||
|
|
226
285
|
stream_id == @qpack_encoder_stream_id ||
|
|
@@ -245,11 +304,6 @@ module Quicsilver
|
|
|
245
304
|
@streams.keys.select { |id| (id & 0x02) == 0 }.max || 0
|
|
246
305
|
end
|
|
247
306
|
|
|
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
307
|
# Frame types forbidden on the control stream
|
|
254
308
|
FORBIDDEN_ON_CONTROL = [
|
|
255
309
|
0x00, # DATA — request streams only
|
|
@@ -261,53 +315,28 @@ module Quicsilver
|
|
|
261
315
|
0x09, # HTTP/2 CONTINUATION (reserved)
|
|
262
316
|
].freeze
|
|
263
317
|
|
|
264
|
-
def
|
|
265
|
-
|
|
266
|
-
|
|
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
|
|
318
|
+
def on_settings_received(settings)
|
|
319
|
+
@settings.merge!(settings)
|
|
320
|
+
end
|
|
281
321
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
end
|
|
322
|
+
def handle_control_frame(type, payload)
|
|
323
|
+
if FORBIDDEN_ON_CONTROL.include?(type)
|
|
324
|
+
raise Protocol::FrameError, "Frame type 0x#{type.to_s(16)} not allowed on control stream"
|
|
325
|
+
end
|
|
287
326
|
|
|
288
|
-
|
|
327
|
+
if type == Protocol::FRAME_PRIORITY_UPDATE
|
|
328
|
+
parse_priority_update(payload)
|
|
289
329
|
end
|
|
290
330
|
end
|
|
291
331
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
|
332
|
+
# RFC 9218 §7: Parse PRIORITY_UPDATE frame.
|
|
333
|
+
# Payload is a stream ID varint followed by a Priority Field Value string.
|
|
334
|
+
def parse_priority_update(payload)
|
|
335
|
+
stream_id, consumed = Protocol.decode_varint(payload.bytes, 0)
|
|
336
|
+
priority_value = payload[consumed..]
|
|
337
|
+
@stream_priorities[stream_id] = Protocol::Priority.parse(priority_value)
|
|
310
338
|
end
|
|
339
|
+
|
|
311
340
|
# RFC 9204 §4.1.3: Validate QPACK encoder stream instructions.
|
|
312
341
|
# We advertise QPACK_MAX_TABLE_CAPACITY = 0, so any Set Dynamic Table Capacity
|
|
313
342
|
# instruction with value > 0 is an error.
|
data/lib/quicsilver/version.rb
CHANGED
data/lib/quicsilver.rb
CHANGED
|
@@ -3,15 +3,21 @@
|
|
|
3
3
|
require "logger"
|
|
4
4
|
require_relative "quicsilver/version"
|
|
5
5
|
|
|
6
|
-
# Protocol layer
|
|
6
|
+
# Protocol layer
|
|
7
7
|
require_relative "quicsilver/protocol/frames"
|
|
8
|
+
require_relative "quicsilver/protocol/priority"
|
|
8
9
|
require_relative "quicsilver/protocol/qpack/encoder"
|
|
9
10
|
require_relative "quicsilver/protocol/request_parser"
|
|
10
11
|
require_relative "quicsilver/protocol/request_encoder"
|
|
11
12
|
require_relative "quicsilver/protocol/response_parser"
|
|
12
13
|
require_relative "quicsilver/protocol/response_encoder"
|
|
14
|
+
require_relative "quicsilver/protocol/stream_input"
|
|
15
|
+
require_relative "quicsilver/protocol/stream_output"
|
|
16
|
+
require_relative "quicsilver/protocol/adapter"
|
|
17
|
+
require_relative "quicsilver/protocol/control_stream_parser"
|
|
18
|
+
require "protocol/rack"
|
|
13
19
|
|
|
14
|
-
# Transport layer
|
|
20
|
+
# Transport layer
|
|
15
21
|
require_relative "quicsilver/transport/stream"
|
|
16
22
|
require_relative "quicsilver/transport/stream_event"
|
|
17
23
|
require_relative "quicsilver/transport/inbound_stream"
|
|
@@ -27,10 +33,17 @@ require_relative "quicsilver/server/server"
|
|
|
27
33
|
|
|
28
34
|
# Client
|
|
29
35
|
require_relative "quicsilver/client/request"
|
|
36
|
+
require_relative "quicsilver/client/connection_pool"
|
|
30
37
|
require_relative "quicsilver/client/client"
|
|
31
38
|
|
|
32
39
|
# C extension
|
|
33
|
-
|
|
40
|
+
# Load precompiled binary if available, fall back to native extension
|
|
41
|
+
begin
|
|
42
|
+
ruby_version = /(\d+\.\d+)/.match(RUBY_VERSION)
|
|
43
|
+
require_relative "quicsilver/#{ruby_version}/quicsilver"
|
|
44
|
+
rescue LoadError
|
|
45
|
+
require_relative "quicsilver/quicsilver"
|
|
46
|
+
end
|
|
34
47
|
|
|
35
48
|
# Rackup handler
|
|
36
49
|
require_relative "rackup/handler/quicsilver"
|
|
@@ -59,4 +72,14 @@ module Quicsilver
|
|
|
59
72
|
end
|
|
60
73
|
end
|
|
61
74
|
end
|
|
75
|
+
|
|
76
|
+
# Release pooled client connections on process exit.
|
|
77
|
+
# Closes connection handles so the OS doesn't leak UDP sockets.
|
|
78
|
+
# MsQuic itself is cleaned up by the OS when the process exits.
|
|
79
|
+
at_exit do
|
|
80
|
+
begin
|
|
81
|
+
Client.close_pool
|
|
82
|
+
rescue StandardError # rubocop:disable Lint/SuppressedException
|
|
83
|
+
end
|
|
84
|
+
end
|
|
62
85
|
end
|
data/quicsilver.gemspec
CHANGED
|
@@ -11,6 +11,7 @@ Gem::Specification.new do |spec|
|
|
|
11
11
|
spec.summary = %q{HTTP/3 server implementation for Ruby}
|
|
12
12
|
spec.description = %q{HTTP/3 server implementation for Ruby}
|
|
13
13
|
spec.homepage = "https://github.com/hahmed/quicsilver"
|
|
14
|
+
spec.license = "MIT"
|
|
14
15
|
|
|
15
16
|
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
|
16
17
|
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
|
@@ -28,12 +29,16 @@ Gem::Specification.new do |spec|
|
|
|
28
29
|
# Specify which files should be added to the gem when it is released.
|
|
29
30
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
30
31
|
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
|
31
|
-
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
32
|
+
files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|vendor)/}) }
|
|
33
|
+
# Include precompiled binaries if present
|
|
34
|
+
files += Dir["lib/quicsilver/libmsquic*"]
|
|
35
|
+
files += Dir["lib/quicsilver/quicsilver.{bundle,so}"]
|
|
36
|
+
files
|
|
32
37
|
end
|
|
33
38
|
spec.bindir = "exe"
|
|
34
39
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
35
40
|
spec.require_paths = ["lib"]
|
|
36
|
-
spec.required_ruby_version = ">= 3.
|
|
41
|
+
spec.required_ruby_version = ">= 3.4.0"
|
|
37
42
|
|
|
38
43
|
spec.extensions = ['ext/quicsilver/extconf.rb']
|
|
39
44
|
|
|
@@ -44,6 +49,9 @@ Gem::Specification.new do |spec|
|
|
|
44
49
|
spec.add_development_dependency "minitest", "~> 5.0"
|
|
45
50
|
spec.add_development_dependency "minitest-focus", "~> 1.3"
|
|
46
51
|
spec.add_development_dependency "benchmark-ips", "~> 2.12"
|
|
52
|
+
spec.add_dependency "protocol-http", "~> 0.49"
|
|
53
|
+
spec.add_dependency "protocol-rack", "~> 0.22"
|
|
54
|
+
spec.add_dependency "console" # TODO: drop when protocol-rack declares this in its gemspec
|
|
47
55
|
spec.add_dependency "logger"
|
|
48
56
|
spec.add_dependency "localhost", "~> 1.6"
|
|
49
57
|
spec.add_dependency "rack", "~> 3.0"
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: quicsilver
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Haroon Ahmed
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-04-25 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: bundler
|
|
@@ -107,6 +107,48 @@ dependencies:
|
|
|
107
107
|
- - "~>"
|
|
108
108
|
- !ruby/object:Gem::Version
|
|
109
109
|
version: '2.12'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: protocol-http
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0.49'
|
|
117
|
+
type: :runtime
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0.49'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: protocol-rack
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - "~>"
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '0.22'
|
|
131
|
+
type: :runtime
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - "~>"
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '0.22'
|
|
138
|
+
- !ruby/object:Gem::Dependency
|
|
139
|
+
name: console
|
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - ">="
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: '0'
|
|
145
|
+
type: :runtime
|
|
146
|
+
prerelease: false
|
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - ">="
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: '0'
|
|
110
152
|
- !ruby/object:Gem::Dependency
|
|
111
153
|
name: logger
|
|
112
154
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -172,6 +214,7 @@ extensions:
|
|
|
172
214
|
extra_rdoc_files: []
|
|
173
215
|
files:
|
|
174
216
|
- ".github/workflows/ci.yml"
|
|
217
|
+
- ".github/workflows/cibuildgem.yaml"
|
|
175
218
|
- ".gitignore"
|
|
176
219
|
- ".gitmodules"
|
|
177
220
|
- ".ruby-version"
|
|
@@ -190,16 +233,33 @@ files:
|
|
|
190
233
|
- bin/console
|
|
191
234
|
- bin/setup
|
|
192
235
|
- examples/README.md
|
|
236
|
+
- examples/benchmark.rb
|
|
237
|
+
- examples/connection_pool_demo.rb
|
|
238
|
+
- examples/example_helper.rb
|
|
239
|
+
- examples/falcon_middleware.rb
|
|
240
|
+
- examples/feature_demo.rb
|
|
241
|
+
- examples/grpc_style.rb
|
|
193
242
|
- examples/minimal_http3_server.rb
|
|
243
|
+
- examples/priorities.rb
|
|
244
|
+
- examples/protocol_http_server.rb
|
|
194
245
|
- examples/rack_http3_server.rb
|
|
195
|
-
- examples/
|
|
246
|
+
- examples/rails_feature_test.rb
|
|
196
247
|
- examples/simple_client_test.rb
|
|
248
|
+
- examples/streaming_sse.rb
|
|
249
|
+
- examples/trailers.rb
|
|
197
250
|
- ext/quicsilver/extconf.rb
|
|
198
251
|
- ext/quicsilver/quicsilver.c
|
|
199
252
|
- lib/quicsilver.rb
|
|
200
253
|
- lib/quicsilver/client/client.rb
|
|
254
|
+
- lib/quicsilver/client/connection_pool.rb
|
|
201
255
|
- lib/quicsilver/client/request.rb
|
|
256
|
+
- lib/quicsilver/libmsquic.2.dylib
|
|
257
|
+
- lib/quicsilver/protocol/adapter.rb
|
|
258
|
+
- lib/quicsilver/protocol/control_stream_parser.rb
|
|
259
|
+
- lib/quicsilver/protocol/frame_parser.rb
|
|
260
|
+
- lib/quicsilver/protocol/frame_reader.rb
|
|
202
261
|
- lib/quicsilver/protocol/frames.rb
|
|
262
|
+
- lib/quicsilver/protocol/priority.rb
|
|
203
263
|
- lib/quicsilver/protocol/qpack/decoder.rb
|
|
204
264
|
- lib/quicsilver/protocol/qpack/encoder.rb
|
|
205
265
|
- lib/quicsilver/protocol/qpack/header_block_decoder.rb
|
|
@@ -208,6 +268,9 @@ files:
|
|
|
208
268
|
- lib/quicsilver/protocol/request_parser.rb
|
|
209
269
|
- lib/quicsilver/protocol/response_encoder.rb
|
|
210
270
|
- lib/quicsilver/protocol/response_parser.rb
|
|
271
|
+
- lib/quicsilver/protocol/stream_input.rb
|
|
272
|
+
- lib/quicsilver/protocol/stream_output.rb
|
|
273
|
+
- lib/quicsilver/quicsilver.bundle
|
|
211
274
|
- lib/quicsilver/server/listener_data.rb
|
|
212
275
|
- lib/quicsilver/server/request_handler.rb
|
|
213
276
|
- lib/quicsilver/server/request_registry.rb
|
|
@@ -222,7 +285,8 @@ files:
|
|
|
222
285
|
- lib/rackup/handler/quicsilver.rb
|
|
223
286
|
- quicsilver.gemspec
|
|
224
287
|
homepage: https://github.com/hahmed/quicsilver
|
|
225
|
-
licenses:
|
|
288
|
+
licenses:
|
|
289
|
+
- MIT
|
|
226
290
|
metadata:
|
|
227
291
|
allowed_push_host: https://rubygems.org
|
|
228
292
|
homepage_uri: https://github.com/hahmed/quicsilver
|
|
@@ -235,7 +299,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
235
299
|
requirements:
|
|
236
300
|
- - ">="
|
|
237
301
|
- !ruby/object:Gem::Version
|
|
238
|
-
version: 3.
|
|
302
|
+
version: 3.4.0
|
|
239
303
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
240
304
|
requirements:
|
|
241
305
|
- - ">="
|
data/examples/setup_certs.sh
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
# Quicsilver Certificate Setup
|
|
4
|
-
# This script generates self-signed certificates for QUIC testing
|
|
5
|
-
|
|
6
|
-
echo "🔐 Generating certificates for Quicsilver QUIC testing..."
|
|
7
|
-
|
|
8
|
-
# Create certs directory if it doesn't exist
|
|
9
|
-
mkdir -p ../certs
|
|
10
|
-
cd ../certs
|
|
11
|
-
|
|
12
|
-
# Create OpenSSL config with proper TLS server extensions
|
|
13
|
-
cat > openssl.conf << 'EOF'
|
|
14
|
-
[req]
|
|
15
|
-
distinguished_name = req_distinguished_name
|
|
16
|
-
req_extensions = v3_req
|
|
17
|
-
prompt = no
|
|
18
|
-
|
|
19
|
-
[req_distinguished_name]
|
|
20
|
-
CN = localhost
|
|
21
|
-
O = QuicsilverTest
|
|
22
|
-
C = US
|
|
23
|
-
|
|
24
|
-
[v3_req]
|
|
25
|
-
keyUsage = keyEncipherment, dataEncipherment
|
|
26
|
-
extendedKeyUsage = serverAuth
|
|
27
|
-
subjectAltName = @alt_names
|
|
28
|
-
|
|
29
|
-
[alt_names]
|
|
30
|
-
DNS.1 = localhost
|
|
31
|
-
IP.1 = 127.0.0.1
|
|
32
|
-
EOF
|
|
33
|
-
|
|
34
|
-
# Generate private key and certificate
|
|
35
|
-
echo "📝 Generating private key..."
|
|
36
|
-
openssl genrsa -out server.key 2048
|
|
37
|
-
|
|
38
|
-
echo "📜 Generating certificate..."
|
|
39
|
-
openssl req -new -x509 -key server.key -out server.crt -days 365 \
|
|
40
|
-
-config openssl.conf -extensions v3_req
|
|
41
|
-
|
|
42
|
-
# Create PKCS#12 format (optional, for other tools)
|
|
43
|
-
echo "📦 Creating PKCS#12 bundle..."
|
|
44
|
-
openssl pkcs12 -export -out server.p12 -inkey server.key -in server.crt \
|
|
45
|
-
-passout pass:password -name "localhost"
|
|
46
|
-
|
|
47
|
-
# Clean up
|
|
48
|
-
rm openssl.conf
|
|
49
|
-
|
|
50
|
-
echo "✅ Certificate files created:"
|
|
51
|
-
echo " 📄 server.crt - Certificate file"
|
|
52
|
-
echo " 🔑 server.key - Private key file"
|
|
53
|
-
echo " 📦 server.p12 - PKCS#12 bundle (password: 'password')"
|
|
54
|
-
echo ""
|
|
55
|
-
echo "🚀 You can now run:"
|
|
56
|
-
echo " Terminal 1: ruby examples/test_server.rb"
|
|
57
|
-
echo " Terminal 2: ruby examples/test_connection.rb"
|