http-2 0.11.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -9
  3. data/lib/http/2/base64.rb +45 -0
  4. data/lib/http/2/client.rb +19 -6
  5. data/lib/http/2/connection.rb +235 -163
  6. data/lib/http/2/emitter.rb +7 -5
  7. data/lib/http/2/error.rb +24 -1
  8. data/lib/http/2/extensions.rb +53 -0
  9. data/lib/http/2/flow_buffer.rb +91 -33
  10. data/lib/http/2/framer.rb +184 -157
  11. data/lib/http/2/header/compressor.rb +157 -0
  12. data/lib/http/2/header/decompressor.rb +144 -0
  13. data/lib/http/2/header/encoding_context.rb +337 -0
  14. data/lib/http/2/{huffman.rb → header/huffman.rb} +25 -19
  15. data/lib/http/2/{huffman_statemachine.rb → header/huffman_statemachine.rb} +2 -0
  16. data/lib/http/2/header.rb +35 -0
  17. data/lib/http/2/server.rb +47 -20
  18. data/lib/http/2/stream.rb +130 -61
  19. data/lib/http/2/version.rb +3 -1
  20. data/lib/http/2.rb +14 -13
  21. data/sig/client.rbs +9 -0
  22. data/sig/connection.rbs +93 -0
  23. data/sig/emitter.rbs +13 -0
  24. data/sig/error.rbs +35 -0
  25. data/sig/extensions.rbs +5 -0
  26. data/sig/flow_buffer.rbs +21 -0
  27. data/sig/frame_buffer.rbs +13 -0
  28. data/sig/framer.rbs +54 -0
  29. data/sig/header/compressor.rbs +27 -0
  30. data/sig/header/decompressor.rbs +22 -0
  31. data/sig/header/encoding_context.rbs +34 -0
  32. data/sig/header/huffman.rbs +9 -0
  33. data/sig/header.rbs +27 -0
  34. data/sig/next.rbs +101 -0
  35. data/sig/server.rbs +12 -0
  36. data/sig/stream.rbs +91 -0
  37. metadata +38 -79
  38. data/.autotest +0 -20
  39. data/.coveralls.yml +0 -1
  40. data/.gitignore +0 -20
  41. data/.gitmodules +0 -3
  42. data/.rspec +0 -5
  43. data/.rubocop.yml +0 -93
  44. data/.rubocop_todo.yml +0 -131
  45. data/.travis.yml +0 -17
  46. data/Gemfile +0 -16
  47. data/Guardfile +0 -18
  48. data/Guardfile.h2spec +0 -12
  49. data/LICENSE +0 -21
  50. data/Rakefile +0 -49
  51. data/example/Gemfile +0 -3
  52. data/example/README.md +0 -44
  53. data/example/client.rb +0 -122
  54. data/example/helper.rb +0 -19
  55. data/example/keys/server.crt +0 -20
  56. data/example/keys/server.key +0 -27
  57. data/example/server.rb +0 -139
  58. data/example/upgrade_client.rb +0 -153
  59. data/example/upgrade_server.rb +0 -203
  60. data/http-2.gemspec +0 -22
  61. data/lib/http/2/buffer.rb +0 -76
  62. data/lib/http/2/compressor.rb +0 -572
  63. data/lib/tasks/generate_huffman_table.rb +0 -166
  64. data/spec/buffer_spec.rb +0 -28
  65. data/spec/client_spec.rb +0 -188
  66. data/spec/compressor_spec.rb +0 -666
  67. data/spec/connection_spec.rb +0 -681
  68. data/spec/emitter_spec.rb +0 -54
  69. data/spec/framer_spec.rb +0 -487
  70. data/spec/h2spec/h2spec.darwin +0 -0
  71. data/spec/h2spec/output/non_secure.txt +0 -317
  72. data/spec/helper.rb +0 -147
  73. data/spec/hpack_test_spec.rb +0 -84
  74. data/spec/huffman_spec.rb +0 -68
  75. data/spec/server_spec.rb +0 -52
  76. data/spec/stream_spec.rb +0 -878
  77. data/spec/support/deep_dup.rb +0 -55
  78. data/spec/support/duplicable.rb +0 -98
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP2
4
+ module Header
5
+ # Responsible for encoding header key-value pairs using HPACK algorithm.
6
+ class Compressor
7
+ include PackingExtensions
8
+
9
+ # @param options [Hash] encoding options
10
+ def initialize(options = {})
11
+ @cc = EncodingContext.new(options)
12
+ end
13
+
14
+ # Set dynamic table size in EncodingContext
15
+ # @param size [Integer] new dynamic table size
16
+ def table_size=(size)
17
+ @cc.table_size = size
18
+ end
19
+
20
+ # Encodes provided value via integer representation.
21
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-5.1
22
+ #
23
+ # If I < 2^N - 1, encode I on N bits
24
+ # Else
25
+ # encode 2^N - 1 on N bits
26
+ # I = I - (2^N - 1)
27
+ # While I >= 128
28
+ # Encode (I % 128 + 128) on 8 bits
29
+ # I = I / 128
30
+ # encode (I) on 8 bits
31
+ #
32
+ # @param i [Integer] value to encode
33
+ # @param n [Integer] number of available bits
34
+ # @param buffer [String] buffer to pack bytes into
35
+ # @param offset [Integer] offset to insert packed bytes in buffer
36
+ # @return [String] binary string
37
+ def integer(i, n, buffer:, offset: 0)
38
+ limit = (2**n) - 1
39
+ return pack([i], "C", buffer: buffer, offset: offset) if i < limit
40
+
41
+ bytes = []
42
+ bytes.push limit unless n.zero?
43
+
44
+ i -= limit
45
+ while i >= 128
46
+ bytes.push((i % 128) + 128)
47
+ i /= 128
48
+ end
49
+
50
+ bytes.push i
51
+ pack(bytes, "C*", buffer: buffer, offset: offset)
52
+ end
53
+
54
+ # Encodes provided value via string literal representation.
55
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-5.2
56
+ #
57
+ # * The string length, defined as the number of bytes needed to store
58
+ # its UTF-8 representation, is represented as an integer with a seven
59
+ # bits prefix. If the string length is strictly less than 127, it is
60
+ # represented as one byte.
61
+ # * If the bit 7 of the first byte is 1, the string value is represented
62
+ # as a list of Huffman encoded octets
63
+ # (padded with bit 1's until next octet boundary).
64
+ # * If the bit 7 of the first byte is 0, the string value is
65
+ # represented as a list of UTF-8 encoded octets.
66
+ #
67
+ # +@options [:huffman]+ controls whether to use Huffman encoding:
68
+ # :never Do not use Huffman encoding
69
+ # :always Always use Huffman encoding
70
+ # :shorter Use Huffman when the result is strictly shorter
71
+ #
72
+ # @param str [String]
73
+ # @return [String] binary string
74
+ def string(str)
75
+ case @cc.options[:huffman]
76
+ when :always
77
+ huffman_string(str)
78
+ when :never
79
+ plain_string(str)
80
+ else
81
+ huffman = huffman_string(str)
82
+
83
+ plain = plain_string(str)
84
+
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 = "".b)
95
+ rep = HEADREP[h[:type]]
96
+
97
+ case h[:type]
98
+ when :indexed
99
+ integer(h[:name] + 1, rep[:prefix], buffer: buffer)
100
+ when :changetablesize
101
+ integer(h[:value], rep[:prefix], buffer: buffer)
102
+ else
103
+ if h[:name].is_a? Integer
104
+ integer(h[:name] + 1, rep[:prefix], buffer: buffer)
105
+ else
106
+ integer(0, rep[:prefix], buffer: buffer)
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 = "".b
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
+
136
+ private
137
+
138
+ # @param str [String]
139
+ # @return [String] binary string
140
+ def huffman_string(str)
141
+ huffman = Huffman.new.encode(str)
142
+ integer(huffman.bytesize, 7, buffer: huffman, offset: 0)
143
+ huffman.setbyte(0, huffman.ord | 0x80)
144
+ huffman
145
+ end
146
+
147
+ # @param str [String]
148
+ # @return [String] binary string
149
+ def plain_string(str)
150
+ plain = "".b
151
+ integer(str.bytesize, 7, buffer: plain)
152
+ plain << str.dup.force_encoding(Encoding::BINARY)
153
+ plain
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP2
4
+ module Header
5
+ using StringExtensions
6
+ # Responsible for decoding received headers and maintaining compression
7
+ # context of the opposing peer. Decompressor must be initialized with
8
+ # appropriate starting context based on local role: client or server.
9
+ #
10
+ # @example
11
+ # server_role = Decompressor.new(:request)
12
+ # client_role = Decompressor.new(:response)
13
+ class Decompressor
14
+ include Error
15
+
16
+ # @param options [Hash] decoding options. Only :table_size is effective.
17
+ def initialize(options = {})
18
+ @cc = EncodingContext.new(options)
19
+ end
20
+
21
+ # Set dynamic table size in EncodingContext
22
+ # @param size [Integer] new dynamic table size
23
+ def table_size=(size)
24
+ @cc.table_size = size
25
+ end
26
+
27
+ # Decodes integer value from provided buffer.
28
+ #
29
+ # @param buf [String]
30
+ # @param n [Integer] number of available bits
31
+ # @return [Integer]
32
+ def integer(buf, n)
33
+ limit = (2**n) - 1
34
+ i = n.zero? ? 0 : (buf.shift_byte & limit)
35
+
36
+ m = 0
37
+ if i == limit
38
+ while (byte = buf.shift_byte)
39
+ i += ((byte & 127) << m)
40
+ m += 7
41
+
42
+ break if (byte & 128).zero?
43
+ end
44
+ end
45
+
46
+ i
47
+ end
48
+
49
+ # Decodes string value from provided buffer.
50
+ #
51
+ # @param buf [String]
52
+ # @return [String] UTF-8 encoded string
53
+ # @raise [CompressionError] when input is malformed
54
+ def string(buf)
55
+ raise CompressionError, "invalid header block fragment" if buf.empty?
56
+
57
+ huffman = (buf.getbyte(0) & 0x80) == 0x80
58
+ len = integer(buf, 7)
59
+ str = buf.read(len)
60
+ raise CompressionError, "string too short" unless str.bytesize == len
61
+
62
+ str = Huffman.new.decode(str) if huffman
63
+ str.force_encoding(Encoding::UTF_8)
64
+ end
65
+
66
+ # Decodes header command from provided buffer.
67
+ #
68
+ # @param buf [Buffer]
69
+ # @return [Hash] command
70
+ def header(buf)
71
+ peek = buf.getbyte(0)
72
+
73
+ header = {}
74
+ header[:type], type = HEADREP.find do |_t, desc|
75
+ mask = (peek >> desc[:prefix]) << desc[:prefix]
76
+ mask == desc[:pattern]
77
+ end
78
+
79
+ raise CompressionError unless header[:type]
80
+
81
+ header[:name] = integer(buf, type[:prefix])
82
+
83
+ case header[:type]
84
+ when :indexed
85
+ raise CompressionError if (header[:name]).zero?
86
+
87
+ header[:name] -= 1
88
+ when :changetablesize
89
+ header[:value] = header[:name]
90
+ else
91
+ if (header[:name]).zero?
92
+ header[:name] = string(buf)
93
+ else
94
+ header[:name] -= 1
95
+ end
96
+ header[:value] = string(buf)
97
+ end
98
+
99
+ header
100
+ end
101
+
102
+ FORBIDDEN_HEADERS = %w[connection te].freeze
103
+
104
+ # Decodes and processes header commands within provided buffer.
105
+ #
106
+ # @param buf [Buffer]
107
+ # @param frame [HTTP2::Frame, nil]
108
+ # @return [Array] +[[name, value], ...]
109
+ def decode(buf, frame = nil)
110
+ list = []
111
+ decoding_pseudo_headers = true
112
+ @cc.listen_on_table do
113
+ until buf.empty?
114
+ field, value = @cc.process(header(buf))
115
+ next if field.nil?
116
+
117
+ is_pseudo_header = field.start_with? ":"
118
+ if !decoding_pseudo_headers && is_pseudo_header
119
+ raise ProtocolError, "one or more pseudo headers encountered after regular headers"
120
+ end
121
+
122
+ decoding_pseudo_headers = is_pseudo_header
123
+ raise ProtocolError, "invalid header received: #{field}" if FORBIDDEN_HEADERS.include?(field)
124
+
125
+ if frame
126
+ case field
127
+ when ":status"
128
+ frame[:status] = Integer(value)
129
+ when ":method"
130
+ frame[:method] = value
131
+ when "content-length"
132
+ frame[:content_length] = Integer(value)
133
+ when "trailer"
134
+ (frame[:trailer] ||= []) << value
135
+ end
136
+ end
137
+ list << [field, value]
138
+ end
139
+ end
140
+ list
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,337 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP2
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.
7
+ module Header
8
+ class EncodingContext
9
+ include Error
10
+
11
+ UPPER = /[[:upper:]]/.freeze
12
+
13
+ # @private
14
+ # Static table
15
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#appendix-A
16
+ STATIC_TABLE = [
17
+ [":authority", ""],
18
+ [":method", "GET"],
19
+ [":method", "POST"],
20
+ [":path", "/"],
21
+ [":path", "/index.html"],
22
+ [":scheme", "http"],
23
+ [":scheme", "https"],
24
+ [":status", "200"],
25
+ [":status", "204"],
26
+ [":status", "206"],
27
+ [":status", "304"],
28
+ [":status", "400"],
29
+ [":status", "404"],
30
+ [":status", "500"],
31
+ ["accept-charset", ""],
32
+ ["accept-encoding", "gzip, deflate"],
33
+ ["accept-language", ""],
34
+ ["accept-ranges", ""],
35
+ ["accept", ""],
36
+ ["access-control-allow-origin", ""],
37
+ ["age", ""],
38
+ ["allow", ""],
39
+ ["authorization", ""],
40
+ ["cache-control", ""],
41
+ ["content-disposition", ""],
42
+ ["content-encoding", ""],
43
+ ["content-language", ""],
44
+ ["content-length", ""],
45
+ ["content-location", ""],
46
+ ["content-range", ""],
47
+ ["content-type", ""],
48
+ ["cookie", ""],
49
+ ["date", ""],
50
+ ["etag", ""],
51
+ ["expect", ""],
52
+ ["expires", ""],
53
+ ["from", ""],
54
+ ["host", ""],
55
+ ["if-match", ""],
56
+ ["if-modified-since", ""],
57
+ ["if-none-match", ""],
58
+ ["if-range", ""],
59
+ ["if-unmodified-since", ""],
60
+ ["last-modified", ""],
61
+ ["link", ""],
62
+ ["location", ""],
63
+ ["max-forwards", ""],
64
+ ["proxy-authenticate", ""],
65
+ ["proxy-authorization", ""],
66
+ ["range", ""],
67
+ ["referer", ""],
68
+ ["refresh", ""],
69
+ ["retry-after", ""],
70
+ ["server", ""],
71
+ ["set-cookie", ""],
72
+ ["strict-transport-security", ""],
73
+ ["transfer-encoding", ""],
74
+ ["user-agent", ""],
75
+ ["vary", ""],
76
+ ["via", ""],
77
+ ["www-authenticate", ""]
78
+ ].each { |pair| pair.each(&:freeze).freeze }.freeze
79
+
80
+ STATIC_TABLE_BY_FIELD = STATIC_TABLE
81
+ .each_with_object({})
82
+ .with_index { |((field, value), hs), idx| (hs[field] ||= []) << [idx, value] }
83
+ .each { |pair| pair.each(&:freeze).freeze }.freeze
84
+
85
+ STATIC_TABLE_SIZE = STATIC_TABLE.size
86
+
87
+ # Current table of header key-value pairs.
88
+ attr_reader :table
89
+
90
+ # Current encoding options
91
+ #
92
+ # :table_size Integer maximum dynamic table size in bytes
93
+ # :huffman Symbol :always, :never, :shorter
94
+ # :index Symbol :all, :static, :never
95
+ attr_reader :options
96
+
97
+ # Initializes compression context with appropriate client/server
98
+ # defaults and maximum size of the dynamic table.
99
+ #
100
+ # @param options [Hash] encoding options
101
+ # :table_size Integer maximum dynamic table size in bytes
102
+ # :huffman Symbol :always, :never, :shorter
103
+ # :index Symbol :all, :static, :never
104
+ def initialize(options = {})
105
+ default_options = {
106
+ huffman: :shorter,
107
+ index: :all,
108
+ table_size: 4096
109
+ }
110
+ @table = []
111
+ @options = default_options.merge(options)
112
+ @limit = @options[:table_size]
113
+ @_table_updated = false
114
+ end
115
+
116
+ # Duplicates current compression context
117
+ # @return [EncodingContext]
118
+ def dup
119
+ other = EncodingContext.new(@options)
120
+ t = @table
121
+ l = @limit
122
+ other.instance_eval do
123
+ @table = t.dup # shallow copy
124
+ @limit = l
125
+ end
126
+ other
127
+ end
128
+
129
+ # Finds an entry in current dynamic table by index.
130
+ # Note that index is zero-based in this module.
131
+ #
132
+ # If the index is greater than the last index in the static table,
133
+ # an entry in the dynamic table is dereferenced.
134
+ #
135
+ # If the index is greater than the last header index, an error is raised.
136
+ #
137
+ # @param index [Integer] zero-based index in the dynamic table.
138
+ # @return [Array] +[key, value]+
139
+ def dereference(index)
140
+ # NOTE: index is zero-based in this module.
141
+ value = STATIC_TABLE[index] || @table[index - STATIC_TABLE_SIZE]
142
+ raise CompressionError, "Index too large" unless value
143
+
144
+ value
145
+ end
146
+
147
+ # Header Block Processing
148
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-4.1
149
+ #
150
+ # @param cmd [Hash] { type:, name:, value:, index: }
151
+ # @return [Array, nil] +[name, value]+ header field that is added to the decoded header list,
152
+ # or nil if +cmd[:type]+ is +:changetablesize+
153
+ def process(cmd)
154
+ emit = nil
155
+
156
+ case cmd[:type]
157
+ when :changetablesize
158
+ raise CompressionError, "tried to change table size after adding elements to table" if @_table_updated
159
+
160
+ # we can receive multiple table size change commands inside a header frame. However,
161
+ # we should blow up if we receive another frame where the new table size is bigger.
162
+ table_size_updated = @limit != @options[:table_size]
163
+
164
+ raise CompressionError, "dynamic table size update exceed limit" if !table_size_updated && cmd[:value] > @limit
165
+
166
+ self.table_size = cmd[:value]
167
+
168
+ when :indexed
169
+ # Indexed Representation
170
+ # An _indexed representation_ entails the following actions:
171
+ # o The header field corresponding to the referenced entry in either
172
+ # the static table or dynamic table is added to the decoded header
173
+ # list.
174
+ idx = cmd[:name]
175
+
176
+ k, v = dereference(idx)
177
+ emit = [k, v]
178
+
179
+ when :incremental, :noindex, :neverindexed
180
+ # A _literal representation_ that is _not added_ to the dynamic table
181
+ # entails the following action:
182
+ # o The header field is added to the decoded header list.
183
+
184
+ # A _literal representation_ that is _added_ to the dynamic table
185
+ # entails the following actions:
186
+ # o The header field is added to the decoded header list.
187
+ # o The header field is inserted at the beginning of the dynamic table.
188
+
189
+ case cmd[:name]
190
+ when Integer
191
+ k, v = dereference(cmd[:name])
192
+
193
+ cmd = cmd.dup
194
+ cmd[:index] ||= cmd[:name]
195
+ cmd[:value] ||= v
196
+ cmd[:name] = k
197
+ when UPPER
198
+ raise ProtocolError, "Invalid uppercase key: #{cmd[:name]}"
199
+ end
200
+
201
+ emit = [cmd[:name], cmd[:value]]
202
+
203
+ add_to_table(emit) if cmd[:type] == :incremental
204
+
205
+ else
206
+ raise CompressionError, "Invalid type: #{cmd[:type]}"
207
+ end
208
+
209
+ emit
210
+ end
211
+
212
+ # Plan header compression according to +@options [:index]+
213
+ # :never Do not use dynamic table or static table reference at all.
214
+ # :static Use static table only.
215
+ # :all Use all of them.
216
+ #
217
+ # @param headers [Array] +[[name, value], ...]+
218
+ # @return [Array] array of commands
219
+ def encode(headers)
220
+ commands = []
221
+ # Literals commands are marked with :noindex when index is not used
222
+ noindex = %i[static never].include?(@options[:index])
223
+ headers.each do |field, value|
224
+ # Literal header names MUST be translated to lowercase before
225
+ # encoding and transmission.
226
+ field = field.downcase if UPPER.match?(field)
227
+ value = "/" if field == ":path" && value.empty?
228
+ cmd = addcmd(field, value)
229
+ cmd[:type] = :noindex if noindex && cmd[:type] == :incremental
230
+ commands << cmd
231
+ process(cmd)
232
+ end
233
+ commands
234
+ end
235
+
236
+ # Emits command for a header.
237
+ # Prefer static table over dynamic table.
238
+ # Prefer exact match over name-only match.
239
+ #
240
+ # +@options [:index]+ controls whether to use the dynamic table,
241
+ # static table, or both.
242
+ # :never Do not use dynamic table or static table reference at all.
243
+ # :static Use static table only.
244
+ # :all Use all of them.
245
+ #
246
+ # @param header [Array] +[name, value]+
247
+ # @return [Hash] command
248
+ def addcmd(*header)
249
+ exact = nil
250
+ name_only = nil
251
+
252
+ if %i[all static].include?(@options[:index])
253
+ field, value = header
254
+ if (svalues = STATIC_TABLE_BY_FIELD[field])
255
+ svalues.each do |i, svalue|
256
+ name_only ||= i
257
+ if svalue == value
258
+ exact = i
259
+ break
260
+ end
261
+ end
262
+ end
263
+ end
264
+ if [:all].include?(@options[:index]) && !exact
265
+ @table.each_index do |i|
266
+ if @table[i] == header
267
+ exact ||= i + STATIC_TABLE_SIZE
268
+ break
269
+ elsif @table[i].first == header.first
270
+ name_only ||= i + STATIC_TABLE_SIZE
271
+ end
272
+ end
273
+ end
274
+
275
+ if exact
276
+ { name: exact, type: :indexed }
277
+ elsif name_only
278
+ { name: name_only, value: header.last, type: :incremental }
279
+ else
280
+ { name: header.first, value: header.last, type: :incremental }
281
+ end
282
+ end
283
+
284
+ # Alter dynamic table size.
285
+ # When the size is reduced, some headers might be evicted.
286
+ def table_size=(size)
287
+ @limit = size
288
+ size_check(nil)
289
+ end
290
+
291
+ # Returns current table size in octets
292
+ # @return [Integer]
293
+ def current_table_size
294
+ @table.inject(0) { |r, (k, v)| r + k.bytesize + v.bytesize + 32 }
295
+ end
296
+
297
+ def listen_on_table
298
+ yield
299
+ ensure
300
+ @_table_updated = false
301
+ end
302
+
303
+ private
304
+
305
+ # Add a name-value pair to the dynamic table.
306
+ # Older entries might have been evicted so that
307
+ # the new entry fits in the dynamic table.
308
+ #
309
+ # @param cmd [Array] +[name, value]+
310
+ def add_to_table(cmd)
311
+ return unless size_check(cmd)
312
+
313
+ @table.unshift(cmd)
314
+ @_table_updated = true
315
+ end
316
+
317
+ # To keep the dynamic table size lower than or equal to @limit,
318
+ # remove one or more entries at the end of the dynamic table.
319
+ #
320
+ # @param cmd [Hash]
321
+ # @return [Boolean] whether +cmd+ fits in the dynamic table.
322
+ def size_check(cmd)
323
+ cursize = current_table_size
324
+ cmdsize = cmd.nil? ? 0 : cmd[0].bytesize + cmd[1].bytesize + 32
325
+
326
+ while cursize + cmdsize > @limit
327
+ break if @table.empty?
328
+
329
+ e = @table.pop
330
+ cursize -= e[0].bytesize + e[1].bytesize + 32
331
+ end
332
+
333
+ cmdsize <= @limit
334
+ end
335
+ end
336
+ end
337
+ end