arachni-rpc-em 0.1.3 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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,180 @@
|
|
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 Client
|
12
|
+
|
13
|
+
#
|
14
|
+
# Transmits `Arachni::RPC::Request` objects and calls callbacks once an
|
15
|
+
# `Arachni::RPC::Response` is received.
|
16
|
+
#
|
17
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
18
|
+
#
|
19
|
+
class Handler < EventMachine::Connection
|
20
|
+
include Protocol
|
21
|
+
include ConnectionUtilities
|
22
|
+
|
23
|
+
# Default amount of tries for failed requests.
|
24
|
+
DEFAULT_TRIES = 9
|
25
|
+
|
26
|
+
# @return [Symbol] Status of the connection, can be:
|
27
|
+
#
|
28
|
+
# * `:idle` -- Just initialized.
|
29
|
+
# * `:ready` -- A connection has been established.
|
30
|
+
# * `:pending` -- Sending request and awaiting response.
|
31
|
+
# * `:done` -- Response received and callback invoked -- ready to be reused.
|
32
|
+
# * `:closed` -- Connection closed.
|
33
|
+
attr_reader :status
|
34
|
+
|
35
|
+
# Prepares an RPC connection and sets {#status} to `:idle`.
|
36
|
+
#
|
37
|
+
# @param [Hash] opts
|
38
|
+
# @option opts [Integer] :max_retries (9)
|
39
|
+
# Default amount of tries for failed requests.
|
40
|
+
#
|
41
|
+
# @option opts [Client] :base
|
42
|
+
# Client instance (needed to {Client#push_connection push} ourselves
|
43
|
+
# back to its connection pool once we're done and we're ready to be reused.)
|
44
|
+
def initialize( opts )
|
45
|
+
@opts = opts.dup
|
46
|
+
|
47
|
+
@max_retries = @opts[:max_retries] || DEFAULT_TRIES
|
48
|
+
|
49
|
+
@client = @opts[:client]
|
50
|
+
|
51
|
+
@opts[:tries] ||= 0
|
52
|
+
@tries ||= @opts[:tries]
|
53
|
+
|
54
|
+
@status = :idle
|
55
|
+
|
56
|
+
@request = nil
|
57
|
+
assume_client_role!
|
58
|
+
end
|
59
|
+
|
60
|
+
# Sends an RPC request (i.e. performs an RPC call) and sets {#status}
|
61
|
+
# to `:pending`.
|
62
|
+
#
|
63
|
+
# @param [Arachni::RPC::Request] req
|
64
|
+
def send_request( req )
|
65
|
+
@request = req
|
66
|
+
@status = :pending
|
67
|
+
super( req )
|
68
|
+
end
|
69
|
+
|
70
|
+
# @note Pushes itself to the client's connection pool to be re-used.
|
71
|
+
#
|
72
|
+
# Handles responses to RPC requests, calls its callback and sets {#status}
|
73
|
+
# to `:done`.
|
74
|
+
#
|
75
|
+
# @param [Arachni::RPC::Response] res
|
76
|
+
#
|
77
|
+
def receive_response( res )
|
78
|
+
if exception?( res )
|
79
|
+
res.obj = RPC::Exceptions.from_response( res )
|
80
|
+
end
|
81
|
+
|
82
|
+
@request.callback.call( res.obj ) if @request.callback
|
83
|
+
ensure
|
84
|
+
@request = nil # Help the GC out.
|
85
|
+
@status = :done
|
86
|
+
@opts[:tries] = @tries = 0
|
87
|
+
@client.push_connection self
|
88
|
+
end
|
89
|
+
|
90
|
+
# Initializes an SSL session once the connection has been established and
|
91
|
+
# sets {#status} # to `:ready`.
|
92
|
+
#
|
93
|
+
# @private
|
94
|
+
def post_init
|
95
|
+
@status = :ready
|
96
|
+
start_ssl
|
97
|
+
end
|
98
|
+
|
99
|
+
# Handles closed connections, cleans up the SSL session, retries (if
|
100
|
+
# necessary) and sets {#status} to `:closed`.
|
101
|
+
#
|
102
|
+
# @private
|
103
|
+
def unbind( reason )
|
104
|
+
end_ssl
|
105
|
+
|
106
|
+
# If there is a request and a callback and the callback hasn't yet be
|
107
|
+
# called (i.e. not done) then we got here by error so retry.
|
108
|
+
if @request && @request.callback && !done?
|
109
|
+
if retry? #&& reason == Errno::ECONNREFUSED
|
110
|
+
#p 'RETRY'
|
111
|
+
#p @client.connection_count
|
112
|
+
retry_request
|
113
|
+
else
|
114
|
+
#p 'FAIL'
|
115
|
+
#p @client.connection_count
|
116
|
+
e = RPC::Exceptions::ConnectionError.new( "Connection closed [#{reason}]" )
|
117
|
+
@request.callback.call( e )
|
118
|
+
@client.connection_failed self
|
119
|
+
end
|
120
|
+
return
|
121
|
+
end
|
122
|
+
|
123
|
+
close_without_retry
|
124
|
+
end
|
125
|
+
|
126
|
+
# @return [Boolean]
|
127
|
+
# `true` when the connection has been closed, `false` otherwise.
|
128
|
+
def closed?
|
129
|
+
@status == :closed
|
130
|
+
end
|
131
|
+
|
132
|
+
# @note If `true`, the connection can be re-used.
|
133
|
+
#
|
134
|
+
# @return [Boolean]
|
135
|
+
# `true` when the connection is done, `false` otherwise.
|
136
|
+
def done?
|
137
|
+
@status == :done
|
138
|
+
end
|
139
|
+
|
140
|
+
# Closes the connection without triggering a retry operation and sets
|
141
|
+
# {#status} to `:closed`.
|
142
|
+
def close_without_retry
|
143
|
+
@request = nil
|
144
|
+
@status = :closed
|
145
|
+
close_connection
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
def retry_request
|
151
|
+
opts = @opts.dup
|
152
|
+
opts[:tries] += 1
|
153
|
+
|
154
|
+
req = @request.dup
|
155
|
+
|
156
|
+
@tries += 1
|
157
|
+
::EM.next_tick {
|
158
|
+
::EM::Timer.new( 0.2 ) {
|
159
|
+
address = opts[:socket] ? opts[:socket] : [opts[:host], opts[:port]]
|
160
|
+
::EM.connect( *[address, self.class, opts ].flatten ).send_request( req )
|
161
|
+
}
|
162
|
+
}
|
163
|
+
|
164
|
+
close_without_retry
|
165
|
+
end
|
166
|
+
|
167
|
+
def retry?
|
168
|
+
@tries < @max_retries
|
169
|
+
end
|
170
|
+
|
171
|
+
# @param [Arachni::RPC::Response] res
|
172
|
+
def exception?( res )
|
173
|
+
res.obj.is_a?( Hash ) && res.obj['exception'] ? true : false
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -13,13 +13,11 @@ module EM
|
|
13
13
|
#
|
14
14
|
# Helper methods to be included in EventMachine::Connection classes
|
15
15
|
#
|
16
|
-
# @author
|
16
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
17
17
|
#
|
18
18
|
module ConnectionUtilities
|
19
19
|
|
20
|
-
#
|
21
20
|
# @return [String] IP address of the client
|
22
|
-
#
|
23
21
|
def peer_ip_addr
|
24
22
|
begin
|
25
23
|
if peername = get_peername
|
data/lib/arachni/rpc/em/em.rb
CHANGED
@@ -12,7 +12,7 @@ module RPC
|
|
12
12
|
#
|
13
13
|
# Provides some convenient methods for EventMachine's Reactor.
|
14
14
|
#
|
15
|
-
# @author
|
15
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
16
16
|
#
|
17
17
|
module EM
|
18
18
|
|
@@ -24,27 +24,21 @@ module EM
|
|
24
24
|
extend self
|
25
25
|
end
|
26
26
|
|
27
|
+
# @note Will make sure EM is running first.
|
27
28
|
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
# @param [Proc] &block
|
31
|
-
#
|
29
|
+
# @param [Block] block Block to be run in the EM reactor.
|
32
30
|
def schedule( &block )
|
33
31
|
ensure_em_running
|
34
32
|
::EM.schedule( &block )
|
35
33
|
end
|
36
34
|
|
37
|
-
#
|
38
35
|
# Blocks until the Reactor stops running
|
39
|
-
#
|
40
36
|
def block
|
41
37
|
# beware of deadlocks, we can't join our own thread
|
42
38
|
::EM.reactor_thread.join if ::EM.reactor_thread && !::EM::reactor_thread?
|
43
39
|
end
|
44
40
|
|
45
|
-
#
|
46
41
|
# Puts the Reactor in its own thread and runs it.
|
47
|
-
#
|
48
42
|
def ensure_em_running
|
49
43
|
if !::EM::reactor_running?
|
50
44
|
|
@@ -11,64 +11,18 @@ module RPC
|
|
11
11
|
module EM
|
12
12
|
|
13
13
|
#
|
14
|
-
# Provides helper transport methods for
|
14
|
+
# Provides helper transport methods for `Arachni::RPC::Message` transmission.
|
15
15
|
#
|
16
|
-
# @author
|
16
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
17
17
|
#
|
18
18
|
module Protocol
|
19
19
|
include ::Arachni::RPC::EM::SSL
|
20
20
|
|
21
|
-
#
|
21
|
+
# Send a maximum of 16kb of data per tick.
|
22
22
|
MAX_CHUNK_SIZE = 1024 * 16
|
23
23
|
|
24
|
-
#
|
25
|
-
|
26
|
-
@role = :server
|
27
|
-
end
|
28
|
-
|
29
|
-
# become a client
|
30
|
-
def assume_client_role!
|
31
|
-
@role = :client
|
32
|
-
end
|
33
|
-
|
34
|
-
#
|
35
|
-
# Stub method, should be implemented by servers.
|
36
|
-
#
|
37
|
-
# @param [Arachni::RPC::EM::Request] request
|
38
|
-
#
|
39
|
-
def receive_request( request )
|
40
|
-
p request
|
41
|
-
end
|
42
|
-
|
43
|
-
#
|
44
|
-
# Stub method, should be implemented by clients.
|
45
|
-
#
|
46
|
-
# @param [Arachni::RPC::EM::Response] response
|
47
|
-
#
|
48
|
-
def receive_response( response )
|
49
|
-
p response
|
50
|
-
end
|
51
|
-
|
52
|
-
#
|
53
|
-
# Converts incoming hash objects to Requests or Response
|
54
|
-
# (depending on the assumed role) and calls receive_request() or receive_response()
|
55
|
-
# accordingly.
|
56
|
-
#
|
57
|
-
# @param [Hash] obj
|
58
|
-
#
|
59
|
-
def receive_object( obj )
|
60
|
-
if @role == :server
|
61
|
-
receive_request( Request.new( obj ) )
|
62
|
-
else
|
63
|
-
receive_response( Response.new( obj ) )
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
#
|
68
|
-
# Sends a message to the peer.
|
69
|
-
#
|
70
|
-
# @param [Arachni::RPC::EM::Message] msg
|
71
|
-
#
|
24
|
+
# @param [Arachni::RPC::Message] msg
|
25
|
+
# Message to send to the peer.
|
72
26
|
def send_message( msg )
|
73
27
|
::EM.schedule { send_object( msg.prepare_for_tx ) }
|
74
28
|
end
|
@@ -78,18 +32,14 @@ module Protocol
|
|
78
32
|
#
|
79
33
|
# Receives data from the network.
|
80
34
|
#
|
81
|
-
#
|
82
|
-
#
|
35
|
+
# Rhe data will be chunks of a serialized object which will be buffered
|
36
|
+
# until the whole transmission has finished.
|
83
37
|
#
|
84
|
-
# It will then
|
38
|
+
# It will then unserialize it and pass it to {#receive_object}.
|
85
39
|
#
|
86
40
|
def receive_data( data )
|
87
|
-
#
|
88
|
-
#
|
89
|
-
#
|
90
|
-
# don't buffer any data from unverified peers if SSL peer
|
91
|
-
# verification has been enabled
|
92
|
-
#
|
41
|
+
# Break out early if the request is sent by an unverified peer and SSL
|
42
|
+
# peer verification has been enabled.
|
93
43
|
if ssl_opts? && !verified_peer? && @role == :server
|
94
44
|
e = Arachni::RPC::Exceptions::SSLPeerVerificationFailed.new( 'Could not verify peer.' )
|
95
45
|
send_response Response.new( :obj => {
|
@@ -114,11 +64,45 @@ module Protocol
|
|
114
64
|
end
|
115
65
|
end
|
116
66
|
|
67
|
+
private
|
68
|
+
|
69
|
+
# Stub method, should be implemented by servers.
|
70
|
+
#
|
71
|
+
# @param [Arachni::RPC::Request] request
|
72
|
+
# @abstract
|
73
|
+
def receive_request( request )
|
74
|
+
p request
|
75
|
+
end
|
76
|
+
|
77
|
+
#
|
78
|
+
# Stub method, should be implemented by clients.
|
117
79
|
#
|
118
|
-
#
|
80
|
+
# @param [Arachni::RPC::Response] response
|
81
|
+
# @abstract
|
82
|
+
def receive_response( response )
|
83
|
+
p response
|
84
|
+
end
|
85
|
+
|
86
|
+
#
|
87
|
+
# Converts incoming hash objects to `Arachni::RPC::Request` and
|
88
|
+
# `Arachni::RPC::Response` objects (depending on the assumed role) and calls
|
89
|
+
# {#receive_request} or {#receive_response} accordingly.
|
90
|
+
#
|
91
|
+
# @param [Hash] obj
|
92
|
+
#
|
93
|
+
def receive_object( obj )
|
94
|
+
if @role == :server
|
95
|
+
receive_request( Request.new( obj ) )
|
96
|
+
else
|
97
|
+
receive_response( Response.new( obj ) )
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
119
101
|
#
|
120
|
-
# Will split the object in chunks of MAX_CHUNK_SIZE and transmit one
|
102
|
+
# @note Will split the object in chunks of MAX_CHUNK_SIZE and transmit one
|
103
|
+
# at a time.
|
121
104
|
#
|
105
|
+
# @param [Object] obj Object to send.
|
122
106
|
def send_object( obj )
|
123
107
|
data = serialize( obj )
|
124
108
|
packed = [data.bytesize, data].pack( 'Na*' )
|
@@ -133,15 +117,20 @@ module Protocol
|
|
133
117
|
end
|
134
118
|
end
|
135
119
|
|
120
|
+
# Become a server.
|
121
|
+
def assume_server_role!
|
122
|
+
@role = :server
|
123
|
+
end
|
124
|
+
|
125
|
+
# Become a client.
|
126
|
+
def assume_client_role!
|
127
|
+
@role = :client
|
128
|
+
end
|
129
|
+
|
130
|
+
# Returns the preferred serializer based on the `serializer` option of the
|
131
|
+
# server.
|
136
132
|
#
|
137
|
-
#
|
138
|
-
#
|
139
|
-
# Defaults to <i>YAML</i>.
|
140
|
-
#
|
141
|
-
# @return [Class] serializer to be used
|
142
|
-
#
|
143
|
-
# @see http://eventmachine.rubyforge.org/EventMachine/Protocols/ObjectProtocol.html#M000369
|
144
|
-
#
|
133
|
+
# @return [.load, .dump] Serializer to be used (Defaults to `YAML`).
|
145
134
|
def serializer
|
146
135
|
return @client_serializer if @client_serializer
|
147
136
|
|
@@ -10,159 +10,27 @@ module Arachni
|
|
10
10
|
module RPC
|
11
11
|
module EM
|
12
12
|
|
13
|
+
require_relative 'server/handler'
|
14
|
+
|
15
|
+
# EventMachine-based RPC server.
|
13
16
|
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
# It's capable of:
|
17
|
-
# - performing and handling a few thousands requests per second (depending on call size, network conditions and the like)
|
18
|
-
# - TLS encryption
|
19
|
-
# - asynchronous and synchronous requests
|
20
|
-
# - handling asynchronous methods that require a block
|
21
|
-
#
|
22
|
-
# @author: Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
23
|
-
#
|
17
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
24
18
|
class Server
|
25
19
|
include ::Arachni::RPC::Exceptions
|
26
20
|
|
27
|
-
#
|
28
|
-
# Handles EventMachine's connection stuff.
|
29
|
-
#
|
30
|
-
# It's responsible for TLS, serializing, transmitting and receiving objects,
|
31
|
-
# as well as authenticating the client using the token.
|
32
|
-
#
|
33
|
-
# It also handles and forwards exceptions.
|
34
|
-
#
|
35
|
-
# @author: Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
36
|
-
#
|
37
|
-
class Proxy < EventMachine::Connection
|
38
|
-
include ::Arachni::RPC::EM::Protocol
|
39
|
-
include ::Arachni::RPC::Exceptions
|
40
|
-
include ::Arachni::RPC::EM::ConnectionUtilities
|
41
|
-
|
42
|
-
INACTIVITY_TIMEOUT = 10
|
43
|
-
|
44
|
-
attr_reader :request
|
45
|
-
|
46
|
-
def initialize( server )
|
47
|
-
super
|
48
|
-
@server = server
|
49
|
-
@opts = server.opts
|
50
|
-
|
51
|
-
assume_server_role!
|
52
|
-
|
53
|
-
@id = nil
|
54
|
-
@request = nil
|
55
|
-
|
56
|
-
# do not tolerate long periods of
|
57
|
-
# inactivity in order to avoid zombie connections
|
58
|
-
set_comm_inactivity_timeout( INACTIVITY_TIMEOUT )
|
59
|
-
end
|
60
|
-
|
61
|
-
# starts TLS
|
62
|
-
def post_init
|
63
|
-
start_ssl
|
64
|
-
end
|
65
|
-
|
66
|
-
def unbind
|
67
|
-
end_ssl
|
68
|
-
end
|
69
|
-
|
70
|
-
def log( severity, progname, msg )
|
71
|
-
sev_sym = Logger.const_get( severity.to_s.upcase.to_sym )
|
72
|
-
@server.logger.add( sev_sym, msg, progname )
|
73
|
-
end
|
74
|
-
|
75
|
-
#
|
76
|
-
# Handles requests and sends back the responses.
|
77
|
-
#
|
78
|
-
# @param [Arachni::RPC::EM::Request] req
|
79
|
-
#
|
80
|
-
def receive_request( req )
|
81
|
-
@request = req
|
82
|
-
|
83
|
-
# the method call may block a little so tell EventMachine to
|
84
|
-
# stick it in its own thread.
|
85
|
-
res = Response.new
|
86
|
-
peer = peer_ip_addr
|
87
|
-
|
88
|
-
begin
|
89
|
-
# token-based authentication
|
90
|
-
authenticate!
|
91
|
-
|
92
|
-
# grab the result of the method call
|
93
|
-
res.merge!( @server.call( self ) )
|
94
|
-
|
95
|
-
# handle exceptions and convert them to a simple hash,
|
96
|
-
# ready to be passed to the client.
|
97
|
-
rescue Exception => e
|
98
|
-
|
99
|
-
type = ''
|
100
|
-
|
101
|
-
# if it's an RPC exception pass the type along as is
|
102
|
-
if e.rpc_exception?
|
103
|
-
type = e.class.name.split( ':' )[-1]
|
104
|
-
# otherwise set it to a RemoteExeption
|
105
|
-
else
|
106
|
-
type = 'RemoteException'
|
107
|
-
end
|
108
|
-
|
109
|
-
res.obj = {
|
110
|
-
'exception' => e.to_s,
|
111
|
-
'backtrace' => e.backtrace,
|
112
|
-
'type' => type
|
113
|
-
}
|
114
|
-
|
115
|
-
msg = "#{e.to_s}\n#{e.backtrace.join( "\n" )}"
|
116
|
-
@server.logger.error( 'Exception' ){ msg + " [on behalf of #{peer}]" }
|
117
|
-
end
|
118
|
-
|
119
|
-
#
|
120
|
-
# pass the result of the RPC call back to the client
|
121
|
-
# along with the callback ID but *only* if it wan't async
|
122
|
-
# because server.call() will have already taken care of it
|
123
|
-
#
|
124
|
-
send_response( res ) if !res.async?
|
125
|
-
end
|
126
|
-
|
127
|
-
#
|
128
|
-
# Authenticates the client based on the token in the request.
|
129
|
-
#
|
130
|
-
# It will raise an exception if the token doesn't check-out.
|
131
|
-
#
|
132
|
-
def authenticate!
|
133
|
-
if !valid_token?( @request.token )
|
134
|
-
|
135
|
-
msg = 'Token missing or invalid while calling: ' + @request.message
|
136
|
-
|
137
|
-
@server.logger.error( 'Authenticator' ){
|
138
|
-
msg + " [on behalf of #{peer_ip_addr}]"
|
139
|
-
}
|
140
|
-
|
141
|
-
fail InvalidToken.new( msg )
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
#
|
146
|
-
# Compares the authentication token in the param with the one of the server.
|
147
|
-
#
|
148
|
-
# @param [String] token
|
149
|
-
#
|
150
|
-
# @return [Bool]
|
151
|
-
#
|
152
|
-
def valid_token?( token )
|
153
|
-
token == @server.token
|
154
|
-
end
|
155
|
-
|
156
|
-
end
|
157
|
-
|
21
|
+
# @return [String] Authentication token.
|
158
22
|
attr_reader :token
|
23
|
+
|
24
|
+
# @return [Hash] Configuration options.
|
159
25
|
attr_reader :opts
|
26
|
+
|
27
|
+
# @return [Logger]
|
160
28
|
attr_reader :logger
|
161
29
|
|
162
30
|
#
|
163
31
|
# Starts EventMachine and the RPC server.
|
164
32
|
#
|
165
|
-
#
|
33
|
+
# @example Example options:
|
166
34
|
#
|
167
35
|
# {
|
168
36
|
# :host => 'localhost',
|
@@ -173,8 +41,6 @@ class Server
|
|
173
41
|
# :token => 'superdupersecret',
|
174
42
|
#
|
175
43
|
# # optional serializer (defaults to YAML)
|
176
|
-
# # see the 'serializer' method at:
|
177
|
-
# # http://eventmachine.rubyforge.org/EventMachine/Protocols/ObjectProtocol.html#M000369
|
178
44
|
# :serializer => Marshal,
|
179
45
|
#
|
180
46
|
# # serializer to use if the first choice fails
|
@@ -193,17 +59,30 @@ class Server
|
|
193
59
|
# }
|
194
60
|
#
|
195
61
|
# @param [Hash] opts
|
62
|
+
# @option opts [String] :host Hostname/IP address.
|
63
|
+
# @option opts [Integer] :port Port number.
|
64
|
+
# @option opts [String] :socket Path to UNIX domain socket.
|
65
|
+
# @option opts [String] :token Optional authentication token.
|
66
|
+
# @option opts [.dump, .load] :serializer (YAML)
|
67
|
+
# Serializer to use for message transmission.
|
68
|
+
# @option opts [.dump, .load] :fallback_serializer
|
69
|
+
# Optional fallback serializer to be used when the primary one fails.
|
70
|
+
# @option opts [Integer] :max_retries
|
71
|
+
# How many times to retry failed requests.
|
72
|
+
# @option opts [String] :ssl_ca SSL CA certificate.
|
73
|
+
# @option opts [String] :ssl_pkey SSL private key.
|
74
|
+
# @option opts [String] :ssl_cert SSL certificate.
|
196
75
|
#
|
197
76
|
def initialize( opts )
|
198
|
-
@opts
|
77
|
+
@opts = opts
|
199
78
|
|
200
79
|
if @opts[:ssl_pkey] && @opts[:ssl_cert]
|
201
80
|
if !File.exist?( @opts[:ssl_pkey] )
|
202
|
-
raise
|
81
|
+
raise "Could not find private key at: #{@opts[:ssl_pkey]}"
|
203
82
|
end
|
204
83
|
|
205
84
|
if !File.exist?( @opts[:ssl_cert] )
|
206
|
-
raise
|
85
|
+
raise "Could not find certificate at: #{@opts[:ssl_cert]}"
|
207
86
|
end
|
208
87
|
end
|
209
88
|
|
@@ -213,17 +92,19 @@ class Server
|
|
213
92
|
@logger.level = Logger::INFO
|
214
93
|
|
215
94
|
@host, @port = @opts[:host], @opts[:port]
|
95
|
+
@socket = @opts[:socket]
|
96
|
+
|
97
|
+
if !@socket && !(@host || @port)
|
98
|
+
fail ArgumentError, 'Needs either a :socket or :host and :port options.'
|
99
|
+
end
|
100
|
+
|
101
|
+
@port = @port.to_i
|
216
102
|
|
217
103
|
clear_handlers
|
218
104
|
end
|
219
105
|
|
220
106
|
#
|
221
|
-
#
|
222
|
-
# instead of simply returning them (which is the most usual operation of async methods.
|
223
|
-
#
|
224
|
-
# So no need to change your coding conventions to fit the RPC stuff,
|
225
|
-
# you can just decide dynamically based on the plethora of data which Ruby provides
|
226
|
-
# by its 'Method' class.
|
107
|
+
# @example
|
227
108
|
#
|
228
109
|
# server.add_async_check do |method|
|
229
110
|
# #
|
@@ -234,35 +115,39 @@ class Server
|
|
234
115
|
# 'async' == method.name.to_s.split( '_' )[0]
|
235
116
|
# end
|
236
117
|
#
|
237
|
-
# @param [
|
118
|
+
# @param [Block] block
|
119
|
+
# Block to identify methods that pass their result to a block instead of
|
120
|
+
# simply returning them (which is the most usual operation of async methods).
|
238
121
|
#
|
239
122
|
def add_async_check( &block )
|
240
123
|
@async_checks << block
|
241
124
|
end
|
242
125
|
|
243
126
|
#
|
244
|
-
#
|
127
|
+
# @example
|
245
128
|
#
|
246
129
|
# server.add_handler( 'myclass', MyClass.new )
|
247
130
|
#
|
248
|
-
# @param [String] name
|
249
|
-
#
|
131
|
+
# @param [String] name
|
132
|
+
# Name by which to make the object available over RPC.
|
133
|
+
# @param [Object] obj Instantiated server object to expose.
|
250
134
|
#
|
251
135
|
def add_handler( name, obj )
|
252
|
-
@objects[name]
|
253
|
-
@methods[name]
|
136
|
+
@objects[name] = obj
|
137
|
+
@methods[name] = Set.new
|
254
138
|
@async_methods[name] = Set.new
|
255
139
|
|
256
140
|
obj.class.public_instance_methods( false ).each do |method|
|
257
|
-
@methods[name]
|
141
|
+
@methods[name] << method.to_s
|
258
142
|
@async_methods[name] << method.to_s if async_check( obj.method( method ) )
|
259
143
|
end
|
260
144
|
end
|
261
145
|
|
146
|
+
# Clears all handlers and their associated information like methods and
|
147
|
+
# async check blocks.
|
262
148
|
#
|
263
|
-
#
|
264
|
-
#
|
265
|
-
#
|
149
|
+
# @see #add_handler
|
150
|
+
# @see #add_async_check
|
266
151
|
def clear_handlers
|
267
152
|
@objects = {}
|
268
153
|
@methods = {}
|
@@ -271,27 +156,33 @@ class Server
|
|
271
156
|
@async_methods = {}
|
272
157
|
end
|
273
158
|
|
274
|
-
#
|
275
|
-
# Runs the server and blocks.
|
276
|
-
#
|
159
|
+
# Runs the server and blocks while EM ir running.
|
277
160
|
def run
|
278
161
|
Arachni::RPC::EM.schedule { start }
|
279
162
|
Arachni::RPC::EM.block
|
280
163
|
end
|
281
164
|
|
282
|
-
#
|
283
165
|
# Starts the server but does not block.
|
284
|
-
#
|
285
166
|
def start
|
286
|
-
@logger.info( 'System' ){
|
287
|
-
@logger.info( 'System' )
|
167
|
+
@logger.info( 'System' ){ 'RPC Server started.' }
|
168
|
+
@logger.info( 'System' ) do
|
169
|
+
interface = @socket ? @socket : "#{@host}:#{@port}"
|
170
|
+
"Listening on #{interface}"
|
171
|
+
end
|
288
172
|
|
289
|
-
|
173
|
+
opts = @socket ? @socket : [@host, @port]
|
174
|
+
::EM.start_server( *[opts, Handler, self].flatten )
|
290
175
|
end
|
291
176
|
|
177
|
+
# @note If the called method is asynchronous it will be sent by this method
|
178
|
+
# directly, otherwise it will be handled by the {Handler}.
|
179
|
+
#
|
180
|
+
# @param [Handler] connection
|
181
|
+
# Connection with request information.
|
182
|
+
#
|
183
|
+
# @return [Arachni::RPC::Response]
|
292
184
|
def call( connection )
|
293
|
-
|
294
|
-
req = connection.request
|
185
|
+
req = connection.request
|
295
186
|
peer_ip_addr = connection.peer_ip_addr
|
296
187
|
|
297
188
|
expr, args = req.message, req.args
|
@@ -311,40 +202,40 @@ class Server
|
|
311
202
|
raise InvalidMethod.new( msg )
|
312
203
|
end
|
313
204
|
|
314
|
-
#
|
315
|
-
#
|
205
|
+
# The handler needs to know if this is an async call because if it is
|
206
|
+
# we'll have already send the response and it doesn't need to do
|
207
|
+
# transmit anything.
|
316
208
|
res = Response.new
|
317
209
|
res.async! if async?( obj_name, meth_name )
|
318
210
|
|
319
|
-
if
|
320
|
-
res.obj = @objects[obj_name].send( meth_name.to_sym, *args )
|
321
|
-
else
|
211
|
+
if res.async?
|
322
212
|
@objects[obj_name].send( meth_name.to_sym, *args ) do |obj|
|
323
213
|
res.obj = obj
|
324
214
|
connection.send_response( res )
|
325
215
|
end
|
216
|
+
else
|
217
|
+
res.obj = @objects[obj_name].send( meth_name.to_sym, *args )
|
326
218
|
end
|
327
219
|
|
328
220
|
res
|
329
221
|
end
|
330
222
|
|
331
|
-
#
|
332
223
|
# @return [TrueClass]
|
333
|
-
#
|
334
224
|
def alive?
|
335
225
|
true
|
336
226
|
end
|
337
227
|
|
338
|
-
#
|
339
228
|
# Shuts down the server after 2 seconds
|
340
|
-
#
|
341
229
|
def shutdown
|
342
230
|
wait_for = 2
|
343
231
|
|
344
232
|
@logger.info( 'System' ){ "Shutting down in #{wait_for} seconds..." }
|
345
233
|
|
346
|
-
#
|
347
|
-
::EM.add_timer( wait_for )
|
234
|
+
# Don't die before returning...
|
235
|
+
::EM.add_timer( wait_for ) do
|
236
|
+
File.unlink( @socket ) if @socket
|
237
|
+
::EM.stop
|
238
|
+
end
|
348
239
|
true
|
349
240
|
end
|
350
241
|
|
@@ -359,7 +250,6 @@ class Server
|
|
359
250
|
false
|
360
251
|
end
|
361
252
|
|
362
|
-
|
363
253
|
def log_call( peer_ip_addr, expr, *args )
|
364
254
|
msg = "#{expr}"
|
365
255
|
|