http-2 0.8.4 → 0.10.2
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 +5 -5
- data/.rspec +1 -0
- data/.rubocop.yml +44 -8
- data/.rubocop_todo.yml +7 -4
- data/.travis.yml +1 -1
- data/Gemfile +1 -1
- data/Guardfile +0 -1
- data/LICENSE +21 -0
- data/README.md +13 -19
- data/example/client.rb +5 -1
- data/example/server.rb +2 -4
- data/example/upgrade_client.rb +153 -0
- data/example/upgrade_server.rb +2 -2
- data/http-2.gemspec +2 -3
- data/lib/http/2/client.rb +18 -1
- data/lib/http/2/compressor.rb +26 -11
- data/lib/http/2/connection.rb +62 -16
- data/lib/http/2/flow_buffer.rb +31 -0
- data/lib/http/2/framer.rb +3 -2
- data/lib/http/2/stream.rb +27 -15
- data/lib/http/2/version.rb +1 -1
- data/spec/client_spec.rb +105 -9
- data/spec/compressor_spec.rb +157 -26
- data/spec/connection_spec.rb +153 -76
- data/spec/framer_spec.rb +5 -5
- data/spec/helper.rb +125 -107
- data/spec/server_spec.rb +2 -1
- data/spec/stream_spec.rb +131 -115
- metadata +9 -8
data/lib/http/2/version.rb
CHANGED
data/spec/client_spec.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
3
|
RSpec.describe HTTP2::Client do
|
4
|
+
include FrameHelpers
|
4
5
|
before(:each) do
|
5
6
|
@client = Client.new
|
6
7
|
end
|
@@ -32,6 +33,35 @@ RSpec.describe HTTP2::Client do
|
|
32
33
|
expect(frame[:type]).to eq :settings
|
33
34
|
expect(frame[:payload]).to include([:settings_max_concurrent_streams, 200])
|
34
35
|
end
|
36
|
+
|
37
|
+
it 'should initialize client when receiving server settings before sending ack' do
|
38
|
+
frames = []
|
39
|
+
@client.on(:frame) { |bytes| frames << bytes }
|
40
|
+
@client << f.generate(settings_frame)
|
41
|
+
|
42
|
+
expect(frames[0]).to eq CONNECTION_PREFACE_MAGIC
|
43
|
+
expect(f.parse(frames[1])[:type]).to eq :settings
|
44
|
+
ack_frame = f.parse(frames[2])
|
45
|
+
expect(ack_frame[:type]).to eq :settings
|
46
|
+
expect(ack_frame[:flags]).to include(:ack)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'upgrade' do
|
51
|
+
it 'fails when client has already created streams' do
|
52
|
+
@client.new_stream
|
53
|
+
expect { @client.upgrade }.to raise_error(HTTP2::Error::ProtocolError)
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'sends the preface' do
|
57
|
+
expect(@client).to receive(:send_connection_preface)
|
58
|
+
@client.upgrade
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'initializes the first stream in the half-closed state' do
|
62
|
+
stream = @client.upgrade
|
63
|
+
expect(stream.state).to be(:half_closed_local)
|
64
|
+
end
|
35
65
|
end
|
36
66
|
|
37
67
|
context 'push' do
|
@@ -43,41 +73,57 @@ RSpec.describe HTTP2::Client do
|
|
43
73
|
|
44
74
|
it 'should raise error on PUSH_PROMISE against stream 0' do
|
45
75
|
expect do
|
46
|
-
@client << set_stream_id(f.generate(
|
76
|
+
@client << set_stream_id(f.generate(push_promise_frame), 0)
|
47
77
|
end.to raise_error(ProtocolError)
|
48
78
|
end
|
49
79
|
|
50
80
|
it 'should raise error on PUSH_PROMISE against bogus stream' do
|
51
81
|
expect do
|
52
|
-
@client << set_stream_id(f.generate(
|
82
|
+
@client << set_stream_id(f.generate(push_promise_frame), 31_415)
|
53
83
|
end.to raise_error(ProtocolError)
|
54
84
|
end
|
55
85
|
|
56
86
|
it 'should raise error on PUSH_PROMISE against non-idle stream' do
|
57
87
|
expect do
|
58
88
|
s = @client.new_stream
|
59
|
-
s.send
|
89
|
+
s.send headers_frame
|
60
90
|
|
61
|
-
@client << set_stream_id(f.generate(
|
62
|
-
@client << set_stream_id(f.generate(
|
91
|
+
@client << set_stream_id(f.generate(push_promise_frame), s.id)
|
92
|
+
@client << set_stream_id(f.generate(push_promise_frame), s.id)
|
63
93
|
end.to raise_error(ProtocolError)
|
64
94
|
end
|
65
95
|
|
66
96
|
it 'should emit stream object for received PUSH_PROMISE' do
|
67
97
|
s = @client.new_stream
|
68
|
-
s.send
|
98
|
+
s.send headers_frame
|
69
99
|
|
70
100
|
promise = nil
|
71
101
|
@client.on(:promise) { |stream| promise = stream }
|
72
|
-
@client << set_stream_id(f.generate(
|
102
|
+
@client << set_stream_id(f.generate(push_promise_frame), s.id)
|
73
103
|
|
74
104
|
expect(promise.id).to eq 2
|
75
105
|
expect(promise.state).to eq :reserved_remote
|
76
106
|
end
|
77
107
|
|
108
|
+
it 'should emit promise headers for received PUSH_PROMISE' do
|
109
|
+
header = nil
|
110
|
+
s = @client.new_stream
|
111
|
+
s.send headers_frame
|
112
|
+
|
113
|
+
@client.on(:promise) do |stream|
|
114
|
+
stream.on(:promise_headers) do |h|
|
115
|
+
header = h
|
116
|
+
end
|
117
|
+
end
|
118
|
+
@client << set_stream_id(f.generate(push_promise_frame), s.id)
|
119
|
+
|
120
|
+
expect(header).to be_a(Array)
|
121
|
+
# expect(header).to eq([%w(a b)])
|
122
|
+
end
|
123
|
+
|
78
124
|
it 'should auto RST_STREAM promises against locally-RST stream' do
|
79
125
|
s = @client.new_stream
|
80
|
-
s.send
|
126
|
+
s.send headers_frame
|
81
127
|
s.close
|
82
128
|
|
83
129
|
allow(@client).to receive(:send)
|
@@ -86,7 +132,57 @@ RSpec.describe HTTP2::Client do
|
|
86
132
|
expect(frame[:stream]).to eq 2
|
87
133
|
end
|
88
134
|
|
89
|
-
@client << set_stream_id(f.generate(
|
135
|
+
@client << set_stream_id(f.generate(push_promise_frame), s.id)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
context 'alt-svc' do
|
140
|
+
context 'received in the connection' do
|
141
|
+
it 'should emit :altsvc when receiving one' do
|
142
|
+
@client << f.generate(settings_frame)
|
143
|
+
frame = nil
|
144
|
+
@client.on(:altsvc) do |f|
|
145
|
+
frame = f
|
146
|
+
end
|
147
|
+
@client << f.generate(altsvc_frame)
|
148
|
+
expect(frame).to be_a(Hash)
|
149
|
+
end
|
150
|
+
it 'should not emit :altsvc when the frame when contains no host' do
|
151
|
+
@client << f.generate(settings_frame)
|
152
|
+
frame = nil
|
153
|
+
@client.on(:altsvc) do |f|
|
154
|
+
frame = f
|
155
|
+
end
|
156
|
+
|
157
|
+
@client << f.generate(altsvc_frame.merge(origin: nil))
|
158
|
+
expect(frame).to be_nil
|
159
|
+
end
|
160
|
+
end
|
161
|
+
context 'received in a stream' do
|
162
|
+
it 'should emit :altsvc' do
|
163
|
+
s = @client.new_stream
|
164
|
+
s.send headers_frame
|
165
|
+
s.close
|
166
|
+
|
167
|
+
frame = nil
|
168
|
+
s.on(:altsvc) { |f| frame = f }
|
169
|
+
|
170
|
+
@client << set_stream_id(f.generate(altsvc_frame.merge(origin: nil)), s.id)
|
171
|
+
|
172
|
+
expect(frame).to be_a(Hash)
|
173
|
+
end
|
174
|
+
it 'should not emit :alt_svc when the frame when contains a origin' do
|
175
|
+
s = @client.new_stream
|
176
|
+
s.send headers_frame
|
177
|
+
s.close
|
178
|
+
|
179
|
+
frame = nil
|
180
|
+
s.on(:altsvc) { |f| frame = f }
|
181
|
+
|
182
|
+
@client << set_stream_id(f.generate(altsvc_frame), s.id)
|
183
|
+
|
184
|
+
expect(frame).to be_nil
|
185
|
+
end
|
90
186
|
end
|
91
187
|
end
|
92
188
|
end
|
data/spec/compressor_spec.rb
CHANGED
@@ -236,6 +236,24 @@ RSpec.describe HTTP2::Header do
|
|
236
236
|
expect(cc.table.size).to be 1
|
237
237
|
expect(cc.table.first[0]).to eq 'test2'
|
238
238
|
end
|
239
|
+
|
240
|
+
it 'should reject table size update if exceed limit' do
|
241
|
+
cc = EncodingContext.new(table_size: 4096)
|
242
|
+
|
243
|
+
expect { cc.process(type: :changetablesize, value: 150_000_000) }.to raise_error(CompressionError)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
context 'encode' do
|
248
|
+
it 'downcases the field' do
|
249
|
+
expect(EncodingContext.new.encode([['Content-Length', '5']]))
|
250
|
+
.to eq(EncodingContext.new.encode([['content-length', '5']]))
|
251
|
+
end
|
252
|
+
|
253
|
+
it 'fills :path if empty' do
|
254
|
+
expect(EncodingContext.new.encode([[':path', '']]))
|
255
|
+
.to eq(EncodingContext.new.encode([[':path', '/']]))
|
256
|
+
end
|
239
257
|
end
|
240
258
|
end
|
241
259
|
|
@@ -339,6 +357,78 @@ RSpec.describe HTTP2::Header do
|
|
339
357
|
},
|
340
358
|
],
|
341
359
|
},
|
360
|
+
{ title: 'D.4.a. Request Examples with Huffman - Client Handling of Improperly Ordered Headers',
|
361
|
+
type: :request,
|
362
|
+
table_size: 4096,
|
363
|
+
huffman: :always,
|
364
|
+
streams: [
|
365
|
+
{ wire: '8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4 ff',
|
366
|
+
emitted: [
|
367
|
+
[':method', 'GET'],
|
368
|
+
[':scheme', 'http'],
|
369
|
+
[':path', '/'],
|
370
|
+
[':authority', 'www.example.com'],
|
371
|
+
],
|
372
|
+
table: [
|
373
|
+
[':authority', 'www.example.com'],
|
374
|
+
],
|
375
|
+
table_size: 57,
|
376
|
+
},
|
377
|
+
{ wire: '8286 84be 5886 a8eb 1064 9cbf',
|
378
|
+
emitted: [
|
379
|
+
[':method', 'GET'],
|
380
|
+
[':scheme', 'http'],
|
381
|
+
['cache-control', 'no-cache'],
|
382
|
+
[':path', '/'],
|
383
|
+
[':authority', 'www.example.com'],
|
384
|
+
],
|
385
|
+
table: [
|
386
|
+
['cache-control', 'no-cache'],
|
387
|
+
[':authority', 'www.example.com'],
|
388
|
+
],
|
389
|
+
table_size: 110,
|
390
|
+
},
|
391
|
+
{ wire: "8287 85bf 4088 25a8 49e9 5ba9 7d7f 8925
|
392
|
+
a849 e95b b8e8 b4bf",
|
393
|
+
emitted: [
|
394
|
+
[':method', 'GET'],
|
395
|
+
[':scheme', 'https'],
|
396
|
+
['custom-key', 'custom-value'],
|
397
|
+
[':path', '/index.html'],
|
398
|
+
[':authority', 'www.example.com'],
|
399
|
+
],
|
400
|
+
table: [
|
401
|
+
['custom-key', 'custom-value'],
|
402
|
+
['cache-control', 'no-cache'],
|
403
|
+
[':authority', 'www.example.com'],
|
404
|
+
],
|
405
|
+
table_size: 164,
|
406
|
+
},
|
407
|
+
],
|
408
|
+
},
|
409
|
+
{ title: 'D.4.b. Request Examples with Huffman - Server Handling of Improperly Ordered Headers',
|
410
|
+
type: :request,
|
411
|
+
bypass_encoder: true,
|
412
|
+
table_size: 4096,
|
413
|
+
huffman: :always,
|
414
|
+
streams: [
|
415
|
+
{ wire: '8286408825a849e95ba97d7f8925a849e95bb8e8b4bf84418cf1e3c2e5f23a6ba0ab90f4ff',
|
416
|
+
emitted: [
|
417
|
+
[':method', 'GET'],
|
418
|
+
[':scheme', 'http'],
|
419
|
+
['custom-key', 'custom-value'],
|
420
|
+
[':path', '/'],
|
421
|
+
[':authority', 'www.example.com'],
|
422
|
+
],
|
423
|
+
table: [
|
424
|
+
['custom-key', 'custom-value'],
|
425
|
+
[':authority', 'www.example.com'],
|
426
|
+
],
|
427
|
+
table_size: 111,
|
428
|
+
has_bad_headers: true,
|
429
|
+
},
|
430
|
+
],
|
431
|
+
},
|
342
432
|
{ title: 'D.5. Response Examples without Huffman',
|
343
433
|
type: :response,
|
344
434
|
table_size: 256,
|
@@ -462,6 +552,22 @@ RSpec.describe HTTP2::Header do
|
|
462
552
|
},
|
463
553
|
],
|
464
554
|
},
|
555
|
+
{ title: 'D.6.a. Response Examples with Huffman - dynamic table size updates should not trigger exceptions',
|
556
|
+
type: :response,
|
557
|
+
table_size: 4096,
|
558
|
+
huffman: :always,
|
559
|
+
bypass_encoder: true,
|
560
|
+
streams: [
|
561
|
+
{ wire: '2088 7689 aa63 55e5 80ae 16d7 17',
|
562
|
+
emitted: [
|
563
|
+
[':status', '200'],
|
564
|
+
['server', 'nginx/1.15.2'],
|
565
|
+
],
|
566
|
+
table: [],
|
567
|
+
table_size: 0,
|
568
|
+
},
|
569
|
+
],
|
570
|
+
},
|
465
571
|
]
|
466
572
|
|
467
573
|
context 'decode' do
|
@@ -473,25 +579,38 @@ RSpec.describe HTTP2::Header do
|
|
473
579
|
before do
|
474
580
|
(0...nth).each do |i|
|
475
581
|
bytes = [ex[:streams][i][:wire].delete(" \n")].pack('H*')
|
476
|
-
|
582
|
+
if ex[:streams][i][:has_bad_headers]
|
583
|
+
expect { @dc.decode(HTTP2::Buffer.new(bytes)) }.to raise_error ProtocolError
|
584
|
+
else
|
585
|
+
@dc.decode(HTTP2::Buffer.new(bytes))
|
586
|
+
end
|
477
587
|
end
|
478
588
|
end
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
589
|
+
if ex[:streams][nth][:has_bad_headers]
|
590
|
+
it 'should raise CompressionError' do
|
591
|
+
bytes = [ex[:streams][nth][:wire].delete(" \n")].pack('H*')
|
592
|
+
expect { @dc.decode(HTTP2::Buffer.new(bytes)) }.to raise_error ProtocolError
|
593
|
+
end
|
594
|
+
else
|
595
|
+
subject do
|
596
|
+
bytes = [ex[:streams][nth][:wire].delete(" \n")].pack('H*')
|
597
|
+
@emitted = @dc.decode(HTTP2::Buffer.new(bytes))
|
598
|
+
end
|
599
|
+
it 'should emit expected headers' do
|
600
|
+
subject
|
601
|
+
# partitioned compare
|
602
|
+
pseudo_headers, headers = ex[:streams][nth][:emitted].partition { |f, _| f.start_with? ':' }
|
603
|
+
partitioned_headers = pseudo_headers + headers
|
604
|
+
expect(@emitted).to eq partitioned_headers
|
605
|
+
end
|
606
|
+
it 'should update header table' do
|
607
|
+
subject
|
608
|
+
expect(@dc.instance_eval { @cc.table }).to eq ex[:streams][nth][:table]
|
609
|
+
end
|
610
|
+
it 'should compute header table size' do
|
611
|
+
subject
|
612
|
+
expect(@dc.instance_eval { @cc.current_table_size }).to eq ex[:streams][nth][:table_size]
|
613
|
+
end
|
495
614
|
end
|
496
615
|
end
|
497
616
|
end
|
@@ -501,6 +620,7 @@ RSpec.describe HTTP2::Header do
|
|
501
620
|
|
502
621
|
context 'encode' do
|
503
622
|
spec_examples.each do |ex|
|
623
|
+
next if ex[:bypass_encoder]
|
504
624
|
context "spec example #{ex[:title]}" do
|
505
625
|
ex[:streams].size.times do |nth|
|
506
626
|
context "request #{nth + 1}" do
|
@@ -510,22 +630,33 @@ RSpec.describe HTTP2::Header do
|
|
510
630
|
end
|
511
631
|
before do
|
512
632
|
(0...nth).each do |i|
|
513
|
-
|
633
|
+
if ex[:streams][i][:has_bad_headers]
|
634
|
+
@cc.encode(ex[:streams][i][:emitted], ensure_proper_ordering: false)
|
635
|
+
else
|
636
|
+
@cc.encode(ex[:streams][i][:emitted])
|
637
|
+
end
|
514
638
|
end
|
515
639
|
end
|
516
640
|
subject do
|
517
|
-
|
641
|
+
if ex[:streams][nth][:has_bad_headers]
|
642
|
+
@cc.encode(ex[:streams][nth][:emitted], ensure_proper_ordering: false)
|
643
|
+
else
|
644
|
+
@cc.encode(ex[:streams][nth][:emitted])
|
645
|
+
end
|
518
646
|
end
|
519
647
|
it 'should emit expected bytes on wire' do
|
648
|
+
puts subject.unpack('H*').first
|
520
649
|
expect(subject.unpack('H*').first).to eq ex[:streams][nth][:wire].delete(" \n")
|
521
650
|
end
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
651
|
+
unless ex[:streams][nth][:has_bad_headers]
|
652
|
+
it 'should update header table' do
|
653
|
+
subject
|
654
|
+
expect(@cc.instance_eval { @cc.table }).to eq ex[:streams][nth][:table]
|
655
|
+
end
|
656
|
+
it 'should compute header table size' do
|
657
|
+
subject
|
658
|
+
expect(@cc.instance_eval { @cc.current_table_size }).to eq ex[:streams][nth][:table_size]
|
659
|
+
end
|
529
660
|
end
|
530
661
|
end
|
531
662
|
end
|
data/spec/connection_spec.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
3
|
RSpec.describe HTTP2::Connection do
|
4
|
+
include FrameHelpers
|
4
5
|
before(:each) do
|
5
6
|
@conn = Client.new
|
6
7
|
end
|
@@ -8,18 +9,21 @@ RSpec.describe HTTP2::Connection do
|
|
8
9
|
let(:f) { Framer.new }
|
9
10
|
|
10
11
|
context 'initialization and settings' do
|
11
|
-
it 'should raise error if first frame is not
|
12
|
-
(
|
13
|
-
|
14
|
-
expect
|
12
|
+
it 'should raise error if first frame is not settings' do
|
13
|
+
(frame_types - [settings_frame]).each do |frame|
|
14
|
+
expect { @conn << frame }.to raise_error(ProtocolError)
|
15
|
+
expect(@conn).to be_closed
|
15
16
|
end
|
17
|
+
end
|
16
18
|
|
17
|
-
|
19
|
+
it 'should not raise error if first frame is SETTINGS' do
|
20
|
+
expect { @conn << f.generate(settings_frame) }.to_not raise_error
|
18
21
|
expect(@conn.state).to eq :connected
|
22
|
+
expect(@conn).to_not be_closed
|
19
23
|
end
|
20
24
|
|
21
25
|
it 'should raise error if SETTINGS stream != 0' do
|
22
|
-
frame = set_stream_id(f.generate(
|
26
|
+
frame = set_stream_id(f.generate(settings_frame), 0x1)
|
23
27
|
expect { @conn << frame }.to raise_error(ProtocolError)
|
24
28
|
end
|
25
29
|
end
|
@@ -38,7 +42,7 @@ RSpec.describe HTTP2::Connection do
|
|
38
42
|
|
39
43
|
it 'should reflect incoming settings when SETTINGS is received' do
|
40
44
|
expect(@conn.remote_settings[:settings_header_table_size]).to eq 4096
|
41
|
-
settings =
|
45
|
+
settings = settings_frame
|
42
46
|
settings[:payload] = [[:settings_header_table_size, 256]]
|
43
47
|
|
44
48
|
@conn << f.generate(settings)
|
@@ -47,16 +51,22 @@ RSpec.describe HTTP2::Connection do
|
|
47
51
|
end
|
48
52
|
|
49
53
|
it 'should send SETTINGS ACK when SETTINGS is received' do
|
50
|
-
settings =
|
54
|
+
settings = settings_frame
|
51
55
|
settings[:payload] = [[:settings_header_table_size, 256]]
|
52
56
|
|
53
|
-
expect(
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
+
# We should expect two frames here (append .twice) - one for the connection setup, and one for the settings ack.
|
58
|
+
frames = []
|
59
|
+
expect(@conn).to receive(:send).twice do |frame|
|
60
|
+
frames << frame
|
57
61
|
end
|
58
62
|
|
63
|
+
@conn.send_connection_preface
|
59
64
|
@conn << f.generate(settings)
|
65
|
+
|
66
|
+
frame = frames.last
|
67
|
+
expect(frame[:type]).to eq :settings
|
68
|
+
expect(frame[:flags]).to eq [:ack]
|
69
|
+
expect(frame[:payload]).to eq []
|
60
70
|
end
|
61
71
|
end
|
62
72
|
|
@@ -66,44 +76,47 @@ RSpec.describe HTTP2::Connection do
|
|
66
76
|
end
|
67
77
|
|
68
78
|
it 'should change stream limit to received SETTINGS value' do
|
69
|
-
@conn << f.generate(
|
79
|
+
@conn << f.generate(settings_frame)
|
70
80
|
expect(@conn.remote_settings[:settings_max_concurrent_streams]).to eq 10
|
71
81
|
end
|
72
82
|
|
73
83
|
it 'should count open streams against stream limit' do
|
74
84
|
s = @conn.new_stream
|
75
85
|
expect(@conn.active_stream_count).to eq 0
|
76
|
-
s.receive
|
86
|
+
s.receive headers_frame
|
77
87
|
expect(@conn.active_stream_count).to eq 1
|
78
88
|
end
|
79
89
|
|
80
90
|
it 'should not count reserved streams against stream limit' do
|
81
91
|
s1 = @conn.new_stream
|
82
|
-
s1.receive
|
92
|
+
s1.receive push_promise_frame
|
83
93
|
expect(@conn.active_stream_count).to eq 0
|
84
94
|
|
85
95
|
s2 = @conn.new_stream
|
86
|
-
s2.send
|
96
|
+
s2.send push_promise_frame
|
87
97
|
expect(@conn.active_stream_count).to eq 0
|
88
98
|
|
89
99
|
# transition to half closed
|
90
|
-
s1.receive
|
91
|
-
s2.send
|
100
|
+
s1.receive headers_frame
|
101
|
+
s2.send headers_frame
|
92
102
|
expect(@conn.active_stream_count).to eq 2
|
93
103
|
|
94
104
|
# transition to closed
|
95
|
-
s1.receive
|
96
|
-
s2.send
|
105
|
+
s1.receive data_frame
|
106
|
+
s2.send data_frame
|
97
107
|
expect(@conn.active_stream_count).to eq 0
|
108
|
+
|
109
|
+
expect(s1).to be_closed
|
110
|
+
expect(s2).to be_closed
|
98
111
|
end
|
99
112
|
|
100
113
|
it 'should not exceed stream limit set by peer' do
|
101
|
-
@conn << f.generate(
|
114
|
+
@conn << f.generate(settings_frame)
|
102
115
|
|
103
116
|
expect do
|
104
117
|
10.times do
|
105
118
|
s = @conn.new_stream
|
106
|
-
s.send
|
119
|
+
s.send headers_frame
|
107
120
|
end
|
108
121
|
end.to_not raise_error
|
109
122
|
|
@@ -111,9 +124,9 @@ RSpec.describe HTTP2::Connection do
|
|
111
124
|
end
|
112
125
|
|
113
126
|
it 'should initialize stream with HEADERS priority value' do
|
114
|
-
@conn << f.generate(
|
127
|
+
@conn << f.generate(settings_frame)
|
115
128
|
|
116
|
-
stream, headers = nil,
|
129
|
+
stream, headers = nil, headers_frame
|
117
130
|
headers[:weight] = 20
|
118
131
|
headers[:stream_dependency] = 0
|
119
132
|
headers[:exclusive] = false
|
@@ -125,16 +138,33 @@ RSpec.describe HTTP2::Connection do
|
|
125
138
|
end
|
126
139
|
|
127
140
|
it 'should initialize idle stream on PRIORITY frame' do
|
128
|
-
@conn << f.generate(
|
141
|
+
@conn << f.generate(settings_frame)
|
129
142
|
|
130
143
|
stream = nil
|
131
144
|
@conn.on(:stream) { |s| stream = s }
|
132
|
-
@conn << f.generate(
|
145
|
+
@conn << f.generate(priority_frame)
|
133
146
|
|
134
147
|
expect(stream.state).to eq :idle
|
135
148
|
end
|
136
149
|
end
|
137
150
|
|
151
|
+
context 'cleanup_recently_closed' do
|
152
|
+
it 'should cleanup old connections' do
|
153
|
+
now_ts = Time.now.to_i
|
154
|
+
stream_ids = Array.new(4) { @conn.new_stream.id }
|
155
|
+
expect(@conn.instance_variable_get('@streams').size).to eq(4)
|
156
|
+
|
157
|
+
# Assume that the first 3 streams were closed in different time
|
158
|
+
recently_closed = stream_ids[0, 3].zip([now_ts - 100, now_ts - 50, now_ts - 5]).to_h
|
159
|
+
@conn.instance_variable_set('@streams_recently_closed', recently_closed)
|
160
|
+
|
161
|
+
# Cleanup should delete streams that were closed earlier than 15s ago
|
162
|
+
@conn.__send__(:cleanup_recently_closed)
|
163
|
+
expect(@conn.instance_variable_get('@streams').size).to eq(2)
|
164
|
+
expect(@conn.instance_variable_get('@streams_recently_closed')).to eq(stream_ids[2] => now_ts - 5)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
138
168
|
context 'Headers pre/post processing' do
|
139
169
|
it 'should not concatenate multiple occurences of a header field with the same name' do
|
140
170
|
input = [
|
@@ -161,16 +191,14 @@ RSpec.describe HTTP2::Connection do
|
|
161
191
|
end
|
162
192
|
|
163
193
|
it 'should not split zero-concatenated header field values' do
|
164
|
-
input = [
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
['cookie', "a=b\0c=d; e=f"],
|
173
|
-
]
|
194
|
+
input = [*RESPONSE_HEADERS,
|
195
|
+
['cache-control', "max-age=60, private\0must-revalidate"],
|
196
|
+
['content-type', 'text/html'],
|
197
|
+
['cookie', "a=b\0c=d; e=f"]]
|
198
|
+
expected = [*RESPONSE_HEADERS,
|
199
|
+
['cache-control', "max-age=60, private\0must-revalidate"],
|
200
|
+
['content-type', 'text/html'],
|
201
|
+
['cookie', "a=b\0c=d; e=f"]]
|
174
202
|
|
175
203
|
result = nil
|
176
204
|
@conn.on(:stream) do |stream|
|
@@ -192,13 +220,13 @@ RSpec.describe HTTP2::Connection do
|
|
192
220
|
end
|
193
221
|
|
194
222
|
it 'should update connection and stream windows on SETTINGS' do
|
195
|
-
settings, data =
|
223
|
+
settings, data = settings_frame, data_frame
|
196
224
|
settings[:payload] = [[:settings_initial_window_size, 1024]]
|
197
225
|
data[:payload] = 'x' * 2048
|
198
226
|
|
199
227
|
stream = @conn.new_stream
|
200
228
|
|
201
|
-
stream.send
|
229
|
+
stream.send headers_frame
|
202
230
|
stream.send data
|
203
231
|
expect(stream.remote_window).to eq(DEFAULT_FLOW_WINDOW - 2048)
|
204
232
|
expect(@conn.remote_window).to eq(DEFAULT_FLOW_WINDOW - 2048)
|
@@ -209,7 +237,7 @@ RSpec.describe HTTP2::Connection do
|
|
209
237
|
end
|
210
238
|
|
211
239
|
it 'should initialize streams with window specified by peer' do
|
212
|
-
settings =
|
240
|
+
settings = settings_frame
|
213
241
|
settings[:payload] = [[:settings_initial_window_size, 1024]]
|
214
242
|
|
215
243
|
@conn << f.generate(settings)
|
@@ -217,35 +245,57 @@ RSpec.describe HTTP2::Connection do
|
|
217
245
|
end
|
218
246
|
|
219
247
|
it 'should observe connection flow control' do
|
220
|
-
settings, data =
|
248
|
+
settings, data = settings_frame, data_frame
|
221
249
|
settings[:payload] = [[:settings_initial_window_size, 1000]]
|
222
250
|
|
223
251
|
@conn << f.generate(settings)
|
224
252
|
s1 = @conn.new_stream
|
225
253
|
s2 = @conn.new_stream
|
226
254
|
|
227
|
-
s1.send
|
255
|
+
s1.send headers_frame
|
228
256
|
s1.send data.merge(payload: 'x' * 900)
|
229
257
|
expect(@conn.remote_window).to eq 100
|
230
258
|
|
231
|
-
s2.send
|
259
|
+
s2.send headers_frame
|
232
260
|
s2.send data.merge(payload: 'x' * 200)
|
233
261
|
expect(@conn.remote_window).to eq 0
|
234
262
|
expect(@conn.buffered_amount).to eq 100
|
235
263
|
|
236
|
-
@conn << f.generate(
|
264
|
+
@conn << f.generate(window_update_frame.merge(stream: 0, increment: 1000))
|
237
265
|
expect(@conn.buffered_amount).to eq 0
|
238
266
|
expect(@conn.remote_window).to eq 900
|
239
267
|
end
|
268
|
+
|
269
|
+
it 'should update window when data received is over half of the maximum local window size' do
|
270
|
+
settings, data = settings_frame, data_frame
|
271
|
+
conn = Client.new(settings_initial_window_size: 500)
|
272
|
+
|
273
|
+
conn.receive f.generate(settings)
|
274
|
+
s1 = conn.new_stream
|
275
|
+
s2 = conn.new_stream
|
276
|
+
|
277
|
+
s1.send headers_frame
|
278
|
+
s2.send headers_frame
|
279
|
+
expect(conn).to receive(:send) do |frame|
|
280
|
+
expect(frame[:type]).to eq :window_update
|
281
|
+
expect(frame[:stream]).to eq 0
|
282
|
+
expect(frame[:increment]).to eq 400
|
283
|
+
end
|
284
|
+
conn.receive f.generate(data.merge(payload: 'x' * 200, end_stream: false, stream: s1.id))
|
285
|
+
conn.receive f.generate(data.merge(payload: 'x' * 200, end_stream: false, stream: s2.id))
|
286
|
+
expect(s1.local_window).to eq 300
|
287
|
+
expect(s2.local_window).to eq 300
|
288
|
+
expect(conn.local_window).to eq 500
|
289
|
+
end
|
240
290
|
end
|
241
291
|
|
242
292
|
context 'framing' do
|
243
293
|
it 'should buffer incomplete frames' do
|
244
|
-
settings =
|
294
|
+
settings = settings_frame
|
245
295
|
settings[:payload] = [[:settings_initial_window_size, 1000]]
|
246
296
|
@conn << f.generate(settings)
|
247
297
|
|
248
|
-
frame = f.generate(
|
298
|
+
frame = f.generate(window_update_frame.merge(stream: 0, increment: 1000))
|
249
299
|
@conn << frame
|
250
300
|
expect(@conn.remote_window).to eq 2000
|
251
301
|
|
@@ -261,10 +311,10 @@ RSpec.describe HTTP2::Connection do
|
|
261
311
|
]
|
262
312
|
|
263
313
|
cc = Compressor.new
|
264
|
-
headers =
|
314
|
+
headers = headers_frame
|
265
315
|
headers[:payload] = cc.encode(req_headers)
|
266
316
|
|
267
|
-
@conn << f.generate(
|
317
|
+
@conn << f.generate(settings_frame)
|
268
318
|
@conn.on(:stream) do |stream|
|
269
319
|
expect(stream).to receive(:<<) do |frame|
|
270
320
|
expect(frame[:payload]).to eq req_headers
|
@@ -281,7 +331,7 @@ RSpec.describe HTTP2::Connection do
|
|
281
331
|
]
|
282
332
|
|
283
333
|
cc = Compressor.new
|
284
|
-
h1, h2 =
|
334
|
+
h1, h2 = headers_frame, continuation_frame
|
285
335
|
|
286
336
|
# Header block fragment might not complete for decompression
|
287
337
|
payload = cc.encode(req_headers)
|
@@ -292,7 +342,7 @@ RSpec.describe HTTP2::Connection do
|
|
292
342
|
h2[:payload] = payload # the remaining
|
293
343
|
h2[:stream] = 5
|
294
344
|
|
295
|
-
@conn << f.generate(
|
345
|
+
@conn << f.generate(settings_frame)
|
296
346
|
@conn.on(:stream) do |stream|
|
297
347
|
expect(stream).to receive(:<<) do |frame|
|
298
348
|
expect(frame[:payload]).to eq req_headers
|
@@ -304,18 +354,18 @@ RSpec.describe HTTP2::Connection do
|
|
304
354
|
end
|
305
355
|
|
306
356
|
it 'should require that split header blocks are a contiguous sequence' do
|
307
|
-
headers =
|
357
|
+
headers = headers_frame
|
308
358
|
headers[:flags] = []
|
309
359
|
|
310
|
-
@conn << f.generate(
|
360
|
+
@conn << f.generate(settings_frame)
|
311
361
|
@conn << f.generate(headers)
|
312
|
-
(
|
362
|
+
(frame_types - [continuation_frame]).each do |frame|
|
313
363
|
expect { @conn << f.generate(frame.deep_dup) }.to raise_error(ProtocolError)
|
314
364
|
end
|
315
365
|
end
|
316
366
|
|
317
367
|
it 'should raise compression error on encode of invalid frame' do
|
318
|
-
@conn << f.generate(
|
368
|
+
@conn << f.generate(settings_frame)
|
319
369
|
stream = @conn.new_stream
|
320
370
|
|
321
371
|
expect do
|
@@ -324,8 +374,8 @@ RSpec.describe HTTP2::Connection do
|
|
324
374
|
end
|
325
375
|
|
326
376
|
it 'should raise connection error on decode of invalid frame' do
|
327
|
-
@conn << f.generate(
|
328
|
-
frame = f.generate(
|
377
|
+
@conn << f.generate(settings_frame)
|
378
|
+
frame = f.generate(data_frame) # Receiving DATA on unopened stream 1 is an error.
|
329
379
|
# Connection errors emit protocol error frames
|
330
380
|
expect { @conn << frame }.to raise_error(ProtocolError)
|
331
381
|
end
|
@@ -336,7 +386,7 @@ RSpec.describe HTTP2::Connection do
|
|
336
386
|
@conn.settings(settings_max_concurrent_streams: 10,
|
337
387
|
settings_initial_window_size: 0x7fffffff)
|
338
388
|
|
339
|
-
expect(bytes).to eq f.generate(
|
389
|
+
expect(bytes).to eq f.generate(settings_frame)
|
340
390
|
end
|
341
391
|
|
342
392
|
it 'should compress stream headers' do
|
@@ -448,79 +498,106 @@ RSpec.describe HTTP2::Connection do
|
|
448
498
|
context 'connection management' do
|
449
499
|
it 'should raise error on invalid connection header' do
|
450
500
|
srv = Server.new
|
451
|
-
expect { srv << f.generate(
|
501
|
+
expect { srv << f.generate(settings_frame) }.to raise_error(HandshakeError)
|
452
502
|
|
453
503
|
srv = Server.new
|
454
504
|
expect do
|
455
505
|
srv << CONNECTION_PREFACE_MAGIC
|
456
|
-
srv << f.generate(
|
506
|
+
srv << f.generate(settings_frame)
|
457
507
|
end.to_not raise_error
|
458
508
|
end
|
459
509
|
|
460
510
|
it 'should respond to PING frames' do
|
461
|
-
@conn << f.generate(
|
511
|
+
@conn << f.generate(settings_frame)
|
462
512
|
expect(@conn).to receive(:send) do |frame|
|
463
513
|
expect(frame[:type]).to eq :ping
|
464
514
|
expect(frame[:flags]).to eq [:ack]
|
465
515
|
expect(frame[:payload]).to eq '12345678'
|
466
516
|
end
|
467
517
|
|
468
|
-
@conn << f.generate(
|
518
|
+
@conn << f.generate(ping_frame)
|
469
519
|
end
|
470
520
|
|
471
521
|
it 'should fire callback on PONG' do
|
472
|
-
@conn << f.generate(
|
522
|
+
@conn << f.generate(settings_frame)
|
473
523
|
|
474
524
|
pong = nil
|
475
525
|
@conn.ping('12345678') { |d| pong = d }
|
476
|
-
@conn << f.generate(
|
526
|
+
@conn << f.generate(pong_frame)
|
477
527
|
expect(pong).to eq '12345678'
|
478
528
|
end
|
479
529
|
|
480
530
|
it 'should fire callback on receipt of GOAWAY' do
|
481
531
|
last_stream, payload, error = nil
|
482
|
-
@conn << f.generate(
|
532
|
+
@conn << f.generate(settings_frame)
|
483
533
|
@conn.on(:goaway) do |s, e, p|
|
484
534
|
last_stream = s
|
485
535
|
error = e
|
486
536
|
payload = p
|
487
537
|
end
|
488
|
-
@conn << f.generate(
|
538
|
+
@conn << f.generate(goaway_frame.merge(last_stream: 17, payload: 'test'))
|
489
539
|
|
490
540
|
expect(last_stream).to eq 17
|
491
541
|
expect(error).to eq :no_error
|
492
542
|
expect(payload).to eq 'test'
|
543
|
+
|
544
|
+
expect(@conn).to be_closed
|
493
545
|
end
|
494
546
|
|
495
547
|
it 'should raise error when opening new stream after sending GOAWAY' do
|
496
548
|
@conn.goaway
|
549
|
+
expect(@conn).to be_closed
|
550
|
+
|
497
551
|
expect { @conn.new_stream }.to raise_error(ConnectionClosed)
|
498
552
|
end
|
499
553
|
|
500
554
|
it 'should raise error when opening new stream after receiving GOAWAY' do
|
501
|
-
@conn << f.generate(
|
502
|
-
@conn << f.generate(
|
555
|
+
@conn << f.generate(settings_frame)
|
556
|
+
@conn << f.generate(goaway_frame)
|
503
557
|
expect { @conn.new_stream }.to raise_error(ConnectionClosed)
|
504
558
|
end
|
505
559
|
|
560
|
+
it 'should not raise error when receiving connection management frames immediately after emitting goaway' do
|
561
|
+
@conn.goaway
|
562
|
+
expect(@conn).to be_closed
|
563
|
+
|
564
|
+
expect { @conn << f.generate(settings_frame) }.not_to raise_error(ProtocolError)
|
565
|
+
expect { @conn << f.generate(ping_frame) }.not_to raise_error(ProtocolError)
|
566
|
+
expect { @conn << f.generate(goaway_frame) }.not_to raise_error(ProtocolError)
|
567
|
+
end
|
568
|
+
|
506
569
|
it 'should process connection management frames after GOAWAY' do
|
507
|
-
@conn << f.generate(
|
508
|
-
@conn << f.generate(
|
509
|
-
@conn << f.generate(
|
510
|
-
@conn << f.generate(
|
511
|
-
@conn << f.generate(
|
570
|
+
@conn << f.generate(settings_frame)
|
571
|
+
@conn << f.generate(headers_frame)
|
572
|
+
@conn << f.generate(goaway_frame)
|
573
|
+
@conn << f.generate(headers_frame.merge(stream: 7))
|
574
|
+
@conn << f.generate(push_promise_frame)
|
512
575
|
|
513
576
|
expect(@conn.active_stream_count).to eq 1
|
514
577
|
end
|
515
578
|
|
516
579
|
it 'should raise error on frame for invalid stream ID' do
|
517
|
-
@conn << f.generate(
|
580
|
+
@conn << f.generate(settings_frame)
|
518
581
|
|
519
582
|
expect do
|
520
|
-
@conn << f.generate(
|
583
|
+
@conn << f.generate(data_frame.merge(stream: 31))
|
521
584
|
end.to raise_error(ProtocolError)
|
522
585
|
end
|
523
586
|
|
587
|
+
it 'should not raise an error on frame for a closed stream ID' do
|
588
|
+
srv = Server.new
|
589
|
+
srv << CONNECTION_PREFACE_MAGIC
|
590
|
+
|
591
|
+
stream = srv.new_stream
|
592
|
+
stream.send headers_frame
|
593
|
+
stream.send data_frame
|
594
|
+
stream.close
|
595
|
+
|
596
|
+
expect do
|
597
|
+
srv << f.generate(rst_stream_frame.merge(stream: stream.id))
|
598
|
+
end.to_not raise_error
|
599
|
+
end
|
600
|
+
|
524
601
|
it 'should send GOAWAY frame on connection error' do
|
525
602
|
stream = @conn.new_stream
|
526
603
|
|
@@ -535,7 +612,7 @@ RSpec.describe HTTP2::Connection do
|
|
535
612
|
[frame]
|
536
613
|
end
|
537
614
|
|
538
|
-
expect { @conn << f.generate(
|
615
|
+
expect { @conn << f.generate(data_frame) }.to raise_error(ProtocolError)
|
539
616
|
end
|
540
617
|
end
|
541
618
|
|
@@ -564,8 +641,8 @@ RSpec.describe HTTP2::Connection do
|
|
564
641
|
end
|
565
642
|
|
566
643
|
it '.goaway should generate GOAWAY frame with last processed stream ID' do
|
567
|
-
@conn << f.generate(
|
568
|
-
@conn << f.generate(
|
644
|
+
@conn << f.generate(settings_frame)
|
645
|
+
@conn << f.generate(headers_frame.merge(stream: 17))
|
569
646
|
|
570
647
|
expect(@conn).to receive(:send) do |frame|
|
571
648
|
expect(frame[:type]).to eq :goaway
|