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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +41 -0
  3. data/.gitignore +3 -1
  4. data/CHANGELOG.md +76 -5
  5. data/Gemfile.lock +18 -4
  6. data/LICENSE +21 -0
  7. data/README.md +33 -53
  8. data/Rakefile +29 -2
  9. data/benchmarks/components.rb +191 -0
  10. data/benchmarks/concurrent.rb +110 -0
  11. data/benchmarks/helpers.rb +88 -0
  12. data/benchmarks/quicsilver_server.rb +46 -0
  13. data/benchmarks/rails.rb +170 -0
  14. data/benchmarks/throughput.rb +113 -0
  15. data/examples/minimal_http3_server.rb +0 -6
  16. data/examples/rack_http3_server.rb +0 -6
  17. data/examples/simple_client_test.rb +26 -0
  18. data/ext/quicsilver/quicsilver.c +615 -138
  19. data/lib/quicsilver/client/client.rb +250 -0
  20. data/lib/quicsilver/client/request.rb +98 -0
  21. data/lib/quicsilver/protocol/frames.rb +327 -0
  22. data/lib/quicsilver/protocol/qpack/decoder.rb +165 -0
  23. data/lib/quicsilver/protocol/qpack/encoder.rb +189 -0
  24. data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +125 -0
  25. data/lib/quicsilver/protocol/qpack/huffman.rb +459 -0
  26. data/lib/quicsilver/protocol/request_encoder.rb +47 -0
  27. data/lib/quicsilver/protocol/request_parser.rb +387 -0
  28. data/lib/quicsilver/protocol/response_encoder.rb +72 -0
  29. data/lib/quicsilver/protocol/response_parser.rb +249 -0
  30. data/lib/quicsilver/server/listener_data.rb +14 -0
  31. data/lib/quicsilver/server/request_handler.rb +86 -0
  32. data/lib/quicsilver/server/request_registry.rb +50 -0
  33. data/lib/quicsilver/server/server.rb +336 -0
  34. data/lib/quicsilver/transport/configuration.rb +132 -0
  35. data/lib/quicsilver/transport/connection.rb +350 -0
  36. data/lib/quicsilver/transport/event_loop.rb +38 -0
  37. data/lib/quicsilver/transport/inbound_stream.rb +33 -0
  38. data/lib/quicsilver/transport/stream.rb +28 -0
  39. data/lib/quicsilver/transport/stream_event.rb +26 -0
  40. data/lib/quicsilver/version.rb +1 -1
  41. data/lib/quicsilver.rb +49 -9
  42. data/lib/rackup/handler/quicsilver.rb +77 -0
  43. data/quicsilver.gemspec +10 -3
  44. metadata +122 -17
  45. data/examples/minimal_http3_client.rb +0 -89
  46. data/lib/quicsilver/client.rb +0 -191
  47. data/lib/quicsilver/http3/request_encoder.rb +0 -112
  48. data/lib/quicsilver/http3/request_parser.rb +0 -158
  49. data/lib/quicsilver/http3/response_encoder.rb +0 -73
  50. data/lib/quicsilver/http3.rb +0 -68
  51. data/lib/quicsilver/listener_data.rb +0 -29
  52. data/lib/quicsilver/server.rb +0 -258
  53. 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
@@ -1,3 +1,3 @@
1
1
  module Quicsilver
2
- VERSION = "0.1.0"
2
+ VERSION = "0.3.0"
3
3
  end
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
- require_relative "quicsilver/client"
5
- require_relative "quicsilver/listener_data"
6
- require_relative "quicsilver/server"
7
- require_relative "quicsilver/server_configuration"
8
- require_relative "quicsilver/http3"
9
- require_relative "quicsilver/http3/request_parser"
10
- require_relative "quicsilver/http3/request_encoder"
11
- require_relative "quicsilver/http3/response_encoder"
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
- end
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{Minimal HTTP/3 server implementation for Ruby}
12
- spec.description = %q{A minimal HTTP/3 server implementation for Ruby using Microsoft's MSQUIC library.}
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", "~> 10.0"
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