http-2 0.8.2 → 0.8.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.
@@ -0,0 +1,27 @@
1
+ -----BEGIN RSA PRIVATE KEY-----
2
+ MIIEpAIBAAKCAQEAoxV61qrUWRDDjv6OgMRm4A2DIIP+7sQhUCTOWeguCCOg/Ffe
3
+ BQ8gnfidCzitoQfkFHRaqQNUvekVtjLyMHb0bdN0STIG33FBdPsOGSLb9sa1vcGg
4
+ nwpi0kQ2YHEgE2nBBervadlbPqOCf3B2nIq0sI8uPpveAoJMbVBRrRnCWhagGegn
5
+ Y+Vr0HSUogCN8d4DQv1BgPuXS7KfPr8Fsfv0zHOpwwZqEVtxh3hvzQKUEodNcN8A
6
+ 79BZlGhUJQOVj8mvybPgsrmIdZol20CRIw5xhmiY56MkQEKT65O9iY/m53zrvZMa
7
+ c5LiyTrG2qH0RZDPvwK+VPm0M7lNBK0PCIfDDwIDAQABAoIBAQCZShphZsccRJ6c
8
+ bPdDX9iW5vyG9qsMgPwTGdWAOrXx3pN2PZ0pwjNVaRcsMgU6JHGlLEz/Kmtf6pQG
9
+ 41I0bcuI48Yc+tHs+sadD1IMHHEHP3Yau8KfWyLSI129Pvf4Z2IQjuik5LJYaVbD
10
+ NNG4iMQYZS0Bmn6Oey0dXu62t0ywYa0qvbIDse/RmjTQSTipuvGg8/QEAeRGABv8
11
+ Nd4Esya0zuxk6hGaNp3hkjyRkeoC7RsBVJbFSnp6gSubPdXwrJyHfySKe9jvrDG3
12
+ Q/AzyHUh/6EODd5n66x0p6rq7oo9/PnLvZJY8jIGWG+aEp68RJyEgimrwll0rAWw
13
+ /buqijGRAoGBANimL8407fFirmct7BceavaeJfXPK5yWiOhVX0XlJ0phAFuaAxK3
14
+ 5HVT7DD+KKV66g1jtS9FUVZGDiYFHlsdsYuHVYcRmr0h5rZr941obrDwNrM9Nf9C
15
+ 0uehN5+n/FaeGoQLR3V4THoP3rlkYTlLpQnI5mKA19JukXnIiJM9ARUZAoGBAMC0
16
+ mcVsVuSKSFwURtQHHIufxL6SqC2kLTwIQ7exqejNYPCqCiif+ZWOmsTqbVGAGbMK
17
+ Ohak4oLwN5IGCl4jNQG+vWagREkx6OXSk5NYcfoNBrOm+0UoFRzoEA85s7Dy6PuD
18
+ tBucNZpt1sGauzkCSx7C8jj4ZlSwkv0XhBFfbTZnAoGBAK2wBjF+U6iq4YFM2rLq
19
+ KvzOa0Z3MdKXCOmiz//cKDTEMaI+heoyzZCWmIvqpzGLqirT3gUowH23Kk6m2eBY
20
+ nOdst0/S+Eha7nkfc9bFe8CUxHXMRAcCTs1ufYadCXtzw3RLCp4NtNpC8N+Wry9d
21
+ CtIeYz1jaCOHi0+kSoIobT65AoGAc6hxWkJp7ITqZQlucTdLdKmRheeztKEC3TMA
22
+ obGqDqWldww3SKarP431ahZhQjcmNYT/1DNmF7xhPe0OL+3llISMXJn4Ig4ogDdg
23
+ h2DgF3nV+eFQkfM6qLzHVrwFE0DXgI1NffzFV0hxSoW5tL+honbStkqv8EiCEBEb
24
+ HOovPCUCgYBpXuPARd2ycInAulVHijJmj2rmK7f41ZhVCWovYjcCWyeJyLIO7j+b
25
+ MBJZbmwpStJhEjW64nE2zZGWg2HCBbvZz5/SXIr3fp7qVXwpn1TvB/TJDf43t0oF
26
+ 3caLgyQYoQCsVHKT3cU4s3wuog/DyHKh9FtRkcJrEy7h9Rrc+ModbA==
27
+ -----END RSA PRIVATE KEY-----
@@ -11,6 +11,10 @@ OptionParser.new do |opts|
11
11
  opts.on('-p', '--port [Integer]', 'listen port') do |v|
12
12
  options[:port] = v
13
13
  end
14
+
15
+ opts.on('-u', '--push', 'Push message') do |_v|
16
+ options[:push] = true
17
+ end
14
18
  end.parse!
15
19
 
16
20
  puts "Starting server on port #{options[:port]}"
@@ -18,9 +22,23 @@ server = TCPServer.new(options[:port])
18
22
 
19
23
  if options[:secure]
20
24
  ctx = OpenSSL::SSL::SSLContext.new
21
- ctx.cert = OpenSSL::X509::Certificate.new(File.open('keys/mycert.pem'))
22
- ctx.key = OpenSSL::PKey::RSA.new(File.open('keys/mykey.pem'))
23
- ctx.npn_protocols = [DRAFT]
25
+ ctx.cert = OpenSSL::X509::Certificate.new(File.open('keys/server.crt'))
26
+ ctx.key = OpenSSL::PKey::RSA.new(File.open('keys/server.key'))
27
+
28
+ ctx.ssl_version = :TLSv1_2
29
+ ctx.options = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options]
30
+ ctx.ciphers = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:ciphers]
31
+
32
+ ctx.alpn_protocols = ['h2']
33
+
34
+ ctx.alpn_select_cb = lambda do |protocols|
35
+ raise "Protocol #{DRAFT} is required" if protocols.index(DRAFT).nil?
36
+ DRAFT
37
+ end
38
+
39
+ ctx.tmp_ecdh_callback = lambda do |*_args|
40
+ OpenSSL::PKey::EC.new 'prime256v1'
41
+ end
24
42
 
25
43
  server = OpenSSL::SSL::SSLServer.new(server, ctx)
26
44
  end
@@ -32,7 +50,7 @@ loop do
32
50
  conn = HTTP2::Server.new
33
51
  conn.on(:frame) do |bytes|
34
52
  # puts "Writing bytes: #{bytes.unpack("H*").first}"
35
- sock.write bytes
53
+ sock.is_a?(TCPSocket) ? sock.sendmsg(bytes) : sock.write(bytes)
36
54
  end
37
55
  conn.on(:frame_sent) do |frame|
38
56
  puts "Sent frame: #{frame.inspect}"
@@ -76,9 +94,35 @@ loop do
76
94
  'content-type' => 'text/plain',
77
95
  }, end_stream: false)
78
96
 
97
+ if options[:push]
98
+ push_streams = []
99
+
100
+ # send 10 promises
101
+ 10.times do |i|
102
+ puts 'sending push'
103
+
104
+ head = { ':method' => 'GET',
105
+ ':authority' => 'localhost',
106
+ ':scheme' => 'https',
107
+ ':path' => "/other_resource/#{i}" }
108
+
109
+ stream.promise(head) do |push|
110
+ push.headers(':status' => '200', 'content-type' => 'text/plain', 'content-length' => '11')
111
+ push_streams << push
112
+ end
113
+ end
114
+ end
115
+
79
116
  # split response into multiple DATA frames
80
117
  stream.data(response.slice!(0, 5), end_stream: false)
81
118
  stream.data(response)
119
+
120
+ if options[:push]
121
+ push_streams.each_with_index do |push, i|
122
+ sleep 1
123
+ push.data("push_data #{i}")
124
+ end
125
+ end
82
126
  end
83
127
  end
84
128
 
@@ -1,6 +1,7 @@
1
+ # frozen_string_literals: true
2
+
1
3
  require_relative 'helper'
2
4
  require 'http_parser'
3
- require 'base64'
4
5
 
5
6
  options = { port: 8080 }
6
7
  OptionParser.new do |opts|
@@ -20,15 +21,24 @@ server = TCPServer.new(options[:port])
20
21
 
21
22
  if options[:secure]
22
23
  ctx = OpenSSL::SSL::SSLContext.new
23
- ctx.cert = OpenSSL::X509::Certificate.new(File.open('keys/mycert.pem'))
24
- ctx.key = OpenSSL::PKey::RSA.new(File.open('keys/mykey.pem'))
24
+ ctx.cert = OpenSSL::X509::Certificate.new(File.open('keys/server.crt'))
25
+ ctx.key = OpenSSL::PKey::RSA.new(File.open('keys/server.key'))
25
26
  ctx.npn_protocols = [DRAFT]
26
27
 
27
28
  server = OpenSSL::SSL::SSLServer.new(server, ctx)
28
29
  end
29
30
 
31
+ def request_header_hash
32
+ Hash.new do |hash, key|
33
+ k = key.to_s.downcase
34
+ k.tr! '_', '-'
35
+ _, value = hash.find { |header_key, _| header_key.downcase == k }
36
+ hash[key] = value if value
37
+ end
38
+ end
39
+
30
40
  class UpgradeHandler
31
- VALID_UPGRADE_METHODS = %w(GET OPTIONS)
41
+ VALID_UPGRADE_METHODS = %w(GET OPTIONS).freeze
32
42
  UPGRADE_RESPONSE = <<-RESP
33
43
  HTTP/1.1 101 Switching Protocols
34
44
  Connection: Upgrade
@@ -41,6 +51,7 @@ RESP
41
51
  def initialize(conn, sock)
42
52
  @conn, @sock = conn, sock
43
53
  @complete, @parsing = false, false
54
+ @headers = request_header_hash
44
55
  @body = ''
45
56
  @parser = ::HTTP::Parser.new(self)
46
57
  end
@@ -68,7 +79,7 @@ RESP
68
79
  end
69
80
 
70
81
  def on_headers_complete(headers)
71
- @headers = headers
82
+ @headers.merge! headers
72
83
  end
73
84
 
74
85
  def on_body(chunk)
@@ -100,7 +111,8 @@ loop do
100
111
 
101
112
  conn.on(:stream) do |stream|
102
113
  log = Logger.new(stream.id)
103
- req, buffer = {}, ''
114
+ req = request_header_hash
115
+ buffer = ''
104
116
 
105
117
  stream.on(:active) { log.info 'client opened new stream' }
106
118
  stream.on(:close) do
@@ -108,7 +120,7 @@ loop do
108
120
  end
109
121
 
110
122
  stream.on(:headers) do |h|
111
- req = Hash[*h.flatten]
123
+ req.merge! Hash[*h.flatten]
112
124
  log.info "request headers: #{h}"
113
125
  end
114
126
 
@@ -122,9 +134,7 @@ loop do
122
134
 
123
135
  if req['Upgrade']
124
136
  log.info "Processing h2c Upgrade request: #{req}"
125
-
126
- # Don't respond to OPTIONS...
127
- if req[':method'] != 'OPTIONS'
137
+ if req[':method'] != 'OPTIONS' # Don't respond to OPTIONS...
128
138
  response = 'Hello h2c world!'
129
139
  stream.headers({
130
140
  ':status' => '200',
@@ -1,35 +1,61 @@
1
+ require 'forwardable'
2
+
1
3
  module HTTP2
2
- # Simple binary buffer backed by string.
3
- #
4
- # TODO: Refactor, it would be better if Buffer were not a String subclass,
5
- # but rather wrap a string and only expose the mutating API needed so that
6
- # the possible surface for things to go wrong stays controllable.
7
- # - https://github.com/igrigorik/http-2/pull/46
4
+ # Binary buffer wraps String.
8
5
  #
9
- class Buffer < String
6
+ class Buffer
7
+ extend Forwardable
8
+
9
+ def_delegators :@buffer, :ord, :encoding, :setbyte, :unpack,
10
+ :size, :each_byte, :to_str, :to_s, :length, :inspect,
11
+ :[], :[]=, :empty?, :bytesize, :include?
12
+
10
13
  UINT32 = 'N'.freeze
11
14
  private_constant :UINT32
12
15
 
13
16
  # Forces binary encoding on the string
14
- def initialize(*)
15
- super.force_encoding(Encoding::BINARY)
17
+ def initialize(str = '')
18
+ str = str.dup if str.frozen?
19
+ @buffer = str.force_encoding(Encoding::BINARY)
16
20
  end
17
21
 
18
22
  # Emulate StringIO#read: slice first n bytes from the buffer.
19
23
  #
20
24
  # @param n [Integer] number of bytes to slice from the buffer
21
25
  def read(n)
22
- Buffer.new(slice!(0, n))
26
+ Buffer.new(@buffer.slice!(0, n))
23
27
  end
24
28
 
25
- # Alias getbyte to readbyte
26
- alias_method :readbyte, :getbyte
27
-
28
29
  # Emulate StringIO#getbyte: slice first byte from buffer.
29
30
  def getbyte
30
31
  read(1).ord
31
32
  end
32
33
 
34
+ def slice!(*args)
35
+ Buffer.new(@buffer.slice!(*args))
36
+ end
37
+
38
+ def slice(*args)
39
+ Buffer.new(@buffer.slice(*args))
40
+ end
41
+
42
+ def force_encoding(*args)
43
+ @buffer = @buffer.force_encoding(*args)
44
+ end
45
+
46
+ def ==(other)
47
+ @buffer == other
48
+ end
49
+
50
+ def +(other)
51
+ @buffer += other
52
+ end
53
+
54
+ # Emulate String#getbyte: return nth byte from buffer.
55
+ def readbyte(n)
56
+ @buffer[n].ord
57
+ end
58
+
33
59
  # Slice unsigned 32-bit integer from buffer.
34
60
  # @return [Integer]
35
61
  def read_uint32
@@ -41,7 +67,9 @@ module HTTP2
41
67
  [:<<, :prepend].each do |mutating_method|
42
68
  define_method(mutating_method) do |string|
43
69
  string = string.dup if string.frozen?
44
- super(string.force_encoding(Encoding::BINARY))
70
+ @buffer.send mutating_method, string.force_encoding(Encoding::BINARY)
71
+
72
+ self
45
73
  end
46
74
  end
47
75
  end
@@ -527,12 +527,12 @@ module HTTP2
527
527
 
528
528
  case header[:type]
529
529
  when :indexed
530
- fail CompressionError if header[:name] == 0
530
+ fail CompressionError if (header[:name]).zero?
531
531
  header[:name] -= 1
532
532
  when :changetablesize
533
533
  header[:value] = header[:name]
534
534
  else
535
- if header[:name] == 0
535
+ if (header[:name]).zero?
536
536
  header[:name] = string(buf)
537
537
  else
538
538
  header[:name] -= 1
@@ -56,7 +56,7 @@ module HTTP2
56
56
  # infinity, but is automatically updated on receipt of peer settings).
57
57
  attr_reader :local_window
58
58
  attr_reader :remote_window
59
- alias_method :window, :local_window
59
+ alias window local_window
60
60
 
61
61
  # Current settings value for local and peer
62
62
  attr_reader :local_settings
@@ -81,6 +81,7 @@ module HTTP2
81
81
 
82
82
  @active_stream_count = 0
83
83
  @streams = {}
84
+ @streams_recently_closed = {}
84
85
  @pending_settings = []
85
86
 
86
87
  @framer = Framer.new
@@ -226,6 +227,10 @@ module HTTP2
226
227
  else
227
228
  case frame[:type]
228
229
  when :headers
230
+ # When server receives even-numbered stream identifier,
231
+ # the endpoint MUST respond with a connection error of type PROTOCOL_ERROR.
232
+ connection_error if frame[:stream].even? && self.is_a?(Server)
233
+
229
234
  # The last frame in a sequence of HEADERS/CONTINUATION
230
235
  # frames MUST have the END_HEADERS flag set.
231
236
  unless frame[:flags].include? :end_headers
@@ -278,6 +283,12 @@ module HTTP2
278
283
  parent = @streams[frame[:stream]]
279
284
  pid = frame[:promise_stream]
280
285
 
286
+ # if PUSH parent is recently closed, RST_STREAM the push
287
+ if @streams_recently_closed[frame[:stream]]
288
+ send(type: :rst_stream, stream: pid, error: :refused_stream)
289
+ return
290
+ end
291
+
281
292
  connection_error(msg: 'missing parent ID') if parent.nil?
282
293
 
283
294
  unless parent.state == :open || parent.state == :half_closed_local
@@ -330,7 +341,7 @@ module HTTP2
330
341
  raise if e.is_a?(Error::Error)
331
342
  connection_error(e: e)
332
343
  end
333
- alias_method :<<, :receive
344
+ alias << receive
334
345
 
335
346
  private
336
347
 
@@ -364,12 +375,10 @@ module HTTP2
364
375
  # @param frame [Hash]
365
376
  # @return [Array of Buffer] encoded frame
366
377
  def encode(frame)
367
- frames = []
368
-
369
- if frame[:type] == :headers || frame[:type] == :push_promise
370
- frames = encode_headers(frame) # HEADERS and PUSH_PROMISE may create more than one frame
378
+ frames = if frame[:type] == :headers || frame[:type] == :push_promise
379
+ encode_headers(frame) # HEADERS and PUSH_PROMISE may create more than one frame
371
380
  else
372
- frames = [frame] # otherwise one frame
381
+ [frame] # otherwise one frame
373
382
  end
374
383
 
375
384
  frames.map { |f| @framer.generate(f) }
@@ -381,7 +390,7 @@ module HTTP2
381
390
  # @param frame [Hash]
382
391
  # @return [Boolean]
383
392
  def connection_frame?(frame)
384
- frame[:stream] == 0 ||
393
+ (frame[:stream]).zero? ||
385
394
  frame[:type] == :settings ||
386
395
  frame[:type] == :ping ||
387
396
  frame[:type] == :goaway
@@ -447,11 +456,11 @@ module HTTP2
447
456
  # Clients MUST reject any attempt to change the
448
457
  # SETTINGS_ENABLE_PUSH setting to a value other than 0 by treating the
449
458
  # message as a connection error (Section 5.4.1) of type PROTOCOL_ERROR.
450
- return ProtocolError.new("invalid #{key} value") unless v == 0
459
+ return ProtocolError.new("invalid #{key} value") unless v.zero?
451
460
  when :client
452
461
  # Any value other than 0 or 1 MUST be treated as a
453
462
  # connection error (Section 5.4.1) of type PROTOCOL_ERROR.
454
- unless v == 0 || v == 1
463
+ unless v.zero? || v == 1
455
464
  return ProtocolError.new("invalid #{key} value")
456
465
  end
457
466
  end
@@ -485,7 +494,7 @@ module HTTP2
485
494
  #
486
495
  # @param frame [Hash]
487
496
  def connection_settings(frame)
488
- connection_error unless frame[:type] == :settings && frame[:stream] == 0
497
+ connection_error unless frame[:type] == :settings && (frame[:stream]).zero?
489
498
 
490
499
  # Apply settings.
491
500
  # side =
@@ -576,7 +585,7 @@ module HTTP2
576
585
  #
577
586
  # @param frame [Hash]
578
587
  def decode_headers(frame)
579
- if frame[:payload].is_a? String
588
+ if frame[:payload].is_a? Buffer
580
589
  frame[:payload] = @decompressor.decode(frame[:payload])
581
590
  end
582
591
 
@@ -590,11 +599,11 @@ module HTTP2
590
599
  # @return [Array of Frame]
591
600
  def encode_headers(frame)
592
601
  payload = frame[:payload]
593
- payload = @compressor.encode(payload) unless payload.is_a? String
602
+ payload = @compressor.encode(payload) unless payload.is_a? Buffer
594
603
 
595
604
  frames = []
596
605
 
597
- while payload.size > 0
606
+ while payload.bytesize > 0
598
607
  cont = frame.dup
599
608
  cont[:type] = :continuation
600
609
  cont[:flags] = []
@@ -632,7 +641,18 @@ module HTTP2
632
641
  # states count toward the maximum number of streams that an endpoint is
633
642
  # permitted to open.
634
643
  stream.once(:active) { @active_stream_count += 1 }
635
- stream.once(:close) { @active_stream_count -= 1 }
644
+ stream.once(:close) do
645
+ @streams.delete id
646
+ @active_stream_count -= 1
647
+
648
+ # Store a reference to the closed stream, such that we can respond
649
+ # to any in-flight frames while close is registered on both sides.
650
+ # References to such streams will be purged whenever another stream
651
+ # is closed, with a minimum of 15s RTT time window.
652
+ @streams_recently_closed.delete_if { |_, v| (Time.now - v) > 15 }
653
+ @streams_recently_closed[id] = Time.now
654
+ end
655
+
636
656
  stream.on(:promise, &method(:promise)) if self.is_a? Server
637
657
  stream.on(:frame, &method(:send))
638
658
  stream.on(:window_update, &method(:window_update))
@@ -656,7 +676,9 @@ module HTTP2
656
676
 
657
677
  @state, @error = :closed, error
658
678
  klass = error.to_s.split('_').map(&:capitalize).join
659
- fail Error.const_get(klass), msg || e.message, e.backtrace || []
679
+ msg ||= e && e.message
680
+ backtrace = (e && e.backtrace) || []
681
+ fail Error.const_get(klass), msg, backtrace
660
682
  end
661
683
  end
662
684
  end
@@ -11,7 +11,7 @@ module HTTP2
11
11
  fail ArgumentError, 'must provide callback' unless block_given?
12
12
  listeners(event.to_sym).push block
13
13
  end
14
- alias_method :on, :add_listener
14
+ alias on add_listener
15
15
 
16
16
  # Subscribe to next event (at most once) for specified type.
17
17
  #