# Ruby webserver app

I am building a web app in Ruby but without Rails. So far I just have a very basic web server. What improvements can I make to it?

require 'socket'
require 'uri'

# Files will be served from this directory
WEB_ROOT = './public'

# Map extensions to their content type
CONTENT_TYPE_MAPPING = {
'html' => 'text/html',
'txt' => 'text/plain',
'png' => 'image/png',
'jpg' => 'image/jpeg'
}

# Treat as binary data if content type cannot be found
DEFAULT_CONTENT_TYPE = 'application/octet-stream'

# This helper function parses the extension of the
# requested file and then looks up its content type.

def content_type(path)
ext = File.extname(path).split(".").last
CONTENT_TYPE_MAPPING.fetch(ext, DEFAULT_CONTENT_TYPE)
end

# This helper function parses the Request-Line and
# generates a path to a file on the server.

def requested_file(request_line)
request_uri  = request_line.split(" ")[1]
path       = URI.unescape(URI(request_uri).path)

clean = []

# Split the path into components
parts = path.split("/")

parts.each do |part|
# skip any empty or current directory (".") path components
next if part.empty? || part == '.'
# If the path component goes up one directory level (".."),
# remove the last clean component.
# Otherwise, add the component to the Array of clean components
part == '..' ? clean.pop : clean << part
end

# return the web root joined to the clean path
File.join(WEB_ROOT, *clean)
end

# Except where noted below, the general approach of
# handling requests and generating responses is
# similar to that of the "Hello World" example
# shown earlier.

server = TCPServer.new('localhost', 2000)

loop do
socket       = server.accept
request_line = socket.gets

STDERR.puts request_line

path = requested_file(request_line)

path = File.join(path, 'index.html') if File.directory?(path)

# Make sure the file exists and is not a directory
# before attempting to open it.
if File.exist?(path) && !File.directory?(path)
File.open(path, "rb") do |file|
socket.print "HTTP/1.1 200 OK\r\n" +
"Content-Type: #{content_type(file)}\r\n" +
"Content-Length: #{file.size}\r\n" +
"Connection: close\r\n"

socket.print "\r\n"

# write the contents of the file to the socket
IO.copy_stream(file, socket)
end
else
message = "The file you were looking for does not exist brother\n"

# respond with a 404 error code to indicate the file does not exist
"Content-Type: text/plain\r\n" +
"Content-Length: #{message.size}\r\n" +
"Connection: close\r\n"

socket.print "\r\n"

socket.print message
end

socket.close
end

• Is the STDERR.puts just for debug? It looks out of place. If it is for debug, consider using a logging tool and keep STDERR clean. – Devon Parsons Mar 9 '15 at 12:45
• Hey Devon Yes it is for debugging and to have some type of server logs... – Joel Mar 9 '15 at 13:03
• Having the file extension ".jpg" does not ensure the file is actually a JPEG file. Of course, since you control which files are served, you can make sure you always serve the right type of file, but it's something to keep in mind. I believe the gem "mime-types" makes an educated guess of the actual mime type of a file. – britishtea Mar 11 '15 at 18:25
• Ok that makes sense thank you! Any chance you could answer this question for me? stackoverflow.com/questions/28966729/… I would really appreciate it.Thanks in advance – Joel Mar 11 '15 at 19:02

A full HTTP 1.1 implementation is extremely complex. I'm not going to remark on incompleteness of features.

Two problems become apparent if you try to fetch a file for which the server does not have read permission: the server crashes without sending a response, and the level of abstraction to fix that crash elegantly is lacking. The main event loop should be kept as short as possible, with the request handler in its own function. Instead of checking to see if the file is likely to be readable, just do it — let the operating system enforce the permissions, and handle any possible exceptions. Finally, I suggest writing a send_response function so that handle_request doesn't have to worry about details such as where to put the CR-LF line terminators.

require 'stringio'

socket.print("HTTP/1.1 #{status_line}\r\n")
headers.each { |k, v| socket.print("#{k}: #{v}\r\n") }
socket.print("\r\n")
end

def handle_request(socket, request_line)
STDERR.puts request_line

path = requested_file(request_line)
path = File.join(path, 'index.html') if File.directory?(path)

begin
File.open(path, 'rb') do |resource|
send_response(socket, '200 OK', {
'Content-Type' => content_type(resource),
'Content-Length' => resource.size,
'Connection' => 'close',
},
resource
)
end
rescue Errno::EACCES
message = "Permission denied"
send_response(socket, '403 Forbidden', {
'Content-Type' => 'text/plain',
'Content-Length' => message.size,
'Connection' => 'close'
},
StringIO.new(message)
)
rescue Errno::ENOENT, Errno::EISDIR
message = "The file you were looking for does not exist brother"
'Content-Type' => 'text/plain',
'Content-Length' => message.size,
'Connection' => 'close'
},
StringIO.new(message)
)
end
end

# Except where noted below, the general approach of
# handling requests and generating responses is
# similar to that of the "Hello World" example
# shown earlier.

server = TCPServer.new('localhost', 2000)

loop do
begin
socket = server.accept
handle_request(socket, socket.gets)
rescue StandardError => e
STDERR.puts e
ensure
socket.close unless socket.nil?
end
end