ftpd 0.11.0 → 0.12.0
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.
- checksums.yaml +4 -4
- data/Changelog.md +7 -1
- data/Gemfile +1 -1
- data/Gemfile.lock +1 -1
- data/README.md +28 -4
- data/VERSION +1 -1
- data/examples/foo.rb +114 -0
- data/ftpd.gemspec +56 -8
- data/lib/ftpd.rb +77 -31
- data/lib/ftpd/ascii_helper.rb +16 -0
- data/lib/ftpd/cmd_abor.rb +13 -0
- data/lib/ftpd/cmd_allo.rb +20 -0
- data/lib/ftpd/cmd_appe.rb +23 -0
- data/lib/ftpd/cmd_auth.rb +21 -0
- data/lib/ftpd/cmd_cdup.rb +16 -0
- data/lib/ftpd/cmd_cwd.rb +20 -0
- data/lib/ftpd/cmd_dele.rb +21 -0
- data/lib/ftpd/cmd_eprt.rb +23 -0
- data/lib/ftpd/cmd_epsv.rb +30 -0
- data/lib/ftpd/cmd_feat.rb +44 -0
- data/lib/ftpd/cmd_help.rb +29 -0
- data/lib/ftpd/cmd_list.rb +33 -0
- data/lib/ftpd/cmd_login.rb +60 -0
- data/lib/ftpd/cmd_mdtm.rb +27 -0
- data/lib/ftpd/cmd_mkd.rb +23 -0
- data/lib/ftpd/cmd_mode.rb +27 -0
- data/lib/ftpd/cmd_nlst.rb +27 -0
- data/lib/ftpd/cmd_noop.rb +14 -0
- data/lib/ftpd/cmd_opts.rb +14 -0
- data/lib/ftpd/cmd_pasv.rb +28 -0
- data/lib/ftpd/cmd_pbsz.rb +23 -0
- data/lib/ftpd/cmd_port.rb +28 -0
- data/lib/ftpd/cmd_prot.rb +34 -0
- data/lib/ftpd/cmd_pwd.rb +15 -0
- data/lib/ftpd/cmd_quit.rb +18 -0
- data/lib/ftpd/cmd_rein.rb +13 -0
- data/lib/ftpd/cmd_rename.rb +32 -0
- data/lib/ftpd/cmd_rest.rb +13 -0
- data/lib/ftpd/cmd_retr.rb +23 -0
- data/lib/ftpd/cmd_rmd.rb +22 -0
- data/lib/ftpd/cmd_site.rb +13 -0
- data/lib/ftpd/cmd_size.rb +21 -0
- data/lib/ftpd/cmd_smnt.rb +13 -0
- data/lib/ftpd/cmd_stat.rb +15 -0
- data/lib/ftpd/cmd_stor.rb +24 -0
- data/lib/ftpd/cmd_stou.rb +24 -0
- data/lib/ftpd/cmd_stru.rb +27 -0
- data/lib/ftpd/cmd_syst.rb +16 -0
- data/lib/ftpd/cmd_type.rb +28 -0
- data/lib/ftpd/command_handler.rb +91 -0
- data/lib/ftpd/command_handler_factory.rb +51 -0
- data/lib/ftpd/command_handlers.rb +60 -0
- data/lib/ftpd/command_loop.rb +77 -0
- data/lib/ftpd/data_connection_helper.rb +124 -0
- data/lib/ftpd/disk_file_system.rb +22 -6
- data/lib/ftpd/error.rb +4 -0
- data/lib/ftpd/file_system_helper.rb +67 -0
- data/lib/ftpd/ftp_server.rb +1 -1
- data/lib/ftpd/server.rb +1 -0
- data/lib/ftpd/session.rb +31 -749
- data/lib/ftpd/tls_server.rb +2 -0
- data/spec/server_spec.rb +66 -0
- metadata +81 -30
@@ -1,3 +1,5 @@
|
|
1
|
+
require_relative 'translate_exceptions'
|
2
|
+
|
1
3
|
module Ftpd
|
2
4
|
|
3
5
|
class DiskFileSystem
|
@@ -67,6 +69,22 @@ module Ftpd
|
|
67
69
|
end
|
68
70
|
end
|
69
71
|
|
72
|
+
class DiskFileSystem
|
73
|
+
|
74
|
+
# DiskFileSystem mixin for writing files. Used by Append and Write.
|
75
|
+
|
76
|
+
module FileWriting
|
77
|
+
|
78
|
+
def write_file(ftp_path, contents, mode)
|
79
|
+
File.open(expand_ftp_path(ftp_path), mode) do |file|
|
80
|
+
file.write contents
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
70
88
|
class DiskFileSystem
|
71
89
|
|
72
90
|
# DiskFileSystem mixin providing file deletion
|
@@ -121,6 +139,7 @@ module Ftpd
|
|
121
139
|
|
122
140
|
module Write
|
123
141
|
|
142
|
+
include FileWriting
|
124
143
|
include TranslateExceptions
|
125
144
|
|
126
145
|
# Write a file to disk.
|
@@ -134,9 +153,7 @@ module Ftpd
|
|
134
153
|
# If missing, then these commands are not supported.
|
135
154
|
|
136
155
|
def write(ftp_path, contents)
|
137
|
-
|
138
|
-
file.write contents
|
139
|
-
end
|
156
|
+
write_file ftp_path, contents, 'wb'
|
140
157
|
end
|
141
158
|
translate_exceptions :write
|
142
159
|
|
@@ -149,6 +166,7 @@ module Ftpd
|
|
149
166
|
|
150
167
|
module Append
|
151
168
|
|
169
|
+
include FileWriting
|
152
170
|
include TranslateExceptions
|
153
171
|
|
154
172
|
# Append to a file. If the file does not exist, create it.
|
@@ -161,9 +179,7 @@ module Ftpd
|
|
161
179
|
# If missing, then these commands are not supported.
|
162
180
|
|
163
181
|
def append(ftp_path, contents)
|
164
|
-
|
165
|
-
file.write contents
|
166
|
-
end
|
182
|
+
write_file ftp_path, contents, 'ab'
|
167
183
|
end
|
168
184
|
translate_exceptions :append
|
169
185
|
|
data/lib/ftpd/error.rb
CHANGED
@@ -0,0 +1,67 @@
|
|
1
|
+
require_relative 'command_handler'
|
2
|
+
|
3
|
+
module Ftpd
|
4
|
+
|
5
|
+
module FileSystemHelper
|
6
|
+
|
7
|
+
def path_list(path)
|
8
|
+
if file_system.directory?(path)
|
9
|
+
path = File.join(path, '*')
|
10
|
+
end
|
11
|
+
file_system.dir(path).sort
|
12
|
+
end
|
13
|
+
|
14
|
+
def ensure_file_system_supports(method)
|
15
|
+
unless file_system.respond_to?(method)
|
16
|
+
unimplemented_error
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def ensure_accessible(path)
|
21
|
+
unless file_system.accessible?(path)
|
22
|
+
error '550 Access denied'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def ensure_exists(path)
|
27
|
+
unless file_system.exists?(path)
|
28
|
+
error '550 No such file or directory'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def ensure_does_not_exist(path)
|
33
|
+
if file_system.exists?(path)
|
34
|
+
error '550 Already exists'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def ensure_directory(path)
|
39
|
+
unless file_system.directory?(path)
|
40
|
+
error '550 Not a directory'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def unique_path(path)
|
45
|
+
suffix = nil
|
46
|
+
100.times do
|
47
|
+
path_with_suffix = [path, suffix].compact.join('.')
|
48
|
+
unless file_system.exists?(path_with_suffix)
|
49
|
+
return path_with_suffix
|
50
|
+
end
|
51
|
+
suffix = generate_suffix
|
52
|
+
end
|
53
|
+
raise "Unable to find unique path"
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def generate_suffix
|
59
|
+
set = ('a'..'z').to_a
|
60
|
+
8.times.map do
|
61
|
+
set[rand(set.size)]
|
62
|
+
end.join
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
data/lib/ftpd/ftp_server.rb
CHANGED
data/lib/ftpd/server.rb
CHANGED
data/lib/ftpd/session.rb
CHANGED
@@ -1,11 +1,25 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
1
|
module Ftpd
|
4
2
|
class Session
|
5
3
|
|
6
4
|
include Error
|
7
5
|
include ListPath
|
8
6
|
|
7
|
+
attr_accessor :command_sequence_checker
|
8
|
+
attr_accessor :data_channel_protection_level
|
9
|
+
attr_accessor :data_server
|
10
|
+
attr_accessor :data_type
|
11
|
+
attr_accessor :logged_in
|
12
|
+
attr_accessor :name_prefix
|
13
|
+
attr_accessor :protection_buffer_size_set
|
14
|
+
attr_accessor :socket
|
15
|
+
attr_reader :config
|
16
|
+
attr_reader :data_hostname
|
17
|
+
attr_reader :data_port
|
18
|
+
attr_reader :file_system
|
19
|
+
attr_writer :epsv_all
|
20
|
+
attr_writer :mode
|
21
|
+
attr_writer :structure
|
22
|
+
|
9
23
|
# @params session_config [SessionConfig] Session configuration
|
10
24
|
# @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] The socket
|
11
25
|
|
@@ -18,300 +32,31 @@ module Ftpd
|
|
18
32
|
@command_sequence_checker = init_command_sequence_checker
|
19
33
|
set_socket_options
|
20
34
|
@protocols = Protocols.new(@socket)
|
35
|
+
@command_handlers = CommandHandlers.new
|
36
|
+
@command_loop = CommandLoop.new(self)
|
37
|
+
register_commands
|
21
38
|
initialize_session
|
22
39
|
end
|
23
40
|
|
24
41
|
def run
|
25
|
-
|
26
|
-
begin
|
27
|
-
reply "220 #{server_name_and_version}"
|
28
|
-
loop do
|
29
|
-
begin
|
30
|
-
s = get_command
|
31
|
-
s = process_telnet_sequences(s)
|
32
|
-
syntax_error unless s =~ /^(\w+)(?: (.*))?$/
|
33
|
-
command, argument = $1.downcase, $2
|
34
|
-
method = 'cmd_' + command
|
35
|
-
unless respond_to?(method, true)
|
36
|
-
unrecognized_error s
|
37
|
-
end
|
38
|
-
@command_sequence_checker.check command
|
39
|
-
send(method, argument)
|
40
|
-
rescue CommandError => e
|
41
|
-
reply e.message
|
42
|
-
end
|
43
|
-
end
|
44
|
-
rescue Errno::ECONNRESET, Errno::EPIPE
|
45
|
-
end
|
46
|
-
end
|
42
|
+
@command_loop.read_and_execute_commands
|
47
43
|
end
|
48
44
|
|
49
45
|
private
|
50
46
|
|
51
|
-
def
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
end
|
56
|
-
|
57
|
-
def cmd_syst(argument)
|
58
|
-
syntax_error if argument
|
59
|
-
reply "215 UNIX Type: L8"
|
60
|
-
end
|
61
|
-
|
62
|
-
def cmd_user(argument)
|
63
|
-
syntax_error unless argument
|
64
|
-
sequence_error if @logged_in
|
65
|
-
@user = argument
|
66
|
-
if @config.auth_level > AUTH_USER
|
67
|
-
reply "331 Password required"
|
68
|
-
expect 'pass'
|
69
|
-
else
|
70
|
-
login(@user)
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def cmd_pass(argument)
|
75
|
-
syntax_error unless argument
|
76
|
-
@password = argument
|
77
|
-
if @config.auth_level > AUTH_PASSWORD
|
78
|
-
reply "332 Account required"
|
79
|
-
expect 'acct'
|
80
|
-
else
|
81
|
-
login(@user, @password)
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
def cmd_acct(argument)
|
86
|
-
syntax_error unless argument
|
87
|
-
account = argument
|
88
|
-
login(@user, @password, account)
|
89
|
-
end
|
90
|
-
|
91
|
-
def cmd_quit(argument)
|
92
|
-
syntax_error if argument
|
93
|
-
ensure_logged_in
|
94
|
-
reply "221 Byebye"
|
95
|
-
@logged_in = false
|
96
|
-
end
|
97
|
-
|
98
|
-
def syntax_error
|
99
|
-
error "501 Syntax error"
|
100
|
-
end
|
101
|
-
|
102
|
-
def cmd_port(argument)
|
103
|
-
ensure_logged_in
|
104
|
-
ensure_not_epsv_all
|
105
|
-
pieces = argument.split(/,/)
|
106
|
-
syntax_error unless pieces.size == 6
|
107
|
-
pieces.collect! do |s|
|
108
|
-
syntax_error unless s =~ /^\d{1,3}$/
|
109
|
-
i = s.to_i
|
110
|
-
syntax_error unless (0..255) === i
|
111
|
-
i
|
112
|
-
end
|
113
|
-
hostname = pieces[0..3].join('.')
|
114
|
-
port = pieces[4] << 8 | pieces[5]
|
115
|
-
set_active_mode_address hostname, port
|
116
|
-
reply "200 PORT command successful"
|
117
|
-
end
|
118
|
-
|
119
|
-
def cmd_stor(argument)
|
120
|
-
close_data_server_socket_when_done do
|
121
|
-
ensure_logged_in
|
122
|
-
ensure_file_system_supports :write
|
123
|
-
path = argument
|
124
|
-
syntax_error unless path
|
125
|
-
path = File.expand_path(path, @name_prefix)
|
126
|
-
ensure_accessible path
|
127
|
-
ensure_exists File.dirname(path)
|
128
|
-
contents = receive_file
|
129
|
-
@file_system.write path, contents
|
130
|
-
reply "226 Transfer complete"
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
def cmd_stou(argument)
|
135
|
-
close_data_server_socket_when_done do
|
136
|
-
ensure_logged_in
|
137
|
-
ensure_file_system_supports :write
|
138
|
-
path = argument || 'ftpd'
|
139
|
-
path = File.expand_path(path, @name_prefix)
|
140
|
-
path = unique_path(path)
|
141
|
-
ensure_accessible path
|
142
|
-
ensure_exists File.dirname(path)
|
143
|
-
contents = receive_file(File.basename(path))
|
144
|
-
@file_system.write path, contents
|
145
|
-
reply "226 Transfer complete"
|
146
|
-
end
|
147
|
-
end
|
148
|
-
|
149
|
-
def cmd_appe(argument)
|
150
|
-
close_data_server_socket_when_done do
|
151
|
-
ensure_logged_in
|
152
|
-
ensure_file_system_supports :append
|
153
|
-
path = argument
|
154
|
-
syntax_error unless path
|
155
|
-
path = File.expand_path(path, @name_prefix)
|
156
|
-
ensure_accessible path
|
157
|
-
contents = receive_file
|
158
|
-
@file_system.append path, contents
|
159
|
-
reply "226 Transfer complete"
|
160
|
-
end
|
161
|
-
end
|
162
|
-
|
163
|
-
def cmd_retr(argument)
|
164
|
-
close_data_server_socket_when_done do
|
165
|
-
ensure_logged_in
|
166
|
-
ensure_file_system_supports :read
|
167
|
-
path = argument
|
168
|
-
syntax_error unless path
|
169
|
-
path = File.expand_path(path, @name_prefix)
|
170
|
-
ensure_accessible path
|
171
|
-
ensure_exists path
|
172
|
-
contents = @file_system.read(path)
|
173
|
-
transmit_file contents
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
def cmd_dele(argument)
|
178
|
-
ensure_logged_in
|
179
|
-
ensure_file_system_supports :delete
|
180
|
-
path = argument
|
181
|
-
error "501 Path required" unless path
|
182
|
-
path = File.expand_path(path, @name_prefix)
|
183
|
-
ensure_accessible path
|
184
|
-
ensure_exists path
|
185
|
-
@file_system.delete path
|
186
|
-
reply "250 DELE command successful"
|
187
|
-
end
|
188
|
-
|
189
|
-
def cmd_list(argument)
|
190
|
-
close_data_server_socket_when_done do
|
191
|
-
ensure_logged_in
|
192
|
-
ensure_file_system_supports :dir
|
193
|
-
ensure_file_system_supports :file_info
|
194
|
-
path = list_path(argument)
|
195
|
-
path = File.expand_path(path, @name_prefix)
|
196
|
-
transmit_file(list(path), 'A')
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
def cmd_nlst(argument)
|
201
|
-
close_data_server_socket_when_done do
|
202
|
-
ensure_logged_in
|
203
|
-
ensure_file_system_supports :dir
|
204
|
-
path = list_path(argument)
|
205
|
-
path = File.expand_path(path, @name_prefix)
|
206
|
-
transmit_file(name_list(path), 'A')
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
|
-
def cmd_type(argument)
|
211
|
-
ensure_logged_in
|
212
|
-
syntax_error unless argument =~ /^(\S)(?: (\S+))?$/
|
213
|
-
type_code = $1
|
214
|
-
format_code = $2
|
215
|
-
unless argument =~ /^([AEI]( [NTC])?|L .*)$/
|
216
|
-
error '504 Invalid type code'
|
217
|
-
end
|
218
|
-
case argument
|
219
|
-
when /^A( [NT])?$/
|
220
|
-
@data_type = 'A'
|
221
|
-
when /^(I|L 8)$/
|
222
|
-
@data_type = 'I'
|
223
|
-
else
|
224
|
-
error '504 Type not implemented'
|
47
|
+
def register_commands
|
48
|
+
handlers = CommandHandlerFactory.standard_command_handlers
|
49
|
+
handlers.each do |klass|
|
50
|
+
@command_handlers << klass.new(self)
|
225
51
|
end
|
226
|
-
reply "200 Type set to #{@data_type}"
|
227
|
-
end
|
228
|
-
|
229
|
-
def cmd_mode(argument)
|
230
|
-
syntax_error unless argument
|
231
|
-
ensure_logged_in
|
232
|
-
name, implemented = TRANSMISSION_MODES[argument]
|
233
|
-
error "504 Invalid mode code" unless name
|
234
|
-
error "504 Mode not implemented" unless implemented
|
235
|
-
@mode = argument
|
236
|
-
reply "200 Mode set to #{name}"
|
237
52
|
end
|
238
53
|
|
239
|
-
def
|
240
|
-
|
241
|
-
ensure_logged_in
|
242
|
-
name, implemented = FILE_STRUCTURES[argument]
|
243
|
-
error "504 Invalid structure code" unless name
|
244
|
-
error "504 Structure not implemented" unless implemented
|
245
|
-
@structure = argument
|
246
|
-
reply "200 File structure set to #{name}"
|
54
|
+
def valid_command?(command)
|
55
|
+
@command_handlers.has?(command)
|
247
56
|
end
|
248
57
|
|
249
|
-
def
|
250
|
-
|
251
|
-
reply "200 Nothing done"
|
252
|
-
end
|
253
|
-
|
254
|
-
def cmd_pasv(argument)
|
255
|
-
ensure_logged_in
|
256
|
-
ensure_not_epsv_all
|
257
|
-
if @data_server
|
258
|
-
reply "200 Already in passive mode"
|
259
|
-
else
|
260
|
-
interface = @socket.addr[3]
|
261
|
-
@data_server = TCPServer.new(interface, 0)
|
262
|
-
ip = @data_server.addr[3]
|
263
|
-
port = @data_server.addr[1]
|
264
|
-
quads = [
|
265
|
-
ip.scan(/\d+/),
|
266
|
-
port >> 8,
|
267
|
-
port & 0xff,
|
268
|
-
].flatten.join(',')
|
269
|
-
reply "227 Entering passive mode (#{quads})"
|
270
|
-
end
|
271
|
-
end
|
272
|
-
|
273
|
-
def cmd_cwd(argument)
|
274
|
-
ensure_logged_in
|
275
|
-
path = File.expand_path(argument, @name_prefix)
|
276
|
-
ensure_accessible path
|
277
|
-
ensure_exists path
|
278
|
-
ensure_directory path
|
279
|
-
@name_prefix = path
|
280
|
-
pwd 250
|
281
|
-
end
|
282
|
-
alias cmd_xcwd :cmd_cwd
|
283
|
-
|
284
|
-
def cmd_mkd(argument)
|
285
|
-
syntax_error unless argument
|
286
|
-
ensure_logged_in
|
287
|
-
ensure_file_system_supports :mkdir
|
288
|
-
path = File.expand_path(argument, @name_prefix)
|
289
|
-
ensure_accessible path
|
290
|
-
ensure_exists File.dirname(path)
|
291
|
-
ensure_directory File.dirname(path)
|
292
|
-
ensure_does_not_exist path
|
293
|
-
@file_system.mkdir path
|
294
|
-
reply %Q'257 "#{path}" created'
|
295
|
-
end
|
296
|
-
alias cmd_xmkd :cmd_mkd
|
297
|
-
|
298
|
-
def cmd_rmd(argument)
|
299
|
-
syntax_error unless argument
|
300
|
-
ensure_logged_in
|
301
|
-
ensure_file_system_supports :rmdir
|
302
|
-
path = File.expand_path(argument, @name_prefix)
|
303
|
-
ensure_accessible path
|
304
|
-
ensure_exists path
|
305
|
-
ensure_directory path
|
306
|
-
@file_system.rmdir path
|
307
|
-
reply '250 RMD command successful'
|
308
|
-
end
|
309
|
-
alias cmd_xrmd :cmd_rmd
|
310
|
-
|
311
|
-
def ensure_file_system_supports(method)
|
312
|
-
unless @file_system.respond_to?(method)
|
313
|
-
unimplemented_error
|
314
|
-
end
|
58
|
+
def execute_command command, argument
|
59
|
+
@command_handlers.execute command, argument
|
315
60
|
end
|
316
61
|
|
317
62
|
def ensure_logged_in
|
@@ -319,30 +64,6 @@ module Ftpd
|
|
319
64
|
error "530 Not logged in"
|
320
65
|
end
|
321
66
|
|
322
|
-
def ensure_accessible(path)
|
323
|
-
unless @file_system.accessible?(path)
|
324
|
-
error '550 Access denied'
|
325
|
-
end
|
326
|
-
end
|
327
|
-
|
328
|
-
def ensure_exists(path)
|
329
|
-
unless @file_system.exists?(path)
|
330
|
-
error '550 No such file or directory'
|
331
|
-
end
|
332
|
-
end
|
333
|
-
|
334
|
-
def ensure_does_not_exist(path)
|
335
|
-
if @file_system.exists?(path)
|
336
|
-
error '550 Already exists'
|
337
|
-
end
|
338
|
-
end
|
339
|
-
|
340
|
-
def ensure_directory(path)
|
341
|
-
unless @file_system.directory?(path)
|
342
|
-
error '550 Not a directory'
|
343
|
-
end
|
344
|
-
end
|
345
|
-
|
346
67
|
def ensure_tls_supported
|
347
68
|
unless tls_enabled?
|
348
69
|
error "534 TLS not enabled"
|
@@ -359,146 +80,6 @@ module Ftpd
|
|
359
80
|
@config.tls != :off
|
360
81
|
end
|
361
82
|
|
362
|
-
def cmd_cdup(argument)
|
363
|
-
syntax_error if argument
|
364
|
-
ensure_logged_in
|
365
|
-
cmd_cwd('..')
|
366
|
-
end
|
367
|
-
alias cmd_xcup :cmd_cdup
|
368
|
-
|
369
|
-
def cmd_pwd(argument)
|
370
|
-
ensure_logged_in
|
371
|
-
pwd 257
|
372
|
-
end
|
373
|
-
alias cmd_xpwd :cmd_pwd
|
374
|
-
|
375
|
-
def cmd_auth(security_scheme)
|
376
|
-
ensure_tls_supported
|
377
|
-
if @socket.encrypted?
|
378
|
-
error "503 AUTH already done"
|
379
|
-
end
|
380
|
-
unless security_scheme =~ /^TLS(-C)?$/i
|
381
|
-
error "504 Security scheme not implemented: #{security_scheme}"
|
382
|
-
end
|
383
|
-
reply "234 AUTH #{security_scheme} OK."
|
384
|
-
@socket.encrypt
|
385
|
-
end
|
386
|
-
|
387
|
-
def cmd_pbsz(buffer_size)
|
388
|
-
ensure_tls_supported
|
389
|
-
syntax_error unless buffer_size =~ /^\d+$/
|
390
|
-
buffer_size = buffer_size.to_i
|
391
|
-
unless @socket.encrypted?
|
392
|
-
error "503 PBSZ must be preceded by AUTH"
|
393
|
-
end
|
394
|
-
unless buffer_size == 0
|
395
|
-
error "501 PBSZ=0"
|
396
|
-
end
|
397
|
-
reply "200 PBSZ=0"
|
398
|
-
@protection_buffer_size_set = true
|
399
|
-
end
|
400
|
-
|
401
|
-
def cmd_prot(level_arg)
|
402
|
-
level_code = level_arg.upcase
|
403
|
-
unless @protection_buffer_size_set
|
404
|
-
error "503 PROT must be preceded by PBSZ"
|
405
|
-
end
|
406
|
-
level = DATA_CHANNEL_PROTECTION_LEVELS[level_code]
|
407
|
-
unless level
|
408
|
-
error "504 Unknown protection level"
|
409
|
-
end
|
410
|
-
unless level == :private
|
411
|
-
error "536 Unsupported protection level #{level}"
|
412
|
-
end
|
413
|
-
@data_channel_protection_level = level
|
414
|
-
reply "200 Data protection level #{level_code}"
|
415
|
-
end
|
416
|
-
|
417
|
-
def cmd_rnfr(argument)
|
418
|
-
ensure_logged_in
|
419
|
-
ensure_file_system_supports :rename
|
420
|
-
syntax_error unless argument
|
421
|
-
from_path = File.expand_path(argument, @name_prefix)
|
422
|
-
ensure_accessible from_path
|
423
|
-
ensure_exists from_path
|
424
|
-
@rename_from_path = from_path
|
425
|
-
reply '350 RNFR accepted; ready for destination'
|
426
|
-
expect 'rnto'
|
427
|
-
end
|
428
|
-
|
429
|
-
def cmd_rnto(argument)
|
430
|
-
ensure_logged_in
|
431
|
-
ensure_file_system_supports :rename
|
432
|
-
syntax_error unless argument
|
433
|
-
to_path = File.expand_path(argument, @name_prefix)
|
434
|
-
ensure_accessible to_path
|
435
|
-
ensure_does_not_exist to_path
|
436
|
-
@file_system.rename(@rename_from_path, to_path)
|
437
|
-
reply '250 Rename successful'
|
438
|
-
end
|
439
|
-
|
440
|
-
def cmd_help(argument)
|
441
|
-
if argument
|
442
|
-
command = argument.upcase
|
443
|
-
if supported_commands.include?(command)
|
444
|
-
reply "214 Command #{command} is recognized"
|
445
|
-
else
|
446
|
-
reply "214 Command #{command} is not recognized"
|
447
|
-
end
|
448
|
-
else
|
449
|
-
reply '214-The following commands are recognized:'
|
450
|
-
supported_commands.sort.each_slice(8) do |commands|
|
451
|
-
line = commands.map do |command|
|
452
|
-
' %-4s' % command
|
453
|
-
end.join
|
454
|
-
reply line
|
455
|
-
end
|
456
|
-
reply '214 Have a nice day.'
|
457
|
-
end
|
458
|
-
end
|
459
|
-
|
460
|
-
def cmd_stat(argument)
|
461
|
-
ensure_logged_in
|
462
|
-
syntax_error if argument
|
463
|
-
reply "211 #{server_name_and_version}"
|
464
|
-
end
|
465
|
-
|
466
|
-
def self.unimplemented(command)
|
467
|
-
method_name = "cmd_#{command}"
|
468
|
-
define_method method_name do |arguments|
|
469
|
-
unimplemented_error
|
470
|
-
end
|
471
|
-
private method_name
|
472
|
-
end
|
473
|
-
|
474
|
-
def cmd_feat(argument)
|
475
|
-
syntax_error if argument
|
476
|
-
reply '211-Extensions supported:'
|
477
|
-
extensions.each do |extension|
|
478
|
-
reply " #{extension}"
|
479
|
-
end
|
480
|
-
reply '211 END'
|
481
|
-
end
|
482
|
-
|
483
|
-
def cmd_opts(argument)
|
484
|
-
syntax_error unless argument
|
485
|
-
error '501 Unsupported option'
|
486
|
-
end
|
487
|
-
|
488
|
-
def cmd_eprt(argument)
|
489
|
-
ensure_logged_in
|
490
|
-
ensure_not_epsv_all
|
491
|
-
delim = argument[0..0]
|
492
|
-
parts = argument.split(delim)[1..-1]
|
493
|
-
syntax_error unless parts.size == 3
|
494
|
-
protocol_code, address, port = *parts
|
495
|
-
protocol_code = protocol_code.to_i
|
496
|
-
ensure_protocol_supported protocol_code
|
497
|
-
port = port.to_i
|
498
|
-
set_active_mode_address address, port
|
499
|
-
reply "200 EPRT command successful"
|
500
|
-
end
|
501
|
-
|
502
83
|
def ensure_protocol_supported(protocol_code)
|
503
84
|
unless @protocols.supports_protocol?(protocol_code)
|
504
85
|
protocol_list = @protocols.protocol_codes.join(',')
|
@@ -507,103 +88,14 @@ module Ftpd
|
|
507
88
|
end
|
508
89
|
end
|
509
90
|
|
510
|
-
def cmd_epsv(argument)
|
511
|
-
ensure_logged_in
|
512
|
-
if @data_server
|
513
|
-
reply "200 Already in passive mode"
|
514
|
-
else
|
515
|
-
if argument == 'ALL'
|
516
|
-
@epsv_all = true
|
517
|
-
reply "220 EPSV now required for port setup"
|
518
|
-
else
|
519
|
-
protocol_code = argument && argument.to_i
|
520
|
-
if protocol_code
|
521
|
-
ensure_protocol_supported protocol_code
|
522
|
-
end
|
523
|
-
interface = @socket.addr[3]
|
524
|
-
@data_server = TCPServer.new(interface, 0)
|
525
|
-
port = @data_server.addr[1]
|
526
|
-
reply "229 Entering extended passive mode (|||#{port}|)"
|
527
|
-
end
|
528
|
-
end
|
529
|
-
end
|
530
|
-
|
531
|
-
def cmd_mdtm(path)
|
532
|
-
ensure_logged_in
|
533
|
-
ensure_file_system_supports :dir
|
534
|
-
ensure_file_system_supports :file_info
|
535
|
-
syntax_error unless path
|
536
|
-
path = File.expand_path(path, @name_prefix)
|
537
|
-
ensure_accessible(path)
|
538
|
-
ensure_exists(path)
|
539
|
-
info = @file_system.file_info(path)
|
540
|
-
mtime = info.mtime.utc
|
541
|
-
# We would like to report fractional seconds, too. Sadly, the
|
542
|
-
# spec declares that we may not report more precision than is
|
543
|
-
# actually there, and there is no spec or API to tell us how
|
544
|
-
# many fractional digits are significant.
|
545
|
-
mtime = mtime.strftime("%Y%m%d%H%M%S")
|
546
|
-
reply "213 #{mtime}"
|
547
|
-
end
|
548
|
-
|
549
|
-
def cmd_size(path)
|
550
|
-
ensure_logged_in
|
551
|
-
ensure_file_system_supports :read
|
552
|
-
syntax_error unless path
|
553
|
-
path = File.expand_path(path, @name_prefix)
|
554
|
-
ensure_accessible(path)
|
555
|
-
ensure_exists(path)
|
556
|
-
contents = @file_system.read(path)
|
557
|
-
contents = (@data_type == 'A') ? unix_to_nvt_ascii(contents) : contents
|
558
|
-
reply "213 #{contents.bytesize}"
|
559
|
-
end
|
560
|
-
|
561
|
-
unimplemented :abor
|
562
|
-
unimplemented :rein
|
563
|
-
unimplemented :rest
|
564
|
-
unimplemented :site
|
565
|
-
unimplemented :smnt
|
566
|
-
|
567
|
-
def extensions
|
568
|
-
[
|
569
|
-
(TLS_EXTENSIONS if tls_enabled?),
|
570
|
-
IPV6_EXTENSIONS,
|
571
|
-
RFC_3659_EXTENSIONS,
|
572
|
-
].flatten.compact
|
573
|
-
end
|
574
|
-
|
575
|
-
TLS_EXTENSIONS = [
|
576
|
-
'AUTH TLS',
|
577
|
-
'PBSZ',
|
578
|
-
'PROT'
|
579
|
-
]
|
580
|
-
|
581
|
-
IPV6_EXTENSIONS = [
|
582
|
-
'EPRT',
|
583
|
-
'EPSV',
|
584
|
-
]
|
585
|
-
|
586
|
-
RFC_3659_EXTENSIONS = [
|
587
|
-
'MDTM',
|
588
|
-
'SIZE',
|
589
|
-
]
|
590
|
-
|
591
91
|
def supported_commands
|
592
|
-
|
593
|
-
method.to_s[/^cmd_(\w+)$/, 1]
|
594
|
-
end.compact.map(&:upcase)
|
92
|
+
@command_handlers.commands.map(&:upcase)
|
595
93
|
end
|
596
94
|
|
597
95
|
def pwd(status_code)
|
598
96
|
reply %Q(#{status_code} "#{@name_prefix}" is current directory)
|
599
97
|
end
|
600
98
|
|
601
|
-
TRANSMISSION_MODES = {
|
602
|
-
'B'=>['Block', false],
|
603
|
-
'C'=>['Compressed', false],
|
604
|
-
'S'=>['Stream', true],
|
605
|
-
}
|
606
|
-
|
607
99
|
FORMAT_TYPES = {
|
608
100
|
'N'=>['Non-print', true],
|
609
101
|
'T'=>['Telnet format effectors', true],
|
@@ -617,19 +109,6 @@ module Ftpd
|
|
617
109
|
'L'=>['LOCAL', false],
|
618
110
|
}
|
619
111
|
|
620
|
-
FILE_STRUCTURES = {
|
621
|
-
'R'=>['Record', false],
|
622
|
-
'F'=>['File', true],
|
623
|
-
'P'=>['Page', false],
|
624
|
-
}
|
625
|
-
|
626
|
-
DATA_CHANNEL_PROTECTION_LEVELS = {
|
627
|
-
'C'=>:clear,
|
628
|
-
'S'=>:safe,
|
629
|
-
'E'=>:confidential,
|
630
|
-
'P'=>:private
|
631
|
-
}
|
632
|
-
|
633
112
|
def expect(command)
|
634
113
|
@command_sequence_checker.expect command
|
635
114
|
end
|
@@ -638,159 +117,16 @@ module Ftpd
|
|
638
117
|
@file_system = FileSystemErrorTranslator.new(file_system)
|
639
118
|
end
|
640
119
|
|
641
|
-
def transmit_file(contents, data_type = @data_type)
|
642
|
-
open_data_connection do |data_socket|
|
643
|
-
contents = unix_to_nvt_ascii(contents) if data_type == 'A'
|
644
|
-
handle_data_disconnect do
|
645
|
-
data_socket.write(contents)
|
646
|
-
end
|
647
|
-
@config.log.debug "Sent #{contents.size} bytes"
|
648
|
-
reply "226 Transfer complete"
|
649
|
-
end
|
650
|
-
end
|
651
|
-
|
652
|
-
def receive_file(path_to_advertise = nil)
|
653
|
-
open_data_connection(path_to_advertise) do |data_socket|
|
654
|
-
contents = handle_data_disconnect do
|
655
|
-
data_socket.read
|
656
|
-
end
|
657
|
-
contents = nvt_ascii_to_unix(contents) if @data_type == 'A'
|
658
|
-
@config.log.debug "Received #{contents.size} bytes"
|
659
|
-
contents
|
660
|
-
end
|
661
|
-
end
|
662
|
-
|
663
|
-
def handle_data_disconnect
|
664
|
-
return yield
|
665
|
-
rescue Errno::ECONNRESET, Errno::EPIPE
|
666
|
-
reply "426 Connection closed; transfer aborted."
|
667
|
-
end
|
668
|
-
|
669
|
-
def unix_to_nvt_ascii(s)
|
670
|
-
return s if s =~ /\r\n/
|
671
|
-
s.gsub(/\n/, "\r\n")
|
672
|
-
end
|
673
|
-
|
674
|
-
def nvt_ascii_to_unix(s)
|
675
|
-
s.gsub(/\r\n/, "\n")
|
676
|
-
end
|
677
|
-
|
678
|
-
def open_data_connection(path_to_advertise = nil, &block)
|
679
|
-
send_start_of_data_connection_reply(path_to_advertise)
|
680
|
-
if @data_server
|
681
|
-
if encrypt_data?
|
682
|
-
open_passive_tls_data_connection(&block)
|
683
|
-
else
|
684
|
-
open_passive_data_connection(&block)
|
685
|
-
end
|
686
|
-
else
|
687
|
-
if encrypt_data?
|
688
|
-
open_active_tls_data_connection(&block)
|
689
|
-
else
|
690
|
-
open_active_data_connection(&block)
|
691
|
-
end
|
692
|
-
end
|
693
|
-
end
|
694
|
-
|
695
|
-
def send_start_of_data_connection_reply(path)
|
696
|
-
if path
|
697
|
-
reply "150 FILE: #{path}"
|
698
|
-
else
|
699
|
-
reply "150 Opening #{data_connection_description}"
|
700
|
-
end
|
701
|
-
end
|
702
|
-
|
703
|
-
def data_connection_description
|
704
|
-
[
|
705
|
-
DATA_TYPES[@data_type][0],
|
706
|
-
"mode data connection",
|
707
|
-
("(TLS)" if encrypt_data?)
|
708
|
-
].compact.join(' ')
|
709
|
-
end
|
710
|
-
|
711
120
|
def command_not_needed
|
712
121
|
reply '202 Command not needed at this site'
|
713
122
|
end
|
714
123
|
|
715
|
-
def encrypt_data?
|
716
|
-
@data_channel_protection_level != :clear
|
717
|
-
end
|
718
|
-
|
719
|
-
def open_active_data_connection
|
720
|
-
data_socket = TCPSocket.new(@data_hostname, @data_port)
|
721
|
-
begin
|
722
|
-
yield(data_socket)
|
723
|
-
ensure
|
724
|
-
data_socket.close
|
725
|
-
end
|
726
|
-
end
|
727
|
-
|
728
|
-
def open_active_tls_data_connection
|
729
|
-
open_active_data_connection do |socket|
|
730
|
-
make_tls_connection(socket) do |ssl_socket|
|
731
|
-
yield(ssl_socket)
|
732
|
-
end
|
733
|
-
end
|
734
|
-
end
|
735
|
-
|
736
|
-
def open_passive_data_connection
|
737
|
-
data_socket = @data_server.accept
|
738
|
-
begin
|
739
|
-
yield(data_socket)
|
740
|
-
ensure
|
741
|
-
data_socket.close
|
742
|
-
end
|
743
|
-
end
|
744
|
-
|
745
|
-
def close_data_server_socket_when_done
|
746
|
-
yield
|
747
|
-
ensure
|
748
|
-
close_data_server_socket
|
749
|
-
end
|
750
|
-
|
751
124
|
def close_data_server_socket
|
752
125
|
return unless @data_server
|
753
126
|
@data_server.close
|
754
127
|
@data_server = nil
|
755
128
|
end
|
756
129
|
|
757
|
-
def open_passive_tls_data_connection
|
758
|
-
open_passive_data_connection do |socket|
|
759
|
-
make_tls_connection(socket) do |ssl_socket|
|
760
|
-
yield(ssl_socket)
|
761
|
-
end
|
762
|
-
end
|
763
|
-
end
|
764
|
-
|
765
|
-
def make_tls_connection(socket)
|
766
|
-
ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, @socket.ssl_context)
|
767
|
-
ssl_socket.accept
|
768
|
-
begin
|
769
|
-
yield(ssl_socket)
|
770
|
-
ensure
|
771
|
-
ssl_socket.close
|
772
|
-
end
|
773
|
-
end
|
774
|
-
|
775
|
-
def get_command
|
776
|
-
s = gets_with_timeout(@socket)
|
777
|
-
throw :done if s.nil?
|
778
|
-
s = s.chomp
|
779
|
-
@config.log.debug s
|
780
|
-
s
|
781
|
-
end
|
782
|
-
|
783
|
-
def gets_with_timeout(socket)
|
784
|
-
ready = IO.select([@socket], nil, nil, @config.session_timeout)
|
785
|
-
timeout if ready.nil?
|
786
|
-
ready[0].first.gets
|
787
|
-
end
|
788
|
-
|
789
|
-
def timeout
|
790
|
-
reply '421 Control connection timed out.'
|
791
|
-
throw :done
|
792
|
-
end
|
793
|
-
|
794
130
|
def reply(s)
|
795
131
|
if @config.response_delay.to_i != 0
|
796
132
|
@config.log.warn "#{@config.response_delay} second delay before replying"
|
@@ -800,25 +136,6 @@ module Ftpd
|
|
800
136
|
@socket.write s + "\r\n"
|
801
137
|
end
|
802
138
|
|
803
|
-
def unique_path(path)
|
804
|
-
suffix = nil
|
805
|
-
100.times do
|
806
|
-
path_with_suffix = [path, suffix].compact.join('.')
|
807
|
-
unless @file_system.exists?(path_with_suffix)
|
808
|
-
return path_with_suffix
|
809
|
-
end
|
810
|
-
suffix = generate_suffix
|
811
|
-
end
|
812
|
-
raise "Unable to find unique path"
|
813
|
-
end
|
814
|
-
|
815
|
-
def generate_suffix
|
816
|
-
set = ('a'..'z').to_a
|
817
|
-
8.times.map do
|
818
|
-
set[rand(set.size)]
|
819
|
-
end.join
|
820
|
-
end
|
821
|
-
|
822
139
|
def init_command_sequence_checker
|
823
140
|
checker = CommandSequenceChecker.new
|
824
141
|
checker.must_expect 'acct'
|
@@ -827,30 +144,6 @@ module Ftpd
|
|
827
144
|
checker
|
828
145
|
end
|
829
146
|
|
830
|
-
def list(path)
|
831
|
-
format_list(path_list(path))
|
832
|
-
end
|
833
|
-
|
834
|
-
def format_list(paths)
|
835
|
-
paths.map do |path|
|
836
|
-
file_info = @file_system.file_info(path)
|
837
|
-
@config.list_formatter.new(file_info).to_s + "\n"
|
838
|
-
end.join
|
839
|
-
end
|
840
|
-
|
841
|
-
def name_list(path)
|
842
|
-
path_list(path).map do |path|
|
843
|
-
File.basename(path) + "\n"
|
844
|
-
end.join
|
845
|
-
end
|
846
|
-
|
847
|
-
def path_list(path)
|
848
|
-
if @file_system.directory?(path)
|
849
|
-
path = File.join(path, '*')
|
850
|
-
end
|
851
|
-
@file_system.dir(path).sort
|
852
|
-
end
|
853
|
-
|
854
147
|
def authenticate(*args)
|
855
148
|
while args.size < @config.driver.method(:authenticate).arity
|
856
149
|
args << nil
|
@@ -859,12 +152,13 @@ module Ftpd
|
|
859
152
|
end
|
860
153
|
|
861
154
|
def login(*auth_tokens)
|
155
|
+
user = auth_tokens.first
|
862
156
|
unless authenticate(*auth_tokens)
|
863
157
|
failed_auth
|
864
158
|
error "530 Login incorrect"
|
865
159
|
end
|
866
160
|
reply "230 Logged in"
|
867
|
-
set_file_system @config.driver.file_system(
|
161
|
+
set_file_system @config.driver.file_system(user)
|
868
162
|
@logged_in = true
|
869
163
|
reset_failed_auths
|
870
164
|
end
|
@@ -882,14 +176,6 @@ module Ftpd
|
|
882
176
|
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_OOBINLINE, 1)
|
883
177
|
end
|
884
178
|
|
885
|
-
def process_telnet_sequences(s)
|
886
|
-
telnet = Telnet.new(s)
|
887
|
-
unless telnet.reply.empty?
|
888
|
-
@socket.write telnet.reply
|
889
|
-
end
|
890
|
-
telnet.plain
|
891
|
-
end
|
892
|
-
|
893
179
|
def reset_failed_auths
|
894
180
|
@failed_auths = 0
|
895
181
|
end
|
@@ -903,10 +189,6 @@ module Ftpd
|
|
903
189
|
end
|
904
190
|
end
|
905
191
|
|
906
|
-
def set_data_address(n)
|
907
|
-
|
908
|
-
end
|
909
|
-
|
910
192
|
def set_active_mode_address(address, port)
|
911
193
|
if port > 0xffff || port < 1024 && !@config.allow_low_data_ports
|
912
194
|
error "504 Command not implemented for that parameter"
|