http-2 0.11.0 → 1.0.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/README.md +10 -9
- data/lib/http/2/base64.rb +45 -0
- data/lib/http/2/client.rb +19 -6
- data/lib/http/2/connection.rb +235 -163
- data/lib/http/2/emitter.rb +7 -5
- data/lib/http/2/error.rb +24 -1
- data/lib/http/2/extensions.rb +53 -0
- data/lib/http/2/flow_buffer.rb +91 -33
- data/lib/http/2/framer.rb +184 -157
- data/lib/http/2/header/compressor.rb +157 -0
- data/lib/http/2/header/decompressor.rb +144 -0
- data/lib/http/2/header/encoding_context.rb +337 -0
- data/lib/http/2/{huffman.rb → header/huffman.rb} +25 -19
- data/lib/http/2/{huffman_statemachine.rb → header/huffman_statemachine.rb} +2 -0
- data/lib/http/2/header.rb +35 -0
- data/lib/http/2/server.rb +47 -20
- data/lib/http/2/stream.rb +130 -61
- data/lib/http/2/version.rb +3 -1
- data/lib/http/2.rb +14 -13
- data/sig/client.rbs +9 -0
- data/sig/connection.rbs +93 -0
- data/sig/emitter.rbs +13 -0
- data/sig/error.rbs +35 -0
- data/sig/extensions.rbs +5 -0
- data/sig/flow_buffer.rbs +21 -0
- data/sig/frame_buffer.rbs +13 -0
- data/sig/framer.rbs +54 -0
- data/sig/header/compressor.rbs +27 -0
- data/sig/header/decompressor.rbs +22 -0
- data/sig/header/encoding_context.rbs +34 -0
- data/sig/header/huffman.rbs +9 -0
- data/sig/header.rbs +27 -0
- data/sig/next.rbs +101 -0
- data/sig/server.rbs +12 -0
- data/sig/stream.rbs +91 -0
- metadata +38 -79
- data/.autotest +0 -20
- data/.coveralls.yml +0 -1
- data/.gitignore +0 -20
- data/.gitmodules +0 -3
- data/.rspec +0 -5
- data/.rubocop.yml +0 -93
- data/.rubocop_todo.yml +0 -131
- data/.travis.yml +0 -17
- data/Gemfile +0 -16
- data/Guardfile +0 -18
- data/Guardfile.h2spec +0 -12
- data/LICENSE +0 -21
- data/Rakefile +0 -49
- data/example/Gemfile +0 -3
- data/example/README.md +0 -44
- data/example/client.rb +0 -122
- data/example/helper.rb +0 -19
- data/example/keys/server.crt +0 -20
- data/example/keys/server.key +0 -27
- data/example/server.rb +0 -139
- data/example/upgrade_client.rb +0 -153
- data/example/upgrade_server.rb +0 -203
- data/http-2.gemspec +0 -22
- data/lib/http/2/buffer.rb +0 -76
- data/lib/http/2/compressor.rb +0 -572
- data/lib/tasks/generate_huffman_table.rb +0 -166
- data/spec/buffer_spec.rb +0 -28
- data/spec/client_spec.rb +0 -188
- data/spec/compressor_spec.rb +0 -666
- data/spec/connection_spec.rb +0 -681
- data/spec/emitter_spec.rb +0 -54
- data/spec/framer_spec.rb +0 -487
- data/spec/h2spec/h2spec.darwin +0 -0
- data/spec/h2spec/output/non_secure.txt +0 -317
- data/spec/helper.rb +0 -147
- data/spec/hpack_test_spec.rb +0 -84
- data/spec/huffman_spec.rb +0 -68
- data/spec/server_spec.rb +0 -52
- data/spec/stream_spec.rb +0 -878
- data/spec/support/deep_dup.rb +0 -55
- data/spec/support/duplicable.rb +0 -98
data/example/client.rb
DELETED
@@ -1,122 +0,0 @@
|
|
1
|
-
require_relative 'helper'
|
2
|
-
|
3
|
-
options = {}
|
4
|
-
OptionParser.new do |opts|
|
5
|
-
opts.banner = 'Usage: client.rb [options]'
|
6
|
-
|
7
|
-
opts.on('-d', '--data [String]', 'HTTP payload') do |v|
|
8
|
-
options[:payload] = v
|
9
|
-
end
|
10
|
-
end.parse!
|
11
|
-
|
12
|
-
uri = URI.parse(ARGV[0] || 'http://localhost:8080/')
|
13
|
-
tcp = TCPSocket.new(uri.host, uri.port)
|
14
|
-
sock = nil
|
15
|
-
|
16
|
-
if uri.scheme == 'https'
|
17
|
-
ctx = OpenSSL::SSL::SSLContext.new
|
18
|
-
ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
19
|
-
|
20
|
-
# For ALPN support, Ruby >= 2.3 and OpenSSL >= 1.0.2 are required
|
21
|
-
|
22
|
-
ctx.alpn_protocols = [DRAFT]
|
23
|
-
ctx.alpn_select_cb = lambda do |protocols|
|
24
|
-
puts "ALPN protocols supported by server: #{protocols}"
|
25
|
-
DRAFT if protocols.include? DRAFT
|
26
|
-
end
|
27
|
-
|
28
|
-
sock = OpenSSL::SSL::SSLSocket.new(tcp, ctx)
|
29
|
-
sock.sync_close = true
|
30
|
-
sock.hostname = uri.hostname
|
31
|
-
sock.connect
|
32
|
-
|
33
|
-
if sock.alpn_protocol != DRAFT
|
34
|
-
puts "Failed to negotiate #{DRAFT} via ALPN"
|
35
|
-
exit
|
36
|
-
end
|
37
|
-
else
|
38
|
-
sock = tcp
|
39
|
-
end
|
40
|
-
|
41
|
-
conn = HTTP2::Client.new
|
42
|
-
stream = conn.new_stream
|
43
|
-
log = Logger.new(stream.id)
|
44
|
-
|
45
|
-
conn.on(:frame) do |bytes|
|
46
|
-
# puts "Sending bytes: #{bytes.unpack("H*").first}"
|
47
|
-
sock.print bytes
|
48
|
-
sock.flush
|
49
|
-
end
|
50
|
-
conn.on(:frame_sent) do |frame|
|
51
|
-
puts "Sent frame: #{frame.inspect}"
|
52
|
-
end
|
53
|
-
conn.on(:frame_received) do |frame|
|
54
|
-
puts "Received frame: #{frame.inspect}"
|
55
|
-
end
|
56
|
-
|
57
|
-
conn.on(:promise) do |promise|
|
58
|
-
promise.on(:promise_headers) do |h|
|
59
|
-
log.info "promise request headers: #{h}"
|
60
|
-
end
|
61
|
-
|
62
|
-
promise.on(:headers) do |h|
|
63
|
-
log.info "promise headers: #{h}"
|
64
|
-
end
|
65
|
-
|
66
|
-
promise.on(:data) do |d|
|
67
|
-
log.info "promise data chunk: <<#{d.size}>>"
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
conn.on(:altsvc) do |f|
|
72
|
-
log.info "received ALTSVC #{f}"
|
73
|
-
end
|
74
|
-
|
75
|
-
stream.on(:close) do
|
76
|
-
log.info 'stream closed'
|
77
|
-
end
|
78
|
-
|
79
|
-
stream.on(:half_close) do
|
80
|
-
log.info 'closing client-end of the stream'
|
81
|
-
end
|
82
|
-
|
83
|
-
stream.on(:headers) do |h|
|
84
|
-
log.info "response headers: #{h}"
|
85
|
-
end
|
86
|
-
|
87
|
-
stream.on(:data) do |d|
|
88
|
-
log.info "response data chunk: <<#{d}>>"
|
89
|
-
end
|
90
|
-
|
91
|
-
stream.on(:altsvc) do |f|
|
92
|
-
log.info "received ALTSVC #{f}"
|
93
|
-
end
|
94
|
-
|
95
|
-
head = {
|
96
|
-
':scheme' => uri.scheme,
|
97
|
-
':method' => (options[:payload].nil? ? 'GET' : 'POST'),
|
98
|
-
':authority' => [uri.host, uri.port].join(':'),
|
99
|
-
':path' => uri.path,
|
100
|
-
'accept' => '*/*',
|
101
|
-
}
|
102
|
-
|
103
|
-
puts 'Sending HTTP 2.0 request'
|
104
|
-
if head[':method'] == 'GET'
|
105
|
-
stream.headers(head, end_stream: true)
|
106
|
-
else
|
107
|
-
stream.headers(head, end_stream: false)
|
108
|
-
stream.data(options[:payload])
|
109
|
-
end
|
110
|
-
|
111
|
-
while !sock.closed? && !sock.eof?
|
112
|
-
data = sock.read_nonblock(1024)
|
113
|
-
# puts "Received bytes: #{data.unpack("H*").first}"
|
114
|
-
|
115
|
-
begin
|
116
|
-
conn << data
|
117
|
-
rescue StandardError => e
|
118
|
-
puts "#{e.class} exception: #{e.message} - closing socket."
|
119
|
-
e.backtrace.each { |l| puts "\t" + l }
|
120
|
-
sock.close
|
121
|
-
end
|
122
|
-
end
|
data/example/helper.rb
DELETED
@@ -1,19 +0,0 @@
|
|
1
|
-
$LOAD_PATH << 'lib' << '../lib'
|
2
|
-
|
3
|
-
require 'optparse'
|
4
|
-
require 'socket'
|
5
|
-
require 'openssl'
|
6
|
-
require 'http/2'
|
7
|
-
require 'uri'
|
8
|
-
|
9
|
-
DRAFT = 'h2'.freeze
|
10
|
-
|
11
|
-
class Logger
|
12
|
-
def initialize(id)
|
13
|
-
@id = id
|
14
|
-
end
|
15
|
-
|
16
|
-
def info(msg)
|
17
|
-
puts "[Stream #{@id}]: #{msg}"
|
18
|
-
end
|
19
|
-
end
|
data/example/keys/server.crt
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
-----BEGIN CERTIFICATE-----
|
2
|
-
MIIDLjCCAhYCCQDIZ/9hq/2pXjANBgkqhkiG9w0BAQUFADBZMQswCQYDVQQGEwJB
|
3
|
-
VTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0
|
4
|
-
cyBQdHkgTHRkMRIwEAYDVQQDEwlsb2NhbGhvc3QwHhcNMTYwNjA2MTk0MzI1WhcN
|
5
|
-
MTcwNjA2MTk0MzI1WjBZMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0
|
6
|
-
ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDEwls
|
7
|
-
b2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCjFXrWqtRZ
|
8
|
-
EMOO/o6AxGbgDYMgg/7uxCFQJM5Z6C4II6D8V94FDyCd+J0LOK2hB+QUdFqpA1S9
|
9
|
-
6RW2MvIwdvRt03RJMgbfcUF0+w4ZItv2xrW9waCfCmLSRDZgcSATacEF6u9p2Vs+
|
10
|
-
o4J/cHacirSwjy4+m94CgkxtUFGtGcJaFqAZ6Cdj5WvQdJSiAI3x3gNC/UGA+5dL
|
11
|
-
sp8+vwWx+/TMc6nDBmoRW3GHeG/NApQSh01w3wDv0FmUaFQlA5WPya/Js+CyuYh1
|
12
|
-
miXbQJEjDnGGaJjnoyRAQpPrk72Jj+bnfOu9kxpzkuLJOsbaofRFkM+/Ar5U+bQz
|
13
|
-
uU0ErQ8Ih8MPAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBACL8HkKjW8kWLlW4TE5K
|
14
|
-
EcfsBad2Ah5ugTocJ/pLnr/YEo8uD91gECDtsuFTXul9n2M7U5jJmzbHZ63cjyC3
|
15
|
-
lb1BxJxUL7aIyaL61IMMcIJMWhC9VGnFUshMDNVBhuRkKs/QvaMD5KefKN1E9I2M
|
16
|
-
mZ72Yww0VihYwNOu3MTn8gUuy9eU6k/gTYPY7PJVh18QmR+Fs2MaaPp+bDwxiqML
|
17
|
-
0o2I6+0ZsqM3vFtcUjxjRASV5s+JkM34pTWFwUOl7TZv1YsxCKSz4f0BXDImZEvU
|
18
|
-
rwqFdlELp5WOG9LJsrszDRbf1wbFUsG1XXZpIBiWo3d6pOiIyRKrak1vKViNfYvI
|
19
|
-
W30=
|
20
|
-
-----END CERTIFICATE-----
|
data/example/keys/server.key
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
-----BEGIN RSA PRIVATE KEY-----
|
2
|
-
MIIEpAIBAAKCAQEAoxV61qrUWRDDjv6OgMRm4A2DIIP+7sQhUCTOWeguCCOg/Ffe
|
3
|
-
BQ8gnfidCzitoQfkFHRaqQNUvekVtjLyMHb0bdN0STIG33FBdPsOGSLb9sa1vcGg
|
4
|
-
nwpi0kQ2YHEgE2nBBervadlbPqOCf3B2nIq0sI8uPpveAoJMbVBRrRnCWhagGegn
|
5
|
-
Y+Vr0HSUogCN8d4DQv1BgPuXS7KfPr8Fsfv0zHOpwwZqEVtxh3hvzQKUEodNcN8A
|
6
|
-
79BZlGhUJQOVj8mvybPgsrmIdZol20CRIw5xhmiY56MkQEKT65O9iY/m53zrvZMa
|
7
|
-
c5LiyTrG2qH0RZDPvwK+VPm0M7lNBK0PCIfDDwIDAQABAoIBAQCZShphZsccRJ6c
|
8
|
-
bPdDX9iW5vyG9qsMgPwTGdWAOrXx3pN2PZ0pwjNVaRcsMgU6JHGlLEz/Kmtf6pQG
|
9
|
-
41I0bcuI48Yc+tHs+sadD1IMHHEHP3Yau8KfWyLSI129Pvf4Z2IQjuik5LJYaVbD
|
10
|
-
NNG4iMQYZS0Bmn6Oey0dXu62t0ywYa0qvbIDse/RmjTQSTipuvGg8/QEAeRGABv8
|
11
|
-
Nd4Esya0zuxk6hGaNp3hkjyRkeoC7RsBVJbFSnp6gSubPdXwrJyHfySKe9jvrDG3
|
12
|
-
Q/AzyHUh/6EODd5n66x0p6rq7oo9/PnLvZJY8jIGWG+aEp68RJyEgimrwll0rAWw
|
13
|
-
/buqijGRAoGBANimL8407fFirmct7BceavaeJfXPK5yWiOhVX0XlJ0phAFuaAxK3
|
14
|
-
5HVT7DD+KKV66g1jtS9FUVZGDiYFHlsdsYuHVYcRmr0h5rZr941obrDwNrM9Nf9C
|
15
|
-
0uehN5+n/FaeGoQLR3V4THoP3rlkYTlLpQnI5mKA19JukXnIiJM9ARUZAoGBAMC0
|
16
|
-
mcVsVuSKSFwURtQHHIufxL6SqC2kLTwIQ7exqejNYPCqCiif+ZWOmsTqbVGAGbMK
|
17
|
-
Ohak4oLwN5IGCl4jNQG+vWagREkx6OXSk5NYcfoNBrOm+0UoFRzoEA85s7Dy6PuD
|
18
|
-
tBucNZpt1sGauzkCSx7C8jj4ZlSwkv0XhBFfbTZnAoGBAK2wBjF+U6iq4YFM2rLq
|
19
|
-
KvzOa0Z3MdKXCOmiz//cKDTEMaI+heoyzZCWmIvqpzGLqirT3gUowH23Kk6m2eBY
|
20
|
-
nOdst0/S+Eha7nkfc9bFe8CUxHXMRAcCTs1ufYadCXtzw3RLCp4NtNpC8N+Wry9d
|
21
|
-
CtIeYz1jaCOHi0+kSoIobT65AoGAc6hxWkJp7ITqZQlucTdLdKmRheeztKEC3TMA
|
22
|
-
obGqDqWldww3SKarP431ahZhQjcmNYT/1DNmF7xhPe0OL+3llISMXJn4Ig4ogDdg
|
23
|
-
h2DgF3nV+eFQkfM6qLzHVrwFE0DXgI1NffzFV0hxSoW5tL+honbStkqv8EiCEBEb
|
24
|
-
HOovPCUCgYBpXuPARd2ycInAulVHijJmj2rmK7f41ZhVCWovYjcCWyeJyLIO7j+b
|
25
|
-
MBJZbmwpStJhEjW64nE2zZGWg2HCBbvZz5/SXIr3fp7qVXwpn1TvB/TJDf43t0oF
|
26
|
-
3caLgyQYoQCsVHKT3cU4s3wuog/DyHKh9FtRkcJrEy7h9Rrc+ModbA==
|
27
|
-
-----END RSA PRIVATE KEY-----
|
data/example/server.rb
DELETED
@@ -1,139 +0,0 @@
|
|
1
|
-
require_relative 'helper'
|
2
|
-
|
3
|
-
options = { port: 8080 }
|
4
|
-
OptionParser.new do |opts|
|
5
|
-
opts.banner = 'Usage: server.rb [options]'
|
6
|
-
|
7
|
-
opts.on('-s', '--secure', 'HTTPS mode') do |v|
|
8
|
-
options[:secure] = v
|
9
|
-
end
|
10
|
-
|
11
|
-
opts.on('-p', '--port [Integer]', 'listen port') do |v|
|
12
|
-
options[:port] = v
|
13
|
-
end
|
14
|
-
|
15
|
-
opts.on('-u', '--push', 'Push message') do |_v|
|
16
|
-
options[:push] = true
|
17
|
-
end
|
18
|
-
end.parse!
|
19
|
-
|
20
|
-
puts "Starting server on port #{options[:port]}"
|
21
|
-
server = TCPServer.new(options[:port])
|
22
|
-
|
23
|
-
if options[:secure]
|
24
|
-
ctx = OpenSSL::SSL::SSLContext.new
|
25
|
-
ctx.cert = OpenSSL::X509::Certificate.new(File.open('keys/server.crt'))
|
26
|
-
ctx.key = OpenSSL::PKey::RSA.new(File.open('keys/server.key'))
|
27
|
-
|
28
|
-
ctx.ssl_version = :TLSv1_2
|
29
|
-
ctx.options = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options]
|
30
|
-
ctx.ciphers = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:ciphers]
|
31
|
-
|
32
|
-
ctx.alpn_protocols = ['h2']
|
33
|
-
|
34
|
-
ctx.alpn_select_cb = lambda do |protocols|
|
35
|
-
raise "Protocol #{DRAFT} is required" if protocols.index(DRAFT).nil?
|
36
|
-
DRAFT
|
37
|
-
end
|
38
|
-
|
39
|
-
ctx.ecdh_curves = 'P-256'
|
40
|
-
|
41
|
-
server = OpenSSL::SSL::SSLServer.new(server, ctx)
|
42
|
-
end
|
43
|
-
|
44
|
-
loop do
|
45
|
-
sock = server.accept
|
46
|
-
puts 'New TCP connection!'
|
47
|
-
|
48
|
-
conn = HTTP2::Server.new
|
49
|
-
conn.on(:frame) do |bytes|
|
50
|
-
# puts "Writing bytes: #{bytes.unpack("H*").first}"
|
51
|
-
sock.is_a?(TCPSocket) ? sock.sendmsg(bytes) : sock.write(bytes)
|
52
|
-
end
|
53
|
-
conn.on(:frame_sent) do |frame|
|
54
|
-
puts "Sent frame: #{frame.inspect}"
|
55
|
-
end
|
56
|
-
conn.on(:frame_received) do |frame|
|
57
|
-
puts "Received frame: #{frame.inspect}"
|
58
|
-
end
|
59
|
-
|
60
|
-
conn.on(:stream) do |stream|
|
61
|
-
log = Logger.new(stream.id)
|
62
|
-
req, buffer = {}, ''
|
63
|
-
|
64
|
-
stream.on(:active) { log.info 'client opened new stream' }
|
65
|
-
stream.on(:close) { log.info 'stream closed' }
|
66
|
-
|
67
|
-
stream.on(:headers) do |h|
|
68
|
-
req = Hash[*h.flatten]
|
69
|
-
log.info "request headers: #{h}"
|
70
|
-
end
|
71
|
-
|
72
|
-
stream.on(:data) do |d|
|
73
|
-
log.info "payload chunk: <<#{d}>>"
|
74
|
-
buffer << d
|
75
|
-
end
|
76
|
-
|
77
|
-
stream.on(:half_close) do
|
78
|
-
log.info 'client closed its end of the stream'
|
79
|
-
|
80
|
-
response = nil
|
81
|
-
if req[':method'] == 'POST'
|
82
|
-
log.info "Received POST request, payload: #{buffer}"
|
83
|
-
response = "Hello HTTP 2.0! POST payload: #{buffer}"
|
84
|
-
else
|
85
|
-
log.info 'Received GET request'
|
86
|
-
response = 'Hello HTTP 2.0! GET request'
|
87
|
-
end
|
88
|
-
|
89
|
-
stream.headers({
|
90
|
-
':status' => '200',
|
91
|
-
'content-length' => response.bytesize.to_s,
|
92
|
-
'content-type' => 'text/plain',
|
93
|
-
}, end_stream: false)
|
94
|
-
|
95
|
-
if options[:push]
|
96
|
-
push_streams = []
|
97
|
-
|
98
|
-
# send 10 promises
|
99
|
-
10.times do |i|
|
100
|
-
puts 'sending push'
|
101
|
-
|
102
|
-
head = { ':method' => 'GET',
|
103
|
-
':authority' => 'localhost',
|
104
|
-
':scheme' => 'https',
|
105
|
-
':path' => "/other_resource/#{i}" }
|
106
|
-
|
107
|
-
stream.promise(head) do |push|
|
108
|
-
push.headers(':status' => '200', 'content-type' => 'text/plain', 'content-length' => '11')
|
109
|
-
push_streams << push
|
110
|
-
end
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
# split response into multiple DATA frames
|
115
|
-
stream.data(response.slice!(0, 5), end_stream: false)
|
116
|
-
stream.data(response)
|
117
|
-
|
118
|
-
if options[:push]
|
119
|
-
push_streams.each_with_index do |push, i|
|
120
|
-
sleep 1
|
121
|
-
push.data("push_data #{i}")
|
122
|
-
end
|
123
|
-
end
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
while !sock.closed? && !(sock.eof? rescue true) # rubocop:disable Style/RescueModifier
|
128
|
-
data = sock.readpartial(1024)
|
129
|
-
# puts "Received bytes: #{data.unpack("H*").first}"
|
130
|
-
|
131
|
-
begin
|
132
|
-
conn << data
|
133
|
-
rescue StandardError => e
|
134
|
-
puts "#{e.class} exception: #{e.message} - closing socket."
|
135
|
-
e.backtrace.each { |l| puts "\t" + l }
|
136
|
-
sock.close
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
data/example/upgrade_client.rb
DELETED
@@ -1,153 +0,0 @@
|
|
1
|
-
# frozen_string_literals: true
|
2
|
-
|
3
|
-
require_relative 'helper'
|
4
|
-
require 'http_parser'
|
5
|
-
|
6
|
-
OptionParser.new do |opts|
|
7
|
-
opts.banner = 'Usage: upgrade_client.rb [options]'
|
8
|
-
end.parse!
|
9
|
-
|
10
|
-
uri = URI.parse(ARGV[0] || 'http://localhost:8080/')
|
11
|
-
sock = TCPSocket.new(uri.host, uri.port)
|
12
|
-
|
13
|
-
conn = HTTP2::Client.new
|
14
|
-
|
15
|
-
def request_header_hash
|
16
|
-
Hash.new do |hash, key|
|
17
|
-
k = key.to_s.downcase
|
18
|
-
k.tr! '_', '-'
|
19
|
-
_, value = hash.find { |header_key, _| header_key.downcase == k }
|
20
|
-
hash[key] = value if value
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
conn.on(:frame) do |bytes|
|
25
|
-
sock.print bytes
|
26
|
-
sock.flush
|
27
|
-
end
|
28
|
-
conn.on(:frame_sent) do |frame|
|
29
|
-
puts "Sent frame: #{frame.inspect}"
|
30
|
-
end
|
31
|
-
conn.on(:frame_received) do |frame|
|
32
|
-
puts "Received frame: #{frame.inspect}"
|
33
|
-
end
|
34
|
-
|
35
|
-
# upgrader module
|
36
|
-
class UpgradeHandler
|
37
|
-
UPGRADE_REQUEST = <<RESP.freeze
|
38
|
-
GET %s HTTP/1.1
|
39
|
-
Connection: Upgrade, HTTP2-Settings
|
40
|
-
HTTP2-Settings: #{HTTP2::Client.settings_header(settings_max_concurrent_streams: 100)}
|
41
|
-
Upgrade: h2c
|
42
|
-
Host: %s
|
43
|
-
User-Agent: http-2 upgrade
|
44
|
-
Accept: */*
|
45
|
-
|
46
|
-
RESP
|
47
|
-
|
48
|
-
attr_reader :complete, :parsing
|
49
|
-
def initialize(conn, sock)
|
50
|
-
@conn = conn
|
51
|
-
@sock = sock
|
52
|
-
@headers = request_header_hash
|
53
|
-
@body = ''.b
|
54
|
-
@complete, @parsing = false, false
|
55
|
-
@parser = ::HTTP::Parser.new(self)
|
56
|
-
end
|
57
|
-
|
58
|
-
def request(uri)
|
59
|
-
host = "#{uri.hostname}#{":#{uri.port}" if uri.port != uri.default_port}"
|
60
|
-
req = format(UPGRADE_REQUEST, uri.request_uri, host)
|
61
|
-
puts req
|
62
|
-
@sock << req
|
63
|
-
end
|
64
|
-
|
65
|
-
def <<(data)
|
66
|
-
@parsing ||= true
|
67
|
-
@parser << data
|
68
|
-
return unless complete
|
69
|
-
upgrade
|
70
|
-
end
|
71
|
-
|
72
|
-
def complete!
|
73
|
-
@complete = true
|
74
|
-
end
|
75
|
-
|
76
|
-
def on_headers_complete(headers)
|
77
|
-
@headers.merge!(headers)
|
78
|
-
puts "received headers: #{headers}"
|
79
|
-
end
|
80
|
-
|
81
|
-
def on_body(chunk)
|
82
|
-
puts "received chunk: #{chunk}"
|
83
|
-
@body << chunk
|
84
|
-
end
|
85
|
-
|
86
|
-
def on_message_complete
|
87
|
-
fail 'could not upgrade to h2c' unless @parser.status_code == 101
|
88
|
-
@parsing = false
|
89
|
-
complete!
|
90
|
-
end
|
91
|
-
|
92
|
-
def upgrade
|
93
|
-
stream = @conn.upgrade
|
94
|
-
log = Logger.new(stream.id)
|
95
|
-
|
96
|
-
stream.on(:close) do
|
97
|
-
log.info 'stream closed'
|
98
|
-
end
|
99
|
-
|
100
|
-
stream.on(:half_close) do
|
101
|
-
log.info 'closing client-end of the stream'
|
102
|
-
end
|
103
|
-
|
104
|
-
stream.on(:headers) do |h|
|
105
|
-
log.info "response headers: #{h}"
|
106
|
-
end
|
107
|
-
|
108
|
-
stream.on(:data) do |d|
|
109
|
-
log.info "response data chunk: <<#{d}>>"
|
110
|
-
end
|
111
|
-
|
112
|
-
stream.on(:altsvc) do |f|
|
113
|
-
log.info "received ALTSVC #{f}"
|
114
|
-
end
|
115
|
-
|
116
|
-
@conn.on(:promise) do |promise|
|
117
|
-
promise.on(:headers) do |h|
|
118
|
-
log.info "promise headers: #{h}"
|
119
|
-
end
|
120
|
-
|
121
|
-
promise.on(:data) do |d|
|
122
|
-
log.info "promise data chunk: <<#{d.size}>>"
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
@conn.on(:altsvc) do |f|
|
127
|
-
log.info "received ALTSVC #{f}"
|
128
|
-
end
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
uh = UpgradeHandler.new(conn, sock)
|
133
|
-
puts 'Sending HTTP/1.1 upgrade request'
|
134
|
-
uh.request(uri)
|
135
|
-
|
136
|
-
while !sock.closed? && !sock.eof?
|
137
|
-
data = sock.read_nonblock(1024)
|
138
|
-
|
139
|
-
begin
|
140
|
-
if !uh.parsing && !uh.complete
|
141
|
-
uh << data
|
142
|
-
elsif uh.parsing && !uh.complete
|
143
|
-
uh << data
|
144
|
-
elsif uh.complete
|
145
|
-
conn << data
|
146
|
-
end
|
147
|
-
rescue StandardError => e
|
148
|
-
puts "#{e.class} exception: #{e.message} - closing socket."
|
149
|
-
e.backtrace.each { |l| puts "\t" + l }
|
150
|
-
conn.close
|
151
|
-
sock.close
|
152
|
-
end
|
153
|
-
end
|
data/example/upgrade_server.rb
DELETED
@@ -1,203 +0,0 @@
|
|
1
|
-
# frozen_string_literals: true
|
2
|
-
|
3
|
-
require_relative 'helper'
|
4
|
-
require 'http_parser'
|
5
|
-
|
6
|
-
options = { port: 8080 }
|
7
|
-
OptionParser.new do |opts|
|
8
|
-
opts.banner = 'Usage: server.rb [options]'
|
9
|
-
|
10
|
-
opts.on('-s', '--secure', 'HTTPS mode') do |v|
|
11
|
-
options[:secure] = v
|
12
|
-
end
|
13
|
-
|
14
|
-
opts.on('-p', '--port [Integer]', 'listen port') do |v|
|
15
|
-
options[:port] = v
|
16
|
-
end
|
17
|
-
end.parse!
|
18
|
-
|
19
|
-
puts "Starting server on port #{options[:port]}"
|
20
|
-
server = TCPServer.new(options[:port])
|
21
|
-
|
22
|
-
if options[:secure]
|
23
|
-
ctx = OpenSSL::SSL::SSLContext.new
|
24
|
-
ctx.cert = OpenSSL::X509::Certificate.new(File.open('keys/server.crt'))
|
25
|
-
ctx.key = OpenSSL::PKey::RSA.new(File.open('keys/server.key'))
|
26
|
-
ctx.npn_protocols = [DRAFT]
|
27
|
-
|
28
|
-
server = OpenSSL::SSL::SSLServer.new(server, ctx)
|
29
|
-
end
|
30
|
-
|
31
|
-
def request_header_hash
|
32
|
-
Hash.new do |hash, key|
|
33
|
-
k = key.to_s.downcase
|
34
|
-
k.tr! '_', '-'
|
35
|
-
_, value = hash.find { |header_key, _| header_key.downcase == k }
|
36
|
-
hash[key] = value if value
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
class UpgradeHandler
|
41
|
-
VALID_UPGRADE_METHODS = %w(GET OPTIONS).freeze
|
42
|
-
UPGRADE_RESPONSE = <<RESP.freeze
|
43
|
-
HTTP/1.1 101 Switching Protocols
|
44
|
-
Connection: Upgrade
|
45
|
-
Upgrade: h2c
|
46
|
-
|
47
|
-
RESP
|
48
|
-
|
49
|
-
attr_reader :complete, :headers, :body, :parsing
|
50
|
-
|
51
|
-
def initialize(conn, sock)
|
52
|
-
@conn, @sock = conn, sock
|
53
|
-
@complete, @parsing = false, false
|
54
|
-
@headers = request_header_hash
|
55
|
-
@body = ''
|
56
|
-
@parser = ::HTTP::Parser.new(self)
|
57
|
-
end
|
58
|
-
|
59
|
-
def <<(data)
|
60
|
-
@parsing ||= true
|
61
|
-
@parser << data
|
62
|
-
return unless complete
|
63
|
-
|
64
|
-
@sock.write UPGRADE_RESPONSE
|
65
|
-
|
66
|
-
settings = headers['http2-settings']
|
67
|
-
request = {
|
68
|
-
':scheme' => 'http',
|
69
|
-
':method' => @parser.http_method,
|
70
|
-
':authority' => headers['Host'],
|
71
|
-
':path' => @parser.request_url,
|
72
|
-
}.merge(headers)
|
73
|
-
|
74
|
-
@conn.upgrade(settings, request, @body)
|
75
|
-
end
|
76
|
-
|
77
|
-
def complete!
|
78
|
-
@complete = true
|
79
|
-
end
|
80
|
-
|
81
|
-
def on_headers_complete(headers)
|
82
|
-
@headers.merge! headers
|
83
|
-
end
|
84
|
-
|
85
|
-
def on_body(chunk)
|
86
|
-
@body << chunk
|
87
|
-
end
|
88
|
-
|
89
|
-
def on_message_complete
|
90
|
-
fail unless VALID_UPGRADE_METHODS.include?(@parser.http_method)
|
91
|
-
@parsing = false
|
92
|
-
complete!
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
loop do
|
97
|
-
sock = server.accept
|
98
|
-
puts 'New TCP connection!'
|
99
|
-
|
100
|
-
conn = HTTP2::Server.new
|
101
|
-
conn.on(:frame) do |bytes|
|
102
|
-
# puts "Writing bytes: #{bytes.unpack("H*").first}"
|
103
|
-
sock.write bytes
|
104
|
-
end
|
105
|
-
conn.on(:frame_sent) do |frame|
|
106
|
-
puts "Sent frame: #{frame.inspect}"
|
107
|
-
end
|
108
|
-
conn.on(:frame_received) do |frame|
|
109
|
-
puts "Received frame: #{frame.inspect}"
|
110
|
-
end
|
111
|
-
|
112
|
-
conn.on(:stream) do |stream|
|
113
|
-
log = Logger.new(stream.id)
|
114
|
-
req = request_header_hash
|
115
|
-
buffer = ''
|
116
|
-
|
117
|
-
stream.on(:active) { log.info 'client opened new stream' }
|
118
|
-
stream.on(:close) do
|
119
|
-
log.info 'stream closed'
|
120
|
-
end
|
121
|
-
|
122
|
-
stream.on(:headers) do |h|
|
123
|
-
req.merge! Hash[*h.flatten]
|
124
|
-
log.info "request headers: #{h}"
|
125
|
-
end
|
126
|
-
|
127
|
-
stream.on(:data) do |d|
|
128
|
-
log.info "payload chunk: <<#{d}>>"
|
129
|
-
buffer << d
|
130
|
-
end
|
131
|
-
|
132
|
-
stream.on(:half_close) do
|
133
|
-
log.info 'client closed its end of the stream'
|
134
|
-
|
135
|
-
if req['Upgrade']
|
136
|
-
log.info "Processing h2c Upgrade request: #{req}"
|
137
|
-
if req[':method'] != 'OPTIONS' # Don't respond to OPTIONS...
|
138
|
-
response = 'Hello h2c world!'
|
139
|
-
stream.headers({
|
140
|
-
':status' => '200',
|
141
|
-
'content-length' => response.bytesize.to_s,
|
142
|
-
'content-type' => 'text/plain',
|
143
|
-
}, end_stream: false)
|
144
|
-
stream.data(response)
|
145
|
-
end
|
146
|
-
else
|
147
|
-
|
148
|
-
response = nil
|
149
|
-
if req[':method'] == 'POST'
|
150
|
-
log.info "Received POST request, payload: #{buffer}"
|
151
|
-
response = "Hello HTTP 2.0! POST payload: #{buffer}"
|
152
|
-
else
|
153
|
-
log.info 'Received GET request'
|
154
|
-
response = 'Hello HTTP 2.0! GET request'
|
155
|
-
end
|
156
|
-
|
157
|
-
stream.headers({
|
158
|
-
':status' => '200',
|
159
|
-
'content-length' => response.bytesize.to_s,
|
160
|
-
'content-type' => 'text/plain',
|
161
|
-
}, end_stream: false)
|
162
|
-
|
163
|
-
# split response into multiple DATA frames
|
164
|
-
stream.data(response.slice!(0, 5), end_stream: false)
|
165
|
-
stream.data(response)
|
166
|
-
end
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
uh = UpgradeHandler.new(conn, sock)
|
171
|
-
|
172
|
-
while !sock.closed? && !(sock.eof? rescue true) # rubocop:disable Style/RescueModifier
|
173
|
-
data = sock.readpartial(1024)
|
174
|
-
# puts "Received bytes: #{data.unpack("H*").first}"
|
175
|
-
|
176
|
-
begin
|
177
|
-
case
|
178
|
-
when !uh.parsing && !uh.complete
|
179
|
-
|
180
|
-
if data.start_with?(*UpgradeHandler::VALID_UPGRADE_METHODS)
|
181
|
-
uh << data
|
182
|
-
else
|
183
|
-
uh.complete!
|
184
|
-
conn << data
|
185
|
-
end
|
186
|
-
|
187
|
-
when uh.parsing && !uh.complete
|
188
|
-
uh << data
|
189
|
-
|
190
|
-
when uh.complete
|
191
|
-
conn << data
|
192
|
-
end
|
193
|
-
|
194
|
-
rescue StandardError => e
|
195
|
-
puts "Exception: #{e}, #{e.message} - closing socket."
|
196
|
-
puts e.backtrace.last(10).join("\n")
|
197
|
-
sock.close
|
198
|
-
end
|
199
|
-
end
|
200
|
-
end
|
201
|
-
|
202
|
-
# echo foo=bar | nghttp -d - -t 0 -vu http://127.0.0.1:8080/
|
203
|
-
# nghttp -vu http://127.0.0.1:8080/
|
data/http-2.gemspec
DELETED
@@ -1,22 +0,0 @@
|
|
1
|
-
lib = File.expand_path('./lib', __dir__)
|
2
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
-
require 'http/2/version'
|
4
|
-
|
5
|
-
Gem::Specification.new do |spec|
|
6
|
-
spec.name = 'http-2'
|
7
|
-
spec.version = HTTP2::VERSION
|
8
|
-
spec.authors = ['Ilya Grigorik', 'Kaoru Maeda']
|
9
|
-
spec.email = ['ilya@igvita.com']
|
10
|
-
spec.description = 'Pure-ruby HTTP 2.0 protocol implementation'
|
11
|
-
spec.summary = spec.description
|
12
|
-
spec.homepage = 'https://github.com/igrigorik/http-2'
|
13
|
-
spec.license = 'MIT'
|
14
|
-
spec.required_ruby_version = '>=2.1.0'
|
15
|
-
|
16
|
-
spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
17
|
-
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
-
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
-
spec.require_paths = ['lib']
|
20
|
-
|
21
|
-
spec.add_development_dependency 'bundler'
|
22
|
-
end
|