Serwer FTP w Ruby
W ramach poszerzania wiedzy o Ruby postanowiłem napisać prosty serwer (właściwie to serwerek) obsługujący protokół FTP. Po kilku godzinach zmagań (ciągle się uczę Ruby API) otrzymałem dosyć zgrabny serwer działający na wątkach - całość zajmuje 153 lini.
Założenia jakie przyjąłem są następujące:
- serwer obsługuje okrojony podzbiór komend ftp, można w nim tylko: podegrać, pobrać i usunąc plik(i) oraz przeglądać katalogi (nie można tworzyć katalogów, ani zmieniać nazw plików/katalogów)
- serwer nie obsługuje 'passive mode'
- serwer uruchamia się z linii komend i oczekuje jednego parametru którym jest scieżka do katalogu - katalog ten staje sie root direm dla serwera
- serwer musi sprawdzic czy katalog istnieje i czy jest zapisywalny
- serwer obsługuje jednego użytkownika login i pass: 'jarmark.org'
Serwer więc jest tylko namiastką prawdziwego serwera FTP, więc jeżeli ktoś chce go rozbudować do prawdziwego poważnego (produkcyjnego?) serwera - gorąco zapraszam. Ponieważ kod serwera jest stosunkowo krótki cytuje go w całości (wszelkie uwagi mile widziane):
RUBY:
[ruby server]
-
require 'GServer'
-
require 'logger'
-
-
# Simplistic FTP server in plain old Ruby
-
# Implemented commands:
-
# USER, PASS, RETR, STOR, DELE, PWD, CWD, LIST, TYPE, QUIT, ABORT, PORT
-
#
-
# Author:: Daniel Owsianski (daniel-at-jarmark-dot-org)
-
class FtpServer <GServer
-
UNIX_DIR_LS_PREFIX = 'drw-r--r-- 1 jarmark jarmark '
-
UNIX_FILE_LS_PREFIX = '-rw-r--r-- 1 jarmark jarmark '
-
-
def initialize(root_dir, port=21, host = DEFAULT_HOST, maxConnections = 4,
-
stdlog = STDOUT, audit = true, debug = true)
-
-
if !root_dir || !File.directory?(root_dir) || !File.writable?(root_dir)
-
puts("Please supply a content root directory")
-
exit(-1)
-
end
-
@root_dir = File.expand_path(root_dir)
-
@logger = Logger.new(STDOUT)
-
super(port, host, maxConnections, stdlog, audit, debug)
-
end
-
-
def connecting(client)
-
client.puts("220 --- Welcome to the Simplistic Ruby FTP server. ---\r");
-
super
-
end
-
-
def serve(io)
-
begin
-
io.each_line do |line|
-
line.chomp!
-
command, argument = line.split(' ', 2)
-
log("cmd=[#{command}] \targ=[#{argument}]")
-
self.current_dir ||= @root_dir
-
-
case command.upcase
-
when 'USER'
-
@user = argument
-
io.puts("331 User okay, password needed\r")
-
when 'PASS'
-
@password = argument;
-
if @user == "jarmark.org" && @user==@password
-
io.puts("230-Welcome to the Simplistic Ruby FTP Server.\r")
-
io.puts("230-Dir root: #{@root_dir}\r")
-
io.puts("230 User #{@user} logged in.\r")
-
else
-
io.puts("530 User not logged in.\r")
-
end
-
when 'RETR'
-
io.puts("150 Binary data connection\r")
-
filename = self.current_dir+'/'+argument
-
if File.exists?(filename)
-
File.open(filename, 'r') do |f|
-
f.binmode
-
tranfer(@socket, f)
-
io.puts("226 Transfer complete\r")
-
@socket.close
-
end
-
else
-
io.puts("550 File #{argument} could not be found.")
-
end
-
when 'STOR'
-
io.puts("150 Binary data connection\r")
-
begin
-
File.open(self.current_dir+'/'+argument, 'w') do |f|
-
f.binmode
-
tranfer(f, @socket)
-
io.puts("226 Transfer complete\r")
-
@socket.close
-
end
-
rescue
-
io.puts("451 Storage failed.\r")
-
end
-
when 'DELE'
-
filename = self.current_dir+'/'+argument
-
if File.exists?(filename) && File.writable?(filename)
-
File.delete(filename)
-
else
-
io.puts("550 #{argument} No such file or no permission")
-
end
-
when 'PWD' || 'XPWD'
-
curr = self.current_dir.dup
-
curr.slice!(@root_dir)
-
io.puts(%Q{257 "#{curr||'/'}" is current directory\r})
-
when 'CWD'
-
new_dir = argument[0,1]=='/' ? @root_dir+argument : self.current_dir+'/'+argument
-
if File.exists?(new_dir) && File.directory?(new_dir)
-
Dir.chdir(new_dir){ self.current_dir = Dir.pwd } unless (argument=='..' && self.current_dir==@root_dir)
-
io.puts("250 Command succesful\r")
-
else
-
io.puts("550 Cannot find directory #{new_dir}.");
-
end
-
when 'LIST'
-
io.puts("150 ASCII data\r");
-
buff = ""
-
Dir.chdir(self.current_dir)
-
Dir.foreach('.') do |file|
-
prefix = File.directory?(file) ? UNIX_DIR_LS_PREFIX : UNIX_FILE_LS_PREFIX
-
last_modification = File.ctime(file).strftime("%b %d %H:%M")
-
buff +="#{prefix} #{File.size(file)} #{last_modification} #{file}\r\n"
-
end
-
@socket.write(buff)
-
@socket.close
-
io.puts("226 Transfer complete\r")
-
-
# miscellaneous commands
-
when 'TYPE'
-
io.puts("200 Type set\r")
-
when 'QUIT'
-
@socket.close if @socket && !@socket.closed?
-
io.close
-
break
-
when 'ABORT'
-
io.puts("426 Transfer aborted abnormally\r");
-
io.puts("226 Transfer aborted\r");
-
@socket.close if @socket and !@socket.closed?
-
when 'PORT'
-
nums = argument.split(',',6)
-
host = nums[0..3].join('.')
-
port = nums[4].to_i*256+nums[5].to_i
-
@socket.close if @socket and !@socket.closed?
-
@socket = TCPSocket.open(host, port)
-
io.puts("200 PORT command successful\r")
-
else
-
io.puts("502 Command not implemented\r")
-
end
-
end
-
rescue => err
-
raise err
-
end
-
end
-
-
def current_dir
-
Thread.current['current_dir']
-
end
-
def current_dir=(dir)
-
Thread.current['current_dir'] = dir
-
end
-
def tranfer(output, input)
-
loop do
-
buff = input.read(2048)
-
break if buff == nil
-
output.write(buff)
-
end
-
end
-
-
end
-
-
if ( $0 == __FILE__ )
-
server = FtpServer.new(ARGV[0]).start.join
-
end









Gf said,
luty 15, 2008 @ 15:43
Powiedz mi czy trudno napisac serwer www?moze byc malenki i jednowatkowy. po prostu chce miec GUI do swojego programu, a ww jest wygodne.
pislbym raz a potem tylko sie laczyl localhost:1234567 i juz
daniel said,
luty 15, 2008 @ 15:53
@Gf
W Rubym? Prościej nawet niż FTP sam protokół jest prostszy. Polecam spojrzenie na EventMachine http://rubyeventmachine.com/ z tym gemem tworzenie wszelakich serwerów jest proste i wydajne.