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.
- checksums.yaml +4 -4
- data/Changelog.md +7 -1
- data/Gemfile +1 -1
- data/Gemfile.lock +1 -1
- data/README.md +28 -4
- data/VERSION +1 -1
- data/examples/foo.rb +114 -0
- data/ftpd.gemspec +56 -8
- data/lib/ftpd.rb +77 -31
- data/lib/ftpd/ascii_helper.rb +16 -0
- data/lib/ftpd/cmd_abor.rb +13 -0
- data/lib/ftpd/cmd_allo.rb +20 -0
- data/lib/ftpd/cmd_appe.rb +23 -0
- data/lib/ftpd/cmd_auth.rb +21 -0
- data/lib/ftpd/cmd_cdup.rb +16 -0
- data/lib/ftpd/cmd_cwd.rb +20 -0
- data/lib/ftpd/cmd_dele.rb +21 -0
- data/lib/ftpd/cmd_eprt.rb +23 -0
- data/lib/ftpd/cmd_epsv.rb +30 -0
- data/lib/ftpd/cmd_feat.rb +44 -0
- data/lib/ftpd/cmd_help.rb +29 -0
- data/lib/ftpd/cmd_list.rb +33 -0
- data/lib/ftpd/cmd_login.rb +60 -0
- data/lib/ftpd/cmd_mdtm.rb +27 -0
- data/lib/ftpd/cmd_mkd.rb +23 -0
- data/lib/ftpd/cmd_mode.rb +27 -0
- data/lib/ftpd/cmd_nlst.rb +27 -0
- data/lib/ftpd/cmd_noop.rb +14 -0
- data/lib/ftpd/cmd_opts.rb +14 -0
- data/lib/ftpd/cmd_pasv.rb +28 -0
- data/lib/ftpd/cmd_pbsz.rb +23 -0
- data/lib/ftpd/cmd_port.rb +28 -0
- data/lib/ftpd/cmd_prot.rb +34 -0
- data/lib/ftpd/cmd_pwd.rb +15 -0
- data/lib/ftpd/cmd_quit.rb +18 -0
- data/lib/ftpd/cmd_rein.rb +13 -0
- data/lib/ftpd/cmd_rename.rb +32 -0
- data/lib/ftpd/cmd_rest.rb +13 -0
- data/lib/ftpd/cmd_retr.rb +23 -0
- data/lib/ftpd/cmd_rmd.rb +22 -0
- data/lib/ftpd/cmd_site.rb +13 -0
- data/lib/ftpd/cmd_size.rb +21 -0
- data/lib/ftpd/cmd_smnt.rb +13 -0
- data/lib/ftpd/cmd_stat.rb +15 -0
- data/lib/ftpd/cmd_stor.rb +24 -0
- data/lib/ftpd/cmd_stou.rb +24 -0
- data/lib/ftpd/cmd_stru.rb +27 -0
- data/lib/ftpd/cmd_syst.rb +16 -0
- data/lib/ftpd/cmd_type.rb +28 -0
- data/lib/ftpd/command_handler.rb +91 -0
- data/lib/ftpd/command_handler_factory.rb +51 -0
- data/lib/ftpd/command_handlers.rb +60 -0
- data/lib/ftpd/command_loop.rb +77 -0
- data/lib/ftpd/data_connection_helper.rb +124 -0
- data/lib/ftpd/disk_file_system.rb +22 -6
- data/lib/ftpd/error.rb +4 -0
- data/lib/ftpd/file_system_helper.rb +67 -0
- data/lib/ftpd/ftp_server.rb +1 -1
- data/lib/ftpd/server.rb +1 -0
- data/lib/ftpd/session.rb +31 -749
- data/lib/ftpd/tls_server.rb +2 -0
- data/spec/server_spec.rb +66 -0
- 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,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
|