http-2-next 0.2.6 → 0.3.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,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP2Next
4
+ module Header
5
+ # Responsible for decoding received headers and maintaining compression
6
+ # context of the opposing peer. Decompressor must be initialized with
7
+ # appropriate starting context based on local role: client or server.
8
+ #
9
+ # @example
10
+ # server_role = Decompressor.new(:request)
11
+ # client_role = Decompressor.new(:response)
12
+ class Decompressor
13
+ include Error
14
+
15
+ # @param options [Hash] decoding options. Only :table_size is effective.
16
+ def initialize(options = {})
17
+ @cc = EncodingContext.new(options)
18
+ end
19
+
20
+ # Set dynamic table size in EncodingContext
21
+ # @param size [Integer] new dynamic table size
22
+ def table_size=(size)
23
+ @cc.table_size = size
24
+ end
25
+
26
+ # Decodes integer value from provided buffer.
27
+ #
28
+ # @param buf [String]
29
+ # @param n [Integer] number of available bits
30
+ # @return [Integer]
31
+ def integer(buf, n)
32
+ limit = 2**n - 1
33
+ i = !n.zero? ? (buf.getbyte & limit) : 0
34
+
35
+ m = 0
36
+ if i == limit
37
+ while (byte = buf.getbyte)
38
+ i += ((byte & 127) << m)
39
+ m += 7
40
+
41
+ break if (byte & 128).zero?
42
+ end
43
+ end
44
+
45
+ i
46
+ end
47
+
48
+ # Decodes string value from provided buffer.
49
+ #
50
+ # @param buf [String]
51
+ # @return [String] UTF-8 encoded string
52
+ # @raise [CompressionError] when input is malformed
53
+ def string(buf)
54
+ raise CompressionError, "invalid header block fragment" if buf.empty?
55
+
56
+ huffman = (buf.readbyte(0) & 0x80) == 0x80
57
+ len = integer(buf, 7)
58
+ str = buf.read(len)
59
+ raise CompressionError, "string too short" unless str.bytesize == len
60
+
61
+ str = Huffman.new.decode(Buffer.new(str)) if huffman
62
+ str.force_encoding(Encoding::UTF_8)
63
+ end
64
+
65
+ # Decodes header command from provided buffer.
66
+ #
67
+ # @param buf [Buffer]
68
+ # @return [Hash] command
69
+ def header(buf)
70
+ peek = buf.readbyte(0)
71
+
72
+ header = {}
73
+ header[:type], type = HEADREP.find do |_t, desc|
74
+ mask = (peek >> desc[:prefix]) << desc[:prefix]
75
+ mask == desc[:pattern]
76
+ end
77
+
78
+ raise CompressionError unless header[:type]
79
+
80
+ header[:name] = integer(buf, type[:prefix])
81
+
82
+ case header[:type]
83
+ when :indexed
84
+ raise CompressionError if (header[:name]).zero?
85
+
86
+ header[:name] -= 1
87
+ when :changetablesize
88
+ header[:value] = header[:name]
89
+ else
90
+ if (header[:name]).zero?
91
+ header[:name] = string(buf)
92
+ else
93
+ header[:name] -= 1
94
+ end
95
+ header[:value] = string(buf)
96
+ end
97
+
98
+ header
99
+ end
100
+
101
+ FORBIDDEN_HEADERS = %w[connection te].freeze
102
+
103
+ # Decodes and processes header commands within provided buffer.
104
+ #
105
+ # @param buf [Buffer]
106
+ # @param frame [HTTP2Next::Frame, nil]
107
+ # @return [Array] +[[name, value], ...]
108
+ def decode(buf, frame = nil)
109
+ list = []
110
+ decoding_pseudo_headers = true
111
+ @cc.listen_on_table do
112
+ until buf.empty?
113
+ field, value = @cc.process(header(buf))
114
+ next if field.nil?
115
+
116
+ is_pseudo_header = field.start_with? ":"
117
+ if !decoding_pseudo_headers && is_pseudo_header
118
+ raise ProtocolError, "one or more pseudo headers encountered after regular headers"
119
+ end
120
+
121
+ decoding_pseudo_headers = is_pseudo_header
122
+ raise ProtocolError, "invalid header received: #{field}" if FORBIDDEN_HEADERS.include?(field)
123
+
124
+ if frame
125
+ case field
126
+ when ":method"
127
+ frame[:method] = value
128
+ when "content-length"
129
+ frame[:content_length] = Integer(value)
130
+ when "trailer"
131
+ (frame[:trailer] ||= []) << value
132
+ end
133
+ end
134
+ list << [field, value]
135
+ end
136
+ end
137
+ list
138
+ end
139
+ end
140
+ end
141
+ end
@@ -1,18 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTP2Next
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
4
+ # To decompress header blocks, a decoder only needs to maintain a
5
+ # dynamic table as a decoding context.
6
+ # No other state information is needed.
8
7
  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
8
  class EncodingContext
13
9
  include Error
14
10
 
15
- using Extensions
11
+ using RegexpExtensions
16
12
 
17
13
  UPPER = /[[:upper:]]/.freeze
18
14
 
@@ -185,14 +181,15 @@ module HTTP2Next
185
181
  # o The header field is added to the decoded header list.
186
182
  # o The header field is inserted at the beginning of the dynamic table.
187
183
 
188
- if cmd[:name].is_a? Integer
184
+ case cmd[:name]
185
+ when Integer
189
186
  k, v = dereference(cmd[:name])
190
187
 
191
188
  cmd = cmd.dup
192
189
  cmd[:index] ||= cmd[:name]
193
190
  cmd[:value] ||= v
194
191
  cmd[:name] = k
195
- elsif UPPER.match?(cmd[:name])
192
+ when UPPER
196
193
  raise ProtocolError, "Invalid uppercase key: #{cmd[:name]}"
197
194
  end
198
195
 
@@ -329,293 +326,5 @@ module HTTP2Next
329
326
  cmdsize <= @limit
330
327
  end
331
328
  end
332
-
333
- # Header representation as defined by the spec.
334
- HEADREP = {
335
- indexed: { prefix: 7, pattern: 0x80 },
336
- incremental: { prefix: 6, pattern: 0x40 },
337
- noindex: { prefix: 4, pattern: 0x00 },
338
- neverindexed: { prefix: 4, pattern: 0x10 },
339
- changetablesize: { prefix: 5, pattern: 0x20 }
340
- }.each_value(&:freeze).freeze
341
-
342
- # Predefined options set for Compressor
343
- # http://mew.org/~kazu/material/2014-hpack.pdf
344
- NAIVE = { index: :never, huffman: :never }.freeze
345
- LINEAR = { index: :all, huffman: :never }.freeze
346
- STATIC = { index: :static, huffman: :never }.freeze
347
- SHORTER = { index: :all, huffman: :never }.freeze
348
- NAIVEH = { index: :never, huffman: :always }.freeze
349
- LINEARH = { index: :all, huffman: :always }.freeze
350
- STATICH = { index: :static, huffman: :always }.freeze
351
- SHORTERH = { index: :all, huffman: :shorter }.freeze
352
-
353
- # Responsible for encoding header key-value pairs using HPACK algorithm.
354
- class Compressor
355
- # @param options [Hash] encoding options
356
- def initialize(options = {})
357
- @cc = EncodingContext.new(options)
358
- end
359
-
360
- # Set dynamic table size in EncodingContext
361
- # @param size [Integer] new dynamic table size
362
- def table_size=(size)
363
- @cc.table_size = size
364
- end
365
-
366
- # Encodes provided value via integer representation.
367
- # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-5.1
368
- #
369
- # If I < 2^N - 1, encode I on N bits
370
- # Else
371
- # encode 2^N - 1 on N bits
372
- # I = I - (2^N - 1)
373
- # While I >= 128
374
- # Encode (I % 128 + 128) on 8 bits
375
- # I = I / 128
376
- # encode (I) on 8 bits
377
- #
378
- # @param i [Integer] value to encode
379
- # @param n [Integer] number of available bits
380
- # @return [String] binary string
381
- def integer(i, n)
382
- limit = 2**n - 1
383
- return [i].pack("C") if i < limit
384
-
385
- bytes = []
386
- bytes.push limit unless n.zero?
387
-
388
- i -= limit
389
- while i >= 128
390
- bytes.push((i % 128) + 128)
391
- i /= 128
392
- end
393
-
394
- bytes.push i
395
- bytes.pack("C*")
396
- end
397
-
398
- # Encodes provided value via string literal representation.
399
- # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-5.2
400
- #
401
- # * The string length, defined as the number of bytes needed to store
402
- # its UTF-8 representation, is represented as an integer with a seven
403
- # bits prefix. If the string length is strictly less than 127, it is
404
- # represented as one byte.
405
- # * If the bit 7 of the first byte is 1, the string value is represented
406
- # as a list of Huffman encoded octets
407
- # (padded with bit 1's until next octet boundary).
408
- # * If the bit 7 of the first byte is 0, the string value is
409
- # represented as a list of UTF-8 encoded octets.
410
- #
411
- # +@options [:huffman]+ controls whether to use Huffman encoding:
412
- # :never Do not use Huffman encoding
413
- # :always Always use Huffman encoding
414
- # :shorter Use Huffman when the result is strictly shorter
415
- #
416
- # @param str [String]
417
- # @return [String] binary string
418
- def string(str)
419
- plain = nil
420
- huffman = nil
421
- plain = integer(str.bytesize, 7) << str.dup.force_encoding(Encoding::BINARY) unless @cc.options[:huffman] == :always
422
- unless @cc.options[:huffman] == :never
423
- huffman = Huffman.new.encode(str)
424
- huffman = integer(huffman.bytesize, 7) << huffman
425
- huffman.setbyte(0, huffman.ord | 0x80)
426
- end
427
- case @cc.options[:huffman]
428
- when :always
429
- huffman
430
- when :never
431
- plain
432
- else
433
- huffman.bytesize < plain.bytesize ? huffman : plain
434
- end
435
- end
436
-
437
- # Encodes header command with appropriate header representation.
438
- #
439
- # @param h [Hash] header command
440
- # @param buffer [String]
441
- # @return [Buffer]
442
- def header(h, buffer = Buffer.new)
443
- rep = HEADREP[h[:type]]
444
-
445
- case h[:type]
446
- when :indexed
447
- buffer << integer(h[:name] + 1, rep[:prefix])
448
- when :changetablesize
449
- buffer << integer(h[:value], rep[:prefix])
450
- else
451
- if h[:name].is_a? Integer
452
- buffer << integer(h[:name] + 1, rep[:prefix])
453
- else
454
- buffer << integer(0, rep[:prefix])
455
- buffer << string(h[:name])
456
- end
457
-
458
- buffer << string(h[:value])
459
- end
460
-
461
- # set header representation pattern on first byte
462
- fb = buffer.ord | rep[:pattern]
463
- buffer.setbyte(0, fb)
464
-
465
- buffer
466
- end
467
-
468
- # Encodes provided list of HTTP headers.
469
- #
470
- # @param headers [Array] +[[name, value], ...]+
471
- # @return [Buffer]
472
- def encode(headers)
473
- buffer = Buffer.new
474
- pseudo_headers, regular_headers = headers.partition { |f, _| f.start_with? ":" }
475
- headers = [*pseudo_headers, *regular_headers]
476
- commands = @cc.encode(headers)
477
- commands.each do |cmd|
478
- buffer << header(cmd)
479
- end
480
-
481
- buffer
482
- end
483
- end
484
-
485
- # Responsible for decoding received headers and maintaining compression
486
- # context of the opposing peer. Decompressor must be initialized with
487
- # appropriate starting context based on local role: client or server.
488
- #
489
- # @example
490
- # server_role = Decompressor.new(:request)
491
- # client_role = Decompressor.new(:response)
492
- class Decompressor
493
- include Error
494
-
495
- # @param options [Hash] decoding options. Only :table_size is effective.
496
- def initialize(options = {})
497
- @cc = EncodingContext.new(options)
498
- end
499
-
500
- # Set dynamic table size in EncodingContext
501
- # @param size [Integer] new dynamic table size
502
- def table_size=(size)
503
- @cc.table_size = size
504
- end
505
-
506
- # Decodes integer value from provided buffer.
507
- #
508
- # @param buf [String]
509
- # @param n [Integer] number of available bits
510
- # @return [Integer]
511
- def integer(buf, n)
512
- limit = 2**n - 1
513
- i = !n.zero? ? (buf.getbyte & limit) : 0
514
-
515
- m = 0
516
- if i == limit
517
- while (byte = buf.getbyte)
518
- i += ((byte & 127) << m)
519
- m += 7
520
-
521
- break if (byte & 128).zero?
522
- end
523
- end
524
-
525
- i
526
- end
527
-
528
- # Decodes string value from provided buffer.
529
- #
530
- # @param buf [String]
531
- # @return [String] UTF-8 encoded string
532
- # @raise [CompressionError] when input is malformed
533
- def string(buf)
534
- raise CompressionError, "invalid header block fragment" if buf.empty?
535
-
536
- huffman = (buf.readbyte(0) & 0x80) == 0x80
537
- len = integer(buf, 7)
538
- str = buf.read(len)
539
- raise CompressionError, "string too short" unless str.bytesize == len
540
-
541
- str = Huffman.new.decode(Buffer.new(str)) if huffman
542
- str.force_encoding(Encoding::UTF_8)
543
- end
544
-
545
- # Decodes header command from provided buffer.
546
- #
547
- # @param buf [Buffer]
548
- # @return [Hash] command
549
- def header(buf)
550
- peek = buf.readbyte(0)
551
-
552
- header = {}
553
- header[:type], type = HEADREP.find do |_t, desc|
554
- mask = (peek >> desc[:prefix]) << desc[:prefix]
555
- mask == desc[:pattern]
556
- end
557
-
558
- raise CompressionError unless header[:type]
559
-
560
- header[:name] = integer(buf, type[:prefix])
561
-
562
- case header[:type]
563
- when :indexed
564
- raise CompressionError if (header[:name]).zero?
565
-
566
- header[:name] -= 1
567
- when :changetablesize
568
- header[:value] = header[:name]
569
- else
570
- if (header[:name]).zero?
571
- header[:name] = string(buf)
572
- else
573
- header[:name] -= 1
574
- end
575
- header[:value] = string(buf)
576
- end
577
-
578
- header
579
- end
580
-
581
- FORBIDDEN_HEADERS = %w[connection te].freeze
582
-
583
- # Decodes and processes header commands within provided buffer.
584
- #
585
- # @param buf [Buffer]
586
- # @param frame [HTTP2Next::Frame, nil]
587
- # @return [Array] +[[name, value], ...]
588
- def decode(buf, frame = nil)
589
- list = []
590
- decoding_pseudo_headers = true
591
- @cc.listen_on_table do
592
- until buf.empty?
593
- field, value = @cc.process(header(buf))
594
- next if field.nil?
595
-
596
- is_pseudo_header = field.start_with? ":"
597
- if !decoding_pseudo_headers && is_pseudo_header
598
- raise ProtocolError, "one or more pseudo headers encountered after regular headers"
599
- end
600
-
601
- decoding_pseudo_headers = is_pseudo_header
602
- raise ProtocolError, "invalid header received: #{field}" if FORBIDDEN_HEADERS.include?(field)
603
-
604
- if frame
605
- case field
606
- when ":method"
607
- frame[:method] = value
608
- when "content-length"
609
- frame[:content_length] = Integer(value)
610
- when "trailer"
611
- (frame[:trailer] ||= []) << value
612
- end
613
- end
614
- list << [field, value]
615
- end
616
- end
617
- list
618
- end
619
- end
620
329
  end
621
330
  end