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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 68d7513a831a0ea2e4b4bf55400d1e1d464a5c16aa7577ba2c6c7937d0a62019
4
- data.tar.gz: dad97d0529ef59f2e233da2a7042b6eeacbc95f77328ac0172ad45fcc24fd9db
3
+ metadata.gz: 1a92a6898bad6aab7de417a5eff15af74a99a41d33e380fd93447f8b15d77ba5
4
+ data.tar.gz: 473061a7d89df0b3956bc396a29102f76f6205b7e3795a452244f30f1f84da3e
5
5
  SHA512:
6
- metadata.gz: eb4574ee23c377e4919f64f8c84d8798e3f52224788555456cac185e923694bc9dec8647a2e495727652aa398ca44ce1bb5dc4ef57b850f066c1621f0714dcde
7
- data.tar.gz: efed6f174ab3a74b6fb33d110ed53cb6cd56fef824a928723800b78c7629b620ef6ee647138cdd22b3e8094235eb49b484e7a2a6c88c1e4c9d3fedac354768dc
6
+ metadata.gz: c76936ffedc6058d29c946a526217f47840248b0e7a1042a2fee816a3488f79aa2a848621f3c443d5d58eed16df8c3f013906fc3f34730eb71a24da6b2022287
7
+ data.tar.gz: e9b25fdf0d6d1481d12faae276941d80d8910361e0916f22d617b059982c3a56cc7310579e63c0b0466797181b2976eb4213b9650b613be55717f7d05060c250
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 4.0.7
1
+ 4.1-dev
@@ -4,7 +4,7 @@ RSpec.describe Server do
4
4
  before do
5
5
  @tcpserver = TCPServer.open(PORT)
6
6
 
7
- Ractor.new(@tcpserver) do |socket|
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(socket)
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
- # | Ractor.new(server, socket) | | |
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
- # | Ractor.new(socket.accept, @proc) --+------>| Ractor.new(tx, stream_id, proc) -----------+ |
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, socket) { |s, sock| s.run(sock) }
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
@@ -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
- self.class.do_send(io, Frame::Goaway.new(@streams_ctx.last_stream_id, ErrorCode::INTERNAL_ERROR, 'internal error'), true)
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(io, @port) do |io_, port_|
60
- loop do
61
- obj = Frame.read(io_)
62
- if obj.nil?
63
- port_.send(:eof, move: true)
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
- break if @recv_done && @streams_ctx.empty?
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 obj.is_a?(StreamError) # terminate the Stream Ractor waiting for a request
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 obj.is_a?(StreamError) # terminate the Stream Ractor waiting for a request
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
- obj = decoder.decode(fragment)
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
@@ -8,6 +8,8 @@ module Biryani
8
8
 
9
9
  # @param s [String]
10
10
  #
11
+ # @raise [HPACK::Error::Error]
12
+ #
11
13
  # @return [Array]
12
14
  def decode(s)
13
15
  Fields.decode(s.force_encoding(Encoding::ASCII_8BIT), @dynamic_table)
@@ -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]
@@ -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 unless cursor.zero? || (io.get_value(:U8, 0) & 0b00100000).positive? && Integer.decode(io, 5, 0)[1] == cursor
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
 
@@ -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]
@@ -8,11 +8,15 @@ module Biryani
8
8
  # @param socket [Socket]
9
9
  def run(socket)
10
10
  loop do
11
- Ractor.new(socket.accept, @proc) do |io, proc|
11
+ io = socket.accept
12
+ r = Ractor.new(@proc) do |proc|
13
+ io_ = Ractor.receive
12
14
  conn = Connection.new(proc)
13
- conn.serve(io)
14
- io.close
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
@@ -18,7 +18,11 @@ module Biryani
18
18
  res = HTTP::Response.internal_server_error
19
19
  end
20
20
 
21
- tx.send([:response, res, stream_id], move: true)
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
@@ -1,3 +1,3 @@
1
1
  module Biryani
2
- VERSION = '0.0.15'.freeze
2
+ VERSION = '0.0.16'.freeze
3
3
  end
data/lib/biryani.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'timeout'
1
2
  require 'uri'
2
3
 
3
4
  require_relative 'biryani/connection_error'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: biryani
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.15
4
+ version: 0.0.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - thekuwayama