toycol 0.2.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE/bug_report.md +29 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/CHANGELOG.md +15 -2
- data/README.md +169 -3
- data/examples/anonymous/Gemfile +5 -0
- data/examples/anonymous/Protocolfile +13 -0
- data/examples/anonymous/config.ru +21 -0
- data/examples/duck/Protocolfile.duck +2 -4
- data/lib/rack/handler/toycol.rb +12 -9
- data/lib/toycol.rb +14 -8
- data/lib/toycol/client.rb +7 -6
- data/lib/toycol/command.rb +47 -26
- data/lib/toycol/helper.rb +6 -2
- data/lib/toycol/protocol.rb +27 -18
- data/lib/toycol/proxy.rb +11 -13
- data/lib/toycol/server.rb +20 -22
- data/lib/toycol/template_generator.rb +62 -0
- data/lib/toycol/templates/application.txt +27 -0
- data/lib/toycol/templates/protocol.txt +39 -0
- data/lib/toycol/version.rb +1 -1
- metadata +10 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b1b8a58a4fd3f08fc0efe5c808ed00f1fdd78e66b8b0c5467f4deb314f504fde
|
4
|
+
data.tar.gz: 86c53cfa48a4331cde4e5888b92a28645bb88b81695b8996b0ef301958364575
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6f98b7f6753378032835b787f2f46c1679d6d5df5b88188a1e8f4350ae486146cad37ea1b8f67ec2af0d6f91e8ed24015154a2048f5891d4fb89f662ae912aee
|
7
|
+
data.tar.gz: '054893839fdaea8dfb45701e1f2a0cca55c19287e03d18a9266f2547502bb4c18d4fe97b822960aeef208044660e30262bc89945d269a047f3c6398c954c99e8'
|
@@ -0,0 +1,29 @@
|
|
1
|
+
---
|
2
|
+
name: Bug report
|
3
|
+
about: Create a report to help us improve
|
4
|
+
title: "[BUG]"
|
5
|
+
labels: bug
|
6
|
+
assignees: ''
|
7
|
+
|
8
|
+
---
|
9
|
+
|
10
|
+
## Describe the bug
|
11
|
+
A clear and concise description of what the bug is.
|
12
|
+
|
13
|
+
## To Reproduce
|
14
|
+
Steps to reproduce the behavior:
|
15
|
+
1. Go to '...'
|
16
|
+
2. Click on '....'
|
17
|
+
3. Scroll down to '....'
|
18
|
+
4. See error
|
19
|
+
|
20
|
+
## Expected behavior
|
21
|
+
A clear and concise description of what you expected to happen.
|
22
|
+
|
23
|
+
## Environment
|
24
|
+
- Ruby Version:
|
25
|
+
- Rack Version:
|
26
|
+
- Puma Version:
|
27
|
+
|
28
|
+
## Additional context
|
29
|
+
Add any other context about the problem here.
|
@@ -0,0 +1,20 @@
|
|
1
|
+
---
|
2
|
+
name: Feature request
|
3
|
+
about: Suggest an idea for this project
|
4
|
+
title: "[FEATURE]"
|
5
|
+
labels: enhancement
|
6
|
+
assignees: ''
|
7
|
+
|
8
|
+
---
|
9
|
+
|
10
|
+
## Is your feature request related to a problem? Please describe.
|
11
|
+
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
12
|
+
|
13
|
+
## Describe the solution you'd like
|
14
|
+
A clear and concise description of what you want to happen.
|
15
|
+
|
16
|
+
## Describe alternatives you've considered
|
17
|
+
A clear and concise description of any alternative solutions or features you've considered.
|
18
|
+
|
19
|
+
## Additional context
|
20
|
+
Add any other context or screenshots about the feature request here.
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,18 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file. For info on how to format all future additions to this file please reference [Keep A Changelog](https://keepachangelog.com/en/1.0.0/).
|
4
|
+
|
1
5
|
## [Unreleased]
|
2
6
|
|
3
|
-
## [0.0
|
7
|
+
## [1.0.0] - 2021-07-27
|
8
|
+
|
9
|
+
The first major release of Toycol.The main features are:
|
4
10
|
|
5
|
-
-
|
11
|
+
- [Added] Toycol::Protocol - For defining orignal application protocol
|
12
|
+
- [Added] Toycol::Proxy - For accepting requests and pass them to the background server
|
13
|
+
- [Added] Toycol::Command - For user friendly CLI
|
14
|
+
- `$ toycol server` - For starting proxy & background server
|
15
|
+
- `$ toycol client` - For sending request message to server
|
16
|
+
- `$ toycol generate` - For generating skeletons of Protocolfile & application
|
17
|
+
- [Added] Toycol::Server - As a built-in background server
|
18
|
+
- [Added] Rack::Handler::Toycol - For running Rack compartible application & switching background server
|
data/README.md
CHANGED
@@ -1,13 +1,122 @@
|
|
1
1
|
# Toycol
|
2
2
|
|
3
|
-
|
3
|
+
Toycol is a small framework for defining toy application protocols.
|
4
|
+
|
5
|
+
You can define your own application protocol only by writing a parser in Toycol DSL for request messages.
|
6
|
+
|
7
|
+
Since the server and client programs to run the protocol are built-in from the beginning, you only need to prepare the following two items to run your custom protocol.
|
8
|
+
- A configuration file named like `Protocolfile`, `Protocolfile.protocol_name` and so on
|
9
|
+
- A Rack compartible application(e.g. `config.ru`)
|
10
|
+
|
11
|
+
In the real world, there is (yet) no full-fledged web server or browser in the world that runs on the custom protocol you devised.
|
12
|
+
Therefore, the protocol defined by this framework is in fact a "toy".
|
13
|
+
However, by using this framework, you will be able to experience and learn how the connection between the application layer and the transport layer is, and how the application protocol works on the transport layer.
|
14
|
+
|
15
|
+
## Example(on original "Duck" protocol)
|
16
|
+
|
17
|
+
In this protocol:
|
18
|
+
- Client would send message like: `"quack, quack /posts<3user_id=1"`
|
19
|
+
- Server would interpret client message: `"GET /posts?user_id=1"`
|
20
|
+
|
21
|
+
You write your definition in Protocolfile
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
# Protocolfile.duck (protocol file)
|
25
|
+
|
26
|
+
Protocol.define(:duck) do
|
27
|
+
# [OPTIONAL] You can add your original request methods
|
28
|
+
add_request_methods "OTHER"
|
29
|
+
|
30
|
+
# [OPTIONAL] You can define your custom status codes
|
31
|
+
custom_status_codes(
|
32
|
+
600 => "I'm afraid you are not a duck..."
|
33
|
+
)
|
34
|
+
|
35
|
+
# [REQUIRED] Define how you parse request path from request message
|
36
|
+
request.path do |message|
|
37
|
+
%r{(?<path>\/\w*)}.match(message)[:path]
|
38
|
+
end
|
39
|
+
|
40
|
+
# [REQUIRED] Define how you parse query from request message
|
41
|
+
request.query do |message|
|
42
|
+
%r{\<3(?<query>.+)}.match(message) { |m| m[:query] }
|
43
|
+
end
|
44
|
+
|
45
|
+
# [REQUIRED] Define how you parse query from request message
|
46
|
+
request.http_method do |message|
|
47
|
+
case message.scan(/quack/).size
|
48
|
+
when 2 then "GET"
|
49
|
+
else "OTHER"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
Don't forget, you need to prepare your application as well.
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
# config_duck.ru (Rack compartible application)
|
59
|
+
|
60
|
+
require "rack"
|
61
|
+
require "toycol"
|
62
|
+
|
63
|
+
# Specify which protocol to use
|
64
|
+
Toycol::Protocol.use(:duck)
|
65
|
+
|
66
|
+
class App
|
67
|
+
def call(env)
|
68
|
+
case env["REQUEST_METHOD"]
|
69
|
+
when "GET"
|
70
|
+
[
|
71
|
+
200,
|
72
|
+
{ "Content-Type" => "text/html" },
|
73
|
+
["Quack, Quack! This app is running by Duck protocol."]
|
74
|
+
]
|
75
|
+
when "OTHER"
|
76
|
+
[
|
77
|
+
600,
|
78
|
+
{ "Content-Type" => "text/html" },
|
79
|
+
["Sorry, this application is only for ducks...\n"]
|
80
|
+
]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
run App.new
|
86
|
+
```
|
87
|
+
|
88
|
+
Then you run server & client
|
89
|
+
|
90
|
+
```
|
91
|
+
# In terminal for server
|
92
|
+
|
93
|
+
$ toycol server config_duck.ru
|
94
|
+
Toycol starts build-in server, listening on unix:///tmp/toycol.socket
|
95
|
+
Toycol is running on localhost:9292
|
96
|
+
=> Use Ctrl-C to stop
|
97
|
+
```
|
98
|
+
|
99
|
+
```
|
100
|
+
# In other terminal for client
|
101
|
+
|
102
|
+
$ toycol client "quack, quack /posts<3user_id=1"
|
103
|
+
[Toycol] Sent request message: quack, quack /posts<3user_id=1
|
104
|
+
---
|
105
|
+
[Toycol] Received response message:
|
106
|
+
|
107
|
+
HTTP/1.1 200 OK
|
108
|
+
Content-Type: text/html
|
109
|
+
Content-Length: 32
|
110
|
+
|
111
|
+
Quack, Quack! This app is running by Duck protocol.
|
112
|
+
```
|
4
113
|
|
5
114
|
## Installation
|
6
115
|
|
7
116
|
Add this line to your application's Gemfile:
|
8
117
|
|
9
118
|
```ruby
|
10
|
-
gem
|
119
|
+
gem "toycol"
|
11
120
|
```
|
12
121
|
|
13
122
|
And then execute:
|
@@ -20,7 +129,64 @@ Or install it yourself as:
|
|
20
129
|
|
21
130
|
## Usage
|
22
131
|
|
23
|
-
|
132
|
+
Toycol provides useful commands to define & run your protocol.
|
133
|
+
|
134
|
+
#### `toycol generate` - To define your new protocol
|
135
|
+
|
136
|
+
You can use `toycol generate` command to generate skeletons of Protocolfile and application.
|
137
|
+
|
138
|
+
```
|
139
|
+
$ toycol generate PROTOCOL_NAME
|
140
|
+
```
|
141
|
+
|
142
|
+
When you run this command, skeletons of `Protocolfile.PROTOCOL_NAME` and `config_PROTOCOL_NAME.ru` will be generated.
|
143
|
+
If you only need one of them, you can specify the type by `-t` option.
|
144
|
+
|
145
|
+
```
|
146
|
+
$ toycol generate PROTOCOL_NAME -t protocol
|
147
|
+
# or
|
148
|
+
$ toycol generate PROTOCOL_NAME -t app
|
149
|
+
```
|
150
|
+
|
151
|
+
If `PROTOCOL_NAME` is not specified, Protocolfile and config.ru will simply be generated.
|
152
|
+
|
153
|
+
```
|
154
|
+
$ toycol generate
|
155
|
+
```
|
156
|
+
|
157
|
+
#### `toycol server` - To run server by your protocol
|
158
|
+
|
159
|
+
After you prepare Protocolfile & application, you need to start server to run the application by `toycol server` command.
|
160
|
+
|
161
|
+
```
|
162
|
+
# Please specify application file name
|
163
|
+
$ toycol server config_`PROTOCOL_NAME`.ru
|
164
|
+
```
|
165
|
+
|
166
|
+
Then the server will start.
|
167
|
+
|
168
|
+
Normally, `toycol server` command will start the server built into toycol.
|
169
|
+
However, if Puma is already installed in your environment, it will start Puma by default.
|
170
|
+
|
171
|
+
If you want to explicitly specify which server to use, you can use the -u option.
|
172
|
+
|
173
|
+
```
|
174
|
+
$ toycol server config_`PROTOCOL_NAME`.ru -u puma
|
175
|
+
# or
|
176
|
+
$ toycol server config_`PROTOCOL_NAME`.ru -u buid_in
|
177
|
+
```
|
178
|
+
|
179
|
+
If you would like to check other options, run the command `toycol server -h`.
|
180
|
+
|
181
|
+
#### `toycol client` - To send request message by your protocol
|
182
|
+
|
183
|
+
When you would like to send the request message to the server, use `toycol client`.
|
184
|
+
|
185
|
+
```
|
186
|
+
$ toycol client "YOUR REQUEST MESSAGE"
|
187
|
+
```
|
188
|
+
|
189
|
+
If you would like to check other options, run the command `toycol client -h`.
|
24
190
|
|
25
191
|
## Development
|
26
192
|
|
@@ -0,0 +1,13 @@
|
|
1
|
+
Toycol::Protocol.define do
|
2
|
+
request.path do |message|
|
3
|
+
%r{(?<path>\/\w*)}.match(message)[:path]
|
4
|
+
end
|
5
|
+
|
6
|
+
request.query do |message|
|
7
|
+
%r{\?(?<query>.+)}.match(message) { |m| m[:query] }
|
8
|
+
end
|
9
|
+
|
10
|
+
request.http_method do |message|
|
11
|
+
"GET"
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack"
|
4
|
+
require "toycol"
|
5
|
+
|
6
|
+
Toycol::Protocol.use
|
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
|
+
["This app has no protocol name\n"]
|
16
|
+
]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
run App.new
|
@@ -1,5 +1,3 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
Toycol::Protocol.define(:duck) do
|
4
2
|
custom_status_codes(
|
5
3
|
600 => "I'm afraid you are not a duck..."
|
@@ -7,11 +5,11 @@ Toycol::Protocol.define(:duck) do
|
|
7
5
|
additional_request_methods "OTHER"
|
8
6
|
|
9
7
|
request.path do |message|
|
10
|
-
%r{(?<path
|
8
|
+
%r{(?<path>\/\w*)}.match(message)[:path]
|
11
9
|
end
|
12
10
|
|
13
11
|
request.query do |message|
|
14
|
-
|
12
|
+
%r{\<3(?<query>.+)}.match(message) { |m| m[:query] }
|
15
13
|
end
|
16
14
|
|
17
15
|
request.http_method do |message|
|
data/lib/rack/handler/toycol.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "rack"
|
4
3
|
require "rack/handler"
|
5
4
|
|
6
5
|
module Rack
|
7
6
|
module Handler
|
8
7
|
class Toycol
|
8
|
+
extend ::Toycol::Helper
|
9
|
+
|
9
10
|
class << self
|
10
11
|
attr_writer :preferred_background_server, :host, :port
|
11
12
|
|
@@ -27,18 +28,20 @@ module Rack
|
|
27
28
|
def select_background_server
|
28
29
|
case @preferred_background_server
|
29
30
|
when "puma"
|
30
|
-
return "puma" if
|
31
|
+
return "puma" if try_require_puma_handler
|
31
32
|
|
32
|
-
|
33
|
-
raise LoadError
|
33
|
+
raise LoadError, "Puma is not installed in your environment."
|
34
34
|
when nil
|
35
|
-
|
35
|
+
try_require_puma_handler ? "puma" : "builtin"
|
36
36
|
else
|
37
|
-
"
|
37
|
+
"builtin"
|
38
38
|
end
|
39
|
+
rescue LoadError
|
40
|
+
Process.kill(:INT, Process.ppid)
|
41
|
+
abort
|
39
42
|
end
|
40
43
|
|
41
|
-
def
|
44
|
+
def try_require_puma_handler
|
42
45
|
require "rack/handler/puma"
|
43
46
|
true
|
44
47
|
rescue LoadError
|
@@ -48,10 +51,10 @@ module Rack
|
|
48
51
|
def run_background_server
|
49
52
|
case select_background_server
|
50
53
|
when "puma"
|
51
|
-
|
54
|
+
logger "Start Puma in single mode, listening on unix://#{::Toycol::UNIX_SOCKET_PATH}"
|
52
55
|
Rack::Handler::Puma.run(@app, **{ Host: ::Toycol::UNIX_SOCKET_PATH, Silent: true })
|
53
56
|
else
|
54
|
-
|
57
|
+
logger "Start built-in server, listening on unix://#{::Toycol::UNIX_SOCKET_PATH}"
|
55
58
|
::Toycol::Server.run(@app, **{ Path: ::Toycol::UNIX_SOCKET_PATH, Port: @port })
|
56
59
|
end
|
57
60
|
end
|
data/lib/toycol.rb
CHANGED
@@ -1,27 +1,33 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "fileutils"
|
4
|
+
require "optparse"
|
5
|
+
require "rack"
|
6
|
+
require "socket"
|
7
|
+
require "stringio"
|
4
8
|
|
5
9
|
require_relative "toycol/const"
|
6
10
|
require_relative "toycol/helper"
|
7
11
|
require_relative "toycol/protocol"
|
8
12
|
require_relative "toycol/proxy"
|
9
13
|
require_relative "toycol/server"
|
10
|
-
require_relative "
|
11
|
-
|
12
|
-
Dir["#{FileUtils.pwd}/Protocolfile*"].sort.each { |f| load f }
|
13
|
-
|
14
|
+
require_relative "toycol/client"
|
15
|
+
require_relative "toycol/template_generator"
|
14
16
|
require_relative "toycol/command"
|
15
17
|
require_relative "toycol/version"
|
16
18
|
|
19
|
+
require_relative "rack/handler/toycol"
|
20
|
+
|
17
21
|
module Toycol
|
18
22
|
class Error < StandardError; end
|
19
23
|
|
20
|
-
class
|
24
|
+
class UnauthorizeError < Error; end
|
21
25
|
|
22
|
-
class
|
26
|
+
class UndefinementError < Error; end
|
23
27
|
|
24
|
-
class
|
28
|
+
class DuplicateProtocolError < Error; end
|
25
29
|
|
26
|
-
class
|
30
|
+
class HTTPError < Error; end
|
27
31
|
end
|
32
|
+
|
33
|
+
Dir["#{FileUtils.pwd}/Protocolfile*"].sort.each { |f| load f }
|
data/lib/toycol/client.rb
CHANGED
@@ -1,19 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "socket"
|
4
|
-
|
5
3
|
module Toycol
|
6
4
|
class Client
|
5
|
+
extend Helper
|
6
|
+
|
7
7
|
@port = 9292
|
8
|
+
@host = "localhost"
|
8
9
|
CHUNK_SIZE = 1024 * 16
|
9
10
|
|
10
11
|
class << self
|
11
|
-
attr_writer :port
|
12
|
+
attr_writer :port, :host
|
12
13
|
|
13
14
|
def execute!(request_message, &block)
|
14
|
-
socket = TCPSocket.new(
|
15
|
+
socket = TCPSocket.new(@host, @port)
|
15
16
|
socket.write(request_message)
|
16
|
-
|
17
|
+
logger "Sent request message: #{request_message}\n---"
|
17
18
|
|
18
19
|
response_message = []
|
19
20
|
response_message << socket.readpartial(CHUNK_SIZE) until socket.eof?
|
@@ -29,7 +30,7 @@ module Toycol
|
|
29
30
|
|
30
31
|
def default_proc
|
31
32
|
proc do |message|
|
32
|
-
|
33
|
+
logger "Received response message:\n\n"
|
33
34
|
puts message
|
34
35
|
end
|
35
36
|
end
|
data/lib/toycol/command.rb
CHANGED
@@ -1,27 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "optparse"
|
4
|
-
require_relative "./client"
|
5
|
-
|
6
3
|
module Toycol
|
7
4
|
class Command
|
8
5
|
class Options
|
6
|
+
@options = {}
|
7
|
+
|
9
8
|
class << self
|
10
9
|
def parse!(argv)
|
11
|
-
options = {}
|
12
10
|
option_parser = create_option_parser
|
13
11
|
sub_command_option_parser = create_sub_command_option_parser
|
14
12
|
|
15
13
|
begin
|
16
14
|
option_parser.order!(argv)
|
17
|
-
options[:command] = argv.shift
|
18
|
-
options[:request_message] = argv.shift if
|
19
|
-
|
15
|
+
@options[:command] = argv.shift
|
16
|
+
@options[:request_message] = argv.shift if request_message?(argv.first)
|
17
|
+
@options[:protocol_name] = argv.shift if protocol_name?(argv.first)
|
18
|
+
sub_command_option_parser[@options[:command]].parse!(argv)
|
20
19
|
rescue OptionParser::MissingArgument, OptionParser::InvalidOption, ArgumentError => e
|
21
20
|
abort e.message
|
22
21
|
end
|
23
22
|
|
24
|
-
options
|
23
|
+
@options
|
25
24
|
end
|
26
25
|
|
27
26
|
def create_option_parser
|
@@ -45,26 +44,46 @@ module Toycol
|
|
45
44
|
|
46
45
|
def create_sub_command_option_parser
|
47
46
|
sub_command_parser = Hash.new { |_k, v| raise ArgumentError, "'#{v}' is not sub command" }
|
48
|
-
sub_command_parser["client"]
|
49
|
-
sub_command_parser["c"]
|
50
|
-
sub_command_parser["server"]
|
51
|
-
sub_command_parser["s"]
|
47
|
+
sub_command_parser["client"] = client_option_parser
|
48
|
+
sub_command_parser["c"] = client_option_parser
|
49
|
+
sub_command_parser["server"] = server_option_parser
|
50
|
+
sub_command_parser["s"] = server_option_parser
|
51
|
+
sub_command_parser["generate"] = generator_option_parser
|
52
|
+
sub_command_parser["g"] = generator_option_parser
|
52
53
|
sub_command_parser
|
53
54
|
end
|
54
55
|
|
55
56
|
private
|
56
57
|
|
58
|
+
def request_message?(arg)
|
59
|
+
%w[client c].include?(@options[:command]) \
|
60
|
+
&& arg != "-p" \
|
61
|
+
&& arg != "-h"
|
62
|
+
end
|
63
|
+
|
64
|
+
def protocol_name?(arg)
|
65
|
+
%w[geberate g].include?(@options[:command]) \
|
66
|
+
&& arg != "-t" \
|
67
|
+
&& arg != "-h"
|
68
|
+
end
|
69
|
+
|
57
70
|
def sub_command_summaries
|
58
71
|
[
|
59
72
|
{ name: "client REQUEST_MESSAGE -p PORT", summary: "Send request message to server" },
|
60
|
-
{ name: "server -u SERVER_NAME", summary: "Start proxy and background server" }
|
73
|
+
{ name: "server -u SERVER_NAME", summary: "Start proxy and background server" },
|
74
|
+
{ name: "generate NAME -t TYPE", summary: "Generate new protocol or Rack app" }
|
61
75
|
]
|
62
76
|
end
|
63
77
|
|
64
78
|
def client_option_parser
|
65
79
|
OptionParser.new do |opt|
|
66
|
-
opt.
|
67
|
-
|
80
|
+
opt.banner = "Usage: #{opt.program_name} client [-h|--help] REQUEST_MESSAGE [arg...]"
|
81
|
+
opt.on("-o HOST", "--host HOST", "connect to HOST (default: localhost)") do |host|
|
82
|
+
Client.host = host
|
83
|
+
end
|
84
|
+
|
85
|
+
opt.on("-p PORT_NUMBER", "--port PORT_NUMBER", "connect to PORT (default: 9292)") do |port|
|
86
|
+
Client.port = port
|
68
87
|
end
|
69
88
|
|
70
89
|
opt.on_head("-h", "--help", "Show this message") { help_command(opt) }
|
@@ -73,6 +92,7 @@ module Toycol
|
|
73
92
|
|
74
93
|
def server_option_parser
|
75
94
|
OptionParser.new do |opt|
|
95
|
+
opt.banner = "Usage: #{opt.program_name} server [-h|--help] APPLICATION_PATH [arg...]"
|
76
96
|
opt.on("-o HOST", "--host HOST", "bind to HOST (default: localhost)") do |host|
|
77
97
|
::Rack::Handler::Toycol.host = host
|
78
98
|
end
|
@@ -81,7 +101,7 @@ module Toycol
|
|
81
101
|
::Rack::Handler::Toycol.port = port
|
82
102
|
end
|
83
103
|
|
84
|
-
opt.on("-u SERVER_NAME", "--use SERVER_NAME", "switch using SERVER(puma/
|
104
|
+
opt.on("-u SERVER_NAME", "--use SERVER_NAME", "switch using SERVER(puma/builtin)") do |server_name|
|
85
105
|
::Rack::Handler::Toycol.preferred_background_server = server_name
|
86
106
|
end
|
87
107
|
|
@@ -89,16 +109,14 @@ module Toycol
|
|
89
109
|
end
|
90
110
|
end
|
91
111
|
|
92
|
-
def
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
112
|
+
def generator_option_parser
|
113
|
+
OptionParser.new do |opt|
|
114
|
+
opt.on("-t TYPE", "--type TYPE", "generate TYPE of template (default: :all)") do |type|
|
115
|
+
@options[:template_type] = type
|
116
|
+
end
|
97
117
|
|
98
|
-
|
99
|
-
|
100
|
-
{ name: "server -u=SERVER_NAME", summary: "Start proxy & background server" }
|
101
|
-
]
|
118
|
+
opt.on_head("-h", "--help", "Show this message") { help_command(opt) }
|
119
|
+
end
|
102
120
|
end
|
103
121
|
|
104
122
|
def help_command(parser)
|
@@ -122,10 +140,13 @@ module Toycol
|
|
122
140
|
|
123
141
|
case command
|
124
142
|
when "client", "c"
|
125
|
-
|
143
|
+
Client.execute!(options[:request_message])
|
126
144
|
when "server", "s"
|
127
145
|
ARGV.push("-q", "-s", "toycol")
|
128
146
|
Rack::Server.start
|
147
|
+
when "generate", "g"
|
148
|
+
type = options[:template_type] || "all"
|
149
|
+
TemplateGenerator.generate!(type: type, name: options[:protocol_name])
|
129
150
|
end
|
130
151
|
end
|
131
152
|
end
|
data/lib/toycol/helper.rb
CHANGED
@@ -2,6 +2,10 @@
|
|
2
2
|
|
3
3
|
module Toycol
|
4
4
|
module Helper
|
5
|
+
def logger(message)
|
6
|
+
puts "[Toycol] #{message}"
|
7
|
+
end
|
8
|
+
|
5
9
|
private
|
6
10
|
|
7
11
|
def safe_execution!(&block)
|
@@ -10,8 +14,8 @@ module Toycol
|
|
10
14
|
|
11
15
|
def safe_executionable_tp
|
12
16
|
@safe_executionable_tp ||= TracePoint.new(:script_compiled) do |tp|
|
13
|
-
if tp.binding.receiver ==
|
14
|
-
raise
|
17
|
+
if tp.binding.receiver == Protocol && tp.method_id.to_s.match?(unauthorized_methods_regex)
|
18
|
+
raise UnauthorizeError, <<~ERROR
|
15
19
|
- Unauthorized method was called!
|
16
20
|
You can't use methods that may cause injections in your protocol.
|
17
21
|
Ex. Kernel.#eval, Kernel.#exec, Kernel.#require and so on.
|
data/lib/toycol/protocol.rb
CHANGED
@@ -5,23 +5,30 @@ module Toycol
|
|
5
5
|
class Protocol
|
6
6
|
@definements = {}
|
7
7
|
@protocol_name = nil
|
8
|
-
@http_status_codes =
|
9
|
-
@http_request_methods =
|
8
|
+
@http_status_codes = DEFAULT_HTTP_STATUS_CODES.dup
|
9
|
+
@http_request_methods = DEFAULT_HTTP_REQUEST_METHODS.dup
|
10
10
|
@custom_status_codes = nil
|
11
11
|
@additional_request_methods = nil
|
12
12
|
|
13
13
|
class << self
|
14
|
-
|
15
|
-
|
14
|
+
attr_reader :protocol_name
|
15
|
+
|
16
|
+
# For Protocolfile to define new protocol
|
17
|
+
def define(protocol_name = :default, &block)
|
18
|
+
if @definements[protocol_name]
|
19
|
+
raise DuplicateProtocolError,
|
20
|
+
"#{protocol_name || "Anonymous"} protocol has already been defined"
|
21
|
+
end
|
22
|
+
|
16
23
|
@definements[protocol_name] = block
|
17
24
|
end
|
18
25
|
|
19
|
-
# For application which
|
20
|
-
def use(protocol_name)
|
26
|
+
# For application to select which protocol to use
|
27
|
+
def use(protocol_name = :default)
|
21
28
|
@protocol_name = protocol_name
|
22
29
|
end
|
23
30
|
|
24
|
-
# For server
|
31
|
+
# For proxy server to interpret protocol definitions and parse messages
|
25
32
|
def run!(message)
|
26
33
|
@request_message = message.chomp
|
27
34
|
|
@@ -67,52 +74,54 @@ module Toycol
|
|
67
74
|
end
|
68
75
|
end
|
69
76
|
|
70
|
-
# For server:
|
77
|
+
# For proxy server: Fetch the request path
|
71
78
|
def request_path
|
72
79
|
request_path = request.instance_variable_get("@path").call(request_message)
|
73
80
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
81
|
+
if request_path.size >= 2048
|
82
|
+
raise UnauthorizeError,
|
83
|
+
"This request path is too long"
|
84
|
+
elsif request_path.scan(%r{[/\w\d\-_]}).size < request_path.size
|
85
|
+
raise UnauthorizeError,
|
78
86
|
"This request path contains unauthorized character"
|
79
87
|
end
|
80
88
|
|
81
89
|
request_path
|
82
90
|
end
|
83
91
|
|
84
|
-
# For server:
|
92
|
+
# For proxy server: Fetch the request method
|
85
93
|
def request_method
|
86
94
|
@http_request_methods.concat @additional_request_methods if @additional_request_methods
|
87
95
|
request_method = request.instance_variable_get("@http_method").call(request_message)
|
88
96
|
|
89
97
|
unless @http_request_methods.include? request_method
|
90
|
-
raise
|
98
|
+
raise UndefinementError,
|
99
|
+
"This request method is undefined"
|
91
100
|
end
|
92
101
|
|
93
102
|
request_method
|
94
103
|
end
|
95
104
|
|
96
|
-
# For server:
|
105
|
+
# For proxy server: Fetch the query string
|
97
106
|
def query
|
98
107
|
return unless (parse_query_block = request.instance_variable_get("@query"))
|
99
108
|
|
100
109
|
parse_query_block.call(request_message)
|
101
110
|
end
|
102
111
|
|
103
|
-
# For server:
|
112
|
+
# For proxy server: Fetch the input body
|
104
113
|
def input
|
105
114
|
return unless (parsed_input_block = request.instance_variable_get("@input"))
|
106
115
|
|
107
116
|
parsed_input_block.call(request_message)
|
108
117
|
end
|
109
118
|
|
110
|
-
# For server:
|
119
|
+
# For proxy server: fetch the message of status code
|
111
120
|
def status_message(status)
|
112
121
|
@http_status_codes.merge!(@custom_status_codes) if @custom_status_codes
|
113
122
|
|
114
123
|
unless (message = @http_status_codes[status])
|
115
|
-
raise
|
124
|
+
raise HTTPError, "Application returns unknown status code"
|
116
125
|
end
|
117
126
|
|
118
127
|
message
|
data/lib/toycol/proxy.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "socket"
|
4
|
-
|
5
3
|
module Toycol
|
6
4
|
class Proxy
|
7
5
|
include Helper
|
@@ -13,15 +11,15 @@ module Toycol
|
|
13
11
|
@path = nil
|
14
12
|
@query = nil
|
15
13
|
@input = nil
|
16
|
-
@protocol =
|
14
|
+
@protocol = Protocol
|
17
15
|
@proxy = TCPServer.new(@host, @port)
|
18
16
|
end
|
19
17
|
|
20
18
|
CHUNK_SIZE = 1024 * 16
|
21
19
|
|
22
20
|
def start
|
23
|
-
|
24
|
-
|
21
|
+
logger <<~MESSAGE
|
22
|
+
Start proxy server on #{@protocol.protocol_name} protocol, listening on #{@host}:#{@port}
|
25
23
|
=> Use Ctrl-C to stop
|
26
24
|
MESSAGE
|
27
25
|
|
@@ -33,13 +31,13 @@ module Toycol
|
|
33
31
|
while !@client.closed? && !@client.eof?
|
34
32
|
begin
|
35
33
|
request = @client.readpartial(CHUNK_SIZE)
|
36
|
-
|
34
|
+
logger "Received message: #{request.inspect.chomp}"
|
37
35
|
|
38
36
|
safe_execution! { @protocol.run!(request) }
|
39
37
|
assign_parsed_attributes!
|
40
38
|
|
41
39
|
http_request_message = build_http_request_message
|
42
|
-
|
40
|
+
logger "Message has been translated to HTTP request message: #{http_request_message.inspect}"
|
43
41
|
transfer_to_server(http_request_message)
|
44
42
|
rescue StandardError => e
|
45
43
|
puts "#{e.class} #{e.message} - closing socket."
|
@@ -82,15 +80,15 @@ module Toycol
|
|
82
80
|
end
|
83
81
|
|
84
82
|
def transfer_to_server(request_message)
|
85
|
-
UNIXSocket.open(
|
83
|
+
UNIXSocket.open(UNIX_SOCKET_PATH) do |server|
|
86
84
|
server.write request_message
|
87
85
|
server.close_write
|
88
|
-
|
86
|
+
logger "Successed to Send HTTP request message to server"
|
89
87
|
|
90
88
|
response_message = []
|
91
89
|
response_message << server.readpartial(CHUNK_SIZE) until server.eof?
|
92
90
|
response_message = response_message.join
|
93
|
-
|
91
|
+
logger "Received response message from server: #{response_message.lines.first}"
|
94
92
|
|
95
93
|
response_line = response_message.lines.first
|
96
94
|
status_number = response_line[9..11]
|
@@ -98,18 +96,18 @@ module Toycol
|
|
98
96
|
|
99
97
|
if (custom_message = @protocol.status_message(status_number.to_i)) != status_message
|
100
98
|
response_message = response_message.sub(status_message, custom_message)
|
101
|
-
|
99
|
+
logger "Status message has been translated to custom status message: #{custom_message}"
|
102
100
|
end
|
103
101
|
|
104
102
|
@client.write response_message
|
105
103
|
@client.close_write
|
106
|
-
|
104
|
+
logger "Finished to response to client"
|
107
105
|
server.close
|
108
106
|
end
|
109
107
|
end
|
110
108
|
|
111
109
|
def shutdown
|
112
|
-
|
110
|
+
logger "Caught SIGINT -> Stop to server"
|
113
111
|
exit
|
114
112
|
end
|
115
113
|
end
|
data/lib/toycol/server.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "stringio"
|
4
|
-
|
5
3
|
module Toycol
|
6
4
|
class Server
|
7
5
|
BACKLOG = 1024
|
@@ -50,19 +48,19 @@ module Toycol
|
|
50
48
|
|
51
49
|
def default_env
|
52
50
|
{
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
51
|
+
PATH_INFO => "",
|
52
|
+
QUERY_STRING => "",
|
53
|
+
REQUEST_METHOD => "",
|
54
|
+
SERVER_NAME => "toycol_server",
|
55
|
+
SERVER_PORT => @port.to_s,
|
56
|
+
CONTENT_LENGTH => "0",
|
57
|
+
RACK_VERSION => Rack::VERSION,
|
58
|
+
RACK_INPUT => stringio(""),
|
59
|
+
RACK_ERRORS => $stderr,
|
60
|
+
RACK_MULTITHREAD => false,
|
61
|
+
RACK_MULTIPROCESS => false,
|
62
|
+
RACK_RUN_ONCE => false,
|
63
|
+
RACK_URL_SCHEME => "http"
|
66
64
|
}
|
67
65
|
end
|
68
66
|
|
@@ -71,7 +69,7 @@ module Toycol
|
|
71
69
|
end
|
72
70
|
|
73
71
|
def response_status_code
|
74
|
-
"HTTP/1.1 #{@returned_status} #{
|
72
|
+
"HTTP/1.1 #{@returned_status} #{DEFAULT_HTTP_STATUS_CODES[@returned_status.to_i] || "CUSTOM"}\r\n"
|
75
73
|
end
|
76
74
|
|
77
75
|
def response_headers
|
@@ -108,17 +106,17 @@ module Toycol
|
|
108
106
|
request_method, request_path, = request_line.split
|
109
107
|
request_path, query_string = request_path.split("?")
|
110
108
|
|
111
|
-
@env[
|
112
|
-
@env[
|
113
|
-
@env[
|
114
|
-
@env[
|
109
|
+
@env[REQUEST_METHOD] = request_method
|
110
|
+
@env[PATH_INFO] = request_path
|
111
|
+
@env[QUERY_STRING] = query_string || ""
|
112
|
+
@env[CONTENT_LENGTH]
|
115
113
|
|
116
114
|
request_headers.each do |request_header|
|
117
115
|
k, v = request_header.split(":").map(&:strip)
|
118
|
-
@env[
|
116
|
+
@env[k.tr("-", "_").upcase.to_s] = v
|
119
117
|
end
|
120
118
|
|
121
|
-
@env[
|
119
|
+
@env[RACK_INPUT] = stringio(request_body)
|
122
120
|
end
|
123
121
|
end
|
124
122
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Toycol
|
4
|
+
class TemplateGenerator
|
5
|
+
include Helper
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def generate!(type:, name:)
|
9
|
+
raise Error, "Unknown Type: This type of template can't be generated" unless valid? type
|
10
|
+
|
11
|
+
if type == "all"
|
12
|
+
new(name, "protocol").generate!
|
13
|
+
new(name, "app").generate!
|
14
|
+
else
|
15
|
+
new(name, type).generate!
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def valid?(type)
|
22
|
+
%w[all app protocol].include? type
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(name, type)
|
27
|
+
@name = name
|
28
|
+
@type = type
|
29
|
+
end
|
30
|
+
|
31
|
+
def generate!
|
32
|
+
raise Error, "#{filename} already exists" unless Dir.glob(filename).empty?
|
33
|
+
|
34
|
+
File.open(filename, "w") { |f| f.print template_text_for_new }
|
35
|
+
logger "Generate #{filename} in #{FileUtils.pwd}"
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def filename
|
41
|
+
@filename ||= case @type
|
42
|
+
when "protocol" then "Protocolfile#{@name ? ".#{@name}" : nil}"
|
43
|
+
when "app" then "config#{@name ? "_#{@name}" : nil}.ru"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def template_text_for_new
|
48
|
+
if @name
|
49
|
+
template_text.sub(":PROTOCOL_NAME", ":#{@name}")
|
50
|
+
else
|
51
|
+
template_text.sub("\(:PROTOCOL_NAME\)", "")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def template_text
|
56
|
+
case @type
|
57
|
+
when "protocol" then File.open("#{__dir__}/templates/protocol.txt", "r", &:read)
|
58
|
+
when "app" then File.open("#{__dir__}/templates/application.txt", "r", &:read)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "rack"
|
2
|
+
require "toycol"
|
3
|
+
|
4
|
+
Toycol::Protocol.use(:PROTOCOL_NAME)
|
5
|
+
|
6
|
+
class App
|
7
|
+
def call(env)
|
8
|
+
# Define your app on request method, request path, request query etc
|
9
|
+
# For example:
|
10
|
+
# case env["REQUEST_METHOD"]
|
11
|
+
# when "GET"
|
12
|
+
# [
|
13
|
+
# 200,
|
14
|
+
# { "Content-Type" => "text/html" },
|
15
|
+
# ["Hello, This app is running by new protocol."]
|
16
|
+
# ]
|
17
|
+
# when "OTHER"
|
18
|
+
# [
|
19
|
+
# 600,
|
20
|
+
# { "Content-Type" => "text/html" },
|
21
|
+
# ["This is response message for additional request method"]
|
22
|
+
# ]
|
23
|
+
# end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
run App.new
|
@@ -0,0 +1,39 @@
|
|
1
|
+
Toycol::Protocol.define(:PROTOCOL_NAME) do
|
2
|
+
# For example
|
3
|
+
# client would send:
|
4
|
+
# quack, quack /posts<3user_id=1
|
5
|
+
# server would interpret client message:
|
6
|
+
# GET /posts?user_id=1
|
7
|
+
|
8
|
+
|
9
|
+
# [OPTIONAL] You can define your additional request methods:
|
10
|
+
# For example:
|
11
|
+
# additional_request_methods "OTHER"
|
12
|
+
|
13
|
+
# [OPTIONAL] You can define your own response status code:
|
14
|
+
# For example:
|
15
|
+
# custom_status_codes(
|
16
|
+
# 600 => "I'm afraid you are not a duck..."
|
17
|
+
# )
|
18
|
+
|
19
|
+
# [REQUIRED] Define how you parse request path from request message
|
20
|
+
request.path do |message|
|
21
|
+
# For example:
|
22
|
+
# %r{(?<path>\/\w*)}.match(message)[:path]
|
23
|
+
end
|
24
|
+
|
25
|
+
# [REQUIRED] Define how you parse query from request message
|
26
|
+
request.query do |message|
|
27
|
+
# For example:
|
28
|
+
# %r{\<3(?<query>.+)}.match(message) { |m| m[:query] }
|
29
|
+
end
|
30
|
+
|
31
|
+
# [REQUIRED] Define how you parse query from request message
|
32
|
+
request.http_method do |message|
|
33
|
+
# For example:
|
34
|
+
# case message.scan(/quack/).size
|
35
|
+
# when 2 then "GET"
|
36
|
+
# else "OTHER"
|
37
|
+
# end
|
38
|
+
end
|
39
|
+
end
|
data/lib/toycol/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: toycol
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.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-
|
11
|
+
date: 2021-07-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -32,6 +32,8 @@ executables:
|
|
32
32
|
extensions: []
|
33
33
|
extra_rdoc_files: []
|
34
34
|
files:
|
35
|
+
- ".github/ISSUE_TEMPLATE/bug_report.md"
|
36
|
+
- ".github/ISSUE_TEMPLATE/feature_request.md"
|
35
37
|
- ".github/dependabot.yml"
|
36
38
|
- ".github/workflows/main.yml"
|
37
39
|
- ".gitignore"
|
@@ -44,6 +46,9 @@ files:
|
|
44
46
|
- Rakefile
|
45
47
|
- bin/console
|
46
48
|
- bin/setup
|
49
|
+
- examples/anonymous/Gemfile
|
50
|
+
- examples/anonymous/Protocolfile
|
51
|
+
- examples/anonymous/config.ru
|
47
52
|
- examples/duck/Gemfile
|
48
53
|
- examples/duck/Protocolfile.duck
|
49
54
|
- examples/duck/config_duck.ru
|
@@ -71,6 +76,9 @@ files:
|
|
71
76
|
- lib/toycol/protocol.rb
|
72
77
|
- lib/toycol/proxy.rb
|
73
78
|
- lib/toycol/server.rb
|
79
|
+
- lib/toycol/template_generator.rb
|
80
|
+
- lib/toycol/templates/application.txt
|
81
|
+
- lib/toycol/templates/protocol.txt
|
74
82
|
- lib/toycol/version.rb
|
75
83
|
- toycol.gemspec
|
76
84
|
homepage: https://github.com/shioimm/toycol
|