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.
- data/CHANGELOG.md +0 -0
- data/LICENSE.md +341 -0
- data/README.md +86 -0
- data/Rakefile +74 -0
- data/examples/client.EM.run.rb +113 -0
- data/examples/client.rb +181 -0
- data/examples/server.rb +80 -0
- data/lib/arachni/rpc/em.rb +26 -0
- data/lib/arachni/rpc/em/client.rb +280 -0
- data/lib/arachni/rpc/em/connection_utilities.rb +44 -0
- data/lib/arachni/rpc/em/em.rb +105 -0
- data/lib/arachni/rpc/em/protocol.rb +160 -0
- data/lib/arachni/rpc/em/server.rb +421 -0
- data/lib/arachni/rpc/em/ssl.rb +180 -0
- data/lib/arachni/rpc/em/version.rb +17 -0
- data/spec/arachni/rpc/em/client_spec.rb +211 -0
- data/spec/arachni/rpc/em/em_spec.rb +4 -0
- data/spec/arachni/rpc/em/server_spec.rb +79 -0
- data/spec/arachni/rpc/em/ssl_spec.rb +68 -0
- data/spec/pems/cacert.pem +39 -0
- data/spec/pems/client/cert.pem +39 -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 +39 -0
- data/spec/pems/server/key.pem +51 -0
- data/spec/servers/basic.rb +3 -0
- data/spec/servers/server.rb +61 -0
- data/spec/servers/with_ssl_primitives.rb +11 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +46 -0
- metadata +134 -0
@@ -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,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
|
+
|