em-ftpd 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,48 @@
1
+ module EM::FTPD
2
+ module Authentication
3
+
4
+ def initialize
5
+ @user = nil
6
+ @requested_user = nil
7
+ super
8
+ end
9
+
10
+ def logged_in?
11
+ @user.nil? ? false : true
12
+ end
13
+
14
+ # handle the USER FTP command. This is a user attempting to login.
15
+ # we simply store the requested user name as an instance variable
16
+ # and wait for the password to be submitted before doing anything
17
+ def cmd_user(param)
18
+ send_param_required and return if param.nil?
19
+ send_response("500 Already logged in") and return unless @user.nil?
20
+ @requested_user = param
21
+ send_response "331 OK, password required"
22
+ end
23
+
24
+ # handle the PASS FTP command. This is the second stage of a user logging in
25
+ def cmd_pass(param)
26
+ send_response "202 User already logged in" and return unless @user.nil?
27
+ send_param_required and return if param.nil?
28
+ send_response "530 password with no username" and return if @requested_user.nil?
29
+
30
+ # return an error message if:
31
+ # - the specified username isn't in our system
32
+ # - the password is wrong
33
+
34
+ @driver.authenticate(@requested_user, param) do |result|
35
+ if result
36
+ @name_prefix = "/"
37
+ @user = @requested_user
38
+ @requested_user = nil
39
+ send_response "230 OK, password correct"
40
+ else
41
+ @user = nil
42
+ send_response "530 incorrect login. not logged in."
43
+ end
44
+ end
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,47 @@
1
+ module BaseSocket
2
+
3
+ attr_reader :aborted
4
+
5
+ def initialize
6
+ @on_stream = nil
7
+ @aborted = false
8
+ end
9
+
10
+ def on_stream &blk
11
+ @on_stream = blk if block_given?
12
+ unless data.empty?
13
+ @on_stream.call(data) # send all data that was collected before the stream hanlder was set
14
+ @data = ""
15
+ end
16
+ @on_stream
17
+ end
18
+
19
+ def data
20
+ @data ||= ""
21
+ end
22
+
23
+ def receive_data(chunk)
24
+ if @on_stream
25
+ @on_stream.call(chunk)
26
+ else
27
+ data << chunk
28
+ end
29
+ end
30
+
31
+ def unbind
32
+ if @aborted
33
+ fail
34
+ else
35
+ if @on_stream
36
+ succeed
37
+ else
38
+ succeed data
39
+ end
40
+ end
41
+ end
42
+
43
+ def abort
44
+ @aborted = true
45
+ close_connection_after_writing
46
+ end
47
+ end
@@ -0,0 +1,120 @@
1
+ # coding: utf-8
2
+
3
+ module EM::FTPD
4
+
5
+ class Configurator
6
+
7
+ def initialize
8
+ @user = nil
9
+ @group = nil
10
+ @daemonise = false
11
+ @name = nil
12
+ @pid_file = nil
13
+ @port = 21
14
+
15
+ @driver = nil
16
+ @driver_args = []
17
+ end
18
+
19
+ def user(val = nil)
20
+ if val
21
+ @user = val.to_s
22
+ else
23
+ @user
24
+ end
25
+ end
26
+
27
+ def uid
28
+ return nil if @user.nil?
29
+
30
+ begin
31
+ detail = Etc.getpwnam(@user)
32
+ return detail.uid
33
+ rescue
34
+ $stderr.puts "user must be nil or a real account" if detail.nil?
35
+ end
36
+ end
37
+
38
+ def group(val = nil)
39
+ if val
40
+ @group = val.to_s
41
+ else
42
+ @group
43
+ end
44
+ end
45
+
46
+ def gid
47
+ return nil if @group.nil?
48
+
49
+ begin
50
+ detail = Etc.getpwnam(@group)
51
+ return detail.gid
52
+ rescue
53
+ $stderr.puts "group must be nil or a real group" if detail.nil?
54
+ end
55
+ end
56
+
57
+
58
+ def daemonise(val = nil)
59
+ if val
60
+ @daemonise = val
61
+ else
62
+ @daemonise
63
+ end
64
+ end
65
+
66
+ def driver(klass = nil)
67
+ if klass
68
+ @driver = klass
69
+ else
70
+ @driver
71
+ end
72
+ end
73
+
74
+ def driver_args(*args)
75
+ if args.empty?
76
+ @driver_args
77
+ else
78
+ @driver_args = args
79
+ end
80
+ end
81
+
82
+ def name(val = nil)
83
+ if val
84
+ @name = val.to_s
85
+ else
86
+ @name
87
+ end
88
+ end
89
+
90
+ def pid_file(val = nil)
91
+ if val
92
+ @pid_file = val.to_s
93
+ else
94
+ @pid_file
95
+ end
96
+ end
97
+
98
+ def port(val = nil)
99
+ if val
100
+ @port = val.to_i
101
+ else
102
+ @port
103
+ end
104
+ end
105
+
106
+ def check!
107
+ if @driver.nil?
108
+ die("driver MUST be specified in the config file")
109
+ end
110
+ end
111
+
112
+ private
113
+
114
+ def die(msg)
115
+ $stderr.puts msg
116
+ exit 1
117
+ end
118
+ end
119
+
120
+ end
@@ -0,0 +1,110 @@
1
+ module EM::FTPD
2
+ module Directories
3
+ # go up a directory, really just an alias
4
+ def cmd_cdup(param)
5
+ send_unauthorised and return unless logged_in?
6
+ cmd_cwd("..")
7
+ end
8
+
9
+ # As per RFC1123, XCUP is a synonym for CDUP
10
+ alias cmd_xcup cmd_cdup
11
+
12
+
13
+ # change directory
14
+ def cmd_cwd(param)
15
+ send_unauthorised and return unless logged_in?
16
+ path = build_path(param)
17
+
18
+ @driver.change_dir(path) do |result|
19
+ if result
20
+ @name_prefix = path
21
+ send_response "250 Directory changed to #{path}"
22
+ else
23
+ send_permission_denied
24
+ end
25
+ end
26
+ end
27
+
28
+ # As per RFC1123, XCWD is a synonym for CWD
29
+ alias cmd_xcwd cmd_cwd
30
+
31
+ # make directory
32
+ def cmd_mkd(param)
33
+ send_unauthorised and return unless logged_in?
34
+ send_param_required and return if param.nil?
35
+
36
+ @driver.make_dir(build_path(param)) do |result|
37
+ if result
38
+ send_response "257 Directory created"
39
+ else
40
+ send_action_not_taken
41
+ end
42
+ end
43
+ end
44
+
45
+ # return a listing of the current directory, one per line, each line
46
+ # separated by the standard FTP EOL sequence. The listing is returned
47
+ # to the client over a data socket.
48
+ #
49
+ def cmd_nlst(param)
50
+ send_unauthorised and return unless logged_in?
51
+ send_response "150 Opening ASCII mode data connection for file list"
52
+
53
+ @driver.dir_contents(build_path(param)) do |files|
54
+ send_outofband_data(files.map(&:name))
55
+ end
56
+ end
57
+
58
+
59
+ def default_files(dir)
60
+ [
61
+ DirectoryItem.new(:name => '.', :permissions => 'rwxrwxrwx', :directory => true),
62
+ DirectoryItem.new(:name => '..', :permissions => 'rwxrwxrwx', :directory => true),
63
+ ]
64
+ end
65
+
66
+ # return a detailed list of files and directories
67
+ def cmd_list(param)
68
+ send_unauthorised and return unless logged_in?
69
+ send_response "150 Opening ASCII mode data connection for file list"
70
+
71
+ param = '' if param.to_s == '-a'
72
+
73
+ @driver.dir_contents(build_path(param)) do |files|
74
+ now = Time.now
75
+ lines = files.map { |item|
76
+ sizestr = (item.size || 0).to_s.rjust(12)
77
+ "#{item.directory ? 'd' : '-'}#{item.permissions || 'rwxrwxrwx'} 1 #{item.owner || 'owner'} #{item.group || 'group'} #{sizestr} #{(item.time || now).strftime("%b %d %H:%M")} #{item.name}"
78
+ }
79
+ send_outofband_data(lines)
80
+ end
81
+ end
82
+
83
+ # return the current directory
84
+ def cmd_pwd(param)
85
+ send_unauthorised and return unless logged_in?
86
+ send_response "257 \"#{@name_prefix}\" is the current directory"
87
+ end
88
+
89
+ # As per RFC1123, XPWD is a synonym for PWD
90
+ alias cmd_xpwd cmd_pwd
91
+
92
+ # delete a directory
93
+ def cmd_rmd(param)
94
+ send_unauthorised and return unless logged_in?
95
+ send_param_required and return if param.nil?
96
+
97
+ @driver.delete_dir(build_path(param)) do |result|
98
+ if result
99
+ send_response "250 Directory deleted."
100
+ else
101
+ send_action_not_taken
102
+ end
103
+ end
104
+ end
105
+
106
+ # As per RFC1123, XRMD is a synonym for RMD
107
+ alias cmd_xrmd cmd_rmd
108
+
109
+ end
110
+ end
@@ -0,0 +1,12 @@
1
+ module EM::FTPD
2
+ class DirectoryItem
3
+ ATTRS = [:name, :owner, :group, :size, :time, :permissions, :directory]
4
+ attr_accessor(*ATTRS)
5
+
6
+ def initialize(options)
7
+ options.each do |attr, value|
8
+ self.send("#{attr}=", value)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,141 @@
1
+ require 'tempfile'
2
+
3
+ module EM::FTPD
4
+ module Files
5
+
6
+ # delete a file
7
+ def cmd_dele(param)
8
+ send_unauthorised and return unless logged_in?
9
+ send_param_required and return if param.nil?
10
+
11
+ path = build_path(param)
12
+
13
+ @driver.delete_file(path) do |result|
14
+ if result
15
+ send_response "250 File deleted"
16
+ else
17
+ send_action_not_taken
18
+ end
19
+ end
20
+ end
21
+
22
+ # resume downloads
23
+ def cmd_rest(param)
24
+ send_response "500 Feature not implemented"
25
+ end
26
+
27
+ # send a file to the client
28
+ def cmd_retr(param)
29
+ send_unauthorised and return unless logged_in?
30
+ send_param_required and return if param.nil?
31
+
32
+ path = build_path(param)
33
+
34
+ @driver.get_file(path) do |data|
35
+ if data
36
+ send_response "150 Data transfer starting #{data.size} bytes"
37
+ send_outofband_data(data)
38
+ else
39
+ send_response "551 file not available"
40
+ end
41
+ end
42
+ end
43
+
44
+ # rename a file
45
+ def cmd_rnfr(param)
46
+ send_unauthorised and return unless logged_in?
47
+ send_param_required and return if param.nil?
48
+
49
+ @from_filename = build_path(param)
50
+ send_response "350 Requested file action pending further information."
51
+ end
52
+
53
+ # rename a file
54
+ def cmd_rnto(param)
55
+ send_unauthorised and return unless logged_in?
56
+ send_param_required and return if param.nil?
57
+
58
+ @driver.rename(@from_filename, build_path(param)) do |result|
59
+ if result
60
+ send_response "250 File renamed."
61
+ else
62
+ send_action_not_taken
63
+ end
64
+ end
65
+ end
66
+
67
+ # return the size of a file in bytes
68
+ def cmd_size(param)
69
+ send_unauthorised and return unless logged_in?
70
+ send_param_required and return if param.nil?
71
+
72
+ @driver.bytes(build_path(param)) do |bytes|
73
+ if bytes
74
+ send_response "213 #{bytes}"
75
+ else
76
+ send_response "450 file not available"
77
+ end
78
+ end
79
+ end
80
+
81
+ # save a file from a client
82
+ def cmd_stor(param)
83
+ send_unauthorised and return unless logged_in?
84
+ send_param_required and return if param.nil?
85
+
86
+ path = build_path(param)
87
+
88
+ if @driver.respond_to?(:put_file_streamed)
89
+ cmd_stor_streamed(path)
90
+ elsif @driver.respond_to?(:put_file)
91
+ cmd_stor_tempfile(path)
92
+ else
93
+ raise "driver MUST respond to put_file OR put_file_streamed"
94
+ end
95
+ end
96
+
97
+ def cmd_stor_streamed(target_path)
98
+ wait_for_datasocket do |datasocket|
99
+ if datasocket
100
+ send_response "150 Data transfer starting"
101
+ @driver.put_file_streamed(target_path, datasocket) do |bytes|
102
+ if bytes
103
+ send_response "200 OK, received #{bytes} bytes"
104
+ else
105
+ send_action_not_taken
106
+ end
107
+ end
108
+ else
109
+ send_response "425 Error establishing connection"
110
+ end
111
+ end
112
+ end
113
+
114
+ def cmd_stor_tempfile(target_path)
115
+ tmpfile = Tempfile.new("em-ftp")
116
+
117
+ wait_for_datasocket do |datasocket|
118
+ datasocket.on_stream { |chunk|
119
+ tmpfile.write chunk
120
+ }
121
+ send_response "150 Data transfer starting"
122
+ datasocket.callback {
123
+ puts "data transfer finished"
124
+ tmpfile.flush
125
+ @driver.put_file(target_path, tmpfile.path) do |bytes|
126
+ if bytes
127
+ send_response "200 OK, received #{bytes} bytes"
128
+ else
129
+ send_action_not_taken
130
+ end
131
+ end
132
+ tmpfile.unlink
133
+ }
134
+ datasocket.errback {
135
+ tmpfile.unlink
136
+ }
137
+ end
138
+ end
139
+
140
+ end
141
+ end