arachni-rpc 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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