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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +7 -1
  3. data/Gemfile +1 -1
  4. data/Gemfile.lock +1 -1
  5. data/README.md +28 -4
  6. data/VERSION +1 -1
  7. data/examples/foo.rb +114 -0
  8. data/ftpd.gemspec +56 -8
  9. data/lib/ftpd.rb +77 -31
  10. data/lib/ftpd/ascii_helper.rb +16 -0
  11. data/lib/ftpd/cmd_abor.rb +13 -0
  12. data/lib/ftpd/cmd_allo.rb +20 -0
  13. data/lib/ftpd/cmd_appe.rb +23 -0
  14. data/lib/ftpd/cmd_auth.rb +21 -0
  15. data/lib/ftpd/cmd_cdup.rb +16 -0
  16. data/lib/ftpd/cmd_cwd.rb +20 -0
  17. data/lib/ftpd/cmd_dele.rb +21 -0
  18. data/lib/ftpd/cmd_eprt.rb +23 -0
  19. data/lib/ftpd/cmd_epsv.rb +30 -0
  20. data/lib/ftpd/cmd_feat.rb +44 -0
  21. data/lib/ftpd/cmd_help.rb +29 -0
  22. data/lib/ftpd/cmd_list.rb +33 -0
  23. data/lib/ftpd/cmd_login.rb +60 -0
  24. data/lib/ftpd/cmd_mdtm.rb +27 -0
  25. data/lib/ftpd/cmd_mkd.rb +23 -0
  26. data/lib/ftpd/cmd_mode.rb +27 -0
  27. data/lib/ftpd/cmd_nlst.rb +27 -0
  28. data/lib/ftpd/cmd_noop.rb +14 -0
  29. data/lib/ftpd/cmd_opts.rb +14 -0
  30. data/lib/ftpd/cmd_pasv.rb +28 -0
  31. data/lib/ftpd/cmd_pbsz.rb +23 -0
  32. data/lib/ftpd/cmd_port.rb +28 -0
  33. data/lib/ftpd/cmd_prot.rb +34 -0
  34. data/lib/ftpd/cmd_pwd.rb +15 -0
  35. data/lib/ftpd/cmd_quit.rb +18 -0
  36. data/lib/ftpd/cmd_rein.rb +13 -0
  37. data/lib/ftpd/cmd_rename.rb +32 -0
  38. data/lib/ftpd/cmd_rest.rb +13 -0
  39. data/lib/ftpd/cmd_retr.rb +23 -0
  40. data/lib/ftpd/cmd_rmd.rb +22 -0
  41. data/lib/ftpd/cmd_site.rb +13 -0
  42. data/lib/ftpd/cmd_size.rb +21 -0
  43. data/lib/ftpd/cmd_smnt.rb +13 -0
  44. data/lib/ftpd/cmd_stat.rb +15 -0
  45. data/lib/ftpd/cmd_stor.rb +24 -0
  46. data/lib/ftpd/cmd_stou.rb +24 -0
  47. data/lib/ftpd/cmd_stru.rb +27 -0
  48. data/lib/ftpd/cmd_syst.rb +16 -0
  49. data/lib/ftpd/cmd_type.rb +28 -0
  50. data/lib/ftpd/command_handler.rb +91 -0
  51. data/lib/ftpd/command_handler_factory.rb +51 -0
  52. data/lib/ftpd/command_handlers.rb +60 -0
  53. data/lib/ftpd/command_loop.rb +77 -0
  54. data/lib/ftpd/data_connection_helper.rb +124 -0
  55. data/lib/ftpd/disk_file_system.rb +22 -6
  56. data/lib/ftpd/error.rb +4 -0
  57. data/lib/ftpd/file_system_helper.rb +67 -0
  58. data/lib/ftpd/ftp_server.rb +1 -1
  59. data/lib/ftpd/server.rb +1 -0
  60. data/lib/ftpd/session.rb +31 -749
  61. data/lib/ftpd/tls_server.rb +2 -0
  62. data/spec/server_spec.rb +66 -0
  63. 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
- File.open(expand_ftp_path(ftp_path), 'wb') do |file|
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
- File.open(expand_ftp_path(ftp_path), 'ab') do |file|
165
- file.write contents
166
- end
182
+ write_file ftp_path, contents, 'ab'
167
183
  end
168
184
  translate_exceptions :append
169
185
 
@@ -25,5 +25,9 @@ module Ftpd
25
25
  error "550 #{message}"
26
26
  end
27
27
 
28
+ def syntax_error
29
+ error "501 Syntax error"
30
+ end
31
+
28
32
  end
29
33
  end
@@ -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
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env ruby
1
+ require_relative 'tls_server'
2
2
 
3
3
  module Ftpd
4
4
  class FtpServer < TlsServer
@@ -50,6 +50,7 @@ module Ftpd
50
50
 
51
51
  def stop
52
52
  @stopping = true
53
+ @server_socket.shutdown
53
54
  @server_socket.close
54
55
  end
55
56
 
@@ -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
- catch :done do
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 cmd_allo(argument)
52
- ensure_logged_in
53
- syntax_error unless argument =~ /^\d+( R \d+)?$/
54
- command_not_needed
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 cmd_stru(argument)
240
- syntax_error unless argument
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 cmd_noop(argument)
250
- syntax_error if argument
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
- private_methods.map do |method|
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(@user)
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"