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,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "frame_parser"
4
+
5
+ module Quicsilver
6
+ module Protocol
7
+ # Incrementally yields CONNECT DATA payloads without waiting for whole
8
+ # frames. Unknown frame payloads are skipped as bytes arrive.
9
+ class ConnectStreamDecoder
10
+ def initialize
11
+ @buffer = "".b
12
+ @frame_type = nil
13
+ @frame_remaining = nil
14
+ end
15
+
16
+ # A caller may break after a terminal capsule. Unprocessed bytes remain
17
+ # available to buffered? so it can reject data following that capsule.
18
+ def each(data)
19
+ @buffer << data if data
20
+ until @buffer.empty?
21
+ break unless @frame_remaining || read_frame_header
22
+
23
+ chunk = consume_payload
24
+ yield chunk if @frame_type == FRAME_DATA
25
+ end
26
+ end
27
+
28
+ def buffered?
29
+ !@buffer.empty?
30
+ end
31
+
32
+ def finish!
33
+ if buffered? || @frame_remaining
34
+ raise FrameError.new("Truncated HTTP/3 frame", error_code: H3_FRAME_ERROR)
35
+ end
36
+ end
37
+
38
+ def clear
39
+ @buffer.clear
40
+ @frame_remaining = nil
41
+ end
42
+
43
+ private
44
+
45
+ def read_frame_header
46
+ type, type_length = Protocol.decode_varint_str(@buffer, 0)
47
+ return false if type_length == 0
48
+ length, length_length = Protocol.decode_varint_str(@buffer, type_length)
49
+ return false if length_length == 0
50
+
51
+ # RFC 9114 §4.4 forbids known non-DATA frames after CONNECT,
52
+ # including HEADERS trailers; these are connection errors.
53
+ if CONTROL_ONLY_FRAME_SET.key?(type) ||
54
+ HTTP2_RESERVED_FRAME_SET.key?(type) ||
55
+ [FRAME_HEADERS, FRAME_PUSH_PROMISE, FRAME_PRIORITY_UPDATE].include?(type)
56
+ raise FrameError, "Frame not allowed on CONNECT"
57
+ end
58
+
59
+ @frame_type = type
60
+ @frame_remaining = length
61
+ @buffer = @buffer.byteslice(type_length + length_length..) || "".b
62
+ true
63
+ end
64
+
65
+ def consume_payload
66
+ count = [@frame_remaining, @buffer.bytesize].min
67
+ chunk = @buffer.byteslice(0, count)
68
+ @buffer = @buffer.byteslice(count..) || "".b
69
+ @frame_remaining -= count
70
+ @frame_remaining = nil if @frame_remaining.zero?
71
+ chunk
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quicsilver
4
+ module Protocol
5
+ # Shared control stream parsing for both server Connection and Client.
6
+ #
7
+ # RFC 9114 §7.2.4: Both endpoints MUST send and process SETTINGS.
8
+ # RFC 9114 §7.2.6: Both endpoints MUST validate incoming GOAWAY.
9
+ #
10
+ # Includer must provide:
11
+ # @settings_received — boolean, initially false
12
+ # @peer_goaway_id — nil initially
13
+ #
14
+ # Includer may override:
15
+ # on_settings_received(settings_hash) — called after SETTINGS parsed
16
+ # on_goaway_received(stream_id) — called after GOAWAY parsed
17
+ # handle_control_frame(type, payload) — called for non-SETTINGS/GOAWAY frames
18
+ module ControlStreamParser
19
+ # RFC 9114 §7.2.4.1 / §11.2.2: HTTP/2 setting identifiers forbidden in HTTP/3
20
+ # 0x00 = SETTINGS_HEADER_TABLE_SIZE (reserved), 0x02-0x05 = various HTTP/2 settings
21
+ # Note: 0x08 (SETTINGS_ENABLE_CONNECT_PROTOCOL) is valid in HTTP/3 per RFC 9220
22
+ HTTP2_SETTINGS = [0x00, 0x02, 0x03, 0x04, 0x05].freeze
23
+
24
+ # Returns the number of bytes consumed. A frame split across receives
25
+ # leaves its bytes unconsumed, and the caller must keep them buffered:
26
+ # control streams never send FIN, so a partial frame is normal.
27
+ def parse_control_frames(data)
28
+ first_frame = !@settings_received
29
+
30
+ Protocol::FrameReader.each(data) do |type, payload|
31
+ if first_frame && type != Protocol::FRAME_SETTINGS
32
+ raise Protocol::FrameError.new("First frame on control stream must be SETTINGS",
33
+ error_code: Protocol::H3_MISSING_SETTINGS)
34
+ end
35
+ first_frame = false
36
+
37
+ case type
38
+ when Protocol::FRAME_SETTINGS
39
+ raise Protocol::FrameError, "Duplicate SETTINGS frame on control stream" if @settings_received
40
+ parse_peer_settings(payload)
41
+ @settings_received = true
42
+ when Protocol::FRAME_GOAWAY
43
+ parse_peer_goaway(payload)
44
+ else
45
+ handle_control_frame(type, payload)
46
+ end
47
+ end
48
+ end
49
+
50
+ def parse_peer_settings(payload)
51
+ offset = 0
52
+ seen = Set.new
53
+ settings = {}
54
+
55
+ while offset < payload.bytesize
56
+ id, id_len = Protocol.decode_varint(payload.bytes, offset)
57
+ value, value_len = Protocol.decode_varint(payload.bytes, offset + id_len)
58
+ break if id_len == 0 || value_len == 0
59
+
60
+ if HTTP2_SETTINGS.include?(id)
61
+ raise Protocol::FrameError.new("HTTP/2 setting identifier 0x#{id.to_s(16)} not allowed in HTTP/3",
62
+ error_code: Protocol::H3_SETTINGS_ERROR)
63
+ end
64
+
65
+ if seen.include?(id)
66
+ raise Protocol::FrameError.new("Duplicate setting identifier 0x#{id.to_s(16)}",
67
+ error_code: Protocol::H3_SETTINGS_ERROR)
68
+ end
69
+ seen.add(id)
70
+
71
+ settings[id] = value
72
+ offset += id_len + value_len
73
+ end
74
+
75
+ on_settings_received(settings)
76
+ end
77
+
78
+ # RFC 9114 §7.2.6: Validate incoming GOAWAY frame.
79
+ # Stream ID must be a client-initiated bidirectional stream ID (divisible by 4)
80
+ # and must not increase from a previous GOAWAY.
81
+ def parse_peer_goaway(payload)
82
+ stream_id, _ = Protocol.decode_varint(payload.bytes, 0)
83
+
84
+ unless stream_id % 4 == 0
85
+ raise Protocol::FrameError.new(
86
+ "GOAWAY stream ID #{stream_id} is not a client-initiated bidirectional stream ID",
87
+ error_code: Protocol::H3_ID_ERROR)
88
+ end
89
+
90
+ if @peer_goaway_id && stream_id > @peer_goaway_id
91
+ raise Protocol::FrameError.new(
92
+ "GOAWAY stream ID #{stream_id} exceeds previous #{@peer_goaway_id}",
93
+ error_code: Protocol::H3_ID_ERROR)
94
+ end
95
+
96
+ @peer_goaway_id = stream_id
97
+ on_goaway_received(stream_id)
98
+ end
99
+
100
+ private
101
+
102
+ # Override in includer to store settings. Default: no-op.
103
+ def on_settings_received(settings)
104
+ end
105
+
106
+ # Override in includer to react to GOAWAY. Default: no-op.
107
+ def on_goaway_received(stream_id)
108
+ end
109
+
110
+ # Override in includer to handle additional frame types on the control stream.
111
+ # Server handles FORBIDDEN_ON_CONTROL and PRIORITY_UPDATE here.
112
+ # Client ignores unknown frames.
113
+ def handle_control_frame(type, payload)
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quicsilver
4
+ module Protocol
5
+ # HTTP/3 DATAGRAM payload framing (RFC 9297).
6
+ #
7
+ # The DATAGRAM frame payload starts with a varint quarter stream ID, followed
8
+ # by the application payload. The quarter stream ID is the HTTP/3 stream ID
9
+ # divided by four, matching the client-initiated bidirectional request stream
10
+ # numbering pattern (0, 4, 8, ...).
11
+ module Datagram
12
+ STREAM_ID_DIVISOR = 4
13
+
14
+ class << self
15
+ def encode(stream_id, payload)
16
+ Protocol.encode_varint(quarter_stream_id(stream_id)) + payload.to_s.b
17
+ end
18
+
19
+ def decode(datagram)
20
+ quarter_stream_id, prefix_length = Protocol.decode_varint_str(datagram, 0)
21
+ return if prefix_length == 0
22
+
23
+ [stream_id(quarter_stream_id), payload(datagram, prefix_length)]
24
+ end
25
+
26
+ def quarter_stream_id(stream_id)
27
+ stream_id / STREAM_ID_DIVISOR
28
+ end
29
+
30
+ def stream_id(quarter_stream_id)
31
+ quarter_stream_id * STREAM_ID_DIVISOR
32
+ end
33
+
34
+ def payload(datagram, prefix_length)
35
+ datagram.byteslice(prefix_length..-1) || "".b
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+ require_relative "frame_reader"
5
+ require_relative "qpack/header_block_decoder"
6
+
7
+ module Quicsilver
8
+ module Protocol
9
+ # Base class for HTTP/3 request and response frame parsing.
10
+ #
11
+ # Handles the shared frame walking loop, HEADERS→DATA→HEADERS ordering,
12
+ # trailer parsing, body accumulation, and size limit enforcement.
13
+ #
14
+ # Subclasses implement:
15
+ # - parse_headers(payload) — decode the first HEADERS frame
16
+ class FrameParser
17
+ # Frame types forbidden on request streams — use hash for O(1) lookup
18
+ # Static-only QPACK decoder (no dynamic table). Used by default.
19
+ # Inject a custom decoder via decoder: kwarg for dynamic QPACK support.
20
+ DEFAULT_DECODER = Qpack::HeaderBlockDecoder.default
21
+
22
+ EMPTY_BODY = StringIO.new("".b).tap { |io| io.set_encoding(Encoding::ASCII_8BIT) }
23
+
24
+ attr_reader :headers, :trailers, :bytes_consumed
25
+
26
+ def frames
27
+ @frames || []
28
+ end
29
+
30
+ def initialize(decoder:, max_body_size: nil, max_header_size: nil, max_header_count: nil, max_frame_payload_size: nil)
31
+ @decoder = decoder
32
+ @max_body_size = max_body_size
33
+ @max_header_size = max_header_size
34
+ @max_header_count = max_header_count
35
+ @max_frame_payload_size = max_frame_payload_size
36
+ @headers = {}
37
+ @trailers = {}
38
+ end
39
+
40
+ def body
41
+ if @body
42
+ @body.rewind
43
+ @body
44
+ elsif @cached_body_str
45
+ @body = StringIO.new(@cached_body_str)
46
+ @body.set_encoding(Encoding::ASCII_8BIT)
47
+ @body
48
+ else
49
+ EMPTY_BODY
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def parse!
56
+ walk_frames(@data)
57
+ end
58
+
59
+ def walk_frames(buffer)
60
+ @headers = {}
61
+ @trailers = {}
62
+ @body = nil
63
+ @frames = nil
64
+ @bytes_consumed = 0
65
+ headers_received = false
66
+ trailers_received = false
67
+
68
+ @bytes_consumed = FrameReader.each(buffer) do |type, payload|
69
+ if @max_frame_payload_size && payload.bytesize > @max_frame_payload_size
70
+ raise Protocol::FrameError.new(
71
+ "Frame payload #{payload.bytesize} exceeds limit #{@max_frame_payload_size}",
72
+ error_code: Protocol::H3_EXCESSIVE_LOAD
73
+ )
74
+ end
75
+
76
+ (@frames ||= []) << { type: type, length: payload.bytesize, payload: payload }
77
+
78
+ Protocol.reject_unexpected_request_frame!(type)
79
+
80
+ case type
81
+ when 0x01 # HEADERS
82
+ if @max_header_size && payload.bytesize > @max_header_size
83
+ raise Protocol::MessageError, "Header block #{payload.bytesize} exceeds limit #{@max_header_size}"
84
+ end
85
+ if trailers_received
86
+ raise Protocol::FrameError, "HEADERS frame after trailers"
87
+ elsif headers_received
88
+ parse_trailers(payload)
89
+ trailers_received = true
90
+ else
91
+ parse_headers(payload)
92
+ headers_received = true
93
+ end
94
+ when 0x00 # DATA
95
+ raise Protocol::FrameError, "DATA frame before HEADERS" unless headers_received
96
+ raise Protocol::FrameError, "DATA frame after trailers" if trailers_received
97
+ unless @body
98
+ @body = StringIO.new
99
+ @body.set_encoding(Encoding::ASCII_8BIT)
100
+ end
101
+ @body.write(payload)
102
+ if @max_body_size && @body.size > @max_body_size
103
+ raise Protocol::MessageError, "Body size #{@body.size} exceeds limit #{@max_body_size}"
104
+ end
105
+ end
106
+ end
107
+
108
+ @body&.rewind
109
+ end
110
+
111
+ # RFC 9110 §5.3: Combine duplicate header values.
112
+ # - set-cookie: join with "\n" (Rack convention, MUST NOT combine with comma)
113
+ # - cookie: join with "; " (RFC 9114 §4.2.1)
114
+ # - all others: join with ", "
115
+ def store_header(name, value)
116
+ if @headers.key?(name)
117
+ separator = case name
118
+ when "set-cookie" then "\n"
119
+ when "cookie" then "; "
120
+ else ", "
121
+ end
122
+ @headers[name] = "#{@headers[name]}#{separator}#{value}"
123
+ else
124
+ if @max_header_count && @headers.size >= @max_header_count
125
+ raise Protocol::MessageError, "Header count exceeds limit #{@max_header_count}"
126
+ end
127
+ @headers[name] = value
128
+ end
129
+ end
130
+
131
+ def parse_trailers(payload)
132
+ @decoder.decode(payload) do |name, value|
133
+ if name.start_with?(":")
134
+ raise Protocol::MessageError, "Pseudo-header '#{name}' in trailers"
135
+ end
136
+ @trailers[name] = value
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quicsilver
4
+ module Protocol
5
+ # Extracts HTTP/3 frames from a byte buffer.
6
+ # Yields (type, payload, offset) per frame. Returns total bytes consumed.
7
+ # Existing callers using |type, payload| are unaffected — offset is ignored.
8
+ #
9
+ # Usage:
10
+ # FrameReader.each(buffer) do |type, payload|
11
+ # case type
12
+ # when FRAME_HEADERS then ...
13
+ # when FRAME_DATA then ...
14
+ # end
15
+ # end
16
+ #
17
+ # With offset (for early exit via throw/catch):
18
+ # strip = FrameReader.skip_while(data) { |type, payload| ... }
19
+ # data = data.byteslice(strip..) if strip > 0
20
+ module FrameReader
21
+ def self.each(buffer)
22
+ offset = 0
23
+ buf_size = buffer.bytesize
24
+
25
+ while offset < buf_size
26
+ break if buf_size - offset < 2
27
+
28
+ # Inline single-byte varint fast path (covers frame types 0x00-0x3F)
29
+ type_byte = buffer.getbyte(offset)
30
+ if type_byte < 0x40
31
+ type = type_byte
32
+ type_len = 1
33
+ else
34
+ type, type_len = Protocol.decode_varint_str(buffer, offset)
35
+ break if type_len == 0
36
+ end
37
+
38
+ len_byte = buffer.getbyte(offset + type_len)
39
+ break unless len_byte
40
+ if len_byte < 0x40
41
+ length = len_byte
42
+ length_len = 1
43
+ else
44
+ length, length_len = Protocol.decode_varint_str(buffer, offset + type_len)
45
+ break if length_len == 0
46
+ end
47
+
48
+ header_len = type_len + length_len
49
+ break if buf_size < offset + header_len + length
50
+
51
+ payload = buffer.byteslice(offset + header_len, length)
52
+ offset += header_len + length
53
+
54
+ yield type, payload, offset
55
+ end
56
+
57
+ offset
58
+ end
59
+
60
+ # Skip leading frames while the block returns true.
61
+ # Returns the byte offset of the first frame that returned false (or 0).
62
+ #
63
+ # skip = FrameReader.skip_while(data) do |type, payload|
64
+ # type == FRAME_HEADERS && informational?(payload)
65
+ # end
66
+ # data = data.byteslice(skip..) if skip > 0
67
+ def self.skip_while(buffer)
68
+ skip_to = 0
69
+
70
+ catch(:stop) do
71
+ each(buffer) do |type, payload, offset|
72
+ if yield(type, payload)
73
+ skip_to = offset
74
+ else
75
+ throw :stop
76
+ end
77
+ end
78
+ end
79
+
80
+ skip_to
81
+ end
82
+ end
83
+ end
84
+ end