ftpd 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of ftpd might be problematic. Click here for more details.
- data/Changelog.md +24 -3
- data/Gemfile +2 -1
- data/Gemfile.lock +9 -2
- data/README.md +20 -9
- data/VERSION +1 -1
- data/doc/rfc.md +277 -0
- data/features/ftp_server/cdup.feature +36 -0
- data/features/ftp_server/command_errors.feature +0 -4
- data/features/ftp_server/delete.feature +1 -1
- data/features/ftp_server/directory_navigation.feature +18 -6
- data/features/ftp_server/get.feature +1 -1
- data/features/ftp_server/get_tls.feature +2 -2
- data/features/ftp_server/implicit_tls.feature +18 -0
- data/features/ftp_server/list_tls.feature +2 -2
- data/features/ftp_server/mkdir.feature +70 -0
- data/features/ftp_server/name_list_tls.feature +2 -2
- data/features/ftp_server/put.feature +1 -1
- data/features/ftp_server/put_tls.feature +2 -2
- data/features/ftp_server/rename.feature +90 -0
- data/features/ftp_server/rmdir.feature +71 -0
- data/features/ftp_server/step_definitions/debug.rb +6 -6
- data/features/ftp_server/step_definitions/test_server.rb +3 -2
- data/features/step_definitions/connect.rb +4 -3
- data/features/step_definitions/{directories.rb → directory_navigation.rb} +4 -0
- data/features/step_definitions/error_replies.rb +5 -5
- data/features/step_definitions/login.rb +2 -2
- data/features/step_definitions/mkdir.rb +9 -0
- data/features/step_definitions/rename.rb +11 -0
- data/features/step_definitions/rmdir.rb +9 -0
- data/features/step_definitions/server_files.rb +9 -0
- data/features/support/test_client.rb +19 -5
- data/features/support/test_server.rb +28 -3
- data/features/support/test_server_files.rb +5 -0
- data/ftpd.gemspec +17 -6
- data/lib/ftpd.rb +1 -0
- data/lib/ftpd/disk_file_system.rb +97 -16
- data/lib/ftpd/error.rb +16 -0
- data/lib/ftpd/exception_translator.rb +1 -1
- data/lib/ftpd/exceptions.rb +10 -4
- data/lib/ftpd/file_system_error_translator.rb +8 -4
- data/lib/ftpd/ftp_server.rb +1 -0
- data/lib/ftpd/session.rb +98 -87
- data/rake_tasks/yard.rake +1 -0
- data/spec/disk_file_system_spec.rb +55 -8
- data/spec/exception_translator_spec.rb +1 -1
- data/spec/file_system_error_translator_spec.rb +20 -4
- data/spec/translate_exceptions_spec.rb +1 -1
- metadata +32 -7
- data/sandbox/em-server.rb +0 -37
data/lib/ftpd/error.rb
CHANGED
@@ -5,5 +5,21 @@ module Ftpd
|
|
5
5
|
raise CommandError, message
|
6
6
|
end
|
7
7
|
|
8
|
+
def transient_error(message)
|
9
|
+
error "450 #{message}"
|
10
|
+
end
|
11
|
+
|
12
|
+
def unrecognized_error(s)
|
13
|
+
error "500 Syntax error, command unrecognized: #{s.chomp}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def unimplemented_error
|
17
|
+
error "502 Command not implemented"
|
18
|
+
end
|
19
|
+
|
20
|
+
def permanent_error(message)
|
21
|
+
error "550 #{message}"
|
22
|
+
end
|
23
|
+
|
8
24
|
end
|
9
25
|
end
|
data/lib/ftpd/exceptions.rb
CHANGED
@@ -11,11 +11,17 @@ module Ftpd
|
|
11
11
|
|
12
12
|
class CommandError < FtpServerError ; end
|
13
13
|
|
14
|
-
#
|
15
|
-
#
|
16
|
-
# way to generate FileSystemError exceptions from other types of
|
17
|
-
# exceptions.
|
14
|
+
# A permanent file system error. Deprecated; use
|
15
|
+
# PermanentFileSystemError instead.
|
18
16
|
|
19
17
|
class FileSystemError < FtpServerError ; end
|
20
18
|
|
19
|
+
# A permanent file system error. The file isn't there, etc.
|
20
|
+
|
21
|
+
class PermanentFileSystemError < FtpServerError ; end
|
22
|
+
|
23
|
+
# A transient file system error. The file is busy, etc.
|
24
|
+
|
25
|
+
class TransientFileSystemError < FtpServerError ; end
|
26
|
+
|
21
27
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module Ftpd
|
2
2
|
|
3
|
-
#
|
4
|
-
#
|
3
|
+
# A proxy file system driver that sends a "450" or "550" error
|
4
|
+
# reply in response to FileSystemError exceptions.
|
5
5
|
|
6
6
|
class FileSystemErrorTranslator
|
7
7
|
|
@@ -12,13 +12,17 @@ module Ftpd
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def respond_to?(method)
|
15
|
-
@file_system.respond_to?(method)
|
15
|
+
@file_system.respond_to?(method) || super
|
16
16
|
end
|
17
17
|
|
18
18
|
def method_missing(method, *args)
|
19
19
|
@file_system.send(method, *args)
|
20
|
+
rescue PermanentFileSystemError => e
|
21
|
+
permanent_error e
|
22
|
+
rescue TransientFileSystemError => e
|
23
|
+
transient_error e
|
20
24
|
rescue FileSystemError => e
|
21
|
-
|
25
|
+
permanent_error e
|
22
26
|
end
|
23
27
|
|
24
28
|
end
|
data/lib/ftpd/ftp_server.rb
CHANGED
data/lib/ftpd/session.rb
CHANGED
@@ -8,6 +8,7 @@ module Ftpd
|
|
8
8
|
def initialize(opts)
|
9
9
|
@driver = opts[:driver]
|
10
10
|
@socket = opts[:socket]
|
11
|
+
@interface = opts[:interface]
|
11
12
|
@tls = opts[:tls]
|
12
13
|
if @tls == :implicit
|
13
14
|
@socket.encrypt
|
@@ -32,12 +33,9 @@ module Ftpd
|
|
32
33
|
s = get_command
|
33
34
|
syntax_error unless s =~ /^(\w+)(?: (.*))?$/
|
34
35
|
command, argument = $1.downcase, $2
|
35
|
-
unless VALID_COMMANDS.include?(command)
|
36
|
-
error "500 Syntax error, command unrecognized: #{s}"
|
37
|
-
end
|
38
36
|
method = 'cmd_' + command
|
39
|
-
unless
|
40
|
-
|
37
|
+
unless respond_to?(method, true)
|
38
|
+
unrecognized_error s
|
41
39
|
end
|
42
40
|
send(method, argument)
|
43
41
|
rescue CommandError => e
|
@@ -50,45 +48,6 @@ module Ftpd
|
|
50
48
|
|
51
49
|
private
|
52
50
|
|
53
|
-
VALID_COMMANDS = [
|
54
|
-
"abor",
|
55
|
-
"acct",
|
56
|
-
"allo",
|
57
|
-
"appe",
|
58
|
-
"auth",
|
59
|
-
"pbsz",
|
60
|
-
"cdup",
|
61
|
-
"cwd",
|
62
|
-
"dele",
|
63
|
-
"help",
|
64
|
-
"list",
|
65
|
-
"mkd",
|
66
|
-
"mode",
|
67
|
-
"nlst",
|
68
|
-
"noop",
|
69
|
-
"pass",
|
70
|
-
"pasv",
|
71
|
-
"port",
|
72
|
-
"prot",
|
73
|
-
"pwd",
|
74
|
-
"quit",
|
75
|
-
"rein",
|
76
|
-
"rest",
|
77
|
-
"retr",
|
78
|
-
"rmd",
|
79
|
-
"rnfr",
|
80
|
-
"rnto",
|
81
|
-
"site",
|
82
|
-
"smnt",
|
83
|
-
"stat",
|
84
|
-
"stor",
|
85
|
-
"stou",
|
86
|
-
"stru",
|
87
|
-
"syst",
|
88
|
-
"type",
|
89
|
-
"user",
|
90
|
-
]
|
91
|
-
|
92
51
|
def cmd_allo(argument)
|
93
52
|
ensure_logged_in
|
94
53
|
syntax_error unless argument =~ /^\d+( R \d+)?$/
|
@@ -102,23 +61,19 @@ module Ftpd
|
|
102
61
|
|
103
62
|
def cmd_user(argument)
|
104
63
|
syntax_error unless argument
|
105
|
-
|
64
|
+
sequence_error unless @state == :user
|
106
65
|
@user = argument
|
107
66
|
@state = :password
|
108
67
|
reply "331 Password required"
|
109
68
|
end
|
110
69
|
|
111
|
-
def
|
70
|
+
def sequence_error
|
112
71
|
error "503 Bad sequence of commands"
|
113
72
|
end
|
114
73
|
|
115
|
-
def unimplemented
|
116
|
-
error "502 Command not implemented"
|
117
|
-
end
|
118
|
-
|
119
74
|
def cmd_pass(argument)
|
120
75
|
syntax_error unless argument
|
121
|
-
|
76
|
+
sequence_error unless @state == :password
|
122
77
|
password = argument
|
123
78
|
unless @driver.authenticate(@user, password)
|
124
79
|
@state = :user
|
@@ -158,7 +113,7 @@ module Ftpd
|
|
158
113
|
def cmd_stor(argument)
|
159
114
|
close_data_server_socket_when_done do
|
160
115
|
ensure_logged_in
|
161
|
-
|
116
|
+
ensure_file_system_supports :write
|
162
117
|
path = argument
|
163
118
|
syntax_error unless path
|
164
119
|
path = File.expand_path(path, @name_prefix)
|
@@ -173,7 +128,7 @@ module Ftpd
|
|
173
128
|
def cmd_retr(argument)
|
174
129
|
close_data_server_socket_when_done do
|
175
130
|
ensure_logged_in
|
176
|
-
|
131
|
+
ensure_file_system_supports :read
|
177
132
|
path = argument
|
178
133
|
syntax_error unless path
|
179
134
|
path = File.expand_path(path, @name_prefix)
|
@@ -186,7 +141,7 @@ module Ftpd
|
|
186
141
|
|
187
142
|
def cmd_dele(argument)
|
188
143
|
ensure_logged_in
|
189
|
-
|
144
|
+
ensure_file_system_supports :delete
|
190
145
|
path = argument
|
191
146
|
error "501 Path required" unless path
|
192
147
|
path = File.expand_path(path, @name_prefix)
|
@@ -199,7 +154,7 @@ module Ftpd
|
|
199
154
|
def cmd_list(argument)
|
200
155
|
close_data_server_socket_when_done do
|
201
156
|
ensure_logged_in
|
202
|
-
|
157
|
+
ensure_file_system_supports :list
|
203
158
|
path = argument
|
204
159
|
path ||= '.'
|
205
160
|
path = File.expand_path(path, @name_prefix)
|
@@ -211,7 +166,7 @@ module Ftpd
|
|
211
166
|
def cmd_nlst(argument)
|
212
167
|
close_data_server_socket_when_done do
|
213
168
|
ensure_logged_in
|
214
|
-
|
169
|
+
ensure_file_system_supports :name_list
|
215
170
|
path = argument
|
216
171
|
path ||= '.'
|
217
172
|
path = File.expand_path(path, @name_prefix)
|
@@ -275,7 +230,7 @@ module Ftpd
|
|
275
230
|
if @data_server
|
276
231
|
reply "200 Already in passive mode"
|
277
232
|
else
|
278
|
-
@data_server = TCPServer.new(
|
233
|
+
@data_server = TCPServer.new(@interface, 0)
|
279
234
|
ip = @data_server.addr[3]
|
280
235
|
port = @data_server.addr[1]
|
281
236
|
quads = [
|
@@ -296,6 +251,40 @@ module Ftpd
|
|
296
251
|
@name_prefix = path
|
297
252
|
pwd
|
298
253
|
end
|
254
|
+
alias cmd_xcwd :cmd_cwd
|
255
|
+
|
256
|
+
def cmd_mkd(argument)
|
257
|
+
syntax_error unless argument
|
258
|
+
ensure_logged_in
|
259
|
+
ensure_file_system_supports :mkdir
|
260
|
+
path = File.expand_path(argument, @name_prefix)
|
261
|
+
ensure_accessible path
|
262
|
+
ensure_exists File.dirname(path)
|
263
|
+
ensure_directory File.dirname(path)
|
264
|
+
ensure_does_not_exist path
|
265
|
+
@file_system.mkdir path
|
266
|
+
reply %Q'257 "#{path}" created'
|
267
|
+
end
|
268
|
+
alias cmd_xmkd :cmd_mkd
|
269
|
+
|
270
|
+
def cmd_rmd(argument)
|
271
|
+
syntax_error unless argument
|
272
|
+
ensure_logged_in
|
273
|
+
ensure_file_system_supports :rmdir
|
274
|
+
path = File.expand_path(argument, @name_prefix)
|
275
|
+
ensure_accessible path
|
276
|
+
ensure_exists path
|
277
|
+
ensure_directory path
|
278
|
+
@file_system.rmdir path
|
279
|
+
reply '250 RMD command successful'
|
280
|
+
end
|
281
|
+
alias cmd_xrmd :cmd_rmd
|
282
|
+
|
283
|
+
def ensure_file_system_supports(method)
|
284
|
+
unless @file_system.respond_to?(method)
|
285
|
+
unimplemented_error
|
286
|
+
end
|
287
|
+
end
|
299
288
|
|
300
289
|
def ensure_logged_in
|
301
290
|
return if @state == :logged_in
|
@@ -314,6 +303,12 @@ module Ftpd
|
|
314
303
|
end
|
315
304
|
end
|
316
305
|
|
306
|
+
def ensure_does_not_exist(path)
|
307
|
+
if @file_system.exists?(path)
|
308
|
+
error '550 Already exists'
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
317
312
|
def ensure_directory(path)
|
318
313
|
unless @file_system.directory?(path)
|
319
314
|
error '550 Not a directory'
|
@@ -326,49 +321,22 @@ module Ftpd
|
|
326
321
|
end
|
327
322
|
end
|
328
323
|
|
329
|
-
def ensure_write_supported
|
330
|
-
unless @file_system.respond_to?(:write)
|
331
|
-
unimplemented
|
332
|
-
end
|
333
|
-
end
|
334
|
-
|
335
|
-
def ensure_read_supported
|
336
|
-
unless @file_system.respond_to?(:read)
|
337
|
-
unimplemented
|
338
|
-
end
|
339
|
-
end
|
340
|
-
|
341
|
-
def ensure_delete_supported
|
342
|
-
unless @file_system.respond_to?(:delete)
|
343
|
-
unimplemented
|
344
|
-
end
|
345
|
-
end
|
346
|
-
|
347
|
-
def ensure_list_supported
|
348
|
-
unless @file_system.respond_to?(:list)
|
349
|
-
unimplemented
|
350
|
-
end
|
351
|
-
end
|
352
|
-
|
353
|
-
def ensure_name_list_supported
|
354
|
-
unless @file_system.respond_to?(:name_list)
|
355
|
-
unimplemented
|
356
|
-
end
|
357
|
-
end
|
358
|
-
|
359
324
|
def tls_enabled?
|
360
325
|
@tls != :off
|
361
326
|
end
|
362
327
|
|
363
328
|
def cmd_cdup(argument)
|
329
|
+
syntax_error if argument
|
364
330
|
ensure_logged_in
|
365
331
|
cmd_cwd('..')
|
366
332
|
end
|
333
|
+
alias cmd_xcup :cmd_cdup
|
367
334
|
|
368
335
|
def cmd_pwd(argument)
|
369
336
|
ensure_logged_in
|
370
337
|
pwd
|
371
338
|
end
|
339
|
+
alias cmd_xpwd :cmd_pwd
|
372
340
|
|
373
341
|
def cmd_auth(security_scheme)
|
374
342
|
ensure_tls_supported
|
@@ -412,6 +380,49 @@ module Ftpd
|
|
412
380
|
reply "200 Data protection level #{level_code}"
|
413
381
|
end
|
414
382
|
|
383
|
+
def cmd_rnfr(argument)
|
384
|
+
ensure_logged_in
|
385
|
+
ensure_file_system_supports :rename
|
386
|
+
syntax_error unless argument
|
387
|
+
from_path = File.expand_path(argument, @name_prefix)
|
388
|
+
ensure_accessible from_path
|
389
|
+
ensure_exists from_path
|
390
|
+
@rename_from_path = from_path
|
391
|
+
reply '350 RNFR accepted; ready for destination'
|
392
|
+
end
|
393
|
+
|
394
|
+
def cmd_rnto(argument)
|
395
|
+
sequence_error unless @rename_from_path
|
396
|
+
ensure_logged_in
|
397
|
+
ensure_file_system_supports :rename
|
398
|
+
syntax_error unless argument
|
399
|
+
to_path = File.expand_path(argument, @name_prefix)
|
400
|
+
ensure_accessible to_path
|
401
|
+
ensure_does_not_exist to_path
|
402
|
+
@file_system.rename(@rename_from_path, to_path)
|
403
|
+
reply '250 Rename successful'
|
404
|
+
@rename_from_path = nil
|
405
|
+
end
|
406
|
+
|
407
|
+
def self.unimplemented(command)
|
408
|
+
method_name = "cmd_#{command}"
|
409
|
+
define_method method_name do |arguments|
|
410
|
+
unimplemented_error
|
411
|
+
end
|
412
|
+
private method_name
|
413
|
+
end
|
414
|
+
|
415
|
+
unimplemented :abor
|
416
|
+
unimplemented :acct
|
417
|
+
unimplemented :appe
|
418
|
+
unimplemented :help
|
419
|
+
unimplemented :rein
|
420
|
+
unimplemented :rest
|
421
|
+
unimplemented :site
|
422
|
+
unimplemented :smnt
|
423
|
+
unimplemented :stat
|
424
|
+
unimplemented :stou
|
425
|
+
|
415
426
|
def pwd
|
416
427
|
reply %Q(257 "#{@name_prefix}" is current directory)
|
417
428
|
end
|
data/rake_tasks/yard.rake
CHANGED
@@ -6,10 +6,10 @@ module Ftpd
|
|
6
6
|
let(:data_dir) {Ftpd::TempDir.make}
|
7
7
|
let(:disk_file_system) {DiskFileSystem.new(data_dir)}
|
8
8
|
let(:missing_file_error) do
|
9
|
-
[Ftpd::
|
9
|
+
[Ftpd::PermanentFileSystemError, /No such file or directory/]
|
10
10
|
end
|
11
11
|
let(:is_a_directory_error) do
|
12
|
-
[Ftpd::
|
12
|
+
[Ftpd::PermanentFileSystemError, /Is a directory/]
|
13
13
|
end
|
14
14
|
let(:missing_path) {'missing_path'}
|
15
15
|
|
@@ -31,6 +31,10 @@ module Ftpd
|
|
31
31
|
File.open(data_path(path), 'rb', &:read)
|
32
32
|
end
|
33
33
|
|
34
|
+
def directory?(path)
|
35
|
+
File.directory?(data_path(path))
|
36
|
+
end
|
37
|
+
|
34
38
|
def exists?(path)
|
35
39
|
File.exists?(data_path(path))
|
36
40
|
end
|
@@ -97,14 +101,14 @@ module Ftpd
|
|
97
101
|
|
98
102
|
describe '#delete' do
|
99
103
|
|
100
|
-
context '(
|
104
|
+
context '(success)' do
|
101
105
|
specify do
|
102
106
|
disk_file_system.delete('file')
|
103
107
|
exists?('file').should be_false
|
104
108
|
end
|
105
109
|
end
|
106
110
|
|
107
|
-
context '(
|
111
|
+
context '(error)' do
|
108
112
|
specify do
|
109
113
|
expect {
|
110
114
|
disk_file_system.delete(missing_path)
|
@@ -116,14 +120,14 @@ module Ftpd
|
|
116
120
|
|
117
121
|
describe '#read' do
|
118
122
|
|
119
|
-
context '(
|
123
|
+
context '(success)' do
|
120
124
|
let(:path) {'file'}
|
121
125
|
specify do
|
122
126
|
disk_file_system.read(path).should == canned_contents(path)
|
123
127
|
end
|
124
128
|
end
|
125
129
|
|
126
|
-
context '(
|
130
|
+
context '(error)' do
|
127
131
|
specify do
|
128
132
|
expect {
|
129
133
|
disk_file_system.read(missing_path)
|
@@ -137,7 +141,7 @@ module Ftpd
|
|
137
141
|
|
138
142
|
let(:contents) {'file contents'}
|
139
143
|
|
140
|
-
context '(
|
144
|
+
context '(success)' do
|
141
145
|
let(:path) {'file_path'}
|
142
146
|
specify do
|
143
147
|
disk_file_system.write(path, contents)
|
@@ -145,7 +149,7 @@ module Ftpd
|
|
145
149
|
end
|
146
150
|
end
|
147
151
|
|
148
|
-
context '(
|
152
|
+
context '(error)' do
|
149
153
|
specify do
|
150
154
|
expect {
|
151
155
|
disk_file_system.write('dir', contents)
|
@@ -155,6 +159,26 @@ module Ftpd
|
|
155
159
|
|
156
160
|
end
|
157
161
|
|
162
|
+
describe '#mkdir' do
|
163
|
+
|
164
|
+
context '(success)' do
|
165
|
+
let(:path) {'another_subdir'}
|
166
|
+
specify do
|
167
|
+
disk_file_system.mkdir(path)
|
168
|
+
directory?(path).should be_true
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
context '(error)' do
|
173
|
+
specify do
|
174
|
+
expect {
|
175
|
+
disk_file_system.mkdir('file')
|
176
|
+
}.to raise_error PermanentFileSystemError, /^File exists/
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
181
|
+
|
158
182
|
describe '#name_list' do
|
159
183
|
|
160
184
|
subject do
|
@@ -235,5 +259,28 @@ module Ftpd
|
|
235
259
|
|
236
260
|
end
|
237
261
|
|
262
|
+
describe '#rename' do
|
263
|
+
|
264
|
+
let(:from_path) {'file'}
|
265
|
+
let(:to_path) {'renamed_file'}
|
266
|
+
|
267
|
+
context '(success)' do
|
268
|
+
specify do
|
269
|
+
disk_file_system.rename(from_path, to_path)
|
270
|
+
exists?(from_path).should be_false
|
271
|
+
exists?(to_path).should be_true
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
context '(error)' do
|
276
|
+
specify do
|
277
|
+
expect {
|
278
|
+
disk_file_system.rename(missing_path, to_path)
|
279
|
+
}.to raise_error *missing_file_error
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
end
|
284
|
+
|
238
285
|
end
|
239
286
|
end
|