http-2 0.8.4 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: df6b5bd32b2217bebfb3205cfab1bd78f2a5456d
4
- data.tar.gz: eb9d683a528819195bf81d4480549b2d8ac7bab6
3
+ metadata.gz: f37bf7f07706136db469b5c188cf13b2995d0c16
4
+ data.tar.gz: e51aeb9218b11e3cada90485ba60b5342e89322f
5
5
  SHA512:
6
- metadata.gz: 686e87ef8beeedd1f3ac1371845b759687231ba62e3bab532d3d15d65febc179bc19f44d4a025708ecc62e2f0e7dd0689cfbbeef19e47c8ee52df9d050066228
7
- data.tar.gz: 41b003cfeb4a127efcf9d4709167db3ff40934a175a08bd19d23a3874d6dd7e615f99ab967c5b85b521e54a4cdbfce33b4087c6ddde7eebd0d0fc11d18a4d551
6
+ metadata.gz: 45ca021e60dfe824fcc7bc6bf61d054ce37096a7f0f41ba7c1a5e45f89ad7848f37f4d6638f28d6a93bf6b41f28e8af66abfac28b4fe60d16556d82c1ec78ae3
7
+ data.tar.gz: c5bddd633904cd5ff9aef237debbdbcc78a6b901dad857cd5aa770e8d13ea28e26c158c902f9b840185b4a8d7dfe7c8e2acd9b380991426f0b5abdbfa4bc2f2f
@@ -37,7 +37,7 @@ Metrics/MethodLength:
37
37
  # Offense count: 1
38
38
  # Configuration parameters: CountKeywordArgs.
39
39
  Metrics/ParameterLists:
40
- Max: 6
40
+ Max: 7
41
41
 
42
42
  # Offense count: 10
43
43
  Metrics/PerceivedComplexity:
@@ -55,6 +55,10 @@ conn.on(:frame_received) do |frame|
55
55
  end
56
56
 
57
57
  conn.on(:promise) do |promise|
58
+ promise.on(:promise_headers) do |h|
59
+ log.info "promise request headers: #{h}"
60
+ end
61
+
58
62
  promise.on(:headers) do |h|
59
63
  log.info "promise headers: #{h}"
60
64
  end
@@ -36,9 +36,7 @@ if options[:secure]
36
36
  DRAFT
37
37
  end
38
38
 
39
- ctx.tmp_ecdh_callback = lambda do |*_args|
40
- OpenSSL::PKey::EC.new 'prime256v1'
41
- end
39
+ ctx.ecdh_curves = 'P-256'
42
40
 
43
41
  server = OpenSSL::SSL::SSLServer.new(server, ctx)
44
42
  end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literals: true
2
+
3
+ require_relative 'helper'
4
+ require 'http_parser'
5
+
6
+ OptionParser.new do |opts|
7
+ opts.banner = 'Usage: upgrade_client.rb [options]'
8
+ end.parse!
9
+
10
+ uri = URI.parse(ARGV[0] || 'http://localhost:8080/')
11
+ sock = TCPSocket.new(uri.host, uri.port)
12
+
13
+ conn = HTTP2::Client.new
14
+
15
+ def request_header_hash
16
+ Hash.new do |hash, key|
17
+ k = key.to_s.downcase
18
+ k.tr! '_', '-'
19
+ _, value = hash.find { |header_key, _| header_key.downcase == k }
20
+ hash[key] = value if value
21
+ end
22
+ end
23
+
24
+ conn.on(:frame) do |bytes|
25
+ sock.print bytes
26
+ sock.flush
27
+ end
28
+ conn.on(:frame_sent) do |frame|
29
+ puts "Sent frame: #{frame.inspect}"
30
+ end
31
+ conn.on(:frame_received) do |frame|
32
+ puts "Received frame: #{frame.inspect}"
33
+ end
34
+
35
+ # upgrader module
36
+ class UpgradeHandler
37
+ UPGRADE_REQUEST = <<-RESP.freeze
38
+ GET %s HTTP/1.1
39
+ Connection: Upgrade, HTTP2-Settings
40
+ HTTP2-Settings: #{HTTP2::Client.settings_header(settings_max_concurrent_streams: 100)}
41
+ Upgrade: h2c
42
+ Host: %s
43
+ User-Agent: http-2 upgrade
44
+ Accept: */*
45
+
46
+ RESP
47
+
48
+ attr_reader :complete, :parsing
49
+ def initialize(conn, sock)
50
+ @conn = conn
51
+ @sock = sock
52
+ @headers = request_header_hash
53
+ @body = ''.b
54
+ @complete, @parsing = false, false
55
+ @parser = ::HTTP::Parser.new(self)
56
+ end
57
+
58
+ def request(uri)
59
+ host = "#{uri.hostname}#{":#{uri.port}" if uri.port != uri.default_port}"
60
+ req = format(UPGRADE_REQUEST, uri.request_uri, host)
61
+ puts req
62
+ @sock << req
63
+ end
64
+
65
+ def <<(data)
66
+ @parsing ||= true
67
+ @parser << data
68
+ return unless complete
69
+ upgrade
70
+ end
71
+
72
+ def complete!
73
+ @complete = true
74
+ end
75
+
76
+ def on_headers_complete(h)
77
+ @headers.merge!(h)
78
+ puts "received headers: #{h}"
79
+ end
80
+
81
+ def on_body(chunk)
82
+ puts "received chunk: #{chunk}"
83
+ @body << chunk
84
+ end
85
+
86
+ def on_message_complete
87
+ fail 'could not upgrade to h2c' unless @parser.status_code == 101
88
+ @parsing = false
89
+ complete!
90
+ end
91
+
92
+ def upgrade
93
+ stream = @conn.upgrade
94
+ log = Logger.new(stream.id)
95
+
96
+ stream.on(:close) do
97
+ log.info 'stream closed'
98
+ end
99
+
100
+ stream.on(:half_close) do
101
+ log.info 'closing client-end of the stream'
102
+ end
103
+
104
+ stream.on(:headers) do |h|
105
+ log.info "response headers: #{h}"
106
+ end
107
+
108
+ stream.on(:data) do |d|
109
+ log.info "response data chunk: <<#{d}>>"
110
+ end
111
+
112
+ stream.on(:altsvc) do |f|
113
+ log.info "received ALTSVC #{f}"
114
+ end
115
+
116
+ @conn.on(:promise) do |promise|
117
+ promise.on(:headers) do |h|
118
+ log.info "promise headers: #{h}"
119
+ end
120
+
121
+ promise.on(:data) do |d|
122
+ log.info "promise data chunk: <<#{d.size}>>"
123
+ end
124
+ end
125
+
126
+ @conn.on(:altsvc) do |f|
127
+ log.info "received ALTSVC #{f}"
128
+ end
129
+ end
130
+ end
131
+
132
+ uh = UpgradeHandler.new(conn, sock)
133
+ puts 'Sending HTTP/1.1 upgrade request'
134
+ uh.request(uri)
135
+
136
+ while !sock.closed? && !sock.eof?
137
+ data = sock.read_nonblock(1024)
138
+
139
+ begin
140
+ if !uh.parsing && !uh.complete
141
+ uh << data
142
+ elsif uh.parsing && !uh.complete
143
+ uh << data
144
+ elsif uh.complete
145
+ conn << data
146
+ end
147
+
148
+ rescue => e
149
+ puts "#{e.class} exception: #{e.message} - closing socket."
150
+ e.backtrace.each { |l| puts "\t" + l }
151
+ conn.close
152
+ sock.close
153
+ end
154
+ end
@@ -38,6 +38,13 @@ module HTTP2
38
38
  super(frame)
39
39
  end
40
40
 
41
+ # sends the preface and initializes the first stream in half-closed state
42
+ def upgrade
43
+ fail ProtocolError unless @stream_id == 1
44
+ send_connection_preface
45
+ new_stream(state: :half_closed_local)
46
+ end
47
+
41
48
  # Emit the connection preface if not yet
42
49
  def send_connection_preface
43
50
  return unless @state == :waiting_connection_preface
@@ -47,5 +54,10 @@ module HTTP2
47
54
  payload = @local_settings.select { |k, v| v != SPEC_DEFAULT_CONNECTION_SETTINGS[k] }
48
55
  settings(payload)
49
56
  end
57
+
58
+ def self.settings_header(**settings)
59
+ frame = Framer.new.generate(type: :settings, stream: 0, payload: settings)
60
+ Base64.urlsafe_encode64(frame[9..-1])
61
+ end
50
62
  end
51
63
  end
@@ -199,8 +199,12 @@ module HTTP2
199
199
  commands = []
200
200
  # Literals commands are marked with :noindex when index is not used
201
201
  noindex = [:static, :never].include?(@options[:index])
202
- headers.each do |h|
203
- cmd = addcmd(h)
202
+ headers.each do |field, value|
203
+ # Literal header names MUST be translated to lowercase before
204
+ # encoding and transmission.
205
+ field = field.downcase
206
+ value = '/' if field == ':path' && value.empty?
207
+ cmd = addcmd(field, value)
204
208
  cmd[:type] = :noindex if noindex && cmd[:type] == :incremental
205
209
  commands << cmd
206
210
  process(cmd)
@@ -220,7 +224,7 @@ module HTTP2
220
224
  #
221
225
  # @param header [Array] +[name, value]+
222
226
  # @return [Hash] command
223
- def addcmd(header)
227
+ def addcmd(*header)
224
228
  exact = nil
225
229
  name_only = nil
226
230
 
@@ -442,10 +446,6 @@ module HTTP2
442
446
  def encode(headers)
443
447
  buffer = Buffer.new
444
448
 
445
- # Literal header names MUST be translated to lowercase before
446
- # encoding and transmission.
447
- headers.map! { |hk, hv| [hk.downcase, hv] }
448
-
449
449
  commands = @cc.encode(headers)
450
450
  commands.each do |cmd|
451
451
  buffer << header(cmd)
@@ -147,6 +147,7 @@ module HTTP2
147
147
  #
148
148
  # @param increment [Integer]
149
149
  def window_update(increment)
150
+ @local_window += increment
150
151
  send(type: :window_update, stream: 0, increment: increment)
151
152
  end
152
153
 
@@ -312,6 +313,10 @@ module HTTP2
312
313
  else
313
314
  if (stream = @streams[frame[:stream]])
314
315
  stream << frame
316
+ if frame[:type] == :data
317
+ update_local_window(frame)
318
+ calculate_window_update(@local_window_limit)
319
+ end
315
320
  else
316
321
  case frame[:type]
317
322
  # The PRIORITY frame can be sent for a stream in the "idle" or
@@ -664,7 +669,6 @@ module HTTP2
664
669
 
665
670
  stream.on(:promise, &method(:promise)) if self.is_a? Server
666
671
  stream.on(:frame, &method(:send))
667
- stream.on(:window_update, &method(:window_update))
668
672
 
669
673
  @streams[id] = stream
670
674
  end
@@ -689,6 +693,7 @@ module HTTP2
689
693
  backtrace = (e && e.backtrace) || []
690
694
  fail Error.const_get(klass), msg, backtrace
691
695
  end
696
+ alias error connection_error
692
697
 
693
698
  def manage_state(_)
694
699
  yield
@@ -13,6 +13,37 @@ module HTTP2
13
13
 
14
14
  private
15
15
 
16
+ def update_local_window(frame)
17
+ frame_size = frame[:payload].bytesize
18
+ frame_size += frame[:padding] || 0
19
+ @local_window -= frame_size
20
+ end
21
+
22
+ def calculate_window_update(window_max_size)
23
+ # If DATA frame is received with length > 0 and
24
+ # current received window size + delta length is strictly larger than
25
+ # local window size, it throws a flow control error.
26
+ #
27
+ error(:flow_control_error) if @local_window < 0
28
+
29
+ # Send WINDOW_UPDATE if the received window size goes over
30
+ # the local window size / 2.
31
+ #
32
+ # The HTTP/2 spec mandates that every DATA frame received
33
+ # generates a WINDOW_UPDATE to send. In some cases however,
34
+ # (ex: DATA frames with short payloads),
35
+ # the noise generated by flow control frames creates enough
36
+ # congestion for this to be deemed very inefficient.
37
+ #
38
+ # This heuristic was inherited from nghttp, which delays the
39
+ # WINDOW_UPDATE until at least half the window is exhausted.
40
+ # This works because the sender doesn't need those increments
41
+ # until the receiver window is exhausted, after which he'll be
42
+ # waiting for the WINDOW_UPDATE frame.
43
+ return unless @local_window <= (window_max_size / 2)
44
+ window_update(window_max_size - @local_window)
45
+ end
46
+
16
47
  # Buffers outgoing DATA frames and applies flow control logic to split
17
48
  # and emit DATA frames based on current flow control window. If the
18
49
  # window is large enough, the data is sent immediately. Otherwise, the
@@ -71,22 +71,24 @@ module HTTP2
71
71
  # @param exclusive [Boolean]
72
72
  # @param window [Integer]
73
73
  # @param parent [Stream]
74
- def initialize(connection:, id:, weight: 16, dependency: 0, exclusive: false, parent: nil)
74
+ # @param state [Symbol]
75
+ def initialize(connection:, id:, weight: 16, dependency: 0, exclusive: false, parent: nil, state: :idle)
75
76
  @connection = connection
76
77
  @id = id
77
78
  @weight = weight
78
79
  @dependency = dependency
79
80
  process_priority(weight: weight, stream_dependency: dependency, exclusive: exclusive)
81
+ @local_window_max_size = connection.local_settings[:settings_initial_window_size]
80
82
  @local_window = connection.local_settings[:settings_initial_window_size]
81
83
  @remote_window = connection.remote_settings[:settings_initial_window_size]
82
84
  @parent = parent
83
- @state = :idle
85
+ @state = state
84
86
  @error = false
85
87
  @closed = false
86
88
  @send_buffer = []
87
89
 
88
90
  on(:window) { |v| @remote_window = v }
89
- on(:local_window) { |v| @local_window = v }
91
+ on(:local_window) { |v| @local_window_max_size = @local_window = v }
90
92
  end
91
93
 
92
94
  # Processes incoming HTTP 2.0 frames. The frames must be decoded upstream.
@@ -97,16 +99,14 @@ module HTTP2
97
99
 
98
100
  case frame[:type]
99
101
  when :data
100
- window_size = frame[:payload].bytesize
101
- window_size += frame[:padding] || 0
102
- @local_window -= window_size
102
+ update_local_window(frame)
103
+ # Emit DATA frame
103
104
  emit(:data, frame[:payload]) unless frame[:ignore]
104
-
105
- # Automatically send WINDOW_UPDATE,
106
- # assuming that emit(:data) can now receive next data
107
- window_update(window_size) if window_size > 0
108
- when :headers, :push_promise
105
+ calculate_window_update(@local_window_max_size)
106
+ when :headers
109
107
  emit(:headers, frame[:payload]) unless frame[:ignore]
108
+ when :push_promise
109
+ emit(:promise_headers, frame[:payload]) unless frame[:ignore]
110
110
  when :priority
111
111
  process_priority(frame)
112
112
  when :window_update
@@ -153,7 +153,7 @@ module HTTP2
153
153
  flags << :end_headers if end_headers
154
154
  flags << :end_stream if end_stream
155
155
 
156
- send(type: :headers, flags: flags, payload: headers.to_a)
156
+ send(type: :headers, flags: flags, payload: headers)
157
157
  end
158
158
 
159
159
  def promise(headers, end_headers: true, &block)
@@ -228,8 +228,6 @@ module HTTP2
228
228
  #
229
229
  # @param increment [Integer]
230
230
  def window_update(increment)
231
- # always emit connection-level WINDOW_UPDATE
232
- emit(:window_update, increment)
233
231
  # emit stream-level WINDOW_UPDATE unless stream is closed
234
232
  return if @state == :closed || @state == :remote_closed
235
233
  send(type: :window_update, increment: increment)
@@ -602,6 +600,7 @@ module HTTP2
602
600
  klass = error.to_s.split('_').map(&:capitalize).join
603
601
  fail Error.const_get(klass), msg
604
602
  end
603
+ alias error stream_error
605
604
 
606
605
  def manage_state(frame)
607
606
  transition(frame, true)
@@ -1,3 +1,3 @@
1
1
  module HTTP2
2
- VERSION = '0.8.4'.freeze
2
+ VERSION = '0.9.0'.freeze
3
3
  end
@@ -34,6 +34,23 @@ RSpec.describe HTTP2::Client do
34
34
  end
35
35
  end
36
36
 
37
+ context 'upgrade' do
38
+ it 'fails when client has already created streams' do
39
+ @client.new_stream
40
+ expect { @client.upgrade }.to raise_error(HTTP2::Error::ProtocolError)
41
+ end
42
+
43
+ it 'sends the preface' do
44
+ expect(@client).to receive(:send_connection_preface)
45
+ @client.upgrade
46
+ end
47
+
48
+ it 'initializes the first stream in the half-closed state' do
49
+ stream = @client.upgrade
50
+ expect(stream.state).to be(:half_closed_local)
51
+ end
52
+ end
53
+
37
54
  context 'push' do
38
55
  it 'should disallow client initiated push' do
39
56
  expect do
@@ -69,12 +86,28 @@ RSpec.describe HTTP2::Client do
69
86
 
70
87
  promise = nil
71
88
  @client.on(:promise) { |stream| promise = stream }
72
- @client << set_stream_id(f.generate(PUSH_PROMISE.dup), s.id)
89
+ @client << set_stream_id(f.generate(PUSH_PROMISE.deep_dup), s.id)
73
90
 
74
91
  expect(promise.id).to eq 2
75
92
  expect(promise.state).to eq :reserved_remote
76
93
  end
77
94
 
95
+ it 'should emit promise headers for received PUSH_PROMISE' do
96
+ header = nil
97
+ s = @client.new_stream
98
+ s.send HEADERS.deep_dup
99
+
100
+ @client.on(:promise) do |stream|
101
+ stream.on(:promise_headers) do |h|
102
+ header = h
103
+ end
104
+ end
105
+ @client << set_stream_id(f.generate(PUSH_PROMISE.deep_dup), s.id)
106
+
107
+ expect(header).to be_a(Array)
108
+ # expect(header).to eq([%w(a b)])
109
+ end
110
+
78
111
  it 'should auto RST_STREAM promises against locally-RST stream' do
79
112
  s = @client.new_stream
80
113
  s.send HEADERS.deep_dup
@@ -237,6 +237,18 @@ RSpec.describe HTTP2::Header do
237
237
  expect(cc.table.first[0]).to eq 'test2'
238
238
  end
239
239
  end
240
+
241
+ context 'encode' do
242
+ it 'downcases the field' do
243
+ expect(EncodingContext.new.encode([['Content-Length', '5']]))
244
+ .to eq(EncodingContext.new.encode([['content-length', '5']]))
245
+ end
246
+
247
+ it 'fills :path if empty' do
248
+ expect(EncodingContext.new.encode([[':path', '']]))
249
+ .to eq(EncodingContext.new.encode([[':path', '/']]))
250
+ end
251
+ end
240
252
  end
241
253
 
242
254
  spec_examples = [
@@ -237,6 +237,28 @@ RSpec.describe HTTP2::Connection do
237
237
  expect(@conn.buffered_amount).to eq 0
238
238
  expect(@conn.remote_window).to eq 900
239
239
  end
240
+
241
+ it 'should update window when data received is over half of the maximum local window size' do
242
+ settings, data = SETTINGS.dup, DATA.dup
243
+ conn = Client.new(settings_initial_window_size: 500)
244
+
245
+ conn.receive f.generate(settings)
246
+ s1 = conn.new_stream
247
+ s2 = conn.new_stream
248
+
249
+ s1.send HEADERS.deep_dup
250
+ s2.send HEADERS.deep_dup
251
+ expect(conn).to receive(:send) do |frame|
252
+ expect(frame[:type]).to eq :window_update
253
+ expect(frame[:stream]).to eq 0
254
+ expect(frame[:increment]).to eq 400
255
+ end
256
+ conn.receive f.generate(data.merge(payload: 'x' * 200, end_stream: false, stream: s1.id))
257
+ conn.receive f.generate(data.merge(payload: 'x' * 200, end_stream: false, stream: s2.id))
258
+ expect(s1.local_window).to eq 300
259
+ expect(s2.local_window).to eq 300
260
+ expect(conn.local_window).to eq 500
261
+ end
240
262
  end
241
263
 
242
264
  context 'framing' do
@@ -598,19 +598,28 @@ RSpec.describe HTTP2::Stream do
598
598
  expect(s1.remote_window).to eq 900
599
599
  end
600
600
 
601
- it 'should keep track of incoming flow control' do
601
+ it 'should not update window when data received is less than half of maximum local window size' do
602
602
  data = DATA.deep_dup
603
603
  datalen = data[:payload].bytesize
604
- expect(@stream).to receive(:send) do |frame|
604
+ expect(@stream).not_to receive(:send) do |frame|
605
605
  expect(frame[:type]).to eq :window_update
606
606
  expect(frame[:increment]).to eq datalen
607
607
  end
608
- expect(@client).to receive(:send) do |frame|
608
+ @stream.receive HEADERS.deep_dup
609
+ @stream.receive data
610
+ end
611
+
612
+ it 'should update window when data received is over half of the maximum local window size' do
613
+ data1 = DATA.merge(payload: 'a'*16_384, flags: [])
614
+ data2 = DATA.merge(payload: 'a'*16_384)
615
+ datalen = 16_384 * 2
616
+ expect(@stream).to receive(:send) do |frame|
609
617
  expect(frame[:type]).to eq :window_update
610
618
  expect(frame[:increment]).to eq datalen
611
619
  end
612
620
  @stream.receive HEADERS.deep_dup
613
- @stream.receive data
621
+ @stream.receive data1
622
+ @stream.receive data2
614
623
  end
615
624
  end
616
625
 
@@ -642,7 +651,7 @@ RSpec.describe HTTP2::Stream do
642
651
 
643
652
  expect(@stream).to receive(:send) do |frame|
644
653
  expect(frame[:type]).to eq :headers
645
- expect(frame[:payload]).to eq payload.to_a
654
+ expect(frame[:payload]).to eq payload
646
655
  expect(frame[:flags]).to eq [:end_headers]
647
656
  end
648
657
 
@@ -822,8 +831,8 @@ RSpec.describe HTTP2::Stream do
822
831
  expect(order).to eq [:reserved, :active, :half_close, :close]
823
832
  end
824
833
 
825
- it 'client: headers > active > headers > .. > data > close' do
826
- order, headers = [], []
834
+ it 'client: promise_headers > active > headers > .. > data > close' do
835
+ order, headers, promise_headers = [], [], []
827
836
  @client.on(:promise) do |push|
828
837
  order << :reserved
829
838
 
@@ -832,6 +841,10 @@ RSpec.describe HTTP2::Stream do
832
841
  push.on(:half_close) { order << :half_close }
833
842
  push.on(:close) { order << :close }
834
843
 
844
+ push.on(:promise_headers) do |h|
845
+ order << :promise_headers
846
+ promise_headers += h
847
+ end
835
848
  push.on(:headers) do |h|
836
849
  order << :headers
837
850
  headers += h
@@ -845,10 +858,11 @@ RSpec.describe HTTP2::Stream do
845
858
  push.data('somedata')
846
859
  end
847
860
 
848
- expect(headers).to eq([%w(key val), %w(key2 val2)])
861
+ expect(promise_headers).to eq([%w(key val)])
862
+ expect(headers).to eq([%w(key2 val2)])
849
863
  expect(order).to eq [
850
864
  :reserved,
851
- :headers,
865
+ :promise_headers,
852
866
  :active,
853
867
  :headers,
854
868
  :half_close,
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: 0.8.4
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ilya Grigorik
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2017-05-09 00:00:00.000000000 Z
12
+ date: 2018-02-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -52,6 +52,7 @@ files:
52
52
  - example/keys/server.crt
53
53
  - example/keys/server.key
54
54
  - example/server.rb
55
+ - example/upgrade_client.rb
55
56
  - example/upgrade_server.rb
56
57
  - http-2.gemspec
57
58
  - lib/http/2.rb