diff --git a/.gitignore b/.gitignore index 62c8935..a047a94 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.idea/ \ No newline at end of file +.idea/ +__pycache__/ \ No newline at end of file diff --git a/messenger/Tests/__init__.py b/messenger/Tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/messenger/Tests/test_client.py b/messenger/Tests/test_client.py new file mode 100644 index 0000000..3b32543 --- /dev/null +++ b/messenger/Tests/test_client.py @@ -0,0 +1,30 @@ +import os +import sys +import unittest + +sys.path.append(os.path.join(os.getcwd(), '..')) +from common.settings import RESPONSE, ERROR, USER, ACCOUNT_NAME, TIME, ACTION, PRESENCE +from client import Client + + +class TestClass(unittest.TestCase): + def setUp(self): + self.client = Client() + + def test_def_presense(self): + test = self.client.presence() + test[TIME] = 5.2 + self.assertEqual(test, {ACTION: PRESENCE, TIME: 5.2, USER: {ACCOUNT_NAME: 'Guest'}}) + + def test_200_ans(self): + self.assertEqual(self.client.response({RESPONSE: 200}), 'Соединение установлено') + + def test_400_ans(self): + self.assertEqual(self.client.response({RESPONSE: 400, ERROR: 'Bad Request'}), 'Ошибка соединения с сервером: Bad Request') + + def test_no_response(self): + self.assertRaises(ValueError, self.client.response, {ERROR: 'Bad Request'}) + + +if __name__ == '__main__': + unittest.main() diff --git a/messenger/Tests/test_server.py b/messenger/Tests/test_server.py new file mode 100644 index 0000000..9ed5e27 --- /dev/null +++ b/messenger/Tests/test_server.py @@ -0,0 +1,43 @@ +import os +import sys +import unittest + +sys.path.append(os.path.join(os.getcwd(), '..')) +from common.settings import RESPONSE, ERROR, USER, ACCOUNT_NAME, TIME, ACTION, PRESENCE +from server import Server + + +class TestServer(unittest.TestCase): + err_dict = { + RESPONSE: 400, + ERROR: 'Bad Request' + } + ok_dict = {RESPONSE: 200} + + def setUp(self): + self.server = Server() + + def test_ok_check(self): + self.assertEqual(self.server.process({ACTION: PRESENCE, TIME: 1.1, USER: {ACCOUNT_NAME: 'Guest'}}), + self.ok_dict) + + def test_no_action(self): + self.assertEqual(self.server.process({TIME: '1.1', USER: {ACCOUNT_NAME: 'Guest'}}), self.err_dict) + + def test_wrong_action(self): + self.assertEqual(self.server.process({ACTION: 'Wrong', TIME: '1.1', USER: {ACCOUNT_NAME: 'Guest'}}), + self.err_dict) + + def test_no_time(self): + self.assertEqual(self.server.process({ACTION: PRESENCE, USER: {ACCOUNT_NAME: 'Guest'}}), self.err_dict) + + def test_no_user(self): + self.assertEqual(self.server.process({ACTION: PRESENCE, TIME: '1.1'}), self.err_dict) + + def test_unknown_user(self): + self.assertEqual(self.server.process({ACTION: PRESENCE, TIME: 1.1, USER: {ACCOUNT_NAME: 'Guest_1'}}), + self.err_dict) + + +if __name__ == '__main__': + unittest.main() diff --git a/messenger/Tests/test_utilites.py b/messenger/Tests/test_utilites.py new file mode 100644 index 0000000..7a250ef --- /dev/null +++ b/messenger/Tests/test_utilites.py @@ -0,0 +1,61 @@ +import os +import sys +import unittest + +sys.path.append(os.path.join(os.getcwd(), '..')) +from common.settings import USER, ACCOUNT_NAME, TIME, ACTION, PRESENCE +from common.utilites import Encoder, Message + +test_dict_send = { + ACTION: PRESENCE, + TIME: 111111.111111, + USER: { + ACCOUNT_NAME: 'test_test' + } +} +test_dict_encoded = b'{"action": "presence", "time": 111111.111111, "user": {"account_name": "test_test"}}' + + +class TestSocket: + def __init__(self, test_dict, dict_encoded): + self.test_dict = test_dict + self.dict_encoded = dict_encoded + self.received_message = None + + def send(self, message_to_send): + self.received_message = message_to_send + return self.received_message + + def recv(self, max_len): + self.max_len = max_len + return self.dict_encoded + + +class TestUtilites(unittest.TestCase): + def setUp(self): + self.encoder = Encoder() + self.message = Message() + self.test_socket = TestSocket(test_dict_send, test_dict_encoded) + + def test_encode_message(self): + self.assertEqual(self.encoder.encoding(test_dict_send), test_dict_encoded) + + def test_encode_error_value(self): + self.assertRaises(ValueError, self.encoder.encoding, 'non_dict_data') + + def test_decode_message(self): + self.assertEqual(self.encoder.decoding(test_dict_encoded), test_dict_send) + + def test_decode_error_json_loads(self): + self.assertRaises(ValueError, self.encoder.encoding, 'non_byte_data') + + def test_get_message(self): + self.assertEqual(self.message.get(self.test_socket), test_dict_send) + + def test_send_message(self): + self.message.send(self.test_socket, test_dict_send) + self.assertEqual(self.test_socket.received_message, test_dict_encoded) + + +if __name__ == '__main__': + unittest.main() diff --git a/messenger/__init__.py b/messenger/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/messenger/client.py b/messenger/client.py new file mode 100644 index 0000000..7159599 --- /dev/null +++ b/messenger/client.py @@ -0,0 +1,79 @@ +import socket +import time +import logging +from log import client_log_config +from json import JSONDecodeError +from sys import argv + +from common.settings import ACTION, PRESENCE, TIME, USER, ACCOUNT_NAME, RESPONSE, ERROR, DEFAULT_IP_ADDRESS, \ + DEFAULT_PORT +from common.utilites import Message + + +CLIENT_LOGGER = logging.getLogger('client') + + +class Client: + def presence(self, account_name='Guest'): + out = { + ACTION: PRESENCE, + TIME: time.time(), + USER: { + ACCOUNT_NAME: account_name + } + } + CLIENT_LOGGER.debug(f'Сформировано {PRESENCE} сообщение для пользователя {account_name}') + return out + + def response(self, message): + CLIENT_LOGGER.debug(f'Разбор сообщения {message} от сервера') + if RESPONSE in message: + if message[RESPONSE] == 200: + return 'Соединение установлено' + return f'Ошибка соединения с сервером: {message[ERROR]}' + CLIENT_LOGGER.error('Неверный формат сообщения от сервера') + raise ValueError + + + def start(self, account_name='Guest'): + try: + if '-a' in argv: + address = argv[argv.index('-a') + 1] + else: + address = DEFAULT_IP_ADDRESS + except IndexError: + CLIENT_LOGGER.critical('После параметра \'a\'- необходимо указать адрес, к которому будет подключаться клиент.') + exit(1) + try: + if '-p' in argv: + port = int(argv[argv.index('-p') + 1]) + else: + port = DEFAULT_PORT + if 1024 > port > 65535: + raise ValueError + except IndexError: + CLIENT_LOGGER.critical('После параметра -\'p\' необходимо указать номер порта.') + exit(1) + except ValueError: + CLIENT_LOGGER.critical('Порт может быть в диапазоне от 1024 до 65535.') + exit(1) + transport = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + CLIENT_LOGGER.info(f'Подключение к серверу {address}:{port}') + transport.connect((address, port)) + except ConnectionRefusedError: + CLIENT_LOGGER.critical(f'Сервер не запущен на адресе {address}:{port}') + exit(1) + message_to_server = self.presence(account_name) + CLIENT_LOGGER.debug(f'Сформировано сообщение для отправки на сервер: {message_to_server}') + Message.send(transport, message_to_server) + try: + answer = self.response(Message.get(transport)) + print(answer) + except (ValueError, JSONDecodeError): + CLIENT_LOGGER.error('Ошибка декодирования сообщения.') + + +if __name__ == '__main__': + client = Client() + client.start() diff --git a/messenger/common/__init__.py b/messenger/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/messenger/common/settings.py b/messenger/common/settings.py new file mode 100644 index 0000000..8db9ff9 --- /dev/null +++ b/messenger/common/settings.py @@ -0,0 +1,31 @@ +"""Здесь производятся настройки приложения""" +import logging + +# Порт по умолчанию для сетевого ваимодействия +DEFAULT_PORT = 7777 + +# IP адрес по умолчанию для подключения клиента +DEFAULT_IP_ADDRESS = '127.0.0.1' + +# Максимальная очередь подключений +MAX_CONNECTIONS = 5 + +# Максимальная длинна сообщения в байтах +MAX_PACKAGE_LENGTH = 1024 + +# Кодировка проекта +ENCODING = 'utf-8' + +# Текущий уровень логирования +LOGGING_LEVEL = logging.DEBUG + +# Прококол JIM основные ключи: +ACTION = 'action' +TIME = 'time' +USER = 'user' +ACCOUNT_NAME = 'account_name' + +# Прочие ключи, используемые в протоколе +PRESENCE = 'presence' +RESPONSE = 'response' +ERROR = 'error' diff --git a/messenger/common/utilites.py b/messenger/common/utilites.py new file mode 100644 index 0000000..7a13d45 --- /dev/null +++ b/messenger/common/utilites.py @@ -0,0 +1,35 @@ +import json +from common.settings import MAX_PACKAGE_LENGTH, ENCODING + + +class Encoder: + @staticmethod + def encoding(data): + if isinstance(data, dict): + return json.dumps(data).encode(ENCODING) + else: + raise ValueError('Данные должны быть словарем') + + @staticmethod + def decoding(data): + if isinstance(data, bytes): + msg = json.loads(data.decode(ENCODING)) + if isinstance(msg, dict): + return msg + else: + raise ValueError('Данные должны быть словарем') + else: + raise ValueError('Данные должны быть байтами') + + +class Message: + @staticmethod + def get(client): + encoded_response = client.recv(MAX_PACKAGE_LENGTH) + response = Encoder.decoding(encoded_response) + return response + + @staticmethod + def send(sock, message): + encoded_message = Encoder.encoding(message) + sock.send(encoded_message) diff --git a/messenger/log/__init__.py b/messenger/log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/messenger/log/client_log_config.py b/messenger/log/client_log_config.py new file mode 100644 index 0000000..a4d677b --- /dev/null +++ b/messenger/log/client_log_config.py @@ -0,0 +1,26 @@ +import sys +import os +import logging +from common.settings import LOGGING_LEVEL +sys.path.append('../') + + +CLIENT_FORMATTER = logging.Formatter('%(asctime)s %(levelname)s %(filename)s %(message)s') +PATH = os.path.dirname(os.path.abspath(__file__)) +PATH = os.path.join(PATH, 'client.log') +STREAM_HANDLER = logging.StreamHandler(sys.stderr) +STREAM_HANDLER.setFormatter(CLIENT_FORMATTER) +STREAM_HANDLER.setLevel(logging.ERROR) +LOG_FILE = logging.FileHandler(PATH, encoding='utf8') +LOG_FILE.setFormatter(CLIENT_FORMATTER) +LOGGER = logging.getLogger('client') +LOGGER.addHandler(STREAM_HANDLER) +LOGGER.addHandler(LOG_FILE) +LOGGER.setLevel(LOGGING_LEVEL) + + +if __name__ == '__main__': + LOGGER.critical('Критическая ошибка') + LOGGER.error('Ошибка') + LOGGER.debug('Отладочная информация') + LOGGER.info('Информационное сообщение') diff --git a/messenger/log/server_log_config.py b/messenger/log/server_log_config.py new file mode 100644 index 0000000..7100972 --- /dev/null +++ b/messenger/log/server_log_config.py @@ -0,0 +1,27 @@ +import sys +import os +# import logging +import logging.handlers +from common.settings import LOGGING_LEVEL +sys.path.append('../') + + +SERVER_FORMATTER = logging.Formatter('%(asctime)s %(levelname)s %(filename)s %(message)s') +PATH = os.path.dirname(os.path.abspath(__file__)) +PATH = os.path.join(PATH, 'server.log') +STREAM_HANDLER = logging.StreamHandler(sys.stderr) +STREAM_HANDLER.setFormatter(SERVER_FORMATTER) +STREAM_HANDLER.setLevel(logging.ERROR) +LOG_FILE = logging.handlers.TimedRotatingFileHandler(PATH, encoding='utf8', interval=1, when='D') +LOG_FILE.setFormatter(SERVER_FORMATTER) +LOGGER = logging.getLogger('server') +LOGGER.addHandler(STREAM_HANDLER) +LOGGER.addHandler(LOG_FILE) +LOGGER.setLevel(LOGGING_LEVEL) + + +if __name__ == '__main__': + LOGGER.critical('Критическая ошибка') + LOGGER.error('Ошибка') + LOGGER.debug('Отладочная информация') + LOGGER.info('Информационное сообщение') \ No newline at end of file diff --git a/messenger/server.py b/messenger/server.py new file mode 100644 index 0000000..64d533e --- /dev/null +++ b/messenger/server.py @@ -0,0 +1,75 @@ +import socket +from json import JSONDecodeError +from sys import argv +import logging +from log import server_log_config + +from common.settings import ACTION, RESPONSE, MAX_CONNECTIONS, PRESENCE, TIME, USER, ACCOUNT_NAME, ERROR, DEFAULT_PORT +from common.utilites import Message + + +SERVER_LOGGER = logging.getLogger('server') + + +class Server: + def process(self, message): + SERVER_LOGGER.debug(f'Разбор сообщения {message} от клиента') + if ACTION in message and message[ACTION] == PRESENCE and TIME in message and USER in message \ + and message[USER][ACCOUNT_NAME] == 'Guest': + return {RESPONSE: 200} + return { + RESPONSE: 400, + ERROR: 'Bad Request' + } + + def start(self): + try: + if '-a' in argv: + address = argv[argv.index('-a') + 1] + else: + address = '' + except IndexError: + SERVER_LOGGER.critical('После параметра \'a\'- необходимо указать адрес, который будет слушать сервер.') + exit(1) + try: + if '-p' in argv: + port = int(argv[argv.index('-p') + 1]) + else: + port = DEFAULT_PORT + if 1024 > port > 65535: + raise ValueError + except IndexError: + SERVER_LOGGER.critical('После параметра -\'p\' необходимо указать номер порта.') + exit(1) + except ValueError: + SERVER_LOGGER.critical('Порт может быть в диапазоне от 1024 до 65535.') + exit(1) + SERVER_LOGGER.info(f'Запущен сервер: ' + f'Адрес(а) с которого(ых) принимаются подключения: {"Все" if not address else address}, ' + f'Порт для подключений: {port}') + transport = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + transport.bind((address, port)) + SERVER_LOGGER.info(f'Сервер начал прослушивание на адресе(ах): ' + f'{"Все" if not address else address}, ' + f'Порт для подключений: {port}') + transport.listen(MAX_CONNECTIONS) + while True: + client, address = transport.accept() + SERVER_LOGGER.info(f'Установлено соедение с клиентом {address}') + try: + message_from_cient = Message.get(client) + SERVER_LOGGER.debug(f'Получено сообщение {message_from_cient}') + print(message_from_cient) + response = self.process(message_from_cient) + SERVER_LOGGER.debug(f'Cформирован ответ клиенту {response}') + Message.send(client, response) + SERVER_LOGGER.debug(f'Отправлен ответ {response}, cоединение с клиентом {address} закрывается.') + client.close() + except JSONDecodeError: + SERVER_LOGGER.error(f'Ошибка декодирования сообщения от клиента {address}.') + client.close() + + +if __name__ == '__main__': + server = Server() + server.start()