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.
@@ -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