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
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
# If an invalid value is provided, the app defaults to INFO
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.
#.idea/
bank.db

View File

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

View File

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

View File

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

View File

@ -1,7 +1,9 @@
from .request import *
from .response import *
from .config import BankNodeConfig
__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_end": self.scan_port_end,
}
__all__ = ["BankNodeConfig"]

View File

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

View File

@ -25,4 +25,16 @@ class DuplicateEntryError(DatabaseError):
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"]

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}$"
ACCOUNT_NUMBER_REGEX = r"[0-9]{9}"
MONEY_AMOUNT_MAXIMUM = (2 ^ 63) - 1
MIN_ACCOUNT_NUMBER = 10_000
MAX_ACCOUNT_NUMBER = 99_999