http-2 0.12.0 → 1.0.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.
@@ -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