mieps_http-2 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|