toycol 0.0.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|