Files
CN2021/client/command.py

257 lines
6.9 KiB
Python

import logging
from abc import ABC, abstractmethod
from typing import Dict, Tuple
from urllib.parse import urlparse
from client.httpclient import HTTPClient
from httplib import parser
from httplib.exceptions import InvalidResponse, InvalidStatusLine, UnsupportedEncoding, UnsupportedProtocol
from httplib.httpsocket import FORMAT
from httplib.message import ResponseMessage as Message
from httplib.retriever import PreambleRetriever
sockets: Dict[str, HTTPClient] = {}
def create(method: str, url: str, port):
"""
Create a corresponding Command instance of the specified HTTP `method` with the specified `url` and `port`.
@param method: The command type to create
@param url: The url for the command
@param port: The port for the command
"""
uri = parser.get_uri(url)
if method == "GET":
return GetCommand(uri, port)
elif method == "HEAD":
return HeadCommand(uri, port)
elif method == "POST":
return PostCommand(uri, port)
elif method == "PUT":
return PutCommand(uri, port)
else:
raise ValueError("Unknown HTTP method")
class AbstractCommand(ABC):
"""
A class representing the command for sending an HTTP request.
"""
_uri: str
_host: str
_path: str
_port: int
def __init__(self, uri: str, port):
self.uri = uri
self._port = int(port)
@property
def uri(self):
return self._uri
@uri.setter
def uri(self, value):
self._uri = value
self._host, self._port, self._path = parser.parse_uri(value)
@property
def host(self):
return self._host
@property
def path(self):
return self._path
@property
def port(self):
return self._port
@property
@abstractmethod
def method(self):
pass
def execute(self, sub_request=False):
"""
Creates and sends the HTTP message for this Command.
@param sub_request: If this execution is in function of a prior command.
"""
client = sockets.get(self.host)
if client and client.is_closed():
sockets.pop(self.host)
client = None
if not client:
logging.info("Connecting to %s", self.host)
client = HTTPClient(self.host)
client.conn.connect((self.host, self.port))
logging.info("Connected.")
sockets[self.host] = client
else:
logging.info("Reusing socket for %s", self.host)
message = f"{self.method} {self.path} HTTP/1.1\r\n"
message += f"Host: {self.host}:{self.port}\r\n"
message += "Accept: */*\r\n"
message += "Accept-Encoding: identity\r\n"
encoded_msg = self._build_message(message)
logging.debug("---request begin---\r\n%s---request end---", encoded_msg.decode(FORMAT))
client.conn.sendall(encoded_msg)
logging.info("HTTP request sent, awaiting response...")
try:
self._await_response(client)
except InvalidResponse as e:
logging.error("Response could not be parsed")
logging.debug("", exc_info=e)
except InvalidStatusLine as e:
logging.error("Invalid status-line in response")
logging.debug("", exc_info=e)
except UnsupportedEncoding as e:
logging.error("Unsupported encoding in response")
logging.debug("", exc_info=e)
except UnsupportedProtocol as e:
logging.error("Unsupported protocol: %s", e.protocol)
logging.debug("", exc_info=e)
finally:
if not sub_request:
client.close()
def _get_preamble(self, client):
"""
Returns the preamble (start-line and headers) of the response of this command.
@param client: the client object to retrieve from
@return: A Message object containing the HTTP-version, status code, status message, headers and buffer
"""
retriever = PreambleRetriever(client)
lines = retriever.retrieve()
(version, status, msg) = parser.parse_status_line(next(lines))
headers = parser.parse_headers(lines)
buffer = retriever.buffer
logging.debug("---response begin---\r\n%s---response end---", "".join(buffer))
return Message(version, status, msg, headers, buffer)
def _await_response(self, client):
"""
Simple response method.
Receives the response and prints to stdout.
"""
msg = self._get_preamble(client)
print("".join(msg.raw))
def _build_message(self, message: str) -> bytes:
return (message + "\r\n").encode(FORMAT)
def parse_uri(self):
"""
Parses the URI and returns the hostname and path.
@return: A tuple of the hostname and path.
"""
parsed = urlparse(self.uri)
# If there is no netloc, the url is invalid, so prepend `//` and try again
if parsed.netloc == "":
parsed = urlparse("http://" + self.uri)
host = parsed.netloc
path = parsed.path
if len(path) == 0 or path[0] != '/':
path = "/" + path
port_pos = host.find(":")
if port_pos >= 0:
host = host[:port_pos]
return host, path
class HeadCommand(AbstractCommand):
"""
A Command for sending a `HEAD` request.
"""
@property
def method(self):
return "HEAD"
class GetCommand(AbstractCommand):
"""
A Command for sending a `GET` request.
"""
dir: str
def __init__(self, uri: str, port, directory=None):
super().__init__(uri, port)
self.dir = directory
self.filename = None
@property
def method(self):
return "GET"
def _await_response(self, client):
"""
Handles the response of this command.
"""
msg = self._get_preamble(client)
from client import responsehandler
self.filename = responsehandler.handle(client, msg, self, self.dir)
class AbstractWithBodyCommand(AbstractCommand, ABC):
"""
The building block for creating an HTTP message for an HTTP method with a body (POST and PUT).
"""
def _build_message(self, message: str) -> bytes:
input_line = input(f"Enter {self.method} data: ")
input_line += "\r\n"
body = input_line.encode(FORMAT)
print()
message += "Content-Type: text/plain\r\n"
message += f"Content-Length: {len(body)}\r\n"
message += "\r\n"
message = message.encode(FORMAT)
message += body
message += b"\r\n"
return message
class PostCommand(AbstractWithBodyCommand):
"""
A command for sending a `POST` request.
"""
@property
def method(self):
return "POST"
class PutCommand(AbstractWithBodyCommand):
"""
A command for sending a `PUT` request.
"""
@property
def method(self):
return "PUT"