Improve documentation, cleanup duplicated code
This commit is contained in:
@@ -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")
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
|
||||
|
Reference in New Issue
Block a user