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.
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
- contents = receive_file
17
- file_system.write path, contents
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
- contents = receive_file(File.basename(path))
17
- file_system.write path, contents
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 "504 Invalid structure code" unless name
12
- error "504 Structure not implemented" unless implemented
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 '504 Invalid type code'
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 '504 Type not implemented'
21
+ error 'Type not implemented', 504
22
22
  end
23
23
  reply "200 Type set to #{data_type}"
24
24
  end
@@ -10,7 +10,6 @@ module Ftpd
10
10
 
11
11
  extend Forwardable
12
12
 
13
- include AsciiHelper
14
13
  include DataConnectionHelper
15
14
  include Error
16
15
  include FileSystemHelper
@@ -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
- unrecognized_error s
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 CommandError => e
29
- reply e.message
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 [CommandError] A "503 Bad sequence" error
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(contents, data_type = session.data_type)
7
+ def transmit_file(io, data_type = session.data_type)
8
8
  open_data_connection do |data_socket|
9
- contents = unix_to_nvt_ascii(contents) if data_type == 'A'
9
+ socket = Ftpd::Stream.new(data_socket, data_type)
10
10
  handle_data_disconnect do
11
- data_socket.write(contents)
11
+ socket.write(io)
12
12
  end
13
- config.log.debug "Sent #{contents.size} bytes"
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
- contents = handle_data_disconnect do
21
- data_socket.read
20
+ socket = Ftpd::Stream.new(data_socket, data_type)
21
+ handle_data_disconnect do
22
+ yield socket
22
23
  end
23
- contents = nvt_ascii_to_unix(contents) if data_type == 'A'
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, contents, mode)
78
+ def write_file(ftp_path, stream, mode)
79
79
  File.open(expand_ftp_path(ftp_path), mode) do |file|
80
- file.write contents
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 into memory.
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', &:read)
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 contents [String] The file's contents
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, contents)
156
- write_file ftp_path, contents, 'wb'
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 contents [String] The file's contents
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, contents)
182
- write_file ftp_path, contents, 'ab'
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 CommandError, message
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 "502 Command not implemented"
9
+ error "Command not implemented", 502
18
10
  end
19
11
 
20
12
  def sequence_error
21
- error "503 Bad sequence of commands"
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 "501 Syntax error"
17
+ error "Syntax error", 501
30
18
  end
31
19
 
32
20
  end
@@ -3,25 +3,60 @@ module Ftpd
3
3
  # All errors (purposefully) generated by this library driver from
4
4
  # this class.
5
5
 
6
- class FtpServerError < StandardError ; end
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
- # Any error that send a reply to the client raises a CommandError.
9
- # The message is the text to send (e.g. "501 Syntax error"). This
10
- # is typically not raised directly, but using the Error mixin.
11
+ class FtpServerError < StandardError
12
+ attr_reader :code
11
13
 
12
- class CommandError < FtpServerError ; end
14
+ def initialize(message, code)
15
+ @code = code
16
+ raise ArgumentError, "Invalid response code" unless valid_response_code?
13
17
 
14
- # A permanent file system error. Deprecated; use
15
- # PermanentFileSystemError instead.
18
+ super(message)
19
+ end
20
+
21
+ def message_with_code
22
+ "#{code} #{message}"
23
+ end
16
24
 
17
- class FileSystemError < FtpServerError ; end
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 ; end
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 ; end
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 '550 Access denied'
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 '550 No such file or directory'
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 '550 Already exists'
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 '550 Not a directory'
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 "530 Not logged in"
64
+ error "Not logged in", 530
65
65
  end
66
66
 
67
67
  def ensure_tls_supported
68
68
  unless tls_enabled?
69
- error "534 TLS not enabled"
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 "501 Not allowed after EPSV ALL"
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("522 Network protocol #{protocol_code} not supported, "\
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 = FileSystemErrorTranslator.new(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 "530 Login incorrect"
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 "504 Command not implemented for that parameter"
194
+ error "Command not implemented for that parameter", 504
195
195
  end
196
196
  @data_hostname = address
197
197
  @data_port = port
@@ -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