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