http-2 1.2.1 → 1.2.3

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: d1db63e3235efa25ad18a270b128adc3955489766148712fd9405142d9cf1bd7
4
- data.tar.gz: d3cfbb973cb9a3bc53466c106a6a33b6521f7ba8415d832a646ef96419c906cd
3
+ metadata.gz: aa40c026d7345ba9879c03ce22a9ca6c8ec981482381fd0f89c2da5f4d814214
4
+ data.tar.gz: 9cf43f76f7aed43428b636dca605c05bc8c882bfd2f56cabbadaeff71c42648b
5
5
  SHA512:
6
- metadata.gz: 9a17f2bdcbc5fcd07ff0797994af22a0f170744f516702b5b5c2c8c032f60a15cf4c70869804c56f4e15e10829c2c51c8ea0fe5575751cf28b1fe0207389543a
7
- data.tar.gz: e599eb9b1bfa7a4985b6a0f53fe1592e6d26ea780fc16d8471d23cc2abe5020a91e28fba80e1d227492525373dd117677e2c436bb289022875c3986f3733bb9e
6
+ metadata.gz: 56813420638e4b6b8e310d64fef3a284ae0c597ce77e8fc83ae316f6cf3070a216fe943c5fd8b0cb9741e8103cf64ec17317e6af45163d355489f55d8f17379b
7
+ data.tar.gz: c6af72766c395fb4ef21ba2c6de0ef86dc1432421b4c8b0cdc486e4f006891d8e8f104225f98b7950d2ca31d111f97a2a1f42ba6633aeb62845c712da0d139e5
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/README.md CHANGED
@@ -85,6 +85,15 @@ Events emitted by the connection object:
85
85
  </tr>
86
86
  </table>
87
87
 
88
+ Connection shutdown happens in two steps. Once a GOAWAY frame is sent or
89
+ received, no new streams may be opened: `closed?` turns true and `new_stream`
90
+ raises `ConnectionClosed` — new work belongs on a new connection. After a
91
+ graceful GOAWAY (`:no_error`), streams the peer may still complete keep
92
+ running: `closing?` distinguishes this draining state, in which the connection
93
+ keeps processing frames — including connection-level flow control — until the
94
+ last tracked stream closes. Streams the GOAWAY refused (id above its
95
+ last-stream-id) are reported via the `:goaway` event, so their callers can
96
+ retry them on a fresh connection.
88
97
 
89
98
  ### Stream lifecycle management
90
99
 
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,25 +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
13
  # Default stream priority (lower values are higher priority).
33
14
  DEFAULT_WEIGHT = 16
34
15
 
@@ -56,7 +37,8 @@ module HTTP2
56
37
  include Error
57
38
  include BufferUtils
58
39
 
59
- # Connection state (:new, :closed).
40
+ # Connection state (:waiting_magic, :waiting_connection_preface,
41
+ # :connected, :closing, :closed).
60
42
  attr_reader :state
61
43
 
62
44
  # Size of current connection flow control window (by default, set to
@@ -77,13 +59,12 @@ module HTTP2
77
59
  attr_accessor :active_stream_count
78
60
 
79
61
  # Initializes new connection object.
80
- #
81
62
  def initialize(settings = {})
82
- @local_settings = DEFAULT_CONNECTION_SETTINGS.merge(settings)
83
- @remote_settings = SPEC_DEFAULT_CONNECTION_SETTINGS.dup
63
+ @local_settings = Settings.new(**settings)
64
+ @remote_settings = Settings.new(settings_max_concurrent_streams: Framer::MAX_STREAM_ID)
84
65
 
85
- @compressor = Header::Compressor.new(settings)
86
- @decompressor = Header::Decompressor.new(settings)
66
+ @compressor = Header::Compressor.new(@local_settings)
67
+ @decompressor = Header::Decompressor.new(@local_settings)
87
68
 
88
69
  @active_stream_count = 0
89
70
  @last_stream_id = 0
@@ -92,11 +73,11 @@ module HTTP2
92
73
  @oldest_stream_recently_closed = nil
93
74
  @pending_settings = []
94
75
 
95
- @framer = Framer.new(@local_settings[:settings_max_frame_size])
76
+ @framer = Framer.new(@local_settings.settings_max_frame_size)
96
77
 
97
- @local_window_limit = @local_settings[:settings_initial_window_size]
78
+ @local_window_limit = @local_settings.settings_initial_window_size
98
79
  @local_window = @local_window_limit
99
- @remote_window_limit = @remote_settings[:settings_initial_window_size]
80
+ @remote_window_limit = @remote_settings.settings_initial_window_size
100
81
  @remote_window = @remote_window_limit
101
82
 
102
83
  @recv_buffer = "".b
@@ -113,8 +94,15 @@ module HTTP2
113
94
  @send_buffer = FrameBuffer.new
114
95
  end
115
96
 
97
+ # No new streams may be opened (a GOAWAY was sent or received).
116
98
  def closed?
117
- @state == :closed
99
+ @state == :closed || @state == :closing
100
+ end
101
+
102
+ # Graceful GOAWAY received: tracked streams may still complete,
103
+ # but no new ones can be opened.
104
+ def closing?
105
+ @state == :closing
118
106
  end
119
107
 
120
108
  # Allocates new stream for current connection.
@@ -123,12 +111,15 @@ module HTTP2
123
111
  # @param window [Integer]
124
112
  # @param parent [Stream]
125
113
  def new_stream(**args)
126
- raise ConnectionClosed if @state == :closed
127
- raise StreamLimitExceeded if @active_stream_count >= @remote_settings[:settings_max_concurrent_streams]
114
+ raise ConnectionClosed if closed?
128
115
 
129
116
  connection_error(:protocol_error, msg: "id is smaller than previous") if @stream_id < @last_stream_id
130
117
 
131
- stream = activate_stream(id: @stream_id, **args)
118
+ stream = activate_stream(
119
+ id: @stream_id,
120
+ max_concurrent_streams: @remote_settings.settings_max_concurrent_streams,
121
+ **args
122
+ )
132
123
  @last_stream_id = stream.id
133
124
 
134
125
  @stream_id += 2
@@ -158,8 +149,7 @@ module HTTP2
158
149
  def goaway(error = :no_error, payload = nil)
159
150
  send(type: :goaway, stream: 0, last_stream: @last_stream_id,
160
151
  error: error, payload: payload)
161
- @state = :closed
162
- @closed_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
152
+ close!
163
153
  end
164
154
 
165
155
  # Sends a WINDOW_UPDATE frame to the peer.
@@ -203,7 +193,7 @@ module HTTP2
203
193
  elsif read_str(@recv_buffer, 24) == CONNECTION_PREFACE_MAGIC
204
194
  # MAGIC is OK. Send our settings
205
195
  @state = :waiting_connection_preface
206
- payload = @local_settings.reject { |k, v| v == SPEC_DEFAULT_CONNECTION_SETTINGS[k] }
196
+ payload = @local_settings.connection_settings.reject { |k, v| v == SPEC_DEFAULT_CONNECTION_SETTINGS[k] }
207
197
  settings(payload)
208
198
  else
209
199
  raise HandshakeError
@@ -244,7 +234,7 @@ module HTTP2
244
234
  # prevent HTTP/2 CONTINUATION FLOOD
245
235
  # same heuristic as the one from HAProxy: https://www.haproxy.com/blog/haproxy-is-resilient-to-the-http-2-continuation-flood
246
236
  # different mitigation (connection closed, instead of 400 response)
247
- unless @continuation_size < @local_settings[:settings_max_frame_size]
237
+ unless @continuation_size < @local_settings.settings_max_frame_size
248
238
  connection_error(:protocol_error,
249
239
  msg: "too many continuations received")
250
240
  end
@@ -295,7 +285,11 @@ module HTTP2
295
285
  # PUSH_PROMISE and CONTINUATION frames MUST be minimally
296
286
  # processed to ensure a consistent compression state
297
287
  decode_headers(frame)
298
- return if @state == :closed
288
+ # After a GOAWAY, HEADERS may only complete a tracked stream at
289
+ # or below the highest id seen (Section 6.8); anything else is
290
+ # discarded - decoded above, so the compression state is intact.
291
+ # Skip only this frame: later ones may serve completing streams.
292
+ next if closed? && (stream_id > @last_stream_id || !@streams.key?(stream_id))
299
293
 
300
294
  stream = @streams[stream_id]
301
295
  if stream.nil?
@@ -323,7 +317,7 @@ module HTTP2
323
317
  end
324
318
 
325
319
  decode_headers(frame)
326
- return if @state == :closed
320
+ next if closed?
327
321
 
328
322
  # PUSH_PROMISE frames MUST be associated with an existing, peer-
329
323
  # initiated stream... A receiver MUST treat the receipt of a
@@ -375,11 +369,16 @@ module HTTP2
375
369
  end
376
370
  else
377
371
  case frame_type
378
- # The PRIORITY frame can be sent for a stream in the "idle" or
379
- # "closed" state. This allows for the reprioritization of a
380
- # group of dependent streams by altering the priority of an
381
- # unused or closed parent stream.
372
+ # Priority signaling is deprecated (RFC 9113 Section 5.3.2), but
373
+ # a PRIORITY frame may still name a stream id not opened yet:
374
+ # activate it, so the priority sticks if the stream opens.
382
375
  when :priority
376
+ # After a GOAWAY, a PRIORITY for a stream already used and
377
+ # closed must not resurrect it: reprioritizing a closed
378
+ # parent is a no-op here, and a phantom stream would hold
379
+ # the draining connection open.
380
+ next if closed? && stream_id <= @last_stream_id
381
+
383
382
  stream = activate_stream(
384
383
  id: stream_id,
385
384
  weight: frame[:weight] || DEFAULT_WEIGHT,
@@ -495,7 +494,7 @@ module HTTP2
495
494
  @state = :connected
496
495
  connection_settings(frame)
497
496
 
498
- when :connected
497
+ when :connected, :closing
499
498
  case frame_type
500
499
  when :settings
501
500
  # @type var frame: settings_frame
@@ -510,8 +509,13 @@ module HTTP2
510
509
  # Receivers of a GOAWAY frame MUST NOT open additional streams on
511
510
  # the connection, although a new connection can be established
512
511
  # for new streams.
513
- @state = :closed
514
- @closed_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
512
+ if frame[:error] == :no_error && !@streams.empty?
513
+ # A graceful GOAWAY lets tracked streams complete before the
514
+ # connection closes (Section 6.8).
515
+ @state = :closing
516
+ else
517
+ close!
518
+ end
515
519
  emit(:goaway, frame[:last_stream], frame[:error], frame[:payload])
516
520
  when :altsvc
517
521
  # @type var frame: altsvc_frame
@@ -728,7 +732,7 @@ module HTTP2
728
732
  #: @type var payload: String
729
733
  headers_frame[:payload] = payload
730
734
 
731
- max_frame_size = @remote_settings[:settings_max_frame_size]
735
+ max_frame_size = @remote_settings.settings_max_frame_size
732
736
 
733
737
  # if single frame, return immediately
734
738
  if payload.bytesize <= max_frame_size
@@ -768,19 +772,28 @@ module HTTP2
768
772
  # connection managemet callbacks.
769
773
  #
770
774
  # @param id [Integer]
775
+ # @param max_concurrent_streams [Symbol] threshold to cap the number of active streams with.
771
776
  # @param priority [Integer]
772
777
  # @param window [Integer]
773
778
  # @param parent [Stream]
774
- def activate_stream(id:, **args)
779
+ def activate_stream(id:, max_concurrent_streams: @local_settings.settings_max_concurrent_streams, **args)
775
780
  connection_error(msg: "Stream ID already exists") if @streams.key?(id)
776
781
 
777
- raise StreamLimitExceeded if @active_stream_count >= @local_settings[:settings_max_concurrent_streams]
782
+ # SETTINGS_MAX_CONCURRENT_STREAMS limits the number of concurrent streams that the sender
783
+ # of the setting permits the receiver to create (RFC 9113, section 5.1.2), so the bounding
784
+ # limit is the one advertised by the endpoint that did *not* open the stream.
785
+ raise StreamLimitExceeded if @active_stream_count >= max_concurrent_streams
778
786
 
779
787
  stream = Stream.new(connection: self, id: id, **args)
780
788
 
781
789
  stream.once(:close) do
782
790
  @streams.delete(id)
783
791
 
792
+ # A graceful GOAWAY leaves the connection :closing until the
793
+ # tracked streams complete (Section 6.8); the last one to close
794
+ # moves it to its terminal state.
795
+ close! if @state == :closing && @streams.empty?
796
+
784
797
  # Store a reference to the closed stream, such that we can respond
785
798
  # to any in-flight frames while close is registered on both sides.
786
799
  # References to such streams will be purged whenever another stream
@@ -802,6 +815,8 @@ module HTTP2
802
815
  true
803
816
  end
804
817
  @oldest_stream_recently_closed = new_oldest
818
+ else
819
+ @oldest_stream_recently_closed ||= now
805
820
  end
806
821
 
807
822
  @streams_recently_closed[id] = now
@@ -811,6 +826,11 @@ module HTTP2
811
826
  @streams[id] = stream
812
827
  end
813
828
 
829
+ def close!
830
+ @state = :closed
831
+ @closed_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
832
+ end
833
+
814
834
  def verify_stream_order(id)
815
835
  return unless id.odd?
816
836
 
data/lib/http/2/error.rb CHANGED
@@ -53,7 +53,7 @@ module HTTP2
53
53
  # Raised if stream has been closed and new frames cannot be sent.
54
54
  class StreamClosed < Error; end
55
55
 
56
- # Raised if connection has been closed (or draining) and new stream
56
+ # Raised if connection has been closed (or closing) and new stream
57
57
  # cannot be opened.
58
58
  class ConnectionClosed < Error; end
59
59
 
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,16 @@ 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
14
+ # Set dynamic table +size+ in EncodingContext
17
15
  def table_size=(size)
18
16
  @cc.table_size = size
19
17
  end
20
18
 
21
- # Encodes provided value via integer representation.
19
+ # Encodes +i+ via integer representation into +buffer+ at the offset set by +offset+.
22
20
  # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-5.1
23
21
  #
24
22
  # If I < 2^N - 1, encode I on N bits
@@ -30,11 +28,6 @@ module HTTP2
30
28
  # I = I / 128
31
29
  # encode (I) on 8 bits
32
30
  #
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
31
  def integer(i, n, buffer:, offset: buffer.size)
39
32
  limit = (1 << n) - 1
40
33
  return pack([i], "C", buffer: buffer, offset: offset) if i < limit
@@ -65,16 +58,8 @@ module HTTP2
65
58
  # * If the bit 7 of the first byte is 0, the string value is
66
59
  # represented as a list of UTF-8 encoded octets.
67
60
  #
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
61
  def string(str, buffer = "".b)
77
- case @cc.options[:huffman]
62
+ case @cc.settings.huffman
78
63
  when :always
79
64
  huffman_string(str, buffer)
80
65
  when :never
@@ -93,11 +78,7 @@ module HTTP2
93
78
  end
94
79
  end
95
80
 
96
- # Encodes header command with appropriate header representation.
97
- #
98
- # @param h [Hash] header command
99
- # @param buffer [String]
100
- # @return [Buffer]
81
+ # Encodes +h+ header command with appropriate header representation into +buffer+.
101
82
  def header(h, buffer = "".b)
102
83
  type = h[:type]
103
84
  rep = HEADREP[type]
@@ -128,9 +109,6 @@ module HTTP2
128
109
  end
129
110
 
130
111
  # Encodes provided list of HTTP headers.
131
- #
132
- # @param headers [Array] +[[name, value], ...]+
133
- # @return [Buffer]
134
112
  def encode(headers)
135
113
  buffer = "".b
136
114
  headers.partition { |f, _| f.start_with? ":" }.each do |hs|
@@ -144,9 +122,7 @@ module HTTP2
144
122
 
145
123
  private
146
124
 
147
- # @param str [String]
148
- # @param buffer [String]
149
- # @return [String] binary string
125
+ # encodes +str+ into +buffer+ using Huffman encoding.
150
126
  def huffman_string(str, buffer = "".b)
151
127
  huffman_offset = buffer.bytesize
152
128
  buffer << "\x00".b
@@ -162,18 +138,14 @@ module HTTP2
162
138
  buffer
163
139
  end
164
140
 
165
- # @param str [String]
166
- # @param buffer [String]
167
- # @return [String] binary string
141
+ # encodes +str+ into +buffer+.
168
142
  def plain_string(str, plain = "".b)
169
143
  integer(str.bytesize, 7, buffer: plain)
170
144
  append_str(plain, str)
171
145
  plain
172
146
  end
173
147
 
174
- # @param buffer [String]
175
- # @param huffman_offset [Integer] buffer offset where huffman string was introduced
176
- # @return [String] binary string
148
+ # encodes the huffman string size from +buffer+ into the string at the offset indicated by +huffman_offset+
177
149
  def set_huffman_size(buffer, huffman_offset)
178
150
  integer(buffer.bytesize - huffman_offset, 7, buffer: buffer, offset: huffman_offset)
179
151
  buffer.setbyte(huffman_offset, buffer.getbyte(huffman_offset) | 0x80)
@@ -5,32 +5,22 @@ 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
+ def initialize(settings = Settings.new)
15
+ @cc = EncodingContext.new(settings)
21
16
  end
22
17
 
23
- # Set dynamic table size in EncodingContext
24
- # @param size [Integer] new dynamic table size
18
+ # Set dynamic table +size+ in EncodingContext
25
19
  def table_size=(size)
26
20
  @cc.table_size = size
27
21
  end
28
22
 
29
- # Decodes integer value from provided buffer.
30
- #
31
- # @param buf [String]
32
- # @param n [Integer] number of available bits
33
- # @return [Integer]
23
+ # Decodes integer value from provided +buffer+.
34
24
  def integer(buf, n)
35
25
  limit = (1 << n) - 1
36
26
  if n.zero?
@@ -55,11 +45,9 @@ module HTTP2
55
45
  i
56
46
  end
57
47
 
58
- # Decodes string value from provided buffer.
48
+ # Decodes string value from provided +buf+.
59
49
  #
60
- # @param buf [String]
61
- # @return [String] UTF-8 encoded string
62
- # @raise [CompressionError] when input is malformed
50
+ # raises CompressionError when input is malformed
63
51
  def string(buf)
64
52
  raise CompressionError, "invalid header block fragment" if buf.empty?
65
53
 
@@ -72,10 +60,7 @@ module HTTP2
72
60
  str.force_encoding(Encoding::UTF_8)
73
61
  end
74
62
 
75
- # Decodes header command from provided buffer.
76
- #
77
- # @param buf [Buffer]
78
- # @return [Hash] command
63
+ # Decodes header command from provided +buf+.
79
64
  def header(buf)
80
65
  peek = buf.getbyte(0)
81
66
 
@@ -109,13 +94,9 @@ module HTTP2
109
94
  end
110
95
  end
111
96
 
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], ...]
97
+ # Decodes and processes header commands within provided +buf+.
117
98
  def decode(buf, frame = nil)
118
- list = []
99
+ list = [] #: Array[header_pair]
119
100
  decoding_pseudo_headers = true
120
101
  @cc.listen_on_table do
121
102
  until buf.empty?
@@ -86,12 +86,6 @@ module HTTP2
86
86
 
87
87
  STATIC_TABLE_SIZE = STATIC_TABLE.size
88
88
 
89
- DEFAULT_OPTIONS = {
90
- huffman: :shorter,
91
- index: :all,
92
- table_size: 4096
93
- }.freeze
94
-
95
89
  STATIC_ALL = %i[all static].freeze
96
90
 
97
91
  STATIC_NEVER = %i[never static].freeze
@@ -99,37 +93,27 @@ module HTTP2
99
93
  # Current table of header key-value pairs.
100
94
  attr_reader :table
101
95
 
102
- # Current encoding options
103
- #
104
- # :table_size Integer maximum dynamic table size in bytes
105
- # :huffman Symbol :always, :never, :shorter
106
- # :index Symbol :all, :static, :never
107
- attr_reader :options
96
+ # Current encoding settings
97
+ attr_reader :settings
108
98
 
109
99
  # Current table size in octets
110
100
  attr_reader :current_table_size
111
101
 
112
102
  # Initializes compression context with appropriate client/server
113
- # defaults and maximum size of the dynamic table.
114
- #
115
- # @param options [Hash] encoding options
116
- # :table_size Integer maximum dynamic table size in bytes
117
- # :huffman Symbol :always, :never, :shorter
118
- # :index Symbol :all, :static, :never
119
- def initialize(options = {})
103
+ # +settings+ and maximum size of the dynamic table.
104
+ def initialize(settings = Settings.new)
120
105
  @table = []
121
106
  @table_by_field = Hash.new { |hs, k| hs[k] = [] }
122
107
  @unshifts = 0
123
- @options = DEFAULT_OPTIONS.merge(options)
124
- @limit = @options[:table_size]
108
+ @settings = settings
109
+ @limit = settings.table_size
125
110
  @_table_updated = false
126
111
  @current_table_size = 0
127
112
  end
128
113
 
129
114
  # Duplicates current compression context
130
- # @return [EncodingContext]
131
115
  def dup
132
- other = EncodingContext.new(@options)
116
+ other = EncodingContext.new(@settings)
133
117
  t = @table
134
118
  tbf = @table_by_field.transform_values(&:dup)
135
119
  unshifts = @unshifts
@@ -143,16 +127,13 @@ module HTTP2
143
127
  other
144
128
  end
145
129
 
146
- # Finds an entry in current dynamic table by index.
147
- # Note that index is zero-based in this module.
130
+ # Finds an entry in current dynamic table by +index+.
131
+ # Note that +index+ is zero-based in this module.
148
132
  #
149
- # If the index is greater than the last index in the static table,
133
+ # If the +index+ is greater than the last index in the static table,
150
134
  # an entry in the dynamic table is dereferenced.
151
135
  #
152
- # If the index is greater than the last header index, an error is raised.
153
- #
154
- # @param index [Integer] zero-based index in the dynamic table.
155
- # @return [Array] +[key, value]+
136
+ # If the +index+ is greater than the last header index, an error is raised.
156
137
  def dereference(index)
157
138
  # NOTE: index is zero-based in this module.
158
139
  return STATIC_TABLE[index] if index < STATIC_TABLE_SIZE
@@ -166,10 +147,6 @@ module HTTP2
166
147
 
167
148
  # Header Block Processing
168
149
  # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-4.1
169
- #
170
- # @param cmd [Hash] { type:, name:, value:, index: }
171
- # @return [Array, nil] +[name, value]+ header field that is added to the decoded header list,
172
- # or nil if +cmd[:type]+ is +:changetablesize+
173
150
  def process(cmd)
174
151
  type = cmd[:type]
175
152
  name = cmd[:name]
@@ -181,7 +158,7 @@ module HTTP2
181
158
 
182
159
  # we can receive multiple table size change commands inside a header frame. However,
183
160
  # we should blow up if we receive another frame where the new table size is bigger.
184
- table_size_updated = @limit != @options[:table_size]
161
+ table_size_updated = @limit != @settings.table_size
185
162
 
186
163
  raise CompressionError, "dynamic table size update exceed limit" if !table_size_updated && value > @limit
187
164
 
@@ -217,11 +194,12 @@ module HTTP2
217
194
  emit = [name, value]
218
195
 
219
196
  # add to table
220
- if type == :incremental && size_check?(name.bytesize + value.bytesize + 32)
197
+ cmdsize = name.bytesize + value.bytesize + 32
198
+ if type == :incremental && size_check?(cmdsize)
221
199
  @table.unshift(emit)
222
200
  @unshifts += 1
223
201
  @table_by_field[name].unshift([value, @unshifts])
224
- @current_table_size += name.bytesize + value.bytesize + 32
202
+ @current_table_size += cmdsize
225
203
  @_table_updated = true
226
204
  end
227
205
 
@@ -231,16 +209,10 @@ module HTTP2
231
209
  end
232
210
  end
233
211
 
234
- # Plan header compression according to +@options [:index]+
235
- # :never Do not use dynamic table or static table reference at all.
236
- # :static Use static table only.
237
- # :all Use all of them.
238
- #
239
- # @param headers [Array] +[[name, value], ...]+
240
- # @return [Array] array of commands
212
+ # Plan +headers+ compression.
241
213
  def encode(headers)
242
214
  # Literals commands are marked with :noindex when index is not used
243
- noindex = STATIC_NEVER.include?(@options[:index])
215
+ noindex = STATIC_NEVER.include?(@settings.index)
244
216
 
245
217
  headers.each do |field, value|
246
218
  # Literal header names MUST be translated to lowercase before
@@ -254,26 +226,17 @@ module HTTP2
254
226
  end
255
227
  end
256
228
 
257
- # Emits command for a header.
229
+ # Emits command for a +field+/+value+ header.
258
230
  # Prefer static table over dynamic table.
259
231
  # Prefer exact match over name-only match.
260
232
  #
261
- # +@options [:index]+ controls whether to use the dynamic table,
262
- # static table, or both.
263
- # :never Do not use dynamic table or static table reference at all.
264
- # :static Use static table only.
265
- # :all Use all of them.
266
- #
267
- # @param field [String] the header field
268
- # @param value [String] the header value
269
- # @return [Hash] command
270
233
  def addcmd(field, value)
271
234
  # @type var exact: Integer?
272
235
  exact = nil
273
236
  # @type var name_only: Integer?
274
237
  name_only = nil
275
238
 
276
- index_type = @options[:index]
239
+ index_type = @settings.index
277
240
 
278
241
  if STATIC_ALL.include?(index_type) &&
279
242
  STATIC_TABLE_BY_FIELD.key?(field)
@@ -337,11 +300,10 @@ module HTTP2
337
300
  end
338
301
  end
339
302
 
303
+ # whether +cmd+ fits in the dynamic table.
304
+ #
340
305
  # To keep the dynamic table size lower than or equal to @limit,
341
306
  # remove one or more entries at the end of the dynamic table.
342
- #
343
- # @param cmdsize [Integer]
344
- # @return [Boolean] whether +cmd+ fits in the dynamic table.
345
307
  def size_check?(cmdsize)
346
308
  resize_table(cmdsize)
347
309
  cmdsize <= @limit
@@ -22,12 +22,7 @@ module HTTP2
22
22
 
23
23
  EOS_PADDING = (0..7).map { |n| ("1" * n).b.freeze }.freeze
24
24
 
25
- # Encodes provided value via huffman encoding.
26
- # Length is not encoded in this method.
27
- #
28
- # @param str [String]
29
- # @param buffer [String]
30
- # @return [String] binary string
25
+ # Encodes provided +str+ via huffman encoding into +buffer+ .
31
26
  def encode(str, buffer = "".b)
32
27
  bitstring = String.new("", encoding: Encoding::BINARY, capacity: (str.bytesize * 30) + ((8 - str.size) % 8))
33
28
  str.each_byte { |chr| append_str(bitstring, ENCODE_TABLE[chr]) }
@@ -35,11 +30,9 @@ module HTTP2
35
30
  pack([bitstring], "B*", buffer: buffer)
36
31
  end
37
32
 
38
- # Decodes provided Huffman coded string.
33
+ # Decodes provided Huffman coded string in +buf+.
39
34
  #
40
- # @param buf [Buffer]
41
- # @return [String] binary string
42
- # @raise [CompressionError] when Huffman coded string is malformed
35
+ # raises CompressionError when Huffman coded string is malformed
43
36
  def decode(buf)
44
37
  emit = "".b
45
38
  state = 0 # start state
data/lib/http/2/server.rb CHANGED
@@ -97,10 +97,10 @@ module HTTP2
97
97
  dependency: 0,
98
98
  exclusive: false,
99
99
  payload: headers
100
- }
100
+ } #: headers_frame
101
101
 
102
102
  if body.empty?
103
- headers_frame[:flags] |= END_HEADERS
103
+ headers_frame[:flags] |= END_STREAM
104
104
  stream << headers_frame
105
105
  else
106
106
  stream << headers_frame
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTP2
4
+ MAX_HEADER_LIST_SIZE = (2 << 30) - 1
5
+
4
6
  # Default values for SETTINGS frame, as defined by the spec.
5
7
  SPEC_DEFAULT_CONNECTION_SETTINGS = {
6
8
  settings_header_table_size: 4096,
@@ -8,32 +10,46 @@ module HTTP2
8
10
  settings_max_concurrent_streams: Framer::MAX_STREAM_ID, # unlimited
9
11
  settings_initial_window_size: 65_535,
10
12
  settings_max_frame_size: 16_384,
11
- settings_max_header_list_size: (2 << 30) - 1 # unlimited
13
+ settings_max_header_list_size: MAX_HEADER_LIST_SIZE # unlimited
12
14
  }.freeze
13
15
 
16
+ ENCODING_CONTEXT_SETTINGS = %i[huffman index table_size].freeze
17
+
14
18
  Settings = Struct.new(
19
+ # connection settings
15
20
  :settings_header_table_size,
16
21
  :settings_enable_push,
17
22
  :settings_max_concurrent_streams,
18
23
  :settings_initial_window_size,
19
24
  :settings_max_frame_size,
20
25
  :settings_max_header_list_size,
26
+ # encoding context settings
27
+ :huffman,
28
+ :index,
29
+ :table_size,
21
30
  keyword_init: true
22
31
  ) do
23
32
  def initialize(
33
+ # connection settings
24
34
  settings_header_table_size: 4096,
25
35
  settings_enable_push: 1,
26
36
  settings_max_concurrent_streams: 100,
27
37
  settings_initial_window_size: 65_535,
28
38
  settings_max_frame_size: 16_384,
29
- settings_max_header_list_size: (2 << 30) - 1
39
+ settings_max_header_list_size: MAX_HEADER_LIST_SIZE,
40
+ # encoding context settings
41
+ huffman: :shorter,
42
+ index: :all,
43
+ table_size: 4096
30
44
  )
31
45
  super
32
46
  end
33
47
 
34
- def each_setting
48
+ def connection_settings
49
+ return enum_for(__method__) unless block_given?
50
+
35
51
  each_pair do |k, v|
36
- next if v == SPEC_DEFAULT_CONNECTION_SETTINGS[k]
52
+ next if ENCODING_CONTEXT_SETTINGS.include?(k)
37
53
 
38
54
  yield k, v
39
55
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTP2
4
- VERSION = "1.2.1"
4
+ VERSION = "1.2.3"
5
5
  end
data/lib/http/2.rb CHANGED
@@ -13,6 +13,7 @@ require "http/2/emitter"
13
13
  require "http/2/flow_buffer"
14
14
  require "http/2/header"
15
15
  require "http/2/framer"
16
+ require "http/2/settings"
16
17
  require "http/2/connection"
17
18
  require "http/2/client"
18
19
  require "http/2/server"
data/sig/2.rbs CHANGED
@@ -11,23 +11,10 @@ module HTTP2
11
11
 
12
12
  type connection_opts = Hash[Symbol, untyped]
13
13
 
14
- type settings_hash = {
15
- settings_header_table_size: Integer,
16
- settings_enable_push: Integer,
17
- settings_max_concurrent_streams: Integer,
18
- settings_initial_window_size: Integer,
19
- settings_max_frame_size: Integer,
20
- settings_max_header_list_size: Integer
21
- }
22
-
23
14
  type settings_ary = Array[settings_enum]
24
15
 
25
16
  type settings_enum = Enumerable[[Symbol, Integer]]
26
17
 
27
- SPEC_DEFAULT_CONNECTION_SETTINGS: settings_hash
28
-
29
- DEFAULT_CONNECTION_SETTINGS: settings_hash
30
-
31
18
  DEFAULT_WEIGHT: Integer
32
19
 
33
20
  CONNECTION_PREFACE_MAGIC: String
@@ -36,6 +23,12 @@ module HTTP2
36
23
 
37
24
  RESPONSE_MANDATORY_HEADERS: Array[String]
38
25
 
26
+ CONNECTION_FRAME_TYPES: Array[Symbol]
27
+
28
+ STREAM_OPEN_STATES: Array[Symbol]
29
+
30
+ HEADERS_FRAME_TYPES: Array[Symbol]
31
+
39
32
  # Frame flags
40
33
  END_STREAM: Integer
41
34
  ACK: Integer
data/sig/connection.rbs CHANGED
@@ -4,16 +4,6 @@ module HTTP2
4
4
  include Emitter
5
5
  include BufferUtils
6
6
 
7
- REQUEST_MANDATORY_HEADERS: Array[String]
8
-
9
- RESPONSE_MANDATORY_HEADERS: Array[String]
10
-
11
- CONNECTION_FRAME_TYPES: Array[Symbol]
12
-
13
- HEADERS_FRAME_TYPES: Array[Symbol]
14
-
15
- STREAM_OPEN_STATES: Array[Symbol]
16
-
17
7
  attr_reader state: Symbol
18
8
 
19
9
  attr_reader local_window: Integer
@@ -21,8 +11,8 @@ module HTTP2
21
11
 
22
12
  alias window local_window
23
13
 
24
- attr_reader remote_settings: settings_hash
25
- attr_reader local_settings: settings_hash
14
+ attr_reader remote_settings: Settings
15
+ attr_reader local_settings: Settings
26
16
  attr_reader pending_settings: settings_ary
27
17
 
28
18
  attr_accessor active_stream_count: Integer
@@ -59,6 +49,10 @@ module HTTP2
59
49
 
60
50
  def closed?: () -> bool
61
51
 
52
+ def closing?: () -> bool
53
+
54
+ def close!: () -> void
55
+
62
56
  def new_stream: (**untyped) -> Stream
63
57
 
64
58
  def ping: (String) -> void
@@ -83,7 +77,7 @@ module HTTP2
83
77
 
84
78
  def connection_frame?: (frame) -> bool
85
79
 
86
- def connection_management: (connection_frame frame) -> void
80
+ def connection_management: (frame frame) -> void
87
81
 
88
82
  def ping_management: (frame) -> void
89
83
 
@@ -95,7 +89,7 @@ module HTTP2
95
89
 
96
90
  def encode_headers: (headers_frame | push_promise_frame) -> void
97
91
 
98
- def activate_stream: (id: Integer, **untyped) -> Stream
92
+ def activate_stream: (id: Integer, ?max_concurrent_streams: Integer, **untyped) -> Stream
99
93
 
100
94
  def verify_stream_order: (Integer id) -> void
101
95
 
@@ -18,7 +18,7 @@ module HTTP2
18
18
 
19
19
  private
20
20
 
21
- def initialize: (?connection_opts options) -> void
21
+ def initialize: (?Settings settings) -> void
22
22
 
23
23
  def huffman_string: (String str, ?String buffer) -> String
24
24
 
@@ -19,7 +19,7 @@ module HTTP2
19
19
  | (String) -> Array[header_pair]
20
20
  private
21
21
 
22
- def initialize: (?connection_opts options) -> void
22
+ def initialize: (?Settings settings) -> void
23
23
  end
24
24
  end
25
25
  end
@@ -11,14 +11,12 @@ module HTTP2
11
11
 
12
12
  STATIC_NEVER: Array[Symbol]
13
13
 
14
- DEFAULT_OPTIONS: context_hash
15
-
16
14
  UPPER: Regexp
17
15
 
18
16
 
19
17
  attr_reader table: Array[header_pair]
20
18
 
21
- attr_reader options: context_hash
19
+ attr_reader settings: Settings
22
20
 
23
21
  attr_reader current_table_size: Integer
24
22
 
@@ -46,7 +44,7 @@ module HTTP2
46
44
 
47
45
  private
48
46
 
49
- def initialize: (?connection_opts options) -> void
47
+ def initialize: (?Settings settings) -> void
50
48
 
51
49
  def add_to_table: (string name, string value) -> void
52
50
 
data/sig/server.rbs CHANGED
@@ -1,5 +1,7 @@
1
1
  module HTTP2
2
2
  class Server < Connection
3
+ @origin_set: Array[String]
4
+ @origins_sent: bool
3
5
 
4
6
  def upgrade: (String settings, Enumerable[header_pair] headers, String body) -> void
5
7
 
data/sig/settings.rbs ADDED
@@ -0,0 +1,71 @@
1
+ module HTTP2
2
+ MAX_HEADER_LIST_SIZE: Integer
3
+
4
+ SPEC_DEFAULT_CONNECTION_SETTINGS: Hash[Symbol, Integer]
5
+
6
+ ENCODING_CONTEXT_SETTINGS: Array[Symbol]
7
+
8
+ type huffman_type = :always | :never | :shorter
9
+
10
+ type index_type = :all | :static | :never
11
+
12
+ # Container for all settings required for the management of an HTTP/2 connection. This
13
+ # includes the managemenet of the settings negotiated via the SETTINGS frame, as well as
14
+ # user-level configuration related with header compression.
15
+ class Settings # this is actually a Struct, but RBS does not support it yet
16
+ # connection settings (send and received in the SETTINGS frame)
17
+
18
+ # maximum size of the header compression table used to decode header blocks, in bytes.
19
+ attr_reader settings_header_table_size: Integer
20
+
21
+ # can be used to disable server push (set to 0)
22
+ attr_reader settings_enable_push: Integer
23
+
24
+ # maximum number of concurrent streams that the sender will allow.
25
+ attr_reader settings_max_concurrent_streams: Integer
26
+
27
+ # initial window size (in bytes) for stream-level flow control.
28
+ attr_reader settings_initial_window_size: Integer
29
+
30
+ # max size of for payload that the sender is willing to receive, in bytes.
31
+ attr_reader settings_max_frame_size: Integer
32
+ attr_reader settings_max_header_list_size: Integer
33
+
34
+ # encoding context settings
35
+
36
+ # controls how to use huffman encoding to encode strings. can be one of the following values:
37
+ #
38
+ # * `:always` - Always use Huffman encoding
39
+ # * `:never` - Do not use Huffman encoding
40
+ # * `:shorter` - Use Huffman when the result is strictly shorter
41
+ attr_reader huffman: huffman_type
42
+
43
+ # controls the configuration of the HPACK indexing tables:
44
+ #
45
+ # * `:static` - use the static table only
46
+ # * `:all` - use the static table and the dynamic table
47
+ # * `:never` - do not use any of the tables.
48
+ attr_reader index: index_type
49
+
50
+ # maximum dynamic table size in bytes
51
+ attr_reader table_size: Integer
52
+
53
+ def initialize: (
54
+ # connection settings
55
+ ?settings_header_table_size: Integer,
56
+ ?settings_enable_push: Integer,
57
+ ?settings_max_concurrent_streams: Integer,
58
+ ?settings_initial_window_size: Integer,
59
+ ?settings_max_frame_size: Integer,
60
+ ?settings_max_header_list_size: Integer,
61
+ # encoding context settings
62
+ ?huffman: huffman_type,
63
+ ?index: index_type,
64
+ ?table_size: Integer
65
+ ) -> void
66
+
67
+ # yields connection-level settings only.
68
+ def connection_settings: () { (Symbol, Integer) -> void } -> self
69
+ | () -> Enumerable[[Symbol, Integer]]
70
+ end
71
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: http-2
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
@@ -19,6 +19,7 @@ executables: []
19
19
  extensions: []
20
20
  extra_rdoc_files: []
21
21
  files:
22
+ - LICENSE
22
23
  - README.md
23
24
  - lib/http/2.rb
24
25
  - lib/http/2/base64.rb
@@ -54,6 +55,7 @@ files:
54
55
  - sig/header/encoding_context.rbs
55
56
  - sig/header/huffman.rbs
56
57
  - sig/server.rbs
58
+ - sig/settings.rbs
57
59
  - sig/stream.rbs
58
60
  homepage: https://github.com/igrigorik/http-2
59
61
  licenses:
@@ -78,7 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
78
80
  - !ruby/object:Gem::Version
79
81
  version: '0'
80
82
  requirements: []
81
- rubygems_version: 3.6.9
83
+ rubygems_version: 4.0.16
82
84
  specification_version: 4
83
85
  summary: Pure-ruby HTTP 2.0 protocol implementation
84
86
  test_files: []