Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added ._webroot
Binary file not shown.
234 changes: 170 additions & 64 deletions src/server.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,65 @@
# -*- coding: utf-8 -*-
"""Server module to to take HTTP requests."""
import io
import mimetypes
import os
import platform
import socket
import sys
import time

OK_200 = b'200 OK'
ERR_400 = b'400: Bad Request'
ERR_405 = b'405: Method Not Allowed'
ERR_505 = b'505: HTTP Version Not Supported'

# def abs_join(*args):
# """Return the full absolute path of two joined path items."""
# return os.path.abspath(os.path.join(*args))


BUFFER_LENGTH = 8
ADDRINFO = ('127.0.0.1', 5000)


# TODO: update response assemble and tests for more sophisticated
# testing of each component. Assert for required parts while disrgarding
# optional components (but making sure they are in the right place).
# split on \r\n\r\n assert there are 2 pieces
# for first line of response, use maxsplit arg to split
# e.g. "HTTP/1.1 500 Internal Server Error".split(' ', maxsplit=2)
# >>> ["HTTP/1.1", "500", "Internal Server Error"]
PARENT_DIR = os.path.dirname(__file__)
GRANDPARENT_DIR = os.path.join(PARENT_DIR, '..')
WEBROOT_PATH = os.path.join(GRANDPARENT_DIR, 'webroot', '')

# Unicode constants for standard re-usable parts.
CRLF = '\r\n'
GET = 'GET'
HTTP1_1 = 'HTTP/1.1'
HOST = 'Host: '
CONTENT_TYPE = 'Content-Type: {}'
CONTENT_LENGTH = 'Content-Length: {}'

TEXT_HTML = 'text/html'
TEXT_PLAIN = 'text/plain'

# Easy Reference dictionary with int keys for code/message part.
HTTP_CODES = {
200: '200 OK',
400: '400 Bad Request',
404: '404 Not Found',
405: '405 Method Not Allowed',
505: '505 HTTP Version Not Supported',
}

# updated HTML templates to use unicode strings
HTML_TEMPLATE = '''
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<h3>{header}</h3>
<ul>
{list_items}
</ul>
</body>
</html>
'''
HTML_LI_TEMPLATE = '''
<li><a href="{full_path}">{short_path}</a></li>
'''


def server():
Expand All @@ -33,26 +74,40 @@ def server():
try:
while True:
conn, addr = serv_sock.accept()
request = []
request_parts = []
while True:
part = conn.recv(BUFFER_LENGTH)
request.append(part)
# conn.sendall(part)
request_parts.append(part)
if len(part) < BUFFER_LENGTH:
break
request = b''.join(request)
print('Request received:')
print(request.decode('utf-8'))
# Immediately decode all of incoming request into unicode.
# In future, may need to check Content-type of incoming request?
request = b''.join(request_parts).decode('utf-8')
print('Request received:\n{}'.format(request))
try:
uri = parse_request(request)
response = response_ok()
# Here body is already a bytestring -- might be an image.
body, body_type = resolve_uri(uri)
body_length = len(body)
response_headers = response_ok(body_type, body_length)

except ValueError as e:
response = response_error(*e.args)
err_code = e.args[0]
response_headers = response_error(err_code)
body = HTTP_CODES[err_code]

# Only re-encode into bytes on the way out.
if not isinstance(response_headers, bytes):
response_headers = response_headers.encode('utf-8')
if not isinstance(body, bytes):
body = body.encode('utf-8')
response = b''.join([response_headers, body])

conn.sendall(response)
conn.close()
time.sleep(0.01)
except KeyboardInterrupt:
pass
except Exception as e:
print(e.msg)
finally:
try:
conn.close()
Expand All @@ -64,63 +119,114 @@ def server():


def parse_request(request):
"""TOOD: Docstring."""
# only accept GET
# only HTTP/1.1
# validate that proper Host: header was specified
# other requests raise appropriate Python error
# if no conditions arise, should return the URI
method = b''
uri = b''
protocol = b''
headers = b''

"""Parse client request."""
method = ''
uri = ''
protocol = ''
headers = ''

# I was able to simplify this into one try/except block.
# The key was to organize it by error type.
try:
headers, body = request.split(b'\r\n\r\n')
headers, body = request.split(CRLF * 2)
headers = headers.split(CRLF)
try:
first_line = headers[0]
except IndexError:
raise ValueError(400)
method, uri, protocol = first_line.split()
headers = headers[1:]
for h in headers:
if h.startswith(HOST):
break
else:
raise ValueError(400)
except ValueError:
raise ValueError(ERR_400)
raise ValueError(400)
if method != GET:
raise ValueError(405)
if protocol != HTTP1_1:
raise ValueError(505)

try:
headers = headers.split(b'\r\n')
first_line = headers[0]
except (ValueError, IndexError):
raise ValueError(ERR_400)
return uri

try:
method, uri, protocol = first_line.split()
except ValueError:
raise ValueError(ERR_400)

headers = headers[1:]
for h in headers:
if h.startswith(b'Host: '):
break
def resolve_uri(uri):
"""Return a tuple containing content and content type."""
print('Requested URI: ', uri)
print('WEBROOT_PATH:', WEBROOT_PATH)

uri = full_uri(uri)
print('URI after join: ', uri)

if os.path.isfile(uri):
print('uri:', uri)
body_type = mimetypes.guess_type(uri)[0]
body = read_file_bytes(uri)

elif os.path.isdir(uri):
body_type = TEXT_HTML
print('windows?', platform.system())
body = display(next(os.walk(uri)))
print(u'body:', type(body))
else:
raise ValueError(ERR_400)
print(uri, 'is not a file or dir.')
raise ValueError(404)
return (body, body_type)

if method != b'GET':
raise ValueError(ERR_405)
if protocol != b'HTTP/1.1':
raise ValueError(ERR_505)

# Hopefully update this using a reference to __file__
def full_uri(uri):
"""Take a unicode uri from webroot and return the absolute path."""
uri = os.path.join(*uri.split('/'))
uri = os.path.join(WEBROOT_PATH, uri)
return uri


def response_ok():
def web_uri(uri):
"""Take a unicode uri and return the uri from webroot."""
return uri.replace(WEBROOT_PATH, '')


def read_file_bytes(path):
"""Return the data in bytestring format from the file at a give path."""
f = io.open(path, 'rb')
data = f.read()
f.close()
return data


# Response_ok doesn't parse body (might be bytes as for an image).
# References key 200 from HTTP_CODES reference dictionary.
def response_ok(body_type, body_length):
"""Return 'HTTP/1.1 200 OK' for when connection ok."""
return (b'HTTP/1.1 %s\r\n'
b'Content-Type: text/plain\r\n'
b'\r\n'
b'Welcome to Imperial Space, rebel scum.\n'
b'|-o-| <-o-> |-o-|') % OK_200
return CRLF.join([' '.join([HTTP1_1, HTTP_CODES[200]]),
CONTENT_TYPE.format(body_type),
CONTENT_LENGTH.format(body_length),
CRLF])


def response_error(err_msg):
# Response_error take an int as err_code, to reference HTTP_CODES dict.
def response_error(err_code):
"""Return 'Internal Server Error' for when problem occurs."""
return (b'HTTP/1.1 %s\r\n'
b'Content-Type: text/plain\r\n'
b'\r\n'
b'Death Star Error. Please build again.') % err_msg
return CRLF.join([' '.join([HTTP1_1, HTTP_CODES[err_code]]),
CONTENT_TYPE.format('text/plain'),
CRLF,
HTTP_CODES[err_code]])


def display(threeple):
"""Split dir threeple into components and return as HTML."""
cur_dir, dir_subdir, dir_files = threeple
cur_dir = web_uri(cur_dir)
dir_list = []
for i in dir_subdir + dir_files:
full_path = os.path.join(cur_dir, i)
html_li = HTML_LI_TEMPLATE.format(full_path=full_path, short_path=i)
dir_list.append(html_li)

return HTML_TEMPLATE.format(header=cur_dir,
list_items=''.join(dir_list))


if __name__ == '__main__':
Expand Down
Loading