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,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sinatra/base"
|
4
|
+
require "sinatra/reloader"
|
5
|
+
require "toycol"
|
6
|
+
require_relative "post"
|
7
|
+
|
8
|
+
Toycol::Protocol.use(:safe_ruby_with_sinatra)
|
9
|
+
|
10
|
+
class App < Sinatra::Base
|
11
|
+
set :server, :toycol
|
12
|
+
set :port, 9292
|
13
|
+
|
14
|
+
get "/posts" do
|
15
|
+
@posts = params[:user_id] ? Post.where(user_id: params[:user_id]) : Post.all
|
16
|
+
|
17
|
+
erb :index
|
18
|
+
end
|
19
|
+
|
20
|
+
post "/posts" do
|
21
|
+
Post.new(user_id: params[:user_id], body: params[:body])
|
22
|
+
@posts = Post.all
|
23
|
+
|
24
|
+
erb :index
|
25
|
+
end
|
26
|
+
|
27
|
+
run! if app_file == $PROGRAM_NAME
|
28
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Post
|
4
|
+
@posts ||= []
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def all
|
8
|
+
@posts
|
9
|
+
end
|
10
|
+
|
11
|
+
def where(user_id: nil)
|
12
|
+
@posts.select do |post|
|
13
|
+
post.user_id == Integer(user_id) if user_id
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def insert(record)
|
18
|
+
@posts << record
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :user_id, :body
|
23
|
+
|
24
|
+
def initialize(user_id:, body:)
|
25
|
+
@user_id = Integer(user_id)
|
26
|
+
@body = body
|
27
|
+
|
28
|
+
self.class.insert self
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Initial records
|
33
|
+
Post.new(user_id: 1, body: "I love Ruby!")
|
34
|
+
Post.new(user_id: 2, body: "I love RubyKaigi!")
|
@@ -0,0 +1,32 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
<meta charset="UTF-8">
|
4
|
+
</head>
|
5
|
+
|
6
|
+
<body>
|
7
|
+
<h1>This app is running on Safe Ruby protocol</h1>
|
8
|
+
|
9
|
+
<div>
|
10
|
+
<ul>
|
11
|
+
<% @posts.each do |post| %>
|
12
|
+
<li><%= "User<#{post.user_id}> #{post.body}" %></li>
|
13
|
+
<% end %>
|
14
|
+
</ul>
|
15
|
+
</div>
|
16
|
+
|
17
|
+
<div>
|
18
|
+
<form action='/posts' method='post'>
|
19
|
+
<div>
|
20
|
+
<label>User ID</label>
|
21
|
+
<input type="number" name="user_id">
|
22
|
+
</div>
|
23
|
+
<div>
|
24
|
+
<label>Content</label>
|
25
|
+
<input type="text" name="body">
|
26
|
+
</div>
|
27
|
+
<div>
|
28
|
+
<input type="submit" value="Post">
|
29
|
+
</div>
|
30
|
+
</form>
|
31
|
+
</body>
|
32
|
+
</html>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
Toycol::Protocol.define(:ruby) do |message|
|
2
|
+
using Module.new {
|
3
|
+
refine String do
|
4
|
+
def get
|
5
|
+
Toycol::Protocol.request.path do |message|
|
6
|
+
/['"](?<path>.+)['"]/.match(message)[:path]
|
7
|
+
end
|
8
|
+
|
9
|
+
Toycol::Protocol.request.http_method { |_| "GET" }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
}
|
13
|
+
|
14
|
+
instance_eval message
|
15
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack"
|
4
|
+
require "toycol"
|
5
|
+
|
6
|
+
Toycol::Protocol.use(:ruby)
|
7
|
+
|
8
|
+
class App
|
9
|
+
def call(env)
|
10
|
+
case env["REQUEST_METHOD"]
|
11
|
+
when "GET"
|
12
|
+
case env["PATH_INFO"]
|
13
|
+
when "/posts"
|
14
|
+
[
|
15
|
+
200,
|
16
|
+
{ "Content-Type" => "text/html" },
|
17
|
+
["I love Ruby!\n", "I've successfully accessed using instance_eval!\n"]
|
18
|
+
]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
run App.new
|
data/exe/toycol
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack"
|
4
|
+
require "rack/handler"
|
5
|
+
|
6
|
+
module Rack
|
7
|
+
module Handler
|
8
|
+
class Toycol
|
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
|
+
raise LoadError, "Puma is not installed in your environment."
|
33
|
+
when nil
|
34
|
+
puma_requireable? ? "puma" : "build_in"
|
35
|
+
else
|
36
|
+
"build_in"
|
37
|
+
end
|
38
|
+
rescue LoadError
|
39
|
+
Process.kill(:INT, Process.ppid)
|
40
|
+
abort
|
41
|
+
end
|
42
|
+
|
43
|
+
def puma_requireable?
|
44
|
+
require "rack/handler/puma"
|
45
|
+
true
|
46
|
+
rescue LoadError
|
47
|
+
false
|
48
|
+
end
|
49
|
+
|
50
|
+
def run_background_server
|
51
|
+
case select_background_server
|
52
|
+
when "puma"
|
53
|
+
puts "Toycol starts Puma in single mode, listening on unix://#{::Toycol::UNIX_SOCKET_PATH}"
|
54
|
+
Rack::Handler::Puma.run(@app, **{ Host: ::Toycol::UNIX_SOCKET_PATH, Silent: true })
|
55
|
+
else
|
56
|
+
puts "Toycol starts build-in server, listening on unix://#{::Toycol::UNIX_SOCKET_PATH}"
|
57
|
+
::Toycol::Server.run(@app, **{ Path: ::Toycol::UNIX_SOCKET_PATH, Port: @port })
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
register :toycol, Toycol
|
64
|
+
end
|
65
|
+
end
|
data/lib/toycol.rb
CHANGED
@@ -1,8 +1,27 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "fileutils"
|
4
|
+
|
5
|
+
require_relative "toycol/const"
|
6
|
+
require_relative "toycol/helper"
|
7
|
+
require_relative "toycol/protocol"
|
8
|
+
require_relative "toycol/proxy"
|
9
|
+
require_relative "toycol/server"
|
10
|
+
require_relative "rack/handler/toycol"
|
11
|
+
|
12
|
+
Dir["#{FileUtils.pwd}/Protocolfile*"].sort.each { |f| load f }
|
13
|
+
|
14
|
+
require_relative "toycol/command"
|
3
15
|
require_relative "toycol/version"
|
4
16
|
|
5
17
|
module Toycol
|
6
18
|
class Error < StandardError; end
|
7
|
-
|
19
|
+
|
20
|
+
class UnauthorizedMethodError < Error; end
|
21
|
+
|
22
|
+
class UnauthorizedRequestError < Error; end
|
23
|
+
|
24
|
+
class UndefinedRequestMethodError < Error; end
|
25
|
+
|
26
|
+
class UnknownStatusCodeError < Error; end
|
8
27
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "socket"
|
4
|
+
|
5
|
+
module Toycol
|
6
|
+
class Client
|
7
|
+
@port = 9292
|
8
|
+
CHUNK_SIZE = 1024 * 16
|
9
|
+
|
10
|
+
class << self
|
11
|
+
attr_writer :port
|
12
|
+
|
13
|
+
def execute!(request_message, &block)
|
14
|
+
socket = TCPSocket.new("localhost", @port)
|
15
|
+
socket.write(request_message)
|
16
|
+
puts "[Toycol] Sent request message: #{request_message}\n---"
|
17
|
+
|
18
|
+
response_message = []
|
19
|
+
response_message << socket.readpartial(CHUNK_SIZE) until socket.eof?
|
20
|
+
response_message = response_message.join
|
21
|
+
|
22
|
+
block ||= default_proc
|
23
|
+
block.call(response_message)
|
24
|
+
ensure
|
25
|
+
socket.close
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def default_proc
|
31
|
+
proc do |message|
|
32
|
+
puts "[Toycol] Received response message:\n\n"
|
33
|
+
puts message
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
require_relative "./client"
|
5
|
+
|
6
|
+
module Toycol
|
7
|
+
class Command
|
8
|
+
class Options
|
9
|
+
class << self
|
10
|
+
def parse!(argv)
|
11
|
+
options = {}
|
12
|
+
option_parser = create_option_parser
|
13
|
+
sub_command_option_parser = create_sub_command_option_parser
|
14
|
+
|
15
|
+
begin
|
16
|
+
option_parser.order!(argv)
|
17
|
+
options[:command] = argv.shift
|
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)
|
20
|
+
rescue OptionParser::MissingArgument, OptionParser::InvalidOption, ArgumentError => e
|
21
|
+
abort e.message
|
22
|
+
end
|
23
|
+
|
24
|
+
options
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_option_parser
|
28
|
+
OptionParser.new do |opt|
|
29
|
+
opt.banner = "Usage: #{opt.program_name} [-h|--help] [-v|--version] COMMAND [arg...]"
|
30
|
+
|
31
|
+
opt.on_head("-v", "--version", "Show Toycol version") do
|
32
|
+
opt.version = Toycol::VERSION
|
33
|
+
puts opt.ver
|
34
|
+
exit
|
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
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def create_sub_command_option_parser
|
47
|
+
sub_command_parser = Hash.new { |_k, v| raise ArgumentError, "'#{v}' is not sub command" }
|
48
|
+
sub_command_parser["client"] = client_option_parser
|
49
|
+
sub_command_parser["c"] = client_option_parser
|
50
|
+
sub_command_parser["server"] = server_option_parser
|
51
|
+
sub_command_parser["s"] = server_option_parser
|
52
|
+
sub_command_parser
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
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
|
+
|
64
|
+
def client_option_parser
|
65
|
+
OptionParser.new do |opt|
|
66
|
+
opt.on("-p PORT_NUMBER", "--port PORT_NUMBER", "listen on PORT (default: 9292)") do |port|
|
67
|
+
::Toycol::Client.port = port
|
68
|
+
end
|
69
|
+
|
70
|
+
opt.on_head("-h", "--help", "Show this message") { help_command(opt) }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
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) }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def client_command_help_messages
|
93
|
+
[
|
94
|
+
{ name: "client -p=PORT_NUMBER", summary: "Send request to server" }
|
95
|
+
]
|
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
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.run(argv)
|
112
|
+
new(argv).execute
|
113
|
+
end
|
114
|
+
|
115
|
+
def initialize(argv)
|
116
|
+
@argv = argv
|
117
|
+
end
|
118
|
+
|
119
|
+
def execute
|
120
|
+
options = Options.parse!(@argv)
|
121
|
+
command = options.delete(:command)
|
122
|
+
|
123
|
+
case command
|
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
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
data/lib/toycol/const.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Toycol
|
4
|
+
# For HTTP Protocol
|
5
|
+
DEFAULT_HTTP_REQUEST_METHODS = %w[
|
6
|
+
GET
|
7
|
+
HEAD
|
8
|
+
POST
|
9
|
+
OPTIONS
|
10
|
+
PUT
|
11
|
+
DELETE
|
12
|
+
TRACE
|
13
|
+
PATCH
|
14
|
+
LINK
|
15
|
+
UNLINK
|
16
|
+
].freeze
|
17
|
+
|
18
|
+
DEFAULT_HTTP_STATUS_CODES = {
|
19
|
+
100 => "Continue",
|
20
|
+
101 => "Switching Protocols",
|
21
|
+
102 => "Processing",
|
22
|
+
200 => "OK",
|
23
|
+
201 => "Created",
|
24
|
+
202 => "Accepted",
|
25
|
+
203 => "Non-Authoritative Information",
|
26
|
+
204 => "No Content",
|
27
|
+
205 => "Reset Content",
|
28
|
+
206 => "Partial Content",
|
29
|
+
207 => "Multi-Status",
|
30
|
+
208 => "Already Reported",
|
31
|
+
226 => "IM Used",
|
32
|
+
300 => "Multiple Choices",
|
33
|
+
301 => "Moved Permanently",
|
34
|
+
302 => "Found",
|
35
|
+
303 => "See Other",
|
36
|
+
304 => "Not Modified",
|
37
|
+
305 => "Use Proxy",
|
38
|
+
307 => "Temporary Redirect",
|
39
|
+
308 => "Permanent Redirect",
|
40
|
+
400 => "Bad Request",
|
41
|
+
401 => "Unauthorized",
|
42
|
+
402 => "Payment Required",
|
43
|
+
403 => "Forbidden",
|
44
|
+
404 => "Not Found",
|
45
|
+
405 => "Method Not Allowed",
|
46
|
+
406 => "Not Acceptable",
|
47
|
+
407 => "Proxy Authentication Required",
|
48
|
+
408 => "Request Timeout",
|
49
|
+
409 => "Conflict",
|
50
|
+
410 => "Gone",
|
51
|
+
411 => "Length Required",
|
52
|
+
412 => "Precondition Failed",
|
53
|
+
413 => "Payload Too Large",
|
54
|
+
414 => "URI Too Long",
|
55
|
+
415 => "Unsupported Media Type",
|
56
|
+
416 => "Range Not Satisfiable",
|
57
|
+
417 => "Expectation Failed",
|
58
|
+
418 => "I'm A Teapot",
|
59
|
+
421 => "Misdirected Request",
|
60
|
+
422 => "Unprocessable Entity",
|
61
|
+
423 => "Locked",
|
62
|
+
424 => "Failed Dependency",
|
63
|
+
426 => "Upgrade Required",
|
64
|
+
428 => "Precondition Required",
|
65
|
+
429 => "Too Many Requests",
|
66
|
+
431 => "Request Header Fields Too Large",
|
67
|
+
451 => "Unavailable For Legal Reasons",
|
68
|
+
500 => "Internal Server Error",
|
69
|
+
501 => "Not Implemented",
|
70
|
+
502 => "Bad Gateway",
|
71
|
+
503 => "Service Unavailable",
|
72
|
+
504 => "Gateway Timeout",
|
73
|
+
505 => "HTTP Version Not Supported",
|
74
|
+
506 => "Variant Also Negotiates",
|
75
|
+
507 => "Insufficient Storage",
|
76
|
+
508 => "Loop Detected",
|
77
|
+
510 => "Not Extended",
|
78
|
+
511 => "Network Authentication Required"
|
79
|
+
}.freeze
|
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
|
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"
|
102
|
+
end
|