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,167 @@
|
|
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 EM
|
5
|
+
web site for more information on licensing and terms of use.
|
6
|
+
|
7
|
+
=end
|
8
|
+
|
9
|
+
module Arachni
|
10
|
+
module RPC
|
11
|
+
class Client
|
12
|
+
|
13
|
+
# Transmits {Request} objects and calls callbacks once an {Response} is received.
|
14
|
+
#
|
15
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
|
16
|
+
class Handler < Reactor::Connection
|
17
|
+
include Protocol
|
18
|
+
|
19
|
+
# Default amount of tries for failed requests.
|
20
|
+
DEFAULT_TRIES = 9
|
21
|
+
|
22
|
+
# @return [Symbol] Status of the connection, can be:
|
23
|
+
#
|
24
|
+
# * `:idle` -- Just initialized.
|
25
|
+
# * `:ready` -- A connection has been established.
|
26
|
+
# * `:pending` -- Sending request and awaiting response.
|
27
|
+
# * `:done` -- Response received and callback invoked -- ready to be reused.
|
28
|
+
# * `:closed` -- Connection closed.
|
29
|
+
attr_reader :status
|
30
|
+
|
31
|
+
# @return [Exceptions::ConnectionError]
|
32
|
+
attr_reader :error
|
33
|
+
|
34
|
+
# Prepares an RPC connection and sets {#status} to `:idle`.
|
35
|
+
#
|
36
|
+
# @param [Hash] opts
|
37
|
+
# @option opts [Integer] :max_retries (9)
|
38
|
+
# Default amount of tries for failed requests.
|
39
|
+
#
|
40
|
+
# @option opts [Client] :base
|
41
|
+
# Client instance needed to {Client#push_connection push} ourselves
|
42
|
+
# back to its connection pool once we're done and we're ready to be reused.
|
43
|
+
def initialize( opts )
|
44
|
+
@opts = opts.dup
|
45
|
+
|
46
|
+
@max_retries = @opts[:max_retries] || DEFAULT_TRIES
|
47
|
+
@client = @opts[:client]
|
48
|
+
|
49
|
+
@opts[:tries] ||= 0
|
50
|
+
@tries ||= @opts[:tries]
|
51
|
+
|
52
|
+
@status = :idle
|
53
|
+
@request = nil
|
54
|
+
end
|
55
|
+
|
56
|
+
# Sends an RPC request (i.e. performs an RPC call) and sets {#status}
|
57
|
+
# to `:pending`.
|
58
|
+
#
|
59
|
+
# @param [Request] req
|
60
|
+
def send_request( req )
|
61
|
+
@request = req
|
62
|
+
@status = :pending
|
63
|
+
super( req )
|
64
|
+
end
|
65
|
+
|
66
|
+
# @note Pushes itself to the client's connection pool to be re-used.
|
67
|
+
#
|
68
|
+
# Handles responses to RPC requests, calls its callback and sets {#status}
|
69
|
+
# to `:done`.
|
70
|
+
#
|
71
|
+
# @param [Arachni::RPC::Response] res
|
72
|
+
def receive_response( res )
|
73
|
+
if res.exception?
|
74
|
+
res.obj = Exceptions.from_response( res )
|
75
|
+
end
|
76
|
+
|
77
|
+
@request.callback.call( res.obj ) if @request.callback
|
78
|
+
ensure
|
79
|
+
@request = nil # Help the GC out.
|
80
|
+
@error = nil # Help the GC out.
|
81
|
+
@status = :done
|
82
|
+
|
83
|
+
@opts[:tries] = @tries = 0
|
84
|
+
@client.push_connection self
|
85
|
+
end
|
86
|
+
|
87
|
+
# Handles closed connections, cleans up the SSL session, retries (if
|
88
|
+
# necessary) and sets {#status} to `:closed`.
|
89
|
+
#
|
90
|
+
# @private
|
91
|
+
def on_close( reason )
|
92
|
+
if @request
|
93
|
+
# If there is a request and a callback and the callback hasn't yet be
|
94
|
+
# called (i.e. not done) then we got here by error so retry.
|
95
|
+
if @request && @request.callback && !done?
|
96
|
+
if retry?
|
97
|
+
retry_request
|
98
|
+
else
|
99
|
+
@error = e = Exceptions::ConnectionError.new( "Connection closed [#{reason}]" )
|
100
|
+
@request.callback.call( e )
|
101
|
+
@client.connection_failed self
|
102
|
+
end
|
103
|
+
|
104
|
+
return
|
105
|
+
end
|
106
|
+
else
|
107
|
+
@error = reason
|
108
|
+
@client.connection_failed self
|
109
|
+
end
|
110
|
+
|
111
|
+
close_without_retry
|
112
|
+
end
|
113
|
+
|
114
|
+
# @note If `true`, the connection can be re-used.
|
115
|
+
#
|
116
|
+
# @return [Boolean]
|
117
|
+
# `true` when the connection is done, `false` otherwise.
|
118
|
+
def done?
|
119
|
+
@status == :done
|
120
|
+
end
|
121
|
+
|
122
|
+
# Closes the connection without triggering a retry operation and sets
|
123
|
+
# {#status} to `:closed`.
|
124
|
+
def close_without_retry
|
125
|
+
@request = nil
|
126
|
+
@status = :closed
|
127
|
+
close_without_callback
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
# Converts incoming hash objects to {Response} objects and calls
|
133
|
+
# {#receive_response}.
|
134
|
+
#
|
135
|
+
# @param [Hash] obj
|
136
|
+
def receive_object( obj )
|
137
|
+
receive_response( Response.new( obj ) )
|
138
|
+
end
|
139
|
+
|
140
|
+
def retry_request
|
141
|
+
opts = @opts.dup
|
142
|
+
opts[:tries] += 1
|
143
|
+
|
144
|
+
req = @request.dup
|
145
|
+
|
146
|
+
# The connection will be detached soon, keep a separate reference to
|
147
|
+
# the reactor.
|
148
|
+
reactor = @reactor
|
149
|
+
|
150
|
+
@tries += 1
|
151
|
+
reactor.delay( 0.2 ) do
|
152
|
+
address = opts[:socket] ? opts[:socket] : [opts[:host], opts[:port]]
|
153
|
+
reactor.connect( *[address, self.class, opts ].flatten ).send_request( req )
|
154
|
+
end
|
155
|
+
|
156
|
+
close_without_retry
|
157
|
+
end
|
158
|
+
|
159
|
+
def retry?
|
160
|
+
@tries < @max_retries
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -6,7 +6,6 @@
|
|
6
6
|
|
7
7
|
=end
|
8
8
|
|
9
|
-
#
|
10
9
|
# RPC Exceptions have methods that help identify them based on type.
|
11
10
|
#
|
12
11
|
# So in order to allow evaluations like:
|
@@ -16,11 +15,10 @@
|
|
16
15
|
# to be possible on all objects these helper methods need to be available for
|
17
16
|
# all objects.
|
18
17
|
#
|
19
|
-
# By default they'll return false, individual RPC Exceptions will overwrite them
|
20
|
-
# return true when applicable.
|
21
|
-
#
|
22
|
-
# @author: Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
|
18
|
+
# By default they'll return false, individual RPC Exceptions will overwrite them
|
19
|
+
# to return true when applicable.
|
23
20
|
#
|
21
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
|
24
22
|
class Object
|
25
23
|
|
26
24
|
# @return [Bool] false
|
@@ -48,6 +46,11 @@ class Object
|
|
48
46
|
false
|
49
47
|
end
|
50
48
|
|
49
|
+
# @return [Bool] true
|
50
|
+
def rpc_ssl_error?
|
51
|
+
false
|
52
|
+
end
|
53
|
+
|
51
54
|
# @return [Bool] false
|
52
55
|
def rpc_exception?
|
53
56
|
false
|
@@ -59,114 +62,87 @@ module Arachni
|
|
59
62
|
module RPC
|
60
63
|
module Exceptions
|
61
64
|
|
62
|
-
#
|
63
65
|
# Returns an exception based on the response object.
|
64
66
|
#
|
65
67
|
# @param [Arachni::RPC::Response] response
|
66
68
|
#
|
67
69
|
# @return [Exception]
|
68
|
-
#
|
69
70
|
def self.from_response( response )
|
70
|
-
|
71
|
-
klass = Arachni::RPC::Exceptions.const_get(
|
72
|
-
e = klass.new(
|
73
|
-
e.set_backtrace(
|
71
|
+
exception = response.exception
|
72
|
+
klass = Arachni::RPC::Exceptions.const_get( exception['type'].to_sym )
|
73
|
+
e = klass.new( exception['message'] )
|
74
|
+
e.set_backtrace( exception['backtrace'] )
|
74
75
|
e
|
75
76
|
end
|
76
77
|
|
77
78
|
class Base < ::RuntimeError
|
78
|
-
|
79
|
+
|
79
80
|
# @return [Bool] true
|
80
|
-
#
|
81
81
|
def rpc_exception?
|
82
82
|
true
|
83
83
|
end
|
84
84
|
end
|
85
85
|
|
86
|
-
#
|
87
86
|
# Signifies an abruptly terminated connection.
|
88
87
|
#
|
89
88
|
# Look for network or SSL errors or a dead server or a mistyped server address/port.
|
90
|
-
#
|
91
89
|
class ConnectionError < Base
|
92
90
|
|
93
|
-
#
|
94
91
|
# @return [Bool] true
|
95
|
-
#
|
96
92
|
def rpc_connection_error?
|
97
93
|
true
|
98
94
|
end
|
99
95
|
end
|
100
96
|
|
101
|
-
#
|
102
97
|
# Signifies an exception that occurred on the server-side.
|
103
98
|
#
|
104
99
|
# Look errors on the remote method and review the server output for more details.
|
105
|
-
#
|
106
100
|
class RemoteException < Base
|
107
101
|
|
108
|
-
#
|
109
102
|
# @return [Bool] true
|
110
|
-
#
|
111
103
|
def rpc_remote_exception?
|
112
104
|
true
|
113
105
|
end
|
114
106
|
end
|
115
107
|
|
116
|
-
#
|
117
108
|
# An invalid object has been called.
|
118
109
|
#
|
119
110
|
# Make sure that there is a server-side handler for the object you called.
|
120
|
-
#
|
121
111
|
class InvalidObject < Base
|
122
112
|
|
123
|
-
#
|
124
113
|
# @return [Bool] true
|
125
|
-
#
|
126
114
|
def rpc_invalid_object_error?
|
127
115
|
true
|
128
116
|
end
|
129
117
|
|
130
118
|
end
|
131
119
|
|
132
|
-
#
|
133
120
|
# An invalid method has been called.
|
134
121
|
#
|
135
122
|
# Occurs when a remote method doesn't exist or isn't public.
|
136
|
-
#
|
137
123
|
class InvalidMethod < Base
|
138
124
|
|
139
|
-
#
|
140
125
|
# @return [Bool] true
|
141
|
-
#
|
142
126
|
def rpc_invalid_method_error?
|
143
127
|
true
|
144
128
|
end
|
145
129
|
|
146
130
|
end
|
147
131
|
|
148
|
-
#
|
149
132
|
# Signifies an authentication token mismatch between the client and the server.
|
150
|
-
#
|
151
133
|
class InvalidToken < Base
|
152
134
|
|
153
|
-
#
|
154
135
|
# @return [Bool] true
|
155
|
-
#
|
156
136
|
def rpc_invalid_token_error?
|
157
137
|
true
|
158
138
|
end
|
159
139
|
|
160
140
|
end
|
161
141
|
|
162
|
-
#
|
163
142
|
# Signifies an authentication token mismatch between the client and the server.
|
164
|
-
|
165
|
-
class SSLPeerVerificationFailed < ConnectionError
|
143
|
+
class SSLPeerVerificationFailed < ConnectionError
|
166
144
|
|
167
|
-
#
|
168
145
|
# @return [Bool] true
|
169
|
-
#
|
170
146
|
def rpc_ssl_error?
|
171
147
|
true
|
172
148
|
end
|
data/lib/arachni/rpc/message.rb
CHANGED
@@ -9,27 +9,22 @@
|
|
9
9
|
module Arachni
|
10
10
|
module RPC
|
11
11
|
|
12
|
-
#
|
13
12
|
# Represents an RPC message, serves as the basis for {Request} and {Response}.
|
14
13
|
#
|
15
|
-
# @author
|
16
|
-
#
|
14
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
|
17
15
|
class Message
|
18
16
|
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
17
|
+
# @param [Hash] opts
|
18
|
+
# Sets instance attributes.
|
22
19
|
def initialize( opts = {} )
|
23
|
-
opts.each_pair { |k, v|
|
20
|
+
opts.each_pair { |k, v| send( "#{k}=".to_sym, v ) }
|
24
21
|
end
|
25
22
|
|
26
|
-
#
|
27
23
|
# Merges the attributes of another message with self.
|
28
24
|
#
|
29
25
|
# (The param doesn't *really* have to be a message, any object will do.)
|
30
26
|
#
|
31
27
|
# @param [Message] message
|
32
|
-
#
|
33
28
|
def merge!( message )
|
34
29
|
message.instance_variables.each do |var|
|
35
30
|
val = message.instance_variable_get( var )
|
@@ -37,14 +32,12 @@ class Message
|
|
37
32
|
end
|
38
33
|
end
|
39
34
|
|
40
|
-
#
|
41
|
-
# Prepares the message for transmission (i.e. converts the message to a Hash).
|
35
|
+
# Prepares the message for transmission (i.e. converts the message to a `Hash`).
|
42
36
|
#
|
43
37
|
# Attributes that should not be included can be skipped by implementing
|
44
38
|
# {#transmit?} and returning the appropriate value.
|
45
39
|
#
|
46
40
|
# @return [Hash]
|
47
|
-
#
|
48
41
|
def prepare_for_tx
|
49
42
|
instance_variables.inject({}) do |h, k|
|
50
43
|
h[normalize( k )] = instance_variable_get( k ) if transmit?( k )
|
@@ -52,11 +45,10 @@ class Message
|
|
52
45
|
end
|
53
46
|
end
|
54
47
|
|
55
|
-
#
|
56
48
|
# Decides which attributes should be skipped by {#prepare_for_tx}.
|
57
49
|
#
|
58
|
-
# @param [Symbol] attr
|
59
|
-
#
|
50
|
+
# @param [Symbol] attr
|
51
|
+
# Instance variable symbol (i.e. `:@token`).
|
60
52
|
def transmit?( attr )
|
61
53
|
true
|
62
54
|
end
|
@@ -0,0 +1,103 @@
|
|
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 EM
|
5
|
+
web site for more information on licensing and terms of use.
|
6
|
+
|
7
|
+
=end
|
8
|
+
|
9
|
+
module Arachni
|
10
|
+
module RPC
|
11
|
+
|
12
|
+
# Provides helper transport methods for {Message} transmission.
|
13
|
+
#
|
14
|
+
# @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
|
15
|
+
module Protocol
|
16
|
+
include Reactor::Connection::TLS
|
17
|
+
|
18
|
+
# Initializes an SSL session once the connection has been established and
|
19
|
+
# sets {#status} to `:ready`.
|
20
|
+
#
|
21
|
+
# @private
|
22
|
+
def on_connect
|
23
|
+
start_tls(
|
24
|
+
ca: @opts[:ssl_ca],
|
25
|
+
private_key: @opts[:ssl_pkey],
|
26
|
+
certificate: @opts[:ssl_cert]
|
27
|
+
)
|
28
|
+
|
29
|
+
@status = :ready
|
30
|
+
end
|
31
|
+
|
32
|
+
# @param [Message] msg
|
33
|
+
# Message to send to the peer.
|
34
|
+
def send_message( msg )
|
35
|
+
send_object( msg.prepare_for_tx )
|
36
|
+
end
|
37
|
+
alias :send_request :send_message
|
38
|
+
alias :send_response :send_message
|
39
|
+
|
40
|
+
# Receives data from the network.
|
41
|
+
#
|
42
|
+
# Rhe data will be chunks of a serialized object which will be buffered
|
43
|
+
# until the whole transmission has finished.
|
44
|
+
#
|
45
|
+
# It will then unserialize it and pass it to {#receive_object}.
|
46
|
+
def on_read( data )
|
47
|
+
(@buf ||= '') << data
|
48
|
+
|
49
|
+
while @buf.size >= 4
|
50
|
+
if @buf.size >= 4 + ( size = @buf.unpack( 'N' ).first )
|
51
|
+
@buf.slice!( 0, 4 )
|
52
|
+
receive_object( unserialize( @buf.slice!( 0, size ) ) )
|
53
|
+
else
|
54
|
+
break
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
# Stub method, should be implemented by servers.
|
62
|
+
#
|
63
|
+
# @param [Request] request
|
64
|
+
# @abstract
|
65
|
+
def receive_request( request )
|
66
|
+
p request
|
67
|
+
end
|
68
|
+
|
69
|
+
# Stub method, should be implemented by clients.
|
70
|
+
#
|
71
|
+
# @param [Response] response
|
72
|
+
# @abstract
|
73
|
+
def receive_response( response )
|
74
|
+
p response
|
75
|
+
end
|
76
|
+
|
77
|
+
# Object to send.
|
78
|
+
def send_object( obj )
|
79
|
+
data = serialize( obj )
|
80
|
+
write [data.bytesize, data].pack( 'Na*' )
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns the preferred serializer based on the `serializer` option of the
|
84
|
+
# server.
|
85
|
+
#
|
86
|
+
# @return [.load, .dump]
|
87
|
+
# Serializer to be used (Defaults to `YAML`).
|
88
|
+
def serializer
|
89
|
+
@opts[:serializer] || YAML
|
90
|
+
end
|
91
|
+
|
92
|
+
def serialize( obj )
|
93
|
+
serializer.dump obj
|
94
|
+
end
|
95
|
+
|
96
|
+
def unserialize( obj )
|
97
|
+
serializer.load( obj )
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|