ftpd 0.5.0 → 0.6.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/Changelog.md +25 -3
- data/Gemfile +0 -1
- data/Gemfile.lock +0 -5
- data/README.md +109 -51
- data/VERSION +1 -1
- data/doc/benchmarks.md +4 -3
- data/doc/references.md +1 -1
- data/doc/rfc-compliance.md +18 -18
- data/examples/example.rb +1 -0
- data/features/ftp_server/abort.feature +13 -0
- data/features/ftp_server/command_errors.feature +0 -12
- data/features/ftp_server/delay_after_failed_login.feature +23 -0
- data/features/ftp_server/disconnect_after_failed_logins.feature +25 -0
- data/features/ftp_server/features.feature +26 -0
- data/features/ftp_server/max_connections.feature +39 -0
- data/features/ftp_server/options.feature +17 -0
- data/features/ftp_server/put_tls.feature +1 -1
- data/features/ftp_server/reinitialize.feature +13 -0
- data/features/ftp_server/site.feature +13 -0
- data/features/ftp_server/status.feature +1 -2
- data/features/ftp_server/step_definitions/test_server.rb +20 -0
- data/features/ftp_server/structure_mount.feature +13 -0
- data/features/ftp_server/timeout.feature +3 -3
- data/features/step_definitions/append.rb +2 -2
- data/features/step_definitions/client.rb +19 -9
- data/features/step_definitions/client_and_server_files.rb +2 -2
- data/features/step_definitions/client_files.rb +4 -4
- data/features/step_definitions/command.rb +1 -1
- data/features/step_definitions/connect.rb +28 -5
- data/features/step_definitions/delete.rb +2 -2
- data/features/step_definitions/directory_navigation.rb +4 -4
- data/features/step_definitions/error_replies.rb +12 -0
- data/features/step_definitions/features.rb +21 -0
- data/features/step_definitions/file_structure.rb +2 -2
- data/features/step_definitions/generic_send.rb +1 -1
- data/features/step_definitions/get.rb +2 -2
- data/features/step_definitions/help.rb +1 -1
- data/features/step_definitions/invalid_commands.rb +2 -2
- data/features/step_definitions/list.rb +2 -2
- data/features/step_definitions/login.rb +3 -3
- data/features/step_definitions/mkdir.rb +1 -1
- data/features/step_definitions/mode.rb +2 -2
- data/features/step_definitions/options.rb +9 -0
- data/features/step_definitions/passive.rb +1 -1
- data/features/step_definitions/port.rb +1 -1
- data/features/step_definitions/put.rb +3 -3
- data/features/step_definitions/quit.rb +2 -2
- data/features/step_definitions/rename.rb +1 -1
- data/features/step_definitions/rmdir.rb +1 -1
- data/features/step_definitions/server_title.rb +12 -0
- data/features/step_definitions/status.rb +1 -9
- data/features/step_definitions/system.rb +1 -1
- data/features/step_definitions/timing.rb +19 -0
- data/features/step_definitions/type.rb +2 -2
- data/features/support/test_client.rb +62 -7
- data/features/support/test_server.rb +4 -0
- data/ftpd.gemspec +21 -9
- data/lib/ftpd.rb +4 -0
- data/lib/ftpd/command_sequence_checker.rb +4 -2
- data/lib/ftpd/config.rb +13 -0
- data/lib/ftpd/connection_throttle.rb +56 -0
- data/lib/ftpd/connection_tracker.rb +110 -0
- data/lib/ftpd/disk_file_system.rb +2 -2
- data/lib/ftpd/ftp_server.rb +118 -35
- data/lib/ftpd/server.rb +27 -3
- data/lib/ftpd/session.rb +84 -25
- data/lib/ftpd/tls_server.rb +11 -5
- data/rake_tasks/cucumber.rake +1 -0
- data/rake_tasks/jeweler.rake +1 -1
- data/spec/connection_throttle_spec.rb +96 -0
- data/spec/connection_tracker_spec.rb +126 -0
- data/spec/spec_helper.rb +1 -0
- metadata +22 -23
- data/config/cucumber.yml +0 -2
- data/features/step_definitions/stop_server.rb +0 -3
- data/features/step_definitions/timeout.rb +0 -11
@@ -0,0 +1,13 @@
|
|
1
|
+
Feature: Abort
|
2
|
+
|
3
|
+
As a client
|
4
|
+
I want to know this command is not supported
|
5
|
+
So that I can avoid using it
|
6
|
+
|
7
|
+
Background:
|
8
|
+
Given the test server is started
|
9
|
+
|
10
|
+
Scenario: Unimplemented
|
11
|
+
Given a successful connection
|
12
|
+
When the client sends command "ABOR"
|
13
|
+
Then the server returns an unimplemented command error
|
@@ -11,15 +11,3 @@ Feature: Command Errors
|
|
11
11
|
Given a successful connection
|
12
12
|
When the client sends command "foo"
|
13
13
|
Then the server returns a command unrecognized error
|
14
|
-
|
15
|
-
Scenario Outline: Unimplemented command
|
16
|
-
Given a successful connection
|
17
|
-
When the client sends command "<command>"
|
18
|
-
Then the server returns an unimplemented command error
|
19
|
-
Examples:
|
20
|
-
| command |
|
21
|
-
| ABOR |
|
22
|
-
| REIN |
|
23
|
-
| REST |
|
24
|
-
| SITE |
|
25
|
-
| SMNT |
|
@@ -0,0 +1,23 @@
|
|
1
|
+
Feature: Delay After Failed Login
|
2
|
+
|
3
|
+
As an administrator
|
4
|
+
I want to make brute force attacks less efficient
|
5
|
+
So that an attacker doesn't gain access
|
6
|
+
|
7
|
+
Scenario: Failed login attempts
|
8
|
+
Given the test server has a failed login delay of 0.2 seconds
|
9
|
+
And the test server is started
|
10
|
+
Given the client connects
|
11
|
+
And the client logs in with bad user
|
12
|
+
And the client logs in with bad user
|
13
|
+
And the client logs in
|
14
|
+
Then it should take at least 0.4 seconds
|
15
|
+
|
16
|
+
Scenario: Failed login attempts
|
17
|
+
Given the test server has a failed login delay of 0.0 seconds
|
18
|
+
And the test server is started
|
19
|
+
Given the client connects
|
20
|
+
And the client logs in with bad user
|
21
|
+
And the client logs in with bad user
|
22
|
+
And the client logs in
|
23
|
+
Then it should take less than 0.4 seconds
|
@@ -0,0 +1,25 @@
|
|
1
|
+
Feature: Disconnect After Failed Logins
|
2
|
+
|
3
|
+
As an administrator
|
4
|
+
I want to make brute force attacks less efficient
|
5
|
+
So that an attacker doesn't gain access
|
6
|
+
|
7
|
+
Scenario: Disconnected after maximum failed attempts
|
8
|
+
Given the test server has a max of 3 failed login attempts
|
9
|
+
And the test server is started
|
10
|
+
Given the client connects
|
11
|
+
And the client logs in with bad user
|
12
|
+
And the client logs in with bad user
|
13
|
+
When the client logs in with bad user
|
14
|
+
Then the server returns a server unavailable error
|
15
|
+
And the client should not be connected
|
16
|
+
|
17
|
+
Scenario: No maximum configured
|
18
|
+
Given the test server has no max failed login attempts
|
19
|
+
And the test server is started
|
20
|
+
Given the client connects
|
21
|
+
And the client logs in with bad user
|
22
|
+
And the client logs in with bad user
|
23
|
+
And the client logs in with bad user
|
24
|
+
Then the server returns a login incorrect error
|
25
|
+
And the client should be connected
|
@@ -0,0 +1,26 @@
|
|
1
|
+
Feature: Features
|
2
|
+
|
3
|
+
As a client
|
4
|
+
I want to know what FTP extension the server supports
|
5
|
+
So that I can use them without trial-and-error
|
6
|
+
|
7
|
+
Background:
|
8
|
+
|
9
|
+
Scenario: TLS Disabled
|
10
|
+
Given the test server is started
|
11
|
+
And the client connects
|
12
|
+
When the client successfully requests features
|
13
|
+
Then the response should not include TLS features
|
14
|
+
|
15
|
+
Scenario: TLS Enabled
|
16
|
+
Given the test server has TLS mode "explicit"
|
17
|
+
And the test server is started
|
18
|
+
And the client connects
|
19
|
+
When the client successfully requests features
|
20
|
+
Then the response should include TLS features
|
21
|
+
|
22
|
+
Scenario: Argument given
|
23
|
+
Given the test server is started
|
24
|
+
And the client connects
|
25
|
+
When the client sends "FEAT FOO"
|
26
|
+
Then the server returns a syntax error
|
@@ -0,0 +1,39 @@
|
|
1
|
+
Feature: Max Connections
|
2
|
+
|
3
|
+
As an administrator
|
4
|
+
I want to limit the number of connections
|
5
|
+
To prevent overload
|
6
|
+
|
7
|
+
Scenario: Total connections
|
8
|
+
Given the test server has max_connections set to 2
|
9
|
+
And the test server is started
|
10
|
+
And the 1st client connects
|
11
|
+
And the 2nd client connects
|
12
|
+
When the 3rd client tries to connect
|
13
|
+
Then the server returns a too many connections error
|
14
|
+
|
15
|
+
Scenario: Connections per user
|
16
|
+
And the test server has max_connections_per_ip set to 1
|
17
|
+
And the test server is started
|
18
|
+
And the 1st client connects from 127.0.0.1
|
19
|
+
And the 2nd client connects from 127.0.0.2
|
20
|
+
When the 3rd client tries to connect from 127.0.0.2
|
21
|
+
Then the server returns a too many connections error
|
22
|
+
|
23
|
+
Scenario: TLS
|
24
|
+
Given the test server has max_connections set to 2
|
25
|
+
And the test server has TLS mode "explicit"
|
26
|
+
And the test server is started
|
27
|
+
And the 1st client connects
|
28
|
+
And the 2nd client connects
|
29
|
+
When the 3rd client tries to connect
|
30
|
+
Then the server returns a too many connections error
|
31
|
+
|
32
|
+
Scenario: Connections per user, TLS
|
33
|
+
And the test server has max_connections_per_ip set to 1
|
34
|
+
And the test server has TLS mode "explicit"
|
35
|
+
And the test server is started
|
36
|
+
And the 1st client connects from 127.0.0.1
|
37
|
+
And the 2nd client connects from 127.0.0.2
|
38
|
+
When the 3rd client tries to connect from 127.0.0.2
|
39
|
+
Then the server returns a too many connections error
|
@@ -0,0 +1,17 @@
|
|
1
|
+
Feature: Options
|
2
|
+
|
3
|
+
As a client
|
4
|
+
I want to know set options
|
5
|
+
To tailor the server's behavior
|
6
|
+
|
7
|
+
Background:
|
8
|
+
Given the test server is started
|
9
|
+
And the client connects
|
10
|
+
|
11
|
+
Scenario: No argument
|
12
|
+
When the client sends "OPTS"
|
13
|
+
Then the server returns a syntax error
|
14
|
+
|
15
|
+
Scenario: Unknown option command
|
16
|
+
When the client sets option "ABC"
|
17
|
+
Then the server returns a bad option error
|
@@ -0,0 +1,13 @@
|
|
1
|
+
Feature: Reinitialize
|
2
|
+
|
3
|
+
As a client
|
4
|
+
I want to know this command is not supported
|
5
|
+
So that I can avoid using it
|
6
|
+
|
7
|
+
Background:
|
8
|
+
Given the test server is started
|
9
|
+
|
10
|
+
Scenario: Unimplemented
|
11
|
+
Given a successful connection
|
12
|
+
When the client sends command "REIN"
|
13
|
+
Then the server returns an unimplemented command error
|
@@ -0,0 +1,13 @@
|
|
1
|
+
Feature: Site
|
2
|
+
|
3
|
+
As a client
|
4
|
+
I want to know this command is not supported
|
5
|
+
So that I can avoid using it
|
6
|
+
|
7
|
+
Background:
|
8
|
+
Given the test server is started
|
9
|
+
|
10
|
+
Scenario: Unimplemented
|
11
|
+
Given a successful connection
|
12
|
+
When the client sends command "SITE"
|
13
|
+
Then the server returns an unimplemented command error
|
@@ -39,3 +39,23 @@ end
|
|
39
39
|
Given /^the test server allows low data ports$/ do
|
40
40
|
server.allow_low_data_ports = true
|
41
41
|
end
|
42
|
+
|
43
|
+
Given /^the test server has max_connections set to (\d+)$/ do |s|
|
44
|
+
server.max_connections = s.to_i
|
45
|
+
end
|
46
|
+
|
47
|
+
Given /^the test server has max_connections_per_ip set to (\d+)$/ do |s|
|
48
|
+
server.max_connections_per_ip = s.to_i
|
49
|
+
end
|
50
|
+
|
51
|
+
Given /^the test server has no max failed login attempts$/ do
|
52
|
+
server.max_failed_logins = nil
|
53
|
+
end
|
54
|
+
|
55
|
+
Given /^the test server has a max of (\d+) failed login attempts$/ do |s|
|
56
|
+
server.max_failed_logins = s.to_i
|
57
|
+
end
|
58
|
+
|
59
|
+
Given /^the test server has a failed login delay of (\S+) seconds$/ do |s|
|
60
|
+
server.failed_login_delay = s.to_f
|
61
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
Feature: Structure Mount
|
2
|
+
|
3
|
+
As a client
|
4
|
+
I want to know this command is not supported
|
5
|
+
So that I can avoid using it
|
6
|
+
|
7
|
+
Background:
|
8
|
+
Given the test server is started
|
9
|
+
|
10
|
+
Scenario: Unimplemented
|
11
|
+
Given a successful connection
|
12
|
+
When the client sends command "SMNT"
|
13
|
+
Then the server returns an unimplemented command error
|
@@ -9,18 +9,18 @@ Feature: Port
|
|
9
9
|
And the test server is started
|
10
10
|
And a successful login
|
11
11
|
When the client is idle for 0.6 seconds
|
12
|
-
Then the client
|
12
|
+
Then the client should not be connected
|
13
13
|
|
14
14
|
Scenario: Session not idle too long
|
15
15
|
Given the test server has session timeout set to 0.5 seconds
|
16
16
|
And the test server is started
|
17
17
|
And a successful login
|
18
18
|
When the client is idle for 0 seconds
|
19
|
-
Then the client
|
19
|
+
Then the client should be connected
|
20
20
|
|
21
21
|
Scenario: Timeout disabled
|
22
22
|
Given the test server has session timeout disabled
|
23
23
|
And the test server is started
|
24
24
|
And a successful login
|
25
25
|
When the client is idle for 0.6 seconds
|
26
|
-
Then the client
|
26
|
+
Then the client should be connected
|
@@ -6,10 +6,10 @@ end
|
|
6
6
|
|
7
7
|
When /^the client successfully appends text "(.*?)" onto "(.*?)"$/ do
|
8
8
|
|local_path, remote_path|
|
9
|
-
|
9
|
+
client.append_text local_path, remote_path
|
10
10
|
end
|
11
11
|
|
12
12
|
When /^the client successfully appends binary "(.*?)" onto "(.*?)"$/ do
|
13
13
|
|local_path, remote_path|
|
14
|
-
|
14
|
+
client.append_binary local_path, remote_path
|
15
15
|
end
|
@@ -1,14 +1,24 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
class Clients
|
4
|
+
|
5
|
+
include Singleton
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@clients = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def [](client_name)
|
12
|
+
@clients[client_name] ||= TestClient.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def close
|
16
|
+
@clients.values.each(&:close)
|
17
|
+
end
|
7
18
|
|
8
|
-
def set_client(client_name, client)
|
9
|
-
instance_variable_set client_variable_name(client_name), client
|
10
19
|
end
|
11
20
|
|
12
21
|
def client(client_name = nil)
|
13
|
-
|
22
|
+
client_name ||= 'client'
|
23
|
+
Clients.instance[client_name]
|
14
24
|
end
|
@@ -7,7 +7,7 @@ Then /^the remote file "(.*?)" should( exactly)? match the local file$/ do
|
|
7
7
|
|remote_path, exactly|
|
8
8
|
local_path = File.basename(remote_path)
|
9
9
|
remote_contents = server.file_contents(remote_path)
|
10
|
-
local_contents =
|
10
|
+
local_contents = client.file_contents(local_path)
|
11
11
|
remote_contents = unix_line_endings(exactly, remote_contents)
|
12
12
|
local_contents = unix_line_endings(exactly, local_contents)
|
13
13
|
remote_contents.should == local_contents
|
@@ -17,7 +17,7 @@ Then /^the local file "(.*?)" should( exactly)? match the remote file$/ do
|
|
17
17
|
|local_path, exactly|
|
18
18
|
remote_path = local_path
|
19
19
|
remote_contents = server.file_contents(remote_path)
|
20
|
-
local_contents =
|
20
|
+
local_contents = client.file_contents(local_path)
|
21
21
|
remote_contents = unix_line_endings(exactly, remote_contents)
|
22
22
|
local_contents = unix_line_endings(exactly, local_contents)
|
23
23
|
local_contents.should == remote_contents
|
@@ -1,14 +1,14 @@
|
|
1
1
|
Given /^the client has file "(.*?)"$/ do |local_path|
|
2
|
-
|
2
|
+
client.add_file local_path
|
3
3
|
end
|
4
4
|
|
5
5
|
Then /^the local file "(.*?)" should have (unix|windows) line endings$/ do
|
6
6
|
|local_path, line_ending_type|
|
7
|
-
line_ending_type(
|
7
|
+
line_ending_type(client.file_contents(local_path)).should ==
|
8
8
|
line_ending_type.to_sym
|
9
9
|
end
|
10
10
|
|
11
11
|
Then /^the local file "(.*?)" should match its template$/ do |local_path|
|
12
|
-
|
13
|
-
|
12
|
+
client.template(local_path).should ==
|
13
|
+
client.file_contents(local_path)
|
14
14
|
end
|
@@ -4,11 +4,34 @@ require 'net/ftp'
|
|
4
4
|
When /^the( \w+)? client connects(?: with (\w+) TLS)?$/ do
|
5
5
|
|client_name, tls_mode|
|
6
6
|
tls_mode ||= :off
|
7
|
-
client =
|
8
|
-
client
|
9
|
-
|
7
|
+
client(client_name).tls_mode = tls_mode.to_sym
|
8
|
+
client(client_name).start
|
9
|
+
client(client_name).connect(server.host, server.port)
|
10
10
|
end
|
11
11
|
|
12
|
-
|
13
|
-
|
12
|
+
When /^the (\d+)rd client tries to connect$/ do |client_name|
|
13
|
+
client(client_name).start
|
14
|
+
capture_error do
|
15
|
+
client(client_name).connect(server.host, server.port)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
When /^the (\S+) client connects from (\S+)$/ do
|
20
|
+
|client_name, source_ip|
|
21
|
+
client(client_name).connect_from(source_ip, server.host, server.port)
|
22
|
+
end
|
23
|
+
|
24
|
+
When /^the (\S+) client tries to connect from (\S+)$/ do
|
25
|
+
|client_name, source_ip|
|
26
|
+
capture_error do
|
27
|
+
step "the #{client_name} client connects from #{source_ip}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
Then /^the client should be connected$/ do
|
32
|
+
client.should be_connected
|
33
|
+
end
|
34
|
+
|
35
|
+
Then /^the client should not be connected$/ do
|
36
|
+
client.should_not be_connected
|
14
37
|
end
|
@@ -5,11 +5,11 @@ When /^the client deletes "(.*?)"$/ do |path|
|
|
5
5
|
end
|
6
6
|
|
7
7
|
When /^the client successfully deletes "(.*?)"$/ do |path|
|
8
|
-
|
8
|
+
client.delete path
|
9
9
|
end
|
10
10
|
|
11
11
|
When /^the client deletes with no path$/ do
|
12
12
|
capture_error do
|
13
|
-
|
13
|
+
client.raw 'DELE'
|
14
14
|
end
|
15
15
|
end
|
@@ -10,17 +10,17 @@ end
|
|
10
10
|
# ensure that CDUP is sent and therefore tested.
|
11
11
|
|
12
12
|
When /^the client successfully cd's up$/ do
|
13
|
-
|
13
|
+
client.raw 'CDUP'
|
14
14
|
end
|
15
15
|
|
16
16
|
When /^the client successfully cd's to "(.*?)"$/ do |path|
|
17
|
-
|
17
|
+
client.chdir path
|
18
18
|
end
|
19
19
|
|
20
20
|
Then /^the current directory should be "(.*?)"$/ do |path|
|
21
|
-
|
21
|
+
client.pwd.should == path
|
22
22
|
end
|
23
23
|
|
24
24
|
Then /^the XPWD directory should be "(.*?)"$/ do |path|
|
25
|
-
|
25
|
+
client.xpwd.should == path
|
26
26
|
end
|