toq 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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