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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 35e03bbef72aa8b10a3ce67964e8ad2ae5a0a59d
4
- data.tar.gz: b961f842f95b6b1e9b784bbb287c9a493b9ccb0c
3
+ metadata.gz: 297f5863f75ffee26a9d5a4d2be51bdf376f282e
4
+ data.tar.gz: 078eeb27ec40ff2202bb806a9a2a9469910b7284
5
5
  SHA512:
6
- metadata.gz: c681277ce02e0857e4feb5e7ac937bb13fa28d6f7451914eecfcf46968631c35d2c491210aa89a4cb97bc0fefe3f15ed40ad0e8a492cd57f28a635505c329ed7
7
- data.tar.gz: de1c8efba094eea904384d94478f8d891387631cf8bd93684174ef389fa7e7f6c2155fab8bb4c4f5a72ad67c198606ce76a311d494844a6de411da607e23b96c
6
+ metadata.gz: 437d37efe70e1f4f4d506c44e5ca388f71c7c71eb12590b12081ec8822ba5a325940fa527465d7eb7439be14adb174331068399134f683cfcd320ad58e5ca6df
7
+ data.tar.gz: 600fb27195f8788c62702167a50b3c82d3c0d95ec6523e8fd31247eaff889dc3188d7b4d2162de2bd92ef027ee5179b24de93bf8ccdd9105ef3691cd9fe6c17b
@@ -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
- # Create a new client for the network
16
- #
17
- # hostname:: The hostname of the server
18
- # port:: The port to connect to
19
- # serialiser:: An object supporting load/dump for serialising objects. Defaults to
20
- # SimpleRPC::Serialiser
21
- # timeout:: The socket timeout. Throws Timeout::TimeoutErrors when exceeded. Set
22
- # to nil to disable.
23
- def initialize(hostname, port, serialiser=Serialiser.new, timeout=nil)
24
- @hostname = hostname
25
- @port = port
26
- @serialiser = serialiser
27
- @timeout = timeout
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
- # or do nothing if already connected
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
- not @s == nil
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
- valid_call = false
161
+
67
162
  result = nil
163
+ success = true
68
164
 
69
165
  @m.synchronize{
70
- already_connected = (not (@s == nil))
71
- _connect if not already_connected
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
- # #puts "c: METHOD: #{m}. ARITY: #{args.length}"
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
- if valid_call then
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
- # If the call wasn't valid, call super
92
- if not valid_call then
93
- result = super
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
- # Connect to the server
101
- def _connect
102
- @s = TCPSocket.open( @hostname, @port )
103
- raise "Failed to connect" if not @s
104
- end
196
+
197
+
198
+ # -------------------------------------------------------------------------
199
+ # Send/Receive
200
+ #
105
201
 
106
202
  # Receive data from the server
107
203
  def _recv
108
- @serialiser.load( SocketProtocol::recv(@s, @timeout) )
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.dump(obj), @timeout)
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 @s
119
- @s.close
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
@@ -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
- # Create a new server for a given proxy object
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
- # obj:: The object to proxy the API for---any ruby object
15
- # port:: The port to listen on
16
- # hostname:: The ip of the interface to listen on, or nil for all
17
- # serialiser:: The serialiser to use
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 = serialiser
90
+ @serialiser = opts[:serialiser] || Marshal
27
91
 
28
- # Silence errors?
29
- @silence_errors = true
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 = false
97
+ @close = false
98
+ @close_in, @close_out = UNIXSocket.pair
33
99
 
34
100
  # Connect/receive timeouts
35
- @timeout = 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 = (threaded == true)
39
- @clients = {} if @threaded
40
- @m = Mutex.new if @threaded
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 @hostname
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
- if @threaded
57
-
58
- # Create the thread
59
- id = rand.hash
60
- thread = Thread.new(id, @m, @s.accept){|id, m, c|
61
- handle_client(c)
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
- # puts "#{id} closing 1"
64
- m.synchronize{
65
- @clients.delete(id)
145
+ # Add to the client list
146
+ @m.synchronize{
147
+ @clients[id] = thread
66
148
  }
67
- # puts "#{id} closing 2"
68
- }
69
-
70
- # Add to the client list
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 not @silence_errors
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 on threads
99
- if @threaded then
100
- @clients.each{|id, thread|
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
- m, arity = recv(c)
223
+ # Disable Nagle's algorithm
224
+ c.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
110
225
 
111
- # Check the call is valid for the proxy object
112
- valid_call = (@obj.respond_to?(m) and @obj.method(m).arity == arity)
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
- send(c, valid_call)
234
+ # Receive encrypted challenge
235
+ raw = SocketProtocol::Simple.recv( c, @timeout )
115
236
 
116
- # Make the call if valid and send the result back
117
- if valid_call then
118
- args = recv(c)
119
- send(c, @obj.send(m, *args) )
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
- @serialiser.load( SocketProtocol::recv(c, @timeout) )
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.dump(obj), @timeout)
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
- # Low-level protocol specification for SimpleRPC.
2
- #
3
- # This defines how data is sent at the socket level, primarily controlling what happens with partial sends/timeouts.
4
- module SimpleRPC::SocketProtocol
5
-
6
- # Send already-serialised payload to socket s
7
- def self.send(s, payload, timeout=nil)
8
- # Send length
9
- raise Timeout::TimeoutError if not IO.select(nil, [s], nil, timeout)
10
- s.puts(payload.length.to_s)
11
-
12
- # Send rest incrementally
13
- #puts "[s] send(#{payload})"
14
- len = payload.length
15
- while( len > 0 and x = IO.select(nil, [s], nil, timeout) )
16
- len -= s.write( payload )
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
- raise Timeout::TimeoutError if not x
19
- #puts "[s] sent(#{payload})"
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
- # Receive raw data from socket s.
23
- def self.recv(s, timeout=nil)
24
- # Read the length of the data
25
- raise Timeout::TimeoutError if not IO.select([s], nil, nil, timeout)
26
- len = s.gets.chomp.to_i
27
-
28
- # Then read the rest incrementally
29
- buf = ""
30
- while( len > 0 and x = IO.select([s], nil, nil, timeout) )
31
- #puts "[s (#{len})]"
32
- x = s.read(len)
33
- len -= x.length
34
- buf += x
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.1.1
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-26 00:00:00.000000000 Z
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
- - Beer
27
+ - Beerware
27
28
  metadata: {}
28
- post_install_message: Have fun Cing RPs :-)
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: '0'
42
+ version: 1.3.1
42
43
  requirements: []
43
44
  rubyforge_project:
44
45
  rubygems_version: 2.0.0
@@ -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
-