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
@@ -0,0 +1,191 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
require 'http_parser'
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
options = { port: 8080 }
|
6
|
+
OptionParser.new do |opts|
|
7
|
+
opts.banner = 'Usage: server.rb [options]'
|
8
|
+
|
9
|
+
opts.on('-s', '--secure', 'HTTPS mode') do |v|
|
10
|
+
options[:secure] = v
|
11
|
+
end
|
12
|
+
|
13
|
+
opts.on('-p', '--port [Integer]', 'listen port') do |v|
|
14
|
+
options[:port] = v
|
15
|
+
end
|
16
|
+
end.parse!
|
17
|
+
|
18
|
+
puts "Starting server on port #{options[:port]}"
|
19
|
+
server = TCPServer.new(options[:port])
|
20
|
+
|
21
|
+
if options[:secure]
|
22
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
23
|
+
ctx.cert = OpenSSL::X509::Certificate.new(File.open('keys/mycert.pem'))
|
24
|
+
ctx.key = OpenSSL::PKey::RSA.new(File.open('keys/mykey.pem'))
|
25
|
+
ctx.npn_protocols = [DRAFT]
|
26
|
+
|
27
|
+
server = OpenSSL::SSL::SSLServer.new(server, ctx)
|
28
|
+
end
|
29
|
+
|
30
|
+
class UpgradeHandler
|
31
|
+
|
32
|
+
VALID_UPGRADE_METHODS = %w[GET OPTIONS]
|
33
|
+
UPGRADE_RESPONSE = ("HTTP/1.1 101 Switching Protocols\n" +
|
34
|
+
"Connection: Upgrade\n" +
|
35
|
+
"Upgrade: h2c\n\n").freeze
|
36
|
+
|
37
|
+
attr_reader :complete, :headers, :body, :parsing
|
38
|
+
|
39
|
+
def initialize conn, sock
|
40
|
+
@conn, @sock = conn, sock
|
41
|
+
@complete, @parsing = false, false
|
42
|
+
@body = ''
|
43
|
+
@parser = ::HTTP::Parser.new(self)
|
44
|
+
end
|
45
|
+
|
46
|
+
def <<(data)
|
47
|
+
@parsing ||= true
|
48
|
+
@parser << data
|
49
|
+
if complete
|
50
|
+
|
51
|
+
@sock.write UPGRADE_RESPONSE
|
52
|
+
|
53
|
+
settings = headers['http2-settings']
|
54
|
+
request = {
|
55
|
+
':scheme' => 'http',
|
56
|
+
':method' => @parser.http_method,
|
57
|
+
':authority' => headers['Host'],
|
58
|
+
':path' => @parser.request_url
|
59
|
+
}.merge(headers)
|
60
|
+
|
61
|
+
@conn.upgrade(settings, request, @body)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def complete!; @complete = true; end
|
66
|
+
|
67
|
+
def on_headers_complete(headers)
|
68
|
+
@headers = headers
|
69
|
+
end
|
70
|
+
|
71
|
+
def on_body(chunk)
|
72
|
+
@body << chunk
|
73
|
+
end
|
74
|
+
|
75
|
+
def on_message_complete
|
76
|
+
raise unless VALID_UPGRADE_METHODS.include?(@parser.http_method)
|
77
|
+
@parsing = false
|
78
|
+
complete!
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
loop do
|
84
|
+
sock = server.accept
|
85
|
+
puts 'New TCP connection!'
|
86
|
+
|
87
|
+
conn = HTTP2::Server.new
|
88
|
+
conn.on(:frame) do |bytes|
|
89
|
+
# puts "Writing bytes: #{bytes.unpack("H*").first}"
|
90
|
+
sock.write bytes
|
91
|
+
end
|
92
|
+
conn.on(:frame_sent) do |frame|
|
93
|
+
puts "Sent frame: #{frame.inspect}"
|
94
|
+
end
|
95
|
+
conn.on(:frame_received) do |frame|
|
96
|
+
puts "Received frame: #{frame.inspect}"
|
97
|
+
end
|
98
|
+
|
99
|
+
conn.on(:stream) do |stream|
|
100
|
+
log = Logger.new(stream.id)
|
101
|
+
req, buffer = {}, ''
|
102
|
+
|
103
|
+
stream.on(:active) { log.info 'client opened new stream' }
|
104
|
+
stream.on(:close) do
|
105
|
+
log.info 'stream closed'
|
106
|
+
end
|
107
|
+
|
108
|
+
stream.on(:headers) do |h|
|
109
|
+
req = Hash[*h.flatten]
|
110
|
+
log.info "request headers: #{h}"
|
111
|
+
end
|
112
|
+
|
113
|
+
stream.on(:data) do |d|
|
114
|
+
log.info "payload chunk: <<#{d}>>"
|
115
|
+
buffer << d
|
116
|
+
end
|
117
|
+
|
118
|
+
stream.on(:half_close) do
|
119
|
+
log.info 'client closed its end of the stream'
|
120
|
+
|
121
|
+
if req['Upgrade']
|
122
|
+
log.info "Processing h2c Upgrade request: #{req}"
|
123
|
+
|
124
|
+
# Don't respond to OPTIONS...
|
125
|
+
if req[':method'] != "OPTIONS"
|
126
|
+
response = 'Hello h2c world!'
|
127
|
+
stream.headers({
|
128
|
+
':status' => '200',
|
129
|
+
'content-length' => response.bytesize.to_s,
|
130
|
+
'content-type' => 'text/plain',
|
131
|
+
}, end_stream: false)
|
132
|
+
stream.data(response)
|
133
|
+
end
|
134
|
+
else
|
135
|
+
|
136
|
+
response = nil
|
137
|
+
if req[':method'] == 'POST'
|
138
|
+
log.info "Received POST request, payload: #{buffer}"
|
139
|
+
response = "Hello HTTP 2.0! POST payload: #{buffer}"
|
140
|
+
else
|
141
|
+
log.info 'Received GET request'
|
142
|
+
response = 'Hello HTTP 2.0! GET request'
|
143
|
+
end
|
144
|
+
|
145
|
+
stream.headers({
|
146
|
+
':status' => '200',
|
147
|
+
'content-length' => response.bytesize.to_s,
|
148
|
+
'content-type' => 'text/plain',
|
149
|
+
}, end_stream: false)
|
150
|
+
|
151
|
+
# split response into multiple DATA frames
|
152
|
+
stream.data(response.slice!(0, 5), end_stream: false)
|
153
|
+
stream.data(response)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
uh = UpgradeHandler.new(conn, sock)
|
159
|
+
|
160
|
+
while !sock.closed? && !(sock.eof? rescue true) # rubocop:disable Style/RescueModifier
|
161
|
+
data = sock.readpartial(1024)
|
162
|
+
# puts "Received bytes: #{data.unpack("H*").first}"
|
163
|
+
|
164
|
+
begin
|
165
|
+
case
|
166
|
+
when !uh.parsing && !uh.complete
|
167
|
+
|
168
|
+
if data.start_with?(*UpgradeHandler::VALID_UPGRADE_METHODS)
|
169
|
+
uh << data
|
170
|
+
else
|
171
|
+
uh.complete!
|
172
|
+
conn << data
|
173
|
+
end
|
174
|
+
|
175
|
+
when uh.parsing && !uh.complete
|
176
|
+
uh << data
|
177
|
+
|
178
|
+
when uh.complete
|
179
|
+
conn << data
|
180
|
+
end
|
181
|
+
|
182
|
+
rescue => e
|
183
|
+
puts "Exception: #{e}, #{e.message} - closing socket."
|
184
|
+
puts e.backtrace.last(10).join("\n")
|
185
|
+
sock.close
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# echo foo=bar | nghttp -d - -t 0 -vu http://127.0.0.1:8080/
|
191
|
+
# nghttp -vu http://127.0.0.1:8080/
|
data/http-2.gemspec
CHANGED
@@ -4,19 +4,20 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'http/2/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
7
|
+
spec.name = 'http-2'
|
8
8
|
spec.version = HTTP2::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
11
|
-
spec.description =
|
9
|
+
spec.authors = ['Ilya Grigorik', 'Kaoru Maeda']
|
10
|
+
spec.email = ['ilya@igvita.com']
|
11
|
+
spec.description = 'Pure-ruby HTTP 2.0 protocol implementation'
|
12
12
|
spec.summary = spec.description
|
13
|
-
spec.homepage =
|
14
|
-
spec.license =
|
13
|
+
spec.homepage = 'https://github.com/igrigorik/http-2'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
spec.required_ruby_version = '>=2.0.0'
|
15
16
|
|
16
|
-
spec.files = `git ls-files`.split(
|
17
|
+
spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
17
18
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
19
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
-
spec.require_paths = [
|
20
|
+
spec.require_paths = ['lib']
|
20
21
|
|
21
|
-
spec.add_development_dependency
|
22
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
22
23
|
end
|
data/lib/http/2.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
10
|
-
require
|
11
|
-
require
|
12
|
-
require
|
13
|
-
require
|
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'
|
data/lib/http/2/buffer.rb
CHANGED
@@ -1,27 +1,24 @@
|
|
1
1
|
module HTTP2
|
2
|
-
|
3
2
|
# Simple binary buffer backed by string.
|
4
3
|
#
|
5
4
|
class Buffer < String
|
6
|
-
|
7
|
-
UINT32
|
8
|
-
BINARY = "binary"
|
9
|
-
private_constant :UINT32, :BINARY
|
5
|
+
UINT32 = 'N'.freeze
|
6
|
+
private_constant :UINT32
|
10
7
|
|
11
8
|
# Forces binary encoding on the string
|
12
|
-
def initialize(
|
13
|
-
super
|
9
|
+
def initialize(*)
|
10
|
+
super.force_encoding(Encoding::BINARY)
|
14
11
|
end
|
15
12
|
|
16
13
|
# Emulate StringIO#read: slice first n bytes from the buffer.
|
17
14
|
#
|
18
15
|
# @param n [Integer] number of bytes to slice from the buffer
|
19
16
|
def read(n)
|
20
|
-
Buffer.new(slice!(0,n))
|
17
|
+
Buffer.new(slice!(0, n))
|
21
18
|
end
|
22
19
|
|
23
20
|
# Alias getbyte to readbyte
|
24
|
-
|
21
|
+
alias_method :readbyte, :getbyte
|
25
22
|
|
26
23
|
# Emulate StringIO#getbyte: slice first byte from buffer.
|
27
24
|
def getbyte
|
@@ -34,5 +31,4 @@ module HTTP2
|
|
34
31
|
read(4).unpack(UINT32).first
|
35
32
|
end
|
36
33
|
end
|
37
|
-
|
38
34
|
end
|
data/lib/http/2/client.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
module HTTP2
|
2
|
-
|
3
2
|
# HTTP 2.0 client connection class that implements appropriate header
|
4
3
|
# compression / decompression algorithms and stream management logic.
|
5
4
|
#
|
@@ -18,7 +17,6 @@ module HTTP2
|
|
18
17
|
# end
|
19
18
|
#
|
20
19
|
class Client < Connection
|
21
|
-
|
22
20
|
# Initialize new HTTP 2.0 client object.
|
23
21
|
def initialize(**settings)
|
24
22
|
@stream_id = 1
|
@@ -42,15 +40,12 @@ module HTTP2
|
|
42
40
|
|
43
41
|
# Emit the connection preface if not yet
|
44
42
|
def send_connection_preface
|
45
|
-
|
46
|
-
|
47
|
-
|
43
|
+
return unless @state == :waiting_connection_preface
|
44
|
+
@state = :connected
|
45
|
+
emit(:frame, CONNECTION_PREFACE_MAGIC)
|
48
46
|
|
49
|
-
|
50
|
-
|
51
|
-
end
|
47
|
+
payload = @local_settings.select { |k, v| v != SPEC_DEFAULT_CONNECTION_SETTINGS[k] }
|
48
|
+
settings(payload)
|
52
49
|
end
|
53
|
-
|
54
50
|
end
|
55
|
-
|
56
51
|
end
|
data/lib/http/2/compressor.rb
CHANGED
@@ -1,101 +1,97 @@
|
|
1
1
|
module HTTP2
|
2
|
-
|
3
2
|
# Implementation of header compression for HTTP 2.0 (HPACK) format adapted
|
4
3
|
# to efficiently represent HTTP headers in the context of HTTP 2.0.
|
5
4
|
#
|
6
|
-
# - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-
|
5
|
+
# - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10
|
7
6
|
module Header
|
8
|
-
|
9
|
-
BINARY = 'binary'
|
10
|
-
|
11
7
|
# To decompress header blocks, a decoder only needs to maintain a
|
12
|
-
#
|
8
|
+
# dynamic table as a decoding context.
|
13
9
|
# No other state information is needed.
|
14
10
|
class EncodingContext
|
15
11
|
include Error
|
16
12
|
|
17
13
|
# @private
|
18
14
|
# Static table
|
19
|
-
# - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-
|
15
|
+
# - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#appendix-A
|
20
16
|
STATIC_TABLE = [
|
21
|
-
[':authority', ''
|
22
|
-
[':method', 'GET'
|
23
|
-
[':method', 'POST'
|
24
|
-
[':path', '/'
|
25
|
-
[':path', '/index.html'
|
26
|
-
[':scheme', 'http'
|
27
|
-
[':scheme', 'https'
|
28
|
-
[':status', '200'
|
29
|
-
[':status', '204'
|
30
|
-
[':status', '206'
|
31
|
-
[':status', '304'
|
32
|
-
[':status', '400'
|
33
|
-
[':status', '404'
|
34
|
-
[':status', '500'
|
35
|
-
['accept-charset', ''
|
36
|
-
['accept-encoding', 'gzip, deflate'
|
37
|
-
['accept-language', ''
|
38
|
-
['accept-ranges', ''
|
39
|
-
['accept', ''
|
40
|
-
['access-control-allow-origin', ''
|
41
|
-
['age', ''
|
42
|
-
['allow', ''
|
43
|
-
['authorization', ''
|
44
|
-
['cache-control', ''
|
45
|
-
['content-disposition', ''
|
46
|
-
['content-encoding', ''
|
47
|
-
['content-language', ''
|
48
|
-
['content-length', ''
|
49
|
-
['content-location', ''
|
50
|
-
['content-range', ''
|
51
|
-
['content-type', ''
|
52
|
-
['cookie', ''
|
53
|
-
['date', ''
|
54
|
-
['etag', ''
|
55
|
-
['expect', ''
|
56
|
-
['expires', ''
|
57
|
-
['from', ''
|
58
|
-
['host', ''
|
59
|
-
['if-match', ''
|
60
|
-
['if-modified-since', ''
|
61
|
-
['if-none-match', ''
|
62
|
-
['if-range', ''
|
63
|
-
['if-unmodified-since', ''
|
64
|
-
['last-modified', ''
|
65
|
-
['link', ''
|
66
|
-
['location', ''
|
67
|
-
['max-forwards', ''
|
68
|
-
['proxy-authenticate', ''
|
69
|
-
['proxy-authorization', ''
|
70
|
-
['range', ''
|
71
|
-
['referer', ''
|
72
|
-
['refresh', ''
|
73
|
-
['retry-after', ''
|
74
|
-
['server', ''
|
75
|
-
['set-cookie', ''
|
76
|
-
['strict-transport-security', ''
|
77
|
-
['transfer-encoding', ''
|
78
|
-
['user-agent', ''
|
79
|
-
['vary', ''
|
80
|
-
['via', ''
|
81
|
-
['www-authenticate', ''
|
82
|
-
].freeze
|
17
|
+
[':authority', ''],
|
18
|
+
[':method', 'GET'],
|
19
|
+
[':method', 'POST'],
|
20
|
+
[':path', '/'],
|
21
|
+
[':path', '/index.html'],
|
22
|
+
[':scheme', 'http'],
|
23
|
+
[':scheme', 'https'],
|
24
|
+
[':status', '200'],
|
25
|
+
[':status', '204'],
|
26
|
+
[':status', '206'],
|
27
|
+
[':status', '304'],
|
28
|
+
[':status', '400'],
|
29
|
+
[':status', '404'],
|
30
|
+
[':status', '500'],
|
31
|
+
['accept-charset', ''],
|
32
|
+
['accept-encoding', 'gzip, deflate'],
|
33
|
+
['accept-language', ''],
|
34
|
+
['accept-ranges', ''],
|
35
|
+
['accept', ''],
|
36
|
+
['access-control-allow-origin', ''],
|
37
|
+
['age', ''],
|
38
|
+
['allow', ''],
|
39
|
+
['authorization', ''],
|
40
|
+
['cache-control', ''],
|
41
|
+
['content-disposition', ''],
|
42
|
+
['content-encoding', ''],
|
43
|
+
['content-language', ''],
|
44
|
+
['content-length', ''],
|
45
|
+
['content-location', ''],
|
46
|
+
['content-range', ''],
|
47
|
+
['content-type', ''],
|
48
|
+
['cookie', ''],
|
49
|
+
['date', ''],
|
50
|
+
['etag', ''],
|
51
|
+
['expect', ''],
|
52
|
+
['expires', ''],
|
53
|
+
['from', ''],
|
54
|
+
['host', ''],
|
55
|
+
['if-match', ''],
|
56
|
+
['if-modified-since', ''],
|
57
|
+
['if-none-match', ''],
|
58
|
+
['if-range', ''],
|
59
|
+
['if-unmodified-since', ''],
|
60
|
+
['last-modified', ''],
|
61
|
+
['link', ''],
|
62
|
+
['location', ''],
|
63
|
+
['max-forwards', ''],
|
64
|
+
['proxy-authenticate', ''],
|
65
|
+
['proxy-authorization', ''],
|
66
|
+
['range', ''],
|
67
|
+
['referer', ''],
|
68
|
+
['refresh', ''],
|
69
|
+
['retry-after', ''],
|
70
|
+
['server', ''],
|
71
|
+
['set-cookie', ''],
|
72
|
+
['strict-transport-security', ''],
|
73
|
+
['transfer-encoding', ''],
|
74
|
+
['user-agent', ''],
|
75
|
+
['vary', ''],
|
76
|
+
['via', ''],
|
77
|
+
['www-authenticate', ''],
|
78
|
+
].each { |pair| pair.each(&:freeze).freeze }.freeze
|
83
79
|
|
84
80
|
# Current table of header key-value pairs.
|
85
81
|
attr_reader :table
|
86
82
|
|
87
83
|
# Current encoding options
|
88
84
|
#
|
89
|
-
# :table_size Integer maximum
|
85
|
+
# :table_size Integer maximum dynamic table size in bytes
|
90
86
|
# :huffman Symbol :always, :never, :shorter
|
91
87
|
# :index Symbol :all, :static, :never
|
92
88
|
attr_reader :options
|
93
89
|
|
94
90
|
# Initializes compression context with appropriate client/server
|
95
|
-
# defaults and maximum size of the
|
91
|
+
# defaults and maximum size of the dynamic table.
|
96
92
|
#
|
97
93
|
# @param options [Hash] encoding options
|
98
|
-
# :table_size Integer maximum
|
94
|
+
# :table_size Integer maximum dynamic table size in bytes
|
99
95
|
# :huffman Symbol :always, :never, :shorter
|
100
96
|
# :index Symbol :all, :static, :never
|
101
97
|
def initialize(**options)
|
@@ -115,32 +111,32 @@ module HTTP2
|
|
115
111
|
other = EncodingContext.new(@options)
|
116
112
|
t = @table
|
117
113
|
l = @limit
|
118
|
-
other.instance_eval
|
114
|
+
other.instance_eval do
|
119
115
|
@table = t.dup # shallow copy
|
120
116
|
@limit = l
|
121
|
-
|
117
|
+
end
|
122
118
|
other
|
123
119
|
end
|
124
120
|
|
125
|
-
# Finds an entry in current
|
121
|
+
# Finds an entry in current dynamic table by index.
|
126
122
|
# Note that index is zero-based in this module.
|
127
123
|
#
|
128
124
|
# If the index is greater than the last index in the static table,
|
129
|
-
# an entry in the
|
125
|
+
# an entry in the dynamic table is dereferenced.
|
130
126
|
#
|
131
127
|
# If the index is greater than the last header index, an error is raised.
|
132
128
|
#
|
133
|
-
# @param index [Integer] zero-based index in the
|
129
|
+
# @param index [Integer] zero-based index in the dynamic table.
|
134
130
|
# @return [Array] +[key, value]+
|
135
131
|
def dereference(index)
|
136
132
|
# NOTE: index is zero-based in this module.
|
137
|
-
STATIC_TABLE[index]
|
138
|
-
|
139
|
-
|
133
|
+
value = STATIC_TABLE[index] || @table[index - STATIC_TABLE.size]
|
134
|
+
fail CompressionError, 'Index too large' unless value
|
135
|
+
value
|
140
136
|
end
|
141
137
|
|
142
138
|
# Header Block Processing
|
143
|
-
# - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-
|
139
|
+
# - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-4.1
|
144
140
|
#
|
145
141
|
# @param cmd [Hash] { type:, name:, value:, index: }
|
146
142
|
# @return [Array] +[name, value]+ header field that is added to the decoded header list
|
@@ -149,13 +145,13 @@ module HTTP2
|
|
149
145
|
|
150
146
|
case cmd[:type]
|
151
147
|
when :changetablesize
|
152
|
-
|
148
|
+
self.table_size = cmd[:value]
|
153
149
|
|
154
150
|
when :indexed
|
155
151
|
# Indexed Representation
|
156
152
|
# An _indexed representation_ entails the following actions:
|
157
153
|
# o The header field corresponding to the referenced entry in either
|
158
|
-
# the static table or
|
154
|
+
# the static table or dynamic table is added to the decoded header
|
159
155
|
# list.
|
160
156
|
idx = cmd[:name]
|
161
157
|
|
@@ -163,14 +159,14 @@ module HTTP2
|
|
163
159
|
emit = [k, v]
|
164
160
|
|
165
161
|
when :incremental, :noindex, :neverindexed
|
166
|
-
# A _literal representation_ that is _not added_ to the
|
162
|
+
# A _literal representation_ that is _not added_ to the dynamic table
|
167
163
|
# entails the following action:
|
168
164
|
# o The header field is added to the decoded header list.
|
169
165
|
|
170
|
-
# A _literal representation_ that is _added_ to the
|
166
|
+
# A _literal representation_ that is _added_ to the dynamic table
|
171
167
|
# entails the following actions:
|
172
168
|
# o The header field is added to the decoded header list.
|
173
|
-
# o The header field is inserted at the beginning of the
|
169
|
+
# o The header field is inserted at the beginning of the dynamic table.
|
174
170
|
|
175
171
|
if cmd[:name].is_a? Integer
|
176
172
|
k, v = dereference(cmd[:name])
|
@@ -183,19 +179,17 @@ module HTTP2
|
|
183
179
|
|
184
180
|
emit = [cmd[:name], cmd[:value]]
|
185
181
|
|
186
|
-
if cmd[:type] == :incremental
|
187
|
-
add_to_table(emit)
|
188
|
-
end
|
182
|
+
add_to_table(emit) if cmd[:type] == :incremental
|
189
183
|
|
190
184
|
else
|
191
|
-
|
185
|
+
fail CompressionError, "Invalid type: #{cmd[:type]}"
|
192
186
|
end
|
193
187
|
|
194
188
|
emit
|
195
189
|
end
|
196
190
|
|
197
191
|
# Plan header compression according to +@options [:index]+
|
198
|
-
# :never Do not use
|
192
|
+
# :never Do not use dynamic table or static table reference at all.
|
199
193
|
# :static Use static table only.
|
200
194
|
# :all Use all of them.
|
201
195
|
#
|
@@ -207,9 +201,7 @@ module HTTP2
|
|
207
201
|
noindex = [:static, :never].include?(@options[:index])
|
208
202
|
headers.each do |h|
|
209
203
|
cmd = addcmd(h)
|
210
|
-
if noindex && cmd[:type] == :incremental
|
211
|
-
cmd[:type] = :noindex
|
212
|
-
end
|
204
|
+
cmd[:type] = :noindex if noindex && cmd[:type] == :incremental
|
213
205
|
commands << cmd
|
214
206
|
process(cmd)
|
215
207
|
end
|
@@ -217,12 +209,12 @@ module HTTP2
|
|
217
209
|
end
|
218
210
|
|
219
211
|
# Emits command for a header.
|
220
|
-
# Prefer static table over
|
212
|
+
# Prefer static table over dynamic table.
|
221
213
|
# Prefer exact match over name-only match.
|
222
214
|
#
|
223
|
-
# +@options [:index]+ controls whether to use the
|
215
|
+
# +@options [:index]+ controls whether to use the dynamic table,
|
224
216
|
# static table, or both.
|
225
|
-
# :never Do not use
|
217
|
+
# :never Do not use dynamic table or static table reference at all.
|
226
218
|
# :static Use static table only.
|
227
219
|
# :all Use all of them.
|
228
220
|
#
|
@@ -262,9 +254,9 @@ module HTTP2
|
|
262
254
|
end
|
263
255
|
end
|
264
256
|
|
265
|
-
# Alter
|
257
|
+
# Alter dynamic table size.
|
266
258
|
# When the size is reduced, some headers might be evicted.
|
267
|
-
def
|
259
|
+
def table_size=(size)
|
268
260
|
@limit = size
|
269
261
|
size_check(nil)
|
270
262
|
end
|
@@ -272,51 +264,49 @@ module HTTP2
|
|
272
264
|
# Returns current table size in octets
|
273
265
|
# @return [Integer]
|
274
266
|
def current_table_size
|
275
|
-
@table.inject(0){|r,(k,v)| r
|
267
|
+
@table.inject(0) { |r, (k, v)| r + k.bytesize + v.bytesize + 32 }
|
276
268
|
end
|
277
269
|
|
278
270
|
private
|
279
271
|
|
280
|
-
# Add a name-value pair to the
|
272
|
+
# Add a name-value pair to the dynamic table.
|
281
273
|
# Older entries might have been evicted so that
|
282
|
-
# the new entry fits in the
|
274
|
+
# the new entry fits in the dynamic table.
|
283
275
|
#
|
284
276
|
# @param cmd [Array] +[name, value]+
|
285
277
|
def add_to_table(cmd)
|
286
|
-
|
287
|
-
|
288
|
-
end
|
278
|
+
return unless size_check(cmd)
|
279
|
+
@table.unshift(cmd)
|
289
280
|
end
|
290
281
|
|
291
|
-
# To keep the
|
292
|
-
# remove one or more entries at the end of the
|
282
|
+
# To keep the dynamic table size lower than or equal to @limit,
|
283
|
+
# remove one or more entries at the end of the dynamic table.
|
293
284
|
#
|
294
285
|
# @param cmd [Hash]
|
295
|
-
# @return [Boolean] whether +cmd+ fits in the
|
286
|
+
# @return [Boolean] whether +cmd+ fits in the dynamic table.
|
296
287
|
def size_check(cmd)
|
297
288
|
cursize = current_table_size
|
298
289
|
cmdsize = cmd.nil? ? 0 : cmd[0].bytesize + cmd[1].bytesize + 32
|
299
290
|
|
300
|
-
while cursize + cmdsize > @limit
|
291
|
+
while cursize + cmdsize > @limit
|
301
292
|
break if @table.empty?
|
302
293
|
|
303
|
-
last_index = @table.size - 1
|
304
294
|
e = @table.pop
|
305
295
|
cursize -= e[0].bytesize + e[1].bytesize + 32
|
306
296
|
end
|
307
297
|
|
308
|
-
|
298
|
+
cmdsize <= @limit
|
309
299
|
end
|
310
300
|
end
|
311
301
|
|
312
302
|
# Header representation as defined by the spec.
|
313
303
|
HEADREP = {
|
314
|
-
indexed: {prefix: 7, pattern: 0x80},
|
315
|
-
incremental: {prefix: 6, pattern: 0x40},
|
316
|
-
noindex: {prefix: 4, pattern: 0x00},
|
317
|
-
neverindexed: {prefix: 4, pattern: 0x10},
|
318
|
-
changetablesize: {prefix: 5, pattern: 0x20},
|
319
|
-
}
|
304
|
+
indexed: { prefix: 7, pattern: 0x80 },
|
305
|
+
incremental: { prefix: 6, pattern: 0x40 },
|
306
|
+
noindex: { prefix: 4, pattern: 0x00 },
|
307
|
+
neverindexed: { prefix: 4, pattern: 0x10 },
|
308
|
+
changetablesize: { prefix: 5, pattern: 0x20 },
|
309
|
+
}.each_value(&:freeze).freeze
|
320
310
|
|
321
311
|
# Predefined options set for Compressor
|
322
312
|
# http://mew.org/~kazu/material/2014-hpack.pdf
|
@@ -336,14 +326,14 @@ module HTTP2
|
|
336
326
|
@cc = EncodingContext.new(options)
|
337
327
|
end
|
338
328
|
|
339
|
-
# Set
|
340
|
-
# @param size [Integer] new
|
341
|
-
def
|
342
|
-
@cc.
|
329
|
+
# Set dynamic table size in EncodingContext
|
330
|
+
# @param size [Integer] new dynamic table size
|
331
|
+
def table_size=(size)
|
332
|
+
@cc.table_size = size
|
343
333
|
end
|
344
334
|
|
345
335
|
# Encodes provided value via integer representation.
|
346
|
-
# - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-
|
336
|
+
# - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-5.1
|
347
337
|
#
|
348
338
|
# If I < 2^N - 1, encode I on N bits
|
349
339
|
# Else
|
@@ -359,15 +349,15 @@ module HTTP2
|
|
359
349
|
# @return [String] binary string
|
360
350
|
def integer(i, n)
|
361
351
|
limit = 2**n - 1
|
362
|
-
return [i].pack('C') if
|
352
|
+
return [i].pack('C') if i < limit
|
363
353
|
|
364
354
|
bytes = []
|
365
|
-
bytes.push limit
|
355
|
+
bytes.push limit unless n.zero?
|
366
356
|
|
367
357
|
i -= limit
|
368
|
-
while (i >= 128)
|
358
|
+
while (i >= 128)
|
369
359
|
bytes.push((i % 128) + 128)
|
370
|
-
i
|
360
|
+
i /= 128
|
371
361
|
end
|
372
362
|
|
373
363
|
bytes.push i
|
@@ -375,7 +365,7 @@ module HTTP2
|
|
375
365
|
end
|
376
366
|
|
377
367
|
# Encodes provided value via string literal representation.
|
378
|
-
# - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-
|
368
|
+
# - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-5.2
|
379
369
|
#
|
380
370
|
# * The string length, defined as the number of bytes needed to store
|
381
371
|
# its UTF-8 representation, is represented as an integer with a seven
|
@@ -397,7 +387,7 @@ module HTTP2
|
|
397
387
|
def string(str)
|
398
388
|
plain, huffman = nil, nil
|
399
389
|
unless @cc.options[:huffman] == :always
|
400
|
-
plain = integer(str.bytesize, 7) << str.dup.force_encoding(BINARY)
|
390
|
+
plain = integer(str.bytesize, 7) << str.dup.force_encoding(Encoding::BINARY)
|
401
391
|
end
|
402
392
|
unless @cc.options[:huffman] == :never
|
403
393
|
huffman = Huffman.new.encode(str)
|
@@ -424,12 +414,12 @@ module HTTP2
|
|
424
414
|
|
425
415
|
case h[:type]
|
426
416
|
when :indexed
|
427
|
-
buffer << integer(h[:name]+1, rep[:prefix])
|
417
|
+
buffer << integer(h[:name] + 1, rep[:prefix])
|
428
418
|
when :changetablesize
|
429
419
|
buffer << integer(h[:value], rep[:prefix])
|
430
420
|
else
|
431
421
|
if h[:name].is_a? Integer
|
432
|
-
buffer << integer(h[:name]+1, rep[:prefix])
|
422
|
+
buffer << integer(h[:name] + 1, rep[:prefix])
|
433
423
|
else
|
434
424
|
buffer << integer(0, rep[:prefix])
|
435
425
|
buffer << string(h[:name])
|
@@ -454,7 +444,7 @@ module HTTP2
|
|
454
444
|
|
455
445
|
# Literal header names MUST be translated to lowercase before
|
456
446
|
# encoding and transmission.
|
457
|
-
headers.map! {|hk,hv| [hk.downcase, hv] }
|
447
|
+
headers.map! { |hk, hv| [hk.downcase, hv] }
|
458
448
|
|
459
449
|
commands = @cc.encode(headers)
|
460
450
|
commands.each do |cmd|
|
@@ -478,10 +468,10 @@ module HTTP2
|
|
478
468
|
@cc = EncodingContext.new(options)
|
479
469
|
end
|
480
470
|
|
481
|
-
# Set
|
482
|
-
# @param size [Integer] new
|
483
|
-
def
|
484
|
-
@cc.
|
471
|
+
# Set dynamic table size in EncodingContext
|
472
|
+
# @param size [Integer] new dynamic table size
|
473
|
+
def table_size=(size)
|
474
|
+
@cc.table_size = size
|
485
475
|
end
|
486
476
|
|
487
477
|
# Decodes integer value from provided buffer.
|
@@ -494,7 +484,7 @@ module HTTP2
|
|
494
484
|
i = !n.zero? ? (buf.getbyte & limit) : 0
|
495
485
|
|
496
486
|
m = 0
|
497
|
-
while byte = buf.getbyte
|
487
|
+
while (byte = buf.getbyte)
|
498
488
|
i += ((byte & 127) << m)
|
499
489
|
m += 7
|
500
490
|
|
@@ -513,10 +503,9 @@ module HTTP2
|
|
513
503
|
huffman = (buf.readbyte(0) & 0x80) == 0x80
|
514
504
|
len = integer(buf, 7)
|
515
505
|
str = buf.read(len)
|
516
|
-
|
517
|
-
|
518
|
-
str
|
519
|
-
str
|
506
|
+
fail CompressionError, 'string too short' unless str.bytesize == len
|
507
|
+
str = Huffman.new.decode(Buffer.new(str)) if huffman
|
508
|
+
str.force_encoding(Encoding::UTF_8)
|
520
509
|
end
|
521
510
|
|
522
511
|
# Decodes header command from provided buffer.
|
@@ -527,18 +516,18 @@ module HTTP2
|
|
527
516
|
peek = buf.readbyte(0)
|
528
517
|
|
529
518
|
header = {}
|
530
|
-
header[:type], type = HEADREP.
|
519
|
+
header[:type], type = HEADREP.find do |_t, desc|
|
531
520
|
mask = (peek >> desc[:prefix]) << desc[:prefix]
|
532
521
|
mask == desc[:pattern]
|
533
|
-
end
|
522
|
+
end
|
534
523
|
|
535
|
-
header[:type]
|
524
|
+
fail CompressionError unless header[:type]
|
536
525
|
|
537
526
|
header[:name] = integer(buf, type[:prefix])
|
538
527
|
|
539
528
|
case header[:type]
|
540
529
|
when :indexed
|
541
|
-
header[:name] == 0
|
530
|
+
fail CompressionError if header[:name] == 0
|
542
531
|
header[:name] -= 1
|
543
532
|
when :changetablesize
|
544
533
|
header[:value] = header[:name]
|
@@ -560,10 +549,9 @@ module HTTP2
|
|
560
549
|
# @return [Array] +[[name, value], ...]+
|
561
550
|
def decode(buf)
|
562
551
|
list = []
|
563
|
-
list << @cc.process(header(buf))
|
552
|
+
list << @cc.process(header(buf)) until buf.empty?
|
564
553
|
list.compact
|
565
554
|
end
|
566
555
|
end
|
567
|
-
|
568
556
|
end
|
569
557
|
end
|