Ruby: Webserver in 70 lines of code

<Updated the sources to add logging and default file index.html handling. Now the code is about 90 lines :(>

I decided to write a http-server in Ruby on Windows to see how much code it requires as I have been reading about how Ruby gets your work done much easily and much faster. Some of the new things in C# 2.0 /3.0 have been already around in Ruby for some time and they make coding in Ruby fun and very interesting. I'll share my experiences about a some of the features in Ruby that I'd like to see in C#.

This is a no-frills minimal implementation which any hacker can break in about 15 minutes :) So I deployed it over the intranet. I hosted my personal site from https://www.geocities.com/basuabhinaba on the server and it worked first time.  The code below should work without much modifications, just replace the IP address xxx.xxx.xxx.xxx to the one on your machine. I was amazed at how soon I was able to code this thing up in Ruby (doesn't say much about code quality though :) )

Features

  1. Multi-threaded server
  2. Allows adding one base local folder from which pages are served. Request out side this folder would be refused (I hope it'll be refused)
  3. Supports common file formats like html, jpeg, gif, txt, css. I'll add more with time or I may just decide to use the Win32 API to read ContentType from the registry so that everything works
  4. Support for page not found

Issues

  1. No security at all!!!! other than redumentary code to stop users from accessing files outside of the base folder
  2. No concept of default file (e.g. index.html) or directory listing in case request comes to access a folder
  3. There is some code in this that is windows specific
  4. No logging support for now

Finally the Code

It took me about 70 lines of code to get this to work.

 require 'socket'

class HttpServer
  def initialize(session, request, basePath)
    @session = session
    @request = request
    @basePath = basePath
  end

  def getFullPath()
    fileName = nil
    if @request =~ /GET .* HTTP.*/
      fileName = @request.gsub(/GET /, '').gsub(/ HTTP.*/, '')
    end
    fileName = fileName.strip
    unless fileName == nil
      fileName = @basePath + fileName
      fileName = File.expand_path(fileName, @defaultPath)
      fileName.gsub!('/', '\\')
    end
    fileName << "\\index.html" if  File.directory?(fileName)
    return fileName
  end

  def serve()
    @fullPath = getFullPath()
    src = nil
    begin
      if File.exist?(@fullPath) and File.file?(@fullPath)
        if @fullPath.index(@basePath) == 0 #path should start with base path
          contentType = getContentType(@fullPath)
          @session.print "HTTP/1.1 200/OK\r\nServer: Makorsha\r\nContent-type: #{contentType}\r\n\r\n"
          src = File.open(@fullPath, "rb")
          while (not src.eof?)
            buffer = src.read(256)
            @session.write(buffer)
          end
          src.close
          src = nil
        else
          # should have sent a 403 Forbidden access but then the attacker knows that such a file exists
          @session.print "HTTP/1.1 404/Object Not Found\r\nServer: Makorsha\r\n\r\n"
        end
      else
        @session.print "HTTP/1.1 404/Object Not Found\r\nServer: Makorsha\r\n\r\n"
      end
    ensure
      src.close unless src == nil
      @session.close
    end
  end

  def getContentType(path)
    #TODO replace with access to HKEY_CLASSES_ROOT => "Content Type"
    ext = File.extname(path)
    return "text/html"  if ext == ".html" or ext == ".htm"
    return "text/plain" if ext == ".txt"
    return "text/css"   if ext == ".css"
    return "image/jpeg" if ext == ".jpeg" or ext == ".jpg"
    return "image/gif"  if ext == ".gif"
    return "image/bmp"  if ext == ".bmp"
    return "text/plain" if ext == ".rb"
    return "text/xml"   if ext == ".xml"
    return "text/xml"   if ext == ".xsl"
    return "text/html"
  end
end

def logger(message)
  logStr =  "\n\n======================================================\n#{message}"
  puts logStr
  $log.puts logStr unless $log == nil
end

basePath = "d:\\web"
server = TCPServer.new('XXX.XXX.XXX.XXX', 9090)
logfile = basePath + "\\log.txt"
$log = File.open(logfile, "w+")

loop do
  session = server.accept
  request = session.gets
  logStr =  "#{session.peeraddr[2]} (#{session.peeraddr[3]})\n"
  logStr += Time.now.localtime.strftime("%Y/%m/%d %H:%M:%S")
  logStr += "\n#{request}"
  logger(logStr)

  Thread.start(session, request) do |session, request|
    HttpServer.new(session, request, basePath).serve()
  end
end
log.close