Many tweaks and fixes and implemented withdraw and balance commands

This commit is contained in:
Thastertyn 2025-02-07 18:01:10 +01:00
parent 1cfc561301
commit 9e62d2517c
13 changed files with 186 additions and 67 deletions

View File

@ -21,4 +21,8 @@
### Platform specific problems
- [Windows sends partial data](https://stackoverflow.com/a/31754798)
- [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/)

View File

@ -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, _):

View File

@ -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")

View File

@ -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"]

View File

@ -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"]

View File

@ -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"]

View File

@ -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

View File

@ -1,2 +1,8 @@
class Peer():
pass
import socket
from dataclasses import dataclass
@dataclass
class BankPeer():
host: str
port: int
bank_socket: socket.socket

View File

@ -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}"

View File

@ -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):

View File

@ -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)

View File

@ -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)

View File

@ -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"