toycol 0.0.1 → 0.2.2
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/.github/ISSUE_TEMPLATE/bug_report.md +29 -0
- data/.github/dependabot.yml +9 -0
- data/.github/workflows/main.yml +4 -1
- data/.gitignore +1 -0
- data/.rubocop.yml +26 -1
- data/CHANGELOG.md +1 -1
- data/README.md +2 -4
- data/examples/duck/Gemfile +5 -0
- data/examples/duck/Protocolfile.duck +23 -0
- data/examples/duck/config_duck.ru +59 -0
- data/examples/rubylike/Gemfile +5 -0
- data/examples/rubylike/Protocolfile.rubylike +18 -0
- data/examples/rubylike/config_rubylike.ru +27 -0
- data/examples/safe_ruby/Gemfile +6 -0
- data/examples/safe_ruby/Protocolfile.safe_ruby +47 -0
- data/examples/safe_ruby/config_safe_ruby.ru +55 -0
- data/examples/safe_ruby_with_sinatra/Gemfile +10 -0
- data/examples/safe_ruby_with_sinatra/Protocolfile.safe_ruby_with_sinatra +44 -0
- data/examples/safe_ruby_with_sinatra/app.rb +28 -0
- data/examples/safe_ruby_with_sinatra/post.rb +34 -0
- data/examples/safe_ruby_with_sinatra/views/index.erb +32 -0
- data/examples/unsafe_ruby/Gemfile +5 -0
- data/examples/unsafe_ruby/Protocolfile.unsafe_ruby +15 -0
- data/examples/unsafe_ruby/config_unsafe_ruby.ru +24 -0
- data/exe/toycol +6 -0
- data/lib/rack/handler/toycol.rb +65 -0
- data/lib/toycol.rb +20 -1
- data/lib/toycol/client.rb +38 -0
- data/lib/toycol/command.rb +132 -0
- data/lib/toycol/const.rb +102 -0
- data/lib/toycol/helper.rb +27 -0
- data/lib/toycol/protocol.rb +126 -0
- data/lib/toycol/proxy.rb +116 -0
- data/lib/toycol/server.rb +124 -0
- data/lib/toycol/version.rb +1 -1
- data/toycol.gemspec +3 -7
- metadata +48 -5
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Toycol
|
4
|
+
module Helper
|
5
|
+
private
|
6
|
+
|
7
|
+
def safe_execution!(&block)
|
8
|
+
safe_executionable_tp.enable(&block)
|
9
|
+
end
|
10
|
+
|
11
|
+
def safe_executionable_tp
|
12
|
+
@safe_executionable_tp ||= TracePoint.new(:script_compiled) do |tp|
|
13
|
+
if tp.binding.receiver == Toycol::Protocol && tp.method_id.to_s.match?(unauthorized_methods_regex)
|
14
|
+
raise Toycol::UnauthorizedMethodError, <<~ERROR
|
15
|
+
- Unauthorized method was called!
|
16
|
+
You can't use methods that may cause injections in your protocol.
|
17
|
+
Ex. Kernel.#eval, Kernel.#exec, Kernel.#require and so on.
|
18
|
+
ERROR
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def unauthorized_methods_regex
|
24
|
+
/(.*eval|.*exec|`.+|%x\(|system|open|require|load)/
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Toycol
|
4
|
+
# This class is for protocol definition and parsing request messages
|
5
|
+
class Protocol
|
6
|
+
@definements = {}
|
7
|
+
@protocol_name = nil
|
8
|
+
@http_status_codes = Toycol::DEFAULT_HTTP_STATUS_CODES.dup
|
9
|
+
@http_request_methods = Toycol::DEFAULT_HTTP_REQUEST_METHODS.dup
|
10
|
+
@custom_status_codes = nil
|
11
|
+
@additional_request_methods = nil
|
12
|
+
|
13
|
+
class << self
|
14
|
+
# For protocol definition
|
15
|
+
def define(protocol_name = nil, &block)
|
16
|
+
@definements[protocol_name] = block
|
17
|
+
end
|
18
|
+
|
19
|
+
# For application which use the protocol
|
20
|
+
def use(protocol_name)
|
21
|
+
@protocol_name = protocol_name
|
22
|
+
end
|
23
|
+
|
24
|
+
# For server which use the protocol
|
25
|
+
def run!(message)
|
26
|
+
@request_message = message.chomp
|
27
|
+
|
28
|
+
return unless (block = @definements[@protocol_name])
|
29
|
+
|
30
|
+
instance_exec(@request_message, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
# For protocol definition: Define custom status codes
|
34
|
+
if RUBY_VERSION >= "2.7"
|
35
|
+
def custom_status_codes(**custom_status_codes)
|
36
|
+
@custom_status_codes = custom_status_codes
|
37
|
+
end
|
38
|
+
else
|
39
|
+
def custom_status_codes(custom_status_codes)
|
40
|
+
@custom_status_codes = custom_status_codes
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# For protocol definition: Define adding request methods
|
45
|
+
def additional_request_methods(*additional_request_methods)
|
46
|
+
@additional_request_methods = additional_request_methods
|
47
|
+
end
|
48
|
+
|
49
|
+
# For protocol definition: Define how to parse the request message
|
50
|
+
def request
|
51
|
+
@request ||= Class.new do
|
52
|
+
def self.path(&block)
|
53
|
+
@path = block
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.query(&block)
|
57
|
+
@query = block
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.http_method(&block)
|
61
|
+
@http_method = block
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.input(&block)
|
65
|
+
@input = block
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# For server: Get the request path
|
71
|
+
def request_path
|
72
|
+
request_path = request.instance_variable_get("@path").call(request_message)
|
73
|
+
|
74
|
+
raise UnauthorizedRequestError, "This request path is too long" if request_path.size >= 2048
|
75
|
+
|
76
|
+
if request_path.scan(%r{[/\w\d\-_]}).size < request_path.size
|
77
|
+
raise UnauthorizedRequestError,
|
78
|
+
"This request path contains unauthorized character"
|
79
|
+
end
|
80
|
+
|
81
|
+
request_path
|
82
|
+
end
|
83
|
+
|
84
|
+
# For server: Get the request method
|
85
|
+
def request_method
|
86
|
+
@http_request_methods.concat @additional_request_methods if @additional_request_methods
|
87
|
+
request_method = request.instance_variable_get("@http_method").call(request_message)
|
88
|
+
|
89
|
+
unless @http_request_methods.include? request_method
|
90
|
+
raise UndefinedRequestMethodError, "This request method is undefined"
|
91
|
+
end
|
92
|
+
|
93
|
+
request_method
|
94
|
+
end
|
95
|
+
|
96
|
+
# For server: Get the query string
|
97
|
+
def query
|
98
|
+
return unless (parse_query_block = request.instance_variable_get("@query"))
|
99
|
+
|
100
|
+
parse_query_block.call(request_message)
|
101
|
+
end
|
102
|
+
|
103
|
+
# For server: Get the input body
|
104
|
+
def input
|
105
|
+
return unless (parsed_input_block = request.instance_variable_get("@input"))
|
106
|
+
|
107
|
+
parsed_input_block.call(request_message)
|
108
|
+
end
|
109
|
+
|
110
|
+
# For server: Get the message of status code
|
111
|
+
def status_message(status)
|
112
|
+
@http_status_codes.merge!(@custom_status_codes) if @custom_status_codes
|
113
|
+
|
114
|
+
unless (message = @http_status_codes[status])
|
115
|
+
raise UnknownStatusCodeError, "Application returns unknown status code"
|
116
|
+
end
|
117
|
+
|
118
|
+
message
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
attr_reader :request_message
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
data/lib/toycol/proxy.rb
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "socket"
|
4
|
+
|
5
|
+
module Toycol
|
6
|
+
class Proxy
|
7
|
+
include Helper
|
8
|
+
|
9
|
+
def initialize(host, port)
|
10
|
+
@host = host
|
11
|
+
@port = port
|
12
|
+
@request_method = nil
|
13
|
+
@path = nil
|
14
|
+
@query = nil
|
15
|
+
@input = nil
|
16
|
+
@protocol = ::Toycol::Protocol
|
17
|
+
@proxy = TCPServer.new(@host, @port)
|
18
|
+
end
|
19
|
+
|
20
|
+
CHUNK_SIZE = 1024 * 16
|
21
|
+
|
22
|
+
def start
|
23
|
+
puts <<~MESSAGE
|
24
|
+
Toycol is running on #{@host}:#{@port}
|
25
|
+
=> Use Ctrl-C to stop
|
26
|
+
MESSAGE
|
27
|
+
|
28
|
+
loop do
|
29
|
+
trap(:INT) { shutdown }
|
30
|
+
|
31
|
+
@client = @proxy.accept
|
32
|
+
|
33
|
+
while !@client.closed? && !@client.eof?
|
34
|
+
begin
|
35
|
+
request = @client.readpartial(CHUNK_SIZE)
|
36
|
+
puts "[Toycol] Received message: #{request.inspect.chomp}"
|
37
|
+
|
38
|
+
safe_execution! { @protocol.run!(request) }
|
39
|
+
assign_parsed_attributes!
|
40
|
+
|
41
|
+
http_request_message = build_http_request_message
|
42
|
+
puts "[Toycol] Message has been translated to HTTP request message: #{http_request_message.inspect}"
|
43
|
+
transfer_to_server(http_request_message)
|
44
|
+
rescue StandardError => e
|
45
|
+
puts "#{e.class} #{e.message} - closing socket."
|
46
|
+
e.backtrace.each { |l| puts "\t#{l}" }
|
47
|
+
@proxy.close
|
48
|
+
@client.close
|
49
|
+
end
|
50
|
+
end
|
51
|
+
@client.close
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def assign_parsed_attributes!
|
58
|
+
@request_method = @protocol.request_method
|
59
|
+
@path = @protocol.request_path
|
60
|
+
@query = @protocol.query
|
61
|
+
@input = @protocol.input
|
62
|
+
end
|
63
|
+
|
64
|
+
def build_http_request_message
|
65
|
+
request_message = "#{request_line}#{request_header}\r\n"
|
66
|
+
request_message += @input if @input
|
67
|
+
request_message
|
68
|
+
end
|
69
|
+
|
70
|
+
def request_line
|
71
|
+
"#{@request_method} #{request_path} HTTP/1.1\r\n"
|
72
|
+
end
|
73
|
+
|
74
|
+
def request_path
|
75
|
+
return @path unless @request_method == "GET"
|
76
|
+
|
77
|
+
"#{@path}#{"?#{@query}" if @query && !@query.empty?}"
|
78
|
+
end
|
79
|
+
|
80
|
+
def request_header
|
81
|
+
"Content-Length: #{@input&.bytesize || 0}\r\n"
|
82
|
+
end
|
83
|
+
|
84
|
+
def transfer_to_server(request_message)
|
85
|
+
UNIXSocket.open(Toycol::UNIX_SOCKET_PATH) do |server|
|
86
|
+
server.write request_message
|
87
|
+
server.close_write
|
88
|
+
puts "[Toycol] Successed to Send HTTP request message to server"
|
89
|
+
|
90
|
+
response_message = []
|
91
|
+
response_message << server.readpartial(CHUNK_SIZE) until server.eof?
|
92
|
+
response_message = response_message.join
|
93
|
+
puts "[Toycol] Received response message from server: #{response_message.lines.first}"
|
94
|
+
|
95
|
+
response_line = response_message.lines.first
|
96
|
+
status_number = response_line[9..11]
|
97
|
+
status_message = response_line[12..].strip
|
98
|
+
|
99
|
+
if (custom_message = @protocol.status_message(status_number.to_i)) != status_message
|
100
|
+
response_message = response_message.sub(status_message, custom_message)
|
101
|
+
puts "[Toycol] Status message has been translated to custom status message: #{custom_message}"
|
102
|
+
end
|
103
|
+
|
104
|
+
@client.write response_message
|
105
|
+
@client.close_write
|
106
|
+
puts "[Toycol] Finished to response to client"
|
107
|
+
server.close
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def shutdown
|
112
|
+
puts "[Toycol] Catched SIGINT -> Stop to server"
|
113
|
+
exit
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "stringio"
|
4
|
+
|
5
|
+
module Toycol
|
6
|
+
class Server
|
7
|
+
BACKLOG = 1024
|
8
|
+
CHUNK_SIZE = 1024 * 16
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def run(app, **options)
|
12
|
+
new(app, **options).run
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(app, **options)
|
17
|
+
@app = app
|
18
|
+
@path = options[:Path]
|
19
|
+
@port = options[:Port]
|
20
|
+
@env = default_env
|
21
|
+
@returned_status = nil
|
22
|
+
@returned_headers = nil
|
23
|
+
@returned_body = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def run
|
27
|
+
verify_file_path!
|
28
|
+
server = UNIXServer.new @path
|
29
|
+
server.listen BACKLOG
|
30
|
+
|
31
|
+
loop do
|
32
|
+
trap(:INT) { exit }
|
33
|
+
|
34
|
+
socket = server.accept
|
35
|
+
|
36
|
+
request_message = []
|
37
|
+
request_message << socket.readpartial(CHUNK_SIZE) until socket.eof?
|
38
|
+
request_message = request_message.join
|
39
|
+
assign_parsed_attributes!(request_message)
|
40
|
+
|
41
|
+
@returned_status, @returned_headers, @returned_body = @app.call(@env)
|
42
|
+
|
43
|
+
socket.puts response_message
|
44
|
+
socket.close_write
|
45
|
+
socket.close
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def default_env
|
52
|
+
{
|
53
|
+
::Toycol::PATH_INFO => "",
|
54
|
+
::Toycol::QUERY_STRING => "",
|
55
|
+
::Toycol::REQUEST_METHOD => "",
|
56
|
+
::Toycol::SERVER_NAME => "toycol_server",
|
57
|
+
::Toycol::SERVER_PORT => @port.to_s,
|
58
|
+
::Toycol::CONTENT_LENGTH => "0",
|
59
|
+
::Toycol::RACK_VERSION => Rack::VERSION,
|
60
|
+
::Toycol::RACK_INPUT => stringio(""),
|
61
|
+
::Toycol::RACK_ERRORS => $stderr,
|
62
|
+
::Toycol::RACK_MULTITHREAD => false,
|
63
|
+
::Toycol::RACK_MULTIPROCESS => false,
|
64
|
+
::Toycol::RACK_RUN_ONCE => false,
|
65
|
+
::Toycol::RACK_URL_SCHEME => "http"
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def response_message
|
70
|
+
"#{response_status_code}#{response_headers}\r\n#{response_body}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def response_status_code
|
74
|
+
"HTTP/1.1 #{@returned_status} #{::Toycol::DEFAULT_HTTP_STATUS_CODES[@returned_status.to_i] || "CUSTOM"}\r\n"
|
75
|
+
end
|
76
|
+
|
77
|
+
def response_headers
|
78
|
+
@returned_headers["Content-Length"] = response_body.size unless @returned_headers["Content-Length"]
|
79
|
+
|
80
|
+
@returned_headers.map { |k, v| "#{k}: #{v}" }.join("\r\n") + "\r\n"
|
81
|
+
end
|
82
|
+
|
83
|
+
def response_body
|
84
|
+
res = []
|
85
|
+
@returned_body.each { |body| res << body }
|
86
|
+
res.join
|
87
|
+
end
|
88
|
+
|
89
|
+
def stringio(body = "")
|
90
|
+
StringIO.new(body).set_encoding("ASCII-8BIT")
|
91
|
+
end
|
92
|
+
|
93
|
+
def verify_file_path!
|
94
|
+
return unless File.exist? @path
|
95
|
+
|
96
|
+
begin
|
97
|
+
bound_file = UNIXSocket.new @path
|
98
|
+
rescue SystemCallError, IOError
|
99
|
+
File.unlink @path
|
100
|
+
else
|
101
|
+
bound_file.close
|
102
|
+
raise "[Toycol] Address already in use: #{@path}"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def assign_parsed_attributes!(request_message)
|
107
|
+
request_line, *request_headers, request_body = request_message.split("\r\n").reject(&:empty?)
|
108
|
+
request_method, request_path, = request_line.split
|
109
|
+
request_path, query_string = request_path.split("?")
|
110
|
+
|
111
|
+
@env[::Toycol::REQUEST_METHOD] = request_method
|
112
|
+
@env[::Toycol::PATH_INFO] = request_path
|
113
|
+
@env[::Toycol::QUERY_STRING] = query_string || ""
|
114
|
+
@env[::Toycol::CONTENT_LENGTH]
|
115
|
+
|
116
|
+
request_headers.each do |request_header|
|
117
|
+
k, v = request_header.split(":").map(&:strip)
|
118
|
+
@env["::Toycol::#{k.tr("-", "_").upcase}"] = v
|
119
|
+
end
|
120
|
+
|
121
|
+
@env[::Toycol::RACK_INPUT] = stringio(request_body)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
data/lib/toycol/version.rb
CHANGED
data/toycol.gemspec
CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
|
|
12
12
|
spec.description = "Toy Application Protocol framework"
|
13
13
|
spec.homepage = "https://github.com/shioimm/toycol"
|
14
14
|
spec.license = "MIT"
|
15
|
-
spec.required_ruby_version = Gem::Requirement.new(">=
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
|
16
16
|
|
17
17
|
spec.metadata["homepage_uri"] = spec.homepage
|
18
18
|
spec.metadata["source_code_uri"] = "https://github.com/shioimm/toycol"
|
@@ -24,12 +24,8 @@ Gem::Specification.new do |spec|
|
|
24
24
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
25
25
|
end
|
26
26
|
spec.bindir = "exe"
|
27
|
-
spec.executables
|
27
|
+
spec.executables << "toycol"
|
28
28
|
spec.require_paths = ["lib"]
|
29
29
|
|
30
|
-
|
31
|
-
# spec.add_dependency "example-gem", "~> 1.0"
|
32
|
-
|
33
|
-
# For more information and examples about making a new gem, checkout our
|
34
|
-
# guide at: https://bundler.io/guides/creating_gem.html
|
30
|
+
spec.add_dependency "rack", "~> 2.0"
|
35
31
|
end
|
metadata
CHANGED
@@ -1,22 +1,39 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: toycol
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Misaki Shioi
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-07-
|
12
|
-
dependencies:
|
11
|
+
date: 2021-07-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
13
27
|
description: Toy Application Protocol framework
|
14
28
|
email:
|
15
29
|
- shioi.mm@gmail.com
|
16
|
-
executables:
|
30
|
+
executables:
|
31
|
+
- toycol
|
17
32
|
extensions: []
|
18
33
|
extra_rdoc_files: []
|
19
34
|
files:
|
35
|
+
- ".github/ISSUE_TEMPLATE/bug_report.md"
|
36
|
+
- ".github/dependabot.yml"
|
20
37
|
- ".github/workflows/main.yml"
|
21
38
|
- ".gitignore"
|
22
39
|
- ".rubocop.yml"
|
@@ -28,7 +45,33 @@ files:
|
|
28
45
|
- Rakefile
|
29
46
|
- bin/console
|
30
47
|
- bin/setup
|
48
|
+
- examples/duck/Gemfile
|
49
|
+
- examples/duck/Protocolfile.duck
|
50
|
+
- examples/duck/config_duck.ru
|
51
|
+
- examples/rubylike/Gemfile
|
52
|
+
- examples/rubylike/Protocolfile.rubylike
|
53
|
+
- examples/rubylike/config_rubylike.ru
|
54
|
+
- examples/safe_ruby/Gemfile
|
55
|
+
- examples/safe_ruby/Protocolfile.safe_ruby
|
56
|
+
- examples/safe_ruby/config_safe_ruby.ru
|
57
|
+
- examples/safe_ruby_with_sinatra/Gemfile
|
58
|
+
- examples/safe_ruby_with_sinatra/Protocolfile.safe_ruby_with_sinatra
|
59
|
+
- examples/safe_ruby_with_sinatra/app.rb
|
60
|
+
- examples/safe_ruby_with_sinatra/post.rb
|
61
|
+
- examples/safe_ruby_with_sinatra/views/index.erb
|
62
|
+
- examples/unsafe_ruby/Gemfile
|
63
|
+
- examples/unsafe_ruby/Protocolfile.unsafe_ruby
|
64
|
+
- examples/unsafe_ruby/config_unsafe_ruby.ru
|
65
|
+
- exe/toycol
|
66
|
+
- lib/rack/handler/toycol.rb
|
31
67
|
- lib/toycol.rb
|
68
|
+
- lib/toycol/client.rb
|
69
|
+
- lib/toycol/command.rb
|
70
|
+
- lib/toycol/const.rb
|
71
|
+
- lib/toycol/helper.rb
|
72
|
+
- lib/toycol/protocol.rb
|
73
|
+
- lib/toycol/proxy.rb
|
74
|
+
- lib/toycol/server.rb
|
32
75
|
- lib/toycol/version.rb
|
33
76
|
- toycol.gemspec
|
34
77
|
homepage: https://github.com/shioimm/toycol
|
@@ -46,7 +89,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
46
89
|
requirements:
|
47
90
|
- - ">="
|
48
91
|
- !ruby/object:Gem::Version
|
49
|
-
version:
|
92
|
+
version: 2.6.0
|
50
93
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
94
|
requirements:
|
52
95
|
- - ">="
|