Improve documentation, cleanup duplicated code

This commit is contained in:
2021-03-28 15:03:42 +02:00
parent 07b018d2ab
commit cd053bc74e
5 changed files with 96 additions and 39 deletions

View File

@@ -231,7 +231,7 @@ class HTMLDownloadHandler(DownloadHandler):
""" """
try: try:
fp = open(tmp_path, "r", encoding="yeetus") fp = open(tmp_path, "r", encoding=charset)
html = fp.read() html = fp.read()
except UnicodeDecodeError or LookupError: except UnicodeDecodeError or LookupError:
fp = open(tmp_path, "r", encoding=FORMAT, errors="replace") fp = open(tmp_path, "r", encoding=FORMAT, errors="replace")

View File

@@ -1,8 +1,13 @@
class HTTPException(Exception): class HTTPException(Exception):
""" Base class for HTTP exceptions """ """
Base class for HTTP exceptions
"""
class UnhandledHTTPCode(Exception): class UnhandledHTTPCode(Exception):
"""
Exception thrown if HTTP codes are not further processed.
"""
status_code: str status_code: str
headers: str headers: str
cause: str cause: str
@@ -40,10 +45,12 @@ class UnsupportedEncoding(HTTPException):
self.enc_type = enc_type self.enc_type = enc_type
self.encoding = encoding self.encoding = encoding
class UnsupportedProtocol(HTTPException): class UnsupportedProtocol(HTTPException):
""" """
Protocol is not supported Protocol is not supported
""" """
def __init__(self, protocol): def __init__(self, protocol):
self.protocol = protocol self.protocol = protocol
@@ -54,7 +61,9 @@ class IncompleteResponse(HTTPException):
class HTTPServerException(HTTPException): class HTTPServerException(HTTPException):
""" Base class for HTTP Server exceptions """ """
Base class for HTTP Server exceptions
"""
status_code: str status_code: str
message: str message: str
body: str body: str
@@ -66,29 +75,39 @@ class HTTPServerException(HTTPException):
class HTTPServerCloseException(HTTPServerException): class HTTPServerCloseException(HTTPServerException):
""" When thrown, the connection should be closed """ """
When raised, the connection should be closed
"""
class BadRequest(HTTPServerCloseException): class BadRequest(HTTPServerCloseException):
""" Malformed HTTP request""" """
Malformed HTTP request
"""
status_code = 400 status_code = 400
message = "Bad Request" message = "Bad Request"
class Forbidden(HTTPServerException): class Forbidden(HTTPServerException):
""" Request not allowed """ """
Request not allowed
"""
status_code = 403 status_code = 403
message = "Forbidden" message = "Forbidden"
class NotFound(HTTPServerException): class NotFound(HTTPServerException):
""" Resource not found """ """
Resource not found
"""
status_code = 404 status_code = 404
message = "Not Found" message = "Not Found"
class MethodNotAllowed(HTTPServerException): class MethodNotAllowed(HTTPServerException):
""" Method is not allowed """ """
Method is not allowed
"""
status_code = 405 status_code = 405
message = "Method Not Allowed" message = "Method Not Allowed"
@@ -97,37 +116,49 @@ class MethodNotAllowed(HTTPServerException):
class InternalServerError(HTTPServerCloseException): class InternalServerError(HTTPServerCloseException):
""" Internal Server Error """ """
Internal Server Error
"""
status_code = 500 status_code = 500
message = "Internal Server Error" message = "Internal Server Error"
class NotImplemented(HTTPServerException): class NotImplemented(HTTPServerException):
""" Functionality not implemented """ """
Functionality not implemented
"""
status_code = 501 status_code = 501
message = "Not Implemented" message = "Not Implemented"
class HTTPVersionNotSupported(HTTPServerCloseException): 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 status_code = 505
message = "HTTP Version Not Supported" message = "HTTP Version Not Supported"
class Conflict(HTTPServerException): class Conflict(HTTPServerException):
""" Conflict in the current state of the target resource """ """
Conflict in the current state of the target resource
"""
status_code = 409 status_code = 409
message = "Conflict" message = "Conflict"
class NotModified(HTTPServerException): class NotModified(HTTPServerException):
""" Requested resource was not modified """ """
Requested resource was not modified
"""
status_code = 304 status_code = 304
message = "Not Modified" message = "Not Modified"
class InvalidRequestLine(BadRequest): class InvalidRequestLine(BadRequest):
""" Request start-line is invalid """ """
Request start-line is invalid
"""
def __init__(self, line): def __init__(self, line):
self.request_line = line self.request_line = line

View File

@@ -3,8 +3,11 @@ import os
import pathlib import pathlib
import re import re
import urllib import urllib
from datetime import datetime
from time import mktime
from typing import Dict from typing import Dict
from urllib.parse import urlparse, urlsplit from urllib.parse import urlparse, urlsplit
from wsgiref.handlers import format_date_time
from httplib.exceptions import InvalidStatusLine, InvalidResponse, BadRequest, InvalidRequestLine from httplib.exceptions import InvalidStatusLine, InvalidResponse, BadRequest, InvalidRequestLine
from httplib.httpsocket import FORMAT from httplib.httpsocket import FORMAT
@@ -58,7 +61,7 @@ def parse_status_line(line: str):
def parse_request_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. Returns the method, target as ParseResult and HTTP version from the request-line.
@param line: the request-line to be parsed @param line: the request-line to be parsed
@@ -89,6 +92,7 @@ def parse_request_line(line: str):
def parse_headers(lines): def parse_headers(lines):
""" """
Parses the lines from the `lines` iterator as headers. Parses the lines from the `lines` iterator as headers.
@param lines: iterator to retrieve the lines from. @param lines: iterator to retrieve the lines from.
@return: A dictionary with header as key and value as value. @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()) root = pathlib.PurePath(os.getcwd())
rel = path_obj.relative_to(root) rel = path_obj.relative_to(root)
return str(rel) 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)

View File

@@ -3,8 +3,6 @@ import os
import sys import sys
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from datetime import datetime from datetime import datetime
from time import mktime
from wsgiref.handlers import format_date_time
from httplib import parser from httplib import parser
from httplib.exceptions import NotFound, Forbidden, NotModified from httplib.exceptions import NotFound, Forbidden, NotModified
@@ -62,14 +60,6 @@ class AbstractCommand(ABC):
def _conditional_headers(self): def _conditional_headers(self):
pass 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 @abstractmethod
def execute(self): def execute(self):
pass pass
@@ -81,7 +71,7 @@ class AbstractCommand(ABC):
self._process_conditional_headers() self._process_conditional_headers()
message = f"HTTP/1.1 {status} {status_message[status]}\r\n" 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) content_length = len(body)
message += f"Content-Length: {content_length}\r\n" message += f"Content-Length: {content_length}\r\n"

View File

@@ -1,12 +1,9 @@
import logging import logging
import os import os
import sys import sys
from datetime import datetime
from socket import socket from socket import socket
from time import mktime
from typing import Union from typing import Union
from urllib.parse import ParseResultBytes, ParseResult from urllib.parse import ParseResultBytes, ParseResult
from wsgiref.handlers import format_date_time
from httplib import parser from httplib import parser
from httplib.exceptions import MethodNotAllowed, BadRequest, UnsupportedEncoding, NotImplemented, NotFound, \ from httplib.exceptions import MethodNotAllowed, BadRequest, UnsupportedEncoding, NotImplemented, NotFound, \
@@ -21,8 +18,11 @@ METHODS = ("GET", "HEAD", "PUT", "POST")
class RequestHandler: class RequestHandler:
"""
Processes incoming HTTP request messages.
"""
conn: HTTPSocket conn: HTTPSocket
root = os.path.join(os.path.dirname(sys.argv[0]), "public")
def __init__(self, conn: socket, host): def __init__(self, conn: socket, host):
self.conn = ServerSocket(conn, host) self.conn = ServerSocket(conn, host)
@@ -42,15 +42,20 @@ class RequestHandler:
def _handle_message(self, retriever, line): def _handle_message(self, retriever, line):
lines = retriever.retrieve() lines = retriever.retrieve()
# Parse the request-line and headers
(method, target, version) = parser.parse_request_line(line) (method, target, version) = parser.parse_request_line(line)
headers = parser.parse_headers(lines) headers = parser.parse_headers(lines)
# Create the response message object
message = Message(version, method, target, headers, retriever.buffer) message = Message(version, method, target, headers, retriever.buffer)
logging.debug("---request begin---\r\n%s---request end---", "".join(message.raw)) logging.debug("---request begin---\r\n%s---request end---", "".join(message.raw))
# validate if the request is valid
self._validate_request(message) self._validate_request(message)
# The body (if available) hasn't been retrieved up till now.
body = b"" body = b""
if self._has_body(headers): if self._has_body(headers):
try: try:
@@ -64,14 +69,25 @@ class RequestHandler:
message.body = body message.body = body
# completed message # message completed
cmd = command.create(message) cmd = command.create(message)
msg = cmd.execute() 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)) 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) self.conn.conn.sendall(msg)
def _check_request_line(self, method: str, target: Union[ParseResultBytes, ParseResult], version): 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: if method not in METHODS:
raise MethodNotAllowed(METHODS) raise MethodNotAllowed(METHODS)
@@ -91,12 +107,25 @@ class RequestHandler:
raise NotFound(str(target)) raise NotFound(str(target))
def _validate_request(self, msg): 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: if msg.version == "1.1" and "host" not in msg.headers:
raise BadRequest("Missing host header") raise BadRequest("Missing host header")
self._check_request_line(msg.method, msg.target, msg.version) self._check_request_line(msg.method, msg.target, msg.version)
def _has_body(self, headers): 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: if "transfer-encoding" in headers:
return True return True
@@ -106,16 +135,10 @@ class RequestHandler:
return False return False
@staticmethod
def _get_date():
now = datetime.now()
stamp = mktime(now.timetuple())
return format_date_time(stamp)
@staticmethod @staticmethod
def send_error(client: socket, code, message): def send_error(client: socket, code, message):
message = f"HTTP/1.1 {code} {message}\r\n" 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 += "Content-Length: 0\r\n"
message += "\r\n" message += "\r\n"