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
@@ -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
|
-
|
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
|
-
|
14
|
+
client.raw 'STRU'
|
15
15
|
end
|
16
16
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
When /^the client successfully gets (text|binary) "(.*?)"$/ \
|
2
2
|
do |mode, remote_path|
|
3
|
-
|
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
|
-
|
14
|
+
client.raw 'RETR'
|
15
15
|
end
|
16
16
|
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(
|
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(
|
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
|
-
|
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
|
-
|
75
|
+
client.raw 'USER', *args
|
76
76
|
end
|
77
77
|
end
|
78
78
|
|
@@ -1,5 +1,5 @@
|
|
1
1
|
When /^the client successfully sets mode "(.*?)"$/ do |mode|
|
2
|
-
|
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
|
-
|
13
|
+
client.raw 'MODE'
|
14
14
|
end
|
15
15
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
When /^the client successfully puts (text|binary) "(.*?)"$/ do
|
2
2
|
|mode, local_path|
|
3
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
13
|
+
client.raw 'QUIT', 'foo'
|
14
14
|
end
|
15
15
|
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
|
-
@
|
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
|
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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
|
119
|
-
tls_mode
|
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
|