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
\$\endgroup\$
  • \$\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\$ – Devon Parsons Mar 9 '15 at 12:45
  • \$\begingroup\$ Hey Devon Yes it is for debugging and to have some type of server logs... \$\endgroup\$ – Joel Mar 9 '15 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\$ – britishtea Mar 11 '15 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\$ – Joel Mar 11 '15 at 19:02
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
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.