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 from httplib.httpsocket import FORMAT from httplib.message import ClientMessage 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 sub_request: bool def __init__(self, uri: str, port): self.uri = uri self.host, _, self.path = parser.parse_uri(uri) self.port = int(port) self.sub_request = False @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. """ self.sub_request = sub_request (host, path) = self.parse_uri() client = sockets.get(host) if client and client.is_closed(): sockets.pop(self.host) client = None if not client: client = HTTPClient(host) client.conn.connect((host, self.port)) sockets[host] = client message = f"{self.method} {path} HTTP/1.1\r\n" message += f"Host: {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.debug("Internal error: Response could not be parsed", exc_info=e) return except InvalidStatusLine as e: logging.debug("Internal error: Invalid status-line in response", exc_info=e) return except UnsupportedEncoding as e: logging.debug("Internal error: Unsupported encoding in response", exc_info=e) finally: if not sub_request: client.close() def _await_response(self, client): """ Simple response method. Receives the response and prints to stdout. """ while True: line = client.read_line() print(line, end="") if line in ("\r\n", "\n", ""): break 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 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: body = input(f"Enter {self.method} data: ").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 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 _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): """ Handles the response of this command. """ msg = self._get_preamble(client) from client import response_handler self.filename = response_handler.handle(client, msg, self, self.dir) 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"