arachni-rpc 0.1.3 → 0.2.0
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.
- 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
|