ftpd 0.2.2 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
data/Changelog.md CHANGED
@@ -1,3 +1,30 @@
1
+ ### 0.3.1
2
+
3
+ API changes
4
+
5
+ The file system interface for directory listing was completely
6
+ rewritten. It no longer shells out to ls, which removes potential
7
+ command injection security holes, and improves prospects for
8
+ portability.
9
+
10
+ * Removed {Ftpd::DiskFileSystem::Ls}
11
+ * Removed {Ftpd::DiskFileSystem::NameList}. NLIST now uses the
12
+ functions in {Ftpd::DiskFileSystem::List}.
13
+ * Removed {Ftpd::DiskFileSystem::List#list}. The formatting of
14
+ directory output is now done by ftpd, not by the file system driver.
15
+ * Added {Ftpd::DiskFileSystem::List#file_info}, used by LIST.
16
+ * Added {Ftpd::DiskFileSystem::List#dir}, used by LIST and NLST.
17
+
18
+ Bug fixes
19
+
20
+ * LIST and NLST support globs again.
21
+ * STOU (store unique) works in Ruby 1.8.7
22
+
23
+ Enhancements
24
+
25
+ * The output of the "LIST" command can be customized (see
26
+ {Ftpd::FtpServer#list_formatter})
27
+
1
28
  ### 0.2.2
2
29
 
3
30
  Bug fixes
@@ -8,9 +35,10 @@ Bug fixes
8
35
  PASS
9
36
  * Open PASV mode data connection on same local IP as control connection.
10
37
  This is required by RFC 1123.
11
- * Disabled globbing in LIST (for now) due to code injection
12
- vulnerability. This patch also disables globbing in NLST, but NLST
13
- probably shouldn't do globbing.
38
+ * Disabled globbing in LIST (for now) due to a command (shell)
39
+ injection vulnerability. This patch also disables globbing in NLST,
40
+ but NLST probably shouldn't do globbing. Thanks to Larry Cashdollar
41
+ for the report.
14
42
 
15
43
  Enhancements
16
44
 
data/Gemfile CHANGED
@@ -9,5 +9,6 @@ group :development do
9
9
  gem 'rake'
10
10
  gem 'redcarpet'
11
11
  gem 'rspec'
12
+ gem 'timecop'
12
13
  gem 'yard'
13
14
  end
data/Gemfile.lock CHANGED
@@ -36,6 +36,7 @@ GEM
36
36
  rspec-expectations (2.13.0)
37
37
  diff-lcs (>= 1.1.3, < 2.0)
38
38
  rspec-mocks (2.13.0)
39
+ timecop (0.5.9.2)
39
40
  yard (0.8.5.2)
40
41
 
41
42
  PLATFORMS
@@ -49,4 +50,5 @@ DEPENDENCIES
49
50
  rake
50
51
  redcarpet
51
52
  rspec
53
+ timecop
52
54
  yard
data/README.md CHANGED
@@ -92,10 +92,6 @@ Ftpd is not yet RFC compliant. It does most of RFC969, and enough TLS
92
92
  to get by. {file:doc/rfc.md Here} is a list of RFCs, indicating how
93
93
  much of each Ftpd complies with.
94
94
 
95
- The DiskFileSystem class only works in Linux. This is because it
96
- shells out to the "ls" command. This affects the example, which uses
97
- the DiskFileSystem.
98
-
99
95
  To bind the server to an external interface, the interface must be set
100
96
  to the public IP of that interface (e.g. "1.2.3.4"), not to "0.0.0.0".
101
97
  That's because the interface IP is used both for binding server ports,
@@ -103,10 +99,6 @@ _and_ for advertising to the client which IP to connect to. Binding
103
99
  to 0.0.0.0 will work fine, but when the client tries to connect to
104
100
  0.0.0.0, it won't get to the server.
105
101
 
106
- LIST doesn't accept globs. It has other problems (it accepts
107
- arbitrary ls arguments!) and needs to be rewritten to not shell out to
108
- "ls".
109
-
110
102
  ## RUBY COMPATABILITY
111
103
 
112
104
  The tests pass with these Rubies:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.2
1
+ 0.3.1
data/doc/references.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # REFERENCES
2
2
 
3
+ ## RFCs
4
+
3
5
  _This list of references comes from the README of the em-ftpd gem,
4
6
  which is licensed under the same MIT license as this gem, and is
5
7
  Copyright (c) 2008 James Healy_
@@ -30,3 +32,8 @@ including the ye old RFC114 from 1971, "A File Transfer Protocol"
30
32
  There is a {http://secureftp-test.com public test server} which is
31
33
  very handy for checking out clients, and seeing how at least one
32
34
  server behaves.
35
+
36
+ ## LIST output format
37
+
38
+ * {http://www.gnu.org/software/coreutils/manual/html_node/What-information-is-listed.html#What-information-is-listed GNU docs for ls}
39
+ * {http://cr.yp.to/ftp/list/eplf.html Easily Parsed LIST format (EPLF)}
data/examples/example.rb CHANGED
@@ -10,6 +10,7 @@ require 'optparse'
10
10
  module Example
11
11
  class Arguments
12
12
 
13
+ attr_reader :eplf
13
14
  attr_reader :interface
14
15
  attr_reader :port
15
16
  attr_reader :tls
@@ -39,6 +40,9 @@ module Example
39
40
  'Select TLS support (off, explicit, implicit)') do |t|
40
41
  @tls = t
41
42
  end
43
+ op.on('--eplf', 'LIST uses EPLF format') do |t|
44
+ @eplf = t
45
+ end
42
46
  end
43
47
  end
44
48
 
@@ -94,6 +98,9 @@ module Example
94
98
  @server.port = @args.port
95
99
  @server.tls = @args.tls
96
100
  @server.certfile_path = insecure_certfile_path
101
+ if @args.eplf
102
+ @server.list_formatter = Ftpd::ListFormat::Eplf
103
+ end
97
104
  @server.start
98
105
  display_connection_info
99
106
  create_connection_script
@@ -0,0 +1,14 @@
1
+ Feature: Example
2
+
3
+ As a programmer
4
+ I want to enable EPLF list format
5
+ So that I can test this library with an EPLF client
6
+
7
+ Background:
8
+ Given the example has argument "--eplf"
9
+ And the example server is started
10
+
11
+ Scenario: List directory
12
+ Given a successful login
13
+ When the client successfully lists the directory
14
+ Then the list should be in EPLF format
@@ -1,3 +1,11 @@
1
+ def example_args
2
+ @example_args ||= []
3
+ end
4
+
5
+ Given /^the example has argument "(.*?)"$/ do |arg|
6
+ example_args << arg
7
+ end
8
+
1
9
  Given /^the example server is started$/ do
2
- @server = ExampleServer.new
10
+ @server = ExampleServer.new(example_args)
3
11
  end
@@ -41,8 +41,15 @@ Feature: List
41
41
  Then the file list should be in long form
42
42
  And the file list should contain "foo"
43
43
 
44
+ Scenario: After CWD
45
+ Given a successful login
46
+ And the server has file "subdir/foo"
47
+ And the client successfully cd's to "subdir"
48
+ When the client successfully lists the directory
49
+ Then the file list should be in long form
50
+ And the file list should contain "foo"
51
+
44
52
  Scenario: Glob
45
- Given PENDING "Disabled (for now) due to code injection vulnerability"
46
53
  Given a successful login
47
54
  And the server has file "foo"
48
55
  And the server has file "bar"
@@ -62,7 +62,7 @@ Feature: Name List
62
62
  Then the server returns a not logged in error
63
63
 
64
64
  Scenario: List not enabled
65
- Given the test server is started without name_list
65
+ Given the test server is started without list
66
66
  And a successful login
67
67
  When the client name-lists the directory
68
68
  Then the server returns an unimplemented command error
@@ -18,6 +18,12 @@ class FileList
18
18
  !long_form?
19
19
  end
20
20
 
21
+ def eplf_format?
22
+ @lines.all? do |line|
23
+ line =~ /^\+.*\t.*$/
24
+ end
25
+ end
26
+
21
27
  def empty?
22
28
  @lines.empty?
23
29
  end
@@ -61,3 +67,7 @@ end
61
67
  Then /^the file list should be empty$/ do
62
68
  @list.should be_empty
63
69
  end
70
+
71
+ Then /^the list should be in EPLF format$/ do
72
+ @list.should be_eplf_format
73
+ end
@@ -9,10 +9,11 @@ class ExampleServer
9
9
  include FileUtils
10
10
  include TestServerFiles
11
11
 
12
- def initialize
12
+ def initialize(args = nil)
13
13
  command = [
14
14
  File.expand_path('../../examples/example.rb',
15
- File.dirname(__FILE__))
15
+ File.dirname(__FILE__)),
16
+ args,
16
17
  ].join(' ')
17
18
  @io = IO.popen(command, 'r+')
18
19
  @output = read_output
@@ -16,7 +16,6 @@ class TestServer
16
16
  attr_accessor :delete
17
17
  attr_accessor :list
18
18
  attr_accessor :mkdir
19
- attr_accessor :name_list
20
19
  attr_accessor :read
21
20
  attr_accessor :rename
22
21
  attr_accessor :rmdir
@@ -27,7 +26,6 @@ class TestServer
27
26
  @delete = true
28
27
  @list = true
29
28
  @mkdir = true
30
- @name_list = true
31
29
  @read = true
32
30
  @rename = true
33
31
  @rmdir = true
@@ -43,7 +41,6 @@ class TestServer
43
41
  :delete => @delete,
44
42
  :list => @list,
45
43
  :mkdir => @mkdir,
46
- :name_list => @name_list,
47
44
  :read => @read,
48
45
  :rename => @rename,
49
46
  :rmdir => @rmdir,
@@ -147,10 +144,6 @@ class TestServer
147
144
  include Ftpd::DiskFileSystem::List
148
145
  end
149
146
 
150
- if opts[:name_list]
151
- include Ftpd::DiskFileSystem::NameList
152
- end
153
-
154
147
  if opts[:mkdir]
155
148
  include Ftpd::DiskFileSystem::Mkdir
156
149
  end
@@ -218,7 +211,6 @@ class TestServer
218
211
  def_delegator :@driver, :'delete='
219
212
  def_delegator :@driver, :'list='
220
213
  def_delegator :@driver, :'mkdir='
221
- def_delegator :@driver, :'name_list='
222
214
  def_delegator :@driver, :'rmdir='
223
215
  def_delegator :@driver, :'read='
224
216
  def_delegator :@driver, :'rename='
data/ftpd.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "ftpd"
8
- s.version = "0.2.2"
8
+ s.version = "0.3.1"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Wayne Conrad"]
12
- s.date = "2013-03-02"
12
+ s.date = "2013-03-04"
13
13
  s.description = "ftpd is a pure Ruby FTP server library. It supports implicit and explicit TLS, passive and active mode, and most of the commands specified in RFC 969. It an be used as part of a test fixture or embedded in a program."
14
14
  s.email = "wconrad@yagni.com"
15
15
  s.extra_rdoc_files = [
@@ -28,6 +28,7 @@ Gem::Specification.new do |s|
28
28
  "doc/rfc-compliance.md",
29
29
  "examples/example.rb",
30
30
  "examples/hello_world.rb",
31
+ "features/example/eplf.feature",
31
32
  "features/example/example.feature",
32
33
  "features/example/step_definitions/example_server.rb",
33
34
  "features/ftp_server/allo.feature",
@@ -110,9 +111,12 @@ Gem::Specification.new do |s|
110
111
  "lib/ftpd/error.rb",
111
112
  "lib/ftpd/exception_translator.rb",
112
113
  "lib/ftpd/exceptions.rb",
114
+ "lib/ftpd/file_info.rb",
113
115
  "lib/ftpd/file_system_error_translator.rb",
114
116
  "lib/ftpd/ftp_server.rb",
115
117
  "lib/ftpd/insecure_certificate.rb",
118
+ "lib/ftpd/list_format/eplf.rb",
119
+ "lib/ftpd/list_format/ls.rb",
116
120
  "lib/ftpd/server.rb",
117
121
  "lib/ftpd/session.rb",
118
122
  "lib/ftpd/temp_dir.rb",
@@ -127,7 +131,10 @@ Gem::Specification.new do |s|
127
131
  "spec/command_sequence_checker_spec.rb",
128
132
  "spec/disk_file_system_spec.rb",
129
133
  "spec/exception_translator_spec.rb",
134
+ "spec/file_info_spec.rb",
130
135
  "spec/file_system_error_translator_spec.rb",
136
+ "spec/list_format/eplf_spec.rb",
137
+ "spec/list_format/ls_spec.rb",
131
138
  "spec/spec_helper.rb",
132
139
  "spec/translate_exceptions_spec.rb"
133
140
  ]
@@ -148,6 +155,7 @@ Gem::Specification.new do |s|
148
155
  s.add_development_dependency(%q<rake>, [">= 0"])
149
156
  s.add_development_dependency(%q<redcarpet>, [">= 0"])
150
157
  s.add_development_dependency(%q<rspec>, [">= 0"])
158
+ s.add_development_dependency(%q<timecop>, [">= 0"])
151
159
  s.add_development_dependency(%q<yard>, [">= 0"])
152
160
  else
153
161
  s.add_dependency(%q<memoizer>, ["~> 1.0.1"])
@@ -157,6 +165,7 @@ Gem::Specification.new do |s|
157
165
  s.add_dependency(%q<rake>, [">= 0"])
158
166
  s.add_dependency(%q<redcarpet>, [">= 0"])
159
167
  s.add_dependency(%q<rspec>, [">= 0"])
168
+ s.add_dependency(%q<timecop>, [">= 0"])
160
169
  s.add_dependency(%q<yard>, [">= 0"])
161
170
  end
162
171
  else
@@ -167,6 +176,7 @@ Gem::Specification.new do |s|
167
176
  s.add_dependency(%q<rake>, [">= 0"])
168
177
  s.add_dependency(%q<redcarpet>, [">= 0"])
169
178
  s.add_dependency(%q<rspec>, [">= 0"])
179
+ s.add_dependency(%q<timecop>, [">= 0"])
170
180
  s.add_dependency(%q<yard>, [">= 0"])
171
181
  end
172
182
  end
data/lib/ftpd.rb CHANGED
@@ -7,10 +7,15 @@ require 'socket'
7
7
  require 'tmpdir'
8
8
 
9
9
  module Ftpd
10
+ module ListFormat
11
+ autoload :Eplf, 'ftpd/list_format/eplf'
12
+ autoload :Ls, 'ftpd/list_format/ls'
13
+ end
10
14
  autoload :CommandSequenceChecker, 'ftpd/command_sequence_checker'
11
15
  autoload :DiskFileSystem, 'ftpd/disk_file_system'
12
16
  autoload :Error, 'ftpd/error'
13
17
  autoload :ExceptionTranslator, 'ftpd/exception_translator'
18
+ autoload :FileInfo, 'ftpd/file_info'
14
19
  autoload :FileSystemErrorTranslator, 'ftpd/file_system_error_translator'
15
20
  autoload :FileSystemMethodMissing, 'ftpd/file_system_method_missing'
16
21
  autoload :FtpServer, 'ftpd/ftp_server'
@@ -1,7 +1,8 @@
1
1
  # Some commands are supposed to occur in sequence. For example, USER
2
2
  # must be immediately followed by PASS. This class keeps track of
3
- # when a specific command is expected, and raises a "bad sequence"
4
- # error when that command is not next.
3
+ # when a specific command either must arrive or must not arrive, and
4
+ # raises a "bad sequence" error when commands arrive in the wrong
5
+ # sequence.
5
6
 
6
7
  module Ftpd
7
8
  class CommandSequenceChecker
@@ -200,38 +200,6 @@ module Ftpd
200
200
 
201
201
  end
202
202
 
203
- class DiskFileSystem
204
-
205
- # Ls interface used by List and NameList
206
-
207
- module Ls
208
-
209
- include Shellwords
210
-
211
- def ls(ftp_path, option)
212
- path = expand_ftp_path(ftp_path)
213
- dirname = File.dirname(path)
214
- filename = File.basename(path)
215
- command = [
216
- 'ls',
217
- option,
218
- filename,
219
- ].compact
220
- if File.exists?(dirname)
221
- list = Dir.chdir(dirname) do
222
- `#{shelljoin(command)} 2>&1`
223
- end
224
- else
225
- list = ''
226
- end
227
- list = "" if $? != 0
228
- list = list.gsub(/^total \d+\n/, '')
229
- end
230
-
231
- end
232
-
233
- end
234
-
235
203
  class DiskFileSystem
236
204
 
237
205
  # DiskFileSystem mixin providing directory listing
@@ -240,65 +208,86 @@ module Ftpd
240
208
 
241
209
  include TranslateExceptions
242
210
 
243
- # Get a file list, long form. This returns a long-form
244
- # directory listing. The FTP standard does not specify the
245
- # format of the listing, but many systems emit a *nix style
246
- # directory listing:
211
+ # Get information about a single file or directory.
247
212
  #
248
- # -rw-r--r-- 1 wayne wayne 4 Feb 18 18:36 a
249
- # -rw-r--r-- 1 wayne wayne 8 Feb 18 18:36 b
213
+ # Should follow symlinks (per
214
+ # {http://cr.yp.to/ftp/list/eplf.html}, "lstat() is not a good
215
+ # idea for FTP directory listings").
216
+ #
217
+ # @return [FileInfo]
218
+ #
219
+ # Called for:
220
+ # * LIST
221
+ #
222
+ # If missing, then these commands are not supported.
223
+
224
+ def file_info(path)
225
+ stat = File.stat(expand_ftp_path(path))
226
+ FileInfo.new(:ftype => stat.ftype,
227
+ :group => gid_name(stat.gid),
228
+ :identifier => identifier(stat),
229
+ :mode => stat.mode,
230
+ :mtime => stat.mtime,
231
+ :nlink => stat.nlink,
232
+ :owner => uid_name(stat.uid),
233
+ :path => path,
234
+ :size => stat.size)
235
+ end
236
+ translate_exceptions :file_info
237
+
238
+ # Expand a path that may contain globs into a list of paths of
239
+ # matching files and directories.
250
240
  #
251
- # some emit a Windows style listing. Some emit EPLF (Easily
252
- # Parsed List Format):
241
+ # * If the path matches no files, returns an empty list.
253
242
  #
254
- # +i8388621.48594,m825718503,r,s280, djb.html
255
- # +i8388621.50690,m824255907,/, 514
256
- # +i8388621.48598,m824253270,r,s612, 514.html
243
+ # * If the path has no glob and matches a directory, then the
244
+ # list contains only that directory.
257
245
  #
258
- # EPLF is a draft internet standard for the output of LIST:
246
+ # * If the patch has a glob, it may return multiple entries.
259
247
  #
260
- # http://cr.yp.to/ftp/list/eplf.html
248
+ # The paths returned are fully qualified, relative to the root
249
+ # of the virtual file system.
250
+ #
251
+ # For example, suppose these files exist on the physical file
252
+ # system:
261
253
  #
262
- # Some FTP clients know how to parse EPLF; those clients will
263
- # display the EPLF in a more user-friendly format. Clients that
264
- # don't recognize EPLF will display it raw. The advantages of
265
- # EPLF are that it's easier for clients to parse, and the client
266
- # can display the LIST output in any format it likes.
254
+ # /var/lib/ftp/foo/foo
255
+ # /var/lib/ftp/foo/subdir/bar
256
+ # /var/lib/ftp/foo/subdir/baz
267
257
  #
268
- # This class emits a *nix style listing. It does so by shelling
269
- # to the "ls" command, so it won't run on Windows at all.
258
+ # and that the directory /var/lib/ftp is the root of the virtual
259
+ # file system. Then:
260
+ #
261
+ # dir('foo') # => ['/foo']
262
+ # dir('subdir') # => ['/subdir']
263
+ # dir('subdir/*') # => ['/subdir/bar', '/subdir/baz']
264
+ # dir('*') # => ['/foo', '/subdir']
270
265
  #
271
266
  # Called for:
272
267
  # * LIST
268
+ # * NLST
273
269
  #
274
270
  # If missing, then these commands are not supported.
275
271
 
276
- def list(ftp_path)
277
- ls(ftp_path, '-l')
272
+ def dir(path)
273
+ Dir[expand_ftp_path(path)].map do |path|
274
+ path.sub(/^#{@data_dir}/, '')
275
+ end
278
276
  end
277
+ translate_exceptions :dir
279
278
 
280
- end
281
- end
282
-
283
- class DiskFileSystem
284
-
285
- # DiskFileSystem mixin providing directory name listing
286
-
287
- module NameList
279
+ private
288
280
 
289
- include Ls
281
+ def uid_name(uid)
282
+ Etc.getpwuid(uid).name
283
+ end
290
284
 
291
- # Get a file list, short form.
292
- #
293
- # This returns one filename per line, and nothing else
294
- #
295
- # Called for:
296
- # * NLST
297
- #
298
- # If missing, then these commands are not supported.
285
+ def gid_name(gid)
286
+ Etc.getgrgid(gid).name
287
+ end
299
288
 
300
- def name_list(ftp_path)
301
- ls(ftp_path, '-1')
289
+ def identifier(stat)
290
+ [stat.dev, stat.ino].join('.')
302
291
  end
303
292
 
304
293
  end
@@ -367,7 +356,6 @@ module Ftpd
367
356
  include DiskFileSystem::Delete
368
357
  include DiskFileSystem::List
369
358
  include DiskFileSystem::Mkdir
370
- include DiskFileSystem::NameList
371
359
  include DiskFileSystem::Read
372
360
  include DiskFileSystem::Rename
373
361
  include DiskFileSystem::Rmdir