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
- Multi-threaded server
- 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)
- 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
- Support for page not found
Issues
- No security at all!!!! other than redumentary code to stop users from accessing files outside of the base folder
- No concept of default file (e.g. index.html) or directory listing in case request comes to access a folder
- There is some code in this that is windows specific
- 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? TIAAnonymous
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 doAnonymous
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 removedAnonymous
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.&nbsp;I found...Anonymous
September 09, 2006
require ’webrick’
WEBrick::HTTPServer.new.startAnonymous
September 10, 2006
true but, here you are not writing web server in 2 lines. you are using it in 2 linesAnonymous
October 15, 2006
log.close should be $log.closeAnonymous
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 ... endAnonymous
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?