quicsilver 0.1.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.
@@ -0,0 +1,258 @@
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
@@ -0,0 +1,49 @@
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
@@ -0,0 +1,3 @@
1
+ module Quicsilver
2
+ VERSION = "0.1.0"
3
+ end
data/lib/quicsilver.rb ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "quicsilver/version"
4
+ require_relative "quicsilver/client"
5
+ require_relative "quicsilver/listener_data"
6
+ require_relative "quicsilver/server"
7
+ require_relative "quicsilver/server_configuration"
8
+ require_relative "quicsilver/http3"
9
+ require_relative "quicsilver/http3/request_parser"
10
+ require_relative "quicsilver/http3/request_encoder"
11
+ require_relative "quicsilver/http3/response_encoder"
12
+ require_relative "quicsilver/quicsilver"
13
+
14
+ module Quicsilver
15
+ class Error < StandardError; end
16
+ class ServerIsRunningError < Error; end
17
+ class ServerConfigurationError < Error; end
18
+ class ServerListenerError < Error; end
19
+ class ServerError < Error; end
20
+ class ConnectionError < Error; end
21
+ class TimeoutError < Error; end
22
+ end
@@ -0,0 +1,44 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "quicsilver/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "quicsilver"
7
+ spec.version = Quicsilver::VERSION
8
+ spec.authors = ["Haroon Ahmed"]
9
+ spec.email = ["haroon.ahmed25@gmail.com"]
10
+
11
+ spec.summary = %q{Minimal HTTP/3 server implementation for Ruby}
12
+ spec.description = %q{A minimal HTTP/3 server implementation for Ruby using Microsoft's MSQUIC library.}
13
+ spec.homepage = "https://github.com/hahmed/quicsilver"
14
+
15
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
16
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
17
+ if spec.respond_to?(:metadata)
18
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
19
+
20
+ spec.metadata["homepage_uri"] = spec.homepage
21
+ spec.metadata["source_code_uri"] = "https://github.com/hahmed/quicsilver"
22
+ spec.metadata["changelog_uri"] = "https://github.com/hahmed/quicsilver/blob/main/CHANGELOG.md"
23
+ else
24
+ raise "RubyGems 2.0 or newer is required to protect against " \
25
+ "public gem pushes."
26
+ end
27
+
28
+ # Specify which files should be added to the gem when it is released.
29
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
30
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
31
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
32
+ end
33
+ spec.bindir = "exe"
34
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
35
+ spec.require_paths = ["lib"]
36
+
37
+ spec.extensions = ['ext/quicsilver/extconf.rb']
38
+
39
+ spec.add_development_dependency "bundler", "~> 2.0"
40
+ spec.add_development_dependency "rake", "~> 10.0"
41
+ spec.add_development_dependency 'rake-compiler', '~> 1.2'
42
+ spec.add_development_dependency 'rake-compiler-dock', '~> 1.3'
43
+ spec.add_development_dependency "minitest", "~> 5.0"
44
+ end
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: quicsilver
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Haroon Ahmed
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2025-10-28 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: bundler
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '10.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '10.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake-compiler
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.2'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.2'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rake-compiler-dock
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.3'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.3'
68
+ - !ruby/object:Gem::Dependency
69
+ name: minitest
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '5.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '5.0'
82
+ description: A minimal HTTP/3 server implementation for Ruby using Microsoft's MSQUIC
83
+ library.
84
+ email:
85
+ - haroon.ahmed25@gmail.com
86
+ executables: []
87
+ extensions:
88
+ - ext/quicsilver/extconf.rb
89
+ extra_rdoc_files: []
90
+ files:
91
+ - ".gitignore"
92
+ - ".gitmodules"
93
+ - ".ruby-version"
94
+ - CHANGELOG.md
95
+ - Gemfile
96
+ - Gemfile.lock
97
+ - README.md
98
+ - Rakefile
99
+ - bin/console
100
+ - bin/setup
101
+ - examples/README.md
102
+ - examples/minimal_http3_client.rb
103
+ - examples/minimal_http3_server.rb
104
+ - examples/rack_http3_server.rb
105
+ - examples/setup_certs.sh
106
+ - ext/quicsilver/extconf.rb
107
+ - ext/quicsilver/quicsilver.c
108
+ - 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
117
+ - lib/quicsilver/version.rb
118
+ - quicsilver.gemspec
119
+ homepage: https://github.com/hahmed/quicsilver
120
+ licenses: []
121
+ metadata:
122
+ allowed_push_host: https://rubygems.org
123
+ homepage_uri: https://github.com/hahmed/quicsilver
124
+ source_code_uri: https://github.com/hahmed/quicsilver
125
+ changelog_uri: https://github.com/hahmed/quicsilver/blob/main/CHANGELOG.md
126
+ rdoc_options: []
127
+ require_paths:
128
+ - lib
129
+ required_ruby_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ requirements: []
140
+ rubygems_version: 3.6.2
141
+ specification_version: 4
142
+ summary: Minimal HTTP/3 server implementation for Ruby
143
+ test_files: []