testftpd 0.1.1

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.
data/bin/testftpd ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/testftpd'
4
+ require 'optparse'
5
+
6
+ options = { :root_dir => File.expand_path('.'), :port => 21 }
7
+ OptionParser.new do |opts|
8
+ opts.on(
9
+ '-r', '--root <ROOT_DIR>',
10
+ 'Serve files from <ROOT_DIR>, defaults to the current directory.'
11
+ ) do |root_dir|
12
+ options[:root_dir] = root_dir
13
+ end
14
+
15
+ opts.on(
16
+ '-p', '--port <FTP_PORT>',
17
+ 'Listen for connections on <FTP_PORT>, defaults to 21.'
18
+ ) do |ftp_port|
19
+ options[:port] = ftp_port
20
+ end
21
+ end.parse!
22
+
23
+ puts 'Starting TestFTPd::Server...'
24
+ puts "Root Directory: '#{options[:root_dir]}'"
25
+ puts " TCP Port: #{options[:port]}"
26
+
27
+ begin
28
+ TestFtpd::Server.new(options).start
29
+ rescue Exception => e
30
+ puts 'There was a problem starting the FTP process...'
31
+ puts e.message
32
+ exit!(255)
33
+ end
34
+
35
+ begin
36
+ sleep 0.1 while true
37
+ rescue Interrupt
38
+ puts 'Interrupted.'
39
+ end
@@ -0,0 +1,81 @@
1
+ module TestFtpd
2
+
3
+ class FileSystemProvider
4
+ attr_reader :path, :ftp_name, :ftp_size, :ftp_date
5
+
6
+ def initialize(path)
7
+ @path = path
8
+ @ftp_name = path.split('/').last
9
+ @ftp_name = '/' unless @ftp_name
10
+ @ftp_dir = File.directory?(path)
11
+ @ftp_size = File.size?(path)
12
+ @ftp_size = 0 unless @ftp_size
13
+ @ftp_date = Time.now
14
+ @ftp_date = File.mtime(path) if File.exists?(path)
15
+ end
16
+
17
+ def directory?
18
+ @ftp_dir
19
+ end
20
+
21
+ def ftp_parent(root = nil)
22
+ return root if root && root.path == path
23
+ path_parts = path.split('/')
24
+ return nil unless path_parts.pop
25
+ return nil if path_parts.size <= 1
26
+ FileSystemProvider.new(path_parts.join('/'))
27
+ end
28
+
29
+ def ftp_list(filter = nil)
30
+ if filter
31
+ if File.directory?(File.join(path, filter))
32
+ entries = Dir.entries(File.join(path, filter)).map { |name| File.join(filter, name) }
33
+ else
34
+ entries = Dir.glob(File.join(path, filter)).map { |name| File.basename(name) }
35
+ end
36
+ else
37
+ entries = Dir.entries(path)
38
+ end
39
+ entries = entries.reject { |name| %w{. ..}.include?(File.basename(name)) }
40
+ entries.map do |name|
41
+ FileSystemProvider.new(File.join(path, name))
42
+ end
43
+ end
44
+
45
+ def ftp_create(name, dir = false)
46
+ return FileSystemProvider.new(path + '/' + name) unless dir
47
+ Dir.mkdir(path + '/' + name)
48
+ FileSystemProvider.new(path + '/' + name)
49
+ rescue
50
+ return false
51
+ end
52
+
53
+ def ftp_retrieve(output)
54
+ File.open(path, 'r') { |io| output << io.read }
55
+ end
56
+
57
+ def ftp_store(input)
58
+ return false unless File.open(path, 'w') do |f|
59
+ f.write input.read
60
+ end
61
+ @ftp_size = File.size?(path)
62
+ @ftp_date = File.mtime(path) if File.exists?(path)
63
+ end
64
+
65
+ def ftp_rename(to_name)
66
+ to_path = File.join(File.dirname(path), to_name)
67
+ FileUtils.mv(path, to_path)
68
+ true
69
+ end
70
+
71
+ def ftp_delete(dir = false)
72
+ if dir
73
+ FileUtils.remove_dir(path)
74
+ else
75
+ FileUtils.remove_file(path)
76
+ end
77
+ true
78
+ end
79
+ end
80
+
81
+ end
@@ -0,0 +1,44 @@
1
+ require_relative '../../vendor/dyn-ftp-serv/dynftp_server'
2
+ require_relative 'file_system_provider'
3
+
4
+ require 'timeout'
5
+
6
+ Thread.abort_on_exception = true
7
+
8
+ module TestFtpd
9
+
10
+ class Server < DynFTPServer
11
+ def initialize(config = {})
12
+ config.merge!(:root => FileSystemProvider.new(config[:root_dir]))
13
+ @ftp_thread = nil
14
+ super(config)
15
+ end
16
+
17
+ def running?
18
+ @ftp_thread && @ftp_thread.alive?
19
+ end
20
+
21
+ def start(timeout = 2)
22
+ Timeout.timeout(timeout) do
23
+ return if @ftp_thread
24
+ @ftp_thread = Thread.new { mainloop }
25
+ sleep 0.1 until running?
26
+ end
27
+ rescue TimeoutError
28
+ raise TimeoutError.new('TestFtpd::Server timeout before start succeeded.')
29
+ end
30
+
31
+ def shutdown(timeout = 2)
32
+ Timeout.timeout(timeout) do
33
+ @server.close unless @server.closed?
34
+ return if @ftp_thread.nil?
35
+ @ftp_thread.kill
36
+ sleep 0.1 while running?
37
+ @ftp_thread = nil
38
+ end
39
+ rescue TimeoutError
40
+ raise TimeoutError.new('TestFtpd::Server timeout before shutdown succeeded.')
41
+ end
42
+ end
43
+
44
+ end
@@ -0,0 +1,3 @@
1
+ module TestFtpd
2
+ VERSION = '0.1.1'
3
+ end
data/lib/testftpd.rb ADDED
@@ -0,0 +1,6 @@
1
+ require_relative 'testftpd/file_system_provider'
2
+ require_relative 'testftpd/server'
3
+
4
+ module TestFtpd
5
+
6
+ end
@@ -0,0 +1 @@
1
+ a file in a sub folder
@@ -0,0 +1 @@
1
+ a file in a sub folder
@@ -0,0 +1 @@
1
+ welcome to my FTP server
@@ -0,0 +1 @@
1
+ this file will be deleted
@@ -0,0 +1 @@
1
+ this file will be renamed
@@ -0,0 +1 @@
1
+ test file
@@ -0,0 +1,174 @@
1
+ require 'spec_helper'
2
+
3
+ require 'net/ftp'
4
+ require 'tmpdir'
5
+
6
+ require 'testftpd'
7
+
8
+ describe TestFtpd do
9
+ def copy_ftproot(target_path)
10
+ FileUtils.cp_r(File.join(APP_PATH, 'spec/fixtures/ftproot/.'), target_path)
11
+ end
12
+
13
+ let(:ftp_port) { 21212 }
14
+ let(:ftp_root) { Dir.mktmpdir('test_ftp_root') }
15
+
16
+ subject { TestFtpd::Server.new(port: ftp_port, root_dir: ftp_root) }
17
+
18
+ before do
19
+ subject.start
20
+ copy_ftproot(ftp_root)
21
+ end
22
+
23
+ after do
24
+ subject.shutdown
25
+ FileUtils.rm_rf(ftp_root)
26
+ end
27
+
28
+ context 'before authenticating' do
29
+ before :each do
30
+ @ftp = Net::FTP.new
31
+ @ftp.connect('127.0.0.1', ftp_port)
32
+ end
33
+
34
+ after :each do
35
+ @ftp.close
36
+ end
37
+
38
+ it 'sends unauthorised message' do
39
+ expect { @ftp.list }.to raise_error(Net::FTPPermError, /Not logged in/)
40
+ end
41
+ end
42
+
43
+ context 'after authenticating' do
44
+ before :each do
45
+ @ftp = Net::FTP.new
46
+ @ftp.connect('127.0.0.1', ftp_port)
47
+ @ftp.login('username', 'password')
48
+ end
49
+
50
+ after :each do
51
+ @ftp.close
52
+ end
53
+
54
+ context 'when listing files' do
55
+ it 'can list remote files' do
56
+ files = @ftp.list
57
+ files.count.should eq(5)
58
+ files.any? { |file| file =~ /test_file/ }.should be_true
59
+ end
60
+
61
+ it 'can list a specific file' do
62
+ files = @ftp.list('test_file')
63
+ files.count.should eq(1)
64
+ files[0].should =~ /test_file/
65
+ end
66
+
67
+ it 'can list a specific file in a subfolder' do
68
+ files = @ftp.list('subfolder/test_file1')
69
+ files.count.should eq(1)
70
+ files[0].should =~ /test_file1/
71
+ end
72
+
73
+ it 'can list a sub folder' do
74
+ files = @ftp.list('subfolder')
75
+ files.count.should eq(3)
76
+ files.any? { |file| file =~ /test_file1/ }.should be_true
77
+ end
78
+ end
79
+
80
+ it 'can query the current remote directory' do
81
+ @ftp.pwd.should eq('/')
82
+ end
83
+
84
+ it 'can nlst remote files' do
85
+ pending 'nlst command not implemented yet'
86
+ end
87
+
88
+ it 'can query modified time of remote file' do
89
+ modified_time = File.mtime(File.join(ftp_root, 'test_file')).strftime('%Y%m%d%H%M%S')
90
+ @ftp.mdtm('test_file').should eql(modified_time)
91
+ end
92
+
93
+ it 'can query size of remote file' do
94
+ size = File.size(File.join(ftp_root, 'test_file'))
95
+ @ftp.size('test_file').should eq(size)
96
+ end
97
+
98
+ context 'when downloading a file' do
99
+ it 'can use binary mode' do
100
+ filename = 'test_file'
101
+ local_filepath = File.join(Dir.tmpdir, filename)
102
+ @ftp.binary = true
103
+ @ftp.get('test_file', local_filepath)
104
+ File.exists?(local_filepath).should be_true
105
+ end
106
+
107
+ it 'can use text mode' do
108
+ filename = 'test_file'
109
+ local_filepath = File.join(Dir.tmpdir, filename)
110
+ @ftp.binary = false
111
+ @ftp.get('test_file', local_filepath)
112
+ File.exists?(local_filepath).should be_true
113
+ end
114
+
115
+ it 'can use passive mode' do
116
+ filename = 'test_file'
117
+ local_filepath = File.join(Dir.tmpdir, filename)
118
+ @ftp.passive = true
119
+ @ftp.get('test_file', local_filepath)
120
+ File.exists?(local_filepath).should be_true
121
+ end
122
+ end
123
+
124
+ context 'when changing folders' do
125
+ it 'can change into sub folders' do
126
+ @ftp.chdir('subfolder')
127
+ @ftp.pwd.should eq(File.join(ftp_root, 'subfolder'))
128
+ end
129
+
130
+ it 'can change into parent folder' do
131
+ @ftp.chdir('subfolder')
132
+ @ftp.chdir('..')
133
+ @ftp.pwd.should eq(ftp_root)
134
+ end
135
+
136
+ it 'does not allow folders outside the ftp root folder' do
137
+ @ftp.chdir('..')
138
+ @ftp.pwd.should eq('/')
139
+ end
140
+ end
141
+
142
+ it 'can create a remote folder' do
143
+ @ftp.mkdir('new_subfolder')
144
+ Dir.exists?(File.join(ftp_root, 'new_subfolder')).should be_true
145
+ end
146
+
147
+ it 'can upload a file' do
148
+ filename = 'test_file_to_upload'
149
+ local_filepath = File.join(APP_PATH, 'spec/fixtures', filename)
150
+ @ftp.put(local_filepath, filename)
151
+ File.exists?(File.join(ftp_root, filename)).should be_true
152
+ end
153
+
154
+ it 'can delete a remote file' do
155
+ @ftp.delete('test_file_to_delete')
156
+ File.exists?(File.join(ftp_root, 'test_file_to_delete')).should be_false
157
+ end
158
+
159
+ it 'can rename a remote file' do
160
+ @ftp.rename('test_file_to_rename', 'test_file_renamed')
161
+ File.exists?(File.join(ftp_root, 'test_file_renamed')).should be_true
162
+ end
163
+
164
+ it 'can delete a remote folder' do
165
+ @ftp.rmdir('subfolder_to_delete')
166
+ File.exists?(File.join(ftp_root, 'subfolder_to_delete')).should be_false
167
+ end
168
+
169
+ it 'responds correctly to unrecognized commands' do
170
+ expect { @ftp.sendcmd('asdf') }.to raise_error(Net::FTPPermError, /command unrecognized/)
171
+ end
172
+ end
173
+
174
+ end
@@ -0,0 +1,20 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'rspec'
4
+
5
+ APP_PATH = File.expand_path(File.join(File.dirname(__FILE__),'..'))
6
+
7
+ if ENV['COVERAGE']
8
+ require 'simplecov'
9
+ SimpleCov.root(APP_PATH)
10
+ end
11
+
12
+ RSpec.configure do |config|
13
+ config.before(:suite) do
14
+
15
+ end
16
+
17
+ config.before(:each) do
18
+
19
+ end
20
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ require 'testftpd'
4
+
5
+ describe TestFtpd::Server do
6
+
7
+ let(:ftp_port) { 21212 }
8
+ let(:ftp_root) { APP_PATH }
9
+
10
+ subject { TestFtpd::Server.new(port: ftp_port, root_dir: ftp_root) }
11
+
12
+ after :each do
13
+ subject.shutdown
14
+ end
15
+
16
+ describe '.start(timeout = 2)' do
17
+ it 'raises exception if ftp process does not start within timeout' do
18
+ Thread.stub(:new) { sleep 0.5 }
19
+ expect { subject.start(0.1) }.to raise_error(TimeoutError)
20
+ end
21
+ end
22
+
23
+ describe '.start(timeout = 2)' do
24
+ it 'raises exception if ftp process does not shutdown within timeout' do
25
+ subject.start
26
+ expect { subject.shutdown(0.01) }.to raise_error(TimeoutError)
27
+ end
28
+ end
29
+
30
+ end
@@ -0,0 +1,15 @@
1
+ 2013-03-09 ftp-dyn-serv 0.1.3 (Christian Schulze)
2
+ Added rename file commands (rnfr/rnto)
3
+ Added remove directory command (rmd)
4
+ minor code improvements
5
+
6
+ 2007-11-11 ftp-dyn-serv 0.1.2
7
+ Added ip masquerading feature.
8
+ Added settings for minimum and maximum passive port numbers.
9
+ Fixed issue with authentication.
10
+
11
+ 2007-11-09 ftp-dyn-serv 0.1.1
12
+ Updated rdoc comments.
13
+
14
+ 2007-11-09 ftp-dyn-serv 0.1.0
15
+ Initial release.
@@ -0,0 +1,22 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2007 Rubtsov Vitaly
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
22
+
@@ -0,0 +1,478 @@
1
+ # :title:Dynamic FTP server in pure Ruby (dyn-ftp-serv)
2
+ # Version:: 0.1.2
3
+ # Author:: Rubtsov Vitaly (vlrubtsov *at* gmail.com)
4
+ # License:: MIT license
5
+ # Website:: http://rubyforge.org/projects/dyn-ftp-serv/
6
+ #
7
+ # This ftp server implementation features an ability to host any content you want.
8
+ # You are not limited to hosting files and directories via FTP interface.
9
+ # With dyn-ftp-serv you are able to represent any hierarchy under the guise of
10
+ # standard files and directories. You will be able to download and upload files
11
+ # and browse dynamic directories.
12
+ # To work with dyn-ftp-serv you must have an object responding to special ftp messages
13
+ # that will represent the ftp content. You can create a new object or extend the
14
+ # existing one with special messages.
15
+ # There are two sets of messages to be handled: directory messages and file messages.
16
+ # Directory messages are:
17
+ # [+directory?+] must return true.
18
+ # [+ftp_name+] must return the name of a directory
19
+ # [+ftp_size+] must return size for directory
20
+ # [+ftp_date+] must return the date for a directory
21
+ # [+ftp_parent+] must return parent object or nil if root
22
+ # [+ftp_list+] must return an array of ftp objects
23
+ # [<tt>ftp_create(name, dir = false)</tt>]
24
+ # must return a new object created with the 'name' given.
25
+ # It can be file (dir=false) or a directory (dir=true). It can return nil if creating is
26
+ # forbidden.
27
+ # [+ftp_delete+] directory deletion request. must return true on success, and false on failure.
28
+ # File messages are:
29
+ # [+directory?+] must return false
30
+ # [+ftp_name+] must return the name of a file
31
+ # [+ftp_size+] must return filesize
32
+ # [+ftp_date+] must return filedate
33
+ # [<tt>ftp_retrieve(output)</tt>] streams file contents via output socket.
34
+ # [<tt>ftp_store(input)</tt>] writes file contents reading from a socket
35
+ # [+ftp_delete+] file deletion request. must return true on success, and false on failure.
36
+ #
37
+ # Please, see an example in 'examples' folder showing an implementation of standard file system
38
+ # ftp server.
39
+
40
+ require 'socket'
41
+
42
+ class DynFTPServer
43
+
44
+ attr_reader :config
45
+
46
+ # Class to instantiate if logger is not given.
47
+ class DummyLogger
48
+ def method_missing(method_name, *args, &block); end
49
+ end
50
+
51
+ # Pass a hash containing options.
52
+ # [<tt>:host</tt>] Local bind address. Default is <em>'0.0.0.0'</em>.
53
+ # [<tt>:port</tt>] Port to listen. Default is <em>21</em>.
54
+ # [<tt>:masquerade_ip</tt>] IP masquerade for passive connections. Use this settings if you are behind a firewall and set it to the external ip address.
55
+ # [<tt>:pasv_min_port</tt>] Minimum port num for passive connections.
56
+ # [<tt>:pasv_max_port</tt>] Maximum port num for passive connections.
57
+ # [<tt>:root</tt>] Root ftp object.
58
+ # [<tt>:authentication</tt>] Function used to check users login information.
59
+ # [<tt>:logger</tt>] Logger object.
60
+ def initialize(config)
61
+ @config = defaults.merge(config)
62
+ raise(ArgumentError, 'Root object must not be null.') unless @config[:root]
63
+ @server = TCPServer.new(@config[:host], @config[:port])
64
+ end
65
+
66
+ def defaults
67
+ {
68
+ host: '',
69
+ port: 21,
70
+ masquerade_ip: nil,
71
+ pasv_min_port: 1024,
72
+ pasv_max_port: 65535,
73
+ root: nil,
74
+ authentication: ->(user, pass) { return true },
75
+ logger: nil
76
+ }
77
+ end
78
+
79
+ def mainloop
80
+ threads = []
81
+ log.debug 'Waiting for connection'
82
+ while (session = @server.accept)
83
+ log.debug "Accepted connection from #{session.addr.join(', ')}"
84
+ threads << Thread.new(session) do |s|
85
+ thread[:socket] = s
86
+ client_loop
87
+ end
88
+ end
89
+ threads.each {|t| t.join }
90
+ end
91
+
92
+ private
93
+
94
+ def log
95
+ return config[:logger] if config[:logger]
96
+ DummyLogger.new
97
+ end
98
+
99
+ def not_implemented
100
+ status(500)
101
+ end
102
+
103
+ def not_authorized
104
+ status(530)
105
+ end
106
+
107
+ def status(code, description = nil)
108
+ unless description.nil?
109
+ message = "#{code.to_s} #{description}"
110
+ log.debug "Response: #{message}"
111
+ thread[:socket].puts "#{message}\r\n"
112
+ return
113
+ end
114
+ case code.to_i
115
+ when 125
116
+ status(code, 'Data connection already open; transfer starting.')
117
+ when 150
118
+ status(code, 'File status okay; about to open data connection.')
119
+ when 200
120
+ status(code, 'Command okey.')
121
+ when 226
122
+ status(code, 'Closing data connection.')
123
+ when 230
124
+ status(code, 'User logged in, proceed.')
125
+ when 250
126
+ status(code, 'Requested file action okay, completed.')
127
+ when 331
128
+ status(code, 'User name okay, need password.')
129
+ when 350
130
+ status(code, 'RNFR completed, continue with RNTO.')
131
+ when 425
132
+ status(code, "Can't open data connection.")
133
+ when 500
134
+ status(code, 'Syntax error, command unrecognized.')
135
+ when 502
136
+ status(code, 'Command not implemented.')
137
+ when 530
138
+ status(code, 'Not logged in.')
139
+ when 550
140
+ status(code, 'Requested action not taken.')
141
+ else
142
+ status(code, '')
143
+ end
144
+ end
145
+
146
+ def data_connection(&block)
147
+ client_socket = nil
148
+ if thread[:passive]
149
+ unless IO.select([thread[:data_socket]], nil, nil, 60000)
150
+ status(425)
151
+ return false
152
+ end
153
+ client_socket = thread[:data_socket].accept
154
+ status(150)
155
+ else
156
+ client_socket = thread[:data_socket]
157
+ status(125)
158
+ end
159
+ yield(client_socket)
160
+ return true
161
+ ensure
162
+ client_socket.close if client_socket && thread[:passive]
163
+ client_socket = nil
164
+ end
165
+
166
+ def passive_server
167
+ server = nil
168
+ port = config[:pasv_min_port]
169
+ while server.nil? && (port <= config[:pasv_max_port])
170
+ begin
171
+ server = TCPServer.new(config[:host], port)
172
+ rescue Errno::EADDRINUSE
173
+ log.error "#{port} is already in use. Trying next port."
174
+ end
175
+ port += 1
176
+ end
177
+ server
178
+ end
179
+
180
+ def open_object(path)
181
+ if (path[0,1] == '/') || (path.is_a?(Array) && (path[0] == ''))
182
+ dir = config[:root]
183
+ else
184
+ dir = thread[:cwd]
185
+ end
186
+ path = path.split('/') unless path.is_a?(Array)
187
+ return dir if path.empty?
188
+ last_element = path.pop
189
+ path.each do |p|
190
+ unless p == ''
191
+ dir = dir.ftp_list.detect {|d| (d.ftp_name.casecmp(p) == 0) && (d.directory?) }
192
+ return nil unless dir
193
+ end
194
+ end
195
+ dir.ftp_list.detect {|d| (d.ftp_name.casecmp(last_element) == 0) } unless last_element == ''
196
+ end
197
+
198
+ def open_path(path)
199
+ result = open_object(path)
200
+ result = nil if result && !result.directory?
201
+ result
202
+ end
203
+
204
+ def open_file(path)
205
+ result = open_object(path)
206
+ result = nil if result && result.directory?
207
+ result
208
+ end
209
+
210
+ def get_path(object)
211
+ return '/' unless object
212
+ return '/' if object == config[:root]
213
+ result = ''
214
+ while object do
215
+ result = '/' + object.ftp_name + result
216
+ object = object.ftp_parent
217
+ end
218
+ result
219
+ end
220
+
221
+ def get_quoted_path(object)
222
+ get_path(object).gsub('"', '""')
223
+ end
224
+
225
+ def thread
226
+ Thread.current
227
+ end
228
+
229
+ # Commands
230
+
231
+ def cmd_cdup(params)
232
+ thread[:cwd] = thread[:cwd].ftp_parent(config[:root])
233
+ thread[:cwd] = config[:root] unless thread[:cwd]
234
+ status(250, 'Directory successfully changed.')
235
+ end
236
+
237
+ def cmd_cwd(path)
238
+ if path == '.'
239
+ status(250, 'Directory successfully changed.')
240
+ elsif (newpath = open_path(path))
241
+ thread[:cwd] = newpath
242
+ status(250, 'Directory successfully changed.')
243
+ else
244
+ status(550, 'Failed to change directory.')
245
+ end
246
+ end
247
+
248
+ def cmd_dele(path)
249
+ if (file = open_file(path)) && file.ftp_delete
250
+ status 250
251
+ else
252
+ status(550, 'Delete operation failed.')
253
+ end
254
+ end
255
+
256
+ # def cmd_feat(params)
257
+ # thread[:socket].puts "211-Features\r\n"
258
+ # thread[:socket].puts " UTF8\r\n"
259
+ # thread[:socket].puts "211 end\r\n"
260
+ # end
261
+
262
+ def cmd_list(file_spec)
263
+ data_connection do |data_socket|
264
+ list = thread[:cwd].ftp_list(file_spec)
265
+ list.each {|file| data_socket.puts(((file.directory?) ? 'd': '-') + 'rw-rw-rw- 1 ftp ftp ' + file.ftp_size.to_s + ' ' + file.ftp_date.strftime('%b %d %H:%M') + ' ' + file.ftp_name + "\r\n") }
266
+ end
267
+ thread[:data_socket].close if thread[:data_socket]
268
+ thread[:data_socket] = nil
269
+
270
+ status(226, 'Transfer complete')
271
+ end
272
+
273
+ def cmd_mdtm(path)
274
+ file = open_file(path)
275
+ if file
276
+ status(213, file.ftp_date.strftime('%Y%m%d%H%M%S'))
277
+ else
278
+ status(550, 'Could not get modification time.')
279
+ end
280
+ end
281
+
282
+ def cmd_mkd(path)
283
+ dir = open_object(path)
284
+ if dir
285
+ status(521, 'Directory already exists')
286
+ return
287
+ end
288
+ splitted_path = path.split('/')
289
+ mkdir = splitted_path.pop
290
+ dir = open_path(splitted_path)
291
+ if dir and (newdir = dir.ftp_create(mkdir, true))
292
+ status(257, '"' + get_quoted_path(newdir) + '" directory created.')
293
+ else
294
+ status(550)
295
+ end
296
+ end
297
+
298
+ def cmd_pass(pass)
299
+ thread[:pass] = pass
300
+ if config[:authentication].call(thread[:user], thread[:pass])
301
+ thread[:authenticated] = true
302
+ status(230)
303
+ else
304
+ thread[:authenticated] = false
305
+ not_authorized
306
+ end
307
+ end
308
+
309
+ def cmd_pasv(params)
310
+ if thread[:data_socket]
311
+ thread[:data_socket].close
312
+ thread[:data_socket] = nil
313
+ end
314
+ thread[:data_socket] = passive_server
315
+ return status(425) if thread[:data_socket].nil?
316
+ thread[:passive] = true
317
+ port = thread[:data_socket].addr[1]
318
+ port_lo = port & '0x00FF'.hex
319
+ port_hi = port >> 8
320
+ ip = thread[:data_socket].addr[3]
321
+ ip = config[:masquerade_ip] if config[:masquerade_ip]
322
+ ip = ip.split('.')
323
+ status(227, "Entering Passive Mode (#{ip[0]},#{ip[1]},#{ip[2]},#{ip[3]},#{port_hi},#{port_lo})")
324
+ end
325
+
326
+ def cmd_port(ip_port)
327
+ s = ip_port.split(',')
328
+ port = s[4].to_i * 256 + s[5].to_i
329
+ host = s[0..3].join('.')
330
+ if thread[:data_socket]
331
+ thread[:data_socket].close
332
+ thread[:data_socket] = nil
333
+ end
334
+ thread[:data_socket] = TCPSocket.new(host, port)
335
+ thread[:passive] = false
336
+ status(200, "Passive connection established (#{port})")
337
+ end
338
+
339
+ def cmd_pwd(params)
340
+ status(257, "\"#{get_quoted_path(thread[:cwd])}\" is the current directory")
341
+ end
342
+
343
+ def cmd_rmd(path)
344
+ dir = open_path(path)
345
+ if dir && dir.ftp_delete(true)
346
+ status(250)
347
+ else
348
+ status(550, 'Remove directory operation failed.')
349
+ end
350
+ end
351
+
352
+ def cmd_quit(params)
353
+ status(200)
354
+ thread[:socket].close
355
+ thread[:socket] = nil
356
+ end
357
+
358
+ def cmd_retr(path)
359
+ file = open_file(path)
360
+ if file
361
+ data_connection do |data_socket|
362
+ if file.ftp_retrieve(data_socket)
363
+ status(226, 'Transfer complete')
364
+ else
365
+ status(550, 'Failed to open file.')
366
+ end
367
+ end
368
+ else
369
+ status(550, 'Failed to open file.')
370
+ end
371
+
372
+ thread[:data_socket].close if thread[:data_socket]
373
+ thread[:data_socket] = nil
374
+ end
375
+
376
+ def cmd_size(path)
377
+ file = open_file(path)
378
+ if file
379
+ status(213, file.ftp_size.to_s)
380
+ else
381
+ status(550, 'Could not get file size.')
382
+ end
383
+ end
384
+
385
+ def cmd_stor(path)
386
+ file = open_file(path)
387
+ if file
388
+ status(553, 'Could not create file.')
389
+ return
390
+ end
391
+ unless file
392
+ splitted_path = path.split('/')
393
+ filename = splitted_path.pop
394
+ dir = open_path(splitted_path)
395
+ file = dir.ftp_create(filename) if dir
396
+ end
397
+ if file
398
+ data_connection do |data_socket|
399
+ file.ftp_store(data_socket)
400
+ end
401
+ status(226, 'Transfer complete')
402
+ else
403
+ status(550, 'Failed to open file.')
404
+ end
405
+
406
+ thread[:data_socket].close if thread[:data_socket]
407
+ thread[:data_socket] = nil
408
+ end
409
+
410
+ def cmd_rnfr(from_name)
411
+ file = open_file(from_name)
412
+ if file
413
+ @rnfr = file
414
+ status(350)
415
+ else
416
+ status(550, 'Rename from operation failed.')
417
+ end
418
+ end
419
+
420
+ def cmd_rnto(to_name)
421
+ if @rnfr
422
+ @rnfr.ftp_rename(to_name)
423
+ status(250)
424
+ else
425
+ status(550, 'Rename to operation failed.')
426
+ end
427
+ end
428
+
429
+ def cmd_syst(params)
430
+ status(215, 'UNIX')
431
+ end
432
+
433
+ def cmd_type(type)
434
+ status(200, 'Type set.')
435
+ end
436
+
437
+ def cmd_user(user)
438
+ thread[:user] = user
439
+ status(331)
440
+ end
441
+
442
+ def welcome
443
+ thread[:authenticated] = false
444
+ thread[:cwd] = config[:root]
445
+ status(220, 'Microsoft FTP Server ready')
446
+ end
447
+
448
+ def client_loop
449
+ welcome
450
+ while thread[:socket]
451
+ s = thread[:socket].gets
452
+ break unless s
453
+ s.chomp!
454
+ log.debug "Request: #{s}"
455
+ params = s.split(' ', 2)
456
+ command = params.first
457
+ command.downcase! if command
458
+ message = "cmd_#{command.to_s}"
459
+ if self.respond_to?(message, true)
460
+ if %w[cmd_user cmd_pass].include?(message) || thread[:authenticated]
461
+ self.send(message, params[1])
462
+ else
463
+ not_authorized
464
+ end
465
+ else
466
+ not_implemented
467
+ end
468
+ end
469
+ #rescue
470
+ # log.error $!
471
+ ensure
472
+ thread[:socket].close if thread[:socket] && !thread[:socket].closed?
473
+ thread[:socket] = nil
474
+ thread[:data_socket].close if thread[:data_socket] && !thread[:data_socket].closed?
475
+ thread[:data_socket] = nil
476
+ end
477
+
478
+ end
@@ -0,0 +1,79 @@
1
+ require '../dynftp_server'
2
+ require 'logger'
3
+
4
+ Thread.abort_on_exception = true
5
+
6
+ class FSProvider
7
+ attr_reader :ftp_name, :ftp_size, :ftp_dir, :ftp_date
8
+
9
+ def ftp_parent
10
+ path = @path.split('/')
11
+ return nil unless path.pop
12
+ return nil if path.size <= 1
13
+ return FSProvider.new(path.join('/'))
14
+ end
15
+
16
+ def ftp_list
17
+ output = Array.new
18
+ Dir.entries(@path).sort.each do |file|
19
+ output << FSProvider.new(@path + (@path == '/'? '': '/') + file)
20
+ end
21
+ return output
22
+ end
23
+
24
+ def ftp_create(name, dir = false)
25
+ if dir
26
+ begin
27
+ Dir.mkdir(@path + '/' + name)
28
+ return FSProvider.new(@path + '/' + name)
29
+ rescue
30
+ return false
31
+ end
32
+ else
33
+ FSProvider.new(@path + '/' + name)
34
+ end
35
+
36
+ end
37
+
38
+ def ftp_retrieve(output)
39
+ output << File.new(@path, 'r').read
40
+ end
41
+
42
+ def ftp_store(input)
43
+ return false unless File.open(@path, 'w') do |f|
44
+ f.write input.read
45
+ end
46
+ @ftp_size = File.size?(@path)
47
+ @ftp_date = File.mtime(@path) if File.exists?(@path)
48
+ end
49
+
50
+ def ftp_delete()
51
+ return false
52
+ end
53
+
54
+ def initialize(path)
55
+ @path = path
56
+ @ftp_name = path.split('/').last
57
+ @ftp_name = '/' unless @ftp_name
58
+ @ftp_dir = File.directory?(path)
59
+ @ftp_size = File.size?(path)
60
+ @ftp_size = 0 unless @ftp_size
61
+ @ftp_date = Time.now
62
+ @ftp_date = File.mtime(path) if File.exists?(path)
63
+ end
64
+
65
+ end
66
+
67
+
68
+ log = Logger.new(STDOUT)
69
+ log.datetime_format = "%H:%M:%S"
70
+ log.progname = "ftpserv.rb"
71
+
72
+ root = FSProvider.new('/')
73
+ auth =
74
+ lambda do |user,pass|
75
+ return false unless user.casecmp('anonymous') == 0
76
+ return true
77
+ end
78
+ s = DynFTPServer.new(:port => 21, :root => root, :authentication => auth, :logger => log)
79
+ s.mainloop
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: testftpd
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Christian Schulze
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-03-13 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 10.0.3
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 10.0.3
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '2.12'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '2.12'
46
+ - !ruby/object:Gem::Dependency
47
+ name: simplecov
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 0.7.1
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 0.7.1
62
+ - !ruby/object:Gem::Dependency
63
+ name: simplecov-rcov
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 0.2.3
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 0.2.3
78
+ description: Simple FTP server written in pure Ruby, allowing integration testing
79
+ of FTP client code without mocks and stubs
80
+ email:
81
+ - christian.schulze@gmail.com
82
+ executables:
83
+ - testftpd
84
+ extensions: []
85
+ extra_rdoc_files: []
86
+ files:
87
+ - lib/testftpd/file_system_provider.rb
88
+ - lib/testftpd/server.rb
89
+ - lib/testftpd/version.rb
90
+ - lib/testftpd.rb
91
+ - vendor/dyn-ftp-serv/CHANGELOG
92
+ - vendor/dyn-ftp-serv/dynftp_server.rb
93
+ - vendor/dyn-ftp-serv/examples/ftpserv.rb
94
+ - vendor/dyn-ftp-serv/LICENSE
95
+ - bin/testftpd
96
+ - spec/fixtures/ftproot/subfolder/test_file1
97
+ - spec/fixtures/ftproot/subfolder/test_file2
98
+ - spec/fixtures/ftproot/test_file
99
+ - spec/fixtures/ftproot/test_file_to_delete
100
+ - spec/fixtures/ftproot/test_file_to_rename
101
+ - spec/fixtures/test_file_to_upload
102
+ - spec/integration/test_ftp_spec.rb
103
+ - spec/spec_helper.rb
104
+ - spec/unit/testftpd/server_spec.rb
105
+ homepage: https://github.com/christian-schulze/testftpd
106
+ licenses: []
107
+ post_install_message:
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ none: false
113
+ requirements:
114
+ - - ~>
115
+ - !ruby/object:Gem::Version
116
+ version: 1.9.2
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ none: false
119
+ requirements:
120
+ - - ~>
121
+ - !ruby/object:Gem::Version
122
+ version: 1.8.25
123
+ requirements: []
124
+ rubyforge_project: testftpd
125
+ rubygems_version: 1.8.25
126
+ signing_key:
127
+ specification_version: 3
128
+ summary: Simple FTP server written in pure Ruby
129
+ test_files:
130
+ - spec/fixtures/ftproot/subfolder/test_file1
131
+ - spec/fixtures/ftproot/subfolder/test_file2
132
+ - spec/fixtures/ftproot/test_file
133
+ - spec/fixtures/ftproot/test_file_to_delete
134
+ - spec/fixtures/ftproot/test_file_to_rename
135
+ - spec/fixtures/test_file_to_upload
136
+ - spec/integration/test_ftp_spec.rb
137
+ - spec/spec_helper.rb
138
+ - spec/unit/testftpd/server_spec.rb