simplerpc 0.1.1 → 0.2.0b
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 +4 -4
- data/lib/simplerpc/client.rb +183 -50
- data/lib/simplerpc/encryption.rb +46 -0
- data/lib/simplerpc/server.rb +253 -60
- data/lib/simplerpc/socket_protocol.rb +87 -34
- data/lib/simplerpc.rb +21 -0
- metadata +9 -8
- data/lib/simplerpc/serialiser.rb +0 -67
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 297f5863f75ffee26a9d5a4d2be51bdf376f282e
|
4
|
+
data.tar.gz: 078eeb27ec40ff2202bb806a9a2a9469910b7284
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 437d37efe70e1f4f4d506c44e5ca388f71c7c71eb12590b12081ec8822ba5a325940fa527465d7eb7439be14adb174331068399134f683cfcd320ad58e5ca6df
|
7
|
+
data.tar.gz: 600fb27195f8788c62702167a50b3c82d3c0d95ec6523e8fd31247eaff889dc3188d7b4d2162de2bd92ef027ee5179b24de93bf8ccdd9105ef3691cd9fe6c17b
|
data/lib/simplerpc/client.rb
CHANGED
@@ -1,44 +1,133 @@
|
|
1
1
|
require 'socket' # Sockets are in standard library
|
2
|
-
require 'simplerpc/serialiser'
|
3
2
|
require 'simplerpc/socket_protocol'
|
4
3
|
|
5
4
|
module SimpleRPC
|
6
5
|
|
6
|
+
# Exception thrown when the client fails to connect.
|
7
|
+
class AuthenticationError < StandardError
|
8
|
+
end
|
9
|
+
|
7
10
|
# The SimpleRPC client connects to a server, either persistently on on-demand, and makes
|
8
11
|
# calls to its proxy object.
|
9
12
|
#
|
10
13
|
# Once created, you should be able to interact with the client as if it were the remote
|
11
|
-
# object.
|
14
|
+
# object, i.e.:
|
15
|
+
#
|
16
|
+
# c = SimpleRPC::Client.new {:hostname => '127.0.0.1', :port => 27045 }
|
17
|
+
# c.length # 2
|
18
|
+
# c.call(:dup) # ["thing", "thing2"]
|
19
|
+
# c.close
|
20
|
+
#
|
21
|
+
# == Making Requests
|
22
|
+
#
|
23
|
+
# Requests can be made on the client object as if it were local, and these will be
|
24
|
+
# proxied to the server. For methods that are clobbered locally (for example '.class',
|
25
|
+
# which will return 'SimpleRPC::Client', you may use #call to send this without local
|
26
|
+
# interaction:
|
27
|
+
#
|
28
|
+
# c.class # SimpleRPC::Client
|
29
|
+
# c.call(:class) # Array
|
30
|
+
#
|
31
|
+
# The client will throw network errors upon failure, (ERRno::... ), so be sure to catch
|
32
|
+
# these in your application.
|
33
|
+
#
|
34
|
+
# == Modes
|
35
|
+
#
|
36
|
+
# It is possible to use the client in two modes: _always-on_ and _connect-on-demand_.
|
37
|
+
# The former of these maintains a single socket to the server, and all requests are
|
38
|
+
# sent over this. Call #connect and #disconnect to control this connection.
|
39
|
+
#
|
40
|
+
# The latter establishes a connection when necessary. This mode is used whenever the
|
41
|
+
# client is not connected, so is a fallback if always-on fails. There is a small
|
42
|
+
# performance hit to reconnecting each time.
|
43
|
+
#
|
44
|
+
# == Serialisation Formats
|
45
|
+
#
|
46
|
+
# By default both client and server use Marshal. This has proven fast and general,
|
47
|
+
# and is capable of sending data directly over sockets.
|
48
|
+
#
|
49
|
+
# The serialiser also supports MessagePack (the msgpack gem), and this yields a small
|
50
|
+
# performance increase at the expense of generality (restrictions on data type).
|
51
|
+
#
|
52
|
+
# Note that JSON and YAML, though they support reading and writing to sockets, do not
|
53
|
+
# properly terminate their reads and cause the system to hang. These methods are
|
54
|
+
# both slow and limited by comparison anyway, and algorithms needed to support their
|
55
|
+
# use require relatively large memory usage. They may be supported in later versions.
|
56
|
+
#
|
57
|
+
# == Authentication
|
58
|
+
#
|
59
|
+
# Setting the :password and :secret options will cause the client to attempt auth
|
60
|
+
# on connection. If this process succeeds, the client will then proceed as before,
|
61
|
+
# else the server will forcibly close the socket. If :fast_auth is on this will cause
|
62
|
+
# some kind of random data loading exception from the serialiser. If :fast_auth is off (default),
|
63
|
+
# this will throw a SimpleRPC::AuthenticationError exception.
|
64
|
+
#
|
65
|
+
# Clients and servers do not tell one another to use auth (such a system would impact
|
66
|
+
# speed) so the results of using mismatched configurations are undefined.
|
67
|
+
#
|
68
|
+
# The auth process is simple and not particularly secure, but is designed to deter casual
|
69
|
+
# connections and attacks. It uses a password that is sent encrypted against a salt sent
|
70
|
+
# by the server to prevent replay attacks. If you want more reliable security, use an SSH tunnel.
|
71
|
+
#
|
72
|
+
# The performance impact of auth is small, and takes about the same time as a simple
|
73
|
+
# request. This can be mitigated by using always-on mode.
|
12
74
|
#
|
13
75
|
class Client
|
14
76
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
77
|
+
attr_reader :hostname, :port
|
78
|
+
attr_accessor :serialiser, :timeout, :fast_auth
|
79
|
+
attr_writer :password, :secret
|
80
|
+
|
81
|
+
# Create a new client for the network.
|
82
|
+
# Takes an options hash, in which :port is required:
|
83
|
+
#
|
84
|
+
# [:hostname] The hostname to connect to. Defaults to localhost
|
85
|
+
# [:port] The port to connect on. Required.
|
86
|
+
# [:serialiser] A class supporting #dump(object, io) and #load(IO), defaults to Marshal.
|
87
|
+
# I recommend using MessagePack if this is not fast enough
|
88
|
+
# [:timeout] Socket timeout in seconds.
|
89
|
+
# [:password] The password clients need to connect
|
90
|
+
# [:secret] The encryption key used during password authentication.
|
91
|
+
# Should be some long random string that matches the server's.
|
92
|
+
# [:fast_auth] Use a slightly faster auth system that is incapable of knowing if it has failed or not.
|
93
|
+
# By default this is off.
|
94
|
+
#
|
95
|
+
def initialize(opts = {})
|
96
|
+
|
97
|
+
# Connection details
|
98
|
+
@hostname = opts[:hostname] || '127.0.0.1'
|
99
|
+
@port = opts[:port]
|
100
|
+
raise "Port required" if not @port
|
101
|
+
@timeout = opts[:timeout]
|
102
|
+
|
103
|
+
# Serialiser.
|
104
|
+
@serialiser = opts[:serialiser] || Marshal
|
105
|
+
|
106
|
+
# Auth system
|
107
|
+
if opts[:password] and opts[:secret] then
|
108
|
+
require 'simplerpc/encryption'
|
109
|
+
@password = opts[:password]
|
110
|
+
@secret = opts[:secret]
|
111
|
+
|
112
|
+
# Check for return from auth?
|
113
|
+
@fast_auth = (opts[:fast_auth] == true)
|
114
|
+
end
|
28
115
|
|
29
116
|
@m = Mutex.new
|
30
117
|
end
|
31
118
|
|
32
|
-
# Connect to the server
|
33
|
-
#
|
119
|
+
# Connect to the server.
|
120
|
+
#
|
121
|
+
# Returns true if connected, or false if not.
|
122
|
+
#
|
123
|
+
# Note that this is only needed if using the client in always-on mode.
|
34
124
|
def connect
|
35
125
|
@m.synchronize{
|
36
126
|
_connect
|
37
127
|
}
|
38
128
|
end
|
39
129
|
|
40
|
-
# Disconnect from the server
|
41
|
-
# or do nothing if already disconnected
|
130
|
+
# Disconnect from the server.
|
42
131
|
def close
|
43
132
|
@m.synchronize{
|
44
133
|
_disconnect
|
@@ -51,74 +140,118 @@ module SimpleRPC
|
|
51
140
|
# Is the client currently connected?
|
52
141
|
def connected?
|
53
142
|
@m.synchronize{
|
54
|
-
|
143
|
+
_connected?
|
55
144
|
}
|
56
145
|
end
|
57
146
|
|
58
147
|
# Call a method that is otherwise clobbered
|
59
|
-
# by the client object
|
148
|
+
# by the client object, e.g.:
|
149
|
+
#
|
150
|
+
# client.call(:dup) # return a copy of the server object
|
151
|
+
#
|
60
152
|
def call(m, *args)
|
61
153
|
method_missing(m, *args)
|
62
154
|
end
|
63
155
|
|
64
|
-
# Calls RPC on the remote object
|
156
|
+
# Calls RPC on the remote object.
|
157
|
+
#
|
158
|
+
# You should not need to call this directly.
|
159
|
+
#
|
65
160
|
def method_missing(m, *args, &block)
|
66
|
-
|
161
|
+
|
67
162
|
result = nil
|
163
|
+
success = true
|
68
164
|
|
69
165
|
@m.synchronize{
|
70
|
-
already_connected =
|
71
|
-
|
166
|
+
already_connected = _connected?
|
167
|
+
if not already_connected
|
168
|
+
raise Errno::ECONNREFUSED, "Failed to connect" if not _connect
|
169
|
+
end
|
72
170
|
# send method name and arity
|
73
|
-
|
74
|
-
_send([m, args.length])
|
75
|
-
|
76
|
-
# receive yey/ney
|
77
|
-
valid_call = _recv
|
78
|
-
|
79
|
-
#puts "=--> #{valid_call}"
|
171
|
+
_send([m, args, already_connected])
|
80
172
|
|
81
173
|
# call with args
|
82
|
-
|
83
|
-
_send( args )
|
84
|
-
result = _recv
|
85
|
-
end
|
174
|
+
success, result = _recv
|
86
175
|
|
87
176
|
# Then d/c
|
88
177
|
_disconnect if not already_connected
|
89
178
|
}
|
90
179
|
|
91
|
-
#
|
92
|
-
|
93
|
-
|
94
|
-
end
|
95
|
-
|
180
|
+
# puts "[c] /calling #{m}..."
|
181
|
+
# If it didn't succeed, treat the payload as an exception
|
182
|
+
raise result if not success
|
96
183
|
return result
|
184
|
+
|
185
|
+
# rescue StandardError => e
|
186
|
+
# $stderr.puts "-> #{e}, #{e.backtrace.join("--")}"
|
187
|
+
# case e
|
188
|
+
# when Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::ETIMEDOUT
|
189
|
+
# c.close
|
190
|
+
# else
|
191
|
+
# raise e
|
192
|
+
# end
|
97
193
|
end
|
98
194
|
|
99
195
|
private
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
196
|
+
|
197
|
+
|
198
|
+
# -------------------------------------------------------------------------
|
199
|
+
# Send/Receive
|
200
|
+
#
|
105
201
|
|
106
202
|
# Receive data from the server
|
107
203
|
def _recv
|
108
|
-
|
204
|
+
SocketProtocol::Stream.recv( @s, @serialiser, @timeout )
|
109
205
|
end
|
110
206
|
|
111
207
|
# Send data to the server
|
112
208
|
def _send(obj)
|
113
|
-
SocketProtocol::send(@s, @serialiser
|
209
|
+
SocketProtocol::Stream.send( @s, obj, @serialiser, @timeout )
|
210
|
+
end
|
211
|
+
|
212
|
+
# -------------------------------------------------------------------------
|
213
|
+
# Socket management
|
214
|
+
#
|
215
|
+
|
216
|
+
# Connect to the server
|
217
|
+
def _connect
|
218
|
+
# Connect to the host
|
219
|
+
@s = Socket.tcp( @hostname, @port, nil, nil, :connect_timeout => @timeout )
|
220
|
+
|
221
|
+
# Disable Nagle's algorithm
|
222
|
+
@s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
223
|
+
|
224
|
+
# if auth is required
|
225
|
+
if @password and @secret
|
226
|
+
salt = SocketProtocol::Simple.recv( @s, @timeout )
|
227
|
+
challenge = Encryption.encrypt( @password, @secret, salt )
|
228
|
+
SocketProtocol::Simple.send( @s, challenge, @timeout )
|
229
|
+
if not @fast_auth
|
230
|
+
if SocketProtocol::Simple.recv( @s, @timeout ) != SocketProtocol::AUTH_SUCCESS
|
231
|
+
@s.close
|
232
|
+
raise AuthenticationError, "Authentication failed"
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# Check and raise
|
238
|
+
return _connected?
|
114
239
|
end
|
115
240
|
|
116
241
|
# Disconnect from the server
|
117
242
|
def _disconnect
|
118
|
-
return if not
|
119
|
-
|
243
|
+
return if not _connected?
|
244
|
+
|
245
|
+
# Then underlying socket
|
246
|
+
@s.close if @s and not @s.closed?
|
120
247
|
@s = nil
|
121
248
|
end
|
249
|
+
|
250
|
+
# Non-mutexed check for connectedness
|
251
|
+
def _connected?
|
252
|
+
@s and not @s.closed?
|
253
|
+
end
|
254
|
+
|
122
255
|
end
|
123
256
|
|
124
257
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
|
2
|
+
require 'openssl'
|
3
|
+
|
4
|
+
module SimpleRPC
|
5
|
+
|
6
|
+
# Handles openssl-based encryption of authentication details
|
7
|
+
#
|
8
|
+
# The auth system used is not terribly secure, but will guard against
|
9
|
+
# casual attackers. If you are particularly concerned, turn it off and
|
10
|
+
# use SSH tunnels.
|
11
|
+
#
|
12
|
+
module Encryption
|
13
|
+
|
14
|
+
# How strong to make the AES encryption
|
15
|
+
CIPHER_STRENGTH = 256
|
16
|
+
|
17
|
+
# Encrypt data
|
18
|
+
def self.encrypt( password, secret, salt )
|
19
|
+
# Encrypt with salted key
|
20
|
+
cipher = OpenSSL::Cipher::AES.new( CIPHER_STRENGTH, :CBC )
|
21
|
+
cipher.encrypt
|
22
|
+
cipher.key = salt_key( salt, secret )
|
23
|
+
return cipher.update(password) + cipher.final
|
24
|
+
rescue StandardError => e
|
25
|
+
return nil # Don't allow anyone to deliberately cause lockups
|
26
|
+
end
|
27
|
+
|
28
|
+
# Decrypt data
|
29
|
+
def self.decrypt( raw, secret, salt )
|
30
|
+
# Decrypt raw input
|
31
|
+
decipher = OpenSSL::Cipher::AES.new( CIPHER_STRENGTH, :CBC )
|
32
|
+
decipher.decrypt
|
33
|
+
decipher.key = salt_key( salt, secret )
|
34
|
+
return decipher.update(raw) + decipher.final
|
35
|
+
rescue StandardError => e
|
36
|
+
return nil # Don't allow anyone to deliberately cause lockups
|
37
|
+
end
|
38
|
+
|
39
|
+
# Salt a key by simply adding the two
|
40
|
+
# together
|
41
|
+
def self.salt_key( salt, key )
|
42
|
+
return salt + key
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
data/lib/simplerpc/server.rb
CHANGED
@@ -1,85 +1,175 @@
|
|
1
1
|
|
2
2
|
|
3
3
|
require 'socket' # Get sockets from stdlib
|
4
|
-
require 'simplerpc/serialiser'
|
5
4
|
require 'simplerpc/socket_protocol'
|
6
5
|
|
7
6
|
module SimpleRPC
|
8
7
|
|
8
|
+
|
9
9
|
# SimpleRPC's server. This wraps an object and exposes its methods to the network.
|
10
|
+
#
|
11
|
+
# i.e.:
|
12
|
+
#
|
13
|
+
# s = SimpleRPC::Server.new( ["thing", "thing2"], :port => 27045 )
|
14
|
+
# Thread.new(){ s.listen }
|
15
|
+
# sleep(10)
|
16
|
+
# s.close # Thread-safe
|
17
|
+
#
|
18
|
+
# == Controlling a Server
|
19
|
+
#
|
20
|
+
# The server is thread-safe, and is designed to be run in a thread when blocking on
|
21
|
+
# #listen --- calling #close on a listening server will cause the following chain of
|
22
|
+
# events:
|
23
|
+
#
|
24
|
+
# 1. The current client requests will end
|
25
|
+
# 2. The socket will close
|
26
|
+
# 3. #listen and #close will return (almost) simultaneously
|
27
|
+
#
|
28
|
+
# == Serialisation Formats
|
29
|
+
#
|
30
|
+
# By default both client and server use Marshal. This has proven fast and general,
|
31
|
+
# and is capable of sending data directly over sockets.
|
32
|
+
#
|
33
|
+
# The serialiser also supports MessagePack (the msgpack gem), and this yields a small
|
34
|
+
# performance increase at the expense of generality (restrictions on data type).
|
35
|
+
#
|
36
|
+
# Note that JSON and YAML, though they support reading and writing to sockets, do not
|
37
|
+
# properly terminate their reads and cause the system to hang. These methods are
|
38
|
+
# both slow and limited by comparison anyway, and algorithms needed to support their
|
39
|
+
# use require relatively large memory usage. They may be supported in later versions.
|
40
|
+
#
|
41
|
+
# == Authentication
|
42
|
+
#
|
43
|
+
# Setting the :password and :secret options will require authentication to connect.
|
44
|
+
#
|
45
|
+
# Clients and servers do not tell one another to use auth (such a system would impact
|
46
|
+
# speed) so the results of using mismatched configurations are undefined.
|
47
|
+
#
|
48
|
+
# The auth process is simple and not particularly secure, but is designed to deter casual
|
49
|
+
# connections and attacks. It uses a password that is sent encrypted against a salt sent
|
50
|
+
# by the server to prevent replay attacks. If you want more reliable security, use an SSH tunnel.
|
51
|
+
#
|
52
|
+
# The performance impact of auth is small, and takes about the same time as a simple
|
53
|
+
# request. This can be mitigated by using always-on mode.
|
54
|
+
#
|
10
55
|
class Server
|
11
56
|
|
12
|
-
|
57
|
+
attr_reader :hostname, :port, :obj, :threaded
|
58
|
+
attr_accessor :verbose_errors, :serialiser, :timeout, :fast_auth
|
59
|
+
attr_writer :password, :secret
|
60
|
+
|
61
|
+
# Create a new server for a given proxy object.
|
62
|
+
#
|
63
|
+
# The single required parameter, obj, is an object you wish to expose to the
|
64
|
+
# network. This is the API that will respond to RPCs.
|
65
|
+
#
|
66
|
+
# Takes an option hash with options:
|
67
|
+
#
|
68
|
+
# [:port] The port on which to listen, or 0 for the OS to set it
|
69
|
+
# [:hostname] The hostname of the interface to listen on (omit for all interfaces)
|
70
|
+
# [:serialiser] A class supporting #load(IO) and #dump(obj, IO) for serialisation.
|
71
|
+
# Defaults to Marshal. I recommend using MessagePack if this is not
|
72
|
+
# fast enough. Note that JSON/YAML do not work as they don't send
|
73
|
+
# terminating characters over the socket.
|
74
|
+
# [:verbose_errors] Report all socket errors from clients (by default these will be quashed).
|
75
|
+
# [:timeout] Socket timeout in seconds. Default is infinite (nil)
|
76
|
+
# [:threaded] Accept more than one client at once? Note that proxy object should be thread-safe for this
|
77
|
+
# Default is single-threaded mode.
|
78
|
+
# [:password] The password clients need to connect
|
79
|
+
# [:secret] The encryption key used during password authentication. Should be some long random string.
|
80
|
+
# [:salt_size] The size of the string used as a nonce during password auth. Defaults to 10 chars
|
81
|
+
# [:fast_auth] Use a slightly faster auth system that is incapable of knowing if it has failed or not.
|
82
|
+
# By default this is off.
|
13
83
|
#
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
# threaded:: Should the server support multiple clients at once?
|
19
|
-
# timeout:: Socket timeout
|
20
|
-
def initialize(obj, port, hostname=nil, serialiser=Serialiser.new, threaded=false, timeout=nil)
|
21
|
-
@obj = obj
|
22
|
-
@port = port
|
23
|
-
@hostname = hostname
|
84
|
+
def initialize(obj, opts = {})
|
85
|
+
@obj = obj
|
86
|
+
@port = opts[:port].to_i
|
87
|
+
@hostname = opts[:hostname]
|
24
88
|
|
25
89
|
# What format to use.
|
26
|
-
@serialiser
|
90
|
+
@serialiser = opts[:serialiser] || Marshal
|
27
91
|
|
28
|
-
# Silence errors?
|
29
|
-
@
|
92
|
+
# Silence errors coming from client connections?
|
93
|
+
@verbose_errors = (opts[:verbose_errors] == true)
|
94
|
+
@fast_auth = (opts[:fast_auth] == true)
|
30
95
|
|
31
96
|
# Should we shut down?
|
32
|
-
@close
|
97
|
+
@close = false
|
98
|
+
@close_in, @close_out = UNIXSocket.pair
|
33
99
|
|
34
100
|
# Connect/receive timeouts
|
35
|
-
@timeout
|
101
|
+
@timeout = opts[:timeout]
|
102
|
+
|
103
|
+
# Auth
|
104
|
+
if opts[:password] and opts[:secret] then
|
105
|
+
require 'simplerpc/encryption'
|
106
|
+
@password = opts[:password]
|
107
|
+
@secret = opts[:secret]
|
108
|
+
@salt_size = opts[:salt_size] || 10 # size of salt on key.
|
109
|
+
end
|
36
110
|
|
37
111
|
# Threaded or not?
|
38
|
-
@threaded
|
39
|
-
|
40
|
-
|
112
|
+
@threaded = (opts[:threaded] == true)
|
113
|
+
if(@threaded)
|
114
|
+
@clients = {}
|
115
|
+
@m = Mutex.new
|
116
|
+
end
|
41
117
|
end
|
42
118
|
|
43
|
-
# Start listening forever
|
119
|
+
# Start listening forever.
|
120
|
+
#
|
121
|
+
# Use threads and .close to stop the server.
|
122
|
+
#
|
44
123
|
def listen
|
45
124
|
# Listen on one interface only if hostname given
|
46
|
-
if @
|
47
|
-
@s = TCPServer.open( @hostname, @port )
|
48
|
-
else
|
49
|
-
@s = TCPServer.open(@port)
|
50
|
-
end
|
125
|
+
create_server_socket if not @s or @s.closed?
|
51
126
|
|
52
127
|
# Handle clients
|
53
128
|
loop{
|
54
129
|
|
55
130
|
begin
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
131
|
+
# Accept in an interruptable manner
|
132
|
+
if( c = interruptable_accept )
|
133
|
+
if @threaded
|
134
|
+
|
135
|
+
# Create the thread
|
136
|
+
id = rand.hash
|
137
|
+
thread = Thread.new(id, @m, c){|id, m, c|
|
138
|
+
handle_client(c)
|
139
|
+
|
140
|
+
m.synchronize{
|
141
|
+
@clients.delete(id)
|
142
|
+
}
|
143
|
+
}
|
62
144
|
|
63
|
-
#
|
64
|
-
m.synchronize{
|
65
|
-
@clients
|
145
|
+
# Add to the client list
|
146
|
+
@m.synchronize{
|
147
|
+
@clients[id] = thread
|
66
148
|
}
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
@m.synchronize{
|
72
|
-
@clients[id] = thread
|
73
|
-
}
|
74
|
-
else
|
75
|
-
handle_client(@s.accept)
|
149
|
+
else
|
150
|
+
# Handle client
|
151
|
+
handle_client(c)
|
152
|
+
end
|
76
153
|
end
|
77
154
|
rescue StandardError => e
|
78
|
-
raise e if
|
155
|
+
raise e if @verbose_errors
|
79
156
|
end
|
80
157
|
|
81
158
|
break if @close
|
82
159
|
}
|
160
|
+
|
161
|
+
# Wait for threads to end
|
162
|
+
if @threaded then
|
163
|
+
@clients.each{|id, thread|
|
164
|
+
thread.join
|
165
|
+
}
|
166
|
+
end
|
167
|
+
|
168
|
+
# Close socket
|
169
|
+
close_server_sockets
|
170
|
+
|
171
|
+
# Finally, say we've closed
|
172
|
+
@close = false if @close
|
83
173
|
end
|
84
174
|
|
85
175
|
# Return the number of active clients.
|
@@ -94,43 +184,146 @@ module SimpleRPC
|
|
94
184
|
def close
|
95
185
|
# Ask the loop to close
|
96
186
|
@close = true
|
187
|
+
@close_in.putc 'x' # Tell select to close
|
97
188
|
|
98
|
-
# Wait
|
99
|
-
|
100
|
-
|
101
|
-
thread.join
|
102
|
-
}
|
189
|
+
# Wait for loop to end
|
190
|
+
while(@close)
|
191
|
+
sleep(0.1)
|
103
192
|
end
|
104
193
|
end
|
105
194
|
|
106
195
|
private
|
196
|
+
|
197
|
+
# -------------------------------------------------------------------------
|
198
|
+
# Client Management
|
199
|
+
#
|
200
|
+
|
201
|
+
# Accept with the ability for other
|
202
|
+
# threads to call close
|
203
|
+
def interruptable_accept
|
204
|
+
c = IO.select([@s, @close_out], nil, nil)
|
205
|
+
|
206
|
+
# puts "--> #{c}"
|
207
|
+
|
208
|
+
return nil if not c
|
209
|
+
if(c[0][0] == @close_out)
|
210
|
+
# @close is set, so consume from socket
|
211
|
+
# and return nil
|
212
|
+
@close_out.getc
|
213
|
+
return nil
|
214
|
+
end
|
215
|
+
return @s.accept if( not @close and c )
|
216
|
+
rescue IOError => e
|
217
|
+
# cover 'closed stream' errors
|
218
|
+
return nil
|
219
|
+
end
|
220
|
+
|
107
221
|
# Handle the protocol for client c
|
108
222
|
def handle_client(c)
|
109
|
-
|
223
|
+
# Disable Nagle's algorithm
|
224
|
+
c.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
110
225
|
|
111
|
-
#
|
112
|
-
|
226
|
+
# Encrypted password auth
|
227
|
+
if @password and @secret
|
228
|
+
# Send challenge
|
229
|
+
# XXX: this is notably not crytographically random,
|
230
|
+
# but it's better than nothing against replay attacks
|
231
|
+
salt = Random.new.bytes( @salt_size )
|
232
|
+
SocketProtocol::Simple.send( c, salt, @timeout )
|
113
233
|
|
114
|
-
|
234
|
+
# Receive encrypted challenge
|
235
|
+
raw = SocketProtocol::Simple.recv( c, @timeout )
|
115
236
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
237
|
+
# D/c if failed
|
238
|
+
if Encryption.decrypt( raw, @secret, salt) != @password
|
239
|
+
SocketProtocol::Simple.send( c, SocketProtocol::AUTH_FAIL, @timeout ) if not @fast_auth
|
240
|
+
c.close
|
241
|
+
return
|
242
|
+
end
|
243
|
+
SocketProtocol::Simple.send( c, SocketProtocol::AUTH_SUCCESS, @timeout ) if not @fast_auth
|
120
244
|
end
|
121
245
|
|
246
|
+
# Handle requests
|
247
|
+
persist = true
|
248
|
+
while(not @close and persist) do
|
249
|
+
|
250
|
+
m, args, persist = recv(c)
|
251
|
+
# puts "Method: #{m}, args: #{args}, persist: #{persist}"
|
252
|
+
|
253
|
+
if(m and args) then
|
254
|
+
|
255
|
+
# Record success status
|
256
|
+
result = nil
|
257
|
+
success = true
|
258
|
+
|
259
|
+
# Try to make the call, catching exceptions
|
260
|
+
begin
|
261
|
+
result = @obj.send(m, *args)
|
262
|
+
rescue StandardError => se
|
263
|
+
result = se
|
264
|
+
success = false
|
265
|
+
end
|
266
|
+
|
267
|
+
# Send over the result
|
268
|
+
# puts "[s] sending result..."
|
269
|
+
send(c, [success, result] )
|
270
|
+
else
|
271
|
+
persist = false
|
272
|
+
end
|
273
|
+
|
274
|
+
end
|
275
|
+
|
276
|
+
# Close
|
122
277
|
c.close
|
278
|
+
rescue StandardError => e
|
279
|
+
case e
|
280
|
+
when Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::ETIMEDOUT
|
281
|
+
c.close
|
282
|
+
else
|
283
|
+
raise e
|
284
|
+
end
|
123
285
|
end
|
124
286
|
|
287
|
+
# -------------------------------------------------------------------------
|
288
|
+
# Send/Receive
|
289
|
+
#
|
290
|
+
|
125
291
|
# Receive data from a client
|
126
292
|
def recv(c)
|
127
|
-
|
293
|
+
SocketProtocol::Stream.recv( c, @serialiser, @timeout )
|
128
294
|
end
|
129
295
|
|
130
296
|
# Send data to a client
|
131
297
|
def send(c, obj)
|
132
|
-
SocketProtocol::send(c, @serialiser
|
298
|
+
SocketProtocol::Stream.send( c, obj, @serialiser, @timeout )
|
133
299
|
end
|
300
|
+
|
301
|
+
# -------------------------------------------------------------------------
|
302
|
+
# Socket Management
|
303
|
+
#
|
304
|
+
|
305
|
+
# Creates a new socket with the given timeout
|
306
|
+
def create_server_socket
|
307
|
+
if @hostname
|
308
|
+
@s = TCPServer.open( @hostname, @port )
|
309
|
+
else
|
310
|
+
@s = TCPServer.open( @port )
|
311
|
+
end
|
312
|
+
@port = @s.addr[1]
|
313
|
+
|
314
|
+
@s.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
|
315
|
+
end
|
316
|
+
|
317
|
+
# Close the server socket
|
318
|
+
def close_server_sockets
|
319
|
+
return if not @s
|
320
|
+
|
321
|
+
# Close underlying socket
|
322
|
+
@s.close if @s and not @s.closed?
|
323
|
+
@s = nil
|
324
|
+
end
|
325
|
+
|
326
|
+
|
134
327
|
end
|
135
328
|
|
136
329
|
|
@@ -1,42 +1,95 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
1
|
+
|
2
|
+
|
3
|
+
module SimpleRPC
|
4
|
+
|
5
|
+
|
6
|
+
# SocketProtocol defines common low-level aspects of data transfer
|
7
|
+
# between client and server.
|
8
|
+
#
|
9
|
+
# In normal use you can safely ignore this class and simply use Client
|
10
|
+
# and Server.
|
11
|
+
#
|
12
|
+
module SocketProtocol
|
13
|
+
|
14
|
+
# Sent when auth succeeds
|
15
|
+
AUTH_SUCCESS = 'C'
|
16
|
+
|
17
|
+
# Sent when auth fails
|
18
|
+
AUTH_FAIL = 'F'
|
19
|
+
|
20
|
+
# Send objects by streaming them through a socket using
|
21
|
+
# a serialiser such as Marshal.
|
22
|
+
#
|
23
|
+
# Fast and with low memory requirements, but is inherently
|
24
|
+
# unsafe (arbitrary code execution) and doesn't work with
|
25
|
+
# some serialisers.
|
26
|
+
#
|
27
|
+
# SimpleRPC uses this library for calls, and uses SocketProtocol::Simple
|
28
|
+
# for auth challenges (since it is safer)
|
29
|
+
#
|
30
|
+
module Stream
|
31
|
+
|
32
|
+
# Send using a serialiser writing through the socket
|
33
|
+
def self.send( s, obj, serialiser, timeout=nil )
|
34
|
+
raise Errno::ETIMEDOUT if not IO.select([], [s], [], timeout)
|
35
|
+
return serialiser.dump( obj, s )
|
17
36
|
end
|
18
|
-
|
19
|
-
#
|
37
|
+
|
38
|
+
# Recieve using a serialiser reading from the socket
|
39
|
+
def self.recv( s, serialiser, timeout=nil )
|
40
|
+
raise Errno::ETIMEDOUT if not IO.select([s], [], [], timeout)
|
41
|
+
return serialiser.load( s )
|
42
|
+
end
|
43
|
+
|
20
44
|
end
|
21
45
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
46
|
+
|
47
|
+
# Sends string buffers back and forth using a simple protocol.
|
48
|
+
#
|
49
|
+
# This method is significantly slower, but significantly more secure,
|
50
|
+
# than SocketProtocol::Stream, and is used for the auth handshake.
|
51
|
+
#
|
52
|
+
module Simple
|
53
|
+
|
54
|
+
# Send a buffer
|
55
|
+
def self.send( s, buf, timeout=nil )
|
56
|
+
# Dump into buffer
|
57
|
+
buflen = buf.length
|
58
|
+
|
59
|
+
# Send buffer length
|
60
|
+
raise Errno::ETIMEDOUT if not IO.select([], [s], [], timeout)
|
61
|
+
s.puts(buflen)
|
62
|
+
|
63
|
+
# Send buffer
|
64
|
+
sent = 0
|
65
|
+
while(sent < buflen and (x = IO.select([], [s], [], timeout))) do
|
66
|
+
sent += s.write( buf[sent..-1] )
|
67
|
+
end
|
68
|
+
raise Errno::ETIMEDOUT if not x
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
# Receive a buffer
|
73
|
+
def self.recv( s, timeout=nil )
|
74
|
+
raise Errno::ETIMEDOUT if not IO.select([s], [], [], timeout)
|
75
|
+
buflen = s.gets.to_s.chomp.to_i
|
76
|
+
|
77
|
+
return nil if buflen <= 0
|
78
|
+
|
79
|
+
buf = ""
|
80
|
+
recieved = 0
|
81
|
+
while( recieved < buflen and (x = IO.select([s], [], [], timeout)) ) do
|
82
|
+
str = s.read( buflen - recieved )
|
83
|
+
buf += str
|
84
|
+
recieved += str.length
|
85
|
+
end
|
86
|
+
raise Errno::ETIMEDOUT if not x
|
87
|
+
|
88
|
+
return buf
|
35
89
|
end
|
36
|
-
raise Timeout::TimeoutError if not x
|
37
90
|
|
38
|
-
return buf
|
39
|
-
#puts "[s] recv(#{buf})"
|
40
91
|
end
|
41
92
|
|
93
|
+
end
|
94
|
+
|
42
95
|
end
|
data/lib/simplerpc.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
require 'simplerpc/server'
|
3
|
+
require 'simplerpc/client'
|
4
|
+
require 'simplerpc/serialiser'
|
5
|
+
|
6
|
+
# SimpleRPC is a very simple RPC library for ruby, designed to be as fast as possible whilst still
|
7
|
+
# retaining a simple API.
|
8
|
+
#
|
9
|
+
# It connects a client to a server which wraps an object, exposing its API over the network socket.
|
10
|
+
# All data to/from the server is serialised using a serialisation object that is passed to the
|
11
|
+
# client/server.
|
12
|
+
#
|
13
|
+
# Author:: Stephen Wattam
|
14
|
+
#
|
15
|
+
# This module simply contains version information,
|
16
|
+
# and including it includes all other project files
|
17
|
+
module SimpleRPC
|
18
|
+
|
19
|
+
VERSION = "0.2.0b"
|
20
|
+
|
21
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: simplerpc
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0b
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stephen Wattam
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-05-
|
11
|
+
date: 2013-05-28 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: A very simple and fast RPC library
|
14
14
|
email: stephenwattam@gmail.com
|
@@ -17,15 +17,16 @@ extensions: []
|
|
17
17
|
extra_rdoc_files: []
|
18
18
|
files:
|
19
19
|
- lib/simplerpc/client.rb
|
20
|
-
- lib/simplerpc/socket_protocol.rb
|
21
|
-
- lib/simplerpc/serialiser.rb
|
22
20
|
- lib/simplerpc/server.rb
|
21
|
+
- lib/simplerpc/encryption.rb
|
22
|
+
- lib/simplerpc/socket_protocol.rb
|
23
|
+
- ./lib/simplerpc.rb
|
23
24
|
- LICENSE
|
24
25
|
homepage: http://stephenwattam.com/projects/simplerpc
|
25
26
|
licenses:
|
26
|
-
-
|
27
|
+
- Beerware
|
27
28
|
metadata: {}
|
28
|
-
post_install_message:
|
29
|
+
post_install_message: Thanks for installing SimpleRPC!
|
29
30
|
rdoc_options: []
|
30
31
|
require_paths:
|
31
32
|
- lib
|
@@ -36,9 +37,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
36
37
|
version: '1.9'
|
37
38
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
38
39
|
requirements:
|
39
|
-
- - '
|
40
|
+
- - '>'
|
40
41
|
- !ruby/object:Gem::Version
|
41
|
-
version:
|
42
|
+
version: 1.3.1
|
42
43
|
requirements: []
|
43
44
|
rubyforge_project:
|
44
45
|
rubygems_version: 2.0.0
|
data/lib/simplerpc/serialiser.rb
DELETED
@@ -1,67 +0,0 @@
|
|
1
|
-
|
2
|
-
module SimpleRPC
|
3
|
-
|
4
|
-
# This class wraps three possible serialisation systems, providing a common interface to them all.
|
5
|
-
#
|
6
|
-
# It's not a necessary part of SimpleRPC---you may use any object that supports load/dump---but it is
|
7
|
-
# rather handy.
|
8
|
-
class Serialiser
|
9
|
-
|
10
|
-
SUPPORTED_METHODS = %w{marshal json msgpack yaml}.map{|s| s.to_sym}
|
11
|
-
|
12
|
-
# Create a new Serialiser with the given method. Optionally provide a binding to have
|
13
|
-
# the serialisation method execute within another context, i.e. for it to pick up
|
14
|
-
# on various libraries and classes (though this will impact performance somewhat).
|
15
|
-
#
|
16
|
-
# Supported methods are:
|
17
|
-
#
|
18
|
-
# :marshal:: Use ruby's Marshal system. A good mix of speed and generality.
|
19
|
-
# :yaml:: Use YAML. Very slow but very general
|
20
|
-
# :msgpack:: Use MessagePack gem. Very fast but not very general (limited data format support)
|
21
|
-
# :json:: Use JSON. Also slow, but better for interoperability than YAML.
|
22
|
-
#
|
23
|
-
def initialize(method = :marshal, binding=nil)
|
24
|
-
@method = method
|
25
|
-
@binding = nil
|
26
|
-
raise "Unrecognised serialisation method" if not SUPPORTED_METHODS.include?(method)
|
27
|
-
|
28
|
-
# Require prerequisites and handle msgpack not installed-iness.
|
29
|
-
case method
|
30
|
-
when :msgpack
|
31
|
-
begin
|
32
|
-
gem "msgpack", "~> 0.5"
|
33
|
-
rescue Gem::LoadError => e
|
34
|
-
$stderr.puts "The :msgpack serialisation method requires the MessagePack gem (msgpack)."
|
35
|
-
$stderr.puts "Please install it or use another serialisation method."
|
36
|
-
raise e
|
37
|
-
end
|
38
|
-
require 'msgpack'
|
39
|
-
@cls = MessagePack
|
40
|
-
when :yaml
|
41
|
-
require 'yaml'
|
42
|
-
@cls = YAML
|
43
|
-
when :json
|
44
|
-
require 'json'
|
45
|
-
@cls = JSON
|
46
|
-
else
|
47
|
-
# marshal is alaways available
|
48
|
-
@cls = Marshal
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
# Serialise to a string
|
53
|
-
def dump(obj)
|
54
|
-
return eval("#{@cls.to_s}.dump(obj)", @binding) if @binding
|
55
|
-
return @cls.send(:dump, obj)
|
56
|
-
end
|
57
|
-
|
58
|
-
# Deserialise from a string
|
59
|
-
def load(bits)
|
60
|
-
return eval("#{@cls.to_s}.load(bits)", @binding) if @binding
|
61
|
-
return @cls.send(:load, bits)
|
62
|
-
end
|
63
|
-
|
64
|
-
end
|
65
|
-
|
66
|
-
end
|
67
|
-
|