172 lines
4.7 KiB
Python
172 lines
4.7 KiB
Python
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):
|
|
if command == "GET":
|
|
return GetCommand(url, port)
|
|
elif command == "HEAD":
|
|
return HeadCommand(url, port)
|
|
elif command == "POST":
|
|
return PostCommand(url, port)
|
|
elif command == "PUT":
|
|
return PutCommand(url, port)
|
|
else:
|
|
raise ValueError()
|
|
|
|
|
|
class AbstractCommand(ABC):
|
|
uri: str
|
|
host: str
|
|
path: str
|
|
port: int
|
|
|
|
def __init__(self, uri: str, port):
|
|
self.uri = uri
|
|
self.host, _, self.path = parser.parse_uri(uri)
|
|
self.port = int(port)
|
|
|
|
@property
|
|
@abstractmethod
|
|
def command(self):
|
|
pass
|
|
|
|
def execute(self, sub_request=False):
|
|
(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):
|
|
|
|
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):
|
|
@property
|
|
def command(self):
|
|
return "HEAD"
|
|
|
|
|
|
class GetCommand(AbstractCommand):
|
|
|
|
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)
|
|
|
|
logging.debug("---response begin---\r\n%s---response end---", "".join(retriever.buffer))
|
|
|
|
return Message(version, status, msg, headers, retriever.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):
|
|
@property
|
|
def command(self):
|
|
return "POST"
|
|
|
|
|
|
class PutCommand(AbstractWithBodyCommand):
|
|
@property
|
|
def command(self):
|
|
return "PUT"
|