quicsilver 0.2.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 +4 -5
- data/.github/workflows/cibuildgem.yaml +93 -0
- data/.gitignore +3 -1
- data/CHANGELOG.md +81 -0
- data/Gemfile.lock +26 -4
- data/README.md +95 -31
- data/Rakefile +95 -3
- data/benchmarks/components.rb +191 -0
- data/benchmarks/concurrent.rb +110 -0
- data/benchmarks/helpers.rb +88 -0
- data/benchmarks/quicsilver_server.rb +1 -1
- data/benchmarks/rails.rb +170 -0
- data/benchmarks/throughput.rb +113 -0
- 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 +568 -181
- data/lib/quicsilver/client/client.rb +349 -0
- data/lib/quicsilver/client/connection_pool.rb +106 -0
- data/lib/quicsilver/client/request.rb +98 -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/{http3.rb → protocol/frames.rb} +146 -30
- data/lib/quicsilver/protocol/priority.rb +56 -0
- data/lib/quicsilver/protocol/qpack/decoder.rb +165 -0
- data/lib/quicsilver/protocol/qpack/encoder.rb +227 -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 +47 -0
- data/lib/quicsilver/protocol/request_parser.rb +275 -0
- data/lib/quicsilver/protocol/response_encoder.rb +97 -0
- data/lib/quicsilver/protocol/response_parser.rb +141 -0
- 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/listener_data.rb +14 -0
- data/lib/quicsilver/server/request_handler.rb +138 -0
- data/lib/quicsilver/server/request_registry.rb +50 -0
- data/lib/quicsilver/server/server.rb +610 -0
- data/lib/quicsilver/transport/configuration.rb +141 -0
- data/lib/quicsilver/transport/connection.rb +379 -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 +55 -14
- data/lib/rackup/handler/quicsilver.rb +1 -2
- data/quicsilver.gemspec +13 -3
- metadata +125 -21
- data/benchmarks/benchmark.rb +0 -68
- data/examples/setup_certs.sh +0 -57
- data/lib/quicsilver/client.rb +0 -261
- data/lib/quicsilver/connection.rb +0 -42
- data/lib/quicsilver/event_loop.rb +0 -38
- data/lib/quicsilver/http3/request_encoder.rb +0 -133
- data/lib/quicsilver/http3/request_parser.rb +0 -176
- data/lib/quicsilver/http3/response_encoder.rb +0 -186
- data/lib/quicsilver/http3/response_parser.rb +0 -160
- data/lib/quicsilver/listener_data.rb +0 -29
- data/lib/quicsilver/quic_stream.rb +0 -36
- data/lib/quicsilver/request_registry.rb +0 -48
- data/lib/quicsilver/server.rb +0 -355
- data/lib/quicsilver/server_configuration.rb +0 -78
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quicsilver
|
|
4
|
+
module Transport
|
|
5
|
+
class Configuration
|
|
6
|
+
attr_reader :cert_file, :key_file, :idle_timeout_ms, :server_resumption_level, :max_concurrent_requests,
|
|
7
|
+
:max_unidirectional_streams, :stream_receive_window, :stream_receive_buffer, :connection_flow_control_window,
|
|
8
|
+
:pacing_enabled, :send_buffering_enabled, :initial_rtt_ms, :initial_window_packets, :max_ack_delay_ms,
|
|
9
|
+
:keep_alive_interval_ms, :congestion_control_algorithm, :migration_enabled,
|
|
10
|
+
:disconnect_timeout_ms, :handshake_idle_timeout_ms,
|
|
11
|
+
:max_body_size, :max_header_size, :max_header_count, :max_frame_payload_size,
|
|
12
|
+
:early_data_policy,
|
|
13
|
+
:mode
|
|
14
|
+
|
|
15
|
+
QUIC_SERVER_RESUME_AND_ZERORTT = 1
|
|
16
|
+
QUIC_SERVER_RESUME_ONLY = 2
|
|
17
|
+
QUIC_SERVER_RESUME_AND_REUSE = 3
|
|
18
|
+
QUIC_SERVER_RESUME_AND_REUSE_ZERORTT = 4
|
|
19
|
+
|
|
20
|
+
# Congestion control algorithms
|
|
21
|
+
CONGESTION_CONTROL_CUBIC = 0
|
|
22
|
+
CONGESTION_CONTROL_BBR = 1
|
|
23
|
+
|
|
24
|
+
DEFAULT_CERT_FILE = "certificates/server.crt"
|
|
25
|
+
DEFAULT_KEY_FILE = "certificates/server.key"
|
|
26
|
+
DEFAULT_ALPN = "h3"
|
|
27
|
+
|
|
28
|
+
# Flow control defaults — cross-referenced with quiche, quic-go, lsquic, RFC 9000
|
|
29
|
+
# See: https://github.com/microsoft/msquic/blob/main/docs/Settings.md
|
|
30
|
+
DEFAULT_STREAM_RECEIVE_WINDOW = 262_144 # 256KB (quiche/quic-go use 1MB, MsQuic default 64KB)
|
|
31
|
+
DEFAULT_STREAM_RECEIVE_BUFFER = 32_768 # 32KB (MsQuic default 4KB — too small for typical responses)
|
|
32
|
+
DEFAULT_CONNECTION_FLOW_CONTROL_WINDOW = 16_777_216 # 16MB - connection-wide flow control
|
|
33
|
+
|
|
34
|
+
# Throughput defaults
|
|
35
|
+
DEFAULT_PACING_ENABLED = true # RFC 9002: MUST pace or limit bursts
|
|
36
|
+
DEFAULT_SEND_BUFFERING_ENABLED = true # MsQuic recommended — coalesces small writes
|
|
37
|
+
DEFAULT_INITIAL_RTT_MS = 100 # MsQuic default 333ms is satellite-grade; 100ms matches Chromium
|
|
38
|
+
DEFAULT_INITIAL_WINDOW_PACKETS = 10 # Matches RFC 9002 recommendation
|
|
39
|
+
DEFAULT_MAX_ACK_DELAY_MS = 25 # Matches RFC 9000 default
|
|
40
|
+
|
|
41
|
+
# Connection management defaults
|
|
42
|
+
DEFAULT_KEEP_ALIVE_INTERVAL_MS = 0 # 0 = disabled. Set to 20000 for NAT traversal
|
|
43
|
+
DEFAULT_CONGESTION_CONTROL_ALGORITHM = CONGESTION_CONTROL_CUBIC # CUBIC (0) or BBR (1)
|
|
44
|
+
DEFAULT_MIGRATION_ENABLED = true # Client IP migration. Disable behind load balancers
|
|
45
|
+
DEFAULT_DISCONNECT_TIMEOUT_MS = 16_000 # How long to wait for ACK before path declared dead
|
|
46
|
+
DEFAULT_HANDSHAKE_IDLE_TIMEOUT_MS = 10_000 # Handshake timeout (separate from connection idle)
|
|
47
|
+
|
|
48
|
+
def initialize(cert_file = nil, key_file = nil, options = {})
|
|
49
|
+
@idle_timeout_ms = options.fetch(:idle_timeout_ms, 10000)
|
|
50
|
+
@server_resumption_level = options.fetch(:server_resumption_level, QUIC_SERVER_RESUME_AND_ZERORTT)
|
|
51
|
+
@max_concurrent_requests = options.fetch(:max_concurrent_requests, 100)
|
|
52
|
+
@max_unidirectional_streams = options.fetch(:max_unidirectional_streams, 10)
|
|
53
|
+
@alpn = options.fetch(:alpn, DEFAULT_ALPN)
|
|
54
|
+
|
|
55
|
+
# Flow control
|
|
56
|
+
@stream_receive_window = options.fetch(:stream_receive_window, DEFAULT_STREAM_RECEIVE_WINDOW)
|
|
57
|
+
@stream_receive_buffer = options.fetch(:stream_receive_buffer, DEFAULT_STREAM_RECEIVE_BUFFER)
|
|
58
|
+
@connection_flow_control_window = options.fetch(:connection_flow_control_window, DEFAULT_CONNECTION_FLOW_CONTROL_WINDOW)
|
|
59
|
+
|
|
60
|
+
# Throughput
|
|
61
|
+
@pacing_enabled = options.fetch(:pacing_enabled, DEFAULT_PACING_ENABLED)
|
|
62
|
+
@send_buffering_enabled = options.fetch(:send_buffering_enabled, DEFAULT_SEND_BUFFERING_ENABLED)
|
|
63
|
+
@initial_rtt_ms = options.fetch(:initial_rtt_ms, DEFAULT_INITIAL_RTT_MS)
|
|
64
|
+
@initial_window_packets = options.fetch(:initial_window_packets, DEFAULT_INITIAL_WINDOW_PACKETS)
|
|
65
|
+
@max_ack_delay_ms = options.fetch(:max_ack_delay_ms, DEFAULT_MAX_ACK_DELAY_MS)
|
|
66
|
+
|
|
67
|
+
# Connection management
|
|
68
|
+
@keep_alive_interval_ms = options.fetch(:keep_alive_interval_ms, DEFAULT_KEEP_ALIVE_INTERVAL_MS)
|
|
69
|
+
@congestion_control_algorithm = options.fetch(:congestion_control_algorithm, DEFAULT_CONGESTION_CONTROL_ALGORITHM)
|
|
70
|
+
@migration_enabled = options.fetch(:migration_enabled, DEFAULT_MIGRATION_ENABLED)
|
|
71
|
+
@disconnect_timeout_ms = options.fetch(:disconnect_timeout_ms, DEFAULT_DISCONNECT_TIMEOUT_MS)
|
|
72
|
+
@handshake_idle_timeout_ms = options.fetch(:handshake_idle_timeout_ms, DEFAULT_HANDSHAKE_IDLE_TIMEOUT_MS)
|
|
73
|
+
|
|
74
|
+
# HTTP/3 parser limits (nil = unlimited)
|
|
75
|
+
@max_body_size = options[:max_body_size]
|
|
76
|
+
@max_header_size = options[:max_header_size]
|
|
77
|
+
@max_header_count = options[:max_header_count]
|
|
78
|
+
@max_frame_payload_size = options[:max_frame_payload_size]
|
|
79
|
+
|
|
80
|
+
# 0-RTT early data policy (RFC 8470)
|
|
81
|
+
# :reject (default) — send 425 Too Early for unsafe methods on 0-RTT
|
|
82
|
+
# :allow — pass all 0-RTT requests to the Rack app with env["quicsilver.early_data"]
|
|
83
|
+
@early_data_policy = options.fetch(:early_data_policy, :reject)
|
|
84
|
+
unless %i[reject allow].include?(@early_data_policy)
|
|
85
|
+
raise ServerConfigurationError, "Invalid early_data_policy: #{@early_data_policy.inspect} (must be :reject or :allow)"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Application interface mode:
|
|
89
|
+
# :rack (default) — app is a Rack app, auto-wrapped with Protocol::Rack::Adapter
|
|
90
|
+
# :falcon — app is a native protocol-http app, used directly
|
|
91
|
+
@mode = options.fetch(:mode, :rack)
|
|
92
|
+
unless %i[rack falcon].include?(@mode)
|
|
93
|
+
raise ServerConfigurationError, "Invalid mode: #{@mode.inspect} (must be :rack or :falcon)"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
@cert_file = cert_file.nil? ? DEFAULT_CERT_FILE : cert_file
|
|
97
|
+
@key_file = key_file.nil? ? DEFAULT_KEY_FILE : key_file
|
|
98
|
+
|
|
99
|
+
unless File.exist?(@cert_file)
|
|
100
|
+
raise ServerConfigurationError, "Certificate file not found: #{@cert_file}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
unless File.exist?(@key_file)
|
|
104
|
+
raise ServerConfigurationError, "Key file not found: #{@key_file}"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Common HTTP/3 ALPN Values:
|
|
109
|
+
# "h3" - HTTP/3 (most common)
|
|
110
|
+
# "h3-29" - HTTP/3 draft version 29
|
|
111
|
+
def alpn
|
|
112
|
+
@alpn
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def to_h
|
|
116
|
+
{
|
|
117
|
+
cert_file: @cert_file,
|
|
118
|
+
key_file: @key_file,
|
|
119
|
+
idle_timeout_ms: @idle_timeout_ms,
|
|
120
|
+
server_resumption_level: @server_resumption_level,
|
|
121
|
+
max_concurrent_requests: @max_concurrent_requests,
|
|
122
|
+
max_unidirectional_streams: @max_unidirectional_streams,
|
|
123
|
+
alpn: alpn,
|
|
124
|
+
stream_receive_window: @stream_receive_window,
|
|
125
|
+
stream_receive_buffer: @stream_receive_buffer,
|
|
126
|
+
connection_flow_control_window: @connection_flow_control_window,
|
|
127
|
+
pacing_enabled: @pacing_enabled ? 1 : 0,
|
|
128
|
+
send_buffering_enabled: @send_buffering_enabled ? 1 : 0,
|
|
129
|
+
initial_rtt_ms: @initial_rtt_ms,
|
|
130
|
+
initial_window_packets: @initial_window_packets,
|
|
131
|
+
max_ack_delay_ms: @max_ack_delay_ms,
|
|
132
|
+
keep_alive_interval_ms: @keep_alive_interval_ms,
|
|
133
|
+
congestion_control_algorithm: @congestion_control_algorithm,
|
|
134
|
+
migration_enabled: @migration_enabled ? 1 : 0,
|
|
135
|
+
disconnect_timeout_ms: @disconnect_timeout_ms,
|
|
136
|
+
handshake_idle_timeout_ms: @handshake_idle_timeout_ms
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quicsilver
|
|
4
|
+
module Transport
|
|
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
|
+
|
|
13
|
+
attr_reader :handle, :data, :streams
|
|
14
|
+
attr_reader :control_stream_id, :qpack_encoder_stream_id, :qpack_decoder_stream_id
|
|
15
|
+
attr_reader :server_control_stream
|
|
16
|
+
attr_reader :peer_goaway_id, :local_goaway_id
|
|
17
|
+
attr_reader :stream_priorities
|
|
18
|
+
|
|
19
|
+
def initialize(handle, data, max_header_size: nil)
|
|
20
|
+
@handle = handle
|
|
21
|
+
@data = data
|
|
22
|
+
@max_header_size = max_header_size
|
|
23
|
+
@streams = {}
|
|
24
|
+
@response_buffers = {}
|
|
25
|
+
@mutex = Mutex.new
|
|
26
|
+
|
|
27
|
+
# Client's control streams (received)
|
|
28
|
+
@control_stream_id = nil
|
|
29
|
+
@qpack_encoder_stream_id = nil
|
|
30
|
+
@qpack_decoder_stream_id = nil
|
|
31
|
+
|
|
32
|
+
# Server's control stream (sent)
|
|
33
|
+
@server_control_stream = nil
|
|
34
|
+
|
|
35
|
+
@settings = {}
|
|
36
|
+
@settings_received = false
|
|
37
|
+
@peer_goaway_id = nil
|
|
38
|
+
@local_goaway_id = nil
|
|
39
|
+
@stream_priorities = {}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# === Setup (called after connection established) ===
|
|
43
|
+
|
|
44
|
+
def setup_http3_streams
|
|
45
|
+
# Control stream (required)
|
|
46
|
+
@server_control_stream = open_stream(unidirectional: true)
|
|
47
|
+
@server_control_stream.send(Protocol.build_control_stream(max_field_section_size: @max_header_size))
|
|
48
|
+
|
|
49
|
+
# QPACK encoder/decoder streams
|
|
50
|
+
[0x02, 0x03].each do |type|
|
|
51
|
+
stream = open_stream(unidirectional: true)
|
|
52
|
+
stream.send([type].pack("C"))
|
|
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)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# === Stream Management ===
|
|
61
|
+
|
|
62
|
+
def add_stream(stream)
|
|
63
|
+
@streams[stream.stream_id] = stream
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def get_stream(stream_id)
|
|
67
|
+
@streams[stream_id]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def remove_stream(stream_id)
|
|
71
|
+
@streams.delete(stream_id)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def track_client_stream(stream_id)
|
|
75
|
+
@streams[stream_id] = true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# === Data Handling ===
|
|
79
|
+
|
|
80
|
+
def buffer_data(stream_id, data)
|
|
81
|
+
@mutex.synchronize do
|
|
82
|
+
(@response_buffers[stream_id] ||= "".b) << data
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def complete_stream(stream_id, final_data)
|
|
87
|
+
@mutex.synchronize do
|
|
88
|
+
buffer = @response_buffers.delete(stream_id)
|
|
89
|
+
(buffer || "".b) + (final_data || "".b)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# === HTTP/3 Frames ===
|
|
94
|
+
|
|
95
|
+
def send_goaway(stream_id = nil)
|
|
96
|
+
return unless @server_control_stream
|
|
97
|
+
|
|
98
|
+
stream_id ||= last_client_stream_id
|
|
99
|
+
validate_goaway_id!(stream_id)
|
|
100
|
+
|
|
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
|
|
105
|
+
rescue => e
|
|
106
|
+
Quicsilver.logger.error("Failed to send GOAWAY: #{e.message}")
|
|
107
|
+
end
|
|
108
|
+
|
|
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)
|
|
129
|
+
|
|
130
|
+
if body.respond_to?(:to_ary)
|
|
131
|
+
Quicsilver.send_stream(stream.stream_handle, encoder.encode, true)
|
|
132
|
+
else
|
|
133
|
+
encoder.stream_encode do |frame_data, fin|
|
|
134
|
+
Quicsilver.send_stream(stream.stream_handle, frame_data, fin) unless frame_data.empty? && !fin
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
rescue RuntimeError => e
|
|
138
|
+
# Stream may have been reset by client - this is expected
|
|
139
|
+
raise unless e.message.include?(MSQUIC_INVALID_STATE) || e.message.include?("StreamSend failed")
|
|
140
|
+
Quicsilver.logger.debug("Stream send failed (client likely reset): #{e.message}")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def send_error(stream, status, message)
|
|
144
|
+
body = ["#{status} #{message}"]
|
|
145
|
+
encoder = Protocol::ResponseEncoder.new(status, { "content-type" => "text/plain" }, body)
|
|
146
|
+
Quicsilver.send_stream(stream.stream_handle, encoder.encode, true)
|
|
147
|
+
rescue RuntimeError => e
|
|
148
|
+
# Stream may have been reset by client - this is expected
|
|
149
|
+
raise unless e.message.include?(MSQUIC_INVALID_STATE) || e.message.include?("StreamSend failed")
|
|
150
|
+
Quicsilver.logger.debug("Stream send failed (client likely reset): #{e.message}")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# === Control Stream Handling ===
|
|
154
|
+
|
|
155
|
+
# Process incoming data on a unidirectional stream incrementally.
|
|
156
|
+
# Called on each RECEIVE event — control streams never send FIN.
|
|
157
|
+
def receive_unidirectional_data(stream_id, data)
|
|
158
|
+
@mutex.synchronize do
|
|
159
|
+
(@response_buffers[stream_id] ||= "".b) << data
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
buf = @mutex.synchronize { @response_buffers[stream_id] || "".b }
|
|
163
|
+
return if buf.empty?
|
|
164
|
+
|
|
165
|
+
# First time seeing this stream: identify stream type
|
|
166
|
+
unless @uni_stream_types&.key?(stream_id)
|
|
167
|
+
@uni_stream_types ||= {}
|
|
168
|
+
stream_type, type_len = Protocol.decode_varint(buf.bytes, 0)
|
|
169
|
+
return if type_len == 0 # need more data
|
|
170
|
+
|
|
171
|
+
case stream_type
|
|
172
|
+
when 0x00 # Control stream
|
|
173
|
+
raise Protocol::FrameError, "Duplicate control stream" if @control_stream_id
|
|
174
|
+
@control_stream_id = stream_id
|
|
175
|
+
@uni_stream_types[stream_id] = :control
|
|
176
|
+
# Remove the stream type byte from the buffer
|
|
177
|
+
@mutex.synchronize { @response_buffers[stream_id] = (buf[type_len..] || "".b) }
|
|
178
|
+
when 0x01
|
|
179
|
+
raise Protocol::FrameError, "Client must not send push streams"
|
|
180
|
+
when 0x02 # QPACK encoder stream
|
|
181
|
+
raise Protocol::FrameError, "Duplicate QPACK encoder stream" if @qpack_encoder_stream_id
|
|
182
|
+
@qpack_encoder_stream_id = stream_id
|
|
183
|
+
@uni_stream_types[stream_id] = :qpack_encoder
|
|
184
|
+
@mutex.synchronize { @response_buffers[stream_id] = (buf[type_len..] || "".b) }
|
|
185
|
+
when 0x03 # QPACK decoder stream
|
|
186
|
+
raise Protocol::FrameError, "Duplicate QPACK decoder stream" if @qpack_decoder_stream_id
|
|
187
|
+
@qpack_decoder_stream_id = stream_id
|
|
188
|
+
@uni_stream_types[stream_id] = :qpack_decoder
|
|
189
|
+
@mutex.synchronize { @response_buffers[stream_id] = (buf[type_len..] || "".b) }
|
|
190
|
+
else
|
|
191
|
+
# Unknown unidirectional stream types MUST be ignored (RFC 9114 §6.2)
|
|
192
|
+
@uni_stream_types[stream_id] = :unknown
|
|
193
|
+
return
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
buf = @mutex.synchronize { @response_buffers[stream_id] || "".b }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
stream_type = @uni_stream_types[stream_id]
|
|
200
|
+
return if buf.empty?
|
|
201
|
+
|
|
202
|
+
case stream_type
|
|
203
|
+
when :control
|
|
204
|
+
parse_control_frames(buf)
|
|
205
|
+
# Clear parsed data from buffer
|
|
206
|
+
@mutex.synchronize { @response_buffers[stream_id] = "".b }
|
|
207
|
+
when :qpack_encoder
|
|
208
|
+
validate_qpack_encoder_data(buf)
|
|
209
|
+
@mutex.synchronize { @response_buffers[stream_id] = "".b }
|
|
210
|
+
when :qpack_decoder
|
|
211
|
+
validate_qpack_decoder_data(buf)
|
|
212
|
+
@mutex.synchronize { @response_buffers[stream_id] = "".b }
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def handle_unidirectional_stream(stream, fin: true)
|
|
217
|
+
stream_id = stream.stream_id
|
|
218
|
+
|
|
219
|
+
# Already known as critical stream — closure via FIN is an error
|
|
220
|
+
if fin && critical_stream?(stream_id)
|
|
221
|
+
raise Protocol::FrameError.new("Closure of critical stream", error_code: Protocol::H3_CLOSED_CRITICAL_STREAM)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
data = stream.data
|
|
225
|
+
return if data.empty?
|
|
226
|
+
|
|
227
|
+
stream_type, type_len = Protocol.decode_varint(data.bytes, 0)
|
|
228
|
+
return if type_len == 0
|
|
229
|
+
payload = data[type_len..-1]
|
|
230
|
+
|
|
231
|
+
case stream_type
|
|
232
|
+
when 0x00
|
|
233
|
+
set_control_stream(stream_id, payload)
|
|
234
|
+
if fin
|
|
235
|
+
raise Protocol::FrameError.new("Closure of critical stream", error_code: Protocol::H3_CLOSED_CRITICAL_STREAM)
|
|
236
|
+
end
|
|
237
|
+
when 0x01
|
|
238
|
+
raise Protocol::FrameError, "Client must not send push streams"
|
|
239
|
+
when 0x02
|
|
240
|
+
raise Protocol::FrameError, "Duplicate QPACK encoder stream" if @qpack_encoder_stream_id
|
|
241
|
+
@qpack_encoder_stream_id = stream_id
|
|
242
|
+
if fin
|
|
243
|
+
raise Protocol::FrameError.new("Closure of critical stream", error_code: Protocol::H3_CLOSED_CRITICAL_STREAM)
|
|
244
|
+
end
|
|
245
|
+
when 0x03
|
|
246
|
+
raise Protocol::FrameError, "Duplicate QPACK decoder stream" if @qpack_decoder_stream_id
|
|
247
|
+
@qpack_decoder_stream_id = stream_id
|
|
248
|
+
if fin
|
|
249
|
+
raise Protocol::FrameError.new("Closure of critical stream", error_code: Protocol::H3_CLOSED_CRITICAL_STREAM)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def set_control_stream(stream_id, payload = nil)
|
|
255
|
+
raise Protocol::FrameError, "Duplicate control stream" if @control_stream_id
|
|
256
|
+
@control_stream_id = stream_id
|
|
257
|
+
parse_control_frames(payload) if payload && !payload.empty?
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def settings
|
|
261
|
+
@settings
|
|
262
|
+
end
|
|
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
|
+
|
|
283
|
+
def critical_stream?(stream_id)
|
|
284
|
+
stream_id == @control_stream_id ||
|
|
285
|
+
stream_id == @qpack_encoder_stream_id ||
|
|
286
|
+
stream_id == @qpack_decoder_stream_id
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# === Shutdown ===
|
|
290
|
+
|
|
291
|
+
def shutdown(error_code = 0)
|
|
292
|
+
send_goaway
|
|
293
|
+
Quicsilver.connection_shutdown(@handle, error_code, false)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
private
|
|
297
|
+
|
|
298
|
+
def open_stream(unidirectional: false)
|
|
299
|
+
handle = Quicsilver.open_stream(@data, unidirectional)
|
|
300
|
+
Stream.new(handle)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def last_client_stream_id
|
|
304
|
+
@streams.keys.select { |id| (id & 0x02) == 0 }.max || 0
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Frame types forbidden on the control stream
|
|
308
|
+
FORBIDDEN_ON_CONTROL = [
|
|
309
|
+
0x00, # DATA — request streams only
|
|
310
|
+
0x01, # HEADERS — request streams only
|
|
311
|
+
0x02, # HTTP/2 PRIORITY (reserved)
|
|
312
|
+
0x05, # PUSH_PROMISE — request streams only
|
|
313
|
+
0x06, # HTTP/2 PING (reserved)
|
|
314
|
+
0x08, # HTTP/2 WINDOW_UPDATE (reserved)
|
|
315
|
+
0x09, # HTTP/2 CONTINUATION (reserved)
|
|
316
|
+
].freeze
|
|
317
|
+
|
|
318
|
+
def on_settings_received(settings)
|
|
319
|
+
@settings.merge!(settings)
|
|
320
|
+
end
|
|
321
|
+
|
|
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
|
|
326
|
+
|
|
327
|
+
if type == Protocol::FRAME_PRIORITY_UPDATE
|
|
328
|
+
parse_priority_update(payload)
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
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)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# RFC 9204 §4.1.3: Validate QPACK encoder stream instructions.
|
|
341
|
+
# We advertise QPACK_MAX_TABLE_CAPACITY = 0, so any Set Dynamic Table Capacity
|
|
342
|
+
# instruction with value > 0 is an error.
|
|
343
|
+
def validate_qpack_encoder_data(data)
|
|
344
|
+
return if data.empty?
|
|
345
|
+
byte = data.bytes[0]
|
|
346
|
+
|
|
347
|
+
# Set Dynamic Table Capacity (001xxxxx)
|
|
348
|
+
if (byte & 0xE0) == 0x20
|
|
349
|
+
capacity, _ = Protocol.decode_varint(data.bytes, 0)
|
|
350
|
+
capacity &= 0x1F # mask off the instruction prefix
|
|
351
|
+
# We advertised capacity 0, any non-zero is an error
|
|
352
|
+
raise Protocol::FrameError.new(
|
|
353
|
+
"Dynamic table capacity exceeds advertised maximum",
|
|
354
|
+
error_code: Protocol::QPACK_ENCODER_STREAM_ERROR
|
|
355
|
+
)
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# RFC 9204 §4.4.3: Validate QPACK decoder stream instructions.
|
|
360
|
+
# Insert Count Increment of 0 is a decoder stream error.
|
|
361
|
+
def validate_qpack_decoder_data(data)
|
|
362
|
+
return if data.empty?
|
|
363
|
+
byte = data.bytes[0]
|
|
364
|
+
|
|
365
|
+
# Insert Count Increment (00xxxxxx)
|
|
366
|
+
if (byte & 0xC0) == 0x00
|
|
367
|
+
increment, _ = Protocol.decode_varint(data.bytes, 0)
|
|
368
|
+
increment &= 0x3F # mask off prefix bits
|
|
369
|
+
if increment == 0
|
|
370
|
+
raise Protocol::FrameError.new(
|
|
371
|
+
"Insert Count Increment of 0 on decoder stream",
|
|
372
|
+
error_code: Protocol::QPACK_DECODER_STREAM_ERROR
|
|
373
|
+
)
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
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