diff --git a/.gitignore b/.gitignore index fb61ed9..3d45ddf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ # created by virtualenv automatically env/ .idea/ +Pipfile +.pytest_cache +.coverage \ No newline at end of file diff --git a/enums/client_messages_response.py b/enums/client_messages_response.py new file mode 100644 index 0000000..8713b69 --- /dev/null +++ b/enums/client_messages_response.py @@ -0,0 +1,5 @@ +import enum + + +class ClientMessagesResponse(enum.Enum): + incorrect_url_or_port = 'Incorrect URL-address or port' diff --git a/enums/requests_names.py b/enums/requests_names.py new file mode 100644 index 0000000..6d2b844 --- /dev/null +++ b/enums/requests_names.py @@ -0,0 +1,9 @@ +import enum + + +class RequestsNames(enum.Enum): + get = 'get' + post = 'post' + patch = 'patch' + delete = 'delete' + put = 'put' diff --git a/enums/server_validating_messages.py b/enums/server_validating_messages.py new file mode 100644 index 0000000..dea5260 --- /dev/null +++ b/enums/server_validating_messages.py @@ -0,0 +1,10 @@ +import enum + + +class ServerValidatingMessages(enum.Enum): + incorrect_url = 'You did not write URL-address' + incorrect_type_request = 'You did not write type of request' + incorrect_format_cookie = 'Incorrect format cookie: type of object which takes cookie must be dictionary' + incorrect_format_headers = 'Incorrect format headers: type of object which takes headers must be dictionary' + incorrect_body_type = 'Incorrect format of body: type of object which takes body is string' + is_validated = 'All parameters is valid' diff --git a/client/http_client.py b/http_client.py similarity index 63% rename from client/http_client.py rename to http_client.py index 38412ab..39e8d27 100644 --- a/client/http_client.py +++ b/http_client.py @@ -1,12 +1,14 @@ import socket +from enums.client_messages_response import ClientMessagesResponse + class HttpClient: def __init__(self, settings: dict): - self.__settings = settings + self._settings = settings - self.__HOST = self.__settings.get("url") - self.__PORT = int(self.__settings.get("port")) + self._HOST = self._settings.get("url") + self._PORT = self._settings.get("port") def get_data(self) -> str: """ @@ -17,13 +19,17 @@ def get_data(self) -> str: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: timeout = 10 - if self.__settings.get("timeout") is not None: - timeout = self.__settings.get("timeout") + if self._settings.get("timeout") is not None: + timeout = self._settings.get("timeout") s.settimeout(timeout) - s.connect((self.__HOST, self.__PORT)) - request = self.create_http_request(self.__settings).encode() + try: + s.connect((self._HOST, int(self._PORT))) + except (ValueError, socket.gaierror): + return str(ClientMessagesResponse.incorrect_url_or_port.value) + + request = self.create_http_request(self._settings).encode() try: s.sendall(request) @@ -33,22 +39,16 @@ def get_data(self) -> str: response = b"" try: - while True: - data = s.recv(4096) - - if not data: - break - - response += data + response = self.receive_data(s) except socket.timeout: pass except Exception as e: print('log: ' + str(e)) - close_request = self.create_http_close_request(self.__settings).encode() + close_request = self.create_http_close_request(self._settings) try: - s.sendall(close_request) + self.send_data(conn=s, data=close_request) except Exception as e: print('log: ' + str(e)) @@ -56,6 +56,41 @@ def get_data(self) -> str: return response + @staticmethod + def receive_data(conn: socket) -> bytes: + response = b"" + + while True: + data = conn.recv(4096) + + if not data: + break + + response += data + + return response + + @staticmethod + def send_data(conn: socket, data: str) -> int: + """ + Отправляет данные клиенту + :param conn: подключение, по которому нужно отправить эти данные + :param data: отправляемые данные + :return: количество отправленных байт + """ + data_to_send = data.encode('utf-8') + bytes_sent = 0 + + while bytes_sent < len(data_to_send): + sent = conn.send(data_to_send[bytes_sent:]) + + if sent == 0: + break + + bytes_sent += sent + + return bytes_sent + @staticmethod def get_headers(settings: dict) -> str: """ @@ -75,7 +110,7 @@ def get_headers(settings: dict) -> str: cookies.append(f'{key}={value};') if len(cookies) != 0: - cookies = 'Cookie: ' + ' '.join(cookies) + cookies = 'Cookie: ' + ' '.join(cookies) cookies = cookies[:-1] cookies += '\r\n' headers.append(cookies) diff --git a/http_server.py b/http_server.py new file mode 100644 index 0000000..0d77475 --- /dev/null +++ b/http_server.py @@ -0,0 +1,232 @@ +import re +import socket +import json + +from enums.client_messages_response import ClientMessagesResponse +from enums.requests_names import RequestsNames +from enums.server_validating_messages import ServerValidatingMessages +from http_client import HttpClient + + +class HttpServer: + """ + HTTP-сервер, который умеет принимать запросы от клиентов и, используя HttpClient возвращает нужные запросы + """ + + def __init__(self): + self.__HOST = '127.0.0.1' + self.__PORT = 8080 + + self._server = None + + self._working = None + + def start(self): + """ + Запускает сервер + """ + + try: + self._create_server() + except OSError as e: + self.stop() + print(e) + + if self._server is None: + return + + self._server.listen() + + self._start_server() + + while self._working: + if self._server is None: + break + + conn, addr = self._server.accept() + + print(f'Client connected by addr: {addr}') + + with conn: + data = conn.recv(8192).decode() + + origin = None + + if data.startswith('OPTIONS / HTTP/1.1'): + origin = re.search(r'(?<=Origin: )(.+?)(?=\r\n)', data).group(0) + + response = self.create_option_response(origin) + + conn.sendall(response.encode()) + + data = conn.recv(8190).decode() + + body = re.search(r'(?<=\r\n\r\n)(.+)', data).group(0) + settings = json.loads(body) + + settings_is_validated = HttpServer.validate_params(settings) + + if not settings_is_validated[0]: + response = HttpServer.create_bad_request_response( + origin, + settings_is_validated[1] + ) + + HttpServer.send_data(conn, response) + else: + client = HttpClient(settings) + client_data = client.get_data() + + if client_data == ClientMessagesResponse.incorrect_url_or_port.value: + response = HttpServer.create_bad_request_response(origin, client_data) + HttpServer.send_data(conn, response) + else: + data = client_data + + if ( + settings.get('request').lower() == RequestsNames.get.value + and + len(settings.get('get_form')) != 0 + ): + data = self._take_data(settings, client_data) + + response = HttpServer.create_ok_request_response(data, origin) + HttpServer.send_data(conn, response) + + print(f'Client with addr {addr} was disconnected') + + def _stop_server(self): + self._working = False + + def _start_server(self): + self._working = True + + def stop(self): + self._stop_server() + + @staticmethod + def _take_data(settings: dict, client_data: str): + data = client_data + + if 'application/json' in client_data: + matches = re.finditer(settings.get('get_form'), client_data, re.IGNORECASE) + data = [match.start() for match in matches] + + return data + + def _create_server(self): + self._server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._server.bind((self.__HOST, self.__PORT)) + + def stop(self): + self._server.close() + self._server = None + + @staticmethod + def send_data(conn: socket, response: str) -> int: + """ + Отправляет данные на сервер + :param conn: подключение, по которому нужно отправить эти данные + :param response: отправляемые данные + :return: количество отправленных байт + """ + data_to_send = response.encode('utf-8') + bytes_sent = 0 + + while bytes_sent < len(data_to_send): + sent = conn.send(data_to_send[bytes_sent:]) + + bytes_sent += sent + + return bytes_sent + + @staticmethod + def validate_params(body: dict) -> tuple: + """ + Проверяет являются ли параметры запроса валидными + :return: возвращает tuple, где первый параметр - это true или false: результат валидности, + а второй сообщение описывающее результат валидации + """ + + if body.get('url') is None: + return False, ServerValidatingMessages.incorrect_url.value + + if body.get('request') is None: + return False, ServerValidatingMessages.incorrect_type_request.value + + if not isinstance(body.get('cookie'), dict): + return False, ServerValidatingMessages.incorrect_format_cookie.value + + if not isinstance(body.get('headers'), dict): + return False, ServerValidatingMessages.incorrect_format_headers.value + + if not isinstance(body.get('body'), str): + return False, ServerValidatingMessages.incorrect_body_type.value + + return True, ServerValidatingMessages.is_validated.value + + @staticmethod + def create_option_response(origin: str): + """ + Создает ответ на OPTION запрос клиента + :param origin: значение заголовка Origin в Option запросе клиента + :return: возвращает готовый ответ для клиента в формате протокола HTTP + """ + + return f"HTTP/1.1 200 OK\r\n" \ + f"Access-Control-Allow-Origin: {origin}\r\n" \ + f"Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n" \ + f"Access-Control-Allow-Headers: Content-Type\r\n" \ + f"Connection: keep-alive\r\n" \ + f"Content-Length: 0\r\n" \ + f"\r\n" + + @staticmethod + def create_response(code: int, code_message: str, data: str, origin: str,): + """ + Создает ответ на пользовательский запрос + :param code: код ответа + :param code_message: код ответа + :param data: данные в теле ответа (должны быть в json формате) + :param origin: значение заголовка Origin в Option запросе клиента + :return: ответ + """ + + response = f'HTTP/1.1 {code} {code_message}\r\n' \ + f'Content-Type: application/json\r\n' \ + f'Content-Length: {len(data.encode())}\r\n' \ + f'Connection: keep-alive\r\n' \ + f'Access-Control-Allow-Origin: {origin}' \ + f'\r\n\r\n{data}' + + return response + + @staticmethod + def create_ok_request_response(data: str, origin: str): + """ + Создает ответ на запрос с кодом успеха + :param data: данные, которые будут помещены в тело запроса + :param origin: значение заголовка Origin в Option запросе клиента + :return: возвращает готовый ответ для клиента в формате протокола HTTP + """ + + body = dict() + + body['data'] = data + body['code'] = 200 + + return HttpServer.create_response(body['code'], 'OK', json.dumps(body), origin) + + @staticmethod + def create_bad_request_response(origin: str, message: str): + """ + Создает ответ с кодом плохого запроса (bad request) + :return: + """ + + body = dict() + + body['message'] = message + body['code'] = 400 + + return HttpServer.create_response(body['code'], 'BAD REQUEST', json.dumps(body), origin) diff --git a/main.py b/main.py index c6f2d73..7ad8a3e 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,16 @@ +from http_server import HttpServer -from server.server import HttpServer -if __name__ == '__main__': - server = HttpServer() - server.start() +class Main: + def __init__(self): + self._server = HttpServer() + + def run(self): + self._server.start() + + def finish(self): + self._server.stop() + +if __name__ == '__main__': + Main().run() diff --git a/resources/index.html b/resources/index.html index cb57976..f10db42 100644 --- a/resources/index.html +++ b/resources/index.html @@ -20,25 +20,26 @@
-