raval 0.0.1

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