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