diff --git a/.env.example b/.env.example index 2025791..4179e50 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore index 0dbf2f2..9258749 100644 --- a/.gitignore +++ b/.gitignore @@ -168,3 +168,4 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +bank.db \ No newline at end of file diff --git a/src/bank_node/bank_node.py b/src/bank_node/bank_node.py index bf646ee..5207dce 100644 --- a/src/bank_node/bank_node.py +++ b/src/bank_node/bank_node.py @@ -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() diff --git a/src/bank_node/bank_worker.py b/src/bank_node/bank_worker.py index 764a727..c09e60a 100644 --- a/src/bank_node/bank_worker.py +++ b/src/bank_node/bank_worker.py @@ -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]) diff --git a/src/bank_protocol/exceptions.py b/src/bank_protocol/exceptions.py index c1f6d95..3db4645 100644 --- a/src/bank_protocol/exceptions.py +++ b/src/bank_protocol/exceptions.py @@ -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 diff --git a/src/bank_protocol/proxy_handler.py b/src/bank_protocol/proxy_handler.py index dc5539b..4c356fa 100644 --- a/src/bank_protocol/proxy_handler.py +++ b/src/bank_protocol/proxy_handler.py @@ -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 diff --git a/src/core/__init__.py b/src/core/__init__.py index 43b6058..22398b8 100644 --- a/src/core/__init__.py +++ b/src/core/__init__.py @@ -1,7 +1,9 @@ from .request import * from .response import * +from .config import BankNodeConfig __all__ = [ *request.__all__, - *response.__all__ -] \ No newline at end of file + *response.__all__, + *config.__all__ +] diff --git a/src/core/config.py b/src/core/config.py index 35fe4b5..0d2c622 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -69,3 +69,6 @@ class BankNodeConfig: "scan_port_start": self.scan_port_start, "scan_port_end": self.scan_port_end, } + + +__all__ = ["BankNodeConfig"] diff --git a/src/database/database_manager.py b/src/database/database_manager.py index dbc0aba..ecee8ae 100644 --- a/src/database/database_manager.py +++ b/src/database/database_manager.py @@ -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 diff --git a/src/database/exceptions.py b/src/database/exceptions.py index 5e7c802..c7469c5 100644 --- a/src/database/exceptions.py +++ b/src/database/exceptions.py @@ -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"] diff --git a/src/services/account_serice.py b/src/services/account_serice.py new file mode 100644 index 0000000..e0aebc6 --- /dev/null +++ b/src/services/account_serice.py @@ -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 diff --git a/src/utils/constants.py b/src/utils/constants.py index a13813c..44ed129 100644 --- a/src/utils/constants.py +++ b/src/utils/constants.py @@ -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