From cd053bc74e79596b53b744d81a11327e3b25249b Mon Sep 17 00:00:00 2001 From: Arthur Bols Date: Sun, 28 Mar 2021 15:03:42 +0200 Subject: [PATCH] Improve documentation, cleanup duplicated code --- client/responsehandler.py | 2 +- httplib/exceptions.py | 57 ++++++++++++++++++++++++++++++--------- httplib/parser.py | 15 ++++++++++- server/command.py | 12 +-------- server/requesthandler.py | 49 ++++++++++++++++++++++++--------- 5 files changed, 96 insertions(+), 39 deletions(-) diff --git a/client/responsehandler.py b/client/responsehandler.py index 067e153..719d80a 100644 --- a/client/responsehandler.py +++ b/client/responsehandler.py @@ -231,7 +231,7 @@ class HTMLDownloadHandler(DownloadHandler): """ try: - fp = open(tmp_path, "r", encoding="yeetus") + fp = open(tmp_path, "r", encoding=charset) html = fp.read() except UnicodeDecodeError or LookupError: fp = open(tmp_path, "r", encoding=FORMAT, errors="replace") diff --git a/httplib/exceptions.py b/httplib/exceptions.py index d31497c..555d989 100644 --- a/httplib/exceptions.py +++ b/httplib/exceptions.py @@ -1,8 +1,13 @@ class HTTPException(Exception): - """ Base class for HTTP exceptions """ + """ + Base class for HTTP exceptions + """ class UnhandledHTTPCode(Exception): + """ + Exception thrown if HTTP codes are not further processed. + """ status_code: str headers: str cause: str @@ -40,10 +45,12 @@ class UnsupportedEncoding(HTTPException): self.enc_type = enc_type self.encoding = encoding + class UnsupportedProtocol(HTTPException): """ Protocol is not supported """ + def __init__(self, protocol): self.protocol = protocol @@ -54,7 +61,9 @@ class IncompleteResponse(HTTPException): class HTTPServerException(HTTPException): - """ Base class for HTTP Server exceptions """ + """ + Base class for HTTP Server exceptions + """ status_code: str message: str body: str @@ -66,29 +75,39 @@ class HTTPServerException(HTTPException): class HTTPServerCloseException(HTTPServerException): - """ When thrown, the connection should be closed """ + """ + When raised, the connection should be closed + """ class BadRequest(HTTPServerCloseException): - """ Malformed HTTP request""" + """ + Malformed HTTP request + """ status_code = 400 message = "Bad Request" class Forbidden(HTTPServerException): - """ Request not allowed """ + """ + Request not allowed + """ status_code = 403 message = "Forbidden" class NotFound(HTTPServerException): - """ Resource not found """ + """ + Resource not found + """ status_code = 404 message = "Not Found" class MethodNotAllowed(HTTPServerException): - """ Method is not allowed """ + """ + Method is not allowed + """ status_code = 405 message = "Method Not Allowed" @@ -97,37 +116,49 @@ class MethodNotAllowed(HTTPServerException): class InternalServerError(HTTPServerCloseException): - """ Internal Server Error """ + """ + Internal Server Error + """ status_code = 500 message = "Internal Server Error" class NotImplemented(HTTPServerException): - """ Functionality not implemented """ + """ + Functionality not implemented + """ status_code = 501 message = "Not Implemented" class HTTPVersionNotSupported(HTTPServerCloseException): - """ The server does not support the major version HTTP used in the request message """ + """ + The server does not support the major version HTTP used in the request message + """ status_code = 505 message = "HTTP Version Not Supported" class Conflict(HTTPServerException): - """ Conflict in the current state of the target resource """ + """ + Conflict in the current state of the target resource + """ status_code = 409 message = "Conflict" class NotModified(HTTPServerException): - """ Requested resource was not modified """ + """ + Requested resource was not modified + """ status_code = 304 message = "Not Modified" class InvalidRequestLine(BadRequest): - """ Request start-line is invalid """ + """ + Request start-line is invalid + """ def __init__(self, line): self.request_line = line diff --git a/httplib/parser.py b/httplib/parser.py index 81fbc36..d77c88b 100644 --- a/httplib/parser.py +++ b/httplib/parser.py @@ -3,8 +3,11 @@ import os import pathlib import re import urllib +from datetime import datetime +from time import mktime from typing import Dict from urllib.parse import urlparse, urlsplit +from wsgiref.handlers import format_date_time from httplib.exceptions import InvalidStatusLine, InvalidResponse, BadRequest, InvalidRequestLine from httplib.httpsocket import FORMAT @@ -58,7 +61,7 @@ def parse_status_line(line: str): def parse_request_line(line: str): """ - Parses the specified line as and HTTP request-line. + Parses the specified line as an HTTP request-line. Returns the method, target as ParseResult and HTTP version from the request-line. @param line: the request-line to be parsed @@ -89,6 +92,7 @@ def parse_request_line(line: str): def parse_headers(lines): """ Parses the lines from the `lines` iterator as headers. + @param lines: iterator to retrieve the lines from. @return: A dictionary with header as key and value as value. """ @@ -218,3 +222,12 @@ def get_relative_save_path(path: str): root = pathlib.PurePath(os.getcwd()) rel = path_obj.relative_to(root) return str(rel) + + +def get_date(): + """ + Returns a string representation of the current date according to RFC 1123. + """ + now = datetime.now() + stamp = mktime(now.timetuple()) + return format_date_time(stamp) diff --git a/server/command.py b/server/command.py index e51720d..fa46580 100644 --- a/server/command.py +++ b/server/command.py @@ -3,8 +3,6 @@ import os import sys from abc import ABC, abstractmethod from datetime import datetime -from time import mktime -from wsgiref.handlers import format_date_time from httplib import parser from httplib.exceptions import NotFound, Forbidden, NotModified @@ -62,14 +60,6 @@ class AbstractCommand(ABC): def _conditional_headers(self): pass - def _get_date(self): - """ - Returns a string representation of the current date according to RFC 1123. - """ - now = datetime.now() - stamp = mktime(now.timetuple()) - return format_date_time(stamp) - @abstractmethod def execute(self): pass @@ -81,7 +71,7 @@ class AbstractCommand(ABC): self._process_conditional_headers() message = f"HTTP/1.1 {status} {status_message[status]}\r\n" - message += f"Date: {self._get_date()}\r\n" + message += f"Date: {parser.get_date()}\r\n" content_length = len(body) message += f"Content-Length: {content_length}\r\n" diff --git a/server/requesthandler.py b/server/requesthandler.py index 74c07f4..d9d9212 100644 --- a/server/requesthandler.py +++ b/server/requesthandler.py @@ -1,12 +1,9 @@ import logging import os import sys -from datetime import datetime from socket import socket -from time import mktime from typing import Union from urllib.parse import ParseResultBytes, ParseResult -from wsgiref.handlers import format_date_time from httplib import parser from httplib.exceptions import MethodNotAllowed, BadRequest, UnsupportedEncoding, NotImplemented, NotFound, \ @@ -21,8 +18,11 @@ METHODS = ("GET", "HEAD", "PUT", "POST") class RequestHandler: + """ + Processes incoming HTTP request messages. + """ + conn: HTTPSocket - root = os.path.join(os.path.dirname(sys.argv[0]), "public") def __init__(self, conn: socket, host): self.conn = ServerSocket(conn, host) @@ -42,15 +42,20 @@ class RequestHandler: def _handle_message(self, retriever, line): lines = retriever.retrieve() + + # Parse the request-line and headers (method, target, version) = parser.parse_request_line(line) headers = parser.parse_headers(lines) + # Create the response message object message = Message(version, method, target, headers, retriever.buffer) logging.debug("---request begin---\r\n%s---request end---", "".join(message.raw)) + # validate if the request is valid self._validate_request(message) + # The body (if available) hasn't been retrieved up till now. body = b"" if self._has_body(headers): try: @@ -64,14 +69,25 @@ class RequestHandler: message.body = body - # completed message - + # message completed cmd = command.create(message) msg = cmd.execute() + logging.debug("---response begin---\r\n%s\r\n---response end---", msg.split(b"\r\n\r\n", 1)[0].decode(FORMAT)) + # Send the response message self.conn.conn.sendall(msg) def _check_request_line(self, method: str, target: Union[ParseResultBytes, ParseResult], version): + """ + Checks if the request-line is valid. Throws an appriopriate exception if not. + @param method: HTTP request method + @param target: The request target + @param version: The HTTP version + @raise MethodNotAllowed: if the method is not any of the allowed methods in `METHODS` + @raise HTTPVersionNotSupported: If the HTTP version is not supported by this server + @raise BadRequest: If the scheme of the target is not supported + @raise NotFound: If the target is not found on this server + """ if method not in METHODS: raise MethodNotAllowed(METHODS) @@ -91,12 +107,25 @@ class RequestHandler: raise NotFound(str(target)) def _validate_request(self, msg): + """ + Validates the message request-line and headers. Throws an error if the message is invalid. + + @see: _check_request_line for exceptions raised when validating the request-line. + @param msg: the message to validate + @raise BadRequest: if HTTP 1.1 and the Host header is missing + """ + if msg.version == "1.1" and "host" not in msg.headers: raise BadRequest("Missing host header") self._check_request_line(msg.method, msg.target, msg.version) def _has_body(self, headers): + """ + Check if the headers notify the existing of a message body. + @param headers: the headers to check + @return: True if the message has a body. False otherwise. + """ if "transfer-encoding" in headers: return True @@ -106,16 +135,10 @@ class RequestHandler: return False - @staticmethod - def _get_date(): - now = datetime.now() - stamp = mktime(now.timetuple()) - return format_date_time(stamp) - @staticmethod def send_error(client: socket, code, message): message = f"HTTP/1.1 {code} {message}\r\n" - message += RequestHandler._get_date() + "\r\n" + message += parser.get_date() + "\r\n" message += "Content-Length: 0\r\n" message += "\r\n"