From 172ab11a22aff958334523f091ecf0d9a1837a12 Mon Sep 17 00:00:00 2001 From: Alexander Boone Date: Thu, 2 Apr 2020 22:04:08 -0700 Subject: [PATCH 1/2] created a functioning http server that serves up files and directories on localhost --- http_server.py | 99 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 63 insertions(+), 36 deletions(-) diff --git a/http_server.py b/http_server.py index 58d7386..1477b5e 100644 --- a/http_server.py +++ b/http_server.py @@ -1,6 +1,9 @@ import socket import sys import traceback +import mimetypes +import os +from pathlib import Path def response_ok(body=b"This is a minimal response", mimetype=b"text/plain"): """ @@ -19,21 +22,31 @@ def response_ok(body=b"This is a minimal response", mimetype=b"text/plain"): ''' """ - # TODO: Implement response_ok - return b"" + return b"\r\n".join([ + b"HTTP/1.1 200 OK", + b"Content-Type: " + mimetype, + b"", + body + ]) def response_method_not_allowed(): """Returns a 405 Method Not Allowed response""" - # TODO: Implement response_method_not_allowed - return b"" + return b"\r\n".join([ + b"HTTP/1.1 405 Method Not Allowed", + b"", + b"You can't do that on this server!" + ]) def response_not_found(): """Returns a 404 Not Found response""" - # TODO: Implement response_not_found - return b"" + return b"\r\n".join([ + b"HTTP/1.1 404 Not Found", + b"", + b"The requested resource could not be found." + ]) def parse_request(request): @@ -44,8 +57,12 @@ def parse_request(request): NotImplementedError if the method of the request is not GET. """ - # TODO: implement parse_request - return "" + method, path, version = request.split("\r\n")[0].split(" ") + + if method != "GET": + raise NotImplementedError + + return path def response_path(path): """ @@ -74,20 +91,32 @@ def response_path(path): response_path('/a_page_that_doesnt_exist.html') -> Raises a NameError """ - - # TODO: Raise a NameError if the requested content is not present - # under webroot. - - # TODO: Fill in the appropriate content and mime_type give the path. - # See the assignment guidelines for help on "mapping mime-types", though - # you might need to create a special case for handling make_time.py - # - # If the path is "make_time.py", then you may OPTIONALLY return the - # result of executing `make_time.py`. But you need only return the - # CONTENTS of `make_time.py`. - - content = b"not implemented" - mime_type = b"not implemented" + fullpath = 'webroot' + path + + # If path is NOT a file or directory in webroot + if not Path(fullpath).is_file() and not Path(fullpath).is_dir(): + raise NameError + + # If the path is a directory in webroot + if Path(fullpath).is_dir(): + mime_type = b'text/plain' + filenames = [file.encode() for file in os.listdir(fullpath)] + content = b'\n'.join(filenames) + + # If the path is a file in webroot + if Path(fullpath).is_file(): + filename = fullpath.split('/')[-1:] + mime_type = mimetypes.guess_type(filename[0])[0].encode() + + content = [] + with open(fullpath,'r+b') as file: + while True: + chunk = file.read(16) + if not chunk: + break + else: + content.append(chunk) + content = b''.join(content) return content, mime_type @@ -114,26 +143,24 @@ def server(log_buffer=sys.stderr): if '\r\n\r\n' in request: break - print("Request received:\n{}\n\n".format(request)) - # TODO: Use parse_request to retrieve the path from the request. - - # TODO: Use response_path to retrieve the content and the mimetype, - # based on the request path. + try: + # Parse the path from the request and determine the content/mimetype + path = parse_request(request) + content, mimetype = response_path(path) - # TODO; If parse_request raised a NotImplementedError, then let - # response be a method_not_allowed response. If response_path raised - # a NameError, then let response be a not_found response. Else, - # use the content and mimetype from response_path to build a - # response_ok. - response = response_ok( - body=b"Welcome to my web server", - mimetype=b"text/plain" - ) + # If no exceptions occur, create response + response = response_ok(content, mimetype) + except NotImplementedError: + response = response_method_not_allowed() # 405 Error + except NameError: + response = response_not_found() # 404 Error + # Send response conn.sendall(response) + except: traceback.print_exc() finally: From 0d975221197e482c8518cf7c1aa999db029693a6 Mon Sep 17 00:00:00 2001 From: Alexander Boone Date: Thu, 2 Apr 2020 22:13:43 -0700 Subject: [PATCH 2/2] conformed to PEP8 compliance using pylint and flake8 --- http_server.py | 26 ++++++++++++++++---------- tests.py | 18 +++++++++++------- unit-tests.py | 2 +- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/http_server.py b/http_server.py index 1477b5e..0a8325c 100644 --- a/http_server.py +++ b/http_server.py @@ -1,3 +1,8 @@ +''' +This module creates a server socket, listens for incoming requests, +and generates responses to be sent over HTTP. +''' + import socket import sys import traceback @@ -5,6 +10,7 @@ import os from pathlib import Path + def response_ok(body=b"This is a minimal response", mimetype=b"text/plain"): """ returns a basic HTTP response @@ -29,6 +35,7 @@ def response_ok(body=b"This is a minimal response", mimetype=b"text/plain"): body ]) + def response_method_not_allowed(): """Returns a 405 Method Not Allowed response""" @@ -64,6 +71,7 @@ def parse_request(request): return path + def response_path(path): """ This method should return appropriate content and a mime type. @@ -107,21 +115,21 @@ def response_path(path): if Path(fullpath).is_file(): filename = fullpath.split('/')[-1:] mime_type = mimetypes.guess_type(filename[0])[0].encode() - + content = [] - with open(fullpath,'r+b') as file: + with open(fullpath, 'r+b') as file: while True: chunk = file.read(16) if not chunk: break - else: - content.append(chunk) + content.append(chunk) content = b''.join(content) return content, mime_type def server(log_buffer=sys.stderr): + '''Create a server socket and accept incoming connections''' address = ('127.0.0.1', 10000) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -147,16 +155,16 @@ def server(log_buffer=sys.stderr): print("Request received:\n{}\n\n".format(request)) try: - # Parse the path from the request and determine the content/mimetype + # Parse the path from the request path = parse_request(request) content, mimetype = response_path(path) # If no exceptions occur, create response response = response_ok(content, mimetype) except NotImplementedError: - response = response_method_not_allowed() # 405 Error + response = response_method_not_allowed() # 405 Error except NameError: - response = response_not_found() # 404 Error + response = response_not_found() # 404 Error # Send response conn.sendall(response) @@ -164,7 +172,7 @@ def server(log_buffer=sys.stderr): except: traceback.print_exc() finally: - conn.close() + conn.close() except KeyboardInterrupt: sock.close() @@ -176,5 +184,3 @@ def server(log_buffer=sys.stderr): if __name__ == '__main__': server() sys.exit(0) - - diff --git a/tests.py b/tests.py index 21da57e..27c79c0 100644 --- a/tests.py +++ b/tests.py @@ -49,7 +49,6 @@ def test_post_yields_method_not_allowed(self): self.assertEqual(response.getcode(), 405) - def test_get_sample_text_content(self): """ A call to /sample.txt returns the correct body @@ -79,7 +78,8 @@ def test_get_sample_text_mime_type(self): response = self.get_response(web_path) self.assertEqual(response.getcode(), 200, error_comment) - self.assertEqual(response.getheader('Content-Type'), 'text/plain', error_comment) + self.assertEqual(response.getheader('Content-Type'), + 'text/plain', error_comment) def test_get_sample_scene_balls_jpeg(self): """ @@ -110,7 +110,8 @@ def test_get_sample_scene_balls_jpeg_mime_type(self): response = self.get_response(web_path) self.assertEqual(response.getcode(), 200, error_comment) - self.assertEqual(response.getheader('Content-Type'), 'image/jpeg', error_comment) + self.assertEqual(response.getheader('Content-Type'), + 'image/jpeg', error_comment) def test_get_sample_1_png(self): """ @@ -141,11 +142,13 @@ def test_get_sample_1_png_mime_type(self): response = self.get_response(web_path) self.assertEqual(response.getcode(), 200, error_comment) - self.assertEqual(response.getheader('Content-Type'), 'image/png', error_comment) + self.assertEqual(response.getheader('Content-Type'), + 'image/png', error_comment) def test_get_404(self): """ - A call to /asdf.txt (a file which does not exist in webroot) yields a 404 error + A call to /asdf.txt (a file which does not exist in webroot) + yields a 404 error """ file = 'asdf.txt' @@ -158,7 +161,8 @@ def test_get_404(self): def test_images_index(self): """ - A call to /images/ yields a list of files in the images directory + A call to /images/ yields a list of files + in the images directory """ directory = 'images' @@ -190,7 +194,7 @@ def test_root_index(self): def test_ok_response_at_root_index(self): """ - A call to / at least yields a 200 OK response + A call to / at least yields a 200 OK response """ directory = '' diff --git a/unit-tests.py b/unit-tests.py index a0c657a..9e823fb 100644 --- a/unit-tests.py +++ b/unit-tests.py @@ -53,7 +53,7 @@ def test_response_path_file(self): content, mime_type = http_server.response_path(path) self.assertEqual(b"text/html", mime_type) - + with open(os.path.join("webroot", "a_web_page.html"), "rb") as f: self.assertEqual(f.read(), content)