WatersOfOblivion-dyn-ftp-serv 0.0.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.
- data/History.txt +4 -0
- data/Manifest.txt +13 -0
- data/README.rdoc +48 -0
- data/Rakefile +28 -0
- data/examples/ftpserv.rb +79 -0
- data/lib/dyn-ftp-serv.rb +450 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/spec/dyn-ftp-serv_spec.rb +11 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +10 -0
- data/tasks/rspec.rake +21 -0
- metadata +88 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
= dyn-ftp-serv
|
2
|
+
|
3
|
+
* http://github.com/WatersOfOblivion/dyn-ftp-serv
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
DynFtpServ is a Dynamic FTP Server. I stole the original code from [here](http://dyn-ftp-serv.rubyforce.org/)
|
8
|
+
|
9
|
+
== FEATURES/PROBLEMS:
|
10
|
+
|
11
|
+
* Nothing, yet
|
12
|
+
|
13
|
+
== SYNOPSIS:
|
14
|
+
|
15
|
+
TODO
|
16
|
+
|
17
|
+
== REQUIREMENTS:
|
18
|
+
|
19
|
+
* None, yet
|
20
|
+
|
21
|
+
== INSTALL:
|
22
|
+
|
23
|
+
* sudo gem install WatersOfOblivion-dyn-ftp-serv -s http://gems.github.com
|
24
|
+
|
25
|
+
== LICENSE:
|
26
|
+
|
27
|
+
(The MIT License)
|
28
|
+
|
29
|
+
Copyright (c) 2009 Jonathan Bryant
|
30
|
+
|
31
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
32
|
+
a copy of this software and associated documentation files (the
|
33
|
+
'Software'), to deal in the Software without restriction, including
|
34
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
35
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
36
|
+
permit persons to whom the Software is furnished to do so, subject to
|
37
|
+
the following conditions:
|
38
|
+
|
39
|
+
The above copyright notice and this permission notice shall be
|
40
|
+
included in all copies or substantial portions of the Software.
|
41
|
+
|
42
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
43
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
44
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
45
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
46
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
47
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
48
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rubygems' unless ENV['NO_RUBYGEMS']
|
2
|
+
%w[rake rake/clean fileutils newgem rubigen].each { |f| require f }
|
3
|
+
require File.dirname(__FILE__) + '/lib/dyn-ftp-serv'
|
4
|
+
|
5
|
+
# Generate all the Rake tasks
|
6
|
+
# Run 'rake -T' to see list of generated tasks (from gem root directory)
|
7
|
+
$hoe = Hoe.new('dyn-ftp-serv', DynFtpServ::VERSION) do |p|
|
8
|
+
p.developer('Jonathan Bryant', 'jonathan@watersofoblivion.com')
|
9
|
+
p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
|
10
|
+
p.rubyforge_name = p.name # TODO this is default value
|
11
|
+
# p.extra_deps = [
|
12
|
+
# ['activesupport','>= 2.0.2'],
|
13
|
+
# ]
|
14
|
+
p.extra_dev_deps = [
|
15
|
+
['newgem', ">= #{::Newgem::VERSION}"]
|
16
|
+
]
|
17
|
+
|
18
|
+
p.clean_globs |= %w[**/.DS_Store tmp *.log]
|
19
|
+
path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
|
20
|
+
p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
|
21
|
+
p.rsync_args = '-av --delete --ignore-errors'
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'newgem/tasks' # load /tasks/*.rake
|
25
|
+
Dir['tasks/**/*.rake'].each { |t| load t }
|
26
|
+
|
27
|
+
# TODO - want other tests/tasks run by default? Add them to the list
|
28
|
+
task :default => :spec
|
data/examples/ftpserv.rb
ADDED
@@ -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
|
data/lib/dyn-ftp-serv.rb
ADDED
@@ -0,0 +1,450 @@
|
|
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
|
+
# standart 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
|
+
# [+ftp_dir+] 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
|
+
# [+ftp_dir+] 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
|
+
module DynFtpServ
|
43
|
+
|
44
|
+
VERSION = '0.0.0'
|
45
|
+
|
46
|
+
class DynFTPServer
|
47
|
+
|
48
|
+
# Class to instantiate if logger is not given.
|
49
|
+
class DummyLogger
|
50
|
+
def method_missing(method_name, *args, &block); end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Pass a hash containing options.
|
54
|
+
# [<tt>:host</tt>] Local bind address. Default is <em>'0.0.0.0'</em>.
|
55
|
+
# [<tt>:port</tt>] Port to listen. Default is <em>21</em>.
|
56
|
+
# [<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.
|
57
|
+
# [<tt>:pasv_min_port</tt>] Minimum port num for passive connections.
|
58
|
+
# [<tt>:pasv_max_port</tt>] Maximum port num for passive connections.
|
59
|
+
# [<tt>:root</tt>] Root ftp object.
|
60
|
+
# [<tt>:authentication</tt>] Function used to check users login information.
|
61
|
+
# [<tt>:logger</tt>] Logger object.
|
62
|
+
def initialize(conf)
|
63
|
+
@config = {
|
64
|
+
:host => '',
|
65
|
+
:port => 21,
|
66
|
+
:masquerade_ip => nil,
|
67
|
+
:pasv_min_port => 1024,
|
68
|
+
:pasv_max_port => 65535,
|
69
|
+
:root => nil,
|
70
|
+
:authentication => lambda {|user,pass| return true; },
|
71
|
+
:logger => nil}.merge(conf)
|
72
|
+
raise(ArgumentError, "Root object must not be null.") unless @config[:root]
|
73
|
+
@server = TCPServer.new(@config[:host], @config[:port])
|
74
|
+
end
|
75
|
+
|
76
|
+
# Starts processing incoming connections
|
77
|
+
def mainloop
|
78
|
+
threads = []
|
79
|
+
log.debug "Waiting for connection"
|
80
|
+
while (session = @server.accept)
|
81
|
+
log.debug "Accepted connection from #{session.addr.join(', ')}"
|
82
|
+
threads << Thread.new(session) do |s|
|
83
|
+
thread[:socket] = s
|
84
|
+
client_loop
|
85
|
+
end
|
86
|
+
end
|
87
|
+
threads.each {|t| t.join }
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# Returns logger
|
93
|
+
def log
|
94
|
+
return @config[:logger] if @config[:logger]
|
95
|
+
return DummyLogger.new
|
96
|
+
end
|
97
|
+
|
98
|
+
def not_implemented
|
99
|
+
status(500);
|
100
|
+
end
|
101
|
+
|
102
|
+
def not_authorized
|
103
|
+
status 530
|
104
|
+
end
|
105
|
+
|
106
|
+
def status(code, descr = nil)
|
107
|
+
unless (descr.nil?)
|
108
|
+
log.debug "Response: " + code.to_s + ' ' + descr
|
109
|
+
thread[:socket].puts code.to_s + ' ' + descr + "\r\n"
|
110
|
+
return
|
111
|
+
end
|
112
|
+
case (code.to_i)
|
113
|
+
when 125
|
114
|
+
status(code, 'Data connection already open; transfer starting.')
|
115
|
+
when 150
|
116
|
+
status(code, 'File status okay; about to open data connection.')
|
117
|
+
when 200
|
118
|
+
status(code, 'Command okey.')
|
119
|
+
when 226
|
120
|
+
status(code, 'Closing data connection.')
|
121
|
+
when 230
|
122
|
+
status(code, 'User logged in, proceed.')
|
123
|
+
when 250
|
124
|
+
status(code, 'Requested file action okay, completed.')
|
125
|
+
when 331
|
126
|
+
status(code, 'User name okay, need password.')
|
127
|
+
when 425
|
128
|
+
status(code, "Can't open data connection.")
|
129
|
+
when 500
|
130
|
+
status(code, 'Syntax error, command unrecognized.')
|
131
|
+
when 502
|
132
|
+
status(code, 'Command not implemented.')
|
133
|
+
when 530
|
134
|
+
status(code, 'Not logged in.')
|
135
|
+
when 550
|
136
|
+
status(code, 'Requested action not taken.')
|
137
|
+
else
|
138
|
+
status(code, '')
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def data_connection(&block)
|
143
|
+
client_socket = nil
|
144
|
+
if (thread[:passive])
|
145
|
+
unless (IO.select([thread[:data_socket]], nil, nil, 60000))
|
146
|
+
status 425
|
147
|
+
return false
|
148
|
+
end
|
149
|
+
client_socket = thread[:data_socket].accept
|
150
|
+
status 150
|
151
|
+
else
|
152
|
+
client_socket = thread[:data_socket]
|
153
|
+
status 125
|
154
|
+
end
|
155
|
+
yield(client_socket)
|
156
|
+
return true
|
157
|
+
ensure
|
158
|
+
client_socket.close if client_socket && thread[:passive]
|
159
|
+
client_socket = nil
|
160
|
+
end
|
161
|
+
|
162
|
+
def passive_server
|
163
|
+
server = nil
|
164
|
+
port = @config[:pasv_min_port]
|
165
|
+
while (server.nil?) and (port <= @config[:pasv_max_port])
|
166
|
+
begin
|
167
|
+
server = TCPServer.new(@config[:host], port)
|
168
|
+
rescue Errno::EADDRINUSE
|
169
|
+
log.error "#{port} is already in use. Trying next port."
|
170
|
+
end
|
171
|
+
port += 1
|
172
|
+
end
|
173
|
+
server
|
174
|
+
end
|
175
|
+
|
176
|
+
def open_object(path)
|
177
|
+
if (path[0,1] == '/') || (path.is_a?(Array) && (path[0] == ''))
|
178
|
+
dir = @config[:root]
|
179
|
+
else
|
180
|
+
dir = thread[:cwd]
|
181
|
+
end
|
182
|
+
path = path.split('/') unless path.is_a?(Array)
|
183
|
+
return dir if path.empty?
|
184
|
+
last_element = path.pop
|
185
|
+
path.each do |p|
|
186
|
+
unless p == ''
|
187
|
+
dir = dir.ftp_list.detect {|d| (d.ftp_name.casecmp(p) == 0) && (d.ftp_dir) }
|
188
|
+
return nil unless dir
|
189
|
+
end
|
190
|
+
end
|
191
|
+
dir = dir.ftp_list.detect {|d| (d.ftp_name.casecmp(last_element) == 0) } unless last_element == ''
|
192
|
+
return dir
|
193
|
+
end
|
194
|
+
|
195
|
+
def open_path(path)
|
196
|
+
result = open_object(path)
|
197
|
+
result = nil if result && !result.ftp_dir
|
198
|
+
return result
|
199
|
+
end
|
200
|
+
|
201
|
+
def open_file(path)
|
202
|
+
result = open_object(path)
|
203
|
+
result = nil if result && result.ftp_dir
|
204
|
+
return result
|
205
|
+
end
|
206
|
+
|
207
|
+
def get_path(object)
|
208
|
+
return '/' unless object
|
209
|
+
return '/' if object == @config[:root]
|
210
|
+
result = ''
|
211
|
+
while object do
|
212
|
+
result = '/' + object.ftp_name + result
|
213
|
+
object = object.ftp_parent
|
214
|
+
end
|
215
|
+
return result
|
216
|
+
end
|
217
|
+
|
218
|
+
def get_quoted_path(object)
|
219
|
+
get_path(object).gsub('"', '""')
|
220
|
+
end
|
221
|
+
|
222
|
+
def thread
|
223
|
+
Thread.current
|
224
|
+
end
|
225
|
+
|
226
|
+
# Commands
|
227
|
+
|
228
|
+
def cmd_cdup(params)
|
229
|
+
thread[:cwd] = thread[:cwd].ftp_parent
|
230
|
+
thread[:cwd] = @config[:root] unless thread[:cwd]
|
231
|
+
status(250, 'Directory successfully changed.')
|
232
|
+
end
|
233
|
+
|
234
|
+
def cmd_cwd(path)
|
235
|
+
if (newpath = open_path(path))
|
236
|
+
thread[:cwd] = newpath
|
237
|
+
status(250, 'Directory successfully changed.')
|
238
|
+
else
|
239
|
+
status(550, 'Failed to change directory.')
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def cmd_dele(path)
|
244
|
+
if (file = open_file(path)) && file.ftp_delete
|
245
|
+
status 250
|
246
|
+
else
|
247
|
+
status(550, 'Delete operation failed.')
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# def cmd_feat(params)
|
252
|
+
# thread[:socket].puts "211-Features\r\n"
|
253
|
+
# thread[:socket].puts " UTF8\r\n"
|
254
|
+
# thread[:socket].puts "211 end\r\n"
|
255
|
+
# end
|
256
|
+
|
257
|
+
def cmd_list(params)
|
258
|
+
data_connection do |data_socket|
|
259
|
+
list = thread[:cwd].ftp_list
|
260
|
+
list.each {|file| data_socket.puts((file.ftp_dir ? '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") }
|
261
|
+
end
|
262
|
+
thread[:data_socket].close if thread[:data_socket]
|
263
|
+
thread[:data_socket] = nil
|
264
|
+
|
265
|
+
status 226, "Transfer complete"
|
266
|
+
end
|
267
|
+
|
268
|
+
def cmd_mdtm(path)
|
269
|
+
if (file = open_file(path))
|
270
|
+
status 213, file.ftp_date.strftime('%Y%m%d%H%M%S')
|
271
|
+
else
|
272
|
+
status(550, 'Could not get modification time.')
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def cmd_mkd(path)
|
277
|
+
dir = open_object(path)
|
278
|
+
if (dir)
|
279
|
+
status 521, "Directory already exists"
|
280
|
+
return
|
281
|
+
end
|
282
|
+
splitted_path = path.split('/')
|
283
|
+
mkdir = splitted_path.pop
|
284
|
+
dir = open_path(splitted_path)
|
285
|
+
if dir && (newone = dir.ftp_create(mkdir, true))
|
286
|
+
status 257, '"'+get_quoted_path(newone)+'" directory created.'
|
287
|
+
else
|
288
|
+
status 550
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def cmd_pass(pass)
|
293
|
+
thread[:pass] = pass
|
294
|
+
if @config[:authentication].call(thread[:user], thread[:pass])
|
295
|
+
thread[:authenticated] = true
|
296
|
+
status 230
|
297
|
+
else
|
298
|
+
thread[:authenticated] = false
|
299
|
+
not_authorized
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
def cmd_pasv(params)
|
304
|
+
if thread[:data_socket]
|
305
|
+
thread[:data_socket].close
|
306
|
+
thread[:data_socket] = nil
|
307
|
+
end
|
308
|
+
thread[:data_socket] = passive_server
|
309
|
+
return status(425) if thread[:data_socket].nil?
|
310
|
+
thread[:passive] = true
|
311
|
+
port = thread[:data_socket].addr[1]
|
312
|
+
port_lo = port & "0x00FF".hex
|
313
|
+
port_hi = port >> 8
|
314
|
+
ip = thread[:data_socket].addr[3]
|
315
|
+
ip = @config[:masquerade_ip] if @config[:masquerade_ip]
|
316
|
+
ip = ip.split('.')
|
317
|
+
status 227, "Entering Passive Mode (#{ip[0]},#{ip[1]},#{ip[2]},#{ip[3]},#{port_hi},#{port_lo})"
|
318
|
+
end
|
319
|
+
|
320
|
+
def cmd_port(ip_port)
|
321
|
+
s = ip_port.split(',')
|
322
|
+
port = s[4].to_i * 256 + s[5].to_i
|
323
|
+
host = s[0..3].join('.')
|
324
|
+
if thread[:data_socket]
|
325
|
+
thread[:data_socket].close
|
326
|
+
thread[:data_socket] = nil
|
327
|
+
end
|
328
|
+
thread[:data_socket] = TCPSocket.new(host, port)
|
329
|
+
thread[:passive] = false
|
330
|
+
status 200, "Passive connection established (#{port})"
|
331
|
+
end
|
332
|
+
|
333
|
+
def cmd_pwd(params)
|
334
|
+
status 257, "\"#{get_quoted_path(thread[:cwd])}\" is the current directory"
|
335
|
+
end
|
336
|
+
|
337
|
+
def cmd_rmd(path)
|
338
|
+
if (dir = open_path(path)) && dir.ftp_delete
|
339
|
+
status 250
|
340
|
+
else
|
341
|
+
status(550, 'Remove directory operation failed.')
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
def cmd_quit(params)
|
346
|
+
status(200)
|
347
|
+
thread[:socket].close
|
348
|
+
thread[:socket] = nil
|
349
|
+
end
|
350
|
+
|
351
|
+
def cmd_retr(path)
|
352
|
+
if (file = open_file(path))
|
353
|
+
data_connection do |data_socket|
|
354
|
+
if file.ftp_retrieve(data_socket)
|
355
|
+
status 226, 'Transfer complete'
|
356
|
+
else
|
357
|
+
status(550, 'Failed to open file.')
|
358
|
+
end
|
359
|
+
end
|
360
|
+
else
|
361
|
+
status(550, 'Failed to open file.')
|
362
|
+
end
|
363
|
+
|
364
|
+
thread[:data_socket].close if thread[:data_socket]
|
365
|
+
thread[:data_socket] = nil
|
366
|
+
end
|
367
|
+
|
368
|
+
def cmd_size(path)
|
369
|
+
if (file = open_file(path))
|
370
|
+
status 213, file.ftp_size.to_s
|
371
|
+
else
|
372
|
+
status(550, 'Could not get file size.')
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
def cmd_stor(path)
|
377
|
+
file = open_file(path)
|
378
|
+
if file
|
379
|
+
status 553, 'Could not create file.'
|
380
|
+
return
|
381
|
+
end
|
382
|
+
unless file
|
383
|
+
splitted_path = path.split('/')
|
384
|
+
filename = splitted_path.pop
|
385
|
+
dir = open_path(splitted_path)
|
386
|
+
file = dir.ftp_create(filename) if dir
|
387
|
+
end
|
388
|
+
if file
|
389
|
+
data_connection do |data_socket|
|
390
|
+
file.ftp_store(data_socket)
|
391
|
+
end
|
392
|
+
status 226, 'Transfer complete'
|
393
|
+
else
|
394
|
+
status 550, 'Failed to open file.'
|
395
|
+
end
|
396
|
+
|
397
|
+
thread[:data_socket].close if thread[:data_socket]
|
398
|
+
thread[:data_socket] = nil
|
399
|
+
end
|
400
|
+
|
401
|
+
def cmd_syst(params)
|
402
|
+
status(215, 'UNIX')
|
403
|
+
end
|
404
|
+
|
405
|
+
def cmd_type(type)
|
406
|
+
status 200, "Type set."
|
407
|
+
end
|
408
|
+
|
409
|
+
def cmd_user(user)
|
410
|
+
thread[:user] = user
|
411
|
+
status(331)
|
412
|
+
end
|
413
|
+
|
414
|
+
def welcome
|
415
|
+
thread[:authenticated] = false
|
416
|
+
thread[:cwd] = @config[:root]
|
417
|
+
status(220, "Microsoft FTP Server ready")
|
418
|
+
end
|
419
|
+
|
420
|
+
def client_loop
|
421
|
+
welcome
|
422
|
+
while (thread[:socket] && (s = thread[:socket].gets))
|
423
|
+
s.chomp!
|
424
|
+
log.debug "Request: #{s}"
|
425
|
+
params = s.split(' ', 2)
|
426
|
+
command = params.first
|
427
|
+
command.downcase! if command
|
428
|
+
m = 'cmd_'+command.to_s
|
429
|
+
if self.respond_to?(m, true)
|
430
|
+
if (['cmd_user', 'cmd_pass'].include?(m)) or (thread[:authenticated])
|
431
|
+
self.send(m, params[1])
|
432
|
+
else
|
433
|
+
not_authorized
|
434
|
+
end
|
435
|
+
else
|
436
|
+
not_implemented
|
437
|
+
end
|
438
|
+
end
|
439
|
+
#rescue
|
440
|
+
# log.error $!
|
441
|
+
ensure
|
442
|
+
thread[:socket].close if thread[:socket] and not thread[:socket].closed?
|
443
|
+
thread[:socket] = nil
|
444
|
+
thread[:data_socket].close if thread[:data_socket] and not thread[:data_socket].closed?
|
445
|
+
thread[:data_socket] = nil
|
446
|
+
end
|
447
|
+
|
448
|
+
end
|
449
|
+
|
450
|
+
end
|
data/script/console
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# File: script/console
|
3
|
+
irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
|
4
|
+
|
5
|
+
libs = " -r irb/completion"
|
6
|
+
# Perhaps use a console_lib to store any extra methods I may want available in the cosole
|
7
|
+
# libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
|
8
|
+
libs << " -r #{File.dirname(__FILE__) + '/../lib/dyn-ftp-serv.rb'}"
|
9
|
+
puts "Loading dyn-ftp-serv gem"
|
10
|
+
exec "#{irb} #{libs} --simple-prompt"
|
data/script/destroy
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'rubigen'
|
6
|
+
rescue LoadError
|
7
|
+
require 'rubygems'
|
8
|
+
require 'rubigen'
|
9
|
+
end
|
10
|
+
require 'rubigen/scripts/destroy'
|
11
|
+
|
12
|
+
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
|
13
|
+
RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
|
14
|
+
RubiGen::Scripts::Destroy.new.run(ARGV)
|
data/script/generate
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'rubigen'
|
6
|
+
rescue LoadError
|
7
|
+
require 'rubygems'
|
8
|
+
require 'rubigen'
|
9
|
+
end
|
10
|
+
require 'rubigen/scripts/generate'
|
11
|
+
|
12
|
+
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
|
13
|
+
RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
|
14
|
+
RubiGen::Scripts::Generate.new.run(ARGV)
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/spec/spec_helper.rb
ADDED
data/tasks/rspec.rake
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
begin
|
2
|
+
require 'spec'
|
3
|
+
rescue LoadError
|
4
|
+
require 'rubygems' unless ENV['NO_RUBYGEMS']
|
5
|
+
require 'spec'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'spec/rake/spectask'
|
9
|
+
rescue LoadError
|
10
|
+
puts <<-EOS
|
11
|
+
To use rspec for testing you must install rspec gem:
|
12
|
+
gem install rspec
|
13
|
+
EOS
|
14
|
+
exit(0)
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Run the specs under spec/models"
|
18
|
+
Spec::Rake::SpecTask.new do |t|
|
19
|
+
t.spec_opts = ['--options', "spec/spec.opts"]
|
20
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: WatersOfOblivion-dyn-ftp-serv
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jonathan Bryant
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-06-18 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: newgem
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.4.1
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: hoe
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.8.0
|
34
|
+
version:
|
35
|
+
description: DynFtpServ is a Dynamic FTP Server. I stole the original code from [here](http://dyn-ftp-serv.rubyforce.org/)
|
36
|
+
email:
|
37
|
+
- jonathan@watersofoblivion.com
|
38
|
+
executables: []
|
39
|
+
|
40
|
+
extensions: []
|
41
|
+
|
42
|
+
extra_rdoc_files:
|
43
|
+
- History.txt
|
44
|
+
- Manifest.txt
|
45
|
+
- README.rdoc
|
46
|
+
files:
|
47
|
+
- History.txt
|
48
|
+
- Manifest.txt
|
49
|
+
- README.rdoc
|
50
|
+
- Rakefile
|
51
|
+
- lib/dyn-ftp-serv.rb
|
52
|
+
- examples/ftpserv.rb
|
53
|
+
- script/console
|
54
|
+
- script/destroy
|
55
|
+
- script/generate
|
56
|
+
- spec/dyn-ftp-serv_spec.rb
|
57
|
+
- spec/spec.opts
|
58
|
+
- spec/spec_helper.rb
|
59
|
+
- tasks/rspec.rake
|
60
|
+
has_rdoc: true
|
61
|
+
homepage: http://github.com/WatersOfOblivion/dyn-ftp-serv
|
62
|
+
post_install_message:
|
63
|
+
rdoc_options:
|
64
|
+
- --main
|
65
|
+
- README.rdoc
|
66
|
+
require_paths:
|
67
|
+
- lib
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: "0"
|
73
|
+
version:
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: "0"
|
79
|
+
version:
|
80
|
+
requirements: []
|
81
|
+
|
82
|
+
rubyforge_project: dyn-ftp-serv
|
83
|
+
rubygems_version: 1.2.0
|
84
|
+
signing_key:
|
85
|
+
specification_version: 2
|
86
|
+
summary: DynFtpServ is a Dynamic FTP Server
|
87
|
+
test_files: []
|
88
|
+
|