toycol 0.1.0 → 0.2.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/.github/dependabot.yml +9 -0
- data/.rubocop.yml +9 -0
- data/examples/safe_ruby/Gemfile +1 -0
- data/examples/safe_ruby_with_sinatra/Gemfile +1 -0
- data/{bin → exe}/toycol +0 -0
- data/lib/rack/handler/toycol.rb +49 -14
- data/lib/toycol.rb +1 -0
- data/lib/toycol/command.rb +58 -22
- data/lib/toycol/const.rb +21 -0
- data/lib/toycol/proxy.rb +4 -2
- data/lib/toycol/server.rb +123 -0
- data/lib/toycol/version.rb +1 -1
- data/toycol.gemspec +1 -2
- metadata +7 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cfdb9a6d989ee0d0939ff53ded3c9603e08326194e6d8546e09ed0e292557022
|
4
|
+
data.tar.gz: d9b9779d6c2ca6c01710c6ecd1758384654aac1835ea343345ebc68a155ea8cc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 59d599b9b4c781428193c5574d5c093492875046d57d842ca120195a548c752ab343dee372e6c2042cf82009bb7066978c520f099ba3196d0a788fe60248f8e7
|
7
|
+
data.tar.gz: 02033640bf6077a6125578bc6ec453c536639eb8882adbef3fd6eec6f0a2ffbedfe32dc9ef7dbb7deda6d85fafd25cdb006a9baf8e57ac4f4d87289fc38638f5
|
data/.rubocop.yml
CHANGED
@@ -14,6 +14,10 @@ AllCops:
|
|
14
14
|
Style/Documentation:
|
15
15
|
Enabled: false
|
16
16
|
|
17
|
+
Style/StringConcatenation:
|
18
|
+
Exclude:
|
19
|
+
- lib/toycol/server.rb
|
20
|
+
|
17
21
|
Style/StringLiterals:
|
18
22
|
Enabled: true
|
19
23
|
EnforcedStyle: double_quotes
|
@@ -25,9 +29,14 @@ Style/StringLiteralsInInterpolation:
|
|
25
29
|
Layout/LineLength:
|
26
30
|
Max: 120
|
27
31
|
|
32
|
+
Layout/HashAlignment:
|
33
|
+
Exclude:
|
34
|
+
- lib/toycol/server.rb
|
35
|
+
|
28
36
|
Metrics:
|
29
37
|
Exclude:
|
30
38
|
- lib/toycol/proxy.rb
|
39
|
+
- lib/toycol/server.rb
|
31
40
|
|
32
41
|
Metrics/AbcSize:
|
33
42
|
Max: 30
|
data/examples/safe_ruby/Gemfile
CHANGED
data/{bin → exe}/toycol
RENAMED
File without changes
|
data/lib/rack/handler/toycol.rb
CHANGED
@@ -1,24 +1,59 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "rack"
|
3
4
|
require "rack/handler"
|
4
|
-
require "rack/handler/puma"
|
5
5
|
|
6
6
|
module Rack
|
7
7
|
module Handler
|
8
8
|
class Toycol
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
host
|
15
|
-
port
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
9
|
+
class << self
|
10
|
+
attr_writer :preferred_background_server, :host, :port
|
11
|
+
|
12
|
+
def run(app, _ = {})
|
13
|
+
@app = app
|
14
|
+
@host ||= ::Toycol::DEFAULT_HOST
|
15
|
+
@port ||= "9292"
|
16
|
+
|
17
|
+
if (child_pid = fork)
|
18
|
+
::Toycol::Proxy.new(@host, @port).start
|
19
|
+
Process.waitpid(child_pid)
|
20
|
+
else
|
21
|
+
run_background_server
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def select_background_server
|
28
|
+
case @preferred_background_server
|
29
|
+
when "puma"
|
30
|
+
return "puma" if puma_requireable?
|
31
|
+
|
32
|
+
puts "Puma is not installed in your environment."
|
33
|
+
raise LoadError
|
34
|
+
when nil
|
35
|
+
puma_requireable? ? "puma" : "build_in"
|
36
|
+
else
|
37
|
+
"build_in"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def puma_requireable?
|
42
|
+
require "rack/handler/puma"
|
43
|
+
true
|
44
|
+
rescue LoadError
|
45
|
+
false
|
46
|
+
end
|
47
|
+
|
48
|
+
def run_background_server
|
49
|
+
case select_background_server
|
50
|
+
when "puma"
|
51
|
+
puts "Toycol starts Puma in single mode, listening on unix://#{::Toycol::UNIX_SOCKET_PATH}"
|
52
|
+
Rack::Handler::Puma.run(@app, **{ Host: ::Toycol::UNIX_SOCKET_PATH, Silent: true })
|
53
|
+
else
|
54
|
+
puts "Toycol starts build-in server, listening on unix://#{::Toycol::UNIX_SOCKET_PATH}"
|
55
|
+
::Toycol::Server.run(@app, **{ Path: ::Toycol::UNIX_SOCKET_PATH, Port: @port })
|
56
|
+
end
|
22
57
|
end
|
23
58
|
end
|
24
59
|
end
|
data/lib/toycol.rb
CHANGED
@@ -6,6 +6,7 @@ require_relative "toycol/const"
|
|
6
6
|
require_relative "toycol/helper"
|
7
7
|
require_relative "toycol/protocol"
|
8
8
|
require_relative "toycol/proxy"
|
9
|
+
require_relative "toycol/server"
|
9
10
|
require_relative "rack/handler/toycol"
|
10
11
|
|
11
12
|
Dir["#{FileUtils.pwd}/Protocolfile*"].sort.each { |f| load f }
|
data/lib/toycol/command.rb
CHANGED
@@ -8,16 +8,15 @@ module Toycol
|
|
8
8
|
class Options
|
9
9
|
class << self
|
10
10
|
def parse!(argv)
|
11
|
-
options
|
12
|
-
option_parser
|
13
|
-
|
11
|
+
options = {}
|
12
|
+
option_parser = create_option_parser
|
13
|
+
sub_command_option_parser = create_sub_command_option_parser
|
14
14
|
|
15
15
|
begin
|
16
16
|
option_parser.order!(argv)
|
17
17
|
options[:command] = argv.shift
|
18
|
-
options[:request_message] = argv.shift if %w[client c].include?
|
19
|
-
|
20
|
-
sub_command_parser[options[:command]].parse!(argv)
|
18
|
+
options[:request_message] = argv.shift if %w[client c].include?(options[:command]) && argv.first != "-h"
|
19
|
+
sub_command_option_parser[options[:command]].parse!(argv)
|
21
20
|
rescue OptionParser::MissingArgument, OptionParser::InvalidOption, ArgumentError => e
|
22
21
|
abort e.message
|
23
22
|
end
|
@@ -27,44 +26,66 @@ module Toycol
|
|
27
26
|
|
28
27
|
def create_option_parser
|
29
28
|
OptionParser.new do |opt|
|
30
|
-
opt.banner = "Usage: #{opt.program_name} [-h|--help] [-v|--version]
|
31
|
-
display_adding_summary(opt)
|
32
|
-
|
33
|
-
opt.on_head("-h", "--help", "Show this message") do
|
34
|
-
puts opt.help
|
35
|
-
exit
|
36
|
-
end
|
29
|
+
opt.banner = "Usage: #{opt.program_name} [-h|--help] [-v|--version] COMMAND [arg...]"
|
37
30
|
|
38
31
|
opt.on_head("-v", "--version", "Show Toycol version") do
|
39
32
|
opt.version = Toycol::VERSION
|
40
33
|
puts opt.ver
|
41
34
|
exit
|
42
35
|
end
|
36
|
+
opt.on_head("-h", "--help", "Show this message") { help_command(opt) }
|
37
|
+
|
38
|
+
opt.separator ""
|
39
|
+
opt.separator "Sub commands:"
|
40
|
+
sub_command_summaries.each do |command|
|
41
|
+
opt.separator [opt.summary_indent, command[:name].ljust(31), command[:summary]].join(" ")
|
42
|
+
end
|
43
43
|
end
|
44
44
|
end
|
45
45
|
|
46
|
-
def
|
46
|
+
def create_sub_command_option_parser
|
47
47
|
sub_command_parser = Hash.new { |_k, v| raise ArgumentError, "'#{v}' is not sub command" }
|
48
48
|
sub_command_parser["client"] = client_option_parser
|
49
49
|
sub_command_parser["c"] = client_option_parser
|
50
|
+
sub_command_parser["server"] = server_option_parser
|
51
|
+
sub_command_parser["s"] = server_option_parser
|
50
52
|
sub_command_parser
|
51
53
|
end
|
52
54
|
|
53
55
|
private
|
54
56
|
|
57
|
+
def sub_command_summaries
|
58
|
+
[
|
59
|
+
{ name: "client REQUEST_MESSAGE -p PORT", summary: "Send request message to server" },
|
60
|
+
{ name: "server -u SERVER_NAME", summary: "Start proxy and background server" }
|
61
|
+
]
|
62
|
+
end
|
63
|
+
|
55
64
|
def client_option_parser
|
56
65
|
OptionParser.new do |opt|
|
57
|
-
opt.on("-p
|
58
|
-
::Toycol::Client.port =
|
66
|
+
opt.on("-p PORT_NUMBER", "--port PORT_NUMBER", "listen on PORT (default: 9292)") do |port|
|
67
|
+
::Toycol::Client.port = port
|
59
68
|
end
|
69
|
+
|
70
|
+
opt.on_head("-h", "--help", "Show this message") { help_command(opt) }
|
60
71
|
end
|
61
72
|
end
|
62
73
|
|
63
|
-
def
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
74
|
+
def server_option_parser
|
75
|
+
OptionParser.new do |opt|
|
76
|
+
opt.on("-o HOST", "--host HOST", "bind to HOST (default: localhost)") do |host|
|
77
|
+
::Rack::Handler::Toycol.host = host
|
78
|
+
end
|
79
|
+
|
80
|
+
opt.on("-p PORT_NUMBER", "--port PORT_NUMBER", "listen on PORT (default: 9292)") do |port|
|
81
|
+
::Rack::Handler::Toycol.port = port
|
82
|
+
end
|
83
|
+
|
84
|
+
opt.on("-u SERVER_NAME", "--use SERVER_NAME", "switch using SERVER(puma/build_in)") do |server_name|
|
85
|
+
::Rack::Handler::Toycol.preferred_background_server = server_name
|
86
|
+
end
|
87
|
+
|
88
|
+
opt.on_head("-h", "--help", "Show this message") { help_command(opt) }
|
68
89
|
end
|
69
90
|
end
|
70
91
|
|
@@ -73,6 +94,17 @@ module Toycol
|
|
73
94
|
{ name: "client -p=PORT_NUMBER", summary: "Send request to server" }
|
74
95
|
]
|
75
96
|
end
|
97
|
+
|
98
|
+
def server_command_help_messages
|
99
|
+
[
|
100
|
+
{ name: "server -u=SERVER_NAME", summary: "Start proxy & background server" }
|
101
|
+
]
|
102
|
+
end
|
103
|
+
|
104
|
+
def help_command(parser)
|
105
|
+
puts parser.help
|
106
|
+
exit
|
107
|
+
end
|
76
108
|
end
|
77
109
|
end
|
78
110
|
|
@@ -89,7 +121,11 @@ module Toycol
|
|
89
121
|
command = options.delete(:command)
|
90
122
|
|
91
123
|
case command
|
92
|
-
when "client", "c"
|
124
|
+
when "client", "c"
|
125
|
+
::Toycol::Client.execute!(options[:request_message])
|
126
|
+
when "server", "s"
|
127
|
+
ARGV.push("-q", "-s", "toycol")
|
128
|
+
Rack::Server.start
|
93
129
|
end
|
94
130
|
end
|
95
131
|
end
|
data/lib/toycol/const.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Toycol
|
4
|
+
# For HTTP Protocol
|
4
5
|
DEFAULT_HTTP_REQUEST_METHODS = %w[
|
5
6
|
GET
|
6
7
|
HEAD
|
@@ -77,5 +78,25 @@ module Toycol
|
|
77
78
|
511 => "Network Authentication Required"
|
78
79
|
}.freeze
|
79
80
|
|
81
|
+
# For environment
|
82
|
+
ENVIRONMENT = ENV["RACK_ENV"] || "development"
|
83
|
+
DEFAULT_HOST = ENVIRONMENT == "development" ? "localhost" : "0.0.0.0"
|
84
|
+
|
85
|
+
# For connection from proxy server to app server
|
80
86
|
UNIX_SOCKET_PATH = ENV["TOYCOL_SOCKET_PATH"] || "/tmp/toycol.socket"
|
87
|
+
|
88
|
+
# Rack compartible environment
|
89
|
+
PATH_INFO = "PATH_INFO"
|
90
|
+
QUERY_STRING = "QUERY_STRING"
|
91
|
+
REQUEST_METHOD = "REQUEST_METHOD"
|
92
|
+
SERVER_NAME = "SERVER_NAME"
|
93
|
+
SERVER_PORT = "SERVER_PORT"
|
94
|
+
CONTENT_LENGTH = "CONTENT_LENGTH"
|
95
|
+
RACK_VERSION = "rack.version"
|
96
|
+
RACK_INPUT = "rack.input"
|
97
|
+
RACK_ERRORS = "rack.errors"
|
98
|
+
RACK_MULTITHREAD = "rack.multithread"
|
99
|
+
RACK_MULTIPROCESS = "rack.multiprocess"
|
100
|
+
RACK_RUN_ONCE = "rack.run_once"
|
101
|
+
RACK_URL_SCHEME = "rack.url_scheme"
|
81
102
|
end
|
data/lib/toycol/proxy.rb
CHANGED
@@ -92,9 +92,11 @@ module Toycol
|
|
92
92
|
response_message = response_message.join
|
93
93
|
puts "[Toycol] Received response message from server: #{response_message.lines.first}"
|
94
94
|
|
95
|
-
|
95
|
+
response_line = response_message.lines.first
|
96
|
+
status_number = response_line[9..11]
|
97
|
+
status_message = response_line[12..].strip
|
96
98
|
|
97
|
-
if (custom_message = @protocol.status_message(
|
99
|
+
if (custom_message = @protocol.status_message(status_number.to_i)) != status_message
|
98
100
|
response_message = response_message.sub(status_message, custom_message)
|
99
101
|
puts "[Toycol] Status message has been translated to custom status message: #{custom_message}"
|
100
102
|
end
|
@@ -0,0 +1,123 @@
|
|
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 => "",
|
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[REQUEST_METHOD] = request_method
|
112
|
+
@env[PATH_INFO] = request_path
|
113
|
+
@env[QUERY_STRING] = query_string || ""
|
114
|
+
|
115
|
+
request_headers.each do |request_header|
|
116
|
+
k, v = request_header.split(":").map(&:strip)
|
117
|
+
@env[k.tr("-", "_").upcase] = v
|
118
|
+
end
|
119
|
+
|
120
|
+
@env[RACK_INPUT] = stringio(request_body)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
data/lib/toycol/version.rb
CHANGED
data/toycol.gemspec
CHANGED
@@ -24,9 +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
|
-
spec.add_dependency "puma"
|
31
30
|
spec.add_dependency "rack", "~> 2.0"
|
32
31
|
end
|
metadata
CHANGED
@@ -1,29 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: toycol
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
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-
|
11
|
+
date: 2021-07-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: puma
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '0'
|
20
|
-
type: :runtime
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - ">="
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '0'
|
27
13
|
- !ruby/object:Gem::Dependency
|
28
14
|
name: rack
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
@@ -41,10 +27,12 @@ dependencies:
|
|
41
27
|
description: Toy Application Protocol framework
|
42
28
|
email:
|
43
29
|
- shioi.mm@gmail.com
|
44
|
-
executables:
|
30
|
+
executables:
|
31
|
+
- toycol
|
45
32
|
extensions: []
|
46
33
|
extra_rdoc_files: []
|
47
34
|
files:
|
35
|
+
- ".github/dependabot.yml"
|
48
36
|
- ".github/workflows/main.yml"
|
49
37
|
- ".gitignore"
|
50
38
|
- ".rubocop.yml"
|
@@ -56,7 +44,6 @@ files:
|
|
56
44
|
- Rakefile
|
57
45
|
- bin/console
|
58
46
|
- bin/setup
|
59
|
-
- bin/toycol
|
60
47
|
- examples/duck/Gemfile
|
61
48
|
- examples/duck/Protocolfile.duck
|
62
49
|
- examples/duck/config_duck.ru
|
@@ -74,6 +61,7 @@ files:
|
|
74
61
|
- examples/unsafe_ruby/Gemfile
|
75
62
|
- examples/unsafe_ruby/Protocolfile.unsafe_ruby
|
76
63
|
- examples/unsafe_ruby/config_unsafe_ruby.ru
|
64
|
+
- exe/toycol
|
77
65
|
- lib/rack/handler/toycol.rb
|
78
66
|
- lib/toycol.rb
|
79
67
|
- lib/toycol/client.rb
|
@@ -82,6 +70,7 @@ files:
|
|
82
70
|
- lib/toycol/helper.rb
|
83
71
|
- lib/toycol/protocol.rb
|
84
72
|
- lib/toycol/proxy.rb
|
73
|
+
- lib/toycol/server.rb
|
85
74
|
- lib/toycol/version.rb
|
86
75
|
- toycol.gemspec
|
87
76
|
homepage: https://github.com/shioimm/toycol
|