arachni-rpc-em 0.1

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,180 @@
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 'openssl'
12
+
13
+ #
14
+ # Adds support for a few helper methods to X509 certs.
15
+ #
16
+ # @see https://gist.github.com/1151454
17
+ #
18
+ class OpenSSL::X509::Certificate
19
+
20
+ def ==( other )
21
+ other.respond_to?( :to_pem ) && to_pem == other.to_pem
22
+ end
23
+
24
+ # A serial *must* be unique for each certificate. Self-signed certificates,
25
+ # and thus root CA certificates, have the same `issuer' as `subject'.
26
+ def top_level?
27
+ serial == serial && issuer.to_s == subject.to_s
28
+ end
29
+
30
+ alias_method :root?, :top_level?
31
+ alias_method :self_signed?, :top_level?
32
+ end
33
+
34
+ module Arachni
35
+ module RPC
36
+ module EM
37
+
38
+ #
39
+ # Adds support for SSL and peer verification.
40
+ #
41
+ # To be included by EventMachine::Connection classes.
42
+ #
43
+ # @see https://gist.github.com/1151454
44
+ #
45
+ # @author: Tasos "Zapotek" Laskos
46
+ # <tasos.laskos@gmail.com>
47
+ # <zapotek@segfault.gr>
48
+ # @version: 0.1
49
+ #
50
+ module SSL
51
+
52
+ include ::Arachni::RPC::EM::ConnectionUtilities
53
+
54
+ #
55
+ # Starts SSL with the supplied keys, certs etc.
56
+ #
57
+ def start_ssl
58
+ @verified_peer = false
59
+ @ssl_requested = true
60
+
61
+ ssl_opts = {}
62
+ if ssl_opts?
63
+
64
+ ssl_opts = {
65
+ :private_key_file => @opts[:ssl_pkey],
66
+ :cert_chain_file => @opts[:ssl_cert],
67
+ :verify_peer => true
68
+ }
69
+
70
+ @last_seen_cert = nil
71
+ end
72
+
73
+ # ap ssl_opts
74
+ start_tls( ssl_opts )
75
+ end
76
+
77
+ def verified_peer?
78
+ @verified_peer
79
+ end
80
+
81
+ #
82
+ # Cleans up any SSL related resources.
83
+ #
84
+ def end_ssl
85
+ end
86
+
87
+ #
88
+ # To be implemented by the parent.
89
+ #
90
+ # By default, it will 'warn' if the severity is :error and will 'raise'
91
+ # if the severity if :fatal.
92
+ #
93
+ # @param [Symbol] severity :fatal, :error, :warn, :info, :debug
94
+ # @param [String] progname name of the component that performed the action
95
+ # @param [String] msg message to log
96
+ #
97
+ def log( severity, progname, msg )
98
+ warn "#{progname}: #{msg}" if severity == :error
99
+ raise "#{progname}: #{msg}" if severity == :fatal
100
+ end
101
+
102
+ #
103
+ # @return [OpenSSL::X509::Store] certificate store
104
+ #
105
+ def ca_store
106
+ if !@ca_store
107
+ if file = @opts[:ssl_ca]
108
+ @ca_store = OpenSSL::X509::Store.new
109
+ @ca_store.add_file( file )
110
+ else
111
+ raise "No CA certificate has been provided."
112
+ end
113
+ end
114
+
115
+ return @ca_store
116
+ end
117
+
118
+ #
119
+ # Verifies the peer cert based on the {#ca_store}.
120
+ #
121
+ # @see http://eventmachine.rubyforge.org/EventMachine/Connection.html#M000271
122
+ #
123
+ def ssl_verify_peer( cert_string )
124
+
125
+ cert = OpenSSL::X509::Certificate.new( cert_string )
126
+
127
+ # Some servers send the same certificate multiple times. I'm not even
128
+ # joking... (gmail.com)
129
+ return true if cert == @last_seen_cert
130
+
131
+ if ca_store.verify( cert )
132
+ @last_seen_cert = cert
133
+
134
+ # A server may send the root certificate, which we already have and thus
135
+ # should not be added to the store again.
136
+ ca_store.add_cert( @last_seen_cert ) if !@last_seen_cert.root?
137
+
138
+ @verified_peer = true
139
+ return true
140
+ else
141
+ log( :error, 'SSL',
142
+ "#{ca_store.error_string.capitalize} ['#{peer_ip_addr}']."
143
+ )
144
+ return false
145
+ end
146
+ end
147
+
148
+ #
149
+ # Checks for an appropriate server cert hostname if run from the client-side.
150
+ #
151
+ # Does nothing when on the server-side.
152
+ #
153
+ # @see http://eventmachine.rubyforge.org/EventMachine/Connection.html#M000270
154
+ #
155
+ def ssl_handshake_completed
156
+ if are_we_a_client? && ssl_opts? &&
157
+ !OpenSSL::SSL.verify_certificate_identity( @last_seen_cert,
158
+ @opts[:host] )
159
+
160
+ log( :error, 'SSL',
161
+ "The hostname '#{@server.opts[:host]}' " +
162
+ "does not match the server certificate."
163
+ )
164
+
165
+ connection_close
166
+ end
167
+ end
168
+
169
+ def are_we_a_client?
170
+ @opts[:role] == :client
171
+ end
172
+
173
+ def ssl_opts?
174
+ @opts[:ssl_ca] && @opts[:ssl_pkey] && @opts[:ssl_cert]
175
+ end
176
+
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,17 @@
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
+ VERSION = '0.1'
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,211 @@
1
+ require File.join( File.expand_path( File.dirname( __FILE__ ) ), '../../../', 'spec_helper' )
2
+
3
+ describe Arachni::RPC::EM::Client do
4
+
5
+ before( :all ) do
6
+ @arg = [
7
+ 'one',
8
+ 2,
9
+ { :three => 3 },
10
+ [ 4 ]
11
+ ]
12
+ end
13
+
14
+ describe "#initialize" do
15
+ it "should be able to properly assign class options (including :role)" do
16
+ opts = rpc_opts.merge( :role => :client )
17
+ start_client( opts ).opts.should == opts
18
+ end
19
+ end
20
+
21
+ describe "raw interface" do
22
+
23
+ context "when using Threads" do
24
+
25
+ it "should be able to perform synchronous calls" do
26
+ @arg.should == start_client( rpc_opts ).call( 'test.foo', @arg )
27
+ end
28
+
29
+ it "should be able to perform asynchronous calls" do
30
+ start_client( rpc_opts ).call( 'test.foo', @arg ) {
31
+ |res|
32
+ @arg.should == res
33
+ ::EM.stop
34
+ }
35
+ Arachni::RPC::EM.block!
36
+ end
37
+ end
38
+
39
+ context "when run inside the Reactor loop" do
40
+
41
+ it "should be able to perform synchronous calls" do
42
+ ::EM.run do
43
+
44
+ ::Arachni::RPC::EM::Synchrony.run do
45
+ @arg.should == start_client( rpc_opts ).call( 'test.foo', @arg )
46
+ ::EM.stop
47
+ end
48
+ end
49
+ end
50
+
51
+ it "should be able to perform asynchronous calls" do
52
+ ::EM.run do
53
+
54
+ start_client( rpc_opts ).call( 'test.foo', @arg ) {
55
+ |res|
56
+ res.should == @arg
57
+ ::EM.stop
58
+ }
59
+
60
+ end
61
+ end
62
+
63
+ end
64
+ end
65
+
66
+ describe "Arachni::RPC::RemoteObjectMapper interface" do
67
+ it "should be able to properly forward synchronous calls" do
68
+ test = Arachni::RPC::RemoteObjectMapper.new( start_client( rpc_opts ), 'test' )
69
+ test.foo( @arg ).should == @arg
70
+ # ::EM.stop
71
+ end
72
+
73
+ it "should be able to properly forward synchronous calls" do
74
+ test = Arachni::RPC::RemoteObjectMapper.new( start_client( rpc_opts ), 'test' )
75
+ test.foo( @arg ) {
76
+ |res|
77
+ res.should == @arg
78
+ ::EM.stop
79
+ }
80
+ Arachni::RPC::EM.block!
81
+ end
82
+ end
83
+
84
+ describe "exception" do
85
+ context 'when performing asynchronous calls' do
86
+
87
+ it "should be returned when requesting inexistent objects" do
88
+ start_client( rpc_opts ).call( 'bar.foo' ) {
89
+ |res|
90
+ res.rpc_invalid_object_error?.should be_true
91
+ ::EM.stop
92
+ }
93
+ Arachni::RPC::EM.block!
94
+ end
95
+
96
+ it "should be returned when requesting inexistent or non-public methods" do
97
+ start_client( rpc_opts ).call( 'test.bar' ) {
98
+ |res|
99
+ res.rpc_invalid_method_error?.should be_true
100
+ ::EM.stop
101
+ }
102
+ Arachni::RPC::EM.block!
103
+ end
104
+
105
+ it "should be returned when there's a remote exception" do
106
+ start_client( rpc_opts ).call( 'test.foo' ) {
107
+ |res|
108
+ res.rpc_remote_exception?.should be_true
109
+ ::EM.stop
110
+ }
111
+ Arachni::RPC::EM.block!
112
+ end
113
+
114
+ end
115
+
116
+ context 'when performing synchronous calls' do
117
+
118
+ it "should be raised when requesting inexistent objects" do
119
+ begin
120
+ start_client( rpc_opts ).call( 'bar2.foo' )
121
+ rescue Exception => e
122
+ e.rpc_invalid_object_error?.should be_true
123
+ end
124
+ end
125
+
126
+ it "should be raised when requesting inexistent or non-public methods" do
127
+ begin
128
+ start_client( rpc_opts ).call( 'test.bar2' )
129
+ rescue Exception => e
130
+ e.rpc_invalid_method_error?.should be_true
131
+ end
132
+
133
+ end
134
+
135
+ it "should be raised when there's a remote exception" do
136
+ begin
137
+ start_client( rpc_opts ).call( 'test.foo' )
138
+ rescue Exception => e
139
+ e.rpc_remote_exception?.should be_true
140
+ end
141
+ end
142
+
143
+ end
144
+ end
145
+
146
+ it "should be able to retain stability and consistency under heavy load" do
147
+ client = start_client( rpc_opts )
148
+
149
+ n = 1000
150
+ cnt = 0
151
+
152
+ mismatches = []
153
+
154
+ n.times {
155
+ |i|
156
+ client.call( 'test.foo', i ) {
157
+ |res|
158
+
159
+ cnt += 1
160
+
161
+ mismatches << [i, res] if i != res
162
+ ::EM.stop if cnt == n || !mismatches.empty?
163
+ }
164
+ }
165
+
166
+ Arachni::RPC::EM.block!
167
+
168
+ mismatches.should be_empty
169
+ end
170
+
171
+ it "should throw error when connecting to inexistent server" do
172
+ start_client( rpc_opts.merge( :port => 9999 ) ).call( 'test.foo', @arg ) {
173
+ |res|
174
+ res.rpc_connection_error?.should be_true
175
+ ::EM.stop
176
+ }
177
+ Arachni::RPC::EM.block!
178
+ end
179
+
180
+ context "when using valid SSL primitives" do
181
+ it "should be able to establish a connection" do
182
+ res = start_client( rpc_opts_with_ssl_primitives ).call( 'test.foo', @arg )
183
+ res.should == @arg
184
+ ::EM.stop
185
+ end
186
+ end
187
+
188
+ context "when using invalid SSL primitives" do
189
+ it "should not be able to establish a connection" do
190
+ start_client( rpc_opts_with_invalid_ssl_primitives ).call( 'test.foo', @arg ){
191
+ |res|
192
+ res.rpc_connection_error?.should be_true
193
+ ::EM.stop
194
+ }
195
+ Arachni::RPC::EM.block!
196
+ end
197
+ end
198
+
199
+ context "when using mixed SSL primitives" do
200
+ it "should not be able to establish a connection" do
201
+ start_client( rpc_opts_with_mixed_ssl_primitives ).call( 'test.foo', @arg ){
202
+ |res|
203
+ res.rpc_connection_error?.should be_true
204
+ res.rpc_ssl_error?.should be_true
205
+ ::EM.stop
206
+ }
207
+ Arachni::RPC::EM.block!
208
+ end
209
+ end
210
+
211
+ end
@@ -0,0 +1,4 @@
1
+ require File.join( File.expand_path( File.dirname( __FILE__ ) ), '../../../', 'spec_helper' )
2
+
3
+ describe Arachni::RPC::EM do
4
+ end
@@ -0,0 +1,79 @@
1
+ require File.join( File.expand_path( File.dirname( __FILE__ ) ), '../../../', 'spec_helper' )
2
+
3
+ class Arachni::RPC::EM::Server
4
+ public :async?, :async_check, :object_exist?, :public_method?
5
+ attr_accessor :proxy
6
+ end
7
+
8
+ describe Arachni::RPC::EM::Server do
9
+
10
+ before( :all ) do
11
+ @opts = rpc_opts.merge( :port => 7333 )
12
+ @server, t = start_server( @opts )
13
+ end
14
+
15
+ describe "#initialize" do
16
+ it "should be able to properly setup class options" do
17
+ @server.opts.should == @opts
18
+ end
19
+ end
20
+
21
+ it "should retain the supplied token" do
22
+ @server.token.should == @opts[:token]
23
+ end
24
+
25
+ it "should have a Logger" do
26
+ @server.logger.class.should == ::Logger
27
+ end
28
+
29
+ describe "#alive?" do
30
+ subject { @server.alive? }
31
+ it { should == true }
32
+ end
33
+
34
+ describe "#async?" do
35
+
36
+ it "should return true for async methods" do
37
+ @server.async?( 'test', 'async_foo' ).should be_true
38
+ end
39
+
40
+ it "should return false for sync methods" do
41
+ @server.async?( 'test', 'foo' ).should be_false
42
+ end
43
+ end
44
+
45
+ describe "#async_check" do
46
+
47
+ it "should return true for async methods" do
48
+ @server.async_check( Test.new.method( :async_foo ) ).should be_true
49
+ end
50
+
51
+ it "should return false for sync methods" do
52
+ @server.async_check( Test.new.method( :foo ) ).should be_false
53
+ end
54
+ end
55
+
56
+ describe "#object_exist?" do
57
+
58
+ it "should return true for valid objects" do
59
+ @server.object_exist?( 'test' ).should be_true
60
+ end
61
+
62
+ it "should return false for inexistent objects" do
63
+ @server.object_exist?( 'foo' ).should be_false
64
+ end
65
+ end
66
+
67
+ describe "#public_method?" do
68
+
69
+ it "should return true for public methods" do
70
+ @server.public_method?( 'test', 'foo' ).should be_true
71
+ end
72
+
73
+ it "should return false for inexistent or non-public methods" do
74
+ @server.public_method?( 'test', 'bar' ).should be_false
75
+ end
76
+ end
77
+
78
+ end
79
+