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.
- 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
|