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