This commit is contained in:
2021-03-26 18:25:03 +01:00
parent 7476870acc
commit fdbd865889
11 changed files with 297 additions and 136 deletions

View File

@@ -33,6 +33,10 @@ class HTTPServerException(Exception):
""" Base class for HTTP Server exceptions """
status_code: str
message: str
body: str
def __init__(self, body=""):
self.body = body
class BadRequest(HTTPServerException):
@@ -66,3 +70,28 @@ class NotFound(HTTPServerException):
""" Resource not found """
status_code = 404
message = "Not Found"
class Forbidden(HTTPServerException):
""" Request not allowed """
status_code = 403
message = "Forbidden"
class Conflict(HTTPServerException):
""" Conflict in the current state of the target resource """
status_code = 409
message = "Conflict"
class HTTPVersionNotSupported(HTTPServerException):
""" The server does not support the major version HTTP used in the request message """
status_code = 505
message = "HTTP Version Not Supported"
class InvalidRequestLine(BadRequest):
""" Request start-line is invalid """
def __init__(self, line):
self.request_line = line

View File

@@ -3,6 +3,8 @@ import socket
from io import BufferedReader
from typing import Tuple
from httplib.exceptions import BadRequest
BUFSIZE = 4096
TIMEOUT = 3
FORMAT = "UTF-8"
@@ -61,17 +63,28 @@ class HTTPSocket:
def read(self, size=BUFSIZE, blocking=True) -> bytes:
if blocking:
return self.file.read(size)
buffer = self.file.read(size)
else:
buffer = self.file.read1(size)
return self.file.read1(size)
if len(buffer) == 0:
raise ConnectionAbortedError
return buffer
def read_line(self):
return str(self.read_bytes_line(), FORMAT)
try:
line = str(self.read_bytes_line(), FORMAT)
except UnicodeDecodeError:
# Expected UTF-8
raise BadRequest()
return line
def read_bytes_line(self) -> bytes:
line = self.file.readline(MAXLINE + 1)
if len(line) > MAXLINE:
raise InvalidResponse("Line too long")
elif len(line) == 0:
raise ConnectionAbortedError
return line

View File

@@ -1,18 +1,35 @@
from abc import ABC
from typing import Dict
from urllib.parse import SplitResult
class Message:
class Message(ABC):
version: str
status: int
msg: str
headers: Dict[str, str]
raw: str
body: bytes
def __init__(self, version: str, status: int, msg: str, headers: Dict[str, str], raw=None, body: bytes = None):
def __init__(self, version: str, headers: Dict[str, str], raw=None, body: bytes = None):
self.version = version
self.status = status
self.msg = msg
self.headers = headers
self.raw = raw
self.body = body
class ClientMessage(Message):
status: int
msg: str
def __init__(self, version: str, status: int, msg: str, headers: Dict[str, str], raw=None, body: bytes = None):
super().__init__(version, headers, raw, body)
self.status = status
class ServerMessage(Message):
method: str
target: SplitResult
def __init__(self, version: str, method: str, target, headers: Dict[str, str], raw=None, body: bytes = None):
super().__init__(version, headers, raw, body)
self.method = method
self.target = target

View File

@@ -3,7 +3,7 @@ import os.path
import re
from urllib.parse import urlparse, urlsplit
from httplib.exceptions import InvalidStatusLine, InvalidResponse, BadRequest
from httplib.exceptions import InvalidStatusLine, InvalidResponse, BadRequest, InvalidRequestLine
from httplib.httpsocket import HTTPSocket
@@ -48,7 +48,7 @@ def parse_status_line(line: str):
if len(split) < 3:
raise InvalidStatusLine(line) # TODO fix exception
(http_version, status, reason) = split
http_version, status, reason = split
if not _is_valid_http_version(http_version):
raise InvalidStatusLine(line)
@@ -63,11 +63,12 @@ def parse_status_line(line: str):
return version, status, reason
def parse_request_line(client: HTTPSocket):
line, (method, target, version) = _get_start_line(client)
logging.debug("Parsed request-line=%r, method=%r, target=%r, version=%r", line, method, target, version)
def parse_request_line(line: str):
split = list(filter(None, line.rstrip().split(" ", 2)))
if len(split) < 3:
raise InvalidRequestLine(line)
method, target, version = split
if method not in ("CONNECT", "DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT", "TRACE"):
raise BadRequest()
@@ -146,7 +147,7 @@ def parse_request_headers(client: HTTPSocket):
def get_headers(client: HTTPSocket):
headers = []
# first header after the status-line may not contain a space
# first header after the status-line may not start with a space
while True:
line = client.read_line()
if line[0].isspace():
@@ -181,21 +182,28 @@ def get_headers(client: HTTPSocket):
def parse_headers(lines):
headers = []
# first header after the status-line may not contain a space
for line in lines:
if line[0].isspace():
continue
else:
break
for line in lines:
if line in ("\r\n", "\n", " "):
break
try:
# first header after the start-line may not start with a space
line = next(lines)
while True:
if line[0].isspace():
continue
else:
break
if line[0].isspace():
headers[-1] = headers[-1].rstrip("\r\n")
while True:
if line in ("\r\n", "\n", ""):
break
headers.append(line.lstrip())
if line[0].isspace():
headers[-1] = headers[-1].rstrip("\r\n")
headers.append(line.lstrip())
line = next(lines)
except StopIteration:
# No more lines to be parsed
pass
result = {}
header_str = "".join(headers)

View File

@@ -44,25 +44,36 @@ class Retriever(ABC):
class PreambleRetriever(Retriever):
client: HTTPSocket
buffer: []
_buffer: []
@property
def buffer(self):
tmp_buffer = self._buffer
self._buffer = []
return tmp_buffer
def __init__(self, client: HTTPSocket):
super().__init__(client)
self.client = client
self.buffer = []
self._buffer = []
def retrieve(self):
line = self.client.read_line()
while True:
self.buffer.append(line)
self._buffer.append(line)
if line in ("\r\n", "\n", ""):
break
return line
yield line
line = self.client.read_line()
def reset_buffer(self, line):
self._buffer.clear()
self._buffer.append(line)
class ContentLengthRetriever(Retriever):
length: int