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.
@@ -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
@@ -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