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.
Files changed (76) hide show
  1. data/Changelog.md +25 -3
  2. data/Gemfile +0 -1
  3. data/Gemfile.lock +0 -5
  4. data/README.md +109 -51
  5. data/VERSION +1 -1
  6. data/doc/benchmarks.md +4 -3
  7. data/doc/references.md +1 -1
  8. data/doc/rfc-compliance.md +18 -18
  9. data/examples/example.rb +1 -0
  10. data/features/ftp_server/abort.feature +13 -0
  11. data/features/ftp_server/command_errors.feature +0 -12
  12. data/features/ftp_server/delay_after_failed_login.feature +23 -0
  13. data/features/ftp_server/disconnect_after_failed_logins.feature +25 -0
  14. data/features/ftp_server/features.feature +26 -0
  15. data/features/ftp_server/max_connections.feature +39 -0
  16. data/features/ftp_server/options.feature +17 -0
  17. data/features/ftp_server/put_tls.feature +1 -1
  18. data/features/ftp_server/reinitialize.feature +13 -0
  19. data/features/ftp_server/site.feature +13 -0
  20. data/features/ftp_server/status.feature +1 -2
  21. data/features/ftp_server/step_definitions/test_server.rb +20 -0
  22. data/features/ftp_server/structure_mount.feature +13 -0
  23. data/features/ftp_server/timeout.feature +3 -3
  24. data/features/step_definitions/append.rb +2 -2
  25. data/features/step_definitions/client.rb +19 -9
  26. data/features/step_definitions/client_and_server_files.rb +2 -2
  27. data/features/step_definitions/client_files.rb +4 -4
  28. data/features/step_definitions/command.rb +1 -1
  29. data/features/step_definitions/connect.rb +28 -5
  30. data/features/step_definitions/delete.rb +2 -2
  31. data/features/step_definitions/directory_navigation.rb +4 -4
  32. data/features/step_definitions/error_replies.rb +12 -0
  33. data/features/step_definitions/features.rb +21 -0
  34. data/features/step_definitions/file_structure.rb +2 -2
  35. data/features/step_definitions/generic_send.rb +1 -1
  36. data/features/step_definitions/get.rb +2 -2
  37. data/features/step_definitions/help.rb +1 -1
  38. data/features/step_definitions/invalid_commands.rb +2 -2
  39. data/features/step_definitions/list.rb +2 -2
  40. data/features/step_definitions/login.rb +3 -3
  41. data/features/step_definitions/mkdir.rb +1 -1
  42. data/features/step_definitions/mode.rb +2 -2
  43. data/features/step_definitions/options.rb +9 -0
  44. data/features/step_definitions/passive.rb +1 -1
  45. data/features/step_definitions/port.rb +1 -1
  46. data/features/step_definitions/put.rb +3 -3
  47. data/features/step_definitions/quit.rb +2 -2
  48. data/features/step_definitions/rename.rb +1 -1
  49. data/features/step_definitions/rmdir.rb +1 -1
  50. data/features/step_definitions/server_title.rb +12 -0
  51. data/features/step_definitions/status.rb +1 -9
  52. data/features/step_definitions/system.rb +1 -1
  53. data/features/step_definitions/timing.rb +19 -0
  54. data/features/step_definitions/type.rb +2 -2
  55. data/features/support/test_client.rb +62 -7
  56. data/features/support/test_server.rb +4 -0
  57. data/ftpd.gemspec +21 -9
  58. data/lib/ftpd.rb +4 -0
  59. data/lib/ftpd/command_sequence_checker.rb +4 -2
  60. data/lib/ftpd/config.rb +13 -0
  61. data/lib/ftpd/connection_throttle.rb +56 -0
  62. data/lib/ftpd/connection_tracker.rb +110 -0
  63. data/lib/ftpd/disk_file_system.rb +2 -2
  64. data/lib/ftpd/ftp_server.rb +118 -35
  65. data/lib/ftpd/server.rb +27 -3
  66. data/lib/ftpd/session.rb +84 -25
  67. data/lib/ftpd/tls_server.rb +11 -5
  68. data/rake_tasks/cucumber.rake +1 -0
  69. data/rake_tasks/jeweler.rake +1 -1
  70. data/spec/connection_throttle_spec.rb +96 -0
  71. data/spec/connection_tracker_spec.rb +126 -0
  72. data/spec/spec_helper.rb +1 -0
  73. metadata +22 -23
  74. data/config/cucumber.yml +0 -2
  75. data/features/step_definitions/stop_server.rb +0 -3
  76. 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
@@ -8,7 +8,7 @@ Feature: Put TLS
8
8
  Given the test server has TLS mode "explicit"
9
9
  And the test server is started
10
10
 
11
- Scenario: TLS
11
+ Scenario: TLS, Active
12
12
  pending "TLS not supported in active mode (see README)"
13
13
 
14
14
  Scenario: TLS, Passive
@@ -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
@@ -15,5 +15,4 @@ Feature: Status
15
15
  Scenario: Server status
16
16
  Given a successful login
17
17
  When the client successfully requests status
18
- Then the server returns its name
19
- And the server returns its version
18
+ Then the server returns its title
@@ -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 is not connected
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 is connected
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 is connected
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
- @client.append_text local_path, remote_path
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
- @client.append_binary local_path, remote_path
14
+ client.append_binary local_path, remote_path
15
15
  end
@@ -1,14 +1,24 @@
1
- def client_variable_name(client_name)
2
- var = '@' + [
3
- client_name,
4
- 'client',
5
- ].compact.map(&:strip).join('_')
6
- end
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
- instance_variable_get(client_variable_name(client_name))
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 = @client.file_contents(local_path)
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 = @client.file_contents(local_path)
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
- @client.add_file local_path
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(@client.file_contents(local_path)).should ==
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
- @client.template(local_path).should ==
13
- @client.file_contents(local_path)
12
+ client.template(local_path).should ==
13
+ client.file_contents(local_path)
14
14
  end
@@ -1,5 +1,5 @@
1
1
  When /^the client sends command "(.*?)"$/ do |command|
2
2
  capture_error do
3
- @client.raw command
3
+ client.raw command
4
4
  end
5
5
  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 = TestClient.new(:tls => tls_mode.to_sym)
8
- client.connect(server.host, server.port)
9
- set_client client_name, client
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
- After do
13
- @client.close if @client
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
- @client.delete path
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
- @client.raw 'DELE'
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
- @client.raw 'CDUP'
13
+ client.raw 'CDUP'
14
14
  end
15
15
 
16
16
  When /^the client successfully cd's to "(.*?)"$/ do |path|
17
- @client.chdir path
17
+ client.chdir path
18
18
  end
19
19
 
20
20
  Then /^the current directory should be "(.*?)"$/ do |path|
21
- @client.pwd.should == path
21
+ client.pwd.should == path
22
22
  end
23
23
 
24
24
  Then /^the XPWD directory should be "(.*?)"$/ do |path|
25
- @client.xpwd.should == path
25
+ client.xpwd.should == path
26
26
  end