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,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
|