ftpd 0.3.2 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +1 -0
- data/Changelog.md +25 -1
- data/README.md +91 -10
- data/VERSION +1 -1
- data/doc/rfc-compliance.md +20 -20
- data/examples/example.rb +69 -11
- data/features/example/read_only.feature +63 -0
- data/features/ftp_server/append.feature +94 -0
- data/features/ftp_server/command_errors.feature +0 -2
- data/features/ftp_server/concurrent_sessions.feature +1 -1
- data/features/ftp_server/debug.feature +4 -2
- data/features/ftp_server/delete.feature +1 -1
- data/features/ftp_server/get.feature +1 -1
- data/features/ftp_server/get_tls.feature +2 -1
- data/features/ftp_server/implicit_tls.feature +2 -1
- data/features/ftp_server/invertability.feature +15 -0
- data/features/ftp_server/list.feature +1 -1
- data/features/ftp_server/list_tls.feature +2 -1
- data/features/ftp_server/login_auth_level_account.feature +51 -0
- data/features/ftp_server/{login.feature → login_auth_level_password.feature} +4 -8
- data/features/ftp_server/login_auth_level_user.feature +31 -0
- data/features/ftp_server/mkdir.feature +1 -1
- data/features/ftp_server/name_list.feature +1 -1
- data/features/ftp_server/name_list_tls.feature +2 -1
- data/features/ftp_server/put.feature +1 -1
- data/features/ftp_server/put_tls.feature +2 -1
- data/features/ftp_server/put_unique.feature +1 -1
- data/features/ftp_server/rename.feature +1 -1
- data/features/ftp_server/rmdir.feature +1 -1
- data/features/ftp_server/step_definitions/debug.rb +1 -1
- data/features/ftp_server/step_definitions/test_server.rb +16 -15
- data/features/ftp_server/type.feature +11 -8
- data/features/step_definitions/append.rb +15 -0
- data/features/step_definitions/client_and_server_files.rb +2 -2
- data/features/step_definitions/client_files.rb +5 -0
- data/features/step_definitions/connect.rb +1 -1
- data/features/step_definitions/error_replies.rb +0 -4
- data/features/step_definitions/login.rb +30 -20
- data/features/step_definitions/server_files.rb +20 -7
- data/features/support/example_server.rb +10 -2
- data/features/support/test_client.rb +18 -0
- data/features/support/test_file_templates.rb +1 -1
- data/features/support/test_server.rb +25 -4
- data/ftpd.gemspec +11 -3
- data/lib/ftpd.rb +2 -0
- data/lib/ftpd/auth_levels.rb +9 -0
- data/lib/ftpd/disk_file_system.rb +48 -19
- data/lib/ftpd/ftp_server.rb +13 -10
- data/lib/ftpd/read_only_disk_file_system.rb +22 -0
- data/lib/ftpd/session.rb +62 -29
- data/spec/disk_file_system_spec.rb +30 -0
- metadata +12 -4
data/lib/ftpd.rb
CHANGED
@@ -20,6 +20,7 @@ module Ftpd
|
|
20
20
|
autoload :FileSystemMethodMissing, 'ftpd/file_system_method_missing'
|
21
21
|
autoload :FtpServer, 'ftpd/ftp_server'
|
22
22
|
autoload :InsecureCertificate, 'ftpd/insecure_certificate'
|
23
|
+
autoload :ReadOnlyDiskFileSystem, 'ftpd/read_only_disk_file_system'
|
23
24
|
autoload :Server, 'ftpd/server'
|
24
25
|
autoload :Session, 'ftpd/session'
|
25
26
|
autoload :TempDir, 'ftpd/temp_dir'
|
@@ -27,4 +28,5 @@ module Ftpd
|
|
27
28
|
autoload :TranslateExceptions, 'ftpd/translate_exceptions'
|
28
29
|
end
|
29
30
|
|
31
|
+
require 'ftpd/auth_levels'
|
30
32
|
require 'ftpd/exceptions'
|
@@ -40,14 +40,6 @@ module Ftpd
|
|
40
40
|
# directory named by the path may not exist.
|
41
41
|
# @param ftp_path [String] The virtual path
|
42
42
|
# @return [Boolean]
|
43
|
-
#
|
44
|
-
# Called for:
|
45
|
-
# * STOR
|
46
|
-
# * RETR
|
47
|
-
# * DELE
|
48
|
-
# * CWD
|
49
|
-
# * MKD
|
50
|
-
# * RMD
|
51
43
|
|
52
44
|
def accessible?(ftp_path)
|
53
45
|
# The server should never try to access a path outside of the
|
@@ -59,13 +51,6 @@ module Ftpd
|
|
59
51
|
# Return true if the file or directory path exists.
|
60
52
|
# @param ftp_path [String] The virtual path
|
61
53
|
# @return [Boolean]
|
62
|
-
#
|
63
|
-
# Called for:
|
64
|
-
# * STOR (with directory)
|
65
|
-
# * RETR
|
66
|
-
# * DELE
|
67
|
-
# * CWD
|
68
|
-
# * MKD
|
69
54
|
|
70
55
|
def exists?(ftp_path)
|
71
56
|
File.exists?(expand_ftp_path(ftp_path))
|
@@ -74,10 +59,6 @@ module Ftpd
|
|
74
59
|
# Return true if the path exists and is a directory.
|
75
60
|
# @param ftp_path [String] The virtual path
|
76
61
|
# @return [Boolean]
|
77
|
-
#
|
78
|
-
# Called for:
|
79
|
-
# * CWD
|
80
|
-
# * MKD
|
81
62
|
|
82
63
|
def directory?(ftp_path)
|
83
64
|
File.directory?(expand_ftp_path(ftp_path))
|
@@ -162,6 +143,33 @@ module Ftpd
|
|
162
143
|
end
|
163
144
|
end
|
164
145
|
|
146
|
+
class DiskFileSystem
|
147
|
+
|
148
|
+
# DiskFileSystem mixin providing file appending
|
149
|
+
|
150
|
+
module Append
|
151
|
+
|
152
|
+
include TranslateExceptions
|
153
|
+
|
154
|
+
# Append to a file. If the file does not exist, create it.
|
155
|
+
# @param ftp_path [String] The virtual path
|
156
|
+
# @contents [String] The file's contents
|
157
|
+
#
|
158
|
+
# Called for:
|
159
|
+
# * APPE
|
160
|
+
#
|
161
|
+
# If missing, then these commands are not supported.
|
162
|
+
|
163
|
+
def append(ftp_path, contents)
|
164
|
+
File.open(expand_ftp_path(ftp_path), 'ab') do |file|
|
165
|
+
file.write contents
|
166
|
+
end
|
167
|
+
end
|
168
|
+
translate_exceptions :append
|
169
|
+
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
165
173
|
class DiskFileSystem
|
166
174
|
|
167
175
|
# DiskFileSystem mixing providing mkdir
|
@@ -360,6 +368,7 @@ module Ftpd
|
|
360
368
|
# can be safely left out with the only effect being to make One or
|
361
369
|
# more commands be unimplemented.
|
362
370
|
|
371
|
+
include DiskFileSystem::Append
|
363
372
|
include DiskFileSystem::Delete
|
364
373
|
include DiskFileSystem::List
|
365
374
|
include DiskFileSystem::Mkdir
|
@@ -377,4 +386,24 @@ module Ftpd
|
|
377
386
|
end
|
378
387
|
|
379
388
|
end
|
389
|
+
|
390
|
+
# A disk file system that does not allow any modification (writes,
|
391
|
+
# deletes, etc.)
|
392
|
+
|
393
|
+
class ReadOnlyDiskFileSystem
|
394
|
+
|
395
|
+
include DiskFileSystem::Base
|
396
|
+
include DiskFileSystem::List
|
397
|
+
include DiskFileSystem::Read
|
398
|
+
|
399
|
+
# Make a new instance to serve a directory. data_dir should be an
|
400
|
+
# absolute path.
|
401
|
+
|
402
|
+
def initialize(data_dir)
|
403
|
+
set_data_dir data_dir
|
404
|
+
translate_exception SystemCallError
|
405
|
+
end
|
406
|
+
|
407
|
+
end
|
408
|
+
|
380
409
|
end
|
data/lib/ftpd/ftp_server.rb
CHANGED
@@ -31,6 +31,14 @@ module Ftpd
|
|
31
31
|
|
32
32
|
attr_accessor :list_formatter
|
33
33
|
|
34
|
+
# @return [Integer] The authentication level
|
35
|
+
# One of:
|
36
|
+
# * Ftpd::AUTH_USER
|
37
|
+
# * Ftpd::AUTH_PASSWORD (default)
|
38
|
+
# * Ftpd::AUTH_ACCOUNT
|
39
|
+
|
40
|
+
attr_accessor :auth_level
|
41
|
+
|
34
42
|
# Create a new FTP server. The server won't start until the
|
35
43
|
# #start method is called.
|
36
44
|
#
|
@@ -38,15 +46,8 @@ module Ftpd
|
|
38
46
|
# authentication and file system access.
|
39
47
|
#
|
40
48
|
# The driver should expose these public methods:
|
41
|
-
#
|
42
|
-
#
|
43
|
-
# # log in.
|
44
|
-
# authenticate(user, password)
|
45
|
-
#
|
46
|
-
# # Return the file system to use for a user. The file system
|
47
|
-
# # should expose the same public methods as
|
48
|
-
# # Ftpd::DiskFileSystem.
|
49
|
-
# def file_system(user)
|
49
|
+
# * {Example::Driver#authenticate authenticate}
|
50
|
+
# * {Example::Driver#file_system file_system}
|
50
51
|
|
51
52
|
def initialize(driver)
|
52
53
|
super()
|
@@ -55,6 +56,7 @@ module Ftpd
|
|
55
56
|
@debug = false
|
56
57
|
@response_delay = 0
|
57
58
|
@list_formatter = ListFormat::Ls
|
59
|
+
@auth_level = AUTH_PASSWORD
|
58
60
|
end
|
59
61
|
|
60
62
|
private
|
@@ -66,7 +68,8 @@ module Ftpd
|
|
66
68
|
:debug_path => debug_path,
|
67
69
|
:list_formatter => @list_formatter,
|
68
70
|
:response_delay => response_delay,
|
69
|
-
:tls => @tls
|
71
|
+
:tls => @tls,
|
72
|
+
:auth_level => @auth_level).run
|
70
73
|
end
|
71
74
|
|
72
75
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Ftpd
|
2
|
+
|
3
|
+
# A disk file system that does not allow any modification (writes,
|
4
|
+
# deletes, etc.)
|
5
|
+
|
6
|
+
class ReadOnlyDiskFileSystem
|
7
|
+
|
8
|
+
include DiskFileSystem::Base
|
9
|
+
include DiskFileSystem::List
|
10
|
+
include DiskFileSystem::Read
|
11
|
+
|
12
|
+
# Make a new instance to serve a directory. data_dir should be an
|
13
|
+
# absolute path.
|
14
|
+
|
15
|
+
def initialize(data_dir)
|
16
|
+
set_data_dir data_dir
|
17
|
+
translate_exception SystemCallError
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
data/lib/ftpd/session.rb
CHANGED
@@ -7,6 +7,7 @@ module Ftpd
|
|
7
7
|
|
8
8
|
def initialize(opts)
|
9
9
|
@driver = opts[:driver]
|
10
|
+
@auth_level = opts[:auth_level]
|
10
11
|
@socket = opts[:socket]
|
11
12
|
@tls = opts[:tls]
|
12
13
|
if @tls == :implicit
|
@@ -18,7 +19,6 @@ module Ftpd
|
|
18
19
|
@list_formatter = opts[:list_formatter]
|
19
20
|
@data_type = 'A'
|
20
21
|
@mode = 'S'
|
21
|
-
@format = 'N'
|
22
22
|
@structure = 'F'
|
23
23
|
@response_delay = opts[:response_delay]
|
24
24
|
@data_channel_protection_level = :clear
|
@@ -65,19 +65,29 @@ module Ftpd
|
|
65
65
|
syntax_error unless argument
|
66
66
|
sequence_error if @logged_in
|
67
67
|
@user = argument
|
68
|
-
|
69
|
-
|
68
|
+
if @auth_level > AUTH_USER
|
69
|
+
reply "331 Password required"
|
70
|
+
expect 'pass'
|
71
|
+
else
|
72
|
+
login(@user)
|
73
|
+
end
|
70
74
|
end
|
71
75
|
|
72
76
|
def cmd_pass(argument)
|
73
77
|
syntax_error unless argument
|
74
|
-
password = argument
|
75
|
-
|
76
|
-
|
78
|
+
@password = argument
|
79
|
+
if @auth_level > AUTH_PASSWORD
|
80
|
+
reply "332 Account required"
|
81
|
+
expect 'acct'
|
82
|
+
else
|
83
|
+
login(@user, @password)
|
77
84
|
end
|
78
|
-
|
79
|
-
|
80
|
-
|
85
|
+
end
|
86
|
+
|
87
|
+
def cmd_acct(argument)
|
88
|
+
syntax_error unless argument
|
89
|
+
account = argument
|
90
|
+
login(@user, @password, account)
|
81
91
|
end
|
82
92
|
|
83
93
|
def cmd_quit(argument)
|
@@ -136,6 +146,20 @@ module Ftpd
|
|
136
146
|
end
|
137
147
|
end
|
138
148
|
|
149
|
+
def cmd_appe(argument)
|
150
|
+
close_data_server_socket_when_done do
|
151
|
+
ensure_logged_in
|
152
|
+
ensure_file_system_supports :append
|
153
|
+
path = argument
|
154
|
+
syntax_error unless path
|
155
|
+
path = File.expand_path(path, @name_prefix)
|
156
|
+
ensure_accessible path
|
157
|
+
contents = receive_file
|
158
|
+
@file_system.append path, contents
|
159
|
+
reply "226 Transfer complete"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
139
163
|
def cmd_retr(argument)
|
140
164
|
close_data_server_socket_when_done do
|
141
165
|
ensure_logged_in
|
@@ -190,26 +214,20 @@ module Ftpd
|
|
190
214
|
syntax_error unless argument =~ /^(\S)(?: (\S+))?$/
|
191
215
|
type_code = $1
|
192
216
|
format_code = $2
|
193
|
-
|
194
|
-
|
217
|
+
unless argument =~ /^([AEI]( [NTC])?|L .*)$/
|
218
|
+
error '504 Invalid type code'
|
219
|
+
end
|
220
|
+
case argument
|
221
|
+
when /^A( [NT])?$/
|
222
|
+
@data_type = 'A'
|
223
|
+
when /^(I|L 8)$/
|
224
|
+
@data_type = 'I'
|
225
|
+
else
|
226
|
+
error '504 Type not implemented'
|
227
|
+
end
|
195
228
|
reply "200 Type set to #{@data_type}"
|
196
229
|
end
|
197
230
|
|
198
|
-
def set_type(type_code)
|
199
|
-
name, implemented = DATA_TYPES[type_code]
|
200
|
-
error "504 Invalid type code" unless name
|
201
|
-
error "504 Type not implemented" unless implemented
|
202
|
-
@data_type = type_code
|
203
|
-
end
|
204
|
-
|
205
|
-
def set_format(format_code)
|
206
|
-
format_code ||= 'N'
|
207
|
-
name, implemented = FORMAT_TYPES[format_code]
|
208
|
-
error "504 Invalid format code" unless name
|
209
|
-
error "504 Format not implemented" unless implemented
|
210
|
-
@data_format = format_code
|
211
|
-
end
|
212
|
-
|
213
231
|
def cmd_mode(argument)
|
214
232
|
syntax_error unless argument
|
215
233
|
ensure_logged_in
|
@@ -443,8 +461,6 @@ module Ftpd
|
|
443
461
|
end
|
444
462
|
|
445
463
|
unimplemented :abor
|
446
|
-
unimplemented :acct
|
447
|
-
unimplemented :appe
|
448
464
|
unimplemented :rein
|
449
465
|
unimplemented :rest
|
450
466
|
unimplemented :site
|
@@ -469,7 +485,7 @@ module Ftpd
|
|
469
485
|
|
470
486
|
FORMAT_TYPES = {
|
471
487
|
'N'=>['Non-print', true],
|
472
|
-
'T'=>['Telnet format effectors',
|
488
|
+
'T'=>['Telnet format effectors', true],
|
473
489
|
'C'=>['Carriage Control (ASA)', false],
|
474
490
|
}
|
475
491
|
|
@@ -674,6 +690,7 @@ module Ftpd
|
|
674
690
|
|
675
691
|
def init_command_sequence_checker
|
676
692
|
checker = CommandSequenceChecker.new
|
693
|
+
checker.must_expect 'acct'
|
677
694
|
checker.must_expect 'pass'
|
678
695
|
checker.must_expect 'rnto'
|
679
696
|
checker
|
@@ -703,5 +720,21 @@ module Ftpd
|
|
703
720
|
@file_system.dir(path).sort
|
704
721
|
end
|
705
722
|
|
723
|
+
def authenticate(*args)
|
724
|
+
while args.size < @driver.method(:authenticate).arity
|
725
|
+
args << nil
|
726
|
+
end
|
727
|
+
@driver.authenticate(*args)
|
728
|
+
end
|
729
|
+
|
730
|
+
def login(*auth_tokens)
|
731
|
+
unless authenticate(*auth_tokens)
|
732
|
+
error "530 Login incorrect"
|
733
|
+
end
|
734
|
+
reply "230 Logged in"
|
735
|
+
set_file_system @driver.file_system(@user)
|
736
|
+
@logged_in = true
|
737
|
+
end
|
738
|
+
|
706
739
|
end
|
707
740
|
end
|
@@ -164,6 +164,36 @@ module Ftpd
|
|
164
164
|
|
165
165
|
end
|
166
166
|
|
167
|
+
describe '#append' do
|
168
|
+
|
169
|
+
let(:contents) {'file contents'}
|
170
|
+
|
171
|
+
context '(destination missing)' do
|
172
|
+
let(:path) {'file_path'}
|
173
|
+
specify do
|
174
|
+
disk_file_system.append(path, contents)
|
175
|
+
read_file(path).should == contents
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
context '(destination exists)' do
|
180
|
+
let(:path) {'file'}
|
181
|
+
specify do
|
182
|
+
disk_file_system.append(path, contents)
|
183
|
+
read_file(path).should == canned_contents(path) + contents
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
context '(error)' do
|
188
|
+
specify do
|
189
|
+
expect {
|
190
|
+
disk_file_system.append('dir', contents)
|
191
|
+
}.to raise_error *is_a_directory_error
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|
196
|
+
|
167
197
|
describe '#mkdir' do
|
168
198
|
|
169
199
|
context '(success)' do
|
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.
|
4
|
+
version: 0.4.0
|
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-03-
|
12
|
+
date: 2013-03-06 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: memoizer
|
@@ -179,8 +179,10 @@ files:
|
|
179
179
|
- examples/hello_world.rb
|
180
180
|
- features/example/eplf.feature
|
181
181
|
- features/example/example.feature
|
182
|
+
- features/example/read_only.feature
|
182
183
|
- features/example/step_definitions/example_server.rb
|
183
184
|
- features/ftp_server/allo.feature
|
185
|
+
- features/ftp_server/append.feature
|
184
186
|
- features/ftp_server/cdup.feature
|
185
187
|
- features/ftp_server/command_errors.feature
|
186
188
|
- features/ftp_server/concurrent_sessions.feature
|
@@ -192,9 +194,12 @@ files:
|
|
192
194
|
- features/ftp_server/get_tls.feature
|
193
195
|
- features/ftp_server/help.feature
|
194
196
|
- features/ftp_server/implicit_tls.feature
|
197
|
+
- features/ftp_server/invertability.feature
|
195
198
|
- features/ftp_server/list.feature
|
196
199
|
- features/ftp_server/list_tls.feature
|
197
|
-
- features/ftp_server/
|
200
|
+
- features/ftp_server/login_auth_level_account.feature
|
201
|
+
- features/ftp_server/login_auth_level_password.feature
|
202
|
+
- features/ftp_server/login_auth_level_user.feature
|
198
203
|
- features/ftp_server/mkdir.feature
|
199
204
|
- features/ftp_server/mode.feature
|
200
205
|
- features/ftp_server/name_list.feature
|
@@ -212,6 +217,7 @@ files:
|
|
212
217
|
- features/ftp_server/syntax_errors.feature
|
213
218
|
- features/ftp_server/syst.feature
|
214
219
|
- features/ftp_server/type.feature
|
220
|
+
- features/step_definitions/append.rb
|
215
221
|
- features/step_definitions/client.rb
|
216
222
|
- features/step_definitions/client_and_server_files.rb
|
217
223
|
- features/step_definitions/client_files.rb
|
@@ -255,6 +261,7 @@ files:
|
|
255
261
|
- ftpd.gemspec
|
256
262
|
- insecure-test-cert.pem
|
257
263
|
- lib/ftpd.rb
|
264
|
+
- lib/ftpd/auth_levels.rb
|
258
265
|
- lib/ftpd/command_sequence_checker.rb
|
259
266
|
- lib/ftpd/disk_file_system.rb
|
260
267
|
- lib/ftpd/error.rb
|
@@ -266,6 +273,7 @@ files:
|
|
266
273
|
- lib/ftpd/insecure_certificate.rb
|
267
274
|
- lib/ftpd/list_format/eplf.rb
|
268
275
|
- lib/ftpd/list_format/ls.rb
|
276
|
+
- lib/ftpd/read_only_disk_file_system.rb
|
269
277
|
- lib/ftpd/server.rb
|
270
278
|
- lib/ftpd/session.rb
|
271
279
|
- lib/ftpd/temp_dir.rb
|
@@ -301,7 +309,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
301
309
|
version: '0'
|
302
310
|
segments:
|
303
311
|
- 0
|
304
|
-
hash:
|
312
|
+
hash: 451937393
|
305
313
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
306
314
|
none: false
|
307
315
|
requirements:
|