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.
@@ -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