From 0991aad10550c726b9e8fdc161fc069745a01b2a Mon Sep 17 00:00:00 2001 From: Thastertyn Date: Sat, 8 Feb 2025 09:17:19 +0100 Subject: [PATCH] Final commit --- .env.example | 3 + README.md | 85 +++++++++++++++++++ requirements.txt | 8 +- src/bank_node/bank_node.py | 3 +- src/bank_node/bank_worker.py | 42 ++++++--- src/bank_protocol/bank_scanner.py | 3 +- src/bank_protocol/command_handler.py | 2 +- .../commands/account_balance_command.py | 2 +- .../commands/account_create_command.py | 2 +- .../commands/account_deposit_command.py | 2 +- .../commands/account_remove_command.py | 36 +++++++- .../commands/account_withdrawal_command.py | 2 +- .../commands/bank_code_command.py | 2 +- .../bank_number_of_clients_command.py | 2 +- .../commands/bank_total_amount_command.py | 4 +- src/bank_protocol/proxy_handler.py | 2 +- src/core/config.py | 2 - 17 files changed, 170 insertions(+), 32 deletions(-) diff --git a/.env.example b/.env.example index 4179e50..4066ee2 100644 --- a/.env.example +++ b/.env.example @@ -16,3 +16,6 @@ VERBOSITY=DEBUG # Only two port numbers must be provided # Anything else results in an exit SCAN_PORT_RANGE=65525:65535 + +# Manually set the IP address +# IP=127.0.0.1 diff --git a/README.md b/README.md index 0843f56..abb5f3e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,90 @@ # nu (ν) +# Running + +## Configuration + +The application can be configured using a `.env` file. Here are the available options: + +`RESPONSE_TIMEOUT` + +- Specifies the response timeout in seconds. + +- Default: `5` + +- Example: `RESPONSE_TIMEOUT=5` + +`CLIENT_IDLE_TIMEOUT` + +- Defines the client idle timeout in seconds. + +- Default: `60` + +- Example: `CLIENT_IDLE_TIMEOUT=60` + +`VERBOSITY` + +- Determines the logging verbosity level. + +- Valid values: DEBUG, INFO, WARNING, ERROR, CRITICAL. + +- Default: `INFO` + +- Example: `VERBOSITY=DEBUG` + +`SCAN_PORT_RANGE` + +- Defines the port range to scan as `PORT:PORT` separated by colon (`:`). + +- The ports must be valid and there must be exactly two of them. + +- Example: `SCAN_PORT_RANGE=65525:65535` + +`IP` + +- Manually set the IP address. + +- Example: `IP=127.0.0.1` + +## Running + +The application requires a few dependenices, specified in `requirements.txt`. + +To install these dependencies run + +```bash +pip install -r requirements.txt +``` + +Upon successful installation, the program is ready to run. The database used is SQLite, hence no SQL structure is needed to be imported, nor are any credentials required. + +To run the app, run + +```bash +python src/app.py +``` + +If the verbosity set in `.env` is `DEBUG`, the program will print many useful descriptions of it's actions together with a detailed location of the action including file, line and process name. + +## Definitions + +The program uses the following protocol over TCP + +| Operation | Request Format | Response Format | +| ---------------------- | -------------------------------- | --------------- | +| Bank code | BC | BC \ | +| Account create | AC \/\ | AC | +| Account deposit | AD \/\ \ | AD | +| Account withdrawal | AW \/\ \ | AW | +| Account balance | AB \/\ | AB \ | +| Account remove | AR \/\ | AR | +| Bank (total) amount | BA | BA \ | +| Bank number of clients | BN | BN \ | + +Any of these commands may return an error message in format + +`ER ` + ## Sources ### Signal catching diff --git a/requirements.txt b/requirements.txt index 25cdcc7..ad353e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -greenlet==3.1.1 ; python_version < "3.14" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and python_version >= "3.12" -python-dotenv==1.0.1 ; python_version >= "3.12" and python_version < "4.0" -sqlalchemy==2.0.37 ; python_version >= "3.12" and python_version < "4.0" -typing-extensions==4.12.2 ; python_version >= "3.12" and python_version < "4.0" +greenlet==3.1.1 +python-dotenv==1.0.1 +sqlalchemy==2.0.37 +typing-extensions==4.12.2 diff --git a/src/bank_node/bank_node.py b/src/bank_node/bank_node.py index ec2ed52..a8873d1 100644 --- a/src/bank_node/bank_node.py +++ b/src/bank_node/bank_node.py @@ -2,6 +2,7 @@ import socket import signal import sys import logging +import errno from core.config import BankNodeConfig from core.exceptions import ConfigError @@ -66,7 +67,7 @@ class BankNode(): self.logger.error("All ports are in use") self.exit_with_error() except socket.error as e: - if e.errno == 99: # Cannot assign to requested address + if e.errno == 98 or e.errno == errno.WSAEADDRINUSE: # Cannot assign to requested address self.logger.critical("Cannot use the IP address %s", self.config.ip) else: self.logger.critical("Unknown error: %s", e) diff --git a/src/bank_node/bank_worker.py b/src/bank_node/bank_worker.py index 781b673..c2c00ba 100644 --- a/src/bank_node/bank_worker.py +++ b/src/bank_node/bank_worker.py @@ -4,12 +4,13 @@ import logging from typing import Tuple import signal import sys +import time from bank_protocol.command_handler import CommandHandler from core import Request, Response, BankNodeConfig from core.exceptions import BankNodeError from utils.logger import setup_logger - +from database import DatabaseManager class BankWorker(multiprocessing.Process): def __init__(self, client_socket: socket.socket, client_address: Tuple, config: BankNodeConfig): @@ -38,14 +39,15 @@ class BankWorker(multiprocessing.Process): # and loses its configuration by default # -> Set it up again in the fresh process if sys.platform == "win32": + DatabaseManager() setup_logger(self.config.verbosity) + self.logger = logging.getLogger(__name__) self.command_handler = CommandHandler(self.config) self.client_socket.setblocking(True) self.client_socket.settimeout(self.config.client_idle_timeout) - self.__setup_signals() with self.client_socket: @@ -55,7 +57,7 @@ class BankWorker(multiprocessing.Process): def serve_client(self): buffer = "" - ending = "\r\n" + ending = None while True: try: @@ -78,34 +80,52 @@ class BankWorker(multiprocessing.Process): ending = "\r" self.logger.debug("CR detected") - request_data, buffer = buffer.split(ending, 1) - self.logger.debug("Processing request: %r", request_data) + elif len(buffer) == 2 or len(buffer) >= 16: + # If no line ending is provided, make an assumption + # Either a 2 character command was sent without any line ending + # or the shorted valid command with arguments has to be 16 or more + # characters long + ending = "\n" + self.logger.debug("Messages appear to be missing line ending, proceeding with processing") - request = Request(request_data) - response: Response = self.command_handler.execute(request) + ending + if ending is None: + self.logger.debug("No line ending found, and messages don't look complete. Assuming another part will come") + continue + + response = self.process_request(buffer, ending) self.client_socket.sendall(response.encode("utf-8")) self.logger.debug("Response sent to %s", self.client_address[0]) + buffer = "" + ending = None + except socket.timeout: self.logger.debug("Client was idle for too long. Ending connection") - response = "ER Idle too long" + ending + response = "ER Idle too long" + (ending or "\n") self.client_socket.sendall(response.encode("utf-8")) self.client_socket.shutdown(socket.SHUT_RDWR) self.client_socket.close() break except UnicodeDecodeError: self.logger.warning("Received a non utf-8 message") - response = "ER Not utf-8 message" + response = "ER Not utf-8 message" + (ending or "\n") self.client_socket.sendall(response.encode("utf-8")) break except BankNodeError as e: - response = "ER " + e.message + ending + response = "ER " + e.message + (ending or "\n") self.client_socket.sendall(response.encode("utf-8")) + buffer = "" except socket.error as e: self.logger.error(e) - response = "ER Internal server error" + ending + response = "ER Internal server error" + (ending or "\n") break + def process_request(self, buffer: str, line_ending: str) -> Response: + self.logger.debug("Processing request: %r", buffer) + request = Request(buffer) + response: Response = self.command_handler.execute(request) + line_ending + return response + def gracefully_exit_worker(self, signum, _): """Log the signal caught and exit with status 0""" diff --git a/src/bank_protocol/bank_scanner.py b/src/bank_protocol/bank_scanner.py index 86d42af..9c66acb 100644 --- a/src/bank_protocol/bank_scanner.py +++ b/src/bank_protocol/bank_scanner.py @@ -1,6 +1,7 @@ import socket import threading import logging +import errno from core.peer import BankPeer @@ -31,7 +32,7 @@ class BankScanner(threading.Thread): except socket.timeout: self.logger.debug("Connection for port %d timed out", port) except socket.error as e: - if e.errno == 111: # Connection refused + if e.errno == 111 or e.errno == errno.WSAECONNREFUSED: # Connection refused self.logger.debug("Port %d not open", port) else: self.logger.debug("Unknown error occurred when probing port: %s", e) diff --git a/src/bank_protocol/command_handler.py b/src/bank_protocol/command_handler.py index 60e6b51..26e7144 100644 --- a/src/bank_protocol/command_handler.py +++ b/src/bank_protocol/command_handler.py @@ -42,7 +42,7 @@ class CommandHandler: try: response = command(request, self.config) if response is not None: - return f"{request.command_code} {response}" + return str(response).strip() else: return request.command_code except DatabaseError as e: diff --git a/src/bank_protocol/commands/account_balance_command.py b/src/bank_protocol/commands/account_balance_command.py index 0cd3527..d3acb3e 100644 --- a/src/bank_protocol/commands/account_balance_command.py +++ b/src/bank_protocol/commands/account_balance_command.py @@ -33,7 +33,7 @@ def account_balance(request: Request, config: BankNodeConfig) -> Response: balance = get_account_balance(account_parsed) - return str(balance) + return f"AB {str(balance)}" __all__ = ["account_balance"] diff --git a/src/bank_protocol/commands/account_create_command.py b/src/bank_protocol/commands/account_create_command.py index 0867455..6e3344f 100644 --- a/src/bank_protocol/commands/account_create_command.py +++ b/src/bank_protocol/commands/account_create_command.py @@ -9,7 +9,7 @@ def account_create(request: Request, config: BankNodeConfig) -> Response: account_number = create_account() - return f"{account_number}/{config.ip}" + return f"AC {account_number}/{config.ip}" diff --git a/src/bank_protocol/commands/account_deposit_command.py b/src/bank_protocol/commands/account_deposit_command.py index 2f27bbd..e40307f 100644 --- a/src/bank_protocol/commands/account_deposit_command.py +++ b/src/bank_protocol/commands/account_deposit_command.py @@ -42,7 +42,7 @@ def account_deposit(request: Request, config: BankNodeConfig) -> Response: deposit_into_account(account_parsed, amount_parsed) - return None + return "AD" __all__ = ["account_deposit"] diff --git a/src/bank_protocol/commands/account_remove_command.py b/src/bank_protocol/commands/account_remove_command.py index db8a873..867382d 100644 --- a/src/bank_protocol/commands/account_remove_command.py +++ b/src/bank_protocol/commands/account_remove_command.py @@ -1,7 +1,37 @@ -from core.request import Request +from core import Request, BankNodeConfig, Response +from bank_protocol.exceptions import InvalidRequest +from services.account_service import delete_account +from bank_protocol.proxy_handler import BankProxy -def account_remove(request: Request): - pass + +def account_remove(request: Request, config: BankNodeConfig) -> Response: + if request.body is None: + raise InvalidRequest("Invalid request format") + + try: + split_body = request.body.split("/") + + account = split_body[0] + ip = split_body[1] + + except IndexError as e: + raise InvalidRequest("Invalid request format") from e + + if ip != config.ip: + bank_proxy = BankProxy(request, ip, config) + return bank_proxy.proxy_request() + + if not account.isdigit(): + raise InvalidRequest("Account must be a number") + + account_parsed = int(account) + + if account_parsed < 10_000 or account_parsed > 99_999: + raise InvalidRequest("Account number out of range") + + delete_account(account_parsed) + + return "AR" __all__ = ["account_remove"] diff --git a/src/bank_protocol/commands/account_withdrawal_command.py b/src/bank_protocol/commands/account_withdrawal_command.py index 27f18e2..1524c7b 100644 --- a/src/bank_protocol/commands/account_withdrawal_command.py +++ b/src/bank_protocol/commands/account_withdrawal_command.py @@ -42,7 +42,7 @@ def account_withdrawal(request: Request, config: BankNodeConfig) -> Response: withdraw_from_account(account_parsed, amount_parsed) - return None + return "AW" __all__ = ["account_withdrawal"] diff --git a/src/bank_protocol/commands/bank_code_command.py b/src/bank_protocol/commands/bank_code_command.py index faa26db..9500a65 100644 --- a/src/bank_protocol/commands/bank_code_command.py +++ b/src/bank_protocol/commands/bank_code_command.py @@ -5,6 +5,6 @@ def bank_code(request: Request, config: BankNodeConfig) -> Response: if request.body is not None: raise InvalidRequest("Incorrect usage") - return config.ip + return f"BC {config.ip}" __all__ = ["bank_code"] diff --git a/src/bank_protocol/commands/bank_number_of_clients_command.py b/src/bank_protocol/commands/bank_number_of_clients_command.py index 472c92b..a86421c 100644 --- a/src/bank_protocol/commands/bank_number_of_clients_command.py +++ b/src/bank_protocol/commands/bank_number_of_clients_command.py @@ -7,6 +7,6 @@ def bank_number_of_clients(request: Request, _) -> Response: raise InvalidRequest("Incorrect usage") number_of_clients = get_account_count() - return number_of_clients + return f"BN {number_of_clients}" __all__ = ["bank_number_of_clients"] diff --git a/src/bank_protocol/commands/bank_total_amount_command.py b/src/bank_protocol/commands/bank_total_amount_command.py index e5b8aa3..88b9fcd 100644 --- a/src/bank_protocol/commands/bank_total_amount_command.py +++ b/src/bank_protocol/commands/bank_total_amount_command.py @@ -2,12 +2,12 @@ from core import Request, Response from bank_protocol.exceptions import InvalidRequest from services.account_service import get_total_balance -def bank_total_amount(request: Request, _): +def bank_total_amount(request: Request, _) -> Response: if request.body is not None: raise InvalidRequest("Incorrect usage") total_balace = get_total_balance() - return total_balace + return f"BA {total_balace}" __all__ = ["bank_total_amount"] diff --git a/src/bank_protocol/proxy_handler.py b/src/bank_protocol/proxy_handler.py index 550c755..a715e1b 100644 --- a/src/bank_protocol/proxy_handler.py +++ b/src/bank_protocol/proxy_handler.py @@ -39,7 +39,7 @@ class BankProxy(): raise NoPortsOpenError("Bank is unreachable") # with self.peer.bank_socket as bank_socket: - self.peer.bank_socket.sendall(str(self.request).encode("utf-8")) + self.peer.bank_socket.sendall((str(self.request) + "\r\n").encode("utf-8")) response = self.peer.bank_socket.recv(1024).decode("utf-8") return response diff --git a/src/core/config.py b/src/core/config.py index c270009..170374d 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -26,13 +26,11 @@ class BankNodeConfig: # Port validation if not scan_port_range or scan_port_range == "": - self.logger.error("Scan port range not defined") raise ConfigError("Scan port range not defined") range_split = scan_port_range.split(":") if len(range_split) != 2 or not range_split[0].isdigit() or not range_split[1].isdigit(): - self.logger.error("Scan port range is not in valid format") raise ConfigError("Scan port range is not in valid format") # Timeout validation