http-2 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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