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.
- data/Changelog.md +41 -0
- data/README.md +70 -3
- data/VERSION +1 -1
- data/examples/example.rb +2 -1
- data/features/ftp_server/allo.feature +33 -0
- data/features/ftp_server/command_errors.feature +0 -2
- data/features/ftp_server/delete.feature +7 -0
- data/features/ftp_server/get.feature +7 -0
- data/features/ftp_server/list.feature +6 -0
- data/features/ftp_server/name_list.feature +6 -0
- data/features/ftp_server/put.feature +7 -0
- data/features/ftp_server/step_definitions/test_server.rb +14 -4
- data/features/ftp_server/syst.feature +18 -0
- data/features/step_definitions/{error.rb → error_replies.rb} +0 -0
- data/features/step_definitions/generic_send.rb +9 -0
- data/features/step_definitions/success_replies.rb +7 -0
- data/features/step_definitions/system.rb +7 -0
- data/features/support/test_client.rb +2 -1
- data/features/support/test_server.rb +144 -41
- data/ftpd.gemspec +11 -4
- data/lib/ftpd/disk_file_system.rb +266 -101
- data/lib/ftpd/session.rb +66 -35
- data/spec/disk_file_system_spec.rb +4 -4
- data/spec/file_system_error_translator_spec.rb +43 -0
- metadata +12 -5
data/Changelog.md
ADDED
@@ -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
|
5
|
-
|
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
|
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
|
+
0.2.0
|
data/examples/example.rb
CHANGED
@@ -109,7 +109,8 @@ module Example
|
|
109
109
|
|
110
110
|
def create_files
|
111
111
|
create_file 'README',
|
112
|
-
|
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
|
@@ -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
|
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
|
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
|
11
|
-
|
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
|
File without changes
|
@@ -8,68 +8,101 @@ require File.expand_path('test_server_files',
|
|
8
8
|
class TestServer
|
9
9
|
class TestServerDriver
|
10
10
|
|
11
|
-
|
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
|
-
|
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
|
39
|
-
|
40
|
-
super
|
51
|
+
def self.included(includer)
|
52
|
+
includer.extend ClassMethods
|
41
53
|
end
|
42
54
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
54
|
-
|
55
|
-
super
|
68
|
+
def force_access_denied?(ftp_path)
|
69
|
+
ftp_path =~ /forbidden/
|
56
70
|
end
|
57
71
|
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
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
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
189
|
+
self.debug = false
|
190
|
+
self.tls = :off
|
101
191
|
end
|
102
192
|
|
103
|
-
|
104
|
-
|
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
|