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.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +30 -0
  3. data/.gitignore +17 -0
  4. data/.rubocop.yml +36 -0
  5. data/.ruby-version +1 -0
  6. data/Gemfile +9 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +48 -0
  9. data/Rakefile +8 -0
  10. data/biryani.gemspec +21 -0
  11. data/example/echo.rb +27 -0
  12. data/example/hello_world.rb +22 -0
  13. data/lib/biryani/connection.rb +464 -0
  14. data/lib/biryani/connection_error.rb +17 -0
  15. data/lib/biryani/data_buffer.rb +42 -0
  16. data/lib/biryani/frame/continuation.rb +48 -0
  17. data/lib/biryani/frame/data.rb +70 -0
  18. data/lib/biryani/frame/goaway.rb +44 -0
  19. data/lib/biryani/frame/headers.rb +110 -0
  20. data/lib/biryani/frame/ping.rb +49 -0
  21. data/lib/biryani/frame/priority.rb +44 -0
  22. data/lib/biryani/frame/push_promise.rb +75 -0
  23. data/lib/biryani/frame/rst_stream.rb +40 -0
  24. data/lib/biryani/frame/settings.rb +66 -0
  25. data/lib/biryani/frame/unknown.rb +42 -0
  26. data/lib/biryani/frame/window_update.rb +43 -0
  27. data/lib/biryani/frame.rb +146 -0
  28. data/lib/biryani/hpack/decoder.rb +22 -0
  29. data/lib/biryani/hpack/dynamic_table.rb +65 -0
  30. data/lib/biryani/hpack/encoder.rb +22 -0
  31. data/lib/biryani/hpack/error.rb +12 -0
  32. data/lib/biryani/hpack/field.rb +357 -0
  33. data/lib/biryani/hpack/fields.rb +28 -0
  34. data/lib/biryani/hpack/huffman.rb +309 -0
  35. data/lib/biryani/hpack/integer.rb +66 -0
  36. data/lib/biryani/hpack/option.rb +24 -0
  37. data/lib/biryani/hpack/string.rb +40 -0
  38. data/lib/biryani/hpack.rb +84 -0
  39. data/lib/biryani/http_request.rb +83 -0
  40. data/lib/biryani/http_response.rb +61 -0
  41. data/lib/biryani/server.rb +19 -0
  42. data/lib/biryani/state.rb +224 -0
  43. data/lib/biryani/stream.rb +19 -0
  44. data/lib/biryani/stream_error.rb +17 -0
  45. data/lib/biryani/streams_context.rb +89 -0
  46. data/lib/biryani/version.rb +3 -0
  47. data/lib/biryani/window.rb +29 -0
  48. data/lib/biryani.rb +17 -0
  49. data/spec/connection/close_all_streams_spec.rb +17 -0
  50. data/spec/connection/handle_connection_window_update_spec.rb +16 -0
  51. data/spec/connection/handle_ping_spec.rb +21 -0
  52. data/spec/connection/handle_rst_stream_spec.rb +21 -0
  53. data/spec/connection/handle_settings_spec.rb +31 -0
  54. data/spec/connection/handle_stream_window_update_spec.rb +20 -0
  55. data/spec/connection/read_http2_magic_spec.rb +26 -0
  56. data/spec/connection/remove_closed_streams_spec.rb +51 -0
  57. data/spec/connection/send_spec.rb +114 -0
  58. data/spec/connection/transition_state_send_spec.rb +39 -0
  59. data/spec/connection/unwrap_spec.rb +28 -0
  60. data/spec/data_buffer_spec.rb +135 -0
  61. data/spec/frame/continuation_spec.rb +39 -0
  62. data/spec/frame/data_spec.rb +25 -0
  63. data/spec/frame/goaway_spec.rb +23 -0
  64. data/spec/frame/headers_spec.rb +52 -0
  65. data/spec/frame/ping_spec.rb +22 -0
  66. data/spec/frame/priority_spec.rb +22 -0
  67. data/spec/frame/push_promise_spec.rb +24 -0
  68. data/spec/frame/read_spec.rb +30 -0
  69. data/spec/frame/rst_stream_spec.rb +21 -0
  70. data/spec/frame/settings_spec.rb +23 -0
  71. data/spec/frame/window_update_spec.rb +21 -0
  72. data/spec/hpack/decoder_spec.rb +170 -0
  73. data/spec/hpack/encoder_spec.rb +48 -0
  74. data/spec/hpack/field_spec.rb +42 -0
  75. data/spec/hpack/fields_spec.rb +17 -0
  76. data/spec/hpack/huffman_spec.rb +20 -0
  77. data/spec/hpack/integer_spec.rb +27 -0
  78. data/spec/hpack/string_spec.rb +19 -0
  79. data/spec/http_request_builder_spec.rb +45 -0
  80. data/spec/spec_helper.rb +9 -0
  81. 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