http-2-next 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,757 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP2Next
4
+ # Default connection and stream flow control window (64KB).
5
+ DEFAULT_FLOW_WINDOW = 65_535
6
+
7
+ # Default header table size
8
+ DEFAULT_HEADER_SIZE = 4096
9
+
10
+ # Default stream_limit
11
+ DEFAULT_MAX_CONCURRENT_STREAMS = 100
12
+
13
+ # Default values for SETTINGS frame, as defined by the spec.
14
+ SPEC_DEFAULT_CONNECTION_SETTINGS = {
15
+ settings_header_table_size: 4096,
16
+ settings_enable_push: 1, # enabled for servers
17
+ settings_max_concurrent_streams: Framer::MAX_STREAM_ID, # unlimited
18
+ settings_initial_window_size: 65_535,
19
+ settings_max_frame_size: 16_384,
20
+ settings_max_header_list_size: 2**31 - 1 # unlimited
21
+ }.freeze
22
+
23
+ DEFAULT_CONNECTION_SETTINGS = {
24
+ settings_header_table_size: 4096,
25
+ settings_enable_push: 1, # enabled for servers
26
+ settings_max_concurrent_streams: 100,
27
+ settings_initial_window_size: 65_535,
28
+ settings_max_frame_size: 16_384,
29
+ settings_max_header_list_size: 2**31 - 1 # unlimited
30
+ }.freeze
31
+
32
+ # Default stream priority (lower values are higher priority).
33
+ DEFAULT_WEIGHT = 16
34
+
35
+ # Default connection "fast-fail" preamble string as defined by the spec.
36
+ CONNECTION_PREFACE_MAGIC = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
37
+
38
+ # Connection encapsulates all of the connection, stream, flow-control,
39
+ # error management, and other processing logic required for a well-behaved
40
+ # HTTP 2.0 endpoint.
41
+ #
42
+ # Note that this class should not be used directly. Instead, you want to
43
+ # use either Client or Server class to drive the HTTP 2.0 exchange.
44
+ #
45
+ # rubocop:disable Metrics/ClassLength
46
+ class Connection
47
+ include FlowBuffer
48
+ include Emitter
49
+ include Error
50
+
51
+ # Connection state (:new, :closed).
52
+ attr_reader :state
53
+
54
+ # Size of current connection flow control window (by default, set to
55
+ # infinity, but is automatically updated on receipt of peer settings).
56
+ attr_reader :local_window
57
+ attr_reader :remote_window
58
+ alias window local_window
59
+
60
+ # Current settings value for local and peer
61
+ attr_reader :local_settings
62
+ attr_reader :remote_settings
63
+
64
+ # Pending settings value
65
+ # Sent but not ack'ed settings
66
+ attr_reader :pending_settings
67
+
68
+ # Number of active streams between client and server (reserved streams
69
+ # are not counted towards the stream limit).
70
+ attr_reader :active_stream_count
71
+
72
+ # Initializes new connection object.
73
+ #
74
+ def initialize(**settings)
75
+ @local_settings = DEFAULT_CONNECTION_SETTINGS.merge(settings)
76
+ @remote_settings = SPEC_DEFAULT_CONNECTION_SETTINGS.dup
77
+
78
+ @compressor = Header::Compressor.new(settings)
79
+ @decompressor = Header::Decompressor.new(settings)
80
+
81
+ @active_stream_count = 0
82
+ @last_activated_stream = 0
83
+ @last_stream_id = 0
84
+ @streams = {}
85
+ @streams_recently_closed = {}
86
+ @pending_settings = []
87
+
88
+ @framer = Framer.new(@local_settings[:settings_max_frame_size])
89
+
90
+ @local_window_limit = @local_settings[:settings_initial_window_size]
91
+ @local_window = @local_window_limit
92
+ @remote_window_limit = @remote_settings[:settings_initial_window_size]
93
+ @remote_window = @remote_window_limit
94
+
95
+ @recv_buffer = Buffer.new
96
+ @send_buffer = []
97
+ @continuation = []
98
+ @error = nil
99
+
100
+ @h2c_upgrade = nil
101
+ @closed_since = nil
102
+ end
103
+
104
+ def closed?
105
+ @state == :closed
106
+ end
107
+
108
+ # Allocates new stream for current connection.
109
+ #
110
+ # @param priority [Integer]
111
+ # @param window [Integer]
112
+ # @param parent [Stream]
113
+ def new_stream(**args)
114
+ raise ConnectionClosed if @state == :closed
115
+ raise StreamLimitExceeded if @active_stream_count >= @remote_settings[:settings_max_concurrent_streams]
116
+
117
+ connection_error(:protocol_error, msg: "id is smaller than previous") if @stream_id < @last_activated_stream
118
+
119
+ stream = activate_stream(id: @stream_id, **args)
120
+ @last_activated_stream = stream.id
121
+
122
+ @stream_id += 2
123
+
124
+ stream
125
+ end
126
+
127
+ # Sends PING frame to the peer.
128
+ #
129
+ # @param payload [String] optional payload must be 8 bytes long
130
+ # @param blk [Proc] callback to execute when PONG is received
131
+ def ping(payload, &blk)
132
+ send(type: :ping, stream: 0, payload: payload)
133
+ once(:ack, &blk) if blk
134
+ end
135
+
136
+ # Sends a GOAWAY frame indicating that the peer should stop creating
137
+ # new streams for current connection.
138
+ #
139
+ # Endpoints MAY append opaque data to the payload of any GOAWAY frame.
140
+ # Additional debug data is intended for diagnostic purposes only and
141
+ # carries no semantic value. Debug data MUST NOT be persistently stored,
142
+ # since it could contain sensitive information.
143
+ #
144
+ # @param error [Symbol]
145
+ # @param payload [String]
146
+ def goaway(error = :no_error, payload = nil)
147
+ last_stream = if (max = @streams.max)
148
+ max.first
149
+ else
150
+ 0
151
+ end
152
+
153
+ send(type: :goaway, last_stream: last_stream,
154
+ error: error, payload: payload)
155
+ @state = :closed
156
+ @closed_since = Time.now
157
+ end
158
+
159
+ # Sends a WINDOW_UPDATE frame to the peer.
160
+ #
161
+ # @param increment [Integer]
162
+ def window_update(increment)
163
+ @local_window += increment
164
+ send(type: :window_update, stream: 0, increment: increment)
165
+ end
166
+
167
+ # Sends a connection SETTINGS frame to the peer.
168
+ # The values are reflected when the corresponding ACK is received.
169
+ #
170
+ # @param settings [Array or Hash]
171
+ def settings(payload)
172
+ payload = payload.to_a
173
+ connection_error if validate_settings(@local_role, payload)
174
+ @pending_settings << payload
175
+ send(type: :settings, stream: 0, payload: payload)
176
+ @pending_settings << payload
177
+ end
178
+
179
+ # Decodes incoming bytes into HTTP 2.0 frames and routes them to
180
+ # appropriate receivers: connection frames are handled directly, and
181
+ # stream frames are passed to appropriate stream objects.
182
+ #
183
+ # @param data [String] Binary encoded string
184
+ def receive(data)
185
+ @recv_buffer << data
186
+
187
+ # Upon establishment of a TCP connection and determination that
188
+ # HTTP/2.0 will be used by both peers, each endpoint MUST send a
189
+ # connection header as a final confirmation and to establish the
190
+ # initial settings for the HTTP/2.0 connection.
191
+ #
192
+ # Client connection header is 24 byte connection header followed by
193
+ # SETTINGS frame. Server connection header is SETTINGS frame only.
194
+ if @state == :waiting_magic
195
+ if @recv_buffer.size < 24
196
+ raise HandshakeError unless CONNECTION_PREFACE_MAGIC.start_with? @recv_buffer
197
+
198
+ return # maybe next time
199
+ elsif @recv_buffer.read(24) == CONNECTION_PREFACE_MAGIC
200
+ # MAGIC is OK. Send our settings
201
+ @state = :waiting_connection_preface
202
+ payload = @local_settings.reject { |k, v| v == SPEC_DEFAULT_CONNECTION_SETTINGS[k] }
203
+ settings(payload)
204
+ else
205
+ raise HandshakeError
206
+ end
207
+ end
208
+
209
+ while (frame = @framer.parse(@recv_buffer))
210
+ # Implementations MUST discard frames
211
+ # that have unknown or unsupported types.
212
+ if frame[:type].nil?
213
+ # However, extension frames that appear in
214
+ # the middle of a header block (Section 4.3) are not permitted; these
215
+ # MUST be treated as a connection error (Section 5.4.1) of type
216
+ # PROTOCOL_ERROR.
217
+ connection_error(:protocol_error) unless @continuation.empty?
218
+ next
219
+ end
220
+
221
+ emit(:frame_received, frame)
222
+
223
+ # Header blocks MUST be transmitted as a contiguous sequence of frames
224
+ # with no interleaved frames of any other type, or from any other stream.
225
+ unless @continuation.empty?
226
+ connection_error unless frame[:type] == :continuation && frame[:stream] == @continuation.first[:stream]
227
+
228
+ @continuation << frame
229
+ next unless frame[:flags].include? :end_headers
230
+
231
+ payload = @continuation.map { |f| f[:payload] }.join
232
+
233
+ frame = @continuation.shift
234
+ @continuation.clear
235
+
236
+ frame.delete(:length)
237
+ frame[:payload] = Buffer.new(payload)
238
+ frame[:flags] << :end_headers
239
+ end
240
+
241
+ # SETTINGS frames always apply to a connection, never a single stream.
242
+ # The stream identifier for a settings frame MUST be zero. If an
243
+ # endpoint receives a SETTINGS frame whose stream identifier field is
244
+ # anything other than 0x0, the endpoint MUST respond with a connection
245
+ # error (Section 5.4.1) of type PROTOCOL_ERROR.
246
+ if connection_frame?(frame)
247
+ connection_error(:protocol_error) unless frame[:stream].zero?
248
+ connection_management(frame)
249
+ else
250
+ case frame[:type]
251
+ when :headers
252
+ # When server receives even-numbered stream identifier,
253
+ # the endpoint MUST respond with a connection error of type PROTOCOL_ERROR.
254
+ connection_error if frame[:stream].even? && is_a?(Server)
255
+
256
+ # The last frame in a sequence of HEADERS/CONTINUATION
257
+ # frames MUST have the END_HEADERS flag set.
258
+ unless frame[:flags].include? :end_headers
259
+ @continuation << frame
260
+ next
261
+ end
262
+
263
+ # After sending a GOAWAY frame, the sender can discard frames
264
+ # for new streams. However, any frames that alter connection
265
+ # state cannot be completely ignored. For instance, HEADERS,
266
+ # PUSH_PROMISE and CONTINUATION frames MUST be minimally
267
+ # processed to ensure a consistent compression state
268
+ decode_headers(frame)
269
+ return if @state == :closed
270
+
271
+ stream = @streams[frame[:stream]]
272
+ if stream.nil?
273
+ stream = activate_stream(
274
+ id: frame[:stream],
275
+ weight: frame[:weight] || DEFAULT_WEIGHT,
276
+ dependency: frame[:dependency] || 0,
277
+ exclusive: frame[:exclusive] || false
278
+ )
279
+ verify_stream_order(stream.id)
280
+ emit(:stream, stream)
281
+ end
282
+
283
+ stream << frame
284
+
285
+ when :push_promise
286
+ # The last frame in a sequence of PUSH_PROMISE/CONTINUATION
287
+ # frames MUST have the END_HEADERS flag set
288
+ unless frame[:flags].include? :end_headers
289
+ @continuation << frame
290
+ return
291
+ end
292
+
293
+ decode_headers(frame)
294
+ return if @state == :closed
295
+
296
+ # PUSH_PROMISE frames MUST be associated with an existing, peer-
297
+ # initiated stream... A receiver MUST treat the receipt of a
298
+ # PUSH_PROMISE on a stream that is neither "open" nor
299
+ # "half-closed (local)" as a connection error (Section 5.4.1) of
300
+ # type PROTOCOL_ERROR. Similarly, a receiver MUST treat the
301
+ # receipt of a PUSH_PROMISE that promises an illegal stream
302
+ # identifier (Section 5.1.1) (that is, an identifier for a stream
303
+ # that is not currently in the "idle" state) as a connection error
304
+ # (Section 5.4.1) of type PROTOCOL_ERROR, unless the receiver
305
+ # recently sent a RST_STREAM frame to cancel the associated stream.
306
+ parent = @streams[frame[:stream]]
307
+ pid = frame[:promise_stream]
308
+
309
+ # if PUSH parent is recently closed, RST_STREAM the push
310
+ if @streams_recently_closed[frame[:stream]]
311
+ send(type: :rst_stream, stream: pid, error: :refused_stream)
312
+ return
313
+ end
314
+
315
+ connection_error(msg: "missing parent ID") if parent.nil?
316
+
317
+ unless parent.state == :open || parent.state == :half_closed_local
318
+ # An endpoint might receive a PUSH_PROMISE frame after it sends
319
+ # RST_STREAM. PUSH_PROMISE causes a stream to become "reserved".
320
+ # The RST_STREAM does not cancel any promised stream. Therefore, if
321
+ # promised streams are not desired, a RST_STREAM can be used to
322
+ # close any of those streams.
323
+ if parent.closed == :local_rst
324
+ # We can either (a) 'resurrect' the parent, or (b) RST_STREAM
325
+ # ... sticking with (b), might need to revisit later.
326
+ send(type: :rst_stream, stream: pid, error: :refused_stream)
327
+ else
328
+ connection_error
329
+ end
330
+ end
331
+
332
+ stream = activate_stream(id: pid, parent: parent)
333
+ verify_stream_order(stream.id)
334
+ emit(:promise, stream)
335
+ stream << frame
336
+ else
337
+ if (stream = @streams[frame[:stream]])
338
+ stream << frame
339
+ if frame[:type] == :data
340
+ update_local_window(frame)
341
+ calculate_window_update(@local_window_limit)
342
+ end
343
+ process_window_update(frame: frame, encode: true) if frame[:type] == :window_update
344
+ else
345
+ case frame[:type]
346
+ # The PRIORITY frame can be sent for a stream in the "idle" or
347
+ # "closed" state. This allows for the reprioritization of a
348
+ # group of dependent streams by altering the priority of an
349
+ # unused or closed parent stream.
350
+ when :priority
351
+ stream = activate_stream(
352
+ id: frame[:stream],
353
+ weight: frame[:weight] || DEFAULT_WEIGHT,
354
+ dependency: frame[:dependency] || 0,
355
+ exclusive: frame[:exclusive] || false
356
+ )
357
+
358
+ emit(:stream, stream)
359
+ stream << frame
360
+
361
+ # WINDOW_UPDATE can be sent by a peer that has sent a frame
362
+ # bearing the END_STREAM flag. This means that a receiver could
363
+ # receive a WINDOW_UPDATE frame on a "half-closed (remote)" or
364
+ # "closed" stream. A receiver MUST NOT treat this as an error
365
+ # (see Section 5.1).
366
+ when :window_update
367
+ stream = @streams_recently_closed[frame[:stream]]
368
+ connection_error(:protocol_error, "sent window update on idle stream") unless stream
369
+ process_window_update(frame: frame, encode: true)
370
+ else
371
+ # An endpoint that receives an unexpected stream identifier
372
+ # MUST respond with a connection error of type PROTOCOL_ERROR.
373
+ connection_error(msg: "stream does not exist")
374
+ end
375
+ end
376
+ end
377
+ end
378
+ end
379
+ rescue StandardError => e
380
+ raise if e.is_a?(Error::Error)
381
+
382
+ connection_error(e: e)
383
+ end
384
+
385
+ def <<(*args)
386
+ receive(*args)
387
+ end
388
+
389
+ private
390
+
391
+ # Send an outgoing frame. DATA frames are subject to connection flow
392
+ # control and may be split and / or buffered based on current window size.
393
+ # All other frames are sent immediately.
394
+ #
395
+ # @note all frames are currently delivered in FIFO order.
396
+ # @param frame [Hash]
397
+ def send(frame)
398
+ emit(:frame_sent, frame)
399
+ if frame[:type] == :data
400
+ send_data(frame, true)
401
+
402
+ elsif frame[:type] == :rst_stream && frame[:error] == :protocol_error
403
+ # An endpoint can end a connection at any time. In particular, an
404
+ # endpoint MAY choose to treat a stream error as a connection error.
405
+
406
+ goaway(frame[:error])
407
+ else
408
+ # HEADERS and PUSH_PROMISE may generate CONTINUATION. Also send
409
+ # RST_STREAM that are not protocol errors
410
+ frames = encode(frame)
411
+ frames.each { |f| emit(:frame, f) }
412
+ end
413
+ end
414
+
415
+ # Applies HTTP 2.0 binary encoding to the frame.
416
+ #
417
+ # @param frame [Hash]
418
+ # @return [Array of Buffer] encoded frame
419
+ def encode(frame)
420
+ frames = if frame[:type] == :headers || frame[:type] == :push_promise
421
+ encode_headers(frame) # HEADERS and PUSH_PROMISE may create more than one frame
422
+ else
423
+ [frame] # otherwise one frame
424
+ end
425
+
426
+ frames.map { |f| @framer.generate(f) }
427
+ end
428
+
429
+ # Check if frame is a connection frame: SETTINGS, PING, GOAWAY, and any
430
+ # frame addressed to stream ID = 0.
431
+ #
432
+ # @param frame [Hash]
433
+ # @return [Boolean]
434
+ def connection_frame?(frame)
435
+ (frame[:stream]).zero? ||
436
+ frame[:type] == :settings ||
437
+ frame[:type] == :ping ||
438
+ frame[:type] == :goaway
439
+ end
440
+
441
+ # Process received connection frame (stream ID = 0).
442
+ # - Handle SETTINGS updates
443
+ # - Connection flow control (WINDOW_UPDATE)
444
+ # - Emit PONG auto-reply to PING frames
445
+ # - Mark connection as closed on GOAWAY
446
+ #
447
+ # @param frame [Hash]
448
+ def connection_management(frame)
449
+ case @state
450
+ when :waiting_connection_preface
451
+ # The first frame MUST be a SETTINGS frame at the start of a connection.
452
+ @state = :connected
453
+ connection_settings(frame)
454
+
455
+ when :connected
456
+ case frame[:type]
457
+ when :settings
458
+ connection_settings(frame)
459
+ when :window_update
460
+ process_window_update(frame: frame, encode: true)
461
+ when :ping
462
+ ping_management(frame)
463
+ when :goaway
464
+ # Receivers of a GOAWAY frame MUST NOT open additional streams on
465
+ # the connection, although a new connection can be established
466
+ # for new streams.
467
+ @state = :closed
468
+ @closed_since = Time.now
469
+ emit(:goaway, frame[:last_stream], frame[:error], frame[:payload])
470
+ when :altsvc
471
+ # 4. The ALTSVC HTTP/2 Frame
472
+ # An ALTSVC frame on stream 0 with empty (length 0) "Origin"
473
+ # information is invalid and MUST be ignored.
474
+ emit(frame[:type], frame) if frame[:origin] && !frame[:origin].empty?
475
+ when :blocked
476
+ emit(frame[:type], frame)
477
+ else
478
+ connection_error
479
+ end
480
+ when :closed
481
+ case frame[:type]
482
+ when :goaway
483
+ connection_error
484
+ when :ping
485
+ ping_management(frame)
486
+ else
487
+ connection_error if (Time.now - @closed_since) > 15
488
+ end
489
+ else
490
+ connection_error
491
+ end
492
+ end
493
+
494
+ def ping_management(frame)
495
+ if frame[:flags].include? :ack
496
+ emit(:ack, frame[:payload])
497
+ else
498
+ send(type: :ping, stream: 0,
499
+ flags: [:ack], payload: frame[:payload])
500
+ end
501
+ end
502
+
503
+ # Validate settings parameters. See sepc Section 6.5.2.
504
+ #
505
+ # @param role [Symbol] The sender's role: :client or :server
506
+ # @return nil if no error. Exception object in case of any error.
507
+ def validate_settings(role, settings)
508
+ settings.each do |key, v|
509
+ case key
510
+ when :settings_header_table_size
511
+ # Any value is valid
512
+ when :settings_enable_push
513
+ case role
514
+ when :server
515
+ # Section 8.2
516
+ # Clients MUST reject any attempt to change the
517
+ # SETTINGS_ENABLE_PUSH setting to a value other than 0 by treating the
518
+ # message as a connection error (Section 5.4.1) of type PROTOCOL_ERROR.
519
+ return ProtocolError.new("invalid #{key} value") unless v.zero?
520
+ when :client
521
+ # Any value other than 0 or 1 MUST be treated as a
522
+ # connection error (Section 5.4.1) of type PROTOCOL_ERROR.
523
+ return ProtocolError.new("invalid #{key} value") unless v.zero? || v == 1
524
+ end
525
+ when :settings_max_concurrent_streams
526
+ # Any value is valid
527
+ when :settings_initial_window_size
528
+ # Values above the maximum flow control window size of 2^31-1 MUST
529
+ # be treated as a connection error (Section 5.4.1) of type
530
+ # FLOW_CONTROL_ERROR.
531
+ return FlowControlError.new("invalid #{key} value") unless v <= 0x7fffffff
532
+ when :settings_max_frame_size
533
+ # The initial value is 2^14 (16,384) octets. The value advertised
534
+ # by an endpoint MUST be between this initial value and the maximum
535
+ # allowed frame size (2^24-1 or 16,777,215 octets), inclusive.
536
+ # Values outside this range MUST be treated as a connection error
537
+ # (Section 5.4.1) of type PROTOCOL_ERROR.
538
+ return ProtocolError.new("invalid #{key} value") unless v >= 16_384 && v <= 16_777_215
539
+ when :settings_max_header_list_size
540
+ # Any value is valid
541
+ # else # ignore unknown settings
542
+ end
543
+ end
544
+ nil
545
+ end
546
+
547
+ # Update connection settings based on parameters set by the peer.
548
+ #
549
+ # @param frame [Hash]
550
+ def connection_settings(frame)
551
+ connection_error unless frame[:type] == :settings && (frame[:stream]).zero?
552
+
553
+ # Apply settings.
554
+ # side =
555
+ # local: previously sent and pended our settings should be effective
556
+ # remote: just received peer settings should immediately be effective
557
+ settings, side = if frame[:flags].include?(:ack)
558
+ # Process pending settings we have sent.
559
+ [@pending_settings.shift, :local]
560
+ else
561
+ check = validate_settings(@remote_role, frame[:payload])
562
+ connection_error(check) if check
563
+ [frame[:payload], :remote]
564
+ end
565
+
566
+ settings.each do |key, v|
567
+ case side
568
+ when :local
569
+ @local_settings[key] = v
570
+ when :remote
571
+ @remote_settings[key] = v
572
+ end
573
+
574
+ case key
575
+ when :settings_max_concurrent_streams
576
+ # Do nothing.
577
+ # The value controls at the next attempt of stream creation.
578
+
579
+ when :settings_initial_window_size
580
+ # A change to SETTINGS_INITIAL_WINDOW_SIZE could cause the available
581
+ # space in a flow control window to become negative. A sender MUST
582
+ # track the negative flow control window, and MUST NOT send new flow
583
+ # controlled frames until it receives WINDOW_UPDATE frames that cause
584
+ # the flow control window to become positive.
585
+ case side
586
+ when :local
587
+ @local_window = @local_window - @local_window_limit + v
588
+ @streams.each_value do |stream|
589
+ stream.emit(:local_window, stream.local_window - @local_window_limit + v)
590
+ end
591
+
592
+ @local_window_limit = v
593
+ when :remote
594
+ @remote_window = @remote_window - @remote_window_limit + v
595
+ @streams.each_value do |stream|
596
+ # Event name is :window, not :remote_window
597
+ stream.emit(:window, stream.remote_window - @remote_window_limit + v)
598
+ end
599
+
600
+ @remote_window_limit = v
601
+ end
602
+
603
+ when :settings_header_table_size
604
+ # Setting header table size might cause some headers evicted
605
+ case side
606
+ when :local
607
+ @compressor.table_size = v
608
+ when :remote
609
+ @decompressor.table_size = v
610
+ end
611
+
612
+ when :settings_enable_push
613
+ # nothing to do
614
+
615
+ when :settings_max_frame_size
616
+ @framer.remote_max_frame_size = v
617
+
618
+ # else # ignore unknown settings
619
+ end
620
+ end
621
+
622
+ case side
623
+ when :local
624
+ # Received a settings_ack. Notify application layer.
625
+ emit(:settings_ack, frame, @pending_settings.size)
626
+ when :remote
627
+ unless @state == :closed || @h2c_upgrade == :start
628
+ # Send ack to peer
629
+ send(type: :settings, stream: 0, payload: [], flags: [:ack])
630
+ # when initial window size changes, we try to flush any buffered
631
+ # data.
632
+ @streams.each_value(&:flush)
633
+ end
634
+ end
635
+ end
636
+
637
+ # Decode headers payload and update connection decompressor state.
638
+ #
639
+ # The receiver endpoint reassembles the header block by concatenating
640
+ # the individual fragments, then decompresses the block to reconstruct
641
+ # the header set - aka, header payloads are buffered until END_HEADERS,
642
+ # or an END_PROMISE flag is seen.
643
+ #
644
+ # @param frame [Hash]
645
+ def decode_headers(frame)
646
+ frame[:payload] = @decompressor.decode(frame[:payload], frame) if frame[:payload].is_a? Buffer
647
+ rescue CompressionError => e
648
+ connection_error(:compression_error, e: e)
649
+ rescue ProtocolError => e
650
+ connection_error(:protocol_error, e: e)
651
+ rescue StandardError => e
652
+ connection_error(:internal_error, e: e)
653
+ end
654
+
655
+ # Encode headers payload and update connection compressor state.
656
+ #
657
+ # @param frame [Hash]
658
+ # @return [Array of Frame]
659
+ def encode_headers(frame)
660
+ payload = frame[:payload]
661
+ payload = @compressor.encode(payload) unless payload.is_a? Buffer
662
+
663
+ frames = []
664
+
665
+ while payload.bytesize > 0
666
+ cont = frame.dup
667
+ cont[:type] = :continuation
668
+ cont[:flags] = []
669
+ cont[:payload] = payload.slice!(0, @remote_settings[:settings_max_frame_size])
670
+ frames << cont
671
+ end
672
+ if frames.empty?
673
+ frames = [frame]
674
+ else
675
+ frames.first[:type] = frame[:type]
676
+ frames.first[:flags] = frame[:flags] - [:end_headers]
677
+ frames.last[:flags] << :end_headers
678
+ end
679
+
680
+ frames
681
+ rescue StandardError => e
682
+ connection_error(:compression_error, e: e)
683
+ nil
684
+ end
685
+
686
+ # Activates new incoming or outgoing stream and registers appropriate
687
+ # connection managemet callbacks.
688
+ #
689
+ # @param id [Integer]
690
+ # @param priority [Integer]
691
+ # @param window [Integer]
692
+ # @param parent [Stream]
693
+ def activate_stream(id: nil, **args)
694
+ connection_error(msg: "Stream ID already exists") if @streams.key?(id)
695
+
696
+ raise StreamLimitExceeded if @active_stream_count >= @local_settings[:settings_max_concurrent_streams]
697
+
698
+ stream = Stream.new(connection: self, id: id, **args)
699
+
700
+ # Streams that are in the "open" state, or either of the "half closed"
701
+ # states count toward the maximum number of streams that an endpoint is
702
+ # permitted to open.
703
+ stream.once(:active) { @active_stream_count += 1 }
704
+ stream.once(:close) do
705
+ # Store a reference to the closed stream, such that we can respond
706
+ # to any in-flight frames while close is registered on both sides.
707
+ # References to such streams will be purged whenever another stream
708
+ # is closed, with a minimum of 15s RTT time window.
709
+ @streams_recently_closed[id] = Time.now
710
+ to_delete = @streams_recently_closed.select { |_, v| (Time.now - v) > 15 }
711
+ to_delete.each do |stream_id|
712
+ @streams.delete stream_id
713
+ @streams_recently_closed.delete stream_id
714
+ end
715
+ end
716
+
717
+ stream.on(:promise, &method(:promise)) if is_a? Server
718
+ stream.on(:frame, &method(:send))
719
+
720
+ @streams[id] = stream
721
+ end
722
+
723
+ def verify_stream_order(id)
724
+ return unless id.odd?
725
+
726
+ connection_error(msg: "Stream ID smaller than previous") if @last_stream_id > id
727
+ @last_stream_id = id
728
+ end
729
+
730
+ # Emit GOAWAY error indicating to peer that the connection is being
731
+ # aborted, and once sent, raise a local exception.
732
+ #
733
+ # @param error [Symbol]
734
+ # @option error [Symbol] :no_error
735
+ # @option error [Symbol] :internal_error
736
+ # @option error [Symbol] :flow_control_error
737
+ # @option error [Symbol] :stream_closed
738
+ # @option error [Symbol] :frame_too_large
739
+ # @option error [Symbol] :compression_error
740
+ # @param msg [String]
741
+ def connection_error(error = :protocol_error, msg: nil, e: nil)
742
+ goaway(error) unless @state == :closed || @state == :new
743
+
744
+ @state = :closed
745
+ @error = error
746
+ msg ||= e ? e.message : "protocol error"
747
+ backtrace = e ? e.backtrace : nil
748
+ raise Error.types[error], msg, backtrace
749
+ end
750
+ alias error connection_error
751
+
752
+ def manage_state(_)
753
+ yield
754
+ end
755
+ end
756
+ # rubocop:enable Metrics/ClassLength
757
+ end