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.
- data/CHANGELOG.md +0 -0
- data/LICENSE.md +341 -0
- data/README.md +86 -0
- data/Rakefile +74 -0
- data/examples/client.EM.run.rb +113 -0
- data/examples/client.rb +181 -0
- data/examples/server.rb +80 -0
- data/lib/arachni/rpc/em.rb +26 -0
- data/lib/arachni/rpc/em/client.rb +280 -0
- data/lib/arachni/rpc/em/connection_utilities.rb +44 -0
- data/lib/arachni/rpc/em/em.rb +105 -0
- data/lib/arachni/rpc/em/protocol.rb +160 -0
- data/lib/arachni/rpc/em/server.rb +421 -0
- data/lib/arachni/rpc/em/ssl.rb +180 -0
- data/lib/arachni/rpc/em/version.rb +17 -0
- data/spec/arachni/rpc/em/client_spec.rb +211 -0
- data/spec/arachni/rpc/em/em_spec.rb +4 -0
- data/spec/arachni/rpc/em/server_spec.rb +79 -0
- data/spec/arachni/rpc/em/ssl_spec.rb +68 -0
- data/spec/pems/cacert.pem +39 -0
- data/spec/pems/client/cert.pem +39 -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 +39 -0
- data/spec/pems/server/key.pem +51 -0
- data/spec/servers/basic.rb +3 -0
- data/spec/servers/server.rb +61 -0
- data/spec/servers/with_ssl_primitives.rb +11 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +46 -0
- metadata +134 -0
@@ -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
|