arachni-rpc 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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