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.
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