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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba5613efe08f91c20db5d9c32fbbac18a2d6f214ba42037c341dd9b40cbffe23
4
- data.tar.gz: c7e0f3a0256a2cf7d106c1be72417c863e37f4c203bc7df6910ff91d135479e6
3
+ metadata.gz: c4ce4e3302e32d2c860cf38d25f82186a233ae9bb446bde932d185ca398d4eec
4
+ data.tar.gz: c2428f11638405fa7c92390745f4c21022627500fe54d564a46f06e83967d126
5
5
  SHA512:
6
- metadata.gz: 645582d2208c23b84120256893828f8fc5f3c7d058848979fd62188a9fd31746aafade3759334d06ae8d53b2ee6ab3bff23d4eb5288112da31ccf6dee8a187cf
7
- data.tar.gz: 3804926e00e854b61a25dbf0e515ae5fcfb82532403909f6d3e4e035f7ebeef162c14bc00d64babd0013cdb22f2a7164f02dc708d1575392fc8b3445ddd953f6
6
+ metadata.gz: 5a10531d34c3d37989a6de029b4bfdca1b9742b7614a7785179bea4d8794568b58bd88641c259721520f803b1f6577c2e41ddb49bff75564edfbdf9d422124fc
7
+ data.tar.gz: 8d9d7e74f52d079a7f9021bcd7679bd7544b1e30d17cdd5f42de24326812c46f4c3f8208f3132ba0e9d0df46b6c947238781a48abbf6d1cacc95da69b3227293
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/http-2-next.svg)](http://rubygems.org/gems/http-2-next)
4
4
  [![Build status](https://gitlab.com/honeyryderchuck/http-2-next/badges/master/pipeline.svg)](https://gitlab.com/honeyryderchuck/http-2-next/commits/master)
5
- [![coverage report](https://gitlab.com/honeyryderchuck/http-2-next/badges/master/coverage.svg)](https://honeyryderchuck.gitlab.io/http-2-next/coverage/#_AllFiles)
5
+ [![coverage report](https://gitlab.com/honeyryderchuck/http-2-next/badges/master/coverage.svg?job=coverage)](https://honeyryderchuck.gitlab.io/http-2-next/coverage/#_AllFiles)
6
6
 
7
7
  **Attention!** This is a fork of the [http-2](https://github.com/igrigorik/http-2) gem.
8
8
 
@@ -6,9 +6,7 @@ require "http/2/next/error"
6
6
  require "http/2/next/emitter"
7
7
  require "http/2/next/buffer"
8
8
  require "http/2/next/flow_buffer"
9
- require "http/2/next/huffman"
10
- require "http/2/next/huffman_statemachine"
11
- require "http/2/next/compressor"
9
+ require "http/2/next/header"
12
10
  require "http/2/next/framer"
13
11
  require "http/2/next/connection"
14
12
  require "http/2/next/client"
@@ -8,6 +8,8 @@ module HTTP2Next
8
8
  class Buffer
9
9
  extend Forwardable
10
10
 
11
+ using StringExtensions
12
+
11
13
  def_delegators :@buffer, :ord, :encoding, :setbyte, :unpack,
12
14
  :size, :each_byte, :to_str, :to_s, :length, :inspect,
13
15
  :[], :[]=, :empty?, :bytesize, :include?
@@ -28,6 +30,10 @@ module HTTP2Next
28
30
  Buffer.new(@buffer.slice!(0, n))
29
31
  end
30
32
 
33
+ def unpack1(*arg)
34
+ @buffer.unpack1(*arg)
35
+ end
36
+
31
37
  # Emulate StringIO#getbyte: slice first byte from buffer.
32
38
  def getbyte
33
39
  read(1).ord
@@ -61,7 +67,7 @@ module HTTP2Next
61
67
  # Slice unsigned 32-bit integer from buffer.
62
68
  # @return [Integer]
63
69
  def read_uint32
64
- read(4).unpack(UINT32).first
70
+ read(4).unpack1(UINT32)
65
71
  end
66
72
 
67
73
  # Ensures that data that is added is binary encoded as well,
@@ -57,12 +57,11 @@ module HTTP2Next
57
57
  # Size of current connection flow control window (by default, set to
58
58
  # infinity, but is automatically updated on receipt of peer settings).
59
59
  attr_reader :local_window
60
- attr_reader :remote_window
60
+ attr_reader :remote_window, :remote_settings
61
61
  alias window local_window
62
62
 
63
63
  # Current settings value for local and peer
64
64
  attr_reader :local_settings
65
- attr_reader :remote_settings
66
65
 
67
66
  # Pending settings value
68
67
  # Sent but not ack'ed settings
@@ -105,6 +104,7 @@ module HTTP2Next
105
104
 
106
105
  @h2c_upgrade = nil
107
106
  @closed_since = nil
107
+ @received_frame = false
108
108
  end
109
109
 
110
110
  def closed?
@@ -176,7 +176,7 @@ module HTTP2Next
176
176
  # @param settings [Array or Hash]
177
177
  def settings(payload)
178
178
  payload = payload.to_a
179
- connection_error if validate_settings(@local_role, payload)
179
+ validate_settings(@local_role, payload)
180
180
  @pending_settings << payload
181
181
  send(type: :settings, stream: 0, payload: payload)
182
182
  @pending_settings << payload
@@ -213,6 +213,11 @@ module HTTP2Next
213
213
  end
214
214
 
215
215
  while (frame = @framer.parse(@recv_buffer))
216
+ if is_a?(Client) && !@received_frame
217
+ connection_error(:protocol_error, msg: "didn't receive settings") if frame[:type] != :settings
218
+ @received_frame = true
219
+ end
220
+
216
221
  # Implementations MUST discard frames
217
222
  # that have unknown or unsupported types.
218
223
  if frame[:type].nil?
@@ -521,8 +526,6 @@ module HTTP2Next
521
526
  def validate_settings(role, settings)
522
527
  settings.each do |key, v|
523
528
  case key
524
- when :settings_header_table_size
525
- # Any value is valid
526
529
  when :settings_enable_push
527
530
  case role
528
531
  when :server
@@ -530,32 +533,41 @@ module HTTP2Next
530
533
  # Clients MUST reject any attempt to change the
531
534
  # SETTINGS_ENABLE_PUSH setting to a value other than 0 by treating the
532
535
  # message as a connection error (Section 5.4.1) of type PROTOCOL_ERROR.
533
- return ProtocolError.new("invalid #{key} value") unless v.zero?
536
+ next if v.zero?
537
+
538
+ connection_error(:protocol_error, msg: "invalid #{key} value")
534
539
  when :client
535
540
  # Any value other than 0 or 1 MUST be treated as a
536
541
  # connection error (Section 5.4.1) of type PROTOCOL_ERROR.
537
- return ProtocolError.new("invalid #{key} value") unless v.zero? || v == 1
542
+ next if v.zero? || v == 1
543
+
544
+ connection_error(:protocol_error, msg: "invalid #{key} value")
538
545
  end
539
- when :settings_max_concurrent_streams
540
- # Any value is valid
541
546
  when :settings_initial_window_size
542
547
  # Values above the maximum flow control window size of 2^31-1 MUST
543
548
  # be treated as a connection error (Section 5.4.1) of type
544
549
  # FLOW_CONTROL_ERROR.
545
- return FlowControlError.new("invalid #{key} value") unless v <= 0x7fffffff
550
+ next if v <= 0x7fffffff
551
+
552
+ connection_error(:flow_control_error, msg: "invalid #{key} value")
546
553
  when :settings_max_frame_size
547
554
  # The initial value is 2^14 (16,384) octets. The value advertised
548
555
  # by an endpoint MUST be between this initial value and the maximum
549
556
  # allowed frame size (2^24-1 or 16,777,215 octets), inclusive.
550
557
  # Values outside this range MUST be treated as a connection error
551
558
  # (Section 5.4.1) of type PROTOCOL_ERROR.
552
- return ProtocolError.new("invalid #{key} value") unless v >= 16_384 && v <= 16_777_215
553
- when :settings_max_header_list_size
559
+ next if v >= 16_384 && v <= 16_777_215
560
+
561
+ connection_error(:protocol_error, msg: "invalid #{key} value")
562
+ # when :settings_max_concurrent_streams
563
+ # Any value is valid
564
+ # when :settings_header_table_size
565
+ # Any value is valid
566
+ # when :settings_max_header_list_size
554
567
  # Any value is valid
555
568
  # else # ignore unknown settings
556
569
  end
557
570
  end
558
- nil
559
571
  end
560
572
 
561
573
  # Update connection settings based on parameters set by the peer.
@@ -572,8 +584,7 @@ module HTTP2Next
572
584
  # Process pending settings we have sent.
573
585
  [@pending_settings.shift, :local]
574
586
  else
575
- check = validate_settings(@remote_role, frame[:payload])
576
- connection_error(check) if check
587
+ validate_settings(@remote_role, frame[:payload])
577
588
  [frame[:payload], :remote]
578
589
  end
579
590
 
@@ -707,7 +718,7 @@ module HTTP2Next
707
718
  # @param priority [Integer]
708
719
  # @param window [Integer]
709
720
  # @param parent [Stream]
710
- def activate_stream(id: nil, **args)
721
+ def activate_stream(id:, **args)
711
722
  connection_error(msg: "Stream ID already exists") if @streams.key?(id)
712
723
 
713
724
  raise StreamLimitExceeded if @active_stream_count >= (@max_streams || @local_settings[:settings_max_concurrent_streams])
@@ -9,19 +9,18 @@ module HTTP2Next
9
9
  #
10
10
  # @param event [Symbol]
11
11
  # @param block [Proc] callback function
12
- def add_listener(event, &block)
12
+ def on(event, &block)
13
13
  raise ArgumentError, "must provide callback" unless block_given?
14
14
 
15
15
  listeners(event.to_sym).push block
16
16
  end
17
- alias on add_listener
18
17
 
19
18
  # Subscribe to next event (at most once) for specified type.
20
19
  #
21
20
  # @param event [Symbol]
22
21
  # @param block [Proc] callback function
23
22
  def once(event, &block)
24
- add_listener(event) do |*args, &callback|
23
+ on(event) do |*args, &callback|
25
24
  block.call(*args, &callback)
26
25
  :delete
27
26
  end
@@ -10,6 +10,7 @@ module HTTP2Next
10
10
 
11
11
  class Error < StandardError
12
12
  def self.inherited(klass)
13
+ super
13
14
  type = klass.name.split("::").last
14
15
  type = type.gsub(/([^\^])([A-Z])/, '\1_\2').downcase.to_sym
15
16
  HTTP2Next::Error.types[type] = klass
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTP2Next
4
- module Extensions
4
+ module RegexpExtensions
5
5
  unless Regexp.method_defined?(:match?)
6
6
  refine Regexp do
7
7
  def match?(*args)
@@ -10,4 +10,14 @@ module HTTP2Next
10
10
  end
11
11
  end
12
12
  end
13
+
14
+ module StringExtensions
15
+ unless String.method_defined?(:unpack1)
16
+ refine String do
17
+ def unpack1(format)
18
+ unpack(format).first
19
+ end
20
+ end
21
+ end
22
+ end
13
23
  end
@@ -73,6 +73,7 @@ module HTTP2Next
73
73
 
74
74
  while (frame = send_buffer.retrieve(@remote_window))
75
75
 
76
+ # puts "#{self.class} -> #{@remote_window}"
76
77
  sent = frame[:payload].bytesize
77
78
 
78
79
  manage_state(frame) do
@@ -4,6 +4,8 @@ module HTTP2Next
4
4
  # Performs encoding, decoding, and validation of binary HTTP/2 frames.
5
5
  #
6
6
  class Framer
7
+ using StringExtensions
8
+
7
9
  include Error
8
10
 
9
11
  # Default value of max frame size (16384 bytes)
@@ -154,6 +156,7 @@ module HTTP2Next
154
156
  # Decodes common 9-byte header.
155
157
  #
156
158
  # @param buf [Buffer]
159
+ # @return [Hash] the corresponding frame
157
160
  def read_common_header(buf)
158
161
  frame = {}
159
162
  len_hi, len_lo, type, flags, stream = buf.slice(0, 9).unpack(HEADERPACK)
@@ -221,7 +224,7 @@ module HTTP2Next
221
224
  raise CompressionError, "Invalid stream ID (#{frame[:stream]})" if (frame[:stream]).nonzero?
222
225
 
223
226
  frame[:payload].each do |(k, v)|
224
- if k.is_a? Integer
227
+ if k.is_a? Integer # rubocop:disable Style/GuardClause
225
228
  DEFINED_SETTINGS.value?(k) || next
226
229
  else
227
230
  k = DEFINED_SETTINGS[k]
@@ -333,10 +336,10 @@ module HTTP2Next
333
336
  #
334
337
  # @param buf [Buffer]
335
338
  def parse(buf)
336
- return nil if buf.size < 9
339
+ return if buf.size < 9
337
340
 
338
341
  frame = read_common_header(buf)
339
- return nil if buf.size < 9 + frame[:length]
342
+ return if buf.size < 9 + frame[:length]
340
343
 
341
344
  raise ProtocolError, "payload too large" if frame[:length] > @local_max_frame_size
342
345
 
@@ -353,7 +356,7 @@ module HTTP2Next
353
356
  if FRAME_TYPES_WITH_PADDING.include?(frame[:type])
354
357
  padded = frame[:flags].include?(:padded)
355
358
  if padded
356
- padlen = payload.read(1).unpack(UINT8).first
359
+ padlen = payload.read(1).unpack1(UINT8)
357
360
  frame[:padding] = padlen + 1
358
361
  raise ProtocolError, "padding too long" if padlen > payload.bytesize
359
362
 
@@ -395,7 +398,7 @@ module HTTP2Next
395
398
  raise ProtocolError, "Invalid stream ID (#{frame[:stream]})" if (frame[:stream]).nonzero?
396
399
 
397
400
  (frame[:length] / 6).times do
398
- id = payload.read(2).unpack(UINT16).first
401
+ id = payload.read(2).unpack1(UINT16)
399
402
  val = payload.read_uint32
400
403
 
401
404
  # Unsupported or unrecognized settings MUST be ignored.
@@ -437,7 +440,7 @@ module HTTP2Next
437
440
  origins = []
438
441
 
439
442
  until payload.empty?
440
- len = payload.read(2).unpack(UINT16).first
443
+ len = payload.read(2).unpack1(UINT16)
441
444
  origins << payload.read(len)
442
445
  end
443
446
 
@@ -452,17 +455,16 @@ module HTTP2Next
452
455
 
453
456
  def pack_error(e)
454
457
  unless e.is_a? Integer
455
- raise CompressionError, "Unknown error ID for #{e}" if DEFINED_ERRORS[e].nil?
456
-
457
458
  e = DEFINED_ERRORS[e]
459
+
460
+ raise CompressionError, "Unknown error ID for #{e}" unless e
458
461
  end
459
462
 
460
463
  [e].pack(UINT32)
461
464
  end
462
465
 
463
466
  def unpack_error(error)
464
- name, = DEFINED_ERRORS.find { |_name, v| v == error }
465
- name || error
467
+ DEFINED_ERRORS.key(error) || error
466
468
  end
467
469
  end
468
470
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
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
8
+ module Header
9
+ # Header representation as defined by the spec.
10
+ HEADREP = {
11
+ indexed: { prefix: 7, pattern: 0x80 },
12
+ incremental: { prefix: 6, pattern: 0x40 },
13
+ noindex: { prefix: 4, pattern: 0x00 },
14
+ neverindexed: { prefix: 4, pattern: 0x10 },
15
+ changetablesize: { prefix: 5, pattern: 0x20 }
16
+ }.each_value(&:freeze).freeze
17
+
18
+ # Predefined options set for Compressor
19
+ # http://mew.org/~kazu/material/2014-hpack.pdf
20
+ NAIVE = { index: :never, huffman: :never }.freeze
21
+ LINEAR = { index: :all, huffman: :never }.freeze
22
+ STATIC = { index: :static, huffman: :never }.freeze
23
+ SHORTER = { index: :all, huffman: :never }.freeze
24
+ NAIVEH = { index: :never, huffman: :always }.freeze
25
+ LINEARH = { index: :all, huffman: :always }.freeze
26
+ STATICH = { index: :static, huffman: :always }.freeze
27
+ SHORTERH = { index: :all, huffman: :shorter }.freeze
28
+ end
29
+ end
30
+
31
+ require "http/2/next/header/huffman"
32
+ require "http/2/next/header/huffman_statemachine"
33
+ require "http/2/next/header/encoding_context"
34
+ require "http/2/next/header/compressor"
35
+ require "http/2/next/header/decompressor"
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP2Next
4
+ module Header
5
+ # Responsible for encoding header key-value pairs using HPACK algorithm.
6
+ class Compressor
7
+ # @param options [Hash] encoding options
8
+ def initialize(options = {})
9
+ @cc = EncodingContext.new(options)
10
+ end
11
+
12
+ # Set dynamic table size in EncodingContext
13
+ # @param size [Integer] new dynamic table size
14
+ def table_size=(size)
15
+ @cc.table_size = size
16
+ end
17
+
18
+ # Encodes provided value via integer representation.
19
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-5.1
20
+ #
21
+ # If I < 2^N - 1, encode I on N bits
22
+ # Else
23
+ # encode 2^N - 1 on N bits
24
+ # I = I - (2^N - 1)
25
+ # While I >= 128
26
+ # Encode (I % 128 + 128) on 8 bits
27
+ # I = I / 128
28
+ # encode (I) on 8 bits
29
+ #
30
+ # @param i [Integer] value to encode
31
+ # @param n [Integer] number of available bits
32
+ # @return [String] binary string
33
+ def integer(i, n)
34
+ limit = 2**n - 1
35
+ return [i].pack("C") if i < limit
36
+
37
+ bytes = []
38
+ bytes.push limit unless n.zero?
39
+
40
+ i -= limit
41
+ while i >= 128
42
+ bytes.push((i % 128) + 128)
43
+ i /= 128
44
+ end
45
+
46
+ bytes.push i
47
+ bytes.pack("C*")
48
+ end
49
+
50
+ # Encodes provided value via string literal representation.
51
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-5.2
52
+ #
53
+ # * The string length, defined as the number of bytes needed to store
54
+ # its UTF-8 representation, is represented as an integer with a seven
55
+ # bits prefix. If the string length is strictly less than 127, it is
56
+ # represented as one byte.
57
+ # * If the bit 7 of the first byte is 1, the string value is represented
58
+ # as a list of Huffman encoded octets
59
+ # (padded with bit 1's until next octet boundary).
60
+ # * If the bit 7 of the first byte is 0, the string value is
61
+ # represented as a list of UTF-8 encoded octets.
62
+ #
63
+ # +@options [:huffman]+ controls whether to use Huffman encoding:
64
+ # :never Do not use Huffman encoding
65
+ # :always Always use Huffman encoding
66
+ # :shorter Use Huffman when the result is strictly shorter
67
+ #
68
+ # @param str [String]
69
+ # @return [String] binary string
70
+ def string(str)
71
+ plain = nil
72
+ huffman = nil
73
+ plain = integer(str.bytesize, 7) << str.dup.force_encoding(Encoding::BINARY) unless @cc.options[:huffman] == :always
74
+ unless @cc.options[:huffman] == :never
75
+ huffman = Huffman.new.encode(str)
76
+ huffman = integer(huffman.bytesize, 7) << huffman
77
+ huffman.setbyte(0, huffman.ord | 0x80)
78
+ end
79
+ case @cc.options[:huffman]
80
+ when :always
81
+ huffman
82
+ when :never
83
+ plain
84
+ else
85
+ huffman.bytesize < plain.bytesize ? huffman : plain
86
+ end
87
+ end
88
+
89
+ # Encodes header command with appropriate header representation.
90
+ #
91
+ # @param h [Hash] header command
92
+ # @param buffer [String]
93
+ # @return [Buffer]
94
+ def header(h, buffer = Buffer.new)
95
+ rep = HEADREP[h[:type]]
96
+
97
+ case h[:type]
98
+ when :indexed
99
+ buffer << integer(h[:name] + 1, rep[:prefix])
100
+ when :changetablesize
101
+ buffer << integer(h[:value], rep[:prefix])
102
+ else
103
+ if h[:name].is_a? Integer
104
+ buffer << integer(h[:name] + 1, rep[:prefix])
105
+ else
106
+ buffer << integer(0, rep[:prefix])
107
+ buffer << string(h[:name])
108
+ end
109
+
110
+ buffer << string(h[:value])
111
+ end
112
+
113
+ # set header representation pattern on first byte
114
+ fb = buffer.ord | rep[:pattern]
115
+ buffer.setbyte(0, fb)
116
+
117
+ buffer
118
+ end
119
+
120
+ # Encodes provided list of HTTP headers.
121
+ #
122
+ # @param headers [Array] +[[name, value], ...]+
123
+ # @return [Buffer]
124
+ def encode(headers)
125
+ buffer = Buffer.new
126
+ pseudo_headers, regular_headers = headers.partition { |f, _| f.start_with? ":" }
127
+ headers = [*pseudo_headers, *regular_headers]
128
+ commands = @cc.encode(headers)
129
+ commands.each do |cmd|
130
+ buffer << header(cmd)
131
+ end
132
+
133
+ buffer
134
+ end
135
+ end
136
+ end
137
+ end