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,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
|
|