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,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "quicsilver"
|
|
4
|
+
require "rackup/handler"
|
|
5
|
+
require "localhost"
|
|
6
|
+
|
|
7
|
+
module Quicsilver
|
|
8
|
+
module RackHandler
|
|
9
|
+
DEFAULT_OPTIONS = {
|
|
10
|
+
Host: "0.0.0.0",
|
|
11
|
+
Port: 4433,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
def self.run(app, **options)
|
|
15
|
+
normalized_options = {
|
|
16
|
+
host: options[:Host] || options[:host] || DEFAULT_OPTIONS[:Host],
|
|
17
|
+
port: (options[:Port] || options[:port] || DEFAULT_OPTIONS[:Port]).to_i,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
cert_file = options[:cert_file]
|
|
21
|
+
key_file = options[:key_file]
|
|
22
|
+
|
|
23
|
+
if cert_file.nil? && key_file.nil?
|
|
24
|
+
env = options[:environment] || ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
|
|
25
|
+
|
|
26
|
+
if env == 'production'
|
|
27
|
+
raise ArgumentError, "cert_file and key_file are required in production"
|
|
28
|
+
else
|
|
29
|
+
require 'localhost/authority'
|
|
30
|
+
authority = Localhost::Authority.fetch
|
|
31
|
+
cert_file = authority.certificate_path
|
|
32
|
+
key_file = authority.key_path
|
|
33
|
+
Quicsilver.logger.info("Using auto-generated certificates for localhost")
|
|
34
|
+
Quicsilver.logger.info(" Cert: #{cert_file}")
|
|
35
|
+
Quicsilver.logger.info(" Key: #{key_file}")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
config = ::Quicsilver::ServerConfiguration.new(cert_file, key_file)
|
|
40
|
+
|
|
41
|
+
server = ::Quicsilver::Server.new(
|
|
42
|
+
normalized_options[:port],
|
|
43
|
+
address: normalized_options[:host],
|
|
44
|
+
app: app,
|
|
45
|
+
server_configuration: config
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
yield server if block_given?
|
|
49
|
+
|
|
50
|
+
server.start
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.valid_options
|
|
54
|
+
{
|
|
55
|
+
"Host=HOST" => "Hostname to listen on (default: 0.0.0.0)",
|
|
56
|
+
"Port=PORT" => "Port to listen on (default: 4433)",
|
|
57
|
+
"cert_file=PATH" => "Path to TLS certificate file (required)",
|
|
58
|
+
"key_file=PATH" => "Path to TLS key file (required)"
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
module Rackup
|
|
66
|
+
module Handler
|
|
67
|
+
module Quicsilver
|
|
68
|
+
def self.run(app, **options, &block)
|
|
69
|
+
::Quicsilver::RackHandler.run(app, **options, &block)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.valid_options
|
|
73
|
+
::Quicsilver::RackHandler.valid_options
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
register :quicsilver, Quicsilver
|
|
77
|
+
end
|
|
78
|
+
end
|
data/quicsilver.gemspec
CHANGED
|
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.authors = ["Haroon Ahmed"]
|
|
9
9
|
spec.email = ["haroon.ahmed25@gmail.com"]
|
|
10
10
|
|
|
11
|
-
spec.summary = %q{
|
|
12
|
-
spec.description = %q{
|
|
11
|
+
spec.summary = %q{HTTP/3 server implementation for Ruby}
|
|
12
|
+
spec.description = %q{HTTP/3 server implementation for Ruby}
|
|
13
13
|
spec.homepage = "https://github.com/hahmed/quicsilver"
|
|
14
14
|
|
|
15
15
|
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
|
@@ -33,6 +33,7 @@ Gem::Specification.new do |spec|
|
|
|
33
33
|
spec.bindir = "exe"
|
|
34
34
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
35
35
|
spec.require_paths = ["lib"]
|
|
36
|
+
spec.required_ruby_version = ">= 3.2.0"
|
|
36
37
|
|
|
37
38
|
spec.extensions = ['ext/quicsilver/extconf.rb']
|
|
38
39
|
|
|
@@ -41,4 +42,8 @@ Gem::Specification.new do |spec|
|
|
|
41
42
|
spec.add_development_dependency 'rake-compiler', '~> 1.2'
|
|
42
43
|
spec.add_development_dependency 'rake-compiler-dock', '~> 1.3'
|
|
43
44
|
spec.add_development_dependency "minitest", "~> 5.0"
|
|
45
|
+
spec.add_development_dependency "minitest-focus", "~> 1.3"
|
|
46
|
+
spec.add_dependency "localhost", "~> 1.6"
|
|
47
|
+
spec.add_dependency "rack", "~> 3.0"
|
|
48
|
+
spec.add_dependency "rackup", "~> 2.0"
|
|
44
49
|
end
|
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.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Haroon Ahmed
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2025-
|
|
10
|
+
date: 2025-12-17 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: bundler
|
|
@@ -79,8 +79,63 @@ 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: localhost
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '1.6'
|
|
103
|
+
type: :runtime
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '1.6'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: rack
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '3.0'
|
|
117
|
+
type: :runtime
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '3.0'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: rackup
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - "~>"
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '2.0'
|
|
131
|
+
type: :runtime
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - "~>"
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '2.0'
|
|
138
|
+
description: HTTP/3 server implementation for Ruby
|
|
84
139
|
email:
|
|
85
140
|
- haroon.ahmed25@gmail.com
|
|
86
141
|
executables: []
|
|
@@ -88,33 +143,43 @@ extensions:
|
|
|
88
143
|
- ext/quicsilver/extconf.rb
|
|
89
144
|
extra_rdoc_files: []
|
|
90
145
|
files:
|
|
146
|
+
- ".github/workflows/ci.yml"
|
|
91
147
|
- ".gitignore"
|
|
92
148
|
- ".gitmodules"
|
|
93
149
|
- ".ruby-version"
|
|
94
150
|
- CHANGELOG.md
|
|
95
151
|
- Gemfile
|
|
96
152
|
- Gemfile.lock
|
|
153
|
+
- LICENSE
|
|
97
154
|
- README.md
|
|
98
155
|
- Rakefile
|
|
156
|
+
- benchmarks/benchmark.rb
|
|
157
|
+
- benchmarks/quicsilver_server.rb
|
|
99
158
|
- bin/console
|
|
100
159
|
- bin/setup
|
|
101
160
|
- examples/README.md
|
|
102
|
-
- examples/minimal_http3_client.rb
|
|
103
161
|
- examples/minimal_http3_server.rb
|
|
104
162
|
- examples/rack_http3_server.rb
|
|
105
163
|
- examples/setup_certs.sh
|
|
164
|
+
- examples/simple_client_test.rb
|
|
106
165
|
- ext/quicsilver/extconf.rb
|
|
107
166
|
- ext/quicsilver/quicsilver.c
|
|
108
167
|
- lib/quicsilver.rb
|
|
109
168
|
- lib/quicsilver/client.rb
|
|
169
|
+
- lib/quicsilver/connection.rb
|
|
170
|
+
- lib/quicsilver/event_loop.rb
|
|
110
171
|
- lib/quicsilver/http3.rb
|
|
111
172
|
- lib/quicsilver/http3/request_encoder.rb
|
|
112
173
|
- lib/quicsilver/http3/request_parser.rb
|
|
113
174
|
- lib/quicsilver/http3/response_encoder.rb
|
|
175
|
+
- lib/quicsilver/http3/response_parser.rb
|
|
114
176
|
- lib/quicsilver/listener_data.rb
|
|
177
|
+
- lib/quicsilver/quic_stream.rb
|
|
178
|
+
- lib/quicsilver/request_registry.rb
|
|
115
179
|
- lib/quicsilver/server.rb
|
|
116
180
|
- lib/quicsilver/server_configuration.rb
|
|
117
181
|
- lib/quicsilver/version.rb
|
|
182
|
+
- lib/rackup/handler/quicsilver.rb
|
|
118
183
|
- quicsilver.gemspec
|
|
119
184
|
homepage: https://github.com/hahmed/quicsilver
|
|
120
185
|
licenses: []
|
|
@@ -130,7 +195,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
130
195
|
requirements:
|
|
131
196
|
- - ">="
|
|
132
197
|
- !ruby/object:Gem::Version
|
|
133
|
-
version:
|
|
198
|
+
version: 3.2.0
|
|
134
199
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
135
200
|
requirements:
|
|
136
201
|
- - ">="
|
|
@@ -139,5 +204,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
139
204
|
requirements: []
|
|
140
205
|
rubygems_version: 3.6.2
|
|
141
206
|
specification_version: 4
|
|
142
|
-
summary:
|
|
207
|
+
summary: HTTP/3 server implementation for Ruby
|
|
143
208
|
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
|