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,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quicsilver
|
|
4
|
+
module Transport
|
|
5
|
+
# Two threads: one drives MsQuic, one delivers its events to Ruby.
|
|
6
|
+
#
|
|
7
|
+
# The poll thread runs Quicsilver.poll, which releases the GVL for the
|
|
8
|
+
# whole MsQuic iteration: decryption, loss recovery, stream reassembly and
|
|
9
|
+
# the callbacks that queue events for Ruby all happen without the lock.
|
|
10
|
+
# The dispatcher thread runs Quicsilver.dispatch_events, which waits on
|
|
11
|
+
# that queue without the GVL, then takes it to call Server.handle_stream
|
|
12
|
+
# (or Client#handle_stream_event) for each event in order.
|
|
13
|
+
#
|
|
14
|
+
# Before, a single thread did both, and every byte MsQuic processed was
|
|
15
|
+
# processed while holding the GVL, because the callback at the end of it
|
|
16
|
+
# called into Ruby. Splitting them lets the datapath run in parallel with
|
|
17
|
+
# the dispatcher and the worker pool.
|
|
18
|
+
#
|
|
19
|
+
# Event order is preserved: the queue is FIFO and there is one consumer.
|
|
20
|
+
class EventLoop
|
|
21
|
+
# Events delivered per GVL hold. Bounds how long a batch runs; the
|
|
22
|
+
# dispatcher yields early only when the server's admission queue is
|
|
23
|
+
# under pressure (Server#enqueue_work). QUICSILVER_DISPATCH_BATCH
|
|
24
|
+
# overrides it for measurement.
|
|
25
|
+
DISPATCH_BATCH = Integer(ENV.fetch("QUICSILVER_DISPATCH_BATCH", "64"))
|
|
26
|
+
DISPATCH_WAIT_MS = 1000
|
|
27
|
+
|
|
28
|
+
def initialize
|
|
29
|
+
@running = false
|
|
30
|
+
@thread = nil
|
|
31
|
+
@dispatcher = nil
|
|
32
|
+
@mutex = Mutex.new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def start
|
|
36
|
+
@mutex.synchronize do
|
|
37
|
+
return if @running
|
|
38
|
+
|
|
39
|
+
@running = true
|
|
40
|
+
Quicsilver.open_event_queue
|
|
41
|
+
@dispatcher = Thread.new do
|
|
42
|
+
Thread.current.name = "quicsilver-dispatch"
|
|
43
|
+
loop do
|
|
44
|
+
delivered = Quicsilver.dispatch_events(DISPATCH_BATCH, DISPATCH_WAIT_MS)
|
|
45
|
+
break if delivered.nil? # queue closed and drained
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
@thread = Thread.new do
|
|
49
|
+
Thread.current.name = "quicsilver-poll"
|
|
50
|
+
Quicsilver.poll while @running
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Stop the poll thread first so nothing else is queued, then close the
|
|
56
|
+
# queue so the dispatcher exits once it has delivered what is there.
|
|
57
|
+
def stop
|
|
58
|
+
@running = false
|
|
59
|
+
Quicsilver.wake
|
|
60
|
+
@thread&.join(2)
|
|
61
|
+
Quicsilver.close_event_queue
|
|
62
|
+
@dispatcher&.join(2)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def join
|
|
66
|
+
@thread&.join
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.event_loop
|
|
72
|
+
@event_loop ||= Transport::EventLoop.new.tap(&:start)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
StreamId.bidirectional?(stream_id)
|
|
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
|
+
|
|
32
|
+
def send(data, fin: false)
|
|
33
|
+
return unless writable?
|
|
34
|
+
Quicsilver.send_stream(@stream_handle, data, fin)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def reset(error_code = Protocol::H3_REQUEST_CANCELLED)
|
|
38
|
+
return unless writable?
|
|
39
|
+
Quicsilver.stream_reset(@stream_handle, error_code)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def stop_sending(error_code = Protocol::H3_REQUEST_CANCELLED)
|
|
43
|
+
return unless writable?
|
|
44
|
+
Quicsilver.stream_stop_sending(@stream_handle, error_code)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def abort(error_code = Protocol::H3_REQUEST_CANCELLED)
|
|
48
|
+
return unless writable?
|
|
49
|
+
Quicsilver.stream_abort(@stream_handle, error_code)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quicsilver
|
|
4
|
+
module Transport
|
|
5
|
+
# The handle is an opaque native registry token, not a pointer.
|
|
6
|
+
class Stream
|
|
7
|
+
attr_reader :handle
|
|
8
|
+
|
|
9
|
+
def initialize(handle)
|
|
10
|
+
@handle = handle
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Returns nil until asynchronous stream startup completes, or after shutdown.
|
|
14
|
+
def stream_id
|
|
15
|
+
Quicsilver.get_stream_id(@handle)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def send(data, fin: false)
|
|
19
|
+
Quicsilver.send_stream(@handle, data, fin)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def reliable_offset=(offset)
|
|
23
|
+
Quicsilver.set_stream_reliable_offset(@handle, offset)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def grant_receive_credit(bytes, chunks)
|
|
27
|
+
Quicsilver.grant_stream_receive_credit(@handle, bytes, chunks)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def reset(error_code = Protocol::H3_REQUEST_CANCELLED)
|
|
31
|
+
Quicsilver.stream_reset(@handle, error_code)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def stop_sending(error_code = Protocol::H3_REQUEST_CANCELLED)
|
|
35
|
+
Quicsilver.stream_stop_sending(@handle, error_code)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def abort(error_code = Protocol::H3_REQUEST_CANCELLED)
|
|
39
|
+
Quicsilver.stream_abort(@handle, error_code)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
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)][final_size(8)]
|
|
9
|
+
# STOP_SENDING: [stream_handle(8)][error_code(8)]
|
|
10
|
+
class StreamEvent
|
|
11
|
+
# A reset stream's final size is unknown to MsQuic until RESET_STREAM or
|
|
12
|
+
# FIN settles it; the C extension reports that as this sentinel so it
|
|
13
|
+
# never has to be confused with a real length.
|
|
14
|
+
FINAL_SIZE_UNKNOWN = (2**64) - 1
|
|
15
|
+
|
|
16
|
+
attr_reader :handle, :data, :error_code
|
|
17
|
+
|
|
18
|
+
# The total number of bytes the peer sent on this stream, from the
|
|
19
|
+
# RESET_STREAM frame (RFC 9000 4.5). nil when it is not settled or the
|
|
20
|
+
# event does not carry one. WebTransport session flow control needs it to
|
|
21
|
+
# charge the bytes a reset discarded (draft-ietf-webtrans-http3-16 5.4).
|
|
22
|
+
attr_reader :final_size
|
|
23
|
+
|
|
24
|
+
def initialize(raw_data, event_type)
|
|
25
|
+
@handle = raw_data[0, 8].unpack1("Q")
|
|
26
|
+
remaining = raw_data[8..] || "".b
|
|
27
|
+
|
|
28
|
+
case event_type
|
|
29
|
+
when "RECEIVE", "RECEIVE_FIN"
|
|
30
|
+
@data = remaining
|
|
31
|
+
when "STREAM_RESET", "STOP_SENDING"
|
|
32
|
+
@error_code = remaining.unpack1("Q")
|
|
33
|
+
@final_size = parse_final_size(remaining)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# Older packings carry only the error code, so a missing final size is
|
|
40
|
+
# not an error.
|
|
41
|
+
def parse_final_size(remaining)
|
|
42
|
+
return unless remaining.bytesize >= 16
|
|
43
|
+
|
|
44
|
+
value = remaining.unpack1("@8Q")
|
|
45
|
+
value unless value == FINAL_SIZE_UNKNOWN
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quicsilver
|
|
4
|
+
module Transport
|
|
5
|
+
module StreamId
|
|
6
|
+
INITIATOR_MASK = 0x01
|
|
7
|
+
DIRECTION_MASK = 0x02
|
|
8
|
+
REQUEST_STREAM_MASK = INITIATOR_MASK | DIRECTION_MASK
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def bidirectional?(stream_id)
|
|
13
|
+
(stream_id & DIRECTION_MASK).zero?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def unidirectional?(stream_id)
|
|
17
|
+
!bidirectional?(stream_id)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# HTTP/3 requests run on client-initiated bidirectional QUIC streams.
|
|
21
|
+
def request?(stream_id)
|
|
22
|
+
(stream_id & REQUEST_STREAM_MASK).zero?
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/quicsilver.rb
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
require_relative "quicsilver/version"
|
|
5
|
+
require_relative "quicsilver/errors"
|
|
6
|
+
require_relative "quicsilver/response"
|
|
7
|
+
|
|
8
|
+
# Protocol layer
|
|
9
|
+
require_relative "quicsilver/protocol/frames"
|
|
10
|
+
require_relative "quicsilver/protocol/datagram"
|
|
11
|
+
require_relative "quicsilver/protocol/capsule"
|
|
12
|
+
require_relative "quicsilver/protocol/web_transport"
|
|
13
|
+
require_relative "quicsilver/protocol/unidirectional_stream"
|
|
14
|
+
require_relative "quicsilver/protocol/priority"
|
|
15
|
+
require_relative "quicsilver/protocol/qpack/encoder"
|
|
16
|
+
require_relative "quicsilver/protocol/request_parser"
|
|
17
|
+
require_relative "quicsilver/protocol/request_encoder"
|
|
18
|
+
require_relative "quicsilver/protocol/response_parser"
|
|
19
|
+
require_relative "quicsilver/protocol/response_encoder"
|
|
20
|
+
require_relative "quicsilver/protocol/stream_input"
|
|
21
|
+
require_relative "quicsilver/protocol/stream_output"
|
|
22
|
+
require_relative "quicsilver/protocol/adapter"
|
|
23
|
+
require_relative "quicsilver/protocol/control_stream_parser"
|
|
24
|
+
require "protocol/rack"
|
|
25
|
+
|
|
26
|
+
# Rack integration
|
|
27
|
+
require_relative "quicsilver/rack/context"
|
|
28
|
+
|
|
29
|
+
# Transport layer
|
|
30
|
+
require_relative "quicsilver/transport/stream_id"
|
|
31
|
+
require_relative "quicsilver/transport/stream"
|
|
32
|
+
require_relative "quicsilver/transport/stream_event"
|
|
33
|
+
require_relative "quicsilver/transport/inbound_stream"
|
|
34
|
+
require_relative "quicsilver/transport/event_loop"
|
|
35
|
+
require_relative "quicsilver/transport/configuration"
|
|
36
|
+
require_relative "quicsilver/transport/connection"
|
|
37
|
+
require_relative "quicsilver/transport/connection_stats"
|
|
38
|
+
|
|
39
|
+
# Server
|
|
40
|
+
require_relative "quicsilver/server/listener_data"
|
|
41
|
+
require_relative "quicsilver/server/request_registry"
|
|
42
|
+
require_relative "quicsilver/server/request_handler"
|
|
43
|
+
require_relative "quicsilver/server/rack_adapter"
|
|
44
|
+
require_relative "quicsilver/server/web_transport_flow_control"
|
|
45
|
+
require_relative "quicsilver/server/web_transport_manager"
|
|
46
|
+
require_relative "quicsilver/server/server"
|
|
47
|
+
|
|
48
|
+
# Client
|
|
49
|
+
require_relative "quicsilver/client/request"
|
|
50
|
+
require_relative "quicsilver/client/connection_pool"
|
|
51
|
+
require_relative "quicsilver/client/client"
|
|
52
|
+
|
|
53
|
+
# C extension
|
|
54
|
+
begin
|
|
55
|
+
ruby_version = /(\d+\.\d+)/.match(RUBY_VERSION)
|
|
56
|
+
require "quicsilver/#{ruby_version}/quicsilver"
|
|
57
|
+
rescue LoadError
|
|
58
|
+
require "quicsilver/quicsilver"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Rackup handler
|
|
62
|
+
require_relative "rackup/handler/quicsilver"
|
|
63
|
+
|
|
64
|
+
module Quicsilver
|
|
65
|
+
class << self
|
|
66
|
+
attr_writer :logger
|
|
67
|
+
|
|
68
|
+
def stream_abort(handle, error_code)
|
|
69
|
+
receiving_stopped = stream_stop_sending(handle, error_code)
|
|
70
|
+
sending_reset = stream_reset(handle, error_code)
|
|
71
|
+
receiving_stopped || sending_reset
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def logger
|
|
75
|
+
@logger ||= default_logger
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def default_logger
|
|
81
|
+
Logger.new($stdout, level: Logger::INFO).tap do |log|
|
|
82
|
+
log.progname = "Quicsilver"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Release pooled client connections on process exit.
|
|
88
|
+
# Closes connection handles so the OS doesn't leak UDP sockets.
|
|
89
|
+
# MsQuic itself is cleaned up by the OS when the process exits.
|
|
90
|
+
at_exit do
|
|
91
|
+
begin
|
|
92
|
+
Client.close_pool
|
|
93
|
+
rescue StandardError # rubocop:disable Lint/SuppressedException
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rackup/handler"
|
|
4
|
+
|
|
5
|
+
module Quicsilver
|
|
6
|
+
module RackHandler
|
|
7
|
+
DEFAULT_OPTIONS = {
|
|
8
|
+
Host: "0.0.0.0",
|
|
9
|
+
Port: 4433,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def 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, key_file = certificate_paths(options)
|
|
20
|
+
config = ::Quicsilver::Transport::Configuration.new(cert_file, key_file)
|
|
21
|
+
|
|
22
|
+
server = ::Quicsilver::Server.new(
|
|
23
|
+
normalized_options[:port],
|
|
24
|
+
address: normalized_options[:host],
|
|
25
|
+
app: app,
|
|
26
|
+
server_configuration: config
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
yield server if block_given?
|
|
30
|
+
|
|
31
|
+
server.start
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def valid_options
|
|
35
|
+
{
|
|
36
|
+
"Host=HOST" => "Hostname to listen on (default: 0.0.0.0)",
|
|
37
|
+
"Port=PORT" => "Port to listen on (default: 4433)",
|
|
38
|
+
"cert_file=PATH" => "Path to TLS certificate file (required in production)",
|
|
39
|
+
"key_file=PATH" => "Path to TLS key file (required in production)"
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def certificate_paths(options)
|
|
46
|
+
cert_file = options[:cert_file]
|
|
47
|
+
key_file = options[:key_file]
|
|
48
|
+
|
|
49
|
+
if cert_file.nil? && key_file.nil?
|
|
50
|
+
if production?(options)
|
|
51
|
+
raise ArgumentError, "cert_file and key_file are required in production"
|
|
52
|
+
else
|
|
53
|
+
localhost_certificate_paths
|
|
54
|
+
end
|
|
55
|
+
elsif cert_file.nil? || key_file.nil?
|
|
56
|
+
raise ArgumentError, "cert_file and key_file must be provided together"
|
|
57
|
+
else
|
|
58
|
+
[cert_file, key_file]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def production?(options)
|
|
63
|
+
env = options[:environment] || ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
|
|
64
|
+
env == "production"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def localhost_certificate_paths
|
|
68
|
+
require "localhost/authority"
|
|
69
|
+
authority = Localhost::Authority.fetch
|
|
70
|
+
cert_file = authority.certificate_path
|
|
71
|
+
key_file = authority.key_path
|
|
72
|
+
|
|
73
|
+
Quicsilver.logger.info("Using auto-generated certificates for localhost")
|
|
74
|
+
Quicsilver.logger.info(" Cert: #{cert_file}")
|
|
75
|
+
Quicsilver.logger.info(" Key: #{key_file}")
|
|
76
|
+
|
|
77
|
+
[cert_file, key_file]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
module Rackup
|
|
84
|
+
module Handler
|
|
85
|
+
module Quicsilver
|
|
86
|
+
def self.run(app, **options, &block)
|
|
87
|
+
::Quicsilver::RackHandler.run(app, **options, &block)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def self.valid_options
|
|
91
|
+
::Quicsilver::RackHandler.valid_options
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
register :quicsilver, Quicsilver
|
|
95
|
+
end
|
|
96
|
+
end
|
data/quicsilver.gemspec
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
3
|
+
require "quicsilver/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "quicsilver"
|
|
7
|
+
spec.version = Quicsilver::VERSION
|
|
8
|
+
spec.authors = ["Haroon Ahmed"]
|
|
9
|
+
spec.email = ["haroon.ahmed25@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = %q{HTTP/3 server implementation for Ruby}
|
|
12
|
+
spec.description = %q{HTTP/3 server implementation for Ruby}
|
|
13
|
+
spec.homepage = "https://github.com/hahmed/quicsilver"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
|
|
16
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
|
17
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
|
18
|
+
if spec.respond_to?(:metadata)
|
|
19
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
|
20
|
+
|
|
21
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
22
|
+
spec.metadata["source_code_uri"] = "https://github.com/hahmed/quicsilver"
|
|
23
|
+
spec.metadata["changelog_uri"] = "https://github.com/hahmed/quicsilver/blob/main/CHANGELOG.md"
|
|
24
|
+
else
|
|
25
|
+
raise "RubyGems 2.0 or newer is required to protect against " \
|
|
26
|
+
"public gem pushes."
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Specify which files should be added to the gem when it is released.
|
|
30
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
31
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
|
32
|
+
files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|vendor)/}) }
|
|
33
|
+
|
|
34
|
+
if ENV["RELEASE"]
|
|
35
|
+
if RUBY_PLATFORM.include?("darwin")
|
|
36
|
+
files += Dir["lib/quicsilver/libmsquic*.dylib"]
|
|
37
|
+
elsif RUBY_PLATFORM.include?("linux")
|
|
38
|
+
files += Dir["lib/quicsilver/libmsquic.so*"]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
files
|
|
43
|
+
end
|
|
44
|
+
spec.bindir = "exe"
|
|
45
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
46
|
+
spec.require_paths = ["lib"]
|
|
47
|
+
spec.required_ruby_version = ">= 3.4.0"
|
|
48
|
+
|
|
49
|
+
spec.extensions = ['ext/quicsilver/extconf.rb']
|
|
50
|
+
|
|
51
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
|
52
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
53
|
+
spec.add_development_dependency 'rake-compiler', '~> 1.2'
|
|
54
|
+
spec.add_development_dependency 'rake-compiler-dock', '~> 1.3'
|
|
55
|
+
spec.add_development_dependency "cibuildgem"
|
|
56
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
|
57
|
+
spec.add_development_dependency "minitest-focus", "~> 1.3"
|
|
58
|
+
spec.add_development_dependency "benchmark-ips", "~> 2.12"
|
|
59
|
+
spec.add_dependency "protocol-http", "~> 0.49"
|
|
60
|
+
spec.add_dependency "protocol-rack", "~> 0.22"
|
|
61
|
+
spec.add_dependency "console" # TODO: drop when protocol-rack declares this in its gemspec
|
|
62
|
+
spec.add_dependency "logger"
|
|
63
|
+
spec.add_dependency "localhost", "~> 1.6"
|
|
64
|
+
spec.add_dependency "rack", "~> 3.0"
|
|
65
|
+
spec.add_dependency "rackup", "~> 2.0"
|
|
66
|
+
end
|