arachni-rpc-em 0.1

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,44 @@
1
+ =begin
2
+ Arachni-RPC
3
+ Copyright (c) 2011 Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
4
+
5
+ This is free software; you can copy and distribute and modify
6
+ this program under the term of the GPL v2.0 License
7
+ (See LICENSE file for details)
8
+
9
+ =end
10
+
11
+ module Arachni
12
+ module RPC
13
+ module EM
14
+
15
+ #
16
+ # Helper methods to be included in EventMachine::Connection classes
17
+ #
18
+ # @author: Tasos "Zapotek" Laskos
19
+ # <tasos.laskos@gmail.com>
20
+ # <zapotek@segfault.gr>
21
+ # @version: 0.1
22
+ #
23
+ module ConnectionUtilities
24
+
25
+ #
26
+ # @return [String] IP address of the client
27
+ #
28
+ def peer_ip_addr
29
+ begin
30
+ if peername = get_peername
31
+ Socket.unpack_sockaddr_in( peername )[1]
32
+ else
33
+ 'n/a'
34
+ end
35
+ rescue
36
+ 'n/a'
37
+ end
38
+ end
39
+
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,105 @@
1
+ =begin
2
+ Arachni-RPC
3
+ Copyright (c) 2011 Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
4
+
5
+ This is free software; you can copy and distribute and modify
6
+ this program under the term of the GPL v2.0 License
7
+ (See LICENSE file for details)
8
+
9
+ =end
10
+
11
+ module Arachni
12
+ module RPC
13
+
14
+ #
15
+ # Provides some convenient methods for EventMachine's Reactor.
16
+ #
17
+ # @author: Tasos "Zapotek" Laskos
18
+ # <tasos.laskos@gmail.com>
19
+ # <zapotek@segfault.gr>
20
+ # @version: 0.1
21
+ #
22
+ module EM
23
+
24
+ module Synchrony
25
+
26
+ def run( &block )
27
+ @@root_f = Fiber.new {
28
+ block.call
29
+ }.resume
30
+ end
31
+
32
+ extend self
33
+
34
+ end
35
+
36
+ #
37
+ # Inits method variables for the Reactor tasks and its Mutex.
38
+ #
39
+ def init
40
+ @@reactor_tasks_mutex ||= Mutex.new
41
+ @@reactor_tasks ||= []
42
+ end
43
+
44
+ #
45
+ # Adds a block in the Reactor.
46
+ #
47
+ # @param [Proc] &block block to be included in the Reactor loop
48
+ #
49
+ def add_to_reactor( &block )
50
+
51
+ self.init
52
+
53
+ # if we're already in the Reactor thread just run the block straight up.
54
+ if ::EM::reactor_thread?
55
+ block.call
56
+ else
57
+ @@reactor_tasks_mutex.lock
58
+ @@reactor_tasks << block
59
+
60
+ ensure_em_running!
61
+ @@reactor_tasks_mutex.unlock
62
+ end
63
+
64
+ end
65
+
66
+ #
67
+ # Blocks until the Reactor stops running
68
+ #
69
+ def block!
70
+ # beware of deadlocks, we can't join our own thread
71
+ ::EM.reactor_thread.join if ::EM.reactor_thread && !::EM::reactor_thread?
72
+ end
73
+
74
+ #
75
+ # Puts the Reactor in its own thread and runs it.
76
+ #
77
+ # It also runs all blocks sent to {#add_to_reactor}.
78
+ #
79
+ def ensure_em_running!
80
+ self.init
81
+
82
+ if !::EM::reactor_running?
83
+ q = Queue.new
84
+
85
+ Thread.new do
86
+ ::EM::run do
87
+
88
+ ::EM.error_handler do |e|
89
+ $stderr.puts "Exception raised during event loop: " +
90
+ "#{e.message} (#{e.class})\n#{(e.backtrace ||
91
+ [])[0..5].join("\n")}"
92
+ end
93
+
94
+ @@reactor_tasks.each { |task| task.call }
95
+ q << true
96
+ end
97
+ end
98
+ q.pop
99
+ end
100
+ end
101
+
102
+ extend self
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,160 @@
1
+ =begin
2
+ Arachni-RPC
3
+ Copyright (c) 2011 Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
4
+
5
+ This is free software; you can copy and distribute and modify
6
+ this program under the term of the GPL v2.0 License
7
+ (See LICENSE file for details)
8
+
9
+ =end
10
+
11
+ module Arachni
12
+ module RPC
13
+ module EM
14
+
15
+ #
16
+ # Provides helper transport methods for {Message} transmission.
17
+ #
18
+ # @author: Tasos "Zapotek" Laskos
19
+ # <tasos.laskos@gmail.com>
20
+ # <zapotek@segfault.gr>
21
+ # @version: 0.1
22
+ #
23
+ module Protocol
24
+ include ::Arachni::RPC::EM::SSL
25
+
26
+ # send a maximum of 16kb of data per tick
27
+ MAX_CHUNK_SIZE = 1024 * 16
28
+
29
+ # become a server
30
+ def assume_server_role!
31
+ @role = :server
32
+ end
33
+
34
+ # become a client
35
+ def assume_client_role!
36
+ @role = :client
37
+ end
38
+
39
+ #
40
+ # Stub method, should be implemented by servers.
41
+ #
42
+ # @param [Arachni::RPC::EM::Request] request
43
+ #
44
+ def receive_request( request )
45
+ p request
46
+ end
47
+
48
+ #
49
+ # Stub method, should be implemented by clients.
50
+ #
51
+ # @param [Arachni::RPC::EM::Response] response
52
+ #
53
+ def receive_response( response )
54
+ p response
55
+ end
56
+
57
+ #
58
+ # Converts incoming hash objects to Requests or Response
59
+ # (depending on the assumed role) and calls receive_request() or receive_response()
60
+ # accordingly.
61
+ #
62
+ # @param [Hash] obj
63
+ #
64
+ def receive_object( obj )
65
+ if @role == :server
66
+ receive_request( Request.new( obj ) )
67
+ else
68
+ receive_response( Response.new( obj ) )
69
+ end
70
+ end
71
+
72
+ #
73
+ # Sends a message to the peer.
74
+ #
75
+ # @param [Arachni::RPC::EM::Message] msg
76
+ #
77
+ def send_message( msg )
78
+ ::EM.schedule {
79
+ send_object( msg.prepare_for_tx )
80
+ }
81
+ end
82
+ alias :send_request :send_message
83
+ alias :send_response :send_message
84
+
85
+ #
86
+ # Receives data from the network.
87
+ #
88
+ # In this case the data will be chunks of a serialized object which
89
+ # will be buffered until the whole transmission has finished.
90
+ #
91
+ # It will then unresialize it and pass it to receive_object().
92
+ #
93
+ def receive_data( data )
94
+ #
95
+ # cut them out as soon as possible
96
+ #
97
+ # don't buffer any data from unverified peers if SSL peer
98
+ # veification has been enabled
99
+ #
100
+ if ssl_opts? && !verified_peer? && @role == :server
101
+ e = Arachni::RPC::Exceptions::SSLPeerVerificationFailed.new( 'Could not verify peer.' )
102
+ send_response Response.new( :obj => {
103
+ 'exception' => e.to_s,
104
+ 'backtrace' => e.backtrace,
105
+ 'type' => 'SSLPeerVerificationFailed'
106
+ })
107
+
108
+ log( :error, 'SSL', " Could not verify peer. ['#{peer_ip_addr}']." )
109
+ return
110
+ end
111
+
112
+ (@buf ||= '') << data
113
+
114
+ while @buf.size >= 4
115
+ if @buf.size >= 4 + ( size = @buf.unpack( 'N' ).first )
116
+ @buf.slice!( 0, 4 )
117
+ receive_object( serializer.load( @buf.slice!( 0, size ) ) )
118
+ else
119
+ break
120
+ end
121
+ end
122
+ end
123
+
124
+ #
125
+ # Sends a ruby object over the network
126
+ #
127
+ # Will split the object in chunks of MAX_CHUNK_SIZE and transmit one at a time.
128
+ #
129
+ def send_object( obj )
130
+ data = serializer.dump( obj )
131
+ packed = [data.bytesize, data].pack( 'Na*' )
132
+
133
+ while( packed )
134
+ if packed.bytesize > MAX_CHUNK_SIZE
135
+ send_data( packed.slice!( 0, MAX_CHUNK_SIZE ) )
136
+ else
137
+ send_data( packed )
138
+ break
139
+ end
140
+ end
141
+ end
142
+
143
+ #
144
+ # Returns the preferred serializer based on the 'serializer' option of the server.
145
+ #
146
+ # Defaults to <i>YAML</i>.
147
+ #
148
+ # @return [Class] serializer to be used
149
+ #
150
+ # @see http://eventmachine.rubyforge.org/EventMachine/Protocols/ObjectProtocol.html#M000369
151
+ #
152
+ def serializer
153
+ @opts[:serializer] ? @opts[:serializer] : YAML
154
+ end
155
+
156
+ end
157
+
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,421 @@
1
+ =begin
2
+ Arachni-RPC
3
+ Copyright (c) 2011 Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
4
+
5
+ This is free software; you can copy and distribute and modify
6
+ this program under the term of the GPL v2.0 License
7
+ (See LICENSE file for details)
8
+
9
+ =end
10
+
11
+ module Arachni
12
+ module RPC
13
+ module EM
14
+
15
+ #
16
+ # EventMachine-based RPC server class.
17
+ #
18
+ # It's capable of:
19
+ # - performing and handling a few thousands requests per second (depending on call size, network conditions and the like)
20
+ # - TLS encryption
21
+ # - asynchronous and synchronous requests
22
+ # - handling asynchronous methods that require a block
23
+ #
24
+ # @author: Tasos "Zapotek" Laskos
25
+ # <tasos.laskos@gmail.com>
26
+ # <zapotek@segfault.gr>
27
+ # @version: 0.1
28
+ #
29
+ class Server
30
+
31
+ include ::Arachni::RPC::Exceptions
32
+
33
+ #
34
+ # Handles EventMachine's connection stuff.
35
+ #
36
+ # It's responsible for TLS, serializing, transmitting and receiving objects,
37
+ # as well as authenticating the client using the token.
38
+ #
39
+ # It also handles and forwards exceptions.
40
+ #
41
+ # @author: Tasos "Zapotek" Laskos
42
+ # <tasos.laskos@gmail.com>
43
+ # <zapotek@segfault.gr>
44
+ # @version: 0.1
45
+ #
46
+ class Proxy < EventMachine::Connection
47
+
48
+ include ::Arachni::RPC::EM::Protocol
49
+ include ::Arachni::RPC::Exceptions
50
+ include ::Arachni::RPC::EM::ConnectionUtilities
51
+
52
+ INACTIVITY_TIMEOUT = 10
53
+
54
+ attr_reader :request
55
+
56
+ def initialize( server )
57
+ super
58
+ @server = server
59
+ @opts = server.opts
60
+
61
+ assume_server_role!
62
+
63
+ @id = nil
64
+ @request = nil
65
+
66
+ # do not tolerate long periods of
67
+ # inactivity in order to avoid zombie connections
68
+ set_comm_inactivity_timeout( INACTIVITY_TIMEOUT )
69
+ end
70
+
71
+ # starts TLS
72
+ def post_init
73
+ start_ssl
74
+ end
75
+
76
+ def unbind
77
+ end_ssl
78
+ end
79
+
80
+ def log( severity, progname, msg )
81
+ sev_sym = Logger.const_get( severity.to_s.upcase.to_sym )
82
+ @server.logger.add( sev_sym, msg, progname )
83
+ end
84
+
85
+ #
86
+ # Handles requests and sends back the responses.
87
+ #
88
+ # @param [Arachni::RPC::EM::Request] req
89
+ #
90
+ def receive_request( req )
91
+ @request = req
92
+
93
+ # the method call may block a little so tell EventMachine to
94
+ # stick it in its own thread.
95
+ # ::EM.defer( proc {
96
+ res = Response.new
97
+ peer = peer_ip_addr
98
+
99
+ begin
100
+ # token-based authentication
101
+ authenticate!
102
+
103
+ # grab the result of the method call
104
+ res.merge!( @server.call( self ) )
105
+
106
+ # handle exceptions and convert them to a simple hash,
107
+ # ready to be passed to the client.
108
+ rescue Exception => e
109
+
110
+ type = ''
111
+
112
+ # if it's an RPC exception pass the type along as is
113
+ if e.rpc_exception?
114
+ type = e.class.name.split( ':' )[-1]
115
+
116
+ # otherwise set it to a RemoteExeption
117
+ else
118
+ type = 'RemoteException'
119
+ end
120
+
121
+ res.obj = {
122
+ 'exception' => e.to_s,
123
+ 'backtrace' => e.backtrace,
124
+ 'type' => type
125
+ }
126
+
127
+ msg = "#{e.to_s}\n#{e.backtrace.join( "\n" )}"
128
+ @server.logger.error( 'Exception' ){ msg + " [on behalf of #{peer}]" }
129
+ end
130
+
131
+ # res
132
+ # }, proc {
133
+ # |res|
134
+
135
+ #
136
+ # pass the result of the RPC call back to the client
137
+ # along with the callback ID but *only* if it wan't async
138
+ # because server.call() will have already taken care of it
139
+ #
140
+ send_response( res ) if !res.async?
141
+ # })
142
+ end
143
+
144
+ #
145
+ # Authenticates the client based on the token in the request.
146
+ #
147
+ # It will raise an exception if the token doesn't check-out.
148
+ #
149
+ # @param [String] peer IP address of the client
150
+ # @param [Hash] req request
151
+ #
152
+ def authenticate!
153
+ if !valid_token?( @request.token )
154
+
155
+ msg = 'Token missing or invalid while calling: ' + @request.message
156
+
157
+ @server.logger.error( 'Authenticator' ){
158
+ msg + " [on behalf of #{peer_ip_addr}]"
159
+ }
160
+
161
+ raise( InvalidToken.new( msg ) )
162
+ end
163
+ end
164
+
165
+ #
166
+ # Compares the authentication token in the param with the one of the server.
167
+ #
168
+ # @param [String] token
169
+ #
170
+ # @return [Bool]
171
+ #
172
+ def valid_token?( token )
173
+ token == @server.token
174
+ end
175
+
176
+ end
177
+
178
+ attr_reader :token
179
+ attr_reader :opts
180
+ attr_reader :logger
181
+
182
+ #
183
+ # Starts EventMachine and the RPC server.
184
+ #
185
+ # opts example:
186
+ #
187
+ # {
188
+ # :host => 'localhost',
189
+ # :port => 7331,
190
+ #
191
+ # # optional authentication token, if it doesn't match the one
192
+ # # set on the server-side you'll be getting exceptions.
193
+ # :token => 'superdupersecret',
194
+ #
195
+ # # optional serializer (defaults to YAML)
196
+ # # see the 'serializer' method at:
197
+ # # http://eventmachine.rubyforge.org/EventMachine/Protocols/ObjectProtocol.html#M000369
198
+ # :serializer => Marshal,
199
+ #
200
+ # #
201
+ # # In order to enable peer verification one must first provide
202
+ # # the following:
203
+ # #
204
+ # # SSL CA certificate
205
+ # :ssl_ca => cwd + '/../spec/pems/cacert.pem',
206
+ # # SSL private key
207
+ # :ssl_pkey => cwd + '/../spec/pems/client/key.pem',
208
+ # # SSL certificate
209
+ # :ssl_cert => cwd + '/../spec/pems/client/cert.pem'
210
+ # }
211
+ #
212
+ # @param [Hash] opts
213
+ #
214
+ def initialize( opts )
215
+ @opts = opts
216
+
217
+ if @opts[:ssl_pkey] && @opts[:ssl_cert]
218
+ if !File.exist?( @opts[:ssl_pkey] )
219
+ raise 'Could not find private key at: ' + @opts[:ssl_pkey]
220
+ end
221
+
222
+ if !File.exist?( @opts[:ssl_cert] )
223
+ raise 'Could not find certificate at: ' + @opts[:ssl_cert]
224
+ end
225
+ end
226
+
227
+ @token = @opts[:token]
228
+
229
+ @logger = ::Logger.new( STDOUT )
230
+ @logger.level = Logger::INFO
231
+
232
+ @host, @port = @opts[:host], @opts[:port]
233
+
234
+ clear_handlers
235
+ end
236
+
237
+ #
238
+ # This is a way to identify methods that pass their result to a block
239
+ # instead of simply returning them (which is the most usual operation of async methods.
240
+ #
241
+ # So no need to change your coding conventions to fit the RPC stuff,
242
+ # you can just decide dynamically based on the plethora of data which Ruby provides
243
+ # by its 'Method' class.
244
+ #
245
+ # server.add_async_check {
246
+ # |method|
247
+ # #
248
+ # # Must return 'true' for async and 'false' for sync.
249
+ # #
250
+ # # Very simple check here...
251
+ # #
252
+ # 'async' == method.name.to_s.split( '_' )[0]
253
+ # }
254
+ #
255
+ # @param [Proc] &block
256
+ #
257
+ def add_async_check( &block )
258
+ @async_checks << block
259
+ end
260
+
261
+ #
262
+ # Adds a handler by name:
263
+ #
264
+ # server.add_handler( 'myclass', MyClass.new )
265
+ #
266
+ # @param [String] name name via which to make the object available over RPC
267
+ # @param [Object] obj object instance
268
+ #
269
+ def add_handler( name, obj )
270
+ @objects[name] = obj
271
+ @methods[name] = Set.new # no lookup overhead please :)
272
+ @async_methods[name] = Set.new
273
+
274
+ obj.class.public_instance_methods( false ).each {
275
+ |method|
276
+ @methods[name] << method.to_s
277
+ @async_methods[name] << method.to_s if async_check( obj.method( method ) )
278
+ }
279
+ end
280
+
281
+ #
282
+ # Clears all handlers and their associated information like methods
283
+ # and async check blocks.
284
+ #
285
+ def clear_handlers
286
+ @objects = {}
287
+ @methods = {}
288
+
289
+ @async_checks = []
290
+ @async_methods = {}
291
+ end
292
+
293
+ #
294
+ # Runs the server and blocks.
295
+ #
296
+ def run
297
+ Arachni::RPC::EM.add_to_reactor {
298
+ start
299
+ }
300
+ Arachni::RPC::EM.block!
301
+ end
302
+
303
+ #
304
+ # Starts the server but does not block.
305
+ #
306
+ def start
307
+ @logger.info( 'System' ){ "RPC Server started." }
308
+ @logger.info( 'System' ){ "Listening on #{@host}:#{@port}" }
309
+
310
+ ::EM.start_server( @host, @port, Proxy, self )
311
+ end
312
+
313
+ def call( connection )
314
+
315
+ req = connection.request
316
+ peer_ip_addr = connection.peer_ip_addr
317
+
318
+ expr, args = req.message, req.args
319
+ meth_name, obj_name = parse_expr( expr )
320
+
321
+ log_call( peer_ip_addr, expr, *args )
322
+
323
+ if !object_exist?( obj_name )
324
+ msg = "Trying to access non-existent object '#{obj_name}'."
325
+ @logger.error( 'Call' ){ msg + " [on behalf of #{peer_ip_addr}]" }
326
+ raise( InvalidObject.new( msg ) )
327
+ end
328
+
329
+ if !public_method?( obj_name, meth_name )
330
+ msg = "Trying to access non-public method '#{meth_name}'."
331
+ @logger.error( 'Call' ){ msg + " [on behalf of #{peer_ip_addr}]" }
332
+ raise( InvalidMethod.new( msg ) )
333
+ end
334
+
335
+ # the proxy needs to know whether this is an async call because if it
336
+ # is we'll have already send the response.
337
+ res = Response.new
338
+ res.async! if async?( obj_name, meth_name )
339
+
340
+ if !res.async?
341
+ res.obj = @objects[obj_name].send( meth_name.to_sym, *args )
342
+ else
343
+ @objects[obj_name].send( meth_name.to_sym, *args ){
344
+ |obj|
345
+ res.obj = obj
346
+ connection.send_response( res )
347
+ }
348
+ end
349
+
350
+ return res
351
+ end
352
+
353
+ #
354
+ # @return [TrueClass]
355
+ #
356
+ def alive?
357
+ return true
358
+ end
359
+
360
+ #
361
+ # Shuts down the server after 2 seconds
362
+ #
363
+ def shutdown
364
+ wait_for = 2
365
+
366
+ @logger.info( 'System' ){ "Shutting down in #{wait_for} seconds..." }
367
+
368
+ # don't die before returning
369
+ EventMachine::add_timer( wait_for ) { ::EM.stop }
370
+ return true
371
+ end
372
+
373
+ private
374
+
375
+ def async?( objname, method )
376
+ @async_methods[objname].include?( method )
377
+ end
378
+
379
+ def async_check( method )
380
+ @async_checks.each {
381
+ |check|
382
+ return true if check.call( method )
383
+ }
384
+ return false
385
+ end
386
+
387
+
388
+ def log_call( peer_ip_addr, expr, *args )
389
+ msg = "#{expr}"
390
+
391
+ # this should be in a @logger.debug call but it'll get out of sync
392
+ if @logger.level == Logger::DEBUG
393
+ cargs = args.map { |arg| arg.inspect }
394
+ msg += "( #{cargs.join( ', ' )} )"
395
+ end
396
+
397
+ msg += " [#{peer_ip_addr}]"
398
+
399
+ @logger.info( 'Call' ){ msg }
400
+ end
401
+
402
+ def parse_expr( expr )
403
+ parts = expr.to_s.split( '.' )
404
+
405
+ # method name, object name
406
+ [ parts.pop, parts.join( '.' ) ]
407
+ end
408
+
409
+ def object_exist?( obj_name )
410
+ @objects[obj_name] ? true : false
411
+ end
412
+
413
+ def public_method?( obj_name, method )
414
+ @methods[obj_name].include?( method )
415
+ end
416
+
417
+ end
418
+
419
+ end
420
+ end
421
+ end