toycol 0.0.1 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e39fd4d4d223eef7eb8e86059fd061ebbb24259a1259b7f5b8bef228d6d9836
4
- data.tar.gz: 8d90ae7ed408d76a46da7726a280560d245e1372bca9d2a0857efe7bc22d6a3d
3
+ metadata.gz: 9eb79d412c62ad9b86922964fa583b4fa1ed64b941c74654874b93d385d5665e
4
+ data.tar.gz: 8d915a6bfc7677096164b7fa773e32fad7669ff0b847eb2306be885899298362
5
5
  SHA512:
6
- metadata.gz: 10d74fd171b7eec5618b1aeca4035a3f9bff05c12864285fb43fa83d1d53a97523276a19ac3a6185929e82854ce3701dd0f7d5963a55c38536cbe71828327809
7
- data.tar.gz: a73c3c5c7992ba1bebb34f8db9bfc8061a066a6c2b30ead33c8dfdc79a8b540092f540b793dd67c164dc277766dd7e6ce030b00b17d30148c28f5993fab4157e
6
+ metadata.gz: c988be01719a879b367ea0da7323bc00100d4373f3516ab504e03fb92d26b3c2f0e3fe9e4411f84d139b97100d2e3342cebb4eb2e344674dadcd62ecadbf06bb
7
+ data.tar.gz: fc5ab9812a9f87006052c22614b6cdae7257db0919c3f95f0f30bba5e238b0787ba2c83ba08eb363089d06df41edd68ff5d048e31fb4116fc1a91bfe7823c872
@@ -5,12 +5,15 @@ on: [push,pull_request]
5
5
  jobs:
6
6
  build:
7
7
  runs-on: ubuntu-latest
8
+ strategy:
9
+ matrix:
10
+ ruby: [2.6, 2.7, 3.0, head]
8
11
  steps:
9
12
  - uses: actions/checkout@v2
10
13
  - name: Set up Ruby
11
14
  uses: ruby/setup-ruby@v1
12
15
  with:
13
- ruby-version: 3.0.0
16
+ ruby-version: ${{ matrix.ruby }}
14
17
  bundler-cache: true
15
18
  - name: Run the default task
16
19
  run: bundle exec rake
data/.gitignore CHANGED
@@ -7,4 +7,5 @@
7
7
  /spec/reports/
8
8
  /tmp/
9
9
 
10
+ .ruby-version
10
11
  Gemfile.lock
data/.rubocop.yml CHANGED
@@ -9,7 +9,10 @@ AllCops:
9
9
  - 'Rakefile'
10
10
  - 'toycol.gemspec'
11
11
  NewCops: enable
12
- TargetRubyVersion: 3.0
12
+ TargetRubyVersion: 2.6
13
+
14
+ Style/Documentation:
15
+ Enabled: false
13
16
 
14
17
  Style/StringLiterals:
15
18
  Enabled: true
@@ -21,3 +24,16 @@ Style/StringLiteralsInInterpolation:
21
24
 
22
25
  Layout/LineLength:
23
26
  Max: 120
27
+
28
+ Metrics:
29
+ Exclude:
30
+ - lib/toycol/proxy.rb
31
+
32
+ Metrics/AbcSize:
33
+ Max: 30
34
+
35
+ Metrics/ClassLength:
36
+ Max: 400
37
+
38
+ Metrics/MethodLength:
39
+ Max: 15
data/CHANGELOG.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.1.0] - 2021-07-12
3
+ ## [0.0.1] - 2021-07-12
4
4
 
5
5
  - Initial release
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # Toycol
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/toycol`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ Toy Application Protocol framework
6
4
 
7
5
  ## Installation
8
6
 
@@ -22,7 +20,7 @@ Or install it yourself as:
22
20
 
23
21
  ## Usage
24
22
 
25
- TODO: Write usage instructions here
23
+ WIP
26
24
 
27
25
  ## Development
28
26
 
data/bin/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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "toycol", path: "../../"
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ Toycol::Protocol.define(:duck) do
4
+ custom_status_codes(
5
+ 600 => "I'm afraid you are not a duck..."
6
+ )
7
+ additional_request_methods "OTHER"
8
+
9
+ request.path do |message|
10
+ %r{(?<path>/\w*)}.match(message)[:path]
11
+ end
12
+
13
+ request.query do |message|
14
+ /\?(?<query>.+)/.match(message) { |m| m[:query] }
15
+ end
16
+
17
+ request.http_method do |message|
18
+ case message.scan(/quack/).size
19
+ when 2 then "GET"
20
+ else "OTHER"
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack"
4
+ require "toycol"
5
+
6
+ Toycol::Protocol.use(:duck)
7
+
8
+ # Duck protocol application
9
+ class App
10
+ def call(env)
11
+ case env["REQUEST_METHOD"]
12
+ when "GET"
13
+ case env["PATH_INFO"]
14
+ when "/posts"
15
+ return app_for_get_with_query if env["QUERY_STRING"] == "user_id=1"
16
+
17
+ app_for_get
18
+ when "/" then app_for_get_to_root
19
+ end
20
+ when "OTHER" then app_for_other
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def app_for_get_with_query
27
+ [
28
+ 200,
29
+ { "Content-Type" => "text/html" },
30
+ ["quack quack, I am the No.1 duck\n"]
31
+ ]
32
+ end
33
+
34
+ def app_for_get
35
+ [
36
+ 200,
37
+ { "Content-Type" => "text/html" },
38
+ ["quack quack, quack quack, quack, quack\n", "quack quack, I am the No.1 duck\n"]
39
+ ]
40
+ end
41
+
42
+ def app_for_get_to_root
43
+ [
44
+ 200,
45
+ { "Content-Type" => "text/html" },
46
+ ["Hello, This app is running on Sample duck protocol.\n"]
47
+ ]
48
+ end
49
+
50
+ def app_for_other
51
+ [
52
+ 600,
53
+ { "Content-Type" => "text/html" },
54
+ ["Sorry, this application is only for ducks...\n"]
55
+ ]
56
+ end
57
+ end
58
+
59
+ run App.new
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "toycol", path: "../../"
@@ -0,0 +1,18 @@
1
+ Toycol::Protocol.define(:rubylike) do
2
+ custom_status_codes(
3
+ 700 => "Hmm...",
4
+ )
5
+
6
+ additional_request_methods "OTHER"
7
+
8
+ request.path do |message|
9
+ /['"](?<path>.+)['"]/.match(message)[:path]
10
+ end
11
+
12
+ request.http_method do |message|
13
+ case /\.(?<method>[A-z]+)/.match(message)&.captures&.first
14
+ when "get" then "GET"
15
+ else "OTHER"
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack"
4
+ require "toycol"
5
+
6
+ Toycol::Protocol.use(:rubylike)
7
+
8
+ class App
9
+ def call(env)
10
+ case env["REQUEST_METHOD"]
11
+ when "GET"
12
+ [
13
+ 200,
14
+ { "Content-Type" => "text/html" },
15
+ ["I love ruby!\n", "I love RubyKaigi!\n"]
16
+ ]
17
+ when "OTHER"
18
+ [
19
+ 700,
20
+ { "Content-Type" => "text/html" },
21
+ ["Sorry, but I'd like you to speak more like a Ruby programmer...\n"]
22
+ ]
23
+ end
24
+ end
25
+ end
26
+
27
+ run App.new
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "toycol", path: "../../"
@@ -0,0 +1,47 @@
1
+ module SafeRuby
2
+ PARSER_REGEX = /["'](?<path>\/.*)["']\.(?<method>[A-z]+)/
3
+ QUERY_REGEX = /query?.*\{(?<query>.*)\}/
4
+ INPUT_REGEX = /input?.*\{(?<input>.*)\}/
5
+ end
6
+
7
+ Toycol::Protocol.define(:safe_ruby) do |message|
8
+ using Module.new {
9
+ refine String do
10
+ # Ex. '/posts'.get
11
+ # Ex. (with query string) '/posts'.get(query: { user_id: 2 })
12
+ def get(options = {})
13
+ Toycol::Protocol.request.query { options[:query] } if options[:query]
14
+ Toycol::Protocol.request.http_method { "GET" }
15
+ end
16
+
17
+ # Ex. '/posts'.post(input: { user_id: 1, body: 'This is a post request' })
18
+ def post(options = {})
19
+ Toycol::Protocol.request.input { options[:input] } if options[:input]
20
+ Toycol::Protocol.request.http_method { "POST" }
21
+ end
22
+
23
+ def parse_as_queries
24
+ split(",").map { |str| str.scan(/\w+/).join("=") }
25
+ end
26
+
27
+ def parse_as_inputs
28
+ split(",").map { |str| str.split(":").map { |s| s.strip! && s.gsub(/['"]/, "") }.join("=") }
29
+ end
30
+ end
31
+ }
32
+
33
+ path, method = SafeRuby::PARSER_REGEX.match(message) { |m| [m[:path], m[:method]] }
34
+ query = SafeRuby::QUERY_REGEX.match(message) { |m| m[:query].parse_as_queries }&.join("&")
35
+ input = SafeRuby::INPUT_REGEX.match(message) { |m| m[:input].parse_as_inputs }&.join("&")
36
+ args = {}
37
+
38
+ request.path { path }
39
+
40
+ %i[query input].each do |k|
41
+ if v = binding.local_variable_get(k)
42
+ args[*k] = v
43
+ end
44
+ end
45
+
46
+ request_path.public_send(method, args)
47
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack"
4
+ require "toycol"
5
+
6
+ Toycol::Protocol.use(:safe_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
+ return app_for_get_with_query if env["QUERY_STRING"] == "user_id=2"
15
+
16
+ app_for_get
17
+ end
18
+ when "POST"
19
+ input = env["rack.input"].gets
20
+ created = input.split("&").map { |str| str.split("=") }.to_h
21
+
22
+ app_for_post(user_id: created["user_id"], body: created["body"])
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def app_for_get_with_query
29
+ [
30
+ 200,
31
+ { "Content-Type" => "text/html" },
32
+ ["User<2> I love RubyKaigi!\n"]
33
+ ]
34
+ end
35
+
36
+ def app_for_get
37
+ [
38
+ 200,
39
+ { "Content-Type" => "text/html" },
40
+ ["User<1> I love Ruby!\n", "User<2> I love RubyKaigi!\n"]
41
+ ]
42
+ end
43
+
44
+ def app_for_post(user_id:, body:)
45
+ [
46
+ 201,
47
+ { "Content-Type" => "text/html", "Location" => "/posts" },
48
+ ["User<1> I love Ruby!\n",
49
+ "User<2> I love RubyKaigi!\n",
50
+ "User<#{user_id}> #{body}\n"]
51
+ ]
52
+ end
53
+ end
54
+
55
+ run App.new
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ gem "sinatra"
8
+ gem "sinatra-contrib"
9
+ gem "toycol", path: "../../"
@@ -0,0 +1,44 @@
1
+ module SafeRubyWithSinatra
2
+ PARSER_REGEX = /["'](?<path>\/.*)["']\.(?<method>[A-z]+)/
3
+ QUERY_REGEX = /query?.*\{(?<query>.*)\}/
4
+ INPUT_REGEX = /input?.*\{(?<input>.*)\}/
5
+ end
6
+
7
+ Toycol::Protocol.define(:safe_ruby_with_sinatra) do |message|
8
+ using Module.new {
9
+ refine String do
10
+ def get(options = {})
11
+ Toycol::Protocol.request.query { options[:query] } if options[:query]
12
+ Toycol::Protocol.request.http_method { "GET" }
13
+ end
14
+
15
+ def post(options = {})
16
+ Toycol::Protocol.request.input { options[:input] } if options[:input]
17
+ Toycol::Protocol.request.http_method { "POST" }
18
+ end
19
+
20
+ def parse_as_queries
21
+ split(",").map { |str| str.scan(/\w+/).join("=") }
22
+ end
23
+
24
+ def parse_as_inputs
25
+ split(",").map { |str| str.split(":").map { |s| s.strip! && s.gsub(/['"]/, "") }.join("=") }
26
+ end
27
+ end
28
+ }
29
+
30
+ path, method = SafeRubyWithSinatra::PARSER_REGEX.match(message) { |m| [m[:path], m[:method]] }
31
+ query = SafeRubyWithSinatra::QUERY_REGEX.match(message) { |m| m[:query].parse_as_queries }&.join("&")
32
+ input = SafeRubyWithSinatra::INPUT_REGEX.match(message) { |m| m[:input].parse_as_inputs }&.join("&")
33
+ args = {}
34
+
35
+ request.path { path }
36
+
37
+ %i[query input].each do |k|
38
+ if v = binding.local_variable_get(k)
39
+ args[*k] = v
40
+ end
41
+ end
42
+
43
+ request_path.public_send(method, args)
44
+ end
@@ -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
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/handler"
4
+ require "rack/handler/puma"
5
+
6
+ module Rack
7
+ module Handler
8
+ class Toycol
9
+ def self.run(app, options = {})
10
+ if (child_pid = fork)
11
+ environment = ENV["RACK_ENV"] || "development"
12
+ default_host = environment == "development" ? "localhost" : "0.0.0.0"
13
+
14
+ host = options.delete(:Host) || default_host
15
+ port = options.delete(:Port) || "9292"
16
+
17
+ ::Toycol::Proxy.new(host, port).start
18
+ Process.waitpid(child_pid)
19
+ else
20
+ puts "Toycol starts Puma in single mode, listening on unix://#{::Toycol::UNIX_SOCKET_PATH}"
21
+ Rack::Handler::Puma.run(app, **{ Host: ::Toycol::UNIX_SOCKET_PATH, Silent: true })
22
+ end
23
+ end
24
+ end
25
+
26
+ register :toycol, Toycol
27
+ end
28
+ end
data/lib/toycol.rb CHANGED
@@ -1,8 +1,26 @@
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 "rack/handler/toycol"
10
+
11
+ Dir["#{FileUtils.pwd}/Protocolfile*"].sort.each { |f| load f }
12
+
13
+ require_relative "toycol/command"
3
14
  require_relative "toycol/version"
4
15
 
5
16
  module Toycol
6
17
  class Error < StandardError; end
7
- # Your code goes here...
18
+
19
+ class UnauthorizedMethodError < Error; end
20
+
21
+ class UnauthorizedRequestError < Error; end
22
+
23
+ class UndefinedRequestMethodError < Error; end
24
+
25
+ class UnknownStatusCodeError < Error; end
8
26
  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,96 @@
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_parser = create_sub_command_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]
19
+
20
+ sub_command_parser[options[:command]].parse!(argv)
21
+ rescue OptionParser::MissingArgument, OptionParser::InvalidOption, ArgumentError => e
22
+ abort e.message
23
+ end
24
+
25
+ options
26
+ end
27
+
28
+ def create_option_parser
29
+ OptionParser.new do |opt|
30
+ opt.banner = "Usage: #{opt.program_name} [-h|--help] [-v|--version] <command> <args>"
31
+ display_adding_summary(opt)
32
+
33
+ opt.on_head("-h", "--help", "Show this message") do
34
+ puts opt.help
35
+ exit
36
+ end
37
+
38
+ opt.on_head("-v", "--version", "Show Toycol version") do
39
+ opt.version = Toycol::VERSION
40
+ puts opt.ver
41
+ exit
42
+ end
43
+ end
44
+ end
45
+
46
+ def create_sub_command_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
51
+ end
52
+
53
+ private
54
+
55
+ def client_option_parser
56
+ OptionParser.new do |opt|
57
+ opt.on("-p=PORT_NUMBER", "--port=PORT_NUMBER", "Set port number") do |n|
58
+ ::Toycol::Client.port = n
59
+ end
60
+ end
61
+ end
62
+
63
+ def display_adding_summary(opt)
64
+ opt.separator ""
65
+ opt.separator "Client command options:"
66
+ client_command_help_messages.each do |command|
67
+ opt.separator [opt.summary_indent, command[:name].ljust(31), command[:summary]].join(" ")
68
+ end
69
+ end
70
+
71
+ def client_command_help_messages
72
+ [
73
+ { name: "client -p=PORT_NUMBER", summary: "Send request to server" }
74
+ ]
75
+ end
76
+ end
77
+ end
78
+
79
+ def self.run(argv)
80
+ new(argv).execute
81
+ end
82
+
83
+ def initialize(argv)
84
+ @argv = argv
85
+ end
86
+
87
+ def execute
88
+ options = Options.parse!(@argv)
89
+ command = options.delete(:command)
90
+
91
+ case command
92
+ when "client", "c" then ::Toycol::Client.execute!(options[:request_message])
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Toycol
4
+ DEFAULT_HTTP_REQUEST_METHODS = %w[
5
+ GET
6
+ HEAD
7
+ POST
8
+ OPTIONS
9
+ PUT
10
+ DELETE
11
+ TRACE
12
+ PATCH
13
+ LINK
14
+ UNLINK
15
+ ].freeze
16
+
17
+ DEFAULT_HTTP_STATUS_CODES = {
18
+ 100 => "Continue",
19
+ 101 => "Switching Protocols",
20
+ 102 => "Processing",
21
+ 200 => "OK",
22
+ 201 => "Created",
23
+ 202 => "Accepted",
24
+ 203 => "Non-Authoritative Information",
25
+ 204 => "No Content",
26
+ 205 => "Reset Content",
27
+ 206 => "Partial Content",
28
+ 207 => "Multi-Status",
29
+ 208 => "Already Reported",
30
+ 226 => "IM Used",
31
+ 300 => "Multiple Choices",
32
+ 301 => "Moved Permanently",
33
+ 302 => "Found",
34
+ 303 => "See Other",
35
+ 304 => "Not Modified",
36
+ 305 => "Use Proxy",
37
+ 307 => "Temporary Redirect",
38
+ 308 => "Permanent Redirect",
39
+ 400 => "Bad Request",
40
+ 401 => "Unauthorized",
41
+ 402 => "Payment Required",
42
+ 403 => "Forbidden",
43
+ 404 => "Not Found",
44
+ 405 => "Method Not Allowed",
45
+ 406 => "Not Acceptable",
46
+ 407 => "Proxy Authentication Required",
47
+ 408 => "Request Timeout",
48
+ 409 => "Conflict",
49
+ 410 => "Gone",
50
+ 411 => "Length Required",
51
+ 412 => "Precondition Failed",
52
+ 413 => "Payload Too Large",
53
+ 414 => "URI Too Long",
54
+ 415 => "Unsupported Media Type",
55
+ 416 => "Range Not Satisfiable",
56
+ 417 => "Expectation Failed",
57
+ 418 => "I'm A Teapot",
58
+ 421 => "Misdirected Request",
59
+ 422 => "Unprocessable Entity",
60
+ 423 => "Locked",
61
+ 424 => "Failed Dependency",
62
+ 426 => "Upgrade Required",
63
+ 428 => "Precondition Required",
64
+ 429 => "Too Many Requests",
65
+ 431 => "Request Header Fields Too Large",
66
+ 451 => "Unavailable For Legal Reasons",
67
+ 500 => "Internal Server Error",
68
+ 501 => "Not Implemented",
69
+ 502 => "Bad Gateway",
70
+ 503 => "Service Unavailable",
71
+ 504 => "Gateway Timeout",
72
+ 505 => "HTTP Version Not Supported",
73
+ 506 => "Variant Also Negotiates",
74
+ 507 => "Insufficient Storage",
75
+ 508 => "Loop Detected",
76
+ 510 => "Not Extended",
77
+ 511 => "Network Authentication Required"
78
+ }.freeze
79
+
80
+ UNIX_SOCKET_PATH = ENV["TOYCOL_SOCKET_PATH"] || "/tmp/toycol.socket"
81
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Toycol
4
+ module Helper
5
+ private
6
+
7
+ def safe_execution!(&block)
8
+ safe_executionable_tp.enable(&block)
9
+ end
10
+
11
+ def safe_executionable_tp
12
+ @safe_executionable_tp ||= TracePoint.new(:script_compiled) do |tp|
13
+ if tp.binding.receiver == Toycol::Protocol && tp.method_id.to_s.match?(unauthorized_methods_regex)
14
+ raise Toycol::UnauthorizedMethodError, <<~ERROR
15
+ - Unauthorized method was called!
16
+ You can't use methods that may cause injections in your protocol.
17
+ Ex. Kernel.#eval, Kernel.#exec, Kernel.#require and so on.
18
+ ERROR
19
+ end
20
+ end
21
+ end
22
+
23
+ def unauthorized_methods_regex
24
+ /(.*eval|.*exec|`.+|%x\(|system|open|require|load)/
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Toycol
4
+ # This class is for protocol definition and parsing request messages
5
+ class Protocol
6
+ @definements = {}
7
+ @protocol_name = nil
8
+ @http_status_codes = Toycol::DEFAULT_HTTP_STATUS_CODES.dup
9
+ @http_request_methods = Toycol::DEFAULT_HTTP_REQUEST_METHODS.dup
10
+ @custom_status_codes = nil
11
+ @additional_request_methods = nil
12
+
13
+ class << self
14
+ # For protocol definition
15
+ def define(protocol_name = nil, &block)
16
+ @definements[protocol_name] = block
17
+ end
18
+
19
+ # For application which use the protocol
20
+ def use(protocol_name)
21
+ @protocol_name = protocol_name
22
+ end
23
+
24
+ # For server which use the protocol
25
+ def run!(message)
26
+ @request_message = message.chomp
27
+
28
+ return unless (block = @definements[@protocol_name])
29
+
30
+ instance_exec(@request_message, &block)
31
+ end
32
+
33
+ # For protocol definition: Define custom status codes
34
+ if RUBY_VERSION >= "2.7"
35
+ def custom_status_codes(**custom_status_codes)
36
+ @custom_status_codes = custom_status_codes
37
+ end
38
+ else
39
+ def custom_status_codes(custom_status_codes)
40
+ @custom_status_codes = custom_status_codes
41
+ end
42
+ end
43
+
44
+ # For protocol definition: Define adding request methods
45
+ def additional_request_methods(*additional_request_methods)
46
+ @additional_request_methods = additional_request_methods
47
+ end
48
+
49
+ # For protocol definition: Define how to parse the request message
50
+ def request
51
+ @request ||= Class.new do
52
+ def self.path(&block)
53
+ @path = block
54
+ end
55
+
56
+ def self.query(&block)
57
+ @query = block
58
+ end
59
+
60
+ def self.http_method(&block)
61
+ @http_method = block
62
+ end
63
+
64
+ def self.input(&block)
65
+ @input = block
66
+ end
67
+ end
68
+ end
69
+
70
+ # For server: Get the request path
71
+ def request_path
72
+ request_path = request.instance_variable_get("@path").call(request_message)
73
+
74
+ raise UnauthorizedRequestError, "This request path is too long" if request_path.size >= 2048
75
+
76
+ if request_path.scan(%r{[/\w\d\-_]}).size < request_path.size
77
+ raise UnauthorizedRequestError,
78
+ "This request path contains unauthorized character"
79
+ end
80
+
81
+ request_path
82
+ end
83
+
84
+ # For server: Get the request method
85
+ def request_method
86
+ @http_request_methods.concat @additional_request_methods if @additional_request_methods
87
+ request_method = request.instance_variable_get("@http_method").call(request_message)
88
+
89
+ unless @http_request_methods.include? request_method
90
+ raise UndefinedRequestMethodError, "This request method is undefined"
91
+ end
92
+
93
+ request_method
94
+ end
95
+
96
+ # For server: Get the query string
97
+ def query
98
+ return unless (parse_query_block = request.instance_variable_get("@query"))
99
+
100
+ parse_query_block.call(request_message)
101
+ end
102
+
103
+ # For server: Get the input body
104
+ def input
105
+ return unless (parsed_input_block = request.instance_variable_get("@input"))
106
+
107
+ parsed_input_block.call(request_message)
108
+ end
109
+
110
+ # For server: Get the message of status code
111
+ def status_message(status)
112
+ @http_status_codes.merge!(@custom_status_codes) if @custom_status_codes
113
+
114
+ unless (message = @http_status_codes[status])
115
+ raise UnknownStatusCodeError, "Application returns unknown status code"
116
+ end
117
+
118
+ message
119
+ end
120
+
121
+ private
122
+
123
+ attr_reader :request_message
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+
5
+ module Toycol
6
+ class Proxy
7
+ include Helper
8
+
9
+ def initialize(host, port)
10
+ @host = host
11
+ @port = port
12
+ @request_method = nil
13
+ @path = nil
14
+ @query = nil
15
+ @input = nil
16
+ @protocol = ::Toycol::Protocol
17
+ @proxy = TCPServer.new(@host, @port)
18
+ end
19
+
20
+ CHUNK_SIZE = 1024 * 16
21
+
22
+ def start
23
+ puts <<~MESSAGE
24
+ Toycol is running on #{@host}:#{@port}
25
+ => Use Ctrl-C to stop
26
+ MESSAGE
27
+
28
+ loop do
29
+ trap(:INT) { shutdown }
30
+
31
+ @client = @proxy.accept
32
+
33
+ while !@client.closed? && !@client.eof?
34
+ begin
35
+ request = @client.readpartial(CHUNK_SIZE)
36
+ puts "[Toycol] Received message: #{request.inspect.chomp}"
37
+
38
+ safe_execution! { @protocol.run!(request) }
39
+ assign_parsed_attributes!
40
+
41
+ http_request_message = build_http_request_message
42
+ puts "[Toycol] Message has been translated to HTTP request message: #{http_request_message.inspect}"
43
+ transfer_to_server(http_request_message)
44
+ rescue StandardError => e
45
+ puts "#{e.class} #{e.message} - closing socket."
46
+ e.backtrace.each { |l| puts "\t#{l}" }
47
+ @proxy.close
48
+ @client.close
49
+ end
50
+ end
51
+ @client.close
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def assign_parsed_attributes!
58
+ @request_method = @protocol.request_method
59
+ @path = @protocol.request_path
60
+ @query = @protocol.query
61
+ @input = @protocol.input
62
+ end
63
+
64
+ def build_http_request_message
65
+ request_message = "#{request_line}#{request_header}\r\n"
66
+ request_message += @input if @input
67
+ request_message
68
+ end
69
+
70
+ def request_line
71
+ "#{@request_method} #{request_path} HTTP/1.1\r\n"
72
+ end
73
+
74
+ def request_path
75
+ return @path unless @request_method == "GET"
76
+
77
+ "#{@path}#{"?#{@query}" if @query && !@query.empty?}"
78
+ end
79
+
80
+ def request_header
81
+ "Content-Length: #{@input&.bytesize || 0}\r\n"
82
+ end
83
+
84
+ def transfer_to_server(request_message)
85
+ UNIXSocket.open(Toycol::UNIX_SOCKET_PATH) do |server|
86
+ server.write request_message
87
+ server.close_write
88
+ puts "[Toycol] Successed to Send HTTP request message to server"
89
+
90
+ response_message = []
91
+ response_message << server.readpartial(CHUNK_SIZE) until server.eof?
92
+ response_message = response_message.join
93
+ puts "[Toycol] Received response message from server: #{response_message.lines.first}"
94
+
95
+ _, status_code, status_message = response_message.lines.first.split
96
+
97
+ if (custom_message = @protocol.status_message(status_code.to_i)) != status_message
98
+ response_message = response_message.sub(status_message, custom_message)
99
+ puts "[Toycol] Status message has been translated to custom status message: #{custom_message}"
100
+ end
101
+
102
+ @client.write response_message
103
+ @client.close_write
104
+ puts "[Toycol] Finished to response to client"
105
+ server.close
106
+ end
107
+ end
108
+
109
+ def shutdown
110
+ puts "[Toycol] Catched SIGINT -> Stop to server"
111
+ exit
112
+ end
113
+ end
114
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Toycol
4
- VERSION = "0.0.1"
4
+ VERSION = "0.1.0"
5
5
  end
data/toycol.gemspec CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
12
  spec.description = "Toy Application Protocol framework"
13
13
  spec.homepage = "https://github.com/shioimm/toycol"
14
14
  spec.license = "MIT"
15
- spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
16
16
 
17
17
  spec.metadata["homepage_uri"] = spec.homepage
18
18
  spec.metadata["source_code_uri"] = "https://github.com/shioimm/toycol"
@@ -27,9 +27,6 @@ Gem::Specification.new do |spec|
27
27
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
28
  spec.require_paths = ["lib"]
29
29
 
30
- # Uncomment to register a new dependency of your gem
31
- # spec.add_dependency "example-gem", "~> 1.0"
32
-
33
- # For more information and examples about making a new gem, checkout our
34
- # guide at: https://bundler.io/guides/creating_gem.html
30
+ spec.add_dependency "puma"
31
+ spec.add_dependency "rack", "~> 2.0"
35
32
  end
metadata CHANGED
@@ -1,15 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: toycol
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.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-12 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2021-07-20 00:00:00.000000000 Z
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
+ - !ruby/object:Gem::Dependency
28
+ name: rack
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
13
41
  description: Toy Application Protocol framework
14
42
  email:
15
43
  - shioi.mm@gmail.com
@@ -28,7 +56,32 @@ files:
28
56
  - Rakefile
29
57
  - bin/console
30
58
  - bin/setup
59
+ - bin/toycol
60
+ - examples/duck/Gemfile
61
+ - examples/duck/Protocolfile.duck
62
+ - examples/duck/config_duck.ru
63
+ - examples/rubylike/Gemfile
64
+ - examples/rubylike/Protocolfile.rubylike
65
+ - examples/rubylike/config_rubylike.ru
66
+ - examples/safe_ruby/Gemfile
67
+ - examples/safe_ruby/Protocolfile.safe_ruby
68
+ - examples/safe_ruby/config_safe_ruby.ru
69
+ - examples/safe_ruby_with_sinatra/Gemfile
70
+ - examples/safe_ruby_with_sinatra/Protocolfile.safe_ruby_with_sinatra
71
+ - examples/safe_ruby_with_sinatra/app.rb
72
+ - examples/safe_ruby_with_sinatra/post.rb
73
+ - examples/safe_ruby_with_sinatra/views/index.erb
74
+ - examples/unsafe_ruby/Gemfile
75
+ - examples/unsafe_ruby/Protocolfile.unsafe_ruby
76
+ - examples/unsafe_ruby/config_unsafe_ruby.ru
77
+ - lib/rack/handler/toycol.rb
31
78
  - lib/toycol.rb
79
+ - lib/toycol/client.rb
80
+ - lib/toycol/command.rb
81
+ - lib/toycol/const.rb
82
+ - lib/toycol/helper.rb
83
+ - lib/toycol/protocol.rb
84
+ - lib/toycol/proxy.rb
32
85
  - lib/toycol/version.rb
33
86
  - toycol.gemspec
34
87
  homepage: https://github.com/shioimm/toycol
@@ -46,7 +99,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
46
99
  requirements:
47
100
  - - ">="
48
101
  - !ruby/object:Gem::Version
49
- version: 3.0.0
102
+ version: 2.6.0
50
103
  required_rubygems_version: !ruby/object:Gem::Requirement
51
104
  requirements:
52
105
  - - ">="