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