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 on the server and it worked first time.  The code below should work without much modifications, just replace the IP address 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 πŸ™‚ )


  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


  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

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

def serve()
@fullPath = getFullPath()
src = nil
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 =, "rb")
while (not src.eof?)
buffer =
src = nil
# 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"
@session.print "HTTP/1.1 404/Object Not Found\r\nServer: Makorsha\r\n\r\n"
src.close unless src == nil

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"

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

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

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

Thread.start(session, request) do |session, request|, request, basePath).serve()


Comments (21)

  1. Rick Scott says:

    Can you please write more frequently. I love your blog so I require more of it... πŸ™‚

  2. William Stacey says:

    Mind doing that one in C# also? TIA

  3. zzz says:

    With C# wouldn't one be using WCF instead and get the security as standard?

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

  5. Matus Telgarsky says:

    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?

  6. Sam says:

    Ruby has a web-server built in.


    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.

  7. Sam, you should read my post on programmers disease of trying to code everything for himself (

    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?

  8. ken says:

    Regarding the statement in your first sentence of "how much code it requires" in ruby, your line count is a bit high.  Without playing games aimed solely at reducing line count, but just looking at using different coding style (I'd say "more idiomatic ruby", but I'm new to ruby also; I'm just exploiting its similarity to perl which I do code idiomatically), I see several opportunites for shrinking the code.  For example, getFullPath() looses 8 lines when coded thus:

     def getFullPath()

       fileName = @request =~ /^GETs+(S+)/i ? $1 : "/"

       fileName = File.expand_path(@basePath+fileName, @defaultPath)

       fileName << "/index.html" if

       return fileName


    To my eye, that's not only shorter, but clearer; YMMV.  A few notes:

     * It might be said that I'm cheating a little in using the ternary (?:) operator and folding the @basePath+fileName inside of the expand_path() call.  So add two lines to my claim.

     * Yes, I removed the gsub replacing / with --- the file path resolution in DOS/Windows is happy with either.  I don't know why it is common practice to change / to everywhere.   is needed whenever or cmd.exe are involved, because of how they parse switches, but these programs are getting nowhere near the interpretation of this code (nor the vast bulk of C# and VB code for that matter).

     * My primary intent in showing this code is not to belittle or brag, but rather to entice one to further explore the power of ruby.  It is way cool! πŸ™‚

  9. Thanks Ken!! So the code get shorter and shorter πŸ™‚

  10. Dynamic languages prove themselves immensly powerful at places you least expect them to be.&amp;nbsp;I found...

  11. Gabriele says:

    require ’webrick’

  12. true but, here you are not writing  web server in 2 lines. you are using it in 2 lines

  13. George says:

    log.close should be $log.close

  14. Eric says:

     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"



    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.

  15. jkennedy3 says:

    You don't actually need to write your own logger either.  Ruby has a great logger built in.

    require 'logger'

    $log ='server.log')

    $"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




  16. Toby says:

    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.

  17. Arjo says:

    How do you get it running with rails?

  18. Joshua says:

    Hi I Copied this and managed to get it working but it says waiting for server . Anyone know how to fix this?

Skip to main content