biryani 0.0.15 → 0.0.16
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/.ruby-version +1 -1
- data/conformance/server_spec.rb +3 -2
- data/example/sqlite3.rb +4 -3
- data/lib/biryani/connection.rb +60 -22
- data/lib/biryani/frame.rb +4 -1
- data/lib/biryani/hpack/decoder.rb +2 -0
- data/lib/biryani/hpack/dynamic_table.rb +1 -1
- data/lib/biryani/hpack/field.rb +3 -2
- data/lib/biryani/hpack/huffman.rb +2 -2
- data/lib/biryani/hpack/integer.rb +11 -0
- data/lib/biryani/hpack/string.rb +7 -0
- data/lib/biryani/server.rb +7 -3
- data/lib/biryani/stream.rb +5 -1
- data/lib/biryani/streams_context.rb +5 -0
- data/lib/biryani/utils.rb +9 -0
- data/lib/biryani/version.rb +1 -1
- data/lib/biryani.rb +1 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1a92a6898bad6aab7de417a5eff15af74a99a41d33e380fd93447f8b15d77ba5
|
|
4
|
+
data.tar.gz: 473061a7d89df0b3956bc396a29102f76f6205b7e3795a452244f30f1f84da3e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c76936ffedc6058d29c946a526217f47840248b0e7a1042a2fee816a3488f79aa2a848621f3c443d5d58eed16df8c3f013906fc3f34730eb71a24da6b2022287
|
|
7
|
+
data.tar.gz: e9b25fdf0d6d1481d12faae276941d80d8910361e0916f22d617b059982c3a56cc7310579e63c0b0466797181b2976eb4213b9650b613be55717f7d05060c250
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.
|
|
1
|
+
4.1-dev
|
data/conformance/server_spec.rb
CHANGED
|
@@ -4,7 +4,7 @@ RSpec.describe Server do
|
|
|
4
4
|
before do
|
|
5
5
|
@tcpserver = TCPServer.open(PORT)
|
|
6
6
|
|
|
7
|
-
Ractor.new
|
|
7
|
+
r = Ractor.new do
|
|
8
8
|
server = Server.new(
|
|
9
9
|
Ractor.shareable_proc do |_, res|
|
|
10
10
|
res.status = 200
|
|
@@ -12,8 +12,9 @@ RSpec.describe Server do
|
|
|
12
12
|
end
|
|
13
13
|
)
|
|
14
14
|
|
|
15
|
-
server.run(
|
|
15
|
+
server.run(Ractor.receive)
|
|
16
16
|
end
|
|
17
|
+
r.send(@tcpserver.dup, move: true)
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
let(:junit_report_file_path) do
|
data/example/sqlite3.rb
CHANGED
|
@@ -20,13 +20,13 @@ require 'biryani'
|
|
|
20
20
|
# | DB_PORT.receive <---------------+-----------+--+---- DB_PORT.send | |
|
|
21
21
|
# | reply.send ---------------------+-----------+--+---> reply.receive | |
|
|
22
22
|
# | | | +------------------------------------------+ |
|
|
23
|
-
# |
|
|
23
|
+
# | r.send(socket, move: true) | | |
|
|
24
24
|
# +----+-------------------------------+ +-------------------------------------------------+
|
|
25
25
|
# | ^
|
|
26
26
|
# v |
|
|
27
27
|
# +-- server Ractor ----------------------+ +-- connection Ractor (1 Ractor / connection) --+--+
|
|
28
28
|
# | server.run(socket) | | Stream.new(tx, stream_id, proc) | |
|
|
29
|
-
# |
|
|
29
|
+
# | r.send(io, move: true) ------------+------>| Ractor.new(tx, stream_id, proc) -----------+ |
|
|
30
30
|
# +---------------------------------------+ +--------------------------------------------------+
|
|
31
31
|
DB_PORT = Ractor::Port.new
|
|
32
32
|
|
|
@@ -159,7 +159,8 @@ server = Biryani::Server.new(
|
|
|
159
159
|
App.call(req, res)
|
|
160
160
|
end
|
|
161
161
|
)
|
|
162
|
-
Ractor.new(server
|
|
162
|
+
r = Ractor.new(server) { |s| s.run(Ractor.receive) }
|
|
163
|
+
r.send(socket, move: true)
|
|
163
164
|
DB.serve(path)
|
|
164
165
|
|
|
165
166
|
# $ bundle exec ruby sqlite3.rb
|
data/lib/biryani/connection.rb
CHANGED
|
@@ -14,8 +14,10 @@ module Biryani
|
|
|
14
14
|
CONNECTION_PREFACE_LENGTH = CONNECTION_PREFACE.length
|
|
15
15
|
DEFAULT_HEADER_TABLE_SIZE = 4_096
|
|
16
16
|
INITIAL_CONNECTION_WINDOW_SIZE = 65_535
|
|
17
|
+
LINGER_TIMEOUT = 5
|
|
17
18
|
|
|
18
|
-
private_constant :CONNECTION_PREFACE, :CONNECTION_PREFACE_LENGTH, :DEFAULT_HEADER_TABLE_SIZE, :INITIAL_CONNECTION_WINDOW_SIZE
|
|
19
|
+
private_constant :CONNECTION_PREFACE, :CONNECTION_PREFACE_LENGTH, :DEFAULT_HEADER_TABLE_SIZE, :INITIAL_CONNECTION_WINDOW_SIZE,
|
|
20
|
+
:LINGER_TIMEOUT
|
|
19
21
|
Ractor.make_shareable(CONNECTION_PREFACE)
|
|
20
22
|
Ractor.make_shareable(CONNECTION_PREFACE_LENGTH)
|
|
21
23
|
|
|
@@ -47,32 +49,33 @@ module Biryani
|
|
|
47
49
|
|
|
48
50
|
recv_loop(io.clone)
|
|
49
51
|
select_loop(io)
|
|
52
|
+
rescue *PEER_CLOSED_ERRORS
|
|
53
|
+
# peer has already closed the connection
|
|
50
54
|
rescue StandardError => e
|
|
51
55
|
puts e.backtrace
|
|
52
|
-
|
|
56
|
+
Biryani.ignore_peer_closed do
|
|
57
|
+
self.class.do_send(io, Frame::Goaway.new(@streams_ctx.last_stream_id, ErrorCode::INTERNAL_ERROR, 'internal error'), true)
|
|
58
|
+
end
|
|
53
59
|
ensure
|
|
54
|
-
io.close_write
|
|
60
|
+
Biryani.ignore_peer_closed { io.close_write }
|
|
55
61
|
end
|
|
56
62
|
|
|
57
63
|
# @param io [IO]
|
|
58
64
|
def recv_loop(io)
|
|
59
|
-
Ractor.new(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
break
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
port_.send([:frame, obj], move: true)
|
|
68
|
-
end
|
|
65
|
+
r = Ractor.new(@port) do |port_|
|
|
66
|
+
io_ = Ractor.receive
|
|
67
|
+
Connection.recv_frames(io_, port_)
|
|
68
|
+
ensure
|
|
69
|
+
Biryani.ignore_peer_closed { io_&.close }
|
|
69
70
|
end
|
|
71
|
+
r.send(io, move: true)
|
|
70
72
|
end
|
|
71
73
|
|
|
72
74
|
# @param io [IO]
|
|
73
75
|
def select_loop(io)
|
|
74
76
|
loop do
|
|
75
|
-
|
|
77
|
+
# After EOF, no frames arrive from the peer, so only half_closed_remote streams can still progress: by the handler's response.
|
|
78
|
+
break if closed? || (@recv_done && @streams_ctx.none_half_closed_remote?)
|
|
76
79
|
|
|
77
80
|
case @port.receive
|
|
78
81
|
in :eof
|
|
@@ -82,8 +85,6 @@ module Biryani
|
|
|
82
85
|
in [:response, res, stream_id]
|
|
83
86
|
handle_response(io, res, stream_id)
|
|
84
87
|
end
|
|
85
|
-
|
|
86
|
-
break if closed?
|
|
87
88
|
end
|
|
88
89
|
end
|
|
89
90
|
|
|
@@ -236,6 +237,14 @@ module Biryani
|
|
|
236
237
|
# @param res [HTTP::Response]
|
|
237
238
|
# @param stream_id [Integer]
|
|
238
239
|
def handle_response(io, res, stream_id)
|
|
240
|
+
ctx = @streams_ctx[stream_id]
|
|
241
|
+
# The RST_STREAM frame fully terminates the referenced stream and
|
|
242
|
+
# causes it to enter the "closed" state. After receiving a RST_STREAM
|
|
243
|
+
# on a stream, the receiver MUST NOT send additional frames for that
|
|
244
|
+
# stream, except for PRIORITY.
|
|
245
|
+
# https://datatracker.ietf.org/doc/html/rfc9113#section-6.4-5
|
|
246
|
+
return if ctx.nil? || ctx.closed?
|
|
247
|
+
|
|
239
248
|
fragment, data = self.class.http_response(res, @encoder)
|
|
240
249
|
max_frame_size = @peer_settings[SettingsID::SETTINGS_MAX_FRAME_SIZE]
|
|
241
250
|
self.class.send_headers(io, stream_id, fragment, data.empty?, max_frame_size, @streams_ctx)
|
|
@@ -360,6 +369,35 @@ module Biryani
|
|
|
360
369
|
ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid connection preface') if s != CONNECTION_PREFACE
|
|
361
370
|
end
|
|
362
371
|
|
|
372
|
+
# @param io [IO]
|
|
373
|
+
# @param port [Ractor::Port]
|
|
374
|
+
def self.recv_frames(io, port)
|
|
375
|
+
loop do
|
|
376
|
+
obj = Frame.read(io)
|
|
377
|
+
break if obj.nil?
|
|
378
|
+
|
|
379
|
+
port.send([:frame, obj], move: true)
|
|
380
|
+
rescue Ractor::ClosedError
|
|
381
|
+
# the connection has finished before the peer closes
|
|
382
|
+
return linger(io)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
port.send(:eof, move: true)
|
|
386
|
+
rescue Ractor::ClosedError
|
|
387
|
+
# the connection has finished after the peer closes
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Lingering close: discard incoming data until EOF so that close does not send RST.
|
|
391
|
+
#
|
|
392
|
+
# @param io [IO]
|
|
393
|
+
def self.linger(io)
|
|
394
|
+
Timeout.timeout(LINGER_TIMEOUT) do
|
|
395
|
+
loop { io.readpartial(16_384) }
|
|
396
|
+
end
|
|
397
|
+
rescue *PEER_CLOSED_ERRORS, Timeout::Error
|
|
398
|
+
# EOF, timeout, connection reset, etc.
|
|
399
|
+
end
|
|
400
|
+
|
|
363
401
|
# @param stream_id [Integer]
|
|
364
402
|
# @param data [String]
|
|
365
403
|
# @param recv_window [Window]
|
|
@@ -377,7 +415,7 @@ module Biryani
|
|
|
377
415
|
ctx.content << data
|
|
378
416
|
if ctx.half_closed_remote?
|
|
379
417
|
obj = http_request(stream_id, ctx.fragment, ctx.content, decoder)
|
|
380
|
-
ctx << nil if
|
|
418
|
+
ctx << nil if Biryani.err?(obj) # terminate the Stream Ractor waiting for a request
|
|
381
419
|
return obj if Biryani.err?(obj)
|
|
382
420
|
|
|
383
421
|
ctx << obj
|
|
@@ -400,7 +438,7 @@ module Biryani
|
|
|
400
438
|
ctx.fragment << headers.fragment
|
|
401
439
|
if ctx.half_closed_remote?
|
|
402
440
|
obj = http_request(headers.stream_id, ctx.fragment, ctx.content, decoder)
|
|
403
|
-
ctx << nil if
|
|
441
|
+
ctx << nil if Biryani.err?(obj) # terminate the Stream Ractor waiting for a request
|
|
404
442
|
return obj if Biryani.err?(obj)
|
|
405
443
|
|
|
406
444
|
ctx << obj
|
|
@@ -475,13 +513,13 @@ module Biryani
|
|
|
475
513
|
#
|
|
476
514
|
# @return [HTTP::Request, ConnectionError, StreamError]
|
|
477
515
|
def self.http_request(stream_id, fragment, content, decoder)
|
|
478
|
-
|
|
479
|
-
return obj if Biryani.err?(obj)
|
|
480
|
-
|
|
481
|
-
fields = obj
|
|
516
|
+
fields = decoder.decode(fragment)
|
|
482
517
|
builder = HTTP::RequestBuilder.new
|
|
483
518
|
builder.fields(fields)
|
|
484
519
|
builder.build(content)
|
|
520
|
+
rescue HPACK::Error::Error => e
|
|
521
|
+
# https://datatracker.ietf.org/doc/html/rfc9113#section-4.3-3
|
|
522
|
+
ConnectionError.new(ErrorCode::COMPRESSION_ERROR, e.message)
|
|
485
523
|
rescue HTTP::Error::MalformedRequestError => e
|
|
486
524
|
# https://datatracker.ietf.org/doc/html/rfc9113#section-8.1.1-3
|
|
487
525
|
StreamError.new(ErrorCode::PROTOCOL_ERROR, stream_id, e.message)
|
data/lib/biryani/frame.rb
CHANGED
|
@@ -157,10 +157,13 @@ module Biryani
|
|
|
157
157
|
|
|
158
158
|
payload_length, f_type, flags, stream_id = read_header(s)
|
|
159
159
|
payload = io.read(payload_length)
|
|
160
|
-
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if payload.bytesize != payload_length
|
|
160
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if payload.nil? || payload.bytesize != payload_length
|
|
161
161
|
return Frame::Unknown.new(f_type, flags, stream_id, payload) unless FRAME_MAP.key?(f_type)
|
|
162
162
|
|
|
163
163
|
FRAME_MAP[f_type].read(payload, flags, stream_id)
|
|
164
|
+
rescue *PEER_CLOSED_ERRORS
|
|
165
|
+
# the peer has closed the connection (e.g. ECONNRESET): treat it as EOF
|
|
166
|
+
nil
|
|
164
167
|
rescue StandardError
|
|
165
168
|
ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'invalid frame')
|
|
166
169
|
end
|
|
@@ -51,7 +51,7 @@ module Biryani
|
|
|
51
51
|
#
|
|
52
52
|
# @return [Array] [name, value]
|
|
53
53
|
def entry_at(index)
|
|
54
|
-
raise Error::HPACKDecodeError if index.zero? || index > STATIC_TABLE_SIZE + count_entries
|
|
54
|
+
raise Error::HPACKDecodeError, "invalid index #{index}" if index.zero? || index > STATIC_TABLE_SIZE + count_entries
|
|
55
55
|
|
|
56
56
|
if index <= STATIC_TABLE_SIZE
|
|
57
57
|
STATIC_TABLE[index - 1]
|
data/lib/biryani/hpack/field.rb
CHANGED
|
@@ -215,10 +215,11 @@ module Biryani
|
|
|
215
215
|
# @return [nil]
|
|
216
216
|
# @return [Integer]
|
|
217
217
|
def self.decode_dynamic_table_size_update(io, cursor, dynamic_table)
|
|
218
|
-
raise Error::HPACKDecodeError
|
|
218
|
+
raise Error::HPACKDecodeError, 'misplaced dynamic table size update' \
|
|
219
|
+
unless cursor.zero? || (io.get_value(:U8, 0) & 0b00100000).positive? && Integer.decode(io, 5, 0)[1] == cursor
|
|
219
220
|
|
|
220
221
|
max_size, c = Integer.decode(io, 5, cursor)
|
|
221
|
-
raise Error::HPACKDecodeError if max_size > dynamic_table.limit
|
|
222
|
+
raise Error::HPACKDecodeError, 'dynamic table size update exceeds the limit' if max_size > dynamic_table.limit
|
|
222
223
|
|
|
223
224
|
dynamic_table.chomp!(max_size)
|
|
224
225
|
[nil, c]
|
|
@@ -550,7 +550,7 @@ module Biryani
|
|
|
550
550
|
(length * 8).times do |i|
|
|
551
551
|
bits_len += 1
|
|
552
552
|
bits += 1 if (bytes[i / 8] & (1 << 7 - (i % 8))).positive?
|
|
553
|
-
raise Error::HuffmanDecodeError if bits == EOS
|
|
553
|
+
raise Error::HuffmanDecodeError, 'EOS in huffman-encoded string' if bits == EOS
|
|
554
554
|
|
|
555
555
|
if (chr = DECODE_TABLE[[bits, bits_len]])
|
|
556
556
|
res << chr
|
|
@@ -560,7 +560,7 @@ module Biryani
|
|
|
560
560
|
bits <<= 1
|
|
561
561
|
end
|
|
562
562
|
end
|
|
563
|
-
raise Error::HuffmanDecodeError if bits_len.positive? && bits != (2 << bits_len) - 2 || bits_len >= 8
|
|
563
|
+
raise Error::HuffmanDecodeError, 'invalid huffman padding' if bits_len.positive? && bits != (2 << bits_len) - 2 || bits_len >= 8
|
|
564
564
|
|
|
565
565
|
res
|
|
566
566
|
end
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
module Biryani
|
|
2
2
|
module HPACK
|
|
3
3
|
module Integer
|
|
4
|
+
# https://datatracker.ietf.org/doc/html/rfc7541#section-7.4
|
|
5
|
+
MAX_DECODED_INTEGER = (1 << 32) - 1
|
|
6
|
+
private_constant :MAX_DECODED_INTEGER
|
|
7
|
+
|
|
4
8
|
# if I < 2^N - 1, encode I on N bits
|
|
5
9
|
# else
|
|
6
10
|
# encode (2^N - 1) on N bits
|
|
@@ -44,10 +48,14 @@ module Biryani
|
|
|
44
48
|
# @param cursor [Integer]
|
|
45
49
|
# @param n [Integer]
|
|
46
50
|
#
|
|
51
|
+
# @raise [HPACKDecodeError]
|
|
52
|
+
#
|
|
47
53
|
# @return [Integer]
|
|
48
54
|
# @return [Integer]
|
|
49
55
|
def self.decode(io, n, cursor)
|
|
50
56
|
limit = (1 << n) - 1
|
|
57
|
+
raise Error::HPACKDecodeError, 'integer exceeds field block' if cursor >= io.size
|
|
58
|
+
|
|
51
59
|
h = io.get_value(:U8, cursor)
|
|
52
60
|
return [h & limit, cursor + 1] if (h & limit) != limit
|
|
53
61
|
|
|
@@ -55,8 +63,11 @@ module Biryani
|
|
|
55
63
|
c = cursor + 1
|
|
56
64
|
i = 0
|
|
57
65
|
loop do
|
|
66
|
+
raise Error::HPACKDecodeError, 'integer exceeds field block' if c + i >= io.size
|
|
67
|
+
|
|
58
68
|
byte = io.get_value(:U8, c + i)
|
|
59
69
|
res += (byte & 127) << (i * 7)
|
|
70
|
+
raise Error::HPACKDecodeError, 'integer overflow' if res > MAX_DECODED_INTEGER
|
|
60
71
|
|
|
61
72
|
break if (byte & 128).zero?
|
|
62
73
|
|
data/lib/biryani/hpack/string.rb
CHANGED
|
@@ -26,11 +26,18 @@ module Biryani
|
|
|
26
26
|
# @param io [IO::Buffer]
|
|
27
27
|
# @param cursor [Integer]
|
|
28
28
|
#
|
|
29
|
+
# @raise [HPACKDecodeError]
|
|
30
|
+
# @raise [HuffmanDecodeError]
|
|
31
|
+
#
|
|
29
32
|
# @return [String]
|
|
30
33
|
# @return [Integer]
|
|
31
34
|
def self.decode(io, cursor)
|
|
35
|
+
raise Error::HPACKDecodeError, 'string exceeds field block' if cursor >= io.size
|
|
36
|
+
|
|
32
37
|
h = (io.get_value(:U8, cursor) & 0b10000000).positive?
|
|
33
38
|
len, c = Integer.decode(io, 7, cursor)
|
|
39
|
+
raise Error::HPACKDecodeError, 'string exceeds field block' if c + len > io.size
|
|
40
|
+
|
|
34
41
|
return [Huffman.decode(io, c, len), c + len] if h
|
|
35
42
|
|
|
36
43
|
[io.get_string(c, len), c + len]
|
data/lib/biryani/server.rb
CHANGED
|
@@ -8,11 +8,15 @@ module Biryani
|
|
|
8
8
|
# @param socket [Socket]
|
|
9
9
|
def run(socket)
|
|
10
10
|
loop do
|
|
11
|
-
|
|
11
|
+
io = socket.accept
|
|
12
|
+
r = Ractor.new(@proc) do |proc|
|
|
13
|
+
io_ = Ractor.receive
|
|
12
14
|
conn = Connection.new(proc)
|
|
13
|
-
conn.serve(
|
|
14
|
-
|
|
15
|
+
conn.serve(io_)
|
|
16
|
+
ensure
|
|
17
|
+
Biryani.ignore_peer_closed { io_&.close }
|
|
15
18
|
end
|
|
19
|
+
r.send(io, move: true)
|
|
16
20
|
end
|
|
17
21
|
end
|
|
18
22
|
end
|
data/lib/biryani/stream.rb
CHANGED
|
@@ -18,7 +18,11 @@ module Biryani
|
|
|
18
18
|
res = HTTP::Response.internal_server_error
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
begin
|
|
22
|
+
tx.send([:response, res, stream_id], move: true)
|
|
23
|
+
rescue Ractor::ClosedError
|
|
24
|
+
# the Connection has already finished, so discard the response
|
|
25
|
+
end
|
|
22
26
|
end
|
|
23
27
|
end
|
|
24
28
|
end
|
|
@@ -50,6 +50,11 @@ module Biryani
|
|
|
50
50
|
@h.values.filter(&:active?).length
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
# @return [Boolean]
|
|
54
|
+
def none_half_closed_remote?
|
|
55
|
+
@h.each_value.none?(&:half_closed_remote?)
|
|
56
|
+
end
|
|
57
|
+
|
|
53
58
|
# @return [Integer, nil]
|
|
54
59
|
def receiving_continuation_stream_id
|
|
55
60
|
@h.find { |_, ctx| ctx.receiving_continuation? }&.first
|
data/lib/biryani/utils.rb
CHANGED
|
@@ -20,4 +20,13 @@ module Biryani
|
|
|
20
20
|
obj
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
|
+
|
|
24
|
+
# Errors raised when the peer has already closed the connection (EOF, EPIPE, ECONNRESET, etc.)
|
|
25
|
+
PEER_CLOSED_ERRORS = Ractor.make_shareable([IOError, SystemCallError])
|
|
26
|
+
|
|
27
|
+
def self.ignore_peer_closed
|
|
28
|
+
yield
|
|
29
|
+
rescue *PEER_CLOSED_ERRORS
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
23
32
|
end
|
data/lib/biryani/version.rb
CHANGED
data/lib/biryani.rb
CHANGED