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:
  1. require 'GServer'
  2. require 'logger'
  3.  
  4. # Simplistic FTP server in plain old Ruby
  5. # Implemented commands:
  6. #    USER, PASS, RETR, STOR, DELE, PWD, CWD, LIST, TYPE, QUIT, ABORT, PORT
  7. #   
  8. # Author:: Daniel Owsianski (daniel-at-jarmark-dot-org)
  9. class FtpServer <GServer
  10.   UNIX_DIR_LS_PREFIX = 'drw-r--r--   1 jarmark    jarmark '
  11.   UNIX_FILE_LS_PREFIX = '-rw-r--r--   1 jarmark    jarmark '
  12.  
  13.   def initialize(root_dir, port=21, host = DEFAULT_HOST, maxConnections = 4,
  14.                  stdlog = STDOUT, audit = true, debug = true)
  15.    
  16.     if !root_dir || !File.directory?(root_dir) || !File.writable?(root_dir)
  17.       puts("Please supply a content root directory")
  18.       exit(-1)
  19.     end
  20.     @root_dir = File.expand_path(root_dir)
  21.     @logger = Logger.new(STDOUT)
  22.     super(port, host, maxConnections, stdlog, audit, debug)
  23.   end
  24.  
  25.   def connecting(client)
  26.     client.puts("220 --- Welcome to the Simplistic Ruby FTP server. ---\r");
  27.     super
  28.   end
  29.  
  30.   def serve(io)
  31.     begin
  32.       io.each_line do |line|
  33.         line.chomp!
  34.         command, argument = line.split(' ', 2)
  35.         log("cmd=[#{command}] \targ=[#{argument}]")
  36.         self.current_dir ||= @root_dir
  37.        
  38.         case command.upcase
  39.         when 'USER'
  40.           @user = argument
  41.           io.puts("331 User okay, password needed\r")     
  42.         when 'PASS'
  43.           @password = argument;
  44.           if @user == "jarmark.org" && @user==@password
  45.             io.puts("230-Welcome to the Simplistic Ruby FTP Server.\r")
  46.             io.puts("230-Dir root: #{@root_dir}\r")
  47.             io.puts("230 User #{@user} logged in.\r")
  48.           else
  49.             io.puts("530 User not logged in.\r")
  50.           end
  51.         when 'RETR'
  52.           io.puts("150 Binary data connection\r")
  53.           filename = self.current_dir+'/'+argument
  54.           if File.exists?(filename)
  55.             File.open(filename, 'r') do |f|
  56.               f.binmode
  57.               tranfer(@socket, f)
  58.               io.puts("226 Transfer complete\r")
  59.               @socket.close
  60.             end
  61.           else
  62.             io.puts("550 File #{argument} could not be found.")
  63.           end         
  64.         when 'STOR'
  65.           io.puts("150 Binary data connection\r")
  66.           begin
  67.             File.open(self.current_dir+'/'+argument, 'w') do |f|
  68.               f.binmode
  69.               tranfer(f, @socket)
  70.               io.puts("226 Transfer complete\r")
  71.               @socket.close
  72.             end
  73.           rescue
  74.             io.puts("451 Storage failed.\r")
  75.           end     
  76.         when 'DELE'
  77.           filename = self.current_dir+'/'+argument
  78.           if File.exists?(filename) && File.writable?(filename)
  79.             File.delete(filename)
  80.           else
  81.             io.puts("550 #{argument} No such file or no permission")
  82.           end       
  83.         when 'PWD' || 'XPWD'
  84.           curr = self.current_dir.dup
  85.           curr.slice!(@root_dir)
  86.           io.puts(%Q{257 "#{curr||'/'}" is current directory\r})
  87.         when 'CWD'
  88.           new_dir = argument[0,1]=='/' ? @root_dir+argument : self.current_dir+'/'+argument
  89.           if File.exists?(new_dir) && File.directory?(new_dir)
  90.             Dir.chdir(new_dir){ self.current_dir = Dir.pwd } unless (argument=='..' && self.current_dir==@root_dir)
  91.             io.puts("250 Command succesful\r")
  92.           else
  93.             io.puts("550 Cannot find directory #{new_dir}.");
  94.           end
  95.         when 'LIST'
  96.           io.puts("150 ASCII data\r");
  97.           buff = ""
  98.           Dir.chdir(self.current_dir)
  99.           Dir.foreach('.') do |file|
  100.             prefix = File.directory?(file) ? UNIX_DIR_LS_PREFIX : UNIX_FILE_LS_PREFIX
  101.             last_modification = File.ctime(file).strftime("%b %d %H:%M")
  102.             buff +="#{prefix} #{File.size(file)} #{last_modification} #{file}\r\n"
  103.           end
  104.           @socket.write(buff)
  105.           @socket.close
  106.           io.puts("226 Transfer complete\r")
  107.          
  108.           # miscellaneous commands
  109.         when 'TYPE'
  110.           io.puts("200 Type set\r")
  111.         when 'QUIT'
  112.           @socket.close if @socket && !@socket.closed?
  113.           io.close
  114.           break                 
  115.         when 'ABORT'
  116.           io.puts("426 Transfer aborted abnormally\r");
  117.           io.puts("226 Transfer aborted\r");
  118.           @socket.close if @socket and !@socket.closed?
  119.         when 'PORT'
  120.           nums = argument.split(',',6)
  121.           host = nums[0..3].join('.')
  122.           port = nums[4].to_i*256+nums[5].to_i
  123.           @socket.close if @socket and !@socket.closed?
  124.           @socket = TCPSocket.open(host, port)
  125.           io.puts("200 PORT command successful\r")
  126.         else
  127.           io.puts("502 Command not implemented\r")
  128.         end
  129.       end     
  130.     rescue => err
  131.       raise err
  132.     end
  133.   end
  134.  
  135.   def current_dir
  136.     Thread.current['current_dir']
  137.   end
  138.   def current_dir=(dir)
  139.     Thread.current['current_dir'] = dir
  140.   end
  141.   def tranfer(output, input)
  142.     loop do
  143.       buff = input.read(2048)
  144.       break if buff == nil
  145.       output.write(buff)
  146.     end
  147.   end
  148.  
  149. end
  150.  
  151. if ( $0 == __FILE__ )
  152.   server = FtpServer.new(ARGV[0]).start.join
  153. end

[ ]
Spodobało się? Podziel się z innymi: These icons link to social bookmarking sites where readers can share and discover new web pages.
  • del.icio.us
  • Wykop
  • Gwar
  • Digg
  • Technorati

Liczba komentarzy: 2 »

  1. 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

  2. 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.

RSS feed for comments on this post · Adres TrackBack

Dodaj komentarz