toq 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3f490887ff8d26a2e18e5c010e286a6fc48ad85a9955471909496d78158ce4fc
4
+ data.tar.gz: f5c0d6ecf126970506d46a0416fd7effedbd590e99a9a5cb7b1601f0898af06d
5
+ SHA512:
6
+ metadata.gz: 760fc6a92f3162b42e91c0c62f217431745887699f743182f91e8265a2557c02b16e8a5f7defbad2cb4ed06559fb9c204d3e49393feb3001793a740d975c02d3
7
+ data.tar.gz: b91e5bafb6631ecc1e09d777b2144f54735b7c6975422bb9c1b359a2a39d2294c6163142196288bc84ffec56c004209346e6e58cd5747e8167570fbb3110ef89
data/CHANGELOG.md ADDED
@@ -0,0 +1 @@
1
+ # ChangeLog
data/LICENSE.md ADDED
@@ -0,0 +1,29 @@
1
+ # License
2
+
3
+ Copyright (C) 2022, Ecsypno <https://ecsypno.com/>
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without modification,
7
+ are permitted provided that the following conditions are met:
8
+
9
+ * Redistributions of source code must retain the above copyright notice,
10
+ this list of conditions and the following disclaimer.
11
+
12
+ * Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ * Neither the name of the copyright holder nor the names of its contributors
17
+ may be used to endorse or promote products derived from this software
18
+ without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
21
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
24
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
25
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
26
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
27
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # Toq
2
+
3
+ <table>
4
+ <tr>
5
+ <th>Version</th>
6
+ <td>0.0.1</td>
7
+ </tr>
8
+ <tr>
9
+ <th>Github page</th>
10
+ <td><a href="http://github.com/qadron/toq">http://github.com/qadron/toq</a></td>
11
+ <tr/>
12
+ <tr>
13
+ <th>Code Documentation</th>
14
+ <td><a href="http://rubydoc.info/github/qadron/toq/">http://rubydoc.info/github/qadron/toq/</a></td>
15
+ </tr>
16
+ <tr>
17
+ <th>Author</th>
18
+ <td><a href="mailto:tasos.laskos@gmail.com">Tasos Laskos</a></td>
19
+ </tr>
20
+ <tr>
21
+ <th>Copyright</th>
22
+ <td>2022 <a href="https://ecsypno.com">Ecsypno</a></td>
23
+ </tr>
24
+ <tr>
25
+ <th>License</th>
26
+ <td><a href="file.LICENSE.html">3-clause BSD</a></td>
27
+ </tr>
28
+ </table>
29
+
30
+ ## Synopsis
31
+
32
+ Toq is a simple and lightweight Remote Procedure Call protocol and implementation.
33
+
34
+ This implementation is based on [Raktr](https://github.com/qadron/raktr).
35
+
36
+ ## Features
37
+
38
+ - Extremely lightweight.
39
+ - Very simple design.
40
+ - TLS encryption.
41
+ - Configurable serializer.
42
+ - Can intercept RPC responses and translate them into native objects for
43
+ when using serializers that only support basic types, like JSON or MessagePack.
44
+ - Token-based authentication.
45
+ - Pure-Ruby.
46
+ - Multi-platform, tested on:
47
+ - Linux
48
+ - OSX
49
+ - Windows
50
+
51
+ ## Installation
52
+
53
+ gem install toq
54
+
55
+ ## Running the Specs
56
+
57
+ bundle install
58
+ rake spec
59
+
60
+ ## Protocol specifications
61
+
62
+ You can find the RPC protocol specification at the
63
+ [Wiki](https://github.com/Arachni/arachni-rpc/wiki).
64
+
65
+ ## License
66
+
67
+ Toq is provided under the 3-clause BSD license.
68
+ See the `LICENSE` file for more information.
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ =begin
2
+
3
+ This file is part of the Arachni-RPC project and may be subject to
4
+ redistribution and commercial restrictions. Please see the Arachni-RPC
5
+ web site for more information on licensing and terms of use.
6
+
7
+ =end
8
+
9
+ require 'rubygems'
10
+ require File.expand_path( File.dirname( __FILE__ ) ) + '/lib/toq/version'
11
+
12
+ begin
13
+ require 'rspec'
14
+ require 'rspec/core/rake_task'
15
+
16
+ RSpec::Core::RakeTask.new
17
+ rescue
18
+ end
19
+
20
+ task default: [ :build, :spec ]
21
+
22
+ desc 'Generate docs'
23
+ task :docs do
24
+ outdir = "../toq"
25
+ sh "rm -rf #{outdir}"
26
+ sh "mkdir -p #{outdir}"
27
+
28
+ sh "yardoc -o #{outdir}"
29
+
30
+ sh "rm -rf .yardoc"
31
+ end
32
+
33
+ desc 'Clean up'
34
+ task :clean do
35
+ sh 'rm *.gem || true'
36
+ end
37
+
38
+ desc 'Build the toq gem.'
39
+ task build: [ :clean ] do
40
+ sh 'gem build toq.gemspec'
41
+ end
42
+
43
+ desc 'Build and install the toq gem.'
44
+ task install: [ :build ] do
45
+ sh "gem install toq-#{Toq::VERSION}.gem"
46
+ end
47
+
48
+ desc 'Push a new version to Rubygems'
49
+ task publish: [ :build ] do
50
+ sh "git tag -a v#{Toq::VERSION} -m 'Version #{Toq::VERSION}'"
51
+ sh "gem push toq-#{Toq::VERSION}.gem"
52
+ end
53
+ task release: [ :publish ]
@@ -0,0 +1,165 @@
1
+ =begin
2
+
3
+ This file is part of the Arachni-RPC project and may be subject to
4
+ redistribution and commercial restrictions. Please see the Arachni-RPC EM
5
+ web site for more information on licensing and terms of use.
6
+
7
+ =end
8
+
9
+ module Toq
10
+ class Client
11
+
12
+ # Transmits {Request} objects and calls callbacks once an {Response} is received.
13
+ #
14
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
15
+ class Handler < Raktr::Connection
16
+ include Toq::Protocol
17
+
18
+ # Default amount of tries for failed requests.
19
+ DEFAULT_TRIES = 9
20
+
21
+ # @return [Symbol] Status of the connection, can be:
22
+ #
23
+ # * `:idle` -- Just initialized.
24
+ # * `:ready` -- A connection has been established.
25
+ # * `:pending` -- Sending request and awaiting response.
26
+ # * `:done` -- Response received and callback invoked -- ready to be reused.
27
+ # * `:closed` -- Connection closed.
28
+ attr_reader :status
29
+
30
+ # @return [Exceptions::ConnectionError]
31
+ attr_reader :error
32
+
33
+ # Prepares an RPC connection and sets {#status} to `:idle`.
34
+ #
35
+ # @param [Hash] opts
36
+ # @option opts [Integer] :max_retries (9)
37
+ # Default amount of tries for failed requests.
38
+ #
39
+ # @option opts [Client] :base
40
+ # Client instance needed to {Client#push_connection push} ourselves
41
+ # back to its connection pool once we're done and we're ready to be reused.
42
+ def initialize( opts )
43
+ @opts = opts.dup
44
+
45
+ @max_retries = @opts[:max_retries] || DEFAULT_TRIES
46
+ @client = @opts[:client]
47
+
48
+ @opts[:tries] ||= 0
49
+ @tries ||= @opts[:tries]
50
+
51
+ @status = :idle
52
+ @request = nil
53
+ end
54
+
55
+ # Sends an RPC request (i.e. performs an RPC call) and sets {#status}
56
+ # to `:pending`.
57
+ #
58
+ # @param [Request] req
59
+ def send_request( req )
60
+ @request = req
61
+ @status = :pending
62
+ super( req )
63
+ end
64
+
65
+ # @note Pushes itself to the client's connection pool to be re-used.
66
+ #
67
+ # Handles responses to RPC requests, calls its callback and sets {#status}
68
+ # to `:done`.
69
+ #
70
+ # @param [Toq::Response] res
71
+ def receive_response( res )
72
+ if res.exception?
73
+ res.obj = Exceptions.from_response( res )
74
+ end
75
+
76
+ @request.callback.call( res.obj ) if @request.callback
77
+ ensure
78
+ @request = nil # Help the GC out.
79
+ @error = nil # Help the GC out.
80
+ @status = :done
81
+
82
+ @opts[:tries] = @tries = 0
83
+ @client.push_connection self
84
+ end
85
+
86
+ # Handles closed connections, cleans up the SSL session, retries (if
87
+ # necessary) and sets {#status} to `:closed`.
88
+ #
89
+ # @private
90
+ def on_close( reason )
91
+ if @request
92
+ # If there is a request and a callback and the callback hasn't yet be
93
+ # called (i.e. not done) then we got here by error so retry.
94
+ if @request && @request.callback && !done?
95
+ if retry?
96
+ retry_request
97
+ else
98
+ @error = e = Exceptions::ConnectionError.new( "Connection closed [#{reason}]" )
99
+ @request.callback.call( e )
100
+ @client.connection_failed self
101
+ end
102
+
103
+ return
104
+ end
105
+ else
106
+ @error = reason
107
+ @client.connection_failed self
108
+ end
109
+
110
+ close_without_retry
111
+ end
112
+
113
+ # @note If `true`, the connection can be re-used.
114
+ #
115
+ # @return [Boolean]
116
+ # `true` when the connection is done, `false` otherwise.
117
+ def done?
118
+ @status == :done
119
+ end
120
+
121
+ # Closes the connection without triggering a retry operation and sets
122
+ # {#status} to `:closed`.
123
+ def close_without_retry
124
+ @request = nil
125
+ @status = :closed
126
+ close_without_callback
127
+ end
128
+
129
+ private
130
+
131
+ # Converts incoming hash objects to {Response} objects and calls
132
+ # {#receive_response}.
133
+ #
134
+ # @param [Hash] obj
135
+ def receive_object( obj )
136
+ receive_response( Response.new( obj ) )
137
+ end
138
+
139
+ def retry_request
140
+ opts = @opts.dup
141
+ opts[:tries] += 1
142
+
143
+ req = @request.dup
144
+
145
+ # The connection will be detached soon, keep a separate reference to
146
+ # the reactor.
147
+ reactor = @reactor
148
+
149
+ @tries += 1
150
+ reactor.delay( 0.2 ) do
151
+ address = opts[:socket] ? opts[:socket] : [opts[:host], opts[:port]]
152
+ reactor.connect( *[address, self.class, opts ].flatten ).send_request( req )
153
+ end
154
+
155
+ close_without_retry
156
+ end
157
+
158
+ def retry?
159
+ @tries < @max_retries
160
+ end
161
+
162
+ end
163
+
164
+ end
165
+ end
data/lib/toq/client.rb ADDED
@@ -0,0 +1,249 @@
1
+ =begin
2
+
3
+ This file is part of the Arachni-RPC project and may be subject to
4
+ redistribution and commercial restrictions. Please see the Arachni-RPC EM
5
+ web site for more information on licensing and terms of use.
6
+
7
+ =end
8
+
9
+ module Toq
10
+
11
+ require_relative 'client/handler'
12
+
13
+ # Simple RPC client capable of:
14
+ #
15
+ # * TLS encryption.
16
+ # * Asynchronous and synchronous requests.
17
+ # * Handling remote asynchronous calls that defer their result.
18
+ #
19
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
20
+ class Client
21
+
22
+ # Default amount of connections to maintain in the re-use pool.
23
+ DEFAULT_CONNECTION_POOL_SIZE = 1
24
+
25
+ # @return [Hash]
26
+ # Options hash.
27
+ attr_reader :opts
28
+
29
+ # @return [Integer]
30
+ # Amount of connections in the pool.
31
+ attr_reader :connection_count
32
+
33
+ # @example Example options:
34
+ #
35
+ # {
36
+ # :host => 'localhost',
37
+ # :port => 7331,
38
+ #
39
+ # # optional authentication token, if it doesn't match the one
40
+ # # set on the server-side you'll be getting exceptions.
41
+ # :token => 'superdupersecret',
42
+ #
43
+ # :serializer => Marshal,
44
+ #
45
+ # :max_retries => 0,
46
+ #
47
+ # # In order to enable peer verification one must first provide
48
+ # # the following:
49
+ # # SSL CA certificate
50
+ # :ssl_ca => cwd + '/../spec/pems/cacert.pem',
51
+ # # SSL private key
52
+ # :ssl_pkey => cwd + '/../spec/pems/client/key.pem',
53
+ # # SSL certificate
54
+ # :ssl_cert => cwd + '/../spec/pems/client/cert.pem'
55
+ # }
56
+ #
57
+ # @param [Hash] opts
58
+ # @option opts [String] :host Hostname/IP address.
59
+ # @option opts [Integer] :port Port number.
60
+ # @option opts [String] :socket Path to UNIX domain socket.
61
+ # @option opts [Integer] :connection_pool_size (1)
62
+ # Amount of connections to keep open.
63
+ # @option opts [String] :token Optional authentication token.
64
+ # @option opts [.dump, .load] :serializer (YAML)
65
+ # Serializer to use for message transmission.
66
+ # @option opts [Integer] :max_retries
67
+ # How many times to retry failed requests.
68
+ # @option opts [String] :ssl_ca SSL CA certificate.
69
+ # @option opts [String] :ssl_pkey SSL private key.
70
+ # @option opts [String] :ssl_cert SSL certificate.
71
+ def initialize( opts )
72
+ @opts = opts.merge( role: :client )
73
+ @token = @opts[:token]
74
+
75
+ @host, @port = @opts[:host], @opts[:port].to_i
76
+ @socket = @opts[:socket]
77
+
78
+ if !@socket && !(@host || @port)
79
+ fail ArgumentError, 'Needs either a :socket or :host and :port options.'
80
+ end
81
+
82
+ @port = @port.to_i
83
+
84
+ if @host && @port <= 0
85
+ fail ArgumentError, "Invalid port: #{@port}"
86
+ end
87
+
88
+ @pool_size = @opts[:connection_pool_size] || DEFAULT_CONNECTION_POOL_SIZE
89
+
90
+ @reactor = Raktr.global
91
+
92
+ @connections = @reactor.create_queue
93
+ @connection_count = 0
94
+ end
95
+
96
+ # Connection factory, will re-use or create new connections as needed to
97
+ # accommodate the workload.
98
+ #
99
+ # @param [Block] block
100
+ # Block to be passed a {Handler connection}.
101
+ #
102
+ # @return [Boolean]
103
+ # `true` if a new connection had to be established, `false` if an existing
104
+ # one was re-used.
105
+ def connect( &block )
106
+ ensure_reactor_running
107
+
108
+ if @connections.empty? && @connection_count < @pool_size
109
+ opts = @socket ? @socket : [@host, @port]
110
+ block.call @reactor.connect( *[opts, Handler, @opts.merge( client: self )].flatten )
111
+ increment_connection_counter
112
+ return true
113
+ end
114
+
115
+ pop_block = proc do |conn|
116
+ # Some connections may have died while they were waiting in the
117
+ # queue, get rid of them and start all over in case the queue has
118
+ # been emptied.
119
+ if !conn.done?
120
+ connection_failed conn
121
+ connect( &block )
122
+ next
123
+ end
124
+
125
+ block.call conn
126
+ end
127
+
128
+ @connections.pop( &pop_block )
129
+
130
+ false
131
+ end
132
+
133
+ # Close all connections.
134
+ def close
135
+ ensure_reactor_running
136
+
137
+ @reactor.on_tick do |task|
138
+ @connections.pop(&:close_without_retry)
139
+ task.done if @connections.empty?
140
+ end
141
+ end
142
+
143
+ def increment_connection_counter
144
+ @connection_count += 1
145
+ end
146
+
147
+ # {Handler#done? Finished} {Handler}s push themselves here to be re-used.
148
+ #
149
+ # @param [Handler] connection
150
+ def push_connection( connection )
151
+ ensure_reactor_running
152
+
153
+ @connections << connection
154
+ end
155
+
156
+ # Handles failed connections.
157
+ #
158
+ # @param [Handler] connection
159
+ def connection_failed( connection )
160
+ ensure_reactor_running
161
+
162
+ @connection_count -= 1
163
+ connection.close_without_retry
164
+ end
165
+
166
+ # Calls a remote method and grabs the result.
167
+ #
168
+ # There are 2 ways to perform a call, async (non-blocking) and sync (blocking).
169
+ #
170
+ # @example To perform an async call you need to provide a block to handle the result.
171
+ #
172
+ # server.call( 'handler.method', arg1, arg2 ) do |res|
173
+ # do_stuff( res )
174
+ # end
175
+ #
176
+ # @example To perform a sync (blocking), call without a block.
177
+ #
178
+ # res = server.call( 'handler.method', arg1, arg2 )
179
+ #
180
+ # @param [String] msg
181
+ # RPC message in the form of `handler.method`.
182
+ # @param [Array] args
183
+ # Collection of arguments to be passed to the method.
184
+ # @param [Block] block
185
+ def call( msg, *args, &block )
186
+ ensure_reactor_running
187
+
188
+ req = Request.new(
189
+ message: msg,
190
+ args: args,
191
+ callback: block,
192
+ token: @token
193
+ )
194
+
195
+ block_given? ? call_async( req ) : call_sync( req )
196
+ end
197
+
198
+ private
199
+
200
+ def set_exception( req, e )
201
+ msg = @socket ? " for '#{@socket}'." : " for '#{@host}:#{@port}'."
202
+
203
+ exc = (e.is_a?( Raktr::Connection::Error::SSL ) ?
204
+ Exceptions::SSLPeerVerificationFailed : Exceptions::ConnectionError
205
+ ).new( e.to_s + msg )
206
+
207
+ exc.set_backtrace e.backtrace
208
+ req.callback.call exc
209
+ end
210
+
211
+ def call_async( req, &block )
212
+ req.callback = block if block_given?
213
+
214
+ begin
215
+ connect do |connection|
216
+ error = (connection.is_a?( Exception ) and connection) || connection.error
217
+ next set_exception( req, error ) if error
218
+
219
+ connection.send_request( req )
220
+ end
221
+ rescue => e
222
+ set_exception( req, e )
223
+ end
224
+ end
225
+
226
+ def call_sync( req )
227
+ # If we're in the Reactor thread use a Fiber and if we're not use a Thread.
228
+ if @reactor.in_same_thread?
229
+ fail 'Cannot perform synchronous calls when running in the ' +
230
+ "#{Raktr} loop."
231
+ end
232
+
233
+ q = Queue.new
234
+ call_async( req ) { |obj| q << obj }
235
+ ret = q.pop
236
+
237
+ raise ret if ret.is_a?( Exception )
238
+
239
+ ret
240
+ end
241
+
242
+ def ensure_reactor_running
243
+ return if @reactor.running?
244
+ @reactor.run_in_thread
245
+ end
246
+
247
+ end
248
+
249
+ end