http-2 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.autotest +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
|