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,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