http-2 0.12.0 → 1.0.2

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.
@@ -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