quicsilver 0.1.0 → 0.2.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 +42 -0
- data/.gitignore +3 -1
- data/CHANGELOG.md +27 -5
- data/Gemfile.lock +10 -0
- data/LICENSE +21 -0
- data/README.md +33 -54
- data/benchmarks/benchmark.rb +68 -0
- data/benchmarks/quicsilver_server.rb +46 -0
- data/examples/minimal_http3_server.rb +0 -6
- data/examples/rack_http3_server.rb +0 -6
- data/examples/simple_client_test.rb +26 -0
- data/ext/quicsilver/quicsilver.c +165 -36
- data/lib/quicsilver/client.rb +171 -101
- data/lib/quicsilver/connection.rb +42 -0
- data/lib/quicsilver/event_loop.rb +38 -0
- data/lib/quicsilver/http3/request_encoder.rb +41 -20
- data/lib/quicsilver/http3/request_parser.rb +42 -24
- data/lib/quicsilver/http3/response_encoder.rb +138 -25
- data/lib/quicsilver/http3/response_parser.rb +160 -0
- data/lib/quicsilver/http3.rb +205 -51
- data/lib/quicsilver/quic_stream.rb +36 -0
- data/lib/quicsilver/request_registry.rb +48 -0
- data/lib/quicsilver/server.rb +257 -160
- data/lib/quicsilver/server_configuration.rb +36 -7
- data/lib/quicsilver/version.rb +1 -1
- data/lib/quicsilver.rb +22 -0
- data/lib/rackup/handler/quicsilver.rb +78 -0
- data/quicsilver.gemspec +7 -2
- metadata +72 -7
- data/examples/minimal_http3_client.rb +0 -89
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quicsilver
|
|
4
|
+
class RequestRegistry
|
|
5
|
+
def initialize
|
|
6
|
+
@requests = {}
|
|
7
|
+
@mutex = Mutex.new
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def track(stream_id, connection_handle, path:, method:, started_at: Time.now)
|
|
11
|
+
@mutex.synchronize do
|
|
12
|
+
@requests[stream_id] = {
|
|
13
|
+
connection_handle: connection_handle,
|
|
14
|
+
path: path,
|
|
15
|
+
method: method,
|
|
16
|
+
started_at: started_at
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def complete(stream_id)
|
|
22
|
+
@mutex.synchronize { @requests.delete(stream_id) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def active_count
|
|
26
|
+
@mutex.synchronize { @requests.size }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def active_requests
|
|
30
|
+
@mutex.synchronize { @requests.dup }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def requests_older_than(seconds)
|
|
34
|
+
cutoff = Time.now - seconds
|
|
35
|
+
@mutex.synchronize do
|
|
36
|
+
@requests.select { |_, r| r[:started_at] < cutoff }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def empty?
|
|
41
|
+
@mutex.synchronize { @requests.empty? }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def include?(stream_id)
|
|
45
|
+
@mutex.synchronize { @requests.key?(stream_id) }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/quicsilver/server.rb
CHANGED
|
@@ -2,109 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
module Quicsilver
|
|
4
4
|
class Server
|
|
5
|
-
attr_reader :address, :port, :server_configuration, :running
|
|
5
|
+
attr_reader :address, :port, :server_configuration, :running, :connections, :request_registry, :shutting_down
|
|
6
6
|
|
|
7
7
|
STREAM_EVENT_RECEIVE = "RECEIVE"
|
|
8
8
|
STREAM_EVENT_RECEIVE_FIN = "RECEIVE_FIN"
|
|
9
9
|
STREAM_EVENT_CONNECTION_ESTABLISHED = "CONNECTION_ESTABLISHED"
|
|
10
10
|
STREAM_EVENT_SEND_COMPLETE = "SEND_COMPLETE"
|
|
11
|
+
STREAM_EVENT_CONNECTION_CLOSED = "CONNECTION_CLOSED"
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
def stream_buffers
|
|
14
|
-
@stream_buffers ||= {}
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def stream_handles
|
|
18
|
-
@stream_handles ||= {}
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def rack_app
|
|
22
|
-
@rack_app
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def rack_app=(app)
|
|
26
|
-
@rack_app = app
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def handle_stream(stream_id, event, data)
|
|
30
|
-
case event
|
|
31
|
-
when STREAM_EVENT_CONNECTION_ESTABLISHED
|
|
32
|
-
puts "🔧 Ruby: Connection established with client"
|
|
33
|
-
connection_handle = data.unpack1('Q') # Unpack 64-bit pointer
|
|
34
|
-
stream = Quicsilver.open_stream(connection_handle, true) # unidirectional
|
|
35
|
-
control_data = Quicsilver::HTTP3.build_control_stream
|
|
36
|
-
Quicsilver.send_stream(stream, control_data, false) # no FIN
|
|
37
|
-
when STREAM_EVENT_SEND_COMPLETE
|
|
38
|
-
puts "🔧 Ruby: Control stream sent to client"
|
|
39
|
-
when STREAM_EVENT_RECEIVE
|
|
40
|
-
# Accumulate data
|
|
41
|
-
stream_buffers[stream_id] ||= ""
|
|
42
|
-
stream_buffers[stream_id] += data
|
|
43
|
-
puts "🔧 Ruby: Stream #{stream_id}: Buffering #{data.bytesize} bytes (total: #{stream_buffers[stream_id].bytesize})"
|
|
44
|
-
when STREAM_EVENT_RECEIVE_FIN
|
|
45
|
-
# Extract stream handle from data (first 8 bytes)
|
|
46
|
-
stream_handle = data[0, 8].unpack1('Q')
|
|
47
|
-
actual_data = data[8..-1] || ""
|
|
48
|
-
|
|
49
|
-
# Store stream handle for later use
|
|
50
|
-
stream_handles[stream_id] = stream_handle
|
|
51
|
-
|
|
52
|
-
# Final chunk - process complete message
|
|
53
|
-
stream_buffers[stream_id] ||= ""
|
|
54
|
-
stream_buffers[stream_id] += actual_data
|
|
55
|
-
complete_data = stream_buffers[stream_id]
|
|
56
|
-
|
|
57
|
-
# Handle bidirectional streams (client requests)
|
|
58
|
-
if bidirectional?(stream_id)
|
|
59
|
-
handle_http3_request(stream_id, complete_data)
|
|
60
|
-
else
|
|
61
|
-
# Unidirectional stream (control/QPACK)
|
|
62
|
-
puts "✅ Ruby: Stream #{stream_id}: Control/QPACK stream (#{complete_data.bytesize} bytes)"
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Clean up buffers
|
|
66
|
-
stream_buffers.delete(stream_id)
|
|
67
|
-
stream_handles.delete(stream_id)
|
|
68
|
-
end
|
|
69
|
-
end
|
|
13
|
+
ServerStopError = Class.new(StandardError)
|
|
70
14
|
|
|
71
|
-
|
|
15
|
+
class << self
|
|
16
|
+
attr_accessor :instance
|
|
72
17
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
(stream_id
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def handle_http3_request(stream_id, data)
|
|
79
|
-
parser = HTTP3::RequestParser.new(data)
|
|
80
|
-
parser.parse
|
|
81
|
-
env = parser.to_rack_env
|
|
82
|
-
|
|
83
|
-
if env && rack_app
|
|
84
|
-
puts "✅ Ruby: #{env['REQUEST_METHOD']} #{env['PATH_INFO']}"
|
|
85
|
-
|
|
86
|
-
# Call Rack app
|
|
87
|
-
status, headers, body = rack_app.call(env)
|
|
88
|
-
|
|
89
|
-
# Encode response
|
|
90
|
-
encoder = HTTP3::ResponseEncoder.new(status, headers, body)
|
|
91
|
-
response_data = encoder.encode
|
|
92
|
-
|
|
93
|
-
# Get stream handle from stored handles
|
|
94
|
-
stream_handle = stream_handles[stream_id]
|
|
95
|
-
if stream_handle
|
|
96
|
-
# Send response
|
|
97
|
-
Quicsilver.send_stream(stream_handle, response_data, true)
|
|
98
|
-
puts "✅ Ruby: Response sent: #{status}"
|
|
99
|
-
else
|
|
100
|
-
puts "❌ Ruby: Stream handle not found for stream #{stream_id}"
|
|
101
|
-
end
|
|
102
|
-
else
|
|
103
|
-
puts "❌ Ruby: Failed to parse request"
|
|
104
|
-
end
|
|
105
|
-
rescue => e
|
|
106
|
-
puts "❌ Ruby: Error handling request: #{e.class} - #{e.message}"
|
|
107
|
-
puts e.backtrace.first(5)
|
|
18
|
+
# Callback from C extension - delegates to server instance
|
|
19
|
+
def handle_stream(connection_data, stream_id, event, data)
|
|
20
|
+
instance&.handle_stream_event(connection_data, stream_id, event, data)
|
|
108
21
|
end
|
|
109
22
|
end
|
|
110
23
|
|
|
@@ -114,30 +27,36 @@ module Quicsilver
|
|
|
114
27
|
@app = app || default_rack_app
|
|
115
28
|
@server_configuration = server_configuration || ServerConfiguration.new
|
|
116
29
|
@running = false
|
|
30
|
+
@shutting_down = false
|
|
117
31
|
@listener_data = nil
|
|
32
|
+
@connections = {}
|
|
33
|
+
@request_registry = RequestRegistry.new
|
|
118
34
|
|
|
119
|
-
|
|
120
|
-
self.class.rack_app = @app
|
|
35
|
+
self.class.instance = self
|
|
121
36
|
end
|
|
122
37
|
|
|
123
38
|
def start
|
|
124
39
|
raise ServerIsRunningError, "Server is already running" if @running
|
|
125
40
|
|
|
126
|
-
# Initialize MSQUIC if not already done
|
|
127
41
|
Quicsilver.open_connection
|
|
128
|
-
|
|
129
42
|
config = Quicsilver.create_server_configuration(@server_configuration.to_h)
|
|
130
|
-
unless config
|
|
131
|
-
raise ServerConfigurationError, "Failed to create server configuration"
|
|
132
|
-
end
|
|
43
|
+
raise ServerConfigurationError, "Failed to create server configuration" unless config
|
|
133
44
|
|
|
134
45
|
# Create and start the listener
|
|
135
|
-
|
|
136
|
-
|
|
46
|
+
result = Quicsilver.create_listener(config)
|
|
47
|
+
@listener_data = ListenerData.new(result[0], result[1])
|
|
48
|
+
raise ServerListenerError, "Failed to create listener #{@address}:#{@port}" unless @listener_data
|
|
49
|
+
|
|
50
|
+
unless Quicsilver.start_listener(@listener_data.listener_handle, @address, @port)
|
|
51
|
+
Quicsilver.close_configuration(config)
|
|
52
|
+
cleanup_failed_server
|
|
53
|
+
raise ServerListenerError, "Failed to start listener on #{@address}:#{@port}"
|
|
54
|
+
end
|
|
137
55
|
|
|
138
56
|
@running = true
|
|
139
57
|
|
|
140
|
-
|
|
58
|
+
Quicsilver.event_loop.start
|
|
59
|
+
Quicsilver.event_loop.join # Block until shutdown
|
|
141
60
|
rescue ServerConfigurationError, ServerListenerError => e
|
|
142
61
|
cleanup_failed_server
|
|
143
62
|
@running = false
|
|
@@ -161,53 +80,107 @@ module Quicsilver
|
|
|
161
80
|
def stop
|
|
162
81
|
return unless @running
|
|
163
82
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
listener_handle = @listener_data[0]
|
|
168
|
-
Quicsilver.stop_listener(listener_handle)
|
|
169
|
-
Quicsilver.close_listener(@listener_data)
|
|
170
|
-
@listener_data = nil
|
|
83
|
+
if @listener_data && @listener_data.listener_handle
|
|
84
|
+
Quicsilver.stop_listener(@listener_data.listener_handle)
|
|
85
|
+
Quicsilver.close_listener([@listener_data.listener_handle, @listener_data.context_handle])
|
|
171
86
|
end
|
|
172
87
|
|
|
88
|
+
Quicsilver.event_loop.stop # Stop event loop so start unblocks
|
|
173
89
|
@running = false
|
|
174
|
-
|
|
175
|
-
rescue
|
|
176
|
-
puts "⚠️ Error during server shutdown"
|
|
177
|
-
# Continue with cleanup even if there are errors
|
|
90
|
+
@listener_data = nil
|
|
91
|
+
rescue => e
|
|
178
92
|
@listener_data = nil
|
|
179
93
|
@running = false
|
|
94
|
+
raise ServerStopError, "Failed to stop server: #{e.message}"
|
|
180
95
|
end
|
|
181
96
|
|
|
182
97
|
def running?
|
|
183
98
|
@running
|
|
184
99
|
end
|
|
185
100
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
101
|
+
# Graceful shutdown: send GOAWAY, wait for in-flight requests, then stop
|
|
102
|
+
def shutdown(timeout: 30)
|
|
103
|
+
return unless @running
|
|
104
|
+
return if @shutting_down
|
|
105
|
+
|
|
106
|
+
@shutting_down = true
|
|
107
|
+
Quicsilver.logger.info("Initiating graceful shutdown (timeout: #{timeout}s)")
|
|
108
|
+
|
|
109
|
+
# Phase 1: Send GOAWAY with max stream ID to all connections
|
|
110
|
+
# This tells clients to stop sending new requests
|
|
111
|
+
@connections.each_value do |connection|
|
|
112
|
+
send_goaway(connection, HTTP3::MAX_STREAM_ID)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Phase 2: Wait for in-flight requests to drain
|
|
116
|
+
deadline = Time.now + timeout
|
|
117
|
+
until @request_registry.empty? || Time.now > deadline
|
|
118
|
+
sleep 0.1
|
|
119
|
+
end
|
|
195
120
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
Quicsilver.
|
|
201
|
-
sleep(0.01) # Poll every 10ms
|
|
121
|
+
# Log any requests that didn't complete
|
|
122
|
+
unless @request_registry.empty?
|
|
123
|
+
@request_registry.active_requests.each do |stream_id, req|
|
|
124
|
+
elapsed = Time.now - req[:started_at]
|
|
125
|
+
Quicsilver.logger.warn("Force-closing request: #{req[:method]} #{req[:path]} (stream: #{stream_id}, elapsed: #{elapsed.round(2)}s)")
|
|
202
126
|
end
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Phase 3: Send final GOAWAY with actual last stream ID and shutdown connections
|
|
130
|
+
@connections.each_value do |connection|
|
|
131
|
+
last_stream_id = connection.streams.keys.select { |id| (id & 0x02) == 0 }.max || 0
|
|
132
|
+
send_goaway(connection, last_stream_id)
|
|
133
|
+
|
|
134
|
+
# Graceful QUIC shutdown (sends CONNECTION_CLOSE to peer)
|
|
135
|
+
Quicsilver.connection_shutdown(connection.handle, 0, false)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Give connections a moment to close gracefully
|
|
139
|
+
sleep 0.1
|
|
140
|
+
|
|
141
|
+
# Phase 4: Hard stop
|
|
142
|
+
stop
|
|
143
|
+
@shutting_down = false
|
|
144
|
+
|
|
145
|
+
Quicsilver.logger.info("Graceful shutdown complete")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def handle_stream_event(connection_data, stream_id, event, data)
|
|
149
|
+
connection_handle = connection_data[0]
|
|
150
|
+
|
|
151
|
+
case event
|
|
152
|
+
when STREAM_EVENT_CONNECTION_ESTABLISHED
|
|
153
|
+
connection = Connection.new(connection_handle, connection_data)
|
|
154
|
+
@connections[connection_handle] = connection
|
|
155
|
+
setup_http3_streams(connection)
|
|
156
|
+
when STREAM_EVENT_CONNECTION_CLOSED
|
|
157
|
+
@connections.delete(connection_handle)&.streams&.clear
|
|
158
|
+
when STREAM_EVENT_SEND_COMPLETE
|
|
159
|
+
# Buffer cleanup handled in C extension
|
|
160
|
+
when STREAM_EVENT_RECEIVE
|
|
161
|
+
return unless connection = @connections[connection_handle]
|
|
162
|
+
|
|
163
|
+
stream = connection.get_stream(stream_id) || QuicStream.new(stream_id)
|
|
164
|
+
connection.add_stream(stream) unless connection.get_stream(stream_id)
|
|
165
|
+
stream.append_data(data)
|
|
166
|
+
when STREAM_EVENT_RECEIVE_FIN
|
|
167
|
+
return unless connection = @connections[connection_handle]
|
|
168
|
+
|
|
169
|
+
# Extract stream handle from data (first 8 bytes)
|
|
170
|
+
stream_handle = data[0, 8].unpack1('Q')
|
|
171
|
+
actual_data = data[8..-1] || ""
|
|
172
|
+
|
|
173
|
+
stream = connection.get_stream(stream_id) || QuicStream.new(stream_id)
|
|
174
|
+
stream.stream_handle = stream_handle
|
|
175
|
+
stream.append_data(actual_data)
|
|
176
|
+
|
|
177
|
+
if stream.bidirectional?
|
|
178
|
+
handle_request(connection, stream)
|
|
179
|
+
else
|
|
180
|
+
handle_unidirectional_stream(connection, stream) # Unidirectional stream (control/QPACK)
|
|
210
181
|
end
|
|
182
|
+
|
|
183
|
+
connection.remove_stream(stream_id)
|
|
211
184
|
end
|
|
212
185
|
end
|
|
213
186
|
|
|
@@ -221,38 +194,162 @@ module Quicsilver
|
|
|
221
194
|
}
|
|
222
195
|
end
|
|
223
196
|
|
|
224
|
-
def
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
197
|
+
def cleanup_failed_server
|
|
198
|
+
if @listener_data
|
|
199
|
+
begin
|
|
200
|
+
Quicsilver.stop_listener(@listener_data.listener_handle) if @listener_data.listener_handle
|
|
201
|
+
Quicsilver.close_listener([@listener_data.listener_handle, @listener_data.context_handle]) if @listener_data.listener_handle
|
|
202
|
+
rescue
|
|
203
|
+
# Ignore cleanup errors
|
|
204
|
+
ensure
|
|
205
|
+
@listener_data = nil
|
|
206
|
+
end
|
|
230
207
|
end
|
|
231
208
|
end
|
|
232
209
|
|
|
233
|
-
def
|
|
234
|
-
|
|
235
|
-
listener_data = ListenerData.new(result[0], result[1])
|
|
210
|
+
def setup_http3_streams(connection)
|
|
211
|
+
connection_data = connection.data
|
|
236
212
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
213
|
+
# Send control stream (required) - store handle for GOAWAY
|
|
214
|
+
control_stream = Quicsilver.open_stream(connection_data, true)
|
|
215
|
+
control_data = HTTP3.build_control_stream
|
|
216
|
+
Quicsilver.send_stream(control_stream, control_data, false)
|
|
217
|
+
connection.server_control_stream = control_stream
|
|
218
|
+
|
|
219
|
+
# Open QPACK encoder/decoder streams (required)
|
|
220
|
+
[0x02, 0x03].each do |type|
|
|
221
|
+
stream = Quicsilver.open_stream(connection_data, true)
|
|
222
|
+
Quicsilver.send_stream(stream, [type].pack('C'), false)
|
|
240
223
|
end
|
|
224
|
+
end
|
|
225
|
+
|
|
241
226
|
|
|
242
|
-
|
|
227
|
+
def handle_control_stream(connection, stream)
|
|
228
|
+
return if stream.data.empty?
|
|
229
|
+
|
|
230
|
+
case stream.data[0].ord
|
|
231
|
+
when 0x00 then connection.set_control_stream(stream.stream_id)
|
|
232
|
+
when 0x02 then connection.set_qpack_encoder_stream(stream.stream_id)
|
|
233
|
+
when 0x03 then connection.set_qpack_decoder_stream(stream.stream_id)
|
|
234
|
+
end
|
|
243
235
|
end
|
|
244
236
|
|
|
245
|
-
def
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
237
|
+
def handle_unidirectional_stream(connection, stream)
|
|
238
|
+
data = stream.data
|
|
239
|
+
return if data.empty?
|
|
240
|
+
|
|
241
|
+
stream_type = data[0].ord
|
|
242
|
+
payload = data[1..-1]
|
|
243
|
+
|
|
244
|
+
case stream_type
|
|
245
|
+
when 0x00 # Control stream
|
|
246
|
+
connection.set_control_stream(stream.stream_id)
|
|
247
|
+
parse_client_control_stream(payload)
|
|
248
|
+
when 0x02 # QPACK encoder stream
|
|
249
|
+
# Store encoder stream for sending dynamic table updates
|
|
250
|
+
connection.set_qpack_encoder_stream(stream.stream_id)
|
|
251
|
+
when 0x03 # QPACK decoder stream
|
|
252
|
+
# Store decoder stream for receiving acknowledgments
|
|
253
|
+
connection.set_qpack_decoder_stream(stream.stream_id)
|
|
254
|
+
else
|
|
255
|
+
raise "⚠️ Ruby: Stream #{stream.stream_id}: Unknown stream type: 0x#{stream_type.to_s(16)}"
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def parse_client_control_stream(data)
|
|
260
|
+
offset = 0
|
|
261
|
+
while offset < data.bytesize
|
|
262
|
+
frame_type, type_len = HTTP3.decode_varint(data.bytes, offset)
|
|
263
|
+
frame_length, length_len = HTTP3.decode_varint(data.bytes, offset + type_len)
|
|
264
|
+
|
|
265
|
+
if frame_type == HTTP3::FRAME_SETTINGS
|
|
266
|
+
# Parse client settings
|
|
267
|
+
settings_payload = data[offset + type_len + length_len, frame_length]
|
|
268
|
+
parse_settings_frame(settings_payload)
|
|
254
269
|
end
|
|
270
|
+
|
|
271
|
+
offset += type_len + length_len + frame_length
|
|
255
272
|
end
|
|
256
273
|
end
|
|
274
|
+
|
|
275
|
+
def parse_settings_frame(payload)
|
|
276
|
+
offset = 0
|
|
277
|
+
settings = {}
|
|
278
|
+
|
|
279
|
+
while offset < payload.bytesize
|
|
280
|
+
setting_id, id_len = HTTP3.decode_varint(payload.bytes, offset)
|
|
281
|
+
setting_value, value_len = HTTP3.decode_varint(payload.bytes, offset + id_len)
|
|
282
|
+
settings[setting_id] = setting_value
|
|
283
|
+
offset += id_len + value_len
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
settings
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def handle_request(connection, stream)
|
|
290
|
+
parser = HTTP3::RequestParser.new(stream.data)
|
|
291
|
+
parser.parse
|
|
292
|
+
env = parser.to_rack_env
|
|
293
|
+
|
|
294
|
+
if env && @app
|
|
295
|
+
# Track request
|
|
296
|
+
@request_registry.track(
|
|
297
|
+
stream.stream_id,
|
|
298
|
+
connection.handle,
|
|
299
|
+
path: env["PATH_INFO"] || "/",
|
|
300
|
+
method: env["REQUEST_METHOD"] || "GET"
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Call Rack app
|
|
304
|
+
status, headers, body = @app.call(env)
|
|
305
|
+
encoder = HTTP3::ResponseEncoder.new(status, headers, body)
|
|
306
|
+
|
|
307
|
+
raise "Stream handle not found for stream #{stream.stream_id}" unless stream.ready_to_send?
|
|
308
|
+
|
|
309
|
+
# Rack convention: body.to_ary means bufferable, otherwise stream
|
|
310
|
+
if body.respond_to?(:to_ary)
|
|
311
|
+
# Buffer mode - small responses (Arrays), send all at once
|
|
312
|
+
Quicsilver.send_stream(stream.stream_handle, encoder.encode, true)
|
|
313
|
+
else
|
|
314
|
+
# Stream mode - lazy bodies (ActionController::Live, SSE), send incrementally
|
|
315
|
+
encoder.stream_encode do |frame_data, fin|
|
|
316
|
+
Quicsilver.send_stream(stream.stream_handle, frame_data, fin) unless frame_data.empty? && !fin
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Mark request complete
|
|
321
|
+
@request_registry.complete(stream.stream_id)
|
|
322
|
+
else
|
|
323
|
+
# failed to parse request
|
|
324
|
+
if stream.ready_to_send?
|
|
325
|
+
error_response = encode_error_response(400, "Bad Request")
|
|
326
|
+
Quicsilver.send_stream(stream.stream_handle, error_response, true)
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
rescue => e
|
|
330
|
+
Quicsilver.logger.error("Error handling request: #{e.class} - #{e.message}")
|
|
331
|
+
Quicsilver.logger.debug(e.backtrace.first(5).join("\n"))
|
|
332
|
+
error_response = encode_error_response(500, "Internal Server Error")
|
|
333
|
+
|
|
334
|
+
Quicsilver.send_stream(stream.stream_handle, error_response, true) if stream.ready_to_send?
|
|
335
|
+
ensure
|
|
336
|
+
# Always complete the request, even on error
|
|
337
|
+
@request_registry.complete(stream.stream_id) if @request_registry.include?(stream.stream_id)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def encode_error_response(status, message)
|
|
341
|
+
body = ["#{status} #{message}"]
|
|
342
|
+
encoder = HTTP3::ResponseEncoder.new(status, {"content-type" => "text/plain"}, body)
|
|
343
|
+
encoder.encode
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def send_goaway(connection, stream_id)
|
|
347
|
+
return unless connection.server_control_stream
|
|
348
|
+
|
|
349
|
+
goaway_frame = HTTP3.build_goaway_frame(stream_id)
|
|
350
|
+
Quicsilver.send_stream(connection.server_control_stream, goaway_frame, false)
|
|
351
|
+
rescue => e
|
|
352
|
+
Quicsilver.logger.error("Failed to send GOAWAY to connection #{connection.handle}: #{e.message}")
|
|
353
|
+
end
|
|
257
354
|
end
|
|
258
355
|
end
|
|
@@ -1,23 +1,49 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
require "localhost"
|
|
4
|
+
|
|
3
5
|
module Quicsilver
|
|
4
6
|
class ServerConfiguration
|
|
5
|
-
attr_reader :cert_file, :key_file, :idle_timeout, :server_resumption_level, :peer_bidi_stream_count,
|
|
6
|
-
:peer_unidi_stream_count
|
|
7
|
+
attr_reader :cert_file, :key_file, :idle_timeout, :server_resumption_level, :peer_bidi_stream_count,
|
|
8
|
+
:peer_unidi_stream_count, :stream_recv_window, :stream_recv_buffer, :conn_flow_control_window
|
|
7
9
|
|
|
8
10
|
QUIC_SERVER_RESUME_AND_ZERORTT = 1
|
|
9
11
|
QUIC_SERVER_RESUME_ONLY = 2
|
|
10
12
|
QUIC_SERVER_RESUME_AND_REUSE = 3
|
|
11
13
|
QUIC_SERVER_RESUME_AND_REUSE_ZERORTT = 4
|
|
12
14
|
|
|
15
|
+
DEFAULT_CERT_FILE = "certificates/server.crt"
|
|
16
|
+
DEFAULT_KEY_FILE = "certificates/server.key"
|
|
17
|
+
DEFAULT_ALPN = "h3"
|
|
18
|
+
|
|
19
|
+
# Flow control defaults (msquic defaults)
|
|
20
|
+
# See: https://github.com/microsoft/msquic/blob/main/docs/Settings.md
|
|
21
|
+
DEFAULT_STREAM_RECV_WINDOW = 65_536 # 64KB - initial stream receive window
|
|
22
|
+
DEFAULT_STREAM_RECV_BUFFER = 4_096 # 4KB - stream buffer size
|
|
23
|
+
DEFAULT_CONN_FLOW_CONTROL_WINDOW = 16_777_216 # 16MB - connection-wide flow control
|
|
24
|
+
|
|
13
25
|
def initialize(cert_file = nil, key_file = nil, options = {})
|
|
14
|
-
@cert_file = cert_file.nil? ? "certs/server.crt" : cert_file
|
|
15
|
-
@key_file = key_file.nil? ? "certs/server.key" : key_file
|
|
16
26
|
@idle_timeout = options[:idle_timeout].nil? ? 10000 : options[:idle_timeout]
|
|
17
27
|
@server_resumption_level = options[:server_resumption_level].nil? ? QUIC_SERVER_RESUME_AND_ZERORTT : options[:server_resumption_level]
|
|
18
28
|
@peer_bidi_stream_count = options[:peer_bidi_stream_count].nil? ? 10 : options[:peer_bidi_stream_count]
|
|
19
29
|
@peer_unidi_stream_count = options[:peer_unidi_stream_count].nil? ? 10 : options[:peer_unidi_stream_count]
|
|
20
|
-
@alpn = options[:alpn].nil? ?
|
|
30
|
+
@alpn = options[:alpn].nil? ? DEFAULT_ALPN : options[:alpn]
|
|
31
|
+
|
|
32
|
+
# Flow control / backpressure settings
|
|
33
|
+
@stream_recv_window = options[:stream_recv_window].nil? ? DEFAULT_STREAM_RECV_WINDOW : options[:stream_recv_window]
|
|
34
|
+
@stream_recv_buffer = options[:stream_recv_buffer].nil? ? DEFAULT_STREAM_RECV_BUFFER : options[:stream_recv_buffer]
|
|
35
|
+
@conn_flow_control_window = options[:conn_flow_control_window].nil? ? DEFAULT_CONN_FLOW_CONTROL_WINDOW : options[:conn_flow_control_window]
|
|
36
|
+
|
|
37
|
+
@cert_file = cert_file.nil? ? DEFAULT_CERT_FILE : cert_file
|
|
38
|
+
@key_file = key_file.nil? ? DEFAULT_KEY_FILE : key_file
|
|
39
|
+
|
|
40
|
+
unless File.exist?(@cert_file)
|
|
41
|
+
raise ServerConfigurationError, "Certificate file not found: #{@cert_file}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
unless File.exist?(@key_file)
|
|
45
|
+
raise ServerConfigurationError, "Key file not found: #{@key_file}"
|
|
46
|
+
end
|
|
21
47
|
end
|
|
22
48
|
|
|
23
49
|
# Common HTTP/3 ALPN Values:
|
|
@@ -42,7 +68,10 @@ module Quicsilver
|
|
|
42
68
|
server_resumption_level: @server_resumption_level,
|
|
43
69
|
peer_bidi_stream_count: @peer_bidi_stream_count,
|
|
44
70
|
peer_unidi_stream_count: @peer_unidi_stream_count,
|
|
45
|
-
alpn: alpn
|
|
71
|
+
alpn: alpn,
|
|
72
|
+
stream_recv_window: @stream_recv_window,
|
|
73
|
+
stream_recv_buffer: @stream_recv_buffer,
|
|
74
|
+
conn_flow_control_window: @conn_flow_control_window
|
|
46
75
|
}
|
|
47
76
|
end
|
|
48
77
|
end
|
data/lib/quicsilver/version.rb
CHANGED
data/lib/quicsilver.rb
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "logger"
|
|
3
4
|
require_relative "quicsilver/version"
|
|
4
5
|
require_relative "quicsilver/client"
|
|
6
|
+
require_relative "quicsilver/connection"
|
|
7
|
+
require_relative "quicsilver/event_loop"
|
|
8
|
+
require_relative "quicsilver/quic_stream"
|
|
5
9
|
require_relative "quicsilver/listener_data"
|
|
10
|
+
require_relative "quicsilver/request_registry"
|
|
6
11
|
require_relative "quicsilver/server"
|
|
7
12
|
require_relative "quicsilver/server_configuration"
|
|
8
13
|
require_relative "quicsilver/http3"
|
|
@@ -10,6 +15,7 @@ require_relative "quicsilver/http3/request_parser"
|
|
|
10
15
|
require_relative "quicsilver/http3/request_encoder"
|
|
11
16
|
require_relative "quicsilver/http3/response_encoder"
|
|
12
17
|
require_relative "quicsilver/quicsilver"
|
|
18
|
+
require_relative "rackup/handler/quicsilver"
|
|
13
19
|
|
|
14
20
|
module Quicsilver
|
|
15
21
|
class Error < StandardError; end
|
|
@@ -19,4 +25,20 @@ module Quicsilver
|
|
|
19
25
|
class ServerError < Error; end
|
|
20
26
|
class ConnectionError < Error; end
|
|
21
27
|
class TimeoutError < Error; end
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
attr_writer :logger
|
|
31
|
+
|
|
32
|
+
def logger
|
|
33
|
+
@logger ||= default_logger
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def default_logger
|
|
39
|
+
Logger.new($stdout, level: Logger::INFO).tap do |log|
|
|
40
|
+
log.progname = "Quicsilver"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
22
44
|
end
|