http-2 0.9.0 → 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rspec +1 -0
- data/.rubocop.yml +44 -8
- data/.rubocop_todo.yml +15 -3
- data/.travis.yml +7 -4
- data/Gemfile +1 -1
- data/Guardfile +0 -1
- data/LICENSE +21 -0
- data/README.md +13 -19
- data/example/client.rb +1 -1
- data/example/server.rb +1 -1
- data/example/upgrade_client.rb +5 -6
- data/example/upgrade_server.rb +2 -2
- data/http-2.gemspec +2 -3
- data/lib/http/2/client.rb +6 -1
- data/lib/http/2/compressor.rb +21 -6
- data/lib/http/2/connection.rb +62 -20
- data/lib/http/2/framer.rb +3 -2
- data/lib/http/2/stream.rb +14 -1
- data/lib/http/2/version.rb +1 -1
- data/spec/client_spec.rb +74 -11
- data/spec/compressor_spec.rb +145 -26
- data/spec/connection_spec.rb +155 -84
- data/spec/framer_spec.rb +5 -5
- data/spec/helper.rb +125 -107
- data/spec/server_spec.rb +3 -2
- data/spec/stream_spec.rb +113 -111
- metadata +11 -11
data/lib/http/2/connection.rb
CHANGED
@@ -20,9 +20,9 @@ module HTTP2
|
|
20
20
|
|
21
21
|
DEFAULT_CONNECTION_SETTINGS = {
|
22
22
|
settings_header_table_size: 4096,
|
23
|
-
settings_enable_push: 1,
|
23
|
+
settings_enable_push: 1, # enabled for servers
|
24
24
|
settings_max_concurrent_streams: 100,
|
25
|
-
settings_initial_window_size: 65_535,
|
25
|
+
settings_initial_window_size: 65_535,
|
26
26
|
settings_max_frame_size: 16_384,
|
27
27
|
settings_max_header_list_size: 2**31 - 1, # unlimited
|
28
28
|
}.freeze
|
@@ -33,6 +33,9 @@ module HTTP2
|
|
33
33
|
# Default connection "fast-fail" preamble string as defined by the spec.
|
34
34
|
CONNECTION_PREFACE_MAGIC = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".freeze
|
35
35
|
|
36
|
+
# Time to hold recently closed streams until purge (seconds)
|
37
|
+
RECENTLY_CLOSED_STREAMS_TTL = 15
|
38
|
+
|
36
39
|
# Connection encapsulates all of the connection, stream, flow-control,
|
37
40
|
# error management, and other processing logic required for a well-behaved
|
38
41
|
# HTTP 2.0 endpoint.
|
@@ -49,9 +52,6 @@ module HTTP2
|
|
49
52
|
# Connection state (:new, :closed).
|
50
53
|
attr_reader :state
|
51
54
|
|
52
|
-
# Last connection error if connection is aborted.
|
53
|
-
attr_reader :error
|
54
|
-
|
55
55
|
# Size of current connection flow control window (by default, set to
|
56
56
|
# infinity, but is automatically updated on receipt of peer settings).
|
57
57
|
attr_reader :local_window
|
@@ -76,8 +76,8 @@ module HTTP2
|
|
76
76
|
@local_settings = DEFAULT_CONNECTION_SETTINGS.merge(settings)
|
77
77
|
@remote_settings = SPEC_DEFAULT_CONNECTION_SETTINGS.dup
|
78
78
|
|
79
|
-
@compressor = Header::Compressor.new(settings)
|
80
|
-
@decompressor = Header::Decompressor.new(settings)
|
79
|
+
@compressor = Header::Compressor.new(**settings)
|
80
|
+
@decompressor = Header::Decompressor.new(**settings)
|
81
81
|
|
82
82
|
@active_stream_count = 0
|
83
83
|
@streams = {}
|
@@ -95,6 +95,13 @@ module HTTP2
|
|
95
95
|
@send_buffer = []
|
96
96
|
@continuation = []
|
97
97
|
@error = nil
|
98
|
+
|
99
|
+
@h2c_upgrade = nil
|
100
|
+
@closed_since = nil
|
101
|
+
end
|
102
|
+
|
103
|
+
def closed?
|
104
|
+
@state == :closed
|
98
105
|
end
|
99
106
|
|
100
107
|
# Allocates new stream for current connection.
|
@@ -141,6 +148,7 @@ module HTTP2
|
|
141
148
|
send(type: :goaway, last_stream: last_stream,
|
142
149
|
error: error, payload: payload)
|
143
150
|
@state = :closed
|
151
|
+
@closed_since = Time.now
|
144
152
|
end
|
145
153
|
|
146
154
|
# Sends a WINDOW_UPDATE frame to the peer.
|
@@ -351,11 +359,14 @@ module HTTP2
|
|
351
359
|
end
|
352
360
|
end
|
353
361
|
|
354
|
-
rescue => e
|
362
|
+
rescue StandardError => e
|
355
363
|
raise if e.is_a?(Error::Error)
|
356
364
|
connection_error(e: e)
|
357
365
|
end
|
358
|
-
|
366
|
+
|
367
|
+
def <<(*args)
|
368
|
+
receive(*args)
|
369
|
+
end
|
359
370
|
|
360
371
|
private
|
361
372
|
|
@@ -443,12 +454,22 @@ module HTTP2
|
|
443
454
|
# the connection, although a new connection can be established
|
444
455
|
# for new streams.
|
445
456
|
@state = :closed
|
457
|
+
@closed_since = Time.now
|
446
458
|
emit(:goaway, frame[:last_stream], frame[:error], frame[:payload])
|
447
|
-
when :altsvc
|
459
|
+
when :altsvc
|
460
|
+
# 4. The ALTSVC HTTP/2 Frame
|
461
|
+
# An ALTSVC frame on stream 0 with empty (length 0) "Origin"
|
462
|
+
# information is invalid and MUST be ignored.
|
463
|
+
if frame[:origin] && !frame[:origin].empty?
|
464
|
+
emit(frame[:type], frame)
|
465
|
+
end
|
466
|
+
when :blocked
|
448
467
|
emit(frame[:type], frame)
|
449
468
|
else
|
450
469
|
connection_error
|
451
470
|
end
|
471
|
+
when :closed
|
472
|
+
connection_error if (Time.now - @closed_since) > 15
|
452
473
|
else
|
453
474
|
connection_error
|
454
475
|
end
|
@@ -493,7 +514,7 @@ module HTTP2
|
|
493
514
|
# allowed frame size (2^24-1 or 16,777,215 octets), inclusive.
|
494
515
|
# Values outside this range MUST be treated as a connection error
|
495
516
|
# (Section 5.4.1) of type PROTOCOL_ERROR.
|
496
|
-
unless
|
517
|
+
unless v >= 16_384 && v <= 16_777_215
|
497
518
|
return ProtocolError.new("invalid #{key} value")
|
498
519
|
end
|
499
520
|
when :settings_max_header_list_size
|
@@ -518,7 +539,7 @@ module HTTP2
|
|
518
539
|
# Process pending settings we have sent.
|
519
540
|
[@pending_settings.shift, :local]
|
520
541
|
else
|
521
|
-
connection_error
|
542
|
+
connection_error if validate_settings(@remote_role, frame[:payload])
|
522
543
|
[frame[:payload], :remote]
|
523
544
|
end
|
524
545
|
|
@@ -572,7 +593,8 @@ module HTTP2
|
|
572
593
|
# nothing to do
|
573
594
|
|
574
595
|
when :settings_max_frame_size
|
575
|
-
#
|
596
|
+
# update framer max_frame_size
|
597
|
+
@framer.max_frame_size = v
|
576
598
|
|
577
599
|
# else # ignore unknown settings
|
578
600
|
end
|
@@ -603,8 +625,12 @@ module HTTP2
|
|
603
625
|
frame[:payload] = @decompressor.decode(frame[:payload])
|
604
626
|
end
|
605
627
|
|
606
|
-
rescue => e
|
628
|
+
rescue CompressionError => e
|
607
629
|
connection_error(:compression_error, e: e)
|
630
|
+
rescue ProtocolError => e
|
631
|
+
connection_error(:protocol_error, e: e)
|
632
|
+
rescue StandardError => e
|
633
|
+
connection_error(:internal_error, e: e)
|
608
634
|
end
|
609
635
|
|
610
636
|
# Encode headers payload and update connection compressor state.
|
@@ -634,7 +660,7 @@ module HTTP2
|
|
634
660
|
|
635
661
|
frames
|
636
662
|
|
637
|
-
rescue => e
|
663
|
+
rescue StandardError => e
|
638
664
|
connection_error(:compression_error, e: e)
|
639
665
|
nil
|
640
666
|
end
|
@@ -649,22 +675,21 @@ module HTTP2
|
|
649
675
|
def activate_stream(id: nil, **args)
|
650
676
|
connection_error(msg: 'Stream ID already exists') if @streams.key?(id)
|
651
677
|
|
652
|
-
stream = Stream.new({ connection: self, id: id }.merge(args))
|
678
|
+
stream = Stream.new(**{ connection: self, id: id }.merge(args))
|
653
679
|
|
654
680
|
# Streams that are in the "open" state, or either of the "half closed"
|
655
681
|
# states count toward the maximum number of streams that an endpoint is
|
656
682
|
# permitted to open.
|
657
683
|
stream.once(:active) { @active_stream_count += 1 }
|
658
684
|
stream.once(:close) do
|
659
|
-
@streams.delete id
|
660
685
|
@active_stream_count -= 1
|
661
686
|
|
662
687
|
# Store a reference to the closed stream, such that we can respond
|
663
688
|
# to any in-flight frames while close is registered on both sides.
|
664
689
|
# References to such streams will be purged whenever another stream
|
665
|
-
# is closed, with a
|
666
|
-
@streams_recently_closed
|
667
|
-
|
690
|
+
# is closed, with a defined RTT time window.
|
691
|
+
@streams_recently_closed[id] = Time.now.to_i
|
692
|
+
cleanup_recently_closed
|
668
693
|
end
|
669
694
|
|
670
695
|
stream.on(:promise, &method(:promise)) if self.is_a? Server
|
@@ -673,6 +698,22 @@ module HTTP2
|
|
673
698
|
@streams[id] = stream
|
674
699
|
end
|
675
700
|
|
701
|
+
# Purge recently streams closed within defined RTT time window.
|
702
|
+
def cleanup_recently_closed
|
703
|
+
now_ts = Time.now.to_i
|
704
|
+
to_delete = []
|
705
|
+
@streams_recently_closed.each do |stream_id, ts|
|
706
|
+
# Ruby Hash enumeration is ordered, so once fresh stream is met we can stop searching.
|
707
|
+
break if now_ts - ts < RECENTLY_CLOSED_STREAMS_TTL
|
708
|
+
to_delete << stream_id
|
709
|
+
end
|
710
|
+
|
711
|
+
to_delete.each do |stream_id|
|
712
|
+
@streams.delete stream_id
|
713
|
+
@streams_recently_closed.delete stream_id
|
714
|
+
end
|
715
|
+
end
|
716
|
+
|
676
717
|
# Emit GOAWAY error indicating to peer that the connection is being
|
677
718
|
# aborted, and once sent, raise a local exception.
|
678
719
|
#
|
@@ -699,4 +740,5 @@ module HTTP2
|
|
699
740
|
yield
|
700
741
|
end
|
701
742
|
end
|
743
|
+
# rubocop:enable ClassLength
|
702
744
|
end
|
data/lib/http/2/framer.rb
CHANGED
@@ -359,14 +359,14 @@ module HTTP2
|
|
359
359
|
if frame[:flags].include? :priority
|
360
360
|
e_sd = payload.read_uint32
|
361
361
|
frame[:stream_dependency] = e_sd & RBIT
|
362
|
-
frame[:exclusive] = (e_sd & EBIT) != 0
|
362
|
+
frame[:exclusive] = (e_sd & EBIT) != 0
|
363
363
|
frame[:weight] = payload.getbyte + 1
|
364
364
|
end
|
365
365
|
frame[:payload] = payload.read(frame[:length])
|
366
366
|
when :priority
|
367
367
|
e_sd = payload.read_uint32
|
368
368
|
frame[:stream_dependency] = e_sd & RBIT
|
369
|
-
frame[:exclusive] = (e_sd & EBIT) != 0
|
369
|
+
frame[:exclusive] = (e_sd & EBIT) != 0
|
370
370
|
frame[:weight] = payload.getbyte + 1
|
371
371
|
when :rst_stream
|
372
372
|
frame[:error] = unpack_error payload.read_uint32
|
@@ -442,4 +442,5 @@ module HTTP2
|
|
442
442
|
name || error
|
443
443
|
end
|
444
444
|
end
|
445
|
+
# rubocop:enable ClassLength
|
445
446
|
end
|
data/lib/http/2/stream.rb
CHANGED
@@ -91,6 +91,10 @@ module HTTP2
|
|
91
91
|
on(:local_window) { |v| @local_window_max_size = @local_window = v }
|
92
92
|
end
|
93
93
|
|
94
|
+
def closed?
|
95
|
+
@state == :closed
|
96
|
+
end
|
97
|
+
|
94
98
|
# Processes incoming HTTP 2.0 frames. The frames must be decoded upstream.
|
95
99
|
#
|
96
100
|
# @param frame [Hash]
|
@@ -111,7 +115,15 @@ module HTTP2
|
|
111
115
|
process_priority(frame)
|
112
116
|
when :window_update
|
113
117
|
process_window_update(frame)
|
114
|
-
when :altsvc
|
118
|
+
when :altsvc
|
119
|
+
# 4. The ALTSVC HTTP/2 Frame
|
120
|
+
# An ALTSVC frame on a
|
121
|
+
# stream other than stream 0 containing non-empty "Origin" information
|
122
|
+
# is invalid and MUST be ignored.
|
123
|
+
if !frame[:origin] || frame[:origin].empty?
|
124
|
+
emit(frame[:type], frame)
|
125
|
+
end
|
126
|
+
when :blocked
|
115
127
|
emit(frame[:type], frame)
|
116
128
|
end
|
117
129
|
|
@@ -144,6 +156,7 @@ module HTTP2
|
|
144
156
|
end
|
145
157
|
|
146
158
|
# Sends a HEADERS frame containing HTTP response headers.
|
159
|
+
# All pseudo-header fields MUST appear in the header block before regular header fields.
|
147
160
|
#
|
148
161
|
# @param headers [Array or Hash] Array of key-value pairs or Hash
|
149
162
|
# @param end_headers [Boolean] indicates that no more headers will be sent
|
data/lib/http/2/version.rb
CHANGED
data/spec/client_spec.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
3
|
RSpec.describe HTTP2::Client do
|
4
|
+
include FrameHelpers
|
4
5
|
before(:each) do
|
5
6
|
@client = Client.new
|
6
7
|
end
|
@@ -32,6 +33,18 @@ RSpec.describe HTTP2::Client do
|
|
32
33
|
expect(frame[:type]).to eq :settings
|
33
34
|
expect(frame[:payload]).to include([:settings_max_concurrent_streams, 200])
|
34
35
|
end
|
36
|
+
|
37
|
+
it 'should initialize client when receiving server settings before sending ack' do
|
38
|
+
frames = []
|
39
|
+
@client.on(:frame) { |bytes| frames << bytes }
|
40
|
+
@client << f.generate(settings_frame)
|
41
|
+
|
42
|
+
expect(frames[0]).to eq CONNECTION_PREFACE_MAGIC
|
43
|
+
expect(f.parse(frames[1])[:type]).to eq :settings
|
44
|
+
ack_frame = f.parse(frames[2])
|
45
|
+
expect(ack_frame[:type]).to eq :settings
|
46
|
+
expect(ack_frame[:flags]).to include(:ack)
|
47
|
+
end
|
35
48
|
end
|
36
49
|
|
37
50
|
context 'upgrade' do
|
@@ -60,33 +73,33 @@ RSpec.describe HTTP2::Client do
|
|
60
73
|
|
61
74
|
it 'should raise error on PUSH_PROMISE against stream 0' do
|
62
75
|
expect do
|
63
|
-
@client << set_stream_id(f.generate(
|
76
|
+
@client << set_stream_id(f.generate(push_promise_frame), 0)
|
64
77
|
end.to raise_error(ProtocolError)
|
65
78
|
end
|
66
79
|
|
67
80
|
it 'should raise error on PUSH_PROMISE against bogus stream' do
|
68
81
|
expect do
|
69
|
-
@client << set_stream_id(f.generate(
|
82
|
+
@client << set_stream_id(f.generate(push_promise_frame), 31_415)
|
70
83
|
end.to raise_error(ProtocolError)
|
71
84
|
end
|
72
85
|
|
73
86
|
it 'should raise error on PUSH_PROMISE against non-idle stream' do
|
74
87
|
expect do
|
75
88
|
s = @client.new_stream
|
76
|
-
s.send
|
89
|
+
s.send headers_frame
|
77
90
|
|
78
|
-
@client << set_stream_id(f.generate(
|
79
|
-
@client << set_stream_id(f.generate(
|
91
|
+
@client << set_stream_id(f.generate(push_promise_frame), s.id)
|
92
|
+
@client << set_stream_id(f.generate(push_promise_frame), s.id)
|
80
93
|
end.to raise_error(ProtocolError)
|
81
94
|
end
|
82
95
|
|
83
96
|
it 'should emit stream object for received PUSH_PROMISE' do
|
84
97
|
s = @client.new_stream
|
85
|
-
s.send
|
98
|
+
s.send headers_frame
|
86
99
|
|
87
100
|
promise = nil
|
88
101
|
@client.on(:promise) { |stream| promise = stream }
|
89
|
-
@client << set_stream_id(f.generate(
|
102
|
+
@client << set_stream_id(f.generate(push_promise_frame), s.id)
|
90
103
|
|
91
104
|
expect(promise.id).to eq 2
|
92
105
|
expect(promise.state).to eq :reserved_remote
|
@@ -95,14 +108,14 @@ RSpec.describe HTTP2::Client do
|
|
95
108
|
it 'should emit promise headers for received PUSH_PROMISE' do
|
96
109
|
header = nil
|
97
110
|
s = @client.new_stream
|
98
|
-
s.send
|
111
|
+
s.send headers_frame
|
99
112
|
|
100
113
|
@client.on(:promise) do |stream|
|
101
114
|
stream.on(:promise_headers) do |h|
|
102
115
|
header = h
|
103
116
|
end
|
104
117
|
end
|
105
|
-
@client << set_stream_id(f.generate(
|
118
|
+
@client << set_stream_id(f.generate(push_promise_frame), s.id)
|
106
119
|
|
107
120
|
expect(header).to be_a(Array)
|
108
121
|
# expect(header).to eq([%w(a b)])
|
@@ -110,7 +123,7 @@ RSpec.describe HTTP2::Client do
|
|
110
123
|
|
111
124
|
it 'should auto RST_STREAM promises against locally-RST stream' do
|
112
125
|
s = @client.new_stream
|
113
|
-
s.send
|
126
|
+
s.send headers_frame
|
114
127
|
s.close
|
115
128
|
|
116
129
|
allow(@client).to receive(:send)
|
@@ -119,7 +132,57 @@ RSpec.describe HTTP2::Client do
|
|
119
132
|
expect(frame[:stream]).to eq 2
|
120
133
|
end
|
121
134
|
|
122
|
-
@client << set_stream_id(f.generate(
|
135
|
+
@client << set_stream_id(f.generate(push_promise_frame), s.id)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
context 'alt-svc' do
|
140
|
+
context 'received in the connection' do
|
141
|
+
it 'should emit :altsvc when receiving one' do
|
142
|
+
@client << f.generate(settings_frame)
|
143
|
+
frame = nil
|
144
|
+
@client.on(:altsvc) do |f|
|
145
|
+
frame = f
|
146
|
+
end
|
147
|
+
@client << f.generate(altsvc_frame)
|
148
|
+
expect(frame).to be_a(Hash)
|
149
|
+
end
|
150
|
+
it 'should not emit :altsvc when the frame when contains no host' do
|
151
|
+
@client << f.generate(settings_frame)
|
152
|
+
frame = nil
|
153
|
+
@client.on(:altsvc) do |f|
|
154
|
+
frame = f
|
155
|
+
end
|
156
|
+
|
157
|
+
@client << f.generate(altsvc_frame.merge(origin: nil))
|
158
|
+
expect(frame).to be_nil
|
159
|
+
end
|
160
|
+
end
|
161
|
+
context 'received in a stream' do
|
162
|
+
it 'should emit :altsvc' do
|
163
|
+
s = @client.new_stream
|
164
|
+
s.send headers_frame
|
165
|
+
s.close
|
166
|
+
|
167
|
+
frame = nil
|
168
|
+
s.on(:altsvc) { |f| frame = f }
|
169
|
+
|
170
|
+
@client << set_stream_id(f.generate(altsvc_frame.merge(origin: nil)), s.id)
|
171
|
+
|
172
|
+
expect(frame).to be_a(Hash)
|
173
|
+
end
|
174
|
+
it 'should not emit :alt_svc when the frame when contains a origin' do
|
175
|
+
s = @client.new_stream
|
176
|
+
s.send headers_frame
|
177
|
+
s.close
|
178
|
+
|
179
|
+
frame = nil
|
180
|
+
s.on(:altsvc) { |f| frame = f }
|
181
|
+
|
182
|
+
@client << set_stream_id(f.generate(altsvc_frame), s.id)
|
183
|
+
|
184
|
+
expect(frame).to be_nil
|
185
|
+
end
|
123
186
|
end
|
124
187
|
end
|
125
188
|
end
|
data/spec/compressor_spec.rb
CHANGED
@@ -236,6 +236,12 @@ RSpec.describe HTTP2::Header do
|
|
236
236
|
expect(cc.table.size).to be 1
|
237
237
|
expect(cc.table.first[0]).to eq 'test2'
|
238
238
|
end
|
239
|
+
|
240
|
+
it 'should reject table size update if exceed limit' do
|
241
|
+
cc = EncodingContext.new(table_size: 4096)
|
242
|
+
|
243
|
+
expect { cc.process(type: :changetablesize, value: 150_000_000) }.to raise_error(CompressionError)
|
244
|
+
end
|
239
245
|
end
|
240
246
|
|
241
247
|
context 'encode' do
|
@@ -351,6 +357,78 @@ RSpec.describe HTTP2::Header do
|
|
351
357
|
},
|
352
358
|
],
|
353
359
|
},
|
360
|
+
{ title: 'D.4.a. Request Examples with Huffman - Client Handling of Improperly Ordered Headers',
|
361
|
+
type: :request,
|
362
|
+
table_size: 4096,
|
363
|
+
huffman: :always,
|
364
|
+
streams: [
|
365
|
+
{ wire: '8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4 ff',
|
366
|
+
emitted: [
|
367
|
+
[':method', 'GET'],
|
368
|
+
[':scheme', 'http'],
|
369
|
+
[':path', '/'],
|
370
|
+
[':authority', 'www.example.com'],
|
371
|
+
],
|
372
|
+
table: [
|
373
|
+
[':authority', 'www.example.com'],
|
374
|
+
],
|
375
|
+
table_size: 57,
|
376
|
+
},
|
377
|
+
{ wire: '8286 84be 5886 a8eb 1064 9cbf',
|
378
|
+
emitted: [
|
379
|
+
[':method', 'GET'],
|
380
|
+
[':scheme', 'http'],
|
381
|
+
['cache-control', 'no-cache'],
|
382
|
+
[':path', '/'],
|
383
|
+
[':authority', 'www.example.com'],
|
384
|
+
],
|
385
|
+
table: [
|
386
|
+
['cache-control', 'no-cache'],
|
387
|
+
[':authority', 'www.example.com'],
|
388
|
+
],
|
389
|
+
table_size: 110,
|
390
|
+
},
|
391
|
+
{ wire: "8287 85bf 4088 25a8 49e9 5ba9 7d7f 8925
|
392
|
+
a849 e95b b8e8 b4bf",
|
393
|
+
emitted: [
|
394
|
+
[':method', 'GET'],
|
395
|
+
[':scheme', 'https'],
|
396
|
+
['custom-key', 'custom-value'],
|
397
|
+
[':path', '/index.html'],
|
398
|
+
[':authority', 'www.example.com'],
|
399
|
+
],
|
400
|
+
table: [
|
401
|
+
['custom-key', 'custom-value'],
|
402
|
+
['cache-control', 'no-cache'],
|
403
|
+
[':authority', 'www.example.com'],
|
404
|
+
],
|
405
|
+
table_size: 164,
|
406
|
+
},
|
407
|
+
],
|
408
|
+
},
|
409
|
+
{ title: 'D.4.b. Request Examples with Huffman - Server Handling of Improperly Ordered Headers',
|
410
|
+
type: :request,
|
411
|
+
bypass_encoder: true,
|
412
|
+
table_size: 4096,
|
413
|
+
huffman: :always,
|
414
|
+
streams: [
|
415
|
+
{ wire: '8286408825a849e95ba97d7f8925a849e95bb8e8b4bf84418cf1e3c2e5f23a6ba0ab90f4ff',
|
416
|
+
emitted: [
|
417
|
+
[':method', 'GET'],
|
418
|
+
[':scheme', 'http'],
|
419
|
+
['custom-key', 'custom-value'],
|
420
|
+
[':path', '/'],
|
421
|
+
[':authority', 'www.example.com'],
|
422
|
+
],
|
423
|
+
table: [
|
424
|
+
['custom-key', 'custom-value'],
|
425
|
+
[':authority', 'www.example.com'],
|
426
|
+
],
|
427
|
+
table_size: 111,
|
428
|
+
has_bad_headers: true,
|
429
|
+
},
|
430
|
+
],
|
431
|
+
},
|
354
432
|
{ title: 'D.5. Response Examples without Huffman',
|
355
433
|
type: :response,
|
356
434
|
table_size: 256,
|
@@ -474,6 +552,22 @@ RSpec.describe HTTP2::Header do
|
|
474
552
|
},
|
475
553
|
],
|
476
554
|
},
|
555
|
+
{ title: 'D.6.a. Response Examples with Huffman - dynamic table size updates should not trigger exceptions',
|
556
|
+
type: :response,
|
557
|
+
table_size: 4096,
|
558
|
+
huffman: :always,
|
559
|
+
bypass_encoder: true,
|
560
|
+
streams: [
|
561
|
+
{ wire: '2088 7689 aa63 55e5 80ae 16d7 17',
|
562
|
+
emitted: [
|
563
|
+
[':status', '200'],
|
564
|
+
['server', 'nginx/1.15.2'],
|
565
|
+
],
|
566
|
+
table: [],
|
567
|
+
table_size: 0,
|
568
|
+
},
|
569
|
+
],
|
570
|
+
},
|
477
571
|
]
|
478
572
|
|
479
573
|
context 'decode' do
|
@@ -485,25 +579,38 @@ RSpec.describe HTTP2::Header do
|
|
485
579
|
before do
|
486
580
|
(0...nth).each do |i|
|
487
581
|
bytes = [ex[:streams][i][:wire].delete(" \n")].pack('H*')
|
488
|
-
|
582
|
+
if ex[:streams][i][:has_bad_headers]
|
583
|
+
expect { @dc.decode(HTTP2::Buffer.new(bytes)) }.to raise_error ProtocolError
|
584
|
+
else
|
585
|
+
@dc.decode(HTTP2::Buffer.new(bytes))
|
586
|
+
end
|
489
587
|
end
|
490
588
|
end
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
589
|
+
if ex[:streams][nth][:has_bad_headers]
|
590
|
+
it 'should raise CompressionError' do
|
591
|
+
bytes = [ex[:streams][nth][:wire].delete(" \n")].pack('H*')
|
592
|
+
expect { @dc.decode(HTTP2::Buffer.new(bytes)) }.to raise_error ProtocolError
|
593
|
+
end
|
594
|
+
else
|
595
|
+
subject do
|
596
|
+
bytes = [ex[:streams][nth][:wire].delete(" \n")].pack('H*')
|
597
|
+
@emitted = @dc.decode(HTTP2::Buffer.new(bytes))
|
598
|
+
end
|
599
|
+
it 'should emit expected headers' do
|
600
|
+
subject
|
601
|
+
# partitioned compare
|
602
|
+
pseudo_headers, headers = ex[:streams][nth][:emitted].partition { |f, _| f.start_with? ':' }
|
603
|
+
partitioned_headers = pseudo_headers + headers
|
604
|
+
expect(@emitted).to eq partitioned_headers
|
605
|
+
end
|
606
|
+
it 'should update header table' do
|
607
|
+
subject
|
608
|
+
expect(@dc.instance_eval { @cc.table }).to eq ex[:streams][nth][:table]
|
609
|
+
end
|
610
|
+
it 'should compute header table size' do
|
611
|
+
subject
|
612
|
+
expect(@dc.instance_eval { @cc.current_table_size }).to eq ex[:streams][nth][:table_size]
|
613
|
+
end
|
507
614
|
end
|
508
615
|
end
|
509
616
|
end
|
@@ -513,6 +620,7 @@ RSpec.describe HTTP2::Header do
|
|
513
620
|
|
514
621
|
context 'encode' do
|
515
622
|
spec_examples.each do |ex|
|
623
|
+
next if ex[:bypass_encoder]
|
516
624
|
context "spec example #{ex[:title]}" do
|
517
625
|
ex[:streams].size.times do |nth|
|
518
626
|
context "request #{nth + 1}" do
|
@@ -522,22 +630,33 @@ RSpec.describe HTTP2::Header do
|
|
522
630
|
end
|
523
631
|
before do
|
524
632
|
(0...nth).each do |i|
|
525
|
-
|
633
|
+
if ex[:streams][i][:has_bad_headers]
|
634
|
+
@cc.encode(ex[:streams][i][:emitted], ensure_proper_ordering: false)
|
635
|
+
else
|
636
|
+
@cc.encode(ex[:streams][i][:emitted])
|
637
|
+
end
|
526
638
|
end
|
527
639
|
end
|
528
640
|
subject do
|
529
|
-
|
641
|
+
if ex[:streams][nth][:has_bad_headers]
|
642
|
+
@cc.encode(ex[:streams][nth][:emitted], ensure_proper_ordering: false)
|
643
|
+
else
|
644
|
+
@cc.encode(ex[:streams][nth][:emitted])
|
645
|
+
end
|
530
646
|
end
|
531
647
|
it 'should emit expected bytes on wire' do
|
648
|
+
puts subject.unpack('H*').first
|
532
649
|
expect(subject.unpack('H*').first).to eq ex[:streams][nth][:wire].delete(" \n")
|
533
650
|
end
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
651
|
+
unless ex[:streams][nth][:has_bad_headers]
|
652
|
+
it 'should update header table' do
|
653
|
+
subject
|
654
|
+
expect(@cc.instance_eval { @cc.table }).to eq ex[:streams][nth][:table]
|
655
|
+
end
|
656
|
+
it 'should compute header table size' do
|
657
|
+
subject
|
658
|
+
expect(@cc.instance_eval { @cc.current_table_size }).to eq ex[:streams][nth][:table_size]
|
659
|
+
end
|
541
660
|
end
|
542
661
|
end
|
543
662
|
end
|