arachni-rpc 0.1.3 → 0.2.0

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,86 @@
1
+ =begin
2
+
3
+ This file is part of the Arachni-RPC project and may be subject to
4
+ redistribution and commercial restrictions. Please see the Arachni-RPC
5
+ web site for more information on licensing and terms of use.
6
+
7
+ =end
8
+
9
+ module Arachni
10
+ module RPC
11
+
12
+ # Maps the methods of remote objects to local ones.
13
+ #
14
+ # You start like:
15
+ #
16
+ # client = Arachni::RPC::Client.new( host: 'localhost', port: 7331 )
17
+ # bench = Arachni::RPC::Proxy.new( client, 'bench' )
18
+ #
19
+ # And it allows you to do this:
20
+ #
21
+ # result = bench.foo( 1, 2, 3 )
22
+ #
23
+ # Instead of:
24
+ #
25
+ # result = client.call( 'bench.foo', 1, 2, 3 )
26
+ #
27
+ # The server on the other end must have an appropriate handler set, like:
28
+ #
29
+ # class Bench
30
+ # def foo( i = 0 )
31
+ # return i
32
+ # end
33
+ # end
34
+ #
35
+ # server = Arachni::RPC::Server.new( host: 'localhost', port: 7331 )
36
+ # server.add_handler( 'bench', Bench.new )
37
+ #
38
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
39
+ class Proxy
40
+
41
+ class <<self
42
+
43
+ # @param [Symbol] method_name
44
+ # Method whose response to translate.
45
+ # @param [Block] translator
46
+ # Block to be passed the response and return a translated object.
47
+ def translate( method_name, &translator )
48
+ define_method method_name do |*args, &b|
49
+ # For blocking calls.
50
+ if !b
51
+ data = forward( method_name, *args )
52
+ return data.rpc_exception? ?
53
+ data : translator.call( data, *args )
54
+ end
55
+
56
+ # For non-blocking calls.
57
+ forward( method_name, *args ) do |data|
58
+ b.call( data.rpc_exception? ?
59
+ data : translator.call( data, *args ) )
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ # @param [Client] client
66
+ # @param [String] handler
67
+ def initialize( client, handler )
68
+ @client = client
69
+ @handler = handler
70
+ end
71
+
72
+ def forward( sym, *args, &block )
73
+ @client.call( "#{@handler}.#{sym.to_s}", *args, &block )
74
+ end
75
+
76
+ private
77
+
78
+ # Used to provide the illusion of locality for remote methods.
79
+ def method_missing( *args, &block )
80
+ forward( *args, &block )
81
+ end
82
+
83
+ end
84
+
85
+ end
86
+ end
@@ -6,67 +6,49 @@
6
6
 
7
7
  =end
8
8
 
9
- require File.join( File.expand_path( File.dirname( __FILE__ ) ), 'message' )
9
+ require_relative 'message'
10
10
 
11
11
  module Arachni
12
12
  module RPC
13
13
 
14
- #
15
14
  # Represents an RPC request.
16
15
  #
17
- # It's here only for formalization purposes,
18
- # it's not actually sent over the wire.
19
- #
20
- # What is sent is a hash generated by {#prepare_for_tx}.
21
- # which is in the form of:
16
+ # It's here only for formalization purposes, it's not actually sent over the wire.
22
17
  #
18
+ # What is sent is a hash generated by {#prepare_for_tx}. which is in the form of:
23
19
  #
24
- # {
25
- # 'message' => msg, # RPC message in the form of 'handler.method'
26
- # 'args' => args, # optional array of arguments for the remote method
27
- # 'token' => token, # optional authentication token
28
- # }
29
20
  #
30
- # Any client that has SSL support and can serialize a Hash
31
- # just like the one above can communicate with the RPC server.
21
+ # {
22
+ # # RPC message in the form of 'handler.method'.
23
+ # 'message' => msg,
24
+ # # Optional array of arguments for the remote method.
25
+ # 'args' => args,
26
+ # # Optional authentication token.
27
+ # 'token' => token
28
+ # }
32
29
  #
33
- # @author: Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
30
+ # Any client that has SSL support and can serialize a Hash just like the one
31
+ # above can communicate with the RPC server.
34
32
  #
33
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
35
34
  class Request < Message
36
35
 
37
- #
38
- # RPC message in the form of 'handler.method'.
39
- #
40
36
  # @return [String]
41
- #
37
+ # RPC message in the form of 'handler.method'.
42
38
  attr_accessor :message
43
39
 
44
- #
45
- # Optional array of arguments for the remote method.
46
- #
47
40
  # @return [Array]
48
- #
41
+ # Optional arguments for the remote method.
49
42
  attr_accessor :args
50
43
 
51
- #
52
- # Optional authentication token.
53
- #
54
44
  # @return [String]
55
- #
45
+ # Optional authentication token.
56
46
  attr_accessor :token
57
47
 
58
- #
59
- # Callback to be invoked on the response.
60
- #
61
48
  # @return [Proc]
62
- #
49
+ # Callback to be invoked on the response.
63
50
  attr_accessor :callback
64
51
 
65
- # @see Message#initialize
66
- def initialize( * )
67
- super
68
- end
69
-
70
52
  private
71
53
 
72
54
  def transmit?( attr )
@@ -6,60 +6,47 @@
6
6
 
7
7
  =end
8
8
 
9
- require File.join( File.expand_path( File.dirname( __FILE__ ) ), 'message' )
10
-
11
9
  module Arachni
12
10
  module RPC
13
11
 
14
- #
15
12
  # Represents an RPC response.
16
13
  #
17
14
  # It's here only for formalization purposes, it's not actually sent over the wire.
18
15
  #
19
- # What is sent is a hash generated by {#prepare_for_tx}
20
- # which is in the form of:
21
- #
16
+ # What is sent is a hash generated by {#prepare_for_tx} which is in the form of:
22
17
  #
23
18
  # {
24
19
  # # result of the RPC call
25
- # 'obj' => object
20
+ # 'obj' => object
26
21
  # }
27
22
  #
28
- # @author: Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
29
- #
23
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
30
24
  class Response < Message
31
25
 
32
- #
33
- # Return object of the {Request#message}.
34
- #
35
- # If there was an exception it will hold a Hash like:
36
- #
37
- # {
38
- # "exception" => "Trying to access non-existent object 'blah'.",
39
- # "backtrace" => [
40
- # [0] "/home/zapotek/workspace/arachni-rpc/lib/arachni/rpc/server.rb:285:in `call'",
41
- # [1] "/home/zapotek/workspace/arachni-rpc/lib/arachni/rpc/server.rb:85:in `block in receive_object'",
42
- # [2] "/home/zapotek/.rvm/gems/ruby-1.9.2-p180/gems/eventmachine-1.0.0.beta.3/lib/eventmachine.rb:1009:in `call'",
43
- # [3] "/home/zapotek/.rvm/gems/ruby-1.9.2-p180/gems/eventmachine-1.0.0.beta.3/lib/eventmachine.rb:1009:in `block in spawn_threadpool'"
44
- # ],
45
- # "type" => "InvalidObject"
46
- # }
47
- #
48
- # For all available exception types look at {Exceptions}.
49
- #
50
26
  # @return [Object]
51
- #
27
+ # Return object of the {Request#message}.
52
28
  attr_accessor :obj
53
29
 
54
- # @see Message#initialize
55
- def initialize( * )
56
- super
30
+ # @return [Hash]
31
+ #
32
+ # {
33
+ # "name" => "Trying to access non-existent object 'blah'.",
34
+ # "backtrace" => [
35
+ # [0] "/home/zapotek/workspace/arachni-rpc/lib/arachni/rpc/server.rb:285:in `call'",
36
+ # [1] "/home/zapotek/workspace/arachni-rpc/lib/arachni/rpc/server.rb:85:in `block in receive_object'",
37
+ # ],
38
+ # "type" => "InvalidObject"
39
+ # }
40
+ #
41
+ # For all available exception types look at {Exceptions}.
42
+ attr_accessor :exception
57
43
 
58
- @async = false
44
+ def exception?
45
+ !!exception
59
46
  end
60
47
 
61
48
  def async?
62
- @async
49
+ !!@async
63
50
  end
64
51
 
65
52
  def async!
@@ -69,10 +56,9 @@ class Response < Message
69
56
  private
70
57
 
71
58
  def transmit?( attr )
72
- ![ :@async ].include?( attr )
59
+ ![:@async].include?( attr )
73
60
  end
74
61
 
75
-
76
62
  end
77
63
 
78
64
  end
@@ -0,0 +1,278 @@
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
+ require 'set'
10
+ require 'logger'
11
+
12
+ module Arachni
13
+ module RPC
14
+
15
+ require_relative 'server/handler'
16
+
17
+ # RPC server.
18
+ #
19
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
20
+ class Server
21
+
22
+ # @return [String]
23
+ # Authentication token.
24
+ attr_reader :token
25
+
26
+ # @return [Hash]
27
+ # Configuration options.
28
+ attr_reader :opts
29
+
30
+ # @return [Logger]
31
+ attr_reader :logger
32
+
33
+ # Starts the RPC server.
34
+ #
35
+ # @example Example options:
36
+ #
37
+ # {
38
+ # :host => 'localhost',
39
+ # :port => 7331,
40
+ #
41
+ # # optional authentication token, if it doesn't match the one
42
+ # # set on the server-side you'll be getting exceptions.
43
+ # :token => 'superdupersecret',
44
+ #
45
+ # # optional serializer (defaults to YAML)
46
+ # :serializer => Marshal,
47
+ #
48
+ # # In order to enable peer verification one must first provide
49
+ # # the following:
50
+ # #
51
+ # # SSL CA certificate
52
+ # :ssl_ca => cwd + '/../spec/pems/cacert.pem',
53
+ # # SSL private key
54
+ # :ssl_pkey => cwd + '/../spec/pems/client/key.pem',
55
+ # # SSL certificate
56
+ # :ssl_cert => cwd + '/../spec/pems/client/cert.pem'
57
+ # }
58
+ #
59
+ # @param [Hash] opts
60
+ # @option opts [String] :host Hostname/IP address.
61
+ # @option opts [Integer] :port Port number.
62
+ # @option opts [String] :socket Path to UNIX domain socket.
63
+ # @option opts [String] :token Optional authentication token.
64
+ # @option opts [.dump, .load] :serializer (YAML)
65
+ # Serializer to use for message transmission.
66
+ # @option opts [.dump, .load] :fallback_serializer
67
+ # Optional fallback serializer to be used when the primary one fails.
68
+ # @option opts [Integer] :max_retries
69
+ # How many times to retry failed requests.
70
+ # @option opts [String] :ssl_ca SSL CA certificate.
71
+ # @option opts [String] :ssl_pkey SSL private key.
72
+ # @option opts [String] :ssl_cert SSL certificate.
73
+ def initialize( opts )
74
+ @opts = opts
75
+
76
+ if @opts[:ssl_pkey] && @opts[:ssl_cert]
77
+ if !File.exist?( @opts[:ssl_pkey] )
78
+ raise "Could not find private key at: #{@opts[:ssl_pkey]}"
79
+ end
80
+
81
+ if !File.exist?( @opts[:ssl_cert] )
82
+ raise "Could not find certificate at: #{@opts[:ssl_cert]}"
83
+ end
84
+ end
85
+
86
+ @token = @opts[:token]
87
+
88
+ @logger = ::Logger.new( STDOUT )
89
+ @logger.level = Logger::INFO
90
+
91
+ @host, @port = @opts[:host], @opts[:port]
92
+ @socket = @opts[:socket]
93
+
94
+ if !@socket && !(@host || @port)
95
+ fail ArgumentError, 'Needs either a :socket or :host and :port options.'
96
+ end
97
+
98
+ @port = @port.to_i
99
+
100
+ @reactor = Reactor.global
101
+
102
+ clear_handlers
103
+ end
104
+
105
+ # @example
106
+ #
107
+ # server.add_async_check do |method|
108
+ # #
109
+ # # Must return 'true' for async and 'false' for sync.
110
+ # #
111
+ # # Very simple check here...
112
+ # #
113
+ # 'async' == method.name.to_s.split( '_' )[0]
114
+ # end
115
+ #
116
+ # @param [Block] block
117
+ # Block to identify methods that pass their result to a block instead of
118
+ # simply returning them (which is the most usual operation of async methods).
119
+ def add_async_check( &block )
120
+ @async_checks << block
121
+ end
122
+
123
+ # @example
124
+ #
125
+ # server.add_handler( 'myclass', MyClass.new )
126
+ #
127
+ # @param [String] name
128
+ # Name by which to make the object available over RPC.
129
+ # @param [Object] obj
130
+ # Instantiated server object to expose.
131
+ def add_handler( name, obj )
132
+ @objects[name] = obj
133
+ @methods[name] = Set.new
134
+ @async_methods[name] = Set.new
135
+
136
+ obj.class.public_instance_methods( false ).each do |method|
137
+ @methods[name] << method.to_s
138
+ @async_methods[name] << method.to_s if async_check( obj.method( method ) )
139
+ end
140
+ end
141
+
142
+ # Clears all handlers and their associated information like methods and
143
+ # async check blocks.
144
+ #
145
+ # @see #add_handler
146
+ # @see #add_async_check
147
+ def clear_handlers
148
+ @objects = {}
149
+ @methods = {}
150
+
151
+ @async_checks = []
152
+ @async_methods = {}
153
+ end
154
+
155
+ # Runs the server and blocks while `Arachni::Reactor` is running.
156
+ def run
157
+ @reactor.run { start }
158
+ end
159
+
160
+ # Starts the server but does not block.
161
+ def start
162
+ @logger.info( 'System' ){ 'RPC Server started.' }
163
+ @logger.info( 'System' ) do
164
+ interface = @socket ? @socket : "#{@host}:#{@port}"
165
+ "Listening on #{interface}"
166
+ end
167
+
168
+ opts = @socket ? @socket : [@host, @port]
169
+ @reactor.listen( *[opts, Handler, self].flatten )
170
+ end
171
+
172
+ # @note If the called method is asynchronous it will be sent by this method
173
+ # directly, otherwise it will be handled by the {Handler}.
174
+ #
175
+ # @param [Handler] connection
176
+ # Connection with request information.
177
+ #
178
+ # @return [Response]
179
+ def call( connection )
180
+ req = connection.request
181
+ peer_ip_addr = connection.peer_address
182
+
183
+ expr, args = req.message, req.args
184
+ meth_name, obj_name = parse_expr( expr )
185
+
186
+ log_call( peer_ip_addr, expr, *args )
187
+
188
+ if !object_exist?( obj_name )
189
+ msg = "Trying to access non-existent object '#{obj_name}'."
190
+ @logger.error( 'Call' ){ msg + " [on behalf of #{peer_ip_addr}]" }
191
+ raise Exceptions::InvalidObject.new( msg )
192
+ end
193
+
194
+ if !public_method?( obj_name, meth_name )
195
+ msg = "Trying to access non-public method '#{meth_name}'."
196
+ @logger.error( 'Call' ){ msg + " [on behalf of #{peer_ip_addr}]" }
197
+ raise Exceptions::InvalidMethod.new( msg )
198
+ end
199
+
200
+ # The handler needs to know if this is an async call because if it is
201
+ # we'll have already send the response and it doesn't need to do
202
+ # transmit anything.
203
+ res = Response.new
204
+ res.async! if async?( obj_name, meth_name )
205
+
206
+ if res.async?
207
+ @objects[obj_name].send( meth_name.to_sym, *args ) do |obj|
208
+ res.obj = obj
209
+ connection.send_response( res )
210
+ end
211
+ else
212
+ res.obj = @objects[obj_name].send( meth_name.to_sym, *args )
213
+ end
214
+
215
+ res
216
+ end
217
+
218
+ # @return [TrueClass]
219
+ def alive?
220
+ true
221
+ end
222
+
223
+ # Shuts down the server after 2 seconds
224
+ def shutdown
225
+ wait_for = 2
226
+
227
+ @logger.info( 'System' ){ "Shutting down in #{wait_for} seconds..." }
228
+
229
+ # Don't die before returning...
230
+ @reactor.delay( wait_for ) do
231
+ @reactor.stop
232
+ end
233
+ true
234
+ end
235
+
236
+ private
237
+
238
+ def async?( objname, method )
239
+ @async_methods[objname].include?( method )
240
+ end
241
+
242
+ def async_check( method )
243
+ @async_checks.each { |check| return true if check.call( method ) }
244
+ false
245
+ end
246
+
247
+ def log_call( peer_ip_addr, expr, *args )
248
+ msg = "#{expr}"
249
+
250
+ # this should be in a @logger.debug call but it'll get out of sync
251
+ if @logger.level == Logger::DEBUG
252
+ cargs = args.map { |arg| arg.inspect }
253
+ msg += "( #{cargs.join( ', ' )} )"
254
+ end
255
+
256
+ msg += " [#{peer_ip_addr}]"
257
+
258
+ @logger.info( 'Call' ){ msg }
259
+ end
260
+
261
+ def parse_expr( expr )
262
+ parts = expr.to_s.split( '.' )
263
+ # method name, object name
264
+ [ parts.pop, parts.join( '.' ) ]
265
+ end
266
+
267
+ def object_exist?( obj_name )
268
+ @objects[obj_name] ? true : false
269
+ end
270
+
271
+ def public_method?( obj_name, method )
272
+ @methods[obj_name].include?( method )
273
+ end
274
+
275
+ end
276
+
277
+ end
278
+ end