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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +41 -0
  3. data/.gitignore +3 -1
  4. data/CHANGELOG.md +76 -5
  5. data/Gemfile.lock +18 -4
  6. data/LICENSE +21 -0
  7. data/README.md +33 -53
  8. data/Rakefile +29 -2
  9. data/benchmarks/components.rb +191 -0
  10. data/benchmarks/concurrent.rb +110 -0
  11. data/benchmarks/helpers.rb +88 -0
  12. data/benchmarks/quicsilver_server.rb +46 -0
  13. data/benchmarks/rails.rb +170 -0
  14. data/benchmarks/throughput.rb +113 -0
  15. data/examples/minimal_http3_server.rb +0 -6
  16. data/examples/rack_http3_server.rb +0 -6
  17. data/examples/simple_client_test.rb +26 -0
  18. data/ext/quicsilver/quicsilver.c +615 -138
  19. data/lib/quicsilver/client/client.rb +250 -0
  20. data/lib/quicsilver/client/request.rb +98 -0
  21. data/lib/quicsilver/protocol/frames.rb +327 -0
  22. data/lib/quicsilver/protocol/qpack/decoder.rb +165 -0
  23. data/lib/quicsilver/protocol/qpack/encoder.rb +189 -0
  24. data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +125 -0
  25. data/lib/quicsilver/protocol/qpack/huffman.rb +459 -0
  26. data/lib/quicsilver/protocol/request_encoder.rb +47 -0
  27. data/lib/quicsilver/protocol/request_parser.rb +387 -0
  28. data/lib/quicsilver/protocol/response_encoder.rb +72 -0
  29. data/lib/quicsilver/protocol/response_parser.rb +249 -0
  30. data/lib/quicsilver/server/listener_data.rb +14 -0
  31. data/lib/quicsilver/server/request_handler.rb +86 -0
  32. data/lib/quicsilver/server/request_registry.rb +50 -0
  33. data/lib/quicsilver/server/server.rb +336 -0
  34. data/lib/quicsilver/transport/configuration.rb +132 -0
  35. data/lib/quicsilver/transport/connection.rb +350 -0
  36. data/lib/quicsilver/transport/event_loop.rb +38 -0
  37. data/lib/quicsilver/transport/inbound_stream.rb +33 -0
  38. data/lib/quicsilver/transport/stream.rb +28 -0
  39. data/lib/quicsilver/transport/stream_event.rb +26 -0
  40. data/lib/quicsilver/version.rb +1 -1
  41. data/lib/quicsilver.rb +49 -9
  42. data/lib/rackup/handler/quicsilver.rb +77 -0
  43. data/quicsilver.gemspec +10 -3
  44. metadata +122 -17
  45. data/examples/minimal_http3_client.rb +0 -89
  46. data/lib/quicsilver/client.rb +0 -191
  47. data/lib/quicsilver/http3/request_encoder.rb +0 -112
  48. data/lib/quicsilver/http3/request_parser.rb +0 -158
  49. data/lib/quicsilver/http3/response_encoder.rb +0 -73
  50. data/lib/quicsilver/http3.rb +0 -68
  51. data/lib/quicsilver/listener_data.rb +0 -29
  52. data/lib/quicsilver/server.rb +0 -258
  53. 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.1.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: 2025-10-28 00:00:00.000000000 Z
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: '10.0'
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: '10.0'
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
- description: A minimal HTTP/3 server implementation for Ruby using Microsoft's MSQUIC
83
- library.
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/http3.rb
111
- - lib/quicsilver/http3/request_encoder.rb
112
- - lib/quicsilver/http3/request_parser.rb
113
- - lib/quicsilver/http3/response_encoder.rb
114
- - lib/quicsilver/listener_data.rb
115
- - lib/quicsilver/server.rb
116
- - lib/quicsilver/server_configuration.rb
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: '0'
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: Minimal HTTP/3 server implementation for Ruby
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
@@ -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