toq 0.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.
@@ -0,0 +1,152 @@
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
5
+ web site for more information on licensing and terms of use.
6
+
7
+ =end
8
+
9
+ # RPC Exceptions have methods that help identify them based on type.
10
+ #
11
+ # So in order to allow evaluations like:
12
+ #
13
+ # my_object.rpc_connection_error?
14
+ #
15
+ # to be possible on all objects these helper methods need to be available for
16
+ # all objects.
17
+ #
18
+ # By default they'll return false, individual RPC Exceptions will overwrite them
19
+ # to return true when applicable.
20
+ #
21
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
22
+ class Object
23
+
24
+ # @return [Bool] false
25
+ def rpc_connection_error?
26
+ false
27
+ end
28
+
29
+ # @return [Bool] false
30
+ def rpc_remote_exception?
31
+ false
32
+ end
33
+
34
+ # @return [Bool] false
35
+ def rpc_invalid_object_error?
36
+ false
37
+ end
38
+
39
+ # @return [Bool] false
40
+ def rpc_invalid_method_error?
41
+ false
42
+ end
43
+
44
+ # @return [Bool] false
45
+ def rpc_invalid_token_error?
46
+ false
47
+ end
48
+
49
+ # @return [Bool] true
50
+ def rpc_ssl_error?
51
+ false
52
+ end
53
+
54
+ # @return [Bool] false
55
+ def rpc_exception?
56
+ false
57
+ end
58
+
59
+ end
60
+
61
+ module Toq
62
+ module Exceptions
63
+
64
+ # Returns an exception based on the response object.
65
+ #
66
+ # @param [Toq::Response] response
67
+ #
68
+ # @return [Exception]
69
+ def self.from_response( response )
70
+ exception = response.exception
71
+ klass = Toq::Exceptions.const_get( exception['type'].to_sym )
72
+ e = klass.new( exception['message'] )
73
+ e.set_backtrace( exception['backtrace'] )
74
+ e
75
+ end
76
+
77
+ class Base < ::RuntimeError
78
+
79
+ # @return [Bool] true
80
+ def rpc_exception?
81
+ true
82
+ end
83
+ end
84
+
85
+ # Signifies an abruptly terminated connection.
86
+ #
87
+ # Look for network or SSL errors or a dead server or a mistyped server address/port.
88
+ class ConnectionError < Base
89
+
90
+ # @return [Bool] true
91
+ def rpc_connection_error?
92
+ true
93
+ end
94
+ end
95
+
96
+ # Signifies an exception that occurred on the server-side.
97
+ #
98
+ # Look errors on the remote method and review the server output for more details.
99
+ class RemoteException < Base
100
+
101
+ # @return [Bool] true
102
+ def rpc_remote_exception?
103
+ true
104
+ end
105
+ end
106
+
107
+ # An invalid object has been called.
108
+ #
109
+ # Make sure that there is a server-side handler for the object you called.
110
+ class InvalidObject < Base
111
+
112
+ # @return [Bool] true
113
+ def rpc_invalid_object_error?
114
+ true
115
+ end
116
+
117
+ end
118
+
119
+ # An invalid method has been called.
120
+ #
121
+ # Occurs when a remote method doesn't exist or isn't public.
122
+ class InvalidMethod < Base
123
+
124
+ # @return [Bool] true
125
+ def rpc_invalid_method_error?
126
+ true
127
+ end
128
+
129
+ end
130
+
131
+ # Signifies an authentication token mismatch between the client and the server.
132
+ class InvalidToken < Base
133
+
134
+ # @return [Bool] true
135
+ def rpc_invalid_token_error?
136
+ true
137
+ end
138
+
139
+ end
140
+
141
+ # Signifies an authentication token mismatch between the client and the server.
142
+ class SSLPeerVerificationFailed < ConnectionError
143
+
144
+ # @return [Bool] true
145
+ def rpc_ssl_error?
146
+ true
147
+ end
148
+
149
+ end
150
+
151
+ end
152
+ end
@@ -0,0 +1,63 @@
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
5
+ web site for more information on licensing and terms of use.
6
+
7
+ =end
8
+
9
+ module Toq
10
+
11
+ # Represents an RPC message, serves as the basis for {Request} and {Response}.
12
+ #
13
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
14
+ class Message
15
+
16
+ # @param [Hash] opts
17
+ # Sets instance attributes.
18
+ def initialize( opts = {} )
19
+ opts.each_pair { |k, v| send( "#{k}=".to_sym, v ) }
20
+ end
21
+
22
+ # Merges the attributes of another message with self.
23
+ #
24
+ # (The param doesn't *really* have to be a message, any object will do.)
25
+ #
26
+ # @param [Message] message
27
+ def merge!( message )
28
+ message.instance_variables.each do |var|
29
+ val = message.instance_variable_get( var )
30
+ instance_variable_set( var, val )
31
+ end
32
+ end
33
+
34
+ # Prepares the message for transmission (i.e. converts the message to a `Hash`).
35
+ #
36
+ # Attributes that should not be included can be skipped by implementing
37
+ # {#transmit?} and returning the appropriate value.
38
+ #
39
+ # @return [Hash]
40
+ def prepare_for_tx
41
+ instance_variables.inject({}) do |h, k|
42
+ h[normalize( k )] = instance_variable_get( k ) if transmit?( k )
43
+ h
44
+ end
45
+ end
46
+
47
+ # Decides which attributes should be skipped by {#prepare_for_tx}.
48
+ #
49
+ # @param [Symbol] attr
50
+ # Instance variable symbol (i.e. `:@token`).
51
+ def transmit?( attr )
52
+ true
53
+ end
54
+
55
+ private
56
+
57
+ def normalize( attr )
58
+ attr.to_s.gsub( '@', '' )
59
+ end
60
+
61
+ end
62
+
63
+ end
@@ -0,0 +1,101 @@
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 Toq
10
+
11
+ # Provides helper transport methods for {Message} transmission.
12
+ #
13
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
14
+ module Protocol
15
+ include Raktr::Connection::TLS
16
+
17
+ # Initializes an SSL session once the connection has been established and
18
+ # sets {#status} to `:ready`.
19
+ #
20
+ # @private
21
+ def on_connect
22
+ start_tls(
23
+ ca: @opts[:ssl_ca],
24
+ private_key: @opts[:ssl_pkey],
25
+ certificate: @opts[:ssl_cert]
26
+ )
27
+
28
+ @status = :ready
29
+ end
30
+
31
+ # @param [Message] msg
32
+ # Message to send to the peer.
33
+ def send_message( msg )
34
+ send_object( msg.prepare_for_tx )
35
+ end
36
+ alias :send_request :send_message
37
+ alias :send_response :send_message
38
+
39
+ # Receives data from the network.
40
+ #
41
+ # Rhe data will be chunks of a serialized object which will be buffered
42
+ # until the whole transmission has finished.
43
+ #
44
+ # It will then unserialize it and pass it to {#receive_object}.
45
+ def on_read( data )
46
+ (@buf ||= '') << data
47
+
48
+ while @buf.size >= 4
49
+ if @buf.size >= 4 + ( size = @buf.unpack( 'N' ).first )
50
+ @buf.slice!( 0, 4 )
51
+ receive_object( unserialize( @buf.slice!( 0, size ) ) )
52
+ else
53
+ break
54
+ end
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ # Stub method, should be implemented by servers.
61
+ #
62
+ # @param [Request] request
63
+ # @abstract
64
+ def receive_request( request )
65
+ p request
66
+ end
67
+
68
+ # Stub method, should be implemented by clients.
69
+ #
70
+ # @param [Response] response
71
+ # @abstract
72
+ def receive_response( response )
73
+ p response
74
+ end
75
+
76
+ # Object to send.
77
+ def send_object( obj )
78
+ data = serialize( obj )
79
+ write [data.bytesize, data].pack( 'Na*' )
80
+ end
81
+
82
+ # Returns the preferred serializer based on the `serializer` option of the
83
+ # server.
84
+ #
85
+ # @return [.load, .dump]
86
+ # Serializer to be used (Defaults to `YAML`).
87
+ def serializer
88
+ @opts[:serializer] || YAML
89
+ end
90
+
91
+ def serialize( obj )
92
+ serializer.dump obj
93
+ end
94
+
95
+ def unserialize( obj )
96
+ serializer.load( obj )
97
+ end
98
+
99
+ end
100
+
101
+ end
data/lib/toq/proxy.rb ADDED
@@ -0,0 +1,84 @@
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
5
+ web site for more information on licensing and terms of use.
6
+
7
+ =end
8
+
9
+ module Toq
10
+
11
+ # Maps the methods of remote objects to local ones.
12
+ #
13
+ # You start like:
14
+ #
15
+ # client = Toq::Client.new( host: 'localhost', port: 7331 )
16
+ # bench = Toq::Proxy.new( client, 'bench' )
17
+ #
18
+ # And it allows you to do this:
19
+ #
20
+ # result = bench.foo( 1, 2, 3 )
21
+ #
22
+ # Instead of:
23
+ #
24
+ # result = client.call( 'bench.foo', 1, 2, 3 )
25
+ #
26
+ # The server on the other end must have an appropriate handler set, like:
27
+ #
28
+ # class Bench
29
+ # def foo( i = 0 )
30
+ # return i
31
+ # end
32
+ # end
33
+ #
34
+ # server = Toq::Server.new( host: 'localhost', port: 7331 )
35
+ # server.add_handler( 'bench', Bench.new )
36
+ #
37
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
38
+ class Proxy
39
+
40
+ class <<self
41
+
42
+ # @param [Symbol] method_name
43
+ # Method whose response to translate.
44
+ # @param [Block] translator
45
+ # Block to be passed the response and return a translated object.
46
+ def translate( method_name, &translator )
47
+ define_method method_name do |*args, &b|
48
+ # For blocking calls.
49
+ if !b
50
+ data = forward( method_name, *args )
51
+ return data.rpc_exception? ?
52
+ data : translator.call( data, *args )
53
+ end
54
+
55
+ # For non-blocking calls.
56
+ forward( method_name, *args ) do |data|
57
+ b.call( data.rpc_exception? ?
58
+ data : translator.call( data, *args ) )
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ # @param [Client] client
65
+ # @param [String] handler
66
+ def initialize( client, handler )
67
+ @client = client
68
+ @handler = handler
69
+ end
70
+
71
+ def forward( sym, *args, &block )
72
+ @client.call( "#{@handler}.#{sym.to_s}", *args, &block )
73
+ end
74
+
75
+ private
76
+
77
+ # Used to provide the illusion of locality for remote methods.
78
+ def method_missing( *args, &block )
79
+ forward( *args, &block )
80
+ end
81
+
82
+ end
83
+
84
+ end
@@ -0,0 +1,59 @@
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
5
+ web site for more information on licensing and terms of use.
6
+
7
+ =end
8
+
9
+ require_relative 'message'
10
+
11
+ module Toq
12
+
13
+ # Represents an RPC request.
14
+ #
15
+ # It's here only for formalization purposes, it's not actually sent over the wire.
16
+ #
17
+ # What is sent is a hash generated by {#prepare_for_tx}. which is in the form of:
18
+ #
19
+ #
20
+ # {
21
+ # # RPC message in the form of 'handler.method'.
22
+ # 'message' => msg,
23
+ # # Optional array of arguments for the remote method.
24
+ # 'args' => args,
25
+ # # Optional authentication token.
26
+ # 'token' => token
27
+ # }
28
+ #
29
+ # Any client that has SSL support and can serialize a Hash just like the one
30
+ # above can communicate with the RPC server.
31
+ #
32
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
33
+ class Request < Message
34
+
35
+ # @return [String]
36
+ # RPC message in the form of 'handler.method'.
37
+ attr_accessor :message
38
+
39
+ # @return [Array]
40
+ # Optional arguments for the remote method.
41
+ attr_accessor :args
42
+
43
+ # @return [String]
44
+ # Optional authentication token.
45
+ attr_accessor :token
46
+
47
+ # @return [Proc]
48
+ # Callback to be invoked on the response.
49
+ attr_accessor :callback
50
+
51
+ private
52
+
53
+ def transmit?( attr )
54
+ ![ :@callback ].include?( attr )
55
+ end
56
+
57
+ end
58
+
59
+ end
@@ -0,0 +1,63 @@
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
5
+ web site for more information on licensing and terms of use.
6
+
7
+ =end
8
+
9
+ module Toq
10
+
11
+ # Represents an RPC response.
12
+ #
13
+ # It's here only for formalization purposes, it's not actually sent over the wire.
14
+ #
15
+ # What is sent is a hash generated by {#prepare_for_tx} which is in the form of:
16
+ #
17
+ # {
18
+ # # result of the RPC call
19
+ # 'obj' => object
20
+ # }
21
+ #
22
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
23
+ class Response < Message
24
+
25
+ # @return [Object]
26
+ # Return object of the {Request#message}.
27
+ attr_accessor :obj
28
+
29
+ # @return [Hash]
30
+ #
31
+ # {
32
+ # "name" => "Trying to access non-existent object 'blah'.",
33
+ # "backtrace" => [
34
+ # [0] "/home/zapotek/workspace/arachni-rpc/lib/toq/server.rb:285:in `call'",
35
+ # [1] "/home/zapotek/workspace/arachni-rpc/lib/toq/server.rb:85:in `block in receive_object'",
36
+ # ],
37
+ # "type" => "InvalidObject"
38
+ # }
39
+ #
40
+ # For all available exception types look at {Exceptions}.
41
+ attr_accessor :exception
42
+
43
+ def exception?
44
+ !!exception
45
+ end
46
+
47
+ def async?
48
+ !!@async
49
+ end
50
+
51
+ def async!
52
+ @async = true
53
+ end
54
+
55
+ private
56
+
57
+ def transmit?( attr )
58
+ ![:@async].include?( attr )
59
+ end
60
+
61
+ end
62
+
63
+ end
@@ -0,0 +1,144 @@
1
+ =begin
2
+
3
+ This file is part of the Arachni-RPC EM 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 Toq
10
+ class Server
11
+
12
+ # Receives {Request} objects and transmits {Response} objects.
13
+ #
14
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
15
+ class Handler < Raktr::Connection
16
+ include Raktr::Connection::PeerInfo
17
+ include Toq::Protocol
18
+
19
+ # @return [Request]
20
+ # Working RPC request.
21
+ attr_reader :request
22
+
23
+ # @param [Server] server
24
+ # RPC server.
25
+ def initialize( server )
26
+ @server = server
27
+ @opts = server.opts.dup
28
+ @request = nil
29
+ end
30
+
31
+ # Handles closed connections and cleans up the SSL session.
32
+ #
33
+ # @private
34
+ def on_close( _ )
35
+ @server = nil
36
+ end
37
+
38
+ # * Handles {Request}
39
+ # * Sets the {#request}.
40
+ # * Sends back {Response}.
41
+ #
42
+ # @param [Request] req
43
+ def receive_request( req )
44
+ @request = req
45
+
46
+ # Create an empty response to be filled in little by little.
47
+ res = Response.new
48
+ peer = peer_ip_address
49
+
50
+ begin
51
+ # Make sure the client is allowed to make RPC calls.
52
+ authenticate!
53
+
54
+ # Grab the partially filled in response which includes the result
55
+ # of the RPC call and merge it with out prepared response.
56
+ res.merge!( @server.call( self ) )
57
+
58
+ # Handle exceptions and convert them to a simple hash, ready to be
59
+ # passed to the client.
60
+ rescue Exception => e
61
+ type = ''
62
+
63
+ # If it's an RPC exception pass the type along as is...
64
+ if e.rpc_exception?
65
+ type = e.class.name.split( ':' )[-1]
66
+
67
+ # ...otherwise set it to a RemoteException.
68
+ else
69
+ type = 'RemoteException'
70
+ end
71
+
72
+ # RPC conventions for exception transmission.
73
+ res.exception = {
74
+ 'type' => type,
75
+ 'message' => e.to_s,
76
+ 'backtrace' => e.backtrace
77
+ }
78
+
79
+ msg = "#{e.to_s}\n#{e.backtrace.join( "\n" )}"
80
+ @server.logger.error( 'Exception' ){ msg + " [on behalf of #{peer}]" }
81
+ end
82
+
83
+ # Pass the result of the RPC call back to the client but *only* if it
84
+ # wasn't async, otherwise {Server#call} will have already taken care of it.
85
+ send_response( res ) if !res.async?
86
+ end
87
+
88
+ private
89
+
90
+ # Converts incoming hash objects to {Request} objects and calls
91
+ # {#receive_request}.
92
+ #
93
+ # @param [Hash] obj
94
+ def receive_object( obj )
95
+ receive_request( Request.new( obj ) )
96
+ end
97
+
98
+ # @param [Symbol] severity
99
+ #
100
+ # Severity of the logged event:
101
+ #
102
+ # * `:debug`
103
+ # * `:info`
104
+ # * `:warn`
105
+ # * `:error`
106
+ # * `:fatal`
107
+ # * `:unknown`
108
+ #
109
+ # @param [String] category
110
+ # Category of message (SSL, Call, etc.).
111
+ # @param [String] msg
112
+ # Message to log.
113
+ def log( severity, category, msg )
114
+ sev_sym = Logger.const_get( severity.to_s.upcase.to_sym )
115
+ @server.logger.add( sev_sym, msg, category )
116
+ end
117
+
118
+ # Authenticates the client based on the token in the request.
119
+ #
120
+ # It will raise an exception if the token doesn't check-out.
121
+ def authenticate!
122
+ return if valid_token?( @request.token )
123
+
124
+ msg = "Token missing or invalid while calling: #{@request.message}"
125
+
126
+ @server.logger.error( 'Authenticator' ){
127
+ msg + " [on behalf of #{peer_ip_address}]"
128
+ }
129
+
130
+ fail Exceptions::InvalidToken.new( msg )
131
+ end
132
+
133
+ # Compares the authentication token in the param with the one of the server.
134
+ #
135
+ # @param [String] token
136
+ #
137
+ # @return [Bool]
138
+ def valid_token?( token )
139
+ token == @server.token
140
+ end
141
+
142
+ end
143
+ end
144
+ end