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.
@@ -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
@@ -9,7 +9,7 @@
9
9
  module Arachni
10
10
  module RPC
11
11
  module EM
12
- VERSION = '0.1.3'
12
+ VERSION = '0.2'
13
13
  end
14
14
  end
15
15
  end
@@ -1,4 +1,4 @@
1
- require File.join( File.expand_path( File.dirname( __FILE__ ) ), '../../../', 'spec_helper' )
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 "should be able to retain stability and consistency under heavy load" do
11
+ it 'retains stability and consistency under heavy load' do
12
12
  client = start_client( rpc_opts )
13
13
 
14
- n = 400
15
- cnt = 0
14
+ n = 100_000
15
+ cnt = 0
16
16
 
17
17
  mismatches = []
18
18
 
19
19
  n.times do |i|
20
- client.call( 'test.foo', i ) do |res|
20
+ arg = 'a' * i
21
+ client.call( 'test.foo', arg ) do |res|
21
22
  cnt += 1
22
- mismatches << [i, res] if i != res
23
- ::EM.stop if cnt == n || !mismatches.empty?
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
- it "should throw error when connecting to inexistent server" do
33
- start_client( rpc_opts.merge( :host => 'dddd', :port => 999339 ) ).call( 'test.foo', @arg ) do |res|
34
- res.rpc_connection_error?.should be_true
35
- ::EM.stop
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
- start_client( rpc_opts.merge( :port => 999339 ) ).call( 'test.foo', @arg ) do |res|
40
- res.rpc_connection_error?.should be_true
41
- ::EM.stop
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
- describe "#initialize" do
47
- it "should be able to properly assign class options (including :role)" do
48
- opts = rpc_opts.merge( :role => :client )
49
- start_client( opts ).opts.should == opts
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 "raw interface" do
67
-
68
- context "when using Threads" do
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 "should be able to perform asynchronous calls" do
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 "when run inside the Reactor loop" do
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 "should be able to perform asynchronous calls" do
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 "Arachni::RPC::RemoteObjectMapper interface" do
107
- it "should be able to properly forward synchronous calls" do
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 "should be able to properly forward synchronous calls" do
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
- describe "exception" do
124
- context 'when performing asynchronous calls' do
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
- it "should be returned when requesting inexistent objects" do
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
- it "should be returned when requesting inexistent or non-public methods" do
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
- it "should be returned when there's a remote exception" do
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
- context 'when performing synchronous calls' do
153
-
154
- it "should be raised when requesting inexistent objects" do
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
- it "should be raised when requesting inexistent or non-public methods" do
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
- it "should be raised when there's a remote exception" do
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 "when using valid SSL primitives" do
183
- it "should be able to establish a connection" do
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 "when using invalid SSL primitives" do
191
- it "should not be able to establish a connection" do
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 "when using mixed SSL primitives" do
201
- it "should not be able to establish a connection" do
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