http-2 0.6.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/.autotest +19 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/README.md +280 -0
- data/Rakefile +11 -0
- data/example/client.rb +46 -0
- data/example/helper.rb +14 -0
- data/example/server.rb +50 -0
- data/http-2.gemspec +24 -0
- data/lib/http/2/buffer.rb +21 -0
- data/lib/http/2/compressor.rb +493 -0
- data/lib/http/2/connection.rb +516 -0
- data/lib/http/2/emitter.rb +47 -0
- data/lib/http/2/error.rb +45 -0
- data/lib/http/2/flow_buffer.rb +64 -0
- data/lib/http/2/framer.rb +302 -0
- data/lib/http/2/stream.rb +474 -0
- data/lib/http/2/version.rb +3 -0
- data/lib/http/2.rb +9 -0
- data/spec/compressor_spec.rb +384 -0
- data/spec/connection_spec.rb +448 -0
- data/spec/emitter_spec.rb +46 -0
- data/spec/framer_spec.rb +325 -0
- data/spec/helper.rb +98 -0
- data/spec/stream_spec.rb +683 -0
- metadata +120 -0
@@ -0,0 +1,516 @@
|
|
1
|
+
module HTTP2
|
2
|
+
|
3
|
+
# Default connection and stream flow control window (64KB).
|
4
|
+
DEFAULT_FLOW_WINDOW = 65535
|
5
|
+
|
6
|
+
# Default stream priority (lower values are higher priority).
|
7
|
+
DEFAULT_PRIORITY = 2**30
|
8
|
+
|
9
|
+
# Default connection "fast-fail" preamble string as defined by the spec.
|
10
|
+
CONNECTION_HEADER = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
|
11
|
+
|
12
|
+
# Connection encapsulates all of the connection, stream, flow-control,
|
13
|
+
# error management, and other processing logic required for a well-behaved
|
14
|
+
# HTTP 2.0 client.
|
15
|
+
#
|
16
|
+
# When the connection object is instantiated you must specify its role
|
17
|
+
# (:client or :server) to initialize appropriate header compression
|
18
|
+
# and decompression algorithms and stream management logic.
|
19
|
+
#
|
20
|
+
# Your code is responsible for feeding data to connection object, which
|
21
|
+
# performs all of the necessary HTTP 2.0 decoding, state management and
|
22
|
+
# the rest, and vice versa, the parser will emit bytes (encoded HTTP 2.0
|
23
|
+
# frames) that you can then route to the destination. Roughly, this works
|
24
|
+
# as follows:
|
25
|
+
#
|
26
|
+
# @example
|
27
|
+
# socket = YourTransport.new
|
28
|
+
#
|
29
|
+
# conn = HTTP2::Connection.new(:client)
|
30
|
+
# conn.on(:frame) {|bytes| socket << bytes }
|
31
|
+
#
|
32
|
+
# while bytes = socket.read
|
33
|
+
# conn << bytes
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
class Connection
|
37
|
+
include FlowBuffer
|
38
|
+
include Emitter
|
39
|
+
include Error
|
40
|
+
|
41
|
+
# Type of connection (:server, :client).
|
42
|
+
attr_reader :type
|
43
|
+
|
44
|
+
# Connection state (:new, :closed).
|
45
|
+
attr_reader :state
|
46
|
+
|
47
|
+
# Last connection error if connection is aborted.
|
48
|
+
attr_reader :error
|
49
|
+
|
50
|
+
# Size of current connection flow control window (by default, set to
|
51
|
+
# infinity, but is automatically updated on receipt of peer settings).
|
52
|
+
attr_reader :window
|
53
|
+
|
54
|
+
# Maximum number of concurrent streams allowed by the peer (automatically
|
55
|
+
# updated on receipt of peer settings).
|
56
|
+
attr_reader :stream_limit
|
57
|
+
|
58
|
+
# Number of active streams between client and server (reserved streams
|
59
|
+
# are not counted towards the stream limit).
|
60
|
+
attr_reader :active_stream_count
|
61
|
+
|
62
|
+
# Initializes new client or server connection object.
|
63
|
+
#
|
64
|
+
# @param type [Symbol]
|
65
|
+
def initialize(type = :client)
|
66
|
+
@type = type
|
67
|
+
|
68
|
+
if @type == :server
|
69
|
+
@stream_id = 2
|
70
|
+
@compressor = Header::Compressor.new(:response)
|
71
|
+
@decompressor = Header::Decompressor.new(:request)
|
72
|
+
else
|
73
|
+
@stream_id = 1
|
74
|
+
@compressor = Header::Compressor.new(:request)
|
75
|
+
@decompressor = Header::Decompressor.new(:response)
|
76
|
+
end
|
77
|
+
|
78
|
+
@stream_limit = Float::INFINITY
|
79
|
+
@active_stream_count = 0
|
80
|
+
@streams = {}
|
81
|
+
|
82
|
+
@framer = Framer.new
|
83
|
+
@window = DEFAULT_FLOW_WINDOW
|
84
|
+
@window_limit = DEFAULT_FLOW_WINDOW
|
85
|
+
|
86
|
+
@recv_buffer = Buffer.new
|
87
|
+
@send_buffer = []
|
88
|
+
@continuation = []
|
89
|
+
@state = :new
|
90
|
+
@error = nil
|
91
|
+
end
|
92
|
+
|
93
|
+
# Allocates new stream for current connection.
|
94
|
+
#
|
95
|
+
# @param priority [Integer]
|
96
|
+
# @param window [Integer]
|
97
|
+
# @param parent [Stream]
|
98
|
+
def new_stream(priority: DEFAULT_PRIORITY, window: @window_limit, parent: nil)
|
99
|
+
raise ConnectionClosed.new if @state == :closed
|
100
|
+
raise StreamLimitExceeded.new if @active_stream_count == @stream_limit
|
101
|
+
|
102
|
+
stream = activate_stream(@stream_id, priority, window, parent)
|
103
|
+
@stream_id += 2
|
104
|
+
|
105
|
+
stream
|
106
|
+
end
|
107
|
+
|
108
|
+
# Sends PING frame to the peer.
|
109
|
+
#
|
110
|
+
# @param payload [String] optional payload must be 8 bytes long
|
111
|
+
# @param blk [Proc] callback to execute when PONG is received
|
112
|
+
def ping(payload, &blk)
|
113
|
+
send({type: :ping, stream: 0, payload: payload})
|
114
|
+
once(:pong, &blk) if blk
|
115
|
+
end
|
116
|
+
|
117
|
+
# Sends a GOAWAY frame indicating that the peer should stop creating
|
118
|
+
# new streams for current connection.
|
119
|
+
#
|
120
|
+
# Endpoints MAY append opaque data to the payload of any GOAWAY frame.
|
121
|
+
# Additional debug data is intended for diagnostic purposes only and
|
122
|
+
# carries no semantic value. Debug data MUST NOT be persistently stored,
|
123
|
+
# since it could contain sensitive information.
|
124
|
+
#
|
125
|
+
# @param error [Symbol]
|
126
|
+
# @param payload [String]
|
127
|
+
def goaway(error = :no_error, payload = nil)
|
128
|
+
send({
|
129
|
+
type: :goaway, last_stream: (@streams.max.first rescue 0),
|
130
|
+
error: error, payload: payload
|
131
|
+
})
|
132
|
+
@state = :closed
|
133
|
+
end
|
134
|
+
|
135
|
+
# Sends a connection SETTINGS frame to the peer.
|
136
|
+
#
|
137
|
+
# @param payload [Hash]
|
138
|
+
# @option payload [Symbol] :settings_max_concurrent_streams
|
139
|
+
# @option payload [Symbol] :settings_flow_control_options
|
140
|
+
# @option payload [Symbol] :settings_initial_window_size
|
141
|
+
def settings(payload)
|
142
|
+
send({type: :settings, stream: 0, payload: payload})
|
143
|
+
end
|
144
|
+
|
145
|
+
# Decodes incoming bytes into HTTP 2.0 frames and routes them to
|
146
|
+
# appropriate receivers: connection frames are handled directly, and
|
147
|
+
# stream frames are passed to appropriate stream objects.
|
148
|
+
#
|
149
|
+
# @param data [String] Binary encoded string
|
150
|
+
def receive(data)
|
151
|
+
@recv_buffer << data
|
152
|
+
|
153
|
+
while frame = @framer.parse(@recv_buffer) do
|
154
|
+
# Header blocks MUST be transmitted as a contiguous sequence of frames
|
155
|
+
# with no interleaved frames of any other type, or from any other stream.
|
156
|
+
if !@continuation.empty?
|
157
|
+
if frame[:type] != :continuation ||
|
158
|
+
frame[:stream] != @continuation.first[:stream]
|
159
|
+
connection_error
|
160
|
+
end
|
161
|
+
|
162
|
+
@continuation << frame
|
163
|
+
return if !frame[:flags].include? :end_headers
|
164
|
+
|
165
|
+
headers = @continuation.collect do |chunk|
|
166
|
+
decode_headers(chunk)
|
167
|
+
chunk[:payload]
|
168
|
+
end.flatten(1)
|
169
|
+
|
170
|
+
frame = @continuation.shift
|
171
|
+
@continuation.clear
|
172
|
+
|
173
|
+
frame.delete(:length)
|
174
|
+
frame[:payload] = headers
|
175
|
+
frame[:flags] << if frame[:type] == :push_promise
|
176
|
+
:end_push_promise
|
177
|
+
else
|
178
|
+
:end_headers
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# SETTINGS frames always apply to a connection, never a single stream.
|
183
|
+
# The stream identifier for a settings frame MUST be zero. If an
|
184
|
+
# endpoint receives a SETTINGS frame whose stream identifier field is
|
185
|
+
# anything other than 0x0, the endpoint MUST respond with a connection
|
186
|
+
# error (Section 5.4.1) of type PROTOCOL_ERROR.
|
187
|
+
if connection_frame?(frame)
|
188
|
+
connection_management(frame)
|
189
|
+
else
|
190
|
+
case frame[:type]
|
191
|
+
when :headers
|
192
|
+
# The last frame in a sequence of HEADERS/CONTINUATION
|
193
|
+
# frames MUST have the END_HEADERS flag set.
|
194
|
+
if !frame[:flags].include? :end_headers
|
195
|
+
@continuation << frame
|
196
|
+
return
|
197
|
+
end
|
198
|
+
|
199
|
+
# After sending a GOAWAY frame, the sender can discard frames
|
200
|
+
# for new streams. However, any frames that alter connection
|
201
|
+
# state cannot be completely ignored. For instance, HEADERS,
|
202
|
+
# PUSH_PROMISE and CONTINUATION frames MUST be minimally
|
203
|
+
# processed to ensure a consistent compression state
|
204
|
+
decode_headers(frame)
|
205
|
+
return if @state == :closed
|
206
|
+
|
207
|
+
stream = @streams[frame[:stream]]
|
208
|
+
if stream.nil?
|
209
|
+
stream = activate_stream(frame[:stream],
|
210
|
+
frame[:priority] || DEFAULT_PRIORITY,
|
211
|
+
@window_limit)
|
212
|
+
emit(:stream, stream)
|
213
|
+
end
|
214
|
+
|
215
|
+
stream << frame
|
216
|
+
|
217
|
+
when :push_promise
|
218
|
+
# The last frame in a sequence of PUSH_PROMISE/CONTINUATION
|
219
|
+
# frames MUST have the END_PUSH_PROMISE/END_HEADERS flag set
|
220
|
+
if !frame[:flags].include? :end_push_promise
|
221
|
+
@continuation << frame
|
222
|
+
return
|
223
|
+
end
|
224
|
+
|
225
|
+
decode_headers(frame)
|
226
|
+
return if @state == :closed
|
227
|
+
|
228
|
+
# PUSH_PROMISE frames MUST be associated with an existing, peer-
|
229
|
+
# initiated stream... A receiver MUST treat the receipt of a
|
230
|
+
# PUSH_PROMISE on a stream that is neither "open" nor
|
231
|
+
# "half-closed (local)" as a connection error (Section 5.4.1) of
|
232
|
+
# type PROTOCOL_ERROR. Similarly, a receiver MUST treat the
|
233
|
+
# receipt of a PUSH_PROMISE that promises an illegal stream
|
234
|
+
# identifier (Section 5.1.1) (that is, an identifier for a stream
|
235
|
+
# that is not currently in the "idle" state) as a connection error
|
236
|
+
# (Section 5.4.1) of type PROTOCOL_ERROR, unless the receiver
|
237
|
+
# recently sent a RST_STREAM frame to cancel the associated stream.
|
238
|
+
parent = @streams[frame[:stream]]
|
239
|
+
pid = frame[:promise_stream]
|
240
|
+
|
241
|
+
connection_error(msg: 'missing parent ID') if parent.nil?
|
242
|
+
|
243
|
+
if !(parent.state == :open || parent.state == :half_closed_local)
|
244
|
+
# An endpoint might receive a PUSH_PROMISE frame after it sends
|
245
|
+
# RST_STREAM. PUSH_PROMISE causes a stream to become "reserved".
|
246
|
+
# The RST_STREAM does not cancel any promised stream. Therefore, if
|
247
|
+
# promised streams are not desired, a RST_STREAM can be used to
|
248
|
+
# close any of those streams.
|
249
|
+
if parent.closed == :local_rst
|
250
|
+
# We can either (a) 'resurrect' the parent, or (b) RST_STREAM
|
251
|
+
# ... sticking with (b), might need to revisit later.
|
252
|
+
send({type: :rst_stream, stream: pid, error: :refused_stream})
|
253
|
+
else
|
254
|
+
connection_error
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
stream = activate_stream(pid, DEFAULT_PRIORITY, @window_limit, parent)
|
259
|
+
emit(:promise, stream)
|
260
|
+
stream << frame
|
261
|
+
else
|
262
|
+
if stream = @streams[frame[:stream]]
|
263
|
+
stream << frame
|
264
|
+
else
|
265
|
+
# An endpoint that receives an unexpected stream identifier
|
266
|
+
# MUST respond with a connection error of type PROTOCOL_ERROR.
|
267
|
+
connection_error
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
alias :<< :receive
|
274
|
+
|
275
|
+
private
|
276
|
+
|
277
|
+
# Send an outgoing frame. DATA frames are subject to connection flow
|
278
|
+
# control and may be split and / or buffered based on current window size.
|
279
|
+
# All other frames are sent immediately.
|
280
|
+
#
|
281
|
+
# @note all frames are currently delivered in FIFO order.
|
282
|
+
# @param frame [Hash]
|
283
|
+
def send(frame)
|
284
|
+
if frame[:type] == :data
|
285
|
+
send_data(frame, true)
|
286
|
+
|
287
|
+
else
|
288
|
+
# An endpoint can end a connection at any time. In particular, an
|
289
|
+
# endpoint MAY choose to treat a stream error as a connection error.
|
290
|
+
if frame[:type] == :rst_stream
|
291
|
+
if frame[:error] == :protocol_error
|
292
|
+
goaway(frame[:error])
|
293
|
+
end
|
294
|
+
else
|
295
|
+
emit(:frame, encode(frame))
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
# Applies HTTP 2.0 binary encoding to the frame.
|
301
|
+
#
|
302
|
+
# @param frame [Hash]
|
303
|
+
# @return [String] encoded frame
|
304
|
+
def encode(frame)
|
305
|
+
if frame[:type] == :headers ||
|
306
|
+
frame[:type] == :push_promise
|
307
|
+
encode_headers(frame)
|
308
|
+
end
|
309
|
+
|
310
|
+
@framer.generate(frame)
|
311
|
+
end
|
312
|
+
|
313
|
+
# Check if frame is a connection frame: SETTINGS, PING, GOAWAY, and any
|
314
|
+
# frame addressed to stream ID = 0.
|
315
|
+
#
|
316
|
+
# @param frame [Hash]
|
317
|
+
# @return [Boolean]
|
318
|
+
def connection_frame?(frame)
|
319
|
+
frame[:stream] == 0 ||
|
320
|
+
frame[:type] == :settings ||
|
321
|
+
frame[:type] == :ping ||
|
322
|
+
frame[:type] == :goaway
|
323
|
+
end
|
324
|
+
|
325
|
+
# Process received connection frame (stream ID = 0).
|
326
|
+
# - Handle SETTINGS updates
|
327
|
+
# - Connection flow control (WINDOW_UPDATE)
|
328
|
+
# - Emit PONG auto-reply to PING frames
|
329
|
+
# - Mark connection as closed on GOAWAY
|
330
|
+
#
|
331
|
+
# @param frame [Hash]
|
332
|
+
def connection_management(frame)
|
333
|
+
case @state
|
334
|
+
when :new
|
335
|
+
# SETTINGS frames MUST be sent at the start of a connection.
|
336
|
+
connection_settings(frame)
|
337
|
+
@state = :connected
|
338
|
+
|
339
|
+
when :connected
|
340
|
+
case frame[:type]
|
341
|
+
when :settings
|
342
|
+
connection_settings(frame)
|
343
|
+
when :window_update
|
344
|
+
flow_control_allowed?
|
345
|
+
@window += frame[:increment]
|
346
|
+
send_data(nil, true)
|
347
|
+
when :ping
|
348
|
+
if frame[:flags].include? :pong
|
349
|
+
emit(:pong, frame[:payload])
|
350
|
+
else
|
351
|
+
send({
|
352
|
+
type: :ping, stream: 0,
|
353
|
+
flags: [:pong], payload: frame[:payload]
|
354
|
+
})
|
355
|
+
end
|
356
|
+
when :goaway
|
357
|
+
# Receivers of a GOAWAY frame MUST NOT open additional streams on
|
358
|
+
# the connection, although a new connection can be established
|
359
|
+
# for new streams.
|
360
|
+
@state = :closed
|
361
|
+
emit(:goaway, frame[:last_stream], frame[:error], frame[:payload])
|
362
|
+
|
363
|
+
else
|
364
|
+
connection_error
|
365
|
+
end
|
366
|
+
else
|
367
|
+
connection_error
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
# Update local connection settings based on parameters set by the peer.
|
372
|
+
#
|
373
|
+
# @param frame [Hash]
|
374
|
+
def connection_settings(frame)
|
375
|
+
if (frame[:type] != :settings || frame[:stream] != 0)
|
376
|
+
connection_error
|
377
|
+
end
|
378
|
+
|
379
|
+
frame[:payload].each do |key,v|
|
380
|
+
case key
|
381
|
+
when :settings_max_concurrent_streams
|
382
|
+
@stream_limit = v
|
383
|
+
|
384
|
+
# A change to SETTINGS_INITIAL_WINDOW_SIZE could cause the available
|
385
|
+
# space in a flow control window to become negative. A sender MUST
|
386
|
+
# track the negative flow control window, and MUST NOT send new flow
|
387
|
+
# controlled frames until it receives WINDOW_UPDATE frames that cause
|
388
|
+
# the flow control window to become positive.
|
389
|
+
when :settings_initial_window_size
|
390
|
+
flow_control_allowed?
|
391
|
+
@window = @window - @window_limit + v
|
392
|
+
@streams.each do |id, stream|
|
393
|
+
stream.emit(:window, stream.window - @window_limit + v)
|
394
|
+
end
|
395
|
+
|
396
|
+
@window_limit = v
|
397
|
+
|
398
|
+
# Flow control can be disabled the entire connection using the
|
399
|
+
# SETTINGS_FLOW_CONTROL_OPTIONS setting. This setting ends all forms
|
400
|
+
# of flow control. An implementation that does not wish to perform
|
401
|
+
# flow control can use this in the initial SETTINGS exchange.
|
402
|
+
when :settings_flow_control_options
|
403
|
+
flow_control_allowed?
|
404
|
+
|
405
|
+
if v == 1
|
406
|
+
@window = @window_limit = Float::INFINITY
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
# Decode headers payload and update connection decompressor state.
|
413
|
+
#
|
414
|
+
# The receiver endpoint reassembles the header block by concatenating
|
415
|
+
# the individual fragments, then decompresses the block to reconstruct
|
416
|
+
# the header set - aka, header payloads are buffered until END_HEADERS,
|
417
|
+
# or an END_PROMISE flag is seen.
|
418
|
+
#
|
419
|
+
# @param frame [Hash]
|
420
|
+
def decode_headers(frame)
|
421
|
+
if frame[:payload].is_a? String
|
422
|
+
frame[:payload] = @decompressor.decode(StringIO.new(frame[:payload]))
|
423
|
+
end
|
424
|
+
|
425
|
+
rescue Exception => e
|
426
|
+
connection_error(:compression_error, msg: e.message)
|
427
|
+
end
|
428
|
+
|
429
|
+
# Encode headers payload and update connection compressor state.
|
430
|
+
#
|
431
|
+
# @param frame [Hash]
|
432
|
+
def encode_headers(frame)
|
433
|
+
if !frame[:payload].is_a? String
|
434
|
+
frame[:payload] = @compressor.encode(frame[:payload])
|
435
|
+
end
|
436
|
+
|
437
|
+
rescue Exception => e
|
438
|
+
connection_error(:compression_error, msg: e.message)
|
439
|
+
end
|
440
|
+
|
441
|
+
# Once disabled, no further flow control operations are permitted.
|
442
|
+
#
|
443
|
+
def flow_control_allowed?
|
444
|
+
if @window_limit == Float::INFINITY
|
445
|
+
connection_error(:flow_control_error)
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
# Activates new incoming or outgoing stream and registers appropriate
|
450
|
+
# connection managemet callbacks.
|
451
|
+
#
|
452
|
+
# @param id [Integer]
|
453
|
+
# @param priority [Integer]
|
454
|
+
# @param window [Integer]
|
455
|
+
# @param parent [Stream]
|
456
|
+
def activate_stream(id, priority, window, parent = nil)
|
457
|
+
if @streams.key?(id)
|
458
|
+
connection_error(msg: 'Stream ID already exists')
|
459
|
+
end
|
460
|
+
|
461
|
+
stream = Stream.new(id, priority, window, parent)
|
462
|
+
|
463
|
+
# Streams that are in the "open" state, or either of the "half closed"
|
464
|
+
# states count toward the maximum number of streams that an endpoint is
|
465
|
+
# permitted to open.
|
466
|
+
stream.once(:active) { @active_stream_count += 1 }
|
467
|
+
stream.once(:close) { @active_stream_count -= 1 }
|
468
|
+
stream.on(:promise, &method(:promise))
|
469
|
+
stream.on(:frame, &method(:send))
|
470
|
+
|
471
|
+
@streams[id] = stream
|
472
|
+
end
|
473
|
+
|
474
|
+
# Handle locally initiated server-push event emitted by the stream.
|
475
|
+
#
|
476
|
+
# @param args [Array]
|
477
|
+
# @param callback [Proc]
|
478
|
+
def promise(*args, &callback)
|
479
|
+
if @type == :client
|
480
|
+
raise ProtocolError.new("client cannot initiate promise")
|
481
|
+
end
|
482
|
+
|
483
|
+
parent, headers, flags = *args
|
484
|
+
promise = new_stream(parent: parent)
|
485
|
+
promise.send({
|
486
|
+
type: :push_promise,
|
487
|
+
flags: flags,
|
488
|
+
stream: parent.id,
|
489
|
+
promise_stream: promise.id,
|
490
|
+
payload: headers.to_a
|
491
|
+
})
|
492
|
+
|
493
|
+
callback.call(promise)
|
494
|
+
end
|
495
|
+
|
496
|
+
# Emit GOAWAY error indicating to peer that the connection is being
|
497
|
+
# aborted, and once sent, raise a local exception.
|
498
|
+
#
|
499
|
+
# @param error [Symbol]
|
500
|
+
# @option error [Symbol] :no_error
|
501
|
+
# @option error [Symbol] :internal_error
|
502
|
+
# @option error [Symbol] :flow_control_error
|
503
|
+
# @option error [Symbol] :stream_closed
|
504
|
+
# @option error [Symbol] :frame_too_large
|
505
|
+
# @option error [Symbol] :compression_error
|
506
|
+
# @param msg [String]
|
507
|
+
def connection_error(error = :protocol_error, msg: nil)
|
508
|
+
goaway(error) if @state != :closed && @state != :new
|
509
|
+
|
510
|
+
@state, @error = :closed, error
|
511
|
+
klass = error.to_s.split('_').map(&:capitalize).join
|
512
|
+
raise Kernel.const_get(klass).new(msg)
|
513
|
+
end
|
514
|
+
|
515
|
+
end
|
516
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module HTTP2
|
2
|
+
|
3
|
+
# Basic event emitter implementation with support for persistent and
|
4
|
+
# one-time event callbacks.
|
5
|
+
#
|
6
|
+
module Emitter
|
7
|
+
|
8
|
+
# Subscribe to all future events for specified type.
|
9
|
+
#
|
10
|
+
# @param event [Symbol]
|
11
|
+
# @param block [Proc] callback function
|
12
|
+
def add_listener(event, &block)
|
13
|
+
raise Exception.new("must provide callback") if !block_given?
|
14
|
+
listeners(event.to_sym).push block
|
15
|
+
end
|
16
|
+
alias :on :add_listener
|
17
|
+
|
18
|
+
# Subscribe to next event (at most once) for specified type.
|
19
|
+
#
|
20
|
+
# @param event [Symbol]
|
21
|
+
# @param block [Proc] callback function
|
22
|
+
def once(event, &block)
|
23
|
+
add_listener(event) do |*args|
|
24
|
+
block.call(*args)
|
25
|
+
:delete
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Emit event with provided arguments.
|
30
|
+
#
|
31
|
+
# @param event [Symbol]
|
32
|
+
# @param args [Array] arguments to be passed to the callbacks
|
33
|
+
# @param block [Proc] callback function
|
34
|
+
def emit(event, *args, &block)
|
35
|
+
listeners(event).delete_if do |cb|
|
36
|
+
cb.call(*args, &block) == :delete
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def listeners(event)
|
43
|
+
@listeners ||= Hash.new { |hash, key| hash[key] = [] }
|
44
|
+
@listeners[event]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/http/2/error.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
module HTTP2
|
2
|
+
|
3
|
+
# Stream, connection, and compressor exceptions.
|
4
|
+
module Error
|
5
|
+
|
6
|
+
# Raised by stream or connection handlers, results in GOAWAY frame
|
7
|
+
# which signals termination of the current connection. You *cannot*
|
8
|
+
# recover from this exception, or any exceptions subclassed from it.
|
9
|
+
class ProtocolError < Exception; end
|
10
|
+
|
11
|
+
# Raised on any header encoding / decoding exception.
|
12
|
+
#
|
13
|
+
# @see ProtocolError
|
14
|
+
class CompressionError < ProtocolError; end
|
15
|
+
|
16
|
+
# Raised on invalid reference for current compression context: the
|
17
|
+
# client and server contexts are out of sync.
|
18
|
+
#
|
19
|
+
# @see ProtocolError
|
20
|
+
class HeaderException < ProtocolError; end
|
21
|
+
|
22
|
+
# Raised on invalid flow control frame or command.
|
23
|
+
#
|
24
|
+
# @see ProtocolError
|
25
|
+
class FlowControlError < ProtocolError; end
|
26
|
+
|
27
|
+
# Raised on invalid stream processing: invalid frame type received or
|
28
|
+
# sent, or invalid command issued.
|
29
|
+
class StreamError < ProtocolError; end
|
30
|
+
|
31
|
+
#
|
32
|
+
# -- Recoverable errors -------------------------------------------------
|
33
|
+
#
|
34
|
+
|
35
|
+
# Raised if stream has been closed and new frames cannot be sent.
|
36
|
+
class StreamClosed < Exception; end
|
37
|
+
|
38
|
+
# Raised if connection has been closed (or draining) and new stream
|
39
|
+
# cannot be opened.
|
40
|
+
class ConnectionClosed < Exception; end
|
41
|
+
|
42
|
+
# Raised if stream limit has been reached and new stream cannot be opened.
|
43
|
+
class StreamLimitExceeded < Exception; end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module HTTP2
|
2
|
+
|
3
|
+
# Maximum size of a DATA payload (16383 bytes, ~16K).
|
4
|
+
MAX_FRAME_SIZE = 2**14-1
|
5
|
+
|
6
|
+
# Implementation of stream and connection DATA flow control: frames may
|
7
|
+
# be split and / or may be buffered based on current flow control window.
|
8
|
+
#
|
9
|
+
module FlowBuffer
|
10
|
+
|
11
|
+
# Amount of buffered data. Only DATA payloads are subject to flow stream
|
12
|
+
# and connection flow control.
|
13
|
+
#
|
14
|
+
# @return [Integer]
|
15
|
+
def buffered_amount
|
16
|
+
@send_buffer.map {|f| f[:length] }.reduce(:+) || 0
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# Buffers outgoing DATA frames and applies flow control logic to split
|
22
|
+
# and emit DATA frames based on current flow control window. If the
|
23
|
+
# window is large enough, the data is sent immediately. Otherwise, the
|
24
|
+
# data is buffered until the flow control window is updated.
|
25
|
+
#
|
26
|
+
# Buffered DATA frames are emitted in FIFO order.
|
27
|
+
#
|
28
|
+
# @param frame [Hash]
|
29
|
+
# @param encode [Boolean] set to true by co
|
30
|
+
def send_data(frame = nil, encode = false)
|
31
|
+
@send_buffer.push frame if !frame.nil?
|
32
|
+
|
33
|
+
while @window > 0 && !@send_buffer.empty? do
|
34
|
+
frame = @send_buffer.shift
|
35
|
+
|
36
|
+
sent, frame_size = 0, frame[:payload].bytesize
|
37
|
+
|
38
|
+
if frame_size > @window
|
39
|
+
payload = frame.delete(:payload)
|
40
|
+
chunk = frame.dup
|
41
|
+
|
42
|
+
frame[:payload] = payload.slice!(0, @window)
|
43
|
+
chunk[:length] = payload.bytesize
|
44
|
+
chunk[:payload] = payload
|
45
|
+
|
46
|
+
# if no longer last frame in sequence...
|
47
|
+
if frame[:flags].include? :end_stream
|
48
|
+
frame[:flags] -= [:end_stream]
|
49
|
+
end
|
50
|
+
|
51
|
+
@send_buffer.unshift chunk
|
52
|
+
sent = @window
|
53
|
+
else
|
54
|
+
sent = frame_size
|
55
|
+
end
|
56
|
+
|
57
|
+
frame = encode(frame) if encode
|
58
|
+
emit(:frame, frame)
|
59
|
+
@window -= sent
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|