toycol 0.0.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +29 -0
  3. data/.github/dependabot.yml +9 -0
  4. data/.github/workflows/main.yml +4 -1
  5. data/.gitignore +1 -0
  6. data/.rubocop.yml +26 -1
  7. data/CHANGELOG.md +1 -1
  8. data/README.md +2 -4
  9. data/examples/duck/Gemfile +5 -0
  10. data/examples/duck/Protocolfile.duck +23 -0
  11. data/examples/duck/config_duck.ru +59 -0
  12. data/examples/rubylike/Gemfile +5 -0
  13. data/examples/rubylike/Protocolfile.rubylike +18 -0
  14. data/examples/rubylike/config_rubylike.ru +27 -0
  15. data/examples/safe_ruby/Gemfile +6 -0
  16. data/examples/safe_ruby/Protocolfile.safe_ruby +47 -0
  17. data/examples/safe_ruby/config_safe_ruby.ru +55 -0
  18. data/examples/safe_ruby_with_sinatra/Gemfile +10 -0
  19. data/examples/safe_ruby_with_sinatra/Protocolfile.safe_ruby_with_sinatra +44 -0
  20. data/examples/safe_ruby_with_sinatra/app.rb +28 -0
  21. data/examples/safe_ruby_with_sinatra/post.rb +34 -0
  22. data/examples/safe_ruby_with_sinatra/views/index.erb +32 -0
  23. data/examples/unsafe_ruby/Gemfile +5 -0
  24. data/examples/unsafe_ruby/Protocolfile.unsafe_ruby +15 -0
  25. data/examples/unsafe_ruby/config_unsafe_ruby.ru +24 -0
  26. data/exe/toycol +6 -0
  27. data/lib/rack/handler/toycol.rb +65 -0
  28. data/lib/toycol.rb +20 -1
  29. data/lib/toycol/client.rb +38 -0
  30. data/lib/toycol/command.rb +132 -0
  31. data/lib/toycol/const.rb +102 -0
  32. data/lib/toycol/helper.rb +27 -0
  33. data/lib/toycol/protocol.rb +126 -0
  34. data/lib/toycol/proxy.rb +116 -0
  35. data/lib/toycol/server.rb +124 -0
  36. data/lib/toycol/version.rb +1 -1
  37. data/toycol.gemspec +3 -7
  38. 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "toycol", path: "../../"
@@ -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,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/toycol"
5
+
6
+ Toycol::Command.run(ARGV)
@@ -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
- # Your code goes here...
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
@@ -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