arachni-rpc 0.1.3 → 0.2.0

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.
@@ -0,0 +1,145 @@
1
+ =begin
2
+
3
+ This file is part of the Arachni-RPC EM 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 Arachni
10
+ module RPC
11
+ class Server
12
+
13
+ # Receives {Request} objects and transmits {Response} objects.
14
+ #
15
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
16
+ class Handler < Reactor::Connection
17
+ include Protocol
18
+
19
+ # @return [Request]
20
+ # Working RPC request.
21
+ attr_reader :request
22
+
23
+ # @param [Server] server
24
+ # RPC server.
25
+ def initialize( server )
26
+ @server = server
27
+ @opts = server.opts.dup
28
+ @request = nil
29
+ end
30
+
31
+ # Handles closed connections and cleans up the SSL session.
32
+ #
33
+ # @private
34
+ def on_close( _ )
35
+ @server = nil
36
+ end
37
+
38
+ # * Handles {Request}
39
+ # * Sets the {#request}.
40
+ # * Sends back {Response}.
41
+ #
42
+ # @param [Request] req
43
+ def receive_request( req )
44
+ @request = req
45
+
46
+ # Create an empty response to be filled in little by little.
47
+ res = Response.new
48
+ peer = peer_ip_address
49
+
50
+ begin
51
+ # Make sure the client is allowed to make RPC calls.
52
+ authenticate!
53
+
54
+ # Grab the partially filled in response which includes the result
55
+ # of the RPC call and merge it with out prepared response.
56
+ res.merge!( @server.call( self ) )
57
+
58
+ # Handle exceptions and convert them to a simple hash, ready to be
59
+ # passed to the client.
60
+ rescue Exception => e
61
+ type = ''
62
+
63
+ # If it's an RPC exception pass the type along as is...
64
+ if e.rpc_exception?
65
+ type = e.class.name.split( ':' )[-1]
66
+
67
+ # ...otherwise set it to a RemoteException.
68
+ else
69
+ type = 'RemoteException'
70
+ end
71
+
72
+ # RPC conventions for exception transmission.
73
+ res.exception = {
74
+ 'type' => type,
75
+ 'message' => e.to_s,
76
+ 'backtrace' => e.backtrace
77
+ }
78
+
79
+ msg = "#{e.to_s}\n#{e.backtrace.join( "\n" )}"
80
+ @server.logger.error( 'Exception' ){ msg + " [on behalf of #{peer}]" }
81
+ end
82
+
83
+ # Pass the result of the RPC call back to the client but *only* if it
84
+ # wasn't async, otherwise {Server#call} will have already taken care of it.
85
+ send_response( res ) if !res.async?
86
+ end
87
+
88
+ private
89
+
90
+ # Converts incoming hash objects to {Request} objects and calls
91
+ # {#receive_request}.
92
+ #
93
+ # @param [Hash] obj
94
+ def receive_object( obj )
95
+ receive_request( Request.new( obj ) )
96
+ end
97
+
98
+ # @param [Symbol] severity
99
+ #
100
+ # Severity of the logged event:
101
+ #
102
+ # * `:debug`
103
+ # * `:info`
104
+ # * `:warn`
105
+ # * `:error`
106
+ # * `:fatal`
107
+ # * `:unknown`
108
+ #
109
+ # @param [String] category
110
+ # Category of message (SSL, Call, etc.).
111
+ # @param [String] msg
112
+ # Message to log.
113
+ def log( severity, category, msg )
114
+ sev_sym = Logger.const_get( severity.to_s.upcase.to_sym )
115
+ @server.logger.add( sev_sym, msg, category )
116
+ end
117
+
118
+ # Authenticates the client based on the token in the request.
119
+ #
120
+ # It will raise an exception if the token doesn't check-out.
121
+ def authenticate!
122
+ return if valid_token?( @request.token )
123
+
124
+ msg = "Token missing or invalid while calling: #{@request.message}"
125
+
126
+ @server.logger.error( 'Authenticator' ){
127
+ msg + " [on behalf of #{peer_ip_address}]"
128
+ }
129
+
130
+ fail Exceptions::InvalidToken.new( msg )
131
+ end
132
+
133
+ # Compares the authentication token in the param with the one of the server.
134
+ #
135
+ # @param [String] token
136
+ #
137
+ # @return [Bool]
138
+ def valid_token?( token )
139
+ token == @server.token
140
+ end
141
+
142
+ end
143
+ end
144
+ end
145
+ end
@@ -8,6 +8,8 @@
8
8
 
9
9
  module Arachni
10
10
  module RPC
11
- VERSION = '0.1.3'
11
+
12
+ VERSION = '0.2.0'
13
+
12
14
  end
13
15
  end
@@ -0,0 +1,400 @@
1
+ require 'spec_helper'
2
+
3
+ describe Arachni::RPC::Client do
4
+
5
+ def wait
6
+ Arachni::Reactor.global.wait rescue Arachni::Reactor::Error::NotRunning
7
+ end
8
+
9
+ before(:each) do
10
+ if Arachni::Reactor.global.running?
11
+ Arachni::Reactor.stop
12
+ end
13
+
14
+ Arachni::Reactor.global.run_in_thread
15
+ end
16
+
17
+ let(:arguments) do
18
+ [
19
+ 'one',
20
+ 2,
21
+ { three: 3 },
22
+ [ 4 ]
23
+ ]
24
+ end
25
+ let(:reactor) { Arachni::Reactor.global }
26
+ let(:handler) { 'test' }
27
+ let(:remote_method) { 'foo' }
28
+ let(:options) { rpc_opts }
29
+ subject do
30
+ start_client( options )
31
+ end
32
+
33
+ def call( &block )
34
+ subject.call( "#{handler}.#{remote_method}", arguments, &block )
35
+ end
36
+
37
+ describe '#initialize' do
38
+ let(:options) { rpc_opts.merge( role: :client ) }
39
+
40
+ it 'assigns instance options (including :role)' do
41
+ subject.opts.should == options
42
+ end
43
+
44
+ context 'when passed no connection information' do
45
+ it 'raises ArgumentError' do
46
+ begin
47
+ described_class.new({})
48
+ rescue => e
49
+ e.should be_kind_of ArgumentError
50
+ end
51
+ end
52
+ end
53
+
54
+ describe 'option' do
55
+ describe :socket, if: Arachni::Reactor.supports_unix_sockets? do
56
+ let(:options) { rpc_opts_with_socket }
57
+
58
+ it 'connects to it' do
59
+ call.should == arguments
60
+ end
61
+
62
+ context 'when under heavy load' do
63
+ it 'retains stability and consistency' do
64
+ n = 10_000
65
+ cnt = 0
66
+
67
+ mismatches = []
68
+
69
+ n.times do |i|
70
+ arg = 'a' * i
71
+ subject.call( "#{handler}.#{remote_method}", arg ) do |res|
72
+ cnt += 1
73
+ mismatches << [i, arg, res] if arg != res
74
+ Arachni::Reactor.stop if cnt == n || mismatches.any?
75
+ end
76
+ end
77
+ wait
78
+
79
+ cnt.should > 0
80
+ mismatches.should be_empty
81
+ end
82
+ end
83
+
84
+ context 'and connecting to a non-existent server' do
85
+ let(:options) { rpc_opts_with_socket.merge( socket: '/' ) }
86
+
87
+ it "returns #{Arachni::RPC::Exceptions::ConnectionError}" do
88
+ response = nil
89
+ call do |res|
90
+ response = res
91
+ Arachni::Reactor.stop
92
+ end
93
+ wait
94
+
95
+ response.should be_rpc_connection_error
96
+ response.should be_kind_of Arachni::RPC::Exceptions::ConnectionError
97
+ end
98
+ end
99
+
100
+ context 'when passed an invalid socket path' do
101
+ it 'raises ArgumentError' do
102
+ begin
103
+ described_class.new( socket: 'blah' )
104
+ rescue => e
105
+ e.should be_kind_of ArgumentError
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ context 'when passed a host but not a port' do
113
+ it 'raises ArgumentError' do
114
+ begin
115
+ described_class.new( host: 'test' )
116
+ rescue => e
117
+ e.should be_kind_of ArgumentError
118
+ end
119
+ end
120
+ end
121
+
122
+ context 'when passed a port but not a host' do
123
+ it 'raises ArgumentError' do
124
+ begin
125
+ described_class.new( port: 9999 )
126
+ rescue => e
127
+ e.should be_kind_of ArgumentError
128
+ end
129
+ end
130
+ end
131
+
132
+ context 'when passed an invalid port' do
133
+ it 'raises ArgumentError' do
134
+ begin
135
+ described_class.new( host: 'tt', port: 'blah' )
136
+ rescue => e
137
+ e.should be_kind_of ArgumentError
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ describe '#call' do
144
+ context 'when calling a remote method that delays its results' do
145
+ let(:remote_method) { 'delay' }
146
+
147
+ it 'it supports it' do
148
+ call.should == arguments
149
+ end
150
+ end
151
+
152
+ context 'when calling a remote method that defers its results' do
153
+ let(:remote_method) { 'defer' }
154
+
155
+ it 'it supports it' do
156
+ call.should == arguments
157
+ end
158
+ end
159
+
160
+ context 'when under heavy load' do
161
+ it 'retains stability and consistency' do
162
+ n = 10_000
163
+ cnt = 0
164
+
165
+ mismatches = []
166
+
167
+ n.times do |i|
168
+ arg = 'a' * i
169
+ subject.call( "#{handler}.#{remote_method}", arg ) do |res|
170
+ cnt += 1
171
+ mismatches << [i, arg, res] if arg != res
172
+ Arachni::Reactor.stop if cnt == n || mismatches.any?
173
+ end
174
+ end
175
+
176
+ wait
177
+
178
+ cnt.should > 0
179
+ mismatches.should be_empty
180
+ end
181
+ end
182
+
183
+ context 'when using Threads' do
184
+ it 'should be able to perform synchronous calls' do
185
+ arguments.should == call
186
+ end
187
+
188
+ it 'should be able to perform asynchronous calls' do
189
+ response = nil
190
+ call do |res|
191
+ response = res
192
+ Arachni::Reactor.stop
193
+ end
194
+ wait
195
+
196
+ response.should == arguments
197
+ end
198
+ end
199
+
200
+ context 'when run inside the Reactor loop' do
201
+ it 'should be able to perform asynchronous calls' do
202
+ response = nil
203
+
204
+ Arachni::Reactor.stop
205
+ reactor.run do
206
+ call do |res|
207
+ response = res
208
+ Arachni::Reactor.stop
209
+ end
210
+ end
211
+
212
+ response.should == arguments
213
+ end
214
+
215
+ it 'should not be able to perform synchronous calls' do
216
+ exception = nil
217
+
218
+ Arachni::Reactor.stop
219
+ reactor.run do
220
+ begin
221
+ call
222
+ rescue => e
223
+ exception = e
224
+ Arachni::Reactor.stop
225
+ end
226
+ end
227
+
228
+ exception.should be_kind_of RuntimeError
229
+ end
230
+ end
231
+
232
+ context 'when performing an asynchronous call' do
233
+ context 'and connecting to a non-existent server' do
234
+ let(:options) { rpc_opts.merge( host: 'dddd', port: 999339 ) }
235
+
236
+ it "returns #{Arachni::RPC::Exceptions::ConnectionError}" do
237
+ response = nil
238
+ call do |res|
239
+ response = res
240
+ Arachni::Reactor.stop
241
+ end
242
+ wait
243
+
244
+ response.should be_rpc_connection_error
245
+ response.should be_kind_of Arachni::RPC::Exceptions::ConnectionError
246
+ end
247
+ end
248
+
249
+ context 'and requesting a non-existent object' do
250
+ let(:handler) { 'bar' }
251
+
252
+ it "returns #{Arachni::RPC::Exceptions::InvalidObject}" do
253
+ response = nil
254
+
255
+ call do |res|
256
+ response = res
257
+ Arachni::Reactor.stop
258
+ end
259
+ wait
260
+
261
+ response.should be_rpc_invalid_object_error
262
+ response.should be_kind_of Arachni::RPC::Exceptions::InvalidObject
263
+ end
264
+ end
265
+
266
+ context 'and requesting a non-public method' do
267
+ let(:remote_method) { 'bar' }
268
+
269
+ it "returns #{Arachni::RPC::Exceptions::InvalidMethod}" do
270
+ response = nil
271
+
272
+ call do |res|
273
+ response = res
274
+ Arachni::Reactor.stop
275
+ end
276
+ wait
277
+
278
+ response.should be_rpc_invalid_method_error
279
+ response.should be_kind_of Arachni::RPC::Exceptions::InvalidMethod
280
+ end
281
+ end
282
+
283
+ context 'and there is a remote exception' do
284
+ let(:remote_method) { :exception }
285
+
286
+ it "returns #{Arachni::RPC::Exceptions::RemoteException}" do
287
+ response = nil
288
+ call do |res|
289
+ response = res
290
+ Arachni::Reactor.stop
291
+ end
292
+ wait
293
+
294
+ response.should be_rpc_remote_exception
295
+ response.should be_kind_of Arachni::RPC::Exceptions::RemoteException
296
+ end
297
+ end
298
+ end
299
+
300
+ context 'when performing a synchronous call' do
301
+ context 'and connecting to a non-existent server' do
302
+ let(:options) { rpc_opts.merge( host: 'dddd', port: 999339 ) }
303
+
304
+ it "raises #{Arachni::RPC::Exceptions::ConnectionError}" do
305
+ begin
306
+ call
307
+ rescue => e
308
+ e.rpc_connection_error?.should be_true
309
+ e.should be_kind_of Arachni::RPC::Exceptions::ConnectionError
310
+ end
311
+ end
312
+ end
313
+
314
+ context 'and requesting a non-existent object' do
315
+ let(:handler) { 'bar' }
316
+
317
+ it "raises #{Arachni::RPC::Exceptions::InvalidObject}" do
318
+ begin
319
+ call
320
+ rescue => e
321
+ e.rpc_invalid_object_error?.should be_true
322
+ e.should be_kind_of Arachni::RPC::Exceptions::InvalidObject
323
+ end
324
+ end
325
+ end
326
+
327
+ context 'and requesting a non-public method' do
328
+ let(:remote_method) { 'bar' }
329
+
330
+ it "raises #{Arachni::RPC::Exceptions::InvalidMethod}" do
331
+ begin
332
+ call
333
+ rescue => e
334
+ e.rpc_invalid_method_error?.should be_true
335
+ e.should be_kind_of Arachni::RPC::Exceptions::InvalidMethod
336
+ end
337
+ end
338
+ end
339
+
340
+ context 'and there is a remote exception' do
341
+ let(:remote_method) { :exception }
342
+
343
+ it "raises #{Arachni::RPC::Exceptions::RemoteException}" do
344
+ begin
345
+ call
346
+ rescue => e
347
+ e.rpc_remote_exception?.should be_true
348
+ e.should be_kind_of Arachni::RPC::Exceptions::RemoteException
349
+ end
350
+ end
351
+ end
352
+ end
353
+
354
+ context 'when using valid SSL configuration' do
355
+ let(:options) { rpc_opts_with_ssl_primitives }
356
+
357
+ it 'should be able to establish a connection' do
358
+ call.should == arguments
359
+ end
360
+ end
361
+
362
+ context 'when using invalid SSL configuration' do
363
+ let(:options) { rpc_opts_with_invalid_ssl_primitives }
364
+
365
+ it 'should not be able to establish a connection' do
366
+ response = nil
367
+
368
+ Arachni::Reactor.stop
369
+ reactor.run do
370
+ call do |res|
371
+ response = res
372
+ Arachni::Reactor.stop
373
+ end
374
+ end
375
+
376
+ response.should be_rpc_connection_error
377
+ end
378
+ end
379
+
380
+ context 'when using mixed SSL configuration' do
381
+ let(:options) { rpc_opts_with_mixed_ssl_primitives }
382
+
383
+ it 'should not be able to establish a connection' do
384
+ response = nil
385
+
386
+ Arachni::Reactor.stop
387
+ reactor.run do
388
+ call do |res|
389
+ response = res
390
+ Arachni::Reactor.stop
391
+ end
392
+ end
393
+
394
+ response.should be_rpc_connection_error
395
+ response.should be_rpc_ssl_error
396
+ end
397
+ end
398
+ end
399
+
400
+ end