quicsilver 0.3.0 → 0.4.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +1 -1
- data/.github/workflows/cibuildgem.yaml +93 -0
- data/.gitignore +3 -1
- data/CHANGELOG.md +32 -0
- data/Gemfile.lock +20 -2
- data/README.md +92 -29
- data/Rakefile +67 -2
- data/benchmarks/concurrent.rb +2 -2
- data/benchmarks/rails.rb +3 -3
- data/benchmarks/throughput.rb +2 -2
- data/examples/README.md +44 -91
- data/examples/benchmark.rb +111 -0
- data/examples/connection_pool_demo.rb +47 -0
- data/examples/example_helper.rb +18 -0
- data/examples/falcon_middleware.rb +44 -0
- data/examples/feature_demo.rb +125 -0
- data/examples/grpc_style.rb +97 -0
- data/examples/minimal_http3_server.rb +6 -18
- data/examples/priorities.rb +60 -0
- data/examples/protocol_http_server.rb +31 -0
- data/examples/rack_http3_server.rb +8 -20
- data/examples/rails_feature_test.rb +260 -0
- data/examples/simple_client_test.rb +2 -2
- data/examples/streaming_sse.rb +33 -0
- data/examples/trailers.rb +69 -0
- data/ext/quicsilver/extconf.rb +14 -0
- data/ext/quicsilver/quicsilver.c +39 -0
- data/lib/quicsilver/client/client.rb +138 -39
- data/lib/quicsilver/client/connection_pool.rb +106 -0
- data/lib/quicsilver/libmsquic.2.dylib +0 -0
- data/lib/quicsilver/protocol/adapter.rb +176 -0
- data/lib/quicsilver/protocol/control_stream_parser.rb +106 -0
- data/lib/quicsilver/protocol/frame_parser.rb +142 -0
- data/lib/quicsilver/protocol/frame_reader.rb +55 -0
- data/lib/quicsilver/protocol/frames.rb +18 -7
- data/lib/quicsilver/protocol/priority.rb +56 -0
- data/lib/quicsilver/protocol/qpack/encoder.rb +39 -1
- data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +16 -1
- data/lib/quicsilver/protocol/request_parser.rb +28 -140
- data/lib/quicsilver/protocol/response_encoder.rb +27 -2
- data/lib/quicsilver/protocol/response_parser.rb +22 -130
- data/lib/quicsilver/protocol/stream_input.rb +98 -0
- data/lib/quicsilver/protocol/stream_output.rb +59 -0
- data/lib/quicsilver/quicsilver.bundle +0 -0
- data/lib/quicsilver/server/request_handler.rb +96 -44
- data/lib/quicsilver/server/server.rb +316 -42
- data/lib/quicsilver/transport/configuration.rb +10 -1
- data/lib/quicsilver/transport/connection.rb +92 -63
- data/lib/quicsilver/version.rb +1 -1
- data/lib/quicsilver.rb +26 -3
- data/quicsilver.gemspec +10 -2
- metadata +69 -5
- data/examples/setup_certs.sh +0 -57
|
@@ -1,39 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require_relative "qpack/header_block_decoder"
|
|
3
|
+
require_relative "frame_parser"
|
|
5
4
|
|
|
6
5
|
module Quicsilver
|
|
7
6
|
module Protocol
|
|
8
|
-
class ResponseParser
|
|
9
|
-
attr_reader :
|
|
10
|
-
|
|
11
|
-
def frames
|
|
12
|
-
@frames || []
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
DEFAULT_DECODER = Qpack::HeaderBlockDecoder.default
|
|
7
|
+
class ResponseParser < FrameParser
|
|
8
|
+
attr_reader :status
|
|
16
9
|
|
|
17
10
|
def initialize(data, **opts)
|
|
11
|
+
decoder = opts.delete(:decoder) || DEFAULT_DECODER
|
|
12
|
+
super(decoder: decoder, max_body_size: opts[:max_body_size],
|
|
13
|
+
max_header_size: opts[:max_header_size])
|
|
18
14
|
@data = data
|
|
19
|
-
|
|
20
|
-
@decoder = DEFAULT_DECODER
|
|
21
|
-
@use_parse_cache = true
|
|
22
|
-
else
|
|
23
|
-
@decoder = opts[:decoder] || DEFAULT_DECODER
|
|
24
|
-
@max_body_size = opts[:max_body_size]
|
|
25
|
-
@max_header_size = opts[:max_header_size]
|
|
26
|
-
@use_parse_cache = @decoder.equal?(DEFAULT_DECODER) && !@max_body_size && !@max_header_size
|
|
27
|
-
end
|
|
15
|
+
@use_parse_cache = @decoder.equal?(DEFAULT_DECODER) && !@max_body_size && !@max_header_size
|
|
28
16
|
end
|
|
29
17
|
|
|
30
18
|
# Reset parser with new data for object reuse (avoids allocation overhead)
|
|
31
19
|
def reset(data)
|
|
32
20
|
@data = data
|
|
33
21
|
@status = nil
|
|
34
|
-
@headers =
|
|
22
|
+
@headers = {}
|
|
23
|
+
@trailers = {}
|
|
35
24
|
@frames = nil
|
|
36
|
-
@
|
|
25
|
+
@body = nil
|
|
37
26
|
@cached_body_str = nil
|
|
38
27
|
end
|
|
39
28
|
|
|
@@ -55,35 +44,22 @@ module Quicsilver
|
|
|
55
44
|
@headers = cached[1]
|
|
56
45
|
@frames = cached[2]
|
|
57
46
|
@cached_body_str = cached[3]
|
|
47
|
+
@trailers = cached[4] || {}
|
|
58
48
|
@last_data = data
|
|
59
49
|
return
|
|
60
50
|
end
|
|
61
51
|
end
|
|
62
52
|
@status = nil
|
|
63
|
-
@headers =
|
|
53
|
+
@headers = {}
|
|
54
|
+
@trailers = {}
|
|
64
55
|
@frames = nil
|
|
65
|
-
@
|
|
56
|
+
@body = nil
|
|
66
57
|
@cached_body_str = nil
|
|
67
58
|
parse!
|
|
68
59
|
cache_result if @use_parse_cache
|
|
69
60
|
end
|
|
70
61
|
|
|
71
|
-
|
|
72
|
-
if @body_io
|
|
73
|
-
@body_io.rewind
|
|
74
|
-
@body_io
|
|
75
|
-
elsif @cached_body_str
|
|
76
|
-
@body_io = StringIO.new(@cached_body_str)
|
|
77
|
-
@body_io.set_encoding(Encoding::ASCII_8BIT)
|
|
78
|
-
@body_io
|
|
79
|
-
else
|
|
80
|
-
EMPTY_BODY
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
EMPTY_BODY = StringIO.new("".b).tap { |io| io.set_encoding(Encoding::ASCII_8BIT) }
|
|
85
|
-
|
|
86
|
-
# Class-level parse result cache: data → [status, headers, frames, body_str]
|
|
62
|
+
# Class-level parse result cache: data → [status, headers, frames, body_str, trailers]
|
|
87
63
|
PARSE_CACHE = {}
|
|
88
64
|
PARSE_CACHE_MAX = 128
|
|
89
65
|
PARSE_OID_CACHE = {}
|
|
@@ -98,6 +74,7 @@ module Quicsilver
|
|
|
98
74
|
@headers = cached[1]
|
|
99
75
|
@frames = cached[2]
|
|
100
76
|
@cached_body_str = cached[3]
|
|
77
|
+
@trailers = cached[4] || {}
|
|
101
78
|
return
|
|
102
79
|
end
|
|
103
80
|
end
|
|
@@ -108,10 +85,10 @@ module Quicsilver
|
|
|
108
85
|
|
|
109
86
|
private def cache_result
|
|
110
87
|
if PARSE_CACHE.size < PARSE_CACHE_MAX && @data.bytesize <= 1024
|
|
111
|
-
body_str = if @
|
|
112
|
-
@
|
|
113
|
-
s = @
|
|
114
|
-
@
|
|
88
|
+
body_str = if @body
|
|
89
|
+
@body.rewind
|
|
90
|
+
s = @body.read
|
|
91
|
+
@body.rewind
|
|
115
92
|
s
|
|
116
93
|
end
|
|
117
94
|
key = @data.frozen? ? @data : @data.dup.freeze
|
|
@@ -119,82 +96,14 @@ module Quicsilver
|
|
|
119
96
|
@status,
|
|
120
97
|
@headers.dup.freeze,
|
|
121
98
|
(@frames || []).freeze,
|
|
122
|
-
body_str&.freeze
|
|
99
|
+
body_str&.freeze,
|
|
100
|
+
(@trailers.empty? ? nil : @trailers.dup.freeze)
|
|
123
101
|
].freeze
|
|
124
102
|
end
|
|
125
103
|
end
|
|
126
104
|
|
|
127
105
|
private
|
|
128
106
|
|
|
129
|
-
# Frame types forbidden on request streams — O(1) lookup
|
|
130
|
-
CONTROL_ONLY_SET = Protocol::CONTROL_ONLY_FRAMES.each_with_object({}) { |f, h| h[f] = true }.freeze
|
|
131
|
-
|
|
132
|
-
def parse!
|
|
133
|
-
@headers = {}
|
|
134
|
-
@status = nil
|
|
135
|
-
@body_io = nil
|
|
136
|
-
@frames = nil
|
|
137
|
-
buffer = @data
|
|
138
|
-
offset = 0
|
|
139
|
-
headers_received = false
|
|
140
|
-
buf_size = buffer.bytesize
|
|
141
|
-
|
|
142
|
-
while offset < buf_size
|
|
143
|
-
break if buf_size - offset < 2
|
|
144
|
-
|
|
145
|
-
# Inline single-byte varint fast path (covers frame types 0x00-0x3F)
|
|
146
|
-
type_byte = buffer.getbyte(offset)
|
|
147
|
-
if type_byte < 0x40
|
|
148
|
-
type = type_byte
|
|
149
|
-
type_len = 1
|
|
150
|
-
else
|
|
151
|
-
type, type_len = Protocol.decode_varint_str(buffer, offset)
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
len_byte = buffer.getbyte(offset + type_len)
|
|
155
|
-
if len_byte < 0x40
|
|
156
|
-
length = len_byte
|
|
157
|
-
length_len = 1
|
|
158
|
-
else
|
|
159
|
-
length, length_len = Protocol.decode_varint_str(buffer, offset + type_len)
|
|
160
|
-
break if length_len == 0
|
|
161
|
-
end
|
|
162
|
-
break if type_len == 0
|
|
163
|
-
|
|
164
|
-
header_len = type_len + length_len
|
|
165
|
-
|
|
166
|
-
break if buf_size < offset + header_len + length
|
|
167
|
-
|
|
168
|
-
payload = buffer.byteslice(offset + header_len, length)
|
|
169
|
-
(@frames ||= []) << { type: type, length: length, payload: payload }
|
|
170
|
-
|
|
171
|
-
if CONTROL_ONLY_SET.key?(type)
|
|
172
|
-
raise Protocol::FrameError, "Frame type 0x#{type.to_s(16)} not allowed on request streams"
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
case type
|
|
176
|
-
when 0x01 # HEADERS
|
|
177
|
-
if @max_header_size && length > @max_header_size
|
|
178
|
-
raise Protocol::MessageError, "Header block #{length} exceeds limit #{@max_header_size}"
|
|
179
|
-
end
|
|
180
|
-
parse_headers(payload)
|
|
181
|
-
headers_received = true
|
|
182
|
-
when 0x00 # DATA
|
|
183
|
-
raise Protocol::FrameError, "DATA frame before HEADERS" unless headers_received
|
|
184
|
-
unless @body_io
|
|
185
|
-
@body_io = StringIO.new
|
|
186
|
-
@body_io.set_encoding(Encoding::ASCII_8BIT)
|
|
187
|
-
end
|
|
188
|
-
@body_io.write(payload)
|
|
189
|
-
if @max_body_size && @body_io.size > @max_body_size
|
|
190
|
-
raise Protocol::MessageError, "Body size #{@body_io.size} exceeds limit #{@max_body_size}"
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
offset += header_len + length
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
|
|
198
107
|
# Cache for validated response header results
|
|
199
108
|
HEADERS_CACHE = {}
|
|
200
109
|
HEADERS_CACHE_MAX = 256
|
|
@@ -227,23 +136,6 @@ module Quicsilver
|
|
|
227
136
|
HEADERS_CACHE[key] = { status: @status, headers: @headers.dup.freeze }.freeze
|
|
228
137
|
end
|
|
229
138
|
end
|
|
230
|
-
|
|
231
|
-
# RFC 9110 §5.3: Combine duplicate header values.
|
|
232
|
-
# - set-cookie: join with "\n" (Rack convention, MUST NOT combine with comma)
|
|
233
|
-
# - cookie: join with "; " (RFC 9114 §4.2.1)
|
|
234
|
-
# - all others: join with ", "
|
|
235
|
-
def store_header(name, value)
|
|
236
|
-
if @headers.key?(name)
|
|
237
|
-
separator = case name
|
|
238
|
-
when "set-cookie" then "\n"
|
|
239
|
-
when "cookie" then "; "
|
|
240
|
-
else ", "
|
|
241
|
-
end
|
|
242
|
-
@headers[name] = "#{@headers[name]}#{separator}#{value}"
|
|
243
|
-
else
|
|
244
|
-
@headers[name] = value
|
|
245
|
-
end
|
|
246
|
-
end
|
|
247
139
|
end
|
|
248
140
|
end
|
|
249
141
|
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "protocol/http/body/writable"
|
|
4
|
+
|
|
5
|
+
module Quicsilver
|
|
6
|
+
module Protocol
|
|
7
|
+
# A streaming request body backed by Protocol::HTTP::Body::Writable.
|
|
8
|
+
#
|
|
9
|
+
# QUIC RECEIVE events push chunks via {write}, while the application
|
|
10
|
+
# reads them via {read}. This enables concurrent streaming — the app
|
|
11
|
+
# can start processing before the full body arrives.
|
|
12
|
+
#
|
|
13
|
+
# Follows the protocol-http Body::Writable contract:
|
|
14
|
+
# - write(chunk) — called by the QUIC transport on RECEIVE events
|
|
15
|
+
# - close_write — called on RECEIVE_FIN to signal end of body
|
|
16
|
+
# - read — called by the application (blocks until data available)
|
|
17
|
+
# - close — called to abort (e.g., stream reset)
|
|
18
|
+
#
|
|
19
|
+
# Optional features:
|
|
20
|
+
# - Back-pressure via Thread::SizedQueue (bounded buffer)
|
|
21
|
+
# - Read timeout for slow client protection
|
|
22
|
+
#
|
|
23
|
+
class StreamInput < ::Protocol::HTTP::Body::Writable
|
|
24
|
+
class ReadTimeout < StandardError; end
|
|
25
|
+
|
|
26
|
+
# @param length [Integer, nil] The content-length if known from headers.
|
|
27
|
+
# @param queue_size [Integer, nil] Maximum buffered chunks for back-pressure.
|
|
28
|
+
# nil (default) = unbounded. When bounded, write blocks if queue is full,
|
|
29
|
+
# which naturally maps to QUIC flow control.
|
|
30
|
+
# @param read_timeout [Numeric, nil] Seconds to wait for data before raising
|
|
31
|
+
# ReadTimeout. nil (default) = wait forever.
|
|
32
|
+
def initialize(length = nil, queue_size: nil, read_timeout: nil)
|
|
33
|
+
queue = if queue_size
|
|
34
|
+
Thread::SizedQueue.new(queue_size)
|
|
35
|
+
else
|
|
36
|
+
Thread::Queue.new
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
super(length, queue: queue)
|
|
40
|
+
@read_timeout = read_timeout
|
|
41
|
+
@bytes_written = 0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @attribute [Numeric, nil] Read timeout in seconds.
|
|
45
|
+
attr_reader :read_timeout
|
|
46
|
+
|
|
47
|
+
# Track bytes written for content-length validation.
|
|
48
|
+
def write(chunk)
|
|
49
|
+
@bytes_written += chunk.bytesize
|
|
50
|
+
super
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Signal that no more data will be written.
|
|
54
|
+
# Validates content-length if declared (RFC 9114 §4.1.2) — raises
|
|
55
|
+
# MessageError if total bytes written don't match.
|
|
56
|
+
def close_write(error = nil)
|
|
57
|
+
if @length && @bytes_written != @length
|
|
58
|
+
raise Protocol::MessageError, "Content-length mismatch: header=#{@length}, body=#{@bytes_written}"
|
|
59
|
+
end
|
|
60
|
+
super
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Read the next available chunk, with optional timeout.
|
|
64
|
+
#
|
|
65
|
+
# @returns [String | Nil] The next chunk, or nil if the body is finished.
|
|
66
|
+
# @raises [ReadTimeout] If no data arrives within the timeout.
|
|
67
|
+
# @raises [Exception] If the body was closed due to an error.
|
|
68
|
+
def read
|
|
69
|
+
if @read_timeout
|
|
70
|
+
read_with_timeout
|
|
71
|
+
else
|
|
72
|
+
super
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def read_with_timeout
|
|
79
|
+
raise @error if @error
|
|
80
|
+
|
|
81
|
+
# Thread::Queue#pop(timeout:) blocks efficiently (no busy-wait).
|
|
82
|
+
# Returns nil on timeout OR on closed empty queue — disambiguate below.
|
|
83
|
+
chunk = @queue.pop(timeout: @read_timeout)
|
|
84
|
+
|
|
85
|
+
raise @error if @error
|
|
86
|
+
|
|
87
|
+
# nil means either timeout or body finished (queue closed + empty).
|
|
88
|
+
# If queue is closed, the body is done — return nil (EOF).
|
|
89
|
+
# If queue is still open, we timed out waiting for data.
|
|
90
|
+
if chunk.nil? && !@queue.closed?
|
|
91
|
+
raise ReadTimeout, "No data received within #{@read_timeout}s"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
chunk
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
module Quicsilver
|
|
5
|
+
module Protocol
|
|
6
|
+
# Reads from a Protocol::HTTP::Body::Readable response body and writes
|
|
7
|
+
# HTTP/3 DATA frames to the transport.
|
|
8
|
+
#
|
|
9
|
+
# Transport-agnostic: takes a writer block/callable that handles the actual
|
|
10
|
+
# sending. Works with msquic, kernel QUIC sockets, or any transport that
|
|
11
|
+
# can send bytes with a FIN flag.
|
|
12
|
+
#
|
|
13
|
+
class StreamOutput
|
|
14
|
+
# @param body [Protocol::HTTP::Body::Readable] The response body to stream.
|
|
15
|
+
# @param writer [#call] A callable that accepts (data, fin) — sends bytes
|
|
16
|
+
# to the transport. `fin: true` signals end of stream.
|
|
17
|
+
def initialize(body, &writer)
|
|
18
|
+
@body = body
|
|
19
|
+
@writer = writer
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Stream all chunks from the response body as HTTP/3 DATA frames.
|
|
23
|
+
#
|
|
24
|
+
# Each chunk is wrapped in an HTTP/3 DATA frame (type 0x00) and sent
|
|
25
|
+
# via the writer. The final chunk is sent with fin=true.
|
|
26
|
+
#
|
|
27
|
+
# @param send_fin [Boolean] Whether to send FIN after the last chunk.
|
|
28
|
+
# Set to false when trailers will follow.
|
|
29
|
+
# @return [void]
|
|
30
|
+
def stream(send_fin: true)
|
|
31
|
+
last_chunk = nil
|
|
32
|
+
|
|
33
|
+
while (chunk = @body.read)
|
|
34
|
+
if last_chunk
|
|
35
|
+
@writer.call(build_data_frame(last_chunk), false)
|
|
36
|
+
end
|
|
37
|
+
last_chunk = chunk
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if last_chunk
|
|
41
|
+
@writer.call(build_data_frame(last_chunk), send_fin)
|
|
42
|
+
elsif send_fin
|
|
43
|
+
@writer.call("".b, true)
|
|
44
|
+
end
|
|
45
|
+
ensure
|
|
46
|
+
@body.close if @body.respond_to?(:close)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def build_data_frame(payload)
|
|
52
|
+
payload = payload.b
|
|
53
|
+
Quicsilver::Protocol.encode_varint(Quicsilver::Protocol::FRAME_DATA) +
|
|
54
|
+
Quicsilver::Protocol.encode_varint(payload.bytesize) +
|
|
55
|
+
payload
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
Binary file
|
|
@@ -3,68 +3,31 @@
|
|
|
3
3
|
module Quicsilver
|
|
4
4
|
class Server
|
|
5
5
|
class RequestHandler
|
|
6
|
-
# Safe HTTP methods allowed in 0-RTT early data (RFC 9110 §9.2.1)
|
|
7
6
|
SAFE_METHODS = %w[GET HEAD OPTIONS].freeze
|
|
8
7
|
|
|
8
|
+
attr_reader :adapter
|
|
9
|
+
|
|
9
10
|
def initialize(app:, configuration:, request_registry:, cancelled_streams:, cancelled_mutex:)
|
|
10
|
-
@app = app
|
|
11
11
|
@configuration = configuration
|
|
12
12
|
@request_registry = request_registry
|
|
13
13
|
@cancelled_streams = cancelled_streams
|
|
14
14
|
@cancelled_mutex = cancelled_mutex
|
|
15
|
+
@adapter = Protocol::Adapter.new(app)
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def call(connection, stream, early_data: false)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
max_body_size: @configuration.max_body_size,
|
|
21
|
-
max_header_size: @configuration.max_header_size,
|
|
22
|
-
max_header_count: @configuration.max_header_count,
|
|
23
|
-
max_frame_payload_size: @configuration.max_frame_payload_size
|
|
24
|
-
)
|
|
25
|
-
parser.parse
|
|
26
|
-
parser.validate_headers! # raises MessageError for missing/invalid pseudo-headers
|
|
27
|
-
env = parser.to_rack_env
|
|
28
|
-
|
|
29
|
-
if env && @app
|
|
30
|
-
env["quicsilver.early_data"] = early_data
|
|
31
|
-
|
|
32
|
-
# RFC 8470: reject unsafe methods on 0-RTT unless app opted in
|
|
33
|
-
if @configuration.early_data_policy == :reject &&
|
|
34
|
-
early_data && !SAFE_METHODS.include?(env["REQUEST_METHOD"])
|
|
35
|
-
connection.send_error(stream, 425, "Too Early") if stream.writable?
|
|
36
|
-
return
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
@request_registry.track(
|
|
40
|
-
stream.stream_id,
|
|
41
|
-
connection.handle,
|
|
42
|
-
path: env["PATH_INFO"] || "/",
|
|
43
|
-
method: env["REQUEST_METHOD"] || "GET"
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
status, headers, body = @app.call(env)
|
|
47
|
-
|
|
48
|
-
if cancelled_stream?(stream.stream_id)
|
|
49
|
-
Quicsilver.logger.debug("Skipping response for cancelled stream #{stream.stream_id}")
|
|
50
|
-
return
|
|
51
|
-
end
|
|
19
|
+
request = parse_request(connection, stream, early_data: early_data)
|
|
20
|
+
return unless request
|
|
52
21
|
|
|
53
|
-
|
|
22
|
+
response = @adapter.call(request)
|
|
54
23
|
|
|
55
|
-
|
|
56
|
-
@request_registry.complete(stream.stream_id)
|
|
57
|
-
else
|
|
58
|
-
connection.send_error(stream, 400, "Bad Request") if stream.writable?
|
|
59
|
-
end
|
|
24
|
+
send_response(connection, stream, request, response)
|
|
60
25
|
rescue Server::DrainTimeoutError
|
|
61
26
|
Quicsilver.logger.debug("Request interrupted by drain: stream #{stream.stream_id}")
|
|
62
27
|
rescue Protocol::FrameError => e
|
|
63
|
-
# Frame errors are connection-level: signal via CONNECTION_CLOSE with H3 error code
|
|
64
28
|
Quicsilver.logger.error("Frame error: #{e.message} (0x#{e.error_code.to_s(16)})")
|
|
65
29
|
Quicsilver.connection_shutdown(connection.handle, e.error_code, false) rescue nil
|
|
66
30
|
rescue Protocol::MessageError => e
|
|
67
|
-
# Message errors are stream-level: signal via RESET_STREAM with H3 error code
|
|
68
31
|
Quicsilver.logger.error("Message error: #{e.message} (0x#{e.error_code.to_s(16)})")
|
|
69
32
|
Quicsilver.stream_reset(stream.stream_handle, e.error_code) if stream.writable?
|
|
70
33
|
rescue => e
|
|
@@ -78,6 +41,95 @@ module Quicsilver
|
|
|
78
41
|
|
|
79
42
|
private
|
|
80
43
|
|
|
44
|
+
def parse_request(connection, stream, early_data: false)
|
|
45
|
+
parser = Protocol::RequestParser.new(
|
|
46
|
+
stream.data,
|
|
47
|
+
max_body_size: @configuration.max_body_size,
|
|
48
|
+
max_header_size: @configuration.max_header_size,
|
|
49
|
+
max_header_count: @configuration.max_header_count,
|
|
50
|
+
max_frame_payload_size: @configuration.max_frame_payload_size
|
|
51
|
+
)
|
|
52
|
+
parser.parse
|
|
53
|
+
parser.validate_headers!
|
|
54
|
+
|
|
55
|
+
headers = parser.headers
|
|
56
|
+
unless headers && !headers.empty?
|
|
57
|
+
connection.send_error(stream, 400, "Bad Request") if stream.writable?
|
|
58
|
+
return
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
method = headers[":method"]
|
|
62
|
+
|
|
63
|
+
if @configuration.early_data_policy == :reject &&
|
|
64
|
+
early_data && !SAFE_METHODS.include?(method)
|
|
65
|
+
connection.send_error(stream, 425, "Too Early") if stream.writable?
|
|
66
|
+
return
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
request, body = @adapter.build_request(headers)
|
|
70
|
+
request.headers.add("quicsilver-early-data", early_data.to_s)
|
|
71
|
+
|
|
72
|
+
# Wire interim_response so apps can send 103 Early Hints.
|
|
73
|
+
# Falcon mode: app calls request.send_interim_response(103, headers)
|
|
74
|
+
# Rack mode: bridged to rack.early_hints via EarlyHintsMiddleware
|
|
75
|
+
request.interim_response = ->(status, headers) {
|
|
76
|
+
connection.send_informational(stream, status, headers)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if body && parser.body && parser.body.size > 0
|
|
80
|
+
parser.body.rewind
|
|
81
|
+
body_data = parser.body.read
|
|
82
|
+
body.write(body_data) unless body_data.empty?
|
|
83
|
+
end
|
|
84
|
+
body&.close_write
|
|
85
|
+
|
|
86
|
+
connection.apply_stream_priority(stream, parser.priority)
|
|
87
|
+
|
|
88
|
+
@request_registry.track(
|
|
89
|
+
stream.stream_id, connection.handle,
|
|
90
|
+
path: headers[":path"] || "/", method: method || "GET"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
request
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def send_response(connection, stream, request, response)
|
|
97
|
+
if cancelled_stream?(stream.stream_id)
|
|
98
|
+
Quicsilver.logger.debug("Skipping response for cancelled stream #{stream.stream_id}")
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
raise "Stream handle not found for stream #{stream.stream_id}" unless stream.writable?
|
|
103
|
+
|
|
104
|
+
headers = response.headers
|
|
105
|
+
|
|
106
|
+
# Extract trailers before flattening (Protocol::HTTP::Headers tracks
|
|
107
|
+
# trailer! state that a plain Hash would lose — needed for gRPC).
|
|
108
|
+
trailers = if headers.respond_to?(:trailer?) && headers.trailer?
|
|
109
|
+
trailer_hash = {}
|
|
110
|
+
headers.trailer.each { |name, value| trailer_hash[name] = value }
|
|
111
|
+
trailer_hash
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
response_headers = {}
|
|
115
|
+
if headers.respond_to?(:header)
|
|
116
|
+
headers.header.each { |name, value| response_headers[name] = value }
|
|
117
|
+
else
|
|
118
|
+
headers&.each { |name, value| response_headers[name] = value }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Protocol-rack moves content-length from headers to body.length —
|
|
122
|
+
# re-add it so the HTTP/3 response includes the header.
|
|
123
|
+
if !response_headers.key?("content-length") && response.body&.length
|
|
124
|
+
response_headers["content-length"] = response.body.length.to_s
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
body = response.body || []
|
|
128
|
+
connection.send_response(stream, response.status, response_headers, body,
|
|
129
|
+
head_request: request.head?, trailers: trailers)
|
|
130
|
+
@request_registry.complete(stream.stream_id)
|
|
131
|
+
end
|
|
132
|
+
|
|
81
133
|
def cancelled_stream?(stream_id)
|
|
82
134
|
@cancelled_mutex.synchronize { @cancelled_streams.include?(stream_id) }
|
|
83
135
|
end
|