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.
- checksums.yaml +4 -4
- data/README.md +10 -7
- data/lib/http/2/base64.rb +45 -0
- data/lib/http/2/client.rb +14 -5
- data/lib/http/2/connection.rb +176 -90
- data/lib/http/2/emitter.rb +4 -5
- data/lib/http/2/error.rb +22 -1
- data/lib/http/2/extensions.rb +53 -0
- data/lib/http/2/flow_buffer.rb +88 -35
- data/lib/http/2/framer.rb +127 -89
- data/lib/http/2/header/compressor.rb +157 -0
- data/lib/http/2/header/decompressor.rb +144 -0
- data/lib/http/2/header/encoding_context.rb +337 -0
- data/lib/http/2/{huffman.rb → header/huffman.rb} +11 -7
- data/lib/http/2/{huffman_statemachine.rb → header/huffman_statemachine.rb} +2 -0
- data/lib/http/2/header.rb +35 -0
- data/lib/http/2/server.rb +39 -14
- data/lib/http/2/stream.rb +86 -17
- data/lib/http/2/version.rb +1 -1
- data/lib/http/2.rb +12 -13
- data/sig/client.rbs +9 -0
- data/sig/connection.rbs +93 -0
- data/sig/emitter.rbs +13 -0
- data/sig/error.rbs +35 -0
- data/sig/extensions.rbs +5 -0
- data/sig/flow_buffer.rbs +21 -0
- data/sig/frame_buffer.rbs +13 -0
- data/sig/framer.rbs +54 -0
- data/sig/header/compressor.rbs +27 -0
- data/sig/header/decompressor.rbs +22 -0
- data/sig/header/encoding_context.rbs +34 -0
- data/sig/header/huffman.rbs +9 -0
- data/sig/header.rbs +27 -0
- data/sig/next.rbs +101 -0
- data/sig/server.rbs +12 -0
- data/sig/stream.rbs +91 -0
- metadata +37 -25
- data/LICENSE +0 -21
- data/lib/http/2/buffer.rb +0 -78
- data/lib/http/2/compressor.rb +0 -580
data/lib/http/2/connection.rb
CHANGED
@@ -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
|
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
|
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
|
-
|
39
|
-
|
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
|
-
|
76
|
+
attr_accessor :active_stream_count
|
73
77
|
|
74
78
|
# Initializes new connection object.
|
75
79
|
#
|
76
|
-
def initialize(
|
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(
|
81
|
-
@decompressor = Header::Decompressor.new(
|
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 =
|
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 =
|
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
|
-
|
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
|
-
|
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] =
|
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
|
-
|
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:
|
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
|
-
|
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 <<(
|
366
|
-
receive(
|
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
|
-
|
440
|
-
send_data(nil, true)
|
484
|
+
process_window_update(frame: frame, encode: true)
|
441
485
|
when :ping
|
442
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
508
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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?
|
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
|
-
|
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
|
-
|
711
|
+
until payload.nil? || payload.empty?
|
632
712
|
cont = frame.dup
|
633
|
-
|
634
|
-
|
635
|
-
|
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
|
-
|
639
|
-
|
640
|
-
|
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
|
660
|
-
connection_error(msg:
|
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
|
675
|
-
|
676
|
-
|
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
|
-
|
686
|
-
|
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
|
-
|
694
|
-
|
770
|
+
connection_error(msg: "Stream ID smaller than previous") if @last_stream_id > id
|
771
|
+
@last_stream_id = id
|
772
|
+
end
|
695
773
|
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
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
|
-
|
719
|
-
|
720
|
-
|
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
|
|
data/lib/http/2/emitter.rb
CHANGED
@@ -9,19 +9,18 @@ module HTTP2
|
|
9
9
|
#
|
10
10
|
# @param event [Symbol]
|
11
11
|
# @param block [Proc] callback function
|
12
|
-
def
|
13
|
-
raise ArgumentError,
|
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
|
-
|
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)
|
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
|
-
|
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
|