toq 0.0.1

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.
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