ftpd 0.11.0 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,27 @@
1
+ require_relative 'command_handler'
2
+
3
+ module Ftpd
4
+
5
+ class CmdStru < CommandHandler
6
+
7
+ def cmd_stru(argument)
8
+ syntax_error unless argument
9
+ ensure_logged_in
10
+ name, implemented = FILE_STRUCTURES[argument]
11
+ error "504 Invalid structure code" unless name
12
+ error "504 Structure not implemented" unless implemented
13
+ self.structure = argument
14
+ reply "200 File structure set to #{name}"
15
+ end
16
+
17
+ private
18
+
19
+ FILE_STRUCTURES = {
20
+ 'R'=>['Record', false],
21
+ 'F'=>['File', true],
22
+ 'P'=>['Page', false],
23
+ }
24
+
25
+ end
26
+
27
+ end
@@ -0,0 +1,16 @@
1
+ require_relative 'command_handler'
2
+
3
+ module Ftpd
4
+
5
+ # The System (SYST) command.
6
+
7
+ class CmdSyst < CommandHandler
8
+
9
+ def cmd_syst(argument)
10
+ syntax_error if argument
11
+ reply "215 UNIX Type: L8"
12
+ end
13
+
14
+ end
15
+
16
+ end
@@ -0,0 +1,28 @@
1
+ require_relative 'command_handler'
2
+
3
+ module Ftpd
4
+
5
+ class CmdType < CommandHandler
6
+
7
+ def cmd_type(argument)
8
+ ensure_logged_in
9
+ syntax_error unless argument =~ /^(\S)(?: (\S+))?$/
10
+ type_code = $1
11
+ format_code = $2
12
+ unless argument =~ /^([AEI]( [NTC])?|L .*)$/
13
+ error '504 Invalid type code'
14
+ end
15
+ case argument
16
+ when /^A( [NT])?$/
17
+ self.data_type = 'A'
18
+ when /^(I|L 8)$/
19
+ self.data_type = 'I'
20
+ else
21
+ error '504 Type not implemented'
22
+ end
23
+ reply "200 Type set to #{data_type}"
24
+ end
25
+
26
+ end
27
+
28
+ end
@@ -0,0 +1,91 @@
1
+ require_relative 'data_connection_helper'
2
+ require_relative 'error'
3
+ require_relative 'file_system_helper'
4
+
5
+ module Ftpd
6
+
7
+ # Command handler base class
8
+
9
+ class CommandHandler
10
+
11
+ extend Forwardable
12
+
13
+ include AsciiHelper
14
+ include DataConnectionHelper
15
+ include Error
16
+ include FileSystemHelper
17
+
18
+ COMMAND_FILENAME_PREFIX = 'cmd_'
19
+ COMMAND_KLASS_PREFIX = 'Cmd'
20
+ COMMAND_METHOD_PREFIX = 'cmd_'
21
+
22
+ # param session [Session] The session
23
+
24
+ def initialize(session)
25
+ @session = session
26
+ end
27
+
28
+ # Return the commands implemented by this handler. For example,
29
+ # if the handler has the method "cmd_allo", this returns ['allo'].
30
+
31
+ class << self
32
+ include Memoizer
33
+ def commands
34
+ public_instance_methods.map(&:to_s).grep(/#{COMMAND_METHOD_PREFIX}/).map do |method|
35
+ method.gsub(/^#{COMMAND_METHOD_PREFIX}/, '')
36
+ end
37
+ end
38
+ memoize :commands
39
+ end
40
+
41
+ def_delegator 'self.class', :commands
42
+
43
+ private
44
+
45
+ attr_reader :session
46
+
47
+ # Forward methods to the session
48
+
49
+ def_delegators :@session,
50
+ :close_data_server_socket,
51
+ :command_not_needed,
52
+ :config,
53
+ :data_channel_protection_level,
54
+ :data_channel_protection_level=,
55
+ :data_hostname,
56
+ :data_port,
57
+ :data_server,
58
+ :data_server=,
59
+ :data_type,
60
+ :data_type=,
61
+ :ensure_logged_in,
62
+ :ensure_not_epsv_all,
63
+ :ensure_protocol_supported,
64
+ :ensure_tls_supported,
65
+ :epsv_all=,
66
+ :execute_command,
67
+ :expect,
68
+ :file_system,
69
+ :list,
70
+ :list_path,
71
+ :logged_in,
72
+ :logged_in=,
73
+ :login,
74
+ :mode=,
75
+ :name_list,
76
+ :name_prefix,
77
+ :name_prefix=,
78
+ :protection_buffer_size_set,
79
+ :protection_buffer_size_set=,
80
+ :pwd,
81
+ :reply,
82
+ :server_name_and_version,
83
+ :set_active_mode_address,
84
+ :socket,
85
+ :structure=,
86
+ :supported_commands,
87
+ :tls_enabled?
88
+
89
+ end
90
+
91
+ end
@@ -0,0 +1,51 @@
1
+ module Ftpd
2
+
3
+ class CommandHandlerFactory
4
+
5
+ def self.standard_command_handlers
6
+ [
7
+ CmdAbor,
8
+ CmdAllo,
9
+ CmdAppe,
10
+ CmdAuth,
11
+ CmdCdup,
12
+ CmdCwd,
13
+ CmdDele,
14
+ CmdEprt,
15
+ CmdEpsv,
16
+ CmdFeat,
17
+ CmdHelp,
18
+ CmdList,
19
+ CmdLogin,
20
+ CmdMdtm,
21
+ CmdMkd,
22
+ CmdMode,
23
+ CmdNlst,
24
+ CmdNoop,
25
+ CmdOpts,
26
+ CmdPasv,
27
+ CmdPbsz,
28
+ CmdPort,
29
+ CmdProt,
30
+ CmdPwd,
31
+ CmdQuit,
32
+ CmdRein,
33
+ CmdRename,
34
+ CmdRest,
35
+ CmdRetr,
36
+ CmdRmd,
37
+ CmdSite,
38
+ CmdSize,
39
+ CmdSmnt,
40
+ CmdStat,
41
+ CmdStor,
42
+ CmdStou,
43
+ CmdStru,
44
+ CmdSyst,
45
+ CmdType,
46
+ ]
47
+ end
48
+
49
+ end
50
+
51
+ end
@@ -0,0 +1,60 @@
1
+ module Ftpd
2
+
3
+ # All FTP commands which the server supports are dispatched by this
4
+ # class.
5
+
6
+ class CommandHandlers
7
+
8
+ def initialize
9
+ @commands = {}
10
+ end
11
+
12
+ # Add a command handler
13
+ #
14
+ # @param command_handler [Command]
15
+
16
+ def <<(command_handler)
17
+ command_handler.commands.each do |command|
18
+ @commands[command] = command_handler
19
+ end
20
+ end
21
+
22
+ # @param command [String] the command (e.g. "STOR"). Case
23
+ # insensitive.
24
+ # @return truthy if the server supports the command.
25
+
26
+ def has?(command)
27
+ command = canonical_command(command)
28
+ @commands.has_key?(command)
29
+ end
30
+
31
+ # Dispatch a command to the appropriate command handler.
32
+ #
33
+ # @param command [String] the command (e.g. "STOR"). Case
34
+ # insensitive.
35
+ # @param argument [String] The argument, or nil if there isn't
36
+ # one.
37
+
38
+ def execute(command, argument)
39
+ command = canonical_command(command)
40
+ method = "cmd_#{command}"
41
+ @commands[command.downcase].send(method, argument)
42
+ end
43
+
44
+ # Return the sorted list of commands supported by this handler
45
+ #
46
+ # @return [Array<String>] Lowercase command
47
+
48
+ def commands
49
+ @commands.keys.sort
50
+ end
51
+
52
+ private
53
+
54
+ def canonical_command(command)
55
+ command.downcase
56
+ end
57
+
58
+ end
59
+
60
+ end
@@ -0,0 +1,77 @@
1
+ module Ftpd
2
+
3
+ class CommandLoop
4
+
5
+ extend Forwardable
6
+
7
+ include Error
8
+
9
+ def initialize(session)
10
+ @session = session
11
+ end
12
+
13
+ def read_and_execute_commands
14
+ catch :done do
15
+ begin
16
+ reply "220 #{server_name_and_version}"
17
+ loop do
18
+ begin
19
+ s = get_command
20
+ s = process_telnet_sequences(s)
21
+ syntax_error unless s =~ /^(\w+)(?: (.*))?$/
22
+ command, argument = $1.downcase, $2
23
+ unless valid_command?(command)
24
+ unrecognized_error s
25
+ end
26
+ command_sequence_checker.check command
27
+ execute_command command, argument
28
+ rescue CommandError => e
29
+ reply e.message
30
+ end
31
+ end
32
+ rescue Errno::ECONNRESET, Errno::EPIPE
33
+ end
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def_delegators :@session,
40
+ :command_sequence_checker,
41
+ :config,
42
+ :execute_command,
43
+ :reply,
44
+ :server_name_and_version,
45
+ :socket,
46
+ :valid_command?
47
+
48
+ def get_command
49
+ s = gets_with_timeout(socket)
50
+ throw :done if s.nil?
51
+ s = s.chomp
52
+ config.log.debug s
53
+ s
54
+ end
55
+
56
+ def gets_with_timeout(socket)
57
+ ready = IO.select([socket], nil, nil, config.session_timeout)
58
+ timeout if ready.nil?
59
+ ready[0].first.gets
60
+ end
61
+
62
+ def timeout
63
+ reply '421 Control connection timed out.'
64
+ throw :done
65
+ end
66
+
67
+ def process_telnet_sequences(s)
68
+ telnet = Telnet.new(s)
69
+ unless telnet.reply.empty?
70
+ socket.write telnet.reply
71
+ end
72
+ telnet.plain
73
+ end
74
+
75
+ end
76
+
77
+ end
@@ -0,0 +1,124 @@
1
+ require_relative 'command_handler'
2
+
3
+ module Ftpd
4
+
5
+ module DataConnectionHelper
6
+
7
+ def transmit_file(contents, data_type = session.data_type)
8
+ open_data_connection do |data_socket|
9
+ contents = unix_to_nvt_ascii(contents) if data_type == 'A'
10
+ handle_data_disconnect do
11
+ data_socket.write(contents)
12
+ end
13
+ config.log.debug "Sent #{contents.size} bytes"
14
+ reply "226 Transfer complete"
15
+ end
16
+ end
17
+
18
+ def receive_file(path_to_advertise = nil)
19
+ open_data_connection(path_to_advertise) do |data_socket|
20
+ contents = handle_data_disconnect do
21
+ data_socket.read
22
+ end
23
+ contents = nvt_ascii_to_unix(contents) if data_type == 'A'
24
+ config.log.debug "Received #{contents.size} bytes"
25
+ contents
26
+ end
27
+ end
28
+
29
+ def open_data_connection(path_to_advertise = nil, &block)
30
+ send_start_of_data_connection_reply(path_to_advertise)
31
+ if data_server
32
+ if encrypt_data?
33
+ open_passive_tls_data_connection(&block)
34
+ else
35
+ open_passive_data_connection(&block)
36
+ end
37
+ else
38
+ if encrypt_data?
39
+ open_active_tls_data_connection(&block)
40
+ else
41
+ open_active_data_connection(&block)
42
+ end
43
+ end
44
+ end
45
+
46
+ def open_passive_tls_data_connection
47
+ open_passive_data_connection do |socket|
48
+ make_tls_connection(socket) do |ssl_socket|
49
+ yield(ssl_socket)
50
+ end
51
+ end
52
+ end
53
+
54
+ def open_active_tls_data_connection
55
+ open_active_data_connection do |socket|
56
+ make_tls_connection(socket) do |ssl_socket|
57
+ yield(ssl_socket)
58
+ end
59
+ end
60
+ end
61
+
62
+ def open_active_data_connection
63
+ data_socket = TCPSocket.new(data_hostname, data_port)
64
+ begin
65
+ yield(data_socket)
66
+ ensure
67
+ data_socket.close
68
+ end
69
+ end
70
+
71
+ def open_passive_data_connection
72
+ data_socket = data_server.accept
73
+ begin
74
+ yield(data_socket)
75
+ ensure
76
+ data_socket.close
77
+ end
78
+ end
79
+
80
+ def handle_data_disconnect
81
+ return yield
82
+ rescue Errno::ECONNRESET, Errno::EPIPE
83
+ reply "426 Connection closed; transfer aborted."
84
+ end
85
+
86
+ def send_start_of_data_connection_reply(path)
87
+ if path
88
+ reply "150 FILE: #{path}"
89
+ else
90
+ reply "150 Opening #{data_connection_description}"
91
+ end
92
+ end
93
+
94
+ def data_connection_description
95
+ [
96
+ Session::DATA_TYPES[data_type][0],
97
+ "mode data connection",
98
+ ("(TLS)" if encrypt_data?)
99
+ ].compact.join(' ')
100
+ end
101
+
102
+ def make_tls_connection(data_socket)
103
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(data_socket, socket.ssl_context)
104
+ ssl_socket.accept
105
+ begin
106
+ yield(ssl_socket)
107
+ ensure
108
+ ssl_socket.close
109
+ end
110
+ end
111
+
112
+ def close_data_server_socket_when_done
113
+ yield
114
+ ensure
115
+ close_data_server_socket
116
+ end
117
+
118
+ def encrypt_data?
119
+ data_channel_protection_level != :clear
120
+ end
121
+
122
+ end
123
+
124
+ end