h2 0.8.0 → 0.8.1
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/CHANGELOG.md +5 -0
- data/Gemfile +1 -2
- data/Rakefile +23 -12
- data/examples/server/hello_world.rb +34 -3
- data/examples/server/https_hello_world.rb +51 -6
- data/examples/server/push_promise.rb +125 -16
- data/examples/server/sse.rb +159 -12
- data/exe/h2 +12 -10
- data/h2.gemspec +3 -4
- data/lib/h2.rb +0 -12
- data/lib/h2/client.rb +14 -14
- data/lib/h2/server.rb +0 -13
- data/lib/h2/server/connection.rb +6 -2
- data/lib/h2/server/stream.rb +9 -8
- data/lib/h2/server/stream/event_source.rb +1 -1
- data/lib/h2/server/stream/request.rb +1 -1
- data/lib/h2/server/stream/response.rb +1 -1
- data/lib/h2/stream.rb +1 -1
- data/lib/h2/version.rb +1 -1
- metadata +6 -21
- data/Guardfile +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2b78f8a3e3aeed1884179c684adaa31e16cb377bdaa123d0af10f4cb174ff86f
|
4
|
+
data.tar.gz: 9beec3d11d01b4848118b17e1572275e231f580a48c62f0db28f52c059801c80
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ce8d9218b3cf9fc848ba56563ccdb8760cf63e30c312825ada05652e30dbcc60405109785b29c9466effcc9a3713ba8c57ccdf73c4a2f2b7984772e9a5f8f5a
|
7
|
+
data.tar.gz: ec1413ce8638d7aa6db8ec4e75f8032581e72dd391c5124e410ad3f9896feaf0f9580ae97895d846407d10e1b8a4a82ff381b57d14929d1d76240655ee55df53
|
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
2
|
|
3
3
|
gemspec
|
4
|
-
gem 'http-2', path: '../http-2'
|
4
|
+
# gem 'http-2', path: '../http-2'
|
5
5
|
|
6
6
|
group :concurrent_ruby do
|
7
7
|
gem 'concurrent-ruby'
|
@@ -14,7 +14,6 @@ end
|
|
14
14
|
group :development, :test do
|
15
15
|
gem 'awesome_print'
|
16
16
|
gem 'certificate_authority'
|
17
|
-
gem 'guard-rake'
|
18
17
|
gem 'pry-byebug', platforms: [:mri]
|
19
18
|
gem 'celluloid-io', '~> 0.17', '>= 0.17.3'
|
20
19
|
end
|
data/Rakefile
CHANGED
@@ -1,14 +1,28 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'fileutils'
|
4
|
+
require File.expand_path '../test/support/create_certs', __FILE__
|
3
5
|
|
4
6
|
task default: :test
|
5
7
|
|
6
|
-
Rake::TestTask.new :test
|
8
|
+
Rake::TestTask.new :test do |t|
|
7
9
|
t.test_files = FileList['test/**/*_test.rb']
|
8
10
|
end
|
9
11
|
|
10
12
|
namespace :test do
|
11
13
|
|
14
|
+
desc 'run tests via official ruby docker image'
|
15
|
+
task :docker, [:tag] do |_,args|
|
16
|
+
tag = args.fetch :tag, 'ruby:2.5'
|
17
|
+
|
18
|
+
FileUtils.mkdir_p 'tmp/docker'
|
19
|
+
system "docker pull #{tag}"
|
20
|
+
system "docker run --rm -v `pwd`:/opt/src/h2 -it #{tag} /bin/sh -c '" +
|
21
|
+
"cd /opt/src/h2 && " +
|
22
|
+
"bundle install --path tmp/docker && " +
|
23
|
+
"bundle exec rake test'"
|
24
|
+
end
|
25
|
+
|
12
26
|
desc 'send TTIN signal to test process'
|
13
27
|
task :ttin do
|
14
28
|
pid = `ps -ef | grep -v grep | grep -e 'ruby.*_test\.rb' | awk '{print $2}'`.strip
|
@@ -18,19 +32,16 @@ namespace :test do
|
|
18
32
|
end
|
19
33
|
end
|
20
34
|
|
21
|
-
task :
|
22
|
-
|
23
|
-
|
24
|
-
require_relative 'test/support/create_certs' unless File.exist? ca_file
|
25
|
-
end
|
35
|
+
task :nginx, [:tag, :ctx] do |_,args|
|
36
|
+
tag = args.fetch :tag, 'h2_nginx_http2'
|
37
|
+
ctx = args.fetch :ctx, 'test/support/nginx'
|
26
38
|
|
27
|
-
|
28
|
-
system "docker build -t h2_nginx_http2 test/support/nginx"
|
39
|
+
system "docker build -t #{tag} #{ctx}"
|
29
40
|
puts "\nstarting nginx with http/2 support"
|
30
|
-
puts "using document root:
|
41
|
+
puts "using docker context/document root: #{ctx}"
|
31
42
|
puts "using TLS certs: tmp/certs/server.*"
|
32
43
|
puts "listening at https://localhost:4430/"
|
33
|
-
system "docker run --rm -v `pwd`/tmp/certs:/usr/local/nginx/certs -v `pwd
|
44
|
+
system "docker run --rm -v `pwd`/tmp/certs:/usr/local/nginx/certs -v `pwd`/#{ctx}:/usr/local/nginx/html -p 4430:443 -it #{tag}"
|
34
45
|
end
|
35
46
|
|
36
47
|
end
|
@@ -4,17 +4,48 @@
|
|
4
4
|
require 'bundler/setup'
|
5
5
|
require 'h2/server'
|
6
6
|
|
7
|
+
# hello world example
|
8
|
+
#
|
9
|
+
# NOTE: this is a plaintext "h2c" type of HTTP/2 server. browsers probably will
|
10
|
+
# never support this, but it may be useful for testing, or for behind
|
11
|
+
# TLS-enabled proxies.
|
12
|
+
|
13
|
+
# crank up the logger level for testing/example purposes
|
14
|
+
#
|
7
15
|
H2::Logger.level = ::Logger::DEBUG
|
8
|
-
H2.verbose!
|
9
16
|
|
10
|
-
|
17
|
+
port = 1234
|
18
|
+
addr = Socket.getaddrinfo('localhost', port).first[3]
|
19
|
+
puts "*** Starting server on http://localhost:#{port}"
|
11
20
|
|
12
|
-
|
21
|
+
# create h2c server on the given address and port.
|
22
|
+
# the constructor requires a block that will be called on each connection.
|
23
|
+
#
|
13
24
|
s = H2::Server::HTTP.new host: addr, port: port do |connection|
|
25
|
+
|
26
|
+
# each connection will have 0 or more streams, so we must give the
|
27
|
+
# connection a stream handler block via the +#each_stream+ method.
|
28
|
+
#
|
14
29
|
connection.each_stream do |stream|
|
30
|
+
|
31
|
+
# here, without checking anything about the request, we respond with 200
|
32
|
+
# and a "hello, world\n" body
|
33
|
+
#
|
34
|
+
# see +H2::Server::Stream#respond+
|
35
|
+
#
|
15
36
|
stream.respond status: 200, body: "hello, world!\n"
|
37
|
+
|
38
|
+
# since HTTP/2 connections are sort of intrinsically "keep-alive", we
|
39
|
+
# tell the client to close immediately with a GOAWAY frame
|
40
|
+
#
|
41
|
+
# see also +H2::Server::Connection#goaway_on_complete+
|
42
|
+
#
|
16
43
|
stream.connection.goaway
|
44
|
+
|
17
45
|
end
|
18
46
|
end
|
19
47
|
|
48
|
+
# now that our server reactor (Celluloid::IO instance) is configured and listening,
|
49
|
+
# we can put the "main" thread to sleep.
|
50
|
+
#
|
20
51
|
sleep
|
@@ -4,31 +4,76 @@
|
|
4
4
|
require 'bundler/setup'
|
5
5
|
require 'h2/server'
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
# hello world TLS example
|
8
|
+
#
|
9
|
+
# NOTE: this is a TLS-enabled "h2" type of HTTP/2 server. we will need some
|
10
|
+
# cryptography to get going. this will check for the existence of a git-
|
11
|
+
# ignored set of testing CA, server, and client certs/keys, creating
|
12
|
+
# them as needed.
|
13
|
+
#
|
14
|
+
# see: test/support/create_certs.rb
|
15
|
+
#
|
16
|
+
require File.expand_path '../../../test/support/create_certs', __FILE__
|
17
|
+
|
18
|
+
# crank up the logger level for testing/example purposes
|
19
|
+
#
|
20
|
+
H2::Logger.level = ::Logger::DEBUG
|
21
|
+
|
22
|
+
port = 1234
|
23
|
+
addr = Socket.getaddrinfo('localhost', port).first[3]
|
24
|
+
puts "*** Starting server on https://localhost:#{port}"
|
10
25
|
|
11
26
|
# if not using SNI, we may pass the underlying opts directly, and the same TLS
|
12
27
|
# cert/key will be used for all incoming connections.
|
13
28
|
#
|
29
|
+
certs_dir = File.expand_path '../../../tmp/certs', __FILE__
|
14
30
|
tls = {
|
15
31
|
cert: certs_dir + '/server.crt',
|
16
32
|
key: certs_dir + '/server.key',
|
17
33
|
# :extra_chain_cert => certs_dir + '/chain.pem'
|
18
34
|
}
|
19
35
|
|
20
|
-
|
21
|
-
|
36
|
+
# create h2 server on the given address and port using the given certificate
|
37
|
+
# and private key for all TLS negotiation. the constructor requires a block
|
38
|
+
# that will be called on each connection.
|
39
|
+
#
|
22
40
|
s = H2::Server::HTTPS.new host: addr, port: port, **tls do |connection|
|
41
|
+
|
42
|
+
# each connection will have 0 or more streams, so we must give the
|
43
|
+
# connection a stream handler block via the +#each_stream+ method.
|
44
|
+
#
|
23
45
|
connection.each_stream do |stream|
|
24
|
-
stream.goaway_on_complete
|
25
46
|
|
47
|
+
# check the request path (HTTP/2 psuedo-header ':path')
|
48
|
+
#
|
49
|
+
# see +H2::Server::Stream#request+ - access the +H2::Server::Stream::Request+ instance
|
50
|
+
#
|
26
51
|
if stream.request.path == '/favicon.ico'
|
52
|
+
|
53
|
+
# since this is a TLS-enabled server, we could actually test it with a
|
54
|
+
# real browser, which will undoubtedly request /favicon.ico.
|
55
|
+
#
|
56
|
+
# see +H2::Server::Stream#respond+
|
57
|
+
#
|
27
58
|
stream.respond status: 404
|
59
|
+
|
28
60
|
else
|
61
|
+
|
62
|
+
# since HTTP/2 connections are sort of intrinsically "keep-alive", we
|
63
|
+
# tell the client to close when this stream is complete with a GOAWAY frame
|
64
|
+
#
|
65
|
+
stream.goaway_on_complete
|
66
|
+
|
67
|
+
# we respond with 200 and a "hello, world\n" body
|
68
|
+
#
|
69
|
+
# see +H2::Server::Stream#respond+
|
70
|
+
#
|
29
71
|
stream.respond status: 200, body: "hello, world!\n"
|
30
72
|
end
|
31
73
|
end
|
32
74
|
end
|
33
75
|
|
76
|
+
# now that our server reactor (Celluloid::IO instance) is configured and listening,
|
77
|
+
# we can put the "main" thread to sleep.
|
78
|
+
#
|
34
79
|
sleep
|
@@ -4,53 +4,162 @@
|
|
4
4
|
require 'bundler/setup'
|
5
5
|
require 'h2/server'
|
6
6
|
|
7
|
+
# push promise example
|
8
|
+
#
|
9
|
+
# NOTE: this is a TLS-enabled "h2" type of HTTP/2 server. we will need some
|
10
|
+
# cryptography to get going. this will check for the existence of a git-
|
11
|
+
# ignored set of testing CA, server, and client certs/keys, creating
|
12
|
+
# them as needed.
|
13
|
+
#
|
14
|
+
# see: test/support/create_certs.rb
|
15
|
+
#
|
16
|
+
require File.expand_path '../../../test/support/create_certs', __FILE__
|
17
|
+
|
18
|
+
# crank up the logger level for testing/example purposes
|
19
|
+
#
|
7
20
|
H2::Logger.level = ::Logger::DEBUG
|
8
|
-
H2.verbose!
|
9
21
|
|
10
|
-
port
|
11
|
-
addr
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
22
|
+
port = 1234
|
23
|
+
addr = Socket.getaddrinfo('localhost', port).first[3]
|
24
|
+
puts "*** Starting server on https://localhost:#{port}"
|
25
|
+
|
26
|
+
# NOTE: since we're going to try a real-world push promise, let's load up some
|
27
|
+
# "useful" example bits to play with: full valid HTML, a PNG of a dog, and
|
28
|
+
# some javscript. we will respond directly with the HTML, but push promise
|
29
|
+
# the dog and js.
|
30
|
+
#
|
31
|
+
html = <<~EOHTML
|
32
|
+
<!DOCTYPE html>
|
33
|
+
<html lang="en">
|
34
|
+
<head>
|
35
|
+
<meta charset="UTF-8">
|
36
|
+
<title>HTTP/2 Push Promise Example</title>
|
37
|
+
</head>
|
38
|
+
<body>
|
39
|
+
wait for it...
|
40
|
+
<img src="/dog.png"/>
|
41
|
+
<script src="/pushed.js"></script>
|
42
|
+
</body>
|
43
|
+
</html>
|
44
|
+
EOHTML
|
45
|
+
|
46
|
+
dog_png_file = File.expand_path '../dog.png', __FILE__
|
47
|
+
dog_png = File.read dog_png_file
|
48
|
+
dog_png_fs = File::Stat.new dog_png_file
|
49
|
+
dog_png_etag = OpenSSL::Digest::SHA.hexdigest dog_png_fs.ino.to_s +
|
50
|
+
dog_png_fs.size.to_s +
|
51
|
+
dog_png_fs.mtime.to_s
|
16
52
|
|
53
|
+
pushed_js = '(()=>{ alert("hello h2 push promise!"); })();'
|
54
|
+
|
55
|
+
# using SNI, we can negotiate TLS for multiple certificates based on the
|
56
|
+
# requested servername.
|
57
|
+
#
|
58
|
+
# see: https://en.wikipedia.org/wiki/Server_Name_Indication
|
59
|
+
# see: https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html#servername_cb
|
60
|
+
#
|
61
|
+
certs_dir = File.expand_path '../../../tmp/certs', __FILE__
|
17
62
|
sni = {
|
18
63
|
'localhost' => {
|
19
64
|
:cert => certs_dir + '/server.crt',
|
20
65
|
:key => certs_dir + '/server.key',
|
21
66
|
# :extra_chain_cert => certs_dir + '/chain.pem'
|
22
67
|
},
|
23
|
-
'
|
24
|
-
:cert => certs_dir + '/
|
25
|
-
:key => certs_dir + '/
|
26
|
-
:extra_chain_cert => certs_dir + '/
|
68
|
+
'example.com' => {
|
69
|
+
:cert => certs_dir + '/example.com.crt',
|
70
|
+
:key => certs_dir + '/example.com.key',
|
71
|
+
:extra_chain_cert => certs_dir + '/example.com-chain.pem'
|
27
72
|
}
|
28
73
|
}
|
29
74
|
|
30
|
-
|
75
|
+
# create h2 server on the given address and port using the given SNI +Hash+
|
76
|
+
# for configuring TLS negotiation. the constructor requires a block that will
|
77
|
+
# be called on each connection.
|
78
|
+
#
|
31
79
|
s = H2::Server::HTTPS.new host: addr, port: port, sni: sni do |connection|
|
80
|
+
|
81
|
+
# each connection will have 0 or more streams, so we must give the
|
82
|
+
# connection a stream handler block via the +#each_stream+ method.
|
83
|
+
#
|
32
84
|
connection.each_stream do |stream|
|
33
85
|
|
86
|
+
# check the request path (HTTP/2 psuedo-header ':path')
|
87
|
+
#
|
88
|
+
# see +H2::Server::Stream#request+
|
89
|
+
#
|
34
90
|
if stream.request.path == '/favicon.ico'
|
91
|
+
|
92
|
+
# since this is a TLS-enabled server, we could actually test it with a
|
93
|
+
# real browser, which will undoubtedly request /favicon.ico.
|
94
|
+
#
|
95
|
+
# see +H2::Server::Stream#respond+
|
96
|
+
#
|
35
97
|
stream.respond status: 404
|
36
98
|
|
37
99
|
else
|
100
|
+
|
101
|
+
# since HTTP/2 connections are sort of intrinsically "keep-alive", we
|
102
|
+
# tell the client to close when this stream is complete with a GOAWAY frame
|
103
|
+
#
|
38
104
|
stream.goaway_on_complete
|
39
105
|
|
40
|
-
#
|
41
|
-
|
106
|
+
# initiate a push promise sub-stream, and queue the "keep" handler.
|
107
|
+
# since a push promise may be canceled, we queue the handler on the server reactor,
|
108
|
+
# after initiating the stream with headers, so that the client has a chance to
|
109
|
+
# cancel with a RST_STREAM frame.
|
110
|
+
#
|
111
|
+
# see +H2::Server::Stream#push_promise+
|
112
|
+
#
|
113
|
+
stream.push_promise path: '/dog.png',
|
114
|
+
headers: {
|
115
|
+
'content-type' => 'image/png',
|
116
|
+
|
117
|
+
# NOTE: "etag" headers are not supplied by the server.
|
118
|
+
#
|
119
|
+
'etag' => dog_png_etag
|
120
|
+
|
121
|
+
},
|
122
|
+
body: dog_png
|
42
123
|
|
43
|
-
#
|
124
|
+
# instantiate a push promise sub-stream, but do not send initial headers
|
125
|
+
# nor "keep" the promise by sending the body.
|
126
|
+
#
|
127
|
+
# see +H2::Server::Stream#push_promise_for+
|
128
|
+
#
|
44
129
|
js_promise = stream.push_promise_for path: '/pushed.js',
|
45
130
|
headers: { 'content-type' => 'application/javascript' },
|
46
131
|
body: pushed_js
|
132
|
+
|
133
|
+
# have this +H2::Server::PushPromise+ initiate the sub-stream on this
|
134
|
+
# stream by sending initial headers.
|
135
|
+
#
|
136
|
+
# see +H2::Server::PushPromise#make_on+
|
137
|
+
#
|
47
138
|
js_promise.make_on stream
|
48
139
|
|
49
|
-
|
140
|
+
# respond with 200 and HTML body
|
141
|
+
#
|
142
|
+
# see +H2::Server::Stream#respond+
|
143
|
+
#
|
144
|
+
stream.respond status: 200, body: html
|
50
145
|
|
146
|
+
# we have now waited until we've sent the entire body of the original
|
147
|
+
# response, so the client probably has received that and the push promise
|
148
|
+
# headers for both the dog and the script. our convenient +H2::Server::Stream#push_promise+
|
149
|
+
# method above queued the actual sending of the body on the server reactor,
|
150
|
+
# but for `js_promise`, we must keep it ourselves. in this case, we keep
|
151
|
+
# it "synchronously", but we may also call `#keep_async` to queue it.
|
152
|
+
#
|
153
|
+
# see +H2::Server::PushPromise#keep+
|
154
|
+
# see +H2::Server::PushPromise#keep_async+
|
155
|
+
#
|
51
156
|
js_promise.keep
|
157
|
+
|
52
158
|
end
|
53
159
|
end
|
54
160
|
end
|
55
161
|
|
162
|
+
# now that our server reactor (Celluloid::IO instance) is configured and listening,
|
163
|
+
# we can put the "main" thread to sleep.
|
164
|
+
#
|
56
165
|
sleep
|
data/examples/server/sse.rb
CHANGED
@@ -1,17 +1,35 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
# Run with: bundle exec examples/server/sse
|
2
|
+
# Run with: bundle exec examples/server/sse.rb
|
3
3
|
|
4
4
|
require 'bundler/setup'
|
5
5
|
require 'h2/server'
|
6
6
|
|
7
|
+
# SSE / event source example
|
8
|
+
#
|
9
|
+
# NOTE: this is a TLS-enabled "h2" type of HTTP/2 server. we will need some
|
10
|
+
# cryptography to get going. this will check for the existence of a git-
|
11
|
+
# ignored set of testing CA, server, and client certs/keys, creating
|
12
|
+
# them as needed.
|
13
|
+
#
|
14
|
+
# see: test/support/create_certs.rb
|
15
|
+
#
|
16
|
+
require File.expand_path '../../../test/support/create_certs', __FILE__
|
17
|
+
|
18
|
+
# crank up the logger level for testing/example purposes
|
19
|
+
#
|
7
20
|
H2::Logger.level = ::Logger::DEBUG
|
8
|
-
H2.verbose!
|
9
21
|
|
10
|
-
port
|
11
|
-
addr
|
12
|
-
|
13
|
-
data, key = Hash.new {|h,k| h[k] = ''}, nil
|
22
|
+
port = 1234
|
23
|
+
addr = Socket.getaddrinfo('localhost', port).first[3]
|
24
|
+
puts "*** Starting server on https://localhost:#{port}"
|
14
25
|
|
26
|
+
# using SNI, we can negotiate TLS for multiple certificates based on the
|
27
|
+
# requested servername.
|
28
|
+
#
|
29
|
+
# see: https://en.wikipedia.org/wiki/Server_Name_Indication
|
30
|
+
# see: https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html#servername_cb
|
31
|
+
#
|
32
|
+
certs_dir = File.expand_path '../../../tmp/certs', __FILE__
|
15
33
|
sni = {
|
16
34
|
'localhost' => {
|
17
35
|
:cert => certs_dir + '/server.crt',
|
@@ -20,52 +38,145 @@ sni = {
|
|
20
38
|
}
|
21
39
|
}
|
22
40
|
|
41
|
+
# this example is a bit more involved and requires more complicated
|
42
|
+
# html and javascript. these two vars are the base for a "poor-man's"
|
43
|
+
# implemenation of sinatra-style inline templates.
|
44
|
+
#
|
45
|
+
data, key = Hash.new {|h,k| h[k] = ''}, nil
|
46
|
+
|
47
|
+
# for example purposes, we're just going to use a top-level array for
|
48
|
+
# keeping track of connected +H2::Server::Stream::EventSource+ objects.
|
49
|
+
#
|
50
|
+
# NOTE: these are not "hijacked" sockets, like a websocket might be. since
|
51
|
+
# streams are multiplexed over one HTTP/2 TCP connection, each object
|
52
|
+
# only represents that stream, and should not hold up other streams
|
53
|
+
# on the connection.
|
54
|
+
#
|
23
55
|
event_sources = []
|
24
56
|
|
25
|
-
|
57
|
+
# create h2 server on the given address and port using the given SNI +Hash+
|
58
|
+
# for configuring TLS negotiation. the constructor requires a block that will
|
59
|
+
# be called on each connection.
|
60
|
+
#
|
26
61
|
s = H2::Server::HTTPS.new host: addr, port: port, sni: sni do |connection|
|
62
|
+
|
63
|
+
# each connection will have 0 or more streams, so we must give the
|
64
|
+
# connection a stream handler block via the +#each_stream+ method.
|
65
|
+
#
|
27
66
|
connection.each_stream do |stream|
|
67
|
+
|
68
|
+
# check the request path (HTTP/2 psuedo-header ':path')
|
69
|
+
#
|
70
|
+
# see +H2::Server::Stream#request+
|
71
|
+
#
|
28
72
|
case stream.request.path
|
29
73
|
when '/favicon.ico'
|
74
|
+
|
75
|
+
# since this is a TLS-enabled server, we could actually test it with a
|
76
|
+
# real browser, which will undoubtedly request /favicon.ico.
|
77
|
+
#
|
78
|
+
# see +H2::Server::Stream#respond+
|
79
|
+
#
|
30
80
|
stream.respond status: 404
|
31
81
|
|
32
82
|
when '/events'
|
33
|
-
|
83
|
+
|
84
|
+
# check request method
|
85
|
+
#
|
86
|
+
case stream.request.method
|
87
|
+
when :get
|
88
|
+
|
89
|
+
# respond with headers turning this stream into an event source, and
|
90
|
+
# stash it in our top-level array.
|
91
|
+
#
|
92
|
+
# see +H2::Server::Stream#to_eventsource+
|
93
|
+
# see +H2::Server::Stream::EventSource+
|
94
|
+
#
|
95
|
+
begin
|
96
|
+
event_sources << stream.to_eventsource
|
97
|
+
rescue H2::Server::StreamError
|
98
|
+
stream.respond status: 400
|
99
|
+
end
|
100
|
+
|
101
|
+
when :delete
|
102
|
+
|
103
|
+
# handle a DELETE /events request by sending a final "die" event, then
|
104
|
+
# closing all connected event sources.
|
105
|
+
#
|
106
|
+
event_sources.each {|es| es.event name: 'die', data: 'later!' rescue nil }
|
34
107
|
event_sources.each &:close
|
35
108
|
event_sources.clear
|
109
|
+
|
110
|
+
# respond with the 200 "ok" status
|
111
|
+
#
|
36
112
|
stream.respond status: 200
|
113
|
+
|
37
114
|
else
|
38
|
-
|
115
|
+
stream.respond status: 404
|
39
116
|
end
|
40
117
|
|
41
118
|
when '/msg'
|
119
|
+
|
120
|
+
# check to make sure this is a POST request
|
121
|
+
#
|
42
122
|
if stream.request.method == :post
|
123
|
+
|
124
|
+
# handle a POST /msg request and send the received body to all
|
125
|
+
# connected event sources as the data of an event named "msg".
|
126
|
+
#
|
43
127
|
msg = stream.request.body
|
44
|
-
event_sources.each {|es| es.event name: 'msg', data: msg}
|
128
|
+
event_sources.each {|es| es.event name: 'msg', data: msg rescue nil }
|
129
|
+
|
130
|
+
# respond with the 201 "created" status
|
131
|
+
#
|
45
132
|
stream.respond status: 201
|
133
|
+
|
46
134
|
else
|
135
|
+
|
136
|
+
# 404 if not post
|
137
|
+
#
|
47
138
|
stream.respond status: 404
|
48
139
|
end
|
49
140
|
|
50
141
|
when '/sse.js'
|
142
|
+
|
143
|
+
# to further the push promise example a bit, here we respond with a 404
|
144
|
+
# if the client requests the script we've linked in the HTML. this means
|
145
|
+
# the *only* way for a client to get that script is to receive the push.
|
146
|
+
#
|
51
147
|
stream.respond status: 404,
|
52
148
|
body: "should have been pushed..."
|
53
149
|
|
54
150
|
else
|
151
|
+
|
152
|
+
# initiate a push promise sub-stream, and queue the "keep" handler.
|
153
|
+
# since a push promise may be canceled, we queue the handler on the server reactor,
|
154
|
+
# after initiating the stream with headers, so that the client has a chance to
|
155
|
+
# cancel with a RST_STREAM frame.
|
156
|
+
#
|
157
|
+
# see +H2::Server::Stream#push_promise+
|
158
|
+
#
|
55
159
|
stream.push_promise path: '/sse.js',
|
56
160
|
headers: { 'content-type' => 'application/javascript' },
|
57
161
|
body: data[:javascript]
|
58
162
|
|
163
|
+
# respond with 200 and HTML body
|
164
|
+
#
|
165
|
+
# see +H2::Server::Stream#respond+
|
166
|
+
#
|
59
167
|
stream.respond status: 200, body: data[:html]
|
168
|
+
|
60
169
|
end
|
61
170
|
end
|
62
171
|
end
|
63
172
|
|
173
|
+
# "poor-man's" sinatra-style inline "templates"
|
174
|
+
#
|
64
175
|
DATA.each_line do |l|
|
65
176
|
if l.start_with?('@@')
|
66
177
|
key = l.strip[2..-1].to_sym
|
67
178
|
else
|
68
|
-
data[key] << l
|
179
|
+
data[key] << l unless l.empty?
|
69
180
|
end
|
70
181
|
end
|
71
182
|
|
@@ -86,18 +197,54 @@ __END__
|
|
86
197
|
</form>
|
87
198
|
<br/>
|
88
199
|
<div><ol id="list"></ol></div>
|
200
|
+
<hr/>
|
201
|
+
<input id="delete" type="button" value="close all"/>
|
89
202
|
</body>
|
90
203
|
</html>
|
91
204
|
|
92
205
|
@@javascript
|
206
|
+
//
|
207
|
+
// client code for SSE/eventsource example
|
208
|
+
//
|
209
|
+
var sse;
|
93
210
|
document.addEventListener('DOMContentLoaded', () => {
|
94
|
-
|
211
|
+
|
212
|
+
// fire up a new EventSource instance. this will initiate the GET /events
|
213
|
+
// request with 'text/event-stream' accept header. it will also continue to
|
214
|
+
// try reconnecting if the connection closes enexpectedly.
|
215
|
+
//
|
216
|
+
sse = new EventSource('/events');
|
217
|
+
|
218
|
+
// add event listeners as normal client JS event handler functions, where the
|
219
|
+
// event "name" is the value given with the `name:` keyword to
|
220
|
+
// H2::Server::Stream::EventSource#event.
|
221
|
+
//
|
95
222
|
sse.addEventListener('msg', (msg) => {
|
223
|
+
|
224
|
+
// in this case, we're listening for the "msg" event and simply adding a
|
225
|
+
// new item to the list already in the DOM with the given data.
|
226
|
+
//
|
96
227
|
let item = document.createElement('li');
|
97
228
|
item.innerHTML = msg.data;
|
98
229
|
document.getElementById('list').appendChild(item);
|
99
230
|
});
|
100
231
|
|
232
|
+
// since SSE will keep trying to reconnect, we want a way to signal a stop
|
233
|
+
// to that. listen for the "die" event and close the EventSource.
|
234
|
+
//
|
235
|
+
sse.addEventListener('die', (e) => {
|
236
|
+
console.log('got die event:', e.data);
|
237
|
+
sse.close();
|
238
|
+
console.log('closed:', sse);
|
239
|
+
document.getElementById('words').setAttribute('disabled', 'disabled');
|
240
|
+
});
|
241
|
+
document.getElementById('delete').onclick = (e) => {
|
242
|
+
fetch('/events', {method: 'delete'});
|
243
|
+
};
|
244
|
+
|
245
|
+
// for the sake of the example, we supply a input field/form and hijack
|
246
|
+
// the "submit" event to post it to our server.
|
247
|
+
//
|
101
248
|
let w = document.getElementById('words');
|
102
249
|
document.getElementById('say').onsubmit = (e) => {
|
103
250
|
e.preventDefault();
|
data/exe/h2
CHANGED
@@ -5,7 +5,6 @@
|
|
5
5
|
#
|
6
6
|
# ---
|
7
7
|
|
8
|
-
require 'colored'
|
9
8
|
require 'optparse'
|
10
9
|
|
11
10
|
begin # {{{
|
@@ -23,11 +22,13 @@ options = {
|
|
23
22
|
debug: false,
|
24
23
|
headers: {},
|
25
24
|
goaway: false,
|
26
|
-
method:
|
25
|
+
method: :get,
|
27
26
|
tls: {},
|
28
27
|
verbose: false
|
29
28
|
}
|
30
29
|
|
30
|
+
options[:tls][:ca_file] = ENV['H2_CAFILE'] if ENV['H2_CAFILE']
|
31
|
+
|
31
32
|
OptionParser.new do |o|
|
32
33
|
|
33
34
|
o.banner = 'Usage: h2 [options] URL'
|
@@ -36,7 +37,7 @@ OptionParser.new do |o|
|
|
36
37
|
options[:block] = true
|
37
38
|
end
|
38
39
|
|
39
|
-
o.on '--cafile [FILE]', String,
|
40
|
+
o.on '--cafile [FILE]', String, "certificate authority bundle (overrides ENV['H2_CAFILE'])" do |ca|
|
40
41
|
raise ArgumentError, "invalid CA file: #{ca}" unless ca && File.exist?(ca)
|
41
42
|
options[:tls][:ca_file] = ca
|
42
43
|
end
|
@@ -72,6 +73,7 @@ OptionParser.new do |o|
|
|
72
73
|
end
|
73
74
|
|
74
75
|
o.on '-H [VALUE]', '--header [VALUE]', String, 'include header in request (format: "key: value")' do |h|
|
76
|
+
warn "psuedo-headers not supported via CLI" if h[0] == ':'
|
75
77
|
kv = h.split(':').map &:strip
|
76
78
|
options[:headers][kv[0]] = kv[1]
|
77
79
|
end
|
@@ -86,16 +88,16 @@ OptionParser.new do |o|
|
|
86
88
|
options[:method] = meth
|
87
89
|
end
|
88
90
|
|
89
|
-
o.on '--version', 'print version information' do
|
90
|
-
puts "#{H2::USER_AGENT['user-agent']}
|
91
|
+
o.on '-V', '--version', 'print version information' do
|
92
|
+
puts "#{H2::USER_AGENT['user-agent']}"
|
93
|
+
puts " * http-2-#{HTTP2::VERSION}"
|
94
|
+
puts " * #{OpenSSL::OPENSSL_VERSION}"
|
95
|
+
puts " * CA file: #{options[:tls][:ca_file]}" if options[:tls][:ca_file]
|
91
96
|
exit
|
92
97
|
end
|
93
98
|
|
94
99
|
end.parse!
|
95
100
|
|
96
|
-
options[:method] ||= :get
|
97
|
-
options[:tls][:ca_file] ||= ENV['H2_CAFILE'] if ENV['H2_CAFILE']
|
98
|
-
|
99
101
|
# }}}
|
100
102
|
|
101
103
|
# --- parse URL {{{
|
@@ -116,8 +118,8 @@ client[:tls] = options[:tls] unless options[:tls].empty?
|
|
116
118
|
|
117
119
|
c = H2::Client.new **client do |c|
|
118
120
|
if options[:debug]
|
119
|
-
c.client.on(:frame_received) {|f| puts "<< #{f.inspect}"
|
120
|
-
c.client.on(:frame_sent) {|f| puts ">> #{f.inspect}"
|
121
|
+
c.client.on(:frame_received) {|f| puts "<< #{f.inspect}" }
|
122
|
+
c.client.on(:frame_sent) {|f| puts ">> #{f.inspect}" }
|
121
123
|
end
|
122
124
|
end
|
123
125
|
|
data/h2.gemspec
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# coding: utf-8
|
2
|
-
|
2
|
+
require File.expand_path '../lib/h2/version', __FILE__
|
3
3
|
|
4
4
|
Gem::Specification.new do |spec|
|
5
5
|
spec.name = "h2"
|
@@ -18,9 +18,8 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.required_ruby_version = '>= 2.2'
|
19
19
|
|
20
20
|
spec.add_dependency 'http-2', '~> 0.10', '>= 0.10.0'
|
21
|
-
spec.add_dependency 'colored', '1.2'
|
22
21
|
|
23
|
-
spec.add_development_dependency "bundler", "~> 1.
|
24
|
-
spec.add_development_dependency "rake", "~>
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
23
|
+
spec.add_development_dependency "rake", "~> 12.0"
|
25
24
|
spec.add_development_dependency "minitest", "~> 5.0"
|
26
25
|
end
|
data/lib/h2.rb
CHANGED
@@ -36,17 +36,6 @@ module H2
|
|
36
36
|
|
37
37
|
class << self
|
38
38
|
|
39
|
-
# turn on extra verbose debug logging
|
40
|
-
#
|
41
|
-
def verbose!
|
42
|
-
@verbose = true
|
43
|
-
end
|
44
|
-
|
45
|
-
def verbose?
|
46
|
-
@verbose = false unless defined?(@verbose)
|
47
|
-
@verbose
|
48
|
-
end
|
49
|
-
|
50
39
|
# convenience wrappers to make requests with HTTP methods
|
51
40
|
#
|
52
41
|
# @see H2.request
|
@@ -140,7 +129,6 @@ module H2
|
|
140
129
|
module FrameDebugger
|
141
130
|
|
142
131
|
def self.included base
|
143
|
-
H2.verbose!
|
144
132
|
base::PARSER_EVENTS.push :frame_sent, :frame_received
|
145
133
|
end
|
146
134
|
|
data/lib/h2/client.rb
CHANGED
@@ -66,29 +66,29 @@ module H2
|
|
66
66
|
#
|
67
67
|
def connect
|
68
68
|
@socket = TCPSocket.new(@host, @port)
|
69
|
-
@socket = tls_socket
|
69
|
+
@socket = tls_socket socket if @tls
|
70
70
|
read
|
71
71
|
end
|
72
72
|
|
73
73
|
def connected?
|
74
|
-
|
74
|
+
!!socket
|
75
75
|
end
|
76
76
|
|
77
77
|
# @return true if the connection is closed
|
78
78
|
#
|
79
79
|
def closed?
|
80
|
-
connected? &&
|
80
|
+
connected? && socket.closed?
|
81
81
|
end
|
82
82
|
|
83
83
|
# close the connection
|
84
84
|
#
|
85
85
|
def close
|
86
86
|
unblock!
|
87
|
-
|
87
|
+
socket.close unless closed?
|
88
88
|
end
|
89
89
|
|
90
90
|
def eof?
|
91
|
-
|
91
|
+
socket.eof?
|
92
92
|
end
|
93
93
|
|
94
94
|
# send a goaway frame and wait until the connection is closed
|
@@ -204,7 +204,7 @@ module H2
|
|
204
204
|
# maintain a ivar for the +Array+ to send to +IO.select+
|
205
205
|
#
|
206
206
|
def selector
|
207
|
-
@selector ||= [
|
207
|
+
@selector ||= [socket]
|
208
208
|
end
|
209
209
|
|
210
210
|
# creates a new +Thread+ to read the given number of bytes each loop from
|
@@ -268,7 +268,7 @@ module H2
|
|
268
268
|
# @param [Integer] maxlen maximum number of bytes to read
|
269
269
|
#
|
270
270
|
def read_from_socket maxlen
|
271
|
-
|
271
|
+
socket.read_nonblock maxlen
|
272
272
|
rescue IO::WaitReadable
|
273
273
|
:wait_readable
|
274
274
|
end
|
@@ -290,12 +290,12 @@ module H2
|
|
290
290
|
def on_frame bytes
|
291
291
|
on :frame, bytes
|
292
292
|
|
293
|
-
if ::H2::Client::TCPSocket ===
|
293
|
+
if ::H2::Client::TCPSocket === socket
|
294
294
|
total = bytes.bytesize
|
295
295
|
loop do
|
296
296
|
n = write_to_socket bytes
|
297
297
|
if n == :wait_writable
|
298
|
-
IO.select nil,
|
298
|
+
IO.select nil, socket.selector
|
299
299
|
elsif n < total
|
300
300
|
bytes = bytes.byteslice n, total
|
301
301
|
else
|
@@ -303,9 +303,9 @@ module H2
|
|
303
303
|
end
|
304
304
|
end
|
305
305
|
else
|
306
|
-
|
306
|
+
socket.write bytes
|
307
307
|
end
|
308
|
-
|
308
|
+
socket.flush
|
309
309
|
end
|
310
310
|
|
311
311
|
# frame_sent callback for parser: used to wait for initial settings frame
|
@@ -324,7 +324,7 @@ module H2
|
|
324
324
|
# @param [String] bytes
|
325
325
|
#
|
326
326
|
def write_to_socket bytes
|
327
|
-
|
327
|
+
socket.write_nonblock bytes
|
328
328
|
rescue IO::WaitWritable
|
329
329
|
:wait_writable
|
330
330
|
end
|
@@ -405,11 +405,11 @@ module H2
|
|
405
405
|
module ExceptionlessIO
|
406
406
|
|
407
407
|
def read_from_socket maxlen
|
408
|
-
|
408
|
+
socket.read_nonblock maxlen, exception: false
|
409
409
|
end
|
410
410
|
|
411
411
|
def write_to_socket bytes
|
412
|
-
|
412
|
+
socket.write_nonblock bytes, exception: false
|
413
413
|
end
|
414
414
|
|
415
415
|
end
|
data/lib/h2/server.rb
CHANGED
@@ -6,19 +6,6 @@ module H2
|
|
6
6
|
|
7
7
|
CONTENT_LENGTH_KEY = 'content-length'
|
8
8
|
|
9
|
-
class << self
|
10
|
-
|
11
|
-
def alpn?
|
12
|
-
!jruby? && OpenSSL::OPENSSL_VERSION_NUMBER >= ALPN_OPENSSL_MIN_VERSION && RUBY_VERSION >= '2.3'
|
13
|
-
end
|
14
|
-
|
15
|
-
def jruby?
|
16
|
-
return @jruby if defined? @jruby
|
17
|
-
@jruby = RUBY_ENGINE == 'jruby'
|
18
|
-
end
|
19
|
-
|
20
|
-
end
|
21
|
-
|
22
9
|
# base H2 server, a +Celluoid::IO+ production
|
23
10
|
#
|
24
11
|
class Server
|
data/lib/h2/server/connection.rb
CHANGED
@@ -27,11 +27,15 @@ module H2
|
|
27
27
|
@parser = ::HTTP2::Server.new
|
28
28
|
@attached = true
|
29
29
|
|
30
|
+
# set a default stream handler that raises +NotImplementedError+
|
31
|
+
#
|
32
|
+
@each_stream = ->(s){ raise NotImplementedError }
|
33
|
+
|
30
34
|
yield self if block_given?
|
31
35
|
|
32
36
|
bind_events
|
33
37
|
|
34
|
-
Logger.debug "new H2::Connection: #{self}"
|
38
|
+
Logger.debug "new H2::Connection: #{self}"
|
35
39
|
end
|
36
40
|
|
37
41
|
# is this connection still attached to the server reactor?
|
@@ -94,7 +98,7 @@ module H2
|
|
94
98
|
|
95
99
|
rescue => e
|
96
100
|
Logger.error "Exception: #{e.message} - closing socket"
|
97
|
-
STDERR.puts e.backtrace if H2.
|
101
|
+
STDERR.puts e.backtrace if H2::Logger.level == ::Logger::DEBUG
|
98
102
|
close
|
99
103
|
|
100
104
|
end
|
data/lib/h2/server/stream.rb
CHANGED
@@ -24,7 +24,8 @@ module H2
|
|
24
24
|
:data
|
25
25
|
]
|
26
26
|
|
27
|
-
attr_reader :
|
27
|
+
attr_reader :complete,
|
28
|
+
:connection,
|
28
29
|
:push_promises,
|
29
30
|
:request,
|
30
31
|
:response,
|
@@ -62,7 +63,7 @@ module H2
|
|
62
63
|
# task on the reactor to deliver the data
|
63
64
|
#
|
64
65
|
def push_promise *args
|
65
|
-
pp = push_promise_for
|
66
|
+
pp = push_promise_for(*args)
|
66
67
|
make_promise pp
|
67
68
|
@connection.server.async.handle_push_promise pp
|
68
69
|
end
|
@@ -95,7 +96,7 @@ module H2
|
|
95
96
|
if block
|
96
97
|
@complete = block
|
97
98
|
elsif @completed = (@responded and push_promises_complete?)
|
98
|
-
@complete[] if Proc ===
|
99
|
+
@complete[] if Proc === complete
|
99
100
|
true
|
100
101
|
else
|
101
102
|
false
|
@@ -149,14 +150,14 @@ module H2
|
|
149
150
|
# called by +@stream+ when this stream is activated
|
150
151
|
#
|
151
152
|
def on_active
|
152
|
-
log :debug, 'active'
|
153
|
+
log :debug, 'active'
|
153
154
|
@request = H2::Server::Stream::Request.new self
|
154
155
|
end
|
155
156
|
|
156
157
|
# called by +@stream+ when this stream is closed
|
157
158
|
#
|
158
159
|
def on_close
|
159
|
-
log :debug, 'close'
|
160
|
+
log :debug, 'close'
|
160
161
|
on_complete
|
161
162
|
@closed = true
|
162
163
|
end
|
@@ -165,14 +166,14 @@ module H2
|
|
165
166
|
#
|
166
167
|
def on_headers h
|
167
168
|
incoming_headers = Hash[h]
|
168
|
-
log :debug, "headers: #{incoming_headers}"
|
169
|
+
log :debug, "headers: #{incoming_headers}"
|
169
170
|
@request.headers.merge! incoming_headers
|
170
171
|
end
|
171
172
|
|
172
173
|
# called by +@stream+ with a +String+ body part
|
173
174
|
#
|
174
175
|
def on_data d
|
175
|
-
log :debug, "data: <<#{d}>>"
|
176
|
+
log :debug, "data: <<#{d}>>"
|
176
177
|
@request.body << d
|
177
178
|
end
|
178
179
|
|
@@ -180,7 +181,7 @@ module H2
|
|
180
181
|
# is ready for response(s)
|
181
182
|
#
|
182
183
|
def on_half_close
|
183
|
-
log :debug, 'half_close'
|
184
|
+
log :debug, 'half_close'
|
184
185
|
connection.server.async.handle_stream self
|
185
186
|
end
|
186
187
|
|
@@ -45,7 +45,7 @@ module H2
|
|
45
45
|
def init_response
|
46
46
|
headers = SSE_HEADER.merge @headers
|
47
47
|
@parser.headers stringify_headers(headers)
|
48
|
-
rescue ::HTTP2::Error::StreamClosed
|
48
|
+
rescue ::HTTP2::Error::StreamClosed
|
49
49
|
@stream.log :warn, "stream closed early by client"
|
50
50
|
end
|
51
51
|
|
data/lib/h2/stream.rb
CHANGED
data/lib/h2/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: h2
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.
|
4
|
+
version: 0.8.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kenichi Nakamura
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-08-
|
11
|
+
date: 2018-08-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: http-2
|
@@ -30,48 +30,34 @@ dependencies:
|
|
30
30
|
- - ">="
|
31
31
|
- !ruby/object:Gem::Version
|
32
32
|
version: 0.10.0
|
33
|
-
- !ruby/object:Gem::Dependency
|
34
|
-
name: colored
|
35
|
-
requirement: !ruby/object:Gem::Requirement
|
36
|
-
requirements:
|
37
|
-
- - '='
|
38
|
-
- !ruby/object:Gem::Version
|
39
|
-
version: '1.2'
|
40
|
-
type: :runtime
|
41
|
-
prerelease: false
|
42
|
-
version_requirements: !ruby/object:Gem::Requirement
|
43
|
-
requirements:
|
44
|
-
- - '='
|
45
|
-
- !ruby/object:Gem::Version
|
46
|
-
version: '1.2'
|
47
33
|
- !ruby/object:Gem::Dependency
|
48
34
|
name: bundler
|
49
35
|
requirement: !ruby/object:Gem::Requirement
|
50
36
|
requirements:
|
51
37
|
- - "~>"
|
52
38
|
- !ruby/object:Gem::Version
|
53
|
-
version: '1.
|
39
|
+
version: '1.16'
|
54
40
|
type: :development
|
55
41
|
prerelease: false
|
56
42
|
version_requirements: !ruby/object:Gem::Requirement
|
57
43
|
requirements:
|
58
44
|
- - "~>"
|
59
45
|
- !ruby/object:Gem::Version
|
60
|
-
version: '1.
|
46
|
+
version: '1.16'
|
61
47
|
- !ruby/object:Gem::Dependency
|
62
48
|
name: rake
|
63
49
|
requirement: !ruby/object:Gem::Requirement
|
64
50
|
requirements:
|
65
51
|
- - "~>"
|
66
52
|
- !ruby/object:Gem::Version
|
67
|
-
version: '
|
53
|
+
version: '12.0'
|
68
54
|
type: :development
|
69
55
|
prerelease: false
|
70
56
|
version_requirements: !ruby/object:Gem::Requirement
|
71
57
|
requirements:
|
72
58
|
- - "~>"
|
73
59
|
- !ruby/object:Gem::Version
|
74
|
-
version: '
|
60
|
+
version: '12.0'
|
75
61
|
- !ruby/object:Gem::Dependency
|
76
62
|
name: minitest
|
77
63
|
requirement: !ruby/object:Gem::Requirement
|
@@ -99,7 +85,6 @@ files:
|
|
99
85
|
- CHANGELOG.md
|
100
86
|
- CODE_OF_CONDUCT.md
|
101
87
|
- Gemfile
|
102
|
-
- Guardfile
|
103
88
|
- LICENSE.txt
|
104
89
|
- README.md
|
105
90
|
- Rakefile
|
data/Guardfile
DELETED