http-2 0.8.4 → 0.10.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)