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
@@ -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