http-2 1.2.2 → 1.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: 1b41c25d0512d4056a38265801e7eef05df3cab5720ecbe11ff088c906752019
4
- data.tar.gz: 0b7322ff7f49e8dd892a9804a1a710be23f3c7105f76f7c67f62b281843616fc
3
+ metadata.gz: 19c1750fca12fb4ff94e29361824fc7f64052b4a320adee6d739c1acffc3c4ab
4
+ data.tar.gz: cd41bb795576baae733b69a364444323b77702512a031d425ac1f8f62c402311
5
5
  SHA512:
6
- metadata.gz: 32b4191d53effc33d5186da7093af95698809f2fec89fe37ce01898628d598e7a1215704875ec06ebd9fcaf84b11517310ee46631fb7a2c6b4a2a17fb3ad8b94
7
- data.tar.gz: 5ce7ec0abedca471a25e45883ba4b8d315eb1e78879ed5a8c563dc6a58a7e128bbe9f78f878a8d9956dd90254718634bc67323b57039cb33e87d6e1552cac392
6
+ metadata.gz: 2a33812ab5c84f41d51b66ce93d0616ffed1592276df92f9be71fb714b2288d96e4b728222a80d5cd7501907259bbe924bdc332a6a61f18ab227e6bdeccfddca
7
+ data.tar.gz: d366d161f3a126e4a1394ccc13266fda028a1f7a949629a90a3ba1b86fb89a08971dc5f08cd3d03beef6f340444dd83d27b5110ca7ddca7aba3a80593115ced7
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2013 Ilya Grigorik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/lib/http/2/base64.rb CHANGED
@@ -30,16 +30,16 @@ elsif !defined?(Base64)
30
30
  str.tr!("+/", "-_")
31
31
  str
32
32
  end
33
- end
34
33
 
35
- def urlsafe_decode64(str)
36
- if !str.end_with?("=") && str.length % 4 != 0
37
- str = str.ljust((str.length + 3) & ~3, "=")
38
- str.tr!("-_", "+/")
39
- else
40
- str = str.tr("-_", "+/")
34
+ def urlsafe_decode64(str)
35
+ if !str.end_with?("=") && str.length % 4 != 0
36
+ str = str.ljust((str.length + 3) & ~3, "=")
37
+ str.tr!("-_", "+/")
38
+ else
39
+ str = str.tr("-_", "+/")
40
+ end
41
+ strict_decode64(str)
41
42
  end
42
- strict_decode64(str)
43
43
  end
44
44
  end
45
45
  end
data/lib/http/2/client.rb CHANGED
@@ -64,7 +64,7 @@ module HTTP2
64
64
  @state = :connected
65
65
  emit(:frame, CONNECTION_PREFACE_MAGIC)
66
66
 
67
- payload = @local_settings.reject { |k, v| v == SPEC_DEFAULT_CONNECTION_SETTINGS[k] }
67
+ payload = @local_settings.connection_settings.reject { |k, v| v == SPEC_DEFAULT_CONNECTION_SETTINGS[k] }
68
68
  settings(payload)
69
69
  end
70
70
 
@@ -10,28 +10,6 @@ module HTTP2
10
10
  # Default stream_limit
11
11
  DEFAULT_MAX_CONCURRENT_STREAMS = 100
12
12
 
13
- # Default values for SETTINGS frame, as defined by the spec.
14
- SPEC_DEFAULT_CONNECTION_SETTINGS = {
15
- settings_header_table_size: 4096,
16
- settings_enable_push: 1, # enabled for servers
17
- settings_max_concurrent_streams: Framer::MAX_STREAM_ID, # unlimited
18
- settings_initial_window_size: 65_535,
19
- settings_max_frame_size: 16_384,
20
- settings_max_header_list_size: (2 << 30) - 1 # unlimited
21
- }.freeze
22
-
23
- DEFAULT_CONNECTION_SETTINGS = {
24
- settings_header_table_size: 4096,
25
- settings_enable_push: 1, # enabled for servers
26
- settings_max_concurrent_streams: 100,
27
- settings_initial_window_size: 65_535,
28
- settings_max_frame_size: 16_384,
29
- settings_max_header_list_size: (2 << 30) - 1 # unlimited
30
- }.freeze
31
-
32
- # Default stream priority (lower values are higher priority).
33
- DEFAULT_WEIGHT = 16
34
-
35
13
  # Default connection "fast-fail" preamble string as defined by the spec.
36
14
  CONNECTION_PREFACE_MAGIC = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
37
15
 
@@ -78,27 +56,26 @@ module HTTP2
78
56
  attr_accessor :active_stream_count
79
57
 
80
58
  # Initializes new connection object.
81
- #
82
59
  def initialize(settings = {})
83
- @local_settings = DEFAULT_CONNECTION_SETTINGS.merge(settings)
84
- @remote_settings = SPEC_DEFAULT_CONNECTION_SETTINGS.dup
60
+ @local_settings = Settings.new(**settings)
61
+ @remote_settings = Settings.new(settings_max_concurrent_streams: Framer::MAX_STREAM_ID)
85
62
 
86
- @compressor = Header::Compressor.new(settings)
87
- @decompressor = Header::Decompressor.new(settings)
63
+ @compressor = Header::Compressor.new(@local_settings)
64
+ @decompressor = Header::Decompressor.new(@local_settings)
88
65
 
89
- @active_stream_count = 0
90
- @last_stream_id = 0
66
+ @active_stream_count =
67
+ @promised_stream_count =
68
+ @last_promised_stream_id =
69
+ @last_stream_id = 0
91
70
  @streams = {}
92
71
  @streams_recently_closed = {}
93
72
  @oldest_stream_recently_closed = nil
94
73
  @pending_settings = []
95
74
 
96
- @framer = Framer.new(@local_settings[:settings_max_frame_size])
75
+ @framer = Framer.new(@local_settings.settings_max_frame_size)
97
76
 
98
- @local_window_limit = @local_settings[:settings_initial_window_size]
99
- @local_window = @local_window_limit
100
- @remote_window_limit = @remote_settings[:settings_initial_window_size]
101
- @remote_window = @remote_window_limit
77
+ @local_window = @local_window_limit = @local_settings.settings_initial_window_size
78
+ @remote_window = @remote_window_limit = @remote_settings.settings_initial_window_size
102
79
 
103
80
  @recv_buffer = "".b
104
81
  @continuation = []
@@ -137,7 +114,7 @@ module HTTP2
137
114
 
138
115
  stream = activate_stream(
139
116
  id: @stream_id,
140
- max_concurrent_streams: @remote_settings[:settings_max_concurrent_streams],
117
+ max_concurrent_streams: @remote_settings.settings_max_concurrent_streams,
141
118
  **args
142
119
  )
143
120
  @last_stream_id = stream.id
@@ -213,7 +190,7 @@ module HTTP2
213
190
  elsif read_str(@recv_buffer, 24) == CONNECTION_PREFACE_MAGIC
214
191
  # MAGIC is OK. Send our settings
215
192
  @state = :waiting_connection_preface
216
- payload = @local_settings.reject { |k, v| v == SPEC_DEFAULT_CONNECTION_SETTINGS[k] }
193
+ payload = @local_settings.connection_settings.reject { |k, v| v == SPEC_DEFAULT_CONNECTION_SETTINGS[k] }
217
194
  settings(payload)
218
195
  else
219
196
  raise HandshakeError
@@ -254,7 +231,7 @@ module HTTP2
254
231
  # prevent HTTP/2 CONTINUATION FLOOD
255
232
  # same heuristic as the one from HAProxy: https://www.haproxy.com/blog/haproxy-is-resilient-to-the-http-2-continuation-flood
256
233
  # different mitigation (connection closed, instead of 400 response)
257
- unless @continuation_size < @local_settings[:settings_max_frame_size]
234
+ unless @continuation_size < @local_settings.settings_max_frame_size
258
235
  connection_error(:protocol_error,
259
236
  msg: "too many continuations received")
260
237
  end
@@ -311,20 +288,25 @@ module HTTP2
311
288
  # Skip only this frame: later ones may serve completing streams.
312
289
  next if closed? && (stream_id > @last_stream_id || !@streams.key?(stream_id))
313
290
 
314
- stream = @streams[stream_id]
315
- if stream.nil?
291
+ unless (stream = @streams[stream_id])
316
292
  verify_pseudo_headers(frame)
317
293
 
318
294
  verify_stream_order(stream_id)
319
- stream = activate_stream(
320
- id: stream_id,
321
- weight: frame[:weight] || DEFAULT_WEIGHT,
322
- dependency: frame[:dependency] || 0,
323
- exclusive: frame[:exclusive] || false
324
- )
325
- emit(:stream, stream)
295
+
296
+ # TODO: remove EMPTY_HASH after dropping support for ruby < 3.4 (recent rubies can
297
+ # receive nil as kwarg splat)
298
+ priority = EMPTY_HASH
299
+ if frame[:flags].anybits?(PRIORITY)
300
+ connection_error(:protocol_error, msg: "stream can't depend on itself") if stream_id == frame[:dependency]
301
+
302
+ priority = frame.slice(:weight, :dependency, :exclusive)
303
+ end
304
+
305
+ stream = activate_stream(id: stream_id, **priority)
326
306
  end
327
307
 
308
+ # streams are only emitted in a non-idle state.
309
+ emit(:stream, stream)
328
310
  stream << frame
329
311
 
330
312
  when :push_promise
@@ -352,6 +334,8 @@ module HTTP2
352
334
  parent = @streams[stream_id]
353
335
  pid = frame[:promise_stream]
354
336
 
337
+ validate_push_promise(pid)
338
+
355
339
  # if PUSH parent is recently closed, RST_STREAM the push
356
340
  if @streams_recently_closed[stream_id]
357
341
  send(type: :rst_stream, stream: pid, error: :refused_stream)
@@ -377,6 +361,12 @@ module HTTP2
377
361
 
378
362
  _verify_pseudo_headers(frame, REQUEST_MANDATORY_HEADERS)
379
363
  verify_stream_order(pid)
364
+
365
+ if @promised_stream_count >= @local_settings.settings_max_concurrent_streams
366
+ send(type: :rst_stream, stream: pid, error: :refused_stream)
367
+ next
368
+ end
369
+
380
370
  stream = activate_stream(id: pid, parent: parent)
381
371
  emit(:promise, stream)
382
372
  stream << frame
@@ -391,7 +381,7 @@ module HTTP2
391
381
  case frame_type
392
382
  # Priority signaling is deprecated (RFC 9113 Section 5.3.2), but
393
383
  # a PRIORITY frame may still name a stream id not opened yet:
394
- # activate it, so the priority sticks if the stream opens.
384
+ # retain its priority without creating a stream until it opens.
395
385
  when :priority
396
386
  # After a GOAWAY, a PRIORITY for a stream already used and
397
387
  # closed must not resurrect it: reprioritizing a closed
@@ -399,16 +389,12 @@ module HTTP2
399
389
  # the draining connection open.
400
390
  next if closed? && stream_id <= @last_stream_id
401
391
 
402
- stream = activate_stream(
403
- id: stream_id,
404
- weight: frame[:weight] || DEFAULT_WEIGHT,
405
- dependency: frame[:dependency] || 0,
406
- exclusive: frame[:exclusive] || false
407
- )
392
+ connection_error(:protocol_error, msg: "stream can't depend on itself") if frame[:stream] == frame[:dependency]
408
393
 
409
- emit(:stream, stream)
410
- stream << frame
394
+ priority = frame.slice(:weight, :dependency, :exclusive)
411
395
 
396
+ stream = activate_stream(id: stream_id, **priority)
397
+ stream << frame
412
398
  # WINDOW_UPDATE can be sent by a peer that has sent a frame
413
399
  # bearing the END_STREAM flag. This means that a receiver could
414
400
  # receive a WINDOW_UPDATE frame on a "half-closed (remote)" or
@@ -577,6 +563,16 @@ module HTTP2
577
563
  end
578
564
  end
579
565
 
566
+ def validate_push_promise(stream_id)
567
+ connection_error(:protocol_error, msg: "clients cannot send PUSH_PROMISE") if @remote_role == :client
568
+ connection_error(:protocol_error, msg: "push promises are disabled") if @local_settings.settings_enable_push.zero?
569
+ unless stream_id.positive? && stream_id.even? && stream_id > @last_promised_stream_id
570
+ connection_error(:protocol_error, msg: "invalid promised stream ID")
571
+ end
572
+
573
+ @last_promised_stream_id = stream_id
574
+ end
575
+
580
576
  # Validate settings parameters. See sepc Section 6.5.2.
581
577
  #
582
578
  # @param role [Symbol] The sender's role: :client or :server
@@ -687,12 +683,15 @@ module HTTP2
687
683
  end
688
684
 
689
685
  when :settings_header_table_size
690
- # Setting header table size might cause some headers evicted
686
+ # The value is the maximum table size for the encoder of the other
687
+ # side: our setting limits the peer's encoder (our decompressor),
688
+ # and the peer's setting limits our encoder (our compressor).
689
+ # The table itself changes with the next dynamic table size update.
691
690
  case side
692
691
  when :local
693
- @compressor.table_size = v
692
+ @decompressor.max_table_size = v
694
693
  when :remote
695
- @decompressor.table_size = v
694
+ @compressor.max_table_size = v
696
695
  end
697
696
 
698
697
  when :settings_enable_push
@@ -701,6 +700,13 @@ module HTTP2
701
700
  when :settings_max_frame_size
702
701
  @framer.remote_max_frame_size = v
703
702
 
703
+ when :settings_max_header_list_size
704
+ case side
705
+ when :remote
706
+ @decompressor.max_header_list_size = v
707
+ # when :local
708
+ # not enforcing this on the client side
709
+ end
704
710
  # else # ignore unknown settings
705
711
  end
706
712
  end
@@ -752,7 +758,7 @@ module HTTP2
752
758
  #: @type var payload: String
753
759
  headers_frame[:payload] = payload
754
760
 
755
- max_frame_size = @remote_settings[:settings_max_frame_size]
761
+ max_frame_size = @remote_settings.settings_max_frame_size
756
762
 
757
763
  # if single frame, return immediately
758
764
  if payload.bytesize <= max_frame_size
@@ -796,7 +802,7 @@ module HTTP2
796
802
  # @param priority [Integer]
797
803
  # @param window [Integer]
798
804
  # @param parent [Stream]
799
- def activate_stream(id:, max_concurrent_streams: @local_settings[:settings_max_concurrent_streams], **args)
805
+ def activate_stream(id:, max_concurrent_streams: @local_settings.settings_max_concurrent_streams, **args)
800
806
  connection_error(msg: "Stream ID already exists") if @streams.key?(id)
801
807
 
802
808
  # SETTINGS_MAX_CONCURRENT_STREAMS limits the number of concurrent streams that the sender
@@ -805,9 +811,11 @@ module HTTP2
805
811
  raise StreamLimitExceeded if @active_stream_count >= max_concurrent_streams
806
812
 
807
813
  stream = Stream.new(connection: self, id: id, **args)
814
+ @promised_stream_count += 1 if stream.parent
808
815
 
809
816
  stream.once(:close) do
810
817
  @streams.delete(id)
818
+ @promised_stream_count -= 1 if stream.parent
811
819
 
812
820
  # A graceful GOAWAY leaves the connection :closing until the
813
821
  # tracked streams complete (Section 6.8); the last one to close
@@ -835,6 +843,8 @@ module HTTP2
835
843
  true
836
844
  end
837
845
  @oldest_stream_recently_closed = new_oldest
846
+ else
847
+ @oldest_stream_recently_closed ||= now
838
848
  end
839
849
 
840
850
  @streams_recently_closed[id] = now
data/lib/http/2/framer.rb CHANGED
@@ -130,10 +130,10 @@ module HTTP2
130
130
 
131
131
  stream_id = frame.fetch(:stream, 0)
132
132
 
133
- raise CompressionError, "Stream ID (#{stream_id}) is too large" if stream_id > MAX_STREAM_ID
133
+ raise CompressionError, "Stream ID (#{stream_id}) is outside the valid range" unless stream_id.between?(0, MAX_STREAM_ID)
134
134
 
135
- if type == :window_update && frame[:increment] > MAX_WINDOWINC
136
- raise CompressionError, "Window increment (#{frame[:increment]}) is too large"
135
+ if type == :window_update && !frame[:increment].between?(1, MAX_WINDOWINC)
136
+ raise CompressionError, "Window increment (#{frame[:increment]}) is outside the valid range"
137
137
  end
138
138
 
139
139
  flags = frame[:flags]
@@ -7,18 +7,18 @@ module HTTP2
7
7
  include PackingExtensions
8
8
  include BufferUtils
9
9
 
10
- # @param options [Hash] encoding options
11
- def initialize(options = {})
12
- @cc = EncodingContext.new(options)
10
+ def initialize(settings = Settings.new)
11
+ @cc = EncodingContext.new(settings)
13
12
  end
14
13
 
15
- # Set dynamic table size in EncodingContext
16
- # @param size [Integer] new dynamic table size
17
- def table_size=(size)
18
- @cc.table_size = size
14
+ # Set the maximum dynamic table +size+ allowed by the peer's
15
+ # SETTINGS_HEADER_TABLE_SIZE. The next header block starts with a
16
+ # dynamic table size update when the table size has to change.
17
+ def max_table_size=(size)
18
+ @cc.max_table_size = size
19
19
  end
20
20
 
21
- # Encodes provided value via integer representation.
21
+ # Encodes +i+ via integer representation into +buffer+ at the offset set by +offset+.
22
22
  # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-5.1
23
23
  #
24
24
  # If I < 2^N - 1, encode I on N bits
@@ -30,11 +30,6 @@ module HTTP2
30
30
  # I = I / 128
31
31
  # encode (I) on 8 bits
32
32
  #
33
- # @param i [Integer] value to encode
34
- # @param n [Integer] number of available bits
35
- # @param buffer [String] buffer to pack bytes into
36
- # @param offset [Integer] offset to insert packed bytes in buffer
37
- # @return [String] binary string
38
33
  def integer(i, n, buffer:, offset: buffer.size)
39
34
  limit = (1 << n) - 1
40
35
  return pack([i], "C", buffer: buffer, offset: offset) if i < limit
@@ -65,16 +60,8 @@ module HTTP2
65
60
  # * If the bit 7 of the first byte is 0, the string value is
66
61
  # represented as a list of UTF-8 encoded octets.
67
62
  #
68
- # +@options [:huffman]+ controls whether to use Huffman encoding:
69
- # :never Do not use Huffman encoding
70
- # :always Always use Huffman encoding
71
- # :shorter Use Huffman when the result is strictly shorter
72
- #
73
- # @param str [String]
74
- # @param buffer [String]
75
- # @return [String] binary string
76
63
  def string(str, buffer = "".b)
77
- case @cc.options[:huffman]
64
+ case @cc.settings.huffman
78
65
  when :always
79
66
  huffman_string(str, buffer)
80
67
  when :never
@@ -93,11 +80,7 @@ module HTTP2
93
80
  end
94
81
  end
95
82
 
96
- # Encodes header command with appropriate header representation.
97
- #
98
- # @param h [Hash] header command
99
- # @param buffer [String]
100
- # @return [Buffer]
83
+ # Encodes +h+ header command with appropriate header representation into +buffer+.
101
84
  def header(h, buffer = "".b)
102
85
  type = h[:type]
103
86
  rep = HEADREP[type]
@@ -128,9 +111,6 @@ module HTTP2
128
111
  end
129
112
 
130
113
  # Encodes provided list of HTTP headers.
131
- #
132
- # @param headers [Array] +[[name, value], ...]+
133
- # @return [Buffer]
134
114
  def encode(headers)
135
115
  buffer = "".b
136
116
  headers.partition { |f, _| f.start_with? ":" }.each do |hs|
@@ -144,9 +124,7 @@ module HTTP2
144
124
 
145
125
  private
146
126
 
147
- # @param str [String]
148
- # @param buffer [String]
149
- # @return [String] binary string
127
+ # encodes +str+ into +buffer+ using Huffman encoding.
150
128
  def huffman_string(str, buffer = "".b)
151
129
  huffman_offset = buffer.bytesize
152
130
  buffer << "\x00".b
@@ -162,18 +140,14 @@ module HTTP2
162
140
  buffer
163
141
  end
164
142
 
165
- # @param str [String]
166
- # @param buffer [String]
167
- # @return [String] binary string
143
+ # encodes +str+ into +buffer+.
168
144
  def plain_string(str, plain = "".b)
169
145
  integer(str.bytesize, 7, buffer: plain)
170
146
  append_str(plain, str)
171
147
  plain
172
148
  end
173
149
 
174
- # @param buffer [String]
175
- # @param huffman_offset [Integer] buffer offset where huffman string was introduced
176
- # @return [String] binary string
150
+ # encodes the huffman string size from +buffer+ into the string at the offset indicated by +huffman_offset+
177
151
  def set_huffman_size(buffer, huffman_offset)
178
152
  integer(buffer.bytesize - huffman_offset, 7, buffer: buffer, offset: huffman_offset)
179
153
  buffer.setbyte(huffman_offset, buffer.getbyte(huffman_offset) | 0x80)
@@ -5,32 +5,32 @@ module HTTP2
5
5
  # Responsible for decoding received headers and maintaining compression
6
6
  # context of the opposing peer. Decompressor must be initialized with
7
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
8
  class Decompressor
13
9
  include Error
14
10
  include BufferUtils
15
11
 
16
12
  FORBIDDEN_HEADERS = %w[connection te].freeze
17
13
 
18
- # @param options [Hash] decoding options. Only :table_size is effective.
19
- def initialize(options = {})
20
- @cc = EncodingContext.new(options)
14
+ attr_writer :max_header_list_size
15
+
16
+ def initialize(settings = Settings.new)
17
+ @cc = EncodingContext.new(settings)
18
+ @max_header_list_size = settings.settings_max_header_list_size
19
+ @accept_invalid_headers = false
21
20
  end
22
21
 
23
- # Set dynamic table size in EncodingContext
24
- # @param size [Integer] new dynamic table size
25
- def table_size=(size)
26
- @cc.table_size = size
22
+ def accept_invalid_headers!
23
+ @accept_invalid_headers = true
27
24
  end
28
25
 
29
- # Decodes integer value from provided buffer.
30
- #
31
- # @param buf [String]
32
- # @param n [Integer] number of available bits
33
- # @return [Integer]
26
+ # Set the maximum dynamic table +size+ allowed by our acknowledged
27
+ # SETTINGS_HEADER_TABLE_SIZE. When the current table is larger, the
28
+ # next header block has to start with a dynamic table size update.
29
+ def max_table_size=(size)
30
+ @cc.max_table_size = size
31
+ end
32
+
33
+ # Decodes integer value from provided +buffer+.
34
34
  def integer(buf, n)
35
35
  limit = (1 << n) - 1
36
36
  if n.zero?
@@ -55,11 +55,9 @@ module HTTP2
55
55
  i
56
56
  end
57
57
 
58
- # Decodes string value from provided buffer.
58
+ # Decodes string value from provided +buf+.
59
59
  #
60
- # @param buf [String]
61
- # @return [String] UTF-8 encoded string
62
- # @raise [CompressionError] when input is malformed
60
+ # raises CompressionError when input is malformed
63
61
  def string(buf)
64
62
  raise CompressionError, "invalid header block fragment" if buf.empty?
65
63
 
@@ -72,10 +70,7 @@ module HTTP2
72
70
  str.force_encoding(Encoding::UTF_8)
73
71
  end
74
72
 
75
- # Decodes header command from provided buffer.
76
- #
77
- # @param buf [Buffer]
78
- # @return [Hash] command
73
+ # Decodes header command from provided +buf+.
79
74
  def header(buf)
80
75
  peek = buf.getbyte(0)
81
76
 
@@ -109,13 +104,10 @@ module HTTP2
109
104
  end
110
105
  end
111
106
 
112
- # Decodes and processes header commands within provided buffer.
113
- #
114
- # @param buf [Buffer]
115
- # @param frame [HTTP2::Frame, nil]
116
- # @return [Array] +[[name, value], ...]
107
+ # Decodes and processes header commands within provided +buf+.
117
108
  def decode(buf, frame = nil)
118
- list = []
109
+ list = [] #: Array[header_pair]
110
+ header_list_size = 0
119
111
  decoding_pseudo_headers = true
120
112
  @cc.listen_on_table do
121
113
  until buf.empty?
@@ -123,12 +115,17 @@ module HTTP2
123
115
  next if field.nil?
124
116
 
125
117
  is_pseudo_header = field.start_with?(":")
126
- if !decoding_pseudo_headers && is_pseudo_header
118
+ if !decoding_pseudo_headers && is_pseudo_header && !@accept_invalid_headers
127
119
  raise ProtocolError, "one or more pseudo headers encountered after regular headers"
128
120
  end
129
121
 
130
122
  decoding_pseudo_headers = is_pseudo_header
131
- raise ProtocolError, "invalid header received: #{field}" if FORBIDDEN_HEADERS.include?(field)
123
+ if !@accept_invalid_headers && FORBIDDEN_HEADERS.include?(field)
124
+ raise ProtocolError, "invalid header received: #{field}"
125
+ end
126
+
127
+ header_list_size += field.bytesize + value.bytesize + 32
128
+ raise ProtocolError, "header list exceeds configured maximum" if header_list_size > @max_header_list_size
132
129
 
133
130
  if frame
134
131
  case field