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:
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")

View File

@@ -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

View File

@@ -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)

View File

@@ -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"

View File

@@ -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"