http-2 0.8.4 → 0.10.2

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.
@@ -39,7 +39,7 @@ end
39
39
 
40
40
  class UpgradeHandler
41
41
  VALID_UPGRADE_METHODS = %w(GET OPTIONS).freeze
42
- UPGRADE_RESPONSE = <<-RESP
42
+ UPGRADE_RESPONSE = <<RESP.freeze
43
43
  HTTP/1.1 101 Switching Protocols
44
44
  Connection: Upgrade
45
45
  Upgrade: h2c
@@ -191,7 +191,7 @@ loop do
191
191
  conn << data
192
192
  end
193
193
 
194
- rescue => e
194
+ rescue StandardError => e
195
195
  puts "Exception: #{e}, #{e.message} - closing socket."
196
196
  puts e.backtrace.last(10).join("\n")
197
197
  sock.close
@@ -1,5 +1,4 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ lib = File.expand_path('./lib', __dir__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
3
  require 'http/2/version'
5
4
 
@@ -19,5 +18,5 @@ Gem::Specification.new do |spec|
19
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
19
  spec.require_paths = ['lib']
21
20
 
22
- spec.add_development_dependency 'bundler', '~> 1.3'
21
+ spec.add_development_dependency 'bundler'
23
22
  end
@@ -38,14 +38,31 @@ module HTTP2
38
38
  super(frame)
39
39
  end
40
40
 
41
+ def receive(frame)
42
+ send_connection_preface
43
+ super(frame)
44
+ end
45
+
46
+ # sends the preface and initializes the first stream in half-closed state
47
+ def upgrade
48
+ fail ProtocolError unless @stream_id == 1
49
+ send_connection_preface
50
+ new_stream(state: :half_closed_local)
51
+ end
52
+
41
53
  # Emit the connection preface if not yet
42
54
  def send_connection_preface
43
55
  return unless @state == :waiting_connection_preface
44
56
  @state = :connected
45
57
  emit(:frame, CONNECTION_PREFACE_MAGIC)
46
58
 
47
- payload = @local_settings.select { |k, v| v != SPEC_DEFAULT_CONNECTION_SETTINGS[k] }
59
+ payload = @local_settings.reject { |k, v| v == SPEC_DEFAULT_CONNECTION_SETTINGS[k] }
48
60
  settings(payload)
49
61
  end
62
+
63
+ def self.settings_header(**settings)
64
+ frame = Framer.new.generate(type: :settings, stream: 0, payload: settings)
65
+ Base64.urlsafe_encode64(frame[9..-1])
66
+ end
50
67
  end
51
68
  end
@@ -139,12 +139,16 @@ module HTTP2
139
139
  # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-4.1
140
140
  #
141
141
  # @param cmd [Hash] { type:, name:, value:, index: }
142
- # @return [Array] +[name, value]+ header field that is added to the decoded header list
142
+ # @return [Array, nil] +[name, value]+ header field that is added to the decoded header list,
143
+ # or nil if +cmd[:type]+ is +:changetablesize+
143
144
  def process(cmd)
144
145
  emit = nil
145
146
 
146
147
  case cmd[:type]
147
148
  when :changetablesize
149
+ if cmd[:value] > @limit
150
+ fail CompressionError, 'dynamic table size update exceed limit'
151
+ end
148
152
  self.table_size = cmd[:value]
149
153
 
150
154
  when :indexed
@@ -199,8 +203,12 @@ module HTTP2
199
203
  commands = []
200
204
  # Literals commands are marked with :noindex when index is not used
201
205
  noindex = [:static, :never].include?(@options[:index])
202
- headers.each do |h|
203
- cmd = addcmd(h)
206
+ headers.each do |field, value|
207
+ # Literal header names MUST be translated to lowercase before
208
+ # encoding and transmission.
209
+ field = field.downcase
210
+ value = '/' if field == ':path' && value.empty?
211
+ cmd = addcmd(field, value)
204
212
  cmd[:type] = :noindex if noindex && cmd[:type] == :incremental
205
213
  commands << cmd
206
214
  process(cmd)
@@ -220,7 +228,7 @@ module HTTP2
220
228
  #
221
229
  # @param header [Array] +[name, value]+
222
230
  # @return [Hash] command
223
- def addcmd(header)
231
+ def addcmd(*header)
224
232
  exact = nil
225
233
  name_only = nil
226
234
 
@@ -441,11 +449,8 @@ module HTTP2
441
449
  # @return [Buffer]
442
450
  def encode(headers)
443
451
  buffer = Buffer.new
444
-
445
- # Literal header names MUST be translated to lowercase before
446
- # encoding and transmission.
447
- headers.map! { |hk, hv| [hk.downcase, hv] }
448
-
452
+ pseudo_headers, regular_headers = headers.partition { |f, _| f.start_with? ':' }
453
+ headers = [*pseudo_headers, *regular_headers]
449
454
  commands = @cc.encode(headers)
450
455
  commands.each do |cmd|
451
456
  buffer << header(cmd)
@@ -549,8 +554,18 @@ module HTTP2
549
554
  # @return [Array] +[[name, value], ...]+
550
555
  def decode(buf)
551
556
  list = []
552
- list << @cc.process(header(buf)) until buf.empty?
553
- list.compact
557
+ decoding_pseudo_headers = true
558
+ until buf.empty?
559
+ next_header = @cc.process(header(buf))
560
+ next if next_header.nil?
561
+ is_pseudo_header = next_header.first.start_with? ':'
562
+ if !decoding_pseudo_headers && is_pseudo_header
563
+ fail ProtocolError, 'one or more pseudo headers encountered after regular headers'
564
+ end
565
+ decoding_pseudo_headers = is_pseudo_header
566
+ list << next_header
567
+ end
568
+ list
554
569
  end
555
570
  end
556
571
  end
@@ -20,9 +20,9 @@ module HTTP2
20
20
 
21
21
  DEFAULT_CONNECTION_SETTINGS = {
22
22
  settings_header_table_size: 4096,
23
- settings_enable_push: 1, # enabled for servers
23
+ settings_enable_push: 1, # enabled for servers
24
24
  settings_max_concurrent_streams: 100,
25
- settings_initial_window_size: 65_535, #
25
+ settings_initial_window_size: 65_535,
26
26
  settings_max_frame_size: 16_384,
27
27
  settings_max_header_list_size: 2**31 - 1, # unlimited
28
28
  }.freeze
@@ -33,6 +33,9 @@ module HTTP2
33
33
  # Default connection "fast-fail" preamble string as defined by the spec.
34
34
  CONNECTION_PREFACE_MAGIC = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".freeze
35
35
 
36
+ # Time to hold recently closed streams until purge (seconds)
37
+ RECENTLY_CLOSED_STREAMS_TTL = 15
38
+
36
39
  # Connection encapsulates all of the connection, stream, flow-control,
37
40
  # error management, and other processing logic required for a well-behaved
38
41
  # HTTP 2.0 endpoint.
@@ -49,9 +52,6 @@ module HTTP2
49
52
  # Connection state (:new, :closed).
50
53
  attr_reader :state
51
54
 
52
- # Last connection error if connection is aborted.
53
- attr_reader :error
54
-
55
55
  # Size of current connection flow control window (by default, set to
56
56
  # infinity, but is automatically updated on receipt of peer settings).
57
57
  attr_reader :local_window
@@ -95,6 +95,13 @@ module HTTP2
95
95
  @send_buffer = []
96
96
  @continuation = []
97
97
  @error = nil
98
+
99
+ @h2c_upgrade = nil
100
+ @closed_since = nil
101
+ end
102
+
103
+ def closed?
104
+ @state == :closed
98
105
  end
99
106
 
100
107
  # Allocates new stream for current connection.
@@ -141,12 +148,14 @@ module HTTP2
141
148
  send(type: :goaway, last_stream: last_stream,
142
149
  error: error, payload: payload)
143
150
  @state = :closed
151
+ @closed_since = Time.now
144
152
  end
145
153
 
146
154
  # Sends a WINDOW_UPDATE frame to the peer.
147
155
  #
148
156
  # @param increment [Integer]
149
157
  def window_update(increment)
158
+ @local_window += increment
150
159
  send(type: :window_update, stream: 0, increment: increment)
151
160
  end
152
161
 
@@ -312,6 +321,10 @@ module HTTP2
312
321
  else
313
322
  if (stream = @streams[frame[:stream]])
314
323
  stream << frame
324
+ if frame[:type] == :data
325
+ update_local_window(frame)
326
+ calculate_window_update(@local_window_limit)
327
+ end
315
328
  else
316
329
  case frame[:type]
317
330
  # The PRIORITY frame can be sent for a stream in the "idle" or
@@ -346,11 +359,14 @@ module HTTP2
346
359
  end
347
360
  end
348
361
 
349
- rescue => e
362
+ rescue StandardError => e
350
363
  raise if e.is_a?(Error::Error)
351
364
  connection_error(e: e)
352
365
  end
353
- alias << receive
366
+
367
+ def <<(*args)
368
+ receive(*args)
369
+ end
354
370
 
355
371
  private
356
372
 
@@ -438,12 +454,22 @@ module HTTP2
438
454
  # the connection, although a new connection can be established
439
455
  # for new streams.
440
456
  @state = :closed
457
+ @closed_since = Time.now
441
458
  emit(:goaway, frame[:last_stream], frame[:error], frame[:payload])
442
- when :altsvc, :blocked
459
+ when :altsvc
460
+ # 4. The ALTSVC HTTP/2 Frame
461
+ # An ALTSVC frame on stream 0 with empty (length 0) "Origin"
462
+ # information is invalid and MUST be ignored.
463
+ if frame[:origin] && !frame[:origin].empty?
464
+ emit(frame[:type], frame)
465
+ end
466
+ when :blocked
443
467
  emit(frame[:type], frame)
444
468
  else
445
469
  connection_error
446
470
  end
471
+ when :closed
472
+ connection_error if (Time.now - @closed_since) > 15
447
473
  else
448
474
  connection_error
449
475
  end
@@ -488,7 +514,7 @@ module HTTP2
488
514
  # allowed frame size (2^24-1 or 16,777,215 octets), inclusive.
489
515
  # Values outside this range MUST be treated as a connection error
490
516
  # (Section 5.4.1) of type PROTOCOL_ERROR.
491
- unless 16_384 <= v && v <= 16_777_215
517
+ unless v >= 16_384 && v <= 16_777_215
492
518
  return ProtocolError.new("invalid #{key} value")
493
519
  end
494
520
  when :settings_max_header_list_size
@@ -598,8 +624,12 @@ module HTTP2
598
624
  frame[:payload] = @decompressor.decode(frame[:payload])
599
625
  end
600
626
 
601
- rescue => e
627
+ rescue CompressionError => e
602
628
  connection_error(:compression_error, e: e)
629
+ rescue ProtocolError => e
630
+ connection_error(:protocol_error, e: e)
631
+ rescue StandardError => e
632
+ connection_error(:internal_error, e: e)
603
633
  end
604
634
 
605
635
  # Encode headers payload and update connection compressor state.
@@ -629,7 +659,7 @@ module HTTP2
629
659
 
630
660
  frames
631
661
 
632
- rescue => e
662
+ rescue StandardError => e
633
663
  connection_error(:compression_error, e: e)
634
664
  nil
635
665
  end
@@ -651,24 +681,38 @@ module HTTP2
651
681
  # permitted to open.
652
682
  stream.once(:active) { @active_stream_count += 1 }
653
683
  stream.once(:close) do
654
- @streams.delete id
655
684
  @active_stream_count -= 1
656
685
 
657
686
  # Store a reference to the closed stream, such that we can respond
658
687
  # to any in-flight frames while close is registered on both sides.
659
688
  # References to such streams will be purged whenever another stream
660
- # is closed, with a minimum of 15s RTT time window.
661
- @streams_recently_closed.delete_if { |_, v| (Time.now - v) > 15 }
662
- @streams_recently_closed[id] = Time.now
689
+ # is closed, with a defined RTT time window.
690
+ @streams_recently_closed[id] = Time.now.to_i
691
+ cleanup_recently_closed
663
692
  end
664
693
 
665
694
  stream.on(:promise, &method(:promise)) if self.is_a? Server
666
695
  stream.on(:frame, &method(:send))
667
- stream.on(:window_update, &method(:window_update))
668
696
 
669
697
  @streams[id] = stream
670
698
  end
671
699
 
700
+ # Purge recently streams closed within defined RTT time window.
701
+ def cleanup_recently_closed
702
+ now_ts = Time.now.to_i
703
+ to_delete = []
704
+ @streams_recently_closed.each do |stream_id, ts|
705
+ # Ruby Hash enumeration is ordered, so once fresh stream is met we can stop searching.
706
+ break if now_ts - ts < RECENTLY_CLOSED_STREAMS_TTL
707
+ to_delete << stream_id
708
+ end
709
+
710
+ to_delete.each do |stream_id|
711
+ @streams.delete stream_id
712
+ @streams_recently_closed.delete stream_id
713
+ end
714
+ end
715
+
672
716
  # Emit GOAWAY error indicating to peer that the connection is being
673
717
  # aborted, and once sent, raise a local exception.
674
718
  #
@@ -689,9 +733,11 @@ module HTTP2
689
733
  backtrace = (e && e.backtrace) || []
690
734
  fail Error.const_get(klass), msg, backtrace
691
735
  end
736
+ alias error connection_error
692
737
 
693
738
  def manage_state(_)
694
739
  yield
695
740
  end
696
741
  end
742
+ # rubocop:enable ClassLength
697
743
  end
@@ -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
@@ -359,14 +359,14 @@ module HTTP2
359
359
  if frame[:flags].include? :priority
360
360
  e_sd = payload.read_uint32
361
361
  frame[:stream_dependency] = e_sd & RBIT
362
- frame[:exclusive] = (e_sd & EBIT) != 0 # rubocop:disable Style/NumericPredicate
362
+ frame[:exclusive] = (e_sd & EBIT) != 0
363
363
  frame[:weight] = payload.getbyte + 1
364
364
  end
365
365
  frame[:payload] = payload.read(frame[:length])
366
366
  when :priority
367
367
  e_sd = payload.read_uint32
368
368
  frame[:stream_dependency] = e_sd & RBIT
369
- frame[:exclusive] = (e_sd & EBIT) != 0 # rubocop:disable Style/NumericPredicate
369
+ frame[:exclusive] = (e_sd & EBIT) != 0
370
370
  frame[:weight] = payload.getbyte + 1
371
371
  when :rst_stream
372
372
  frame[:error] = unpack_error payload.read_uint32
@@ -442,4 +442,5 @@ module HTTP2
442
442
  name || error
443
443
  end
444
444
  end
445
+ # rubocop:enable ClassLength
445
446
  end
@@ -71,22 +71,28 @@ 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 }
92
+ end
93
+
94
+ def closed?
95
+ @state == :closed
90
96
  end
91
97
 
92
98
  # Processes incoming HTTP 2.0 frames. The frames must be decoded upstream.
@@ -97,21 +103,27 @@ module HTTP2
97
103
 
98
104
  case frame[:type]
99
105
  when :data
100
- window_size = frame[:payload].bytesize
101
- window_size += frame[:padding] || 0
102
- @local_window -= window_size
106
+ update_local_window(frame)
107
+ # Emit DATA frame
103
108
  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
109
+ calculate_window_update(@local_window_max_size)
110
+ when :headers
109
111
  emit(:headers, frame[:payload]) unless frame[:ignore]
112
+ when :push_promise
113
+ emit(:promise_headers, frame[:payload]) unless frame[:ignore]
110
114
  when :priority
111
115
  process_priority(frame)
112
116
  when :window_update
113
117
  process_window_update(frame)
114
- when :altsvc, :blocked
118
+ when :altsvc
119
+ # 4. The ALTSVC HTTP/2 Frame
120
+ # An ALTSVC frame on a
121
+ # stream other than stream 0 containing non-empty "Origin" information
122
+ # is invalid and MUST be ignored.
123
+ if !frame[:origin] || frame[:origin].empty?
124
+ emit(frame[:type], frame)
125
+ end
126
+ when :blocked
115
127
  emit(frame[:type], frame)
116
128
  end
117
129
 
@@ -144,6 +156,7 @@ module HTTP2
144
156
  end
145
157
 
146
158
  # Sends a HEADERS frame containing HTTP response headers.
159
+ # All pseudo-header fields MUST appear in the header block before regular header fields.
147
160
  #
148
161
  # @param headers [Array or Hash] Array of key-value pairs or Hash
149
162
  # @param end_headers [Boolean] indicates that no more headers will be sent
@@ -153,7 +166,7 @@ module HTTP2
153
166
  flags << :end_headers if end_headers
154
167
  flags << :end_stream if end_stream
155
168
 
156
- send(type: :headers, flags: flags, payload: headers.to_a)
169
+ send(type: :headers, flags: flags, payload: headers)
157
170
  end
158
171
 
159
172
  def promise(headers, end_headers: true, &block)
@@ -228,8 +241,6 @@ module HTTP2
228
241
  #
229
242
  # @param increment [Integer]
230
243
  def window_update(increment)
231
- # always emit connection-level WINDOW_UPDATE
232
- emit(:window_update, increment)
233
244
  # emit stream-level WINDOW_UPDATE unless stream is closed
234
245
  return if @state == :closed || @state == :remote_closed
235
246
  send(type: :window_update, increment: increment)
@@ -602,6 +613,7 @@ module HTTP2
602
613
  klass = error.to_s.split('_').map(&:capitalize).join
603
614
  fail Error.const_get(klass), msg
604
615
  end
616
+ alias error stream_error
605
617
 
606
618
  def manage_state(frame)
607
619
  transition(frame, true)