ftpd 0.5.0 → 0.6.0

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