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.
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +149 -0
- data/bin/em-ftpd +18 -0
- data/examples/fake.rb +90 -0
- data/examples/redis.rb +121 -0
- data/lib/em-ftpd.rb +12 -0
- data/lib/em-ftpd/active_socket.rb +14 -0
- data/lib/em-ftpd/app.rb +88 -0
- data/lib/em-ftpd/authentication.rb +48 -0
- data/lib/em-ftpd/base_socket.rb +47 -0
- data/lib/em-ftpd/configurator.rb +120 -0
- data/lib/em-ftpd/directories.rb +110 -0
- data/lib/em-ftpd/directory_item.rb +12 -0
- data/lib/em-ftpd/files.rb +141 -0
- data/lib/em-ftpd/passive_socket.rb +30 -0
- data/lib/em-ftpd/server.rb +343 -0
- metadata +158 -0
@@ -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,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
|