arachni-rpc-em 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,113 @@
1
+ =begin
2
+ Arachni-RPC
3
+ Copyright (c) 2011 Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
4
+
5
+ This is free software; you can copy and distribute and modify
6
+ this program under the term of the GPL v2.0 License
7
+ (See LICENSE file for details)
8
+
9
+ =end
10
+
11
+ # require 'arachni/rpc'
12
+ require File.join( File.expand_path( File.dirname( __FILE__ ) ), '../lib/arachni/rpc/', 'em' )
13
+
14
+ #
15
+ # You don't *need* to stick the whole thing inside an ::EM.run block,
16
+ # the system will manage EM on its own...however you *can* if you want to
17
+ # or if you're working inside a framework that already runs EventMachine.
18
+ ::EM.run do
19
+
20
+ # connect to the server
21
+ client = Arachni::RPC::EM::Client.new(
22
+ :host => 'localhost',
23
+ :port => 7332,
24
+
25
+ # optional authentication token, if it doesn't match the one
26
+ # set on the server-side you'll be getting exceptions.
27
+ :token => 'superdupersecret',
28
+
29
+ # :keep_alive => false,
30
+
31
+ # optional serializer (defaults to YAML)
32
+ # see the 'serializer' method at:
33
+ # http://eventmachine.rubyforge.org/EventMachine/Protocols/ObjectProtocol.html#M000369
34
+ :serializer => Marshal
35
+ )
36
+
37
+ bench = Arachni::RPC::RemoteObjectMapper.new( client, 'bench' )
38
+
39
+ #
40
+ # There's one downside though, if you want to run this thing inside an
41
+ # ::EM.run block: you'll have to wrap all sync calls in a ::Arachni::RPC::EM::EM::Synchrony.run block.
42
+ #
43
+ # Like so:
44
+ ::Arachni::RPC::EM::Synchrony.run do
45
+ p bench.foo( 'First sync call in individual Synchrony block.' )
46
+ # => "First sync call in individual Synchrony block."
47
+ end
48
+
49
+ # you can use it again individually
50
+ ::Arachni::RPC::EM::Synchrony.run do
51
+ p bench.foo( 'Second sync call in individual Synchrony block.' )
52
+ # => "Second sync call in individual Synchrony block."
53
+ end
54
+
55
+ # or just wrap lots of calls in it
56
+ ::Arachni::RPC::EM::Synchrony.run do
57
+ p bench.foo( 'Third sync call in individual Synchrony block.' )
58
+ # => "Third sync call in individual Synchrony block."
59
+
60
+ p bench.foo( '--> And this one is in the same block as well.' )
61
+ # => "--> And this one is in the same block as well."
62
+
63
+ p bench.async_foo( 'This is a sync call to an async remote method.' )
64
+ # => "This is a sync call to an async remote method."
65
+ end
66
+
67
+ # async calls are the same
68
+ bench.foo( 'This is an async call... business as usual. :)' ) {
69
+ |res|
70
+ p res
71
+ # => "This is an async call... business as usual. :)"
72
+ }
73
+
74
+ bench.async_foo( 'This is an async call to an async remote method.' ) {
75
+ |res|
76
+ p res
77
+ # => "This is an async call to an async remote method."
78
+ }
79
+
80
+
81
+ #
82
+ # The system uses 2 methods to make calls appear sync:
83
+ # - Threads (when the code is *not* run directly inside the Reactor's thread, see example.rb)
84
+ # - Fibers (when the code *is* run inside the Reactor's thread, like right here)
85
+ #
86
+ # For performance reasons callbacks are ::EM.defer'ed.
87
+ #
88
+ # This means that they're already in their own thread so you don't need
89
+ # ::Arachni::RPC::EM::EM::Synchrony.run to perform sync calls from inside callbacks.
90
+ #
91
+ bench.async_foo( 'Coo-coo' ) {
92
+ |res|
93
+
94
+ p res
95
+ # => "Coo-coo"
96
+
97
+ p bench.async_foo( 'Coo-coo 2' )
98
+ # => "Coo-coo 2"
99
+
100
+ bench.async_foo( 'Coo-coo 3' ) {
101
+ |res|
102
+
103
+ p res
104
+ # => "Coo-coo 3"
105
+
106
+ p bench.foo( 'Coo-coo 4' )
107
+ # => "Coo-coo 4"
108
+ }
109
+
110
+ }
111
+
112
+
113
+ end
@@ -0,0 +1,181 @@
1
+ =begin
2
+ Arachni-RPC
3
+ Copyright (c) 2011 Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
4
+
5
+ This is free software; you can copy and distribute and modify
6
+ this program under the term of the GPL v2.0 License
7
+ (See LICENSE file for details)
8
+
9
+ =end
10
+
11
+ cwd = File.expand_path( File.dirname( __FILE__ ) )
12
+ require File.join( cwd, '../lib/arachni/rpc/', 'em' )
13
+
14
+
15
+ # connect to the server
16
+ client = Arachni::RPC::EM::Client.new(
17
+ :host => 'localhost',
18
+ :port => 7332,
19
+
20
+ # optional authentication token, if it doesn't match the one
21
+ # set on the server-side you'll be getting exceptions.
22
+ :token => 'superdupersecret',
23
+
24
+ # optional serializer (defaults to YAML)
25
+ # see the 'serializer' method at:
26
+ # http://eventmachine.rubyforge.org/EventMachine/Protocols/ObjectProtocol.html#M000369
27
+ :serializer => Marshal,
28
+
29
+ #
30
+ # Connection keep alive is set to true by default, this means that
31
+ # a single connection will be maintained and all calls will pass
32
+ # through it.
33
+ # This bypasses a bug in EventMachine and allows you to perform thousands
34
+ # of calls without issue.
35
+ #
36
+ # However, you are responsible for closing the connection when you're done.
37
+ #
38
+ # If keep alive is set to false then each call will go through its own connection
39
+ # and the responsibility for closing that connection falls on Arachni-RPC.
40
+ #
41
+ # Unfortunately, if you try to make a greater number of calls than your system's
42
+ # maximum open file descriptors limit EventMachine will freak-out.
43
+ #
44
+ :keep_alive => false,
45
+
46
+ #
47
+ # In order to enable peer verification one must first provide
48
+ # the following:
49
+ #
50
+ # SSL CA certificate
51
+ # :ssl_ca => cwd + '/../spec/pems/cacert.pem',
52
+ # SSL private key
53
+ # :ssl_pkey => cwd + '/../spec/pems/client/key.pem',
54
+ # SSL certificate
55
+ # :ssl_cert => cwd + '/../spec/pems/client/cert.pem'
56
+ )
57
+
58
+ # Make things easy on the eyes using the mapper, it allows you to do this:
59
+ #
60
+ # res = bench.foo( arg )
61
+ #
62
+ # Instead of:
63
+ #
64
+ # res = client.call( 'bench.foo', arg )
65
+ #
66
+ bench = Arachni::RPC::RemoteObjectMapper.new( client, 'bench' )
67
+
68
+ #
69
+ # In order to perform an asynchronous call you will need to provide a block,
70
+ # even if it is an empty one.
71
+ #
72
+ bench.foo( 'This is an async call to "bench.foo".' ) {
73
+ |res|
74
+
75
+ p res
76
+ # => "This is an async call to \"bench.foo\"."
77
+
78
+ # did something RPC related go wrong?
79
+ # p res.rpc_exception?
80
+ # => false
81
+
82
+ # did something go wrong on the server-side?
83
+ # p res.rpc_remote_exception?
84
+ # => false
85
+
86
+ # did the connection die abruptly?
87
+ # p res.rpc_connection_error?
88
+ # => false
89
+
90
+ # did we call an object for which there is no handler on the server-side?
91
+ # p res.rpc_invalid_object_error?
92
+ # => false
93
+
94
+ # did we call a server-side method that isn't existent or public?
95
+ # p res.rpc_invalid_method_error?
96
+ # => false
97
+
98
+ # was there an authentication token mismatch?
99
+ # p res.rpc_invalid_token_error?
100
+ # => false
101
+ }
102
+
103
+
104
+
105
+ #
106
+ # On the server-side this is an async method but works just like everything else here.
107
+ #
108
+ # You'll need to kind-of specify the async methods on the server-side,
109
+ # check the server example file for more info.
110
+ #
111
+ bench.async_foo( 'This is an async call to "bench.async_foo".' ) {
112
+ |res|
113
+ p res
114
+ # => "This is an async call to \"bench.async_foo\"."
115
+ }
116
+
117
+ p bench.async_foo( 'This is a sync call to "bench.async_foo".' )
118
+ # => "This is a sync call to \"bench.async_foo\"."
119
+
120
+
121
+ #
122
+ # To perform a sync (blocking) call do the usual stuff.
123
+ #
124
+ # This is thread safe so if you'd rather use Threads instead of async calls
125
+ # for that extra performance kick you go ahead and do your thing now...
126
+ #
127
+ p bench.foo( 'This is a sync call to "bench.foo".' )
128
+ # => "This is a sync call to \"bench.foo\"."
129
+
130
+
131
+ #
132
+ # When you are performing a synchronous call and things go wrong
133
+ # an exception will be thrown.
134
+ #
135
+ # Exceptions on the server-side unrelated to the RPC system will be forwarded.
136
+ #
137
+
138
+ #
139
+ # Non-existent object.
140
+ #
141
+ blah = Arachni::RPC::RemoteObjectMapper.new( client, 'blah' )
142
+ begin
143
+ p blah.something
144
+ rescue Exception => e
145
+ p e # => #<Arachni::RPC::EM::Exceptions::InvalidObject: Trying to access non-existent object 'blah'.>
146
+ end
147
+
148
+ #
149
+ # Non-existent or non-public method.
150
+ #
151
+ begin
152
+ p bench.fdoo
153
+ rescue Exception => e
154
+ p e # => #<Arachni::RPC::EM::Exceptions::InvalidMethod: Trying to access non-public method 'fdoo'.>
155
+ end
156
+
157
+ #
158
+ # When you are performing an asynchronous call and things go wrong
159
+ # an exception will be returned.
160
+ #
161
+ # It will *NOT* be thrown!
162
+ # It will be *RETURNED*!
163
+ #
164
+ blah.something {
165
+ |res|
166
+ p res # => #<Arachni::RPC::EM::Exceptions::InvalidObject: Trying to access non-existent object 'blah'.>
167
+
168
+ # RPC Exception helper methods have been added to all Ruby objects (except BasicObject)
169
+ # so they'll always be there when you need them.
170
+
171
+ # p res.rpc_exception? # => true
172
+ # p res.rpc_invalid_object_error? # => true
173
+ }
174
+
175
+ #
176
+ # We don't know when async calls will return so we wait forever.
177
+ #
178
+ # Call ::EM.stop to break-out.
179
+ #
180
+ Arachni::RPC::EM.block!
181
+
@@ -0,0 +1,80 @@
1
+ =begin
2
+ Arachni-RPC
3
+ Copyright (c) 2011 Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
4
+
5
+ This is free software; you can copy and distribute and modify
6
+ this program under the term of the GPL v2.0 License
7
+ (See LICENSE file for details)
8
+
9
+ =end
10
+
11
+ cwd = File.expand_path( File.dirname( __FILE__ ) )
12
+ require File.join( cwd, '../lib/arachni/rpc/', 'em' )
13
+
14
+ class Parent
15
+ def foo( arg )
16
+ return arg
17
+ end
18
+ end
19
+
20
+ class Bench < Parent
21
+
22
+ # in order to make inherited methods accessible you've got to explicitly
23
+ # make them public
24
+ private :foo
25
+ public :foo
26
+
27
+ #
28
+ # Uses EventMachine to call the block asynchronously
29
+ #
30
+ def async_foo( arg, &block )
31
+ ::EM.schedule {
32
+ ::EM.defer {
33
+ block.call( arg ) if block_given?
34
+ }
35
+ }
36
+ end
37
+
38
+ end
39
+
40
+ server = Arachni::RPC::EM::Server.new(
41
+ :host => 'localhost',
42
+ :port => 7332,
43
+
44
+ # optional authentication token, if it doesn't match the one
45
+ # set on the client-side the client won't be able to do anything
46
+ # and keep getting exceptions.
47
+ :token => 'superdupersecret',
48
+
49
+ # optional serializer (defaults to YAML)
50
+ # see the 'serializer' method at:
51
+ # http://eventmachine.rubyforge.org/EventMachine/Protocols/ObjectProtocol.html#M000369
52
+ :serializer => Marshal,
53
+
54
+ # :ssl_ca => cwd + '/../spec/pems/cacert.pem',
55
+ # :ssl_pkey => cwd + '/../spec/pems/server/key.pem',
56
+ # :ssl_cert => cwd + '/../spec/pems/server/cert.pem'
57
+ )
58
+
59
+ #
60
+ # This is a way for you to identify methods that pass their result to a block
61
+ # instead of simply returning them (which is the most usual operation of async methods.
62
+ #
63
+ # So no need to change your coding convetions to fit the RPC stuff,
64
+ # you can just decide dynamically based on a plethora of data which Ruby provides
65
+ # by its 'Method' class.
66
+ #
67
+ server.add_async_check {
68
+ |method|
69
+ #
70
+ # Must return 'true' for async and 'false' for sync.
71
+ #
72
+ # Very simple check here...
73
+ #
74
+ 'async' == method.name.to_s.split( '_' )[0]
75
+ }
76
+
77
+ server.add_handler( 'bench', Bench.new )
78
+
79
+ # this will block forever, call server.shutdown to kill the server.
80
+ server.run
@@ -0,0 +1,26 @@
1
+ =begin
2
+ Arachni
3
+ Copyright (c) 2010-2011 Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
4
+
5
+ This is free software; you can copy and distribute and modify
6
+ this program under the term of the GPL v2.0 License
7
+ (See LICENSE file for details)
8
+
9
+ =end
10
+
11
+ require 'eventmachine'
12
+ require 'socket'
13
+ require 'logger'
14
+ require 'fiber'
15
+
16
+ require 'arachni/rpc'
17
+
18
+ require 'yaml'
19
+ YAML::ENGINE.yamler = 'syck'
20
+
21
+ require File.join( File.expand_path( File.dirname( __FILE__ ) ), 'em', 'connection_utilities' )
22
+ require File.join( File.expand_path( File.dirname( __FILE__ ) ), 'em', 'ssl' )
23
+ require File.join( File.expand_path( File.dirname( __FILE__ ) ), 'em', 'protocol' )
24
+ require File.join( File.expand_path( File.dirname( __FILE__ ) ), 'em', 'server' )
25
+ require File.join( File.expand_path( File.dirname( __FILE__ ) ), 'em', 'client' )
26
+ require File.join( File.expand_path( File.dirname( __FILE__ ) ), 'em', 'em' )
@@ -0,0 +1,280 @@
1
+ =begin
2
+ Arachni-RPC
3
+ Copyright (c) 2011 Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
4
+
5
+ This is free software; you can copy and distribute and modify
6
+ this program under the term of the GPL v2.0 License
7
+ (See LICENSE file for details)
8
+
9
+ =end
10
+
11
+ module Arachni
12
+ module RPC
13
+ module EM
14
+
15
+ #
16
+ # Simple EventMachine-based RPC client.
17
+ #
18
+ # It's capable of:
19
+ # - performing and handling a few thousands requests per second (depending on call size, network conditions and the like)
20
+ # - TLS encryption
21
+ # - asynchronous and synchronous requests
22
+ # - handling remote asynchronous calls that require a block
23
+ #
24
+ # @author: Tasos "Zapotek" Laskos
25
+ # <tasos.laskos@gmail.com>
26
+ # <zapotek@segfault.gr>
27
+ # @version: 0.1
28
+ #
29
+ class Client
30
+
31
+ include ::Arachni::RPC::Exceptions
32
+
33
+ #
34
+ # Handles EventMachine's connection and RPC related stuff.
35
+ #
36
+ # It's responsible for TLS, storing and calling callbacks as well as
37
+ # serializing, transmitting and receiving objects.
38
+ #
39
+ # @author: Tasos "Zapotek" Laskos
40
+ # <tasos.laskos@gmail.com>
41
+ # <zapotek@segfault.gr>
42
+ # @version: 0.1
43
+ #
44
+ class Handler < EventMachine::Connection
45
+ include ::Arachni::RPC::EM::Protocol
46
+ include ::Arachni::RPC::EM::ConnectionUtilities
47
+
48
+ def initialize( opts )
49
+ @opts = opts
50
+ @status = :idle
51
+
52
+ @request = nil
53
+ assume_client_role!
54
+ end
55
+
56
+ def post_init
57
+ @status = :active
58
+ start_ssl
59
+ end
60
+
61
+ def unbind( reason )
62
+ end_ssl
63
+
64
+ if @request && @request.callback && @status != :done
65
+ e = Arachni::RPC::Exceptions::ConnectionError.new( "Connection closed [#{reason}]" )
66
+ @request.callback.call( e )
67
+ end
68
+
69
+ @status = :closed
70
+ end
71
+
72
+ def connection_completed
73
+ @status = :established
74
+ end
75
+
76
+ def status
77
+ @status
78
+ end
79
+
80
+ #
81
+ # Used to handle responses.
82
+ #
83
+ # @param [Arachni::RPC::EM::Response] res
84
+ #
85
+ def receive_response( res )
86
+
87
+ if exception?( res )
88
+ res.obj = Arachni::RPC::Exceptions.from_response( res )
89
+ end
90
+
91
+ if cb = @request.callback
92
+
93
+ callback = Proc.new {
94
+ |obj|
95
+ cb.call( obj )
96
+
97
+ @status = :done
98
+ close_connection
99
+ }
100
+
101
+ if @request.defer?
102
+ # the callback might block a bit so tell EM to put it in a thread
103
+ ::EM.defer {
104
+ callback.call( res.obj )
105
+ }
106
+ else
107
+ callback.call( res.obj )
108
+ end
109
+ end
110
+ end
111
+
112
+ # @param [Arachni::RPC::EM::Response] res
113
+ def exception?( res )
114
+ res.obj.is_a?( Hash ) && res.obj['exception'] ? true : false
115
+ end
116
+
117
+ #
118
+ # Sends the request.
119
+ #
120
+ # @param [Arachni::RPC::EM::Request] req
121
+ #
122
+ def send_request( req )
123
+ @status = :pending
124
+ @request = req
125
+ super( req )
126
+ end
127
+ end
128
+
129
+ #
130
+ # Options hash
131
+ #
132
+ # @return [Hash]
133
+ #
134
+ attr_reader :opts
135
+
136
+ #
137
+ # Starts EventMachine and connects to the remote server.
138
+ #
139
+ # opts example:
140
+ #
141
+ # {
142
+ # :host => 'localhost',
143
+ # :port => 7331,
144
+ #
145
+ # # optional authentication token, if it doesn't match the one
146
+ # # set on the server-side you'll be getting exceptions.
147
+ # :token => 'superdupersecret',
148
+ #
149
+ # # optional serializer (defaults to YAML)
150
+ # # see the 'serializer' method at:
151
+ # # http://eventmachine.rubyforge.org/EventMachine/Protocols/ObjectProtocol.html#M000369
152
+ # :serializer => Marshal,
153
+ #
154
+ # #
155
+ # # In order to enable peer verification one must first provide
156
+ # # the following:
157
+ # #
158
+ # # SSL CA certificate
159
+ # :ssl_ca => cwd + '/../spec/pems/cacert.pem',
160
+ # # SSL private key
161
+ # :ssl_pkey => cwd + '/../spec/pems/client/key.pem',
162
+ # # SSL certificate
163
+ # :ssl_cert => cwd + '/../spec/pems/client/cert.pem'
164
+ # }
165
+ #
166
+ # @param [Hash] opts
167
+ #
168
+ def initialize( opts )
169
+
170
+ begin
171
+ @opts = opts.merge( :role => :client )
172
+ @token = @opts[:token]
173
+
174
+ @host, @port = @opts[:host], @opts[:port]
175
+
176
+ Arachni::RPC::EM.ensure_em_running!
177
+ rescue EventMachine::ConnectionError => e
178
+ exc = ConnectionError.new( e.to_s + " for '#{@k}'." )
179
+ exc.set_backtrace( e.backtrace )
180
+ raise exc
181
+ end
182
+ end
183
+
184
+ #
185
+ # Calls a remote method and grabs the result.
186
+ #
187
+ # There are 2 ways to perform a call, async (non-blocking) and sync (blocking).
188
+ #
189
+ # To perform an async call you need to provide a block which will be passed
190
+ # the return value once the method has finished executing.
191
+ #
192
+ # server.call( 'handler.method', arg1, arg2 ){
193
+ # |res|
194
+ # do_stuff( res )
195
+ # }
196
+ #
197
+ #
198
+ # To perform a sync (blocking) call do not pass a block, the value will be
199
+ # returned as usual.
200
+ #
201
+ # res = server.call( 'handler.method', arg1, arg2 )
202
+ #
203
+ # @param [String] msg in the form of <i>handler.method</i>
204
+ # @param [Array] args collection of argumenta to be passed to the method
205
+ # @param [Proc] &block
206
+ #
207
+ def call( msg, *args, &block )
208
+
209
+ req = Request.new(
210
+ :message => msg,
211
+ :args => args,
212
+ :callback => block,
213
+ :token => @token
214
+ )
215
+
216
+ if block_given?
217
+ call_async( req )
218
+ else
219
+ return call_sync( req )
220
+ end
221
+ end
222
+
223
+ private
224
+
225
+ def connect
226
+ ::EM.connect( @host, @port, Handler, @opts )
227
+ end
228
+
229
+ def call_async( req, &block )
230
+ ::EM.schedule {
231
+ req.callback = block if block_given?
232
+ connect.send_request( req )
233
+ }
234
+ end
235
+
236
+ def call_sync( req )
237
+
238
+ ret = nil
239
+ # if we're in the Reactor thread use a Fiber and if we're not
240
+ # use a Thread
241
+ if !::EM::reactor_thread?
242
+ t = Thread.current
243
+ call_async( req ) {
244
+ |obj|
245
+ t.wakeup
246
+ ret = obj
247
+ }
248
+ sleep
249
+ else
250
+ # Fibers do not work across threads so don't defer the callback
251
+ # once the Handler gets to it
252
+ req.do_not_defer!
253
+
254
+ f = Fiber.current
255
+ call_async( req ) {
256
+ |obj|
257
+ f.resume( obj )
258
+ }
259
+
260
+ begin
261
+ ret = Fiber.yield
262
+ rescue FiberError => e
263
+ msg = e.to_s + "\n"
264
+ msg += '(Consider wrapping your sync code in a' +
265
+ ' "::Arachni::RPC::EM::Synchrony.run" ' +
266
+ 'block when your app is running inside the Reactor\'s thread)'
267
+
268
+ raise( msg )
269
+ end
270
+ end
271
+
272
+ raise ret if ret.is_a?( Exception )
273
+ return ret
274
+ end
275
+
276
+ end
277
+
278
+ end
279
+ end
280
+ end