quicsilver 0.5.0-x86_64-linux

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 (107) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +49 -0
  3. data/.github/workflows/cibuildgem.yaml +117 -0
  4. data/.gitignore +21 -0
  5. data/.gitmodules +3 -0
  6. data/.ruby-version +1 -0
  7. data/CHANGELOG.md +175 -0
  8. data/Gemfile +6 -0
  9. data/Gemfile.lock +67 -0
  10. data/LICENSE +21 -0
  11. data/README.md +261 -0
  12. data/Rakefile +130 -0
  13. data/benchmarks/baseline.rb +175 -0
  14. data/benchmarks/compare.rb +398 -0
  15. data/benchmarks/components.rb +191 -0
  16. data/benchmarks/helpers.rb +162 -0
  17. data/benchmarks/latency.rb +128 -0
  18. data/benchmarks/loss.rb +193 -0
  19. data/benchmarks/mobile.rb +125 -0
  20. data/bin/console +14 -0
  21. data/bin/setup +8 -0
  22. data/docs/connection_identity.md +141 -0
  23. data/examples/README.md +58 -0
  24. data/examples/benchmark.rb +191 -0
  25. data/examples/connection_pool_demo.rb +47 -0
  26. data/examples/example_helper.rb +69 -0
  27. data/examples/falcon_middleware.rb +44 -0
  28. data/examples/feature_demo.rb +125 -0
  29. data/examples/grpc_style.rb +97 -0
  30. data/examples/latency_aware_routing.rb +382 -0
  31. data/examples/lifecycle_drain.rb +264 -0
  32. data/examples/minimal_http3_server.rb +14 -0
  33. data/examples/priorities.rb +60 -0
  34. data/examples/protocol_http_server.rb +31 -0
  35. data/examples/rack_http3_server.rb +42 -0
  36. data/examples/rails_feature_test.rb +280 -0
  37. data/examples/server_stats.rb +117 -0
  38. data/examples/simple_client_test.rb +26 -0
  39. data/examples/streaming_sse.rb +33 -0
  40. data/examples/trailers.rb +69 -0
  41. data/examples/webtransport_smoke.rb +546 -0
  42. data/examples/worker_load_balancing.rb +425 -0
  43. data/ext/quicsilver/extconf.rb +124 -0
  44. data/ext/quicsilver/include/msquic.h +2226 -0
  45. data/ext/quicsilver/include/msquic_posix.h +696 -0
  46. data/ext/quicsilver/include/quic_sal_stub.h +314 -0
  47. data/ext/quicsilver/msquic_patch.rb +68 -0
  48. data/ext/quicsilver/patches/reliable-reset.patch +723 -0
  49. data/ext/quicsilver/patches/stream-final-size.patch +47 -0
  50. data/ext/quicsilver/quicsilver.c +2741 -0
  51. data/lib/quicsilver/3.4/quicsilver.so +0 -0
  52. data/lib/quicsilver/4.0/quicsilver.so +0 -0
  53. data/lib/quicsilver/client/client.rb +708 -0
  54. data/lib/quicsilver/client/connection_pool.rb +202 -0
  55. data/lib/quicsilver/client/request.rb +190 -0
  56. data/lib/quicsilver/errors.rb +39 -0
  57. data/lib/quicsilver/libmsquic.so.2 +0 -0
  58. data/lib/quicsilver/protocol/adapter.rb +196 -0
  59. data/lib/quicsilver/protocol/capsule.rb +38 -0
  60. data/lib/quicsilver/protocol/connect_stream_decoder.rb +75 -0
  61. data/lib/quicsilver/protocol/control_stream_parser.rb +117 -0
  62. data/lib/quicsilver/protocol/datagram.rb +40 -0
  63. data/lib/quicsilver/protocol/frame_parser.rb +141 -0
  64. data/lib/quicsilver/protocol/frame_reader.rb +84 -0
  65. data/lib/quicsilver/protocol/frames.rb +454 -0
  66. data/lib/quicsilver/protocol/priority.rb +65 -0
  67. data/lib/quicsilver/protocol/qpack/decoder.rb +127 -0
  68. data/lib/quicsilver/protocol/qpack/encoder.rb +217 -0
  69. data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +140 -0
  70. data/lib/quicsilver/protocol/qpack/huffman.rb +459 -0
  71. data/lib/quicsilver/protocol/request_encoder.rb +50 -0
  72. data/lib/quicsilver/protocol/request_parser.rb +289 -0
  73. data/lib/quicsilver/protocol/response_encoder.rb +107 -0
  74. data/lib/quicsilver/protocol/response_parser.rb +147 -0
  75. data/lib/quicsilver/protocol/stream_input.rb +98 -0
  76. data/lib/quicsilver/protocol/stream_output.rb +59 -0
  77. data/lib/quicsilver/protocol/unidirectional_stream.rb +12 -0
  78. data/lib/quicsilver/protocol/web_transport.rb +214 -0
  79. data/lib/quicsilver/rack/context.rb +33 -0
  80. data/lib/quicsilver/response.rb +40 -0
  81. data/lib/quicsilver/server/listener_data.rb +14 -0
  82. data/lib/quicsilver/server/rack_adapter.rb +77 -0
  83. data/lib/quicsilver/server/receive_queue.rb +106 -0
  84. data/lib/quicsilver/server/request_handler.rb +149 -0
  85. data/lib/quicsilver/server/request_registry.rb +63 -0
  86. data/lib/quicsilver/server/scheduler.rb +50 -0
  87. data/lib/quicsilver/server/schedulers/fiber_scheduler.rb +79 -0
  88. data/lib/quicsilver/server/schedulers/thread_scheduler.rb +76 -0
  89. data/lib/quicsilver/server/server.rb +1080 -0
  90. data/lib/quicsilver/server/web_transport_flow_control.rb +270 -0
  91. data/lib/quicsilver/server/web_transport_manager.rb +281 -0
  92. data/lib/quicsilver/server/web_transport_registry.rb +40 -0
  93. data/lib/quicsilver/server/web_transport_session.rb +812 -0
  94. data/lib/quicsilver/server/web_transport_stream.rb +326 -0
  95. data/lib/quicsilver/transport/configuration.rb +234 -0
  96. data/lib/quicsilver/transport/connection.rb +594 -0
  97. data/lib/quicsilver/transport/connection_stats.rb +53 -0
  98. data/lib/quicsilver/transport/event_loop.rb +74 -0
  99. data/lib/quicsilver/transport/inbound_stream.rb +53 -0
  100. data/lib/quicsilver/transport/stream.rb +43 -0
  101. data/lib/quicsilver/transport/stream_event.rb +49 -0
  102. data/lib/quicsilver/transport/stream_id.rb +26 -0
  103. data/lib/quicsilver/version.rb +3 -0
  104. data/lib/quicsilver.rb +96 -0
  105. data/lib/rackup/handler/quicsilver.rb +96 -0
  106. data/quicsilver.gemspec +66 -0
  107. metadata +362 -0
@@ -0,0 +1,326 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "protocol/http/body/writable"
4
+ require_relative "receive_queue"
5
+
6
+ module Quicsilver
7
+ class Server
8
+ # A reliable bidirectional stream within a WebTransport session.
9
+ #
10
+ # Streams are opened by either the client or server. Data is reliable
11
+ # and ordered — unlike datagrams, nothing is dropped.
12
+ #
13
+ # Usage:
14
+ # session.on_stream do |stream|
15
+ # Thread.new do
16
+ # while (data = stream.read)
17
+ # stream.write("echo: #{data}")
18
+ # end
19
+ # stream.close_write
20
+ # end
21
+ # end
22
+ #
23
+ # # Server-initiated:
24
+ # stream = session.open_stream
25
+ # stream.write("server push")
26
+ # stream.close
27
+ #
28
+ class WebTransportStream
29
+ class ResetError < Quicsilver::Error
30
+ attr_reader :http_error_code, :application_error_code
31
+
32
+ def initialize(http_error_code)
33
+ @http_error_code = http_error_code
34
+ @application_error_code = Protocol::WebTransport.http_to_application_error(http_error_code)
35
+ super("WebTransport stream reset (HTTP/3 code 0x#{http_error_code.to_s(16)})")
36
+ end
37
+ end
38
+
39
+ attr_reader :stream_id, :session
40
+
41
+ def self.validate_overflow_code(code)
42
+ unless code.is_a?(Integer) && code.between?(0, 0xffff_ffff)
43
+ raise ArgumentError, "Receive overflow code must be a 32-bit application error code"
44
+ end
45
+ end
46
+
47
+ def self.validate_backpressure(enabled, bytes, chunks)
48
+ unless enabled == true || enabled == false
49
+ raise ArgumentError, "Receive backpressure must be true or false"
50
+ end
51
+ if enabled && [bytes, chunks].any? { |limit| limit > 0xffff_ffff_ffff_ffff }
52
+ raise ArgumentError, "Receive backpressure limits must fit in 64 bits"
53
+ end
54
+ end
55
+
56
+ # The receive policy has one home: WebTransportSession#accept! defines
57
+ # the defaults and passes them in, so there is nothing to keep in sync.
58
+ def initialize(session:, stream:, stream_id:, direction: :bidi,
59
+ receive_buffer_bytes:, receive_buffer_chunks:, receive_overflow_code:,
60
+ receive_backpressure:)
61
+ @session = session
62
+ @stream = stream
63
+ @stream_id = stream_id
64
+ # Stream body bytes accepted from the peer, for session accounting.
65
+ @body_received = 0
66
+ @session_stream_closed = false
67
+ @direction = direction
68
+ @read_open = direction != :send_only
69
+ @write_open = direction != :receive_only
70
+ self.class.validate_overflow_code(receive_overflow_code)
71
+ @receive_overflow_error = Protocol::WebTransport.application_error_to_http(receive_overflow_code)
72
+ @receive_queue = ReceiveQueue.new(bytes: receive_buffer_bytes, chunks: receive_buffer_chunks)
73
+ self.class.validate_backpressure(receive_backpressure, receive_buffer_bytes, receive_buffer_chunks)
74
+ @receive_backpressure = receive_backpressure
75
+ @input = ::Protocol::HTTP::Body::Writable.new(queue: @receive_queue)
76
+ @close_callback = nil
77
+ @peer_reset_callback = nil
78
+ @peer_stop_sending_callback = nil
79
+ @close_notified = false
80
+ end
81
+
82
+ def stream_handle
83
+ @stream.handle if @stream.respond_to?(:handle)
84
+ end
85
+
86
+ def notify_start(stream_id)
87
+ @stream_id = stream_id
88
+ end
89
+
90
+ def replace_stream_handle(handle)
91
+ @stream = Transport::Stream.new(handle)
92
+ end
93
+
94
+ def write(data)
95
+ raise "Cannot write to a receive-only stream" if @direction == :receive_only
96
+ raise IOError, "Stream is closed for writing" unless @write_open
97
+
98
+ payload = data.to_s.b
99
+ # Charge the session budget before the bytes leave. Raises SendBlocked
100
+ # when the peer has not granted enough, having told it so (draft-16
101
+ # §5.6.4); nothing is sent in that case.
102
+ @session&.count_sent_data(payload.bytesize)
103
+ @stream.send(payload)
104
+ end
105
+
106
+ # Whether `bytes` can be written now. Writes are all or nothing, so a
107
+ # write past the session's remaining credit raises SendBlocked and sends
108
+ # nothing; check here to avoid that.
109
+ def writable?(bytes = 1)
110
+ return false unless @write_open && @direction != :receive_only
111
+
112
+ @session.nil? || @session.can_send?(bytes)
113
+ end
114
+
115
+ # Finish sending and discard unread input; use close_write to keep reading.
116
+ def close
117
+ close_write
118
+ ensure
119
+ discard_input
120
+ end
121
+
122
+ # One application reader; never call from the transport event loop.
123
+ def read
124
+ raise IOError, "Cannot read from a send-only stream" if @direction == :send_only
125
+
126
+ chunk = @input.read
127
+ # Reading frees session window, which is what the peer is granted more
128
+ # against (draft-16 §5.6).
129
+ @session&.count_consumed_data(chunk.bytesize) if chunk
130
+ chunk
131
+ end
132
+
133
+ def on_close(&block)
134
+ @close_callback = block
135
+ end
136
+
137
+ # The peer reset its sending side (RESET_STREAM): our read side is now
138
+ # closed and `read` raises ResetError. Writing is unaffected.
139
+ #
140
+ # Yields the application error code, or nil when the peer used a code
141
+ # outside the WebTransport range (draft-16 §4.4 — still a reset, just no
142
+ # application code to report).
143
+ def on_peer_reset(&block)
144
+ @peer_reset_callback = block
145
+ end
146
+
147
+ # The peer asked us to stop sending (STOP_SENDING): our write side is now
148
+ # closed and `write` raises IOError. Reading is unaffected.
149
+ #
150
+ # Yields the application error code on the same terms as on_peer_reset.
151
+ def on_peer_stop_sending(&block)
152
+ @peer_stop_sending_callback = block
153
+ end
154
+
155
+ # Reset this stream with an application error code.
156
+ #
157
+ # §4.4: application codes are 32-bit and share the HTTP/3 error space, so
158
+ # they are mapped into the reserved WT_APPLICATION_ERROR range.
159
+ def reset(error_code)
160
+ return unless @write_open || @read_open
161
+
162
+ abort(Protocol::WebTransport.application_error_to_http(error_code))
163
+ end
164
+
165
+ def abort(http_error_code)
166
+ @stream.abort(http_error_code)
167
+ ensure
168
+ notify_close(error: ResetError.new(http_error_code))
169
+ end
170
+
171
+ def open?
172
+ @read_open || @write_open
173
+ end
174
+
175
+ def enable_receive_backpressure
176
+ return unless @receive_backpressure && @read_open
177
+
178
+ @receive_queue.release_capacity_to do |bytes, chunks|
179
+ unless @stream.grant_receive_credit(bytes, chunks)
180
+ abort(@receive_overflow_error) if @read_open
181
+ end
182
+ end
183
+ end
184
+
185
+ # Called by Server when data arrives on this stream. :nodoc:
186
+ def receive_data(data)
187
+ return true unless @read_open
188
+
189
+ data ||= "".b
190
+ # Every byte here was sent inside a limit we advertised, so it is
191
+ # accepted (RFC 9000 §4.1). With backpressure on, the pause is that we
192
+ # stop granting native receive credit until the reader drains (§4.2);
193
+ # credit bounds every receive after the first grant, and the first is
194
+ # bounded by the initial stream window from the handshake. With
195
+ # backpressure off there is no pause, only the overflow policy below.
196
+ accepted = data.bytesize
197
+ if accepted.positive?
198
+ @input.write(data)
199
+ # Stream body only; the prefix that links this stream to its session
200
+ # is stripped before we see it and costs no credit (draft-16 §5.4).
201
+ @body_received += accepted
202
+ @session&.count_received_data(accepted)
203
+ end
204
+ return overflow! if !@receive_backpressure && @receive_queue.over_limit?
205
+
206
+ enable_receive_backpressure
207
+ true
208
+ rescue ::Protocol::HTTP::Body::Writable::Closed, ClosedQueueError, ResetError
209
+ # Closing can race the write after the read-open check above.
210
+ raise if @read_open
211
+
212
+ # The read side is already closed, so this data was not consumed and
213
+ # there is no read side left to close. Never report full consumption.
214
+ false
215
+ end
216
+
217
+ # receive_backpressure: false. The reader is too far behind and the app
218
+ # chose not to slow the peer, so ask it to stop (RFC 9000 §3.5) and close
219
+ # our read side; the write side stays open so the app can still say why.
220
+ def overflow!
221
+ @stream.stop_sending(@receive_overflow_error)
222
+ false
223
+ ensure
224
+ close_read(ResetError.new(@receive_overflow_error))
225
+ end
226
+
227
+ # A FIN only ends the read side once its data is fully consumed; under
228
+ # backpressure a deferred suffix is still outstanding. :nodoc:
229
+ def receive_fin(data)
230
+ notify_read_close if receive_data(data)
231
+ end
232
+
233
+ # Called by Server when the peer has closed its write side. :nodoc:
234
+ def notify_read_close
235
+ return unless @read_open
236
+
237
+ @read_open = false
238
+ @input.close_write
239
+ notify_close_callback
240
+ end
241
+
242
+ # RESET_STREAM ends our read side; the write side stays open. :nodoc:
243
+ def notify_peer_reset(http_error_code, final_size = nil)
244
+ error = ResetError.new(http_error_code)
245
+ begin
246
+ # The sender counted everything up to the final size, including what
247
+ # never reached us, so the session owes credit for the difference
248
+ # (§5.4).
249
+ @session&.count_reset_stream(final_size, @body_received)
250
+ close_read(error)
251
+ ensure
252
+ @peer_reset_callback&.call(error.application_error_code)
253
+ end
254
+ end
255
+
256
+ # STOP_SENDING ends our write side; MsQuic answers it, and we keep
257
+ # receiving on the read side. :nodoc:
258
+ def notify_peer_stop_sending(http_error_code)
259
+ @write_open = false
260
+ begin
261
+ notify_close_callback unless open?
262
+ ensure
263
+ @peer_stop_sending_callback&.call(Protocol::WebTransport.http_to_application_error(http_error_code))
264
+ end
265
+ end
266
+
267
+ # Called by Server when the stream is reset or fully closed. :nodoc:
268
+ #
269
+ # Under backpressure an errored close also sends STOP_SENDING. abort
270
+ # already requests a transport abort, so this is belt and braces for a
271
+ # stream with paused input; it is not known to be required. It cannot
272
+ # re-enter notify_peer_stop_sending, since our own STOP_SENDING is not
273
+ # delivered back to us as a peer event.
274
+ def notify_close(error: nil)
275
+ if error && @receive_backpressure && @read_open
276
+ @stream.stop_sending(error.http_error_code)
277
+ end
278
+ ensure
279
+ @read_open = false
280
+ @write_open = false
281
+ error ? @input.close(error) : @input.close_write
282
+ notify_close_callback
283
+ end
284
+
285
+ def close_write
286
+ return unless @write_open
287
+
288
+ @stream.send("".b, fin: true)
289
+ @write_open = false
290
+ notify_close_callback unless open?
291
+ end
292
+
293
+ private
294
+
295
+ # Close the read half and fail pending reads, leaving the write half as is.
296
+ def close_read(error)
297
+ @read_open = false
298
+ @input.close(error)
299
+ notify_close_callback unless open?
300
+ end
301
+
302
+ def discard_input
303
+ @stream.stop_sending(Protocol::WebTransport.application_error_to_http(0)) if @read_open
304
+ ensure
305
+ @read_open = false
306
+ @input.close
307
+ notify_close_callback
308
+ end
309
+
310
+ def notify_session_stream_closed
311
+ return if @session_stream_closed
312
+
313
+ @session_stream_closed = true
314
+ @session&.count_closed_stream(@direction == :bidi ? :bidi : :uni)
315
+ end
316
+
317
+ def notify_close_callback
318
+ return if @close_notified
319
+
320
+ @close_notified = true
321
+ notify_session_stream_closed
322
+ @close_callback&.call
323
+ end
324
+ end
325
+ end
326
+ end
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quicsilver
4
+ module Transport
5
+ class Configuration
6
+ # True when max_concurrent_requests is our default rather than the
7
+ # application's choice. :nodoc:
8
+ def max_concurrent_requests_default? = @max_concurrent_requests_default
9
+
10
+ # Advertise a stream limit the server can actually serve. RFC 9114 A.1:
11
+ # "In contrast to HTTP/2, stream concurrency in HTTP/3 is managed by
12
+ # QUIC." Advertising more than we can hold means a peer opens streams we
13
+ # then reject with 503, which is a refusal for doing exactly what we
14
+ # permitted. :nodoc:
15
+ def limit_concurrent_requests!(capacity)
16
+ return unless @max_concurrent_requests_default
17
+ return if capacity >= @max_concurrent_requests
18
+
19
+ @max_concurrent_requests = capacity
20
+ end
21
+
22
+ attr_reader :cert_file, :key_file, :idle_timeout_ms, :server_resumption_level, :max_concurrent_requests,
23
+ :max_unidirectional_streams, :stream_receive_window, :stream_receive_buffer, :connection_flow_control_window,
24
+ :pacing_enabled, :send_buffering_enabled, :initial_rtt_ms, :initial_window_packets, :max_ack_delay_ms,
25
+ :keep_alive_interval_ms, :congestion_control_algorithm, :migration_enabled,
26
+ :disconnect_timeout_ms, :handshake_idle_timeout_ms,
27
+ :max_body_size, :max_header_size, :max_header_count, :max_frame_payload_size,
28
+ :early_data_policy,
29
+ :cibir_id, :transport_server_id,
30
+ :mode
31
+
32
+ QUIC_SERVER_RESUME_AND_ZERORTT = 1
33
+ QUIC_SERVER_RESUME_ONLY = 2
34
+ QUIC_SERVER_RESUME_AND_REUSE = 3
35
+ QUIC_SERVER_RESUME_AND_REUSE_ZERORTT = 4
36
+
37
+ # Congestion control algorithms
38
+ CONGESTION_CONTROL_CUBIC = 0
39
+ CONGESTION_CONTROL_BBR = 1
40
+
41
+ DEFAULT_ALPN = "h3"
42
+
43
+ # Flow control defaults — cross-referenced with quiche, quic-go, lsquic, RFC 9000
44
+ # See: https://github.com/microsoft/msquic/blob/main/docs/Settings.md
45
+ DEFAULT_STREAM_RECEIVE_WINDOW = 262_144 # 256KB (quiche/quic-go use 1MB, MsQuic default 64KB)
46
+ DEFAULT_STREAM_RECEIVE_BUFFER = 32_768 # 32KB (MsQuic default 4KB — too small for typical responses)
47
+ DEFAULT_CONNECTION_FLOW_CONTROL_WINDOW = 16_777_216 # 16MB - connection-wide flow control
48
+
49
+ # Throughput defaults
50
+ DEFAULT_PACING_ENABLED = true # RFC 9002: MUST pace or limit bursts
51
+ DEFAULT_SEND_BUFFERING_ENABLED = true # MsQuic recommended — coalesces small writes
52
+ DEFAULT_INITIAL_RTT_MS = 100 # MsQuic default 333ms is satellite-grade; 100ms matches Chromium
53
+ DEFAULT_INITIAL_WINDOW_PACKETS = 10 # Matches RFC 9002 recommendation
54
+ DEFAULT_MAX_ACK_DELAY_MS = 25 # Matches RFC 9000 default
55
+
56
+ # HTTP/3 parser limits — prevent OOM from malicious clients
57
+ DEFAULT_MAX_BODY_SIZE = 10 * 1024 * 1024 # 10MB — matches common reverse proxy defaults
58
+ DEFAULT_MAX_HEADER_SIZE = 65_536 # 64KB — matches Envoy/nginx defaults
59
+ DEFAULT_MAX_HEADER_COUNT = 100 # Matches nginx default
60
+ DEFAULT_MAX_FRAME_PAYLOAD_SIZE = 1_048_576 # 1MB — single frame can't exceed this
61
+
62
+
63
+ # Connection management defaults
64
+ DEFAULT_KEEP_ALIVE_INTERVAL_MS = 0 # 0 = disabled. Set to 20000 for NAT traversal
65
+ DEFAULT_CONGESTION_CONTROL_ALGORITHM = CONGESTION_CONTROL_CUBIC # CUBIC (0) or BBR (1)
66
+ DEFAULT_MIGRATION_ENABLED = true # Client IP migration. Disable behind load balancers
67
+ DEFAULT_DISCONNECT_TIMEOUT_MS = 16_000 # How long to wait for ACK before path declared dead
68
+ DEFAULT_HANDSHAKE_IDLE_TIMEOUT_MS = 10_000 # Handshake timeout (separate from connection idle)
69
+
70
+ # Matches MsQuic and quic-go. A handler slower than this loses its
71
+ # response: the lower of the two endpoints' values applies.
72
+ DEFAULT_IDLE_TIMEOUT_MS = 30_000
73
+
74
+ def initialize(cert_file = nil, key_file = nil, options = {})
75
+ @idle_timeout_ms = options.fetch(:idle_timeout_ms, DEFAULT_IDLE_TIMEOUT_MS)
76
+ @server_resumption_level = options.fetch(:server_resumption_level, QUIC_SERVER_RESUME_AND_ZERORTT)
77
+ # Maximum concurrent bidirectional streams the peer can open.
78
+ # Matches quic-go and Chromium defaults. MsQuic ceiling: 65,535.
79
+ @max_concurrent_requests = options.fetch(:max_concurrent_requests, 100)
80
+ # Whether the application chose this, or it is our default. A server
81
+ # that knows its own capacity lowers the default so the limit it
82
+ # advertises is one it can honour; an explicit value is never touched.
83
+ @max_concurrent_requests_default = !options.key?(:max_concurrent_requests)
84
+
85
+ # Maximum concurrent unidirectional streams the peer can open.
86
+ # HTTP/3 requires 3 (control, QPACK encoder, QPACK decoder).
87
+ # Default of 10 provides headroom for GREASE and server push.
88
+ # Bump to 100 for WebTransport (opens many uni streams for
89
+ # server-initiated data channels) or MASQUE proxy tunnels.
90
+ @max_unidirectional_streams = options.fetch(:max_unidirectional_streams, 10)
91
+ @alpn = options.fetch(:alpn, DEFAULT_ALPN)
92
+
93
+ # Flow control
94
+ @stream_receive_window = options.fetch(:stream_receive_window, DEFAULT_STREAM_RECEIVE_WINDOW)
95
+ @stream_receive_buffer = options.fetch(:stream_receive_buffer, DEFAULT_STREAM_RECEIVE_BUFFER)
96
+ @connection_flow_control_window = options.fetch(:connection_flow_control_window, DEFAULT_CONNECTION_FLOW_CONTROL_WINDOW)
97
+
98
+ # Throughput
99
+ @pacing_enabled = options.fetch(:pacing_enabled, DEFAULT_PACING_ENABLED)
100
+ @send_buffering_enabled = options.fetch(:send_buffering_enabled, DEFAULT_SEND_BUFFERING_ENABLED)
101
+ @initial_rtt_ms = options.fetch(:initial_rtt_ms, DEFAULT_INITIAL_RTT_MS)
102
+ @initial_window_packets = options.fetch(:initial_window_packets, DEFAULT_INITIAL_WINDOW_PACKETS)
103
+ @max_ack_delay_ms = options.fetch(:max_ack_delay_ms, DEFAULT_MAX_ACK_DELAY_MS)
104
+
105
+ # Connection management
106
+ @keep_alive_interval_ms = options.fetch(:keep_alive_interval_ms, DEFAULT_KEEP_ALIVE_INTERVAL_MS)
107
+ @congestion_control_algorithm = options.fetch(:congestion_control_algorithm, DEFAULT_CONGESTION_CONTROL_ALGORITHM)
108
+ @migration_enabled = options.fetch(:migration_enabled, DEFAULT_MIGRATION_ENABLED)
109
+ @disconnect_timeout_ms = options.fetch(:disconnect_timeout_ms, DEFAULT_DISCONNECT_TIMEOUT_MS)
110
+ @handshake_idle_timeout_ms = options.fetch(:handshake_idle_timeout_ms, DEFAULT_HANDSHAKE_IDLE_TIMEOUT_MS)
111
+
112
+ # HTTP/3 parser limits — sensible defaults prevent OOM from malicious clients.
113
+ # RFC 9114 §4.2.2: SETTINGS_MAX_FIELD_SECTION_SIZE limits header block size.
114
+ # Override with nil to disable (not recommended in production).
115
+ @max_body_size = options.fetch(:max_body_size, DEFAULT_MAX_BODY_SIZE)
116
+ @max_header_size = options.fetch(:max_header_size, DEFAULT_MAX_HEADER_SIZE)
117
+ @max_header_count = options.fetch(:max_header_count, DEFAULT_MAX_HEADER_COUNT)
118
+ @max_frame_payload_size = options.fetch(:max_frame_payload_size, DEFAULT_MAX_FRAME_PAYLOAD_SIZE)
119
+
120
+ # 0-RTT early data policy (RFC 8470)
121
+ # :reject (default) — send 425 Too Early for unsafe methods on 0-RTT
122
+ # :allow — pass all 0-RTT requests to the Rack app with env["quicsilver.early_data"]
123
+ @early_data_policy = options.fetch(:early_data_policy, :reject)
124
+ unless %i[reject allow].include?(@early_data_policy)
125
+ raise ServerConfigurationError, "Invalid early_data_policy: #{@early_data_policy.inspect} (must be :reject or :allow)"
126
+ end
127
+
128
+ # MsQuic CIBIR (Connection ID Based Implicit Routing). Applications own
129
+ # the meaning of these bytes; Quicsilver validates and passes them to MsQuic.
130
+ # MsQuic currently supports CIBIR at offset 0 only, so offset is not configurable.
131
+ @cibir_id = normalize_cibir_id(options[:cibir_id])
132
+
133
+ # MsQuic fixed Server ID for QUIC-aware load balancers. This is
134
+ # process-global MsQuic state; Server applies it before opening MsQuic.
135
+ @transport_server_id = normalize_transport_server_id(options[:transport_server_id])
136
+
137
+ # Application interface mode:
138
+ # :rack (default) — app is a Rack app, auto-wrapped with Protocol::Rack::Adapter
139
+ # :falcon — app is a native protocol-http app, used directly
140
+ @mode = options.fetch(:mode, :rack)
141
+ unless %i[rack falcon].include?(@mode)
142
+ raise ServerConfigurationError, "Invalid mode: #{@mode.inspect} (must be :rack or :falcon)"
143
+ end
144
+
145
+ validate_certificate_paths!(cert_file, key_file)
146
+
147
+ @cert_file = cert_file
148
+ @key_file = key_file
149
+
150
+ unless File.exist?(@cert_file)
151
+ raise ServerConfigurationError, "Certificate file not found: #{@cert_file}"
152
+ end
153
+
154
+ unless File.exist?(@key_file)
155
+ raise ServerConfigurationError, "Key file not found: #{@key_file}"
156
+ end
157
+ end
158
+
159
+ # Common HTTP/3 ALPN Values:
160
+ # "h3" - HTTP/3 (most common)
161
+ # "h3-29" - HTTP/3 draft version 29
162
+ def alpn
163
+ @alpn
164
+ end
165
+
166
+ def cibir_bytes
167
+ [@cibir_id].pack("H*") if @cibir_id
168
+ end
169
+
170
+ def to_h
171
+ {
172
+ cert_file: @cert_file,
173
+ key_file: @key_file,
174
+ idle_timeout_ms: @idle_timeout_ms,
175
+ server_resumption_level: @server_resumption_level,
176
+ max_concurrent_requests: @max_concurrent_requests,
177
+ max_unidirectional_streams: @max_unidirectional_streams,
178
+ alpn: alpn,
179
+ stream_receive_window: @stream_receive_window,
180
+ stream_receive_buffer: @stream_receive_buffer,
181
+ connection_flow_control_window: @connection_flow_control_window,
182
+ pacing_enabled: @pacing_enabled ? 1 : 0,
183
+ send_buffering_enabled: @send_buffering_enabled ? 1 : 0,
184
+ initial_rtt_ms: @initial_rtt_ms,
185
+ initial_window_packets: @initial_window_packets,
186
+ max_ack_delay_ms: @max_ack_delay_ms,
187
+ keep_alive_interval_ms: @keep_alive_interval_ms,
188
+ congestion_control_algorithm: @congestion_control_algorithm,
189
+ migration_enabled: @migration_enabled ? 1 : 0,
190
+ disconnect_timeout_ms: @disconnect_timeout_ms,
191
+ handshake_idle_timeout_ms: @handshake_idle_timeout_ms
192
+ }
193
+ end
194
+
195
+ private
196
+ HEX = /\A[[:xdigit:]]+\z/
197
+ TRANSPORT_SERVER_ID = /\A[[:xdigit:]]{8}\z/
198
+
199
+ def normalize_cibir_id(value)
200
+ return if value.nil?
201
+
202
+ cibir_id = value.to_s
203
+ if cibir_id.empty? || cibir_id.bytesize.odd? || cibir_id.bytesize > 12 || !cibir_id.match?(HEX)
204
+ raise ServerConfigurationError, "cibir_id must be 1..6 bytes encoded as an even-length hex string"
205
+ end
206
+
207
+ cibir_id.downcase
208
+ end
209
+
210
+ def normalize_transport_server_id(value)
211
+ return if value.nil?
212
+
213
+ unless value.is_a?(String) && value.match?(TRANSPORT_SERVER_ID)
214
+ raise ServerConfigurationError, "transport_server_id must be exactly 4 bytes encoded as an 8-character hex string"
215
+ end
216
+
217
+ value.downcase
218
+ end
219
+
220
+ def validate_certificate_paths!(cert_file, key_file)
221
+ cert_file_missing = cert_file.to_s.empty?
222
+ key_file_missing = key_file.to_s.empty?
223
+
224
+ if cert_file_missing && key_file_missing
225
+ raise ServerConfigurationError, "cert_file and key_file are required"
226
+ elsif cert_file_missing
227
+ raise ServerConfigurationError, "cert_file is required when key_file is provided"
228
+ elsif key_file_missing
229
+ raise ServerConfigurationError, "key_file is required when cert_file is provided"
230
+ end
231
+ end
232
+ end
233
+ end
234
+ end