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,65 @@
1
+ module Biryani
2
+ module HPACK
3
+ class DynamicTable
4
+ attr_reader :size, :limit
5
+
6
+ # @param limit [Integer]
7
+ def initialize(limit)
8
+ @table = []
9
+ @size = 0
10
+ @limit = limit
11
+ end
12
+
13
+ # @return [Integer]
14
+ def count_entries
15
+ @table.length
16
+ end
17
+
18
+ # @param name [String]
19
+ # @param value [String]
20
+ def store(name, value)
21
+ @table.unshift([name, value])
22
+ @size += name.bytesize + value.bytesize + 32
23
+ chomp!(@limit)
24
+ end
25
+
26
+ # @param name [String]
27
+ # @param value [String]
28
+ #
29
+ # @return [Array]
30
+ # @return [Integer]
31
+ def find_field(name, value)
32
+ @table.each_with_index.find { |nv, _| nv[0] == name && nv[1] == value }
33
+ end
34
+
35
+ # @param name [String]
36
+ #
37
+ # @return [Array]
38
+ # @return [Integer]
39
+ def find_name(name)
40
+ @table.each_with_index.find { |nv, _| nv[0] == name }
41
+ end
42
+
43
+ # @param name [Integer]
44
+ #
45
+ # @return [Array, nil]
46
+ def [](index)
47
+ @table[index]
48
+ end
49
+
50
+ # @param new_limit [Integer]
51
+ def chomp!(new_limit)
52
+ while @size > new_limit
53
+ n, v = @table.pop
54
+ @size -= n.bytesize + v.bytesize + 32
55
+ end
56
+ end
57
+
58
+ # @param new_limit [Integer]
59
+ def limit!(new_limit)
60
+ chomp!(new_limit)
61
+ @limit = new_limit
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,22 @@
1
+ module Biryani
2
+ module HPACK
3
+ class Encoder
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 fields [Array]
10
+ #
11
+ # @return [String]
12
+ def encode(fields)
13
+ Fields.encode(fields, @dynamic_table).force_encoding(Encoding::ASCII_8BIT)
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
@@ -0,0 +1,12 @@
1
+ module Biryani
2
+ module HPACK
3
+ module Error
4
+ # Generic error, common for all classes under Biryani::HPACK::Error module.
5
+ class Error < StandardError; end
6
+
7
+ class HuffmanDecodeError < Error; end
8
+
9
+ class HPACKDecodeError < Error; end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,357 @@
1
+ module Biryani
2
+ module HPACK
3
+ # rubocop: disable Metrics/ModuleLength
4
+ module Field
5
+ # @param name [String]
6
+ # @param value [String]
7
+ # @param dynamic_table [DynamicTable]
8
+ #
9
+ # @return [String]
10
+ def self.encode(name, value, dynamic_table)
11
+ case find(name, value, dynamic_table)
12
+ in Some(index, v) if v.nil?
13
+ res = encode_indexed(index)
14
+ in Some(index, v)
15
+ res = encode_literal_value(index, v)
16
+ dynamic_table.store(name, v)
17
+ in None
18
+ res = encode_literal_field(name, value)
19
+ dynamic_table.store(name, value)
20
+ end
21
+
22
+ res
23
+ end
24
+
25
+ # @param name [String]
26
+ # @param value [String]
27
+ # @param dynamic_table [DynamicTable]
28
+ #
29
+ # @return [Some, None]
30
+ # rubocop: disable Metrics/CyclomaticComplexity
31
+ def self.find(name, value, dynamic_table)
32
+ found, i = STATIC_TABLE.each_with_index.find { |nv, _| nv[0] == name && nv[1] == value }
33
+ return Some.new(i + 1, nil) unless found.nil?
34
+
35
+ found, i = dynamic_table.find_field(name, value)
36
+ return Some.new(i + 1 + STATIC_TABLE_SIZE, nil) unless found.nil?
37
+
38
+ found, i = STATIC_TABLE.each_with_index.find { |nv, _| nv[0] == name }
39
+ return Some.new(i + 1, value) unless found.nil?
40
+
41
+ found, i = dynamic_table.find_name(name)
42
+ return Some.new(i + 1 + STATIC_TABLE_SIZE, value) unless found.nil?
43
+
44
+ None.new
45
+ end
46
+ # rubocop: enable Metrics/CyclomaticComplexity
47
+
48
+ # 0 1 2 3 4 5 6 7
49
+ # +---+---+---+---+---+---+---+---+
50
+ # | 1 | Index (7+) |
51
+ # +---+---------------------------+
52
+ # https://datatracker.ietf.org/doc/html/rfc7541#section-6.1
53
+ #
54
+ # @param index [Integer]
55
+ #
56
+ # @return [String]
57
+ def self.encode_indexed(index)
58
+ Integer.encode(index, 7, 0b10000000)
59
+ end
60
+
61
+ # 0 1 2 3 4 5 6 7
62
+ # +---+---+---+---+---+---+---+---+
63
+ # | 0 | 1 | Index (6+) |
64
+ # +---+---+-----------------------+
65
+ # | H | Value Length (7+) |
66
+ # +---+---------------------------+
67
+ # | Value String (Length octets) |
68
+ # +-------------------------------+
69
+ # https://datatracker.ietf.org/doc/html/rfc7541#section-6.2.1
70
+ #
71
+ # @param index [Integer]
72
+ # @param value [String]
73
+ #
74
+ # @return [String]
75
+ def self.encode_literal_value(index, value)
76
+ Integer.encode(index, 6, 0b01000000) + String.encode(value)
77
+ end
78
+
79
+ # 0 1 2 3 4 5 6 7
80
+ # +---+---+---+---+---+---+---+---+
81
+ # | 0 | 1 | 0 |
82
+ # +---+---+-----------------------+
83
+ # | H | Name Length (7+) |
84
+ # +---+---------------------------+
85
+ # | Name String (Length octets) |
86
+ # +---+---------------------------+
87
+ # | H | Value Length (7+) |
88
+ # +---+---------------------------+
89
+ # | Value String (Length octets) |
90
+ # +-------------------------------+
91
+ # https://datatracker.ietf.org/doc/html/rfc7541#section-6.2.1
92
+ #
93
+ # @param name [String]
94
+ # @param value [String]
95
+ #
96
+ # @return [String]
97
+ def self.encode_literal_field(name, value)
98
+ "\x40#{String.encode(name)}#{String.encode(value)}"
99
+ end
100
+
101
+ # @param s [String]
102
+ # @param cursor [Integer]
103
+ # @param dynamic_table [DynamicTable]
104
+ #
105
+ # @return [Array]
106
+ # @return [Integer]
107
+ # rubocop: disable Metrics/CyclomaticComplexity
108
+ # rubocop: disable Metrics/PerceivedComplexity
109
+ def self.decode(s, cursor, dynamic_table)
110
+ byte = s.getbyte(cursor)
111
+ if (byte & 0b10000000).positive?
112
+ decode_indexed(s, cursor, dynamic_table)
113
+ elsif byte == 0b01000000
114
+ decode_literal_field_incremental_indexing(s, cursor, dynamic_table)
115
+ elsif (byte & 0b01000000).positive?
116
+ decode_literal_value_incremental_indexing(s, cursor, dynamic_table)
117
+ elsif (byte & 0b00100000).positive?
118
+ decode_dynamic_table_size_update(s, cursor, dynamic_table)
119
+ elsif byte == 0b00010000
120
+ decode_literal_field_never_indexed(s, cursor)
121
+ elsif (byte & 0b00010000).positive?
122
+ decode_literal_value_never_indexed(s, cursor, dynamic_table)
123
+ elsif byte.zero?
124
+ decode_literal_field_without_indexing(s, cursor)
125
+ elsif (byte & 0b11110000).zero?
126
+ decode_literal_value_without_indexing(s, cursor, dynamic_table)
127
+ else
128
+ raise 'unreachable'
129
+ end
130
+ end
131
+ # rubocop: enable Metrics/CyclomaticComplexity
132
+ # rubocop: enable Metrics/PerceivedComplexity
133
+
134
+ # 0 1 2 3 4 5 6 7
135
+ # +---+---+---+---+---+---+---+---+
136
+ # | 1 | Index (7+) |
137
+ # +---+---------------------------+
138
+ # https://datatracker.ietf.org/doc/html/rfc7541#section-6.1
139
+ #
140
+ # @param s [String]
141
+ # @param cursor [Integer]
142
+ # @param dynamic_table [DynamicTable]
143
+ #
144
+ # @return [Array]
145
+ # @return [Integer]
146
+ def self.decode_indexed(s, cursor, dynamic_table)
147
+ index, c = Integer.decode(s, 7, cursor)
148
+ raise Error::HPACKDecodeError if index.zero?
149
+ raise Error::HPACKDecodeError if index > STATIC_TABLE_SIZE + dynamic_table.count_entries
150
+
151
+ field = if index <= STATIC_TABLE_SIZE
152
+ STATIC_TABLE[index - 1]
153
+ else
154
+ dynamic_table[index - 1 - STATIC_TABLE_SIZE]
155
+ end
156
+
157
+ [field, c]
158
+ end
159
+
160
+ # 0 1 2 3 4 5 6 7
161
+ # +---+---+---+---+---+---+---+---+
162
+ # | 0 | 1 | 0 |
163
+ # +---+---+-----------------------+
164
+ # | H | Name Length (7+) |
165
+ # +---+---------------------------+
166
+ # | Name String (Length octets) |
167
+ # +---+---------------------------+
168
+ # | H | Value Length (7+) |
169
+ # +---+---------------------------+
170
+ # | Value String (Length octets) |
171
+ # +-------------------------------+
172
+ # https://datatracker.ietf.org/doc/html/rfc7541#section-6.2.1
173
+ #
174
+ # @param s [String]
175
+ # @param cursor [Integer]
176
+ # @param dynamic_table [DynamicTable]
177
+ #
178
+ # @return [Array]
179
+ # @return [Integer]
180
+ def self.decode_literal_field_incremental_indexing(s, cursor, dynamic_table)
181
+ name, c = String.decode(s, cursor + 1)
182
+ value, c = String.decode(s, c)
183
+ dynamic_table.store(name, value)
184
+
185
+ [[name, value], c]
186
+ end
187
+
188
+ # 0 1 2 3 4 5 6 7
189
+ # +---+---+---+---+---+---+---+---+
190
+ # | 0 | 1 | Index (6+) |
191
+ # +---+---+-----------------------+
192
+ # | H | Value Length (7+) |
193
+ # +---+---------------------------+
194
+ # | Value String (Length octets) |
195
+ # +-------------------------------+
196
+ # https://datatracker.ietf.org/doc/html/rfc7541#section-6.2.1
197
+ #
198
+ # @param s [String]
199
+ # @param cursor [Integer]
200
+ # @param dynamic_table [DynamicTable]
201
+ #
202
+ # @return [Array]
203
+ # @return [Integer]
204
+ def self.decode_literal_value_incremental_indexing(s, cursor, dynamic_table)
205
+ index, c = Integer.decode(s, 6, cursor)
206
+ raise Error::HPACKDecodeError if index.zero?
207
+ raise Error::HPACKDecodeError if index > STATIC_TABLE_SIZE + dynamic_table.count_entries
208
+
209
+ name = if index <= STATIC_TABLE_SIZE
210
+ STATIC_TABLE[index - 1][0]
211
+ else
212
+ dynamic_table[index - 1 - STATIC_TABLE_SIZE][0]
213
+ end
214
+ value, c = String.decode(s, c)
215
+ dynamic_table.store(name, value)
216
+
217
+ [[name, value], c]
218
+ end
219
+
220
+ # 0 1 2 3 4 5 6 7
221
+ # +---+---+---+---+---+---+---+---+
222
+ # | 0 | 0 | 1 | Max size (5+) |
223
+ # +---+---------------------------+
224
+ # https://datatracker.ietf.org/doc/html/rfc7541#section-6.3
225
+ #
226
+ # @param s [String]
227
+ # @param cursor [Integer]
228
+ # @param dynamic_table [DynamicTable]
229
+ #
230
+ # @return [nil]
231
+ # @return [Integer]
232
+ def self.decode_dynamic_table_size_update(s, cursor, dynamic_table)
233
+ raise Error::HPACKDecodeError unless cursor.zero? || (s.getbyte(0) & 0b00100000).positive? && Integer.decode(s, 5, 0)[1] == cursor
234
+
235
+ max_size, c = Integer.decode(s, 5, cursor)
236
+ raise Error::HPACKDecodeError if max_size > dynamic_table.limit
237
+
238
+ dynamic_table.chomp!(max_size)
239
+ [nil, c]
240
+ end
241
+
242
+ # 0 1 2 3 4 5 6 7
243
+ # +---+---+---+---+---+---+---+---+
244
+ # | 0 | 0 | 0 | 1 | 0 |
245
+ # +---+---+-----------------------+
246
+ # | H | Name Length (7+) |
247
+ # +---+---------------------------+
248
+ # | Name String (Length octets) |
249
+ # +---+---------------------------+
250
+ # | H | Value Length (7+) |
251
+ # +---+---------------------------+
252
+ # | Value String (Length octets) |
253
+ # +-------------------------------+
254
+ # https://datatracker.ietf.org/doc/html/rfc7541#section-6.2.3
255
+ #
256
+ # @param s [String]
257
+ # @param cursor [Integer]
258
+ #
259
+ # @return [Array]
260
+ # @return [Integer]
261
+ def self.decode_literal_field_never_indexed(s, cursor)
262
+ name, c = String.decode(s, cursor + 1)
263
+ value, c = String.decode(s, c)
264
+
265
+ [[name, value], c]
266
+ end
267
+
268
+ # 0 1 2 3 4 5 6 7
269
+ # +---+---+---+---+---+---+---+---+
270
+ # | 0 | 0 | 0 | 1 | Index (4+) |
271
+ # +---+---+-----------------------+
272
+ # | H | Value Length (7+) |
273
+ # +---+---------------------------+
274
+ # | Value String (Length octets) |
275
+ # +-------------------------------+
276
+ # https://datatracker.ietf.org/doc/html/rfc7541#section-6.2.3
277
+ #
278
+ # @param s [String]
279
+ # @param cursor [Integer]
280
+ # @param dynamic_table [DynamicTable]
281
+ #
282
+ # @return [Array]
283
+ # @return [Integer]
284
+ def self.decode_literal_value_never_indexed(s, cursor, dynamic_table)
285
+ index, c = Integer.decode(s, 4, cursor)
286
+ raise Error::HPACKDecodeError if index.zero?
287
+ raise Error::HPACKDecodeError if index > STATIC_TABLE_SIZE + dynamic_table.count_entries
288
+
289
+ name = if index <= STATIC_TABLE_SIZE
290
+ STATIC_TABLE[index - 1][0]
291
+ else
292
+ dynamic_table[index - 1 - STATIC_TABLE_SIZE][0]
293
+ end
294
+ value, c = String.decode(s, c)
295
+
296
+ [[name, value], c]
297
+ end
298
+
299
+ # 0 1 2 3 4 5 6 7
300
+ # +---+---+---+---+---+---+---+---+
301
+ # | 0 | 0 | 0 | 1 | 0 |
302
+ # +---+---+-----------------------+
303
+ # | H | Name Length (7+) |
304
+ # +---+---------------------------+
305
+ # | Name String (Length octets) |
306
+ # +---+---------------------------+
307
+ # | H | Value Length (7+) |
308
+ # +---+---------------------------+
309
+ # | Value String (Length octets) |
310
+ # +-------------------------------+
311
+ # https://datatracker.ietf.org/doc/html/rfc7541#section-6.2.2
312
+ #
313
+ # @param s [String]
314
+ # @param cursor [Integer]
315
+ #
316
+ # @return [Array]
317
+ # @return [Integer]
318
+ def self.decode_literal_field_without_indexing(s, cursor)
319
+ name, c = String.decode(s, cursor + 1)
320
+ value, c = String.decode(s, c)
321
+
322
+ [[name, value], c]
323
+ end
324
+
325
+ # 0 1 2 3 4 5 6 7
326
+ # +---+---+---+---+---+---+---+---+
327
+ # | 0 | 0 | 0 | 0 | Index (4+) |
328
+ # +---+---+-----------------------+
329
+ # | H | Value Length (7+) |
330
+ # +---+---------------------------+
331
+ # | Value String (Length octets) |
332
+ # +-------------------------------+
333
+ #
334
+ # @param s [String]
335
+ # @param cursor [Integer]
336
+ # @param dynamic_table [DynamicTable]
337
+ #
338
+ # @return [Array]
339
+ # @return [Integer]
340
+ def self.decode_literal_value_without_indexing(s, cursor, dynamic_table)
341
+ index, c = Integer.decode(s, 4, cursor)
342
+ raise Error::HPACKDecodeError if index.zero?
343
+ raise Error::HPACKDecodeError if index > STATIC_TABLE_SIZE + dynamic_table.count_entries
344
+
345
+ name = if index <= STATIC_TABLE_SIZE
346
+ STATIC_TABLE[index - 1][0]
347
+ else
348
+ dynamic_table[index - 1 - STATIC_TABLE_SIZE][0]
349
+ end
350
+ value, c = String.decode(s, c)
351
+
352
+ [[name, value], c]
353
+ end
354
+ end
355
+ # rubocop: enable Metrics/ModuleLength
356
+ end
357
+ end
@@ -0,0 +1,28 @@
1
+ module Biryani
2
+ module HPACK
3
+ module Fields
4
+ # @param fields [Array]
5
+ # @param dynamic_table [DynamicTable]
6
+ #
7
+ # @return [String]
8
+ def self.encode(fields, dynamic_table)
9
+ fields.each_with_object(''.b) { |nv, acc| acc << Field.encode(nv[0].to_s, nv[1].to_s, dynamic_table) }
10
+ end
11
+
12
+ # @param s [String]
13
+ # @param dynamic_table [DynamicTable]
14
+ #
15
+ # @return [Array]
16
+ def self.decode(s, dynamic_table)
17
+ cursor = 0
18
+ fields = []
19
+ while cursor < s.bytesize
20
+ field, cursor = Field.decode(s, cursor, dynamic_table)
21
+ fields << field unless field.nil?
22
+ end
23
+
24
+ fields
25
+ end
26
+ end
27
+ end
28
+ end