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 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.1.4)
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.3)
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 (3.12.1)
28
+ rdoc (4.0.0)
29
29
  json (~> 1.4)
30
30
  redcarpet (2.2.2)
31
- rspec (2.12.0)
32
- rspec-core (~> 2.12.0)
33
- rspec-expectations (~> 2.12.0)
34
- rspec-mocks (~> 2.12.0)
35
- rspec-core (2.12.2)
36
- rspec-expectations (2.12.1)
37
- diff-lcs (~> 1.1.3)
38
- rspec-mocks (2.12.2)
39
- yard (0.8.4.1)
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
- The automated tests don't cover TLS in passive mode (although it works
92
- fine): The FTPS client used by the test locks up in active mode.
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
- ## VERSIONS
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
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 No --- Help
32
+ HELP Yes 0.2.2 Help
33
33
  LIST Yes 0.1.0 List directory
34
- MKD Yes dev Make directory
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 dev Remove directory
49
- RNFR Yes dev Rename file (from)
50
- RNTO Yes dev Rename file (to)
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 No --- Store with unique name
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
- Restruct PASV/PORT to non-priv. ports No ---
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 --- Authentication/Security Mechanism
287
+ AUTH Yes 0.1.0 Authentication/Security Mechanism
273
288
  CCC No --- Clear Command Channel
274
- PBSZ Yes --- Protection Buffer Size
275
- PROT Yes --- Data Channel Protection Level.
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>
@@ -21,10 +21,8 @@ Feature: Command Errors
21
21
  | ABOR |
22
22
  | ACCT |
23
23
  | APPE |
24
- | HELP |
25
24
  | REIN |
26
25
  | REST |
27
26
  | SITE |
28
27
  | SMNT |
29
28
  | STAT |
30
- | STOU |
@@ -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"
@@ -42,6 +42,7 @@ Feature: List
42
42
  And the file list should contain "foo"
43
43
 
44
44
  Scenario: Glob
45
+ Given PENDING "Disabled (for now) due to code injection vulnerability"
45
46
  Given a successful login
46
47
  And the server has file "foo"
47
48
  And the server has file "bar"
@@ -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 password with no parameter
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: USRE without parameter
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"
@@ -1,9 +1,8 @@
1
1
  Feature: Put
2
2
 
3
3
  As a client
4
- I want to securely put a file
4
+ I want to upload a file
5
5
  So that someone else can have it
6
- But nobody else can
7
6
 
8
7
  Background:
9
8
  Given the test server is started
@@ -1,8 +1,8 @@
1
1
  Feature: Put TLS
2
2
 
3
3
  As a client
4
- I want to put a file
5
- So that someone else can have it
4
+ I want to put a file securely
5
+ So that nobody can intercept it
6
6
 
7
7
  Background:
8
8
  Given the test server is started with explicit TLS
@@ -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
@@ -0,0 +1,3 @@
1
+ Given /^PENDING/ do
2
+ pending
3
+ 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.puts @templates[File.basename(full_path)]
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.puts @templates[File.basename(full_path)]
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.1"
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-28"
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/rfc.md",
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.24"
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
- '2>&1',
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
@@ -17,6 +17,10 @@ module Ftpd
17
17
  error "502 Command not implemented"
18
18
  end
19
19
 
20
+ def sequence_error
21
+ error "503 Bad sequence of commands"
22
+ end
23
+
20
24
  def permanent_error(message)
21
25
  error "550 #{message}"
22
26
  end
@@ -54,7 +54,6 @@ module Ftpd
54
54
 
55
55
  def session(socket)
56
56
  Session.new(:socket => socket,
57
- :interface => interface,
58
57
  :driver => @driver,
59
58
  :debug => @debug,
60
59
  :debug_path => debug_path,
data/lib/ftpd/server.rb CHANGED
@@ -21,7 +21,7 @@ module Ftpd
21
21
  @port = 0
22
22
  end
23
23
 
24
- # The port the server is bound to. Must be called until after
24
+ # The port the server is bound to. Must not be called until after
25
25
  # #start is called.
26
26
 
27
27
  def bound_port
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 unless @state == :user
65
+ sequence_error if @logged_in
65
66
  @user = argument
66
- @state = :password
67
67
  reply "331 Password required"
68
- end
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
- @state = :logged_in
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
- @state = :user
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(path)
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
- @data_server = TCPServer.new(@interface, 0)
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 @state == :logged_in
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
- @rename_from_path = nil
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
- unimplemented :stou
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(path)
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
- reply "150 Opening #{data_connection_description}"
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.1
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-28 00:00:00.000000000 Z
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/rfc.md
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: 537238415
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.24
289
+ rubygems_version: 1.8.25
283
290
  signing_key:
284
291
  specification_version: 3
285
292
  summary: Pure Ruby FTP server library