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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +9 -3
- data/LICENSE.md +1 -1
- data/README.md +28 -34
- data/Rakefile +16 -19
- data/lib/arachni/rpc.rb +4 -8
- data/lib/arachni/rpc/client.rb +236 -0
- data/lib/arachni/rpc/client/handler.rb +167 -0
- data/lib/arachni/rpc/exceptions.rb +14 -38
- data/lib/arachni/rpc/message.rb +7 -15
- data/lib/arachni/rpc/protocol.rb +103 -0
- data/lib/arachni/rpc/proxy.rb +86 -0
- data/lib/arachni/rpc/request.rb +18 -36
- data/lib/arachni/rpc/response.rb +21 -35
- data/lib/arachni/rpc/server.rb +278 -0
- data/lib/arachni/rpc/server/handler.rb +145 -0
- data/lib/arachni/rpc/version.rb +3 -1
- data/spec/arachni/rpc/client_spec.rb +400 -0
- data/spec/arachni/rpc/exceptions_spec.rb +77 -0
- data/spec/arachni/rpc/message_spec.rb +47 -0
- data/spec/arachni/rpc/proxy_spec.rb +99 -0
- data/spec/arachni/rpc/request_spec.rb +53 -0
- data/spec/arachni/rpc/response_spec.rb +49 -0
- data/spec/arachni/rpc/server_spec.rb +129 -0
- data/spec/pems/cacert.pem +37 -0
- data/spec/pems/client/cert.pem +37 -0
- data/spec/pems/client/foo-cert.pem +39 -0
- data/spec/pems/client/foo-key.pem +51 -0
- data/spec/pems/client/key.pem +51 -0
- data/spec/pems/server/cert.pem +37 -0
- data/spec/pems/server/key.pem +51 -0
- data/spec/servers/basic.rb +3 -0
- data/spec/servers/server.rb +83 -0
- data/spec/servers/unix_socket.rb +8 -0
- data/spec/servers/with_ssl_primitives.rb +11 -0
- data/spec/spec_helper.rb +39 -0
- metadata +78 -21
- data/lib/arachni/rpc/remote_object_mapper.rb +0 -65
@@ -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
|
data/lib/arachni/rpc/version.rb
CHANGED
@@ -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
|