ftpd 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of ftpd might be problematic. Click here for more details.

@@ -0,0 +1,41 @@
1
+ ### dev
2
+
3
+ Bug fixes
4
+
5
+ * Fixed formatting in Changelog
6
+
7
+ ### 0.2.0
8
+
9
+ API changes
10
+
11
+ * Renamed two of the file system methods:
12
+
13
+ * `list_long -> long`
14
+ * `list_short -> short`
15
+
16
+ This will affect anyone who has written their own disk system.
17
+ Anyone using Ftpd::DiskFileSystem won't notice this change.
18
+
19
+ Enhancements
20
+
21
+ * Some commands are now optional, depending upon the file system.
22
+ These are RETR, DELE, LIST and NLST. See the comments in
23
+ Ftpd::DiskFileSystem for what command depends upon what method.
24
+ * Better text in example's ephemeral README
25
+ * Divided the DiskFileSystem into mixins.
26
+ * Improved documentation.
27
+ * Support SYST
28
+ * Support ALLO
29
+ * Removed dead code
30
+ * Added more tests
31
+
32
+ ### 0.1.1
33
+
34
+ Enhancements
35
+
36
+ * Improved documentation.
37
+ * Gemfile: development gems no longer lock down version
38
+
39
+ ### 0.1.0
40
+
41
+ First usable release
data/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # FTPD
2
2
 
3
3
  ftpd is a pure Ruby FTP server library. It supports implicit and
4
- explicit TLS, and is suitable for use by a program such as a test
5
- fixture or small FTP daemon.
4
+ explicit TLS, and can be used as part of a test fixture or to embed in
5
+ another program.
6
6
 
7
7
  ## HELLO WORLD
8
8
 
9
9
  This is examples/hello_world.rb, a bare minimum FTP server. It allows
10
- any user/password, and serves files in the diretory '/tmp/ftp'. It
10
+ any user/password, and serves files in a temporary directory. It
11
11
  binds to an ephemeral port on the local interface:
12
12
 
13
13
  require 'ftpd'
@@ -40,6 +40,51 @@ binds to an ephemeral port on the local interface:
40
40
  A more full-featured example that allows TLS and takes options is in
41
41
  examples/example.rb
42
42
 
43
+ ## DRIVER
44
+
45
+ Ftpd's dynamic behavior such as authentication and file retrieval is
46
+ controlled by a driver that you supply. The Driver class in the
47
+ "hello world" example above shows a rudimentary driver. Ftpd calls
48
+ the authenticate method to decide who can log in. Once someone is
49
+ logged on, it calls the file_system method to obtain a file system
50
+ driver for that user.
51
+
52
+ There is no base class for a driver. Any class with that signature
53
+ will do.
54
+
55
+ ## FILE SYSTEM
56
+
57
+ The file system object that the driver supplies to Ftpd is Ftpds
58
+ gateway to the logical file system. Ftpd doesn't know or care whether
59
+ it's serving files from disk, memory, or any other means.
60
+
61
+ The file system can be very minimal. If the file system is missing
62
+ certain methods, the server simply disables the commands which need
63
+ that method. For example, if there is no write method, then STOR is
64
+ not supported and causes a "502 Command not implemented" response to
65
+ the client.
66
+
67
+ The canonical and commented example of an Ftpd file system is
68
+ Ftpd::DiskFileSystem.
69
+
70
+ ## DEBUGGING
71
+
72
+ Ftpd can write debugging information (essentially a transcript of its
73
+ conversation with a client) to a file. If you turn the debug flag on,
74
+ the server will write debug information to stdout:
75
+
76
+ server = Ftpd::FtpServer.new(driver)
77
+ server.debug = true
78
+
79
+ If you want to send the debug output to somewhere else, set
80
+ debug_path:
81
+
82
+ server.debug_path = '/tmp/ftp_session'
83
+
84
+ Debug output can also be enabled by setting the environment variable
85
+ FTPD_DEBUG to a non-zero value. This is a convenient way to get debug
86
+ output without having to change any code.
87
+
43
88
  ## LIMITATIONS
44
89
 
45
90
  TLS is only supported in passive mode, not active, but I don't know
@@ -50,6 +95,28 @@ The DiskFileSystem class only works in Linux. This is because it
50
95
  shells out to the "ls" command. This affects the example, which uses
51
96
  the DiskFileSystem.
52
97
 
98
+ The control connection is supposed to be a Telnet session. It's not.
99
+ In practice, it doesn't seem to matter whether it's a Telnet session
100
+ or just plain sending and receiving characters.
101
+
102
+ The following commands defined by RFC969 are understood, but not
103
+ implemented. They result in a "502 Command not implemented" response.
104
+
105
+ * ABOR - Abort
106
+ * ACCT - Account
107
+ * APPE - Append (with create)
108
+ * HELP - Help
109
+ * MKD - Make directory
110
+ * REIN - Reinitialize
111
+ * REST - Restart
112
+ * RMD - Remove directory
113
+ * RNFR - Rename from
114
+ * RNTO - Rename to
115
+ * SITE - Site parameters
116
+ * SMNT - Structure mount
117
+ * STAT - Status
118
+ * STOU - Store Unique
119
+
53
120
  ## DEVELOPMENT
54
121
 
55
122
  ### TESTS
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.2.0
@@ -109,7 +109,8 @@ module Example
109
109
 
110
110
  def create_files
111
111
  create_file 'README',
112
- "Temporary directory created by ftpd sample program\n"
112
+ "This file, and the directory it is in, will go away\n"
113
+ "When this example exits.\n"
113
114
  end
114
115
 
115
116
  def create_file(path, contents)
@@ -0,0 +1,33 @@
1
+ Feature: Port
2
+
3
+ As a client
4
+ I want to reserve file system space
5
+ So that my put will succeed
6
+
7
+ Background:
8
+ Given the test server is started
9
+
10
+ Scenario: With count
11
+ Given a successful login
12
+ When the client successfully sends "ALLO 1024"
13
+ Then the server returns a not necessary reply
14
+
15
+ Scenario: With count and record size
16
+ Given a successful login
17
+ When the client successfully sends "ALLO 1024 R 128"
18
+ Then the server returns a not necessary reply
19
+
20
+ Scenario: Not logged in
21
+ Given a successful connection
22
+ When the client sends "ALLO 1024"
23
+ Then the server returns a not logged in error
24
+
25
+ Scenario: Missing argument
26
+ Given a successful login
27
+ When the client sends "ALLO"
28
+ Then the server returns a syntax error
29
+
30
+ Scenario: Invalid argument
31
+ Given a successful login
32
+ When the client sends "ALLO XYZ"
33
+ Then the server returns a syntax error
@@ -20,7 +20,6 @@ Feature: Command Errors
20
20
  | command |
21
21
  | ABOR |
22
22
  | ACCT |
23
- | ALLO |
24
23
  | APPE |
25
24
  | HELP |
26
25
  | MKD |
@@ -33,4 +32,3 @@ Feature: Command Errors
33
32
  | SMNT |
34
33
  | STAT |
35
34
  | STOU |
36
- | SYST |
@@ -51,3 +51,10 @@ Feature: Delete
51
51
  Given a successful connection
52
52
  When the client deletes "foo"
53
53
  Then the server returns a not logged in error
54
+
55
+ Scenario: Delete not enabled
56
+ Given the test server is started without delete
57
+ And a successful login
58
+ And the server has file "foo"
59
+ When the client deletes "foo"
60
+ Then the server returns an unimplemented command error
@@ -71,3 +71,10 @@ Feature: Get
71
71
  Given a successful login
72
72
  When the client gets text "unable"
73
73
  Then the server returns an action not taken error
74
+
75
+ Scenario: Read not enabled
76
+ Given the test server is started without read
77
+ And a successful login
78
+ And the server has file "foo"
79
+ When the client gets text "foo"
80
+ Then the server returns an unimplemented command error
@@ -69,3 +69,9 @@ Feature: List
69
69
  Given a successful connection
70
70
  When the client lists the directory
71
71
  Then the server returns a not logged in error
72
+
73
+ Scenario: List not enabled
74
+ Given the test server is started without list
75
+ And a successful login
76
+ When the client lists the directory
77
+ Then the server returns an unimplemented command error
@@ -69,3 +69,9 @@ Feature: Name List
69
69
  Given a successful connection
70
70
  When the client name-lists the directory
71
71
  Then the server returns a not logged in error
72
+
73
+ Scenario: List not enabled
74
+ Given the test server is started without name_list
75
+ And a successful login
76
+ When the client name-lists the directory
77
+ Then the server returns an unimplemented command error
@@ -71,3 +71,10 @@ Feature: Put
71
71
  And the client has file "unable"
72
72
  When the client puts text "unable"
73
73
  Then the server returns an action not taken error
74
+
75
+ Scenario: Write not enabled
76
+ Given the test server is started without write
77
+ And a successful login
78
+ And the client has file "foo"
79
+ When the client puts text "foo"
80
+ Then the server returns an unimplemented command error
@@ -1,12 +1,22 @@
1
1
  Given /^the test server is started$/ do
2
- @server = TestServer.new(:tls => :off)
2
+ @server = TestServer.new
3
+ @server.start
3
4
  end
4
5
 
5
6
  Given /^the test server is started with TLS$/ do
6
- @server = TestServer.new(:tls => :explicit)
7
+ @server = TestServer.new
8
+ @server.tls = :explicit
9
+ @server.start
7
10
  end
8
11
 
9
12
  Given /^the test server is started with debug$/ do
10
- @server = TestServer.new(:tls => :off,
11
- :debug => true)
13
+ @server = TestServer.new
14
+ @server.debug = true
15
+ @server.start
16
+ end
17
+
18
+ Given /^the test server is started without (\w+)$/ do |feature|
19
+ @server = TestServer.new
20
+ @server.send "#{feature}=", false
21
+ @server.start
12
22
  end
@@ -0,0 +1,18 @@
1
+ Feature: Port
2
+
3
+ As a client
4
+ I want to identify the server
5
+ So that I know how it will behave
6
+
7
+ Background:
8
+ Given the test server is started
9
+
10
+ Scenario: Success
11
+ Given a successful connection
12
+ When the client successfully queries system ID
13
+ Then the server returns a system ID reply
14
+
15
+ Scenario: With argument
16
+ Given a successful login
17
+ When the client sends "SYST 1"
18
+ Then the server returns a syntax error
@@ -0,0 +1,9 @@
1
+ When /^the client successfully sends "(.*?)"$/ do |command|
2
+ @reply = @client.raw command
3
+ end
4
+
5
+ When /^the client sends "(.*?)"$/ do |command|
6
+ capture_error do
7
+ step %Q'the client successfully sends "#{command}"'
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ Then /^the server returns a "(.*?)" reply$/ do |reply|
2
+ @reply.should == reply + "\n"
3
+ end
4
+
5
+ Then /^the server returns a not necessary reply$/ do
6
+ step 'the server returns a "202 Command not needed at this site" reply'
7
+ end
@@ -0,0 +1,7 @@
1
+ When /^the client successfully queries system ID$/ do
2
+ @reply = @client.system
3
+ end
4
+
5
+ Then /^the server returns a system ID reply$/ do
6
+ step 'the server returns a "UNIX Type: L8" reply'
7
+ end
@@ -28,7 +28,8 @@ class TestClient
28
28
  :noop,
29
29
  :passive=,
30
30
  :pwd,
31
- :quit
31
+ :quit,
32
+ :system
32
33
 
33
34
  def raw(*command)
34
35
  @ftp.sendcmd command.compact.join(' ')
@@ -8,68 +8,101 @@ require File.expand_path('test_server_files',
8
8
  class TestServer
9
9
  class TestServerDriver
10
10
 
11
- def initialize(temp_dir)
12
- @temp_dir = temp_dir
13
- end
11
+ extend Forwardable
14
12
 
15
13
  USER = 'user'
16
14
  PASSWORD = 'password'
17
15
 
16
+ attr_accessor :delete
17
+ attr_accessor :list
18
+ attr_accessor :name_list
19
+ attr_accessor :read
20
+ attr_accessor :write
21
+
22
+ def initialize(temp_dir)
23
+ @temp_dir = temp_dir
24
+ @delete = true
25
+ @list = true
26
+ @name_list = true
27
+ @read = true
28
+ @write = true
29
+ end
30
+
18
31
  def authenticate(user, password)
19
32
  user == USER && password == PASSWORD
20
33
  end
21
34
 
22
35
  def file_system(user)
23
- TestServerFileSystem.new(@temp_dir)
36
+ TestServerFileSystem.new(@temp_dir,
37
+ :delete => @delete,
38
+ :list => @list,
39
+ :name_list => @name_list,
40
+ :read => @read,
41
+ :write => @write)
24
42
  end
25
43
 
26
44
  end
27
45
  end
28
46
 
29
47
  class TestServer
30
- class TestServerFileSystem < Ftpd::DiskFileSystem
31
48
 
32
- def accessible?(ftp_path)
33
- return false if force_access_denied?(ftp_path)
34
- return true if force_file_system_error?(ftp_path)
35
- super
36
- end
49
+ module ForcesAccessDenied
37
50
 
38
- def exists?(ftp_path)
39
- return true if force_file_system_error?(ftp_path)
40
- super
51
+ def self.included(includer)
52
+ includer.extend ClassMethods
41
53
  end
42
54
 
43
- def directory?(ftp_path)
44
- return true if force_file_system_error?(ftp_path)
45
- super
46
- end
55
+ module ClassMethods
56
+
57
+ def return_false_on_force_access_denied(method_name)
58
+ original_method = instance_method(method_name)
59
+ define_method method_name do |*args|
60
+ ftp_path = args.first
61
+ return false if force_access_denied?(ftp_path)
62
+ original_method.bind(self).call *args
63
+ end
64
+ end
47
65
 
48
- def delete(ftp_path)
49
- force_file_system_error(ftp_path)
50
- super
51
66
  end
52
67
 
53
- def read(ftp_path)
54
- force_file_system_error(ftp_path)
55
- super
68
+ def force_access_denied?(ftp_path)
69
+ ftp_path =~ /forbidden/
56
70
  end
57
71
 
58
- def write(ftp_path, contents)
59
- force_file_system_error(ftp_path)
60
- super
72
+ end
73
+
74
+ end
75
+
76
+ class TestServer
77
+
78
+ module ForcesFileSystemError
79
+
80
+ def self.included(includer)
81
+ includer.extend ClassMethods
61
82
  end
62
83
 
63
- private
84
+ module ClassMethods
85
+
86
+ def raise_on_file_system_error(method_name)
87
+ original_method = instance_method(method_name)
88
+ define_method method_name do |*args|
89
+ ftp_path = args.first
90
+ if force_file_system_error?(ftp_path)
91
+ raise Ftpd::FileSystemError, 'Unable to do it'
92
+ end
93
+ original_method.bind(self).call *args
94
+ end
95
+ end
64
96
 
65
- def force_file_system_error(ftp_path)
66
- if force_file_system_error?(ftp_path)
67
- raise Ftpd::FileSystemError, 'Unable to do it'
97
+ def return_true_on_file_system_error(method_name)
98
+ original_method = instance_method(method_name)
99
+ define_method method_name do |*args|
100
+ ftp_path = args.first
101
+ return true if force_file_system_error?(ftp_path)
102
+ original_method.bind(self).call *args
103
+ end
68
104
  end
69
- end
70
105
 
71
- def force_access_denied?(ftp_path)
72
- ftp_path =~ /forbidden/
73
106
  end
74
107
 
75
108
  def force_file_system_error?(ftp_path)
@@ -79,35 +112,105 @@ class TestServer
79
112
  end
80
113
  end
81
114
 
115
+ class TestServer
116
+ class TestServerFileSystem
117
+
118
+ # In order to test ftpd's ability to adapt itself to the driver's
119
+ # signature, we create a new, anonymous instance of the file
120
+ # system class for each test. The option flags determine whether
121
+ # or not to mix in certain behavior such as writing files, reading
122
+ # files, etc.
123
+
124
+ def self.new(data_dir, opts)
125
+ Class.new do
126
+
127
+ include ForcesAccessDenied
128
+ include ForcesFileSystemError
129
+
130
+ include Ftpd::DiskFileSystem::Base
131
+
132
+ if opts[:delete]
133
+ include Ftpd::DiskFileSystem::Delete
134
+ raise_on_file_system_error :delete
135
+ end
136
+
137
+ if opts[:list]
138
+ include Ftpd::DiskFileSystem::List
139
+ end
140
+
141
+ if opts[:name_list]
142
+ include Ftpd::DiskFileSystem::NameList
143
+ end
144
+
145
+ if opts[:read]
146
+ include Ftpd::DiskFileSystem::Read
147
+ raise_on_file_system_error :read
148
+ end
149
+
150
+ if opts[:write]
151
+ include Ftpd::DiskFileSystem::Write
152
+ raise_on_file_system_error :write
153
+ end
154
+
155
+ def initialize(data_dir)
156
+ set_data_dir data_dir
157
+ translate_exception SystemCallError
158
+ end
159
+
160
+ return_false_on_force_access_denied :accessible?
161
+
162
+ return_true_on_file_system_error :accessible?
163
+ return_true_on_file_system_error :exists?
164
+ return_true_on_file_system_error :directory?
165
+
166
+ end.new(data_dir)
167
+ end
168
+
169
+ end
170
+ end
171
+
82
172
  class TestServer
83
173
 
174
+ extend Forwardable
84
175
  include FileUtils
85
- include TestServerFiles
86
176
  include Ftpd::InsecureCertificate
177
+ include TestServerFiles
87
178
 
88
179
  def initialize(opts = {})
89
180
  tls = opts[:tls] || :off
90
181
  @temp_dir = Ftpd::TempDir.make
91
182
  @debug_file = Tempfile.new('ftp-server-debug-output')
92
183
  @debug_file.close
93
- driver = TestServerDriver.new(@temp_dir)
94
- @server = Ftpd::FtpServer.new(driver)
95
- @server.tls = tls
184
+ @driver = TestServerDriver.new(@temp_dir)
185
+ @server = Ftpd::FtpServer.new(@driver)
96
186
  @server.certfile_path = insecure_certfile_path
97
187
  @server.debug_path = @debug_file.path
98
- @server.debug = opts[:debug]
99
188
  @templates = TestFileTemplates.new
100
- @server.start
189
+ self.debug = false
190
+ self.tls = :off
101
191
  end
102
192
 
103
- def wrote_debug_output?
104
- File.size(@debug_file.path) > 0
193
+ def_delegator :@server, :'debug='
194
+ def_delegator :@server, :'tls='
195
+
196
+ def_delegator :@driver, :'delete='
197
+ def_delegator :@driver, :'list='
198
+ def_delegator :@driver, :'name_list='
199
+ def_delegator :@driver, :'read='
200
+ def_delegator :@driver, :'write='
201
+
202
+ def start
203
+ @server.start
105
204
  end
106
205
 
107
206
  def stop
108
207
  @server.stop
109
208
  end
110
209
 
210
+ def wrote_debug_output?
211
+ File.size(@debug_file.path) > 0
212
+ end
213
+
111
214
  def host
112
215
  'localhost'
113
216
  end