ftpd 0.16.0 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
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