http-2 1.1.3 → 1.2.1

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: a18cb0edf24b0b14f60e7f2e2e6f5db5329402434e2cc073c32f827650f7908d
4
- data.tar.gz: 1c0e51febce4eff4080e368c8e967797f4508a4be20ee13edc75309c71035a44
3
+ metadata.gz: d1db63e3235efa25ad18a270b128adc3955489766148712fd9405142d9cf1bd7
4
+ data.tar.gz: d3cfbb973cb9a3bc53466c106a6a33b6521f7ba8415d832a646ef96419c906cd
5
5
  SHA512:
6
- metadata.gz: 33e26c894fa29c1fde511b83f3eaed235e8816a64ea2f82329875ab115008f0c42f9a651414e287f9137ed7dac98a8a6a85cc74a829f80ecd4dbdd9cf84350ec
7
- data.tar.gz: fb2c13c2801f742f30680a694f8d6058009ab8731208e2f0e97ce63d7c933fc8d9bb006391847b35bb4504d1ca0136e2634284c9e6b2ecb087c9594b332469ca
6
+ metadata.gz: 9a17f2bdcbc5fcd07ff0797994af22a0f170744f516702b5b5c2c8c032f60a15cf4c70869804c56f4e15e10829c2c51c8ea0fe5575751cf28b1fe0207389543a
7
+ data.tar.gz: e599eb9b1bfa7a4985b6a0f53fe1592e6d26ea780fc16d8471d23cc2abe5020a91e28fba80e1d227492525373dd117677e2c436bb289022875c3986f3733bb9e
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
@@ -396,7 +399,6 @@ module HTTP2
396
399
  unless @streams_recently_closed.key?(stream_id)
397
400
  connection_error(:protocol_error, msg: "sent window update on idle stream")
398
401
  end
399
- stream = @streams_recently_closed[stream_id]
400
402
  process_window_update(frame: frame, encode: true)
401
403
  # Endpoints MUST ignore
402
404
  # WINDOW_UPDATE or RST_STREAM frames received in this state (closed), though
@@ -434,20 +436,27 @@ module HTTP2
434
436
  # @note all frames are currently delivered in FIFO order.
435
437
  # @param frame [Hash]
436
438
  def send(frame)
437
- frame_type = frame[:type]
438
-
439
439
  emit(:frame_sent, frame)
440
- if frame_type == :data
441
- send_data(frame, true)
442
440
 
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.
441
+ frame_type = frame[:type] #: Symbol
446
442
 
447
- goaway(:protocol_error)
448
- else
443
+ case frame_type
444
+ when :data
445
+ #: @type var frame: data_frame
446
+ send_data(frame, true)
447
+ when :headers, :push_promise
449
448
  # HEADERS and PUSH_PROMISE may generate CONTINUATION. Also send
450
449
  # RST_STREAM that are not protocol errors
450
+ #: @type var frame: headers_frame | push_promise_frame
451
+ encode_headers(frame) # HEADERS and PUSH_PROMISE may create more than one frame
452
+ else
453
+ if frame_type == :rst_stream && frame[:error] == :protocol_error
454
+ # An endpoint can end a connection at any time. In particular, an
455
+ # endpoint MAY choose to treat a stream error as a connection error.
456
+
457
+ goaway(:protocol_error)
458
+ end
459
+ #: @type var frame: connection_frame
451
460
  encode(frame)
452
461
  end
453
462
  end
@@ -456,11 +465,7 @@ module HTTP2
456
465
  #
457
466
  # @param frame [Hash]
458
467
  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
468
+ emit(:frame, @framer.generate(frame))
464
469
  end
465
470
 
466
471
  # Check if frame is a connection frame: SETTINGS, PING, GOAWAY, and any
@@ -493,12 +498,15 @@ module HTTP2
493
498
  when :connected
494
499
  case frame_type
495
500
  when :settings
501
+ # @type var frame: settings_frame
496
502
  connection_settings(frame)
497
503
  when :window_update
504
+ # @type var frame: window_update_frame
498
505
  process_window_update(frame: frame, encode: true)
499
506
  when :ping
500
507
  ping_management(frame)
501
508
  when :goaway
509
+ # @type var frame: goaway_frame
502
510
  # Receivers of a GOAWAY frame MUST NOT open additional streams on
503
511
  # the connection, although a new connection can be established
504
512
  # for new streams.
@@ -506,19 +514,19 @@ module HTTP2
506
514
  @closed_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
507
515
  emit(:goaway, frame[:last_stream], frame[:error], frame[:payload])
508
516
  when :altsvc
517
+ # @type var frame: altsvc_frame
509
518
  origin = frame[:origin]
510
519
  # 4. The ALTSVC HTTP/2 Frame
511
520
  # An ALTSVC frame on stream 0 with empty (length 0) "Origin"
512
521
  # information is invalid and MUST be ignored.
513
522
  emit(:altsvc, frame) if origin && !origin.empty?
514
523
  when :origin
515
- return if @h2c_upgrade || !frame[:flags].empty?
524
+ # @type var frame: origin_frame
525
+ return if @h2c_upgrade || !frame[:flags].zero?
516
526
 
517
527
  frame[:payload].each do |orig|
518
528
  emit(:origin, orig)
519
529
  end
520
- when :blocked
521
- emit(:blocked, frame)
522
530
  else
523
531
  connection_error
524
532
  end
@@ -538,11 +546,10 @@ module HTTP2
538
546
  end
539
547
 
540
548
  def ping_management(frame)
541
- if frame[:flags].include? :ack
549
+ if frame[:flags].anybits?(ACK)
542
550
  emit(:ack, frame[:payload])
543
551
  else
544
- send(type: :ping, stream: 0,
545
- flags: [:ack], payload: frame[:payload])
552
+ send(type: :ping, stream: 0, flags: ACK, payload: frame[:payload])
546
553
  end
547
554
  end
548
555
 
@@ -605,13 +612,13 @@ module HTTP2
605
612
  # side =
606
613
  # local: previously sent and pended our settings should be effective
607
614
  # remote: just received peer settings should immediately be effective
608
- if frame[:flags].include?(:ack)
615
+ if frame[:flags].anybits?(ACK)
609
616
  # Process pending settings we have sent.
610
617
  settings = @pending_settings.shift
611
618
  side = :local
612
619
  else
613
- validate_settings(@remote_role, frame[:payload])
614
620
  settings = frame[:payload]
621
+ validate_settings(@remote_role, settings)
615
622
  side = :remote
616
623
  end
617
624
 
@@ -681,7 +688,7 @@ module HTTP2
681
688
  when :remote
682
689
  unless @state == :closed || @h2c_upgrade == :start
683
690
  # Send ack to peer
684
- send(type: :settings, stream: 0, payload: [], flags: [:ack])
691
+ send(type: :settings, stream: 0, payload: EMPTY, flags: ACK)
685
692
  # when initial window size changes, we try to flush any buffered
686
693
  # data.
687
694
  @streams.each_value(&:flush)
@@ -711,45 +718,49 @@ module HTTP2
711
718
  #
712
719
  # @param headers_frame [Hash]
713
720
  def encode_headers(headers_frame)
714
- payload = headers_frame[:payload]
721
+ headers_payload = headers_frame[:payload]
715
722
  begin
716
- payload = headers_frame[:payload] = @compressor.encode(payload) unless payload.is_a?(String)
723
+ payload = headers_payload.is_a?(String) ? headers_payload : @compressor.encode(headers_payload)
717
724
  rescue StandardError => e
718
725
  connection_error(:compression_error, e: e)
719
726
  end
720
727
 
728
+ #: @type var payload: String
729
+ headers_frame[:payload] = payload
730
+
721
731
  max_frame_size = @remote_settings[:settings_max_frame_size]
722
732
 
723
733
  # if single frame, return immediately
724
734
  if payload.bytesize <= max_frame_size
725
- emit(:frame, @framer.generate(headers_frame))
735
+ encode(headers_frame)
726
736
  return
727
737
  end
728
738
 
729
739
  # split into multiple CONTINUATION frames
730
- headers_frame[:flags].delete(:end_headers)
740
+ total = payload.bytesize
741
+ headers_frame[:flags] ^= END_HEADERS
731
742
  headers_frame[:payload] = payload.byteslice(0, max_frame_size)
732
- payload = payload.byteslice(max_frame_size..-1)
743
+ # payload = payload.byteslice(max_frame_size..-1)
744
+ offset = max_frame_size
733
745
 
734
746
  # emit first HEADERS frame
735
- emit(:frame, @framer.generate(headers_frame))
747
+ encode(headers_frame)
736
748
 
737
- loop do
738
- continuation_frame = headers_frame.merge(
739
- type: :continuation,
740
- flags: EMPTY,
741
- payload: payload.byteslice(0, max_frame_size)
742
- )
749
+ stream_id = headers_frame[:stream]
743
750
 
744
- payload = payload.byteslice(max_frame_size..-1)
751
+ while offset < total
752
+ chunk_end = offset + max_frame_size
753
+ is_last = chunk_end >= total
745
754
 
746
- if payload.nil? || payload.empty?
747
- continuation_frame[:flags] = [:end_headers]
748
- emit(:frame, @framer.generate(continuation_frame))
749
- break
750
- end
755
+ continuation_frame = {
756
+ type: :continuation,
757
+ flags: is_last ? END_HEADERS : 0,
758
+ stream: stream_id,
759
+ payload: payload.byteslice(offset, max_frame_size)
760
+ } #: continuation_frame
751
761
 
752
- emit(:frame, @framer.generate(continuation_frame))
762
+ encode(continuation_frame)
763
+ offset = chunk_end
753
764
  end
754
765
  end
755
766
 
@@ -776,19 +787,24 @@ module HTTP2
776
787
  # is closed, with a minimum of 15s RTT time window.
777
788
  now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
778
789
 
779
- _, closed_since = @streams_recently_closed.first
780
-
781
790
  # forego recently closed recycling if empty or the first element
782
791
  # hasn't expired yet (it's ordered).
783
- if closed_since && (now - closed_since) > 15
792
+ if @oldest_stream_recently_closed && (now - @oldest_stream_recently_closed) > 15
793
+ new_oldest = nil
784
794
  # discards all streams which have closed for a while.
785
795
  # 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
796
+ @streams_recently_closed.delete_if do |_, since|
797
+ unless (now - since) > 15
798
+ new_oldest ||= since
799
+ break
800
+ end
801
+
802
+ true
803
+ end
804
+ @oldest_stream_recently_closed = new_oldest
789
805
  end
790
806
 
791
- @streams_recently_closed[id] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
807
+ @streams_recently_closed[id] = now
792
808
  end
793
809
 
794
810
  stream.on(:frame, &method(:send))
@@ -22,22 +22,35 @@ 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
+ enc = str.encoding
30
+
31
+ if enc != Encoding::BINARY
32
+ str = str.dup if str.frozen?
33
+ str.force_encoding(Encoding::BINARY)
34
+ end
35
+
36
+ chunk = str.byteslice(0, n)
37
+ str.bytesplice(0, chunk.length, "")
38
+ chunk.force_encoding(Encoding::BINARY)
39
+ end
40
+ else
41
+ def read_str(str, n)
42
+ return "".b if n == 0
43
+
44
+ chunk = str.byteslice(0, n)
45
+ remaining = str.byteslice(n, str.size - n)
46
+ remaining ? str.replace(remaining) : str.clear
47
+ chunk.force_encoding(Encoding::BINARY)
48
+ end
32
49
  end
33
50
 
34
51
  def read_uint32(str)
35
52
  read_str(str, 4).unpack1("N")
36
53
  end
37
-
38
- def shift_byte(str)
39
- read_str(str, 1).ord
40
- end
41
54
  end
42
55
 
43
56
  # 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
@@ -140,7 +142,7 @@ module HTTP2
140
142
  frame = @buffer.first or return
141
143
 
142
144
  frame_size = frame[:payload].bytesize
143
- end_stream = frame[:flags].include?(:end_stream)
145
+ end_stream = frame[:flags].anybits?(END_STREAM)
144
146
 
145
147
  # Frames with zero length with the END_STREAM flag set (that
146
148
  # is, an empty DATA frame) MAY be sent if there is no available space
@@ -148,19 +150,17 @@ module HTTP2
148
150
  return if window_size <= 0 && !(frame_size.zero? && end_stream)
149
151
 
150
152
  if frame_size > window_size
151
- chunk = frame.dup
152
- payload = frame[:payload]
153
+ chunk = frame.dup
153
154
 
154
155
  # Split frame so that it fits in the window
155
156
  # TODO: consider padding!
156
157
 
157
- chunk[:payload] = payload.byteslice(0, window_size)
158
+ chunk[:payload] = read_str(frame[:payload], window_size) # mutates frame[:payload]
158
159
  chunk[:length] = window_size
159
- frame[:payload] = payload.byteslice(window_size..-1)
160
160
  frame[:length] = frame_size - window_size
161
161
 
162
162
  # if no longer last frame in sequence...
163
- chunk[:flags] -= [:end_stream] if end_stream
163
+ chunk[:flags] ^= END_STREAM if end_stream
164
164
 
165
165
  @bytesize -= window_size
166
166
  chunk