mieps_http-2 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 +7 -0
- data/.autotest +20 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +17 -0
- data/.gitmodules +3 -0
- data/.rspec +4 -0
- data/.rubocop.yml +18 -0
- data/.rubocop_todo.yml +46 -0
- data/.travis.yml +13 -0
- data/Gemfile +18 -0
- data/README.md +285 -0
- data/Rakefile +11 -0
- data/example/README.md +40 -0
- data/example/client.rb +117 -0
- data/example/helper.rb +19 -0
- data/example/keys/mycert.pem +23 -0
- data/example/keys/mykey.pem +27 -0
- data/example/server.rb +97 -0
- data/example/upgrade_server.rb +193 -0
- data/http-2.gemspec +23 -0
- data/lib/http/2/buffer.rb +34 -0
- data/lib/http/2/client.rb +51 -0
- data/lib/http/2/compressor.rb +557 -0
- data/lib/http/2/connection.rb +654 -0
- data/lib/http/2/emitter.rb +45 -0
- data/lib/http/2/error.rb +44 -0
- data/lib/http/2/flow_buffer.rb +67 -0
- data/lib/http/2/framer.rb +440 -0
- data/lib/http/2/huffman.rb +323 -0
- data/lib/http/2/huffman_statemachine.rb +272 -0
- data/lib/http/2/server.rb +132 -0
- data/lib/http/2/stream.rb +576 -0
- data/lib/http/2/version.rb +3 -0
- data/lib/http/2.rb +13 -0
- data/lib/tasks/generate_huffman_table.rb +166 -0
- data/spec/buffer_spec.rb +21 -0
- data/spec/client_spec.rb +92 -0
- data/spec/compressor_spec.rb +535 -0
- data/spec/connection_spec.rb +581 -0
- data/spec/emitter_spec.rb +54 -0
- data/spec/framer_spec.rb +487 -0
- data/spec/helper.rb +128 -0
- data/spec/hpack_test_spec.rb +79 -0
- data/spec/huffman_spec.rb +68 -0
- data/spec/server_spec.rb +51 -0
- data/spec/stream_spec.rb +794 -0
- metadata +116 -0
data/lib/http/2.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'http/2/version'
|
2
|
+
require 'http/2/error'
|
3
|
+
require 'http/2/emitter'
|
4
|
+
require 'http/2/buffer'
|
5
|
+
require 'http/2/flow_buffer'
|
6
|
+
require 'http/2/huffman'
|
7
|
+
require 'http/2/huffman_statemachine'
|
8
|
+
require 'http/2/compressor'
|
9
|
+
require 'http/2/framer'
|
10
|
+
require 'http/2/connection'
|
11
|
+
require 'http/2/client'
|
12
|
+
require 'http/2/server'
|
13
|
+
require 'http/2/stream'
|
@@ -0,0 +1,166 @@
|
|
1
|
+
desc 'Generate Huffman precompiled table in huffman_statemachine.rb'
|
2
|
+
task :generate_table do
|
3
|
+
HuffmanTable::Node.generate_state_table
|
4
|
+
end
|
5
|
+
|
6
|
+
require_relative '../http/2/huffman'
|
7
|
+
|
8
|
+
# @private
|
9
|
+
module HuffmanTable
|
10
|
+
BITS_AT_ONCE = HTTP2::Header::Huffman::BITS_AT_ONCE
|
11
|
+
EOS = 256
|
12
|
+
|
13
|
+
class Node
|
14
|
+
attr_accessor :next, :emit, :final, :depth
|
15
|
+
attr_accessor :transitions
|
16
|
+
attr_accessor :id
|
17
|
+
@@id = 0 # rubocop:disable Style/ClassVars
|
18
|
+
def initialize(depth)
|
19
|
+
@next = [nil, nil]
|
20
|
+
@id = @@id
|
21
|
+
@@id += 1 # rubocop:disable Style/ClassVars
|
22
|
+
@final = false
|
23
|
+
@depth = depth
|
24
|
+
end
|
25
|
+
|
26
|
+
def add(code, len, chr)
|
27
|
+
self.final = true if chr == EOS && @depth <= 7
|
28
|
+
if len == 0
|
29
|
+
@emit = chr
|
30
|
+
else
|
31
|
+
bit = (code & (1 << (len - 1))) == 0 ? 0 : 1
|
32
|
+
node = @next[bit] ||= Node.new(@depth + 1)
|
33
|
+
node.add(code, len - 1, chr)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Transition
|
38
|
+
attr_accessor :emit, :node
|
39
|
+
def initialize(emit, node)
|
40
|
+
@emit = emit
|
41
|
+
@node = node
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.generate_tree
|
46
|
+
@root = new(0)
|
47
|
+
HTTP2::Header::Huffman::CODES.each_with_index do |c, chr|
|
48
|
+
code, len = c
|
49
|
+
@root.add(code, len, chr)
|
50
|
+
end
|
51
|
+
puts "#{@@id} nodes"
|
52
|
+
@root
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.generate_machine
|
56
|
+
generate_tree
|
57
|
+
togo = Set[@root]
|
58
|
+
@states = Set[@root]
|
59
|
+
|
60
|
+
until togo.empty?
|
61
|
+
node = togo.first
|
62
|
+
togo.delete(node)
|
63
|
+
|
64
|
+
next if node.transitions
|
65
|
+
node.transitions = Array[1 << BITS_AT_ONCE]
|
66
|
+
|
67
|
+
(1 << BITS_AT_ONCE).times do |input|
|
68
|
+
n = node
|
69
|
+
emit = ''
|
70
|
+
(BITS_AT_ONCE - 1).downto(0) do |i|
|
71
|
+
bit = (input & (1 << i)) == 0 ? 0 : 1
|
72
|
+
n = n.next[bit]
|
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
|
78
|
+
end
|
79
|
+
n = @root
|
80
|
+
end
|
81
|
+
node.transitions[input] = Transition.new(emit, n)
|
82
|
+
togo << n
|
83
|
+
@states << n
|
84
|
+
end
|
85
|
+
end
|
86
|
+
puts "#{@states.size} states"
|
87
|
+
@root
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.generate_state_table
|
91
|
+
generate_machine
|
92
|
+
state_id = {}
|
93
|
+
id_state = {}
|
94
|
+
state_id[@root] = 0
|
95
|
+
id_state[0] = @root
|
96
|
+
max_final = 0
|
97
|
+
id = 1
|
98
|
+
(@states - [@root]).sort_by { |s| s.final ? 0 : 1 }.each do |s|
|
99
|
+
state_id[s] = id
|
100
|
+
id_state[id] = s
|
101
|
+
max_final = id if s.final
|
102
|
+
id += 1
|
103
|
+
end
|
104
|
+
|
105
|
+
File.open(File.expand_path('../http/2/huffman_statemachine.rb', File.dirname(__FILE__)), 'w') do |f|
|
106
|
+
f.print <<HEADER
|
107
|
+
# Machine generated Huffman decoder state machine.
|
108
|
+
# DO NOT EDIT THIS FILE.
|
109
|
+
|
110
|
+
# The following task generates this file.
|
111
|
+
# rake generate_huffman_table
|
112
|
+
|
113
|
+
module HTTP2
|
114
|
+
module Header
|
115
|
+
class Huffman
|
116
|
+
# :nodoc:
|
117
|
+
MAX_FINAL_STATE = #{max_final}
|
118
|
+
MACHINE = [
|
119
|
+
HEADER
|
120
|
+
id.times do |i|
|
121
|
+
n = id_state[i]
|
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)
|
134
|
+
f.print "],\n"
|
135
|
+
end
|
136
|
+
f.print <<TAILER
|
137
|
+
].each { |arr| arr.each { |subarr| subarr.each(&:freeze) }.freeze }.freeze
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
TAILER
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
class << self
|
146
|
+
attr_reader :root
|
147
|
+
end
|
148
|
+
|
149
|
+
# Test decoder
|
150
|
+
def self.decode(input)
|
151
|
+
emit = ''
|
152
|
+
n = root
|
153
|
+
nibbles = input.unpack('C*').flat_map { |b| [((b & 0xf0) >> 4), b & 0xf] }
|
154
|
+
until nibbles.empty?
|
155
|
+
nb = nibbles.shift
|
156
|
+
t = n.transitions[nb]
|
157
|
+
emit << t.emit
|
158
|
+
n = t.node
|
159
|
+
end
|
160
|
+
unless n.final && nibbles.all? { |x| x == 0xf }
|
161
|
+
puts "len = #{emit.size} n.final = #{n.final} nibbles = #{nibbles}"
|
162
|
+
end
|
163
|
+
emit
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
data/spec/buffer_spec.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
RSpec.describe HTTP2::Buffer do
|
4
|
+
let(:b) { Buffer.new('émalgré') }
|
5
|
+
|
6
|
+
it 'should force 8-bit encoding' do
|
7
|
+
expect(b.encoding.to_s).to eq 'ASCII-8BIT'
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should return bytesize of the buffer' do
|
11
|
+
expect(b.size).to eq 9
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should read single byte at a time' do
|
15
|
+
9.times { expect(b.read(1)).not_to be_nil }
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should unpack an unsigned 32-bit int' do
|
19
|
+
expect(Buffer.new([256].pack('N')).read_uint32).to eq 256
|
20
|
+
end
|
21
|
+
end
|
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
RSpec.describe HTTP2::Client do
|
4
|
+
before(:each) do
|
5
|
+
@client = Client.new
|
6
|
+
end
|
7
|
+
|
8
|
+
let(:f) { Framer.new }
|
9
|
+
|
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
|
+
end
|
14
|
+
|
15
|
+
it 'should emit connection header and SETTINGS on new client connection' do
|
16
|
+
frames = []
|
17
|
+
@client.on(:frame) { |bytes| frames << bytes }
|
18
|
+
@client.ping('12345678')
|
19
|
+
|
20
|
+
expect(frames[0]).to eq CONNECTION_PREFACE_MAGIC
|
21
|
+
expect(f.parse(frames[1])[:type]).to eq :settings
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should initialize client with custom connection settings' do
|
25
|
+
frames = []
|
26
|
+
|
27
|
+
@client = Client.new(settings_max_concurrent_streams: 200)
|
28
|
+
@client.on(:frame) { |bytes| frames << bytes }
|
29
|
+
@client.ping('12345678')
|
30
|
+
|
31
|
+
frame = f.parse(frames[1])
|
32
|
+
expect(frame[:type]).to eq :settings
|
33
|
+
expect(frame[:payload]).to include([:settings_max_concurrent_streams, 200])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'push' do
|
38
|
+
it 'should disallow client initiated push' do
|
39
|
+
expect do
|
40
|
+
@client.promise({}) {}
|
41
|
+
end.to raise_error(NoMethodError)
|
42
|
+
end
|
43
|
+
|
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
|
+
end
|
49
|
+
|
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
|
+
end
|
55
|
+
|
56
|
+
it 'should raise error on PUSH_PROMISE against non-idle stream' do
|
57
|
+
expect do
|
58
|
+
s = @client.new_stream
|
59
|
+
s.send HEADERS
|
60
|
+
|
61
|
+
@client << set_stream_id(f.generate(PUSH_PROMISE), s.id)
|
62
|
+
@client << set_stream_id(f.generate(PUSH_PROMISE), s.id)
|
63
|
+
end.to raise_error(ProtocolError)
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'should emit stream object for received PUSH_PROMISE' do
|
67
|
+
s = @client.new_stream
|
68
|
+
s.send HEADERS.deep_dup
|
69
|
+
|
70
|
+
promise = nil
|
71
|
+
@client.on(:promise) { |stream| promise = stream }
|
72
|
+
@client << set_stream_id(f.generate(PUSH_PROMISE.dup), s.id)
|
73
|
+
|
74
|
+
expect(promise.id).to eq 2
|
75
|
+
expect(promise.state).to eq :reserved_remote
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'should auto RST_STREAM promises against locally-RST stream' do
|
79
|
+
s = @client.new_stream
|
80
|
+
s.send HEADERS.deep_dup
|
81
|
+
s.close
|
82
|
+
|
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
|
+
end
|
88
|
+
|
89
|
+
@client << set_stream_id(f.generate(PUSH_PROMISE.dup), s.id)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|