ftpd 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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