import logging from abc import ABC, abstractmethod from typing import Dict, Tuple from urllib.parse import urlparse from client.httpclient import FORMAT, HTTPClient from httplib import parser from httplib.exceptions import InvalidResponse, InvalidStatusLine, UnsupportedEncoding from httplib.message import ClientMessage as Message from httplib.retriever import PreambleRetriever sockets: Dict[str, HTTPClient] = {} def create(command: str, url: str, port): """ Create a corresponding Command instance of the specified HTTP `command` with the specified `url` and `port`. @param command: 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 command == "GET": return GetCommand(uri, port) elif command == "HEAD": return HeadCommand(uri, port) elif command == "POST": return PostCommand(uri, port) elif command == "PUT": return PutCommand(uri, port) else: raise ValueError() class AbstractCommand(ABC): """ A class representing the command for sending an HTTP command. """ 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 command(self): pass def execute(self, sub_request=False): 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.command} {path} HTTP/1.1\r\n" message += f"Host: {host}:{self.port}\r\n" message += "Accept: */*\r\nAccept-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: retriever = PreambleRetriever(client) self._await_response(client, retriever) 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, retriever): 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): parsed = urlparse(self.uri) # If there is no netloc, the url is invalid, so prepend `//` and try again if parsed.netloc == "": parsed = urlparse("//" + 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 command with a body. """ def _build_message(self, message: str) -> bytes: body = input(f"Enter {self.command} 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` message. """ @property def command(self): return "HEAD" class GetCommand(AbstractCommand): """ A Command for sending a `GET` message. """ def __init__(self, uri: str, port, dir=None): super().__init__(uri, port) self.dir = dir self.filename = None @property def command(self): return "GET" def _get_preamble(self, retriever): 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, retriever): msg = self._get_preamble(retriever) from client import response_handler self.filename = response_handler.handle(client, msg, self, self.dir) class PostCommand(AbstractWithBodyCommand): """ A command for sending a `POST` command. """ @property def command(self): return "POST" class PutCommand(AbstractWithBodyCommand): """ A command for sending a `PUT` command. """ @property def command(self): return "PUT"