mieps_http-2 0.8.0
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 +20 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +17 -0
- data/.gitmodules +3 -0
- data/.rspec +4 -0
- data/.rubocop.yml +18 -0
- data/.rubocop_todo.yml +46 -0
- data/.travis.yml +13 -0
- data/Gemfile +18 -0
- data/README.md +285 -0
- data/Rakefile +11 -0
- data/example/README.md +40 -0
- data/example/client.rb +117 -0
- data/example/helper.rb +19 -0
- data/example/keys/mycert.pem +23 -0
- data/example/keys/mykey.pem +27 -0
- data/example/server.rb +97 -0
- data/example/upgrade_server.rb +193 -0
- data/http-2.gemspec +23 -0
- data/lib/http/2/buffer.rb +34 -0
- data/lib/http/2/client.rb +51 -0
- data/lib/http/2/compressor.rb +557 -0
- data/lib/http/2/connection.rb +654 -0
- data/lib/http/2/emitter.rb +45 -0
- data/lib/http/2/error.rb +44 -0
- data/lib/http/2/flow_buffer.rb +67 -0
- data/lib/http/2/framer.rb +440 -0
- data/lib/http/2/huffman.rb +323 -0
- data/lib/http/2/huffman_statemachine.rb +272 -0
- data/lib/http/2/server.rb +132 -0
- data/lib/http/2/stream.rb +576 -0
- data/lib/http/2/version.rb +3 -0
- data/lib/http/2.rb +13 -0
- data/lib/tasks/generate_huffman_table.rb +166 -0
- data/spec/buffer_spec.rb +21 -0
- data/spec/client_spec.rb +92 -0
- data/spec/compressor_spec.rb +535 -0
- data/spec/connection_spec.rb +581 -0
- data/spec/emitter_spec.rb +54 -0
- data/spec/framer_spec.rb +487 -0
- data/spec/helper.rb +128 -0
- data/spec/hpack_test_spec.rb +79 -0
- data/spec/huffman_spec.rb +68 -0
- data/spec/server_spec.rb +51 -0
- data/spec/stream_spec.rb +794 -0
- metadata +116 -0
@@ -0,0 +1,581 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
RSpec.describe HTTP2::Connection do
|
4
|
+
before(:each) do
|
5
|
+
@conn = Client.new
|
6
|
+
end
|
7
|
+
|
8
|
+
let(:f) { Framer.new }
|
9
|
+
|
10
|
+
context 'initialization and settings' do
|
11
|
+
it 'should raise error if first frame is not SETTINGS' do
|
12
|
+
(FRAME_TYPES - [SETTINGS]).each do |frame|
|
13
|
+
frame = set_stream_id(f.generate(frame.deep_dup), 0x0)
|
14
|
+
expect { @conn.dup << frame }.to raise_error(ProtocolError)
|
15
|
+
end
|
16
|
+
|
17
|
+
expect { @conn << f.generate(SETTINGS.dup) }.to_not raise_error
|
18
|
+
expect(@conn.state).to eq :connected
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should raise error if SETTINGS stream != 0' do
|
22
|
+
frame = set_stream_id(f.generate(SETTINGS.dup), 0x1)
|
23
|
+
expect { @conn << frame }.to raise_error(ProtocolError)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'settings synchronization' do
|
28
|
+
it 'should reflect outgoing settings when ack is received' do
|
29
|
+
expect(@conn.local_settings[:settings_header_table_size]).to eq 4096
|
30
|
+
@conn.settings(settings_header_table_size: 256)
|
31
|
+
expect(@conn.local_settings[:settings_header_table_size]).to eq 4096
|
32
|
+
|
33
|
+
ack = { type: :settings, stream: 0, payload: [], flags: [:ack] }
|
34
|
+
@conn << f.generate(ack)
|
35
|
+
|
36
|
+
expect(@conn.local_settings[:settings_header_table_size]).to eq 256
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should reflect incoming settings when SETTINGS is received' do
|
40
|
+
expect(@conn.remote_settings[:settings_header_table_size]).to eq 4096
|
41
|
+
settings = SETTINGS.dup
|
42
|
+
settings[:payload] = [[:settings_header_table_size, 256]]
|
43
|
+
|
44
|
+
@conn << f.generate(settings)
|
45
|
+
|
46
|
+
expect(@conn.remote_settings[:settings_header_table_size]).to eq 256
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should send SETTINGS ACK when SETTINGS is received' do
|
50
|
+
settings = SETTINGS.dup
|
51
|
+
settings[:payload] = [[:settings_header_table_size, 256]]
|
52
|
+
|
53
|
+
expect(@conn).to receive(:send) do |frame|
|
54
|
+
expect(frame[:type]).to eq :settings
|
55
|
+
expect(frame[:flags]).to eq [:ack]
|
56
|
+
expect(frame[:payload]).to eq []
|
57
|
+
end
|
58
|
+
|
59
|
+
@conn << f.generate(settings)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'stream management' do
|
64
|
+
it 'should initialize to default stream limit (100)' do
|
65
|
+
expect(@conn.local_settings[:settings_max_concurrent_streams]).to eq 100
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'should change stream limit to received SETTINGS value' do
|
69
|
+
@conn << f.generate(SETTINGS.dup)
|
70
|
+
expect(@conn.remote_settings[:settings_max_concurrent_streams]).to eq 10
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'should count open streams against stream limit' do
|
74
|
+
s = @conn.new_stream
|
75
|
+
expect(@conn.active_stream_count).to eq 0
|
76
|
+
s.receive HEADERS
|
77
|
+
expect(@conn.active_stream_count).to eq 1
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'should not count reserved streams against stream limit' do
|
81
|
+
s1 = @conn.new_stream
|
82
|
+
s1.receive PUSH_PROMISE
|
83
|
+
expect(@conn.active_stream_count).to eq 0
|
84
|
+
|
85
|
+
s2 = @conn.new_stream
|
86
|
+
s2.send PUSH_PROMISE.deep_dup
|
87
|
+
expect(@conn.active_stream_count).to eq 0
|
88
|
+
|
89
|
+
# transition to half closed
|
90
|
+
s1.receive HEADERS
|
91
|
+
s2.send HEADERS.deep_dup
|
92
|
+
expect(@conn.active_stream_count).to eq 2
|
93
|
+
|
94
|
+
# transition to closed
|
95
|
+
s1.receive DATA
|
96
|
+
s2.send DATA.dup
|
97
|
+
expect(@conn.active_stream_count).to eq 0
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'should not exceed stream limit set by peer' do
|
101
|
+
@conn << f.generate(SETTINGS.dup)
|
102
|
+
|
103
|
+
expect do
|
104
|
+
10.times do
|
105
|
+
s = @conn.new_stream
|
106
|
+
s.send HEADERS.deep_dup
|
107
|
+
end
|
108
|
+
end.to_not raise_error
|
109
|
+
|
110
|
+
expect { @conn.new_stream }.to raise_error(StreamLimitExceeded)
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'should initialize stream with HEADERS priority value' do
|
114
|
+
@conn << f.generate(SETTINGS.dup)
|
115
|
+
|
116
|
+
stream, headers = nil, HEADERS.dup
|
117
|
+
headers[:weight] = 20
|
118
|
+
headers[:stream_dependency] = 0
|
119
|
+
headers[:exclusive] = false
|
120
|
+
|
121
|
+
@conn.on(:stream) { |s| stream = s }
|
122
|
+
@conn << f.generate(headers)
|
123
|
+
|
124
|
+
expect(stream.weight).to eq 20
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'should initialize idle stream on PRIORITY frame' do
|
128
|
+
@conn << f.generate(SETTINGS.dup)
|
129
|
+
|
130
|
+
stream = nil
|
131
|
+
@conn.on(:stream) { |s| stream = s }
|
132
|
+
@conn << f.generate(PRIORITY.dup)
|
133
|
+
|
134
|
+
expect(stream.state).to eq :idle
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
context 'Headers pre/post processing' do
|
139
|
+
it 'should not concatenate multiple occurences of a header field with the same name' do
|
140
|
+
input = [
|
141
|
+
['Content-Type', 'text/html'],
|
142
|
+
['Cache-Control', 'max-age=60, private'],
|
143
|
+
['Cache-Control', 'must-revalidate'],
|
144
|
+
]
|
145
|
+
expected = [
|
146
|
+
['content-type', 'text/html'],
|
147
|
+
['cache-control', 'max-age=60, private'],
|
148
|
+
['cache-control', 'must-revalidate'],
|
149
|
+
]
|
150
|
+
headers = []
|
151
|
+
@conn.on(:frame) do |bytes|
|
152
|
+
# bytes[3]: frame's type field
|
153
|
+
headers << f.parse(bytes) if [1, 5, 9].include?(bytes[3].ord)
|
154
|
+
end
|
155
|
+
|
156
|
+
stream = @conn.new_stream
|
157
|
+
stream.headers(input)
|
158
|
+
|
159
|
+
expect(headers.size).to eq 1
|
160
|
+
emitted = Decompressor.new.decode(headers.first[:payload])
|
161
|
+
expect(emitted).to match_array(expected)
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'should not split zero-concatenated header field values' do
|
165
|
+
input = [
|
166
|
+
['cache-control', "max-age=60, private\0must-revalidate"],
|
167
|
+
['content-type', 'text/html'],
|
168
|
+
['cookie', "a=b\0c=d; e=f"],
|
169
|
+
]
|
170
|
+
expected = [
|
171
|
+
['cache-control', "max-age=60, private\0must-revalidate"],
|
172
|
+
['content-type', 'text/html'],
|
173
|
+
['cookie', "a=b\0c=d; e=f"],
|
174
|
+
]
|
175
|
+
|
176
|
+
result = nil
|
177
|
+
@conn.on(:stream) do |stream|
|
178
|
+
stream.on(:headers) { |h| result = h }
|
179
|
+
end
|
180
|
+
|
181
|
+
srv = Server.new
|
182
|
+
srv.on(:frame) { |bytes| @conn << bytes }
|
183
|
+
stream = srv.new_stream
|
184
|
+
stream.headers(input)
|
185
|
+
|
186
|
+
expect(result).to eq expected
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
context 'flow control' do
|
191
|
+
it 'should initialize to default flow window' do
|
192
|
+
expect(@conn.remote_window).to eq DEFAULT_FLOW_WINDOW
|
193
|
+
end
|
194
|
+
|
195
|
+
it 'should update connection and stream windows on SETTINGS' do
|
196
|
+
settings, data = SETTINGS.dup, DATA.dup
|
197
|
+
settings[:payload] = [[:settings_initial_window_size, 1024]]
|
198
|
+
data[:payload] = 'x' * 2048
|
199
|
+
|
200
|
+
stream = @conn.new_stream
|
201
|
+
|
202
|
+
stream.send HEADERS.deep_dup
|
203
|
+
stream.send data
|
204
|
+
expect(stream.remote_window).to eq(DEFAULT_FLOW_WINDOW - 2048)
|
205
|
+
expect(@conn.remote_window).to eq(DEFAULT_FLOW_WINDOW - 2048)
|
206
|
+
|
207
|
+
@conn << f.generate(settings)
|
208
|
+
expect(@conn.remote_window).to eq(-1024)
|
209
|
+
expect(stream.remote_window).to eq(-1024)
|
210
|
+
end
|
211
|
+
|
212
|
+
it 'should initialize streams with window specified by peer' do
|
213
|
+
settings = SETTINGS.dup
|
214
|
+
settings[:payload] = [[:settings_initial_window_size, 1024]]
|
215
|
+
|
216
|
+
@conn << f.generate(settings)
|
217
|
+
expect(@conn.new_stream.remote_window).to eq 1024
|
218
|
+
end
|
219
|
+
|
220
|
+
it 'should observe connection flow control' do
|
221
|
+
settings, data = SETTINGS.dup, DATA.dup
|
222
|
+
settings[:payload] = [[:settings_initial_window_size, 1000]]
|
223
|
+
|
224
|
+
@conn << f.generate(settings)
|
225
|
+
s1 = @conn.new_stream
|
226
|
+
s2 = @conn.new_stream
|
227
|
+
|
228
|
+
s1.send HEADERS.deep_dup
|
229
|
+
s1.send data.merge(payload: 'x' * 900)
|
230
|
+
expect(@conn.remote_window).to eq 100
|
231
|
+
|
232
|
+
s2.send HEADERS.deep_dup
|
233
|
+
s2.send data.merge(payload: 'x' * 200)
|
234
|
+
expect(@conn.remote_window).to eq 0
|
235
|
+
expect(@conn.buffered_amount).to eq 100
|
236
|
+
|
237
|
+
@conn << f.generate(WINDOW_UPDATE.merge(stream: 0, increment: 1000))
|
238
|
+
expect(@conn.buffered_amount).to eq 0
|
239
|
+
expect(@conn.remote_window).to eq 900
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
context 'framing' do
|
244
|
+
it 'should buffer incomplete frames' do
|
245
|
+
settings = SETTINGS.dup
|
246
|
+
settings[:payload] = [[:settings_initial_window_size, 1000]]
|
247
|
+
@conn << f.generate(settings)
|
248
|
+
|
249
|
+
frame = f.generate(WINDOW_UPDATE.merge(stream: 0, increment: 1000))
|
250
|
+
@conn << frame
|
251
|
+
expect(@conn.remote_window).to eq 2000
|
252
|
+
|
253
|
+
@conn << frame.slice!(0, 1)
|
254
|
+
@conn << frame
|
255
|
+
expect(@conn.remote_window).to eq 3000
|
256
|
+
end
|
257
|
+
|
258
|
+
it 'should decompress header blocks regardless of stream state' do
|
259
|
+
req_headers = [
|
260
|
+
['content-length', '20'],
|
261
|
+
['x-my-header', 'first'],
|
262
|
+
]
|
263
|
+
|
264
|
+
cc = Compressor.new
|
265
|
+
headers = HEADERS.dup
|
266
|
+
headers[:payload] = cc.encode(req_headers)
|
267
|
+
|
268
|
+
@conn << f.generate(SETTINGS.dup)
|
269
|
+
@conn.on(:stream) do |stream|
|
270
|
+
expect(stream).to receive(:<<) do |frame|
|
271
|
+
expect(frame[:payload]).to eq req_headers
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
@conn << f.generate(headers)
|
276
|
+
end
|
277
|
+
|
278
|
+
it 'should decode non-contiguous header blocks' do
|
279
|
+
req_headers = [
|
280
|
+
['content-length', '15'],
|
281
|
+
['x-my-header', 'first'],
|
282
|
+
]
|
283
|
+
|
284
|
+
cc = Compressor.new
|
285
|
+
h1, h2 = HEADERS.dup, CONTINUATION.dup
|
286
|
+
|
287
|
+
# Header block fragment might not complete for decompression
|
288
|
+
payload = cc.encode(req_headers)
|
289
|
+
h1[:payload] = payload.slice!(0, payload.size / 2) # first half
|
290
|
+
h1[:stream] = 5
|
291
|
+
h1[:flags] = []
|
292
|
+
|
293
|
+
h2[:payload] = payload # the remaining
|
294
|
+
h2[:stream] = 5
|
295
|
+
|
296
|
+
@conn << f.generate(SETTINGS.dup)
|
297
|
+
@conn.on(:stream) do |stream|
|
298
|
+
expect(stream).to receive(:<<) do |frame|
|
299
|
+
expect(frame[:payload]).to eq req_headers
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
@conn << f.generate(h1)
|
304
|
+
@conn << f.generate(h2)
|
305
|
+
end
|
306
|
+
|
307
|
+
it 'should require that split header blocks are a contiguous sequence' do
|
308
|
+
headers = HEADERS.dup
|
309
|
+
headers[:flags] = []
|
310
|
+
|
311
|
+
@conn << f.generate(SETTINGS.dup)
|
312
|
+
@conn << f.generate(headers)
|
313
|
+
(FRAME_TYPES - [CONTINUATION]).each do |frame|
|
314
|
+
expect { @conn << f.generate(frame.deep_dup) }.to raise_error(ProtocolError)
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
it 'should raise compression error on encode of invalid frame' do
|
319
|
+
@conn << f.generate(SETTINGS.dup)
|
320
|
+
stream = @conn.new_stream
|
321
|
+
|
322
|
+
expect do
|
323
|
+
stream.headers('name' => Float::INFINITY)
|
324
|
+
end.to raise_error(CompressionError)
|
325
|
+
end
|
326
|
+
|
327
|
+
it 'should raise connection error on decode of invalid frame' do
|
328
|
+
@conn << f.generate(SETTINGS.dup)
|
329
|
+
frame = f.generate(DATA.dup) # Receiving DATA on unopened stream 1 is an error.
|
330
|
+
# Connection errors emit protocol error frames
|
331
|
+
expect { @conn << frame }.to raise_error(ProtocolError)
|
332
|
+
end
|
333
|
+
|
334
|
+
it 'should emit encoded frames via on(:frame)' do
|
335
|
+
bytes = nil
|
336
|
+
@conn.on(:frame) { |d| bytes = d }
|
337
|
+
@conn.settings(settings_max_concurrent_streams: 10,
|
338
|
+
settings_initial_window_size: 0x7fffffff)
|
339
|
+
|
340
|
+
expect(bytes).to eq f.generate(SETTINGS.dup)
|
341
|
+
end
|
342
|
+
|
343
|
+
it 'should compress stream headers' do
|
344
|
+
@conn.on(:frame) do |bytes|
|
345
|
+
expect(bytes).not_to include('get')
|
346
|
+
expect(bytes).not_to include('http')
|
347
|
+
expect(bytes).not_to include('www.example.org') # should be huffman encoded
|
348
|
+
end
|
349
|
+
|
350
|
+
stream = @conn.new_stream
|
351
|
+
stream.headers(':method' => 'get',
|
352
|
+
':scheme' => 'http',
|
353
|
+
':authority' => 'www.example.org',
|
354
|
+
':path' => '/resource')
|
355
|
+
end
|
356
|
+
|
357
|
+
it 'should generate CONTINUATION if HEADERS is too long' do
|
358
|
+
headers = []
|
359
|
+
@conn.on(:frame) do |bytes|
|
360
|
+
# bytes[3]: frame's type field
|
361
|
+
headers << f.parse(bytes) if [1, 5, 9].include?(bytes[3].ord)
|
362
|
+
end
|
363
|
+
|
364
|
+
stream = @conn.new_stream
|
365
|
+
stream.headers({
|
366
|
+
':method' => 'get',
|
367
|
+
':scheme' => 'http',
|
368
|
+
':authority' => 'www.example.org',
|
369
|
+
':path' => '/resource',
|
370
|
+
'custom' => 'q' * 44_000,
|
371
|
+
}, end_stream: true)
|
372
|
+
expect(headers.size).to eq 3
|
373
|
+
expect(headers[0][:type]).to eq :headers
|
374
|
+
expect(headers[1][:type]).to eq :continuation
|
375
|
+
expect(headers[2][:type]).to eq :continuation
|
376
|
+
expect(headers[0][:flags]).to eq [:end_stream]
|
377
|
+
expect(headers[1][:flags]).to eq []
|
378
|
+
expect(headers[2][:flags]).to eq [:end_headers]
|
379
|
+
end
|
380
|
+
|
381
|
+
it 'should not generate CONTINUATION if HEADERS fits exactly in a frame' do
|
382
|
+
headers = []
|
383
|
+
@conn.on(:frame) do |bytes|
|
384
|
+
# bytes[3]: frame's type field
|
385
|
+
headers << f.parse(bytes) if [1, 5, 9].include?(bytes[3].ord)
|
386
|
+
end
|
387
|
+
|
388
|
+
stream = @conn.new_stream
|
389
|
+
stream.headers({
|
390
|
+
':method' => 'get',
|
391
|
+
':scheme' => 'http',
|
392
|
+
':authority' => 'www.example.org',
|
393
|
+
':path' => '/resource',
|
394
|
+
'custom' => 'q' * 18_682, # this number should be updated when Huffman table is changed
|
395
|
+
}, end_stream: true)
|
396
|
+
expect(headers[0][:length]).to eq @conn.remote_settings[:settings_max_frame_size]
|
397
|
+
expect(headers.size).to eq 1
|
398
|
+
expect(headers[0][:type]).to eq :headers
|
399
|
+
expect(headers[0][:flags]).to include(:end_headers)
|
400
|
+
expect(headers[0][:flags]).to include(:end_stream)
|
401
|
+
end
|
402
|
+
|
403
|
+
it 'should not generate CONTINUATION if HEADERS fits exactly in a frame' do
|
404
|
+
headers = []
|
405
|
+
@conn.on(:frame) do |bytes|
|
406
|
+
# bytes[3]: frame's type field
|
407
|
+
headers << f.parse(bytes) if [1, 5, 9].include?(bytes[3].ord)
|
408
|
+
end
|
409
|
+
|
410
|
+
stream = @conn.new_stream
|
411
|
+
stream.headers({
|
412
|
+
':method' => 'get',
|
413
|
+
':scheme' => 'http',
|
414
|
+
':authority' => 'www.example.org',
|
415
|
+
':path' => '/resource',
|
416
|
+
'custom' => 'q' * 18_682, # this number should be updated when Huffman table is changed
|
417
|
+
}, end_stream: true)
|
418
|
+
expect(headers[0][:length]).to eq @conn.remote_settings[:settings_max_frame_size]
|
419
|
+
expect(headers.size).to eq 1
|
420
|
+
expect(headers[0][:type]).to eq :headers
|
421
|
+
expect(headers[0][:flags]).to include(:end_headers)
|
422
|
+
expect(headers[0][:flags]).to include(:end_stream)
|
423
|
+
end
|
424
|
+
|
425
|
+
it 'should generate CONTINUATION if HEADERS exceed the max payload by one byte' do
|
426
|
+
headers = []
|
427
|
+
@conn.on(:frame) do |bytes|
|
428
|
+
headers << f.parse(bytes) if [1, 5, 9].include?(bytes[3].ord)
|
429
|
+
end
|
430
|
+
|
431
|
+
stream = @conn.new_stream
|
432
|
+
stream.headers({
|
433
|
+
':method' => 'get',
|
434
|
+
':scheme' => 'http',
|
435
|
+
':authority' => 'www.example.org',
|
436
|
+
':path' => '/resource',
|
437
|
+
'custom' => 'q' * 18_683, # this number should be updated when Huffman table is changed
|
438
|
+
}, end_stream: true)
|
439
|
+
expect(headers[0][:length]).to eq @conn.remote_settings[:settings_max_frame_size]
|
440
|
+
expect(headers[1][:length]).to eq 1
|
441
|
+
expect(headers.size).to eq 2
|
442
|
+
expect(headers[0][:type]).to eq :headers
|
443
|
+
expect(headers[1][:type]).to eq :continuation
|
444
|
+
expect(headers[0][:flags]).to eq [:end_stream]
|
445
|
+
expect(headers[1][:flags]).to eq [:end_headers]
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
context 'connection management' do
|
450
|
+
it 'should raise error on invalid connection header' do
|
451
|
+
srv = Server.new
|
452
|
+
expect { srv << f.generate(SETTINGS.dup) }.to raise_error(HandshakeError)
|
453
|
+
|
454
|
+
srv = Server.new
|
455
|
+
expect do
|
456
|
+
srv << CONNECTION_PREFACE_MAGIC
|
457
|
+
srv << f.generate(SETTINGS.dup)
|
458
|
+
end.to_not raise_error
|
459
|
+
end
|
460
|
+
|
461
|
+
it 'should respond to PING frames' do
|
462
|
+
@conn << f.generate(SETTINGS.dup)
|
463
|
+
expect(@conn).to receive(:send) do |frame|
|
464
|
+
expect(frame[:type]).to eq :ping
|
465
|
+
expect(frame[:flags]).to eq [:ack]
|
466
|
+
expect(frame[:payload]).to eq '12345678'
|
467
|
+
end
|
468
|
+
|
469
|
+
@conn << f.generate(PING.dup)
|
470
|
+
end
|
471
|
+
|
472
|
+
it 'should fire callback on PONG' do
|
473
|
+
@conn << f.generate(SETTINGS.dup)
|
474
|
+
|
475
|
+
pong = nil
|
476
|
+
@conn.ping('12345678') { |d| pong = d }
|
477
|
+
@conn << f.generate(PONG.dup)
|
478
|
+
expect(pong).to eq '12345678'
|
479
|
+
end
|
480
|
+
|
481
|
+
it 'should fire callback on receipt of GOAWAY' do
|
482
|
+
last_stream, payload, error = nil
|
483
|
+
@conn << f.generate(SETTINGS.dup)
|
484
|
+
@conn.on(:goaway) do |s, e, p|
|
485
|
+
last_stream = s
|
486
|
+
error = e
|
487
|
+
payload = p
|
488
|
+
end
|
489
|
+
@conn << f.generate(GOAWAY.merge(last_stream: 17, payload: 'test'))
|
490
|
+
|
491
|
+
expect(last_stream).to eq 17
|
492
|
+
expect(error).to eq :no_error
|
493
|
+
expect(payload).to eq 'test'
|
494
|
+
end
|
495
|
+
|
496
|
+
it 'should raise error when opening new stream after sending GOAWAY' do
|
497
|
+
@conn.goaway
|
498
|
+
expect { @conn.new_stream }.to raise_error(ConnectionClosed)
|
499
|
+
end
|
500
|
+
|
501
|
+
it 'should raise error when opening new stream after receiving GOAWAY' do
|
502
|
+
@conn << f.generate(SETTINGS.dup)
|
503
|
+
@conn << f.generate(GOAWAY.dup)
|
504
|
+
expect { @conn.new_stream }.to raise_error(ConnectionClosed)
|
505
|
+
end
|
506
|
+
|
507
|
+
it 'should process connection management frames after GOAWAY' do
|
508
|
+
@conn << f.generate(SETTINGS.dup)
|
509
|
+
@conn << f.generate(HEADERS.dup)
|
510
|
+
@conn << f.generate(GOAWAY.dup)
|
511
|
+
@conn << f.generate(HEADERS.merge(stream: 7))
|
512
|
+
@conn << f.generate(PUSH_PROMISE.dup)
|
513
|
+
|
514
|
+
expect(@conn.active_stream_count).to eq 1
|
515
|
+
end
|
516
|
+
|
517
|
+
it 'should raise error on frame for invalid stream ID' do
|
518
|
+
@conn << f.generate(SETTINGS.dup)
|
519
|
+
|
520
|
+
expect do
|
521
|
+
@conn << f.generate(DATA.dup.merge(stream: 31))
|
522
|
+
end.to raise_error(ProtocolError)
|
523
|
+
end
|
524
|
+
|
525
|
+
it 'should send GOAWAY frame on connection error' do
|
526
|
+
stream = @conn.new_stream
|
527
|
+
|
528
|
+
expect(@conn).to receive(:encode) do |frame|
|
529
|
+
expect(frame[:type]).to eq :settings
|
530
|
+
[frame]
|
531
|
+
end
|
532
|
+
expect(@conn).to receive(:encode) do |frame|
|
533
|
+
expect(frame[:type]).to eq :goaway
|
534
|
+
expect(frame[:last_stream]).to eq stream.id
|
535
|
+
expect(frame[:error]).to eq :protocol_error
|
536
|
+
[frame]
|
537
|
+
end
|
538
|
+
|
539
|
+
expect { @conn << f.generate(DATA.dup) }.to raise_error(ProtocolError)
|
540
|
+
end
|
541
|
+
end
|
542
|
+
|
543
|
+
context 'API' do
|
544
|
+
it '.settings should emit SETTINGS frames' do
|
545
|
+
expect(@conn).to receive(:send) do |frame|
|
546
|
+
expect(frame[:type]).to eq :settings
|
547
|
+
expect(frame[:payload]).to eq([
|
548
|
+
[:settings_max_concurrent_streams, 10],
|
549
|
+
[:settings_initial_window_size, 0x7fffffff],
|
550
|
+
])
|
551
|
+
expect(frame[:stream]).to eq 0
|
552
|
+
end
|
553
|
+
|
554
|
+
@conn.settings(settings_max_concurrent_streams: 10,
|
555
|
+
settings_initial_window_size: 0x7fffffff)
|
556
|
+
end
|
557
|
+
|
558
|
+
it '.ping should generate PING frames' do
|
559
|
+
expect(@conn).to receive(:send) do |frame|
|
560
|
+
expect(frame[:type]).to eq :ping
|
561
|
+
expect(frame[:payload]).to eq 'somedata'
|
562
|
+
end
|
563
|
+
|
564
|
+
@conn.ping('somedata')
|
565
|
+
end
|
566
|
+
|
567
|
+
it '.goaway should generate GOAWAY frame with last processed stream ID' do
|
568
|
+
@conn << f.generate(SETTINGS.dup)
|
569
|
+
@conn << f.generate(HEADERS.merge(stream: 17))
|
570
|
+
|
571
|
+
expect(@conn).to receive(:send) do |frame|
|
572
|
+
expect(frame[:type]).to eq :goaway
|
573
|
+
expect(frame[:last_stream]).to eq 17
|
574
|
+
expect(frame[:error]).to eq :internal_error
|
575
|
+
expect(frame[:payload]).to eq 'payload'
|
576
|
+
end
|
577
|
+
|
578
|
+
@conn.goaway(:internal_error, 'payload')
|
579
|
+
end
|
580
|
+
end
|
581
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
RSpec.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
|
+
expect(@cnt).to 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
|
+
expect(args).to eq 123
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should pass emitted callbacks to listeners' do
|
35
|
+
@w.on(:a) { |&block| block.call }
|
36
|
+
@w.once(:a) { |&block| block.call }
|
37
|
+
@w.emit(:a) { @cnt += 1 }
|
38
|
+
|
39
|
+
expect(@cnt).to eq 2
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'should allow events with no callbacks' do
|
43
|
+
expect { @w.emit(:missing) }.to_not raise_error
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should execute callback exactly once' do
|
47
|
+
@w.on(:a) { @cnt += 1 }
|
48
|
+
@w.once(:a) { @cnt += 1 }
|
49
|
+
@w.emit(:a)
|
50
|
+
@w.emit(:a)
|
51
|
+
|
52
|
+
expect(@cnt).to eq 3
|
53
|
+
end
|
54
|
+
end
|