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.

Files changed (49) hide show
  1. data/Changelog.md +24 -3
  2. data/Gemfile +2 -1
  3. data/Gemfile.lock +9 -2
  4. data/README.md +20 -9
  5. data/VERSION +1 -1
  6. data/doc/rfc.md +277 -0
  7. data/features/ftp_server/cdup.feature +36 -0
  8. data/features/ftp_server/command_errors.feature +0 -4
  9. data/features/ftp_server/delete.feature +1 -1
  10. data/features/ftp_server/directory_navigation.feature +18 -6
  11. data/features/ftp_server/get.feature +1 -1
  12. data/features/ftp_server/get_tls.feature +2 -2
  13. data/features/ftp_server/implicit_tls.feature +18 -0
  14. data/features/ftp_server/list_tls.feature +2 -2
  15. data/features/ftp_server/mkdir.feature +70 -0
  16. data/features/ftp_server/name_list_tls.feature +2 -2
  17. data/features/ftp_server/put.feature +1 -1
  18. data/features/ftp_server/put_tls.feature +2 -2
  19. data/features/ftp_server/rename.feature +90 -0
  20. data/features/ftp_server/rmdir.feature +71 -0
  21. data/features/ftp_server/step_definitions/debug.rb +6 -6
  22. data/features/ftp_server/step_definitions/test_server.rb +3 -2
  23. data/features/step_definitions/connect.rb +4 -3
  24. data/features/step_definitions/{directories.rb → directory_navigation.rb} +4 -0
  25. data/features/step_definitions/error_replies.rb +5 -5
  26. data/features/step_definitions/login.rb +2 -2
  27. data/features/step_definitions/mkdir.rb +9 -0
  28. data/features/step_definitions/rename.rb +11 -0
  29. data/features/step_definitions/rmdir.rb +9 -0
  30. data/features/step_definitions/server_files.rb +9 -0
  31. data/features/support/test_client.rb +19 -5
  32. data/features/support/test_server.rb +28 -3
  33. data/features/support/test_server_files.rb +5 -0
  34. data/ftpd.gemspec +17 -6
  35. data/lib/ftpd.rb +1 -0
  36. data/lib/ftpd/disk_file_system.rb +97 -16
  37. data/lib/ftpd/error.rb +16 -0
  38. data/lib/ftpd/exception_translator.rb +1 -1
  39. data/lib/ftpd/exceptions.rb +10 -4
  40. data/lib/ftpd/file_system_error_translator.rb +8 -4
  41. data/lib/ftpd/ftp_server.rb +1 -0
  42. data/lib/ftpd/session.rb +98 -87
  43. data/rake_tasks/yard.rake +1 -0
  44. data/spec/disk_file_system_spec.rb +55 -8
  45. data/spec/exception_translator_spec.rb +1 -1
  46. data/spec/file_system_error_translator_spec.rb +20 -4
  47. data/spec/translate_exceptions_spec.rb +1 -1
  48. metadata +32 -7
  49. data/sandbox/em-server.rb +0 -37
@@ -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
@@ -23,7 +23,7 @@ module Ftpd
23
23
  begin
24
24
  return yield
25
25
  rescue *@exceptions => e
26
- raise FileSystemError, e.message
26
+ raise PermanentFileSystemError, e.message
27
27
  end
28
28
  end
29
29
 
@@ -11,11 +11,17 @@ module Ftpd
11
11
 
12
12
  class CommandError < FtpServerError ; end
13
13
 
14
- # Any errors raised by a file system driver are (or are derived
15
- # from) this class. See the mixin TranslateExceptions for an easy
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
- # This class is a proxy file system driver that sends "450" replies
4
- # when the wrapped file system driver raises a FileSystemError.
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
- error "450 #{e}"
25
+ permanent_error e
22
26
  end
23
27
 
24
28
  end
@@ -54,6 +54,7 @@ module Ftpd
54
54
 
55
55
  def session(socket)
56
56
  Session.new(:socket => socket,
57
+ :interface => interface,
57
58
  :driver => @driver,
58
59
  :debug => @debug,
59
60
  :debug_path => debug_path,
@@ -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 self.class.private_method_defined?(method)
40
- unimplemented
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
- bad_sequence unless @state == :user
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 bad_sequence
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
- bad_sequence unless @state == :password
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
- ensure_write_supported
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
- ensure_read_supported
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
- ensure_delete_supported
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
- ensure_list_supported
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
- ensure_name_list_supported
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('localhost', 0)
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
@@ -1,3 +1,4 @@
1
1
  require 'yard'
2
2
  YARD::Rake::YardocTask.new do |t|
3
+ t.options += ['-o', 'doc-api']
3
4
  end
@@ -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::FileSystemError, /No such file or directory/]
9
+ [Ftpd::PermanentFileSystemError, /No such file or directory/]
10
10
  end
11
11
  let(:is_a_directory_error) do
12
- [Ftpd::FileSystemError, /Is a directory/]
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 '(normal)' do
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 '(file system error)' do
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 '(normal)' do
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 '(file system error)' do
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 '(normal)' do
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 '(file system error)' do
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