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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/README.md +2 -2
- data/biryani.gemspec +5 -2
- data/example/echo.rb +2 -2
- data/example/hello_world.rb +2 -2
- data/lib/biryani/connection.rb +62 -47
- data/lib/biryani/connection_error.rb +1 -1
- data/lib/biryani/frame/goaway.rb +5 -3
- data/lib/biryani/hpack/field.rb +5 -9
- data/lib/biryani/http/error.rb +10 -0
- data/lib/biryani/http/request.rb +97 -0
- data/lib/biryani/http/response.rb +68 -0
- data/lib/biryani/http.rb +3 -0
- data/lib/biryani/state.rb +1 -1
- data/lib/biryani/stream.rb +3 -3
- data/lib/biryani/streams_context.rb +16 -16
- data/lib/biryani/version.rb +1 -1
- data/lib/biryani.rb +1 -3
- metadata +7 -77
- data/.github/workflows/ci.yml +0 -30
- data/.github/workflows/conformance.yml +0 -46
- data/.gitignore +0 -18
- data/lib/biryani/error.rb +0 -8
- data/lib/biryani/http_request.rb +0 -95
- data/lib/biryani/http_response.rb +0 -66
- data/spec/connection/handle_connection_window_update_spec.rb +0 -16
- data/spec/connection/handle_data_spec.rb +0 -58
- data/spec/connection/handle_headers_spec.rb +0 -19
- data/spec/connection/handle_ping_spec.rb +0 -21
- data/spec/connection/handle_rst_stream_spec.rb +0 -16
- data/spec/connection/handle_settings_spec.rb +0 -37
- data/spec/connection/handle_stream_window_update_spec.rb +0 -20
- data/spec/connection/read_http2_magic_spec.rb +0 -26
- data/spec/connection/send_spec.rb +0 -104
- data/spec/connection/transition_stream_state_send_spec.rb +0 -39
- data/spec/data_buffer_spec.rb +0 -135
- data/spec/frame/continuation_spec.rb +0 -39
- data/spec/frame/data_spec.rb +0 -25
- data/spec/frame/goaway_spec.rb +0 -23
- data/spec/frame/headers_spec.rb +0 -52
- data/spec/frame/ping_spec.rb +0 -22
- data/spec/frame/priority_spec.rb +0 -22
- data/spec/frame/push_promise_spec.rb +0 -24
- data/spec/frame/read_spec.rb +0 -30
- data/spec/frame/rst_stream_spec.rb +0 -21
- data/spec/frame/settings_spec.rb +0 -23
- data/spec/frame/window_update_spec.rb +0 -21
- data/spec/hpack/decoder_spec.rb +0 -170
- data/spec/hpack/encoder_spec.rb +0 -48
- data/spec/hpack/field_spec.rb +0 -43
- data/spec/hpack/fields_spec.rb +0 -17
- data/spec/hpack/huffman_spec.rb +0 -20
- data/spec/hpack/integer_spec.rb +0 -27
- data/spec/hpack/string_spec.rb +0 -19
- data/spec/http_request_spec.rb +0 -62
- data/spec/http_response_spec.rb +0 -12
- data/spec/spec_helper.rb +0 -9
- data/spec/streams_context_spec.rb +0 -79
- data/spec/utils_spec.rb +0 -41
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1aa3fb4dd49abcba22df142cd67d38c5eedc6c8beb6ca8fbcb130147c7e7506a
|
|
4
|
+
data.tar.gz: 54bd5a493c027047eeaada80b4ff938ab806c29b80069a6cc112a54f5bb78efc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3a8c93ee8ce069cfe77854f7792abadf528addc7e258cd55c84f1b8b2da0b25b278b768e3c8b41a0897a9adc1ee497a9cac4d2ced4c3dd5c644d17f9eb1327c8
|
|
7
|
+
data.tar.gz: 2909206008b7794ce6ec3a7c850acf66c475c33bc543d55defd9222d8f606fb661d31b99443cc4ba7a0d3d1da920d3a85209d0f8e0cca3de1cd611ccc43634ac
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.0.
|
|
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::
|
|
36
|
-
# @param res [Biryani::
|
|
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
|
|
17
|
-
|
|
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::
|
|
13
|
-
# @param res [Biryani::
|
|
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'
|
data/example/hello_world.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::
|
|
13
|
-
# @param res [Biryani::
|
|
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!'
|
data/lib/biryani/connection.rb
CHANGED
|
@@ -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
|
-
|
|
46
|
+
select_loop(io)
|
|
47
47
|
rescue StandardError => e
|
|
48
48
|
puts e.backtrace
|
|
49
|
-
self.class.do_send(io, Frame::Goaway.new(
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
break if ports.empty? && @sock.closed?
|
|
69
|
+
break if @sock.closed? && @streams_ctx.empty?
|
|
76
70
|
|
|
77
|
-
port, obj = Ractor.select(@sock,
|
|
71
|
+
port, obj = Ractor.select(@sock, @streams_ctx.tx)
|
|
78
72
|
if port == @sock
|
|
79
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 [
|
|
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 =
|
|
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 [
|
|
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
|
-
|
|
482
|
+
HTTP::ResponseParser.new(res).parse(encoder)
|
|
468
483
|
end
|
|
469
484
|
|
|
470
485
|
# @return [Hash<Integer, Integer>]
|
data/lib/biryani/frame/goaway.rb
CHANGED
|
@@ -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(
|
|
9
|
+
def initialize(last_stream_id, error_code, debug)
|
|
10
10
|
@f_type = FrameType::GOAWAY
|
|
11
|
-
@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(
|
|
42
|
+
Goaway.new(last_stream_id, error_code, debug)
|
|
41
43
|
end
|
|
42
44
|
end
|
|
43
45
|
end
|
data/lib/biryani/hpack/field.rb
CHANGED
|
@@ -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,
|
|
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,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
|
data/lib/biryani/http.rb
ADDED
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
|
data/lib/biryani/stream.rb
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
-
|
|
37
|
-
|
|
39
|
+
# @return [Boolean]
|
|
40
|
+
def empty?
|
|
41
|
+
@h.empty?
|
|
38
42
|
end
|
|
39
43
|
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
94
|
+
@h.delete(id)
|
|
92
95
|
end
|
|
93
96
|
end
|
|
94
97
|
|
|
95
98
|
def close_all
|
|
96
|
-
each
|
|
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 :
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
@stream = Stream.new(
|
|
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 [
|
|
120
|
+
# @param req [HTTP::Request]
|
|
121
121
|
def <<(req)
|
|
122
122
|
@stream.rx.send(req, move: true)
|
|
123
123
|
end
|
data/lib/biryani/version.rb
CHANGED