http-2 0.12.0 → 1.0.0

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