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,464 @@
1
+ module Biryani
2
+ module SettingsID
3
+ SETTINGS_HEADER_TABLE_SIZE = 0x0001
4
+ SETTINGS_ENABLE_PUSH = 0x0002
5
+ SETTINGS_MAX_CONCURRENT_STREAMS = 0x0003
6
+ SETTINGS_INITIAL_WINDOW_SIZE = 0x0004
7
+ SETTINGS_MAX_FRAME_SIZE = 0x0005
8
+ SETTINGS_MAX_HEADER_LIST_SIZE = 0x0006
9
+ end
10
+
11
+ # rubocop: disable Metrics/ClassLength
12
+ class Connection
13
+ CONNECTION_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".freeze
14
+ CONNECTION_PREFACE_LENGTH = CONNECTION_PREFACE.length
15
+
16
+ private_constant :CONNECTION_PREFACE, :CONNECTION_PREFACE_LENGTH
17
+ Ractor.make_shareable(CONNECTION_PREFACE)
18
+ Ractor.make_shareable(CONNECTION_PREFACE_LENGTH)
19
+
20
+ # proc [Proc]
21
+ def initialize(proc)
22
+ @sock = nil # Ractor
23
+ @proc = proc
24
+ @streams_ctx = StreamsContext.new
25
+ @encoder = HPACK::Encoder.new(4_096)
26
+ @decoder = HPACK::Decoder.new(4_096)
27
+ @send_window = Window.new
28
+ @recv_window = Window.new
29
+ @data_buffer = DataBuffer.new
30
+ @send_settings = self.class.default_settings # Hash<Integer, Integer>
31
+ @recv_settings = self.class.default_settings # Hash<Integer, Integer>
32
+ @closed = false
33
+ end
34
+
35
+ # @param io [IO]
36
+ def serve(io)
37
+ err = self.class.read_http2_magic(io)
38
+ unless err.nil?
39
+ self.class.do_send(io, err.goaway(@streams_ctx.last_stream_id), true)
40
+ return
41
+ end
42
+
43
+ self.class.do_send(io, Frame::Settings.new(false, 0, {}), true)
44
+
45
+ recv_loop(io.clone)
46
+ send_loop(io)
47
+ rescue StandardError
48
+ self.class.do_send(io, Frame::Goaway.new(0, @streams_ctx.last_stream_id, ErrorCode::INTERNAL_ERROR, 'internal error'), true)
49
+ ensure
50
+ io&.close_write
51
+ end
52
+
53
+ # @param io [IO]
54
+ def recv_loop(io)
55
+ Ractor.new(io, @sock = Ractor::Port.new) do |io_, sock_|
56
+ loop do
57
+ obj = Frame.read(io_)
58
+ break if obj.nil?
59
+
60
+ sock_ << obj
61
+ end
62
+ end
63
+ end
64
+
65
+ # @param io [IO]
66
+ # rubocop: disable Metrics/AbcSize
67
+ # rubocop: disable Metrics/BlockLength
68
+ # rubocop: disable Metrics/CyclomaticComplexity
69
+ # rubocop: disable Metrics/PerceivedComplexity
70
+ def send_loop(io)
71
+ loop do
72
+ ports = @streams_ctx.txs
73
+ ports << @sock
74
+ next if ports.empty?
75
+
76
+ port, obj = Ractor.select(*ports)
77
+ if port == @sock
78
+ if obj.is_a?(StreamError) || obj.is_a?(ConnectionError)
79
+ reply_frame = self.class.unwrap(obj, @streams_ctx.last_stream_id)
80
+ self.class.do_send(io, reply_frame, true)
81
+ close if self.class.transition_state_send(reply_frame, @streams_ctx)
82
+ elsif obj.length > @send_settings[SettingsID::SETTINGS_MAX_FRAME_SIZE]
83
+ 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)
84
+ close
85
+ else
86
+ recv_dispatch(obj).each do |frame|
87
+ reply_frame = self.class.unwrap(frame, @streams_ctx.last_stream_id)
88
+ self.class.do_send(io, reply_frame, true)
89
+ close if self.class.transition_state_send(reply_frame, @streams_ctx)
90
+ end
91
+ end
92
+ else
93
+ self.class.http_response(*obj, @encoder, @send_settings[SettingsID::SETTINGS_MAX_FRAME_SIZE]).each do |send_frame|
94
+ close if self.class.send(io, send_frame, @send_window, @streams_ctx, @data_buffer)
95
+ end
96
+
97
+ self.class.remove_closed_streams(@streams_ctx, @data_buffer)
98
+ end
99
+
100
+ break if closed?
101
+ end
102
+ ensure
103
+ self.class.clear_all_streams(@streams_ctx)
104
+ end
105
+ # rubocop: enable Metrics/AbcSize
106
+ # rubocop: enable Metrics/BlockLength
107
+ # rubocop: enable Metrics/CyclomaticComplexity
108
+ # rubocop: enable Metrics/PerceivedComplexity
109
+
110
+ # @param frame [Object]
111
+ #
112
+ # @return [Array<Object>, Array<ConnectionError>, Array<StreamError>] frames or errors
113
+ def recv_dispatch(frame)
114
+ if frame.stream_id.zero?
115
+ handle_connection_frame(frame)
116
+ else
117
+ handle_stream_frame(frame)
118
+ end
119
+ end
120
+
121
+ # @param frame [Object]
122
+ #
123
+ # @return [Array<Object>, Array<ConnectionError>, Array<StreamError>] frames or errors
124
+ # rubocop: disable Metrics/CyclomaticComplexity
125
+ def handle_connection_frame(frame)
126
+ typ = frame.f_type
127
+ case typ
128
+ when FrameType::DATA, FrameType::HEADERS, FrameType::PRIORITY, FrameType::RST_STREAM, FrameType::PUSH_PROMISE, FrameType::CONTINUATION
129
+ [ConnectionError.new(ErrorCode::PROTOCOL_ERROR, "invalid frame type #{format('0x%02x', typ)} for stream identifier 0x00")]
130
+ when FrameType::SETTINGS
131
+ obj = self.class.handle_settings(frame, @send_settings, @decoder)
132
+ return [] if obj.nil?
133
+
134
+ settings_ack = obj
135
+ [settings_ack]
136
+ when FrameType::PING
137
+ obj = self.class.handle_ping(frame)
138
+ return [] if obj.nil?
139
+
140
+ [obj]
141
+ when FrameType::GOAWAY
142
+ self.class.handle_goaway(frame)
143
+ # TODO: logging error
144
+ []
145
+ when FrameType::WINDOW_UPDATE
146
+ err = self.class.handle_connection_window_update(frame, @send_window)
147
+ return [err] unless err.nil?
148
+
149
+ @data_buffer.take!(@send_window, @streams_ctx)
150
+ else
151
+ # ignore unknown frame type
152
+ []
153
+ end
154
+ end
155
+ # rubocop: enable Metrics/CyclomaticComplexity
156
+
157
+ # @param frame [Object]
158
+ #
159
+ # @return [Array<Object>, Array<ConnectionError>, Array<StreamError>] frames or errors
160
+ # rubocop: disable Metrics/AbcSize
161
+ # rubocop: disable Metrics/CyclomaticComplexity
162
+ # rubocop: disable Metrics/MethodLength
163
+ # rubocop: disable Metrics/PerceivedComplexity
164
+ def handle_stream_frame(frame)
165
+ stream_id = frame.stream_id
166
+ typ = frame.f_type
167
+ return [ConnectionError.new(ErrorCode::PROTOCOL_ERROR, "invalid frame type #{format('0x%02x', typ)} for stream identifier #{format('0x%02x', stream_id)}")] \
168
+ if [FrameType::SETTINGS, FrameType::PING, FrameType::GOAWAY].include?(typ)
169
+
170
+ obj = self.class.transition_state_recv(frame, @streams_ctx, stream_id, @send_settings[SettingsID::SETTINGS_MAX_CONCURRENT_STREAMS], @proc)
171
+ return [obj] if obj.is_a?(StreamError) || obj.is_a?(ConnectionError)
172
+
173
+ ctx = obj
174
+ case typ
175
+ when FrameType::DATA
176
+ ctx.content << frame.data
177
+ if ctx.state.half_closed_remote?
178
+ obj = self.class.http_request(ctx.fragment.string, ctx.content.string, @decoder)
179
+ return [obj] if obj.is_a?(StreamError) || obj.is_a?(ConnectionError)
180
+
181
+ ctx.stream.rx << obj
182
+ end
183
+
184
+ []
185
+ when FrameType::HEADERS, FrameType::CONTINUATION
186
+ ctx.fragment << frame.fragment
187
+ if ctx.state.half_closed_remote?
188
+ obj = self.class.http_request(ctx.fragment.string, ctx.content.string, @decoder)
189
+ return [obj] if obj.is_a?(StreamError) || obj.is_a?(ConnectionError)
190
+
191
+ ctx.stream.rx << obj
192
+ end
193
+
194
+ []
195
+ when FrameType::PRIORITY
196
+ # ignore PRIORITY Frame
197
+ []
198
+ when FrameType::PUSH_PROMISE
199
+ # TODO
200
+ []
201
+ when FrameType::RST_STREAM
202
+ self.class.handle_rst_stream(frame, @streams_ctx)
203
+ []
204
+ when FrameType::WINDOW_UPDATE
205
+ err = self.class.handle_stream_window_update(frame, @streams_ctx)
206
+ return [err] unless err.nil?
207
+
208
+ @data_buffer.take!(@send_window, @streams_ctx)
209
+ else
210
+ # ignore UNKNOWN Frame
211
+ []
212
+ end
213
+ end
214
+ # rubocop: enable Metrics/AbcSize
215
+ # rubocop: enable Metrics/CyclomaticComplexity
216
+ # rubocop: enable Metrics/MethodLength
217
+ # rubocop: enable Metrics/PerceivedComplexity
218
+
219
+ def close
220
+ @closed = true
221
+ end
222
+
223
+ # @return [Boolean]
224
+ def closed?
225
+ @closed
226
+ end
227
+
228
+ # @param obj [Object, ConnectionError, StreamError] frame or error
229
+ # @param last_stream_id [Integer]
230
+ #
231
+ # @return [Frame]
232
+ def self.unwrap(obj, last_stream_id)
233
+ case obj
234
+ when ConnectionError
235
+ obj.goaway(last_stream_id)
236
+ when StreamError
237
+ obj.rst_stream
238
+ else
239
+ obj
240
+ end
241
+ end
242
+
243
+ # @param recv_frame [Object]
244
+ # @param streams_ctx [StreamsContext]
245
+ # @param stream_id [Integer]
246
+ # @param max_streams [Integer]
247
+ # @param proc [Proc]
248
+ #
249
+ # @return [StreamContext, StreamError, ConnectionError]
250
+ # rubocop: disable Metrics/CyclomaticComplexity
251
+ # rubocop: disable Metrics/PerceivedComplexity
252
+ def self.transition_state_recv(recv_frame, streams_ctx, stream_id, max_streams, proc)
253
+ ctx = streams_ctx[stream_id]
254
+ return StreamError.new(ErrorCode::PROTOCOL_ERROR, stream_id, 'exceed max concurrent streams') if ctx.nil? && streams_ctx.count_active + 1 > max_streams
255
+ return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'even-numbered stream identifier') if ctx.nil? && stream_id.even?
256
+ 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
257
+
258
+ ctx = streams_ctx.new_context(stream_id, proc) if ctx.nil?
259
+ obj = ctx.state.transition!(recv_frame, :recv)
260
+ return obj if obj.is_a?(StreamError) || obj.is_a?(ConnectionError)
261
+
262
+ ctx
263
+ end
264
+ # rubocop: enable Metrics/CyclomaticComplexity
265
+ # rubocop: enable Metrics/PerceivedComplexity
266
+
267
+ # @param send_frame [Object]
268
+ # @param streams_ctx [StreamsContext]
269
+ #
270
+ # @return [Boolean] should close connection?
271
+ def self.transition_state_send(send_frame, streams_ctx)
272
+ stream_id = send_frame.stream_id
273
+ typ = send_frame.f_type
274
+ case typ
275
+ when FrameType::SETTINGS, FrameType::PING
276
+ false
277
+ when FrameType::GOAWAY
278
+ close_all_streams(streams_ctx)
279
+ true
280
+ else
281
+ streams_ctx[stream_id].state.transition!(send_frame, :send)
282
+ false
283
+ end
284
+ end
285
+
286
+ # @param streams_ctx [StreamsContext]
287
+ def self.clear_all_streams(streams_ctx)
288
+ streams_ctx.each do |ctx|
289
+ ctx.tx.close
290
+ ctx.stream.rx << nil
291
+ ctx.fragment.close
292
+ ctx.content.close
293
+ end
294
+ end
295
+
296
+ # @param streams_ctx [StreamsContext]
297
+ def self.close_all_streams(streams_ctx)
298
+ streams_ctx.each do |ctx|
299
+ ctx.tx.close
300
+ ctx.fragment.close
301
+ ctx.content.close
302
+ ctx.state.close
303
+ end
304
+ end
305
+
306
+ # @param streams_ctx [StreamsContext]
307
+ # @param data_buffer [DataBuffer]
308
+ def self.remove_closed_streams(streams_ctx, data_buffer)
309
+ closed_ids = streams_ctx.closed_stream_ids.filter { |id| !data_buffer.has?(id) }
310
+ closed_ids.each do |id|
311
+ streams_ctx[id].tx.close
312
+ streams_ctx[id].stream.rx << nil
313
+ streams_ctx[id].fragment.close
314
+ streams_ctx[id].content.close
315
+ end
316
+ end
317
+
318
+ # @param io [IO]
319
+ # @param frame [Object]
320
+ # @param flush [Boolean]
321
+ def self.do_send(io, frame, flush)
322
+ io.write(frame.to_binary_s)
323
+ io.flush if flush
324
+ end
325
+
326
+ # @param io [IO]
327
+ # @param frame [Object]
328
+ # @param send_window [Window]
329
+ # @param streams_ctx [StreamsContext]
330
+ # @param data_buffer [DataBuffer]
331
+ #
332
+ # @return [Boolean] should close connection?
333
+ def self.send(io, frame, send_window, streams_ctx, data_buffer)
334
+ if frame.f_type != FrameType::DATA
335
+ do_send(io, frame, false)
336
+ return transition_state_send(frame, streams_ctx)
337
+ end
338
+
339
+ data = frame
340
+ if sendable?(data, send_window, streams_ctx)
341
+ do_send(io, data, false)
342
+ send_window.consume!(data.length)
343
+ streams_ctx[data.stream_id].send_window.consume!(data.length)
344
+ return transition_state_send(frame, streams_ctx)
345
+ end
346
+
347
+ data_buffer << data
348
+ false
349
+ end
350
+
351
+ # @param data [Data]
352
+ # @param send_window [Window]
353
+ # @param streams_ctx [StreamsContext]
354
+ #
355
+ # @return [Boolean]
356
+ def self.sendable?(data, send_window, streams_ctx)
357
+ length = data.length
358
+ stream_id = data.stream_id
359
+ send_window.available?(length) && streams_ctx[stream_id].send_window.available?(length)
360
+ end
361
+
362
+ # @param io [IO]
363
+ #
364
+ # @return [nil, Error]
365
+ def self.read_http2_magic(io)
366
+ s = io.read(CONNECTION_PREFACE_LENGTH)
367
+ ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid connection preface') if s != CONNECTION_PREFACE
368
+ end
369
+
370
+ # @param rst_stream [RstStream]
371
+ # @param streams_ctx [StreamsContext]
372
+ def self.handle_rst_stream(rst_stream, streams_ctx)
373
+ stream_id = rst_stream.stream_id
374
+ streams_ctx[stream_id].state.close
375
+ end
376
+
377
+ # @param settings [Settings]
378
+ # @param send_settings [Hash<Integer, Integer>]
379
+ # @param decoder [Decoder]
380
+ #
381
+ # @return [Settings]
382
+ def self.handle_settings(settings, send_settings, decoder)
383
+ return nil if settings.ack?
384
+
385
+ send_settings.merge!(settings.setting)
386
+ decoder.limit!(send_settings[SettingsID::SETTINGS_HEADER_TABLE_SIZE])
387
+ Frame::Settings.new(true, 0, {})
388
+ end
389
+
390
+ # @param ping [Ping]
391
+ #
392
+ # @return [Ping, nil, ConnectionError]
393
+ def self.handle_ping(ping)
394
+ Frame::Ping.new(true, 0, ping.opaque) unless ping.ack?
395
+ end
396
+
397
+ # @param _goaway [Goaway]
398
+ def self.handle_goaway(_goaway); end
399
+
400
+ # @param window_update [WindowUpdate]
401
+ # @param send_window [Window]
402
+ #
403
+ # @return [nil, ConnectionError]
404
+ def self.handle_connection_window_update(window_update, send_window)
405
+ # TODO: send WINDOW_UPDATE
406
+ send_window.increase!(window_update.window_size_increment)
407
+ nil
408
+ end
409
+
410
+ # @param window_update [WindowUpdate]
411
+ # @param streams_ctx [StreamsContext]
412
+ #
413
+ # @return [nil, ConnectionError]
414
+ def self.handle_stream_window_update(window_update, streams_ctx)
415
+ return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'WINDOW_UPDATE invalid window size increment 0') if window_update.window_size_increment.zero?
416
+ return StreamError.new(ErrorCode::FLOW_CONTROL_ERROR, window_update.stream_id, 'WINDOW_UPDATE invalid window size increment greater than 2^31-1') \
417
+ if window_update.window_size_increment > 2**31 - 1
418
+
419
+ streams_ctx[window_update.stream_id].send_window.increase!(window_update.window_size_increment)
420
+ nil
421
+ end
422
+
423
+ # @param fragment [String]
424
+ # @param content [String]
425
+ # @param decoder [Decoder]
426
+ #
427
+ # @return [HTTPRequest, ConnectionError]
428
+ def self.http_request(fragment, content, decoder)
429
+ obj = decoder.decode(fragment)
430
+ return obj if obj.is_a?(StreamError) || obj.is_a?(ConnectionError)
431
+
432
+ fields = obj
433
+ builder = HTTPRequestBuilder.new
434
+ err = builder.fields(fields)
435
+ return err unless err.nil?
436
+
437
+ builder.build(content)
438
+ end
439
+
440
+ # @param res [HTTPResponse]
441
+ # @param stream_id [Integer]
442
+ # @param encoder [Encoder]
443
+ # @param max_frame_size [Integer]
444
+ #
445
+ # @return [Array<Object>] frames
446
+ def self.http_response(res, stream_id, encoder, max_frame_size)
447
+ HTTPResponseParser.new(res).parse(stream_id, encoder, max_frame_size)
448
+ end
449
+
450
+ # @return [Hash<Integer, Integer>]
451
+ def self.default_settings
452
+ # https://datatracker.ietf.org/doc/html/rfc9113#section-6.5.2
453
+ {
454
+ SettingsID::SETTINGS_HEADER_TABLE_SIZE => 4_096,
455
+ SettingsID::SETTINGS_ENABLE_PUSH => 1,
456
+ SettingsID::SETTINGS_MAX_CONCURRENT_STREAMS => 0xffffffff,
457
+ SettingsID::SETTINGS_INITIAL_WINDOW_SIZE => 65_535,
458
+ SettingsID::SETTINGS_MAX_FRAME_SIZE => 16_384,
459
+ SettingsID::SETTINGS_MAX_HEADER_LIST_SIZE => 0xffffffff
460
+ }
461
+ end
462
+ end
463
+ # rubocop: enable Metrics/ClassLength
464
+ end
@@ -0,0 +1,17 @@
1
+ module Biryani
2
+ class ConnectionError
3
+ # @param code [Integer]
4
+ # @param debug [String]
5
+ def initialize(code, debug)
6
+ @code = code
7
+ @debug = debug
8
+ end
9
+
10
+ # @param last_stream_id [Integer]
11
+ #
12
+ # @return [Goaway]
13
+ def goaway(last_stream_id)
14
+ Frame::Goaway.new(0, last_stream_id, @code, @debug)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,42 @@
1
+ module Biryani
2
+ class DataBuffer
3
+ def initialize
4
+ @buffer = [] # Array<Data>
5
+ end
6
+
7
+ # @param data [Data]
8
+ def <<(data)
9
+ @buffer << data
10
+ end
11
+
12
+ # @param send_window [Window]
13
+ # @param stream_ctxs [Hash<Integer, StreamContext>]
14
+ #
15
+ # @return [Array<Data>]
16
+ def take!(send_window, stream_ctxs)
17
+ datas = {}
18
+ @buffer.each_with_index.each do |data, i|
19
+ next unless Connection.sendable?(data, send_window, stream_ctxs)
20
+
21
+ send_window.consume!(data.length)
22
+ stream_ctxs[data.stream_id].send_window.consume!(data.length)
23
+ datas[i] = data
24
+ end
25
+
26
+ @buffer = @buffer.each_with_index.filter { |_, i| datas.keys.include?(i) }.map(&:first)
27
+ datas.values
28
+ end
29
+
30
+ # @return [Integer]
31
+ def length
32
+ @buffer.length
33
+ end
34
+
35
+ # @param stream_id [Integer]
36
+ #
37
+ # @return [Boolean]
38
+ def has?(stream_id)
39
+ @buffer.filter { |data| data.stream_id == stream_id }.any?
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,48 @@
1
+ module Biryani
2
+ module Frame
3
+ class Continuation
4
+ attr_reader :f_type, :stream_id, :fragment
5
+
6
+ # @param end_headers [Boolean]
7
+ # @param stream_id [Integer]
8
+ # @param fragment [String]
9
+ def initialize(end_headers, stream_id, fragment)
10
+ @f_type = FrameType::CONTINUATION
11
+ @end_headers = end_headers
12
+ @stream_id = stream_id
13
+ @fragment = fragment
14
+ end
15
+
16
+ # @return [Boolean]
17
+ def end_headers?
18
+ @end_headers
19
+ end
20
+
21
+ # @return [Integer]
22
+ def length
23
+ @fragment.bytesize
24
+ end
25
+
26
+ # @return [String]
27
+ def to_binary_s
28
+ payload_length = length
29
+ flags = Frame.to_flags(end_headers: end_headers?)
30
+
31
+ Frame.to_binary_s_header(payload_length, @f_type, flags, @stream_id) + @fragment
32
+ end
33
+
34
+ # @param s [String]
35
+ #
36
+ # @return [Continuation]
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
+
41
+ end_headers = Frame.read_end_headers(flags)
42
+ fragment = s[9..]
43
+
44
+ Continuation.new(end_headers, stream_id, fragment)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,70 @@
1
+ module Biryani
2
+ module Frame
3
+ class Data
4
+ attr_reader :f_type, :stream_id, :data, :padding
5
+
6
+ # @param end_stream [Boolean]
7
+ # @param stream_id [Integer]
8
+ # @param data [String]
9
+ # @param padding [String, nil]
10
+ def initialize(end_stream, stream_id, data, padding)
11
+ @f_type = FrameType::DATA
12
+ @end_stream = end_stream
13
+ @stream_id = stream_id
14
+ @data = data
15
+ @padding = padding
16
+ end
17
+
18
+ # @return [Boolean]
19
+ def padded?
20
+ !@padding.nil?
21
+ end
22
+
23
+ # @return [Boolean]
24
+ def end_stream?
25
+ @end_stream
26
+ end
27
+
28
+ # @return [Integer]
29
+ def length
30
+ @data.bytesize + (padded? ? 1 + @padding.bytesize : 0)
31
+ end
32
+
33
+ # @return [String]
34
+ def to_binary_s
35
+ payload_length = length
36
+ flags = Frame.to_flags(padded: padded?, end_stream: end_stream?)
37
+ pad_length = padded? ? @padding.bytesize.chr : ''
38
+ padding = @padding || ''
39
+
40
+ Frame.to_binary_s_header(payload_length, @f_type, flags, @stream_id) + pad_length + @data + padding
41
+ end
42
+
43
+ # @param s [String]
44
+ #
45
+ # @return [Data]
46
+ def self.read(s)
47
+ payload_length, _, flags, stream_id = Frame.read_header(s)
48
+ return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if s[9..].bytesize != payload_length
49
+
50
+ padded = Frame.read_padded(flags)
51
+ end_stream = Frame.read_end_stream(flags)
52
+
53
+ if padded
54
+ pad_length = s[9].unpack1('C')
55
+ data_length = payload_length - pad_length - 1
56
+ data = s[10...10 + data_length]
57
+ padding = s[10 + data_length..]
58
+ return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if pad_length >= payload_length
59
+ return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if padding.bytesize != pad_length
60
+ else
61
+ data = s[9..]
62
+ padding = nil
63
+ return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if data.bytesize != payload_length
64
+ end
65
+
66
+ Data.new(end_stream, stream_id, data, padding)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,44 @@
1
+ module Biryani
2
+ module Frame
3
+ class Goaway
4
+ attr_reader :f_type, :stream_id, :last_stream_id, :error_code, :debug
5
+
6
+ # @param last_stream_id [Integer]
7
+ # @param error_code [Integer]
8
+ # @param debug [String]
9
+ def initialize(stream_id, last_stream_id, error_code, debug)
10
+ @f_type = FrameType::GOAWAY
11
+ @stream_id = stream_id
12
+ @last_stream_id = last_stream_id
13
+ @error_code = error_code
14
+ @debug = debug
15
+ end
16
+
17
+ # @return [Integer]
18
+ def length
19
+ @debug.bytesize + 8
20
+ end
21
+
22
+ # @return [String]
23
+ def to_binary_s
24
+ payload_length = length
25
+ flags = 0x00
26
+
27
+ Frame.to_binary_s_header(payload_length, @f_type, flags, @stream_id) + [@last_stream_id, @error_code].pack('NN') + @debug
28
+ end
29
+
30
+ # @param s [String]
31
+ #
32
+ # @return [Goaway]
33
+ def self.read(s)
34
+ payload_length, _, _, stream_id = Frame.read_header(s)
35
+ return ConnectionError.new(ErrorCode::PROTOCOL_ERROR, 'invalid frame') if s[9..].bytesize != payload_length
36
+
37
+ last_stream_id, error_code = s[9..16].unpack('NN')
38
+ debug = s[17..]
39
+
40
+ Goaway.new(stream_id, last_stream_id, error_code, debug)
41
+ end
42
+ end
43
+ end
44
+ end