Started working on account service and related protocol commands

This commit is contained in:
Thastertyn 2025-02-05 14:48:30 +01:00
parent 587756a51b
commit 35a9e64a10
12 changed files with 126 additions and 27 deletions

View File

@ -8,10 +8,6 @@ RESPONSE_TIMEOUT=5
# within this timeframe # within this timeframe
CLIENT_IDLE_TIMEOUT=60 CLIENT_IDLE_TIMEOUT=60
# A valid port number
# If not provided or invalid, defaults to 65526
PORT=65526
# DEBUG, INFO, WARNING, ERROR, CRITICAL are valid # DEBUG, INFO, WARNING, ERROR, CRITICAL are valid
# If an invalid value is provided, the app defaults to INFO # If an invalid value is provided, the app defaults to INFO
VERBOSITY=DEBUG VERBOSITY=DEBUG

1
.gitignore vendored
View File

@ -168,3 +168,4 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
bank.db

View File

@ -95,3 +95,5 @@ class BankNode():
def cleanup(self): def cleanup(self):
self.logger.debug("Closing socket server") self.logger.debug("Closing socket server")
self.socket_server.close() self.socket_server.close()
self.logger.debug("Closing database connection")
self.database_manager.cleanup()

View File

@ -1,23 +1,23 @@
import socket import socket
import multiprocessing import multiprocessing
import logging import logging
from typing import Tuple, Dict from typing import Tuple
import signal import signal
import sys import sys
from bank_protocol.command_handler import CommandHandler from bank_protocol.command_handler import CommandHandler
from core import Request, Response from core import Request, Response, BankNodeConfig
from core.exceptions import BankNodeError from core.exceptions import BankNodeError
class BankWorker(multiprocessing.Process): class BankWorker(multiprocessing.Process):
def __init__(self, client_socket: socket.socket, client_address: Tuple, config: Dict): def __init__(self, client_socket: socket.socket, client_address: Tuple, config: BankNodeConfig):
super().__init__() super().__init__()
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.client_socket = client_socket self.client_socket = client_socket
self.client_socket.settimeout(config["client_idle_timeout"]) self.client_socket.settimeout(config.client_idle_timeout)
self.client_address = client_address self.client_address = client_address
self.command_handler = CommandHandler(config) self.command_handler = CommandHandler(config)
@ -65,8 +65,8 @@ class BankWorker(multiprocessing.Process):
response = "ER " + e.message + "\n\r" response = "ER " + e.message + "\n\r"
self.client_socket.sendall(response.encode("utf-8")) self.client_socket.sendall(response.encode("utf-8"))
except socket.error as e: except socket.error as e:
response = "ER Internal server error\n\r"
self.logger.error(e) self.logger.error(e)
response = "ER Internal server error\n\r"
break break
self.logger.debug("Closing process for %s", self.client_address[0]) self.logger.debug("Closing process for %s", self.client_address[0])

View File

@ -12,3 +12,21 @@ class InvalidRequest(BankNodeError):
def __init__(self, message): def __init__(self, message):
super().__init__(message) super().__init__(message)
self.message = message self.message = message
class RequestTimeoutError(BankNodeError):
def __init__(self, message):
super().__init__(message)
self.message = message
class HostUnreachableError(BankNodeError):
def __init__(self, message):
super().__init__(message)
self.message = message
class NoPortsOpenError(BankNodeError):
def __init__(self, message):
super().__init__(message)
self.message = message

View File

@ -1,23 +1,37 @@
import socket import socket
from typing import Tuple import logging
from core import Request, Response from core import Request, Response, BankNodeConfig
from bank_protocol.exceptions import ProxyError from bank_protocol.exceptions import RequestTimeoutError, NoPortsOpenError, HostUnreachableError
class BankProxy(): class BankProxy():
def __init__(self, request: Request, address: Tuple): def __init__(self, request: Request, address: str, config: BankNodeConfig):
self.request = request self.request = request
self.address = address self.address = address
self.config = config
self.logger = logging.getLogger(__name__)
def proxy_request(self) -> Response: def proxy_request(self) -> Response:
for port in range(self.config.scan_port_start, self.config.scan_port_end + 1):
self.logger.debug("Connecting to port %d", port)
try: 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)
self.logger.warning("No ports open on the destination host")
raise NoPortsOpenError("Destination host has no open ports from range")
def __proxy_request(self, port):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket:
client_socket.connect(self.address) client_socket.connect((self.address, port))
client_socket.sendall(self.request.as_request()) client_socket.sendall(self.request.as_request())
response = client_socket.recv(1024) response = client_socket.recv(1024)
return response return response
except socket.error as e:
raise ProxyError("Proxy error") from e

View File

@ -1,7 +1,9 @@
from .request import * from .request import *
from .response import * from .response import *
from .config import BankNodeConfig
__all__ = [ __all__ = [
*request.__all__, *request.__all__,
*response.__all__ *response.__all__,
*config.__all__
] ]

View File

@ -69,3 +69,6 @@ class BankNodeConfig:
"scan_port_start": self.scan_port_start, "scan_port_start": self.scan_port_start,
"scan_port_end": self.scan_port_end, "scan_port_end": self.scan_port_end,
} }
__all__ = ["BankNodeConfig"]

View File

@ -1,9 +1,9 @@
import logging import logging
from typing import Generator from typing import Generator
from contextlib import contextmanager
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy import create_engine, text from sqlalchemy import create_engine, text
from sqlalchemy.exc import DatabaseError from sqlalchemy.exc import DatabaseError
from database.exceptions import DatabaseConnectionError from database.exceptions import DatabaseConnectionError
@ -29,13 +29,13 @@ class DatabaseManager():
self.engine = create_engine('sqlite:///bank.db') self.engine = create_engine('sqlite:///bank.db')
self.Session = sessionmaker(bind=self.engine) self.Session = sessionmaker(bind=self.engine)
self.create_tables()
def create_tables(self): def create_tables(self):
self.logger.debug("Creating tables") self.logger.debug("Creating tables")
Base.metadata.create_all(self.engine) Base.metadata.create_all(self.engine)
def cleanup(self) -> None: def cleanup(self) -> None:
self.logger.debug("Closing connection")
self.engine.dispose() self.engine.dispose()
def test_connection(self) -> bool: def test_connection(self) -> bool:
@ -52,7 +52,8 @@ class DatabaseManager():
return False return False
@classmethod @classmethod
def get_session(cls) -> Generator: @contextmanager
def get_session(cls) -> Generator[Session]:
session = cls._instance.Session() session = cls._instance.Session()
try: try:
yield session yield session

View File

@ -25,4 +25,16 @@ class DuplicateEntryError(DatabaseError):
self.message = message self.message = message
class NonexistentAccountError(DatabaseError):
def __init__(self, message: str):
super().__init__(message)
self.message = message
class OutOfAccountSpaceError(DatabaseError):
def __init__(self, message: str):
super().__init__(message)
self.message = message
__all__ = ["DatabaseError", "DatabaseConnectionError", "DuplicateEntryError"] __all__ = ["DatabaseError", "DatabaseConnectionError", "DuplicateEntryError"]

View File

@ -0,0 +1,47 @@
from sqlalchemy import func
from models import Account
from database import DatabaseManager
from database.exceptions import OutOfAccountSpaceError, NonexistentAccountError
from utils.constants import MIN_ACCOUNT_NUMBER, MAX_ACCOUNT_NUMBER
def get_next_id() -> int:
with DatabaseManager.get_session() as session:
new_id = session.query(func.max(Account.account_number)).scalar()
new_id = new_id if new_id is not None else MIN_ACCOUNT_NUMBER
if new_id > MAX_ACCOUNT_NUMBER:
raise OutOfAccountSpaceError("Too many users already exist, cannot open new account")
return new_id
def create_account() -> int:
new_id = get_next_id()
with DatabaseManager.get_session() as session:
new_account = Account(account_number=new_id, balance=0)
session.add(new_account)
session.commit()
return new_id
def get_account_balance(account_number: int) -> int:
with DatabaseManager.get_session() as session:
account: Account = session.query(Account).where(Account.account_number == account_number).one_or_none()
if account is NotImplemented:
raise NonexistentAccountError(f"Account with number {account_number} doesn't exist")
return account.balance
def withdraw_from_account():
pass
def deposit_into_account():
pass
def delete_account():
pass

View File

@ -3,3 +3,6 @@ import re
IP_REGEX = r"^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$" IP_REGEX = r"^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$"
ACCOUNT_NUMBER_REGEX = r"[0-9]{9}" ACCOUNT_NUMBER_REGEX = r"[0-9]{9}"
MONEY_AMOUNT_MAXIMUM = (2 ^ 63) - 1 MONEY_AMOUNT_MAXIMUM = (2 ^ 63) - 1
MIN_ACCOUNT_NUMBER = 10_000
MAX_ACCOUNT_NUMBER = 99_999