ftpd 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,36 @@
1
+ Feature: EPSV
2
+
3
+ As a programmer
4
+ I want good error messages
5
+ So that I can correct problems
6
+
7
+ Background:
8
+ Given the test server is bound to "::"
9
+ And the test server is started
10
+
11
+ Scenario: No argument
12
+ Given a successful login
13
+ Then the client successfully sends "EPSV"
14
+
15
+ Scenario: Explicit IPV4
16
+ Given a successful login
17
+ Then the client successfully sends "EPSV 1"
18
+
19
+ Scenario: Explicit IPV6
20
+ Given a successful login
21
+ Then the client successfully sends "EPSV 2"
22
+
23
+ Scenario: After "EPSV ALL"
24
+ Given a successful login
25
+ Given the client successfully sends "EPSV ALL"
26
+ Then the client successfully sends "EPSV"
27
+
28
+ Scenario: Not logged in
29
+ Given a successful connection
30
+ When the client sends "EPSV"
31
+ Then the server returns a not logged in error
32
+
33
+ Scenario: Unknown network protocol
34
+ Given a successful login
35
+ When the client sends "EPSV 99"
36
+ Then the server returns a network protocol not supported error
@@ -0,0 +1,43 @@
1
+ Feature: Get IPV6
2
+
3
+ As a client
4
+ I want to get a file
5
+ So that I have it on my computer
6
+
7
+ Scenario: Active
8
+ Given the test server is bound to "::1"
9
+ And the test server is started
10
+ And a successful login
11
+ And the server has file "ascii_unix"
12
+ And the client is in active mode
13
+ When the client successfully gets text "ascii_unix"
14
+ Then the local file "ascii_unix" should match the remote file
15
+
16
+ Scenario: Passive
17
+ Given the test server is bound to "::1"
18
+ And the test server is started
19
+ And a successful login
20
+ And the server has file "ascii_unix"
21
+ And the client is in passive mode
22
+ When the client successfully gets text "ascii_unix"
23
+ Then the local file "ascii_unix" should match the remote file
24
+
25
+ Scenario: Active, TLS
26
+ Given the test server is bound to "::1"
27
+ And the test server has TLS mode "explicit"
28
+ And the test server is started
29
+ And a successful login
30
+ And the server has file "ascii_unix"
31
+ And the client is in active mode
32
+ When the client successfully gets text "ascii_unix"
33
+ Then the local file "ascii_unix" should match the remote file
34
+
35
+ Scenario: Passive, TLS
36
+ Given the test server is bound to "::1"
37
+ And the test server has TLS mode "explicit"
38
+ And the test server is started
39
+ And a successful login
40
+ And the server has file "ascii_unix"
41
+ And the client is in passive mode
42
+ When the client successfully gets text "ascii_unix"
43
+ Then the local file "ascii_unix" should match the remote file
@@ -68,6 +68,15 @@ Feature: List
68
68
  And the file list should contain "foo"
69
69
  And the file list should contain "bar"
70
70
 
71
+ Scenario: -a
72
+ Given a successful login
73
+ And the server has file "foo"
74
+ And the server has file "bar"
75
+ When the client successfully lists the directory "-a"
76
+ Then the file list should be in long form
77
+ And the file list should contain "foo"
78
+ And the file list should contain "bar"
79
+
71
80
  Scenario: Missing directory
72
81
  Given a successful login
73
82
  When the client successfully lists the directory "missing/file"
@@ -41,6 +41,15 @@ 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: '-a'
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 "-a"
49
+ Then the file list should be in short form
50
+ And the file list should contain "foo"
51
+ And the file list should contain "bar"
52
+
44
53
  Scenario: Passive
45
54
  Given a successful login
46
55
  And the server has file "foo"
@@ -0,0 +1,23 @@
1
+ Feature: PASV
2
+
3
+ As a programmer
4
+ I want good error messages
5
+ So that I can correct problems
6
+
7
+ Background:
8
+ Given the test server is started
9
+
10
+ Scenario: No argument
11
+ Given a successful login
12
+ Then the client successfully sends "PASV"
13
+
14
+ Scenario: After "EPSV ALL"
15
+ Given a successful login
16
+ Given the client successfully sends "EPSV ALL"
17
+ When the client sends "PASV"
18
+ Then the server sends a not allowed after epsv all error
19
+
20
+ Scenario: Not logged in
21
+ Given a successful connection
22
+ When the client sends "EPSV"
23
+ Then the server returns a not logged in error
@@ -1,4 +1,4 @@
1
- Feature: Port
1
+ Feature: PORT
2
2
 
3
3
  As a programmer
4
4
  I want good error messages
@@ -41,3 +41,9 @@ Feature: Port
41
41
  Given a successful login
42
42
  When the client sends PORT "1,2,3,4,5,256"
43
43
  Then the server returns a syntax error
44
+
45
+ Scenario: After "EPSV ALL"
46
+ Given a successful login
47
+ Given the client successfully sends "EPSV ALL"
48
+ When the client sends "PORT 1,2,3,4,4,0"
49
+ Then the server sends a not allowed after epsv all error
@@ -10,6 +10,10 @@ Given /^the test server has TLS mode "(\w+)"$/ do |mode|
10
10
  server.tls = mode.to_sym
11
11
  end
12
12
 
13
+ Given(/^the test server is bound to "(.*?)"$/) do |ip_address|
14
+ server.interface = ip_address
15
+ end
16
+
13
17
  Given /^the test server has logging (enabled|disabled)$/ do |state|
14
18
  server.logging = state == 'enabled'
15
19
  end
@@ -3,7 +3,7 @@ require 'net/ftp'
3
3
 
4
4
  When /^the( \w+)? client connects(?: with (\w+) TLS)?$/ do
5
5
  |client_name, tls_mode|
6
- tls_mode ||= :off
6
+ tls_mode ||= 'off'
7
7
  client(client_name).tls_mode = tls_mode.to_sym
8
8
  client(client_name).start
9
9
  client(client_name).connect(server.host, server.port)
@@ -105,3 +105,11 @@ end
105
105
  Then /^the server returns an already exists error$/ do
106
106
  step 'the server returns a "550 Already exists" error'
107
107
  end
108
+
109
+ Then /^the server returns a network protocol not supported error$/ do
110
+ step 'the server returns a "522 Network protocol" error'
111
+ end
112
+
113
+ Then /^the server sends a not allowed after epsv all error$/ do
114
+ step 'the server returns a "501 Not allowed after EPSV ALL" error'
115
+ end
@@ -219,6 +219,7 @@ class TestServer
219
219
  def_delegator :@server, :'auth_level'
220
220
  def_delegator :@server, :'auth_level='
221
221
  def_delegator :@server, :'failed_login_delay='
222
+ def_delegator :@server, :'interface='
222
223
  def_delegator :@server, :'max_connections='
223
224
  def_delegator :@server, :'max_connections_per_ip='
224
225
  def_delegator :@server, :'max_failed_logins='
@@ -250,7 +251,7 @@ class TestServer
250
251
  end
251
252
 
252
253
  def host
253
- 'localhost'
254
+ @server.interface
254
255
  end
255
256
 
256
257
  def user
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.8.0"
8
+ s.version = "0.9.0"
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-09-15"
12
+ s.date = "2013-10-01"
13
13
  s.description = "ftpd is a pure Ruby FTP server library. It supports implicit and explicit TLS, passive and active mode, and is unconditionally compliant per [RFC-1123][1]. 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 = [
@@ -29,6 +29,7 @@ Gem::Specification.new do |s|
29
29
  "doc/references.md",
30
30
  "doc/rfc-compliance.md",
31
31
  "examples/example.rb",
32
+ "examples/example_spec.rb",
32
33
  "examples/hello_world.rb",
33
34
  "features/example/eplf.feature",
34
35
  "features/example/example.feature",
@@ -44,9 +45,12 @@ Gem::Specification.new do |s|
44
45
  "features/ftp_server/delete.feature",
45
46
  "features/ftp_server/directory_navigation.feature",
46
47
  "features/ftp_server/disconnect_after_failed_logins.feature",
48
+ "features/ftp_server/eprt.feature",
49
+ "features/ftp_server/epsv.feature",
47
50
  "features/ftp_server/features.feature",
48
51
  "features/ftp_server/file_structure.feature",
49
52
  "features/ftp_server/get.feature",
53
+ "features/ftp_server/get_ipv6.feature",
50
54
  "features/ftp_server/get_tls.feature",
51
55
  "features/ftp_server/help.feature",
52
56
  "features/ftp_server/implicit_tls.feature",
@@ -64,6 +68,7 @@ Gem::Specification.new do |s|
64
68
  "features/ftp_server/name_list_tls.feature",
65
69
  "features/ftp_server/noop.feature",
66
70
  "features/ftp_server/options.feature",
71
+ "features/ftp_server/pasv.feature",
67
72
  "features/ftp_server/port.feature",
68
73
  "features/ftp_server/put.feature",
69
74
  "features/ftp_server/put_tls.feature",
@@ -144,7 +149,9 @@ Gem::Specification.new do |s|
144
149
  "lib/ftpd/insecure_certificate.rb",
145
150
  "lib/ftpd/list_format/eplf.rb",
146
151
  "lib/ftpd/list_format/ls.rb",
152
+ "lib/ftpd/list_path.rb",
147
153
  "lib/ftpd/null_logger.rb",
154
+ "lib/ftpd/protocols.rb",
148
155
  "lib/ftpd/read_only_disk_file_system.rb",
149
156
  "lib/ftpd/server.rb",
150
157
  "lib/ftpd/session.rb",
@@ -167,7 +174,9 @@ Gem::Specification.new do |s|
167
174
  "spec/file_system_error_translator_spec.rb",
168
175
  "spec/list_format/eplf_spec.rb",
169
176
  "spec/list_format/ls_spec.rb",
177
+ "spec/list_path_spec.rb",
170
178
  "spec/null_logger_spec.rb",
179
+ "spec/protocols_spec.rb",
171
180
  "spec/spec_helper.rb",
172
181
  "spec/telnet_spec.rb",
173
182
  "spec/translate_exceptions_spec.rb"
@@ -175,17 +184,17 @@ Gem::Specification.new do |s|
175
184
  s.homepage = "http://github.com/wconrad/ftpd"
176
185
  s.licenses = ["MIT"]
177
186
  s.require_paths = ["lib"]
178
- s.rubygems_version = "1.8.25"
187
+ s.rubygems_version = "2.0.0"
179
188
  s.summary = "Pure Ruby FTP server library"
180
189
 
181
190
  if s.respond_to? :specification_version then
182
- s.specification_version = 3
191
+ s.specification_version = 4
183
192
 
184
193
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
185
194
  s.add_runtime_dependency(%q<memoizer>, ["~> 1.0.1"])
186
195
  s.add_development_dependency(%q<cucumber>, [">= 0"])
187
196
  s.add_development_dependency(%q<double-bag-ftps>, [">= 0"])
188
- s.add_development_dependency(%q<jeweler>, ["= 1.8.4"])
197
+ s.add_development_dependency(%q<jeweler>, [">= 0"])
189
198
  s.add_development_dependency(%q<rake>, [">= 0"])
190
199
  s.add_development_dependency(%q<redcarpet>, [">= 0"])
191
200
  s.add_development_dependency(%q<rspec>, [">= 0"])
@@ -195,7 +204,7 @@ Gem::Specification.new do |s|
195
204
  s.add_dependency(%q<memoizer>, ["~> 1.0.1"])
196
205
  s.add_dependency(%q<cucumber>, [">= 0"])
197
206
  s.add_dependency(%q<double-bag-ftps>, [">= 0"])
198
- s.add_dependency(%q<jeweler>, ["= 1.8.4"])
207
+ s.add_dependency(%q<jeweler>, [">= 0"])
199
208
  s.add_dependency(%q<rake>, [">= 0"])
200
209
  s.add_dependency(%q<redcarpet>, [">= 0"])
201
210
  s.add_dependency(%q<rspec>, [">= 0"])
@@ -206,7 +215,7 @@ Gem::Specification.new do |s|
206
215
  s.add_dependency(%q<memoizer>, ["~> 1.0.1"])
207
216
  s.add_dependency(%q<cucumber>, [">= 0"])
208
217
  s.add_dependency(%q<double-bag-ftps>, [">= 0"])
209
- s.add_dependency(%q<jeweler>, ["= 1.8.4"])
218
+ s.add_dependency(%q<jeweler>, [">= 0"])
210
219
  s.add_dependency(%q<rake>, [">= 0"])
211
220
  s.add_dependency(%q<redcarpet>, [">= 0"])
212
221
  s.add_dependency(%q<rspec>, [">= 0"])
data/lib/ftpd.rb CHANGED
@@ -25,7 +25,9 @@ module Ftpd
25
25
  autoload :FileSystemMethodMissing, 'ftpd/file_system_method_missing'
26
26
  autoload :FtpServer, 'ftpd/ftp_server'
27
27
  autoload :InsecureCertificate, 'ftpd/insecure_certificate'
28
+ autoload :ListPath, 'ftpd/list_path'
28
29
  autoload :NullLogger, 'ftpd/null_logger'
30
+ autoload :Protocols, 'ftpd/protocols'
29
31
  autoload :ReadOnlyDiskFileSystem, 'ftpd/read_only_disk_file_system'
30
32
  autoload :Server, 'ftpd/server'
31
33
  autoload :Session, 'ftpd/session'
@@ -0,0 +1,28 @@
1
+ module Ftpd
2
+
3
+ # Functions for manipulating LIST and NLST arguments
4
+
5
+ module ListPath
6
+
7
+ # Turn the argument to LIST/NLST into a path
8
+ #
9
+ # @param argument [String] The argument, or nil if not present
10
+ # @return [String] The path
11
+ #
12
+ # Although compliant with the spec, this function does not do
13
+ # these things that traditional Unix FTP servers do:
14
+ #
15
+ # * Allow multiple paths
16
+ # * Handle switches such as "-a"
17
+ #
18
+ # See: http://cr.yp.to/ftp/list.html sections "LIST parameters"
19
+ # and "LIST wildcards"
20
+
21
+ def list_path(argument)
22
+ argument ||= '.'
23
+ argument = '' if argument =~ /^-/
24
+ argument
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,60 @@
1
+ module Ftpd
2
+
3
+ # With the commands EPORT and EPSV, the client sends a protocol code
4
+ # to indicate whether it wants an IPV4 or an IPV6 connection. This
5
+ # class contains functions related to that protocol code.
6
+
7
+ class Protocols
8
+
9
+ module Codes
10
+ IPV4 = 1
11
+ IPV6 = 2
12
+ end
13
+ include Codes
14
+
15
+ # @param socket [TCPSocket, OpenSSL::SSL::SSLSocket] The socket.
16
+ # It doesn't matter whether it's the server socket (the one on
17
+ # which #accept is called), or the socket returned by #accept.
18
+
19
+ def initialize(socket)
20
+ @socket = socket
21
+ end
22
+
23
+ # Can the socket support a connection in the indicated protocol?
24
+ #
25
+ # @param protocol_code [Integer] protocol code
26
+
27
+ def supports_protocol?(protocol_code)
28
+ protocol_codes.include?(protocol_code)
29
+ end
30
+
31
+ # What protocol codes does the socket support?
32
+ #
33
+ # @return [Array<Integer>] List of protocol codes
34
+
35
+ def protocol_codes
36
+ [
37
+ (IPV4 if supports_ipv4?),
38
+ (IPV6 if supports_ipv6?),
39
+ ].compact
40
+ end
41
+
42
+ private
43
+
44
+ def supports_ipv4?
45
+ @socket.local_address.ipv4? || ipv6_dual_stack?
46
+ end
47
+
48
+ def supports_ipv6?
49
+ @socket.local_address.ipv6?
50
+ end
51
+
52
+ def ipv6_dual_stack?
53
+ v6only = @socket.getsockopt(Socket::IPPROTO_IPV6,
54
+ Socket::IPV6_V6ONLY).unpack('i')
55
+ v6only == [0]
56
+ end
57
+
58
+ end
59
+
60
+ end
data/lib/ftpd/session.rb CHANGED
@@ -4,6 +4,7 @@ module Ftpd
4
4
  class Session
5
5
 
6
6
  include Error
7
+ include ListPath
7
8
 
8
9
  def initialize(opts)
9
10
  @failed_login_delay = opts[:failed_login_delay]
@@ -25,6 +26,7 @@ module Ftpd
25
26
  end
26
27
  @command_sequence_checker = init_command_sequence_checker
27
28
  set_socket_options
29
+ @protocols = Protocols.new(@socket)
28
30
  initialize_session
29
31
  end
30
32
 
@@ -108,6 +110,7 @@ module Ftpd
108
110
 
109
111
  def cmd_port(argument)
110
112
  ensure_logged_in
113
+ ensure_not_epsv_all
111
114
  pieces = argument.split(/,/)
112
115
  syntax_error unless pieces.size == 6
113
116
  pieces.collect! do |s|
@@ -118,11 +121,7 @@ module Ftpd
118
121
  end
119
122
  hostname = pieces[0..3].join('.')
120
123
  port = pieces[4] << 8 | pieces[5]
121
- if port < 1024 && !@allow_low_data_ports
122
- error "504 Command not implemented for that parameter"
123
- end
124
- @data_hostname = hostname
125
- @data_port = port
124
+ set_active_mode_address hostname, port
126
125
  reply "200 PORT command successful"
127
126
  end
128
127
 
@@ -201,8 +200,7 @@ module Ftpd
201
200
  ensure_logged_in
202
201
  ensure_file_system_supports :dir
203
202
  ensure_file_system_supports :file_info
204
- path = argument
205
- path ||= '.'
203
+ path = list_path(argument)
206
204
  path = File.expand_path(path, @name_prefix)
207
205
  transmit_file(list(path), 'A')
208
206
  end
@@ -212,8 +210,7 @@ module Ftpd
212
210
  close_data_server_socket_when_done do
213
211
  ensure_logged_in
214
212
  ensure_file_system_supports :dir
215
- path = argument
216
- path ||= '.'
213
+ path = list_path(argument)
217
214
  path = File.expand_path(path, @name_prefix)
218
215
  transmit_file(name_list(path), 'A')
219
216
  end
@@ -265,6 +262,7 @@ module Ftpd
265
262
 
266
263
  def cmd_pasv(argument)
267
264
  ensure_logged_in
265
+ ensure_not_epsv_all
268
266
  if @data_server
269
267
  reply "200 Already in passive mode"
270
268
  else
@@ -360,6 +358,12 @@ module Ftpd
360
358
  end
361
359
  end
362
360
 
361
+ def ensure_not_epsv_all
362
+ if @epsv_all
363
+ error "501 Not allowed after EPSV ALL"
364
+ end
365
+ end
366
+
363
367
  def tls_enabled?
364
368
  @tls != :off
365
369
  end
@@ -490,6 +494,49 @@ module Ftpd
490
494
  error '501 Unsupported option'
491
495
  end
492
496
 
497
+ def cmd_eprt(argument)
498
+ ensure_logged_in
499
+ ensure_not_epsv_all
500
+ delim = argument[0..0]
501
+ parts = argument.split(delim)[1..-1]
502
+ syntax_error unless parts.size == 3
503
+ protocol_code, address, port = *parts
504
+ protocol_code = protocol_code.to_i
505
+ ensure_protocol_supported protocol_code
506
+ port = port.to_i
507
+ set_active_mode_address address, port
508
+ reply "200 EPRT command successful"
509
+ end
510
+
511
+ def ensure_protocol_supported(protocol_code)
512
+ unless @protocols.supports_protocol?(protocol_code)
513
+ protocol_list = @protocols.protocol_codes.join(',')
514
+ error("522 Network protocol #{protocol_code} not supported, "\
515
+ "use (#{protocol_list})")
516
+ end
517
+ end
518
+
519
+ def cmd_epsv(argument)
520
+ ensure_logged_in
521
+ if @data_server
522
+ reply "200 Already in passive mode"
523
+ else
524
+ if argument == 'ALL'
525
+ @epsv_all = true
526
+ reply "220 EPSV now required for port setup"
527
+ else
528
+ protocol_code = argument && argument.to_i
529
+ if protocol_code
530
+ ensure_protocol_supported protocol_code
531
+ end
532
+ interface = @socket.addr[3]
533
+ @data_server = TCPServer.new(interface, 0)
534
+ port = @data_server.addr[1]
535
+ reply "229 Entering extended passive mode (|||#{port}|)"
536
+ end
537
+ end
538
+ end
539
+
493
540
  unimplemented :abor
494
541
  unimplemented :rein
495
542
  unimplemented :rest
@@ -498,7 +545,8 @@ module Ftpd
498
545
 
499
546
  def extensions
500
547
  [
501
- (TLS_EXTENSIONS if tls_enabled?)
548
+ (TLS_EXTENSIONS if tls_enabled?),
549
+ IPV6_EXTENSIONS,
502
550
  ].flatten.compact
503
551
  end
504
552
 
@@ -508,6 +556,11 @@ module Ftpd
508
556
  'PROT'
509
557
  ]
510
558
 
559
+ IPV6_EXTENSIONS = [
560
+ 'EPRT',
561
+ 'EPSV',
562
+ ]
563
+
511
564
  def supported_commands
512
565
  private_methods.map do |method|
513
566
  method.to_s[/^cmd_(\w+)$/, 1]
@@ -823,6 +876,18 @@ module Ftpd
823
876
  end
824
877
  end
825
878
 
879
+ def set_data_address(n)
880
+
881
+ end
882
+
883
+ def set_active_mode_address(address, port)
884
+ if port > 0xffff || port < 1024 && !@allow_low_data_ports
885
+ error "504 Command not implemented for that parameter"
886
+ end
887
+ @data_hostname = address
888
+ @data_port = port
889
+ end
890
+
826
891
  def initialize_session
827
892
  @logged_in = false
828
893
  @data_type = 'A'
@@ -833,6 +898,7 @@ module Ftpd
833
898
  @data_hostname = nil
834
899
  @data_port = nil
835
900
  @protection_buffer_size_set = 0
901
+ @epsv_all = false
836
902
  close_data_server_socket
837
903
  reset_failed_auths
838
904
  end