mieps_http-2 0.8.0

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