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.
- 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
|