quicsilver 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +41 -0
- data/.gitignore +3 -1
- data/CHANGELOG.md +76 -5
- data/Gemfile.lock +18 -4
- data/LICENSE +21 -0
- data/README.md +33 -53
- 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 +46 -0
- data/benchmarks/rails.rb +170 -0
- data/benchmarks/throughput.rb +113 -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 +615 -138
- data/lib/quicsilver/client/client.rb +250 -0
- data/lib/quicsilver/client/request.rb +98 -0
- data/lib/quicsilver/protocol/frames.rb +327 -0
- 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 +49 -9
- data/lib/rackup/handler/quicsilver.rb +77 -0
- data/quicsilver.gemspec +10 -3
- metadata +122 -17
- data/examples/minimal_http3_client.rb +0 -89
- data/lib/quicsilver/client.rb +0 -191
- data/lib/quicsilver/http3/request_encoder.rb +0 -112
- data/lib/quicsilver/http3/request_parser.rb +0 -158
- data/lib/quicsilver/http3/response_encoder.rb +0 -73
- data/lib/quicsilver/http3.rb +0 -68
- data/lib/quicsilver/listener_data.rb +0 -29
- data/lib/quicsilver/server.rb +0 -258
- data/lib/quicsilver/server_configuration.rb +0 -49
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'stringio'
|
|
4
|
-
|
|
5
|
-
module Quicsilver
|
|
6
|
-
module HTTP3
|
|
7
|
-
class RequestParser
|
|
8
|
-
attr_reader :frames, :headers, :body
|
|
9
|
-
|
|
10
|
-
def initialize(data)
|
|
11
|
-
@data = data
|
|
12
|
-
@frames = []
|
|
13
|
-
@headers = {}
|
|
14
|
-
@body = StringIO.new
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def parse
|
|
18
|
-
parse!
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def to_rack_env(stream_info = {})
|
|
22
|
-
return nil if @headers.empty?
|
|
23
|
-
|
|
24
|
-
# Extract path and query string
|
|
25
|
-
path_full = @headers[':path'] || '/'
|
|
26
|
-
path, query = path_full.split('?', 2)
|
|
27
|
-
|
|
28
|
-
# Extract host and port
|
|
29
|
-
authority = @headers[':authority'] || 'localhost:4433'
|
|
30
|
-
host, port = authority.split(':', 2)
|
|
31
|
-
port ||= '4433'
|
|
32
|
-
|
|
33
|
-
env = {
|
|
34
|
-
'REQUEST_METHOD' => @headers[':method'] || 'GET',
|
|
35
|
-
'PATH_INFO' => path,
|
|
36
|
-
'QUERY_STRING' => query || '',
|
|
37
|
-
'SERVER_NAME' => host,
|
|
38
|
-
'SERVER_PORT' => port,
|
|
39
|
-
'SERVER_PROTOCOL' => 'HTTP/3',
|
|
40
|
-
'rack.version' => [1, 3],
|
|
41
|
-
'rack.url_scheme' => @headers[':scheme'] || 'https',
|
|
42
|
-
'rack.input' => @body,
|
|
43
|
-
'rack.errors' => $stderr,
|
|
44
|
-
'rack.multithread' => true,
|
|
45
|
-
'rack.multiprocess' => false,
|
|
46
|
-
'rack.run_once' => false,
|
|
47
|
-
'rack.hijack?' => false,
|
|
48
|
-
'SCRIPT_NAME' => '',
|
|
49
|
-
'CONTENT_LENGTH' => @body.size.to_s,
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
# Add regular headers as HTTP_*
|
|
53
|
-
@headers.each do |name, value|
|
|
54
|
-
next if name.start_with?(':')
|
|
55
|
-
env["HTTP_#{name.upcase.tr('-', '_')}"] = value
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
env
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
private
|
|
62
|
-
|
|
63
|
-
def parse!
|
|
64
|
-
buffer = @data.dup
|
|
65
|
-
offset = 0
|
|
66
|
-
|
|
67
|
-
while offset < buffer.bytesize
|
|
68
|
-
break if buffer.bytesize - offset < 2
|
|
69
|
-
|
|
70
|
-
type, type_len = HTTP3.decode_varint(buffer.bytes, offset)
|
|
71
|
-
length, length_len = HTTP3.decode_varint(buffer.bytes, offset + type_len)
|
|
72
|
-
header_len = type_len + length_len
|
|
73
|
-
|
|
74
|
-
break if buffer.bytesize < offset + header_len + length
|
|
75
|
-
|
|
76
|
-
payload = buffer[offset + header_len, length]
|
|
77
|
-
@frames << { type: type, length: length, payload: payload }
|
|
78
|
-
|
|
79
|
-
case type
|
|
80
|
-
when 0x01 # HEADERS
|
|
81
|
-
parse_headers(payload)
|
|
82
|
-
when 0x00 # DATA
|
|
83
|
-
@body.write(payload)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
offset += header_len + length
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
@body.rewind
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def parse_headers(payload)
|
|
93
|
-
# Skip QPACK required insert count (1 byte) + delta base (1 byte)
|
|
94
|
-
offset = 2
|
|
95
|
-
return if payload.bytesize < offset
|
|
96
|
-
|
|
97
|
-
while offset < payload.bytesize
|
|
98
|
-
byte = payload.bytes[offset]
|
|
99
|
-
|
|
100
|
-
# Indexed field line (static table, starts with 0x4X or 0x5X)
|
|
101
|
-
if (byte & 0xC0) == 0x40 || (byte & 0xC0) == 0x80
|
|
102
|
-
index = byte & 0x3F
|
|
103
|
-
offset += 1
|
|
104
|
-
|
|
105
|
-
# Check if this is a complete field (name+value) or just name
|
|
106
|
-
field = decode_static_table_field(index)
|
|
107
|
-
|
|
108
|
-
if field.is_a?(Hash)
|
|
109
|
-
# Complete field with both name and value (e.g., :method GET)
|
|
110
|
-
@headers.merge!(field)
|
|
111
|
-
elsif field
|
|
112
|
-
# Name-only entry, value follows
|
|
113
|
-
value_len, len_bytes = HTTP3.decode_varint(payload.bytes, offset)
|
|
114
|
-
offset += len_bytes
|
|
115
|
-
value = payload[offset, value_len]
|
|
116
|
-
offset += value_len
|
|
117
|
-
@headers[field] = value
|
|
118
|
-
end
|
|
119
|
-
# Literal with literal name (starts with 0x2X)
|
|
120
|
-
elsif (byte & 0xE0) == 0x20
|
|
121
|
-
name_len = byte & 0x1F
|
|
122
|
-
offset += 1
|
|
123
|
-
name = payload[offset, name_len]
|
|
124
|
-
offset += name_len
|
|
125
|
-
|
|
126
|
-
value_len, len_bytes = HTTP3.decode_varint(payload.bytes, offset)
|
|
127
|
-
offset += len_bytes
|
|
128
|
-
value = payload[offset, value_len]
|
|
129
|
-
offset += value_len
|
|
130
|
-
|
|
131
|
-
@headers[name] = value
|
|
132
|
-
else
|
|
133
|
-
break # Unknown encoding
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# QPACK static table decoder (RFC 9204 Appendix A)
|
|
139
|
-
# Returns Hash for complete fields, String for name-only fields
|
|
140
|
-
def decode_static_table_field(index)
|
|
141
|
-
case index
|
|
142
|
-
when 0 then ':authority' # Name only
|
|
143
|
-
when 1 then ':path' # Name only
|
|
144
|
-
when 15 then {':method' => 'CONNECT'}
|
|
145
|
-
when 16 then {':method' => 'DELETE'}
|
|
146
|
-
when 17 then {':method' => 'GET'}
|
|
147
|
-
when 18 then {':method' => 'HEAD'}
|
|
148
|
-
when 19 then {':method' => 'OPTIONS'}
|
|
149
|
-
when 20 then {':method' => 'POST'}
|
|
150
|
-
when 21 then {':method' => 'PUT'}
|
|
151
|
-
when 22 then {':scheme' => 'http'}
|
|
152
|
-
when 23 then {':scheme' => 'https'}
|
|
153
|
-
else nil
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
end
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Quicsilver
|
|
4
|
-
module HTTP3
|
|
5
|
-
class ResponseEncoder
|
|
6
|
-
def initialize(status, headers, body)
|
|
7
|
-
@status = status
|
|
8
|
-
@headers = headers
|
|
9
|
-
@body = body
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def encode
|
|
13
|
-
frames = ""
|
|
14
|
-
|
|
15
|
-
# HEADERS frame
|
|
16
|
-
frames += encode_headers_frame
|
|
17
|
-
|
|
18
|
-
# DATA frame(s)
|
|
19
|
-
@body.each do |chunk|
|
|
20
|
-
frames += encode_data_frame(chunk) unless chunk.empty?
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
@body.close if @body.respond_to?(:close)
|
|
24
|
-
|
|
25
|
-
frames
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
def encode_headers_frame
|
|
31
|
-
payload = encode_qpack_response
|
|
32
|
-
|
|
33
|
-
frame_type = HTTP3.encode_varint(0x01) # HEADERS
|
|
34
|
-
frame_length = HTTP3.encode_varint(payload.bytesize)
|
|
35
|
-
|
|
36
|
-
frame_type + frame_length + payload
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def encode_data_frame(data)
|
|
40
|
-
frame_type = HTTP3.encode_varint(0x00) # DATA
|
|
41
|
-
frame_length = HTTP3.encode_varint(data.bytesize)
|
|
42
|
-
|
|
43
|
-
frame_type + frame_length + data
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def encode_qpack_response
|
|
47
|
-
# QPACK prefix: Required Insert Count = 0, Delta Base = 0
|
|
48
|
-
encoded = [0x00, 0x00].pack('C*')
|
|
49
|
-
|
|
50
|
-
# :status pseudo-header (literal indexed)
|
|
51
|
-
status_str = @status.to_s
|
|
52
|
-
encoded += [0x58, status_str.bytesize].pack('C*')
|
|
53
|
-
encoded += status_str
|
|
54
|
-
|
|
55
|
-
# Regular headers (literal with literal name)
|
|
56
|
-
@headers.each do |name, value|
|
|
57
|
-
next if name.start_with?('rack.') # Skip Rack internals
|
|
58
|
-
|
|
59
|
-
name = name.downcase
|
|
60
|
-
value = value.to_s
|
|
61
|
-
|
|
62
|
-
# Literal field line with literal name (0x2X prefix)
|
|
63
|
-
encoded += [0x20 | name.bytesize].pack('C')
|
|
64
|
-
encoded += name
|
|
65
|
-
encoded += HTTP3.encode_varint(value.bytesize)
|
|
66
|
-
encoded += value
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
encoded
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
data/lib/quicsilver/http3.rb
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Quicsilver
|
|
4
|
-
module HTTP3
|
|
5
|
-
# Encode variable-length integer
|
|
6
|
-
def self.encode_varint(value)
|
|
7
|
-
case value
|
|
8
|
-
when 0..63
|
|
9
|
-
[value].pack('C')
|
|
10
|
-
when 64..16383
|
|
11
|
-
[0x40 | (value >> 8), value & 0xFF].pack('C*')
|
|
12
|
-
when 16384..1073741823
|
|
13
|
-
[0x80 | (value >> 24), (value >> 16) & 0xFF,
|
|
14
|
-
(value >> 8) & 0xFF, value & 0xFF].pack('C*')
|
|
15
|
-
else
|
|
16
|
-
[0xC0 | (value >> 56), (value >> 48) & 0xFF,
|
|
17
|
-
(value >> 40) & 0xFF, (value >> 32) & 0xFF,
|
|
18
|
-
(value >> 24) & 0xFF, (value >> 16) & 0xFF,
|
|
19
|
-
(value >> 8) & 0xFF, value & 0xFF].pack('C*')
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def self.build_settings_frame(settings = {})
|
|
24
|
-
payload = ""
|
|
25
|
-
settings.each do |id, value|
|
|
26
|
-
payload += encode_varint(id)
|
|
27
|
-
payload += encode_varint(value)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
frame_type = encode_varint(0x04) # SETTINGS
|
|
31
|
-
frame_length = encode_varint(payload.bytesize)
|
|
32
|
-
|
|
33
|
-
frame_type + frame_length + payload
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Build control stream data
|
|
37
|
-
def self.build_control_stream
|
|
38
|
-
stream_type = [0x00].pack('C') # Control stream type
|
|
39
|
-
settings = build_settings_frame({
|
|
40
|
-
# 0x01 => 4096, # QPACK_MAX_TABLE_CAPACITY (optional)
|
|
41
|
-
# 0x06 => 16384 # MAX_HEADER_LIST_SIZE (optional)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
stream_type + settings
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Decode variable-length integer (RFC 9000)
|
|
48
|
-
# Returns [value, bytes_consumed]
|
|
49
|
-
def self.decode_varint(bytes, offset = 0)
|
|
50
|
-
first = bytes[offset]
|
|
51
|
-
case (first & 0xC0) >> 6
|
|
52
|
-
when 0
|
|
53
|
-
[first & 0x3F, 1]
|
|
54
|
-
when 1
|
|
55
|
-
[(first & 0x3F) << 8 | bytes[offset + 1], 2]
|
|
56
|
-
when 2
|
|
57
|
-
[(first & 0x3F) << 24 | bytes[offset + 1] << 16 |
|
|
58
|
-
bytes[offset + 2] << 8 | bytes[offset + 3], 4]
|
|
59
|
-
else # when 3
|
|
60
|
-
[(first & 0x3F) << 56 | bytes[offset + 1] << 48 |
|
|
61
|
-
bytes[offset + 2] << 40 | bytes[offset + 3] << 32 |
|
|
62
|
-
bytes[offset + 4] << 24 | bytes[offset + 5] << 16 |
|
|
63
|
-
bytes[offset + 6] << 8 | bytes[offset + 7], 8]
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Quicsilver
|
|
4
|
-
class ListenerData
|
|
5
|
-
attr_reader :listener_handle, :context_handle, :started, :stopped, :failed, :configuration
|
|
6
|
-
|
|
7
|
-
def initialize(listener_handle, context_handle)
|
|
8
|
-
@listener_handle = listener_handle # The MSQUIC listener handle
|
|
9
|
-
@context_handle = context_handle # The C context pointer
|
|
10
|
-
# NOTE: Fetch this from the context handle, or improve return values from the C extension
|
|
11
|
-
@started = false
|
|
12
|
-
@stopped = false
|
|
13
|
-
@failed = false
|
|
14
|
-
@configuration = nil
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def started?
|
|
18
|
-
@started
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def stopped?
|
|
22
|
-
@stopped
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def failed?
|
|
26
|
-
@failed
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
data/lib/quicsilver/server.rb
DELETED
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Quicsilver
|
|
4
|
-
class Server
|
|
5
|
-
attr_reader :address, :port, :server_configuration, :running
|
|
6
|
-
|
|
7
|
-
STREAM_EVENT_RECEIVE = "RECEIVE"
|
|
8
|
-
STREAM_EVENT_RECEIVE_FIN = "RECEIVE_FIN"
|
|
9
|
-
STREAM_EVENT_CONNECTION_ESTABLISHED = "CONNECTION_ESTABLISHED"
|
|
10
|
-
STREAM_EVENT_SEND_COMPLETE = "SEND_COMPLETE"
|
|
11
|
-
|
|
12
|
-
class << self
|
|
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
|
|
70
|
-
|
|
71
|
-
private
|
|
72
|
-
|
|
73
|
-
def bidirectional?(stream_id)
|
|
74
|
-
# Client-initiated bidirectional streams have bit 0x02 clear
|
|
75
|
-
(stream_id & 0x02) == 0
|
|
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)
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def initialize(port = 4433, address: "0.0.0.0", app: nil, server_configuration: nil)
|
|
112
|
-
@port = port
|
|
113
|
-
@address = address
|
|
114
|
-
@app = app || default_rack_app
|
|
115
|
-
@server_configuration = server_configuration || ServerConfiguration.new
|
|
116
|
-
@running = false
|
|
117
|
-
@listener_data = nil
|
|
118
|
-
|
|
119
|
-
# Set class-level rack app so handle_stream can access it
|
|
120
|
-
self.class.rack_app = @app
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def start
|
|
124
|
-
raise ServerIsRunningError, "Server is already running" if @running
|
|
125
|
-
|
|
126
|
-
# Initialize MSQUIC if not already done
|
|
127
|
-
Quicsilver.open_connection
|
|
128
|
-
|
|
129
|
-
config = Quicsilver.create_server_configuration(@server_configuration.to_h)
|
|
130
|
-
unless config
|
|
131
|
-
raise ServerConfigurationError, "Failed to create server configuration"
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
# Create and start the listener
|
|
135
|
-
@listener_data = start_listener(config)
|
|
136
|
-
start_server(config)
|
|
137
|
-
|
|
138
|
-
@running = true
|
|
139
|
-
|
|
140
|
-
puts "✅ QUIC server started successfully on #{@address}:#{@port}"
|
|
141
|
-
rescue ServerConfigurationError, ServerListenerError => e
|
|
142
|
-
cleanup_failed_server
|
|
143
|
-
@running = false
|
|
144
|
-
raise e
|
|
145
|
-
rescue => e
|
|
146
|
-
cleanup_failed_server
|
|
147
|
-
@running = false
|
|
148
|
-
|
|
149
|
-
error_msg = case e.message
|
|
150
|
-
when /0x16/
|
|
151
|
-
"Invalid parameter error - check certificate files and network configuration"
|
|
152
|
-
when /0x30/
|
|
153
|
-
"Address already in use - port #{@port} may be occupied"
|
|
154
|
-
else
|
|
155
|
-
e.message
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
raise ServerError, "Server start failed: #{error_msg}"
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def stop
|
|
162
|
-
return unless @running
|
|
163
|
-
|
|
164
|
-
puts "🛑 Stopping QUIC server..."
|
|
165
|
-
|
|
166
|
-
if @listener_data
|
|
167
|
-
listener_handle = @listener_data[0]
|
|
168
|
-
Quicsilver.stop_listener(listener_handle)
|
|
169
|
-
Quicsilver.close_listener(@listener_data)
|
|
170
|
-
@listener_data = nil
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
@running = false
|
|
174
|
-
puts "👋 Server stopped"
|
|
175
|
-
rescue
|
|
176
|
-
puts "⚠️ Error during server shutdown"
|
|
177
|
-
# Continue with cleanup even if there are errors
|
|
178
|
-
@listener_data = nil
|
|
179
|
-
@running = false
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def running?
|
|
183
|
-
@running
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def server_info
|
|
187
|
-
{
|
|
188
|
-
address: @address,
|
|
189
|
-
port: @port,
|
|
190
|
-
running: @running,
|
|
191
|
-
cert_file: @cert_file,
|
|
192
|
-
key_file: @key_file
|
|
193
|
-
}
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
def wait_for_connections(timeout: nil)
|
|
197
|
-
if timeout
|
|
198
|
-
end_time = Time.now + timeout
|
|
199
|
-
while Time.now < end_time && @running
|
|
200
|
-
Quicsilver.process_events
|
|
201
|
-
sleep(0.01) # Poll every 10ms
|
|
202
|
-
end
|
|
203
|
-
else
|
|
204
|
-
# Keep the server running indefinitely
|
|
205
|
-
# Process events from MSQUIC callbacks
|
|
206
|
-
loop do
|
|
207
|
-
Quicsilver.process_events
|
|
208
|
-
sleep(0.01) # Poll every 10ms
|
|
209
|
-
break unless @running
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
private
|
|
215
|
-
|
|
216
|
-
def default_rack_app
|
|
217
|
-
->(env) {
|
|
218
|
-
[200,
|
|
219
|
-
{'Content-Type' => 'text/plain'},
|
|
220
|
-
["Hello from Quicsilver!\nMethod: #{env['REQUEST_METHOD']}\nPath: #{env['PATH_INFO']}\n"]]
|
|
221
|
-
}
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
def start_server(config)
|
|
225
|
-
result = Quicsilver.start_listener(@listener_data.listener_handle, @address, @port)
|
|
226
|
-
unless result
|
|
227
|
-
Quicsilver.close_configuration(config)
|
|
228
|
-
cleanup_failed_server
|
|
229
|
-
raise ServerListenerError, "Failed to start listener on #{@address}:#{@port}"
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
def start_listener(config)
|
|
234
|
-
result = Quicsilver.create_listener(config)
|
|
235
|
-
listener_data = ListenerData.new(result[0], result[1])
|
|
236
|
-
|
|
237
|
-
unless listener_data
|
|
238
|
-
Quicsilver.close_configuration(config)
|
|
239
|
-
raise ServerListenerError, "Failed to create listener on #{@address}:#{@port}"
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
listener_data
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
def cleanup_failed_server
|
|
246
|
-
if @listener_data
|
|
247
|
-
begin
|
|
248
|
-
Quicsilver.stop_listener(@listener_data)
|
|
249
|
-
Quicsilver.close_listener(@listener_data)
|
|
250
|
-
rescue
|
|
251
|
-
# Ignore cleanup errors
|
|
252
|
-
ensure
|
|
253
|
-
@listener_data = nil
|
|
254
|
-
end
|
|
255
|
-
end
|
|
256
|
-
end
|
|
257
|
-
end
|
|
258
|
-
end
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Quicsilver
|
|
4
|
-
class ServerConfiguration
|
|
5
|
-
attr_reader :cert_file, :key_file, :idle_timeout, :server_resumption_level, :peer_bidi_stream_count,
|
|
6
|
-
:peer_unidi_stream_count
|
|
7
|
-
|
|
8
|
-
QUIC_SERVER_RESUME_AND_ZERORTT = 1
|
|
9
|
-
QUIC_SERVER_RESUME_ONLY = 2
|
|
10
|
-
QUIC_SERVER_RESUME_AND_REUSE = 3
|
|
11
|
-
QUIC_SERVER_RESUME_AND_REUSE_ZERORTT = 4
|
|
12
|
-
|
|
13
|
-
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
|
-
@idle_timeout = options[:idle_timeout].nil? ? 10000 : options[:idle_timeout]
|
|
17
|
-
@server_resumption_level = options[:server_resumption_level].nil? ? QUIC_SERVER_RESUME_AND_ZERORTT : options[:server_resumption_level]
|
|
18
|
-
@peer_bidi_stream_count = options[:peer_bidi_stream_count].nil? ? 10 : options[:peer_bidi_stream_count]
|
|
19
|
-
@peer_unidi_stream_count = options[:peer_unidi_stream_count].nil? ? 10 : options[:peer_unidi_stream_count]
|
|
20
|
-
@alpn = options[:alpn].nil? ? "h3" : options[:alpn]
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# Common HTTP/3 ALPN Values:
|
|
24
|
-
# "h3" - HTTP/3 (most common)
|
|
25
|
-
# "h3-29" - HTTP/3 draft version 29
|
|
26
|
-
# "h3-28" - HTTP/3 draft version 28
|
|
27
|
-
# "h3-27" - HTTP/3 draft version 27
|
|
28
|
-
# Other QUIC ALPN Values:
|
|
29
|
-
# "hq-interop" - HTTP/0.9 over QUIC (testing)
|
|
30
|
-
# "hq-29" - HTTP/0.9 over QUIC draft 29
|
|
31
|
-
# "doq" - DNS over QUIC
|
|
32
|
-
# "doq-i03" - DNS over QUIC draft
|
|
33
|
-
def alpn
|
|
34
|
-
@alpn
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def to_h
|
|
38
|
-
{
|
|
39
|
-
cert_file: @cert_file,
|
|
40
|
-
key_file: @key_file,
|
|
41
|
-
idle_timeout: @idle_timeout,
|
|
42
|
-
server_resumption_level: @server_resumption_level,
|
|
43
|
-
peer_bidi_stream_count: @peer_bidi_stream_count,
|
|
44
|
-
peer_unidi_stream_count: @peer_unidi_stream_count,
|
|
45
|
-
alpn: alpn
|
|
46
|
-
}
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|