raval 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,8 @@
1
+ # coding: utf-8
2
+
3
+ require 'raval/app'
4
+ require 'raval/server'
5
+
6
+ module Raval
7
+ LBRK = "\r\n"
8
+ end
@@ -0,0 +1,49 @@
1
+ # coding: utf-8
2
+
3
+ require 'celluloid/io'
4
+ require 'socket'
5
+
6
+ module Raval
7
+ # In Active FTP mode, the client opens a listening data socket on their host
8
+ # and we connect to it.
9
+ #
10
+ # A different class is used when operating in passive FTP mode. They both
11
+ # have a #read and #write method, so the quack like each other in the ways
12
+ # that matter.
13
+ class ActiveSocket
14
+ include Celluloid::IO
15
+
16
+ def initialize(host, port)
17
+ @host, @port = host, port
18
+ @socket = nil
19
+ end
20
+
21
+ def connect
22
+ unless @socket
23
+ @socket = ::TCPSocket.new(@host, @port)
24
+ end
25
+ end
26
+
27
+ def read
28
+ connect unless @socket
29
+ @socket.readpartial(4096)
30
+ rescue EOFError, Errno::ECONNRESET
31
+ close
32
+ nil
33
+ end
34
+
35
+ def write(data)
36
+ connect unless @socket
37
+ @socket.write(data)
38
+ end
39
+
40
+ def connected?
41
+ @socket != nil
42
+ end
43
+
44
+ def close
45
+ @socket.close if @socket
46
+ @socket = nil
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,137 @@
1
+ # coding: utf-8
2
+
3
+ module Raval
4
+
5
+ # Holds the logic for booting a raval server and configuring the
6
+ # process as required, binding to a port, writing a pid file, etc.
7
+ #
8
+ # Use it like so:
9
+ #
10
+ # Raval::App.start(:port => 3000)
11
+ #
12
+ # Options:
13
+ #
14
+ # :driver - the driver class that implements persistance
15
+ # :driver_opts - an optional Hash of options to be passed to the driver
16
+ # constructor
17
+ # :host - the host IP to listen on. [default: 127.0.0.1]
18
+ # :port - the TCP port to listen on [default: 21]
19
+ # :pid_file - a path to write the process pid to. Useful for monitoring
20
+ # :user - the user ID to change the process owner to
21
+ # :group - the group ID to change the process owner to
22
+ # :name - an optional name to place in the process description
23
+ class App
24
+
25
+ def initialize(options = {})
26
+ @options = options
27
+ end
28
+
29
+ def self.start(options = {})
30
+ self.new(options).start
31
+ end
32
+
33
+ def start
34
+ update_procline
35
+
36
+ puts "Starting ftp server on 0.0.0.0:#{port}"
37
+ Raval::Server.supervise(host,port, driver, driver_opts)
38
+
39
+ change_gid
40
+ change_uid
41
+ write_pid
42
+ setup_signal_handlers
43
+ sleep # for ever
44
+ end
45
+
46
+ private
47
+
48
+ def name
49
+ @options[:name]
50
+ end
51
+
52
+ def host
53
+ @options.fetch(:host, "127.0.0.1")
54
+ end
55
+
56
+ def port
57
+ @options.fetch(:port, 21)
58
+ end
59
+
60
+ def driver_opts
61
+ @options[:driver_opts]
62
+ end
63
+
64
+ def driver
65
+ @options.fetch(:driver)
66
+ rescue KeyError
67
+ raise ArgumentError, "the :driver option must be provided"
68
+ end
69
+
70
+ def uid
71
+ if @options[:user]
72
+ begin
73
+ detail = Etc.getpwnam(@options[:user])
74
+ return detail.uid
75
+ rescue
76
+ $stderr.puts "user must be nil or a real account" if detail.nil?
77
+ end
78
+ end
79
+ end
80
+
81
+ def gid
82
+ if @options[:group]
83
+ begin
84
+ detail = Etc.getgrnam(@options[:group])
85
+ return detail.gid
86
+ rescue
87
+ $stderr.puts "group must be nil or a real group" if detail.nil?
88
+ end
89
+ end
90
+ end
91
+
92
+ def pid_file
93
+ @options[:pid_file]
94
+ end
95
+
96
+ def write_pid
97
+ if pid_file
98
+ File.open(pid_file, "w") { |io| io.write pid }
99
+ end
100
+ end
101
+
102
+ def update_procline
103
+ if name
104
+ $0 = "raval ftpd [#{name}]"
105
+ else
106
+ $0 = "raval"
107
+ end
108
+ end
109
+
110
+ def change_gid
111
+ if gid && Process.gid == 0
112
+ Process.gid = gid
113
+ end
114
+ end
115
+
116
+ def change_uid
117
+ if uid && Process.euid == 0
118
+ Process::Sys.setuid(uid)
119
+ end
120
+ end
121
+
122
+ def setup_signal_handlers
123
+ =begin
124
+ trap('QUIT') do
125
+ EM.stop
126
+ end
127
+ trap('TERM') do
128
+ EM.stop
129
+ end
130
+ trap('INT') do
131
+ EM.stop
132
+ end
133
+ =end
134
+ end
135
+
136
+ end
137
+ end
@@ -0,0 +1,50 @@
1
+ # coding: utf-8
2
+
3
+ module Raval
4
+ # wraps a raw TCP socket and simplifies working with FTP style data.
5
+ #
6
+ # FTP commands and responses are always a single line ending in a line break,
7
+ # so make it easy to read and write single lines.
8
+ class Connection
9
+
10
+ BUFFER_SIZE = 1024
11
+
12
+ attr_reader :port, :host
13
+ attr_reader :myport, :myhost
14
+
15
+ def initialize(handler, socket)
16
+ @socket, @handler = socket, handler
17
+ _, @port, @host = socket.peeraddr
18
+ _, @myport, @myhost = socket.addr
19
+ handler.new_connection(self)
20
+ puts "*** Received connection from #{host}:#{port}"
21
+ end
22
+
23
+ def send_data(str)
24
+ @socket.write(str)
25
+ end
26
+
27
+ def send_response(code, message)
28
+ @socket.write("#{code} #{message}#{Raval::LBRK}")
29
+ end
30
+
31
+ # Close the connection
32
+ def close
33
+ @socket.close
34
+ end
35
+
36
+ def read_commands
37
+ input = ""
38
+ while true
39
+ input << @socket.readpartial(BUFFER_SIZE)
40
+ match = input.match(/(^.+\r\n)/)
41
+ if match
42
+ line = match[1]
43
+ input = input[line.bytesize, line.bytesize]
44
+ @handler.receive_line(line)
45
+ end
46
+ end
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,549 @@
1
+ # coding: utf-8
2
+
3
+ require 'raval/active_socket'
4
+ require 'raval/passive_socket'
5
+ require 'raval/list_formatter'
6
+ require 'stringio'
7
+ require 'tempfile'
8
+
9
+ module Raval
10
+ # implement the FTP wire protocol, deferring the users driver when interaction
11
+ # with the persistence layer is required. Nothing in this class is network
12
+ # aware, it should remain ignorant of where the FTP connection is from and
13
+ # assume the connection will handle it.
14
+ #
15
+ # A new instance of this class is created for each active FTP connection, so
16
+ # it's OK to store connection specific state in instance variables.
17
+ class Handler
18
+
19
+ COMMANDS = %w[quit type user retr stor eprt port cdup cwd dele rmd pwd
20
+ list size syst mkd pass xcup xpwd xcwd xrmd rest allo nlst
21
+ pasv epsv help noop mode rnfr rnto stru mdtm]
22
+
23
+ REQUIRE_AUTH = %w[cdup xcup cwd xcwd eprt epsv mode nlst list mdtm pasv port
24
+ pwd xpwd retr size stor stru syst type]
25
+ REQUIRE_PARAM = %w[cwd xcwd eprt mdtm mode pass port retr size stor stru
26
+ type user]
27
+
28
+ attr_reader :connection, :name_prefix
29
+
30
+ def initialize(driver)
31
+ @driver = driver
32
+ end
33
+
34
+ def new_connection(connection)
35
+ @mode = :binary
36
+ @user = nil
37
+ @name_prefix = "/"
38
+ @connection = connection
39
+ @connection.send_response(220, "FTP server (raval) ready")
40
+ end
41
+
42
+ def receive_line(line)
43
+ cmd, param = parse_request(line)
44
+
45
+ # if the command is contained in the whitelist, and there is a method
46
+ # to handle it, call it. Otherwise send an appropriate response to the
47
+ # client
48
+ if REQUIRE_AUTH.include?(cmd) && !logged_in?
49
+ @connection.send_response(530, "Not logged in")
50
+ elsif REQUIRE_PARAM.include?(cmd) && param.nil?
51
+ @connection.send_response(553, "action aborted, required param missing")
52
+ elsif COMMANDS.include?(cmd) && self.respond_to?("cmd_#{cmd}".to_sym, true)
53
+ self.__send__("cmd_#{cmd}".to_sym, param)
54
+ else
55
+ @connection.send_response(500, "Sorry, I don't understand #{cmd.upcase}")
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def cmd_allo(param)
62
+ @connection.send_response(202, "Obsolete")
63
+ end
64
+
65
+ # handle the HELP FTP command by sending a list of available commands.
66
+ def cmd_help(param)
67
+ @connection.send_response("214-", "The following commands are recognized.")
68
+ commands = COMMANDS
69
+ str = ""
70
+ commands.sort.each_slice(3) { |slice|
71
+ str += " " + slice.join("\t\t") + Raval::LBRK
72
+ }
73
+ @connection.send_response(str, "")
74
+ @connection.send_response(214, "End of list.")
75
+ end
76
+
77
+ # the original FTP spec had various options for hosts to negotiate how data
78
+ # would be sent over the data socket, In reality these days (S)tream mode
79
+ # is all that is used for the mode - data is just streamed down the data
80
+ # socket unchanged.
81
+ #
82
+ def cmd_mode(param)
83
+ if param.upcase.eql?("S")
84
+ @connection.send_response(200, "OK")
85
+ else
86
+ @connection.send_response(504, "MODE is an obsolete command")
87
+ end
88
+ end
89
+
90
+ # handle the NOOP FTP command. This is essentially a ping from the client
91
+ # so we just respond with an empty 200 message.
92
+ def cmd_noop(param)
93
+ @connection.send_response(200, "")
94
+ end
95
+
96
+ # handle the QUIT FTP command by closing the connection
97
+ def cmd_quit(param)
98
+ @connection.send_response(221, "Bye")
99
+ @connection.close
100
+ end
101
+
102
+ # like the MODE and TYPE commands, stru[cture] dates back to a time when the FTP
103
+ # protocol was more aware of the content of the files it was transferring, and
104
+ # would sometimes be expected to translate things like EOL markers on the fly.
105
+ #
106
+ # These days files are sent unmodified, and F(ile) mode is the only one we
107
+ # really need to support.
108
+ def cmd_stru(param)
109
+ if param.upcase.eql?("F")
110
+ @connection.send_response(200, "OK")
111
+ else
112
+ @connection.send_response(504, "STRU is an obsolete command")
113
+ end
114
+ end
115
+
116
+ # return the name of the server
117
+ def cmd_syst(param)
118
+ @connection.send_response(215, "UNIX Type: L8")
119
+ end
120
+
121
+ # like the MODE and STRU commands, TYPE dates back to a time when the FTP
122
+ # protocol was more aware of the content of the files it was transferring, and
123
+ # would sometimes be expected to translate things like EOL markers on the fly.
124
+ #
125
+ # Valid options were A(SCII), I(mage), E(BCDIC) or LN (for local type). Since
126
+ # we plan to just accept bytes from the client unchanged, I think Image mode is
127
+ # adequate. The RFC requires we accept ASCII mode however, so accept it, but
128
+ # ignore it.
129
+ def cmd_type(param)
130
+ if param.upcase.eql?("A")
131
+ @connection.send_response(200, "Type set to ASCII")
132
+ elsif param.upcase.eql?("I")
133
+ @connection.send_response(200, "Type set to binary")
134
+ else
135
+ @connection.send_response(500, "Invalid type")
136
+ end
137
+ end
138
+
139
+ # handle the USER FTP command. This is a user attempting to login.
140
+ # we simply store the requested user name as an instance variable
141
+ # and wait for the password to be submitted before doing anything
142
+ def cmd_user(param)
143
+ @connection.send_response(500, "Already logged in") and return unless @user.nil?
144
+ @requested_user = param
145
+ @connection.send_response(331, "OK, password required")
146
+ end
147
+
148
+ # handle the PASS FTP command. This is the second stage of a user logging in
149
+ def cmd_pass(param)
150
+ @connection.send_response(202, "User already logged in") and return unless @user.nil?
151
+ @connection.send_response(530, "password with no username") and return if @requested_user.nil?
152
+
153
+ if @driver.authenticate(@requested_user, param)
154
+ @name_prefix = "/"
155
+ @user = @requested_user
156
+ @requested_user = nil
157
+ @connection.send_response(230, "OK, password correct")
158
+ else
159
+ @user = nil
160
+ @connection.send_response(530, "incorrect login. not logged in.")
161
+ end
162
+ end
163
+
164
+ # Passive FTP. At the clients request, listen on a port for an incoming
165
+ # data connection. The listening socket is opened on a random port, so
166
+ # the host and port is sent back to the client on the control socket.
167
+ #
168
+ def cmd_pasv(param)
169
+ host, port = start_passive_socket
170
+
171
+ p1, p2 = *port.divmod(256)
172
+ host_with_commas = host.split(".").join(",")
173
+ port_with_commas = "#{p1},#{p2}"
174
+ message = "Entering Passive Mode (#{host_with_commas},#{port_with_commas})"
175
+
176
+ @connection.send_response(227, message)
177
+ end
178
+
179
+ # listen on a port, see RFC 2428
180
+ #
181
+ def cmd_epsv(param)
182
+ host, port = start_passive_socket
183
+
184
+ send_response "229 Entering Extended Passive Mode (|||#{port}|)"
185
+ end
186
+
187
+ # Active FTP. An alternative to Passive FTP. The client has a listening socket
188
+ # open, waiting for us to connect and establish a data socket. Attempt to
189
+ # open a connection to the host and port they specify and save the connection,
190
+ # ready for either end to send something down it.
191
+ def cmd_port(param)
192
+ nums = param.split(',')
193
+ port = nums[4].to_i * 256 + nums[5].to_i
194
+ host = nums[0..3].join('.')
195
+ close_datasocket
196
+ start_active_socket(host, port)
197
+ end
198
+
199
+ # open a data connection to the client on the requested port. See RFC 2428
200
+ def cmd_eprt(param)
201
+ delim = param[0,1]
202
+ m, af, host, port = *param.match(/#{delim}(.+?)#{delim}(.+?)#{delim}(.+?)#{delim}/)
203
+ port = port.to_i
204
+ close_datasocket
205
+
206
+ if af.to_i != 1 && ad.to_i != 2
207
+ @connection.send_response(522, "Network protocol not supported, use (1,2)")
208
+ else
209
+ start_active_socket(host, port)
210
+ end
211
+ end
212
+
213
+ # go up a directory, really just an alias
214
+ def cmd_cdup(param)
215
+ cmd_cwd("..")
216
+ end
217
+ alias cmd_xcup cmd_cdup
218
+
219
+ # change directory
220
+ def cmd_cwd(param)
221
+ path = build_path(param)
222
+
223
+ if @driver.change_dir(path)
224
+ @name_prefix = path
225
+ @connection.send_response(250, "Directory changed to #{path}")
226
+ else
227
+ send_permission_denied
228
+ end
229
+ end
230
+
231
+ # As per RFC1123, XCWD is a synonym for CWD
232
+ alias cmd_xcwd cmd_cwd
233
+
234
+ # make directory
235
+ def cmd_mkd(param)
236
+ send_unauthorised and return unless logged_in?
237
+ send_param_required and return if param.nil?
238
+
239
+ if @driver.make_dir(build_path(param))
240
+ @connection.send_response(257, "Directory created")
241
+ else
242
+ send_action_not_taken
243
+ end
244
+ end
245
+
246
+ # return the current directory
247
+ def cmd_pwd(param)
248
+ @connection.send_response(257, "\"#{@name_prefix}\" is the current directory")
249
+ end
250
+
251
+ # As per RFC1123, XPWD is a synonym for PWD
252
+ alias cmd_xpwd cmd_pwd
253
+
254
+ # delete a directory
255
+ def cmd_rmd(param)
256
+ send_unauthorised and return unless logged_in?
257
+ send_param_required and return if param.nil?
258
+
259
+ if @driver.delete_dir(build_path(param))
260
+ @connection.send_response(250, "Directory deleted.")
261
+ else
262
+ send_action_not_taken
263
+ end
264
+ end
265
+
266
+ # As per RFC1123, XRMD is a synonym for RMD
267
+ alias cmd_xrmd cmd_rmd
268
+
269
+ # return a listing of the current directory, one per line, each line
270
+ # separated by the standard FTP EOL sequence. The listing is returned
271
+ # to the client over a data socket.
272
+ #
273
+ def cmd_nlst(param)
274
+ @connection.send_response(150, "Opening ASCII mode data connection for file list")
275
+
276
+ files = @driver.dir_contents(build_path(param))
277
+ formatter = ListFormatter.new(files)
278
+ send_outofband_data(formatter.short)
279
+ end
280
+
281
+ # return a detailed list of files and directories
282
+ def cmd_list(param)
283
+ @connection.send_response(150, "Opening ASCII mode data connection for file list")
284
+
285
+ param = '' if param.to_s == '-a'
286
+
287
+ files = @driver.dir_contents(build_path(param))
288
+ formatter = ListFormatter.new(files)
289
+ send_outofband_data(formatter.detailed)
290
+ end
291
+
292
+ # delete a file
293
+ def cmd_dele(param)
294
+ send_unauthorised and return unless logged_in?
295
+ send_param_required and return if param.nil?
296
+
297
+ path = build_path(param)
298
+
299
+ if @driver.delete_file(path)
300
+ @connection.send_response(250, "File deleted")
301
+ else
302
+ send_action_not_taken
303
+ end
304
+ end
305
+
306
+ # resume downloads
307
+ def cmd_rest(param)
308
+ @connection.send_response(500, "Feature not implemented")
309
+ end
310
+
311
+ # send a file to the client
312
+ def cmd_retr(param)
313
+ path = build_path(param)
314
+
315
+ io = @driver.get_file(path)
316
+ if io
317
+ @connection.send_response(150, "Data transfer starting #{io.size} bytes")
318
+ send_outofband_data(io)
319
+ else
320
+ @connection.send_response(551, "file not available")
321
+ end
322
+ end
323
+
324
+ # rename a file
325
+ def cmd_rnfr(param)
326
+ send_unauthorised and return unless logged_in?
327
+ send_param_required and return if param.nil?
328
+
329
+ @from_filename = build_path(param)
330
+ @connection.send_response(350, "Requested file action pending further information.")
331
+ end
332
+
333
+ # rename a file
334
+ def cmd_rnto(param)
335
+ send_unauthorised and return unless logged_in?
336
+ send_param_required and return if param.nil?
337
+
338
+ if @driver.rename(@from_filename, build_path(param))
339
+ @connection.send_response(250, "File renamed.")
340
+ else
341
+ send_action_not_taken
342
+ end
343
+ end
344
+
345
+ # return the size of a file in bytes
346
+ def cmd_size(param)
347
+ bytes = @driver.bytes(build_path(param))
348
+ if bytes
349
+ @connection.send_response(213, bytes)
350
+ else
351
+ @connection.send_response(450, "file not available")
352
+ end
353
+ end
354
+
355
+ # return the last modified time of a file
356
+ #
357
+ def cmd_mdtm(param)
358
+ time = @driver.modified_time(build_path(param))
359
+ if time
360
+ @connection.send_response(213, "#{time.strftime("%Y%m%d%H%M%S")}")
361
+ else
362
+ @connection.send_response(450, "file not available")
363
+ end
364
+ end
365
+
366
+ # save a file from a client
367
+ def cmd_stor(param)
368
+ path = build_path(param)
369
+
370
+ if @driver.respond_to?(:put_file_streamed)
371
+ cmd_stor_streamed(path)
372
+ elsif @driver.respond_to?(:put_file)
373
+ cmd_stor_tempfile(path)
374
+ else
375
+ raise "driver MUST respond to put_file OR put_file_streamed"
376
+ end
377
+ end
378
+
379
+ def cmd_stor_streamed(target_path)
380
+ if @datasocket
381
+ @connection.send_response(150, "Data transfer starting")
382
+ @driver.put_file_streamed(target_path, @datasocket) do |bytes|
383
+ if bytes
384
+ @connection.send_response(226, "OK, received #{bytes} bytes")
385
+ else
386
+ send_action_not_taken
387
+ end
388
+ end
389
+ else
390
+ @connection.send_response(425, "Error establishing connection")
391
+ end
392
+ end
393
+
394
+ def cmd_stor_tempfile(target_path)
395
+ Tempfile.open("raval") do |tmpfile|
396
+ tmpfile.binmode
397
+
398
+ @connection.send_response(150, "Data transfer starting")
399
+ while chunk = @datasocket.read
400
+ tmpfile.write(chunk)
401
+ end
402
+ tmpfile.flush
403
+ tmpfile.close
404
+ bytes = @driver.put_file(target_path, tmpfile.path)
405
+ if bytes
406
+ @connection.send_response(226, "OK, received #{bytes} bytes")
407
+ else
408
+ send_action_not_taken
409
+ end
410
+ end
411
+ end
412
+
413
+ def build_path(filename = nil)
414
+ if filename && filename[0,1] == "/"
415
+ path = File.expand_path(filename)
416
+ elsif filename && filename != '-a'
417
+ path = File.expand_path("#{@name_prefix}/#{filename}")
418
+ else
419
+ path = File.expand_path(@name_prefix)
420
+ end
421
+ path.gsub(/\/+/,"/")
422
+ end
423
+
424
+ # send data to the client across the data socket.
425
+ #
426
+ def send_outofband_data(data)
427
+ wait_for_datasocket do |datasocket|
428
+ if datasocket.nil?
429
+ @connection.send_response(425, "Error establishing connection")
430
+ return
431
+ end
432
+
433
+ if data.is_a?(Array)
434
+ data = data.join(LBRK) << LBRK
435
+ end
436
+ data = StringIO.new(data) if data.kind_of?(String)
437
+
438
+ # blocks until all data is sent
439
+ begin
440
+ bytes = 0
441
+ data.each do |line|
442
+ datasocket.write(line)
443
+ bytes += line.bytesize
444
+ end
445
+ @connection.send_response(226, "Closing data connection, sent #{bytes} bytes")
446
+ rescue => e
447
+ puts e.inspect
448
+ ensure
449
+ close_datasocket
450
+ data.close if data.respond_to?(:close)
451
+ end
452
+ end
453
+ end
454
+
455
+ def start_active_socket(host, port)
456
+ # close any existing data socket
457
+ close_datasocket
458
+
459
+ @datasocket = ActiveSocket.new(host, port)
460
+ @datasocket.async.connect
461
+ wait_for_datasocket do
462
+ @connection.send_response(200, "Connection established (#{port})")
463
+ end
464
+ rescue => e
465
+ puts "Error opening data connection to #{host}:#{port}"
466
+ puts e.inspect
467
+ @connection.send_response(425, "Data connection failed")
468
+ end
469
+
470
+ def start_passive_socket
471
+ # close any existing data socket
472
+ close_datasocket
473
+
474
+ # open a listening socket on the appropriate host
475
+ # and on a random port
476
+ @datasocket = PassiveSocket.new(@connection.myhost)
477
+
478
+ [@connection.myhost, @datasocket.port]
479
+ end
480
+
481
+ # split a client's request into command and parameter components
482
+ def parse_request(data)
483
+ data.strip!
484
+ space = data.index(" ")
485
+ if space
486
+ cmd = data[0, space]
487
+ param = data[space+1, data.length - space]
488
+ param = nil if param.strip.size == 0
489
+ else
490
+ cmd = data
491
+ param = nil
492
+ end
493
+
494
+ [cmd.downcase, param]
495
+ end
496
+
497
+ def close_datasocket
498
+ if @datasocket
499
+ @datasocket.close
500
+ @datasocket.terminate
501
+ @datasocket = nil
502
+ end
503
+
504
+ # stop listening for data socket connections, we have one
505
+ #if @listen_sig
506
+ # PassiveSocket.stop(@listen_sig)
507
+ # @listen_sig = nil
508
+ #end
509
+ end
510
+
511
+ # waits for the data socket to be established
512
+ def wait_for_datasocket(interval = 0.1, &block)
513
+ if (@datasocket.nil? || !@datasocket.connected?) && interval < 25
514
+ sleep interval
515
+ wait_for_datasocket(interval * 2, &block)
516
+ return
517
+ end
518
+ if @datasocket.connected?
519
+ yield @datasocket
520
+ else
521
+ yield nil
522
+ end
523
+ end
524
+
525
+ def logged_in?
526
+ @user.nil? ? false : true
527
+ end
528
+
529
+ def send_param_required
530
+ @connection.send_response(553, "action aborted, required param missing")
531
+ end
532
+
533
+ def send_permission_denied
534
+ @connection.send_response(550, "Permission denied")
535
+ end
536
+
537
+ def send_action_not_taken
538
+ @connection.send_response(550, "Action not taken")
539
+ end
540
+
541
+ def send_illegal_params
542
+ @connection.send_response(553, "action aborted, illegal params")
543
+ end
544
+
545
+ def send_unauthorised
546
+ @connection.send_response(530, "Not logged in")
547
+ end
548
+ end
549
+ end