http-2 0.6.3 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.coveralls.yml +1 -0
- data/.gitmodules +3 -0
- data/Gemfile +5 -0
- data/README.md +5 -4
- data/Rakefile +1 -0
- data/example/client.rb +20 -5
- data/example/helper.rb +1 -1
- data/example/keys/mycert.pem +21 -22
- data/example/keys/mykey.pem +25 -25
- data/example/server.rb +10 -3
- data/http-2.gemspec +1 -1
- data/lib/http/2.rb +2 -0
- data/lib/http/2/client.rb +16 -10
- data/lib/http/2/compressor.rb +346 -286
- data/lib/http/2/connection.rb +254 -95
- data/lib/http/2/error.rb +0 -6
- data/lib/http/2/flow_buffer.rb +12 -10
- data/lib/http/2/framer.rb +203 -57
- data/lib/http/2/huffman.rb +332 -0
- data/lib/http/2/huffman_statemachine.rb +272 -0
- data/lib/http/2/server.rb +5 -4
- data/lib/http/2/stream.rb +72 -35
- data/lib/http/2/version.rb +1 -1
- data/lib/tasks/generate_huffman_table.rb +160 -0
- data/spec/client_spec.rb +3 -3
- data/spec/compressor_spec.rb +422 -281
- data/spec/connection_spec.rb +236 -56
- data/spec/framer_spec.rb +213 -45
- data/spec/helper.rb +42 -15
- data/spec/hpack_test_spec.rb +83 -0
- data/spec/huffman_spec.rb +68 -0
- data/spec/server_spec.rb +7 -6
- data/spec/stream_spec.rb +81 -54
- metadata +21 -11
data/spec/connection_spec.rb
CHANGED
@@ -24,14 +24,50 @@ describe HTTP2::Connection do
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
+
context "settings synchronization" do
|
28
|
+
it "should reflect outgoing settings when ack is received" do
|
29
|
+
@conn.local_settings[:settings_header_table_size].should eq 4096
|
30
|
+
@conn.settings(settings_header_table_size: 256)
|
31
|
+
@conn.local_settings[:settings_header_table_size].should eq 4096
|
32
|
+
|
33
|
+
ack = { type: :settings, stream: 0, payload: [], flags: [:ack] }
|
34
|
+
@conn << f.generate(ack)
|
35
|
+
|
36
|
+
@conn.local_settings[:settings_header_table_size].should eq 256
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should reflect incoming settings when SETTINGS is received" do
|
40
|
+
@conn.remote_settings[:settings_header_table_size].should eq 4096
|
41
|
+
settings = SETTINGS.dup
|
42
|
+
settings[:payload] = [[:settings_header_table_size, 256]]
|
43
|
+
|
44
|
+
@conn << f.generate(settings)
|
45
|
+
|
46
|
+
@conn.remote_settings[:settings_header_table_size].should 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
|
+
@conn.should_receive(:send) do |frame|
|
54
|
+
frame[:type].should eq :settings
|
55
|
+
frame[:flags].should eq [:ack]
|
56
|
+
frame[:payload].should eq []
|
57
|
+
end
|
58
|
+
|
59
|
+
@conn << f.generate(settings)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
27
63
|
context "stream management" do
|
28
64
|
it "should initialize to default stream limit (100)" do
|
29
|
-
@conn.
|
65
|
+
@conn.local_settings[:settings_max_concurrent_streams].should eq 100
|
30
66
|
end
|
31
67
|
|
32
68
|
it "should change stream limit to received SETTINGS value" do
|
33
69
|
@conn << f.generate(SETTINGS)
|
34
|
-
@conn.
|
70
|
+
@conn.remote_settings[:settings_max_concurrent_streams].should eq 10
|
35
71
|
end
|
36
72
|
|
37
73
|
it "should count open streams against stream limit" do
|
@@ -78,62 +114,104 @@ describe HTTP2::Connection do
|
|
78
114
|
@conn << f.generate(SETTINGS)
|
79
115
|
|
80
116
|
stream, headers = nil, HEADERS.dup
|
81
|
-
headers[:
|
117
|
+
headers[:weight] = 20
|
118
|
+
headers[:stream_dependency] = 0
|
119
|
+
headers[:exclusive] = false
|
82
120
|
|
83
121
|
@conn.on(:stream) {|s| stream = s }
|
84
122
|
@conn << f.generate(headers)
|
85
123
|
|
86
|
-
stream.
|
124
|
+
stream.weight.should eq 20
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
context "Headers pre/post processing" do
|
129
|
+
it "should not concatenate multiple occurences of a header field with the same name" do
|
130
|
+
input = [
|
131
|
+
["Content-Type", "text/html"],
|
132
|
+
["Cache-Control", "max-age=60, private"],
|
133
|
+
["Cache-Control", "must-revalidate"],
|
134
|
+
]
|
135
|
+
expected = [
|
136
|
+
["content-type", "text/html"],
|
137
|
+
["cache-control", "max-age=60, private"],
|
138
|
+
["cache-control", "must-revalidate"],
|
139
|
+
]
|
140
|
+
headers = []
|
141
|
+
@conn.on(:frame) do |bytes|
|
142
|
+
bytes.force_encoding('binary')
|
143
|
+
# bytes[3]: frame's type field
|
144
|
+
[1,5,9].include?(bytes[3].ord) and headers << f.parse(bytes)
|
145
|
+
end
|
146
|
+
|
147
|
+
stream = @conn.new_stream
|
148
|
+
stream.headers(input)
|
149
|
+
|
150
|
+
headers.size.should eq 1
|
151
|
+
emitted = Decompressor.new.decode(headers.first[:payload])
|
152
|
+
emitted.should match_array(expected)
|
153
|
+
end
|
154
|
+
|
155
|
+
it "should not split zero-concatenated header field values" do
|
156
|
+
input = [
|
157
|
+
["cache-control", "max-age=60, private\0must-revalidate"],
|
158
|
+
["content-type", "text/html"],
|
159
|
+
["cookie", "a=b\0c=d; e=f"],
|
160
|
+
]
|
161
|
+
expected = [
|
162
|
+
["cache-control", "max-age=60, private\0must-revalidate"],
|
163
|
+
["content-type", "text/html"],
|
164
|
+
["cookie", "a=b\0c=d; e=f"],
|
165
|
+
]
|
166
|
+
|
167
|
+
result = nil
|
168
|
+
@conn.on(:stream) do |stream|
|
169
|
+
stream.on(:headers) {|h| result = h}
|
170
|
+
end
|
171
|
+
|
172
|
+
srv = Server.new
|
173
|
+
srv.on(:frame) {|bytes| @conn << bytes}
|
174
|
+
stream = srv.new_stream
|
175
|
+
stream.headers(input)
|
176
|
+
|
177
|
+
result.should eq expected
|
178
|
+
|
87
179
|
end
|
88
180
|
end
|
89
181
|
|
90
182
|
context "flow control" do
|
91
183
|
it "should initialize to default flow window" do
|
92
|
-
@conn.
|
184
|
+
@conn.remote_window.should eq DEFAULT_FLOW_WINDOW
|
93
185
|
end
|
94
186
|
|
95
187
|
it "should update connection and stream windows on SETTINGS" do
|
96
188
|
settings, data = SETTINGS.dup, DATA.dup
|
97
|
-
settings[:payload] =
|
189
|
+
settings[:payload] = [[:settings_initial_window_size, 1024]]
|
98
190
|
data[:payload] = 'x'*2048
|
99
191
|
|
100
192
|
stream = @conn.new_stream
|
101
193
|
|
102
194
|
stream.send HEADERS
|
103
195
|
stream.send data
|
104
|
-
stream.
|
105
|
-
@conn.
|
196
|
+
stream.remote_window.should eq (DEFAULT_FLOW_WINDOW - 2048)
|
197
|
+
@conn.remote_window.should eq (DEFAULT_FLOW_WINDOW - 2048)
|
106
198
|
|
107
199
|
@conn << f.generate(settings)
|
108
|
-
@conn.
|
109
|
-
stream.
|
200
|
+
@conn.remote_window.should eq -1024
|
201
|
+
stream.remote_window.should eq -1024
|
110
202
|
end
|
111
203
|
|
112
204
|
it "should initialize streams with window specified by peer" do
|
113
205
|
settings = SETTINGS.dup
|
114
|
-
settings[:payload] =
|
206
|
+
settings[:payload] = [[:settings_initial_window_size, 1024]]
|
115
207
|
|
116
208
|
@conn << f.generate(settings)
|
117
|
-
@conn.new_stream.
|
118
|
-
end
|
119
|
-
|
120
|
-
it "should support global disable of flow control" do
|
121
|
-
@conn << f.generate(SETTINGS)
|
122
|
-
@conn.window.should eq Float::INFINITY
|
123
|
-
end
|
124
|
-
|
125
|
-
it "should raise error on flow control after disabling it" do
|
126
|
-
expect { @conn << f.generate(SETTINGS) }.to_not raise_error
|
127
|
-
expect {
|
128
|
-
[WINDOW_UPDATE, SETTINGS].each do |frame|
|
129
|
-
@conn.dup << f.generate(frame)
|
130
|
-
end
|
131
|
-
}.to raise_error(FlowControlError)
|
209
|
+
@conn.new_stream.remote_window.should eq 1024
|
132
210
|
end
|
133
211
|
|
134
212
|
it "should observe connection flow control" do
|
135
213
|
settings, data = SETTINGS.dup, DATA.dup
|
136
|
-
settings[:payload] =
|
214
|
+
settings[:payload] = [[:settings_initial_window_size, 1000]]
|
137
215
|
|
138
216
|
@conn << f.generate(settings)
|
139
217
|
s1 = @conn.new_stream
|
@@ -141,32 +219,32 @@ describe HTTP2::Connection do
|
|
141
219
|
|
142
220
|
s1.send HEADERS
|
143
221
|
s1.send data.merge({payload: "x" * 900})
|
144
|
-
@conn.
|
222
|
+
@conn.remote_window.should eq 100
|
145
223
|
|
146
224
|
s2.send HEADERS
|
147
225
|
s2.send data.merge({payload: "x" * 200})
|
148
|
-
@conn.
|
226
|
+
@conn.remote_window.should eq 0
|
149
227
|
@conn.buffered_amount.should eq 100
|
150
228
|
|
151
229
|
@conn << f.generate(WINDOW_UPDATE.merge({stream: 0, increment: 1000}))
|
152
230
|
@conn.buffered_amount.should eq 0
|
153
|
-
@conn.
|
231
|
+
@conn.remote_window.should eq 900
|
154
232
|
end
|
155
233
|
end
|
156
234
|
|
157
235
|
context "framing" do
|
158
236
|
it "should buffer incomplete frames" do
|
159
237
|
settings = SETTINGS.dup
|
160
|
-
settings[:payload] =
|
238
|
+
settings[:payload] = [[:settings_initial_window_size, 1000]]
|
161
239
|
@conn << f.generate(settings)
|
162
240
|
|
163
241
|
frame = f.generate(WINDOW_UPDATE.merge({stream: 0, increment: 1000}))
|
164
242
|
@conn << frame
|
165
|
-
@conn.
|
243
|
+
@conn.remote_window.should eq 2000
|
166
244
|
|
167
245
|
@conn << frame.slice!(0,1)
|
168
246
|
@conn << frame
|
169
|
-
@conn.
|
247
|
+
@conn.remote_window.should eq 3000
|
170
248
|
end
|
171
249
|
|
172
250
|
it "should decompress header blocks regardless of stream state" do
|
@@ -175,7 +253,7 @@ describe HTTP2::Connection do
|
|
175
253
|
["x-my-header", "first"]
|
176
254
|
]
|
177
255
|
|
178
|
-
cc = Compressor.new
|
256
|
+
cc = Compressor.new
|
179
257
|
headers = HEADERS.dup
|
180
258
|
headers[:payload] = cc.encode(req_headers)
|
181
259
|
|
@@ -195,13 +273,16 @@ describe HTTP2::Connection do
|
|
195
273
|
["x-my-header", "first"]
|
196
274
|
]
|
197
275
|
|
198
|
-
cc = Compressor.new
|
276
|
+
cc = Compressor.new
|
199
277
|
h1, h2 = HEADERS.dup, CONTINUATION.dup
|
200
|
-
|
278
|
+
|
279
|
+
# Header block fragment might not complete for decompression
|
280
|
+
payload = cc.encode(req_headers)
|
281
|
+
h1[:payload] = payload.slice!(0, payload.size/2) # first half
|
201
282
|
h1[:stream] = 5
|
202
283
|
h1[:flags] = []
|
203
284
|
|
204
|
-
h2[:payload] =
|
285
|
+
h2[:payload] = payload # the remaining
|
205
286
|
h2[:stream] = 5
|
206
287
|
|
207
288
|
@conn << f.generate(SETTINGS)
|
@@ -226,7 +307,7 @@ describe HTTP2::Connection do
|
|
226
307
|
end
|
227
308
|
end
|
228
309
|
|
229
|
-
it "should raise
|
310
|
+
it "should raise compression error on encode of invalid frame" do
|
230
311
|
@conn << f.generate(SETTINGS)
|
231
312
|
stream = @conn.new_stream
|
232
313
|
|
@@ -235,48 +316,143 @@ describe HTTP2::Connection do
|
|
235
316
|
}.to raise_error(CompressionError)
|
236
317
|
end
|
237
318
|
|
238
|
-
it "should raise connection error on decode
|
319
|
+
it "should raise connection error on decode of invalid frame" do
|
239
320
|
@conn << f.generate(SETTINGS)
|
240
|
-
frame = f.generate(
|
241
|
-
|
242
|
-
|
321
|
+
frame = f.generate(DATA.dup) # Receiving DATA on unopened stream 1 is an error.
|
322
|
+
# Connection errors emit protocol error frames
|
243
323
|
expect { @conn << frame }.to raise_error(ProtocolError)
|
244
324
|
end
|
245
325
|
|
246
326
|
it "should emit encoded frames via on(:frame)" do
|
247
327
|
bytes = nil
|
248
328
|
@conn.on(:frame) {|d| bytes = d }
|
249
|
-
@conn.settings(
|
329
|
+
@conn.settings(settings_max_concurrent_streams: 10,
|
330
|
+
settings_initial_window_size: 0x7fffffff)
|
250
331
|
|
251
332
|
bytes.should eq f.generate(SETTINGS)
|
252
333
|
end
|
253
334
|
|
254
335
|
it "should compress stream headers" do
|
255
|
-
@conn.ping("12345678")
|
256
336
|
@conn.on(:frame) do |bytes|
|
257
337
|
bytes.force_encoding('binary')
|
258
338
|
bytes.should_not match('get')
|
259
339
|
bytes.should_not match('http')
|
260
|
-
bytes.
|
340
|
+
bytes.should_not match('www.example.org') # should be huffman encoded
|
261
341
|
end
|
262
342
|
|
263
343
|
stream = @conn.new_stream
|
264
344
|
stream.headers({
|
265
345
|
':method' => 'get',
|
266
346
|
':scheme' => 'http',
|
267
|
-
':
|
347
|
+
':authority' => 'www.example.org',
|
268
348
|
':path' => '/resource'
|
269
349
|
})
|
270
350
|
end
|
351
|
+
|
352
|
+
it "should generate CONTINUATION if HEADERS is too long" do
|
353
|
+
headers = []
|
354
|
+
@conn.on(:frame) do |bytes|
|
355
|
+
bytes.force_encoding('binary')
|
356
|
+
# bytes[3]: frame's type field
|
357
|
+
[1,5,9].include?(bytes[3].ord) and headers << f.parse(bytes)
|
358
|
+
end
|
359
|
+
|
360
|
+
stream = @conn.new_stream
|
361
|
+
stream.headers({
|
362
|
+
':method' => 'get',
|
363
|
+
':scheme' => 'http',
|
364
|
+
':authority' => 'www.example.org',
|
365
|
+
':path' => '/resource',
|
366
|
+
'custom' => 'q' * 44000,
|
367
|
+
}, end_stream: true)
|
368
|
+
headers.size.should eq 3
|
369
|
+
headers[0][:type].should eq :headers
|
370
|
+
headers[1][:type].should eq :continuation
|
371
|
+
headers[2][:type].should eq :continuation
|
372
|
+
headers[0][:flags].should eq [:end_stream]
|
373
|
+
headers[1][:flags].should eq []
|
374
|
+
headers[2][:flags].should eq [:end_headers]
|
375
|
+
end
|
376
|
+
|
377
|
+
it "should not generate CONTINUATION if HEADERS fits exactly in a frame" do
|
378
|
+
headers = []
|
379
|
+
@conn.on(:frame) do |bytes|
|
380
|
+
bytes.force_encoding('binary')
|
381
|
+
# bytes[3]: frame's type field
|
382
|
+
[1,5,9].include?(bytes[3].ord) and headers << f.parse(bytes)
|
383
|
+
end
|
384
|
+
|
385
|
+
stream = @conn.new_stream
|
386
|
+
stream.headers({
|
387
|
+
':method' => 'get',
|
388
|
+
':scheme' => 'http',
|
389
|
+
':authority' => 'www.example.org',
|
390
|
+
':path' => '/resource',
|
391
|
+
'custom' => 'q' * 18682, # this number should be updated when Huffman table is changed
|
392
|
+
}, end_stream: true)
|
393
|
+
headers[0][:length].should eq @conn.remote_settings[:settings_max_frame_size]
|
394
|
+
headers.size.should eq 1
|
395
|
+
headers[0][:type].should eq :headers
|
396
|
+
headers[0][:flags].should include(:end_headers)
|
397
|
+
headers[0][:flags].should include(:end_stream)
|
398
|
+
end
|
399
|
+
|
400
|
+
it "should not generate CONTINUATION if HEADERS fits exactly in a frame" do
|
401
|
+
headers = []
|
402
|
+
@conn.on(:frame) do |bytes|
|
403
|
+
bytes.force_encoding('binary')
|
404
|
+
# bytes[3]: frame's type field
|
405
|
+
[1,5,9].include?(bytes[3].ord) and headers << f.parse(bytes)
|
406
|
+
end
|
407
|
+
|
408
|
+
stream = @conn.new_stream
|
409
|
+
stream.headers({
|
410
|
+
':method' => 'get',
|
411
|
+
':scheme' => 'http',
|
412
|
+
':authority' => 'www.example.org',
|
413
|
+
':path' => '/resource',
|
414
|
+
'custom' => 'q' * 18682, # this number should be updated when Huffman table is changed
|
415
|
+
}, end_stream: true)
|
416
|
+
headers[0][:length].should eq @conn.remote_settings[:settings_max_frame_size]
|
417
|
+
headers.size.should eq 1
|
418
|
+
headers[0][:type].should eq :headers
|
419
|
+
headers[0][:flags].should include(:end_headers)
|
420
|
+
headers[0][:flags].should include(:end_stream)
|
421
|
+
end
|
422
|
+
|
423
|
+
it "should generate CONTINUATION if HEADERS exceed the max payload by one byte" do
|
424
|
+
headers = []
|
425
|
+
@conn.on(:frame) do |bytes|
|
426
|
+
bytes.force_encoding('binary')
|
427
|
+
[1,5,9].include?(bytes[3].ord) and headers << f.parse(bytes)
|
428
|
+
end
|
429
|
+
|
430
|
+
stream = @conn.new_stream
|
431
|
+
stream.headers({
|
432
|
+
':method' => 'get',
|
433
|
+
':scheme' => 'http',
|
434
|
+
':authority' => 'www.example.org',
|
435
|
+
':path' => '/resource',
|
436
|
+
'custom' => 'q' * 18683, # this number should be updated when Huffman table is changed
|
437
|
+
}, end_stream: true)
|
438
|
+
headers[0][:length].should eq @conn.remote_settings[:settings_max_frame_size]
|
439
|
+
headers[1][:length].should eq 1
|
440
|
+
headers.size.should eq 2
|
441
|
+
headers[0][:type].should eq :headers
|
442
|
+
headers[1][:type].should eq :continuation
|
443
|
+
headers[0][:flags].should eq [:end_stream]
|
444
|
+
headers[1][:flags].should eq [:end_headers]
|
445
|
+
end
|
271
446
|
end
|
272
447
|
|
273
448
|
context "connection management" do
|
274
449
|
it "should raise error on invalid connection header" do
|
275
450
|
srv = Server.new
|
276
|
-
expect { srv
|
451
|
+
expect { srv << f.generate(SETTINGS) }.to raise_error(HandshakeError)
|
277
452
|
|
453
|
+
srv = Server.new
|
278
454
|
expect {
|
279
|
-
srv <<
|
455
|
+
srv << CONNECTION_PREFACE_MAGIC
|
280
456
|
srv << f.generate(SETTINGS)
|
281
457
|
}.to_not raise_error
|
282
458
|
end
|
@@ -285,7 +461,7 @@ describe HTTP2::Connection do
|
|
285
461
|
@conn << f.generate(SETTINGS)
|
286
462
|
@conn.should_receive(:send) do |frame|
|
287
463
|
frame[:type].should eq :ping
|
288
|
-
frame[:flags].should eq [:
|
464
|
+
frame[:flags].should eq [:ack]
|
289
465
|
frame[:payload].should eq "12345678"
|
290
466
|
end
|
291
467
|
|
@@ -344,14 +520,17 @@ describe HTTP2::Connection do
|
|
344
520
|
it "should send GOAWAY frame on connection error" do
|
345
521
|
stream = @conn.new_stream
|
346
522
|
|
347
|
-
@conn.
|
523
|
+
@conn.should_receive(:encode) do |frame|
|
524
|
+
frame[:type].should eq :settings
|
525
|
+
[frame]
|
526
|
+
end
|
348
527
|
@conn.should_receive(:encode) do |frame|
|
349
528
|
frame[:type].should eq :goaway
|
350
529
|
frame[:last_stream].should eq stream.id
|
351
530
|
frame[:error].should eq :protocol_error
|
531
|
+
[frame]
|
352
532
|
end
|
353
533
|
|
354
|
-
@conn << f.generate(SETTINGS)
|
355
534
|
expect { @conn << f.generate(DATA) }.to raise_error(ProtocolError)
|
356
535
|
end
|
357
536
|
end
|
@@ -360,14 +539,15 @@ describe HTTP2::Connection do
|
|
360
539
|
it ".settings should emit SETTINGS frames" do
|
361
540
|
@conn.should_receive(:send) do |frame|
|
362
541
|
frame[:type].should eq :settings
|
363
|
-
frame[:payload].should eq(
|
364
|
-
settings_max_concurrent_streams
|
365
|
-
|
366
|
-
|
542
|
+
frame[:payload].should eq([
|
543
|
+
[:settings_max_concurrent_streams, 10],
|
544
|
+
[:settings_initial_window_size, 0x7fffffff],
|
545
|
+
])
|
367
546
|
frame[:stream].should eq 0
|
368
547
|
end
|
369
548
|
|
370
|
-
@conn.settings(
|
549
|
+
@conn.settings(settings_max_concurrent_streams: 10,
|
550
|
+
settings_initial_window_size: 0x7fffffff)
|
371
551
|
end
|
372
552
|
|
373
553
|
it ".ping should generate PING frames" do
|