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,448 @@
|
|
1
|
+
require "helper"
|
2
|
+
|
3
|
+
describe HTTP2::Connection do
|
4
|
+
before(:each) do
|
5
|
+
@conn = Connection.new
|
6
|
+
end
|
7
|
+
|
8
|
+
let(:f) { Framer.new }
|
9
|
+
|
10
|
+
context "initialization and settings" do
|
11
|
+
it "should return odd ids for client requests" do
|
12
|
+
@conn = Connection.new(:client)
|
13
|
+
@conn.new_stream.id.should_not be_even
|
14
|
+
|
15
|
+
@conn = Connection.new(:server)
|
16
|
+
@conn.new_stream.id.should be_even
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should raise error if first frame is not SETTINGS" do
|
20
|
+
(FRAME_TYPES - [SETTINGS]).each do |frame|
|
21
|
+
frame = set_stream_id(f.generate(frame), 0x0)
|
22
|
+
expect { @conn.dup << frame }.to raise_error(ProtocolError)
|
23
|
+
end
|
24
|
+
|
25
|
+
expect { @conn << f.generate(SETTINGS) }.to_not raise_error
|
26
|
+
@conn.state.should eq :connected
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should raise error if SETTINGS stream != 0" do
|
30
|
+
frame = set_stream_id(f.generate(SETTINGS), 0x1)
|
31
|
+
expect { @conn << frame }.to raise_error(ProtocolError)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "stream management" do
|
36
|
+
it "should initialize to default stream limit (infinite)" do
|
37
|
+
@conn.stream_limit.should eq Float::INFINITY
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should change stream limit to received SETTINGS value" do
|
41
|
+
@conn << f.generate(SETTINGS)
|
42
|
+
@conn.stream_limit.should eq 10
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should count open streams against stream limit" do
|
46
|
+
s = @conn.new_stream
|
47
|
+
@conn.active_stream_count.should eq 0
|
48
|
+
s.receive HEADERS
|
49
|
+
@conn.active_stream_count.should eq 1
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should not count reserved streams against stream limit" do
|
53
|
+
s1 = @conn.new_stream
|
54
|
+
s1.receive PUSH_PROMISE
|
55
|
+
@conn.active_stream_count.should eq 0
|
56
|
+
|
57
|
+
s2 = @conn.new_stream
|
58
|
+
s2.send PUSH_PROMISE
|
59
|
+
@conn.active_stream_count.should eq 0
|
60
|
+
|
61
|
+
# transition to half closed
|
62
|
+
s1.receive HEADERS
|
63
|
+
s2.send HEADERS
|
64
|
+
@conn.active_stream_count.should eq 2
|
65
|
+
|
66
|
+
# transition to closed
|
67
|
+
s1.receive DATA
|
68
|
+
s2.send DATA
|
69
|
+
@conn.active_stream_count.should eq 0
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should not exceed stream limit set by peer" do
|
73
|
+
@conn << f.generate(SETTINGS)
|
74
|
+
|
75
|
+
expect {
|
76
|
+
10.times do
|
77
|
+
s = @conn.new_stream
|
78
|
+
s.send HEADERS
|
79
|
+
end
|
80
|
+
}.to_not raise_error
|
81
|
+
|
82
|
+
expect { @conn.new_stream }.to raise_error(StreamLimitExceeded)
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should initialize stream with HEADERS priority value" do
|
86
|
+
@conn << f.generate(SETTINGS)
|
87
|
+
|
88
|
+
stream, headers = nil, HEADERS.dup
|
89
|
+
headers[:priority] = 20
|
90
|
+
|
91
|
+
@conn.on(:stream) {|s| stream = s }
|
92
|
+
@conn << f.generate(headers)
|
93
|
+
|
94
|
+
stream.priority.should eq 20
|
95
|
+
end
|
96
|
+
|
97
|
+
context "push" do
|
98
|
+
it "should raise error on PUSH_PROMISE against stream 0" do
|
99
|
+
expect {
|
100
|
+
@conn << set_stream_id(f.generate(PUSH_PROMISE), 0)
|
101
|
+
}.to raise_error(ProtocolError)
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should raise error on PUSH_PROMISE against bogus stream" do
|
105
|
+
expect {
|
106
|
+
@conn << set_stream_id(f.generate(PUSH_PROMISE), 31415)
|
107
|
+
}.to raise_error(ProtocolError)
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should raise error on PUSH_PROMISE against non-idle stream" do
|
111
|
+
expect {
|
112
|
+
s = @conn.new_stream
|
113
|
+
s.send HEADERS
|
114
|
+
|
115
|
+
@conn << set_stream_id(f.generate(PUSH_PROMISE), s.id)
|
116
|
+
@conn << set_stream_id(f.generate(PUSH_PROMISE), s.id)
|
117
|
+
}.to raise_error(ProtocolError)
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should emit stream object for received PUSH_PROMISE" do
|
121
|
+
s = @conn.new_stream
|
122
|
+
s.send HEADERS
|
123
|
+
|
124
|
+
promise = nil
|
125
|
+
@conn.on(:promise) {|s| promise = s }
|
126
|
+
@conn << set_stream_id(f.generate(PUSH_PROMISE), s.id)
|
127
|
+
|
128
|
+
promise.id.should eq 2
|
129
|
+
promise.state.should eq :reserved_remote
|
130
|
+
end
|
131
|
+
|
132
|
+
it "should auto RST_STREAM promises against locally-RST stream" do
|
133
|
+
s = @conn.new_stream
|
134
|
+
s.send HEADERS
|
135
|
+
s.close
|
136
|
+
|
137
|
+
@conn.stub(:send)
|
138
|
+
@conn.should_receive(:send) do |frame|
|
139
|
+
frame[:type].should eq :rst_stream
|
140
|
+
frame[:stream].should eq 2
|
141
|
+
end
|
142
|
+
|
143
|
+
@conn << set_stream_id(f.generate(PUSH_PROMISE), s.id)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
context "flow control" do
|
149
|
+
it "should initialize to default flow window" do
|
150
|
+
@conn.window.should eq DEFAULT_FLOW_WINDOW
|
151
|
+
end
|
152
|
+
|
153
|
+
it "should update connection and stream windows on SETTINGS" do
|
154
|
+
settings, data = SETTINGS.dup, DATA.dup
|
155
|
+
settings[:payload] = { settings_initial_window_size: 1024 }
|
156
|
+
data[:payload] = 'x'*2048
|
157
|
+
|
158
|
+
stream = @conn.new_stream
|
159
|
+
|
160
|
+
stream.send HEADERS
|
161
|
+
stream.send data
|
162
|
+
stream.window.should eq (DEFAULT_FLOW_WINDOW - 2048)
|
163
|
+
@conn.window.should eq (DEFAULT_FLOW_WINDOW - 2048)
|
164
|
+
|
165
|
+
@conn << f.generate(settings)
|
166
|
+
@conn.window.should eq -1024
|
167
|
+
stream.window.should eq -1024
|
168
|
+
end
|
169
|
+
|
170
|
+
it "should initialize streams with window specified by peer" do
|
171
|
+
settings = SETTINGS.dup
|
172
|
+
settings[:payload] = { settings_initial_window_size: 1024 }
|
173
|
+
|
174
|
+
@conn << f.generate(settings)
|
175
|
+
@conn.new_stream.window.should eq 1024
|
176
|
+
end
|
177
|
+
|
178
|
+
it "should support global disable of flow control" do
|
179
|
+
@conn << f.generate(SETTINGS)
|
180
|
+
@conn.window.should eq Float::INFINITY
|
181
|
+
end
|
182
|
+
|
183
|
+
it "should raise error on flow control after disabling it" do
|
184
|
+
expect { @conn << f.generate(SETTINGS) }.to_not raise_error
|
185
|
+
expect {
|
186
|
+
[WINDOW_UPDATE, SETTINGS].each do |frame|
|
187
|
+
@conn.dup << f.generate(frame)
|
188
|
+
end
|
189
|
+
}.to raise_error(FlowControlError)
|
190
|
+
end
|
191
|
+
|
192
|
+
it "should observe connection flow control" do
|
193
|
+
settings, data = SETTINGS.dup, DATA.dup
|
194
|
+
settings[:payload] = { settings_initial_window_size: 1000 }
|
195
|
+
|
196
|
+
@conn << f.generate(settings)
|
197
|
+
s1 = @conn.new_stream
|
198
|
+
s2 = @conn.new_stream
|
199
|
+
|
200
|
+
s1.send HEADERS
|
201
|
+
s1.send data.merge({payload: "x" * 900})
|
202
|
+
@conn.window.should eq 100
|
203
|
+
|
204
|
+
s2.send HEADERS
|
205
|
+
s2.send data.merge({payload: "x" * 200})
|
206
|
+
@conn.window.should eq 0
|
207
|
+
@conn.buffered_amount.should eq 100
|
208
|
+
|
209
|
+
@conn << f.generate(WINDOW_UPDATE.merge({stream: 0, increment: 1000}))
|
210
|
+
@conn.buffered_amount.should eq 0
|
211
|
+
@conn.window.should eq 900
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
context "framing" do
|
216
|
+
it "should buffer incomplete frames" do
|
217
|
+
settings = SETTINGS.dup
|
218
|
+
settings[:payload] = { settings_initial_window_size: 1000 }
|
219
|
+
@conn << f.generate(settings)
|
220
|
+
|
221
|
+
frame = f.generate(WINDOW_UPDATE.merge({stream: 0, increment: 1000}))
|
222
|
+
@conn << frame
|
223
|
+
@conn.window.should eq 2000
|
224
|
+
|
225
|
+
@conn << frame.slice!(0,1)
|
226
|
+
@conn << frame
|
227
|
+
@conn.window.should eq 3000
|
228
|
+
end
|
229
|
+
|
230
|
+
it "should decompress header blocks regardless of stream state" do
|
231
|
+
req_headers = [
|
232
|
+
["content-length", "20"],
|
233
|
+
["x-my-header", "first"]
|
234
|
+
]
|
235
|
+
|
236
|
+
cc = Compressor.new(:response)
|
237
|
+
headers = HEADERS.dup
|
238
|
+
headers[:payload] = cc.encode(req_headers)
|
239
|
+
|
240
|
+
@conn << f.generate(SETTINGS)
|
241
|
+
@conn.on(:stream) do |stream|
|
242
|
+
stream.should_receive(:<<) do |frame|
|
243
|
+
frame[:payload].should eq req_headers
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
@conn << f.generate(headers)
|
248
|
+
end
|
249
|
+
|
250
|
+
it "should decode non-contiguous header blocks" do
|
251
|
+
req_headers = [
|
252
|
+
["content-length", "15"],
|
253
|
+
["x-my-header", "first"]
|
254
|
+
]
|
255
|
+
|
256
|
+
cc = Compressor.new(:response)
|
257
|
+
h1, h2 = HEADERS.dup, CONTINUATION.dup
|
258
|
+
h1[:payload] = cc.encode([req_headers.first])
|
259
|
+
h1[:stream] = 5
|
260
|
+
h1[:flags] = []
|
261
|
+
|
262
|
+
h2[:payload] = cc.encode([req_headers.last])
|
263
|
+
h2[:stream] = 5
|
264
|
+
|
265
|
+
@conn << f.generate(SETTINGS)
|
266
|
+
@conn.on(:stream) do |stream|
|
267
|
+
stream.should_receive(:<<) do |frame|
|
268
|
+
frame[:payload].should eq req_headers
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
@conn << f.generate(h1)
|
273
|
+
@conn << f.generate(h2)
|
274
|
+
end
|
275
|
+
|
276
|
+
it "should require that split header blocks are a contiguous sequence" do
|
277
|
+
headers, continutation = HEADERS.dup, CONTINUATION.dup
|
278
|
+
headers[:flags] = []
|
279
|
+
|
280
|
+
@conn << f.generate(SETTINGS)
|
281
|
+
@conn << f.generate(headers)
|
282
|
+
(FRAME_TYPES - [CONTINUATION]).each do |frame|
|
283
|
+
expect { @conn << f.generate(frame) }.to raise_error(ProtocolError)
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
it "should raise connection error on encode exception" do
|
288
|
+
@conn << f.generate(SETTINGS)
|
289
|
+
stream = @conn.new_stream
|
290
|
+
|
291
|
+
expect {
|
292
|
+
stream.headers({"name" => Float::INFINITY})
|
293
|
+
}.to raise_error(CompressionError)
|
294
|
+
end
|
295
|
+
|
296
|
+
it "should raise connection error on decode exception" do
|
297
|
+
@conn << f.generate(SETTINGS)
|
298
|
+
|
299
|
+
headers = HEADERS.dup
|
300
|
+
headers[:payload] = [0x44, 0x16].pack("C*")
|
301
|
+
expect { @conn << f.generate(headers) }.to raise_error(CompressionError)
|
302
|
+
end
|
303
|
+
|
304
|
+
it "should emit encoded frames via on(:frame)" do
|
305
|
+
bytes = nil
|
306
|
+
@conn.on(:frame) {|d| bytes = d }
|
307
|
+
@conn.settings({
|
308
|
+
settings_max_concurrent_streams: 10,
|
309
|
+
settings_flow_control_options: 1
|
310
|
+
})
|
311
|
+
|
312
|
+
bytes.should eq f.generate(SETTINGS)
|
313
|
+
end
|
314
|
+
|
315
|
+
it "should compress stream headers" do
|
316
|
+
@conn.on(:frame) do |bytes|
|
317
|
+
bytes.force_encoding('binary')
|
318
|
+
bytes.should_not match('get')
|
319
|
+
bytes.should_not match('http')
|
320
|
+
bytes.should match('www.example.org')
|
321
|
+
end
|
322
|
+
|
323
|
+
stream = @conn.new_stream
|
324
|
+
stream.headers({
|
325
|
+
':method' => 'get',
|
326
|
+
':scheme' => 'http',
|
327
|
+
':host' => 'www.example.org',
|
328
|
+
':path' => '/resource'
|
329
|
+
})
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
context "connection management" do
|
334
|
+
it "should respond to PING frames" do
|
335
|
+
@conn << f.generate(SETTINGS)
|
336
|
+
@conn.should_receive(:send) do |frame|
|
337
|
+
frame[:type].should eq :ping
|
338
|
+
frame[:flags].should eq [:pong]
|
339
|
+
frame[:payload].should eq "12345678"
|
340
|
+
end
|
341
|
+
|
342
|
+
@conn << f.generate(PING)
|
343
|
+
end
|
344
|
+
|
345
|
+
it "should fire callback on PONG" do
|
346
|
+
@conn << f.generate(SETTINGS)
|
347
|
+
|
348
|
+
pong = nil
|
349
|
+
@conn.ping("12345678") {|d| pong = d }
|
350
|
+
@conn << f.generate(PONG)
|
351
|
+
pong.should eq "12345678"
|
352
|
+
end
|
353
|
+
|
354
|
+
it "should fire callback on receipt of GOAWAY" do
|
355
|
+
last_stream, payload, error = nil
|
356
|
+
@conn << f.generate(SETTINGS)
|
357
|
+
@conn.on(:goaway) {|s,e,p| last_stream = s; error = e; payload = p}
|
358
|
+
@conn << f.generate(GOAWAY.merge({last_stream: 17, payload: "test"}))
|
359
|
+
|
360
|
+
last_stream.should eq 17
|
361
|
+
error.should eq :no_error
|
362
|
+
payload.should eq "test"
|
363
|
+
end
|
364
|
+
|
365
|
+
it "should raise error when opening new stream after sending GOAWAY" do
|
366
|
+
@conn.goaway
|
367
|
+
expect { @conn.new_stream }.to raise_error(ConnectionClosed)
|
368
|
+
end
|
369
|
+
|
370
|
+
it "should raise error when opening new stream after receiving GOAWAY" do
|
371
|
+
@conn << f.generate(SETTINGS)
|
372
|
+
@conn << f.generate(GOAWAY)
|
373
|
+
expect { @conn.new_stream }.to raise_error(ConnectionClosed)
|
374
|
+
end
|
375
|
+
|
376
|
+
it "should process connection management frames after GOAWAY" do
|
377
|
+
@conn << f.generate(SETTINGS)
|
378
|
+
@conn << f.generate(HEADERS)
|
379
|
+
@conn << f.generate(GOAWAY)
|
380
|
+
@conn << f.generate(HEADERS.merge({stream: 7}))
|
381
|
+
@conn << f.generate(PUSH_PROMISE)
|
382
|
+
|
383
|
+
@conn.active_stream_count.should eq 1
|
384
|
+
end
|
385
|
+
|
386
|
+
it "should raise error on frame for invalid stream ID" do
|
387
|
+
@conn << f.generate(SETTINGS)
|
388
|
+
|
389
|
+
expect {
|
390
|
+
@conn << f.generate(DATA.dup.merge({:stream => 31}))
|
391
|
+
}.to raise_error(ProtocolError)
|
392
|
+
end
|
393
|
+
|
394
|
+
it "should send GOAWAY frame on connection error" do
|
395
|
+
stream = @conn.new_stream
|
396
|
+
|
397
|
+
@conn.stub(:encode)
|
398
|
+
@conn.should_receive(:encode) do |frame|
|
399
|
+
frame[:type].should eq :goaway
|
400
|
+
frame[:last_stream].should eq stream.id
|
401
|
+
frame[:error].should eq :protocol_error
|
402
|
+
end
|
403
|
+
|
404
|
+
@conn << f.generate(SETTINGS)
|
405
|
+
expect { @conn << f.generate(DATA) }.to raise_error(ProtocolError)
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
context "API" do
|
410
|
+
it ".settings should emit SETTINGS frames" do
|
411
|
+
settings = {
|
412
|
+
settings_max_concurrent_streams: 10,
|
413
|
+
settings_flow_control_options: 1
|
414
|
+
}
|
415
|
+
|
416
|
+
@conn.should_receive(:send) do |frame|
|
417
|
+
frame[:type].should eq :settings
|
418
|
+
frame[:payload].should eq settings
|
419
|
+
frame[:stream].should eq 0
|
420
|
+
end
|
421
|
+
|
422
|
+
@conn.settings settings
|
423
|
+
end
|
424
|
+
|
425
|
+
it ".ping should generate PING frames" do
|
426
|
+
@conn.should_receive(:send) do |frame|
|
427
|
+
frame[:type].should eq :ping
|
428
|
+
frame[:payload].should eq "somedata"
|
429
|
+
end
|
430
|
+
|
431
|
+
@conn.ping("somedata")
|
432
|
+
end
|
433
|
+
|
434
|
+
it ".goaway should generate GOAWAY frame with last processed stream ID" do
|
435
|
+
@conn << f.generate(SETTINGS)
|
436
|
+
@conn << f.generate(HEADERS.merge({stream: 17}))
|
437
|
+
|
438
|
+
@conn.should_receive(:send) do |frame|
|
439
|
+
frame[:type].should eq :goaway
|
440
|
+
frame[:last_stream].should eq 17
|
441
|
+
frame[:error].should eq :internal_error
|
442
|
+
frame[:payload].should eq "payload"
|
443
|
+
end
|
444
|
+
|
445
|
+
@conn.goaway(:internal_error, "payload")
|
446
|
+
end
|
447
|
+
end
|
448
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "helper"
|
2
|
+
|
3
|
+
describe HTTP2::Emitter do
|
4
|
+
class Worker
|
5
|
+
include Emitter
|
6
|
+
end
|
7
|
+
|
8
|
+
before(:each) do
|
9
|
+
@w = Worker.new
|
10
|
+
@cnt = 0
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should raise error on missing callback" do
|
14
|
+
expect { @w.on(:a) {} }.to_not raise_error
|
15
|
+
expect { @w.on(:a) }.to raise_error
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should allow multiple callbacks on single event" do
|
19
|
+
@w.on(:a) { @cnt += 1 }
|
20
|
+
@w.on(:a) { @cnt += 1 }
|
21
|
+
@w.emit(:a)
|
22
|
+
|
23
|
+
@cnt.should eq 2
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should execute callback with optional args" do
|
27
|
+
args = nil
|
28
|
+
@w.on(:a) { |a| args = a }
|
29
|
+
@w.emit(:a, 123)
|
30
|
+
|
31
|
+
args.should eq 123
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should allow events with no callbacks" do
|
35
|
+
expect { @w.emit(:missing) }.to_not raise_error
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should execute callback exactly once" do
|
39
|
+
@w.on(:a) { @cnt += 1 }
|
40
|
+
@w.once(:a) { @cnt += 1 }
|
41
|
+
@w.emit(:a)
|
42
|
+
@w.emit(:a)
|
43
|
+
|
44
|
+
@cnt.should eq 3
|
45
|
+
end
|
46
|
+
end
|