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
@@ -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
@@ -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
@@ -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