http-2 0.9.0 → 0.11.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 +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
|