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.
@@ -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 to
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
- obj = response.obj
71
- klass = Arachni::RPC::Exceptions.const_get( obj['type'].to_sym )
72
- e = klass.new( obj['exception'] )
73
- e.set_backtrace( obj['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
@@ -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: Tasos "Zapotek" Laskos <tasos.laskos@gmail.com>
16
- #
14
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
17
15
  class Message
18
16
 
19
- #
20
- # @param [Hash] opts sets instance attributes
21
- #
17
+ # @param [Hash] opts
18
+ # Sets instance attributes.
22
19
  def initialize( opts = {} )
23
- opts.each_pair { |k, v| instance_variable_set( "@#{k}".to_sym, 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 attribute symbol (i.e. :@token)
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