biryani 0.0.1
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 +7 -0
- data/.github/workflows/ci.yml +30 -0
- data/.gitignore +17 -0
- data/.rubocop.yml +36 -0
- data/.ruby-version +1 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +48 -0
- data/Rakefile +8 -0
- data/biryani.gemspec +21 -0
- data/example/echo.rb +27 -0
- data/example/hello_world.rb +22 -0
- data/lib/biryani/connection.rb +464 -0
- data/lib/biryani/connection_error.rb +17 -0
- data/lib/biryani/data_buffer.rb +42 -0
- data/lib/biryani/frame/continuation.rb +48 -0
- data/lib/biryani/frame/data.rb +70 -0
- data/lib/biryani/frame/goaway.rb +44 -0
- data/lib/biryani/frame/headers.rb +110 -0
- data/lib/biryani/frame/ping.rb +49 -0
- data/lib/biryani/frame/priority.rb +44 -0
- data/lib/biryani/frame/push_promise.rb +75 -0
- data/lib/biryani/frame/rst_stream.rb +40 -0
- data/lib/biryani/frame/settings.rb +66 -0
- data/lib/biryani/frame/unknown.rb +42 -0
- data/lib/biryani/frame/window_update.rb +43 -0
- data/lib/biryani/frame.rb +146 -0
- data/lib/biryani/hpack/decoder.rb +22 -0
- data/lib/biryani/hpack/dynamic_table.rb +65 -0
- data/lib/biryani/hpack/encoder.rb +22 -0
- data/lib/biryani/hpack/error.rb +12 -0
- data/lib/biryani/hpack/field.rb +357 -0
- data/lib/biryani/hpack/fields.rb +28 -0
- data/lib/biryani/hpack/huffman.rb +309 -0
- data/lib/biryani/hpack/integer.rb +66 -0
- data/lib/biryani/hpack/option.rb +24 -0
- data/lib/biryani/hpack/string.rb +40 -0
- data/lib/biryani/hpack.rb +84 -0
- data/lib/biryani/http_request.rb +83 -0
- data/lib/biryani/http_response.rb +61 -0
- data/lib/biryani/server.rb +19 -0
- data/lib/biryani/state.rb +224 -0
- data/lib/biryani/stream.rb +19 -0
- data/lib/biryani/stream_error.rb +17 -0
- data/lib/biryani/streams_context.rb +89 -0
- data/lib/biryani/version.rb +3 -0
- data/lib/biryani/window.rb +29 -0
- data/lib/biryani.rb +17 -0
- data/spec/connection/close_all_streams_spec.rb +17 -0
- data/spec/connection/handle_connection_window_update_spec.rb +16 -0
- data/spec/connection/handle_ping_spec.rb +21 -0
- data/spec/connection/handle_rst_stream_spec.rb +21 -0
- data/spec/connection/handle_settings_spec.rb +31 -0
- data/spec/connection/handle_stream_window_update_spec.rb +20 -0
- data/spec/connection/read_http2_magic_spec.rb +26 -0
- data/spec/connection/remove_closed_streams_spec.rb +51 -0
- data/spec/connection/send_spec.rb +114 -0
- data/spec/connection/transition_state_send_spec.rb +39 -0
- data/spec/connection/unwrap_spec.rb +28 -0
- data/spec/data_buffer_spec.rb +135 -0
- data/spec/frame/continuation_spec.rb +39 -0
- data/spec/frame/data_spec.rb +25 -0
- data/spec/frame/goaway_spec.rb +23 -0
- data/spec/frame/headers_spec.rb +52 -0
- data/spec/frame/ping_spec.rb +22 -0
- data/spec/frame/priority_spec.rb +22 -0
- data/spec/frame/push_promise_spec.rb +24 -0
- data/spec/frame/read_spec.rb +30 -0
- data/spec/frame/rst_stream_spec.rb +21 -0
- data/spec/frame/settings_spec.rb +23 -0
- data/spec/frame/window_update_spec.rb +21 -0
- data/spec/hpack/decoder_spec.rb +170 -0
- data/spec/hpack/encoder_spec.rb +48 -0
- data/spec/hpack/field_spec.rb +42 -0
- data/spec/hpack/fields_spec.rb +17 -0
- data/spec/hpack/huffman_spec.rb +20 -0
- data/spec/hpack/integer_spec.rb +27 -0
- data/spec/hpack/string_spec.rb +19 -0
- data/spec/http_request_builder_spec.rb +45 -0
- data/spec/spec_helper.rb +9 -0
- metadata +165 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
module Biryani
|
|
2
|
+
module Frame
|
|
3
|
+
class Headers
|
|
4
|
+
attr_reader :f_type, :stream_id, :stream_dependency, :weight, :fragment, :padding
|
|
5
|
+
|
|
6
|
+
# @param end_headers [Boolean]
|
|
7
|
+
# @param end_stream [Boolean]
|
|
8
|
+
# @param stream_id [Integer]
|
|
9
|
+
# @param stream_dependency [Integer, nil]
|
|
10
|
+
# @param weight [Integer, nil]
|
|
11
|
+
# @param fragment [String]
|
|
12
|
+
# @param padding [String, nil]
|
|
13
|
+
# rubocop: disable Metrics/ParameterLists
|
|
14
|
+
def initialize(end_headers, end_stream, stream_id, stream_dependency, weight, fragment, padding)
|
|
15
|
+
@f_type = FrameType::HEADERS
|
|
16
|
+
@end_headers = end_headers
|
|
17
|
+
@end_stream = end_stream
|
|
18
|
+
@stream_id = stream_id
|
|
19
|
+
@stream_dependency = stream_dependency
|
|
20
|
+
@weight = weight
|
|
21
|
+
@fragment = fragment
|
|
22
|
+
@padding = padding
|
|
23
|
+
end
|
|
24
|
+
# rubocop: enable Metrics/ParameterLists
|
|
25
|
+
|
|
26
|
+
# @return [Boolean]
|
|
27
|
+
def priority?
|
|
28
|
+
!@stream_dependency.nil? && !@weight.nil?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [Boolean]
|
|
32
|
+
def padded?
|
|
33
|
+
!@padding.nil?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [Boolean]
|
|
37
|
+
def end_headers?
|
|
38
|
+
@end_headers
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [Boolean]
|
|
42
|
+
def end_stream?
|
|
43
|
+
@end_stream
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [Integer]
|
|
47
|
+
def length
|
|
48
|
+
@fragment.bytesize + (padded? ? 1 + @padding.bytesize : 0) + (priority? ? 5 : 0)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @return [String]
|
|
52
|
+
def to_binary_s
|
|
53
|
+
payload_length = length
|
|
54
|
+
flags = Frame.to_flags(priority: priority?, padded: padded?, end_headers: end_headers?, end_stream: end_stream?)
|
|
55
|
+
pad_length = padded? ? @padding.bytesize.chr : ''
|
|
56
|
+
stream_dependency_weight = priority? ? [@stream_dependency, @weight].pack('NC') : ''
|
|
57
|
+
padding = @padding || ''
|
|
58
|
+
|
|
59
|
+
Frame.to_binary_s_header(payload_length, @f_type, flags, @stream_id) + pad_length + stream_dependency_weight + @fragment + padding
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @param s [String]
|
|
63
|
+
#
|
|
64
|
+
# @return [Headers]
|
|
65
|
+
# rubocop: disable Metrics/AbcSize
|
|
66
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
|
67
|
+
# rubocop: disable Metrics/PerceivedComplexity
|
|
68
|
+
def self.read(s)
|
|
69
|
+
payload_length, _, flags, stream_id = Frame.read_header(s)
|
|
70
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if s[9..].bytesize != payload_length
|
|
71
|
+
|
|
72
|
+
priority = Frame.read_priority(flags)
|
|
73
|
+
padded = Frame.read_padded(flags)
|
|
74
|
+
end_headers = Frame.read_end_headers(flags)
|
|
75
|
+
end_stream = Frame.read_end_stream(flags)
|
|
76
|
+
|
|
77
|
+
if priority && padded
|
|
78
|
+
pad_length, stream_dependency, weight = s[9..14].unpack('CNC')
|
|
79
|
+
fragment_length = payload_length - pad_length - 6
|
|
80
|
+
# exclusive = (stream_dependency / 2**31).positive?
|
|
81
|
+
stream_dependency %= 2**31
|
|
82
|
+
fragment = s[15...15 + fragment_length]
|
|
83
|
+
padding = s[15 + fragment_length..]
|
|
84
|
+
return ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'invalid frame') if padding.bytesize != pad_length
|
|
85
|
+
elsif priority
|
|
86
|
+
stream_dependency, weight = s[9..13].unpack('NC')
|
|
87
|
+
# exclusive = (stream_dependency / 2**31).positive?
|
|
88
|
+
stream_dependency %= 2**31
|
|
89
|
+
fragment = s[14..]
|
|
90
|
+
return ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'invalid frame') if fragment.bytesize + 5 != payload_length
|
|
91
|
+
elsif padded
|
|
92
|
+
pad_length = s[9].unpack1('C')
|
|
93
|
+
fragment_length = payload_length - pad_length - 1
|
|
94
|
+
fragment = s[10...10 + fragment_length]
|
|
95
|
+
padding = s[10 + fragment_length..]
|
|
96
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if pad_length >= payload_length
|
|
97
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if padding.bytesize != pad_length
|
|
98
|
+
else
|
|
99
|
+
fragment = s[9..]
|
|
100
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if fragment.bytesize != payload_length
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
Headers.new(end_headers, end_stream, stream_id, stream_dependency, weight, fragment, padding)
|
|
104
|
+
end
|
|
105
|
+
# rubocop: enable Metrics/AbcSize
|
|
106
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
|
107
|
+
# rubocop: enable Metrics/PerceivedComplexity
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Biryani
|
|
2
|
+
module Frame
|
|
3
|
+
class Ping
|
|
4
|
+
attr_reader :f_type, :stream_id, :opaque
|
|
5
|
+
|
|
6
|
+
# @param ack [Boolean]
|
|
7
|
+
# @param stream_id [Integer]
|
|
8
|
+
# @param opaque [String]
|
|
9
|
+
def initialize(ack, stream_id, opaque)
|
|
10
|
+
@f_type = FrameType::PING
|
|
11
|
+
@ack = ack
|
|
12
|
+
@stream_id = stream_id
|
|
13
|
+
@opaque = opaque
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [Boolean]
|
|
17
|
+
def ack?
|
|
18
|
+
@ack
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Integer]
|
|
22
|
+
def length
|
|
23
|
+
8
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [String]
|
|
27
|
+
def to_binary_s
|
|
28
|
+
payload_length = length
|
|
29
|
+
flags = Frame.to_flags(ack: ack?)
|
|
30
|
+
|
|
31
|
+
Frame.to_binary_s_header(payload_length, @f_type, flags, @stream_id) + opaque
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param s [String]
|
|
35
|
+
#
|
|
36
|
+
# @return [Ping]
|
|
37
|
+
def self.read(s)
|
|
38
|
+
payload_length, _, flags, stream_id = Frame.read_header(s)
|
|
39
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if s[9..].bytesize != payload_length
|
|
40
|
+
return ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'PING payload length MUST be 8') if s[9..].bytesize != 8
|
|
41
|
+
|
|
42
|
+
ack = Frame.read_ack(flags)
|
|
43
|
+
opaque = s[9..]
|
|
44
|
+
|
|
45
|
+
Ping.new(ack, stream_id, opaque)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Biryani
|
|
2
|
+
module Frame
|
|
3
|
+
class Priority
|
|
4
|
+
attr_reader :f_type, :stream_id, :stream_dependency, :weight
|
|
5
|
+
|
|
6
|
+
# @param stream_id [Integer]
|
|
7
|
+
# @param stream_dependency [Integer]
|
|
8
|
+
# @param weight [Integer]
|
|
9
|
+
def initialize(stream_id, stream_dependency, weight)
|
|
10
|
+
@f_type = FrameType::PRIORITY
|
|
11
|
+
@stream_id = stream_id
|
|
12
|
+
@stream_dependency = stream_dependency
|
|
13
|
+
@weight = weight
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [Integer]
|
|
17
|
+
def length
|
|
18
|
+
5
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [String]
|
|
22
|
+
def to_binary_s
|
|
23
|
+
payload_length = length
|
|
24
|
+
flags = 0x00
|
|
25
|
+
|
|
26
|
+
Frame.to_binary_s_header(payload_length, @f_type, flags, @stream_id) + [@stream_dependency, @weight].pack('NC')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param s [String]
|
|
30
|
+
#
|
|
31
|
+
# @return [Priority]
|
|
32
|
+
def self.read(s)
|
|
33
|
+
payload_length, _, _, stream_id = Frame.read_header(s)
|
|
34
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if s[9..].bytesize != payload_length
|
|
35
|
+
return ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'PRIORITY payload length MUST be 5') if payload_length != 5
|
|
36
|
+
|
|
37
|
+
stream_dependency, weight = s[9..13].unpack('NC')
|
|
38
|
+
stream_dependency %= 2**31
|
|
39
|
+
|
|
40
|
+
Priority.new(stream_id, stream_dependency, weight)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module Biryani
|
|
2
|
+
module Frame
|
|
3
|
+
class PushPromise
|
|
4
|
+
attr_reader :f_type, :stream_id, :promised_stream_id, :fragment, :padding
|
|
5
|
+
|
|
6
|
+
# @param end_headers [Boolean]
|
|
7
|
+
# @param stream_id [Integer]
|
|
8
|
+
# @param promised_stream_id [Integer]
|
|
9
|
+
# @param fragment [String]
|
|
10
|
+
# @param padding [String, nil]
|
|
11
|
+
def initialize(end_headers, stream_id, promised_stream_id, fragment, padding)
|
|
12
|
+
@f_type = FrameType::PUSH_PROMISE
|
|
13
|
+
@end_headers = end_headers
|
|
14
|
+
@stream_id = stream_id
|
|
15
|
+
@promised_stream_id = promised_stream_id
|
|
16
|
+
@fragment = fragment
|
|
17
|
+
@padding = padding
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [Boolean]
|
|
21
|
+
def padded?
|
|
22
|
+
!@padding.nil?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @return [Boolean]
|
|
26
|
+
def end_headers?
|
|
27
|
+
@end_headers
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [Integer]
|
|
31
|
+
def length
|
|
32
|
+
@fragment.bytesize + 4 + (padded? ? 1 + @padding.bytesize : 0)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [String]
|
|
36
|
+
def to_binary_s
|
|
37
|
+
payload_length = length
|
|
38
|
+
flags = Frame.to_flags(padded: padded?, end_headers: end_headers?)
|
|
39
|
+
pad_length = padded? ? @padding.bytesize.chr : ''
|
|
40
|
+
padding = @padding || ''
|
|
41
|
+
|
|
42
|
+
Frame.to_binary_s_header(payload_length, @f_type, flags, @stream_id) + pad_length + [@promised_stream_id].pack('N') + @fragment + padding
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param s [String]
|
|
46
|
+
#
|
|
47
|
+
# @return [PushPromise]
|
|
48
|
+
# rubocop: disable Metrics/AbcSize
|
|
49
|
+
def self.read(s)
|
|
50
|
+
payload_length, _, flags, stream_id = Frame.read_header(s)
|
|
51
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if s[9..].bytesize != payload_length
|
|
52
|
+
|
|
53
|
+
padded = Frame.read_padded(flags)
|
|
54
|
+
end_headers = Frame.read_end_headers(flags)
|
|
55
|
+
|
|
56
|
+
if padded
|
|
57
|
+
pad_length = s[9].unpack1('C')
|
|
58
|
+
promised_stream_id = s[10..13].unpack1('N')
|
|
59
|
+
fragment_length = payload_length - pad_length - 5
|
|
60
|
+
fragment = s[14...14 + fragment_length]
|
|
61
|
+
padding = s[14 + fragment_length..]
|
|
62
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if pad_length >= payload_length
|
|
63
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if padding.bytesize != pad_length
|
|
64
|
+
else
|
|
65
|
+
promised_stream_id = s[9..12].unpack1('N')
|
|
66
|
+
fragment = s[13..]
|
|
67
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if fragment.bytesize + 4 != payload_length
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
PushPromise.new(end_headers, stream_id, promised_stream_id, fragment, padding)
|
|
71
|
+
end
|
|
72
|
+
# rubocop: enable Metrics/AbcSize
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Biryani
|
|
2
|
+
module Frame
|
|
3
|
+
class RstStream
|
|
4
|
+
attr_reader :f_type, :stream_id, :error_code
|
|
5
|
+
|
|
6
|
+
# @param stream_id [Integer]
|
|
7
|
+
# @param error_code [Integer]
|
|
8
|
+
def initialize(stream_id, error_code)
|
|
9
|
+
@f_type = FrameType::RST_STREAM
|
|
10
|
+
@stream_id = stream_id
|
|
11
|
+
@error_code = error_code
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @return [Integer]
|
|
15
|
+
def length
|
|
16
|
+
4
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @return [String]
|
|
20
|
+
def to_binary_s
|
|
21
|
+
payload_length = length
|
|
22
|
+
flags = 0x00
|
|
23
|
+
|
|
24
|
+
Frame.to_binary_s_header(payload_length, @f_type, flags, @stream_id) + [@error_code].pack('N')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param s [String]
|
|
28
|
+
#
|
|
29
|
+
# @return [RstStream]
|
|
30
|
+
def self.read(s)
|
|
31
|
+
payload_length, _, _, stream_id = Frame.read_header(s)
|
|
32
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if s[9..].bytesize != payload_length
|
|
33
|
+
return ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'RST_STREAM payload length MUST be 4') if s[9..].bytesize != 4
|
|
34
|
+
|
|
35
|
+
error_code = s[9..].unpack1('N')
|
|
36
|
+
RstStream.new(stream_id, error_code)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module Biryani
|
|
2
|
+
module Frame
|
|
3
|
+
class Settings
|
|
4
|
+
attr_reader :f_type, :stream_id, :setting
|
|
5
|
+
|
|
6
|
+
# @param ack [Boolean]
|
|
7
|
+
# @param stream_id [Integer]
|
|
8
|
+
# @param setting [Hash<Integer, Integer>] uint16 uint32
|
|
9
|
+
def initialize(ack, stream_id, setting)
|
|
10
|
+
@f_type = FrameType::SETTINGS
|
|
11
|
+
@ack = ack
|
|
12
|
+
@stream_id = stream_id
|
|
13
|
+
@setting = setting
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [Boolean]
|
|
17
|
+
def ack?
|
|
18
|
+
@ack
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Integer]
|
|
22
|
+
def length
|
|
23
|
+
@setting.length * 6
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [String]
|
|
27
|
+
def to_binary_s
|
|
28
|
+
payload_length = length
|
|
29
|
+
flags = Frame.to_flags(ack: ack?)
|
|
30
|
+
setting = @setting.map { |x| x.pack('nN') }.join
|
|
31
|
+
|
|
32
|
+
Frame.to_binary_s_header(payload_length, @f_type, flags, @stream_id) + setting
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param s [String]
|
|
36
|
+
#
|
|
37
|
+
# @return [Settings]
|
|
38
|
+
# rubocop: disable Metrics/AbcSize
|
|
39
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
|
40
|
+
# rubocop: disable Metrics/PerceivedComplexity
|
|
41
|
+
def self.read(s)
|
|
42
|
+
payload_length, _, flags, stream_id = Frame.read_header(s)
|
|
43
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if s[9..].bytesize != payload_length
|
|
44
|
+
return ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'SETTINGS payload length MUST be a multiple of 6') if payload_length % 6 != 0
|
|
45
|
+
|
|
46
|
+
ack = Frame.read_ack(flags)
|
|
47
|
+
setting = s[9..].unpack('nN' * (payload_length / 6)).each_slice(2).to_h
|
|
48
|
+
return ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'SETTINGS MUST NOT have setting with ack') \
|
|
49
|
+
if ack && setting.any?
|
|
50
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid SETTINGS_ENABLE_PUSH') \
|
|
51
|
+
if setting.key?(SettingsID::SETTINGS_ENABLE_PUSH) && ![0, 1].include?(setting[SettingsID::SETTINGS_ENABLE_PUSH])
|
|
52
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid SETTINGS_MAX_FRAME_SIZE') \
|
|
53
|
+
if setting.key?(SettingsID::SETTINGS_MAX_FRAME_SIZE) && setting[SettingsID::SETTINGS_MAX_FRAME_SIZE] < 16_384
|
|
54
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid SETTINGS_MAX_FRAME_SIZE') \
|
|
55
|
+
if setting.key?(SettingsID::SETTINGS_MAX_FRAME_SIZE) && setting[SettingsID::SETTINGS_MAX_FRAME_SIZE] > 16_777_215
|
|
56
|
+
return ConnectionError.new(ErrorCode::FLOW_CONTROL_ERROR, 'invalid SETTINGS_INITIAL_WINDOW_SIZE') \
|
|
57
|
+
if setting.key?(SettingsID::SETTINGS_INITIAL_WINDOW_SIZE) && setting[SettingsID::SETTINGS_INITIAL_WINDOW_SIZE] > 2_147_483_647
|
|
58
|
+
|
|
59
|
+
Settings.new(ack, stream_id, setting)
|
|
60
|
+
end
|
|
61
|
+
# rubocop: enable Metrics/AbcSize
|
|
62
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
|
63
|
+
# rubocop: enable Metrics/PerceivedComplexity
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Biryani
|
|
2
|
+
module Frame
|
|
3
|
+
class Unknown
|
|
4
|
+
attr_reader :f_type, :flags, :stream_id, :payload
|
|
5
|
+
|
|
6
|
+
# @param f_type [Integer]
|
|
7
|
+
# @param flags [Integer]
|
|
8
|
+
# @param stream_id [Integer]
|
|
9
|
+
# @param payload [String, nil]
|
|
10
|
+
def initialize(f_type, flags, stream_id, payload)
|
|
11
|
+
@f_type = f_type
|
|
12
|
+
@flags = flags
|
|
13
|
+
@stream_id = stream_id
|
|
14
|
+
@payload = payload
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @return [Integer]
|
|
18
|
+
def length
|
|
19
|
+
@payload.bytesize
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [String]
|
|
23
|
+
def to_binary_s
|
|
24
|
+
payload_length = length
|
|
25
|
+
|
|
26
|
+
Frame.to_binary_s_header(payload_length, @f_type, flags, @stream_id) + @payload
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param s [String]
|
|
30
|
+
#
|
|
31
|
+
# @return [Unknown]
|
|
32
|
+
def self.read(s)
|
|
33
|
+
payload_length, f_type, flags, stream_id = Frame.read_header(s)
|
|
34
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if s[9..].bytesize != payload_length
|
|
35
|
+
|
|
36
|
+
payload = s[9..]
|
|
37
|
+
|
|
38
|
+
Unknown.new(f_type, flags, stream_id, payload)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module Biryani
|
|
2
|
+
module Frame
|
|
3
|
+
class WindowUpdate
|
|
4
|
+
attr_reader :f_type, :stream_id, :window_size_increment
|
|
5
|
+
|
|
6
|
+
# @param stream_id [Integer]
|
|
7
|
+
# @param window_size_increment [Integer]
|
|
8
|
+
def initialize(stream_id, window_size_increment)
|
|
9
|
+
@f_type = FrameType::WINDOW_UPDATE
|
|
10
|
+
@stream_id = stream_id
|
|
11
|
+
@window_size_increment = window_size_increment
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @return [Integer]
|
|
15
|
+
def length
|
|
16
|
+
4
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @return [String]
|
|
20
|
+
def to_binary_s
|
|
21
|
+
payload_length = length
|
|
22
|
+
flags = 0x00
|
|
23
|
+
|
|
24
|
+
Frame.to_binary_s_header(payload_length, @f_type, flags, @stream_id) + [@window_size_increment].pack('N')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param s [String]
|
|
28
|
+
#
|
|
29
|
+
# @return [WindowUpdate]
|
|
30
|
+
def self.read(s)
|
|
31
|
+
payload_length, _, _, stream_id = Frame.read_header(s)
|
|
32
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if s[9..].bytesize != payload_length
|
|
33
|
+
return ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'WINDOW_UPDATE payload length MUST be 4') if s[9..].bytesize != 4
|
|
34
|
+
|
|
35
|
+
window_size_increment = s[9..].unpack1('N')
|
|
36
|
+
return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'WINDOW_UPDATE invalid window size increment 0') if window_size_increment.zero?
|
|
37
|
+
return ConnectionError.new(ErrorCode::FLOW_CONTROL_ERROR, 'WINDOW_UPDATE invalid window size increment greater than 2^31-1') if window_size_increment > 2**31 - 1
|
|
38
|
+
|
|
39
|
+
WindowUpdate.new(stream_id, window_size_increment)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
module Biryani
|
|
2
|
+
module FrameType
|
|
3
|
+
DATA = 0x00
|
|
4
|
+
HEADERS = 0x01
|
|
5
|
+
PRIORITY = 0x02
|
|
6
|
+
RST_STREAM = 0x03
|
|
7
|
+
SETTINGS = 0x04
|
|
8
|
+
PUSH_PROMISE = 0x05
|
|
9
|
+
PING = 0x06
|
|
10
|
+
GOAWAY = 0x07
|
|
11
|
+
WINDOW_UPDATE = 0x08
|
|
12
|
+
CONTINUATION = 0x09
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module ErrorCode
|
|
16
|
+
NO_ERROR = 0x00
|
|
17
|
+
PROTOCOL_ERROR = 0x01
|
|
18
|
+
INTERNAL_ERROR = 0x02
|
|
19
|
+
FLOW_CONTROL_ERROR = 0x03
|
|
20
|
+
SETTINGS_TIMEOUT = 0x04
|
|
21
|
+
STREAM_CLOSED = 0x05
|
|
22
|
+
FRAME_SIZE_ERROR = 0x06
|
|
23
|
+
REFUSED_STREAM = 0x07
|
|
24
|
+
CANCEL = 0x08
|
|
25
|
+
COMPRESSION_ERROR = 0x09
|
|
26
|
+
CONNECT_ERROR = 0x0a
|
|
27
|
+
ENHANCE_YOUR_CALM = 0x0b
|
|
28
|
+
INADEQUATE_SECURITY = 0x0c
|
|
29
|
+
HTTP_1_1_REQUIRED = 0x0d
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module Frame
|
|
33
|
+
# @param s [String]
|
|
34
|
+
#
|
|
35
|
+
# @return [Integer]
|
|
36
|
+
# @return [Integer]
|
|
37
|
+
# @return [Integer]
|
|
38
|
+
# @return [Integer]
|
|
39
|
+
def self.read_header(s)
|
|
40
|
+
b0, b1, b2, f_type, uint8 = s[0..4].bytes
|
|
41
|
+
payload_length = (b0 << 16) | (b1 << 8) | b2
|
|
42
|
+
stream_id = s[5..8].unpack1('N') % 2**31 # Stream Identifier (31)
|
|
43
|
+
|
|
44
|
+
[payload_length, f_type, uint8, stream_id]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @param uint8 [Integer]
|
|
48
|
+
#
|
|
49
|
+
# @return [Boolean]
|
|
50
|
+
def self.read_priority(uint8)
|
|
51
|
+
(uint8 & 0b00100000).positive?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @param uint8 [Integer]
|
|
55
|
+
#
|
|
56
|
+
# @return [Boolean]
|
|
57
|
+
def self.read_padded(uint8)
|
|
58
|
+
(uint8 & 0b00001000).positive?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @param uint8 [Integer]
|
|
62
|
+
#
|
|
63
|
+
# @return [Boolean]
|
|
64
|
+
def self.read_end_headers(uint8)
|
|
65
|
+
(uint8 & 0b00000100).positive?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @param uint8 [Integer]
|
|
69
|
+
#
|
|
70
|
+
# @return [Boolean]
|
|
71
|
+
def self.read_end_stream(uint8)
|
|
72
|
+
(uint8 & 0b00000001).positive?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @param uint8 [Integer]
|
|
76
|
+
#
|
|
77
|
+
# @return [Boolean]
|
|
78
|
+
def self.read_ack(uint8)
|
|
79
|
+
(uint8 & 0b00000001).positive?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @return [Integer]
|
|
83
|
+
def self.to_flags(priority: false, padded: false, end_headers: false, end_stream: false, ack: false)
|
|
84
|
+
(priority ? 32 : 0) + (padded ? 8 : 0) + (end_headers ? 4 : 0) + (end_stream || ack ? 1 : 0)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @param payload_length [Integer]
|
|
88
|
+
# @param f_type [Integer]
|
|
89
|
+
# @param flags [Integer]
|
|
90
|
+
# @param stream_id
|
|
91
|
+
#
|
|
92
|
+
# @return [String]
|
|
93
|
+
def self.to_binary_s_header(payload_length, f_type, flags, stream_id)
|
|
94
|
+
[payload_length, f_type, flags, stream_id].pack('NCCN')[1..]
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
require_relative 'frame/continuation'
|
|
100
|
+
require_relative 'frame/data'
|
|
101
|
+
require_relative 'frame/goaway'
|
|
102
|
+
require_relative 'frame/headers'
|
|
103
|
+
require_relative 'frame/ping'
|
|
104
|
+
require_relative 'frame/priority'
|
|
105
|
+
require_relative 'frame/push_promise'
|
|
106
|
+
require_relative 'frame/rst_stream'
|
|
107
|
+
require_relative 'frame/settings'
|
|
108
|
+
require_relative 'frame/unknown'
|
|
109
|
+
require_relative 'frame/window_update'
|
|
110
|
+
|
|
111
|
+
module Biryani
|
|
112
|
+
module Frame
|
|
113
|
+
FRAME_MAP = {
|
|
114
|
+
FrameType::DATA => Data,
|
|
115
|
+
FrameType::HEADERS => Headers,
|
|
116
|
+
FrameType::PRIORITY => Priority,
|
|
117
|
+
FrameType::RST_STREAM => RstStream,
|
|
118
|
+
FrameType::SETTINGS => Settings,
|
|
119
|
+
FrameType::PUSH_PROMISE => PushPromise,
|
|
120
|
+
FrameType::PING => Ping,
|
|
121
|
+
FrameType::GOAWAY => Goaway,
|
|
122
|
+
FrameType::WINDOW_UPDATE => WindowUpdate,
|
|
123
|
+
FrameType::CONTINUATION => Continuation
|
|
124
|
+
}.freeze
|
|
125
|
+
|
|
126
|
+
private_constant :FRAME_MAP
|
|
127
|
+
Ractor.make_shareable(FRAME_MAP)
|
|
128
|
+
|
|
129
|
+
# @param io [IO]
|
|
130
|
+
#
|
|
131
|
+
# @return [Object, nil, ConnectionError] frame or error
|
|
132
|
+
def self.read(io)
|
|
133
|
+
s = io.read(9)
|
|
134
|
+
return nil if s.nil?
|
|
135
|
+
return ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'invalid header length') if s.bytesize != 9
|
|
136
|
+
|
|
137
|
+
payload_length, f_type, flags, stream_id = read_header(s)
|
|
138
|
+
payload = io.read(payload_length)
|
|
139
|
+
return Frame::Unknown.new(f_type, flags, stream_id, payload) unless FRAME_MAP.key?(f_type)
|
|
140
|
+
|
|
141
|
+
FRAME_MAP[f_type].read(s + payload)
|
|
142
|
+
rescue StandardError
|
|
143
|
+
ConnectionError.new(ErrorCode::FRAME_SIZE_ERROR, 'invalid frame')
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Biryani
|
|
2
|
+
module HPACK
|
|
3
|
+
class Decoder
|
|
4
|
+
# @param dynamic_table_limit [Integer]
|
|
5
|
+
def initialize(dynamic_table_limit)
|
|
6
|
+
@dynamic_table = DynamicTable.new(dynamic_table_limit)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# @param s [String]
|
|
10
|
+
#
|
|
11
|
+
# @return [Array]
|
|
12
|
+
def decode(s)
|
|
13
|
+
Fields.decode(s.force_encoding(Encoding::ASCII_8BIT), @dynamic_table)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param new_limit [Integer]
|
|
17
|
+
def limit!(new_limit)
|
|
18
|
+
@dynamic_table.limit!(new_limit)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|