http-2 0.6.1 → 0.6.3
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 +4 -4
- data/.autotest +2 -1
- data/Gemfile +8 -1
- data/README.md +27 -21
- data/example/README.md +53 -0
- data/example/client.rb +88 -30
- data/example/helper.rb +5 -0
- data/example/keys/mycert.pem +24 -0
- data/example/keys/mykey.pem +27 -0
- data/example/server.rb +47 -8
- data/http-2.gemspec +0 -2
- data/lib/http/2.rb +2 -0
- data/lib/http/2/buffer.rb +21 -4
- data/lib/http/2/client.rb +50 -0
- data/lib/http/2/compressor.rb +197 -181
- data/lib/http/2/connection.rb +57 -83
- data/lib/http/2/emitter.rb +2 -2
- data/lib/http/2/error.rb +5 -0
- data/lib/http/2/framer.rb +32 -31
- data/lib/http/2/server.rb +55 -0
- data/lib/http/2/stream.rb +1 -1
- data/lib/http/2/version.rb +1 -1
- data/spec/buffer_spec.rb +23 -0
- data/spec/client_spec.rb +93 -0
- data/spec/compressor_spec.rb +89 -80
- data/spec/connection_spec.rb +24 -75
- data/spec/emitter_spec.rb +8 -0
- data/spec/framer_spec.rb +36 -40
- data/spec/helper.rb +6 -2
- data/spec/server_spec.rb +50 -0
- data/spec/stream_spec.rb +23 -30
- metadata +13 -30
data/lib/http/2/connection.rb
CHANGED
@@ -11,36 +11,15 @@ module HTTP2
|
|
11
11
|
|
12
12
|
# Connection encapsulates all of the connection, stream, flow-control,
|
13
13
|
# error management, and other processing logic required for a well-behaved
|
14
|
-
# HTTP 2.0
|
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
|
14
|
+
# HTTP 2.0 endpoint.
|
35
15
|
#
|
16
|
+
# Note that this class should not be used directly. Instead, you want to
|
17
|
+
# use either Client or Server class to drive the HTTP 2.0 exchange.
|
36
18
|
class Connection
|
37
19
|
include FlowBuffer
|
38
20
|
include Emitter
|
39
21
|
include Error
|
40
22
|
|
41
|
-
# Type of connection (:server, :client).
|
42
|
-
attr_reader :type
|
43
|
-
|
44
23
|
# Connection state (:new, :closed).
|
45
24
|
attr_reader :state
|
46
25
|
|
@@ -59,34 +38,20 @@ module HTTP2
|
|
59
38
|
# are not counted towards the stream limit).
|
60
39
|
attr_reader :active_stream_count
|
61
40
|
|
62
|
-
# Initializes new
|
41
|
+
# Initializes new connection object.
|
63
42
|
#
|
64
|
-
|
65
|
-
|
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
|
43
|
+
def initialize(streams: 100, window: DEFAULT_FLOW_WINDOW)
|
44
|
+
@stream_limit = streams
|
79
45
|
@active_stream_count = 0
|
80
46
|
@streams = {}
|
81
47
|
|
82
48
|
@framer = Framer.new
|
83
|
-
@window =
|
84
|
-
@window_limit =
|
49
|
+
@window = window
|
50
|
+
@window_limit = window
|
85
51
|
|
86
52
|
@recv_buffer = Buffer.new
|
87
53
|
@send_buffer = []
|
88
54
|
@continuation = []
|
89
|
-
@state = :new
|
90
55
|
@error = nil
|
91
56
|
end
|
92
57
|
|
@@ -95,11 +60,11 @@ module HTTP2
|
|
95
60
|
# @param priority [Integer]
|
96
61
|
# @param window [Integer]
|
97
62
|
# @param parent [Stream]
|
98
|
-
def new_stream(priority: DEFAULT_PRIORITY,
|
63
|
+
def new_stream(priority: DEFAULT_PRIORITY, parent: nil)
|
99
64
|
raise ConnectionClosed.new if @state == :closed
|
100
65
|
raise StreamLimitExceeded.new if @active_stream_count == @stream_limit
|
101
66
|
|
102
|
-
stream = activate_stream(@stream_id, priority,
|
67
|
+
stream = activate_stream(@stream_id, priority, parent)
|
103
68
|
@stream_id += 2
|
104
69
|
|
105
70
|
stream
|
@@ -132,13 +97,19 @@ module HTTP2
|
|
132
97
|
@state = :closed
|
133
98
|
end
|
134
99
|
|
135
|
-
# Sends a connection SETTINGS frame to the peer.
|
100
|
+
# Sends a connection SETTINGS frame to the peer. Setting window size
|
101
|
+
# to Float::INFINITY disables flow control.
|
136
102
|
#
|
137
|
-
# @param
|
138
|
-
# @
|
139
|
-
|
140
|
-
|
141
|
-
|
103
|
+
# @param stream_limit [Integer] maximum number of concurrent streams
|
104
|
+
# @param window_limit [Float] maximum flow window size
|
105
|
+
def settings(stream_limit: @stream_limit, window_limit: @window_limit)
|
106
|
+
payload = { settings_max_concurrent_streams: stream_limit }
|
107
|
+
if window_limit.to_f.infinite?
|
108
|
+
payload[:settings_flow_control_options] = 1
|
109
|
+
else
|
110
|
+
payload[:settings_initial_window_size] = window_limit
|
111
|
+
end
|
112
|
+
|
142
113
|
send({type: :settings, stream: 0, payload: payload})
|
143
114
|
end
|
144
115
|
|
@@ -150,6 +121,29 @@ module HTTP2
|
|
150
121
|
def receive(data)
|
151
122
|
@recv_buffer << data
|
152
123
|
|
124
|
+
# Upon establishment of a TCP connection and determination that
|
125
|
+
# HTTP/2.0 will be used by both peers, each endpoint MUST send a
|
126
|
+
# connection header as a final confirmation and to establish the
|
127
|
+
# initial settings for the HTTP/2.0 connection.
|
128
|
+
#
|
129
|
+
# Client connection header is 24 byte connection header followed by
|
130
|
+
# SETTINGS frame. Server connection header is SETTINGS frame only.
|
131
|
+
if @state == :new
|
132
|
+
if @recv_buffer.size < 24
|
133
|
+
if !CONNECTION_HEADER.start_with? @recv_buffer
|
134
|
+
raise HandshakeError.new
|
135
|
+
else
|
136
|
+
return
|
137
|
+
end
|
138
|
+
|
139
|
+
elsif @recv_buffer.read(24) != CONNECTION_HEADER
|
140
|
+
raise HandshakeError.new
|
141
|
+
else
|
142
|
+
@state = :connection_header
|
143
|
+
settings(stream_limit: @stream_limit, window_limit: @window_limit)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
153
147
|
while frame = @framer.parse(@recv_buffer) do
|
154
148
|
# Header blocks MUST be transmitted as a contiguous sequence of frames
|
155
149
|
# with no interleaved frames of any other type, or from any other stream.
|
@@ -207,8 +201,7 @@ module HTTP2
|
|
207
201
|
stream = @streams[frame[:stream]]
|
208
202
|
if stream.nil?
|
209
203
|
stream = activate_stream(frame[:stream],
|
210
|
-
frame[:priority] || DEFAULT_PRIORITY
|
211
|
-
@window_limit)
|
204
|
+
frame[:priority] || DEFAULT_PRIORITY)
|
212
205
|
emit(:stream, stream)
|
213
206
|
end
|
214
207
|
|
@@ -255,7 +248,7 @@ module HTTP2
|
|
255
248
|
end
|
256
249
|
end
|
257
250
|
|
258
|
-
stream = activate_stream(pid, DEFAULT_PRIORITY,
|
251
|
+
stream = activate_stream(pid, DEFAULT_PRIORITY, parent)
|
259
252
|
emit(:promise, stream)
|
260
253
|
stream << frame
|
261
254
|
else
|
@@ -269,6 +262,9 @@ module HTTP2
|
|
269
262
|
end
|
270
263
|
end
|
271
264
|
end
|
265
|
+
|
266
|
+
rescue
|
267
|
+
connection_error
|
272
268
|
end
|
273
269
|
alias :<< :receive
|
274
270
|
|
@@ -300,7 +296,7 @@ module HTTP2
|
|
300
296
|
# Applies HTTP 2.0 binary encoding to the frame.
|
301
297
|
#
|
302
298
|
# @param frame [Hash]
|
303
|
-
# @return [
|
299
|
+
# @return [Buffer] encoded frame
|
304
300
|
def encode(frame)
|
305
301
|
if frame[:type] == :headers ||
|
306
302
|
frame[:type] == :push_promise
|
@@ -331,7 +327,7 @@ module HTTP2
|
|
331
327
|
# @param frame [Hash]
|
332
328
|
def connection_management(frame)
|
333
329
|
case @state
|
334
|
-
when :
|
330
|
+
when :connection_header
|
335
331
|
# SETTINGS frames MUST be sent at the start of a connection.
|
336
332
|
connection_settings(frame)
|
337
333
|
@state = :connected
|
@@ -419,7 +415,7 @@ module HTTP2
|
|
419
415
|
# @param frame [Hash]
|
420
416
|
def decode_headers(frame)
|
421
417
|
if frame[:payload].is_a? String
|
422
|
-
frame[:payload] = @decompressor.decode(
|
418
|
+
frame[:payload] = @decompressor.decode(frame[:payload])
|
423
419
|
end
|
424
420
|
|
425
421
|
rescue Exception => e
|
@@ -453,46 +449,24 @@ module HTTP2
|
|
453
449
|
# @param priority [Integer]
|
454
450
|
# @param window [Integer]
|
455
451
|
# @param parent [Stream]
|
456
|
-
def activate_stream(id, priority,
|
452
|
+
def activate_stream(id, priority, parent = nil)
|
457
453
|
if @streams.key?(id)
|
458
454
|
connection_error(msg: 'Stream ID already exists')
|
459
455
|
end
|
460
456
|
|
461
|
-
stream = Stream.new(id, priority,
|
457
|
+
stream = Stream.new(id, priority, @window_limit, parent)
|
462
458
|
|
463
459
|
# Streams that are in the "open" state, or either of the "half closed"
|
464
460
|
# states count toward the maximum number of streams that an endpoint is
|
465
461
|
# permitted to open.
|
466
462
|
stream.once(:active) { @active_stream_count += 1 }
|
467
463
|
stream.once(:close) { @active_stream_count -= 1 }
|
468
|
-
stream.on(:promise, &method(:promise))
|
464
|
+
stream.on(:promise, &method(:promise)) if self.is_a? Server
|
469
465
|
stream.on(:frame, &method(:send))
|
470
466
|
|
471
467
|
@streams[id] = stream
|
472
468
|
end
|
473
469
|
|
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
470
|
# Emit GOAWAY error indicating to peer that the connection is being
|
497
471
|
# aborted, and once sent, raise a local exception.
|
498
472
|
#
|
@@ -509,7 +483,7 @@ module HTTP2
|
|
509
483
|
|
510
484
|
@state, @error = :closed, error
|
511
485
|
klass = error.to_s.split('_').map(&:capitalize).join
|
512
|
-
raise
|
486
|
+
raise Error.const_get(klass).new(msg)
|
513
487
|
end
|
514
488
|
|
515
489
|
end
|
data/lib/http/2/emitter.rb
CHANGED
@@ -20,8 +20,8 @@ module HTTP2
|
|
20
20
|
# @param event [Symbol]
|
21
21
|
# @param block [Proc] callback function
|
22
22
|
def once(event, &block)
|
23
|
-
add_listener(event) do |*args|
|
24
|
-
block.call(*args)
|
23
|
+
add_listener(event) do |*args, &callback|
|
24
|
+
block.call(*args, &callback)
|
25
25
|
:delete
|
26
26
|
end
|
27
27
|
end
|
data/lib/http/2/error.rb
CHANGED
@@ -3,6 +3,11 @@ module HTTP2
|
|
3
3
|
# Stream, connection, and compressor exceptions.
|
4
4
|
module Error
|
5
5
|
|
6
|
+
# Raised if connection header is missing or invalid indicating that
|
7
|
+
# this is an invalid HTTP 2.0 request - no frames are emitted and the
|
8
|
+
# connection must be aborted.
|
9
|
+
class HandshakeError < Exception; end
|
10
|
+
|
6
11
|
# Raised by stream or connection handlers, results in GOAWAY frame
|
7
12
|
# which signals termination of the current connection. You *cannot*
|
8
13
|
# recover from this exception, or any exceptions subclassed from it.
|
data/lib/http/2/framer.rb
CHANGED
@@ -71,8 +71,8 @@ module HTTP2
|
|
71
71
|
|
72
72
|
RBIT = 0x7fffffff
|
73
73
|
RBYTE = 0x0fffffff
|
74
|
-
HEADERPACK = "
|
75
|
-
UINT32 = "
|
74
|
+
HEADERPACK = "nCCN"
|
75
|
+
UINT32 = "N"
|
76
76
|
|
77
77
|
private_constant :RBIT, :RBYTE, :HEADERPACK, :UINT32
|
78
78
|
|
@@ -118,7 +118,7 @@ module HTTP2
|
|
118
118
|
|
119
119
|
# Decodes common 8-byte header.
|
120
120
|
#
|
121
|
-
# @param buf [
|
121
|
+
# @param buf [Buffer]
|
122
122
|
def readCommonHeader(buf)
|
123
123
|
frame = {}
|
124
124
|
frame[:length], type, flags, stream = buf.slice(0,8).unpack(HEADERPACK)
|
@@ -138,7 +138,7 @@ module HTTP2
|
|
138
138
|
#
|
139
139
|
# @param frame [Hash]
|
140
140
|
def generate(frame)
|
141
|
-
bytes =
|
141
|
+
bytes = Buffer.new
|
142
142
|
length = 0
|
143
143
|
|
144
144
|
frame[:flags] ||= []
|
@@ -146,7 +146,7 @@ module HTTP2
|
|
146
146
|
|
147
147
|
case frame[:type]
|
148
148
|
when :data
|
149
|
-
bytes
|
149
|
+
bytes << frame[:payload]
|
150
150
|
length += frame[:payload].bytesize
|
151
151
|
|
152
152
|
when :headers
|
@@ -155,19 +155,19 @@ module HTTP2
|
|
155
155
|
end
|
156
156
|
|
157
157
|
if frame[:flags].include? :priority
|
158
|
-
bytes
|
158
|
+
bytes << [frame[:priority] & RBIT].pack(UINT32)
|
159
159
|
length += 4
|
160
160
|
end
|
161
161
|
|
162
|
-
bytes
|
162
|
+
bytes << frame[:payload]
|
163
163
|
length += frame[:payload].bytesize
|
164
164
|
|
165
165
|
when :priority
|
166
|
-
bytes
|
166
|
+
bytes << [frame[:priority] & RBIT].pack(UINT32)
|
167
167
|
length += 4
|
168
168
|
|
169
169
|
when :rst_stream
|
170
|
-
bytes
|
170
|
+
bytes << pack_error(frame[:error])
|
171
171
|
length += 4
|
172
172
|
|
173
173
|
when :settings
|
@@ -184,14 +184,14 @@ module HTTP2
|
|
184
184
|
end
|
185
185
|
end
|
186
186
|
|
187
|
-
bytes
|
188
|
-
bytes
|
187
|
+
bytes << [k & RBYTE].pack(UINT32)
|
188
|
+
bytes << [v].pack(UINT32)
|
189
189
|
length += 8
|
190
190
|
end
|
191
191
|
|
192
192
|
when :push_promise
|
193
|
-
bytes
|
194
|
-
bytes
|
193
|
+
bytes << [frame[:promise_stream] & RBIT].pack(UINT32)
|
194
|
+
bytes << frame[:payload]
|
195
195
|
length += 4 + frame[:payload].bytesize
|
196
196
|
|
197
197
|
when :ping
|
@@ -199,36 +199,36 @@ module HTTP2
|
|
199
199
|
raise CompressionError.new("Invalid payload size \
|
200
200
|
(#{frame[:payload].size} != 8 bytes)")
|
201
201
|
end
|
202
|
-
bytes
|
202
|
+
bytes << frame[:payload]
|
203
203
|
length += 8
|
204
204
|
|
205
205
|
when :goaway
|
206
|
-
bytes
|
207
|
-
bytes
|
206
|
+
bytes << [frame[:last_stream] & RBIT].pack(UINT32)
|
207
|
+
bytes << pack_error(frame[:error])
|
208
208
|
length += 8
|
209
209
|
|
210
210
|
if frame[:payload]
|
211
|
-
bytes
|
211
|
+
bytes << frame[:payload]
|
212
212
|
length += frame[:payload].bytesize
|
213
213
|
end
|
214
214
|
|
215
215
|
when :window_update
|
216
|
-
bytes
|
216
|
+
bytes << [frame[:increment] & RBIT].pack(UINT32)
|
217
217
|
length += 4
|
218
218
|
|
219
219
|
when :continuation
|
220
|
-
bytes
|
220
|
+
bytes << frame[:payload]
|
221
221
|
length += frame[:payload].bytesize
|
222
222
|
end
|
223
223
|
|
224
224
|
frame[:length] = length
|
225
|
-
commonHeader(frame)
|
225
|
+
bytes.prepend(commonHeader(frame))
|
226
226
|
end
|
227
227
|
|
228
228
|
# Decodes complete HTTP 2.0 frame from provided buffer. If the buffer
|
229
229
|
# does not contain enough data, no further work is performed.
|
230
230
|
#
|
231
|
-
# @param buf [
|
231
|
+
# @param buf [Buffer]
|
232
232
|
def parse(buf)
|
233
233
|
return nil if buf.size < 8
|
234
234
|
frame = readCommonHeader(buf)
|
@@ -242,36 +242,37 @@ module HTTP2
|
|
242
242
|
frame[:payload] = payload.read(frame[:length])
|
243
243
|
when :headers
|
244
244
|
if frame[:flags].include? :priority
|
245
|
-
frame[:priority] = payload.
|
245
|
+
frame[:priority] = payload.read_uint32 & RBIT
|
246
246
|
end
|
247
247
|
frame[:payload] = payload.read(frame[:length])
|
248
248
|
when :priority
|
249
|
-
frame[:priority] = payload.
|
249
|
+
frame[:priority] = payload.read_uint32 & RBIT
|
250
250
|
when :rst_stream
|
251
|
-
frame[:error] = unpack_error payload.
|
251
|
+
frame[:error] = unpack_error payload.read_uint32
|
252
252
|
|
253
253
|
when :settings
|
254
254
|
frame[:payload] = {}
|
255
255
|
(frame[:length] / 8).times do
|
256
|
-
id = payload.
|
257
|
-
val = payload.
|
256
|
+
id = payload.read_uint32 & RBYTE
|
257
|
+
val = payload.read_uint32
|
258
258
|
|
259
|
+
# Unsupported or unrecognized settings MUST be ignored.
|
259
260
|
name, _ = DEFINED_SETTINGS.select { |name, v| v == id }.first
|
260
|
-
frame[:payload][name
|
261
|
+
frame[:payload][name] = val if name
|
261
262
|
end
|
262
263
|
when :push_promise
|
263
|
-
frame[:promise_stream] = payload.
|
264
|
+
frame[:promise_stream] = payload.read_uint32 & RBIT
|
264
265
|
frame[:payload] = payload.read(frame[:length])
|
265
266
|
when :ping
|
266
267
|
frame[:payload] = payload.read(frame[:length])
|
267
268
|
when :goaway
|
268
|
-
frame[:last_stream] = payload.
|
269
|
-
frame[:error] = unpack_error payload.
|
269
|
+
frame[:last_stream] = payload.read_uint32 & RBIT
|
270
|
+
frame[:error] = unpack_error payload.read_uint32
|
270
271
|
|
271
272
|
size = frame[:length] - 8
|
272
273
|
frame[:payload] = payload.read(size) if size > 0
|
273
274
|
when :window_update
|
274
|
-
frame[:increment] = payload.
|
275
|
+
frame[:increment] = payload.read_uint32 & RBIT
|
275
276
|
when :continuation
|
276
277
|
frame[:payload] = payload.read(frame[:length])
|
277
278
|
end
|