arachni-rpc-em 0.1.3 → 0.2
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.
- data/CHANGELOG.md +17 -1
- data/README.md +26 -15
- data/Rakefile +12 -31
- data/lib/arachni/rpc/em.rb +0 -1
- data/lib/arachni/rpc/em/client.rb +149 -145
- data/lib/arachni/rpc/em/client/handler.rb +180 -0
- data/lib/arachni/rpc/em/connection_utilities.rb +1 -3
- data/lib/arachni/rpc/em/em.rb +3 -9
- data/lib/arachni/rpc/em/protocol.rb +59 -70
- data/lib/arachni/rpc/em/server.rb +75 -185
- data/lib/arachni/rpc/em/server/handler.rb +161 -0
- data/lib/arachni/rpc/em/version.rb +1 -1
- data/spec/arachni/rpc/em/client_spec.rb +165 -55
- data/spec/arachni/rpc/em/em_spec.rb +1 -1
- data/spec/arachni/rpc/em/server_spec.rb +82 -30
- data/spec/arachni/rpc/em/ssl_spec.rb +1 -1
- data/spec/servers/server.rb +23 -14
- data/spec/servers/unix_socket.rb +11 -0
- data/spec/servers/with_fallback.rb +2 -3
- data/spec/servers/with_ssl_primitives.rb +3 -0
- data/spec/spec_helper.rb +6 -8
- metadata +45 -2
@@ -0,0 +1,161 @@
|
|
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::EM
|
11
|
+
class Server
|
12
|
+
|
13
|
+
#
|
14
|
+
# Receives `Arachni::RPC::Request` objects and transmits `Arachni::RPC::Response`
|
15
|
+
# objects.
|
16
|
+
#
|
17
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
18
|
+
#
|
19
|
+
class Handler < EventMachine::Connection
|
20
|
+
include Protocol
|
21
|
+
include ConnectionUtilities
|
22
|
+
include ::Arachni::RPC::Exceptions
|
23
|
+
|
24
|
+
#INACTIVITY_TIMEOUT = 10
|
25
|
+
|
26
|
+
# @return [Arachni::RPC::Request] Working RPC request.
|
27
|
+
attr_reader :request
|
28
|
+
|
29
|
+
# @param [Server] server RPC server.
|
30
|
+
def initialize( server )
|
31
|
+
super
|
32
|
+
|
33
|
+
@server = server
|
34
|
+
@opts = server.opts.dup
|
35
|
+
@request = nil
|
36
|
+
|
37
|
+
assume_server_role!
|
38
|
+
|
39
|
+
# Do not tolerate long periods of inactivity in order to avoid zombie
|
40
|
+
# connections.
|
41
|
+
#set_comm_inactivity_timeout( INACTIVITY_TIMEOUT )
|
42
|
+
end
|
43
|
+
|
44
|
+
# Initializes an SSL session once the connection has been established.
|
45
|
+
#
|
46
|
+
# @private
|
47
|
+
def post_init
|
48
|
+
start_ssl
|
49
|
+
end
|
50
|
+
|
51
|
+
# Handles closed connections and cleans up the SSL session.
|
52
|
+
#
|
53
|
+
# @private
|
54
|
+
def unbind
|
55
|
+
end_ssl
|
56
|
+
@server = nil
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# * Handles a `Arachni::RPC::Request`
|
61
|
+
# * Sets the {#request}.
|
62
|
+
# * Sends back a `Arachni::RPC::Response`.
|
63
|
+
#
|
64
|
+
# @param [Arachni::RPC::EM::Request] req
|
65
|
+
#
|
66
|
+
def receive_request( req )
|
67
|
+
@request = req
|
68
|
+
|
69
|
+
# Create an empty response to be filled in little by little.
|
70
|
+
res = ::Arachni::RPC::Response.new
|
71
|
+
peer = peer_ip_addr
|
72
|
+
|
73
|
+
begin
|
74
|
+
# Make sure the client is allowed to make RPC calls.
|
75
|
+
authenticate!
|
76
|
+
|
77
|
+
# Grab the partially filled in response which includes the result
|
78
|
+
# of the RPC call and merge it with out prepared response.
|
79
|
+
res.merge!( @server.call( self ) )
|
80
|
+
|
81
|
+
# Handle exceptions and convert them to a simple hash, ready to be
|
82
|
+
# passed to the client.
|
83
|
+
rescue Exception => e
|
84
|
+
type = ''
|
85
|
+
|
86
|
+
# If it's an RPC exception pass the type along as is...
|
87
|
+
if e.rpc_exception?
|
88
|
+
type = e.class.name.split( ':' )[-1]
|
89
|
+
|
90
|
+
# ...otherwise set it to a RemoteException.
|
91
|
+
else
|
92
|
+
type = 'RemoteException'
|
93
|
+
end
|
94
|
+
|
95
|
+
# RPC conventions for exception transmission.
|
96
|
+
res.obj = {
|
97
|
+
'exception' => e.to_s,
|
98
|
+
'backtrace' => e.backtrace,
|
99
|
+
'type' => type
|
100
|
+
}
|
101
|
+
|
102
|
+
msg = "#{e.to_s}\n#{e.backtrace.join( "\n" )}"
|
103
|
+
@server.logger.error( 'Exception' ){ msg + " [on behalf of #{peer}]" }
|
104
|
+
end
|
105
|
+
|
106
|
+
# Pass the result of the RPC call back to the client but *only* if it
|
107
|
+
# wasn't async, otherwise {Server#call} will have already taken care of it
|
108
|
+
send_response( res ) if !res.async?
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
# @param [Symbol] severity Severity of the logged event:
|
114
|
+
# * `:debug`
|
115
|
+
# * `:info`
|
116
|
+
# * `:warn`
|
117
|
+
# * `:error`
|
118
|
+
# * `:fatal`
|
119
|
+
# * `:unknown`
|
120
|
+
#
|
121
|
+
# @param [String] category Category of message (SSL, Call, etc.).
|
122
|
+
# @param [String] msg Message to log.
|
123
|
+
#
|
124
|
+
def log( severity, category, msg )
|
125
|
+
sev_sym = Logger.const_get( severity.to_s.upcase.to_sym )
|
126
|
+
@server.logger.add( sev_sym, msg, category )
|
127
|
+
end
|
128
|
+
|
129
|
+
#
|
130
|
+
# Authenticates the client based on the token in the request.
|
131
|
+
#
|
132
|
+
# It will raise an exception if the token doesn't check-out.
|
133
|
+
#
|
134
|
+
def authenticate!
|
135
|
+
if !valid_token?( @request.token )
|
136
|
+
|
137
|
+
msg = 'Token missing or invalid while calling: ' + @request.message
|
138
|
+
|
139
|
+
@server.logger.error( 'Authenticator' ){
|
140
|
+
msg + " [on behalf of #{peer_ip_addr}]"
|
141
|
+
}
|
142
|
+
|
143
|
+
fail InvalidToken.new( msg )
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
#
|
148
|
+
# Compares the authentication token in the param with the one of the server.
|
149
|
+
#
|
150
|
+
# @param [String] token
|
151
|
+
#
|
152
|
+
# @return [Bool]
|
153
|
+
#
|
154
|
+
def valid_token?( token )
|
155
|
+
token == @server.token
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require
|
1
|
+
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Arachni::RPC::EM::Client do
|
4
4
|
|
@@ -8,19 +8,20 @@ describe Arachni::RPC::EM::Client do
|
|
8
8
|
]
|
9
9
|
end
|
10
10
|
|
11
|
-
it
|
11
|
+
it 'retains stability and consistency under heavy load' do
|
12
12
|
client = start_client( rpc_opts )
|
13
13
|
|
14
|
-
n
|
15
|
-
cnt
|
14
|
+
n = 100_000
|
15
|
+
cnt = 0
|
16
16
|
|
17
17
|
mismatches = []
|
18
18
|
|
19
19
|
n.times do |i|
|
20
|
-
|
20
|
+
arg = 'a' * i
|
21
|
+
client.call( 'test.foo', arg ) do |res|
|
21
22
|
cnt += 1
|
22
|
-
mismatches << [i, res] if
|
23
|
-
::EM.stop if cnt == n ||
|
23
|
+
mismatches << [i, arg, res] if arg != res
|
24
|
+
::EM.stop if cnt == n || mismatches.any?
|
24
25
|
end
|
25
26
|
end
|
26
27
|
|
@@ -29,24 +30,103 @@ describe Arachni::RPC::EM::Client do
|
|
29
30
|
mismatches.should be_empty
|
30
31
|
end
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
describe '#initialize' do
|
34
|
+
it 'should be able to properly assign class options (including :role)' do
|
35
|
+
opts = rpc_opts.merge( role: :client )
|
36
|
+
start_client( opts ).opts.should == opts
|
36
37
|
end
|
37
|
-
Arachni::RPC::EM.block
|
38
38
|
|
39
|
-
|
40
|
-
|
41
|
-
|
39
|
+
context 'when passed no connection information' do
|
40
|
+
it 'raises ArgumentError' do
|
41
|
+
begin
|
42
|
+
described_class.new({})
|
43
|
+
rescue => e
|
44
|
+
e.should be_kind_of ArgumentError
|
45
|
+
end
|
46
|
+
end
|
42
47
|
end
|
43
|
-
Arachni::RPC::EM.block
|
44
|
-
end
|
45
48
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
49
|
+
describe 'option' do
|
50
|
+
describe :socket do
|
51
|
+
it 'connects to it' do
|
52
|
+
client = start_client( rpc_opts_with_socket )
|
53
|
+
client.call( 'test.foo', 1 ).should == 1
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'and connecting to a non-existent server' do
|
57
|
+
it 'returns Arachni::RPC::Exceptions::ConnectionError' do
|
58
|
+
options = rpc_opts_with_socket.merge( socket: '/' )
|
59
|
+
start_client( options ).call( 'test.foo', @arg ) do |res|
|
60
|
+
res.rpc_connection_error?.should be_true
|
61
|
+
res.should be_kind_of Arachni::RPC::Exceptions::ConnectionError
|
62
|
+
::EM.stop
|
63
|
+
end
|
64
|
+
Arachni::RPC::EM.block
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'retains stability and consistency under heavy load' do
|
69
|
+
client = start_client( rpc_opts_with_socket )
|
70
|
+
|
71
|
+
n = 100_000
|
72
|
+
cnt = 0
|
73
|
+
|
74
|
+
mismatches = []
|
75
|
+
|
76
|
+
n.times do |i|
|
77
|
+
arg = 'a' * i
|
78
|
+
client.call( 'test.foo', arg ) do |res|
|
79
|
+
cnt += 1
|
80
|
+
mismatches << [i, arg, res] if arg != res
|
81
|
+
::EM.stop if cnt == n || mismatches.any?
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
Arachni::RPC::EM.block
|
86
|
+
cnt.should > 0
|
87
|
+
mismatches.should be_empty
|
88
|
+
end
|
89
|
+
|
90
|
+
context 'when passed an invalid socket path' do
|
91
|
+
it 'raises ArgumentError' do
|
92
|
+
begin
|
93
|
+
described_class.new( socket: 'blah' )
|
94
|
+
rescue => e
|
95
|
+
e.should be_kind_of ArgumentError
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context 'when passed a host but not a port' do
|
103
|
+
it 'raises ArgumentError' do
|
104
|
+
begin
|
105
|
+
described_class.new( host: 'test' )
|
106
|
+
rescue => e
|
107
|
+
e.should be_kind_of ArgumentError
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context 'when passed a port but not a host' do
|
113
|
+
it 'raises ArgumentError' do
|
114
|
+
begin
|
115
|
+
described_class.new( port: 9999 )
|
116
|
+
rescue => e
|
117
|
+
e.should be_kind_of ArgumentError
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
context 'when passed an invalid port' do
|
123
|
+
it 'raises ArgumentError' do
|
124
|
+
begin
|
125
|
+
described_class.new( host: 'tt', port: 'blah' )
|
126
|
+
rescue => e
|
127
|
+
e.should be_kind_of ArgumentError
|
128
|
+
end
|
129
|
+
end
|
50
130
|
end
|
51
131
|
end
|
52
132
|
|
@@ -60,18 +140,15 @@ describe Arachni::RPC::EM::Client do
|
|
60
140
|
start_client( opts ).call( 'test.foo', @arg ).should == @arg
|
61
141
|
end
|
62
142
|
end
|
63
|
-
|
64
143
|
end
|
65
144
|
|
66
|
-
describe
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
it "should be able to perform synchronous calls" do
|
145
|
+
describe 'raw interface' do
|
146
|
+
context 'when using Threads' do
|
147
|
+
it 'should be able to perform synchronous calls' do
|
71
148
|
@arg.should == start_client( rpc_opts ).call( 'test.foo', @arg )
|
72
149
|
end
|
73
150
|
|
74
|
-
it
|
151
|
+
it 'should be able to perform asynchronous calls' do
|
75
152
|
start_client( rpc_opts ).call( 'test.foo', @arg ) do |res|
|
76
153
|
@arg.should == res
|
77
154
|
::EM.stop
|
@@ -80,9 +157,8 @@ describe Arachni::RPC::EM::Client do
|
|
80
157
|
end
|
81
158
|
end
|
82
159
|
|
83
|
-
context
|
84
|
-
|
85
|
-
it "should be able to perform synchronous calls" do
|
160
|
+
context 'when run inside the Reactor loop' do
|
161
|
+
it 'should be able to perform synchronous calls' do
|
86
162
|
::EM.run {
|
87
163
|
::Arachni::RPC::EM::Synchrony.run do
|
88
164
|
@arg.should == start_client( rpc_opts ).call( 'test.foo', @arg )
|
@@ -91,7 +167,7 @@ describe Arachni::RPC::EM::Client do
|
|
91
167
|
}
|
92
168
|
end
|
93
169
|
|
94
|
-
it
|
170
|
+
it 'should be able to perform asynchronous calls' do
|
95
171
|
::EM.run {
|
96
172
|
start_client( rpc_opts ).call( 'test.foo', @arg ) do |res|
|
97
173
|
res.should == @arg
|
@@ -103,14 +179,13 @@ describe Arachni::RPC::EM::Client do
|
|
103
179
|
end
|
104
180
|
end
|
105
181
|
|
106
|
-
describe
|
107
|
-
it
|
182
|
+
describe 'Arachni::RPC::RemoteObjectMapper interface' do
|
183
|
+
it 'should be able to properly forward synchronous calls' do
|
108
184
|
test = Arachni::RPC::RemoteObjectMapper.new( start_client( rpc_opts ), 'test' )
|
109
185
|
test.foo( @arg ).should == @arg
|
110
|
-
# ::EM.stop
|
111
186
|
end
|
112
187
|
|
113
|
-
it
|
188
|
+
it 'should be able to properly forward synchronous calls' do
|
114
189
|
test = Arachni::RPC::RemoteObjectMapper.new( start_client( rpc_opts ), 'test' )
|
115
190
|
test.foo( @arg ) do |res|
|
116
191
|
res.should == @arg
|
@@ -120,75 +195,110 @@ describe Arachni::RPC::EM::Client do
|
|
120
195
|
end
|
121
196
|
end
|
122
197
|
|
123
|
-
|
124
|
-
context '
|
198
|
+
context 'when performing an asynchronous call' do
|
199
|
+
context 'and connecting to a non-existent server' do
|
200
|
+
it 'returns Arachni::RPC::Exceptions::ConnectionError' do
|
201
|
+
options = rpc_opts.merge( host: 'dddd', port: 999339 )
|
202
|
+
start_client( options ).call( 'test.foo', @arg ) do |res|
|
203
|
+
res.rpc_connection_error?.should be_true
|
204
|
+
res.should be_kind_of Arachni::RPC::Exceptions::ConnectionError
|
205
|
+
::EM.stop
|
206
|
+
end
|
207
|
+
Arachni::RPC::EM.block
|
208
|
+
end
|
209
|
+
end
|
125
210
|
|
126
|
-
|
211
|
+
context 'and requesting a non-existent object' do
|
212
|
+
it 'returns Arachni::RPC::Exceptions::InvalidObject' do
|
127
213
|
start_client( rpc_opts ).call( 'bar.foo' ) do |res|
|
128
214
|
res.rpc_invalid_object_error?.should be_true
|
215
|
+
res.should be_kind_of Arachni::RPC::Exceptions::InvalidObject
|
129
216
|
::EM.stop
|
130
217
|
end
|
131
218
|
Arachni::RPC::EM.block
|
132
219
|
end
|
220
|
+
end
|
133
221
|
|
134
|
-
|
222
|
+
context 'and requesting a non-public method' do
|
223
|
+
it 'returns Arachni::RPC::Exceptions::InvalidMethod' do
|
135
224
|
start_client( rpc_opts ).call( 'test.bar' ) do |res|
|
136
225
|
res.rpc_invalid_method_error?.should be_true
|
226
|
+
res.should be_kind_of Arachni::RPC::Exceptions::InvalidMethod
|
137
227
|
::EM.stop
|
138
228
|
end
|
139
229
|
Arachni::RPC::EM.block
|
140
230
|
end
|
231
|
+
end
|
141
232
|
|
142
|
-
|
233
|
+
context 'and there is a remote exception' do
|
234
|
+
it 'returns Arachni::RPC::Exceptions::RemoteException' do
|
143
235
|
start_client( rpc_opts ).call( 'test.foo' ) do |res|
|
144
236
|
res.rpc_remote_exception?.should be_true
|
237
|
+
res.should be_kind_of Arachni::RPC::Exceptions::RemoteException
|
145
238
|
::EM.stop
|
146
239
|
end
|
147
240
|
Arachni::RPC::EM.block
|
148
241
|
end
|
149
|
-
|
150
242
|
end
|
243
|
+
end
|
151
244
|
|
152
|
-
|
153
|
-
|
154
|
-
|
245
|
+
context 'when performing a synchronous call' do
|
246
|
+
#context 'and connecting to a non-existent server' do
|
247
|
+
# it 'raises Arachni::RPC::Exceptions::ConnectionError' do
|
248
|
+
# begin
|
249
|
+
# options = rpc_opts.merge( host: 'dddd', port: 999339 )
|
250
|
+
# start_client( options ).call( 'test.foo', @arg )
|
251
|
+
# rescue => e
|
252
|
+
# e.rpc_connection_error?.should be_true
|
253
|
+
# e.should be_kind_of Arachni::RPC::Exceptions::ConnectionError
|
254
|
+
# end
|
255
|
+
# end
|
256
|
+
#end
|
257
|
+
|
258
|
+
context 'and requesting a non-existent object' do
|
259
|
+
it 'raises Arachni::RPC::Exceptions::InvalidObject' do
|
155
260
|
begin
|
156
261
|
start_client( rpc_opts ).call( 'bar2.foo' )
|
157
262
|
rescue Exception => e
|
158
263
|
e.rpc_invalid_object_error?.should be_true
|
264
|
+
e.should be_kind_of Arachni::RPC::Exceptions::InvalidObject
|
159
265
|
end
|
160
266
|
end
|
267
|
+
end
|
161
268
|
|
162
|
-
|
269
|
+
context 'and requesting a non-public method' do
|
270
|
+
it 'raises Arachni::RPC::Exceptions::InvalidMethod' do
|
163
271
|
begin
|
164
272
|
start_client( rpc_opts ).call( 'test.bar2' )
|
165
273
|
rescue Exception => e
|
166
274
|
e.rpc_invalid_method_error?.should be_true
|
275
|
+
e.should be_kind_of Arachni::RPC::Exceptions::InvalidMethod
|
167
276
|
end
|
168
|
-
|
169
277
|
end
|
278
|
+
end
|
170
279
|
|
171
|
-
|
280
|
+
context 'and there is a remote exception' do
|
281
|
+
it 'raises Arachni::RPC::Exceptions::RemoteException' do
|
172
282
|
begin
|
173
283
|
start_client( rpc_opts ).call( 'test.foo' )
|
174
284
|
rescue Exception => e
|
175
285
|
e.rpc_remote_exception?.should be_true
|
286
|
+
e.should be_kind_of Arachni::RPC::Exceptions::RemoteException
|
176
287
|
end
|
177
288
|
end
|
178
|
-
|
179
289
|
end
|
180
290
|
end
|
181
291
|
|
182
|
-
context
|
183
|
-
it
|
292
|
+
context 'when using valid SSL primitives' do
|
293
|
+
it 'should be able to establish a connection' do
|
184
294
|
res = start_client( rpc_opts_with_ssl_primitives ).call( 'test.foo', @arg )
|
185
295
|
res.should == @arg
|
186
296
|
::EM.stop
|
187
297
|
end
|
188
298
|
end
|
189
299
|
|
190
|
-
context
|
191
|
-
it
|
300
|
+
context 'when using invalid SSL primitives' do
|
301
|
+
it 'should not be able to establish a connection' do
|
192
302
|
start_client( rpc_opts_with_invalid_ssl_primitives ).call( 'test.foo', @arg ) do |res|
|
193
303
|
res.rpc_connection_error?.should be_true
|
194
304
|
::EM.stop
|
@@ -197,8 +307,8 @@ describe Arachni::RPC::EM::Client do
|
|
197
307
|
end
|
198
308
|
end
|
199
309
|
|
200
|
-
context
|
201
|
-
it
|
310
|
+
context 'when using mixed SSL primitives' do
|
311
|
+
it 'should not be able to establish a connection' do
|
202
312
|
start_client( rpc_opts_with_mixed_ssl_primitives ).call( 'test.foo', @arg ) do |res|
|
203
313
|
res.rpc_connection_error?.should be_true
|
204
314
|
res.rpc_ssl_error?.should be_true
|