1
\$\begingroup\$

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
 socket.print "HTTP/1.1 404 Not Found\r\n" +
 "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
200_success
145k22 gold badges190 silver badges478 bronze badges
asked Mar 7, 2015 at 22:45
\$\endgroup\$
4
  • \$\begingroup\$ 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. \$\endgroup\$ Commented Mar 9, 2015 at 12:45
  • \$\begingroup\$ Hey Devon Yes it is for debugging and to have some type of server logs... \$\endgroup\$ Commented Mar 9, 2015 at 13:03
  • \$\begingroup\$ 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. \$\endgroup\$ Commented Mar 11, 2015 at 18:25
  • \$\begingroup\$ 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 \$\endgroup\$ Commented Mar 11, 2015 at 19:02

1 Answer 1

1
\$\begingroup\$

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'
def send_response(socket, status_line, headers, payload)
 socket.print("HTTP/1.1 #{status_line}\r\n")
 headers.each { |k, v| socket.print("#{k}: #{v}\r\n") }
 socket.print("\r\n")
 IO.copy_stream(payload, socket)
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"
 send_response(socket, '404 Not Found', {
 '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
answered Mar 9, 2015 at 8:02
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.