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,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,116 @@
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
+ response_line = response_message.lines.first
96
+ status_number = response_line[9..11]
97
+ status_message = response_line[12..].strip
98
+
99
+ if (custom_message = @protocol.status_message(status_number.to_i)) != status_message
100
+ response_message = response_message.sub(status_message, custom_message)
101
+ puts "[Toycol] Status message has been translated to custom status message: #{custom_message}"
102
+ end
103
+
104
+ @client.write response_message
105
+ @client.close_write
106
+ puts "[Toycol] Finished to response to client"
107
+ server.close
108
+ end
109
+ end
110
+
111
+ def shutdown
112
+ puts "[Toycol] Catched SIGINT -> Stop to server"
113
+ exit
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ module Toycol
6
+ class Server
7
+ BACKLOG = 1024
8
+ CHUNK_SIZE = 1024 * 16
9
+
10
+ class << self
11
+ def run(app, **options)
12
+ new(app, **options).run
13
+ end
14
+ end
15
+
16
+ def initialize(app, **options)
17
+ @app = app
18
+ @path = options[:Path]
19
+ @port = options[:Port]
20
+ @env = default_env
21
+ @returned_status = nil
22
+ @returned_headers = nil
23
+ @returned_body = nil
24
+ end
25
+
26
+ def run
27
+ verify_file_path!
28
+ server = UNIXServer.new @path
29
+ server.listen BACKLOG
30
+
31
+ loop do
32
+ trap(:INT) { exit }
33
+
34
+ socket = server.accept
35
+
36
+ request_message = []
37
+ request_message << socket.readpartial(CHUNK_SIZE) until socket.eof?
38
+ request_message = request_message.join
39
+ assign_parsed_attributes!(request_message)
40
+
41
+ @returned_status, @returned_headers, @returned_body = @app.call(@env)
42
+
43
+ socket.puts response_message
44
+ socket.close_write
45
+ socket.close
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def default_env
52
+ {
53
+ ::Toycol::PATH_INFO => "",
54
+ ::Toycol::QUERY_STRING => "",
55
+ ::Toycol::REQUEST_METHOD => "",
56
+ ::Toycol::SERVER_NAME => "toycol_server",
57
+ ::Toycol::SERVER_PORT => @port.to_s,
58
+ ::Toycol::CONTENT_LENGTH => "0",
59
+ ::Toycol::RACK_VERSION => Rack::VERSION,
60
+ ::Toycol::RACK_INPUT => stringio(""),
61
+ ::Toycol::RACK_ERRORS => $stderr,
62
+ ::Toycol::RACK_MULTITHREAD => false,
63
+ ::Toycol::RACK_MULTIPROCESS => false,
64
+ ::Toycol::RACK_RUN_ONCE => false,
65
+ ::Toycol::RACK_URL_SCHEME => "http"
66
+ }
67
+ end
68
+
69
+ def response_message
70
+ "#{response_status_code}#{response_headers}\r\n#{response_body}"
71
+ end
72
+
73
+ def response_status_code
74
+ "HTTP/1.1 #{@returned_status} #{::Toycol::DEFAULT_HTTP_STATUS_CODES[@returned_status.to_i] || "CUSTOM"}\r\n"
75
+ end
76
+
77
+ def response_headers
78
+ @returned_headers["Content-Length"] = response_body.size unless @returned_headers["Content-Length"]
79
+
80
+ @returned_headers.map { |k, v| "#{k}: #{v}" }.join("\r\n") + "\r\n"
81
+ end
82
+
83
+ def response_body
84
+ res = []
85
+ @returned_body.each { |body| res << body }
86
+ res.join
87
+ end
88
+
89
+ def stringio(body = "")
90
+ StringIO.new(body).set_encoding("ASCII-8BIT")
91
+ end
92
+
93
+ def verify_file_path!
94
+ return unless File.exist? @path
95
+
96
+ begin
97
+ bound_file = UNIXSocket.new @path
98
+ rescue SystemCallError, IOError
99
+ File.unlink @path
100
+ else
101
+ bound_file.close
102
+ raise "[Toycol] Address already in use: #{@path}"
103
+ end
104
+ end
105
+
106
+ def assign_parsed_attributes!(request_message)
107
+ request_line, *request_headers, request_body = request_message.split("\r\n").reject(&:empty?)
108
+ request_method, request_path, = request_line.split
109
+ request_path, query_string = request_path.split("?")
110
+
111
+ @env[::Toycol::REQUEST_METHOD] = request_method
112
+ @env[::Toycol::PATH_INFO] = request_path
113
+ @env[::Toycol::QUERY_STRING] = query_string || ""
114
+ @env[::Toycol::CONTENT_LENGTH]
115
+
116
+ request_headers.each do |request_header|
117
+ k, v = request_header.split(":").map(&:strip)
118
+ @env["::Toycol::#{k.tr("-", "_").upcase}"] = v
119
+ end
120
+
121
+ @env[::Toycol::RACK_INPUT] = stringio(request_body)
122
+ end
123
+ end
124
+ 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.2.2"
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"
@@ -24,12 +24,8 @@ Gem::Specification.new do |spec|
24
24
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
25
25
  end
26
26
  spec.bindir = "exe"
27
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
27
+ spec.executables << "toycol"
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 "rack", "~> 2.0"
35
31
  end
metadata CHANGED
@@ -1,22 +1,39 @@
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.2.2
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-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
13
27
  description: Toy Application Protocol framework
14
28
  email:
15
29
  - shioi.mm@gmail.com
16
- executables: []
30
+ executables:
31
+ - toycol
17
32
  extensions: []
18
33
  extra_rdoc_files: []
19
34
  files:
35
+ - ".github/ISSUE_TEMPLATE/bug_report.md"
36
+ - ".github/dependabot.yml"
20
37
  - ".github/workflows/main.yml"
21
38
  - ".gitignore"
22
39
  - ".rubocop.yml"
@@ -28,7 +45,33 @@ files:
28
45
  - Rakefile
29
46
  - bin/console
30
47
  - bin/setup
48
+ - examples/duck/Gemfile
49
+ - examples/duck/Protocolfile.duck
50
+ - examples/duck/config_duck.ru
51
+ - examples/rubylike/Gemfile
52
+ - examples/rubylike/Protocolfile.rubylike
53
+ - examples/rubylike/config_rubylike.ru
54
+ - examples/safe_ruby/Gemfile
55
+ - examples/safe_ruby/Protocolfile.safe_ruby
56
+ - examples/safe_ruby/config_safe_ruby.ru
57
+ - examples/safe_ruby_with_sinatra/Gemfile
58
+ - examples/safe_ruby_with_sinatra/Protocolfile.safe_ruby_with_sinatra
59
+ - examples/safe_ruby_with_sinatra/app.rb
60
+ - examples/safe_ruby_with_sinatra/post.rb
61
+ - examples/safe_ruby_with_sinatra/views/index.erb
62
+ - examples/unsafe_ruby/Gemfile
63
+ - examples/unsafe_ruby/Protocolfile.unsafe_ruby
64
+ - examples/unsafe_ruby/config_unsafe_ruby.ru
65
+ - exe/toycol
66
+ - lib/rack/handler/toycol.rb
31
67
  - lib/toycol.rb
68
+ - lib/toycol/client.rb
69
+ - lib/toycol/command.rb
70
+ - lib/toycol/const.rb
71
+ - lib/toycol/helper.rb
72
+ - lib/toycol/protocol.rb
73
+ - lib/toycol/proxy.rb
74
+ - lib/toycol/server.rb
32
75
  - lib/toycol/version.rb
33
76
  - toycol.gemspec
34
77
  homepage: https://github.com/shioimm/toycol
@@ -46,7 +89,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
46
89
  requirements:
47
90
  - - ">="
48
91
  - !ruby/object:Gem::Version
49
- version: 3.0.0
92
+ version: 2.6.0
50
93
  required_rubygems_version: !ruby/object:Gem::Requirement
51
94
  requirements:
52
95
  - - ">="