Share via


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

Comments

  • Anonymous
    September 29, 2005
    Can you please write more frequently. I love your blog so I require more of it... :)

  • Anonymous
    October 05, 2005
    Mind doing that one in C# also? TIA

  • Anonymous
    October 08, 2005
    Nice!

  • Anonymous
    October 14, 2005
    With C# wouldn't one be using WCF instead and get the security as standard?

  • Anonymous
    October 17, 2005
    By saying that this web-server has no security I mean is that there is no built-in security in the code. So you can potentially hand-craft GET requests and crash the web-server or get access to folder above the base web-server folder.

    You can use some OS security features to block accesses to folder other than the ones under basePath but even then crashing the web-server of DOS attacks are very very easy on this.

    If you port this to C# even then you'll have the same issues. This is nothing to do with Ruby vs C#. You need to add verification code and defensive mechanisms to ensure that any external request cannot make your web-server do things you do not want it to do

  • Anonymous
    October 30, 2005
    this is NOT multithreaded--ruby has no multithreading; the interpreter core multiplexes the different contexts itself. Therefore any entry into the kernel blocks all of the fake threads. And guess where your web server will be sitting a lot?

  • Anonymous
    February 05, 2006
    Ruby has a web-server built in.

    See: http://www.webrick.org/

    It's part of the Standard Library, so nothing extra to install. So you can actually whip up a HTTP Server in Ruby in about 5 lines of code.

    Sure, you can point out that out-of-process calls will block, or that you might want additional security, but if so, you're missing the point.

    If you want a production quality webserver, use one.

    If you just want to share some docs, an internal Wiki, whatever, then spending about 60 seconds writing a bit of bootstraping for a Webrick server is obviously pretty cool.

  • Anonymous
    February 05, 2006
    Sam, you should read my post on programmers disease of trying to code everything for himself (http://blogs.msdn.com/abhinaba/archive/2005/12/12/502661.aspx).

    In the introduction of this post I clearly stated that I did not need the webserver. I did this just for kicks to see how much effort this takes to be done in Ruby as I had already done the same thing in C++. In case I wanted a web-server I'd use IIS or Apache, why'd I even code it or use some Ruby web-server who's strengths, robustness, handling capacity I'm not aware of?

  • Anonymous
    March 05, 2006
    The comment has been removed

  • Anonymous
    March 07, 2006
    Thanks Ken!! So the code get shorter and shorter :)

  • Anonymous
    March 19, 2006
    Dynamic languages prove themselves immensly powerful at places you least expect them to be.&amp;nbsp;I found...

  • Anonymous
    September 09, 2006
    require ’webrick’
    WEBrick::HTTPServer.new.start

  • Anonymous
    September 10, 2006
    true but, here you are not writing  web server in 2 lines. you are using it in 2 lines

  • Anonymous
    October 15, 2006
    log.close should be $log.close

  • Anonymous
    November 23, 2006
    PingBack from http://wind333.wordpress.com/2006/11/24/my-daily-readings-11242006/

  • Anonymous
    November 24, 2006
    PingBack from http://wind333.wordpress.com/2006/11/24/test-11242006/

  • Anonymous
    August 08, 2007
     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/plain" if ext == ".rb"    return "text/xml"   if ext == ".xml"    return "text/xml"   if ext == ".xsl"    return "text/html"  end end You could replace the two text/plains with another ext == "" or ext == "" expression... same with the xml and xsl. That's pretty nifty, by the way.

  • Anonymous
    September 13, 2007
    You don't actually need to write your own logger either.  Ruby has a great logger built in. require 'logger' $log = Logger.new('server.log') $log.info("your message") It supports different levels of logs (info, debug, error, warning) and you wouldn't have to add a timestamp yourself. Also, from a design perspective, I would make your HttpServer extend TCPServer.  If I create a new instance of your server, I shouldn't need to create a separate TCPServer.  I think it's implied that HTTP runs on top TCP. class HTTPServer < TCPServer  def initialize    super(9090) #starts a server on localhost  end  ... end

  • Anonymous
    November 18, 2009
    Thanks for this! I'm using your code to implement a JSP customtags-style system for Ruby, which I hope to eventually integrate into Rails.

  • Anonymous
    December 18, 2011
    How do you get it running with rails?

  • Anonymous
    December 01, 2013
    Hi I Copied this and managed to get it working but it says waiting for server . Anyone know how to fix this?