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,546 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Minimal browser WebTransport smoke test.
5
+ #
6
+ # ruby examples/webtransport_smoke.rb
7
+ # open https://localhost:8443/
8
+ #
9
+ # Chrome discovers HTTP/3 using Alt-Svc from a normal HTTPS/TCP origin. This
10
+ # example therefore starts two servers:
11
+ #
12
+ # - Tiny HTTPS/TCP server on https://localhost:8443 serving this page + Alt-Svc.
13
+ # - Quicsilver HTTP/3/UDP on localhost:8443 handling the same origin via Alt-Svc.
14
+ #
15
+ # It intentionally avoids Rails, framing, endpoints, and stress testing. It only
16
+ # answers: does browser WebTransport reach Rack as
17
+ # env["quicsilver.context"].webtransport, can we accept it, and can one bidi
18
+ # stream echo bytes?
19
+
20
+ require_relative "example_helper"
21
+ require "openssl"
22
+ require "socket"
23
+ require "tmpdir"
24
+
25
+ HTTPS_PORT = Integer(ENV.fetch("HTTPS_PORT", "8443"))
26
+ H3_PORT = Integer(ENV.fetch("H3_PORT", HTTPS_PORT.to_s))
27
+ HOST = ENV.fetch("HOST", "quicsilver.test")
28
+ ALT_SVC = %(h3=":#{H3_PORT}"; ma=3600)
29
+ CERT_DIR = File.dirname(EXAMPLE_TLS_CONFIG.cert_file)
30
+ ISSUER_CERT = File.join(CERT_DIR, "development.crt")
31
+ PAGE_CERT = ENV.fetch("PAGE_CERT") { File.join(CERT_DIR, "#{HOST}.crt") }
32
+ PAGE_KEY = ENV.fetch("PAGE_KEY") { File.join(CERT_DIR, "#{HOST}.key") }
33
+ PAGE_CERT = EXAMPLE_TLS_CONFIG.cert_file unless File.exist?(PAGE_CERT)
34
+ PAGE_KEY = EXAMPLE_TLS_CONFIG.key_file unless File.exist?(PAGE_KEY)
35
+
36
+ CERT_FILE = File.join(Dir.tmpdir, "quicsilver-#{HOST}-short.crt")
37
+ KEY_FILE = File.join(Dir.tmpdir, "quicsilver-#{HOST}-short.key")
38
+
39
+ key = OpenSSL::PKey::EC.generate("prime256v1")
40
+ cert = OpenSSL::X509::Certificate.new
41
+ cert.version = 2
42
+ cert.serial = Random.rand(1..2**64)
43
+ cert.subject = OpenSSL::X509::Name.parse("/CN=#{HOST}")
44
+ cert.issuer = cert.subject
45
+ cert.public_key = key
46
+ cert.not_before = Time.now - 60
47
+ cert.not_after = Time.now + (13 * 24 * 60 * 60)
48
+
49
+ extensions = OpenSSL::X509::ExtensionFactory.new
50
+ extensions.subject_certificate = cert
51
+ extensions.issuer_certificate = cert
52
+ cert.add_extension extensions.create_extension("basicConstraints", "CA:FALSE", true)
53
+ cert.add_extension extensions.create_extension("keyUsage", "digitalSignature", true)
54
+ cert.add_extension extensions.create_extension("extendedKeyUsage", "serverAuth", false)
55
+ cert.add_extension extensions.create_extension("subjectAltName", "DNS:#{HOST},DNS:localhost,IP:127.0.0.1,IP:::1", false)
56
+ cert.sign(key, OpenSSL::Digest::SHA256.new)
57
+
58
+ File.write(CERT_FILE, cert.to_pem)
59
+ File.write(KEY_FILE, key.to_pem)
60
+ CERT_SHA256 = [OpenSSL::Digest::SHA256.digest(cert.to_der)].pack("m0")
61
+ SMOKE_TLS_CONFIG = Quicsilver::Transport::Configuration.new(
62
+ CERT_FILE,
63
+ KEY_FILE,
64
+ idle_timeout_ms: 120_000,
65
+ keep_alive_interval_ms: 20_000,
66
+ max_unidirectional_streams: 100
67
+ )
68
+
69
+ HTML = <<~HTML
70
+ <!doctype html>
71
+ <meta charset="utf-8">
72
+ <title>Quicsilver WebTransport Smoke</title>
73
+ <style>
74
+ body { font-family: system-ui, sans-serif; margin: 2rem; }
75
+ button { margin: .25rem; }
76
+ pre { background: #111; color: #0f0; padding: 1rem; min-height: 16rem; white-space: pre-wrap; }
77
+ </style>
78
+
79
+ <h1>Quicsilver WebTransport Smoke</h1>
80
+
81
+ <p>
82
+ This page is served over HTTPS/TCP with <code>Alt-Svc: #{ALT_SVC}</code>.
83
+ The browser should use HTTP/3 on UDP port #{H3_PORT} for WebTransport.
84
+ TCP and UDP intentionally share the same port, matching production HTTPS/H3 deployments.
85
+ </p>
86
+
87
+ <button onclick="connectWT()">Connect WebTransport</button>
88
+ <button onclick="sendBidi()">Open bidi stream + send hello</button>
89
+ <button onclick="sendUni()">Open uni stream + send hello</button>
90
+ <button onclick="sendDatagram()">Send datagram</button>
91
+ <button onclick="sendBulk()">Send 20 MiB (exercise flow control)</button>
92
+ <button onclick="requestServerBidi()">Request server bidi stream</button>
93
+ <button onclick="requestServerUni()">Request server uni stream</button>
94
+ <button onclick="closeWT()">Close</button>
95
+ <button onclick="clearLog()">Clear</button>
96
+
97
+ <pre id="log"></pre>
98
+
99
+ <script type="module">
100
+ let transport
101
+ let readyPromise
102
+ const encoder = new TextEncoder()
103
+ const decoder = new TextDecoder()
104
+
105
+ function log(message) {
106
+ console.log(message)
107
+ document.getElementById("log").textContent += `${new Date().toISOString()} ${message}\n`
108
+ }
109
+
110
+ window.clearLog = function() {
111
+ document.getElementById("log").textContent = ""
112
+ }
113
+
114
+ function wtURL() {
115
+ return new URL("/wt", location.href).toString()
116
+ }
117
+
118
+ window.connectWT = async function() {
119
+ try {
120
+ if (transport && readyPromise) {
121
+ log("using existing WebTransport")
122
+ await readyPromise
123
+ return transport
124
+ }
125
+
126
+ log(`creating WebTransport ${wtURL()}`)
127
+ log(`using serverCertificateHashes sha-256 #{CERT_SHA256}`)
128
+ const certificateHash = Uint8Array.from(atob("#{CERT_SHA256}"), character => character.charCodeAt(0))
129
+ transport = new WebTransport(wtURL(), {
130
+ serverCertificateHashes: [{
131
+ algorithm: "sha-256",
132
+ value: certificateHash,
133
+ }],
134
+ })
135
+ readyPromise = transport.ready
136
+
137
+ transport.closed.then(
138
+ () => log("transport closed cleanly"),
139
+ error => log(`transport closed with error: ${error}`)
140
+ ).finally(() => {
141
+ transport = null
142
+ readyPromise = null
143
+ })
144
+
145
+ log("waiting for ready")
146
+ await readyPromise
147
+ log("ready")
148
+ readIncomingBidi(transport)
149
+ readIncomingUni(transport)
150
+ return transport
151
+ } catch (error) {
152
+ transport = null
153
+ readyPromise = null
154
+ log(`connect failed: ${error.stack || error}`)
155
+ }
156
+ }
157
+
158
+ window.sendBidi = async function() {
159
+ try {
160
+ const wt = await connectWT()
161
+ if (!wt) return
162
+
163
+ log("creating bidirectional stream")
164
+ const stream = await wt.createBidirectionalStream()
165
+ const writer = stream.writable.getWriter()
166
+ const reader = stream.readable.getReader()
167
+
168
+ const message = `hello from browser ${Date.now()}`
169
+ log(`writing: ${message}`)
170
+ await writer.write(encoder.encode(message))
171
+ await writer.close()
172
+
173
+ log("waiting for echo")
174
+ while (true) {
175
+ const { value, done } = await reader.read()
176
+ if (done) {
177
+ log("reader done")
178
+ break
179
+ }
180
+
181
+ log(`read: ${decoder.decode(value)}`)
182
+ }
183
+ } catch (error) {
184
+ log(`send failed: ${error.stack || error}`)
185
+ }
186
+ }
187
+
188
+ // Push more than the 16 MiB session window the server advertises, so that
189
+ // if session flow control negotiated, the server must issue WT_MAX_DATA as
190
+ // it reads. Without flow control this is just a large upload.
191
+ window.sendBulk = async function() {
192
+ try {
193
+ const wt = await connectWT()
194
+ if (!wt) return
195
+
196
+ const total = 20 * 1024 * 1024
197
+ const chunk = new Uint8Array(64 * 1024).fill(120)
198
+ log(`bulk: sending ${total} bytes`)
199
+ const started = performance.now()
200
+ const stream = await wt.createUnidirectionalStream()
201
+ const writer = stream.getWriter()
202
+ let sent = 0
203
+ while (sent < total) {
204
+ await writer.ready
205
+ await writer.write(chunk)
206
+ sent += chunk.byteLength
207
+ if (sent % (4 * 1024 * 1024) === 0) log(`bulk: ${sent} bytes sent`)
208
+ }
209
+ await writer.close()
210
+ log(`bulk: done, ${total} bytes in ${Math.round(performance.now() - started)}ms`)
211
+ } catch (error) {
212
+ log(`bulk failed: ${error}`)
213
+ }
214
+ }
215
+
216
+ window.sendUni = async function() {
217
+ try {
218
+ const wt = await connectWT()
219
+ if (!wt) return
220
+
221
+ log("creating unidirectional stream")
222
+ const stream = await wt.createUnidirectionalStream()
223
+ const writer = stream.getWriter()
224
+
225
+ const message = `hello uni from browser ${Date.now()}`
226
+ log(`writing uni: ${message}`)
227
+ await writer.write(encoder.encode(message))
228
+ await writer.close()
229
+ log("uni writer closed")
230
+ } catch (error) {
231
+ log(`uni failed: ${error.stack || error}`)
232
+ }
233
+ }
234
+
235
+ async function readIncomingBidi(wt) {
236
+ try {
237
+ const reader = wt.incomingBidirectionalStreams.getReader()
238
+ while (true) {
239
+ const { value: stream, done } = await reader.read()
240
+ if (done) return
241
+
242
+ log("incoming server bidi stream")
243
+ const streamReader = stream.readable.getReader()
244
+ while (true) {
245
+ const { value, done } = await streamReader.read()
246
+ if (done) {
247
+ log("incoming server bidi reader done")
248
+ break
249
+ }
250
+ log(`incoming server bidi read: ${decoder.decode(value)}`)
251
+ }
252
+ }
253
+ } catch (error) {
254
+ log(`incoming bidi failed: ${error.stack || error}`)
255
+ }
256
+ }
257
+
258
+ async function readIncomingUni(wt) {
259
+ try {
260
+ const reader = wt.incomingUnidirectionalStreams.getReader()
261
+ while (true) {
262
+ const { value: stream, done } = await reader.read()
263
+ if (done) return
264
+
265
+ log("incoming server uni stream")
266
+ const streamReader = stream.getReader()
267
+ while (true) {
268
+ const { value, done } = await streamReader.read()
269
+ if (done) {
270
+ log("incoming server uni reader done")
271
+ break
272
+ }
273
+ log(`incoming server uni read: ${decoder.decode(value)}`)
274
+ }
275
+ }
276
+ } catch (error) {
277
+ log(`incoming uni failed: ${error.stack || error}`)
278
+ }
279
+ }
280
+
281
+ window.sendDatagram = async function() {
282
+ try {
283
+ const wt = await connectWT()
284
+ if (!wt) return
285
+
286
+ const message = `datagram from browser ${Date.now()}`
287
+ log(`sending datagram: ${message}`)
288
+ const writer = wt.datagrams.writable.getWriter()
289
+ await writer.write(encoder.encode(message))
290
+ writer.releaseLock()
291
+
292
+ log("waiting for datagram echo")
293
+ const reader = wt.datagrams.readable.getReader()
294
+ const { value, done } = await reader.read()
295
+ reader.releaseLock()
296
+
297
+ if (done) {
298
+ log("datagram reader done")
299
+ } else {
300
+ log(`datagram read: ${decoder.decode(value)}`)
301
+ }
302
+ } catch (error) {
303
+ log(`datagram failed: ${error.stack || error}`)
304
+ }
305
+ }
306
+
307
+ async function sendControlDatagram(command) {
308
+ const wt = await connectWT()
309
+ if (!wt) return
310
+ const writer = wt.datagrams.writable.getWriter()
311
+ await writer.write(encoder.encode(command))
312
+ writer.releaseLock()
313
+ }
314
+
315
+ window.requestServerBidi = async function() {
316
+ try {
317
+ log("requesting server bidi stream")
318
+ await sendControlDatagram("__open_server_bidi__")
319
+ } catch (error) {
320
+ log(`server bidi request failed: ${error.stack || error}`)
321
+ }
322
+ }
323
+
324
+ window.requestServerUni = async function() {
325
+ try {
326
+ log("requesting server uni stream")
327
+ await sendControlDatagram("__open_server_uni__")
328
+ } catch (error) {
329
+ log(`server uni request failed: ${error.stack || error}`)
330
+ }
331
+ }
332
+
333
+ window.closeWT = function() {
334
+ if (transport) {
335
+ log("closing transport")
336
+ transport.close()
337
+ }
338
+ }
339
+ </script>
340
+ HTML
341
+
342
+ close_stream = lambda do |stream|
343
+ stream.close
344
+ true
345
+ rescue StandardError => error
346
+ warn "WT stream #{stream.stream_id} cleanup error: #{error.class}: #{error.message}"
347
+ false
348
+ end
349
+
350
+ app = lambda do |env|
351
+ path = env["PATH_INFO"]
352
+ method = env["REQUEST_METHOD"]
353
+ context = env["quicsilver.context"]
354
+ session = context&.webtransport || env["quicsilver.webtransport"]
355
+
356
+ puts "H3 HTTP #{method} #{path} authority=#{env['HTTP_HOST']} protocol=#{env['SERVER_PROTOCOL']} wt=#{!!session} context=#{context.class if context}"
357
+
358
+ if path == "/wt"
359
+ unless session
360
+ puts "WT endpoint hit without WebTransport session"
361
+ next [426, { "content-type" => "text/plain", "alt-svc" => ALT_SVC }, ["No WebTransport session\n"]]
362
+ end
363
+
364
+ puts "WT session arrived path=#{session.path.inspect} authority=#{session.authority.inspect} stream_id=#{session.stream_id}"
365
+ session.accept!
366
+ puts "WT session accepted"
367
+
368
+ # Did session flow control negotiate (draft-ietf-webtrans-http3-16 5.1)?
369
+ # It needs a non-zero WT_INITIAL_MAX_* from both endpoints. We send ours;
370
+ # this reports whether the browser sends any, which decides whether the
371
+ # whole of section 5 is reachable against a browser at all.
372
+ peer = session.connection.settings
373
+ offered = {
374
+ "WT_INITIAL_MAX_DATA" => peer[Quicsilver::Protocol::SETTINGS_WT_INITIAL_MAX_DATA],
375
+ "WT_INITIAL_MAX_STREAMS_BIDI" => peer[Quicsilver::Protocol::SETTINGS_WT_INITIAL_MAX_STREAMS_BIDI],
376
+ "WT_INITIAL_MAX_STREAMS_UNI" => peer[Quicsilver::Protocol::SETTINGS_WT_INITIAL_MAX_STREAMS_UNI]
377
+ }
378
+ puts "WT peer flow control settings: #{offered.inspect}"
379
+ puts "WT session flow control enabled: #{session.connection.wt_flow_control_enabled?}"
380
+ puts "WT peer SETTINGS seen: #{peer.keys.map { |k| "0x#{k.to_s(16)}" }.join(", ")}"
381
+
382
+ session.on_stream do |stream|
383
+ puts "WT accepted bidi stream id=#{stream.stream_id}"
384
+
385
+ Thread.new do
386
+ total = 0
387
+ # A bulk sender pushes more than the advertised window, so reading it
388
+ # is what makes us grant more credit. Report totals rather than every
389
+ # chunk, or the logging dominates.
390
+ while (chunk = stream.read)
391
+ total += chunk.bytesize
392
+ if chunk.bytesize > 4096 || total > 65_536
393
+ puts "WT stream #{stream.stream_id} read #{total} bytes so far"
394
+ next
395
+ end
396
+ puts "WT stream #{stream.stream_id} read #{chunk.bytesize} bytes"
397
+ response = "echo: #{chunk}"
398
+ stream.write(response)
399
+ puts "WT stream #{stream.stream_id} wrote #{response.bytesize} bytes"
400
+ end
401
+ puts "WT stream #{stream.stream_id} finished after #{total} bytes"
402
+ rescue => error
403
+ warn "WT stream #{stream.stream_id} error: #{error.class}: #{error.message}"
404
+ warn error.backtrace&.first(5)&.join("\n")
405
+ ensure
406
+ close_stream.call(stream)
407
+ end
408
+ end
409
+
410
+ session.on_uni_stream do |stream|
411
+ puts "WT accepted uni stream id=#{stream.stream_id}"
412
+
413
+ Thread.new do
414
+ total = 0
415
+ # A bulk upload arrives here. Reading it is what releases session
416
+ # window, so the server grants more credit as this loop drains it.
417
+ # Totals only, or the logging drowns out everything else.
418
+ while (chunk = stream.read)
419
+ total += chunk.bytesize
420
+ next if total > 65_536
421
+
422
+ puts "WT uni stream #{stream.stream_id} read #{chunk.bytesize} bytes"
423
+ end
424
+ puts "WT uni stream #{stream.stream_id} finished after #{total} bytes"
425
+ rescue => error
426
+ warn "WT uni stream #{stream.stream_id} error: #{error.class}: #{error.message}"
427
+ warn error.backtrace&.first(5)&.join("\n")
428
+ ensure
429
+ puts "WT uni stream #{stream.stream_id} closed" if close_stream.call(stream)
430
+ end
431
+ end
432
+
433
+ session.on_datagram do |datagram|
434
+ puts "WT datagram read #{datagram.bytesize} bytes"
435
+
436
+ case datagram
437
+ when "__open_server_bidi__"
438
+ stream = session.open_stream
439
+ message = "hello from server bidi #{Time.now.to_f}"
440
+ stream.write(message)
441
+ stream.close
442
+ puts "WT server bidi stream #{stream.stream_id || "pending (token #{stream.stream_handle})"} wrote #{message.bytesize} bytes"
443
+ when "__open_server_uni__"
444
+ stream = session.open_uni_stream
445
+ message = "hello from server uni #{Time.now.to_f}"
446
+ stream.write(message)
447
+ stream.close
448
+ puts "WT server uni stream #{stream.stream_id || "pending (token #{stream.stream_handle})"} wrote #{message.bytesize} bytes"
449
+ else
450
+ response = "echo: #{datagram}"
451
+ session.send_datagram(response)
452
+ puts "WT datagram wrote #{response.bytesize} bytes"
453
+ end
454
+ rescue => error
455
+ warn "WT datagram error: #{error.class}: #{error.message}"
456
+ warn error.backtrace&.first(5)&.join("\n")
457
+ end
458
+
459
+ session.on_close do
460
+ puts "WT session closed"
461
+ end
462
+
463
+ next [200, { "alt-svc" => ALT_SVC }, []]
464
+ end
465
+
466
+ [200, { "content-type" => "text/html", "alt-svc" => ALT_SVC }, [HTML]]
467
+ end
468
+
469
+ h3_server = Quicsilver::Server.new(H3_PORT, app: app, server_configuration: SMOKE_TLS_CONFIG)
470
+ h3_thread = Thread.new { h3_server.start }
471
+
472
+ https_server = TCPServer.new(HOST, HTTPS_PORT)
473
+ ssl_context = OpenSSL::SSL::SSLContext.new
474
+ ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(PAGE_CERT))
475
+ ssl_context.extra_chain_cert = [OpenSSL::X509::Certificate.new(File.read(ISSUER_CERT))] if File.exist?(ISSUER_CERT)
476
+ ssl_context.key = OpenSSL::PKey.read(File.read(PAGE_KEY))
477
+ stopping = false
478
+
479
+ https_thread = Thread.new do
480
+ until stopping
481
+ next unless IO.select([https_server], nil, nil, 0.1)
482
+ socket = https_server.accept_nonblock(exception: false)
483
+ next if socket == :wait_readable
484
+
485
+ Thread.new(socket) do |client|
486
+ begin
487
+ client = OpenSSL::SSL::SSLSocket.new(client, ssl_context)
488
+ client.sync_close = true
489
+ client.accept
490
+ request_line = client.gets&.strip
491
+ puts "HTTPS #{request_line}" if request_line
492
+
493
+ # Drain request headers.
494
+ client.gets until $_ == "\r\n" || $_ == "\n" || $_.nil?
495
+
496
+ body = HTML
497
+ headers = [
498
+ "HTTP/1.1 200 OK",
499
+ "content-type: text/html; charset=utf-8",
500
+ "content-length: #{body.bytesize}",
501
+ "alt-svc: #{ALT_SVC}",
502
+ "connection: close",
503
+ "",
504
+ ""
505
+ ].join("\r\n")
506
+
507
+ client.write(headers)
508
+ client.write(body)
509
+ rescue => error
510
+ warn "HTTPS server error: #{error.class}: #{error.message}"
511
+ ensure
512
+ client.close rescue nil
513
+ end
514
+ end
515
+ end
516
+ end
517
+
518
+ trap("INT") { stopping = true }
519
+ trap("TERM") { stopping = true }
520
+
521
+ puts "🚇 WebTransport smoke test"
522
+ puts " Page: https://#{HOST}:#{HTTPS_PORT}/ (HTTPS/TCP + Alt-Svc)"
523
+ puts " WebTransport: https://#{HOST}:#{HTTPS_PORT}/wt (H3 UDP :#{H3_PORT})"
524
+ puts " Alt-Svc: #{ALT_SVC}"
525
+ puts " Page cert: #{PAGE_CERT}"
526
+ puts " Page key: #{PAGE_KEY}"
527
+ puts " WT cert: #{CERT_FILE}"
528
+ puts " WT key: #{KEY_FILE}"
529
+ puts " WT SHA256: #{CERT_SHA256}"
530
+ puts " /etc/hosts: 127.0.0.1 #{HOST}"
531
+ puts " Watch this terminal and the browser console."
532
+ puts
533
+
534
+ begin
535
+ https_thread.join
536
+ ensure
537
+ stopping = true
538
+ puts "\nStopping..."
539
+ begin
540
+ https_thread.join
541
+ ensure
542
+ https_server.close
543
+ h3_server.stop
544
+ h3_thread.join(2)
545
+ end
546
+ end