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
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: quicsilver
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Haroon Ahmed
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-03-23 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: bundler
|
|
@@ -29,14 +29,14 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '
|
|
32
|
+
version: '13.0'
|
|
33
33
|
type: :development
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '
|
|
39
|
+
version: '13.0'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: rake-compiler
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -79,8 +79,91 @@ dependencies:
|
|
|
79
79
|
- - "~>"
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
81
|
version: '5.0'
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: minitest-focus
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '1.3'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '1.3'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: benchmark-ips
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '2.12'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '2.12'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: logger
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0'
|
|
117
|
+
type: :runtime
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: localhost
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - "~>"
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '1.6'
|
|
131
|
+
type: :runtime
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - "~>"
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '1.6'
|
|
138
|
+
- !ruby/object:Gem::Dependency
|
|
139
|
+
name: rack
|
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - "~>"
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: '3.0'
|
|
145
|
+
type: :runtime
|
|
146
|
+
prerelease: false
|
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - "~>"
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: '3.0'
|
|
152
|
+
- !ruby/object:Gem::Dependency
|
|
153
|
+
name: rackup
|
|
154
|
+
requirement: !ruby/object:Gem::Requirement
|
|
155
|
+
requirements:
|
|
156
|
+
- - "~>"
|
|
157
|
+
- !ruby/object:Gem::Version
|
|
158
|
+
version: '2.0'
|
|
159
|
+
type: :runtime
|
|
160
|
+
prerelease: false
|
|
161
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
162
|
+
requirements:
|
|
163
|
+
- - "~>"
|
|
164
|
+
- !ruby/object:Gem::Version
|
|
165
|
+
version: '2.0'
|
|
166
|
+
description: HTTP/3 server implementation for Ruby
|
|
84
167
|
email:
|
|
85
168
|
- haroon.ahmed25@gmail.com
|
|
86
169
|
executables: []
|
|
@@ -88,33 +171,55 @@ extensions:
|
|
|
88
171
|
- ext/quicsilver/extconf.rb
|
|
89
172
|
extra_rdoc_files: []
|
|
90
173
|
files:
|
|
174
|
+
- ".github/workflows/ci.yml"
|
|
91
175
|
- ".gitignore"
|
|
92
176
|
- ".gitmodules"
|
|
93
177
|
- ".ruby-version"
|
|
94
178
|
- CHANGELOG.md
|
|
95
179
|
- Gemfile
|
|
96
180
|
- Gemfile.lock
|
|
181
|
+
- LICENSE
|
|
97
182
|
- README.md
|
|
98
183
|
- Rakefile
|
|
184
|
+
- benchmarks/components.rb
|
|
185
|
+
- benchmarks/concurrent.rb
|
|
186
|
+
- benchmarks/helpers.rb
|
|
187
|
+
- benchmarks/quicsilver_server.rb
|
|
188
|
+
- benchmarks/rails.rb
|
|
189
|
+
- benchmarks/throughput.rb
|
|
99
190
|
- bin/console
|
|
100
191
|
- bin/setup
|
|
101
192
|
- examples/README.md
|
|
102
|
-
- examples/minimal_http3_client.rb
|
|
103
193
|
- examples/minimal_http3_server.rb
|
|
104
194
|
- examples/rack_http3_server.rb
|
|
105
195
|
- examples/setup_certs.sh
|
|
196
|
+
- examples/simple_client_test.rb
|
|
106
197
|
- ext/quicsilver/extconf.rb
|
|
107
198
|
- ext/quicsilver/quicsilver.c
|
|
108
199
|
- lib/quicsilver.rb
|
|
109
|
-
- lib/quicsilver/client.rb
|
|
110
|
-
- lib/quicsilver/
|
|
111
|
-
- lib/quicsilver/
|
|
112
|
-
- lib/quicsilver/
|
|
113
|
-
- lib/quicsilver/
|
|
114
|
-
- lib/quicsilver/
|
|
115
|
-
- lib/quicsilver/
|
|
116
|
-
- lib/quicsilver/
|
|
200
|
+
- lib/quicsilver/client/client.rb
|
|
201
|
+
- lib/quicsilver/client/request.rb
|
|
202
|
+
- lib/quicsilver/protocol/frames.rb
|
|
203
|
+
- lib/quicsilver/protocol/qpack/decoder.rb
|
|
204
|
+
- lib/quicsilver/protocol/qpack/encoder.rb
|
|
205
|
+
- lib/quicsilver/protocol/qpack/header_block_decoder.rb
|
|
206
|
+
- lib/quicsilver/protocol/qpack/huffman.rb
|
|
207
|
+
- lib/quicsilver/protocol/request_encoder.rb
|
|
208
|
+
- lib/quicsilver/protocol/request_parser.rb
|
|
209
|
+
- lib/quicsilver/protocol/response_encoder.rb
|
|
210
|
+
- lib/quicsilver/protocol/response_parser.rb
|
|
211
|
+
- lib/quicsilver/server/listener_data.rb
|
|
212
|
+
- lib/quicsilver/server/request_handler.rb
|
|
213
|
+
- lib/quicsilver/server/request_registry.rb
|
|
214
|
+
- lib/quicsilver/server/server.rb
|
|
215
|
+
- lib/quicsilver/transport/configuration.rb
|
|
216
|
+
- lib/quicsilver/transport/connection.rb
|
|
217
|
+
- lib/quicsilver/transport/event_loop.rb
|
|
218
|
+
- lib/quicsilver/transport/inbound_stream.rb
|
|
219
|
+
- lib/quicsilver/transport/stream.rb
|
|
220
|
+
- lib/quicsilver/transport/stream_event.rb
|
|
117
221
|
- lib/quicsilver/version.rb
|
|
222
|
+
- lib/rackup/handler/quicsilver.rb
|
|
118
223
|
- quicsilver.gemspec
|
|
119
224
|
homepage: https://github.com/hahmed/quicsilver
|
|
120
225
|
licenses: []
|
|
@@ -130,7 +235,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
130
235
|
requirements:
|
|
131
236
|
- - ">="
|
|
132
237
|
- !ruby/object:Gem::Version
|
|
133
|
-
version:
|
|
238
|
+
version: 3.2.0
|
|
134
239
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
135
240
|
requirements:
|
|
136
241
|
- - ">="
|
|
@@ -139,5 +244,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
139
244
|
requirements: []
|
|
140
245
|
rubygems_version: 3.6.2
|
|
141
246
|
specification_version: 4
|
|
142
|
-
summary:
|
|
247
|
+
summary: HTTP/3 server implementation for Ruby
|
|
143
248
|
test_files: []
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
|
|
3
|
-
require "bundler/setup"
|
|
4
|
-
require "quicsilver"
|
|
5
|
-
|
|
6
|
-
puts "🔌 Minimal HTTP/3 Client Example"
|
|
7
|
-
puts "=" * 40
|
|
8
|
-
|
|
9
|
-
# Create client
|
|
10
|
-
client = Quicsilver::Client.new("127.0.0.1", 4433, unsecure: true)
|
|
11
|
-
|
|
12
|
-
puts "🔧 Connecting to server..."
|
|
13
|
-
begin
|
|
14
|
-
client.connect
|
|
15
|
-
puts "✅ Connected successfully!"
|
|
16
|
-
puts "📋 Connection info: #{client.connection_info}"
|
|
17
|
-
|
|
18
|
-
# HTTP/3 requests using RequestEncoder
|
|
19
|
-
require_relative '../lib/quicsilver/http3/request_encoder'
|
|
20
|
-
|
|
21
|
-
request1 = Quicsilver::HTTP3::RequestEncoder.new(
|
|
22
|
-
method: 'GET',
|
|
23
|
-
path: '/api/users',
|
|
24
|
-
authority: 'example.com'
|
|
25
|
-
)
|
|
26
|
-
client.send_data(request1.encode)
|
|
27
|
-
|
|
28
|
-
request2 = Quicsilver::HTTP3::RequestEncoder.new(
|
|
29
|
-
method: 'GET',
|
|
30
|
-
path: '/api/posts/123',
|
|
31
|
-
authority: 'example.com'
|
|
32
|
-
)
|
|
33
|
-
client.send_data(request2.encode)
|
|
34
|
-
|
|
35
|
-
request3 = Quicsilver::HTTP3::RequestEncoder.new(
|
|
36
|
-
method: 'POST',
|
|
37
|
-
path: '/api/messages',
|
|
38
|
-
authority: 'example.com',
|
|
39
|
-
body: '{"text":"Hello world"}'
|
|
40
|
-
)
|
|
41
|
-
client.send_data(request3.encode)
|
|
42
|
-
|
|
43
|
-
# JSON payloads (API requests) - now as proper HTTP/3 POST requests
|
|
44
|
-
request4 = Quicsilver::HTTP3::RequestEncoder.new(
|
|
45
|
-
method: 'POST',
|
|
46
|
-
path: '/api/subscribe',
|
|
47
|
-
authority: 'example.com',
|
|
48
|
-
headers: { 'content-type' => 'application/json' },
|
|
49
|
-
body: '{"action":"subscribe","channel":"orders"}'
|
|
50
|
-
)
|
|
51
|
-
client.send_data(request4.encode)
|
|
52
|
-
|
|
53
|
-
request5 = Quicsilver::HTTP3::RequestEncoder.new(
|
|
54
|
-
method: 'POST',
|
|
55
|
-
path: '/api/update',
|
|
56
|
-
authority: 'example.com',
|
|
57
|
-
headers: { 'content-type' => 'application/json' },
|
|
58
|
-
body: '{"action":"update","user_id":42,"status":"online"}'
|
|
59
|
-
)
|
|
60
|
-
client.send_data(request5.encode)
|
|
61
|
-
|
|
62
|
-
# These old manually-crafted requests use incorrect QPACK indices - removed
|
|
63
|
-
|
|
64
|
-
# Metrics/telemetry
|
|
65
|
-
# client.send_data("METRIC:cpu=45.2,mem=1024,ts=#{Time.now.to_i}")
|
|
66
|
-
# client.send_data("EVENT:login,user=alice,ip=192.168.1.100")
|
|
67
|
-
|
|
68
|
-
# Large message test - now as HTTP/3 request
|
|
69
|
-
request8 = Quicsilver::HTTP3::RequestEncoder.new(
|
|
70
|
-
method: 'POST',
|
|
71
|
-
path: '/upload',
|
|
72
|
-
authority: 'example.com',
|
|
73
|
-
body: "X" * 50000 # 50KB
|
|
74
|
-
)
|
|
75
|
-
client.send_data(request8.encode)
|
|
76
|
-
|
|
77
|
-
# Keep connection alive for a bit
|
|
78
|
-
puts "⏳ Connection established. Press Enter to disconnect..."
|
|
79
|
-
gets
|
|
80
|
-
|
|
81
|
-
rescue Quicsilver::ConnectionError => e
|
|
82
|
-
puts "❌ Connection failed: #{e.message}"
|
|
83
|
-
rescue Quicsilver::TimeoutError => e
|
|
84
|
-
puts "⏰ Connection timed out: #{e.message}"
|
|
85
|
-
ensure
|
|
86
|
-
puts "🔌 Disconnecting..."
|
|
87
|
-
client.disconnect
|
|
88
|
-
puts "👋 Disconnected"
|
|
89
|
-
end
|
data/lib/quicsilver/client.rb
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Quicsilver
|
|
4
|
-
class Client
|
|
5
|
-
attr_reader :hostname, :port, :unsecure, :connection_timeout
|
|
6
|
-
|
|
7
|
-
def initialize(hostname, port = 4433, options = {})
|
|
8
|
-
@hostname = hostname
|
|
9
|
-
@port = port
|
|
10
|
-
@unsecure = options[:unsecure] || true
|
|
11
|
-
@connection_timeout = options[:connection_timeout] || 5000
|
|
12
|
-
|
|
13
|
-
@connection_data = nil
|
|
14
|
-
@connected = false
|
|
15
|
-
@connection_start_time = nil
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def connect
|
|
19
|
-
raise Error, "Already connected" if @connected
|
|
20
|
-
|
|
21
|
-
# Initialize MSQUIC if not already done
|
|
22
|
-
result = Quicsilver.open_connection
|
|
23
|
-
|
|
24
|
-
# Create configuration
|
|
25
|
-
config = Quicsilver.create_configuration(@unsecure)
|
|
26
|
-
raise ConnectionError, "Failed to create configuration" if config.nil?
|
|
27
|
-
|
|
28
|
-
# Create connection (returns [handle, context])
|
|
29
|
-
@connection_data = Quicsilver.create_connection
|
|
30
|
-
raise ConnectionError, "Failed to create connection" if @connection_data.nil?
|
|
31
|
-
|
|
32
|
-
connection_handle = @connection_data[0]
|
|
33
|
-
context_handle = @connection_data[1]
|
|
34
|
-
|
|
35
|
-
# Start the connection
|
|
36
|
-
success = Quicsilver.start_connection(connection_handle, config, @hostname, @port)
|
|
37
|
-
unless success
|
|
38
|
-
Quicsilver.close_configuration(config)
|
|
39
|
-
cleanup_failed_connection
|
|
40
|
-
raise ConnectionError, "Failed to start connection"
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# Wait for connection to establish or fail
|
|
44
|
-
result = Quicsilver.wait_for_connection(context_handle, @connection_timeout)
|
|
45
|
-
|
|
46
|
-
if result.key?("error")
|
|
47
|
-
error_status = result["status"]
|
|
48
|
-
error_code = result["code"]
|
|
49
|
-
Quicsilver.close_configuration(config)
|
|
50
|
-
cleanup_failed_connection
|
|
51
|
-
error_msg = "Connection failed with status: 0x#{error_status.to_s(16)}, code: #{error_code}"
|
|
52
|
-
raise ConnectionError, error_msg
|
|
53
|
-
elsif result.key?("timeout")
|
|
54
|
-
Quicsilver.close_configuration(config)
|
|
55
|
-
cleanup_failed_connection
|
|
56
|
-
error_msg = "Connection timed out after #{@connection_timeout}ms"
|
|
57
|
-
raise TimeoutError, error_msg
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
@connected = true
|
|
61
|
-
@connection_start_time = Time.now
|
|
62
|
-
|
|
63
|
-
send_control_stream
|
|
64
|
-
|
|
65
|
-
# Clean up config since connection is established
|
|
66
|
-
Quicsilver.close_configuration(config)
|
|
67
|
-
rescue => e
|
|
68
|
-
cleanup_failed_connection
|
|
69
|
-
|
|
70
|
-
if e.is_a?(ConnectionError) || e.is_a?(TimeoutError)
|
|
71
|
-
raise e
|
|
72
|
-
else
|
|
73
|
-
raise ConnectionError, "Connection failed: #{e.message}"
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def disconnect
|
|
78
|
-
return unless @connected || @connection_data
|
|
79
|
-
|
|
80
|
-
begin
|
|
81
|
-
if @connection_data
|
|
82
|
-
Quicsilver.close_connection_handle(@connection_data)
|
|
83
|
-
@connection_data = nil
|
|
84
|
-
end
|
|
85
|
-
rescue
|
|
86
|
-
# Ignore disconnect errors
|
|
87
|
-
ensure
|
|
88
|
-
@connected = false
|
|
89
|
-
@connection_start_time = nil
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def connected?
|
|
94
|
-
return false unless @connected && @connection_data
|
|
95
|
-
|
|
96
|
-
# Get connection status from the C extension
|
|
97
|
-
context_handle = @connection_data[1]
|
|
98
|
-
info = Quicsilver.connection_status(context_handle)
|
|
99
|
-
if info && info.key?("connected")
|
|
100
|
-
is_connected = info["connected"] && !info["failed"]
|
|
101
|
-
|
|
102
|
-
# If C extension says we're disconnected, update our state
|
|
103
|
-
if !is_connected && @connected
|
|
104
|
-
@connected = false
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
is_connected
|
|
108
|
-
else
|
|
109
|
-
# If we can't get status, assume disconnected
|
|
110
|
-
if @connected
|
|
111
|
-
@connected = false
|
|
112
|
-
end
|
|
113
|
-
false
|
|
114
|
-
end
|
|
115
|
-
rescue
|
|
116
|
-
# If there's an error checking status, assume disconnected
|
|
117
|
-
if @connected
|
|
118
|
-
@connected = false
|
|
119
|
-
end
|
|
120
|
-
false
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def connection_info
|
|
124
|
-
base_info = if @connection_data
|
|
125
|
-
begin
|
|
126
|
-
context_handle = @connection_data[1]
|
|
127
|
-
Quicsilver.connection_status(context_handle) || {}
|
|
128
|
-
rescue
|
|
129
|
-
{}
|
|
130
|
-
end
|
|
131
|
-
else
|
|
132
|
-
{}
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
base_info.merge({
|
|
136
|
-
hostname: @hostname,
|
|
137
|
-
port: @port,
|
|
138
|
-
uptime: connection_uptime
|
|
139
|
-
})
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def connection_uptime
|
|
143
|
-
return 0 unless @connection_start_time
|
|
144
|
-
Time.now - @connection_start_time
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def send_data(data)
|
|
148
|
-
raise Error, "Not connected" unless @connected
|
|
149
|
-
|
|
150
|
-
stream = open_stream
|
|
151
|
-
unless stream
|
|
152
|
-
puts "❌ Failed to open stream"
|
|
153
|
-
return false
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
result = Quicsilver.send_stream(stream, data, true)
|
|
157
|
-
puts "✅ Sent #{data.bytesize} bytes"
|
|
158
|
-
result
|
|
159
|
-
rescue => e
|
|
160
|
-
puts "❌ Send data error: #{e.class} - #{e.message}"
|
|
161
|
-
false
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
private
|
|
165
|
-
|
|
166
|
-
def cleanup_failed_connection
|
|
167
|
-
Quicsilver.close_connection_handle(@connection_data) if @connection_data
|
|
168
|
-
@connection_data = nil
|
|
169
|
-
@connected = false
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def open_stream
|
|
173
|
-
Quicsilver.open_stream(@connection_data[0], false)
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def open_unidirectional_stream
|
|
177
|
-
Quicsilver.open_stream(@connection_data[0], true)
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
def send_control_stream
|
|
181
|
-
# Open unidirectional stream
|
|
182
|
-
stream = open_unidirectional_stream
|
|
183
|
-
|
|
184
|
-
# Build and send control stream data
|
|
185
|
-
control_data = Quicsilver::HTTP3.build_control_stream
|
|
186
|
-
Quicsilver.send_stream(stream, control_data, false)
|
|
187
|
-
|
|
188
|
-
@control_stream = stream
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
end
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Quicsilver
|
|
4
|
-
module HTTP3
|
|
5
|
-
class RequestEncoder
|
|
6
|
-
def initialize(method:, path:, scheme: 'https', authority: 'localhost:4433', headers: {}, body: nil)
|
|
7
|
-
@method = method.upcase
|
|
8
|
-
@path = path
|
|
9
|
-
@scheme = scheme
|
|
10
|
-
@authority = authority
|
|
11
|
-
@headers = headers
|
|
12
|
-
@body = body
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def encode
|
|
16
|
-
frames = []
|
|
17
|
-
|
|
18
|
-
# Build HEADERS frame
|
|
19
|
-
headers_payload = encode_headers
|
|
20
|
-
frames << build_frame(0x01, headers_payload) # 0x01 = HEADERS frame
|
|
21
|
-
|
|
22
|
-
# Build DATA frame if body present
|
|
23
|
-
if @body && !@body.empty?
|
|
24
|
-
body_data = @body.is_a?(String) ? @body : @body.join
|
|
25
|
-
frames << build_frame(0x00, body_data) # 0x00 = DATA frame
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
frames.join.force_encoding(Encoding::BINARY)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
private
|
|
32
|
-
|
|
33
|
-
def build_frame(type, payload)
|
|
34
|
-
frame_type = HTTP3.encode_varint(type)
|
|
35
|
-
frame_length = HTTP3.encode_varint(payload.bytesize)
|
|
36
|
-
frame_type + frame_length + payload
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def encode_headers
|
|
40
|
-
payload = "".b
|
|
41
|
-
|
|
42
|
-
# QPACK prefix: Required Insert Count = 0, Delta Base = 0
|
|
43
|
-
payload += "\x00\x00".b
|
|
44
|
-
|
|
45
|
-
# Encode pseudo-headers using Indexed Field Line with Post-Base Index
|
|
46
|
-
# Pattern: 0x50 (0101 0000) for :method, :scheme
|
|
47
|
-
# Pattern: 0x40 | index for :authority, :path (literal name, literal value)
|
|
48
|
-
|
|
49
|
-
# :method (use literal since GET/POST have specific indices but we want flexibility)
|
|
50
|
-
payload += encode_literal_pseudo_header(':method', @method)
|
|
51
|
-
|
|
52
|
-
# :scheme
|
|
53
|
-
payload += encode_literal_pseudo_header(':scheme', @scheme)
|
|
54
|
-
|
|
55
|
-
# :authority
|
|
56
|
-
payload += encode_literal_pseudo_header(':authority', @authority)
|
|
57
|
-
|
|
58
|
-
# :path
|
|
59
|
-
payload += encode_literal_pseudo_header(':path', @path)
|
|
60
|
-
|
|
61
|
-
# Encode regular headers
|
|
62
|
-
@headers.each do |name, value|
|
|
63
|
-
payload += encode_literal_header(name.to_s.downcase, value.to_s)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
payload
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Literal field line with literal name for pseudo-headers
|
|
70
|
-
# Pattern: 0x50 (indexed name from static table) + value
|
|
71
|
-
def encode_literal_pseudo_header(name, value)
|
|
72
|
-
# For pseudo-headers, use indexed name reference from static table
|
|
73
|
-
# with literal value (pattern: 0101xxxx where xxxx = static table index)
|
|
74
|
-
static_index = case name
|
|
75
|
-
when ':authority' then 0
|
|
76
|
-
when ':path' then 1
|
|
77
|
-
when ':method' then (@method == 'GET' ? 17 : 20) # GET=17, POST=20
|
|
78
|
-
when ':scheme' then (@scheme == 'http' ? 22 : 23)
|
|
79
|
-
else nil
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
if static_index
|
|
83
|
-
# Use indexed field line (0x40 | index)
|
|
84
|
-
result = "".b
|
|
85
|
-
result += [0x40 | static_index].pack('C')
|
|
86
|
-
# For non-exact matches, append literal value
|
|
87
|
-
if name == ':authority' || name == ':path'
|
|
88
|
-
result += HTTP3.encode_varint(value.bytesize)
|
|
89
|
-
result += value.to_s.b
|
|
90
|
-
end
|
|
91
|
-
result
|
|
92
|
-
else
|
|
93
|
-
# Fallback to literal name
|
|
94
|
-
encode_literal_header(name, value)
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
# Literal field line with literal name
|
|
99
|
-
# Pattern: 0x20 | name_length, name_bytes, value_length, value_bytes
|
|
100
|
-
def encode_literal_header(name, value)
|
|
101
|
-
result = "".b
|
|
102
|
-
# 0x20 = literal with literal name (no indexing)
|
|
103
|
-
name_len = name.bytesize
|
|
104
|
-
result += [0x20 | (name_len & 0x1F)].pack('C')
|
|
105
|
-
result += name.b
|
|
106
|
-
result += HTTP3.encode_varint(value.bytesize)
|
|
107
|
-
result += value.to_s.b
|
|
108
|
-
result
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
end
|