ftpd 0.0.1.pre → 0.1.0

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 (70) hide show
  1. data/Gemfile +3 -1
  2. data/Gemfile.lock +14 -14
  3. data/README.md +71 -23
  4. data/Rakefile +9 -3
  5. data/VERSION +1 -1
  6. data/examples/example.rb +132 -53
  7. data/examples/hello_world.rb +32 -0
  8. data/features/example/example.feature +18 -0
  9. data/features/example/step_definitions/example_server.rb +3 -0
  10. data/features/{command_errors.feature → ftp_server/command_errors.feature} +3 -0
  11. data/features/ftp_server/concurrent_sessions.feature +14 -0
  12. data/features/ftp_server/debug.feature +15 -0
  13. data/features/{delete.feature → ftp_server/delete.feature} +23 -2
  14. data/features/{directory_navigation.feature → ftp_server/directory_navigation.feature} +17 -4
  15. data/features/ftp_server/file_structure.feature +43 -0
  16. data/features/{get.feature → ftp_server/get.feature} +21 -10
  17. data/features/ftp_server/get_tls.feature +18 -0
  18. data/features/{list.feature → ftp_server/list.feature} +24 -30
  19. data/features/ftp_server/list_tls.feature +21 -0
  20. data/features/{login.feature → ftp_server/login.feature} +4 -2
  21. data/features/ftp_server/mode.feature +43 -0
  22. data/features/{name_list.feature → ftp_server/name_list.feature} +25 -31
  23. data/features/ftp_server/name_list_tls.feature +22 -0
  24. data/features/{noop.feature → ftp_server/noop.feature} +3 -0
  25. data/features/{port.feature → ftp_server/port.feature} +3 -0
  26. data/features/{put.feature → ftp_server/put.feature} +19 -11
  27. data/features/ftp_server/put_tls.feature +18 -0
  28. data/features/{quit.feature → ftp_server/quit.feature} +3 -0
  29. data/features/ftp_server/step_definitions/debug.rb +8 -0
  30. data/features/ftp_server/step_definitions/test_server.rb +12 -0
  31. data/features/{syntax_errors.feature → ftp_server/syntax_errors.feature} +3 -0
  32. data/features/ftp_server/type.feature +56 -0
  33. data/features/step_definitions/error.rb +10 -3
  34. data/features/step_definitions/list.rb +21 -4
  35. data/features/step_definitions/login.rb +0 -2
  36. data/features/step_definitions/server_files.rb +4 -0
  37. data/features/step_definitions/stop_server.rb +3 -0
  38. data/features/support/example_server.rb +58 -0
  39. data/features/support/test_client.rb +1 -1
  40. data/features/support/test_server.rb +106 -24
  41. data/features/support/test_server_files.rb +30 -0
  42. data/ftpd.gemspec +56 -25
  43. data/lib/ftpd.rb +22 -4
  44. data/lib/ftpd/disk_file_system.rb +137 -0
  45. data/lib/ftpd/error.rb +9 -0
  46. data/lib/ftpd/exception_translator.rb +29 -0
  47. data/lib/ftpd/exceptions.rb +13 -0
  48. data/lib/ftpd/file_system_error_translator.rb +21 -0
  49. data/lib/ftpd/ftp_server.rb +8 -645
  50. data/lib/ftpd/insecure_certificate.rb +10 -0
  51. data/lib/ftpd/server.rb +15 -12
  52. data/lib/ftpd/session.rb +569 -0
  53. data/lib/ftpd/temp_dir.rb +10 -11
  54. data/lib/ftpd/tls_server.rb +27 -15
  55. data/lib/ftpd/translate_exceptions.rb +44 -0
  56. data/rake_tasks/cucumber.rake +4 -2
  57. data/rake_tasks/default.rake +1 -0
  58. data/rake_tasks/spec.rake +3 -0
  59. data/rake_tasks/test.rake +2 -0
  60. data/sandbox/em-server.rb +37 -0
  61. data/spec/disk_file_system_spec.rb +239 -0
  62. data/spec/exception_translator_spec.rb +35 -0
  63. data/spec/spec_helper.rb +5 -0
  64. data/spec/translate_exceptions_spec.rb +40 -0
  65. metadata +143 -115
  66. data/features/concurrent_sessions.feature +0 -11
  67. data/features/file_structure.feature +0 -40
  68. data/features/mode.feature +0 -40
  69. data/features/step_definitions/server.rb +0 -7
  70. data/features/type.feature +0 -53
@@ -0,0 +1,10 @@
1
+ module Ftpd
2
+ module InsecureCertificate
3
+
4
+ def insecure_certfile_path
5
+ File.expand_path('../../insecure-test-cert.pem',
6
+ File.dirname(__FILE__))
7
+ end
8
+
9
+ end
10
+ end
@@ -1,30 +1,33 @@
1
- require 'socket'
2
-
3
1
  module Ftpd
4
2
  class Server
5
3
 
4
+ include Memoizer
5
+
6
+ attr_accessor :interface
7
+ attr_accessor :port
8
+
6
9
  def initialize
7
- @server_socket = make_server_socket
8
- @server_thread = make_server_thread
10
+ @interface = 'localhost'
11
+ @port = 0
9
12
  end
10
13
 
11
- def port
14
+ def bound_port
12
15
  @server_socket.addr[1]
13
16
  end
14
17
 
15
- def close
16
- # An apparent race condition causes this to sometimes not stop the
17
- # thread. When this happens, the thread remains blocked in the
18
- # accept method; I hypothesize that this happens whenever the
19
- # close happens first. Once this bug is fixed, join on the
20
- # thread.
18
+ def start
19
+ @server_socket = make_server_socket
20
+ @server_thread = make_server_thread
21
+ end
22
+
23
+ def stop
21
24
  @server_socket.close
22
25
  end
23
26
 
24
27
  private
25
28
 
26
29
  def make_server_socket
27
- return TCPServer.new('localhost', 0)
30
+ return TCPServer.new(@interface, @port)
28
31
  end
29
32
 
30
33
  def make_server_thread
@@ -0,0 +1,569 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module Ftpd
4
+ class Session
5
+
6
+ include Error
7
+
8
+ def initialize(opts)
9
+ @driver = opts[:driver]
10
+ @socket = opts[:socket]
11
+ @tls = opts[:tls]
12
+ if @tls == :implicit
13
+ @socket.encrypt
14
+ end
15
+ @name_prefix = '/'
16
+ @debug_path = opts[:debug_path]
17
+ @debug = opts[:debug]
18
+ @data_type = 'A'
19
+ @mode = 'S'
20
+ @format = 'N'
21
+ @structure = 'F'
22
+ @response_delay = opts[:response_delay]
23
+ @data_channel_protection_level = :clear
24
+ end
25
+
26
+ def run
27
+ reply "220 FakeFtpServer"
28
+ @state = :user
29
+ catch :done do
30
+ loop do
31
+ begin
32
+ s = get_command
33
+ syntax_error unless s =~ /^(\w+)(?: (.*))?$/
34
+ command, argument = $1.downcase, $2
35
+ unless VALID_COMMANDS.include?(command)
36
+ error "500 Syntax error, command unrecognized: #{s}"
37
+ end
38
+ method = 'cmd_' + command
39
+ unless self.class.private_method_defined?(method)
40
+ error "502 Command not implemented: #{command}"
41
+ end
42
+ send(method, argument)
43
+ rescue CommandError => e
44
+ reply e.message
45
+ rescue Errno::ECONNRESET, Errno::EPIPE
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ private
52
+
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
+ def cmd_user(argument)
93
+ syntax_error unless argument
94
+ bad_sequence unless @state == :user
95
+ @user = argument
96
+ @state = :password
97
+ reply "331 Password required"
98
+ end
99
+
100
+ def bad_sequence
101
+ error "503 Bad sequence of commands"
102
+ end
103
+
104
+ def cmd_pass(argument)
105
+ syntax_error unless argument
106
+ bad_sequence unless @state == :password
107
+ password = argument
108
+ unless @driver.authenticate(@user, password)
109
+ @state = :user
110
+ error "530 Login incorrect"
111
+ end
112
+ reply "230 Logged in"
113
+ set_file_system @driver.file_system(@user)
114
+ @state = :logged_in
115
+ end
116
+
117
+ def cmd_quit(argument)
118
+ syntax_error if argument
119
+ ensure_logged_in
120
+ reply "221 Byebye"
121
+ @state = :user
122
+ end
123
+
124
+ def syntax_error
125
+ error "501 Syntax error"
126
+ end
127
+
128
+ def cmd_port(argument)
129
+ ensure_logged_in
130
+ pieces = argument.split(/,/)
131
+ syntax_error unless pieces.size == 6
132
+ pieces.collect! do |s|
133
+ syntax_error unless s =~ /^\d{1,3}$/
134
+ i = s.to_i
135
+ syntax_error unless (0..255) === i
136
+ i
137
+ end
138
+ @data_hostname = pieces[0..3].join('.')
139
+ @data_port = pieces[4] << 8 | pieces[5]
140
+ reply "200 PORT command successful"
141
+ end
142
+
143
+ def cmd_stor(argument)
144
+ close_data_server_socket_when_done do
145
+ ensure_logged_in
146
+ path = argument
147
+ syntax_error unless path
148
+ path = File.expand_path(path, @name_prefix)
149
+ ensure_accessible path
150
+ ensure_exists File.dirname(path)
151
+ contents = receive_file(path)
152
+ @file_system.write path, contents
153
+ reply "226 Transfer complete"
154
+ end
155
+ end
156
+
157
+ def cmd_retr(argument)
158
+ close_data_server_socket_when_done do
159
+ ensure_logged_in
160
+ path = argument
161
+ syntax_error unless path
162
+ path = File.expand_path(path, @name_prefix)
163
+ ensure_accessible path
164
+ ensure_exists path
165
+ contents = @file_system.read(path)
166
+ transmit_file contents
167
+ end
168
+ end
169
+
170
+ def cmd_dele(argument)
171
+ ensure_logged_in
172
+ path = argument
173
+ error "501 Path required" unless path
174
+ path = File.expand_path(path, @name_prefix)
175
+ ensure_accessible path
176
+ ensure_exists path
177
+ @file_system.delete path
178
+ reply "250 DELE command successful"
179
+ end
180
+
181
+ def cmd_list(argument)
182
+ ls(argument, :list_long)
183
+ end
184
+
185
+ def cmd_nlst(argument)
186
+ ls(argument, :list_short)
187
+ end
188
+
189
+ def ls(path, file_system_method)
190
+ close_data_server_socket_when_done do
191
+ ensure_logged_in
192
+ path ||= '.'
193
+ path = File.expand_path(path, @name_prefix)
194
+ list = @file_system.send(file_system_method, path)
195
+ transmit_file(list, 'A')
196
+ end
197
+ end
198
+
199
+ def realpath(pathname)
200
+ handle_system_error do
201
+ basename = File.basename(pathname.to_s)
202
+ if is_glob?(basename)
203
+ pathname.dirname.realpath + basename
204
+ else
205
+ pathname.realpath
206
+ end
207
+ end
208
+ end
209
+
210
+ def is_glob?(filename)
211
+ filename =~ /[.*]/
212
+ end
213
+
214
+ def cmd_type(argument)
215
+ ensure_logged_in
216
+ syntax_error unless argument =~ /^(\S)(?: (\S+))?$/
217
+ type_code = $1
218
+ format_code = $2
219
+ set_type(type_code)
220
+ set_format(format_code)
221
+ reply "200 Type set to #{@data_type}"
222
+ end
223
+
224
+ def set_type(type_code)
225
+ name, implemented = DATA_TYPES[type_code]
226
+ error "504 Invalid type code" unless name
227
+ error "504 Type not implemented" unless implemented
228
+ @data_type = type_code
229
+ end
230
+
231
+ def set_format(format_code)
232
+ format_code ||= 'N'
233
+ name, implemented = FORMAT_TYPES[format_code]
234
+ error "504 Invalid format code" unless name
235
+ error "504 Format not implemented" unless implemented
236
+ @data_format = format_code
237
+ end
238
+
239
+ def cmd_mode(argument)
240
+ syntax_error unless argument
241
+ ensure_logged_in
242
+ name, implemented = TRANSMISSION_MODES[argument]
243
+ error "504 Invalid mode code" unless name
244
+ error "504 Mode not implemented" unless implemented
245
+ @mode = argument
246
+ reply "200 Mode set to #{name}"
247
+ end
248
+
249
+ def cmd_stru(argument)
250
+ syntax_error unless argument
251
+ ensure_logged_in
252
+ name, implemented = FILE_STRUCTURES[argument]
253
+ error "504 Invalid structure code" unless name
254
+ error "504 Structure not implemented" unless implemented
255
+ @structure = argument
256
+ reply "200 File structure set to #{name}"
257
+ end
258
+
259
+ def cmd_noop(argument)
260
+ syntax_error if argument
261
+ reply "200 Nothing done"
262
+ end
263
+
264
+ def cmd_pasv(argument)
265
+ ensure_logged_in
266
+ if @data_server
267
+ reply "200 Already in passive mode"
268
+ else
269
+ @data_server = TCPServer.new('localhost', 0)
270
+ ip = @data_server.addr[3]
271
+ port = @data_server.addr[1]
272
+ quads = [
273
+ ip.scan(/\d+/),
274
+ port >> 8,
275
+ port & 0xff,
276
+ ].flatten.join(',')
277
+ reply "227 Entering passive mode (#{quads})"
278
+ end
279
+ end
280
+
281
+ def cmd_cwd(argument)
282
+ ensure_logged_in
283
+ path = File.expand_path(argument, @name_prefix)
284
+ ensure_accessible path
285
+ ensure_exists path
286
+ ensure_directory path
287
+ @name_prefix = path
288
+ pwd
289
+ end
290
+
291
+ def ensure_logged_in
292
+ return if @state == :logged_in
293
+ error "530 Not logged in"
294
+ end
295
+
296
+ def ensure_accessible(path)
297
+ unless @file_system.accessible?(path)
298
+ error '550 Access denied'
299
+ end
300
+ end
301
+
302
+ def ensure_exists(path)
303
+ unless @file_system.exists?(path)
304
+ error '550 No such file or directory'
305
+ end
306
+ end
307
+
308
+ def ensure_directory(path)
309
+ unless @file_system.directory?(path)
310
+ error '550 Not a directory'
311
+ end
312
+ end
313
+
314
+ def ensure_tls_supported
315
+ unless tls_enabled?
316
+ error "534 TLS not enabled"
317
+ end
318
+ end
319
+
320
+ def tls_enabled?
321
+ @tls != :off
322
+ end
323
+
324
+ def cmd_cdup(argument)
325
+ ensure_logged_in
326
+ cmd_cwd('..')
327
+ end
328
+
329
+ def cmd_pwd(argument)
330
+ ensure_logged_in
331
+ pwd
332
+ end
333
+
334
+ def cmd_auth(security_scheme)
335
+ ensure_tls_supported
336
+ if @socket.encrypted?
337
+ error "503 AUTH already done"
338
+ end
339
+ unless security_scheme =~ /^TLS(-C)?$/i
340
+ error "504 Security scheme not implemented: #{security_scheme}"
341
+ end
342
+ reply "234 AUTH #{security_scheme} OK."
343
+ @socket.encrypt
344
+ end
345
+
346
+ def cmd_pbsz(buffer_size)
347
+ ensure_tls_supported
348
+ syntax_error unless buffer_size =~ /^\d+$/
349
+ buffer_size = buffer_size.to_i
350
+ unless @socket.encrypted?
351
+ error "503 PBSZ must be preceded by AUTH"
352
+ end
353
+ unless buffer_size == 0
354
+ error "501 PBSZ=0"
355
+ end
356
+ reply "200 PBSZ=0"
357
+ @protection_buffer_size_set = true
358
+ end
359
+
360
+ def cmd_prot(level_arg)
361
+ level_code = level_arg.upcase
362
+ unless @protection_buffer_size_set
363
+ error "503 PROT must be preceded by PBSZ"
364
+ end
365
+ level = DATA_CHANNEL_PROTECTION_LEVELS[level_code]
366
+ unless level
367
+ error "504 Unknown protection level"
368
+ end
369
+ unless level == :private
370
+ error "536 Unsupported protection level #{level}"
371
+ end
372
+ @data_channel_protection_level = level
373
+ reply "200 Data protection level #{level_code}"
374
+ end
375
+
376
+ def pwd
377
+ reply %Q(257 "#{@name_prefix}" is current directory)
378
+ end
379
+
380
+ TRANSMISSION_MODES = {
381
+ 'B'=>['Block', false],
382
+ 'C'=>['Compressed', false],
383
+ 'S'=>['Stream', true],
384
+ }
385
+
386
+ FORMAT_TYPES = {
387
+ 'N'=>['Non-print', true],
388
+ 'T'=>['Telnet format effectors', false],
389
+ 'C'=>['Carriage Control (ASA)', false],
390
+ }
391
+
392
+ DATA_TYPES = {
393
+ 'A'=>['ASCII', true],
394
+ 'E'=>['EBCDIC', false],
395
+ 'I'=>['BINARY', true],
396
+ 'L'=>['LOCAL', false],
397
+ }
398
+
399
+ FILE_STRUCTURES = {
400
+ 'R'=>['Record', false],
401
+ 'F'=>['File', true],
402
+ 'P'=>['Page', false],
403
+ }
404
+
405
+ DATA_CHANNEL_PROTECTION_LEVELS = {
406
+ 'C'=>:clear,
407
+ 'S'=>:safe,
408
+ 'E'=>:confidential,
409
+ 'P'=>:private
410
+ }
411
+
412
+ def set_file_system(file_system)
413
+ @file_system = FileSystemErrorTranslator.new(file_system)
414
+ end
415
+
416
+ def child_path_of?(parent, child)
417
+ child.cleanpath.to_s.index(parent.cleanpath.to_s) == 0
418
+ end
419
+
420
+ def handle_system_error
421
+ begin
422
+ yield
423
+ rescue SystemCallError => e
424
+ error "550 #{e}"
425
+ end
426
+ end
427
+
428
+ def transmit_file(contents, data_type = @data_type)
429
+ open_data_connection do |data_socket|
430
+ contents = unix_to_nvt_ascii(contents) if data_type == 'A'
431
+ data_socket.write(contents)
432
+ debug("Sent #{contents.size} bytes")
433
+ reply "226 Transfer complete"
434
+ end
435
+ end
436
+
437
+ def receive_file(path)
438
+ open_data_connection do |data_socket|
439
+ contents = data_socket.read
440
+ contents = nvt_ascii_to_unix(contents) if @data_type == 'A'
441
+ debug("Received #{contents.size} bytes")
442
+ contents
443
+ end
444
+ end
445
+
446
+ def unix_to_nvt_ascii(s)
447
+ return s if s =~ /\r\n/
448
+ s.gsub(/\n/, "\r\n")
449
+ end
450
+
451
+ def nvt_ascii_to_unix(s)
452
+ s.gsub(/\r\n/, "\n")
453
+ end
454
+
455
+ def open_data_connection(&block)
456
+ reply "150 Opening #{data_connection_description}"
457
+ if @data_server
458
+ if encrypt_data?
459
+ open_passive_tls_data_connection(&block)
460
+ else
461
+ open_passive_data_connection(&block)
462
+ end
463
+ else
464
+ if encrypt_data?
465
+ open_active_tls_data_connection(&block)
466
+ else
467
+ open_active_data_connection(&block)
468
+ end
469
+ end
470
+ end
471
+
472
+ def data_connection_description
473
+ [
474
+ DATA_TYPES[@data_type][0],
475
+ "mode data connection",
476
+ ("(TLS)" if encrypt_data?)
477
+ ].compact.join(' ')
478
+ end
479
+
480
+ def encrypt_data?
481
+ @data_channel_protection_level != :clear
482
+ end
483
+
484
+ def open_active_data_connection
485
+ data_socket = TCPSocket.new(@data_hostname, @data_port)
486
+ begin
487
+ yield(data_socket)
488
+ ensure
489
+ data_socket.close
490
+ end
491
+ end
492
+
493
+ def open_active_tls_data_connection
494
+ open_active_data_connection do |socket|
495
+ make_tls_connection(socket) do |ssl_socket|
496
+ yield(ssl_socket)
497
+ end
498
+ end
499
+ end
500
+
501
+ def open_passive_data_connection
502
+ data_socket = @data_server.accept
503
+ begin
504
+ yield(data_socket)
505
+ ensure
506
+ data_socket.close
507
+ end
508
+ end
509
+
510
+ def close_data_server_socket_when_done
511
+ yield
512
+ ensure
513
+ close_data_server_socket
514
+ end
515
+
516
+ def close_data_server_socket
517
+ return unless @data_server
518
+ @data_server.close
519
+ @data_server = nil
520
+ end
521
+
522
+ def open_passive_tls_data_connection
523
+ open_passive_data_connection do |socket|
524
+ make_tls_connection(socket) do |ssl_socket|
525
+ yield(ssl_socket)
526
+ end
527
+ end
528
+ end
529
+
530
+ def make_tls_connection(socket)
531
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, @socket.ssl_context)
532
+ ssl_socket.accept
533
+ begin
534
+ yield(ssl_socket)
535
+ ensure
536
+ ssl_socket.close
537
+ end
538
+ end
539
+
540
+ def get_command
541
+ s = @socket.gets
542
+ throw :done if s.nil?
543
+ s = s.chomp
544
+ debug(s)
545
+ s
546
+ end
547
+
548
+ def reply(s)
549
+ if @response_delay.to_i != 0
550
+ debug "#{@response_delay} second delay before replying"
551
+ sleep @response_delay
552
+ end
553
+ debug(s)
554
+ @socket.puts(s)
555
+ end
556
+
557
+ def debug(*s)
558
+ return unless debug?
559
+ File.open(@debug_path, 'a') do |file|
560
+ file.puts(*s)
561
+ end
562
+ end
563
+
564
+ def debug?
565
+ @debug || ENV['FTPD_DEBUG'].to_i != 0
566
+ end
567
+
568
+ end
569
+ end