http-2 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.rubocop.yml +18 -0
- data/.rubocop_todo.yml +46 -0
- data/.travis.yml +10 -2
- data/Gemfile +7 -5
- data/README.md +35 -37
- data/Rakefile +9 -10
- data/example/README.md +0 -13
- data/example/client.rb +12 -15
- data/example/helper.rb +2 -2
- data/example/server.rb +19 -19
- data/example/upgrade_server.rb +191 -0
- data/http-2.gemspec +10 -9
- data/lib/http/2.rb +13 -13
- data/lib/http/2/buffer.rb +6 -10
- data/lib/http/2/client.rb +5 -10
- data/lib/http/2/compressor.rb +134 -146
- data/lib/http/2/connection.rb +104 -100
- data/lib/http/2/emitter.rb +2 -4
- data/lib/http/2/error.rb +7 -7
- data/lib/http/2/flow_buffer.rb +11 -10
- data/lib/http/2/framer.rb +78 -87
- data/lib/http/2/huffman.rb +265 -274
- data/lib/http/2/huffman_statemachine.rb +257 -257
- data/lib/http/2/server.rb +81 -6
- data/lib/http/2/stream.rb +195 -130
- data/lib/http/2/version.rb +1 -1
- data/lib/tasks/generate_huffman_table.rb +30 -24
- data/spec/buffer_spec.rb +11 -13
- data/spec/client_spec.rb +41 -42
- data/spec/compressor_spec.rb +243 -242
- data/spec/connection_spec.rb +252 -248
- data/spec/emitter_spec.rb +12 -12
- data/spec/framer_spec.rb +177 -179
- data/spec/helper.rb +56 -57
- data/spec/huffman_spec.rb +33 -33
- data/spec/server_spec.rb +15 -15
- data/spec/stream_spec.rb +356 -265
- metadata +7 -6
- data/spec/hpack_test_spec.rb +0 -83
data/lib/http/2/version.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
desc
|
1
|
+
desc 'Generate Huffman precompiled table in huffman_statemachine.rb'
|
2
2
|
task :generate_table do
|
3
3
|
HuffmanTable::Node.generate_state_table
|
4
4
|
end
|
@@ -14,16 +14,17 @@ module HuffmanTable
|
|
14
14
|
attr_accessor :next, :emit, :final, :depth
|
15
15
|
attr_accessor :transitions
|
16
16
|
attr_accessor :id
|
17
|
-
@@id = 0
|
17
|
+
@@id = 0 # rubocop:disable Style/ClassVars
|
18
18
|
def initialize(depth)
|
19
19
|
@next = [nil, nil]
|
20
20
|
@id = @@id
|
21
|
-
@@id += 1
|
21
|
+
@@id += 1 # rubocop:disable Style/ClassVars
|
22
22
|
@final = false
|
23
23
|
@depth = depth
|
24
24
|
end
|
25
|
+
|
25
26
|
def add(code, len, chr)
|
26
|
-
chr == EOS && @depth <= 7
|
27
|
+
self.final = true if chr == EOS && @depth <= 7
|
27
28
|
if len == 0
|
28
29
|
@emit = chr
|
29
30
|
else
|
@@ -69,14 +70,13 @@ module HuffmanTable
|
|
69
70
|
(BITS_AT_ONCE - 1).downto(0) do |i|
|
70
71
|
bit = (input & (1 << i)) == 0 ? 0 : 1
|
71
72
|
n = n.next[bit]
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
end
|
78
|
-
n = @root
|
73
|
+
next unless n.emit
|
74
|
+
if n.emit == EOS
|
75
|
+
emit = EOS # cause error on decoding
|
76
|
+
else
|
77
|
+
emit << n.emit.chr(Encoding::BINARY) unless emit == EOS
|
79
78
|
end
|
79
|
+
n = @root
|
80
80
|
end
|
81
81
|
node.transitions[input] = Transition.new(emit, n)
|
82
82
|
togo << n
|
@@ -95,14 +95,14 @@ module HuffmanTable
|
|
95
95
|
id_state[0] = @root
|
96
96
|
max_final = 0
|
97
97
|
id = 1
|
98
|
-
(@states - [@root]).sort_by{|s|s.final ? 0 : 1}.each do |s|
|
98
|
+
(@states - [@root]).sort_by { |s| s.final ? 0 : 1 }.each do |s|
|
99
99
|
state_id[s] = id
|
100
100
|
id_state[id] = s
|
101
101
|
max_final = id if s.final
|
102
102
|
id += 1
|
103
103
|
end
|
104
104
|
|
105
|
-
File.open(File.expand_path(
|
105
|
+
File.open(File.expand_path('../http/2/huffman_statemachine.rb', File.dirname(__FILE__)), 'w') do |f|
|
106
106
|
f.print <<HEADER
|
107
107
|
# Machine generated Huffman decoder state machine.
|
108
108
|
# DO NOT EDIT THIS FILE.
|
@@ -119,16 +119,22 @@ module HTTP2
|
|
119
119
|
HEADER
|
120
120
|
id.times do |i|
|
121
121
|
n = id_state[i]
|
122
|
-
f.print
|
123
|
-
(1 << BITS_AT_ONCE).times do |t|
|
124
|
-
|
125
|
-
emit
|
126
|
-
|
127
|
-
|
122
|
+
f.print ' ['
|
123
|
+
string = (1 << BITS_AT_ONCE).times.map do |t|
|
124
|
+
transition = n.transitions.fetch(t)
|
125
|
+
emit = transition.emit
|
126
|
+
unless emit == EOS
|
127
|
+
bytes = emit.bytes
|
128
|
+
fail ArgumentError if bytes.size > 1
|
129
|
+
emit = bytes.first
|
130
|
+
end
|
131
|
+
"[#{emit.inspect}, #{state_id.fetch(transition.node)}]"
|
132
|
+
end.join(', ')
|
133
|
+
f.print(string)
|
128
134
|
f.print "],\n"
|
129
135
|
end
|
130
136
|
f.print <<TAILER
|
131
|
-
]
|
137
|
+
].each { |arr| arr.each { |subarr| subarr.each(&:freeze) }.freeze }.freeze
|
132
138
|
end
|
133
139
|
end
|
134
140
|
end
|
@@ -136,22 +142,22 @@ TAILER
|
|
136
142
|
end
|
137
143
|
end
|
138
144
|
|
139
|
-
|
140
|
-
|
145
|
+
class << self
|
146
|
+
attr_reader :root
|
141
147
|
end
|
142
148
|
|
143
149
|
# Test decoder
|
144
150
|
def self.decode(input)
|
145
151
|
emit = ''
|
146
152
|
n = root
|
147
|
-
nibbles = input.unpack(
|
153
|
+
nibbles = input.unpack('C*').flat_map { |b| [((b & 0xf0) >> 4), b & 0xf] }
|
148
154
|
until nibbles.empty?
|
149
155
|
nb = nibbles.shift
|
150
156
|
t = n.transitions[nb]
|
151
157
|
emit << t.emit
|
152
158
|
n = t.node
|
153
159
|
end
|
154
|
-
unless n.final && nibbles.all?{|x| x == 0xf}
|
160
|
+
unless n.final && nibbles.all? { |x| x == 0xf }
|
155
161
|
puts "len = #{emit.size} n.final = #{n.final} nibbles = #{nibbles}"
|
156
162
|
end
|
157
163
|
emit
|
data/spec/buffer_spec.rb
CHANGED
@@ -1,23 +1,21 @@
|
|
1
|
-
require
|
1
|
+
require 'helper'
|
2
2
|
|
3
|
-
describe HTTP2::Buffer do
|
3
|
+
RSpec.describe HTTP2::Buffer do
|
4
|
+
let(:b) { Buffer.new('émalgré') }
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
it "should force 8-bit encoding" do
|
8
|
-
b.encoding.to_s.should eq "ASCII-8BIT"
|
6
|
+
it 'should force 8-bit encoding' do
|
7
|
+
expect(b.encoding.to_s).to eq 'ASCII-8BIT'
|
9
8
|
end
|
10
9
|
|
11
|
-
it
|
12
|
-
b.size.
|
10
|
+
it 'should return bytesize of the buffer' do
|
11
|
+
expect(b.size).to eq 9
|
13
12
|
end
|
14
13
|
|
15
|
-
it
|
16
|
-
9.times { b.read(1).
|
14
|
+
it 'should read single byte at a time' do
|
15
|
+
9.times { expect(b.read(1)).not_to be_nil }
|
17
16
|
end
|
18
17
|
|
19
|
-
it
|
20
|
-
Buffer.new([256].pack(
|
18
|
+
it 'should unpack an unsigned 32-bit int' do
|
19
|
+
expect(Buffer.new([256].pack('N')).read_uint32).to eq 256
|
21
20
|
end
|
22
|
-
|
23
21
|
end
|
data/spec/client_spec.rb
CHANGED
@@ -1,93 +1,92 @@
|
|
1
|
-
require
|
1
|
+
require 'helper'
|
2
2
|
|
3
|
-
describe HTTP2::Client do
|
3
|
+
RSpec.describe HTTP2::Client do
|
4
4
|
before(:each) do
|
5
5
|
@client = Client.new
|
6
6
|
end
|
7
7
|
|
8
8
|
let(:f) { Framer.new }
|
9
9
|
|
10
|
-
context
|
11
|
-
it
|
12
|
-
@client.new_stream.id.
|
10
|
+
context 'initialization and settings' do
|
11
|
+
it 'should return odd stream IDs' do
|
12
|
+
expect(@client.new_stream.id).not_to be_even
|
13
13
|
end
|
14
14
|
|
15
|
-
it
|
15
|
+
it 'should emit connection header and SETTINGS on new client connection' do
|
16
16
|
frames = []
|
17
17
|
@client.on(:frame) { |bytes| frames << bytes }
|
18
|
-
@client.ping(
|
18
|
+
@client.ping('12345678')
|
19
19
|
|
20
|
-
frames[0].
|
21
|
-
f.parse(frames[1])[:type].
|
20
|
+
expect(frames[0]).to eq CONNECTION_PREFACE_MAGIC
|
21
|
+
expect(f.parse(frames[1])[:type]).to eq :settings
|
22
22
|
end
|
23
23
|
|
24
|
-
it
|
24
|
+
it 'should initialize client with custom connection settings' do
|
25
25
|
frames = []
|
26
26
|
|
27
|
-
@client = Client.new(:
|
27
|
+
@client = Client.new(settings_max_concurrent_streams: 200)
|
28
28
|
@client.on(:frame) { |bytes| frames << bytes }
|
29
|
-
@client.ping(
|
29
|
+
@client.ping('12345678')
|
30
30
|
|
31
31
|
frame = f.parse(frames[1])
|
32
|
-
frame[:type].
|
33
|
-
frame[:payload].
|
32
|
+
expect(frame[:type]).to eq :settings
|
33
|
+
expect(frame[:payload]).to include([:settings_max_concurrent_streams, 200])
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
37
|
-
context
|
38
|
-
it
|
37
|
+
context 'push' do
|
38
|
+
it 'should disallow client initiated push' do
|
39
39
|
expect do
|
40
40
|
@client.promise({}) {}
|
41
|
-
end.to raise_error(
|
41
|
+
end.to raise_error(NoMethodError)
|
42
42
|
end
|
43
43
|
|
44
|
-
it
|
45
|
-
expect
|
46
|
-
@client << set_stream_id(f.generate(PUSH_PROMISE), 0)
|
47
|
-
|
44
|
+
it 'should raise error on PUSH_PROMISE against stream 0' do
|
45
|
+
expect do
|
46
|
+
@client << set_stream_id(f.generate(PUSH_PROMISE.dup), 0)
|
47
|
+
end.to raise_error(ProtocolError)
|
48
48
|
end
|
49
49
|
|
50
|
-
it
|
51
|
-
expect
|
52
|
-
@client << set_stream_id(f.generate(PUSH_PROMISE),
|
53
|
-
|
50
|
+
it 'should raise error on PUSH_PROMISE against bogus stream' do
|
51
|
+
expect do
|
52
|
+
@client << set_stream_id(f.generate(PUSH_PROMISE.dup), 31_415)
|
53
|
+
end.to raise_error(ProtocolError)
|
54
54
|
end
|
55
55
|
|
56
|
-
it
|
57
|
-
expect
|
56
|
+
it 'should raise error on PUSH_PROMISE against non-idle stream' do
|
57
|
+
expect do
|
58
58
|
s = @client.new_stream
|
59
59
|
s.send HEADERS
|
60
60
|
|
61
61
|
@client << set_stream_id(f.generate(PUSH_PROMISE), s.id)
|
62
62
|
@client << set_stream_id(f.generate(PUSH_PROMISE), s.id)
|
63
|
-
|
63
|
+
end.to raise_error(ProtocolError)
|
64
64
|
end
|
65
65
|
|
66
|
-
it
|
66
|
+
it 'should emit stream object for received PUSH_PROMISE' do
|
67
67
|
s = @client.new_stream
|
68
|
-
s.send HEADERS
|
68
|
+
s.send HEADERS.deep_dup
|
69
69
|
|
70
70
|
promise = nil
|
71
|
-
@client.on(:promise) {|
|
72
|
-
@client << set_stream_id(f.generate(PUSH_PROMISE), s.id)
|
71
|
+
@client.on(:promise) { |stream| promise = stream }
|
72
|
+
@client << set_stream_id(f.generate(PUSH_PROMISE.dup), s.id)
|
73
73
|
|
74
|
-
promise.id.
|
75
|
-
promise.state.
|
74
|
+
expect(promise.id).to eq 2
|
75
|
+
expect(promise.state).to eq :reserved_remote
|
76
76
|
end
|
77
77
|
|
78
|
-
it
|
78
|
+
it 'should auto RST_STREAM promises against locally-RST stream' do
|
79
79
|
s = @client.new_stream
|
80
|
-
s.send HEADERS
|
80
|
+
s.send HEADERS.deep_dup
|
81
81
|
s.close
|
82
82
|
|
83
|
-
@client.
|
84
|
-
@client.
|
85
|
-
frame[:type].
|
86
|
-
frame[:stream].
|
83
|
+
allow(@client).to receive(:send)
|
84
|
+
expect(@client).to receive(:send) do |frame|
|
85
|
+
expect(frame[:type]).to eq :rst_stream
|
86
|
+
expect(frame[:stream]).to eq 2
|
87
87
|
end
|
88
88
|
|
89
|
-
@client << set_stream_id(f.generate(PUSH_PROMISE), s.id)
|
89
|
+
@client << set_stream_id(f.generate(PUSH_PROMISE.dup), s.id)
|
90
90
|
end
|
91
91
|
end
|
92
|
-
|
93
92
|
end
|
data/spec/compressor_spec.rb
CHANGED
@@ -1,246 +1,246 @@
|
|
1
|
-
require
|
2
|
-
|
3
|
-
describe HTTP2::Header do
|
1
|
+
require 'helper'
|
4
2
|
|
3
|
+
RSpec.describe HTTP2::Header do
|
5
4
|
let(:c) { Compressor.new }
|
6
5
|
let(:d) { Decompressor.new }
|
7
6
|
|
8
|
-
context
|
9
|
-
context
|
10
|
-
it
|
7
|
+
context 'literal representation' do
|
8
|
+
context 'integer' do
|
9
|
+
it 'should encode 10 using a 5-bit prefix' do
|
11
10
|
buf = c.integer(10, 5)
|
12
|
-
buf.
|
13
|
-
d.integer(Buffer.new(buf), 5).
|
11
|
+
expect(buf).to eq [10].pack('C')
|
12
|
+
expect(d.integer(Buffer.new(buf), 5)).to eq 10
|
14
13
|
end
|
15
14
|
|
16
|
-
it
|
15
|
+
it 'should encode 10 using a 0-bit prefix' do
|
17
16
|
buf = c.integer(10, 0)
|
18
|
-
buf.
|
19
|
-
d.integer(Buffer.new(buf), 0).
|
17
|
+
expect(buf).to eq [10].pack('C')
|
18
|
+
expect(d.integer(Buffer.new(buf), 0)).to eq 10
|
20
19
|
end
|
21
20
|
|
22
|
-
it
|
21
|
+
it 'should encode 1337 using a 5-bit prefix' do
|
23
22
|
buf = c.integer(1337, 5)
|
24
|
-
buf.
|
25
|
-
d.integer(Buffer.new(buf), 5).
|
23
|
+
expect(buf).to eq [31, 128 + 26, 10].pack('C*')
|
24
|
+
expect(d.integer(Buffer.new(buf), 5)).to eq 1337
|
26
25
|
end
|
27
26
|
|
28
|
-
it
|
29
|
-
buf = c.integer(1337,0)
|
30
|
-
buf.
|
31
|
-
d.integer(Buffer.new(buf), 0).
|
27
|
+
it 'should encode 1337 using a 0-bit prefix' do
|
28
|
+
buf = c.integer(1337, 0)
|
29
|
+
expect(buf).to eq [128 + 57, 10].pack('C*')
|
30
|
+
expect(d.integer(Buffer.new(buf), 0)).to eq 1337
|
32
31
|
end
|
33
32
|
end
|
34
33
|
|
35
|
-
context
|
36
|
-
[
|
37
|
-
['
|
38
|
-
|
34
|
+
context 'string' do
|
35
|
+
[
|
36
|
+
['with huffman', :always, 0x80],
|
37
|
+
['without huffman', :never, 0],
|
38
|
+
].each do |desc, option, msb|
|
39
|
+
let(:trailer) { 'trailer' }
|
39
40
|
|
40
41
|
[
|
41
42
|
['ascii codepoints', 'abcdefghij'],
|
42
43
|
['utf-8 codepoints', 'éáűőúöüó€'],
|
43
|
-
['long utf-8 strings', 'éáűőúöüó€'*100],
|
44
|
+
['long utf-8 strings', 'éáűőúöüó€' * 100],
|
44
45
|
].each do |datatype, plain|
|
45
46
|
it "should handle #{datatype} #{desc}" do
|
46
47
|
# NOTE: don't put this new in before{} because of test case shuffling
|
47
48
|
@c = Compressor.new(huffman: option)
|
48
49
|
str = @c.string(plain)
|
49
|
-
(str.getbyte(0) & 0x80).
|
50
|
+
expect(str.getbyte(0) & 0x80).to eq msb
|
50
51
|
|
51
52
|
buf = Buffer.new(str + trailer)
|
52
|
-
d.string(buf).
|
53
|
-
buf.
|
53
|
+
expect(d.string(buf)).to eq plain
|
54
|
+
expect(buf).to eq trailer
|
54
55
|
end
|
55
56
|
end
|
56
57
|
end
|
57
|
-
context
|
58
|
-
[
|
59
|
-
|
60
|
-
|
58
|
+
context 'choosing shorter representation' do
|
59
|
+
[['日本語', :plain],
|
60
|
+
['200', :huffman],
|
61
|
+
['xq', :plain], # prefer plain if equal size
|
61
62
|
].each do |string, choice|
|
62
63
|
before { @c = Compressor.new(huffman: :shorter) }
|
63
64
|
|
64
65
|
it "should return #{choice} representation" do
|
65
66
|
wire = @c.string(string)
|
66
|
-
(wire.getbyte(0) & 0x80).
|
67
|
+
expect(wire.getbyte(0) & 0x80).to eq(choice == :plain ? 0 : 0x80)
|
67
68
|
end
|
68
69
|
end
|
69
70
|
end
|
70
71
|
end
|
71
72
|
end
|
72
73
|
|
73
|
-
context
|
74
|
-
it
|
75
|
-
h = {name: 10, type: :indexed}
|
74
|
+
context 'header representation' do
|
75
|
+
it 'should handle indexed representation' do
|
76
|
+
h = { name: 10, type: :indexed }
|
76
77
|
wire = c.header(h)
|
77
|
-
(wire.readbyte(0) & 0x80).
|
78
|
-
(wire.readbyte(0) & 0x7f).
|
79
|
-
d.header(wire).
|
78
|
+
expect(wire.readbyte(0) & 0x80).to eq 0x80
|
79
|
+
expect(wire.readbyte(0) & 0x7f).to eq h[:name] + 1
|
80
|
+
expect(d.header(wire)).to eq h
|
80
81
|
end
|
81
|
-
it
|
82
|
-
h = {name: 10, type: :indexed}
|
82
|
+
it 'should raise when decoding indexed representation with index zero' do
|
83
|
+
h = { name: 10, type: :indexed }
|
83
84
|
wire = c.header(h)
|
84
|
-
wire[0] = 0x80.chr(
|
85
|
+
wire[0] = 0x80.chr(Encoding::BINARY)
|
85
86
|
expect { d.header(wire) }.to raise_error CompressionError
|
86
87
|
end
|
87
88
|
|
88
|
-
context
|
89
|
-
it
|
90
|
-
h = {name: 10, value:
|
89
|
+
context 'literal w/o indexing representation' do
|
90
|
+
it 'should handle indexed header' do
|
91
|
+
h = { name: 10, value: 'my-value', type: :noindex }
|
91
92
|
wire = c.header(h)
|
92
|
-
(wire.readbyte(0) & 0xf0).
|
93
|
-
(wire.readbyte(0) & 0x0f).
|
94
|
-
d.header(wire).
|
93
|
+
expect(wire.readbyte(0) & 0xf0).to eq 0x0
|
94
|
+
expect(wire.readbyte(0) & 0x0f).to eq h[:name] + 1
|
95
|
+
expect(d.header(wire)).to eq h
|
95
96
|
end
|
96
97
|
|
97
|
-
it
|
98
|
-
h = {name:
|
98
|
+
it 'should handle literal header' do
|
99
|
+
h = { name: 'x-custom', value: 'my-value', type: :noindex }
|
99
100
|
wire = c.header(h)
|
100
|
-
(wire.readbyte(0) & 0xf0).
|
101
|
-
(wire.readbyte(0) & 0x0f).
|
102
|
-
d.header(wire).
|
101
|
+
expect(wire.readbyte(0) & 0xf0).to eq 0x0
|
102
|
+
expect(wire.readbyte(0) & 0x0f).to eq 0
|
103
|
+
expect(d.header(wire)).to eq h
|
103
104
|
end
|
104
105
|
end
|
105
106
|
|
106
|
-
context
|
107
|
-
it
|
108
|
-
h = {name: 10, value:
|
107
|
+
context 'literal w/ incremental indexing' do
|
108
|
+
it 'should handle indexed header' do
|
109
|
+
h = { name: 10, value: 'my-value', type: :incremental }
|
109
110
|
wire = c.header(h)
|
110
|
-
(wire.readbyte(0) & 0xc0).
|
111
|
-
(wire.readbyte(0) & 0x3f).
|
112
|
-
d.header(wire).
|
111
|
+
expect(wire.readbyte(0) & 0xc0).to eq 0x40
|
112
|
+
expect(wire.readbyte(0) & 0x3f).to eq h[:name] + 1
|
113
|
+
expect(d.header(wire)).to eq h
|
113
114
|
end
|
114
115
|
|
115
|
-
it
|
116
|
-
h = {name:
|
116
|
+
it 'should handle literal header' do
|
117
|
+
h = { name: 'x-custom', value: 'my-value', type: :incremental }
|
117
118
|
wire = c.header(h)
|
118
|
-
(wire.readbyte(0) & 0xc0).
|
119
|
-
(wire.readbyte(0) & 0x3f).
|
120
|
-
d.header(wire).
|
119
|
+
expect(wire.readbyte(0) & 0xc0).to eq 0x40
|
120
|
+
expect(wire.readbyte(0) & 0x3f).to eq 0
|
121
|
+
expect(d.header(wire)).to eq h
|
121
122
|
end
|
122
123
|
end
|
123
124
|
|
124
|
-
context
|
125
|
-
it
|
126
|
-
h = {name: 10, value:
|
125
|
+
context 'literal never indexed' do
|
126
|
+
it 'should handle indexed header' do
|
127
|
+
h = { name: 10, value: 'my-value', type: :neverindexed }
|
127
128
|
wire = c.header(h)
|
128
|
-
(wire.readbyte(0) & 0xf0).
|
129
|
-
(wire.readbyte(0) & 0x0f).
|
130
|
-
d.header(wire).
|
129
|
+
expect(wire.readbyte(0) & 0xf0).to eq 0x10
|
130
|
+
expect(wire.readbyte(0) & 0x0f).to eq h[:name] + 1
|
131
|
+
expect(d.header(wire)).to eq h
|
131
132
|
end
|
132
133
|
|
133
|
-
it
|
134
|
-
h = {name:
|
134
|
+
it 'should handle literal header' do
|
135
|
+
h = { name: 'x-custom', value: 'my-value', type: :neverindexed }
|
135
136
|
wire = c.header(h)
|
136
|
-
(wire.readbyte(0) & 0xf0).
|
137
|
-
(wire.readbyte(0) & 0x0f).
|
138
|
-
d.header(wire).
|
137
|
+
expect(wire.readbyte(0) & 0xf0).to eq 0x10
|
138
|
+
expect(wire.readbyte(0) & 0x0f).to eq 0
|
139
|
+
expect(d.header(wire)).to eq h
|
139
140
|
end
|
140
141
|
end
|
141
142
|
end
|
142
143
|
|
143
|
-
context
|
144
|
+
context 'shared compression context' do
|
144
145
|
before(:each) { @cc = EncodingContext.new }
|
145
146
|
|
146
|
-
it
|
147
|
+
it 'should be initialized with empty headers' do
|
147
148
|
cc = EncodingContext.new
|
148
|
-
cc.table.
|
149
|
+
expect(cc.table).to be_empty
|
149
150
|
end
|
150
151
|
|
151
|
-
context
|
152
|
-
[
|
153
|
-
[
|
152
|
+
context 'processing' do
|
153
|
+
[
|
154
|
+
['no indexing', :noindex],
|
155
|
+
['never indexed', :neverindexed],
|
156
|
+
].each do |desc, type|
|
154
157
|
context "#{desc}" do
|
155
|
-
it
|
158
|
+
it 'should process indexed header with literal value' do
|
156
159
|
original_table = @cc.table.dup
|
157
160
|
|
158
|
-
emit = @cc.process(
|
159
|
-
emit.
|
160
|
-
@cc.table.
|
161
|
+
emit = @cc.process(name: 4, value: '/path', type: type)
|
162
|
+
expect(emit).to eq [':path', '/path']
|
163
|
+
expect(@cc.table).to eq original_table
|
161
164
|
end
|
162
165
|
|
163
|
-
it
|
166
|
+
it 'should process literal header with literal value' do
|
164
167
|
original_table = @cc.table.dup
|
165
168
|
|
166
|
-
emit = @cc.process(
|
167
|
-
emit.
|
168
|
-
@cc.table.
|
169
|
+
emit = @cc.process(name: 'x-custom', value: 'random', type: type)
|
170
|
+
expect(emit).to eq ['x-custom', 'random']
|
171
|
+
expect(@cc.table).to eq original_table
|
169
172
|
end
|
170
173
|
end
|
171
174
|
end
|
172
175
|
|
173
|
-
context
|
174
|
-
it
|
176
|
+
context 'incremental indexing' do
|
177
|
+
it 'should process indexed header with literal value' do
|
175
178
|
original_table = @cc.table.dup
|
176
179
|
|
177
|
-
emit = @cc.process(
|
178
|
-
emit.
|
179
|
-
(@cc.table - original_table).
|
180
|
+
emit = @cc.process(name: 4, value: '/path', type: :incremental)
|
181
|
+
expect(emit).to eq [':path', '/path']
|
182
|
+
expect(@cc.table - original_table).to eq [[':path', '/path']]
|
180
183
|
end
|
181
184
|
|
182
|
-
it
|
185
|
+
it 'should process literal header with literal value' do
|
183
186
|
original_table = @cc.table.dup
|
184
187
|
|
185
|
-
@cc.process(
|
186
|
-
(@cc.table - original_table).
|
188
|
+
@cc.process(name: 'x-custom', value: 'random', type: :incremental)
|
189
|
+
expect(@cc.table - original_table).to eq [['x-custom', 'random']]
|
187
190
|
end
|
188
191
|
end
|
189
192
|
|
190
|
-
context
|
191
|
-
it
|
193
|
+
context 'size bounds' do
|
194
|
+
it 'should drop headers from end of table' do
|
192
195
|
cc = EncodingContext.new(table_size: 2048)
|
193
196
|
cc.instance_eval do
|
194
|
-
add_to_table([
|
195
|
-
add_to_table([
|
197
|
+
add_to_table(['test1', '1' * 1024])
|
198
|
+
add_to_table(['test2', '2' * 500])
|
196
199
|
end
|
197
200
|
|
198
201
|
original_table = cc.table.dup
|
199
|
-
original_size = original_table.join.bytesize +
|
200
|
-
original_table.size * 32
|
202
|
+
original_size = original_table.join.bytesize + original_table.size * 32
|
201
203
|
|
202
|
-
cc.process(
|
203
|
-
|
204
|
-
|
205
|
-
type: :incremental
|
206
|
-
})
|
204
|
+
cc.process(name: 'x-custom',
|
205
|
+
value: 'a' * (2048 - original_size),
|
206
|
+
type: :incremental)
|
207
207
|
|
208
|
-
cc.table.first[0].
|
209
|
-
cc.table.size.
|
208
|
+
expect(cc.table.first[0]).to eq 'x-custom'
|
209
|
+
expect(cc.table.size).to eq original_table.size # number of entries
|
210
210
|
end
|
211
211
|
end
|
212
212
|
|
213
|
-
it
|
213
|
+
it 'should clear table if entry exceeds table size' do
|
214
214
|
cc = EncodingContext.new(table_size: 2048)
|
215
215
|
cc.instance_eval do
|
216
|
-
add_to_table([
|
217
|
-
add_to_table([
|
216
|
+
add_to_table(['test1', '1' * 1024])
|
217
|
+
add_to_table(['test2', '2' * 500])
|
218
218
|
end
|
219
219
|
|
220
|
-
h = { name:
|
221
|
-
e = { name:
|
220
|
+
h = { name: 'x-custom', value: 'a', index: 0, type: :incremental }
|
221
|
+
e = { name: 'large', value: 'a' * 2048, index: 0 }
|
222
222
|
|
223
223
|
cc.process(h)
|
224
|
-
cc.process(e.merge(
|
225
|
-
cc.table.
|
224
|
+
cc.process(e.merge(type: :incremental))
|
225
|
+
expect(cc.table).to be_empty
|
226
226
|
end
|
227
227
|
|
228
|
-
it
|
228
|
+
it 'should shrink table if set smaller size' do
|
229
229
|
cc = EncodingContext.new(table_size: 2048)
|
230
230
|
cc.instance_eval do
|
231
|
-
add_to_table([
|
232
|
-
add_to_table([
|
231
|
+
add_to_table(['test1', '1' * 1024])
|
232
|
+
add_to_table(['test2', '2' * 500])
|
233
233
|
end
|
234
234
|
|
235
|
-
cc.process(
|
236
|
-
cc.table.size.
|
237
|
-
cc.table.first[0].
|
235
|
+
cc.process(type: :changetablesize, value: 1500)
|
236
|
+
expect(cc.table.size).to be 1
|
237
|
+
expect(cc.table.first[0]).to eq 'test2'
|
238
238
|
end
|
239
239
|
end
|
240
240
|
end
|
241
241
|
|
242
242
|
spec_examples = [
|
243
|
-
{ title:
|
243
|
+
{ title: 'D.3. Request Examples without Huffman',
|
244
244
|
type: :request,
|
245
245
|
table_size: 4096,
|
246
246
|
huffman: :never,
|
@@ -248,98 +248,98 @@ describe HTTP2::Header do
|
|
248
248
|
{ wire: "8286 8441 0f77 7777 2e65 7861 6d70 6c65
|
249
249
|
2e63 6f6d",
|
250
250
|
emitted: [
|
251
|
-
[
|
252
|
-
[
|
253
|
-
[
|
254
|
-
[
|
251
|
+
[':method', 'GET'],
|
252
|
+
[':scheme', 'http'],
|
253
|
+
[':path', '/'],
|
254
|
+
[':authority', 'www.example.com'],
|
255
255
|
],
|
256
256
|
table: [
|
257
|
-
[
|
257
|
+
[':authority', 'www.example.com'],
|
258
258
|
],
|
259
259
|
table_size: 57,
|
260
260
|
},
|
261
|
-
{ wire:
|
261
|
+
{ wire: '8286 84be 5808 6e6f 2d63 6163 6865',
|
262
262
|
emitted: [
|
263
|
-
[
|
264
|
-
[
|
265
|
-
[
|
266
|
-
[
|
267
|
-
[
|
263
|
+
[':method', 'GET'],
|
264
|
+
[':scheme', 'http'],
|
265
|
+
[':path', '/'],
|
266
|
+
[':authority', 'www.example.com'],
|
267
|
+
['cache-control', 'no-cache'],
|
268
268
|
],
|
269
269
|
table: [
|
270
|
-
[
|
271
|
-
[
|
270
|
+
['cache-control', 'no-cache'],
|
271
|
+
[':authority', 'www.example.com'],
|
272
272
|
],
|
273
273
|
table_size: 110,
|
274
274
|
},
|
275
275
|
{ wire: "8287 85bf 400a 6375 7374 6f6d 2d6b 6579
|
276
276
|
0c63 7573 746f 6d2d 7661 6c75 65",
|
277
277
|
emitted: [
|
278
|
-
[
|
279
|
-
[
|
280
|
-
[
|
281
|
-
[
|
282
|
-
[
|
278
|
+
[':method', 'GET'],
|
279
|
+
[':scheme', 'https'],
|
280
|
+
[':path', '/index.html'],
|
281
|
+
[':authority', 'www.example.com'],
|
282
|
+
['custom-key', 'custom-value'],
|
283
283
|
],
|
284
284
|
table: [
|
285
|
-
[
|
286
|
-
[
|
287
|
-
[
|
285
|
+
['custom-key', 'custom-value'],
|
286
|
+
['cache-control', 'no-cache'],
|
287
|
+
[':authority', 'www.example.com'],
|
288
288
|
],
|
289
289
|
table_size: 164,
|
290
|
-
}
|
290
|
+
},
|
291
291
|
],
|
292
292
|
},
|
293
|
-
{ title:
|
293
|
+
{ title: 'D.4. Request Examples with Huffman',
|
294
294
|
type: :request,
|
295
295
|
table_size: 4096,
|
296
296
|
huffman: :always,
|
297
297
|
streams: [
|
298
|
-
{ wire:
|
298
|
+
{ wire: '8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4 ff',
|
299
299
|
emitted: [
|
300
|
-
[
|
301
|
-
[
|
302
|
-
[
|
303
|
-
[
|
300
|
+
[':method', 'GET'],
|
301
|
+
[':scheme', 'http'],
|
302
|
+
[':path', '/'],
|
303
|
+
[':authority', 'www.example.com'],
|
304
304
|
],
|
305
305
|
table: [
|
306
|
-
[
|
306
|
+
[':authority', 'www.example.com'],
|
307
307
|
],
|
308
308
|
table_size: 57,
|
309
309
|
},
|
310
|
-
{ wire:
|
310
|
+
{ wire: '8286 84be 5886 a8eb 1064 9cbf',
|
311
311
|
emitted: [
|
312
|
-
[
|
313
|
-
[
|
314
|
-
[
|
315
|
-
[
|
316
|
-
[
|
312
|
+
[':method', 'GET'],
|
313
|
+
[':scheme', 'http'],
|
314
|
+
[':path', '/'],
|
315
|
+
[':authority', 'www.example.com'],
|
316
|
+
['cache-control', 'no-cache'],
|
317
317
|
],
|
318
318
|
table: [
|
319
|
-
[
|
320
|
-
[
|
319
|
+
['cache-control', 'no-cache'],
|
320
|
+
[':authority', 'www.example.com'],
|
321
321
|
],
|
322
322
|
table_size: 110,
|
323
323
|
},
|
324
324
|
{ wire: "8287 85bf 4088 25a8 49e9 5ba9 7d7f 8925
|
325
325
|
a849 e95b b8e8 b4bf",
|
326
326
|
emitted: [
|
327
|
-
[
|
328
|
-
[
|
329
|
-
[
|
330
|
-
[
|
331
|
-
[
|
327
|
+
[':method', 'GET'],
|
328
|
+
[':scheme', 'https'],
|
329
|
+
[':path', '/index.html'],
|
330
|
+
[':authority', 'www.example.com'],
|
331
|
+
['custom-key', 'custom-value'],
|
332
332
|
],
|
333
333
|
table: [
|
334
|
-
[
|
335
|
-
[
|
336
|
-
[
|
334
|
+
['custom-key', 'custom-value'],
|
335
|
+
['cache-control', 'no-cache'],
|
336
|
+
[':authority', 'www.example.com'],
|
337
337
|
],
|
338
338
|
table_size: 164,
|
339
339
|
},
|
340
340
|
],
|
341
341
|
},
|
342
|
-
{ title:
|
342
|
+
{ title: 'D.5. Response Examples without Huffman',
|
343
343
|
type: :response,
|
344
344
|
table_size: 256,
|
345
345
|
huffman: :never,
|
@@ -350,31 +350,31 @@ describe HTTP2::Header do
|
|
350
350
|
7474 7073 3a2f 2f77 7777 2e65 7861 6d70
|
351
351
|
6c65 2e63 6f6d",
|
352
352
|
emitted: [
|
353
|
-
[
|
354
|
-
[
|
355
|
-
[
|
356
|
-
[
|
353
|
+
[':status', '302'],
|
354
|
+
['cache-control', 'private'],
|
355
|
+
['date', 'Mon, 21 Oct 2013 20:13:21 GMT'],
|
356
|
+
['location', 'https://www.example.com'],
|
357
357
|
],
|
358
358
|
table: [
|
359
|
-
[
|
360
|
-
[
|
361
|
-
[
|
362
|
-
[
|
359
|
+
['location', 'https://www.example.com'],
|
360
|
+
['date', 'Mon, 21 Oct 2013 20:13:21 GMT'],
|
361
|
+
['cache-control', 'private'],
|
362
|
+
[':status', '302'],
|
363
363
|
],
|
364
364
|
table_size: 222,
|
365
365
|
},
|
366
|
-
{ wire:
|
366
|
+
{ wire: '4803 3330 37c1 c0bf',
|
367
367
|
emitted: [
|
368
|
-
[
|
369
|
-
[
|
370
|
-
[
|
371
|
-
[
|
368
|
+
[':status', '307'],
|
369
|
+
['cache-control', 'private'],
|
370
|
+
['date', 'Mon, 21 Oct 2013 20:13:21 GMT'],
|
371
|
+
['location', 'https://www.example.com'],
|
372
372
|
],
|
373
373
|
table: [
|
374
|
-
[
|
375
|
-
[
|
376
|
-
[
|
377
|
-
[
|
374
|
+
[':status', '307'],
|
375
|
+
['location', 'https://www.example.com'],
|
376
|
+
['date', 'Mon, 21 Oct 2013 20:13:21 GMT'],
|
377
|
+
['cache-control', 'private'],
|
378
378
|
],
|
379
379
|
table_size: 222,
|
380
380
|
},
|
@@ -386,23 +386,23 @@ describe HTTP2::Header do
|
|
386
386
|
6765 3d33 3630 303b 2076 6572 7369 6f6e
|
387
387
|
3d31",
|
388
388
|
emitted: [
|
389
|
-
[
|
390
|
-
[
|
391
|
-
[
|
392
|
-
[
|
393
|
-
[
|
394
|
-
[
|
389
|
+
[':status', '200'],
|
390
|
+
['cache-control', 'private'],
|
391
|
+
['date', 'Mon, 21 Oct 2013 20:13:22 GMT'],
|
392
|
+
['location', 'https://www.example.com'],
|
393
|
+
['content-encoding', 'gzip'],
|
394
|
+
['set-cookie', 'foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1'],
|
395
395
|
],
|
396
396
|
table: [
|
397
|
-
[
|
398
|
-
[
|
399
|
-
[
|
397
|
+
['set-cookie', 'foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1'],
|
398
|
+
['content-encoding', 'gzip'],
|
399
|
+
['date', 'Mon, 21 Oct 2013 20:13:22 GMT'],
|
400
400
|
],
|
401
401
|
table_size: 215,
|
402
402
|
},
|
403
403
|
],
|
404
404
|
},
|
405
|
-
{ title:
|
405
|
+
{ title: 'D.6. Response Examples with Huffman',
|
406
406
|
type: :response,
|
407
407
|
table_size: 256,
|
408
408
|
huffman: :always,
|
@@ -412,31 +412,31 @@ describe HTTP2::Header do
|
|
412
412
|
2d1b ff6e 919d 29ad 1718 63c7 8f0b 97c8
|
413
413
|
e9ae 82ae 43d3",
|
414
414
|
emitted: [
|
415
|
-
[
|
416
|
-
[
|
417
|
-
[
|
418
|
-
[
|
415
|
+
[':status', '302'],
|
416
|
+
['cache-control', 'private'],
|
417
|
+
['date', 'Mon, 21 Oct 2013 20:13:21 GMT'],
|
418
|
+
['location', 'https://www.example.com'],
|
419
419
|
],
|
420
420
|
table: [
|
421
|
-
[
|
422
|
-
[
|
423
|
-
[
|
424
|
-
[
|
421
|
+
['location', 'https://www.example.com'],
|
422
|
+
['date', 'Mon, 21 Oct 2013 20:13:21 GMT'],
|
423
|
+
['cache-control', 'private'],
|
424
|
+
[':status', '302'],
|
425
425
|
],
|
426
426
|
table_size: 222,
|
427
427
|
},
|
428
|
-
{ wire:
|
428
|
+
{ wire: '4883 640e ffc1 c0bf',
|
429
429
|
emitted: [
|
430
|
-
[
|
431
|
-
[
|
432
|
-
[
|
433
|
-
[
|
430
|
+
[':status', '307'],
|
431
|
+
['cache-control', 'private'],
|
432
|
+
['date', 'Mon, 21 Oct 2013 20:13:21 GMT'],
|
433
|
+
['location', 'https://www.example.com'],
|
434
434
|
],
|
435
435
|
table: [
|
436
|
-
[
|
437
|
-
[
|
438
|
-
[
|
439
|
-
[
|
436
|
+
[':status', '307'],
|
437
|
+
['location', 'https://www.example.com'],
|
438
|
+
['date', 'Mon, 21 Oct 2013 20:13:21 GMT'],
|
439
|
+
['cache-control', 'private'],
|
440
440
|
],
|
441
441
|
table_size: 222,
|
442
442
|
},
|
@@ -446,17 +446,17 @@ describe HTTP2::Header do
|
|
446
446
|
3960 d5af 2708 7f36 72c1 ab27 0fb5 291f
|
447
447
|
9587 3160 65c0 03ed 4ee5 b106 3d50 07",
|
448
448
|
emitted: [
|
449
|
-
[
|
450
|
-
[
|
451
|
-
[
|
452
|
-
[
|
453
|
-
[
|
454
|
-
[
|
449
|
+
[':status', '200'],
|
450
|
+
['cache-control', 'private'],
|
451
|
+
['date', 'Mon, 21 Oct 2013 20:13:22 GMT'],
|
452
|
+
['location', 'https://www.example.com'],
|
453
|
+
['content-encoding', 'gzip'],
|
454
|
+
['set-cookie', 'foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1'],
|
455
455
|
],
|
456
456
|
table: [
|
457
|
-
[
|
458
|
-
[
|
459
|
-
[
|
457
|
+
['set-cookie', 'foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1'],
|
458
|
+
['content-encoding', 'gzip'],
|
459
|
+
['date', 'Mon, 21 Oct 2013 20:13:22 GMT'],
|
460
460
|
],
|
461
461
|
table_size: 215,
|
462
462
|
},
|
@@ -464,34 +464,34 @@ describe HTTP2::Header do
|
|
464
464
|
},
|
465
465
|
]
|
466
466
|
|
467
|
-
context
|
467
|
+
context 'decode' do
|
468
468
|
spec_examples.each do |ex|
|
469
469
|
context "spec example #{ex[:title]}" do
|
470
470
|
ex[:streams].size.times do |nth|
|
471
|
-
context "request #{nth+1}" do
|
471
|
+
context "request #{nth + 1}" do
|
472
472
|
before { @dc = Decompressor.new(table_size: ex[:table_size]) }
|
473
473
|
before do
|
474
474
|
(0...nth).each do |i|
|
475
|
-
bytes = [ex[:streams][i][:wire].delete(" \n")].pack(
|
475
|
+
bytes = [ex[:streams][i][:wire].delete(" \n")].pack('H*')
|
476
476
|
@dc.decode(HTTP2::Buffer.new(bytes))
|
477
477
|
end
|
478
478
|
end
|
479
479
|
subject do
|
480
|
-
bytes = [ex[:streams][nth][:wire].delete(" \n")].pack(
|
480
|
+
bytes = [ex[:streams][nth][:wire].delete(" \n")].pack('H*')
|
481
481
|
@emitted = @dc.decode(HTTP2::Buffer.new(bytes))
|
482
482
|
end
|
483
|
-
it
|
483
|
+
it 'should emit expected headers' do
|
484
484
|
subject
|
485
485
|
# order-perserving compare
|
486
|
-
@emitted.
|
486
|
+
expect(@emitted).to eq ex[:streams][nth][:emitted]
|
487
487
|
end
|
488
|
-
it
|
488
|
+
it 'should update header table' do
|
489
489
|
subject
|
490
|
-
@dc.instance_eval{@cc.table}.
|
490
|
+
expect(@dc.instance_eval { @cc.table }).to eq ex[:streams][nth][:table]
|
491
491
|
end
|
492
|
-
it
|
492
|
+
it 'should compute header table size' do
|
493
493
|
subject
|
494
|
-
@dc.instance_eval{@cc.current_table_size}.
|
494
|
+
expect(@dc.instance_eval { @cc.current_table_size }).to eq ex[:streams][nth][:table_size]
|
495
495
|
end
|
496
496
|
end
|
497
497
|
end
|
@@ -499,13 +499,15 @@ describe HTTP2::Header do
|
|
499
499
|
end
|
500
500
|
end
|
501
501
|
|
502
|
-
context
|
502
|
+
context 'encode' do
|
503
503
|
spec_examples.each do |ex|
|
504
504
|
context "spec example #{ex[:title]}" do
|
505
505
|
ex[:streams].size.times do |nth|
|
506
|
-
context "request #{nth+1}" do
|
507
|
-
before
|
508
|
-
|
506
|
+
context "request #{nth + 1}" do
|
507
|
+
before do
|
508
|
+
@cc = Compressor.new(table_size: ex[:table_size],
|
509
|
+
huffman: ex[:huffman])
|
510
|
+
end
|
509
511
|
before do
|
510
512
|
(0...nth).each do |i|
|
511
513
|
@cc.encode(ex[:streams][i][:emitted])
|
@@ -514,21 +516,20 @@ describe HTTP2::Header do
|
|
514
516
|
subject do
|
515
517
|
@cc.encode(ex[:streams][nth][:emitted])
|
516
518
|
end
|
517
|
-
it
|
518
|
-
subject.unpack(
|
519
|
+
it 'should emit expected bytes on wire' do
|
520
|
+
expect(subject.unpack('H*').first).to eq ex[:streams][nth][:wire].delete(" \n")
|
519
521
|
end
|
520
|
-
it
|
522
|
+
it 'should update header table' do
|
521
523
|
subject
|
522
|
-
@cc.instance_eval{@cc.table}.
|
524
|
+
expect(@cc.instance_eval { @cc.table }).to eq ex[:streams][nth][:table]
|
523
525
|
end
|
524
|
-
it
|
526
|
+
it 'should compute header table size' do
|
525
527
|
subject
|
526
|
-
@cc.instance_eval{@cc.current_table_size}.
|
528
|
+
expect(@cc.instance_eval { @cc.current_table_size }).to eq ex[:streams][nth][:table_size]
|
527
529
|
end
|
528
530
|
end
|
529
531
|
end
|
530
532
|
end
|
531
533
|
end
|
532
534
|
end
|
533
|
-
|
534
535
|
end
|