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