http-2 0.12.0 → 1.0.0

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