http-2 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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