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,493 @@
1
+ require "stringio"
2
+
3
+ module HTTP2
4
+
5
+ # Implementation of header compression for HTTP 2.0 (HPACK) format adapted
6
+ # to efficiently represent HTTP headers in the context of HTTP 2.0.
7
+ #
8
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression
9
+ module Header
10
+
11
+ # The set of components used to encode or decode a header set form an
12
+ # encoding context: an encoding context contains a header table and a
13
+ # reference set - there is one encoding context for each direction.
14
+ #
15
+ class CompressionContext
16
+ include Error
17
+
18
+ # TODO: replace StringIO with Buffer...
19
+
20
+ # Default request working set as defined by the spec.
21
+ REQ_DEFAULTS = [
22
+ [':scheme' ,'http' ],
23
+ [':scheme' ,'https'],
24
+ [':host' ,'' ],
25
+ [':path' ,'/' ],
26
+ [':method' ,'get' ],
27
+ ['accept' ,'' ],
28
+ ['accept-charset' ,'' ],
29
+ ['accept-encoding' ,'' ],
30
+ ['accept-language' ,'' ],
31
+ ['cookie' ,'' ],
32
+ ['if-modified-since' ,'' ],
33
+ ['keep-alive' ,'' ],
34
+ ['user-agent' ,'' ],
35
+ ['proxy-connection' ,'' ],
36
+ ['referer' ,'' ],
37
+ ['accept-datetime' ,'' ],
38
+ ['authorization' ,'' ],
39
+ ['allow' ,'' ],
40
+ ['cache-control' ,'' ],
41
+ ['connection' ,'' ],
42
+ ['content-length' ,'' ],
43
+ ['content-md5' ,'' ],
44
+ ['content-type' ,'' ],
45
+ ['date' ,'' ],
46
+ ['expect' ,'' ],
47
+ ['from' ,'' ],
48
+ ['if-match' ,'' ],
49
+ ['if-none-match' ,'' ],
50
+ ['if-range' ,'' ],
51
+ ['if-unmodified-since','' ],
52
+ ['max-forwards' ,'' ],
53
+ ['pragma' ,'' ],
54
+ ['proxy-authorization','' ],
55
+ ['range' ,'' ],
56
+ ['te' ,'' ],
57
+ ['upgrade' ,'' ],
58
+ ['via' ,'' ],
59
+ ['warning' ,'' ]
60
+ ];
61
+
62
+ # Default response working set as defined by the spec.
63
+ RESP_DEFAULTS = [
64
+ [':status' ,'200'],
65
+ ['age' ,'' ],
66
+ ['cache-control' ,'' ],
67
+ ['content-length' ,'' ],
68
+ ['content-type' ,'' ],
69
+ ['date' ,'' ],
70
+ ['etag' ,'' ],
71
+ ['expires' ,'' ],
72
+ ['last-modified' ,'' ],
73
+ ['server' ,'' ],
74
+ ['set-cookie' ,'' ],
75
+ ['vary' ,'' ],
76
+ ['via' ,'' ],
77
+ ['access-control-allow-origin','' ],
78
+ ['accept-ranges' ,'' ],
79
+ ['allow' ,'' ],
80
+ ['connection' ,'' ],
81
+ ['content-disposition' ,'' ],
82
+ ['content-encoding' ,'' ],
83
+ ['content-language' ,'' ],
84
+ ['content-location' ,'' ],
85
+ ['content-md5' ,'' ],
86
+ ['content-range' ,'' ],
87
+ ['link' ,'' ],
88
+ ['location' ,'' ],
89
+ ['p3p' ,'' ],
90
+ ['pragma' ,'' ],
91
+ ['proxy-authenticate' ,'' ],
92
+ ['refresh' ,'' ],
93
+ ['retry-after' ,'' ],
94
+ ['strict-transport-security' ,'' ],
95
+ ['trailer' ,'' ],
96
+ ['transfer-encoding' ,'' ],
97
+ ['warning' ,'' ],
98
+ ['www-authenticate' ,'' ]
99
+ ];
100
+
101
+ # Current table of header key-value pairs.
102
+ attr_reader :table
103
+
104
+ # Current working set of header key-value pairs.
105
+ attr_reader :workset
106
+
107
+ # Initializes compression context with appropriate client/server
108
+ # defaults and maximum size of the header table.
109
+ #
110
+ # @param type [Symbol] either :request or :response
111
+ # @param limit [Integer] maximum header table size in bytes
112
+ def initialize(type, limit = 4096)
113
+ @type = type
114
+ @table = (type == :request) ? REQ_DEFAULTS.dup : RESP_DEFAULTS.dup
115
+ @limit = limit
116
+ @workset = []
117
+ end
118
+
119
+ # Performs differential coding based on provided command type.
120
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-01#section-3.1
121
+ #
122
+ # @param cmd [Hash]
123
+ def process(cmd)
124
+ # indexed representation
125
+ if cmd[:type] == :indexed
126
+ # For an indexed representation, the decoder checks whether the index
127
+ # is present in the working set. If true, the corresponding entry is
128
+ # removed from the working set. If several entries correspond to this
129
+ # encoded index, all these entries are removed from the working set.
130
+ # If the index is not present in the working set, it is used to
131
+ # retrieve the corresponding header from the header table, and a new
132
+ # entry is added to the working set representing this header.
133
+ cur = @workset.find_index {|(i,v)| i == cmd[:name]}
134
+
135
+ if cur
136
+ @workset.delete_at(cur)
137
+ else
138
+ @workset.push [cmd[:name], @table[cmd[:name]]]
139
+ end
140
+
141
+ else
142
+ # For a literal representation, a new entry is added to the working
143
+ # set representing this header. If the literal representation specifies
144
+ # that the header is to be indexed, the header is added accordingly to
145
+ # the header table, and its index is included in the entry in the working
146
+ # set. Otherwise, the entry in the working set contains an undefined index.
147
+ if cmd[:name].is_a? Integer
148
+ k,v = @table[cmd[:name]]
149
+
150
+ cmd[:index] ||= cmd[:name]
151
+ cmd[:value] ||= v
152
+ cmd[:name] = k
153
+ end
154
+
155
+ newval = [cmd[:name], cmd[:value]]
156
+
157
+ if cmd[:type] != :noindex
158
+ size_check cmd
159
+
160
+ case cmd[:type]
161
+ when :incremental
162
+ cmd[:index] = @table.size
163
+ when :substitution
164
+ if @table[cmd[:index]].nil?
165
+ raise HeaderException.new("invalid index")
166
+ end
167
+ when :prepend
168
+ @table = [newval] + @table
169
+ end
170
+
171
+ @table[cmd[:index]] = newval
172
+ end
173
+
174
+ @workset.push [cmd[:index], newval]
175
+ end
176
+ end
177
+
178
+ # First, upon starting the decoding of a new set of headers, the
179
+ # reference set of headers is interpreted into the working set of
180
+ # headers: for each header in the reference set, an entry is added to
181
+ # the working set, containing the header name, its value, and its
182
+ # current index in the header table.
183
+ #
184
+ # @return [Array] current working set
185
+ def update_sets
186
+ # new refset is the the workset sans headers not in header table
187
+ refset = @workset.reject {|(i,h)| !@table.include? h}
188
+
189
+ # new workset is the refset with index of each header in header table
190
+ @workset = refset.collect {|(i,h)| [@table.find_index(h), h]}
191
+ end
192
+
193
+ # Emits best available command to encode provided header.
194
+ #
195
+ # @param header [Hash]
196
+ def addcmd(header)
197
+ # check if we have an exact match in header table
198
+ if idx = @table.index(header)
199
+ if !active? idx
200
+ return { name: idx, type: :indexed }
201
+ end
202
+ end
203
+
204
+ # check if we have a partial match on header name
205
+ if idx = @table.index {|(k,_)| k == header.first}
206
+ # default to incremental indexing
207
+ cmd = { name: idx, value: header.last, type: :incremental}
208
+
209
+ # TODO: implement literal without indexing strategy
210
+ # TODO: implement substitution strategy (if it makes sense)
211
+ # if default? idx
212
+ # cmd[:type] = :incremental
213
+ # else
214
+ # cmd[:type] = :substitution
215
+ # cmd[:index] = idx
216
+ # end
217
+
218
+ return cmd
219
+ end
220
+
221
+ return { name: header.first, value: header.last, type: :incremental }
222
+ end
223
+
224
+ # Emits command to remove current index from working set.
225
+ #
226
+ # @param idx [Integer]
227
+ def removecmd(idx)
228
+ {name: idx, type: :indexed}
229
+ end
230
+
231
+ private
232
+
233
+ # Before adding a new entry to the header table or changing an existing
234
+ # one, a check has to be performed to ensure that the change will not
235
+ # cause the table to grow in size beyond the SETTINGS_MAX_BUFFER_SIZE
236
+ # limit. If necessary, one or more items from the beginning of the
237
+ # table are removed until there is enough free space available to make
238
+ # the modification. Dropping an entry from the beginning of the table
239
+ # causes the index positions of the remaining entries in the table to
240
+ # be decremented by 1.
241
+ #
242
+ # @param cmd [Hash]
243
+ def size_check(cmd)
244
+ cursize = @table.join.bytesize + @table.size * 32
245
+ cmdsize = cmd[:name].bytesize + cmd[:value].bytesize + 32
246
+
247
+ cur = 0
248
+ while (cursize + cmdsize) > @limit do
249
+ e = @table.shift
250
+
251
+ # When using substitution indexing, it is possible that the existing
252
+ # item being replaced might be one of the items removed when performing
253
+ # the necessary size adjustment. In such cases, the substituted value
254
+ # being added to the header table is inserted at the beginning of the
255
+ # header table (at index position #0) and the index positions of the
256
+ # other remaining entries in the table are incremented by 1.
257
+ if cmd[:type] == :substitution && cur == cmd[:index]
258
+ cmd[:type] = :prepend
259
+ end
260
+
261
+ cursize -= (e.join.bytesize + 32)
262
+ end
263
+ end
264
+
265
+ def active?(idx)
266
+ !@workset.find {|i,_| i == idx }.nil?
267
+ end
268
+
269
+ def default?(idx)
270
+ t = (@type == :request) ? REQ_DEFAULTS : RESP_DEFAULTS
271
+ idx < t.size
272
+ end
273
+ end
274
+
275
+ # Header representation as defined by the spec.
276
+ HEADREP = {
277
+ indexed: {prefix: 7, pattern: 0x80},
278
+ noindex: {prefix: 5, pattern: 0x60},
279
+ incremental: {prefix: 5, pattern: 0x40},
280
+ substitution: {prefix: 6, pattern: 0x00}
281
+ }
282
+
283
+ # Responsible for encoding header key-value pairs using HPACK algorithm.
284
+ # Compressor must be initialized with appropriate starting context based
285
+ # on local role: client or server.
286
+ #
287
+ # @example
288
+ # client_role = Compressor.new(:request)
289
+ # server_role = Compressor.new(:response)
290
+ class Compressor
291
+ def initialize(type)
292
+ @cc = CompressionContext.new(type)
293
+ end
294
+
295
+ # Encodes provided value via integer representation.
296
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-01#section-4.2.1
297
+ #
298
+ # If I < 2^N - 1, encode I on N bits
299
+ # Else, encode 2^N - 1 on N bits and do the following steps:
300
+ # Set I to (I - (2^N - 1)) and Q to 1
301
+ # While Q > 0
302
+ # Compute Q and R, quotient and remainder of I divided by 2^7
303
+ # If Q is strictly greater than 0, write one 1 bit; otherwise, write one 0 bit
304
+ # Encode R on the next 7 bits
305
+ # I = Q
306
+ #
307
+ # @param i [Integer] value to encode
308
+ # @param n [Integer] number of available bits
309
+ # @return [String] binary string
310
+ def integer(i, n)
311
+ limit = 2**n - 1
312
+ return [i].pack('C') if (i < limit)
313
+
314
+ bytes = []
315
+ bytes.push limit if !n.zero?
316
+
317
+ i -= limit
318
+ q = 1
319
+
320
+ while (q > 0) do
321
+ q, r = i.divmod(128)
322
+ r += 128 if (q > 0)
323
+ i = q
324
+
325
+ bytes.push(r)
326
+ end
327
+
328
+ bytes.pack('C*')
329
+ end
330
+
331
+ # Encodes provided value via string literal representation.
332
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-01#section-4.2.2
333
+ #
334
+ # * The string length, defined as the number of bytes needed to store
335
+ # its UTF-8 representation, is represented as an integer with a zero
336
+ # bits prefix. If the string length is strictly less than 128, it is
337
+ # represented as one byte.
338
+ # * The string value represented as a list of UTF-8 character
339
+ #
340
+ # @param str [String]
341
+ # @return [String] binary string
342
+ def string(str)
343
+ integer(str.bytesize, 0) + str.dup.force_encoding('binary')
344
+ end
345
+
346
+ # Encodes header command with appropriate header representation.
347
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-01#section-4.3
348
+ #
349
+ # @param h [Hash] header command
350
+ # @param buffer [String]
351
+ def header(h, buffer = "")
352
+ rep = HEADREP[h[:type]]
353
+
354
+ if h[:type] == :indexed
355
+ buffer << integer(h[:name], rep[:prefix])
356
+
357
+ else
358
+ if h[:name].is_a? Integer
359
+ buffer << integer(h[:name]+1, rep[:prefix])
360
+ else
361
+ buffer << integer(0, rep[:prefix])
362
+ buffer << string(h[:name])
363
+ end
364
+
365
+ if h[:type] == :substitution
366
+ buffer << integer(h[:index], 0)
367
+ end
368
+
369
+ if h[:value].is_a? Integer
370
+ buffer << integer(h[:value], 0)
371
+ else
372
+ buffer << string(h[:value])
373
+ end
374
+ end
375
+
376
+ # set header representation pattern on first byte
377
+ fb = buffer[0].unpack("C").first | rep[:pattern]
378
+ buffer.setbyte(0, fb)
379
+
380
+ buffer
381
+ end
382
+
383
+ # Encodes provided list of HTTP headers.
384
+ #
385
+ # @param headers [Hash]
386
+ # @return [String] binary string
387
+ def encode(headers)
388
+ commands = []
389
+ @cc.update_sets
390
+
391
+ # Remove missing headers from the working set
392
+ @cc.workset.each do |idx, (wk,wv)|
393
+ if headers.find {|(hk,hv)| hk == wk && hv == wv }.nil?
394
+ commands.push @cc.removecmd idx
395
+ end
396
+ end
397
+
398
+ # Add missing headers to the working set
399
+ headers.each do |(hk,hv)|
400
+ if @cc.workset.find {|i,(wk,wv)| hk == wk && hv == wv}.nil?
401
+ commands.push @cc.addcmd [hk, hv]
402
+ end
403
+ end
404
+
405
+ commands.map do |cmd|
406
+ @cc.process cmd.dup
407
+ header cmd
408
+ end.join
409
+ end
410
+ end
411
+
412
+ # Responsible for decoding received headers and maintaining compression
413
+ # context of the opposing peer. Decompressor must be initialized with
414
+ # appropriate starting context based on local role: client or server.
415
+ #
416
+ # @example
417
+ # server_role = Decompressor.new(:request)
418
+ # client_role = Decompressor.new(:response)
419
+ class Decompressor
420
+ def initialize(type)
421
+ @cc = CompressionContext.new(type)
422
+ end
423
+
424
+ # Decodes integer value from provided buffer.
425
+ #
426
+ # @param buf [String]
427
+ # @param n [Integer] number of available bits
428
+ def integer(buf, n)
429
+ limit = 2**n - 1
430
+ i = !n.zero? ? (buf.getbyte & limit) : 0
431
+
432
+ m = 0
433
+ buf.each_byte do |byte|
434
+ i += ((byte & 127) << m)
435
+ m += 7
436
+
437
+ break if (byte & 128).zero?
438
+ end if (i == limit)
439
+
440
+ i
441
+ end
442
+
443
+ # Decodes string value from provided buffer.
444
+ #
445
+ # @param buf [String]
446
+ # @return [String] UTF-8 encoded string
447
+ def string(buf)
448
+ buf.read(integer(buf, 0)).force_encoding('utf-8')
449
+ end
450
+
451
+ # Decodes header command from provided buffer.
452
+ #
453
+ # @param buf [String]
454
+ def header(buf)
455
+ peek = buf.getbyte
456
+ buf.seek(-1, IO::SEEK_CUR)
457
+
458
+ header = {}
459
+ header[:type], type = HEADREP.select do |t, desc|
460
+ mask = (peek >> desc[:prefix]) << desc[:prefix]
461
+ mask == desc[:pattern]
462
+ end.first
463
+
464
+ header[:name] = integer(buf, type[:prefix])
465
+ if header[:type] != :indexed
466
+ header[:name] -= 1
467
+
468
+ if header[:name] == -1
469
+ header[:name] = string(buf)
470
+ end
471
+
472
+ if header[:type] == :substitution
473
+ header[:index] = integer(buf, 0)
474
+ end
475
+
476
+ header[:value] = string(buf)
477
+ end
478
+
479
+ header
480
+ end
481
+
482
+ # Decodes and processes header commands within provided buffer.
483
+ #
484
+ # @param buf [String]
485
+ def decode(buf)
486
+ @cc.update_sets
487
+ @cc.process(header(buf)) while !buf.eof?
488
+ @cc.workset.map {|i,header| header}
489
+ end
490
+ end
491
+
492
+ end
493
+ end