biryani 0.0.8 → 0.0.10

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/README.md +2 -2
  4. data/biryani.gemspec +5 -2
  5. data/example/echo.rb +2 -2
  6. data/example/hello_world.rb +2 -2
  7. data/lib/biryani/connection.rb +62 -47
  8. data/lib/biryani/connection_error.rb +1 -1
  9. data/lib/biryani/frame/goaway.rb +5 -3
  10. data/lib/biryani/hpack/field.rb +5 -9
  11. data/lib/biryani/http/error.rb +10 -0
  12. data/lib/biryani/http/request.rb +97 -0
  13. data/lib/biryani/http/response.rb +68 -0
  14. data/lib/biryani/http.rb +3 -0
  15. data/lib/biryani/state.rb +1 -1
  16. data/lib/biryani/stream.rb +3 -3
  17. data/lib/biryani/streams_context.rb +16 -16
  18. data/lib/biryani/version.rb +1 -1
  19. data/lib/biryani.rb +1 -3
  20. metadata +7 -77
  21. data/.github/workflows/ci.yml +0 -30
  22. data/.github/workflows/conformance.yml +0 -46
  23. data/.gitignore +0 -18
  24. data/lib/biryani/error.rb +0 -8
  25. data/lib/biryani/http_request.rb +0 -95
  26. data/lib/biryani/http_response.rb +0 -66
  27. data/spec/connection/handle_connection_window_update_spec.rb +0 -16
  28. data/spec/connection/handle_data_spec.rb +0 -58
  29. data/spec/connection/handle_headers_spec.rb +0 -19
  30. data/spec/connection/handle_ping_spec.rb +0 -21
  31. data/spec/connection/handle_rst_stream_spec.rb +0 -16
  32. data/spec/connection/handle_settings_spec.rb +0 -37
  33. data/spec/connection/handle_stream_window_update_spec.rb +0 -20
  34. data/spec/connection/read_http2_magic_spec.rb +0 -26
  35. data/spec/connection/send_spec.rb +0 -104
  36. data/spec/connection/transition_stream_state_send_spec.rb +0 -39
  37. data/spec/data_buffer_spec.rb +0 -135
  38. data/spec/frame/continuation_spec.rb +0 -39
  39. data/spec/frame/data_spec.rb +0 -25
  40. data/spec/frame/goaway_spec.rb +0 -23
  41. data/spec/frame/headers_spec.rb +0 -52
  42. data/spec/frame/ping_spec.rb +0 -22
  43. data/spec/frame/priority_spec.rb +0 -22
  44. data/spec/frame/push_promise_spec.rb +0 -24
  45. data/spec/frame/read_spec.rb +0 -30
  46. data/spec/frame/rst_stream_spec.rb +0 -21
  47. data/spec/frame/settings_spec.rb +0 -23
  48. data/spec/frame/window_update_spec.rb +0 -21
  49. data/spec/hpack/decoder_spec.rb +0 -170
  50. data/spec/hpack/encoder_spec.rb +0 -48
  51. data/spec/hpack/field_spec.rb +0 -43
  52. data/spec/hpack/fields_spec.rb +0 -17
  53. data/spec/hpack/huffman_spec.rb +0 -20
  54. data/spec/hpack/integer_spec.rb +0 -27
  55. data/spec/hpack/string_spec.rb +0 -19
  56. data/spec/http_request_spec.rb +0 -62
  57. data/spec/http_response_spec.rb +0 -12
  58. data/spec/spec_helper.rb +0 -9
  59. data/spec/streams_context_spec.rb +0 -79
  60. data/spec/utils_spec.rb +0 -41
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 62514e8d943c7f5b5c21f863faabb9e45ec813225f6b5f0b60f2afb5c784c1ce
4
- data.tar.gz: 9767186d0fbef80e454f7e5a5fb3183cf36637160bb73c969bf9ce20075764d0
3
+ metadata.gz: 1aa3fb4dd49abcba22df142cd67d38c5eedc6c8beb6ca8fbcb130147c7e7506a
4
+ data.tar.gz: 54bd5a493c027047eeaada80b4ff938ab806c29b80069a6cc112a54f5bb78efc
5
5
  SHA512:
6
- metadata.gz: 3127dc1a2ac8bd3b7c46c7cb6bab33f62a3f74aee3a9a645a1e3afe08705ceee7c35365f7e3d4374c08f80c473fc6dd712ff4cdc0713d1cc9e0da17605294a19
7
- data.tar.gz: 2af8f94e69c662da0b69a39fa8c41adcc506352a9db81bc4c469283f457fd724a5d56cd966916648a44d3a12bd8f392f9fb816010d28c141b0a7d611d7070ba7
6
+ metadata.gz: 3a8c93ee8ce069cfe77854f7792abadf528addc7e258cd55c84f1b8b2da0b25b278b768e3c8b41a0897a9adc1ee497a9cac4d2ced4c3dd5c644d17f9eb1327c8
7
+ data.tar.gz: 2909206008b7794ce6ec3a7c850acf66c475c33bc543d55defd9222d8f606fb661d31b99443cc4ba7a0d3d1da920d3a85209d0f8e0cca3de1cd611ccc43634ac
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 4.0.1
1
+ 4.0.2
data/README.md CHANGED
@@ -32,8 +32,8 @@ port = ARGV[0] || 8888
32
32
  socket = TCPServer.new(port)
33
33
 
34
34
  server = Biryani::Server.new(
35
- # @param _req [Biryani::HTTPRequest]
36
- # @param res [Biryani::HTTPResponse]
35
+ # @param _req [Biryani::HTTP::Request]
36
+ # @param res [Biryani::HTTP::Response]
37
37
  Ractor.shareable_proc do |_req, res|
38
38
  res.status = 200
39
39
  res.content = 'Hello, world!'
data/biryani.gemspec CHANGED
@@ -13,8 +13,11 @@ Gem::Specification.new do |spec|
13
13
  spec.license = 'MIT'
14
14
  spec.required_ruby_version = '>=4.0'
15
15
 
16
- spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
17
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
16
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
17
+ `git ls-files -z`.split("\x0").reject do |f|
18
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
19
+ end
20
+ end
18
21
  spec.require_paths = ['lib']
19
22
 
20
23
  spec.add_development_dependency 'bundler'
data/example/echo.rb CHANGED
@@ -9,8 +9,8 @@ port = ARGV[0] || 8888
9
9
  socket = TCPServer.new(port)
10
10
 
11
11
  server = Biryani::Server.new(
12
- # @param req [Biryani::HTTPRequest]
13
- # @param res [Biryani::HTTPResponse]
12
+ # @param req [Biryani::HTTP::Request]
13
+ # @param res [Biryani::HTTP::Response]
14
14
  Ractor.shareable_proc do |req, res|
15
15
  res.status = 200
16
16
  res.content = if req.method.upcase == 'POST'
@@ -9,8 +9,8 @@ port = ARGV[0] || 8888
9
9
  socket = TCPServer.new(port)
10
10
 
11
11
  server = Biryani::Server.new(
12
- # @param _req [Biryani::HTTPRequest]
13
- # @param res [Biryani::HTTPResponse]
12
+ # @param _req [Biryani::HTTP::Request]
13
+ # @param res [Biryani::HTTP::Response]
14
14
  Ractor.shareable_proc do |_req, res|
15
15
  res.status = 200
16
16
  res.content = 'Hello, world!'
@@ -19,7 +19,7 @@ module Biryani
19
19
 
20
20
  # proc [Proc]
21
21
  def initialize(proc)
22
- @sock = nil # Ractor
22
+ @sock = nil # Ractor::Port
23
23
  @proc = proc
24
24
  @streams_ctx = StreamsContext.new(proc)
25
25
  @encoder = HPACK::Encoder.new(4_096)
@@ -43,10 +43,10 @@ module Biryani
43
43
  self.class.do_send(io, Frame::Settings.new(false, 0, {}), true)
44
44
 
45
45
  recv_loop(io.clone)
46
- send_loop(io)
46
+ select_loop(io)
47
47
  rescue StandardError => e
48
48
  puts e.backtrace
49
- self.class.do_send(io, Frame::Goaway.new(0, @streams_ctx.last_stream_id, ErrorCode::INTERNAL_ERROR, 'internal error'), true)
49
+ self.class.do_send(io, Frame::Goaway.new(@streams_ctx.last_stream_id, ErrorCode::INTERNAL_ERROR, 'internal error'), true)
50
50
  ensure
51
51
  io.close_write
52
52
  end
@@ -64,60 +64,59 @@ module Biryani
64
64
  end
65
65
 
66
66
  # @param io [IO]
67
- # rubocop: disable Metrics/AbcSize
68
- # rubocop: disable Metrics/BlockLength
69
- # rubocop: disable Metrics/CyclomaticComplexity
70
- # rubocop: disable Metrics/MethodLength
71
- # rubocop: disable Metrics/PerceivedComplexity
72
- def send_loop(io)
67
+ def select_loop(io)
73
68
  loop do
74
- ports = @streams_ctx.txs
75
- break if ports.empty? && @sock.closed?
69
+ break if @sock.closed? && @streams_ctx.empty?
76
70
 
77
- port, obj = Ractor.select(@sock, *ports)
71
+ port, obj = Ractor.select(@sock, @streams_ctx.tx)
78
72
  if port == @sock
79
- if Biryani.err?(obj)
80
- reply_frame = Biryani.unwrap(obj, @streams_ctx.last_stream_id)
81
- self.class.do_send(io, reply_frame, true)
82
- close if self.class.transition_stream_state_send(reply_frame, @streams_ctx)
83
- elsif obj.length > @settings[SettingsID::SETTINGS_MAX_FRAME_SIZE]
84
- self.class.do_send(io, Frame::Goaway.new(0, @streams_ctx.last_stream_id, ErrorCode::FRAME_SIZE_ERROR, 'payload length greater than SETTINGS_MAX_FRAME_SIZE'), true)
85
- close
86
- else
87
- recv_dispatch(obj).each do |frame|
88
- reply_frame = Biryani.unwrap(frame, @streams_ctx.last_stream_id)
89
- self.class.do_send(io, reply_frame, true)
90
- if reply_frame.f_type == FrameType::WINDOW_UPDATE && reply_frame.stream_id.zero?
91
- @recv_window.increase!(reply_frame.window_size_increment)
92
- elsif reply_frame.f_type == FrameType::WINDOW_UPDATE
93
- @streams_ctx[reply_frame.stream_id].recv_window.increase!(reply_frame.window_size_increment)
94
- end
95
-
96
- close if self.class.transition_stream_state_send(reply_frame, @streams_ctx)
97
- end
98
- end
73
+ recv_dispatch(io, obj)
99
74
  else
100
75
  res, stream_id = obj
101
- fragment, data = self.class.http_response(res, @encoder)
102
- max_frame_size = @peer_settings[SettingsID::SETTINGS_MAX_FRAME_SIZE]
103
- self.class.send_headers(io, stream_id, fragment, data.empty?, max_frame_size, @streams_ctx)
104
- self.class.send_data(io, stream_id, data, @send_window, max_frame_size, @streams_ctx, @data_buffer) unless data.empty?
76
+ handle_response(io, res, stream_id)
105
77
  end
106
78
 
107
- @streams_ctx.remove_closed(@data_buffer)
108
79
  break if closed?
109
80
  end
110
81
  end
82
+
83
+ # @param io [IO]
84
+ # @param obj [Object]
85
+ #
86
+ # @return [Array<Object>, Array<ConnectionError>, Array<StreamError>] frames or errors
87
+ # rubocop: disable Metrics/AbcSize
88
+ # rubocop: disable Metrics/CyclomaticComplexity
89
+ # rubocop: disable Metrics/PerceivedComplexity
90
+ def recv_dispatch(io, obj)
91
+ if Biryani.err?(obj)
92
+ reply_frame = Biryani.unwrap(obj, @streams_ctx.last_stream_id)
93
+ self.class.do_send(io, reply_frame, true)
94
+ close if self.class.transition_stream_state_send(reply_frame, @streams_ctx)
95
+ elsif obj.length > @settings[SettingsID::SETTINGS_MAX_FRAME_SIZE]
96
+ self.class.do_send(io, Frame::Goaway.new(@streams_ctx.last_stream_id, ErrorCode::FRAME_SIZE_ERROR, 'payload length greater than SETTINGS_MAX_FRAME_SIZE'), true)
97
+ close
98
+ else
99
+ do_recv_dispatch(obj).each do |frame|
100
+ reply_frame = Biryani.unwrap(frame, @streams_ctx.last_stream_id)
101
+ self.class.do_send(io, reply_frame, true)
102
+ if reply_frame.f_type == FrameType::WINDOW_UPDATE && reply_frame.stream_id.zero?
103
+ @recv_window.increase!(reply_frame.window_size_increment)
104
+ elsif reply_frame.f_type == FrameType::WINDOW_UPDATE
105
+ @streams_ctx[reply_frame.stream_id].recv_window.increase!(reply_frame.window_size_increment)
106
+ end
107
+
108
+ close if self.class.transition_stream_state_send(reply_frame, @streams_ctx)
109
+ end
110
+ end
111
+ end
111
112
  # rubocop: enable Metrics/AbcSize
112
- # rubocop: enable Metrics/BlockLength
113
113
  # rubocop: enable Metrics/CyclomaticComplexity
114
- # rubocop: enable Metrics/MethodLength
115
114
  # rubocop: enable Metrics/PerceivedComplexity
116
115
 
117
116
  # @param frame [Object]
118
117
  #
119
118
  # @return [Array<Object>, Array<ConnectionError>, Array<StreamError>] frames or errors
120
- def recv_dispatch(frame)
119
+ def do_recv_dispatch(frame)
121
120
  receiving_continuation_stream_id = @streams_ctx.receiving_continuation_stream_id
122
121
  return [ConnectionError.new(ErrorCode::PROTOCOL_ERROR, "invalid frame type #{format('0x%02x', typ)} for stream identifier #{format('0x%02x', stream_id)}")] \
123
122
  if !receiving_continuation_stream_id.nil? && frame.stream_id != receiving_continuation_stream_id
@@ -182,7 +181,7 @@ module Biryani
182
181
  max_streams = @peer_settings[SettingsID::SETTINGS_MAX_CONCURRENT_STREAMS]
183
182
  send_initial_window_size = @peer_settings[SettingsID::SETTINGS_INITIAL_WINDOW_SIZE]
184
183
  recv_initial_window_size = @settings[SettingsID::SETTINGS_INITIAL_WINDOW_SIZE]
185
- obj = self.class.transition_stream_state_recv(frame, @streams_ctx, stream_id, max_streams, send_initial_window_size, recv_initial_window_size)
184
+ obj = self.class.transition_stream_state_recv(frame, @streams_ctx, @data_buffer, stream_id, max_streams, send_initial_window_size, recv_initial_window_size)
186
185
  return [obj] if Biryani.err?(obj)
187
186
 
188
187
  ctx = obj
@@ -225,6 +224,16 @@ module Biryani
225
224
  # rubocop: enable Metrics/CyclomaticComplexity
226
225
  # rubocop: enable Metrics/MethodLength
227
226
 
227
+ # @param io [IO]
228
+ # @param res [HTTP::Response]
229
+ # @param stream_id [Integer]
230
+ def handle_response(io, res, stream_id)
231
+ fragment, data = self.class.http_response(res, @encoder)
232
+ max_frame_size = @peer_settings[SettingsID::SETTINGS_MAX_FRAME_SIZE]
233
+ self.class.send_headers(io, stream_id, fragment, data.empty?, max_frame_size, @streams_ctx)
234
+ self.class.send_data(io, stream_id, data, @send_window, max_frame_size, @streams_ctx, @data_buffer) unless data.empty?
235
+ end
236
+
228
237
  def close
229
238
  @closed = true
230
239
  end
@@ -237,6 +246,7 @@ module Biryani
237
246
  # @param recv_frame [Object]
238
247
  # @param streams_ctx [StreamsContext]
239
248
  # @param stream_id [Integer]
249
+ # @param data_buffer [DataBuffer]
240
250
  # @param max_streams [Integer]
241
251
  # @param send_initial_window_size [Integer]
242
252
  # @param recv_initial_window_size [Integer]
@@ -244,13 +254,18 @@ module Biryani
244
254
  # @return [StreamContext, StreamError, ConnectionError]
245
255
  # rubocop: disable Metrics/CyclomaticComplexity
246
256
  # rubocop: disable Metrics/PerceivedComplexity
247
- def self.transition_stream_state_recv(recv_frame, streams_ctx, stream_id, max_streams, send_initial_window_size, recv_initial_window_size)
257
+ def self.transition_stream_state_recv(recv_frame, streams_ctx, data_buffer, stream_id, max_streams, send_initial_window_size, recv_initial_window_size)
248
258
  ctx = streams_ctx[stream_id]
249
259
  return StreamError.new(ErrorCode::PROTOCOL_ERROR, stream_id, 'exceed max concurrent streams') if ctx.nil? && streams_ctx.count_active + 1 > max_streams
250
260
  return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'even-numbered stream identifier') if ctx.nil? && stream_id.even?
251
261
  return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'new stream identifier is less than the existing stream identifiers') if ctx.nil? && streams_ctx.last_stream_id > stream_id
252
262
 
253
- ctx = streams_ctx.new_context(stream_id, send_initial_window_size, recv_initial_window_size) if ctx.nil?
263
+ if ctx.nil?
264
+ ctx = streams_ctx.new_context(stream_id, send_initial_window_size, recv_initial_window_size)
265
+ # An ideal implementation would wait the RTT before removing the stream.
266
+ streams_ctx.remove_closed(data_buffer)
267
+ end
268
+
254
269
  obj = ctx.state_transition!(recv_frame, :recv)
255
270
  return obj if Biryani.err?(obj)
256
271
 
@@ -445,26 +460,26 @@ module Biryani
445
460
  # @param content [String]
446
461
  # @param decoder [Decoder]
447
462
  #
448
- # @return [HTTPRequest, ConnectionError]
463
+ # @return [HTTP::Request, ConnectionError]
449
464
  def self.http_request(fragment, content, decoder)
450
465
  obj = decoder.decode(fragment)
451
466
  return obj if Biryani.err?(obj)
452
467
 
453
468
  fields = obj
454
- builder = HTTPRequestBuilder.new
469
+ builder = HTTP::RequestBuilder.new
455
470
  err = builder.fields(fields)
456
471
  return err unless err.nil?
457
472
 
458
473
  builder.build(content)
459
474
  end
460
475
 
461
- # @param res [HTTPResponse]
476
+ # @param res [HTTP::Response]
462
477
  # @param encoder [Encoder]
463
478
  #
464
479
  # @return [String] fragment
465
480
  # @return [String] data
466
481
  def self.http_response(res, encoder)
467
- HTTPResponseParser.new(res).parse(encoder)
482
+ HTTP::ResponseParser.new(res).parse(encoder)
468
483
  end
469
484
 
470
485
  # @return [Hash<Integer, Integer>]
@@ -11,7 +11,7 @@ module Biryani
11
11
  #
12
12
  # @return [Goaway]
13
13
  def goaway(last_stream_id)
14
- Frame::Goaway.new(0, last_stream_id, @code, @debug)
14
+ Frame::Goaway.new(last_stream_id, @code, @debug)
15
15
  end
16
16
  end
17
17
  end
@@ -6,9 +6,9 @@ module Biryani
6
6
  # @param last_stream_id [Integer]
7
7
  # @param error_code [Integer]
8
8
  # @param debug [String]
9
- def initialize(stream_id, last_stream_id, error_code, debug)
9
+ def initialize(last_stream_id, error_code, debug)
10
10
  @f_type = FrameType::GOAWAY
11
- @stream_id = stream_id
11
+ @stream_id = 0
12
12
  @last_stream_id = last_stream_id
13
13
  @error_code = error_code
14
14
  @debug = debug
@@ -33,11 +33,13 @@ module Biryani
33
33
  #
34
34
  # @return [Goaway]
35
35
  def self.read(s, _flags, stream_id)
36
+ return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') unless stream_id.zero?
37
+
36
38
  io = IO::Buffer.for(s)
37
39
  last_stream_id, error_code = io.get_values(%i[U32 U32], 0)
38
40
  debug = io.get_string(8)
39
41
 
40
- Goaway.new(stream_id, last_stream_id, error_code, debug)
42
+ Goaway.new(last_stream_id, error_code, debug)
41
43
  end
42
44
  end
43
45
  end
@@ -9,7 +9,7 @@ module Biryani
9
9
  # @return [String]
10
10
  def self.encode(name, value, dynamic_table)
11
11
  case find(name, value, dynamic_table)
12
- in Some(index, v) if v.nil?
12
+ in Some(index, nil)
13
13
  res = encode_indexed(index)
14
14
  in Some(index, v)
15
15
  res = encode_literal_value(index, v)
@@ -145,8 +145,7 @@ module Biryani
145
145
  # @return [Integer]
146
146
  def self.decode_indexed(io, cursor, dynamic_table)
147
147
  index, c = Integer.decode(io, 7, cursor)
148
- raise Error::HPACKDecodeError if index.zero?
149
- raise Error::HPACKDecodeError if index > STATIC_TABLE_SIZE + dynamic_table.count_entries
148
+ raise Error::HPACKDecodeError if index.zero? || index > STATIC_TABLE_SIZE + dynamic_table.count_entries
150
149
 
151
150
  field = if index <= STATIC_TABLE_SIZE
152
151
  STATIC_TABLE[index - 1]
@@ -203,8 +202,7 @@ module Biryani
203
202
  # @return [Integer]
204
203
  def self.decode_literal_value_incremental_indexing(io, cursor, dynamic_table)
205
204
  index, c = Integer.decode(io, 6, cursor)
206
- raise Error::HPACKDecodeError if index.zero?
207
- raise Error::HPACKDecodeError if index > STATIC_TABLE_SIZE + dynamic_table.count_entries
205
+ raise Error::HPACKDecodeError if index.zero? || index > STATIC_TABLE_SIZE + dynamic_table.count_entries
208
206
 
209
207
  name = if index <= STATIC_TABLE_SIZE
210
208
  STATIC_TABLE[index - 1][0]
@@ -283,8 +281,7 @@ module Biryani
283
281
  # @return [Integer]
284
282
  def self.decode_literal_value_never_indexed(io, cursor, dynamic_table)
285
283
  index, c = Integer.decode(io, 4, cursor)
286
- raise Error::HPACKDecodeError if index.zero?
287
- raise Error::HPACKDecodeError if index > STATIC_TABLE_SIZE + dynamic_table.count_entries
284
+ raise Error::HPACKDecodeError if index.zero? || index > STATIC_TABLE_SIZE + dynamic_table.count_entries
288
285
 
289
286
  name = if index <= STATIC_TABLE_SIZE
290
287
  STATIC_TABLE[index - 1][0]
@@ -339,8 +336,7 @@ module Biryani
339
336
  # @return [Integer]
340
337
  def self.decode_literal_value_without_indexing(io, cursor, dynamic_table)
341
338
  index, c = Integer.decode(io, 4, cursor)
342
- raise Error::HPACKDecodeError if index.zero?
343
- raise Error::HPACKDecodeError if index > STATIC_TABLE_SIZE + dynamic_table.count_entries
339
+ raise Error::HPACKDecodeError if index.zero? || index > STATIC_TABLE_SIZE + dynamic_table.count_entries
344
340
 
345
341
  name = if index <= STATIC_TABLE_SIZE
346
342
  STATIC_TABLE[index - 1][0]
@@ -0,0 +1,10 @@
1
+ module Biryani
2
+ module HTTP
3
+ module Error
4
+ # Generic error, common for all classes under Biryani::HTTP::Error module.
5
+ class Error < StandardError; end
6
+
7
+ class InvalidHTTPResponseError < Error; end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,97 @@
1
+ module Biryani
2
+ module HTTP
3
+ class Request
4
+ attr_accessor :method, :uri, :fields, :content
5
+
6
+ # @param method [String]
7
+ # @param uri [URI]
8
+ # @param fields [Hash<String, Array<String>>]
9
+ # @param content [String]
10
+ def initialize(method, uri, fields, content)
11
+ @method = method
12
+ @uri = uri
13
+ @fields = fields
14
+ @content = content
15
+ end
16
+
17
+ # @return [Array<String>, nil]
18
+ def trailers
19
+ # https://datatracker.ietf.org/doc/html/rfc9110#section-6.6.2-4
20
+ keys = (@fields['trailer'] || []).flat_map { |x| x.split(',').map(&:strip) }
21
+ @fields.slice(*keys)
22
+ end
23
+ end
24
+
25
+ class RequestBuilder
26
+ PSEUDO_HEADER_FIELDS = [':authority', ':method', ':path', ':scheme'].freeze
27
+ Ractor.make_shareable(PSEUDO_HEADER_FIELDS)
28
+
29
+ def initialize
30
+ @h = {}
31
+ end
32
+
33
+ # @param name [String]
34
+ # @param value [String]
35
+ #
36
+ # @return [nil, ConnectioError]
37
+ # rubocop: disable Metrics/AbcSize
38
+ # rubocop: disable Metrics/CyclomaticComplexity
39
+ # rubocop: disable Metrics/PerceivedComplexity
40
+ def field(name, value)
41
+ return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'field name has uppercase letter') if name.downcase != name
42
+ return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'unknown pseudo-header field name') if name[0] == ':' && !PSEUDO_HEADER_FIELDS.include?(name)
43
+ return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'appear pseudo-header fields after regular fields') if name[0] == ':' && @h.any? { |name_, _| name_[0] != ':' }
44
+ return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'duplicated pseudo-header fields') if PSEUDO_HEADER_FIELDS.include?(name) && @h.key?(name)
45
+ return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, "invalid `#{name}` field") if PSEUDO_HEADER_FIELDS.include?(name) && value.empty?
46
+ return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'connection-specific field is forbidden') if name == 'connection'
47
+ return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, '`TE` field has a value other than `trailers`') if name == 'te' && value != 'trailers'
48
+
49
+ @h[name] = [] unless @h.key?(name)
50
+ @h[name] << value
51
+
52
+ nil
53
+ end
54
+ # rubocop: enable Metrics/AbcSize
55
+ # rubocop: enable Metrics/CyclomaticComplexity
56
+ # rubocop: enable Metrics/PerceivedComplexity
57
+
58
+ # @param arr [Array]
59
+ #
60
+ # @return [nil, ConnectioError]
61
+ def fields(arr)
62
+ arr.each do |name, value|
63
+ err = field(name, value)
64
+ return err unless err.nil?
65
+ end
66
+
67
+ nil
68
+ end
69
+
70
+ # @param s [String]
71
+ #
72
+ # @return [Request]
73
+ def build(s)
74
+ # `Ractor.send(req, move: true)` moves entries in HPACK::DynamicTable; therefore, a `dup` call is required.
75
+ h = @h.transform_values { |x| x.map(&:dup) }
76
+ self.class.build(h, s)
77
+ end
78
+
79
+ # @param h [Hash<String, Array<String>>]
80
+ # @param s [String]
81
+ #
82
+ # @return [Request, ConnectionError]
83
+ def self.build(h, s)
84
+ return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'missing pseudo-header fields') unless PSEUDO_HEADER_FIELDS.all? { |x| h.key?(x) }
85
+ return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid content-length') if h.key?('content-length') && !s.empty? && s.length != h['content-length'].to_i
86
+
87
+ scheme = h[':scheme'][0]
88
+ domain = h[':authority'][0]
89
+ path = h[':path'][0]
90
+ uri = URI("#{scheme}://#{domain}#{path}")
91
+ method = h[':method'][0]
92
+ h['cookie'] = [h['cookie'].join('; ')] if h.key?('cookie')
93
+ Request.new(method, uri, h, s)
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,68 @@
1
+ module Biryani
2
+ module HTTP
3
+ class Response
4
+ FORBIDDEN_KEY_CHARS = (0x00..0x20).chain([0x3a]).chain(0x41..0x5a).chain(0x7f..0xff).to_a.freeze
5
+ FORBIDDEN_VALUE_CHARS = [0x00, 0x0a, 0x0d].freeze
6
+
7
+ attr_accessor :status, :fields, :content
8
+
9
+ # @param status [Integer]
10
+ # @param fields [Hash]
11
+ # @param content [String, nil]
12
+ def initialize(status, fields, content)
13
+ @status = status
14
+ @fields = fields
15
+ @content = content
16
+ end
17
+
18
+ # @raise [InvalidHTTPResponseError]
19
+ # rubocop: disable Metrics/CyclomaticComplexity
20
+ def validate
21
+ # https://datatracker.ietf.org/doc/html/rfc9113#section-8.2.1
22
+ raise Error::InvalidHTTPResponseError, 'invalid HTTP status' if @status < 100 || @status >= 600
23
+ raise Error::InvalidHTTPResponseError, 'HTTP field name contains invalid characters' if (@fields.keys.join.downcase.bytes.uniq & FORBIDDEN_KEY_CHARS).any?
24
+ raise Error::InvalidHTTPResponseError, 'HTTP field value contains NUL, LF or CR' if (@fields.values.join.bytes.uniq & FORBIDDEN_VALUE_CHARS).any?
25
+ raise Error::InvalidHTTPResponseError, 'HTTP field value starts/ends with SP or HTAB' if @fields.values.filter { |s| s.start_with?("\t", ' ') || s.end_with?("\t", ' ') }.any?
26
+ end
27
+ # rubocop: enable Metrics/CyclomaticComplexity
28
+
29
+ def self.default
30
+ Response.new(0, {}, nil)
31
+ end
32
+
33
+ def self.internal_server_error
34
+ Response.new(500, {}, 'Internal Server Error')
35
+ end
36
+ end
37
+
38
+ class ResponseParser
39
+ # @param res [Response]
40
+ def initialize(res)
41
+ @res = res
42
+ end
43
+
44
+ # @return [Array]
45
+ def fields
46
+ fields = [[':status', @res.status.to_s]]
47
+ @res.fields.each do |name, value|
48
+ fields << [name.to_s.downcase, value.to_s]
49
+ end
50
+
51
+ fields
52
+ end
53
+
54
+ # @return [String]
55
+ def content
56
+ @res.content
57
+ end
58
+
59
+ # @param encoder [Encoder]
60
+ #
61
+ # @return [String] fragment
62
+ # @return [String] data
63
+ def parse(encoder)
64
+ [encoder.encode(fields), content]
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,3 @@
1
+ require_relative 'http/error'
2
+ require_relative 'http/request'
3
+ require_relative 'http/response'
data/lib/biryani/state.rb CHANGED
@@ -255,7 +255,7 @@ module Biryani
255
255
 
256
256
  # @return [Boolean]
257
257
  def receiving_continuation?
258
- %i[receiving_continuation receiving_continuation_and_data].include?(@state)
258
+ %i[receiving_continuation receiving_continuation_and_data receiving_trailer_continuation].include?(@state)
259
259
  end
260
260
  end
261
261
  end
@@ -2,20 +2,20 @@ module Biryani
2
2
  class Stream
3
3
  attr_accessor :rx
4
4
 
5
- # @param tx [Port]
5
+ # @param tx [Ractor::Port]
6
6
  # @param stream_id [Integer]
7
7
  # @param proc [Proc]
8
8
  def initialize(tx, stream_id, proc)
9
9
  @rx = Ractor.new(tx, stream_id, proc) do |tx, stream_id, proc|
10
10
  unless (req = Ractor.recv).nil?
11
- res = HTTPResponse.default
11
+ res = HTTP::Response.default
12
12
 
13
13
  begin
14
14
  proc.call(req, res)
15
15
  res.validate
16
16
  rescue StandardError => e
17
17
  puts e.backtrace
18
- res = HTTPResponse.internal_server_error
18
+ res = HTTP::Response.internal_server_error
19
19
  end
20
20
 
21
21
  tx.send([res, stream_id], move: true)
@@ -1,8 +1,11 @@
1
1
  module Biryani
2
2
  class StreamsContext
3
+ attr_accessor :tx
4
+
3
5
  def initialize(proc)
4
6
  @h = {} # Hash<Integer, StreamContext>
5
7
  @proc = proc
8
+ @tx = Ractor::Port.new
6
9
  end
7
10
 
8
11
  # @param stream_id [Integer]
@@ -11,7 +14,7 @@ module Biryani
11
14
  #
12
15
  # @return [StreamContext]
13
16
  def new_context(stream_id, send_initial_window_size, recv_initial_window_size)
14
- ctx = StreamContext.new(stream_id, send_initial_window_size, recv_initial_window_size, @proc)
17
+ ctx = StreamContext.new(stream_id, send_initial_window_size, recv_initial_window_size, @proc, @tx)
15
18
  @h[stream_id] = ctx
16
19
  ctx
17
20
  end
@@ -33,13 +36,13 @@ module Biryani
33
36
  @h.length
34
37
  end
35
38
 
36
- def each(&block)
37
- @h.each_value(&block)
39
+ # @return [Boolean]
40
+ def empty?
41
+ @h.empty?
38
42
  end
39
43
 
40
- # @return [Array<Port>]
41
- def txs
42
- @h.values.filter { |ctx| !ctx.closed? }.map(&:tx)
44
+ def each(&block)
45
+ @h.each_value(&block)
43
46
  end
44
47
 
45
48
  # @return [Integer]
@@ -88,28 +91,25 @@ module Biryani
88
91
  def remove_closed(data_buffer)
89
92
  closed_ids = closed_stream_ids.filter { |id| !data_buffer.has?(id) }
90
93
  closed_ids.each do |id|
91
- @h[id].tx.close
94
+ @h.delete(id)
92
95
  end
93
96
  end
94
97
 
95
98
  def close_all
96
- each do |ctx|
97
- ctx.tx.close
98
- ctx.close
99
- end
99
+ each(&:close)
100
100
  end
101
101
  end
102
102
 
103
103
  class StreamContext
104
- attr_accessor :tx, :send_window, :recv_window, :fragment, :content
104
+ attr_accessor :send_window, :recv_window, :fragment, :content
105
105
 
106
106
  # @param stream_id [Integer]
107
107
  # @param send_initial_window_size [Integer]
108
108
  # @param recv_initial_window_size [Integer]
109
109
  # @param proc [Proc]
110
- def initialize(stream_id, send_initial_window_size, recv_initial_window_size, proc)
111
- @tx = Ractor::Port.new
112
- @stream = Stream.new(@tx, stream_id, proc)
110
+ # @param tx [Ractor::Port]
111
+ def initialize(stream_id, send_initial_window_size, recv_initial_window_size, proc, tx)
112
+ @stream = Stream.new(tx, stream_id, proc)
113
113
  @send_window = Window.new(send_initial_window_size)
114
114
  @recv_window = Window.new(recv_initial_window_size)
115
115
  @fragment = ''.b
@@ -117,7 +117,7 @@ module Biryani
117
117
  @state = State.new
118
118
  end
119
119
 
120
- # @param req [HTTPRequest]
120
+ # @param req [HTTP::Request]
121
121
  def <<(req)
122
122
  @stream.rx.send(req, move: true)
123
123
  end
@@ -1,3 +1,3 @@
1
1
  module Biryani
2
- VERSION = '0.0.8'.freeze
2
+ VERSION = '0.0.10'.freeze
3
3
  end