quicsilver 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +3 -4
- data/CHANGELOG.md +49 -0
- data/Gemfile.lock +8 -4
- data/README.md +7 -6
- data/Rakefile +29 -2
- data/benchmarks/components.rb +191 -0
- data/benchmarks/concurrent.rb +110 -0
- data/benchmarks/helpers.rb +88 -0
- data/benchmarks/quicsilver_server.rb +1 -1
- data/benchmarks/rails.rb +170 -0
- data/benchmarks/throughput.rb +113 -0
- data/ext/quicsilver/quicsilver.c +529 -181
- data/lib/quicsilver/client/client.rb +250 -0
- data/lib/quicsilver/client/request.rb +98 -0
- data/lib/quicsilver/{http3.rb → protocol/frames.rb} +133 -28
- data/lib/quicsilver/protocol/qpack/decoder.rb +165 -0
- data/lib/quicsilver/protocol/qpack/encoder.rb +189 -0
- data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +125 -0
- data/lib/quicsilver/protocol/qpack/huffman.rb +459 -0
- data/lib/quicsilver/protocol/request_encoder.rb +47 -0
- data/lib/quicsilver/protocol/request_parser.rb +387 -0
- data/lib/quicsilver/protocol/response_encoder.rb +72 -0
- data/lib/quicsilver/protocol/response_parser.rb +249 -0
- data/lib/quicsilver/server/listener_data.rb +14 -0
- data/lib/quicsilver/server/request_handler.rb +86 -0
- data/lib/quicsilver/server/request_registry.rb +50 -0
- data/lib/quicsilver/server/server.rb +336 -0
- data/lib/quicsilver/transport/configuration.rb +132 -0
- data/lib/quicsilver/transport/connection.rb +350 -0
- data/lib/quicsilver/transport/event_loop.rb +38 -0
- data/lib/quicsilver/transport/inbound_stream.rb +33 -0
- data/lib/quicsilver/transport/stream.rb +28 -0
- data/lib/quicsilver/transport/stream_event.rb +26 -0
- data/lib/quicsilver/version.rb +1 -1
- data/lib/quicsilver.rb +31 -13
- data/lib/rackup/handler/quicsilver.rb +1 -2
- data/quicsilver.gemspec +3 -1
- metadata +58 -18
- data/benchmarks/benchmark.rb +0 -68
- 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,250 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "timeout"
|
|
4
|
+
|
|
5
|
+
module Quicsilver
|
|
6
|
+
class Client
|
|
7
|
+
attr_reader :hostname, :port, :unsecure, :connection_timeout, :request_timeout
|
|
8
|
+
|
|
9
|
+
AlreadyConnectedError = Class.new(StandardError)
|
|
10
|
+
NotConnectedError = Class.new(StandardError)
|
|
11
|
+
StreamFailedToOpenError = Class.new(StandardError)
|
|
12
|
+
|
|
13
|
+
FINISHED_EVENTS = %w[RECEIVE_FIN RECEIVE STREAM_RESET STOP_SENDING].freeze
|
|
14
|
+
|
|
15
|
+
DEFAULT_REQUEST_TIMEOUT = 30 # seconds
|
|
16
|
+
DEFAULT_CONNECTION_TIMEOUT = 5000 # ms
|
|
17
|
+
|
|
18
|
+
def initialize(hostname, port = 4433, options = {})
|
|
19
|
+
@hostname = hostname
|
|
20
|
+
@port = port
|
|
21
|
+
@unsecure = options.fetch(:unsecure, true)
|
|
22
|
+
@connection_timeout = options.fetch(:connection_timeout, DEFAULT_CONNECTION_TIMEOUT)
|
|
23
|
+
@request_timeout = options.fetch(:request_timeout, DEFAULT_REQUEST_TIMEOUT)
|
|
24
|
+
@max_body_size = options[:max_body_size]
|
|
25
|
+
@max_header_size = options[:max_header_size]
|
|
26
|
+
|
|
27
|
+
@connection_data = nil
|
|
28
|
+
@connected = false
|
|
29
|
+
@connection_start_time = nil
|
|
30
|
+
|
|
31
|
+
@response_buffers = {}
|
|
32
|
+
@pending_requests = {} # handle => Request
|
|
33
|
+
@mutex = Mutex.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def connect
|
|
37
|
+
raise AlreadyConnectedError if @connected
|
|
38
|
+
|
|
39
|
+
Quicsilver.open_connection
|
|
40
|
+
config = Quicsilver.create_configuration(@unsecure)
|
|
41
|
+
raise ConnectionError, "Failed to create configuration" if config.nil?
|
|
42
|
+
|
|
43
|
+
start_connection(config)
|
|
44
|
+
@connected = true
|
|
45
|
+
@connection_start_time = Time.now
|
|
46
|
+
send_control_stream
|
|
47
|
+
Quicsilver.event_loop.start
|
|
48
|
+
|
|
49
|
+
self
|
|
50
|
+
rescue => e
|
|
51
|
+
cleanup_failed_connection
|
|
52
|
+
raise e.is_a?(ConnectionError) || e.is_a?(TimeoutError) ? e : ConnectionError.new("Connection failed: #{e.message}")
|
|
53
|
+
ensure
|
|
54
|
+
Quicsilver.close_configuration(config)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def disconnect
|
|
58
|
+
return unless @connection_data
|
|
59
|
+
|
|
60
|
+
@connected = false
|
|
61
|
+
|
|
62
|
+
# Fail pending requests
|
|
63
|
+
@mutex.synchronize do
|
|
64
|
+
@pending_requests.each_value { |req| req.fail(0, "Connection closed") }
|
|
65
|
+
@pending_requests.clear
|
|
66
|
+
@response_buffers.clear
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
Quicsilver.close_connection_handle(@connection_data) if @connection_data
|
|
70
|
+
@connection_data = nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# HTTP methods - block gives you request control, no block returns response directly
|
|
74
|
+
#
|
|
75
|
+
# # Simple (blocking)
|
|
76
|
+
# response = client.get("/users")
|
|
77
|
+
#
|
|
78
|
+
# # With control
|
|
79
|
+
# client.get("/users") do |req|
|
|
80
|
+
# req.cancel if should_abort?
|
|
81
|
+
# req.response
|
|
82
|
+
# end
|
|
83
|
+
#
|
|
84
|
+
%i[get post patch delete head put].each do |method|
|
|
85
|
+
define_method(method) do |path, headers: {}, body: nil, &block|
|
|
86
|
+
req = build_request(method.to_s.upcase, path, headers: headers, body: body)
|
|
87
|
+
block ? block.call(req) : req.response
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Build and send request, returns Request object for lifecycle control
|
|
92
|
+
def build_request(method, path, headers: {}, body: nil)
|
|
93
|
+
raise NotConnectedError unless @connected
|
|
94
|
+
|
|
95
|
+
stream = open_stream
|
|
96
|
+
raise StreamFailedToOpenError unless stream
|
|
97
|
+
|
|
98
|
+
request = Request.new(self, stream)
|
|
99
|
+
@mutex.synchronize { @pending_requests[stream.handle] = request }
|
|
100
|
+
|
|
101
|
+
send_to_stream(stream, method, path, headers, body)
|
|
102
|
+
|
|
103
|
+
request
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def connected?
|
|
107
|
+
@connected && @connection_data && connection_alive?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def connection_info
|
|
111
|
+
info = @connection_data ? Quicsilver.connection_status(@connection_data[1]) : {}
|
|
112
|
+
info.merge(hostname: @hostname, port: @port, uptime: connection_uptime)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def connection_uptime
|
|
116
|
+
return 0 unless @connection_start_time
|
|
117
|
+
Time.now - @connection_start_time
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def authority
|
|
121
|
+
"#{@hostname}:#{@port}"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Called directly by C extension via dispatch_to_ruby
|
|
125
|
+
def handle_stream_event(stream_id, event, data, _early_data) # :nodoc:
|
|
126
|
+
return unless FINISHED_EVENTS.include?(event)
|
|
127
|
+
|
|
128
|
+
@mutex.synchronize do
|
|
129
|
+
case event
|
|
130
|
+
when "RECEIVE"
|
|
131
|
+
(@response_buffers[stream_id] ||= StringIO.new("".b)).write(data)
|
|
132
|
+
|
|
133
|
+
when "RECEIVE_FIN"
|
|
134
|
+
event = Transport::StreamEvent.new(data, "RECEIVE_FIN")
|
|
135
|
+
|
|
136
|
+
buffer = @response_buffers.delete(stream_id)
|
|
137
|
+
full_data = (buffer&.string || "".b) + event.data
|
|
138
|
+
|
|
139
|
+
response_parser = Protocol::ResponseParser.new(full_data, max_body_size: @max_body_size,
|
|
140
|
+
max_header_size: @max_header_size)
|
|
141
|
+
response_parser.parse
|
|
142
|
+
|
|
143
|
+
response = {
|
|
144
|
+
status: response_parser.status,
|
|
145
|
+
headers: response_parser.headers,
|
|
146
|
+
body: response_parser.body.read
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
request = @pending_requests.delete(event.handle)
|
|
150
|
+
request&.complete(response)
|
|
151
|
+
|
|
152
|
+
when "STREAM_RESET"
|
|
153
|
+
event = Transport::StreamEvent.new(data, "STREAM_RESET")
|
|
154
|
+
request = @pending_requests.delete(event.handle)
|
|
155
|
+
request&.fail(event.error_code, "Stream reset by peer")
|
|
156
|
+
|
|
157
|
+
when "STOP_SENDING"
|
|
158
|
+
event = Transport::StreamEvent.new(data, "STOP_SENDING")
|
|
159
|
+
request = @pending_requests.delete(event.handle)
|
|
160
|
+
request&.fail(event.error_code, "Peer sent STOP_SENDING")
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
rescue => e
|
|
164
|
+
Quicsilver.logger.error("Error handling client stream: #{e.class} - #{e.message}")
|
|
165
|
+
Quicsilver.logger.debug(e.backtrace.first(5).join("\n"))
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
private
|
|
169
|
+
|
|
170
|
+
def start_connection(config)
|
|
171
|
+
connection_handle, context_handle = create_connection
|
|
172
|
+
unless Quicsilver.start_connection(connection_handle, config, @hostname, @port)
|
|
173
|
+
cleanup_failed_connection
|
|
174
|
+
raise ConnectionError, "Failed to start connection"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
result = Quicsilver.wait_for_connection(context_handle, @connection_timeout)
|
|
178
|
+
handle_connection_result(result)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def create_connection
|
|
182
|
+
@connection_data = Quicsilver.create_connection(self)
|
|
183
|
+
raise ConnectionError, "Failed to create connection" if @connection_data.nil?
|
|
184
|
+
|
|
185
|
+
@connection_data
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def cleanup_failed_connection
|
|
189
|
+
Quicsilver.close_connection_handle(@connection_data) if @connection_data
|
|
190
|
+
@connection_data = nil
|
|
191
|
+
@connected = false
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def open_stream
|
|
195
|
+
handle = Quicsilver.open_stream(@connection_data, false)
|
|
196
|
+
Transport::Stream.new(handle)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def open_unidirectional_stream
|
|
200
|
+
handle = Quicsilver.open_stream(@connection_data, true)
|
|
201
|
+
Transport::Stream.new(handle)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def send_control_stream
|
|
205
|
+
@control_stream = open_unidirectional_stream
|
|
206
|
+
@control_stream.send(Protocol.build_control_stream)
|
|
207
|
+
|
|
208
|
+
# RFC 9204: QPACK encoder (0x02) and decoder (0x03) streams
|
|
209
|
+
[0x02, 0x03].each do |type|
|
|
210
|
+
stream = open_unidirectional_stream
|
|
211
|
+
stream.send([type].pack("C"))
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def handle_connection_result(result)
|
|
216
|
+
if result.key?("error")
|
|
217
|
+
cleanup_failed_connection
|
|
218
|
+
raise ConnectionError, "Connection failed: status 0x#{result['status'].to_s(16)}, code: #{result['code']}"
|
|
219
|
+
elsif result.key?("timeout")
|
|
220
|
+
cleanup_failed_connection
|
|
221
|
+
raise TimeoutError, "Connection timed out after #{@connection_timeout}ms"
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def connection_alive?
|
|
226
|
+
return false unless (info = Quicsilver.connection_status(@connection_data[1]))
|
|
227
|
+
info["connected"] && !info["failed"]
|
|
228
|
+
rescue
|
|
229
|
+
false
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def send_to_stream(stream, method, path, headers, body)
|
|
233
|
+
encoded_response = Protocol::RequestEncoder.new(
|
|
234
|
+
method: method,
|
|
235
|
+
path: path,
|
|
236
|
+
scheme: "https",
|
|
237
|
+
authority: authority,
|
|
238
|
+
headers: headers,
|
|
239
|
+
body: body
|
|
240
|
+
).encode
|
|
241
|
+
|
|
242
|
+
result = stream.send(encoded_response, fin: true)
|
|
243
|
+
|
|
244
|
+
unless result
|
|
245
|
+
@mutex.synchronize { @pending_requests.delete(stream.handle) }
|
|
246
|
+
raise Error, "Failed to send request"
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quicsilver
|
|
4
|
+
class Client
|
|
5
|
+
class Request
|
|
6
|
+
attr_reader :stream, :status
|
|
7
|
+
|
|
8
|
+
CancelledError = Class.new(StandardError)
|
|
9
|
+
|
|
10
|
+
class ResetError < StandardError
|
|
11
|
+
attr_reader :error_code
|
|
12
|
+
|
|
13
|
+
def initialize(message, error_code = nil)
|
|
14
|
+
super(message)
|
|
15
|
+
@error_code = error_code
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(client, stream)
|
|
20
|
+
@client = client
|
|
21
|
+
@stream = stream
|
|
22
|
+
@status = :pending
|
|
23
|
+
@queue = Queue.new
|
|
24
|
+
@mutex = Mutex.new
|
|
25
|
+
@response = nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Block until response arrives or timeout
|
|
29
|
+
# Returns response hash { status:, headers:, body: }
|
|
30
|
+
# Raises TimeoutError, CancelledError, or ResetError
|
|
31
|
+
def response(timeout: nil)
|
|
32
|
+
timeout ||= @client.request_timeout
|
|
33
|
+
return @response if @status == :completed
|
|
34
|
+
|
|
35
|
+
result = @queue.pop(timeout: timeout)
|
|
36
|
+
|
|
37
|
+
@mutex.synchronize do
|
|
38
|
+
case result
|
|
39
|
+
when nil
|
|
40
|
+
raise Quicsilver::TimeoutError, "Request timeout after #{timeout}s"
|
|
41
|
+
when Hash
|
|
42
|
+
if result[:error]
|
|
43
|
+
@status = :error
|
|
44
|
+
raise ResetError.new(result[:message] || "Stream reset by peer", result[:error_code])
|
|
45
|
+
else
|
|
46
|
+
@status = :completed
|
|
47
|
+
@response = result
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
@response
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Cancel the request (sends RESET_STREAM + STOP_SENDING to server)
|
|
56
|
+
# error_code defaults to H3_REQUEST_CANCELLED (0x10c)
|
|
57
|
+
def cancel(error_code: Protocol::H3_REQUEST_CANCELLED)
|
|
58
|
+
@mutex.synchronize do
|
|
59
|
+
return false unless @status == :pending
|
|
60
|
+
|
|
61
|
+
@stream.reset(error_code)
|
|
62
|
+
@stream.stop_sending(error_code)
|
|
63
|
+
@status = :cancelled
|
|
64
|
+
end
|
|
65
|
+
true
|
|
66
|
+
rescue => e
|
|
67
|
+
Quicsilver.logger.error("Failed to cancel request: #{e.message}")
|
|
68
|
+
false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def pending?
|
|
72
|
+
@status == :pending
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def completed?
|
|
76
|
+
@status == :completed
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def cancelled?
|
|
80
|
+
@status == :cancelled
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Called by Client when response arrives
|
|
84
|
+
def complete(response) # :nodoc:
|
|
85
|
+
@queue.push(response)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Called by Client on stream reset from peer or connection close
|
|
89
|
+
def fail(error_code, message = nil) # :nodoc:
|
|
90
|
+
@mutex.synchronize do
|
|
91
|
+
return if @status == :cancelled
|
|
92
|
+
@status = :error
|
|
93
|
+
end
|
|
94
|
+
@queue.push({ error: true, error_code: error_code, message: message })
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Quicsilver
|
|
4
|
-
module
|
|
4
|
+
module Protocol
|
|
5
5
|
# HTTP/3 Frame Types (RFC 9114)
|
|
6
6
|
FRAME_DATA = 0x00
|
|
7
7
|
FRAME_HEADERS = 0x01
|
|
@@ -11,6 +11,52 @@ module Quicsilver
|
|
|
11
11
|
FRAME_GOAWAY = 0x07
|
|
12
12
|
FRAME_MAX_PUSH_ID = 0x0d
|
|
13
13
|
|
|
14
|
+
# Frame types forbidden on request streams (RFC 9114 Section 7.2.4, 7.2.6, 7.2.7)
|
|
15
|
+
CONTROL_ONLY_FRAMES = [FRAME_CANCEL_PUSH, FRAME_SETTINGS, FRAME_GOAWAY, FRAME_MAX_PUSH_ID].freeze
|
|
16
|
+
|
|
17
|
+
# HTTP/3 Error Codes (RFC 9114 Section 8.1)
|
|
18
|
+
H3_NO_ERROR = 0x100
|
|
19
|
+
H3_GENERAL_PROTOCOL_ERROR = 0x101
|
|
20
|
+
H3_INTERNAL_ERROR = 0x102
|
|
21
|
+
H3_STREAM_CREATION_ERROR = 0x103
|
|
22
|
+
H3_CLOSED_CRITICAL_STREAM = 0x104
|
|
23
|
+
H3_FRAME_UNEXPECTED = 0x105
|
|
24
|
+
H3_FRAME_ERROR = 0x106
|
|
25
|
+
H3_EXCESSIVE_LOAD = 0x107
|
|
26
|
+
H3_ID_ERROR = 0x108
|
|
27
|
+
H3_SETTINGS_ERROR = 0x109
|
|
28
|
+
H3_MISSING_SETTINGS = 0x10a
|
|
29
|
+
H3_REQUEST_REJECTED = 0x10b
|
|
30
|
+
H3_REQUEST_CANCELLED = 0x10c
|
|
31
|
+
H3_REQUEST_INCOMPLETE = 0x10d
|
|
32
|
+
H3_MESSAGE_ERROR = 0x10e
|
|
33
|
+
H3_CONNECT_ERROR = 0x10f
|
|
34
|
+
H3_VERSION_FALLBACK = 0x110
|
|
35
|
+
|
|
36
|
+
# QPACK Error Codes (RFC 9204 Section 6)
|
|
37
|
+
QPACK_DECOMPRESSION_FAILED = 0x200
|
|
38
|
+
QPACK_ENCODER_STREAM_ERROR = 0x201
|
|
39
|
+
QPACK_DECODER_STREAM_ERROR = 0x202
|
|
40
|
+
|
|
41
|
+
# Protocol errors that carry an HTTP/3 error code for CONNECTION_CLOSE / RESET_STREAM.
|
|
42
|
+
# FrameError → connection error (CONNECTION_CLOSE)
|
|
43
|
+
# MessageError → stream error (RESET_STREAM) on request streams
|
|
44
|
+
class FrameError < StandardError
|
|
45
|
+
attr_reader :error_code
|
|
46
|
+
def initialize(msg = nil, error_code: H3_FRAME_UNEXPECTED)
|
|
47
|
+
@error_code = error_code
|
|
48
|
+
super(msg)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class MessageError < StandardError
|
|
53
|
+
attr_reader :error_code
|
|
54
|
+
def initialize(msg = nil, error_code: H3_MESSAGE_ERROR)
|
|
55
|
+
@error_code = error_code
|
|
56
|
+
super(msg)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
14
60
|
# QPACK Static Table Indices (RFC 9204 Appendix A)
|
|
15
61
|
STATIC_TABLE = [
|
|
16
62
|
[':authority', ''], # 0
|
|
@@ -70,8 +116,8 @@ module Quicsilver
|
|
|
70
116
|
['content-type', 'text/plain;charset=utf-8'], # 54
|
|
71
117
|
['range', 'bytes=0-'], # 55
|
|
72
118
|
['strict-transport-security', 'max-age=31536000'], # 56
|
|
73
|
-
['strict-transport-security', 'max-age=31536000;
|
|
74
|
-
['strict-transport-security', 'max-age=31536000;
|
|
119
|
+
['strict-transport-security', 'max-age=31536000; includeSubDomains'], # 57
|
|
120
|
+
['strict-transport-security', 'max-age=31536000; includeSubDomains; preload'], # 58
|
|
75
121
|
['vary', 'accept-encoding'], # 59
|
|
76
122
|
['vary', 'origin'], # 60
|
|
77
123
|
['x-content-type-options', 'nosniff'], # 61
|
|
@@ -134,22 +180,40 @@ module Quicsilver
|
|
|
134
180
|
QPACK_CONTENT_TYPE_JSON = 46
|
|
135
181
|
QPACK_CONTENT_TYPE_PLAIN = 53
|
|
136
182
|
|
|
183
|
+
# Maximum stream ID for initial GOAWAY (2^62 - 4, per RFC 9114)
|
|
184
|
+
MAX_STREAM_ID = (2**62) - 4
|
|
185
|
+
|
|
137
186
|
class << self
|
|
187
|
+
# Precomputed varint encodings for single-byte values (0-63)
|
|
188
|
+
VARINT_SMALL = Array.new(64) { |v| [v].pack('C').freeze }.freeze
|
|
189
|
+
VARINT_MED = Array.new(16384 - 64) { |i| v = i + 64; [0x40 | (v >> 8), v & 0xFF].pack('C*').freeze }.freeze
|
|
190
|
+
|
|
191
|
+
# Cache for large varint encodings (values >= 16384)
|
|
192
|
+
VARINT_LARGE_CACHE = {}
|
|
193
|
+
VARINT_LARGE_CACHE_MAX = 256
|
|
194
|
+
|
|
138
195
|
# Encode variable-length integer
|
|
139
196
|
def encode_varint(value)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
[0x40 | (value >> 8), value & 0xFF].pack('C*')
|
|
145
|
-
when 16384..1073741823
|
|
146
|
-
[0x80 | (value >> 24), (value >> 16) & 0xFF,
|
|
147
|
-
(value >> 8) & 0xFF, value & 0xFF].pack('C*')
|
|
197
|
+
if value < 64
|
|
198
|
+
VARINT_SMALL[value]
|
|
199
|
+
elsif value < 16384
|
|
200
|
+
VARINT_MED[value - 64]
|
|
148
201
|
else
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
202
|
+
cached = VARINT_LARGE_CACHE[value]
|
|
203
|
+
return cached if cached
|
|
204
|
+
|
|
205
|
+
result = if value < 1073741824
|
|
206
|
+
[0x80 | (value >> 24), (value >> 16) & 0xFF,
|
|
207
|
+
(value >> 8) & 0xFF, value & 0xFF].pack('C*').freeze
|
|
208
|
+
else
|
|
209
|
+
[0xC0 | (value >> 56), (value >> 48) & 0xFF,
|
|
210
|
+
(value >> 40) & 0xFF, (value >> 32) & 0xFF,
|
|
211
|
+
(value >> 24) & 0xFF, (value >> 16) & 0xFF,
|
|
212
|
+
(value >> 8) & 0xFF, value & 0xFF].pack('C*').freeze
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
VARINT_LARGE_CACHE[value] = result if VARINT_LARGE_CACHE.size < VARINT_LARGE_CACHE_MAX
|
|
216
|
+
result
|
|
153
217
|
end
|
|
154
218
|
end
|
|
155
219
|
|
|
@@ -169,7 +233,10 @@ module Quicsilver
|
|
|
169
233
|
# Build control stream data
|
|
170
234
|
def build_control_stream
|
|
171
235
|
stream_type = [0x00].pack('C') # Control stream type
|
|
172
|
-
settings = build_settings_frame({
|
|
236
|
+
settings = build_settings_frame({
|
|
237
|
+
0x01 => 0, # QPACK_MAX_TABLE_CAPACITY = 0 (no dynamic table)
|
|
238
|
+
0x07 => 0 # QPACK_BLOCKED_STREAMS = 0
|
|
239
|
+
})
|
|
173
240
|
|
|
174
241
|
stream_type + settings
|
|
175
242
|
end
|
|
@@ -184,32 +251,71 @@ module Quicsilver
|
|
|
184
251
|
frame_type + frame_length + payload
|
|
185
252
|
end
|
|
186
253
|
|
|
187
|
-
#
|
|
188
|
-
|
|
254
|
+
# Cache for decode_varint_str: (object_id << 16 | offset) → [value, consumed]
|
|
255
|
+
VARINT_STR_CACHE = {}
|
|
256
|
+
VARINT_STR_CACHE_MAX = 256
|
|
257
|
+
|
|
258
|
+
# Decode variable-length integer from a String using getbyte (no array needed)
|
|
259
|
+
# Returns [value, bytes_consumed]
|
|
260
|
+
def decode_varint_str(str, offset = 0)
|
|
261
|
+
cache_key = (str.object_id << 16) | offset
|
|
262
|
+
cached = VARINT_STR_CACHE[cache_key]
|
|
263
|
+
return cached if cached
|
|
264
|
+
|
|
265
|
+
first = str.getbyte(offset)
|
|
266
|
+
return [0, 0] unless first
|
|
267
|
+
|
|
268
|
+
if first < 0x40
|
|
269
|
+
result = VARINT_DECODE_SMALL[first]
|
|
270
|
+
else
|
|
271
|
+
prefix = (first & 0xC0) >> 6
|
|
272
|
+
length = 1 << prefix
|
|
273
|
+
|
|
274
|
+
return [0, 0] if offset + length > str.bytesize
|
|
275
|
+
|
|
276
|
+
result = case prefix
|
|
277
|
+
when 0
|
|
278
|
+
[first & 0x3F, 1]
|
|
279
|
+
when 1
|
|
280
|
+
[(first & 0x3F) << 8 | str.getbyte(offset + 1), 2]
|
|
281
|
+
when 2
|
|
282
|
+
[(first & 0x3F) << 24 | str.getbyte(offset + 1) << 16 |
|
|
283
|
+
str.getbyte(offset + 2) << 8 | str.getbyte(offset + 3), 4]
|
|
284
|
+
else
|
|
285
|
+
[(first & 0x3F) << 56 | str.getbyte(offset + 1) << 48 |
|
|
286
|
+
str.getbyte(offset + 2) << 40 | str.getbyte(offset + 3) << 32 |
|
|
287
|
+
str.getbyte(offset + 4) << 24 | str.getbyte(offset + 5) << 16 |
|
|
288
|
+
str.getbyte(offset + 6) << 8 | str.getbyte(offset + 7), 8]
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
VARINT_STR_CACHE[cache_key] = result if VARINT_STR_CACHE.size < VARINT_STR_CACHE_MAX
|
|
293
|
+
result
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Precomputed decode results for single-byte varints (0-63)
|
|
297
|
+
VARINT_DECODE_SMALL = Array.new(64) { |v| [v, 1].freeze }.freeze
|
|
189
298
|
|
|
190
299
|
# Decode variable-length integer (RFC 9000)
|
|
191
300
|
# Returns [value, bytes_consumed]
|
|
192
301
|
def decode_varint(bytes, offset = 0)
|
|
193
|
-
return [0, 0] if offset >= bytes.size
|
|
194
|
-
|
|
195
302
|
first = bytes[offset]
|
|
196
|
-
return [0, 0]
|
|
303
|
+
return [0, 0] unless first
|
|
197
304
|
|
|
198
|
-
|
|
199
|
-
|
|
305
|
+
# Fast path for single-byte varints (most common: frame types, small lengths)
|
|
306
|
+
return VARINT_DECODE_SMALL[first] if first < 0x40
|
|
200
307
|
|
|
201
|
-
|
|
308
|
+
prefix = (first & 0xC0) >> 6
|
|
309
|
+
length = 1 << prefix
|
|
202
310
|
return [0, 0] if offset + length > bytes.size
|
|
203
311
|
|
|
204
312
|
case prefix
|
|
205
|
-
when 0
|
|
206
|
-
[first & 0x3F, 1]
|
|
207
313
|
when 1
|
|
208
314
|
[(first & 0x3F) << 8 | bytes[offset + 1], 2]
|
|
209
315
|
when 2
|
|
210
316
|
[(first & 0x3F) << 24 | bytes[offset + 1] << 16 |
|
|
211
317
|
bytes[offset + 2] << 8 | bytes[offset + 3], 4]
|
|
212
|
-
else #
|
|
318
|
+
else # 3
|
|
213
319
|
[(first & 0x3F) << 56 | bytes[offset + 1] << 48 |
|
|
214
320
|
bytes[offset + 2] << 40 | bytes[offset + 3] << 32 |
|
|
215
321
|
bytes[offset + 4] << 24 | bytes[offset + 5] << 16 |
|
|
@@ -219,4 +325,3 @@ module Quicsilver
|
|
|
219
325
|
end
|
|
220
326
|
end
|
|
221
327
|
end
|
|
222
|
-
|