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
@@ -13,6 +13,14 @@ Then /^the server returns a "(.*?)" error$/ do |error_message|
13
13
  (@error || '').should include error_message
14
14
  end
15
15
 
16
+ Then /^the server returns a too many connections error$/ do
17
+ step 'the server returns a "421 Too many connections" error'
18
+ end
19
+
20
+ Then /^the server returns a server unavailable error$/ do
21
+ step 'the server returns a "421 server unavailable" error'
22
+ end
23
+
16
24
  Then /^the server returns a not a directory error$/ do
17
25
  step 'the server returns a "550 Not a directory" error'
18
26
  end
@@ -41,6 +49,10 @@ Then /^the server returns a syntax error$/ do
41
49
  step 'the server returns a "501 Syntax error" error'
42
50
  end
43
51
 
52
+ Then /^the server returns a bad option error$/ do
53
+ step 'the server returns a "501 Unsupported option" error'
54
+ end
55
+
44
56
  Then /^the server returns a mode not implemented error$/ do
45
57
  step 'the server returns a "504 Mode not implemented" error'
46
58
  end
@@ -0,0 +1,21 @@
1
+ When /^the client successfully requests features$/ do
2
+ @feature_reply = client.raw "FEAT"
3
+ end
4
+
5
+ def feature_regexp(feature)
6
+ /^ #{feature}$/
7
+ end
8
+
9
+ Then /^the response should include feature "(.*?)"$/ do |feature|
10
+ @feature_reply.should =~ feature_regexp(feature)
11
+ end
12
+
13
+ Then /^the response should not include feature "(.*?)"$/ do |feature|
14
+ @feature_reply.should_not =~ feature_regexp(feature)
15
+ end
16
+
17
+ Then /^the response should( not)? include TLS features$/ do |neg|
18
+ step %Q'the response should#{neg} include feature "AUTH TLS"'
19
+ step %Q'the response should#{neg} include feature "PBSZ"'
20
+ step %Q'the response should#{neg} include feature "PROT"'
21
+ end
@@ -1,6 +1,6 @@
1
1
  When /^the client successfully sets file structure "(.*?)"$/ do
2
2
  |file_structure|
3
- @client.raw 'STRU', file_structure
3
+ client.raw 'STRU', file_structure
4
4
  end
5
5
 
6
6
  When /^the client sets file structure "(.*?)"$/ do |file_structure|
@@ -11,6 +11,6 @@ end
11
11
 
12
12
  When /^the client sets file structure with no parameter$/ do
13
13
  capture_error do
14
- @client.raw 'STRU'
14
+ client.raw 'STRU'
15
15
  end
16
16
  end
@@ -1,5 +1,5 @@
1
1
  When /^the client successfully sends "(.*?)"$/ do |command|
2
- @reply = @client.raw command
2
+ @reply = client.raw command
3
3
  end
4
4
 
5
5
  When /^the client sends "(.*?)"$/ do |command|
@@ -1,6 +1,6 @@
1
1
  When /^the client successfully gets (text|binary) "(.*?)"$/ \
2
2
  do |mode, remote_path|
3
- @client.get mode, remote_path
3
+ client.get mode, remote_path
4
4
  end
5
5
 
6
6
  When /^the client gets (\S+) "(.*?)"$/ do |mode, path|
@@ -11,6 +11,6 @@ end
11
11
 
12
12
  When /^the client gets with no path$/ do
13
13
  capture_error do
14
- @client.raw 'RETR'
14
+ client.raw 'RETR'
15
15
  end
16
16
  end
@@ -1,6 +1,6 @@
1
1
  When /^the client successfully asks for help(?: for "(.*?)")?$/ do
2
2
  |command|
3
- @help_reply = @client.help(command)
3
+ @help_reply = client.help(command)
4
4
  end
5
5
 
6
6
  Then /^the server should return a list of commands$/ do
@@ -1,11 +1,11 @@
1
1
  When /^the client sends an empty command$/ do
2
2
  capture_error do
3
- @client.raw ''
3
+ client.raw ''
4
4
  end
5
5
  end
6
6
 
7
7
  When /^the client sends a non-word command$/ do
8
8
  capture_error do
9
- @client.raw '*'
9
+ client.raw '*'
10
10
  end
11
11
  end
@@ -31,7 +31,7 @@ class FileList
31
31
  end
32
32
 
33
33
  When /^the client successfully lists the directory(?: "(.*?)")?$/ do |directory|
34
- @list = FileList.new(@client.ls(*[directory].compact))
34
+ @list = FileList.new(client.ls(*[directory].compact))
35
35
  end
36
36
 
37
37
  When /^the client lists the directory( "(?:.*?)")?$/ do |directory|
@@ -42,7 +42,7 @@ end
42
42
 
43
43
  When /^the client successfully name-lists the directory(?: "(.*?)")?$/ do
44
44
  |directory|
45
- @list = FileList.new(@client.nlst(*[directory].compact))
45
+ @list = FileList.new(client.nlst(*[directory].compact))
46
46
  end
47
47
 
48
48
  When /^the client name-lists the directory( "(?:.*?)")?$/ do |directory|
@@ -42,7 +42,7 @@ When /^the(?: (\w+))? client logs in(?: with bad (\w+))?$/ do
42
42
  @server.account
43
43
  end,
44
44
  ][0..server.auth_level]
45
- login(tokens, client_name)
45
+ @error = login(tokens, client_name)
46
46
  end
47
47
 
48
48
  Then /^the client should( not)? be logged in$/ do |neg|
@@ -61,7 +61,7 @@ When /^the client sends a password( with no parameter)?$/ do |no_param|
61
61
  else
62
62
  [server.password]
63
63
  end
64
- @client.raw 'PASS', *args
64
+ client.raw 'PASS', *args
65
65
  end
66
66
  end
67
67
 
@@ -72,7 +72,7 @@ When /^the client sends a user( with no parameter)?$/ do |no_param|
72
72
  else
73
73
  [server.user]
74
74
  end
75
- @client.raw 'USER', *args
75
+ client.raw 'USER', *args
76
76
  end
77
77
  end
78
78
 
@@ -5,5 +5,5 @@ When /^the client makes directory "(.*?)"$/ do |path|
5
5
  end
6
6
 
7
7
  When /^the client successfully makes directory "(.*?)"$/ do |path|
8
- mkdir_response = @client.mkdir path
8
+ mkdir_response = client.mkdir path
9
9
  end
@@ -1,5 +1,5 @@
1
1
  When /^the client successfully sets mode "(.*?)"$/ do |mode|
2
- @client.raw 'MODE', mode
2
+ client.raw 'MODE', mode
3
3
  end
4
4
 
5
5
  When /^the client sets mode "(.*?)"$/ do |mode|
@@ -10,6 +10,6 @@ end
10
10
 
11
11
  When /^the client sets mode with no parameter$/ do
12
12
  capture_error do
13
- @client.raw 'MODE'
13
+ client.raw 'MODE'
14
14
  end
15
15
  end
@@ -0,0 +1,9 @@
1
+ When /^the client successfully sets option "(.*?)"$/ do |option|
2
+ client.set_option option
3
+ end
4
+
5
+ When /^the client sets option "(.*?)"$/ do |option|
6
+ capture_error do
7
+ step %q'the client successfully sets option "#{option}"'
8
+ end
9
+ end
@@ -1,3 +1,3 @@
1
1
  Given /^the client is in (passive|active) mode$/ do |mode|
2
- @client.passive = mode == 'passive'
2
+ client.passive = mode == 'passive'
3
3
  end
@@ -1,5 +1,5 @@
1
1
  When /^the client sends PORT "(.*?)"$/ do |param|
2
2
  capture_error do
3
- @client.raw 'PORT', param
3
+ client.raw 'PORT', param
4
4
  end
5
5
  end
@@ -1,6 +1,6 @@
1
1
  When /^the client successfully puts (text|binary) "(.*?)"$/ do
2
2
  |mode, local_path|
3
- @client.put mode, local_path
3
+ client.put mode, local_path
4
4
  end
5
5
 
6
6
  When /^the client puts (\S+) "(.*?)"$/ do |mode, path|
@@ -11,13 +11,13 @@ end
11
11
 
12
12
  When /^the client puts with no path$/ do
13
13
  capture_error do
14
- @client.raw 'STOR'
14
+ client.raw 'STOR'
15
15
  end
16
16
  end
17
17
 
18
18
  When /^the client successfully stores unique "(.*?)"(?: to "(.*?)")?$/ do
19
19
  |local_path, remote_path|
20
- @client.store_unique local_path, remote_path
20
+ client.store_unique local_path, remote_path
21
21
  end
22
22
 
23
23
  When /^the client stores unique "(.*?)"( to ".*?")?$/ do
@@ -1,5 +1,5 @@
1
1
  When /^the client successfully quits$/ do
2
- @client.quit
2
+ client.quit
3
3
  end
4
4
 
5
5
  When /^the client quits$/ do
@@ -10,6 +10,6 @@ end
10
10
 
11
11
  When /^the client quits with a parameter$/ do
12
12
  capture_error do
13
- @client.raw 'QUIT', 'foo'
13
+ client.raw 'QUIT', 'foo'
14
14
  end
15
15
  end
@@ -7,5 +7,5 @@ end
7
7
 
8
8
  When /^the client successfully renames "(.*?)" to "(.*?)"$/ do
9
9
  |from_path, to_path|
10
- @client.rename(from_path, to_path)
10
+ client.rename(from_path, to_path)
11
11
  end
@@ -5,5 +5,5 @@ When /^the client removes directory "(.*?)"$/ do |path|
5
5
  end
6
6
 
7
7
  When /^the client successfully removes directory "(.*?)"$/ do |path|
8
- mkdir_response = @client.rmdir path
8
+ mkdir_response = client.rmdir path
9
9
  end
@@ -0,0 +1,12 @@
1
+ Then /^the server returns its title$/ do
2
+ step 'the server returns its name'
3
+ step 'the server returns its version'
4
+ end
5
+
6
+ Then /^the server returns its name$/ do
7
+ @response.should include @server.server_name
8
+ end
9
+
10
+ Then /^the server returns its version$/ do
11
+ @response.should =~ /\b\d+\.\d+\.\d+\b/
12
+ end
@@ -1,5 +1,5 @@
1
1
  When /^the client successfully requests status$/ do
2
- @status = @client.status
2
+ @response = client.status
3
3
  end
4
4
 
5
5
  When /^the client requests status$/ do
@@ -7,11 +7,3 @@ When /^the client requests status$/ do
7
7
  step 'the client successfully requests status'
8
8
  end
9
9
  end
10
-
11
- Then /^the server returns its name$/ do
12
- @status.should include @server.server_name
13
- end
14
-
15
- Then /^the server returns its version$/ do
16
- @status.should =~ /\b\d+\.\d+\.\d+\b/
17
- end
@@ -1,5 +1,5 @@
1
1
  When /^the client successfully queries system ID$/ do
2
- @reply = @client.system
2
+ @reply = client.system
3
3
  end
4
4
 
5
5
  Then /^the server returns a system ID reply$/ do
@@ -0,0 +1,19 @@
1
+ Before do
2
+ @start_time = Time.now
3
+ end
4
+
5
+ When /^the client is idle for (\S+) seconds$/ do |seconds|
6
+ sleep seconds.to_f
7
+ end
8
+
9
+ Then /^it should take at least (\S+) seconds$/ do |s|
10
+ min_elapsed_time = s.to_f
11
+ elapsed_time = Time.now - @start_time
12
+ elapsed_time.should >= min_elapsed_time
13
+ end
14
+
15
+ Then /^it should take less than (\S+) seconds$/ do |s|
16
+ max_elapsed_time = s.to_f
17
+ elapsed_time = Time.now - @start_time
18
+ elapsed_time.should < max_elapsed_time
19
+ end
@@ -1,5 +1,5 @@
1
1
  When /^the client successfully sets type "(.*?)"$/ do |type|
2
- @client.raw 'TYPE', type
2
+ client.raw 'TYPE', type
3
3
  end
4
4
 
5
5
  When /^the client sets type "(.*?)"$/ do |type|
@@ -10,6 +10,6 @@ end
10
10
 
11
11
  When /^the client sets type with no parameter$/ do
12
12
  capture_error do
13
- @client.raw 'TYPE'
13
+ client.raw 'TYPE'
14
14
  end
15
15
  end
@@ -6,13 +6,20 @@ class TestClient
6
6
  extend Forwardable
7
7
  include FileUtils
8
8
 
9
- def initialize(opts = {})
9
+ attr_accessor :tls_mode
10
+
11
+ def initialize
12
+ @tls_mode = :off
10
13
  @temp_dir = Ftpd::TempDir.make
11
- @ftp = make_ftp(opts)
12
14
  @templates = TestFileTemplates.new
13
15
  end
14
16
 
17
+ def start
18
+ @ftp = make_ftp
19
+ end
20
+
15
21
  def close
22
+ return unless @ftp
16
23
  @ftp.close
17
24
  end
18
25
 
@@ -36,6 +43,20 @@ class TestClient
36
43
  :status,
37
44
  :system
38
45
 
46
+ # Make a connection from a specific IP. Net::FTP doesn't have a way
47
+ # to force the local IP, so fake it here.
48
+
49
+ def connect_from(source_ip, host, port)
50
+ in_addr = Socket.pack_sockaddr_in(0, source_ip)
51
+ out_addr = Socket.pack_sockaddr_in(port, host)
52
+ socket = Socket.open(Socket::AF_INET, Socket::SOCK_STREAM, 0)
53
+ socket.bind(in_addr)
54
+ socket.connect(out_addr)
55
+ decorate_socket socket
56
+ @ftp = make_ftp
57
+ @ftp.set_socket(socket)
58
+ end
59
+
39
60
  def raw(*command)
40
61
  @ftp.sendcmd command.compact.join(' ')
41
62
  end
@@ -67,7 +88,7 @@ class TestClient
67
88
  end
68
89
 
69
90
  def xpwd
70
- response = raw('XPWD')
91
+ response = @ftp.sendcmd('XPWD')
71
92
  response[/"(.+)"/, 1]
72
93
  end
73
94
 
@@ -103,6 +124,10 @@ class TestClient
103
124
  end
104
125
  end
105
126
 
127
+ def set_option(option)
128
+ @ftp.sendcmd "OPTS #{option}"
129
+ end
130
+
106
131
  private
107
132
 
108
133
  RAW_METHOD_REGEX = /^send_(.*)$/
@@ -115,9 +140,8 @@ class TestClient
115
140
  File.expand_path(path, @temp_dir)
116
141
  end
117
142
 
118
- def make_ftp(opts)
119
- tls_mode = opts[:tls] || :off
120
- case tls_mode
143
+ def make_ftp
144
+ case @tls_mode
121
145
  when :off
122
146
  make_non_tls_ftp
123
147
  when :implicit
@@ -125,7 +149,7 @@ class TestClient
125
149
  when :explicit
126
150
  make_tls_ftp(:explicit)
127
151
  else
128
- raise "Unknown TLS mode: #{tls_mode}"
152
+ raise "Unknown TLS mode: #{@tls_mode}"
129
153
  end
130
154
  end
131
155
 
@@ -143,4 +167,35 @@ class TestClient
143
167
  Net::FTP.new
144
168
  end
145
169
 
170
+ # Ruby 2.0's Ftp class is expecting a TCPSocket, not a Socket. The
171
+ # trouble comes with Ftp#close, which closes sockets by first doing
172
+ # a shutdown, setting the read timeout, and doing a read. Plain
173
+ # Socket doesn't have those methods, so fake it.
174
+ #
175
+ # Plain socket _does_ have #close, but we short-circuit it, too,
176
+ # because it takes a few seconds. We're in a hurry when running
177
+ # tests, and can afford to be a little sloppy when cleaning up.
178
+
179
+ def decorate_socket(sock)
180
+
181
+ def sock.shutdown(how)
182
+ @shutdown = true
183
+ end
184
+
185
+ def sock.read_timeout=(seconds)
186
+ end
187
+
188
+ # Skip read after shutdown. Prevents 2.0 from hanging in
189
+ # Ftp#close
190
+
191
+ def sock.read(*args)
192
+ return if @shutdown
193
+ super(*args)
194
+ end
195
+
196
+ def close
197
+ end
198
+
199
+ end
200
+
146
201
  end