ftpd 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Changelog.md +19 -0
- data/Gemfile.lock +12 -12
- data/README.md +14 -48
- data/VERSION +1 -1
- data/doc/references.md +32 -0
- data/doc/{rfc.md → rfc-compliance.md} +32 -17
- data/features/ftp_server/command_errors.feature +0 -2
- data/features/ftp_server/help.feature +21 -0
- data/features/ftp_server/list.feature +1 -0
- data/features/ftp_server/login.feature +9 -2
- data/features/ftp_server/name_list.feature +0 -9
- data/features/ftp_server/put.feature +1 -2
- data/features/ftp_server/put_tls.feature +2 -2
- data/features/ftp_server/put_unique.feature +56 -0
- data/features/ftp_server/rename.feature +7 -0
- data/features/step_definitions/help.rb +18 -0
- data/features/step_definitions/pending.rb +3 -0
- data/features/step_definitions/put.rb +13 -0
- data/features/step_definitions/server_files.rb +10 -0
- data/features/support/test_client.rb +9 -1
- data/features/support/test_server_files.rb +18 -1
- data/ftpd.gemspec +11 -4
- data/lib/ftpd.rb +2 -0
- data/lib/ftpd/command_sequence_checker.rb +55 -0
- data/lib/ftpd/disk_file_system.rb +5 -3
- data/lib/ftpd/error.rb +4 -0
- data/lib/ftpd/ftp_server.rb +0 -1
- data/lib/ftpd/server.rb +1 -1
- data/lib/ftpd/session.rb +95 -23
- data/spec/command_sequence_checker_spec.rb +81 -0
- metadata +12 -5
data/Changelog.md
CHANGED
@@ -1,3 +1,22 @@
|
|
1
|
+
### 0.2.2
|
2
|
+
|
3
|
+
Bug fixes
|
4
|
+
|
5
|
+
* Respond with sequence error if RNFR is not immediately followed by
|
6
|
+
RNTO
|
7
|
+
* Respond with sequence error if USER is not immediately followed by
|
8
|
+
PASS
|
9
|
+
* Open PASV mode data connection on same local IP as control connection.
|
10
|
+
This is required by RFC 1123.
|
11
|
+
* Disabled globbing in LIST (for now) due to code injection
|
12
|
+
vulnerability. This patch also disables globbing in NLST, but NLST
|
13
|
+
probably shouldn't do globbing.
|
14
|
+
|
15
|
+
Enhancements
|
16
|
+
|
17
|
+
* Support STOU (store unique)
|
18
|
+
* Support HELP
|
19
|
+
|
1
20
|
### 0.2.1
|
2
21
|
|
3
22
|
API changes
|
data/Gemfile.lock
CHANGED
@@ -7,13 +7,13 @@ GIT
|
|
7
7
|
GEM
|
8
8
|
remote: http://rubygems.org/
|
9
9
|
specs:
|
10
|
-
builder (3.
|
10
|
+
builder (3.2.0)
|
11
11
|
cucumber (1.2.1)
|
12
12
|
builder (>= 2.1.2)
|
13
13
|
diff-lcs (>= 1.1.3)
|
14
14
|
gherkin (~> 2.11.0)
|
15
15
|
json (>= 1.4.6)
|
16
|
-
diff-lcs (1.1
|
16
|
+
diff-lcs (1.2.1)
|
17
17
|
gherkin (2.11.6)
|
18
18
|
json (>= 1.7.6)
|
19
19
|
git (1.2.5)
|
@@ -25,18 +25,18 @@ GEM
|
|
25
25
|
json (1.7.7)
|
26
26
|
memoizer (1.0.1)
|
27
27
|
rake (10.0.3)
|
28
|
-
rdoc (
|
28
|
+
rdoc (4.0.0)
|
29
29
|
json (~> 1.4)
|
30
30
|
redcarpet (2.2.2)
|
31
|
-
rspec (2.
|
32
|
-
rspec-core (~> 2.
|
33
|
-
rspec-expectations (~> 2.
|
34
|
-
rspec-mocks (~> 2.
|
35
|
-
rspec-core (2.
|
36
|
-
rspec-expectations (2.
|
37
|
-
diff-lcs (
|
38
|
-
rspec-mocks (2.
|
39
|
-
yard (0.8.
|
31
|
+
rspec (2.13.0)
|
32
|
+
rspec-core (~> 2.13.0)
|
33
|
+
rspec-expectations (~> 2.13.0)
|
34
|
+
rspec-mocks (~> 2.13.0)
|
35
|
+
rspec-core (2.13.0)
|
36
|
+
rspec-expectations (2.13.0)
|
37
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
38
|
+
rspec-mocks (2.13.0)
|
39
|
+
yard (0.8.5.2)
|
40
40
|
|
41
41
|
PLATFORMS
|
42
42
|
ruby
|
data/README.md
CHANGED
@@ -88,31 +88,14 @@ output without having to change any code.
|
|
88
88
|
|
89
89
|
## LIMITATIONS
|
90
90
|
|
91
|
-
|
92
|
-
|
91
|
+
Ftpd is not yet RFC compliant. It does most of RFC969, and enough TLS
|
92
|
+
to get by. {file:doc/rfc.md Here} is a list of RFCs, indicating how
|
93
|
+
much of each Ftpd complies with.
|
93
94
|
|
94
95
|
The DiskFileSystem class only works in Linux. This is because it
|
95
96
|
shells out to the "ls" command. This affects the example, which uses
|
96
97
|
the DiskFileSystem.
|
97
98
|
|
98
|
-
The control connection is supposed to be a Telnet session. It's not.
|
99
|
-
In practice, it doesn't seem to matter whether it's a Telnet session
|
100
|
-
or just plain sending and receiving characters.
|
101
|
-
|
102
|
-
The following commands defined by RFC969 are understood, but not
|
103
|
-
implemented. They result in a "502 Command not implemented" response.
|
104
|
-
|
105
|
-
* ABOR - Abort
|
106
|
-
* ACCT - Account
|
107
|
-
* APPE - Append (with create)
|
108
|
-
* HELP - Help
|
109
|
-
* REIN - Reinitialize
|
110
|
-
* REST - Restart
|
111
|
-
* SITE - Site parameters
|
112
|
-
* SMNT - Structure mount
|
113
|
-
* STAT - Status
|
114
|
-
* STOU - Store Unique
|
115
|
-
|
116
99
|
To bind the server to an external interface, the interface must be set
|
117
100
|
to the public IP of that interface (e.g. "1.2.3.4"), not to "0.0.0.0".
|
118
101
|
That's because the interface IP is used both for binding server ports,
|
@@ -120,7 +103,11 @@ _and_ for advertising to the client which IP to connect to. Binding
|
|
120
103
|
to 0.0.0.0 will work fine, but when the client tries to connect to
|
121
104
|
0.0.0.0, it won't get to the server.
|
122
105
|
|
123
|
-
|
106
|
+
LIST doesn't accept globs. It has other problems (it accepts
|
107
|
+
arbitrary ls arguments!) and needs to be rewritten to not shell out to
|
108
|
+
"ls".
|
109
|
+
|
110
|
+
## RUBY COMPATABILITY
|
124
111
|
|
125
112
|
The tests pass with these Rubies:
|
126
113
|
|
@@ -156,33 +143,6 @@ The example prints its port, username and password to the console.
|
|
156
143
|
You can connect to the stand-alone example with any FTP client. This
|
157
144
|
is useful when testing how the server responds to a given FTP client.
|
158
145
|
|
159
|
-
## REFERENCES
|
160
|
-
|
161
|
-
(This list of references comes from the README of the em-ftpd gem,
|
162
|
-
which is licensed under the same MIT license as this gem, and is
|
163
|
-
Copyright (c) 2008 James Healy)
|
164
|
-
|
165
|
-
There are a range of RFCs that together specify the FTP protocol. In
|
166
|
-
chronological order, the more useful ones are:
|
167
|
-
|
168
|
-
* <http://tools.ietf.org/rfc/rfc959.txt>
|
169
|
-
* <http://tools.ietf.org/rfc/rfc1123.txt>
|
170
|
-
* <http://tools.ietf.org/rfc/rfc2228.txt>
|
171
|
-
* <http://tools.ietf.org/rfc/rfc2389.txt>
|
172
|
-
* <http://tools.ietf.org/rfc/rfc2428.txt>
|
173
|
-
* <http://tools.ietf.org/rfc/rfc3659.txt>
|
174
|
-
* <http://tools.ietf.org/rfc/rfc4217.txt>
|
175
|
-
|
176
|
-
For an english summary that's somewhat more legible than the RFCs, and
|
177
|
-
provides some commentary on what features are actually useful or
|
178
|
-
relevant 24 years after RFC959 was published:
|
179
|
-
|
180
|
-
* <http://cr.yp.to/ftp.html>
|
181
|
-
|
182
|
-
For a history lesson, check out Appendix III of RCF959. It lists the
|
183
|
-
preceding (obsolete) RFC documents that relate to file transfers,
|
184
|
-
including the ye old RFC114 from 1971, "A File Transfer Protocol"
|
185
|
-
|
186
146
|
## ORIGIN
|
187
147
|
|
188
148
|
I created ftpd to support the test framework I wrote for Databill,
|
@@ -197,3 +157,9 @@ Wayne Conrad <wconrad@yagni.com>
|
|
197
157
|
|
198
158
|
Thanks to Databill, LLC, which supported the creation of this library,
|
199
159
|
and granted permission to donate it to the community.
|
160
|
+
|
161
|
+
## See also
|
162
|
+
|
163
|
+
* {file:Changelog.md}
|
164
|
+
* {file:doc/rfc-compliance.md RFC compliance}
|
165
|
+
* {file:doc/references.md}
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.2.
|
1
|
+
0.2.2
|
data/doc/references.md
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# REFERENCES
|
2
|
+
|
3
|
+
_This list of references comes from the README of the em-ftpd gem,
|
4
|
+
which is licensed under the same MIT license as this gem, and is
|
5
|
+
Copyright (c) 2008 James Healy_
|
6
|
+
|
7
|
+
There are a range of RFCs that together specify the FTP protocol. In
|
8
|
+
chronological order, the more useful ones are:
|
9
|
+
|
10
|
+
* {http://tools.ietf.org/rfc/rfc959.txt RFC-959} - File Transfer Protocol
|
11
|
+
* {http://tools.ietf.org/rfc/rfc1123.txt RFC-1123} - Requirements for Internet Hosts
|
12
|
+
* {http://tools.ietf.org/rfc/rfc2228.txt RFC-2228} - FTP Security Extensions
|
13
|
+
* {http://tools.ietf.org/rfc/rfc2389.txt RFC-2389} - Feature negotiation mechanism for the File Transfer Protocol
|
14
|
+
* {http://tools.ietf.org/rfc/rfc2428.txt RFC-2428} - FTP Extensions for IPv6 and NATs
|
15
|
+
* {http://tools.ietf.org/rfc/rfc2577.txt RFC-2577} - FTP Security Considerations
|
16
|
+
* {http://tools.ietf.org/rfc/rfc2640.txt RFC-2640} - Internationalization of the File Transfer Protocol
|
17
|
+
* {http://tools.ietf.org/rfc/rfc3659.txt RFC-3659} - Extensions to FTP
|
18
|
+
* {http://tools.ietf.org/rfc/rfc4217.txt RFC-4217} - Internationalization of the File Transfer Protocol
|
19
|
+
|
20
|
+
For an english summary that's somewhat more legible than the RFCs, and
|
21
|
+
provides some commentary on what features are actually useful or
|
22
|
+
relevant 24 years after RFC959 was published:
|
23
|
+
|
24
|
+
* <http://cr.yp.to/ftp.html>
|
25
|
+
|
26
|
+
For a history lesson, check out Appendix III of RCF959. It lists the
|
27
|
+
preceding (obsolete) RFC documents that relate to file transfers,
|
28
|
+
including the ye old RFC114 from 1971, "A File Transfer Protocol"
|
29
|
+
|
30
|
+
There is a {http://secureftp-test.com public test server} which is
|
31
|
+
very handy for checking out clients, and seeing how at least one
|
32
|
+
server behaves.
|
@@ -29,9 +29,9 @@ Commands supported:
|
|
29
29
|
CDUP Yes 0.1.0 Change to parent directory
|
30
30
|
CWD Yes 0.1.0 Change working directory
|
31
31
|
DELE Yes 0.1.0 Delete file
|
32
|
-
HELP
|
32
|
+
HELP Yes 0.2.2 Help
|
33
33
|
LIST Yes 0.1.0 List directory
|
34
|
-
MKD Yes
|
34
|
+
MKD Yes 0.2.1 Make directory
|
35
35
|
MODE Yes 0.1.0 Set transfer mode
|
36
36
|
"Stream" mode supported; "Block" and
|
37
37
|
"Compressed" are not
|
@@ -45,14 +45,14 @@ Commands supported:
|
|
45
45
|
REIN No --- Reinitialize session
|
46
46
|
REST No --- Restart transfer
|
47
47
|
RETR Yes 0.1.0 Retrieve file
|
48
|
-
RMD Yes
|
49
|
-
RNFR Yes
|
50
|
-
RNTO Yes
|
48
|
+
RMD Yes 0.2.1 Remove directory
|
49
|
+
RNFR Yes 0.2.1 Rename file (from)
|
50
|
+
RNTO Yes 0.2.1 Rename file (to)
|
51
51
|
SITE No --- Site specific commands
|
52
52
|
SMNT No --- Structure Mount
|
53
53
|
STAT No --- Server status
|
54
54
|
STOR Yes 0.1.0 Store file
|
55
|
-
STOU
|
55
|
+
STOU Yes 0.2.2 Store with unique name
|
56
56
|
STRU Yes 0.1.0 Set file structure
|
57
57
|
Supports "File" structure only. "Record" and
|
58
58
|
"Page" are not supported
|
@@ -90,16 +90,16 @@ FEATURE |SECTION | | | |T|T|e
|
|
90
90
|
Implement TYPE T if same as TYPE N |4.1.2.2 | |x| | | |
|
91
91
|
File/Record transform invertible if poss. |4.1.2.4 | |x| | | |
|
92
92
|
Server-FTP implement PASV |4.1.2.6 |x| | | | | C
|
93
|
-
PASV is per-transfer |4.1.2.6 |x| | | | |
|
93
|
+
PASV is per-transfer |4.1.2.6 |x| | | | | C
|
94
94
|
NLST reply usable in RETR cmds |4.1.2.7 |x| | | | | C
|
95
95
|
Implied type for LIST and NLST |4.1.2.7 | |x| | | | C
|
96
96
|
SITE cmd for non-standard features |4.1.2.8 | |x| | | |
|
97
|
-
STOU cmd return pathname as specified |4.1.2.9 |x| | | | |
|
97
|
+
STOU cmd return pathname as specified |4.1.2.9 |x| | | | | C
|
98
98
|
Use TCP READ boundaries on control conn. |4.1.2.10 | | | | |x| C
|
99
99
|
Server-FTP send only correct reply format |4.1.2.11 |x| | | | | C
|
100
100
|
Server-FTP use defined reply code if poss. |4.1.2.11 | |x| | | | C
|
101
101
|
New reply code following Section 4.2 |4.1.2.11 | | |x| | |
|
102
|
-
Default data port same IP addr as ctl conn |4.1.2.12 |x| | | | |
|
102
|
+
Default data port same IP addr as ctl conn |4.1.2.12 |x| | | | | C
|
103
103
|
Server-FTP handle Telnet options |4.1.2.12 |x| | | | |
|
104
104
|
Handle "Experimental" directory cmds |4.1.3.1 | |x| | | | C
|
105
105
|
Idle timeout in server-FTP |4.1.3.2 | |x| | | |
|
@@ -143,7 +143,7 @@ Support commands: | | | | | | |
|
|
143
143
|
| | | | | | |
|
144
144
|
RETR |4.1.2.13 |x| | | | | C
|
145
145
|
STOR |4.1.2.13 |x| | | | | C
|
146
|
-
STOU |959 5.3.1 | | |x| | |
|
146
|
+
STOU |959 5.3.1 | | |x| | | C
|
147
147
|
APPE |4.1.2.13 |x| | | | |
|
148
148
|
ALLO |959 5.3.1 | | |x| | | C
|
149
149
|
REST |959 5.3.1 | | |x| | |
|
@@ -158,8 +158,8 @@ Support commands: | | | | | | |
|
|
158
158
|
NLST |4.1.2.13 |x| | | | | C
|
159
159
|
SITE |4.1.2.8 | | |x| | |
|
160
160
|
STAT |4.1.2.13 |x| | | | |
|
161
|
-
SYST |4.1.2.13 |x| | | | |
|
162
|
-
HELP |4.1.2.13 |x| | | | |
|
161
|
+
SYST |4.1.2.13 |x| | | | | C
|
162
|
+
HELP |4.1.2.13 |x| | | | | C
|
163
163
|
NOOP |4.1.2.13 |x| | | | | C
|
164
164
|
|
165
165
|
Footnotes:
|
@@ -202,6 +202,21 @@ FEAT No --- List new supported commands
|
|
202
202
|
OPTS No --- Set options for certain commands
|
203
203
|
</pre>
|
204
204
|
|
205
|
+
## RFC-2428 - FTP Extensions for IPv6 and NATs
|
206
|
+
|
207
|
+
Introduces the new commands EPRT and EPSV extending FTP to enable its
|
208
|
+
use over various network protocols, and the new response codes 522 and
|
209
|
+
229.
|
210
|
+
|
211
|
+
* Issued: September 1998
|
212
|
+
* Status: PROPOSED STANDARD
|
213
|
+
* [link](http://tools.ietf.org/rfc/rfc2428.txt)
|
214
|
+
|
215
|
+
<pre>
|
216
|
+
EPRT No --- Set active data connection over IPv4 or IPv6
|
217
|
+
EPSV No --- Set passive data connection over IPv4 or IPv6
|
218
|
+
</pre>
|
219
|
+
|
205
220
|
##RFC-2577 - FTP Security Considerations
|
206
221
|
|
207
222
|
Provides several configuration and implementation suggestions to
|
@@ -215,11 +230,11 @@ attempts and third-party "proxy FTP" transfers, which can be used in
|
|
215
230
|
|
216
231
|
<pre>
|
217
232
|
FTP bounce protection
|
218
|
-
|
233
|
+
Restrict PASV/PORT to non-priv. ports No ---
|
219
234
|
Disconnect after so many wrong auths. No ---
|
220
235
|
Delay on invalid password No ---
|
221
236
|
Per-source IP limit No ---
|
222
|
-
Do not reject wrong usernames Yes
|
237
|
+
Do not reject wrong usernames Yes 0.1.0
|
223
238
|
Port stealing protection No ---
|
224
239
|
</pre>
|
225
240
|
|
@@ -269,9 +284,9 @@ Provides a description on how to implement TLS as a security mechanism to secure
|
|
269
284
|
* [link](http://tools.ietf.org/rfc/rfc4217.txt)
|
270
285
|
|
271
286
|
<pre>
|
272
|
-
AUTH Yes
|
287
|
+
AUTH Yes 0.1.0 Authentication/Security Mechanism
|
273
288
|
CCC No --- Clear Command Channel
|
274
|
-
PBSZ Yes
|
275
|
-
PROT Yes
|
289
|
+
PBSZ Yes 0.1.0 Protection Buffer Size
|
290
|
+
PROT Yes 0.1.0 Data Channel Protection Level.
|
276
291
|
Support only "Private" level
|
277
292
|
</pre>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
Feature: Help
|
2
|
+
|
3
|
+
As a client
|
4
|
+
I want to ask for help
|
5
|
+
So that I can know which commands are supported
|
6
|
+
|
7
|
+
Background:
|
8
|
+
Given the test server is started
|
9
|
+
And a successful connection
|
10
|
+
|
11
|
+
Scenario: No argument
|
12
|
+
When the client successfully asks for help
|
13
|
+
Then the server should return a list of commands
|
14
|
+
|
15
|
+
Scenario: Known command
|
16
|
+
When the client successfully asks for help for "NOOP"
|
17
|
+
Then the server should return help for "NOOP"
|
18
|
+
|
19
|
+
Scenario: Unknown command
|
20
|
+
When the client successfully asks for help for "FOO"
|
21
|
+
Then the server should return no help for "FOO"
|
@@ -47,10 +47,17 @@ Feature: Login
|
|
47
47
|
|
48
48
|
Scenario: PASS without parameter
|
49
49
|
Given a successful connection
|
50
|
-
And the client sends a
|
50
|
+
And the client sends a user
|
51
|
+
When the client sends a password with no parameter
|
51
52
|
Then the server returns a syntax error
|
52
53
|
|
53
|
-
Scenario:
|
54
|
+
Scenario: USER without parameter
|
54
55
|
Given a successful connection
|
55
56
|
And the client sends a user with no parameter
|
56
57
|
Then the server returns a syntax error
|
58
|
+
|
59
|
+
Scenario: USER not followed by PASS
|
60
|
+
Given a successful connection
|
61
|
+
And the client sends a user
|
62
|
+
When the client sends "NOOP"
|
63
|
+
Then the server returns a bad sequence error
|
@@ -41,15 +41,6 @@ Feature: Name List
|
|
41
41
|
Then the file list should be in short form
|
42
42
|
And the file list should contain "foo"
|
43
43
|
|
44
|
-
Scenario: Glob
|
45
|
-
Given a successful login
|
46
|
-
And the server has file "foo"
|
47
|
-
And the server has file "bar"
|
48
|
-
When the client successfully name-lists the directory "f*"
|
49
|
-
Then the file list should be in short form
|
50
|
-
And the file list should contain "foo"
|
51
|
-
And the file list should not contain "bar"
|
52
|
-
|
53
44
|
Scenario: Passive
|
54
45
|
Given a successful login
|
55
46
|
And the server has file "foo"
|
@@ -0,0 +1,56 @@
|
|
1
|
+
Feature: Put Unique
|
2
|
+
|
3
|
+
As a client
|
4
|
+
I want to upload a file with a unique name
|
5
|
+
So that it will not overwrite an existing file
|
6
|
+
|
7
|
+
Background:
|
8
|
+
Given the test server is started
|
9
|
+
|
10
|
+
Scenario: File does not exist
|
11
|
+
Given a successful login
|
12
|
+
And the client has file "foo"
|
13
|
+
When the client successfully stores unique "foo"
|
14
|
+
Then the server should have a file with the contents of "foo"
|
15
|
+
|
16
|
+
Scenario: Suggest name
|
17
|
+
Given a successful login
|
18
|
+
And the client has file "foo"
|
19
|
+
When the client successfully stores unique "foo" to "bar"
|
20
|
+
Then the server should have a file with the contents of "foo"
|
21
|
+
And the server should have 1 file with "bar" in the name
|
22
|
+
|
23
|
+
Scenario: Suggested name exists
|
24
|
+
Given a successful login
|
25
|
+
And the client has file "foo"
|
26
|
+
And the server has file "bar"
|
27
|
+
When the client successfully stores unique "foo" to "bar"
|
28
|
+
Then the server should have a file with the contents of "foo"
|
29
|
+
Then the server should have a file with the contents of "bar"
|
30
|
+
And the server should have 2 files with "bar" in the name
|
31
|
+
|
32
|
+
Scenario: Non-root working directory
|
33
|
+
Given a successful login
|
34
|
+
And the client has file "bar"
|
35
|
+
And the server has directory "foo"
|
36
|
+
And the client successfully cd's to "foo"
|
37
|
+
When the client successfully stores unique "bar" to "bar"
|
38
|
+
Then the remote file "foo/bar" should match the local file
|
39
|
+
|
40
|
+
Scenario: Missing directory
|
41
|
+
Given a successful login
|
42
|
+
And the client has file "bar"
|
43
|
+
When the client stores unique "bar" to "foo/bar"
|
44
|
+
Then the server returns a not found error
|
45
|
+
|
46
|
+
Scenario: Not logged in
|
47
|
+
Given a successful connection
|
48
|
+
When the client sends "STOU"
|
49
|
+
Then the server returns a not logged in error
|
50
|
+
|
51
|
+
Scenario: Write not enabled
|
52
|
+
Given the test server is started without write
|
53
|
+
And a successful login
|
54
|
+
And the client has file "foo"
|
55
|
+
When the client stores unique "foo"
|
56
|
+
Then the server returns an unimplemented command error
|
@@ -88,3 +88,10 @@ Feature: Rename
|
|
88
88
|
Given a successful login
|
89
89
|
When the client sends "RNTO bar"
|
90
90
|
Then the server returns a bad sequence error
|
91
|
+
|
92
|
+
Scenario: RNFR not followed by RNTO
|
93
|
+
Given a successful login
|
94
|
+
And the server has file "foo"
|
95
|
+
And the client sends "RNFR foo"
|
96
|
+
When the client sends "NOOP"
|
97
|
+
Then the server returns a bad sequence error
|
@@ -0,0 +1,18 @@
|
|
1
|
+
When /^the client successfully asks for help(?: for "(.*?)")?$/ do
|
2
|
+
|command|
|
3
|
+
@help_reply = @client.help(command)
|
4
|
+
end
|
5
|
+
|
6
|
+
Then /^the server should return a list of commands$/ do
|
7
|
+
commands = @help_reply.scan(/\b([A-Z][A-Z]+)\b/).flatten
|
8
|
+
commands.should include 'NOOP'
|
9
|
+
commands.should include 'USER'
|
10
|
+
end
|
11
|
+
|
12
|
+
Then /^the server should return help for "(.*?)"$/ do |command|
|
13
|
+
@help_reply.should =~ /Command #{command} is recognized/
|
14
|
+
end
|
15
|
+
|
16
|
+
Then /^the server should return no help for "(.*?)"$/ do |command|
|
17
|
+
@help_reply.should =~ /Command #{command} is not recognized/
|
18
|
+
end
|
@@ -14,3 +14,16 @@ When /^the client puts with no path$/ do
|
|
14
14
|
@client.raw 'STOR'
|
15
15
|
end
|
16
16
|
end
|
17
|
+
|
18
|
+
When /^the client successfully stores unique "(.*?)"(?: to "(.*?)")?$/ do
|
19
|
+
|local_path, remote_path|
|
20
|
+
@client.store_unique local_path, remote_path
|
21
|
+
end
|
22
|
+
|
23
|
+
When /^the client stores unique "(.*?)"( to ".*?")?$/ do
|
24
|
+
|local_path, remote_path|
|
25
|
+
capture_error do
|
26
|
+
step(%Q'the client successfully stores unique ' +
|
27
|
+
%Q'"#{local_path}"#{remote_path}')
|
28
|
+
end
|
29
|
+
end
|
@@ -29,3 +29,13 @@ Then /^the remote file "(.*?)" should have (unix|windows) line endings$/ do
|
|
29
29
|
line_ending_type(@server.file_contents(remote_path)).should ==
|
30
30
|
line_ending_type.to_sym
|
31
31
|
end
|
32
|
+
|
33
|
+
Then /^the server should have a file with the contents of "(.*?)"$/ do
|
34
|
+
|path|
|
35
|
+
@server.has_file_with_contents_of?(path).should be_true
|
36
|
+
end
|
37
|
+
|
38
|
+
Then /^the server should have (\d+) files? with "(.*?)" in the name$/ do
|
39
|
+
|count, name|
|
40
|
+
@server.files_named_like(name).size.should == count.to_i
|
41
|
+
end
|
@@ -22,6 +22,7 @@ class TestClient
|
|
22
22
|
:delete,
|
23
23
|
:getbinaryfile,
|
24
24
|
:gettextfile,
|
25
|
+
:help,
|
25
26
|
:login,
|
26
27
|
:ls,
|
27
28
|
:mkdir,
|
@@ -52,7 +53,7 @@ class TestClient
|
|
52
53
|
full_path = temp_path(path)
|
53
54
|
mkdir_p File.dirname(full_path)
|
54
55
|
File.open(full_path, 'wb') do |file|
|
55
|
-
file.
|
56
|
+
file.write @templates[File.basename(full_path)]
|
56
57
|
end
|
57
58
|
end
|
58
59
|
|
@@ -65,6 +66,13 @@ class TestClient
|
|
65
66
|
response[/"(.+)"/, 1]
|
66
67
|
end
|
67
68
|
|
69
|
+
def store_unique(local_path, remote_path)
|
70
|
+
command = ['STOU', remote_path].compact.join(' ')
|
71
|
+
File.open(temp_path(local_path), 'rb') do |file|
|
72
|
+
@ftp.storbinary command, file, Net::FTP::DEFAULT_BLOCKSIZE
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
68
76
|
private
|
69
77
|
|
70
78
|
RAW_METHOD_REGEX = /^send_(.*)$/
|
@@ -4,7 +4,7 @@ module TestServerFiles
|
|
4
4
|
full_path = temp_path(path)
|
5
5
|
mkdir_p File.dirname(full_path)
|
6
6
|
File.open(full_path, 'wb') do |file|
|
7
|
-
file.
|
7
|
+
file.write @templates[File.basename(full_path)]
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
@@ -18,6 +18,19 @@ module TestServerFiles
|
|
18
18
|
File.exists?(full_path)
|
19
19
|
end
|
20
20
|
|
21
|
+
def has_file_with_contents_of?(path)
|
22
|
+
expected_contents = @templates[File.basename(path)]
|
23
|
+
all_paths.any? do |path|
|
24
|
+
File.open(path, 'rb', &:read) == expected_contents
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def files_named_like(name)
|
29
|
+
all_paths.select do |path|
|
30
|
+
path.include?(name)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
21
34
|
def has_directory?(path)
|
22
35
|
full_path = temp_path(path)
|
23
36
|
File.directory?(full_path)
|
@@ -32,4 +45,8 @@ module TestServerFiles
|
|
32
45
|
File.expand_path(path, temp_dir)
|
33
46
|
end
|
34
47
|
|
48
|
+
def all_paths
|
49
|
+
Dir[temp_path('**/*')]
|
50
|
+
end
|
51
|
+
|
35
52
|
end
|
data/ftpd.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "ftpd"
|
8
|
-
s.version = "0.2.
|
8
|
+
s.version = "0.2.2"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Wayne Conrad"]
|
12
|
-
s.date = "2013-02
|
12
|
+
s.date = "2013-03-02"
|
13
13
|
s.description = "ftpd is a pure Ruby FTP server library. It supports implicit and explicit TLS, passive and active mode, and most of the commands specified in RFC 969. It an be used as part of a test fixture or embedded in a program."
|
14
14
|
s.email = "wconrad@yagni.com"
|
15
15
|
s.extra_rdoc_files = [
|
@@ -24,7 +24,8 @@ Gem::Specification.new do |s|
|
|
24
24
|
"README.md",
|
25
25
|
"Rakefile",
|
26
26
|
"VERSION",
|
27
|
-
"doc/
|
27
|
+
"doc/references.md",
|
28
|
+
"doc/rfc-compliance.md",
|
28
29
|
"examples/example.rb",
|
29
30
|
"examples/hello_world.rb",
|
30
31
|
"features/example/example.feature",
|
@@ -39,6 +40,7 @@ Gem::Specification.new do |s|
|
|
39
40
|
"features/ftp_server/file_structure.feature",
|
40
41
|
"features/ftp_server/get.feature",
|
41
42
|
"features/ftp_server/get_tls.feature",
|
43
|
+
"features/ftp_server/help.feature",
|
42
44
|
"features/ftp_server/implicit_tls.feature",
|
43
45
|
"features/ftp_server/list.feature",
|
44
46
|
"features/ftp_server/list_tls.feature",
|
@@ -51,6 +53,7 @@ Gem::Specification.new do |s|
|
|
51
53
|
"features/ftp_server/port.feature",
|
52
54
|
"features/ftp_server/put.feature",
|
53
55
|
"features/ftp_server/put_tls.feature",
|
56
|
+
"features/ftp_server/put_unique.feature",
|
54
57
|
"features/ftp_server/quit.feature",
|
55
58
|
"features/ftp_server/rename.feature",
|
56
59
|
"features/ftp_server/rmdir.feature",
|
@@ -70,6 +73,7 @@ Gem::Specification.new do |s|
|
|
70
73
|
"features/step_definitions/file_structure.rb",
|
71
74
|
"features/step_definitions/generic_send.rb",
|
72
75
|
"features/step_definitions/get.rb",
|
76
|
+
"features/step_definitions/help.rb",
|
73
77
|
"features/step_definitions/invalid_commands.rb",
|
74
78
|
"features/step_definitions/line_endings.rb",
|
75
79
|
"features/step_definitions/list.rb",
|
@@ -78,6 +82,7 @@ Gem::Specification.new do |s|
|
|
78
82
|
"features/step_definitions/mode.rb",
|
79
83
|
"features/step_definitions/noop.rb",
|
80
84
|
"features/step_definitions/passive.rb",
|
85
|
+
"features/step_definitions/pending.rb",
|
81
86
|
"features/step_definitions/port.rb",
|
82
87
|
"features/step_definitions/put.rb",
|
83
88
|
"features/step_definitions/quit.rb",
|
@@ -100,6 +105,7 @@ Gem::Specification.new do |s|
|
|
100
105
|
"ftpd.gemspec",
|
101
106
|
"insecure-test-cert.pem",
|
102
107
|
"lib/ftpd.rb",
|
108
|
+
"lib/ftpd/command_sequence_checker.rb",
|
103
109
|
"lib/ftpd/disk_file_system.rb",
|
104
110
|
"lib/ftpd/error.rb",
|
105
111
|
"lib/ftpd/exception_translator.rb",
|
@@ -118,6 +124,7 @@ Gem::Specification.new do |s|
|
|
118
124
|
"rake_tasks/spec.rake",
|
119
125
|
"rake_tasks/test.rake",
|
120
126
|
"rake_tasks/yard.rake",
|
127
|
+
"spec/command_sequence_checker_spec.rb",
|
121
128
|
"spec/disk_file_system_spec.rb",
|
122
129
|
"spec/exception_translator_spec.rb",
|
123
130
|
"spec/file_system_error_translator_spec.rb",
|
@@ -127,7 +134,7 @@ Gem::Specification.new do |s|
|
|
127
134
|
s.homepage = "http://github.com/wconrad/ftpd"
|
128
135
|
s.licenses = ["MIT"]
|
129
136
|
s.require_paths = ["lib"]
|
130
|
-
s.rubygems_version = "1.8.
|
137
|
+
s.rubygems_version = "1.8.25"
|
131
138
|
s.summary = "Pure Ruby FTP server library"
|
132
139
|
|
133
140
|
if s.respond_to? :specification_version then
|
data/lib/ftpd.rb
CHANGED
@@ -2,10 +2,12 @@ require 'fileutils'
|
|
2
2
|
require 'memoizer'
|
3
3
|
require 'openssl'
|
4
4
|
require 'pathname'
|
5
|
+
require 'shellwords'
|
5
6
|
require 'socket'
|
6
7
|
require 'tmpdir'
|
7
8
|
|
8
9
|
module Ftpd
|
10
|
+
autoload :CommandSequenceChecker, 'ftpd/command_sequence_checker'
|
9
11
|
autoload :DiskFileSystem, 'ftpd/disk_file_system'
|
10
12
|
autoload :Error, 'ftpd/error'
|
11
13
|
autoload :ExceptionTranslator, 'ftpd/exception_translator'
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# Some commands are supposed to occur in sequence. For example, USER
|
2
|
+
# must be immediately followed by PASS. This class keeps track of
|
3
|
+
# when a specific command is expected, and raises a "bad sequence"
|
4
|
+
# error when that command is not next.
|
5
|
+
|
6
|
+
module Ftpd
|
7
|
+
class CommandSequenceChecker
|
8
|
+
|
9
|
+
include Error
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@must_expect = []
|
13
|
+
end
|
14
|
+
|
15
|
+
# Set the command to expect next. If not set, then any command
|
16
|
+
# will be accepted, so long as it hasn't been registered using
|
17
|
+
# {#must_expect}.
|
18
|
+
#
|
19
|
+
# @param command [String] The command. Must be lowercase.
|
20
|
+
|
21
|
+
def expect(command)
|
22
|
+
@expected_command = command
|
23
|
+
end
|
24
|
+
|
25
|
+
# Register a command that must be expected. When that command is
|
26
|
+
# received without {#expect} having been called for it, a sequence
|
27
|
+
# error will result.
|
28
|
+
|
29
|
+
def must_expect(command)
|
30
|
+
@must_expect << command
|
31
|
+
end
|
32
|
+
|
33
|
+
# Check a command. If expecting a specific command and this
|
34
|
+
# command isn't it, then raise an error that will cause a "503 Bad
|
35
|
+
# sequence" error to be sent. After checking, the expected
|
36
|
+
# command is cleared and any command will be accepted, unless
|
37
|
+
# {#expect} is called again.
|
38
|
+
#
|
39
|
+
# @param command [String] The command. Must be lowercase.
|
40
|
+
# @raise [CommandError] A "503 Bad sequence" error
|
41
|
+
|
42
|
+
def check(command)
|
43
|
+
if @expected_command
|
44
|
+
begin
|
45
|
+
sequence_error unless command == @expected_command
|
46
|
+
ensure
|
47
|
+
@expected_command = nil
|
48
|
+
end
|
49
|
+
else
|
50
|
+
sequence_error if @must_expect.include?(command)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
@@ -138,6 +138,7 @@ module Ftpd
|
|
138
138
|
#
|
139
139
|
# Called for:
|
140
140
|
# * STOR
|
141
|
+
# * STOU
|
141
142
|
#
|
142
143
|
# If missing, then these commands are not supported.
|
143
144
|
|
@@ -205,6 +206,8 @@ module Ftpd
|
|
205
206
|
|
206
207
|
module Ls
|
207
208
|
|
209
|
+
include Shellwords
|
210
|
+
|
208
211
|
def ls(ftp_path, option)
|
209
212
|
path = expand_ftp_path(ftp_path)
|
210
213
|
dirname = File.dirname(path)
|
@@ -213,11 +216,10 @@ module Ftpd
|
|
213
216
|
'ls',
|
214
217
|
option,
|
215
218
|
filename,
|
216
|
-
|
217
|
-
].compact.join(' ')
|
219
|
+
].compact
|
218
220
|
if File.exists?(dirname)
|
219
221
|
list = Dir.chdir(dirname) do
|
220
|
-
`#{command}`
|
222
|
+
`#{shelljoin(command)} 2>&1`
|
221
223
|
end
|
222
224
|
else
|
223
225
|
list = ''
|
data/lib/ftpd/error.rb
CHANGED
data/lib/ftpd/ftp_server.rb
CHANGED
data/lib/ftpd/server.rb
CHANGED
data/lib/ftpd/session.rb
CHANGED
@@ -8,7 +8,6 @@ module Ftpd
|
|
8
8
|
def initialize(opts)
|
9
9
|
@driver = opts[:driver]
|
10
10
|
@socket = opts[:socket]
|
11
|
-
@interface = opts[:interface]
|
12
11
|
@tls = opts[:tls]
|
13
12
|
if @tls == :implicit
|
14
13
|
@socket.encrypt
|
@@ -22,11 +21,12 @@ module Ftpd
|
|
22
21
|
@structure = 'F'
|
23
22
|
@response_delay = opts[:response_delay]
|
24
23
|
@data_channel_protection_level = :clear
|
24
|
+
@command_sequence_checker = init_command_sequence_checker
|
25
|
+
@logged_in = false
|
25
26
|
end
|
26
27
|
|
27
28
|
def run
|
28
29
|
reply "220 ftpd"
|
29
|
-
@state = :user
|
30
30
|
catch :done do
|
31
31
|
loop do
|
32
32
|
begin
|
@@ -37,6 +37,7 @@ module Ftpd
|
|
37
37
|
unless respond_to?(method, true)
|
38
38
|
unrecognized_error s
|
39
39
|
end
|
40
|
+
@command_sequence_checker.check command
|
40
41
|
send(method, argument)
|
41
42
|
rescue CommandError => e
|
42
43
|
reply e.message
|
@@ -61,34 +62,28 @@ module Ftpd
|
|
61
62
|
|
62
63
|
def cmd_user(argument)
|
63
64
|
syntax_error unless argument
|
64
|
-
sequence_error
|
65
|
+
sequence_error if @logged_in
|
65
66
|
@user = argument
|
66
|
-
@state = :password
|
67
67
|
reply "331 Password required"
|
68
|
-
|
69
|
-
|
70
|
-
def sequence_error
|
71
|
-
error "503 Bad sequence of commands"
|
68
|
+
expect 'pass'
|
72
69
|
end
|
73
70
|
|
74
71
|
def cmd_pass(argument)
|
75
72
|
syntax_error unless argument
|
76
|
-
sequence_error unless @state == :password
|
77
73
|
password = argument
|
78
74
|
unless @driver.authenticate(@user, password)
|
79
|
-
@state = :user
|
80
75
|
error "530 Login incorrect"
|
81
76
|
end
|
82
77
|
reply "230 Logged in"
|
83
78
|
set_file_system @driver.file_system(@user)
|
84
|
-
@
|
79
|
+
@logged_in = true
|
85
80
|
end
|
86
81
|
|
87
82
|
def cmd_quit(argument)
|
88
83
|
syntax_error if argument
|
89
84
|
ensure_logged_in
|
90
85
|
reply "221 Byebye"
|
91
|
-
@
|
86
|
+
@logged_in = false
|
92
87
|
end
|
93
88
|
|
94
89
|
def syntax_error
|
@@ -119,7 +114,22 @@ module Ftpd
|
|
119
114
|
path = File.expand_path(path, @name_prefix)
|
120
115
|
ensure_accessible path
|
121
116
|
ensure_exists File.dirname(path)
|
122
|
-
contents = receive_file
|
117
|
+
contents = receive_file
|
118
|
+
@file_system.write path, contents
|
119
|
+
reply "226 Transfer complete"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def cmd_stou(argument)
|
124
|
+
close_data_server_socket_when_done do
|
125
|
+
ensure_logged_in
|
126
|
+
ensure_file_system_supports :write
|
127
|
+
path = argument || 'ftpd'
|
128
|
+
path = File.expand_path(path, @name_prefix)
|
129
|
+
path = unique_path(path)
|
130
|
+
ensure_accessible path
|
131
|
+
ensure_exists File.dirname(path)
|
132
|
+
contents = receive_file(File.basename(path))
|
123
133
|
@file_system.write path, contents
|
124
134
|
reply "226 Transfer complete"
|
125
135
|
end
|
@@ -230,7 +240,8 @@ module Ftpd
|
|
230
240
|
if @data_server
|
231
241
|
reply "200 Already in passive mode"
|
232
242
|
else
|
233
|
-
|
243
|
+
interface = @socket.addr[3]
|
244
|
+
@data_server = TCPServer.new(interface, 0)
|
234
245
|
ip = @data_server.addr[3]
|
235
246
|
port = @data_server.addr[1]
|
236
247
|
quads = [
|
@@ -287,7 +298,7 @@ module Ftpd
|
|
287
298
|
end
|
288
299
|
|
289
300
|
def ensure_logged_in
|
290
|
-
return if @
|
301
|
+
return if @logged_in
|
291
302
|
error "530 Not logged in"
|
292
303
|
end
|
293
304
|
|
@@ -389,10 +400,10 @@ module Ftpd
|
|
389
400
|
ensure_exists from_path
|
390
401
|
@rename_from_path = from_path
|
391
402
|
reply '350 RNFR accepted; ready for destination'
|
403
|
+
expect 'rnto'
|
392
404
|
end
|
393
405
|
|
394
406
|
def cmd_rnto(argument)
|
395
|
-
sequence_error unless @rename_from_path
|
396
407
|
ensure_logged_in
|
397
408
|
ensure_file_system_supports :rename
|
398
409
|
syntax_error unless argument
|
@@ -401,7 +412,26 @@ module Ftpd
|
|
401
412
|
ensure_does_not_exist to_path
|
402
413
|
@file_system.rename(@rename_from_path, to_path)
|
403
414
|
reply '250 Rename successful'
|
404
|
-
|
415
|
+
end
|
416
|
+
|
417
|
+
def cmd_help(argument)
|
418
|
+
if argument
|
419
|
+
command = argument.upcase
|
420
|
+
if supported_commands.include?(command)
|
421
|
+
reply "214 Command #{command} is recognized"
|
422
|
+
else
|
423
|
+
reply "214 Command #{command} is not recognized"
|
424
|
+
end
|
425
|
+
else
|
426
|
+
reply '214-The following commands are recognized:'
|
427
|
+
supported_commands.sort.each_slice(8) do |commands|
|
428
|
+
line = commands.map do |command|
|
429
|
+
' %-4s' % command
|
430
|
+
end.join
|
431
|
+
reply line
|
432
|
+
end
|
433
|
+
reply '214 Have a nice day.'
|
434
|
+
end
|
405
435
|
end
|
406
436
|
|
407
437
|
def self.unimplemented(command)
|
@@ -415,13 +445,17 @@ module Ftpd
|
|
415
445
|
unimplemented :abor
|
416
446
|
unimplemented :acct
|
417
447
|
unimplemented :appe
|
418
|
-
unimplemented :help
|
419
448
|
unimplemented :rein
|
420
449
|
unimplemented :rest
|
421
450
|
unimplemented :site
|
422
451
|
unimplemented :smnt
|
423
452
|
unimplemented :stat
|
424
|
-
|
453
|
+
|
454
|
+
def supported_commands
|
455
|
+
private_methods.map do |method|
|
456
|
+
method.to_s[/^cmd_(\w+)$/, 1]
|
457
|
+
end.compact.map(&:upcase)
|
458
|
+
end
|
425
459
|
|
426
460
|
def pwd
|
427
461
|
reply %Q(257 "#{@name_prefix}" is current directory)
|
@@ -459,6 +493,10 @@ module Ftpd
|
|
459
493
|
'P'=>:private
|
460
494
|
}
|
461
495
|
|
496
|
+
def expect(command)
|
497
|
+
@command_sequence_checker.expect command
|
498
|
+
end
|
499
|
+
|
462
500
|
def set_file_system(file_system)
|
463
501
|
@file_system = FileSystemErrorTranslator.new(file_system)
|
464
502
|
end
|
@@ -472,8 +510,8 @@ module Ftpd
|
|
472
510
|
end
|
473
511
|
end
|
474
512
|
|
475
|
-
def receive_file(
|
476
|
-
open_data_connection do |data_socket|
|
513
|
+
def receive_file(path_to_advertise = nil)
|
514
|
+
open_data_connection(path_to_advertise) do |data_socket|
|
477
515
|
contents = data_socket.read
|
478
516
|
contents = nvt_ascii_to_unix(contents) if @data_type == 'A'
|
479
517
|
debug("Received #{contents.size} bytes")
|
@@ -490,8 +528,8 @@ module Ftpd
|
|
490
528
|
s.gsub(/\r\n/, "\n")
|
491
529
|
end
|
492
530
|
|
493
|
-
def open_data_connection(&block)
|
494
|
-
|
531
|
+
def open_data_connection(path_to_advertise = nil, &block)
|
532
|
+
send_start_of_data_connection_reply(path_to_advertise)
|
495
533
|
if @data_server
|
496
534
|
if encrypt_data?
|
497
535
|
open_passive_tls_data_connection(&block)
|
@@ -507,6 +545,14 @@ module Ftpd
|
|
507
545
|
end
|
508
546
|
end
|
509
547
|
|
548
|
+
def send_start_of_data_connection_reply(path)
|
549
|
+
if path
|
550
|
+
reply "150 FILE: #{path}"
|
551
|
+
else
|
552
|
+
reply "150 Opening #{data_connection_description}"
|
553
|
+
end
|
554
|
+
end
|
555
|
+
|
510
556
|
def data_connection_description
|
511
557
|
[
|
512
558
|
DATA_TYPES[@data_type][0],
|
@@ -596,6 +642,25 @@ module Ftpd
|
|
596
642
|
@socket.puts(s)
|
597
643
|
end
|
598
644
|
|
645
|
+
def unique_path(path)
|
646
|
+
suffix = nil
|
647
|
+
100.times do
|
648
|
+
path_with_suffix = [path, suffix].compact.join('.')
|
649
|
+
unless @file_system.exists?(path_with_suffix)
|
650
|
+
return path_with_suffix
|
651
|
+
end
|
652
|
+
suffix = generate_suffix
|
653
|
+
end
|
654
|
+
raise "Unable to find unique path"
|
655
|
+
end
|
656
|
+
|
657
|
+
def generate_suffix
|
658
|
+
set = ('a'..'z').to_a
|
659
|
+
8.times.map do
|
660
|
+
set.sample
|
661
|
+
end.join
|
662
|
+
end
|
663
|
+
|
599
664
|
def debug(*s)
|
600
665
|
return unless debug?
|
601
666
|
File.open(@debug_path, 'a') do |file|
|
@@ -607,5 +672,12 @@ module Ftpd
|
|
607
672
|
@debug || ENV['FTPD_DEBUG'].to_i != 0
|
608
673
|
end
|
609
674
|
|
675
|
+
def init_command_sequence_checker
|
676
|
+
checker = CommandSequenceChecker.new
|
677
|
+
checker.must_expect 'pass'
|
678
|
+
checker.must_expect 'rnto'
|
679
|
+
checker
|
680
|
+
end
|
681
|
+
|
610
682
|
end
|
611
683
|
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require File.expand_path('spec_helper', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
module Ftpd
|
4
|
+
describe CommandSequenceChecker do
|
5
|
+
|
6
|
+
let(:sequence_error) {[CommandError, '503 Bad sequence of commands']}
|
7
|
+
subject(:checker) {CommandSequenceChecker.new}
|
8
|
+
|
9
|
+
context 'initial' do
|
10
|
+
|
11
|
+
it 'accepts any command' do
|
12
|
+
checker.check 'NOOP'
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'when a specific command is expected' do
|
18
|
+
|
19
|
+
before(:each) {checker.expect 'PASS'}
|
20
|
+
|
21
|
+
it 'accepts that command' do
|
22
|
+
checker.check 'PASS'
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'rejects any other command' do
|
26
|
+
expect {
|
27
|
+
checker.check 'NOOP'
|
28
|
+
}.to raise_error *sequence_error
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'after the expected command has arrived' do
|
34
|
+
|
35
|
+
before(:each) do
|
36
|
+
checker.expect 'PASS'
|
37
|
+
checker.check 'PASS'
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'accepts any other command' do
|
41
|
+
checker.check 'NOOP'
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'after a command is rejected' do
|
47
|
+
|
48
|
+
before(:each) do
|
49
|
+
checker.expect 'PASS'
|
50
|
+
expect {
|
51
|
+
checker.check 'NOOP'
|
52
|
+
}.to raise_error *sequence_error
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'accepts any other command' do
|
56
|
+
checker.check 'NOOP'
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'when a command must be expected' do
|
62
|
+
|
63
|
+
before(:each) do
|
64
|
+
checker.must_expect 'PASS'
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'rejects that command if not expected' do
|
68
|
+
expect {
|
69
|
+
checker.check 'PASS'
|
70
|
+
}.to raise_error *sequence_error
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'accepts that command when it is accepted' do
|
74
|
+
checker.expect 'PASS'
|
75
|
+
checker.check 'PASS'
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ftpd
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-02
|
12
|
+
date: 2013-03-02 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: memoizer
|
@@ -156,7 +156,8 @@ files:
|
|
156
156
|
- README.md
|
157
157
|
- Rakefile
|
158
158
|
- VERSION
|
159
|
-
- doc/
|
159
|
+
- doc/references.md
|
160
|
+
- doc/rfc-compliance.md
|
160
161
|
- examples/example.rb
|
161
162
|
- examples/hello_world.rb
|
162
163
|
- features/example/example.feature
|
@@ -171,6 +172,7 @@ files:
|
|
171
172
|
- features/ftp_server/file_structure.feature
|
172
173
|
- features/ftp_server/get.feature
|
173
174
|
- features/ftp_server/get_tls.feature
|
175
|
+
- features/ftp_server/help.feature
|
174
176
|
- features/ftp_server/implicit_tls.feature
|
175
177
|
- features/ftp_server/list.feature
|
176
178
|
- features/ftp_server/list_tls.feature
|
@@ -183,6 +185,7 @@ files:
|
|
183
185
|
- features/ftp_server/port.feature
|
184
186
|
- features/ftp_server/put.feature
|
185
187
|
- features/ftp_server/put_tls.feature
|
188
|
+
- features/ftp_server/put_unique.feature
|
186
189
|
- features/ftp_server/quit.feature
|
187
190
|
- features/ftp_server/rename.feature
|
188
191
|
- features/ftp_server/rmdir.feature
|
@@ -202,6 +205,7 @@ files:
|
|
202
205
|
- features/step_definitions/file_structure.rb
|
203
206
|
- features/step_definitions/generic_send.rb
|
204
207
|
- features/step_definitions/get.rb
|
208
|
+
- features/step_definitions/help.rb
|
205
209
|
- features/step_definitions/invalid_commands.rb
|
206
210
|
- features/step_definitions/line_endings.rb
|
207
211
|
- features/step_definitions/list.rb
|
@@ -210,6 +214,7 @@ files:
|
|
210
214
|
- features/step_definitions/mode.rb
|
211
215
|
- features/step_definitions/noop.rb
|
212
216
|
- features/step_definitions/passive.rb
|
217
|
+
- features/step_definitions/pending.rb
|
213
218
|
- features/step_definitions/port.rb
|
214
219
|
- features/step_definitions/put.rb
|
215
220
|
- features/step_definitions/quit.rb
|
@@ -232,6 +237,7 @@ files:
|
|
232
237
|
- ftpd.gemspec
|
233
238
|
- insecure-test-cert.pem
|
234
239
|
- lib/ftpd.rb
|
240
|
+
- lib/ftpd/command_sequence_checker.rb
|
235
241
|
- lib/ftpd/disk_file_system.rb
|
236
242
|
- lib/ftpd/error.rb
|
237
243
|
- lib/ftpd/exception_translator.rb
|
@@ -250,6 +256,7 @@ files:
|
|
250
256
|
- rake_tasks/spec.rake
|
251
257
|
- rake_tasks/test.rake
|
252
258
|
- rake_tasks/yard.rake
|
259
|
+
- spec/command_sequence_checker_spec.rb
|
253
260
|
- spec/disk_file_system_spec.rb
|
254
261
|
- spec/exception_translator_spec.rb
|
255
262
|
- spec/file_system_error_translator_spec.rb
|
@@ -270,7 +277,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
270
277
|
version: '0'
|
271
278
|
segments:
|
272
279
|
- 0
|
273
|
-
hash:
|
280
|
+
hash: 460714675
|
274
281
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
275
282
|
none: false
|
276
283
|
requirements:
|
@@ -279,7 +286,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
279
286
|
version: '0'
|
280
287
|
requirements: []
|
281
288
|
rubyforge_project:
|
282
|
-
rubygems_version: 1.8.
|
289
|
+
rubygems_version: 1.8.25
|
283
290
|
signing_key:
|
284
291
|
specification_version: 3
|
285
292
|
summary: Pure Ruby FTP server library
|