Many tweaks and fixes and implemented withdraw and balance commands
This commit is contained in:
parent
1cfc561301
commit
9e62d2517c
@ -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/)
|
||||
|
@ -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, _):
|
||||
|
@ -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")
|
||||
|
@ -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"]
|
||||
|
@ -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"]
|
||||
|
@ -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"]
|
||||
|
@ -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
|
||||
|
@ -1,2 +1,8 @@
|
||||
class Peer():
|
||||
pass
|
||||
import socket
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class BankPeer():
|
||||
host: str
|
||||
port: int
|
||||
bank_socket: socket.socket
|
||||
|
@ -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}"
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user