toycol 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
  - - ">="