http-2 0.12.0 → 1.0.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.
@@ -17,7 +17,7 @@ module HTTP2
17
17
  settings_max_concurrent_streams: Framer::MAX_STREAM_ID, # unlimited
18
18
  settings_initial_window_size: 65_535,
19
19
  settings_max_frame_size: 16_384,
20
- settings_max_header_list_size: (2**31) - 1 # unlimited
20
+ settings_max_header_list_size: (2 << 30) - 1 # unlimited
21
21
  }.freeze
22
22
 
23
23
  DEFAULT_CONNECTION_SETTINGS = {
@@ -26,7 +26,7 @@ module HTTP2
26
26
  settings_max_concurrent_streams: 100,
27
27
  settings_initial_window_size: 65_535,
28
28
  settings_max_frame_size: 16_384,
29
- settings_max_header_list_size: (2**31) - 1 # unlimited
29
+ settings_max_header_list_size: (2 << 30) - 1 # unlimited
30
30
  }.freeze
31
31
 
32
32
  # Default stream priority (lower values are higher priority).
@@ -35,8 +35,10 @@ module HTTP2
35
35
  # Default connection "fast-fail" preamble string as defined by the spec.
36
36
  CONNECTION_PREFACE_MAGIC = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
37
37
 
38
- # Time to hold recently closed streams until purge (seconds)
39
- RECENTLY_CLOSED_STREAMS_TTL = 15
38
+ REQUEST_MANDATORY_HEADERS = %w[:scheme :method :authority :path].freeze
39
+ RESPONSE_MANDATORY_HEADERS = %w[:status].freeze
40
+
41
+ EMPTY = [].freeze
40
42
 
41
43
  # Connection encapsulates all of the connection, stream, flow-control,
42
44
  # error management, and other processing logic required for a well-behaved
@@ -50,6 +52,7 @@ module HTTP2
50
52
  include FlowBuffer
51
53
  include Emitter
52
54
  include Error
55
+ include BufferUtils
53
56
 
54
57
  # Connection state (:new, :closed).
55
58
  attr_reader :state
@@ -69,36 +72,38 @@ module HTTP2
69
72
 
70
73
  # Number of active streams between client and server (reserved streams
71
74
  # are not counted towards the stream limit).
72
- attr_reader :active_stream_count
75
+ attr_accessor :active_stream_count
73
76
 
74
77
  # Initializes new connection object.
75
78
  #
76
- def initialize(**settings)
79
+ def initialize(settings = {})
77
80
  @local_settings = DEFAULT_CONNECTION_SETTINGS.merge(settings)
78
81
  @remote_settings = SPEC_DEFAULT_CONNECTION_SETTINGS.dup
79
82
 
80
- @compressor = Header::Compressor.new(**settings)
81
- @decompressor = Header::Decompressor.new(**settings)
83
+ @compressor = Header::Compressor.new(settings)
84
+ @decompressor = Header::Decompressor.new(settings)
82
85
 
83
86
  @active_stream_count = 0
87
+ @last_activated_stream = 0
88
+ @last_stream_id = 0
84
89
  @streams = {}
85
90
  @streams_recently_closed = {}
86
91
  @pending_settings = []
87
92
 
88
- @framer = Framer.new
93
+ @framer = Framer.new(@local_settings[:settings_max_frame_size])
89
94
 
90
95
  @local_window_limit = @local_settings[:settings_initial_window_size]
91
96
  @local_window = @local_window_limit
92
97
  @remote_window_limit = @remote_settings[:settings_initial_window_size]
93
98
  @remote_window = @remote_window_limit
94
99
 
95
- @recv_buffer = Buffer.new
96
- @send_buffer = []
100
+ @recv_buffer = "".b
97
101
  @continuation = []
98
102
  @error = nil
99
103
 
100
104
  @h2c_upgrade = nil
101
105
  @closed_since = nil
106
+ @received_frame = false
102
107
  end
103
108
 
104
109
  def closed?
@@ -114,7 +119,11 @@ module HTTP2
114
119
  raise ConnectionClosed if @state == :closed
115
120
  raise StreamLimitExceeded if @active_stream_count >= @remote_settings[:settings_max_concurrent_streams]
116
121
 
122
+ connection_error(:protocol_error, msg: "id is smaller than previous") if @stream_id < @last_activated_stream
123
+
117
124
  stream = activate_stream(id: @stream_id, **args)
125
+ @last_activated_stream = stream.id
126
+
118
127
  @stream_id += 2
119
128
 
120
129
  stream
@@ -149,7 +158,7 @@ module HTTP2
149
158
  send(type: :goaway, last_stream: last_stream,
150
159
  error: error, payload: payload)
151
160
  @state = :closed
152
- @closed_since = Time.now
161
+ @closed_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
153
162
  end
154
163
 
155
164
  # Sends a WINDOW_UPDATE frame to the peer.
@@ -166,7 +175,7 @@ module HTTP2
166
175
  # @param settings [Array or Hash]
167
176
  def settings(payload)
168
177
  payload = payload.to_a
169
- connection_error if validate_settings(@local_role, payload)
178
+ validate_settings(@local_role, payload)
170
179
  @pending_settings << payload
171
180
  send(type: :settings, stream: 0, payload: payload)
172
181
  @pending_settings << payload
@@ -192,8 +201,7 @@ module HTTP2
192
201
  raise HandshakeError unless CONNECTION_PREFACE_MAGIC.start_with? @recv_buffer
193
202
 
194
203
  return # maybe next time
195
-
196
- elsif @recv_buffer.read(24) == CONNECTION_PREFACE_MAGIC
204
+ elsif read_str(@recv_buffer, 24) == CONNECTION_PREFACE_MAGIC
197
205
  # MAGIC is OK. Send our settings
198
206
  @state = :waiting_connection_preface
199
207
  payload = @local_settings.reject { |k, v| v == SPEC_DEFAULT_CONNECTION_SETTINGS[k] }
@@ -204,6 +212,22 @@ module HTTP2
204
212
  end
205
213
 
206
214
  while (frame = @framer.parse(@recv_buffer))
215
+ if is_a?(Client) && !@received_frame
216
+ connection_error(:protocol_error, msg: "didn't receive settings") if frame[:type] != :settings
217
+ @received_frame = true
218
+ end
219
+
220
+ # Implementations MUST discard frames
221
+ # that have unknown or unsupported types.
222
+ if frame[:type].nil?
223
+ # However, extension frames that appear in
224
+ # the middle of a header block (Section 4.3) are not permitted; these
225
+ # MUST be treated as a connection error (Section 5.4.1) of type
226
+ # PROTOCOL_ERROR.
227
+ connection_error(:protocol_error) unless @continuation.empty?
228
+ next
229
+ end
230
+
207
231
  emit(:frame_received, frame)
208
232
 
209
233
  # Header blocks MUST be transmitted as a contiguous sequence of frames
@@ -212,7 +236,18 @@ module HTTP2
212
236
  connection_error unless frame[:type] == :continuation && frame[:stream] == @continuation.first[:stream]
213
237
 
214
238
  @continuation << frame
215
- return unless frame[:flags].include? :end_headers
239
+ unless frame[:flags].include? :end_headers
240
+ buffered_payload = @continuation.sum { |f| f[:payload].bytesize }
241
+ # prevent HTTP/2 CONTINUATION FLOOD
242
+ # same heuristic as the one from HAProxy: https://www.haproxy.com/blog/haproxy-is-resilient-to-the-http-2-continuation-flood
243
+ # different mitigation (connection closed, instead of 400 response)
244
+ unless buffered_payload < @local_settings[:settings_max_frame_size]
245
+ connection_error(:protocol_error,
246
+ msg: "too many continuations received")
247
+ end
248
+
249
+ next
250
+ end
216
251
 
217
252
  payload = @continuation.map { |f| f[:payload] }.join
218
253
 
@@ -220,7 +255,7 @@ module HTTP2
220
255
  @continuation.clear
221
256
 
222
257
  frame.delete(:length)
223
- frame[:payload] = Buffer.new(payload)
258
+ frame[:payload] = payload
224
259
  frame[:flags] << :end_headers
225
260
  end
226
261
 
@@ -230,6 +265,7 @@ module HTTP2
230
265
  # anything other than 0x0, the endpoint MUST respond with a connection
231
266
  # error (Section 5.4.1) of type PROTOCOL_ERROR.
232
267
  if connection_frame?(frame)
268
+ connection_error(:protocol_error) unless frame[:stream].zero?
233
269
  connection_management(frame)
234
270
  else
235
271
  case frame[:type]
@@ -242,7 +278,7 @@ module HTTP2
242
278
  # frames MUST have the END_HEADERS flag set.
243
279
  unless frame[:flags].include? :end_headers
244
280
  @continuation << frame
245
- return
281
+ next
246
282
  end
247
283
 
248
284
  # After sending a GOAWAY frame, the sender can discard frames
@@ -255,6 +291,9 @@ module HTTP2
255
291
 
256
292
  stream = @streams[frame[:stream]]
257
293
  if stream.nil?
294
+ verify_pseudo_headers(frame)
295
+
296
+ verify_stream_order(frame[:stream])
258
297
  stream = activate_stream(
259
298
  id: frame[:stream],
260
299
  weight: frame[:weight] || DEFAULT_WEIGHT,
@@ -296,7 +335,7 @@ module HTTP2
296
335
  return
297
336
  end
298
337
 
299
- connection_error(msg: 'missing parent ID') if parent.nil?
338
+ connection_error(msg: "missing parent ID") if parent.nil?
300
339
 
301
340
  unless parent.state == :open || parent.state == :half_closed_local
302
341
  # An endpoint might receive a PUSH_PROMISE frame after it sends
@@ -313,6 +352,8 @@ module HTTP2
313
352
  end
314
353
  end
315
354
 
355
+ _verify_pseudo_headers(frame, REQUEST_MANDATORY_HEADERS)
356
+ verify_stream_order(pid)
316
357
  stream = activate_stream(id: pid, parent: parent)
317
358
  emit(:promise, stream)
318
359
  stream << frame
@@ -346,11 +387,20 @@ module HTTP2
346
387
  # "closed" stream. A receiver MUST NOT treat this as an error
347
388
  # (see Section 5.1).
348
389
  when :window_update
349
- process_window_update(frame)
390
+ stream = @streams_recently_closed[frame[:stream]]
391
+ connection_error(:protocol_error, msg: "sent window update on idle stream") unless stream
392
+ process_window_update(frame: frame, encode: true)
393
+ # Endpoints MUST ignore
394
+ # WINDOW_UPDATE or RST_STREAM frames received in this state (closed), though
395
+ # endpoints MAY choose to treat frames that arrive a significant
396
+ # time after sending END_STREAM as a connection error.
397
+ when :rst_stream
398
+ stream = @streams_recently_closed[frame[:stream]]
399
+ connection_error(:protocol_error, msg: "sent window update on idle stream") unless stream
350
400
  else
351
401
  # An endpoint that receives an unexpected stream identifier
352
402
  # MUST respond with a connection error of type PROTOCOL_ERROR.
353
- connection_error
403
+ connection_error(msg: "stream does not exist")
354
404
  end
355
405
  end
356
406
  end
@@ -362,8 +412,8 @@ module HTTP2
362
412
  connection_error(e: e)
363
413
  end
364
414
 
365
- def <<(*args)
366
- receive(*args)
415
+ def <<(data)
416
+ receive(data)
367
417
  end
368
418
 
369
419
  private
@@ -382,6 +432,7 @@ module HTTP2
382
432
  elsif frame[:type] == :rst_stream && frame[:error] == :protocol_error
383
433
  # An endpoint can end a connection at any time. In particular, an
384
434
  # endpoint MAY choose to treat a stream error as a connection error.
435
+
385
436
  goaway(frame[:error])
386
437
  else
387
438
  # HEADERS and PUSH_PROMISE may generate CONTINUATION. Also send
@@ -436,39 +487,55 @@ module HTTP2
436
487
  when :settings
437
488
  connection_settings(frame)
438
489
  when :window_update
439
- @remote_window += frame[:increment]
440
- send_data(nil, true)
490
+ process_window_update(frame: frame, encode: true)
441
491
  when :ping
442
- if frame[:flags].include? :ack
443
- emit(:ack, frame[:payload])
444
- else
445
- send(type: :ping, stream: 0,
446
- flags: [:ack], payload: frame[:payload])
447
- end
492
+ ping_management(frame)
448
493
  when :goaway
449
494
  # Receivers of a GOAWAY frame MUST NOT open additional streams on
450
495
  # the connection, although a new connection can be established
451
496
  # for new streams.
452
497
  @state = :closed
453
- @closed_since = Time.now
498
+ @closed_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
454
499
  emit(:goaway, frame[:last_stream], frame[:error], frame[:payload])
455
500
  when :altsvc
456
501
  # 4. The ALTSVC HTTP/2 Frame
457
502
  # An ALTSVC frame on stream 0 with empty (length 0) "Origin"
458
503
  # information is invalid and MUST be ignored.
459
504
  emit(frame[:type], frame) if frame[:origin] && !frame[:origin].empty?
505
+ when :origin
506
+ return if @h2c_upgrade || !frame[:flags].empty?
507
+
508
+ frame[:payload].each do |origin|
509
+ emit(frame[:type], origin)
510
+ end
460
511
  when :blocked
461
512
  emit(frame[:type], frame)
462
513
  else
463
514
  connection_error
464
515
  end
465
516
  when :closed
466
- connection_error if (Time.now - @closed_since) > 15
517
+ case frame[:type]
518
+ when :goaway
519
+ connection_error
520
+ when :ping
521
+ ping_management(frame)
522
+ else
523
+ connection_error if (Process.clock_gettime(Process::CLOCK_MONOTONIC) - @closed_since) > 15
524
+ end
467
525
  else
468
526
  connection_error
469
527
  end
470
528
  end
471
529
 
530
+ def ping_management(frame)
531
+ if frame[:flags].include? :ack
532
+ emit(:ack, frame[:payload])
533
+ else
534
+ send(type: :ping, stream: 0,
535
+ flags: [:ack], payload: frame[:payload])
536
+ end
537
+ end
538
+
472
539
  # Validate settings parameters. See sepc Section 6.5.2.
473
540
  #
474
541
  # @param role [Symbol] The sender's role: :client or :server
@@ -476,8 +543,6 @@ module HTTP2
476
543
  def validate_settings(role, settings)
477
544
  settings.each do |key, v|
478
545
  case key
479
- when :settings_header_table_size
480
- # Any value is valid
481
546
  when :settings_enable_push
482
547
  case role
483
548
  when :server
@@ -485,32 +550,41 @@ module HTTP2
485
550
  # Clients MUST reject any attempt to change the
486
551
  # SETTINGS_ENABLE_PUSH setting to a value other than 0 by treating the
487
552
  # message as a connection error (Section 5.4.1) of type PROTOCOL_ERROR.
488
- return ProtocolError.new("invalid #{key} value") unless v.zero?
553
+ next if v.zero?
554
+
555
+ connection_error(:protocol_error, msg: "invalid #{key} value")
489
556
  when :client
490
557
  # Any value other than 0 or 1 MUST be treated as a
491
558
  # connection error (Section 5.4.1) of type PROTOCOL_ERROR.
492
- return ProtocolError.new("invalid #{key} value") unless v.zero? || v == 1
559
+ next if v.zero? || v == 1
560
+
561
+ connection_error(:protocol_error, msg: "invalid #{key} value")
493
562
  end
494
- when :settings_max_concurrent_streams
495
- # Any value is valid
496
563
  when :settings_initial_window_size
497
564
  # Values above the maximum flow control window size of 2^31-1 MUST
498
565
  # be treated as a connection error (Section 5.4.1) of type
499
566
  # FLOW_CONTROL_ERROR.
500
- return FlowControlError.new("invalid #{key} value") unless v <= 0x7fffffff
567
+ next if v <= 0x7fffffff
568
+
569
+ connection_error(:flow_control_error, msg: "invalid #{key} value")
501
570
  when :settings_max_frame_size
502
571
  # The initial value is 2^14 (16,384) octets. The value advertised
503
572
  # by an endpoint MUST be between this initial value and the maximum
504
573
  # allowed frame size (2^24-1 or 16,777,215 octets), inclusive.
505
574
  # Values outside this range MUST be treated as a connection error
506
575
  # (Section 5.4.1) of type PROTOCOL_ERROR.
507
- return ProtocolError.new("invalid #{key} value") unless v >= 16_384 && v <= 16_777_215
508
- when :settings_max_header_list_size
576
+ next if v >= 16_384 && v <= 16_777_215
577
+
578
+ connection_error(:protocol_error, msg: "invalid #{key} value")
579
+ # when :settings_max_concurrent_streams
580
+ # Any value is valid
581
+ # when :settings_header_table_size
582
+ # Any value is valid
583
+ # when :settings_max_header_list_size
509
584
  # Any value is valid
510
585
  # else # ignore unknown settings
511
586
  end
512
587
  end
513
- nil
514
588
  end
515
589
 
516
590
  # Update connection settings based on parameters set by the peer.
@@ -527,7 +601,7 @@ module HTTP2
527
601
  # Process pending settings we have sent.
528
602
  [@pending_settings.shift, :local]
529
603
  else
530
- connection_error if validate_settings(@remote_role, frame[:payload])
604
+ validate_settings(@remote_role, frame[:payload])
531
605
  [frame[:payload], :remote]
532
606
  end
533
607
 
@@ -559,7 +633,10 @@ module HTTP2
559
633
 
560
634
  @local_window_limit = v
561
635
  when :remote
562
- @remote_window = @remote_window - @remote_window_limit + v
636
+ # can adjust the initial window size for new streams by including a
637
+ # value for SETTINGS_INITIAL_WINDOW_SIZE in the SETTINGS frame.
638
+ # The connection flow-control window can only be changed using
639
+ # WINDOW_UPDATE frames.
563
640
  @streams.each_value do |stream|
564
641
  # Event name is :window, not :remote_window
565
642
  stream.emit(:window, stream.remote_window - @remote_window_limit + v)
@@ -581,8 +658,7 @@ module HTTP2
581
658
  # nothing to do
582
659
 
583
660
  when :settings_max_frame_size
584
- # update framer max_frame_size
585
- @framer.max_frame_size = v
661
+ @framer.remote_max_frame_size = v
586
662
 
587
663
  # else # ignore unknown settings
588
664
  end
@@ -596,6 +672,9 @@ module HTTP2
596
672
  unless @state == :closed || @h2c_upgrade == :start
597
673
  # Send ack to peer
598
674
  send(type: :settings, stream: 0, payload: [], flags: [:ack])
675
+ # when initial window size changes, we try to flush any buffered
676
+ # data.
677
+ @streams.each_value(&:flush)
599
678
  end
600
679
  end
601
680
  end
@@ -609,7 +688,7 @@ module HTTP2
609
688
  #
610
689
  # @param frame [Hash]
611
690
  def decode_headers(frame)
612
- frame[:payload] = @decompressor.decode(frame[:payload]) if frame[:payload].is_a? Buffer
691
+ frame[:payload] = @decompressor.decode(frame[:payload], frame) if frame[:payload].is_a?(String)
613
692
  rescue CompressionError => e
614
693
  connection_error(:compression_error, e: e)
615
694
  rescue ProtocolError => e
@@ -624,29 +703,36 @@ module HTTP2
624
703
  # @return [Array of Frame]
625
704
  def encode_headers(frame)
626
705
  payload = frame[:payload]
627
- payload = @compressor.encode(payload) unless payload.is_a? Buffer
706
+ begin
707
+ payload = frame[:payload] = @compressor.encode(payload) unless payload.is_a?(String)
708
+ rescue StandardError => e
709
+ connection_error(:compression_error, e: e)
710
+ end
711
+
712
+ # if single frame, return immediately
713
+ return [frame] if payload.bytesize <= @remote_settings[:settings_max_frame_size]
628
714
 
629
715
  frames = []
630
716
 
631
- while payload.bytesize.positive?
717
+ until payload.nil? || payload.empty?
632
718
  cont = frame.dup
633
- cont[:type] = :continuation
634
- cont[:flags] = []
635
- cont[:payload] = payload.slice!(0, @remote_settings[:settings_max_frame_size])
719
+
720
+ # first frame remains HEADERS
721
+ unless frames.empty?
722
+ cont[:type] = :continuation
723
+ cont[:flags] = EMPTY
724
+ end
725
+
726
+ cont[:payload] = payload.byteslice(0, @remote_settings[:settings_max_frame_size])
727
+ payload = payload.byteslice(@remote_settings[:settings_max_frame_size]..-1)
728
+
636
729
  frames << cont
637
730
  end
638
- if frames.empty?
639
- frames = [frame]
640
- else
641
- frames.first[:type] = frame[:type]
642
- frames.first[:flags] = frame[:flags] - [:end_headers]
643
- frames.last[:flags] << :end_headers
644
- end
731
+
732
+ frames.first[:flags].delete(:end_headers)
733
+ frames.last[:flags] = [:end_headers]
645
734
 
646
735
  frames
647
- rescue StandardError => e
648
- connection_error(:compression_error, e: e)
649
- nil
650
736
  end
651
737
 
652
738
  # Activates new incoming or outgoing stream and registers appropriate
@@ -656,24 +742,28 @@ module HTTP2
656
742
  # @param priority [Integer]
657
743
  # @param window [Integer]
658
744
  # @param parent [Stream]
659
- def activate_stream(id: nil, **args)
660
- connection_error(msg: 'Stream ID already exists') if @streams.key?(id)
745
+ def activate_stream(id:, **args)
746
+ connection_error(msg: "Stream ID already exists") if @streams.key?(id)
747
+
748
+ raise StreamLimitExceeded if @active_stream_count >= @local_settings[:settings_max_concurrent_streams]
661
749
 
662
750
  stream = Stream.new(connection: self, id: id, **args)
663
751
 
664
- # Streams that are in the "open" state, or either of the "half closed"
665
- # states count toward the maximum number of streams that an endpoint is
666
- # permitted to open.
667
- stream.once(:active) { @active_stream_count += 1 }
668
752
  stream.once(:close) do
669
- @active_stream_count -= 1
753
+ @streams.delete(id)
670
754
 
671
755
  # Store a reference to the closed stream, such that we can respond
672
756
  # to any in-flight frames while close is registered on both sides.
673
757
  # References to such streams will be purged whenever another stream
674
- # is closed, with a defined RTT time window.
675
- @streams_recently_closed[id] = Time.now.to_i
676
- cleanup_recently_closed
758
+ # is closed, with a minimum of 15s RTT time window.
759
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
760
+
761
+ # TODO: use a drop_while! variant whenever there is one.
762
+ @streams_recently_closed = @streams_recently_closed.drop_while do |_, v|
763
+ (now - v) > 15
764
+ end.to_h
765
+
766
+ @streams_recently_closed[id] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
677
767
  end
678
768
 
679
769
  stream.on(:promise, &method(:promise)) if is_a? Server
@@ -682,21 +772,26 @@ module HTTP2
682
772
  @streams[id] = stream
683
773
  end
684
774
 
685
- # Purge recently streams closed within defined RTT time window.
686
- def cleanup_recently_closed
687
- now_ts = Time.now.to_i
688
- to_delete = []
689
- @streams_recently_closed.each do |stream_id, ts|
690
- # Ruby Hash enumeration is ordered, so once fresh stream is met we can stop searching.
691
- break if now_ts - ts < RECENTLY_CLOSED_STREAMS_TTL
775
+ def verify_stream_order(id)
776
+ return unless id.odd?
692
777
 
693
- to_delete << stream_id
694
- end
778
+ connection_error(msg: "Stream ID smaller than previous") if @last_stream_id >= id
779
+ @last_stream_id = id
780
+ end
695
781
 
696
- to_delete.each do |stream_id|
697
- @streams.delete stream_id
698
- @streams_recently_closed.delete stream_id
699
- end
782
+ def _verify_pseudo_headers(frame, mandatory_headers)
783
+ headers = frame[:payload]
784
+ return if headers.is_a?(String)
785
+
786
+ pseudo_headers = headers.take_while do |field, value|
787
+ # use this loop to validate pseudo-headers
788
+ connection_error(:protocol_error, msg: "path is empty") if field == ":path" && value.empty?
789
+ field.start_with?(":")
790
+ end.map(&:first)
791
+ return if mandatory_headers.size == pseudo_headers.size &&
792
+ (mandatory_headers - pseudo_headers).empty?
793
+
794
+ connection_error(:protocol_error, msg: "invalid pseudo-headers")
700
795
  end
701
796
 
702
797
  # Emit GOAWAY error indicating to peer that the connection is being
@@ -715,10 +810,9 @@ module HTTP2
715
810
 
716
811
  @state = :closed
717
812
  @error = error
718
- klass = error.to_s.split('_').map(&:capitalize).join
719
- msg ||= e&.message
720
- backtrace = e&.backtrace || []
721
- raise Error.const_get(klass), msg, backtrace
813
+ msg ||= e ? e.message : "protocol error"
814
+ backtrace = e ? e.backtrace : nil
815
+ raise Error.types[error], msg, backtrace
722
816
  end
723
817
  alias error connection_error
724
818
 
@@ -9,19 +9,18 @@ module HTTP2
9
9
  #
10
10
  # @param event [Symbol]
11
11
  # @param block [Proc] callback function
12
- def add_listener(event, &block)
13
- raise ArgumentError, 'must provide callback' unless block_given?
12
+ def on(event, &block)
13
+ raise ArgumentError, "must provide callback" unless block
14
14
 
15
15
  listeners(event.to_sym).push block
16
16
  end
17
- alias on add_listener
18
17
 
19
18
  # Subscribe to next event (at most once) for specified type.
20
19
  #
21
20
  # @param event [Symbol]
22
21
  # @param block [Proc] callback function
23
22
  def once(event, &block)
24
- add_listener(event) do |*args, &callback|
23
+ on(event) do |*args, &callback|
25
24
  block.call(*args, &callback)
26
25
  :delete
27
26
  end
@@ -34,7 +33,7 @@ module HTTP2
34
33
  # @param block [Proc] callback function
35
34
  def emit(event, *args, &block)
36
35
  listeners(event).delete_if do |cb|
37
- cb.call(*args, &block) == :delete
36
+ :delete == cb.call(*args, &block) # rubocop:disable Style/YodaCondition
38
37
  end
39
38
  end
40
39
 
data/lib/http/2/error.rb CHANGED
@@ -3,7 +3,24 @@
3
3
  module HTTP2
4
4
  # Stream, connection, and compressor exceptions.
5
5
  module Error
6
- class Error < StandardError; end
6
+ @types = {}
7
+
8
+ class << self
9
+ attr_reader :types
10
+ end
11
+
12
+ class Error < StandardError
13
+ def self.inherited(klass)
14
+ super
15
+
16
+ type = klass.name or return
17
+
18
+ type = type.split("::").last or return
19
+
20
+ type = type.gsub(/([^\^])([A-Z])/, '\1_\2').downcase.to_sym
21
+ HTTP2::Error.types[type] = klass
22
+ end
23
+ end
7
24
 
8
25
  # Raised if connection header is missing or invalid indicating that
9
26
  # this is an invalid HTTP 2.0 request - no frames are emitted and the
@@ -42,5 +59,9 @@ module HTTP2
42
59
 
43
60
  # Raised if stream limit has been reached and new stream cannot be opened.
44
61
  class StreamLimitExceeded < Error; end
62
+
63
+ class FrameSizeError < Error; end
64
+
65
+ @types.freeze
45
66
  end
46
67
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP2
4
+ module BufferUtils
5
+ def read_str(str, n)
6
+ return "".b if n == 0
7
+
8
+ chunk = str.byteslice(0..n - 1)
9
+ remaining = str.byteslice(n..-1)
10
+ remaining ? str.replace(remaining) : str.clear
11
+ chunk
12
+ end
13
+
14
+ def read_uint32(str)
15
+ read_str(str, 4).unpack1("N")
16
+ end
17
+
18
+ def shift_byte(str)
19
+ read_str(str, 1).ord
20
+ end
21
+ end
22
+
23
+ # this mixin handles backwards-compatibility for the new packing options
24
+ # shipping with ruby 3.3 (see https://docs.ruby-lang.org/en/3.3/packed_data_rdoc.html)
25
+ module PackingExtensions
26
+ if RUBY_VERSION < "3.3.0"
27
+ def pack(array_to_pack, template, buffer:, offset: -1)
28
+ packed_str = array_to_pack.pack(template)
29
+ case offset
30
+ when -1
31
+ buffer << packed_str
32
+ when 0
33
+ buffer.prepend(packed_str)
34
+ else
35
+ buffer.insert(offset, packed_str)
36
+ end
37
+ end
38
+ else
39
+ def pack(array_to_pack, template, buffer:, offset: -1)
40
+ case offset
41
+ when -1
42
+ array_to_pack.pack(template, buffer: buffer)
43
+ when 0
44
+ buffer.prepend(array_to_pack.pack(template))
45
+ else
46
+ buffer.insert(offset, array_to_pack.pack(template))
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end