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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +9 -3
- data/LICENSE.md +1 -1
- data/README.md +28 -34
- data/Rakefile +16 -19
- data/lib/arachni/rpc.rb +4 -8
- data/lib/arachni/rpc/client.rb +236 -0
- data/lib/arachni/rpc/client/handler.rb +167 -0
- data/lib/arachni/rpc/exceptions.rb +14 -38
- data/lib/arachni/rpc/message.rb +7 -15
- data/lib/arachni/rpc/protocol.rb +103 -0
- data/lib/arachni/rpc/proxy.rb +86 -0
- data/lib/arachni/rpc/request.rb +18 -36
- data/lib/arachni/rpc/response.rb +21 -35
- data/lib/arachni/rpc/server.rb +278 -0
- data/lib/arachni/rpc/server/handler.rb +145 -0
- data/lib/arachni/rpc/version.rb +3 -1
- data/spec/arachni/rpc/client_spec.rb +400 -0
- data/spec/arachni/rpc/exceptions_spec.rb +77 -0
- data/spec/arachni/rpc/message_spec.rb +47 -0
- data/spec/arachni/rpc/proxy_spec.rb +99 -0
- data/spec/arachni/rpc/request_spec.rb +53 -0
- data/spec/arachni/rpc/response_spec.rb +49 -0
- data/spec/arachni/rpc/server_spec.rb +129 -0
- data/spec/pems/cacert.pem +37 -0
- data/spec/pems/client/cert.pem +37 -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 +37 -0
- data/spec/pems/server/key.pem +51 -0
- data/spec/servers/basic.rb +3 -0
- data/spec/servers/server.rb +83 -0
- data/spec/servers/unix_socket.rb +8 -0
- data/spec/servers/with_ssl_primitives.rb +11 -0
- data/spec/spec_helper.rb +39 -0
- metadata +78 -21
- data/lib/arachni/rpc/remote_object_mapper.rb +0 -65
@@ -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
|
data/lib/arachni/rpc/request.rb
CHANGED
@@ -6,67 +6,49 @@
|
|
6
6
|
|
7
7
|
=end
|
8
8
|
|
9
|
-
|
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
|
-
#
|
31
|
-
#
|
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
|
-
#
|
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 )
|
data/lib/arachni/rpc/response.rb
CHANGED
@@ -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'
|
20
|
+
# 'obj' => object
|
26
21
|
# }
|
27
22
|
#
|
28
|
-
# @author
|
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
|
-
# @
|
55
|
-
|
56
|
-
|
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
|
-
|
44
|
+
def exception?
|
45
|
+
!!exception
|
59
46
|
end
|
60
47
|
|
61
48
|
def async?
|
62
|
-
|
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
|
-
![
|
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
|