ftpd 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/.yardopts +1 -0
  2. data/Changelog.md +25 -1
  3. data/README.md +91 -10
  4. data/VERSION +1 -1
  5. data/doc/rfc-compliance.md +20 -20
  6. data/examples/example.rb +69 -11
  7. data/features/example/read_only.feature +63 -0
  8. data/features/ftp_server/append.feature +94 -0
  9. data/features/ftp_server/command_errors.feature +0 -2
  10. data/features/ftp_server/concurrent_sessions.feature +1 -1
  11. data/features/ftp_server/debug.feature +4 -2
  12. data/features/ftp_server/delete.feature +1 -1
  13. data/features/ftp_server/get.feature +1 -1
  14. data/features/ftp_server/get_tls.feature +2 -1
  15. data/features/ftp_server/implicit_tls.feature +2 -1
  16. data/features/ftp_server/invertability.feature +15 -0
  17. data/features/ftp_server/list.feature +1 -1
  18. data/features/ftp_server/list_tls.feature +2 -1
  19. data/features/ftp_server/login_auth_level_account.feature +51 -0
  20. data/features/ftp_server/{login.feature → login_auth_level_password.feature} +4 -8
  21. data/features/ftp_server/login_auth_level_user.feature +31 -0
  22. data/features/ftp_server/mkdir.feature +1 -1
  23. data/features/ftp_server/name_list.feature +1 -1
  24. data/features/ftp_server/name_list_tls.feature +2 -1
  25. data/features/ftp_server/put.feature +1 -1
  26. data/features/ftp_server/put_tls.feature +2 -1
  27. data/features/ftp_server/put_unique.feature +1 -1
  28. data/features/ftp_server/rename.feature +1 -1
  29. data/features/ftp_server/rmdir.feature +1 -1
  30. data/features/ftp_server/step_definitions/debug.rb +1 -1
  31. data/features/ftp_server/step_definitions/test_server.rb +16 -15
  32. data/features/ftp_server/type.feature +11 -8
  33. data/features/step_definitions/append.rb +15 -0
  34. data/features/step_definitions/client_and_server_files.rb +2 -2
  35. data/features/step_definitions/client_files.rb +5 -0
  36. data/features/step_definitions/connect.rb +1 -1
  37. data/features/step_definitions/error_replies.rb +0 -4
  38. data/features/step_definitions/login.rb +30 -20
  39. data/features/step_definitions/server_files.rb +20 -7
  40. data/features/support/example_server.rb +10 -2
  41. data/features/support/test_client.rb +18 -0
  42. data/features/support/test_file_templates.rb +1 -1
  43. data/features/support/test_server.rb +25 -4
  44. data/ftpd.gemspec +11 -3
  45. data/lib/ftpd.rb +2 -0
  46. data/lib/ftpd/auth_levels.rb +9 -0
  47. data/lib/ftpd/disk_file_system.rb +48 -19
  48. data/lib/ftpd/ftp_server.rb +13 -10
  49. data/lib/ftpd/read_only_disk_file_system.rb +22 -0
  50. data/lib/ftpd/session.rb +62 -29
  51. data/spec/disk_file_system_spec.rb +30 -0
  52. 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'
@@ -0,0 +1,9 @@
1
+ module Ftpd
2
+
3
+ # Authorization levels for FtpServer#auth_level
4
+
5
+ AUTH_USER = 0
6
+ AUTH_PASSWORD = 1
7
+ AUTH_ACCOUNT = 2
8
+
9
+ end
@@ -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
@@ -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
- # # Return truthy if the user/password should be allowed to
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).run
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
- reply "331 Password required"
69
- expect 'pass'
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
- unless @driver.authenticate(@user, password)
76
- error "530 Login incorrect"
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
- reply "230 Logged in"
79
- set_file_system @driver.file_system(@user)
80
- @logged_in = true
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
- set_type(type_code)
194
- set_format(format_code)
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', false],
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.3.2
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-05 00:00:00.000000000 Z
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/login.feature
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: 286911087
312
+ hash: 451937393
305
313
  required_rubygems_version: !ruby/object:Gem::Requirement
306
314
  none: false
307
315
  requirements: