http-2 1.1.2 → 1.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 05c943cd91ee0339a8542cde981636d694618d64c80e30ff0f6ccb55fc636d45
4
- data.tar.gz: 325abe52e215d7d7007f9fe383a3e8ebd8b1cdfae1a004fc227b5371efb637b8
3
+ metadata.gz: 37f0171472c53a3f13cb8f8259302aed5f07c9dbc90f27893ddf8b868c78e41a
4
+ data.tar.gz: 52b05bc6ebc9d08d2a761ba465f39315e32ad3738a4c22d1d9a3ab58e7357867
5
5
  SHA512:
6
- metadata.gz: 88a97115e7d09b247b669c304fd3bced538c8be53263ca4f2650ddae125e7d7a3fa6b97419e2f8dbac9dcd3c95a0c581a494fc4b185664ae5d2d64bee9da3d7c
7
- data.tar.gz: f8f257ed1e984d82c8209677d2496c21f000c9dc23ca6f754fe50b1309c4c2b92491fb4d6dfb608eb5f54923cf47fb1a2f508bbb97fa4ec2b1d62b5f51dbb145
6
+ metadata.gz: 9f9c98e6f3d031388f451d22b142764d221967bcf72252689652182ffcd9e47bb60b90003491b7a7056eea51a7faaaece880c942ef4d02d223579924587ab103
7
+ data.tar.gz: 1bbe211ac07f751a5381b2c8d633392bb5460b5324af0f717f7ad06f4ff8695c7a3a8122bcca8ceb01b7b5063cd3f95c4e8fdff14a1bbcc3ce640e5609d90da9
data/lib/http/2/client.rb CHANGED
@@ -69,7 +69,7 @@ module HTTP2
69
69
  end
70
70
 
71
71
  def self.settings_header(settings)
72
- frame = Framer.new.generate(type: :settings, stream: 0, payload: settings)
72
+ frame = Framer.new.generate(type: :settings, stream: 0, flags: 0, payload: settings)
73
73
  Base64.urlsafe_encode64(frame[9..-1])
74
74
  end
75
75
 
@@ -41,8 +41,6 @@ module HTTP2
41
41
 
42
42
  CONNECTION_FRAME_TYPES = %i[settings ping goaway].freeze
43
43
 
44
- HEADERS_FRAME_TYPES = %i[headers push_promise].freeze
45
-
46
44
  STREAM_OPEN_STATES = %i[open half_closed_local].freeze
47
45
 
48
46
  # Connection encapsulates all of the connection, stream, flow-control,
@@ -91,6 +89,7 @@ module HTTP2
91
89
  @last_stream_id = 0
92
90
  @streams = {}
93
91
  @streams_recently_closed = {}
92
+ @oldest_stream_recently_closed = nil
94
93
  @pending_settings = []
95
94
 
96
95
  @framer = Framer.new(@local_settings[:settings_max_frame_size])
@@ -102,6 +101,7 @@ module HTTP2
102
101
 
103
102
  @recv_buffer = "".b
104
103
  @continuation = []
104
+ @continuation_size = 0
105
105
  @error = nil
106
106
 
107
107
  @h2c_upgrade = nil
@@ -156,7 +156,7 @@ module HTTP2
156
156
  # @param error [Symbol]
157
157
  # @param payload [String]
158
158
  def goaway(error = :no_error, payload = nil)
159
- send(type: :goaway, last_stream: @last_stream_id,
159
+ send(type: :goaway, stream: 0, last_stream: @last_stream_id,
160
160
  error: error, payload: payload)
161
161
  @state = :closed
162
162
  @closed_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -178,7 +178,6 @@ module HTTP2
178
178
  validate_settings(@local_role, payload)
179
179
  @pending_settings << payload
180
180
  send(type: :settings, stream: 0, payload: payload)
181
- @pending_settings << payload
182
181
  end
183
182
 
184
183
  # Decodes incoming bytes into HTTP 2.0 frames and routes them to
@@ -212,9 +211,8 @@ module HTTP2
212
211
  end
213
212
 
214
213
  while (frame = @framer.parse(@recv_buffer))
215
- # @type var stream_id: Integer
216
- stream_id = frame[:stream]
217
- frame_type = frame[:type]
214
+ stream_id = frame[:stream] #: Integer
215
+ frame_type = frame[:type] #: Symbol?
218
216
 
219
217
  if is_a?(Client) && !@received_frame
220
218
  connection_error(:protocol_error, msg: "didn't receive settings") if frame_type != :settings
@@ -237,15 +235,16 @@ module HTTP2
237
235
  # Header blocks MUST be transmitted as a contiguous sequence of frames
238
236
  # with no interleaved frames of any other type, or from any other stream.
239
237
  unless @continuation.empty?
238
+ # @type var frame: continuation_frame
240
239
  connection_error unless frame_type == :continuation && stream_id == @continuation.first[:stream]
241
240
 
242
241
  @continuation << frame
243
- unless frame[:flags].include? :end_headers
244
- buffered_payload = @continuation.sum { |f| f[:payload].bytesize }
242
+ @continuation_size += frame[:payload].bytesize
243
+ unless frame[:flags].anybits?(END_HEADERS)
245
244
  # prevent HTTP/2 CONTINUATION FLOOD
246
245
  # same heuristic as the one from HAProxy: https://www.haproxy.com/blog/haproxy-is-resilient-to-the-http-2-continuation-flood
247
246
  # different mitigation (connection closed, instead of 400 response)
248
- unless buffered_payload < @local_settings[:settings_max_frame_size]
247
+ unless @continuation_size < @local_settings[:settings_max_frame_size]
249
248
  connection_error(:protocol_error,
250
249
  msg: "too many continuations received")
251
250
  end
@@ -259,10 +258,11 @@ module HTTP2
259
258
  frame_type = frame[:type]
260
259
 
261
260
  @continuation.clear
261
+ @continuation_size = 0
262
262
 
263
263
  frame.delete(:length)
264
264
  frame[:payload] = payload
265
- frame[:flags] << :end_headers
265
+ frame[:flags] |= END_HEADERS
266
266
  end
267
267
 
268
268
  # SETTINGS frames always apply to a connection, never a single stream.
@@ -271,18 +271,20 @@ module HTTP2
271
271
  # anything other than 0x0, the endpoint MUST respond with a connection
272
272
  # error (Section 5.4.1) of type PROTOCOL_ERROR.
273
273
  if connection_frame?(frame)
274
+ # @type var frame: connection_frame
274
275
  connection_error(:protocol_error) unless stream_id.zero?
275
276
  connection_management(frame)
276
277
  else
277
278
  case frame_type
278
279
  when :headers
280
+ # @type var frame: headers_frame
279
281
  # When server receives even-numbered stream identifier,
280
282
  # the endpoint MUST respond with a connection error of type PROTOCOL_ERROR.
281
283
  connection_error if stream_id.even? && is_a?(Server)
282
284
 
283
285
  # The last frame in a sequence of HEADERS/CONTINUATION
284
286
  # frames MUST have the END_HEADERS flag set.
285
- unless frame[:flags].include? :end_headers
287
+ unless frame[:flags].anybits?(END_HEADERS)
286
288
  @continuation << frame
287
289
  next
288
290
  end
@@ -312,9 +314,10 @@ module HTTP2
312
314
  stream << frame
313
315
 
314
316
  when :push_promise
317
+ # @type var frame: push_promise_frame
315
318
  # The last frame in a sequence of PUSH_PROMISE/CONTINUATION
316
319
  # frames MUST have the END_HEADERS flag set
317
- unless frame[:flags].include? :end_headers
320
+ unless frame[:flags].anybits?(END_HEADERS)
318
321
  @continuation << frame
319
322
  return
320
323
  end
@@ -434,20 +437,27 @@ module HTTP2
434
437
  # @note all frames are currently delivered in FIFO order.
435
438
  # @param frame [Hash]
436
439
  def send(frame)
437
- frame_type = frame[:type]
438
-
439
440
  emit(:frame_sent, frame)
440
- if frame_type == :data
441
- send_data(frame, true)
442
441
 
443
- elsif frame_type == :rst_stream && frame[:error] == :protocol_error
444
- # An endpoint can end a connection at any time. In particular, an
445
- # endpoint MAY choose to treat a stream error as a connection error.
442
+ frame_type = frame[:type] #: Symbol
446
443
 
447
- goaway(:protocol_error)
448
- else
444
+ case frame_type
445
+ when :data
446
+ #: @type var frame: data_frame
447
+ send_data(frame, true)
448
+ when :headers, :push_promise
449
449
  # HEADERS and PUSH_PROMISE may generate CONTINUATION. Also send
450
450
  # RST_STREAM that are not protocol errors
451
+ #: @type var frame: headers_frame | push_promise_frame
452
+ encode_headers(frame) # HEADERS and PUSH_PROMISE may create more than one frame
453
+ else
454
+ if frame_type == :rst_stream && frame[:error] == :protocol_error
455
+ # An endpoint can end a connection at any time. In particular, an
456
+ # endpoint MAY choose to treat a stream error as a connection error.
457
+
458
+ goaway(:protocol_error)
459
+ end
460
+ #: @type var frame: connection_frame
451
461
  encode(frame)
452
462
  end
453
463
  end
@@ -456,11 +466,7 @@ module HTTP2
456
466
  #
457
467
  # @param frame [Hash]
458
468
  def encode(frame)
459
- if HEADERS_FRAME_TYPES.include?(frame[:type])
460
- encode_headers(frame) # HEADERS and PUSH_PROMISE may create more than one frame
461
- else
462
- emit(:frame, @framer.generate(frame))
463
- end
469
+ emit(:frame, @framer.generate(frame))
464
470
  end
465
471
 
466
472
  # Check if frame is a connection frame: SETTINGS, PING, GOAWAY, and any
@@ -493,12 +499,15 @@ module HTTP2
493
499
  when :connected
494
500
  case frame_type
495
501
  when :settings
502
+ # @type var frame: settings_frame
496
503
  connection_settings(frame)
497
504
  when :window_update
505
+ # @type var frame: window_update_frame
498
506
  process_window_update(frame: frame, encode: true)
499
507
  when :ping
500
508
  ping_management(frame)
501
509
  when :goaway
510
+ # @type var frame: goaway_frame
502
511
  # Receivers of a GOAWAY frame MUST NOT open additional streams on
503
512
  # the connection, although a new connection can be established
504
513
  # for new streams.
@@ -506,19 +515,19 @@ module HTTP2
506
515
  @closed_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
507
516
  emit(:goaway, frame[:last_stream], frame[:error], frame[:payload])
508
517
  when :altsvc
518
+ # @type var frame: altsvc_frame
509
519
  origin = frame[:origin]
510
520
  # 4. The ALTSVC HTTP/2 Frame
511
521
  # An ALTSVC frame on stream 0 with empty (length 0) "Origin"
512
522
  # information is invalid and MUST be ignored.
513
523
  emit(:altsvc, frame) if origin && !origin.empty?
514
524
  when :origin
515
- return if @h2c_upgrade || !frame[:flags].empty?
525
+ # @type var frame: origin_frame
526
+ return if @h2c_upgrade || !frame[:flags].zero?
516
527
 
517
528
  frame[:payload].each do |orig|
518
529
  emit(:origin, orig)
519
530
  end
520
- when :blocked
521
- emit(:blocked, frame)
522
531
  else
523
532
  connection_error
524
533
  end
@@ -538,11 +547,10 @@ module HTTP2
538
547
  end
539
548
 
540
549
  def ping_management(frame)
541
- if frame[:flags].include? :ack
550
+ if frame[:flags].anybits?(ACK)
542
551
  emit(:ack, frame[:payload])
543
552
  else
544
- send(type: :ping, stream: 0,
545
- flags: [:ack], payload: frame[:payload])
553
+ send(type: :ping, stream: 0, flags: ACK, payload: frame[:payload])
546
554
  end
547
555
  end
548
556
 
@@ -605,13 +613,13 @@ module HTTP2
605
613
  # side =
606
614
  # local: previously sent and pended our settings should be effective
607
615
  # remote: just received peer settings should immediately be effective
608
- if frame[:flags].include?(:ack)
616
+ if frame[:flags].anybits?(ACK)
609
617
  # Process pending settings we have sent.
610
618
  settings = @pending_settings.shift
611
619
  side = :local
612
620
  else
613
- validate_settings(@remote_role, frame[:payload])
614
621
  settings = frame[:payload]
622
+ validate_settings(@remote_role, settings)
615
623
  side = :remote
616
624
  end
617
625
 
@@ -681,7 +689,7 @@ module HTTP2
681
689
  when :remote
682
690
  unless @state == :closed || @h2c_upgrade == :start
683
691
  # Send ack to peer
684
- send(type: :settings, stream: 0, payload: [], flags: [:ack])
692
+ send(type: :settings, stream: 0, payload: EMPTY, flags: ACK)
685
693
  # when initial window size changes, we try to flush any buffered
686
694
  # data.
687
695
  @streams.each_value(&:flush)
@@ -711,45 +719,49 @@ module HTTP2
711
719
  #
712
720
  # @param headers_frame [Hash]
713
721
  def encode_headers(headers_frame)
714
- payload = headers_frame[:payload]
722
+ headers_payload = headers_frame[:payload]
715
723
  begin
716
- payload = headers_frame[:payload] = @compressor.encode(payload) unless payload.is_a?(String)
724
+ payload = headers_payload.is_a?(String) ? headers_payload : @compressor.encode(headers_payload)
717
725
  rescue StandardError => e
718
726
  connection_error(:compression_error, e: e)
719
727
  end
720
728
 
729
+ #: @type var payload: String
730
+ headers_frame[:payload] = payload
731
+
721
732
  max_frame_size = @remote_settings[:settings_max_frame_size]
722
733
 
723
734
  # if single frame, return immediately
724
735
  if payload.bytesize <= max_frame_size
725
- emit(:frame, @framer.generate(headers_frame))
736
+ encode(headers_frame)
726
737
  return
727
738
  end
728
739
 
729
740
  # split into multiple CONTINUATION frames
730
- headers_frame[:flags].delete(:end_headers)
741
+ total = payload.bytesize
742
+ headers_frame[:flags] ^= END_HEADERS
731
743
  headers_frame[:payload] = payload.byteslice(0, max_frame_size)
732
- payload = payload.byteslice(max_frame_size..-1)
744
+ # payload = payload.byteslice(max_frame_size..-1)
745
+ offset = max_frame_size
733
746
 
734
747
  # emit first HEADERS frame
735
- emit(:frame, @framer.generate(headers_frame))
748
+ encode(headers_frame)
736
749
 
737
- loop do
738
- continuation_frame = headers_frame.merge(
739
- type: :continuation,
740
- flags: EMPTY,
741
- payload: payload.byteslice(0, max_frame_size)
742
- )
750
+ stream_id = headers_frame[:stream]
743
751
 
744
- payload = payload.byteslice(max_frame_size..-1)
752
+ while offset < total
753
+ chunk_end = offset + max_frame_size
754
+ is_last = chunk_end >= total
745
755
 
746
- if payload.nil? || payload.empty?
747
- continuation_frame[:flags] = [:end_headers]
748
- emit(:frame, @framer.generate(continuation_frame))
749
- break
750
- end
756
+ continuation_frame = {
757
+ type: :continuation,
758
+ flags: is_last ? END_HEADERS : 0,
759
+ stream: stream_id,
760
+ payload: payload.byteslice(offset, max_frame_size)
761
+ } #: continuation_frame
751
762
 
752
- emit(:frame, @framer.generate(continuation_frame))
763
+ encode(continuation_frame)
764
+ offset = chunk_end
753
765
  end
754
766
  end
755
767
 
@@ -776,19 +788,24 @@ module HTTP2
776
788
  # is closed, with a minimum of 15s RTT time window.
777
789
  now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
778
790
 
779
- _, closed_since = @streams_recently_closed.first
780
-
781
791
  # forego recently closed recycling if empty or the first element
782
792
  # hasn't expired yet (it's ordered).
783
- if closed_since && (now - closed_since) > 15
793
+ if @oldest_stream_recently_closed && (now - @oldest_stream_recently_closed) > 15
794
+ new_oldest = nil
784
795
  # discards all streams which have closed for a while.
785
796
  # TODO: use a drop_while! variant whenever there is one.
786
- @streams_recently_closed = @streams_recently_closed.drop_while do |_, since|
787
- (now - since) > 15
788
- end.to_h
797
+ @streams_recently_closed.delete_if do |_, since|
798
+ unless (now - since) > 15
799
+ new_oldest ||= since
800
+ break
801
+ end
802
+
803
+ true
804
+ end
805
+ @oldest_stream_recently_closed = new_oldest
789
806
  end
790
807
 
791
- @streams_recently_closed[id] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
808
+ @streams_recently_closed[id] = now
792
809
  end
793
810
 
794
811
  stream.on(:frame, &method(:send))
@@ -22,22 +22,28 @@ module HTTP2
22
22
  end
23
23
  end
24
24
 
25
- def read_str(str, n)
26
- return "".b if n == 0
25
+ if String.method_defined?(:bytesplice)
26
+ def read_str(str, n)
27
+ return "".b if n == 0
27
28
 
28
- chunk = str.byteslice(0..(n - 1))
29
- remaining = str.byteslice(n..-1)
30
- remaining ? str.replace(remaining) : str.clear
31
- chunk
29
+ chunk = str.byteslice(0, n)
30
+ str.bytesplice(0, chunk.length, "")
31
+ chunk
32
+ end
33
+ else
34
+ def read_str(str, n)
35
+ return "".b if n == 0
36
+
37
+ chunk = str.byteslice(0, n)
38
+ remaining = str.byteslice(n, str.size - n)
39
+ remaining ? str.replace(remaining) : str.clear
40
+ chunk
41
+ end
32
42
  end
33
43
 
34
44
  def read_uint32(str)
35
45
  read_str(str, 4).unpack1("N")
36
46
  end
37
-
38
- def shift_byte(str)
39
- read_str(str, 1).ord
40
- end
41
47
  end
42
48
 
43
49
  # this mixin handles backwards-compatibility for the new packing options
@@ -70,7 +70,7 @@ module HTTP2
70
70
  if frame
71
71
  if @send_buffer.empty?
72
72
  frame_size = frame[:payload].bytesize
73
- end_stream = frame[:flags].include?(:end_stream)
73
+ end_stream = frame[:flags].anybits?(END_STREAM)
74
74
  # if buffer is empty, and frame is either end 0 length OR
75
75
  # is within available window size, skip buffering and send immediately.
76
76
  if @remote_window.positive?
@@ -115,6 +115,8 @@ module HTTP2
115
115
  end
116
116
 
117
117
  class FrameBuffer
118
+ include BufferUtils
119
+
118
120
  attr_reader :bytesize
119
121
 
120
122
  def initialize
@@ -127,6 +129,11 @@ module HTTP2
127
129
  @bytesize += frame[:payload].bytesize
128
130
  end
129
131
 
132
+ def clear
133
+ @buffer.clear
134
+ @bytesize = 0
135
+ end
136
+
130
137
  def empty?
131
138
  @buffer.empty?
132
139
  end
@@ -135,7 +142,7 @@ module HTTP2
135
142
  frame = @buffer.first or return
136
143
 
137
144
  frame_size = frame[:payload].bytesize
138
- end_stream = frame[:flags].include?(:end_stream)
145
+ end_stream = frame[:flags].anybits?(END_STREAM)
139
146
 
140
147
  # Frames with zero length with the END_STREAM flag set (that
141
148
  # is, an empty DATA frame) MAY be sent if there is no available space
@@ -143,19 +150,17 @@ module HTTP2
143
150
  return if window_size <= 0 && !(frame_size.zero? && end_stream)
144
151
 
145
152
  if frame_size > window_size
146
- chunk = frame.dup
147
- payload = frame[:payload]
153
+ chunk = frame.dup
148
154
 
149
155
  # Split frame so that it fits in the window
150
156
  # TODO: consider padding!
151
157
 
152
- chunk[:payload] = payload.byteslice(0, window_size)
158
+ chunk[:payload] = read_str(frame[:payload], window_size) # mutates frame[:payload]
153
159
  chunk[:length] = window_size
154
- frame[:payload] = payload.byteslice(window_size..-1)
155
160
  frame[:length] = frame_size - window_size
156
161
 
157
162
  # if no longer last frame in sequence...
158
- chunk[:flags] -= [:end_stream] if end_stream
163
+ chunk[:flags] ^= END_STREAM if end_stream
159
164
 
160
165
  @bytesize -= window_size
161
166
  chunk