ftpd 0.16.0 → 0.17.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 +24 -1
- data/README.md +1 -0
- data/VERSION +1 -1
- data/doc/benchmarks.md +43 -54
- data/features/support/test_server.rb +4 -4
- data/ftpd.gemspec +7 -8
- data/lib/ftpd.rb +1 -4
- data/lib/ftpd/cmd_appe.rb +3 -2
- data/lib/ftpd/cmd_auth.rb +2 -2
- data/lib/ftpd/cmd_dele.rb +1 -1
- data/lib/ftpd/cmd_list.rb +1 -1
- data/lib/ftpd/cmd_mode.rb +2 -2
- data/lib/ftpd/cmd_nlst.rb +1 -1
- data/lib/ftpd/cmd_opts.rb +1 -1
- data/lib/ftpd/cmd_pbsz.rb +2 -2
- data/lib/ftpd/cmd_prot.rb +3 -3
- data/lib/ftpd/cmd_retr.rb +3 -2
- data/lib/ftpd/cmd_size.rb +11 -3
- data/lib/ftpd/cmd_stor.rb +3 -2
- data/lib/ftpd/cmd_stou.rb +3 -2
- data/lib/ftpd/cmd_stru.rb +2 -2
- data/lib/ftpd/cmd_type.rb +2 -2
- data/lib/ftpd/command_handler.rb +0 -1
- data/lib/ftpd/command_loop.rb +3 -3
- data/lib/ftpd/command_sequence_checker.rb +1 -1
- data/lib/ftpd/data_connection_helper.rb +9 -10
- data/lib/ftpd/disk_file_system.rb +21 -12
- data/lib/ftpd/error.rb +5 -17
- data/lib/ftpd/exceptions.rb +45 -10
- data/lib/ftpd/file_system_helper.rb +4 -4
- data/lib/ftpd/session.rb +8 -8
- data/lib/ftpd/stream.rb +80 -0
- data/lib/ftpd/translate_exceptions.rb +3 -3
- data/spec/command_sequence_checker_spec.rb +6 -4
- data/spec/disk_file_system_spec.rb +12 -7
- data/spec/ftp_server_error_spec.rb +13 -0
- metadata +27 -28
- data/lib/ftpd/ascii_helper.rb +0 -16
- data/lib/ftpd/file_system_error_translator.rb +0 -29
- data/spec/file_system_error_translator_spec.rb +0 -59
data/lib/ftpd/cmd_stor.rb
CHANGED
@@ -13,8 +13,9 @@ module Ftpd
|
|
13
13
|
path = File.expand_path(path, name_prefix)
|
14
14
|
ensure_accessible path
|
15
15
|
ensure_exists File.dirname(path)
|
16
|
-
|
17
|
-
|
16
|
+
receive_file do |data_socket|
|
17
|
+
file_system.write path, data_socket
|
18
|
+
end
|
18
19
|
reply "226 Transfer complete"
|
19
20
|
end
|
20
21
|
end
|
data/lib/ftpd/cmd_stou.rb
CHANGED
@@ -13,8 +13,9 @@ module Ftpd
|
|
13
13
|
path = unique_path(path)
|
14
14
|
ensure_accessible path
|
15
15
|
ensure_exists File.dirname(path)
|
16
|
-
|
17
|
-
|
16
|
+
receive_file(File.basename(path)) do |data_socket|
|
17
|
+
file_system.write path, data_socket
|
18
|
+
end
|
18
19
|
reply "226 Transfer complete"
|
19
20
|
end
|
20
21
|
end
|
data/lib/ftpd/cmd_stru.rb
CHANGED
@@ -8,8 +8,8 @@ module Ftpd
|
|
8
8
|
syntax_error unless argument
|
9
9
|
ensure_logged_in
|
10
10
|
name, implemented = FILE_STRUCTURES[argument]
|
11
|
-
error "
|
12
|
-
error "
|
11
|
+
error "Invalid structure code", 504 unless name
|
12
|
+
error "Structure not implemented", 504 unless implemented
|
13
13
|
self.structure = argument
|
14
14
|
reply "200 File structure set to #{name}"
|
15
15
|
end
|
data/lib/ftpd/cmd_type.rb
CHANGED
@@ -10,7 +10,7 @@ module Ftpd
|
|
10
10
|
type_code = $1
|
11
11
|
format_code = $2
|
12
12
|
unless argument =~ /^([AEI]( [NTC])?|L .*)$/
|
13
|
-
error '
|
13
|
+
error 'Invalid type code', 504
|
14
14
|
end
|
15
15
|
case argument
|
16
16
|
when /^A( [NT])?$/
|
@@ -18,7 +18,7 @@ module Ftpd
|
|
18
18
|
when /^(I|L 8)$/
|
19
19
|
self.data_type = 'I'
|
20
20
|
else
|
21
|
-
error '
|
21
|
+
error 'Type not implemented', 504
|
22
22
|
end
|
23
23
|
reply "200 Type set to #{data_type}"
|
24
24
|
end
|
data/lib/ftpd/command_handler.rb
CHANGED
data/lib/ftpd/command_loop.rb
CHANGED
@@ -21,12 +21,12 @@ module Ftpd
|
|
21
21
|
syntax_error unless s =~ /^(\w+)(?: (.*))?$/
|
22
22
|
command, argument = $1.downcase, $2
|
23
23
|
unless valid_command?(command)
|
24
|
-
|
24
|
+
error "Syntax error, command unrecognized: #{s.chomp}", 500
|
25
25
|
end
|
26
26
|
command_sequence_checker.check command
|
27
27
|
execute_command command, argument
|
28
|
-
rescue
|
29
|
-
reply e.
|
28
|
+
rescue FtpServerError => e
|
29
|
+
reply e.message_with_code
|
30
30
|
end
|
31
31
|
end
|
32
32
|
rescue Errno::ECONNRESET, Errno::EPIPE
|
@@ -40,7 +40,7 @@ module Ftpd
|
|
40
40
|
# {#expect} is called again.
|
41
41
|
#
|
42
42
|
# @param command [String] The command. Must be lowercase.
|
43
|
-
# @raise [
|
43
|
+
# @raise [FtpServerError] A "503 Bad sequence" error
|
44
44
|
|
45
45
|
def check(command)
|
46
46
|
if @expected_command
|
@@ -4,25 +4,24 @@ module Ftpd
|
|
4
4
|
|
5
5
|
module DataConnectionHelper
|
6
6
|
|
7
|
-
def transmit_file(
|
7
|
+
def transmit_file(io, data_type = session.data_type)
|
8
8
|
open_data_connection do |data_socket|
|
9
|
-
|
9
|
+
socket = Ftpd::Stream.new(data_socket, data_type)
|
10
10
|
handle_data_disconnect do
|
11
|
-
|
11
|
+
socket.write(io)
|
12
12
|
end
|
13
|
-
config.log.debug "Sent #{
|
13
|
+
config.log.debug "Sent #{socket.byte_count} bytes"
|
14
14
|
reply "226 Transfer complete"
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
-
def receive_file(path_to_advertise = nil)
|
18
|
+
def receive_file(path_to_advertise = nil, &block)
|
19
19
|
open_data_connection(path_to_advertise) do |data_socket|
|
20
|
-
|
21
|
-
|
20
|
+
socket = Ftpd::Stream.new(data_socket, data_type)
|
21
|
+
handle_data_disconnect do
|
22
|
+
yield socket
|
22
23
|
end
|
23
|
-
|
24
|
-
config.log.debug "Received #{contents.size} bytes"
|
25
|
-
contents
|
24
|
+
config.log.debug "Received #{socket.byte_count} bytes"
|
26
25
|
end
|
27
26
|
end
|
28
27
|
|
@@ -75,9 +75,11 @@ module Ftpd
|
|
75
75
|
|
76
76
|
module FileWriting
|
77
77
|
|
78
|
-
def write_file(ftp_path,
|
78
|
+
def write_file(ftp_path, stream, mode)
|
79
79
|
File.open(expand_ftp_path(ftp_path), mode) do |file|
|
80
|
-
|
80
|
+
while line = stream.read
|
81
|
+
file.write line
|
82
|
+
end
|
81
83
|
end
|
82
84
|
end
|
83
85
|
|
@@ -117,16 +119,22 @@ module Ftpd
|
|
117
119
|
|
118
120
|
include TranslateExceptions
|
119
121
|
|
120
|
-
# Read a file
|
122
|
+
# Read a file from disk.
|
121
123
|
# @param ftp_path [String] The virtual path
|
124
|
+
# @yield [io] Passes an IO object to the block
|
122
125
|
#
|
123
126
|
# Called for:
|
124
127
|
# * RETR
|
125
128
|
#
|
126
129
|
# If missing, then these commands are not supported.
|
127
130
|
|
128
|
-
def read(ftp_path)
|
129
|
-
File.open(expand_ftp_path(ftp_path), 'rb'
|
131
|
+
def read(ftp_path, &block)
|
132
|
+
io = File.open(expand_ftp_path(ftp_path), 'rb')
|
133
|
+
begin
|
134
|
+
yield(io)
|
135
|
+
ensure
|
136
|
+
io.close
|
137
|
+
end
|
130
138
|
end
|
131
139
|
translate_exceptions :read
|
132
140
|
|
@@ -144,7 +152,7 @@ module Ftpd
|
|
144
152
|
|
145
153
|
# Write a file to disk.
|
146
154
|
# @param ftp_path [String] The virtual path
|
147
|
-
# @param
|
155
|
+
# @param stream [Ftpd::Stream] Stream that contains the data to write
|
148
156
|
#
|
149
157
|
# Called for:
|
150
158
|
# * STOR
|
@@ -152,8 +160,8 @@ module Ftpd
|
|
152
160
|
#
|
153
161
|
# If missing, then these commands are not supported.
|
154
162
|
|
155
|
-
def write(ftp_path,
|
156
|
-
write_file ftp_path,
|
163
|
+
def write(ftp_path, stream)
|
164
|
+
write_file ftp_path, stream, 'wb'
|
157
165
|
end
|
158
166
|
translate_exceptions :write
|
159
167
|
|
@@ -171,15 +179,15 @@ module Ftpd
|
|
171
179
|
|
172
180
|
# Append to a file. If the file does not exist, create it.
|
173
181
|
# @param ftp_path [String] The virtual path
|
174
|
-
# @param
|
182
|
+
# @param stream [Ftpd::Stream] Stream that contains the data to write
|
175
183
|
#
|
176
184
|
# Called for:
|
177
185
|
# * APPE
|
178
186
|
#
|
179
187
|
# If missing, then these commands are not supported.
|
180
188
|
|
181
|
-
def append(ftp_path,
|
182
|
-
write_file ftp_path,
|
189
|
+
def append(ftp_path, stream)
|
190
|
+
write_file ftp_path, stream, 'ab'
|
183
191
|
end
|
184
192
|
translate_exceptions :append
|
185
193
|
|
@@ -368,7 +376,8 @@ module Ftpd
|
|
368
376
|
# found") or TransientFileSystemError (e.g. "file busy"). A
|
369
377
|
# PermanentFileSystemError will cause a "550" error response to be
|
370
378
|
# sent; a TransientFileSystemError will cause a "450" error response
|
371
|
-
# to be sent.
|
379
|
+
# to be sent. Methods may also raise an FtpServerError with any
|
380
|
+
# desired error code.
|
372
381
|
#
|
373
382
|
# The class is divided into modules that may be included piecemeal.
|
374
383
|
# By including some mixins and not others, you can compose a disk
|
data/lib/ftpd/error.rb
CHANGED
@@ -1,32 +1,20 @@
|
|
1
1
|
module Ftpd
|
2
2
|
module Error
|
3
3
|
|
4
|
-
def error(message)
|
5
|
-
raise
|
6
|
-
end
|
7
|
-
|
8
|
-
def transient_error(message)
|
9
|
-
error "450 #{message}"
|
10
|
-
end
|
11
|
-
|
12
|
-
def unrecognized_error(s)
|
13
|
-
error "500 Syntax error, command unrecognized: #{s.chomp}"
|
4
|
+
def error(message, code)
|
5
|
+
raise FtpServerError.new(message, code)
|
14
6
|
end
|
15
7
|
|
16
8
|
def unimplemented_error
|
17
|
-
error "
|
9
|
+
error "Command not implemented", 502
|
18
10
|
end
|
19
11
|
|
20
12
|
def sequence_error
|
21
|
-
error "
|
22
|
-
end
|
23
|
-
|
24
|
-
def permanent_error(message)
|
25
|
-
error "550 #{message}"
|
13
|
+
error "Bad sequence of commands", 503
|
26
14
|
end
|
27
15
|
|
28
16
|
def syntax_error
|
29
|
-
error "
|
17
|
+
error "Syntax error", 501
|
30
18
|
end
|
31
19
|
|
32
20
|
end
|
data/lib/ftpd/exceptions.rb
CHANGED
@@ -3,25 +3,60 @@ module Ftpd
|
|
3
3
|
# All errors (purposefully) generated by this library driver from
|
4
4
|
# this class.
|
5
5
|
|
6
|
-
|
6
|
+
# Any error that send a reply to the client raises a FtpServerError.
|
7
|
+
# The message is the text to send (e.g. "Syntax error") and the code
|
8
|
+
# is the FTP response code to send (e.g. "502"). This is typically not
|
9
|
+
# raised directly, but using the Error mixin.
|
7
10
|
|
8
|
-
|
9
|
-
|
10
|
-
# is typically not raised directly, but using the Error mixin.
|
11
|
+
class FtpServerError < StandardError
|
12
|
+
attr_reader :code
|
11
13
|
|
12
|
-
|
14
|
+
def initialize(message, code)
|
15
|
+
@code = code
|
16
|
+
raise ArgumentError, "Invalid response code" unless valid_response_code?
|
13
17
|
|
14
|
-
|
15
|
-
|
18
|
+
super(message)
|
19
|
+
end
|
20
|
+
|
21
|
+
def message_with_code
|
22
|
+
"#{code} #{message}"
|
23
|
+
end
|
16
24
|
|
17
|
-
|
25
|
+
private
|
26
|
+
def valid_response_code?
|
27
|
+
(400..599).cover?(code)
|
28
|
+
end
|
29
|
+
end
|
18
30
|
|
19
31
|
# A permanent file system error. The file isn't there, etc.
|
20
32
|
|
21
|
-
class PermanentFileSystemError < FtpServerError
|
33
|
+
class PermanentFileSystemError < FtpServerError
|
34
|
+
def initialize(message, code = 550)
|
35
|
+
super
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def valid_response_code?
|
40
|
+
(550..559).cover?(code)
|
41
|
+
end
|
42
|
+
end
|
22
43
|
|
23
44
|
# A transient file system error. The file is busy, etc.
|
24
45
|
|
25
|
-
class TransientFileSystemError < FtpServerError
|
46
|
+
class TransientFileSystemError < FtpServerError
|
47
|
+
def initialize(message, code = 450)
|
48
|
+
super
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
def valid_response_code?
|
53
|
+
(450..459).cover?(code)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# A permanent file system error. Deprecated; use
|
58
|
+
# PermanentFileSystemError instead.
|
59
|
+
|
60
|
+
class FileSystemError < PermanentFileSystemError ; end
|
26
61
|
|
27
62
|
end
|
@@ -19,25 +19,25 @@ module Ftpd
|
|
19
19
|
|
20
20
|
def ensure_accessible(path)
|
21
21
|
unless file_system.accessible?(path)
|
22
|
-
error '
|
22
|
+
error 'Access denied', 550
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
26
|
def ensure_exists(path)
|
27
27
|
unless file_system.exists?(path)
|
28
|
-
error '
|
28
|
+
error 'No such file or directory', 550
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
32
|
def ensure_does_not_exist(path)
|
33
33
|
if file_system.exists?(path)
|
34
|
-
error '
|
34
|
+
error 'Already exists', 550
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
38
38
|
def ensure_directory(path)
|
39
39
|
unless file_system.directory?(path)
|
40
|
-
error '
|
40
|
+
error 'Not a directory', 550
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
data/lib/ftpd/session.rb
CHANGED
@@ -61,18 +61,18 @@ module Ftpd
|
|
61
61
|
|
62
62
|
def ensure_logged_in
|
63
63
|
return if @logged_in
|
64
|
-
error "
|
64
|
+
error "Not logged in", 530
|
65
65
|
end
|
66
66
|
|
67
67
|
def ensure_tls_supported
|
68
68
|
unless tls_enabled?
|
69
|
-
error "
|
69
|
+
error "TLS not enabled", 534
|
70
70
|
end
|
71
71
|
end
|
72
72
|
|
73
73
|
def ensure_not_epsv_all
|
74
74
|
if @epsv_all
|
75
|
-
error "
|
75
|
+
error "Not allowed after EPSV ALL", 501
|
76
76
|
end
|
77
77
|
end
|
78
78
|
|
@@ -83,8 +83,8 @@ module Ftpd
|
|
83
83
|
def ensure_protocol_supported(protocol_code)
|
84
84
|
unless @protocols.supports_protocol?(protocol_code)
|
85
85
|
protocol_list = @protocols.protocol_codes.join(',')
|
86
|
-
error("
|
87
|
-
"use (#{protocol_list})")
|
86
|
+
error("Network protocol #{protocol_code} not supported, "\
|
87
|
+
"use (#{protocol_list})", 522)
|
88
88
|
end
|
89
89
|
end
|
90
90
|
|
@@ -114,7 +114,7 @@ module Ftpd
|
|
114
114
|
end
|
115
115
|
|
116
116
|
def set_file_system(file_system)
|
117
|
-
@file_system =
|
117
|
+
@file_system = file_system
|
118
118
|
end
|
119
119
|
|
120
120
|
def command_not_needed
|
@@ -155,7 +155,7 @@ module Ftpd
|
|
155
155
|
user = auth_tokens.first
|
156
156
|
unless authenticate(*auth_tokens)
|
157
157
|
failed_auth
|
158
|
-
error "
|
158
|
+
error "Login incorrect", 530
|
159
159
|
end
|
160
160
|
reply "230 Logged in"
|
161
161
|
set_file_system @config.driver.file_system(user)
|
@@ -191,7 +191,7 @@ module Ftpd
|
|
191
191
|
|
192
192
|
def set_active_mode_address(address, port)
|
193
193
|
if port > 0xffff || port < 1024 && !@config.allow_low_data_ports
|
194
|
-
error "
|
194
|
+
error "Command not implemented for that parameter", 504
|
195
195
|
end
|
196
196
|
@data_hostname = address
|
197
197
|
@data_port = port
|
data/lib/ftpd/stream.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
module Ftpd
|
2
|
+
|
3
|
+
class Stream
|
4
|
+
|
5
|
+
CHUNK_SIZE = 1024 * 100 # 100kb
|
6
|
+
|
7
|
+
attr_reader :data_type
|
8
|
+
attr_reader :byte_count
|
9
|
+
|
10
|
+
# @param io [IO] The stream to read from or write to
|
11
|
+
# @param data_type [String] The FTP data type of the stream
|
12
|
+
|
13
|
+
def initialize(io, data_type)
|
14
|
+
@io, @data_type = io, data_type
|
15
|
+
@byte_count = 0
|
16
|
+
end
|
17
|
+
|
18
|
+
# Read and convert a chunk of up to CHUNK_SIZE from the stream
|
19
|
+
# @return [String] if any bytes remain to read from the stream
|
20
|
+
# @return [NilClass] if no bytes remain
|
21
|
+
|
22
|
+
def read
|
23
|
+
chunk = converted_chunk(@io)
|
24
|
+
return unless chunk
|
25
|
+
chunk = nvt_ascii_to_unix(chunk) if data_type == 'A'
|
26
|
+
record_bytes(chunk)
|
27
|
+
chunk
|
28
|
+
end
|
29
|
+
|
30
|
+
# Convert and write a chunk of up to CHUNK_SIZE to the stream from the
|
31
|
+
# provided IO object
|
32
|
+
#
|
33
|
+
# @param io [IO] The data to be written to the stream
|
34
|
+
|
35
|
+
def write(io)
|
36
|
+
while chunk = converted_chunk(io)
|
37
|
+
chunk = unix_to_nvt_ascii(chunk) if data_type == 'A'
|
38
|
+
result = @io.write(chunk)
|
39
|
+
record_bytes(chunk)
|
40
|
+
result
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# We never want to break up any \r\n sequences in the file. To avoid
|
47
|
+
# this in an efficient way, we always pull an "extra" character from the
|
48
|
+
# stream and add it to the buffer. If the character is a \r, then we put
|
49
|
+
# it back onto the stream instead of adding it to the buffer.
|
50
|
+
|
51
|
+
def converted_chunk(io)
|
52
|
+
chunk = io.read(CHUNK_SIZE)
|
53
|
+
return unless chunk
|
54
|
+
if data_type == 'A'
|
55
|
+
next_char = io.getc
|
56
|
+
if next_char == "\r"
|
57
|
+
io.ungetc(next_char)
|
58
|
+
elsif next_char
|
59
|
+
chunk += next_char
|
60
|
+
end
|
61
|
+
end
|
62
|
+
chunk
|
63
|
+
end
|
64
|
+
|
65
|
+
def unix_to_nvt_ascii(s)
|
66
|
+
return s if s =~ /\r\n/
|
67
|
+
s.gsub(/\n/, "\r\n")
|
68
|
+
end
|
69
|
+
|
70
|
+
def nvt_ascii_to_unix(s)
|
71
|
+
s.gsub(/\r\n/, "\n")
|
72
|
+
end
|
73
|
+
|
74
|
+
def record_bytes(chunk)
|
75
|
+
@byte_count += chunk.size if chunk
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|