http-2 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.autotest +19 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/README.md +280 -0
- data/Rakefile +11 -0
- data/example/client.rb +46 -0
- data/example/helper.rb +14 -0
- data/example/server.rb +50 -0
- data/http-2.gemspec +24 -0
- data/lib/http/2/buffer.rb +21 -0
- data/lib/http/2/compressor.rb +493 -0
- data/lib/http/2/connection.rb +516 -0
- data/lib/http/2/emitter.rb +47 -0
- data/lib/http/2/error.rb +45 -0
- data/lib/http/2/flow_buffer.rb +64 -0
- data/lib/http/2/framer.rb +302 -0
- data/lib/http/2/stream.rb +474 -0
- data/lib/http/2/version.rb +3 -0
- data/lib/http/2.rb +9 -0
- data/spec/compressor_spec.rb +384 -0
- data/spec/connection_spec.rb +448 -0
- data/spec/emitter_spec.rb +46 -0
- data/spec/framer_spec.rb +325 -0
- data/spec/helper.rb +98 -0
- data/spec/stream_spec.rb +683 -0
- metadata +120 -0
@@ -0,0 +1,384 @@
|
|
1
|
+
require "helper"
|
2
|
+
|
3
|
+
describe HTTP2::Header do
|
4
|
+
|
5
|
+
let(:c) { Compressor.new :request }
|
6
|
+
let(:d) { Decompressor.new :response }
|
7
|
+
|
8
|
+
context "literal representation" do
|
9
|
+
context "integer" do
|
10
|
+
it "should encode 10 using a 5-bit prefix" do
|
11
|
+
buf = c.integer(10, 5)
|
12
|
+
buf.should eq [10].pack('C')
|
13
|
+
d.integer(StringIO.new(buf), 5).should eq 10
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should encode 10 using a 0-bit prefix" do
|
17
|
+
buf = c.integer(10, 0)
|
18
|
+
buf.should eq [10].pack('C')
|
19
|
+
d.integer(StringIO.new(buf), 0).should eq 10
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should encode 1337 using a 5-bit prefix" do
|
23
|
+
buf = c.integer(1337, 5)
|
24
|
+
buf.should eq [31,128+26,10].pack('C*')
|
25
|
+
d.integer(StringIO.new(buf), 5).should eq 1337
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should encode 1337 using a 0-bit prefix" do
|
29
|
+
buf = c.integer(1337,0)
|
30
|
+
buf.should eq [128+57,10].pack('C*')
|
31
|
+
d.integer(StringIO.new(buf), 0).should eq 1337
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "string" do
|
36
|
+
it "should handle ascii codepoints" do
|
37
|
+
ascii = "abcdefghij"
|
38
|
+
str = c.string(ascii)
|
39
|
+
|
40
|
+
buf = StringIO.new(str+"trailer")
|
41
|
+
d.string(buf).should eq ascii
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should handle utf-8 codepoints" do
|
45
|
+
utf8 = "éáűőúöüó€"
|
46
|
+
str = c.string(utf8)
|
47
|
+
|
48
|
+
buf = StringIO.new(str+"trailer")
|
49
|
+
d.string(buf).should eq utf8
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should handle long utf-8 strings" do
|
53
|
+
utf8 = "éáűőúöüó€"*100
|
54
|
+
str = c.string(utf8)
|
55
|
+
|
56
|
+
buf = StringIO.new(str+"trailer")
|
57
|
+
d.string(buf).should eq utf8
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context "header representation" do
|
63
|
+
it "should handle indexed representation" do
|
64
|
+
h = {name: 10, type: :indexed}
|
65
|
+
|
66
|
+
indexed = StringIO.new(c.header(h))
|
67
|
+
d.header(indexed).should eq h
|
68
|
+
end
|
69
|
+
|
70
|
+
context "literal w/o indexing representation" do
|
71
|
+
it "should handle indexed header" do
|
72
|
+
h = {name: 10, value: "my-value", type: :noindex}
|
73
|
+
|
74
|
+
literal = StringIO.new(c.header(h))
|
75
|
+
d.header(literal).should eq h
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should handle literal header" do
|
79
|
+
h = {name: "x-custom", value: "my-value", type: :noindex}
|
80
|
+
|
81
|
+
literal = StringIO.new(c.header(h))
|
82
|
+
d.header(literal).should eq h
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context "literal w/ incremental indexing" do
|
87
|
+
it "should handle indexed header" do
|
88
|
+
h = {name: 10, value: "my-value", type: :incremental}
|
89
|
+
|
90
|
+
literal = StringIO.new(c.header(h))
|
91
|
+
d.header(literal).should eq h
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should handle literal header" do
|
95
|
+
h = {name: "x-custom", value: "my-value", type: :incremental}
|
96
|
+
|
97
|
+
literal = StringIO.new(c.header(h))
|
98
|
+
d.header(literal).should eq h
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context "literal w/ substitution indexing" do
|
103
|
+
it "should handle indexed header" do
|
104
|
+
h = {name: 1, value: "my-value", index: 10, type: :substitution}
|
105
|
+
|
106
|
+
literal = StringIO.new(c.header(h))
|
107
|
+
d.header(literal).should eq h
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should handle literal header" do
|
111
|
+
h = {name: "x-new", value: "my-value", index: 10, type: :substitution}
|
112
|
+
|
113
|
+
literal = StringIO.new(c.header(h))
|
114
|
+
d.header(literal).should eq h
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context "differential coding" do
|
120
|
+
context "shared compression context" do
|
121
|
+
before(:each) { @cc = CompressionContext.new(:request) }
|
122
|
+
|
123
|
+
it "should be initialized with pre-defined headers" do
|
124
|
+
cc = CompressionContext.new(:request)
|
125
|
+
cc.table.size.should eq 38
|
126
|
+
|
127
|
+
cc = CompressionContext.new(:response)
|
128
|
+
cc.table.size.should eq 35
|
129
|
+
end
|
130
|
+
|
131
|
+
it "should be initialized with empty working set" do
|
132
|
+
@cc.workset.should be_empty
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should update working set based on prior state" do
|
136
|
+
@cc.update_sets
|
137
|
+
@cc.workset.should be_empty
|
138
|
+
|
139
|
+
@cc.process({name: 0, type: :indexed})
|
140
|
+
@cc.update_sets
|
141
|
+
@cc.workset.should eq [[0, [":scheme", "http"]]]
|
142
|
+
|
143
|
+
@cc.process({name: 0, type: :indexed})
|
144
|
+
@cc.update_sets
|
145
|
+
@cc.workset.should be_empty
|
146
|
+
end
|
147
|
+
|
148
|
+
context "processing" do
|
149
|
+
it "should toggle index representation headers in working set" do
|
150
|
+
@cc.process({name: 0, type: :indexed})
|
151
|
+
@cc.workset.first.should eq [0, [":scheme", "http"]]
|
152
|
+
|
153
|
+
@cc.process({name: 0, type: :indexed})
|
154
|
+
@cc.workset.should be_empty
|
155
|
+
end
|
156
|
+
|
157
|
+
context "no indexing" do
|
158
|
+
it "should process indexed header with literal value" do
|
159
|
+
original_table = @cc.table
|
160
|
+
|
161
|
+
@cc.process({name: 3, value: "/path", type: :noindex})
|
162
|
+
@cc.workset.first.should eq [3, [":path", "/path"]]
|
163
|
+
@cc.table.should eq original_table
|
164
|
+
end
|
165
|
+
|
166
|
+
it "should process indexed header with default value" do
|
167
|
+
original_table = @cc.table
|
168
|
+
|
169
|
+
@cc.process({name: 3, type: :noindex})
|
170
|
+
@cc.workset.first.should eq [3, [":path", "/"]]
|
171
|
+
@cc.table.should eq original_table
|
172
|
+
end
|
173
|
+
|
174
|
+
it "should process literal header with literal value" do
|
175
|
+
original_table = @cc.table
|
176
|
+
|
177
|
+
@cc.process({name: "x-custom", value: "random", type: :noindex})
|
178
|
+
@cc.workset.first.should eq [nil, ["x-custom", "random"]]
|
179
|
+
@cc.table.should eq original_table
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
context "incremental indexing" do
|
184
|
+
it "should process literal header with literal value" do
|
185
|
+
original_table = @cc.table.dup
|
186
|
+
|
187
|
+
@cc.process({name: "x-custom", value: "random", type: :incremental})
|
188
|
+
@cc.workset.first.should eq [original_table.size, ["x-custom", "random"]]
|
189
|
+
(@cc.table - original_table).should eq [["x-custom", "random"]]
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
context "substitution indexing" do
|
194
|
+
it "should process literal header with literal value" do
|
195
|
+
original_table = @cc.table.dup
|
196
|
+
idx = original_table.size-1
|
197
|
+
|
198
|
+
@cc.process({
|
199
|
+
name: "x-custom", value: "random",
|
200
|
+
index: idx, type: :substitution
|
201
|
+
})
|
202
|
+
|
203
|
+
@cc.workset.first.should eq [idx, ["x-custom", "random"]]
|
204
|
+
(@cc.table - original_table).should eq [["x-custom", "random"]]
|
205
|
+
(original_table - @cc.table).should eq [["warning", ""]]
|
206
|
+
end
|
207
|
+
|
208
|
+
it "should raise error on invalid substitution index" do
|
209
|
+
lambda {
|
210
|
+
@cc.process({
|
211
|
+
name: "x-custom", value: "random",
|
212
|
+
index: 1000, type: :substitution
|
213
|
+
})
|
214
|
+
}.should raise_error(HeaderException)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
context "size bounds" do
|
219
|
+
it "should drop headers from beginning of table" do
|
220
|
+
cc = CompressionContext.new(:request, 2048)
|
221
|
+
original_table = cc.table.dup
|
222
|
+
original_size = original_table.join.bytesize +
|
223
|
+
original_table.size * 32
|
224
|
+
|
225
|
+
cc.process({
|
226
|
+
name: "x-custom",
|
227
|
+
value: "a" * (2048 - original_size),
|
228
|
+
type: :incremental
|
229
|
+
})
|
230
|
+
|
231
|
+
cc.table.last[0].should eq "x-custom"
|
232
|
+
cc.table.size.should eq original_table.size
|
233
|
+
end
|
234
|
+
|
235
|
+
it "should prepend on dropped substitution index" do
|
236
|
+
cc = CompressionContext.new(:request, 2048)
|
237
|
+
original_table = cc.table.dup
|
238
|
+
original_size = original_table.join.bytesize +
|
239
|
+
original_table.size * 32
|
240
|
+
|
241
|
+
cc.process({
|
242
|
+
name: "x-custom",
|
243
|
+
value: "a" * (2048 - original_size),
|
244
|
+
index: 0, type: :substitution
|
245
|
+
})
|
246
|
+
|
247
|
+
cc.table[0][0].should eq "x-custom"
|
248
|
+
cc.table[1][0].should eq ":scheme"
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
context "integration" do
|
255
|
+
before (:all) { @cc = CompressionContext.new(:request) }
|
256
|
+
|
257
|
+
it "should match first header set in spec appendix" do
|
258
|
+
req_headers = [
|
259
|
+
{name: 3, value: "/my-example/index.html"},
|
260
|
+
{name: 12, value: "my-user-agent"},
|
261
|
+
{name: "x-my-header", value: "first"}
|
262
|
+
]
|
263
|
+
|
264
|
+
req_headers.each {|h| @cc.process(h.merge({type: :incremental})) }
|
265
|
+
|
266
|
+
@cc.table[38].should eq [":path", "/my-example/index.html"]
|
267
|
+
@cc.table[39].should eq ["user-agent", "my-user-agent"]
|
268
|
+
@cc.table[40].should eq req_headers[2].values
|
269
|
+
end
|
270
|
+
|
271
|
+
it "should match second header set in spec appendix" do
|
272
|
+
@cc.process({name: 38, type: :indexed})
|
273
|
+
@cc.process({name: 39, type: :indexed})
|
274
|
+
@cc.process({
|
275
|
+
name: 3, value: "/my-example/resources/script.js",
|
276
|
+
index: 38, type: :substitution
|
277
|
+
})
|
278
|
+
@cc.process({name: 40, value: "second", type: :incremental})
|
279
|
+
|
280
|
+
@cc.table[38].should eq [":path", "/my-example/resources/script.js"]
|
281
|
+
@cc.table[39].should eq ["user-agent", "my-user-agent"]
|
282
|
+
@cc.table[40].should eq ["x-my-header", "first"]
|
283
|
+
@cc.table[41].should eq ["x-my-header", "second"]
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
context "encode and decode" do
|
289
|
+
# http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-01#appendix-B
|
290
|
+
|
291
|
+
before (:all) do
|
292
|
+
@cc = Compressor.new(:request)
|
293
|
+
@dc = Decompressor.new(:request)
|
294
|
+
end
|
295
|
+
|
296
|
+
E1_BYTES = [
|
297
|
+
0x44, # (literal header with incremental indexing, name index = 3)
|
298
|
+
0x16, # (header value string length = 22)
|
299
|
+
"/my-example/index.html".bytes,
|
300
|
+
0x4D, # (literal header with incremental indexing, name index = 12)
|
301
|
+
0x0D, # (header value string length = 13)
|
302
|
+
"my-user-agent".bytes,
|
303
|
+
0x40, # (literal header with incremental indexing, new name)
|
304
|
+
0x0B, # (header name string length = 11)
|
305
|
+
"x-my-header".bytes,
|
306
|
+
0x05, # (header value string length = 5)
|
307
|
+
"first".bytes
|
308
|
+
].flatten
|
309
|
+
|
310
|
+
E1_HEADERS = [
|
311
|
+
[":path", "/my-example/index.html"],
|
312
|
+
["user-agent", "my-user-agent"],
|
313
|
+
["x-my-header", "first"]
|
314
|
+
]
|
315
|
+
|
316
|
+
it "should match first header set in spec appendix" do
|
317
|
+
@cc.encode(E1_HEADERS).bytes.should eq E1_BYTES
|
318
|
+
end
|
319
|
+
|
320
|
+
it "should decode first header set in spec appendix" do
|
321
|
+
@dc.decode(StringIO.new(E1_BYTES.pack("C*"))).should eq E1_HEADERS
|
322
|
+
end
|
323
|
+
|
324
|
+
E2_BYTES = [
|
325
|
+
0xa6, # (indexed header, index = 38: removal from reference set)
|
326
|
+
0xa8, # (indexed header, index = 40: removal from reference set)
|
327
|
+
0x04, # (literal header, substitution indexing, name index = 3)
|
328
|
+
0x26, # (replaced entry index = 38)
|
329
|
+
0x1f, # (header value string length = 31)
|
330
|
+
"/my-example/resources/script.js".bytes,
|
331
|
+
0x5f,
|
332
|
+
0x0a, # (literal header, incremental indexing, name index = 40)
|
333
|
+
0x06, # (header value string length = 6)
|
334
|
+
"second".bytes
|
335
|
+
].flatten
|
336
|
+
|
337
|
+
E2_HEADERS = [
|
338
|
+
[":path", "/my-example/resources/script.js"],
|
339
|
+
["user-agent", "my-user-agent"],
|
340
|
+
["x-my-header", "second"]
|
341
|
+
]
|
342
|
+
|
343
|
+
it "should match second header set in spec appendix" do
|
344
|
+
# Force incremental indexing, the spec doesn't specify any strategy
|
345
|
+
# for deciding when to use incremental vs substitution indexing, and
|
346
|
+
# early implementations defer to incremental by default:
|
347
|
+
# - https://github.com/sludin/http2-perl/blob/master/lib/HTTP2/Draft/Compress.pm#L157
|
348
|
+
# - https://github.com/MSOpenTech/http2-katana/blob/master/Shared/SharedProtocol/Compression/HeadersDeltaCompression/CompressionProcessor.cs#L259
|
349
|
+
# - https://hg.mozilla.org/try/file/9d9a29992e4d/netwerk/protocol/http/Http2CompressionDraft00.cpp#l636
|
350
|
+
#
|
351
|
+
e2bytes = E2_BYTES.dup
|
352
|
+
e2bytes[2] = 0x44 # incremental indexing, name index = 3
|
353
|
+
e2bytes.delete_at(3) # remove replacement index byte
|
354
|
+
|
355
|
+
@cc.encode(E2_HEADERS).bytes.should eq e2bytes
|
356
|
+
end
|
357
|
+
|
358
|
+
it "should decode second header set in spec appendix" do
|
359
|
+
@dc.decode(StringIO.new(E2_BYTES.pack("C*"))).should match_array E2_HEADERS
|
360
|
+
end
|
361
|
+
|
362
|
+
it "encode-decode should be invariant" do
|
363
|
+
cc = Compressor.new(:request)
|
364
|
+
dc = Decompressor.new(:request)
|
365
|
+
|
366
|
+
E1_HEADERS.should match_array dc.decode(StringIO.new(cc.encode(E1_HEADERS)))
|
367
|
+
E2_HEADERS.should match_array dc.decode(StringIO.new(cc.encode(E2_HEADERS)))
|
368
|
+
end
|
369
|
+
|
370
|
+
it "should encode-decode request set of headers" do
|
371
|
+
cc = Compressor.new(:request)
|
372
|
+
dc = Decompressor.new(:request)
|
373
|
+
|
374
|
+
req = [
|
375
|
+
[":method", "get"],
|
376
|
+
[":host", "localhost"],
|
377
|
+
[":path", "/resource"],
|
378
|
+
["accept", "*/*"]
|
379
|
+
]
|
380
|
+
|
381
|
+
dc.decode(StringIO.new(cc.encode(req))).should eq req
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|