From 9e62d2517cb685abd4b46945531ffaec7a069fa4 Mon Sep 17 00:00:00 2001 From: Thastertyn Date: Fri, 7 Feb 2025 18:01:10 +0100 Subject: [PATCH] Many tweaks and fixes and implemented withdraw and balance commands --- README.md | 6 ++- src/bank_node/bank_worker.py | 20 ++++---- src/bank_protocol/bank_scanner.py | 48 ++++++++++++------- .../commands/account_balance_command.py | 39 +++++++++++++-- .../commands/account_deposit_command.py | 14 ++++-- .../commands/account_withdrawal_command.py | 47 ++++++++++++++++-- src/bank_protocol/proxy_handler.py | 46 +++++++++++------- src/core/peer.py | 10 +++- src/core/request.py | 7 +-- src/database/exceptions.py | 1 + src/models/account_model.py | 4 +- src/services/account_service.py | 9 +++- src/utils/logger.py | 2 +- 13 files changed, 186 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index af5c40e..0843f56 100644 --- a/README.md +++ b/README.md @@ -21,4 +21,8 @@ ### Platform specific problems -- [Windows sends partial data](https://stackoverflow.com/a/31754798) \ No newline at end of file +- [Windows sends partial data](https://stackoverflow.com/a/31754798) + +### Threading + +- [Events to stop scan](https://www.instructables.com/Starting-and-Stopping-Python-Threads-With-Events-i/) diff --git a/src/bank_node/bank_worker.py b/src/bank_node/bank_worker.py index e97814a..781b673 100644 --- a/src/bank_node/bank_worker.py +++ b/src/bank_node/bank_worker.py @@ -43,8 +43,8 @@ class BankWorker(multiprocessing.Process): self.logger = logging.getLogger(__name__) self.command_handler = CommandHandler(self.config) - self.client_socket.settimeout(self.config.client_idle_timeout) self.client_socket.setblocking(True) + self.client_socket.settimeout(self.config.client_idle_timeout) self.__setup_signals() @@ -55,6 +55,7 @@ class BankWorker(multiprocessing.Process): def serve_client(self): buffer = "" + ending = "\r\n" while True: try: @@ -68,27 +69,26 @@ class BankWorker(multiprocessing.Process): self.logger.debug("Buffer updated: %r", buffer) if "\r\n" in buffer: + ending = "\r\n" self.logger.debug("CRLF detected") - request_data, buffer = buffer.split("\r\n", 1) elif "\n" in buffer: + ending = "\n" self.logger.debug("LF detected") - request_data, buffer = buffer.split("\n", 1) elif "\r" in buffer: + ending = "\r" self.logger.debug("CR detected") - request_data, buffer = buffer.split("\r", 1) - else: - continue + request_data, buffer = buffer.split(ending, 1) self.logger.debug("Processing request: %r", request_data) request = Request(request_data) - response: Response = self.command_handler.execute(request) + "\r\n" + response: Response = self.command_handler.execute(request) + ending self.client_socket.sendall(response.encode("utf-8")) self.logger.debug("Response sent to %s", self.client_address[0]) except socket.timeout: self.logger.debug("Client was idle for too long. Ending connection") - response = "ER Idle too long\n\r" + response = "ER Idle too long" + ending self.client_socket.sendall(response.encode("utf-8")) self.client_socket.shutdown(socket.SHUT_RDWR) self.client_socket.close() @@ -99,11 +99,11 @@ class BankWorker(multiprocessing.Process): self.client_socket.sendall(response.encode("utf-8")) break except BankNodeError as e: - response = "ER " + e.message + "\n\r" + response = "ER " + e.message + ending self.client_socket.sendall(response.encode("utf-8")) except socket.error as e: self.logger.error(e) - response = "ER Internal server error\n\r" + response = "ER Internal server error" + ending break def gracefully_exit_worker(self, signum, _): diff --git a/src/bank_protocol/bank_scanner.py b/src/bank_protocol/bank_scanner.py index 06420b0..86d42af 100644 --- a/src/bank_protocol/bank_scanner.py +++ b/src/bank_protocol/bank_scanner.py @@ -1,41 +1,53 @@ -from typing import Optional import socket import threading import logging -from core.peer import Peer +from core.peer import BankPeer class BankScanner(threading.Thread): - def __init__(self, host: str, port_start: int, port_end: int): + def __init__(self, host: str, port: str, result_peer: BankPeer, bank_found_event: threading.Event, lock: threading.Lock, timeout: int): + super().__init__(name="BankScannerThread-{self.host}:{port}") self.logger = logging.getLogger(__name__) self.host = host - self.port_start = port_start - self.port_end = port_end - self.peer = Peer() + self.port = port + self.result_peer = result_peer + self.bank_found_event = bank_found_event + self.lock = lock + self.timeout = timeout - def scan(self) -> Optional[socket]: - threads = [] + def run(self): + if self.bank_found_event.is_set(): + return - for port in range(self.port_start, self.port_end + 1): - t = threading.Thread(target=self.__probe_for_open_ports, args=(self.host, port,), name=f"BankScannerThread-{self.host}:{port}") - threads.append(t) + self.__probe_for_open_ports(self.host, self.port) - def __probe_for_open_ports(self, host: str, port: int) -> Optional[socket]: + def __probe_for_open_ports(self, host: str, port: int): try: connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + connection.settimeout(self.timeout) connection.connect((host, port)) self.__scan_for_bank(connection) + 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: # Connection refused self.logger.debug("Port %d not open", port) + else: + self.logger.debug("Unknown error occurred when probing port: %s", e) def __scan_for_bank(self, connection: socket.socket): ping_command = "BC" connection.sendall(ping_command.encode("utf-8")) - response = connection.recv(1024) + response = connection.recv(1024).decode("utf-8") - host, port = connection.getpeername() - if response == f"BC {host}\n\r": - self.logger.debug("Bank application found on %s:%s", host, port) - return connection + if response.strip() == f"BC {self.host}": + self.logger.debug("Bank application found on %s:%s", self.host, self.port) + + with self.lock: + if not self.bank_found_event.is_set(): + self.result_peer.port = self.port + self.result_peer.bank_socket = connection + self.bank_found_event.set() + else: + self.logger.debug("Port is open, but no bank application found") diff --git a/src/bank_protocol/commands/account_balance_command.py b/src/bank_protocol/commands/account_balance_command.py index cc5f3e5..0cd3527 100644 --- a/src/bank_protocol/commands/account_balance_command.py +++ b/src/bank_protocol/commands/account_balance_command.py @@ -1,8 +1,39 @@ -from core.request import Request -import re +import logging +from core import Request, BankNodeConfig, Response +from bank_protocol.exceptions import InvalidRequest +from services.account_service import get_account_balance +from bank_protocol.proxy_handler import BankProxy -def account_balance(request: Request): - pass +def account_balance(request: Request, config: BankNodeConfig) -> Response: + logger = logging.getLogger(__name__) + + 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") + + balance = get_account_balance(account_parsed) + + return str(balance) __all__ = ["account_balance"] diff --git a/src/bank_protocol/commands/account_deposit_command.py b/src/bank_protocol/commands/account_deposit_command.py index 6716bbf..2f27bbd 100644 --- a/src/bank_protocol/commands/account_deposit_command.py +++ b/src/bank_protocol/commands/account_deposit_command.py @@ -1,10 +1,11 @@ -from core import Request, BankNodeConfig +import logging +from core import Request, BankNodeConfig, Response from bank_protocol.exceptions import InvalidRequest from services.account_service import deposit_into_account from utils.constants import MONEY_AMOUNT_MAXIMUM +from bank_protocol.proxy_handler import BankProxy - -def account_deposit(request: Request, config: BankNodeConfig): +def account_deposit(request: Request, config: BankNodeConfig) -> Response: if request.body is None: raise InvalidRequest("Invalid request format") @@ -15,9 +16,14 @@ def account_deposit(request: Request, config: BankNodeConfig): account = split_body[0] ip = split_ip[0] amount = split_ip[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") @@ -36,5 +42,7 @@ def account_deposit(request: Request, config: BankNodeConfig): deposit_into_account(account_parsed, amount_parsed) + return None + __all__ = ["account_deposit"] diff --git a/src/bank_protocol/commands/account_withdrawal_command.py b/src/bank_protocol/commands/account_withdrawal_command.py index 2111852..27f18e2 100644 --- a/src/bank_protocol/commands/account_withdrawal_command.py +++ b/src/bank_protocol/commands/account_withdrawal_command.py @@ -1,7 +1,48 @@ -from core.request import Request +from core import Request, BankNodeConfig, Response +from bank_protocol.exceptions import InvalidRequest +from services.account_service import withdraw_from_account +from utils.constants import MONEY_AMOUNT_MAXIMUM +from bank_protocol.proxy_handler import BankProxy -def account_withdrawal(request: Request): - pass + +def account_withdrawal(request: Request, config: BankNodeConfig) -> Response: + if request.body is None: + raise InvalidRequest("Invalid request format") + + try: + split_body = request.body.split("/") + split_ip = split_body[1].split(" ") + + account = split_body[0] + ip = split_ip[0] + amount = split_ip[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") + + if not amount.isdigit(): + raise InvalidRequest("Deposit amount must be a number") + + amount_parsed = int(amount) + + if amount_parsed > MONEY_AMOUNT_MAXIMUM: + raise InvalidRequest("Cannot deposit this much") + + withdraw_from_account(account_parsed, amount_parsed) + + return None __all__ = ["account_withdrawal"] diff --git a/src/bank_protocol/proxy_handler.py b/src/bank_protocol/proxy_handler.py index bb847f1..550c755 100644 --- a/src/bank_protocol/proxy_handler.py +++ b/src/bank_protocol/proxy_handler.py @@ -1,31 +1,45 @@ -import socket import logging +import threading from core import Request, Response, BankNodeConfig -from bank_protocol.exceptions import RequestTimeoutError, NoPortsOpenError, HostUnreachableError +from core.peer import BankPeer +from bank_protocol.exceptions import NoPortsOpenError +from bank_protocol.bank_scanner import BankScanner class BankProxy(): - def __init__(self, request: Request, address: str, config: BankNodeConfig): + def __init__(self, request: Request, host: str, config: BankNodeConfig): self.request = request - self.address = address + self.host = host self.config = config + self.peer = BankPeer(self.host, None, None) + self.bank_found_event = threading.Event() + self.lock = threading.Lock() + self.logger = logging.getLogger(__name__) + self.logger.info("Proxying request") def proxy_request(self) -> Response: + scanner_threads = [] + for port in range(self.config.scan_port_start, self.config.scan_port_end + 1): - self.logger.debug("Connecting to port %d", port) - try: - self.config.used_port = port - self.__proxy_request(port) - return - except socket.error as e: - if e.errno == 111: # Connection refused - self.logger.debug("Port %d not open", port) + if self.bank_found_event.is_set(): + break - self.logger.warning("No ports open on the destination host") - raise NoPortsOpenError("Destination host has no open ports from range") + t = BankScanner(self.host, port, self.peer, self.bank_found_event, self.lock, self.config.timeout) + t.start() + scanner_threads.append(t) - def __proxy_request(self, port): - pass + for t in scanner_threads: + t.join() + + if not self.bank_found_event.is_set(): + self.logger.warning("No bank application found on host %s", self.host) + raise NoPortsOpenError("Bank is unreachable") + + # with self.peer.bank_socket as bank_socket: + self.peer.bank_socket.sendall(str(self.request).encode("utf-8")) + response = self.peer.bank_socket.recv(1024).decode("utf-8") + + return response diff --git a/src/core/peer.py b/src/core/peer.py index ed6fd85..086919f 100644 --- a/src/core/peer.py +++ b/src/core/peer.py @@ -1,2 +1,8 @@ -class Peer(): - pass \ No newline at end of file +import socket +from dataclasses import dataclass + +@dataclass +class BankPeer(): + host: str + port: int + bank_socket: socket.socket diff --git a/src/core/request.py b/src/core/request.py index 5cfa83d..133f1ca 100644 --- a/src/core/request.py +++ b/src/core/request.py @@ -8,6 +8,7 @@ class Request(): def __init__(self, raw_request: str): logger = logging.getLogger(__name__) + raw_request = raw_request.strip() if re.match(r"^[A-Z]{2}$", raw_request): logger.debug("Found 2 char command") @@ -16,7 +17,7 @@ class Request(): elif re.match(r"^[A-Z]{2} .+", raw_request): logger.debug("Found command with arguments") command_code: str = raw_request[0:2] - body: str = raw_request[3:-1] or "" + body: str = raw_request[3:] or "" if len(body.split("\n")) > 1: raise InvalidRequest("Multiline requests are not supported") @@ -26,10 +27,6 @@ class Request(): else: raise InvalidRequest("Invalid request") - def as_request(self): - """Returns a valid request string with CRLF ending""" - return str(self) + "\r\n" - def __str__(self): if self.body: return f"{self.command_code} {self.body}" diff --git a/src/database/exceptions.py b/src/database/exceptions.py index c12ac43..fc62e2d 100644 --- a/src/database/exceptions.py +++ b/src/database/exceptions.py @@ -10,6 +10,7 @@ class DatabaseError(Exception): OUT_OF_ACCOUNT_SPACE = 5 INSUFFICIENT_BALANCE = 6 INVALID_OPERATION = 7 + MONEY_OVERFLOW = 8 def __init__(self, message: str, errno: int, **kwargs): diff --git a/src/models/account_model.py b/src/models/account_model.py index 35ca5a5..83c7ca8 100644 --- a/src/models/account_model.py +++ b/src/models/account_model.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, Integer, CheckConstraint +from sqlalchemy import Column, BigInteger, Integer, CheckConstraint from .base_model import Base @@ -7,7 +7,7 @@ class Account(Base): __tablename__ = 'account' __table_args__ = (CheckConstraint('account_number >= 10000 and account_number <= 99999'),) - account_number = Column(Integer, nullable=False, primary_key=True) + account_number = Column(BigInteger, nullable=False, primary_key=True) balance = Column(Integer) diff --git a/src/services/account_service.py b/src/services/account_service.py index abd18a8..d405967 100644 --- a/src/services/account_service.py +++ b/src/services/account_service.py @@ -4,7 +4,7 @@ from models import Account from database import DatabaseManager from database.exceptions import DatabaseError from database.exception_catcher_decorator import handle_database_errors -from utils.constants import MIN_ACCOUNT_NUMBER, MAX_ACCOUNT_NUMBER +from utils.constants import MIN_ACCOUNT_NUMBER, MAX_ACCOUNT_NUMBER, MONEY_AMOUNT_MAXIMUM @handle_database_errors @@ -58,7 +58,12 @@ def modify_balance(account_number: int, amount: int, add: bool): raise DatabaseError(f"Account with number {account_number} doesn't exist", DatabaseError.NONEXISTENT_ACCOUNT) if add: - account.balance += amount + if account.balance == MONEY_AMOUNT_MAXIMUM: + raise DatabaseError("Reached maximum funds amount", DatabaseError.MONEY_OVERFLOW) + elif account.balance + amount > MONEY_AMOUNT_MAXIMUM: + account.balance = MONEY_AMOUNT_MAXIMUM + else: + account.balance += amount else: if account.balance - amount < 0: raise DatabaseError("Not enough funds on account to withdraw this much", DatabaseError.INSUFFICIENT_BALANCE) diff --git a/src/utils/logger.py b/src/utils/logger.py index 212e325..a0c4dca 100644 --- a/src/utils/logger.py +++ b/src/utils/logger.py @@ -4,7 +4,7 @@ import logging def setup_logger(verbosity: str): if verbosity == "DEBUG": - log_format = "[ %(levelname)s / %(processName)s ] - %(module)s/%(filename)s:%(lineno)d - %(message)s" + log_format = "[ %(levelname)s / %(processName)s ] - %(filename)s:%(lineno)d - %(message)s" else: log_format = "[ %(levelname)s ] - %(message)s"