toycol 0.0.1 → 0.2.2

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