mieps_http-2 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,557 @@
1
+ module HTTP2
2
+ # Implementation of header compression for HTTP 2.0 (HPACK) format adapted
3
+ # to efficiently represent HTTP headers in the context of HTTP 2.0.
4
+ #
5
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10
6
+ module Header
7
+ # To decompress header blocks, a decoder only needs to maintain a
8
+ # dynamic table as a decoding context.
9
+ # No other state information is needed.
10
+ class EncodingContext
11
+ include Error
12
+
13
+ # @private
14
+ # Static table
15
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#appendix-A
16
+ STATIC_TABLE = [
17
+ [':authority', ''],
18
+ [':method', 'GET'],
19
+ [':method', 'POST'],
20
+ [':path', '/'],
21
+ [':path', '/index.html'],
22
+ [':scheme', 'http'],
23
+ [':scheme', 'https'],
24
+ [':status', '200'],
25
+ [':status', '204'],
26
+ [':status', '206'],
27
+ [':status', '304'],
28
+ [':status', '400'],
29
+ [':status', '404'],
30
+ [':status', '500'],
31
+ ['accept-charset', ''],
32
+ ['accept-encoding', 'gzip, deflate'],
33
+ ['accept-language', ''],
34
+ ['accept-ranges', ''],
35
+ ['accept', ''],
36
+ ['access-control-allow-origin', ''],
37
+ ['age', ''],
38
+ ['allow', ''],
39
+ ['authorization', ''],
40
+ ['cache-control', ''],
41
+ ['content-disposition', ''],
42
+ ['content-encoding', ''],
43
+ ['content-language', ''],
44
+ ['content-length', ''],
45
+ ['content-location', ''],
46
+ ['content-range', ''],
47
+ ['content-type', ''],
48
+ ['cookie', ''],
49
+ ['date', ''],
50
+ ['etag', ''],
51
+ ['expect', ''],
52
+ ['expires', ''],
53
+ ['from', ''],
54
+ ['host', ''],
55
+ ['if-match', ''],
56
+ ['if-modified-since', ''],
57
+ ['if-none-match', ''],
58
+ ['if-range', ''],
59
+ ['if-unmodified-since', ''],
60
+ ['last-modified', ''],
61
+ ['link', ''],
62
+ ['location', ''],
63
+ ['max-forwards', ''],
64
+ ['proxy-authenticate', ''],
65
+ ['proxy-authorization', ''],
66
+ ['range', ''],
67
+ ['referer', ''],
68
+ ['refresh', ''],
69
+ ['retry-after', ''],
70
+ ['server', ''],
71
+ ['set-cookie', ''],
72
+ ['strict-transport-security', ''],
73
+ ['transfer-encoding', ''],
74
+ ['user-agent', ''],
75
+ ['vary', ''],
76
+ ['via', ''],
77
+ ['www-authenticate', ''],
78
+ ].each { |pair| pair.each(&:freeze).freeze }.freeze
79
+
80
+ # Current table of header key-value pairs.
81
+ attr_reader :table
82
+
83
+ # Current encoding options
84
+ #
85
+ # :table_size Integer maximum dynamic table size in bytes
86
+ # :huffman Symbol :always, :never, :shorter
87
+ # :index Symbol :all, :static, :never
88
+ attr_reader :options
89
+
90
+ # Initializes compression context with appropriate client/server
91
+ # defaults and maximum size of the dynamic table.
92
+ #
93
+ # @param options [Hash] encoding options
94
+ # :table_size Integer maximum dynamic table size in bytes
95
+ # :huffman Symbol :always, :never, :shorter
96
+ # :index Symbol :all, :static, :never
97
+ def initialize(**options)
98
+ default_options = {
99
+ huffman: :shorter,
100
+ index: :all,
101
+ table_size: 4096,
102
+ }
103
+ @table = []
104
+ @options = default_options.merge(options)
105
+ @limit = @options[:table_size]
106
+ end
107
+
108
+ # Duplicates current compression context
109
+ # @return [EncodingContext]
110
+ def dup
111
+ other = EncodingContext.new(@options)
112
+ t = @table
113
+ l = @limit
114
+ other.instance_eval do
115
+ @table = t.dup # shallow copy
116
+ @limit = l
117
+ end
118
+ other
119
+ end
120
+
121
+ # Finds an entry in current dynamic table by index.
122
+ # Note that index is zero-based in this module.
123
+ #
124
+ # If the index is greater than the last index in the static table,
125
+ # an entry in the dynamic table is dereferenced.
126
+ #
127
+ # If the index is greater than the last header index, an error is raised.
128
+ #
129
+ # @param index [Integer] zero-based index in the dynamic table.
130
+ # @return [Array] +[key, value]+
131
+ def dereference(index)
132
+ # NOTE: index is zero-based in this module.
133
+ value = STATIC_TABLE[index] || @table[index - STATIC_TABLE.size]
134
+ fail CompressionError, 'Index too large' unless value
135
+ value
136
+ end
137
+
138
+ # Header Block Processing
139
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-4.1
140
+ #
141
+ # @param cmd [Hash] { type:, name:, value:, index: }
142
+ # @return [Array] +[name, value]+ header field that is added to the decoded header list
143
+ def process(cmd)
144
+ emit = nil
145
+
146
+ case cmd[:type]
147
+ when :changetablesize
148
+ self.table_size = cmd[:value]
149
+
150
+ when :indexed
151
+ # Indexed Representation
152
+ # An _indexed representation_ entails the following actions:
153
+ # o The header field corresponding to the referenced entry in either
154
+ # the static table or dynamic table is added to the decoded header
155
+ # list.
156
+ idx = cmd[:name]
157
+
158
+ k, v = dereference(idx)
159
+ emit = [k, v]
160
+
161
+ when :incremental, :noindex, :neverindexed
162
+ # A _literal representation_ that is _not added_ to the dynamic table
163
+ # entails the following action:
164
+ # o The header field is added to the decoded header list.
165
+
166
+ # A _literal representation_ that is _added_ to the dynamic table
167
+ # entails the following actions:
168
+ # o The header field is added to the decoded header list.
169
+ # o The header field is inserted at the beginning of the dynamic table.
170
+
171
+ if cmd[:name].is_a? Integer
172
+ k, v = dereference(cmd[:name])
173
+
174
+ cmd = cmd.dup
175
+ cmd[:index] ||= cmd[:name]
176
+ cmd[:value] ||= v
177
+ cmd[:name] = k
178
+ end
179
+
180
+ emit = [cmd[:name], cmd[:value]]
181
+
182
+ add_to_table(emit) if cmd[:type] == :incremental
183
+
184
+ else
185
+ fail CompressionError, "Invalid type: #{cmd[:type]}"
186
+ end
187
+
188
+ emit
189
+ end
190
+
191
+ # Plan header compression according to +@options [:index]+
192
+ # :never Do not use dynamic table or static table reference at all.
193
+ # :static Use static table only.
194
+ # :all Use all of them.
195
+ #
196
+ # @param headers [Array] +[[name, value], ...]+
197
+ # @return [Array] array of commands
198
+ def encode(headers)
199
+ commands = []
200
+ # Literals commands are marked with :noindex when index is not used
201
+ noindex = [:static, :never].include?(@options[:index])
202
+ headers.each do |h|
203
+ cmd = addcmd(h)
204
+ cmd[:type] = :noindex if noindex && cmd[:type] == :incremental
205
+ commands << cmd
206
+ process(cmd)
207
+ end
208
+ commands
209
+ end
210
+
211
+ # Emits command for a header.
212
+ # Prefer static table over dynamic table.
213
+ # Prefer exact match over name-only match.
214
+ #
215
+ # +@options [:index]+ controls whether to use the dynamic table,
216
+ # static table, or both.
217
+ # :never Do not use dynamic table or static table reference at all.
218
+ # :static Use static table only.
219
+ # :all Use all of them.
220
+ #
221
+ # @param header [Array] +[name, value]+
222
+ # @return [Hash] command
223
+ def addcmd(header)
224
+ exact = nil
225
+ name_only = nil
226
+
227
+ if [:all, :static].include?(@options[:index])
228
+ STATIC_TABLE.each_index do |i|
229
+ if STATIC_TABLE[i] == header
230
+ exact ||= i
231
+ break
232
+ elsif STATIC_TABLE[i].first == header.first
233
+ name_only ||= i
234
+ end
235
+ end
236
+ end
237
+ if [:all].include?(@options[:index]) && !exact
238
+ @table.each_index do |i|
239
+ if @table[i] == header
240
+ exact ||= i + STATIC_TABLE.size
241
+ break
242
+ elsif @table[i].first == header.first
243
+ name_only ||= i + STATIC_TABLE.size
244
+ end
245
+ end
246
+ end
247
+
248
+ if exact
249
+ { name: exact, type: :indexed }
250
+ elsif name_only
251
+ { name: name_only, value: header.last, type: :incremental }
252
+ else
253
+ { name: header.first, value: header.last, type: :incremental }
254
+ end
255
+ end
256
+
257
+ # Alter dynamic table size.
258
+ # When the size is reduced, some headers might be evicted.
259
+ def table_size=(size)
260
+ @limit = size
261
+ size_check(nil)
262
+ end
263
+
264
+ # Returns current table size in octets
265
+ # @return [Integer]
266
+ def current_table_size
267
+ @table.inject(0) { |r, (k, v)| r + k.bytesize + v.bytesize + 32 }
268
+ end
269
+
270
+ private
271
+
272
+ # Add a name-value pair to the dynamic table.
273
+ # Older entries might have been evicted so that
274
+ # the new entry fits in the dynamic table.
275
+ #
276
+ # @param cmd [Array] +[name, value]+
277
+ def add_to_table(cmd)
278
+ return unless size_check(cmd)
279
+ @table.unshift(cmd)
280
+ end
281
+
282
+ # To keep the dynamic table size lower than or equal to @limit,
283
+ # remove one or more entries at the end of the dynamic table.
284
+ #
285
+ # @param cmd [Hash]
286
+ # @return [Boolean] whether +cmd+ fits in the dynamic table.
287
+ def size_check(cmd)
288
+ cursize = current_table_size
289
+ cmdsize = cmd.nil? ? 0 : cmd[0].bytesize + cmd[1].bytesize + 32
290
+
291
+ while cursize + cmdsize > @limit
292
+ break if @table.empty?
293
+
294
+ e = @table.pop
295
+ cursize -= e[0].bytesize + e[1].bytesize + 32
296
+ end
297
+
298
+ cmdsize <= @limit
299
+ end
300
+ end
301
+
302
+ # Header representation as defined by the spec.
303
+ HEADREP = {
304
+ indexed: { prefix: 7, pattern: 0x80 },
305
+ incremental: { prefix: 6, pattern: 0x40 },
306
+ noindex: { prefix: 4, pattern: 0x00 },
307
+ neverindexed: { prefix: 4, pattern: 0x10 },
308
+ changetablesize: { prefix: 5, pattern: 0x20 },
309
+ }.each_value(&:freeze).freeze
310
+
311
+ # Predefined options set for Compressor
312
+ # http://mew.org/~kazu/material/2014-hpack.pdf
313
+ NAIVE = { index: :never, huffman: :never }.freeze
314
+ LINEAR = { index: :all, huffman: :never }.freeze
315
+ STATIC = { index: :static, huffman: :never }.freeze
316
+ SHORTER = { index: :all, huffman: :never }.freeze
317
+ NAIVEH = { index: :never, huffman: :always }.freeze
318
+ LINEARH = { index: :all, huffman: :always }.freeze
319
+ STATICH = { index: :static, huffman: :always }.freeze
320
+ SHORTERH = { index: :all, huffman: :shorter }.freeze
321
+
322
+ # Responsible for encoding header key-value pairs using HPACK algorithm.
323
+ class Compressor
324
+ # @param options [Hash] encoding options
325
+ def initialize(**options)
326
+ @cc = EncodingContext.new(options)
327
+ end
328
+
329
+ # Set dynamic table size in EncodingContext
330
+ # @param size [Integer] new dynamic table size
331
+ def table_size=(size)
332
+ @cc.table_size = size
333
+ end
334
+
335
+ # Encodes provided value via integer representation.
336
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-5.1
337
+ #
338
+ # If I < 2^N - 1, encode I on N bits
339
+ # Else
340
+ # encode 2^N - 1 on N bits
341
+ # I = I - (2^N - 1)
342
+ # While I >= 128
343
+ # Encode (I % 128 + 128) on 8 bits
344
+ # I = I / 128
345
+ # encode (I) on 8 bits
346
+ #
347
+ # @param i [Integer] value to encode
348
+ # @param n [Integer] number of available bits
349
+ # @return [String] binary string
350
+ def integer(i, n)
351
+ limit = 2**n - 1
352
+ return [i].pack('C') if i < limit
353
+
354
+ bytes = []
355
+ bytes.push limit unless n.zero?
356
+
357
+ i -= limit
358
+ while (i >= 128)
359
+ bytes.push((i % 128) + 128)
360
+ i /= 128
361
+ end
362
+
363
+ bytes.push i
364
+ bytes.pack('C*')
365
+ end
366
+
367
+ # Encodes provided value via string literal representation.
368
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-5.2
369
+ #
370
+ # * The string length, defined as the number of bytes needed to store
371
+ # its UTF-8 representation, is represented as an integer with a seven
372
+ # bits prefix. If the string length is strictly less than 127, it is
373
+ # represented as one byte.
374
+ # * If the bit 7 of the first byte is 1, the string value is represented
375
+ # as a list of Huffman encoded octets
376
+ # (padded with bit 1's until next octet boundary).
377
+ # * If the bit 7 of the first byte is 0, the string value is
378
+ # represented as a list of UTF-8 encoded octets.
379
+ #
380
+ # +@options [:huffman]+ controls whether to use Huffman encoding:
381
+ # :never Do not use Huffman encoding
382
+ # :always Always use Huffman encoding
383
+ # :shorter Use Huffman when the result is strictly shorter
384
+ #
385
+ # @param str [String]
386
+ # @return [String] binary string
387
+ def string(str)
388
+ plain, huffman = nil, nil
389
+ unless @cc.options[:huffman] == :always
390
+ plain = integer(str.bytesize, 7) << str.dup.force_encoding(Encoding::BINARY)
391
+ end
392
+ unless @cc.options[:huffman] == :never
393
+ huffman = Huffman.new.encode(str)
394
+ huffman = integer(huffman.bytesize, 7) << huffman
395
+ huffman.setbyte(0, huffman.ord | 0x80)
396
+ end
397
+ case @cc.options[:huffman]
398
+ when :always
399
+ huffman
400
+ when :never
401
+ plain
402
+ else
403
+ huffman.bytesize < plain.bytesize ? huffman : plain
404
+ end
405
+ end
406
+
407
+ # Encodes header command with appropriate header representation.
408
+ #
409
+ # @param h [Hash] header command
410
+ # @param buffer [String]
411
+ # @return [Buffer]
412
+ def header(h, buffer = Buffer.new)
413
+ rep = HEADREP[h[:type]]
414
+
415
+ case h[:type]
416
+ when :indexed
417
+ buffer << integer(h[:name] + 1, rep[:prefix])
418
+ when :changetablesize
419
+ buffer << integer(h[:value], rep[:prefix])
420
+ else
421
+ if h[:name].is_a? Integer
422
+ buffer << integer(h[:name] + 1, rep[:prefix])
423
+ else
424
+ buffer << integer(0, rep[:prefix])
425
+ buffer << string(h[:name])
426
+ end
427
+
428
+ buffer << string(h[:value])
429
+ end
430
+
431
+ # set header representation pattern on first byte
432
+ fb = buffer.ord | rep[:pattern]
433
+ buffer.setbyte(0, fb)
434
+
435
+ buffer
436
+ end
437
+
438
+ # Encodes provided list of HTTP headers.
439
+ #
440
+ # @param headers [Array] +[[name, value], ...]+
441
+ # @return [Buffer]
442
+ def encode(headers)
443
+ buffer = Buffer.new
444
+
445
+ # Literal header names MUST be translated to lowercase before
446
+ # encoding and transmission.
447
+ headers.map! { |hk, hv| [hk.downcase, hv] }
448
+
449
+ commands = @cc.encode(headers)
450
+ commands.each do |cmd|
451
+ buffer << header(cmd)
452
+ end
453
+
454
+ buffer
455
+ end
456
+ end
457
+
458
+ # Responsible for decoding received headers and maintaining compression
459
+ # context of the opposing peer. Decompressor must be initialized with
460
+ # appropriate starting context based on local role: client or server.
461
+ #
462
+ # @example
463
+ # server_role = Decompressor.new(:request)
464
+ # client_role = Decompressor.new(:response)
465
+ class Decompressor
466
+ # @param options [Hash] decoding options. Only :table_size is effective.
467
+ def initialize(**options)
468
+ @cc = EncodingContext.new(options)
469
+ end
470
+
471
+ # Set dynamic table size in EncodingContext
472
+ # @param size [Integer] new dynamic table size
473
+ def table_size=(size)
474
+ @cc.table_size = size
475
+ end
476
+
477
+ # Decodes integer value from provided buffer.
478
+ #
479
+ # @param buf [String]
480
+ # @param n [Integer] number of available bits
481
+ # @return [Integer]
482
+ def integer(buf, n)
483
+ limit = 2**n - 1
484
+ i = !n.zero? ? (buf.getbyte & limit) : 0
485
+
486
+ m = 0
487
+ while (byte = buf.getbyte)
488
+ i += ((byte & 127) << m)
489
+ m += 7
490
+
491
+ break if (byte & 128).zero?
492
+ end if (i == limit)
493
+
494
+ i
495
+ end
496
+
497
+ # Decodes string value from provided buffer.
498
+ #
499
+ # @param buf [String]
500
+ # @return [String] UTF-8 encoded string
501
+ # @raise [CompressionError] when input is malformed
502
+ def string(buf)
503
+ huffman = (buf.readbyte(0) & 0x80) == 0x80
504
+ len = integer(buf, 7)
505
+ str = buf.read(len)
506
+ fail CompressionError, 'string too short' unless str.bytesize == len
507
+ str = Huffman.new.decode(Buffer.new(str)) if huffman
508
+ str.force_encoding(Encoding::UTF_8)
509
+ end
510
+
511
+ # Decodes header command from provided buffer.
512
+ #
513
+ # @param buf [Buffer]
514
+ # @return [Hash] command
515
+ def header(buf)
516
+ peek = buf.readbyte(0)
517
+
518
+ header = {}
519
+ header[:type], type = HEADREP.find do |_t, desc|
520
+ mask = (peek >> desc[:prefix]) << desc[:prefix]
521
+ mask == desc[:pattern]
522
+ end
523
+
524
+ fail CompressionError unless header[:type]
525
+
526
+ header[:name] = integer(buf, type[:prefix])
527
+
528
+ case header[:type]
529
+ when :indexed
530
+ fail CompressionError if header[:name] == 0
531
+ header[:name] -= 1
532
+ when :changetablesize
533
+ header[:value] = header[:name]
534
+ else
535
+ if header[:name] == 0
536
+ header[:name] = string(buf)
537
+ else
538
+ header[:name] -= 1
539
+ end
540
+ header[:value] = string(buf)
541
+ end
542
+
543
+ header
544
+ end
545
+
546
+ # Decodes and processes header commands within provided buffer.
547
+ #
548
+ # @param buf [Buffer]
549
+ # @return [Array] +[[name, value], ...]+
550
+ def decode(buf)
551
+ list = []
552
+ list << @cc.process(header(buf)) until buf.empty?
553
+ list.compact
554
+ end
555
+ end
556
+ end
557
+ end