Final commit

This commit is contained in:
Thastertyn 2025-02-08 09:17:19 +01:00
parent 9e62d2517c
commit 0991aad105
17 changed files with 170 additions and 32 deletions

View File

@ -16,3 +16,6 @@ VERBOSITY=DEBUG
# Only two port numbers must be provided # Only two port numbers must be provided
# Anything else results in an exit # Anything else results in an exit
SCAN_PORT_RANGE=65525:65535 SCAN_PORT_RANGE=65525:65535
# Manually set the IP address
# IP=127.0.0.1

View File

@ -1,5 +1,90 @@
# nu (ν) # nu (ν)
# Running
## Configuration
The application can be configured using a `.env` file. Here are the available options:
`RESPONSE_TIMEOUT`
- Specifies the response timeout in seconds.
- Default: `5`
- Example: `RESPONSE_TIMEOUT=5`
`CLIENT_IDLE_TIMEOUT`
- Defines the client idle timeout in seconds.
- Default: `60`
- Example: `CLIENT_IDLE_TIMEOUT=60`
`VERBOSITY`
- Determines the logging verbosity level.
- Valid values: DEBUG, INFO, WARNING, ERROR, CRITICAL.
- Default: `INFO`
- Example: `VERBOSITY=DEBUG`
`SCAN_PORT_RANGE`
- Defines the port range to scan as `PORT:PORT` separated by colon (`:`).
- The ports must be valid and there must be exactly two of them.
- Example: `SCAN_PORT_RANGE=65525:65535`
`IP`
- Manually set the IP address.
- Example: `IP=127.0.0.1`
## Running
The application requires a few dependenices, specified in `requirements.txt`.
To install these dependencies run
```bash
pip install -r requirements.txt
```
Upon successful installation, the program is ready to run. The database used is SQLite, hence no SQL structure is needed to be imported, nor are any credentials required.
To run the app, run
```bash
python src/app.py
```
If the verbosity set in `.env` is `DEBUG`, the program will print many useful descriptions of it's actions together with a detailed location of the action including file, line and process name.
## Definitions
The program uses the following protocol over TCP
| Operation | Request Format | Response Format |
| ---------------------- | -------------------------------- | --------------- |
| Bank code | BC | BC \<ip\> |
| Account create | AC \<account\>/\<ip\> | AC |
| Account deposit | AD \<account\>/\<ip\> \<number\> | AD |
| Account withdrawal | AW \<account\>/\<ip\> \<number\> | AW |
| Account balance | AB \<account\>/\<ip\> | AB \<number\> |
| Account remove | AR \<account\>/\<ip\> | AR |
| Bank (total) amount | BA | BA \<number\> |
| Bank number of clients | BN | BN \<number\> |
Any of these commands may return an error message in format
`ER <message>`
## Sources ## Sources
### Signal catching ### Signal catching

View File

@ -1,4 +1,4 @@
greenlet==3.1.1 ; python_version < "3.14" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and python_version >= "3.12" greenlet==3.1.1
python-dotenv==1.0.1 ; python_version >= "3.12" and python_version < "4.0" python-dotenv==1.0.1
sqlalchemy==2.0.37 ; python_version >= "3.12" and python_version < "4.0" sqlalchemy==2.0.37
typing-extensions==4.12.2 ; python_version >= "3.12" and python_version < "4.0" typing-extensions==4.12.2

View File

@ -2,6 +2,7 @@ import socket
import signal import signal
import sys import sys
import logging import logging
import errno
from core.config import BankNodeConfig from core.config import BankNodeConfig
from core.exceptions import ConfigError from core.exceptions import ConfigError
@ -66,7 +67,7 @@ class BankNode():
self.logger.error("All ports are in use") self.logger.error("All ports are in use")
self.exit_with_error() self.exit_with_error()
except socket.error as e: except socket.error as e:
if e.errno == 99: # Cannot assign to requested address if e.errno == 98 or e.errno == errno.WSAEADDRINUSE: # Cannot assign to requested address
self.logger.critical("Cannot use the IP address %s", self.config.ip) self.logger.critical("Cannot use the IP address %s", self.config.ip)
else: else:
self.logger.critical("Unknown error: %s", e) self.logger.critical("Unknown error: %s", e)

View File

@ -4,12 +4,13 @@ import logging
from typing import Tuple from typing import Tuple
import signal import signal
import sys import sys
import time
from bank_protocol.command_handler import CommandHandler from bank_protocol.command_handler import CommandHandler
from core import Request, Response, BankNodeConfig from core import Request, Response, BankNodeConfig
from core.exceptions import BankNodeError from core.exceptions import BankNodeError
from utils.logger import setup_logger from utils.logger import setup_logger
from database import DatabaseManager
class BankWorker(multiprocessing.Process): class BankWorker(multiprocessing.Process):
def __init__(self, client_socket: socket.socket, client_address: Tuple, config: BankNodeConfig): def __init__(self, client_socket: socket.socket, client_address: Tuple, config: BankNodeConfig):
@ -38,14 +39,15 @@ class BankWorker(multiprocessing.Process):
# and loses its configuration by default # and loses its configuration by default
# -> Set it up again in the fresh process # -> Set it up again in the fresh process
if sys.platform == "win32": if sys.platform == "win32":
DatabaseManager()
setup_logger(self.config.verbosity) setup_logger(self.config.verbosity)
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.command_handler = CommandHandler(self.config) self.command_handler = CommandHandler(self.config)
self.client_socket.setblocking(True) self.client_socket.setblocking(True)
self.client_socket.settimeout(self.config.client_idle_timeout) self.client_socket.settimeout(self.config.client_idle_timeout)
self.__setup_signals() self.__setup_signals()
with self.client_socket: with self.client_socket:
@ -55,7 +57,7 @@ class BankWorker(multiprocessing.Process):
def serve_client(self): def serve_client(self):
buffer = "" buffer = ""
ending = "\r\n" ending = None
while True: while True:
try: try:
@ -78,34 +80,52 @@ class BankWorker(multiprocessing.Process):
ending = "\r" ending = "\r"
self.logger.debug("CR detected") self.logger.debug("CR detected")
request_data, buffer = buffer.split(ending, 1) elif len(buffer) == 2 or len(buffer) >= 16:
self.logger.debug("Processing request: %r", request_data) # If no line ending is provided, make an assumption
# Either a 2 character command was sent without any line ending
# or the shorted valid command with arguments has to be 16 or more
# characters long
ending = "\n"
self.logger.debug("Messages appear to be missing line ending, proceeding with processing")
request = Request(request_data) if ending is None:
response: Response = self.command_handler.execute(request) + ending self.logger.debug("No line ending found, and messages don't look complete. Assuming another part will come")
continue
response = self.process_request(buffer, ending)
self.client_socket.sendall(response.encode("utf-8")) self.client_socket.sendall(response.encode("utf-8"))
self.logger.debug("Response sent to %s", self.client_address[0]) self.logger.debug("Response sent to %s", self.client_address[0])
buffer = ""
ending = None
except socket.timeout: except socket.timeout:
self.logger.debug("Client was idle for too long. Ending connection") self.logger.debug("Client was idle for too long. Ending connection")
response = "ER Idle too long" + ending response = "ER Idle too long" + (ending or "\n")
self.client_socket.sendall(response.encode("utf-8")) self.client_socket.sendall(response.encode("utf-8"))
self.client_socket.shutdown(socket.SHUT_RDWR) self.client_socket.shutdown(socket.SHUT_RDWR)
self.client_socket.close() self.client_socket.close()
break break
except UnicodeDecodeError: except UnicodeDecodeError:
self.logger.warning("Received a non utf-8 message") self.logger.warning("Received a non utf-8 message")
response = "ER Not utf-8 message" response = "ER Not utf-8 message" + (ending or "\n")
self.client_socket.sendall(response.encode("utf-8")) self.client_socket.sendall(response.encode("utf-8"))
break break
except BankNodeError as e: except BankNodeError as e:
response = "ER " + e.message + ending response = "ER " + e.message + (ending or "\n")
self.client_socket.sendall(response.encode("utf-8")) self.client_socket.sendall(response.encode("utf-8"))
buffer = ""
except socket.error as e: except socket.error as e:
self.logger.error(e) self.logger.error(e)
response = "ER Internal server error" + ending response = "ER Internal server error" + (ending or "\n")
break break
def process_request(self, buffer: str, line_ending: str) -> Response:
self.logger.debug("Processing request: %r", buffer)
request = Request(buffer)
response: Response = self.command_handler.execute(request) + line_ending
return response
def gracefully_exit_worker(self, signum, _): def gracefully_exit_worker(self, signum, _):
"""Log the signal caught and exit with status 0""" """Log the signal caught and exit with status 0"""

View File

@ -1,6 +1,7 @@
import socket import socket
import threading import threading
import logging import logging
import errno
from core.peer import BankPeer from core.peer import BankPeer
@ -31,7 +32,7 @@ class BankScanner(threading.Thread):
except socket.timeout: except socket.timeout:
self.logger.debug("Connection for port %d timed out", port) self.logger.debug("Connection for port %d timed out", port)
except socket.error as e: except socket.error as e:
if e.errno == 111: # Connection refused if e.errno == 111 or e.errno == errno.WSAECONNREFUSED: # Connection refused
self.logger.debug("Port %d not open", port) self.logger.debug("Port %d not open", port)
else: else:
self.logger.debug("Unknown error occurred when probing port: %s", e) self.logger.debug("Unknown error occurred when probing port: %s", e)

View File

@ -42,7 +42,7 @@ class CommandHandler:
try: try:
response = command(request, self.config) response = command(request, self.config)
if response is not None: if response is not None:
return f"{request.command_code} {response}" return str(response).strip()
else: else:
return request.command_code return request.command_code
except DatabaseError as e: except DatabaseError as e:

View File

@ -33,7 +33,7 @@ def account_balance(request: Request, config: BankNodeConfig) -> Response:
balance = get_account_balance(account_parsed) balance = get_account_balance(account_parsed)
return str(balance) return f"AB {str(balance)}"
__all__ = ["account_balance"] __all__ = ["account_balance"]

View File

@ -9,7 +9,7 @@ def account_create(request: Request, config: BankNodeConfig) -> Response:
account_number = create_account() account_number = create_account()
return f"{account_number}/{config.ip}" return f"AC {account_number}/{config.ip}"

View File

@ -42,7 +42,7 @@ def account_deposit(request: Request, config: BankNodeConfig) -> Response:
deposit_into_account(account_parsed, amount_parsed) deposit_into_account(account_parsed, amount_parsed)
return None return "AD"
__all__ = ["account_deposit"] __all__ = ["account_deposit"]

View File

@ -1,7 +1,37 @@
from core.request import Request from core import Request, BankNodeConfig, Response
from bank_protocol.exceptions import InvalidRequest
from services.account_service import delete_account
from bank_protocol.proxy_handler import BankProxy
def account_remove(request: Request):
pass def account_remove(request: Request, config: BankNodeConfig) -> Response:
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")
delete_account(account_parsed)
return "AR"
__all__ = ["account_remove"] __all__ = ["account_remove"]

View File

@ -42,7 +42,7 @@ def account_withdrawal(request: Request, config: BankNodeConfig) -> Response:
withdraw_from_account(account_parsed, amount_parsed) withdraw_from_account(account_parsed, amount_parsed)
return None return "AW"
__all__ = ["account_withdrawal"] __all__ = ["account_withdrawal"]

View File

@ -5,6 +5,6 @@ def bank_code(request: Request, config: BankNodeConfig) -> Response:
if request.body is not None: if request.body is not None:
raise InvalidRequest("Incorrect usage") raise InvalidRequest("Incorrect usage")
return config.ip return f"BC {config.ip}"
__all__ = ["bank_code"] __all__ = ["bank_code"]

View File

@ -7,6 +7,6 @@ def bank_number_of_clients(request: Request, _) -> Response:
raise InvalidRequest("Incorrect usage") raise InvalidRequest("Incorrect usage")
number_of_clients = get_account_count() number_of_clients = get_account_count()
return number_of_clients return f"BN {number_of_clients}"
__all__ = ["bank_number_of_clients"] __all__ = ["bank_number_of_clients"]

View File

@ -2,12 +2,12 @@ from core import Request, Response
from bank_protocol.exceptions import InvalidRequest from bank_protocol.exceptions import InvalidRequest
from services.account_service import get_total_balance from services.account_service import get_total_balance
def bank_total_amount(request: Request, _): def bank_total_amount(request: Request, _) -> Response:
if request.body is not None: if request.body is not None:
raise InvalidRequest("Incorrect usage") raise InvalidRequest("Incorrect usage")
total_balace = get_total_balance() total_balace = get_total_balance()
return total_balace return f"BA {total_balace}"
__all__ = ["bank_total_amount"] __all__ = ["bank_total_amount"]

View File

@ -39,7 +39,7 @@ class BankProxy():
raise NoPortsOpenError("Bank is unreachable") raise NoPortsOpenError("Bank is unreachable")
# with self.peer.bank_socket as bank_socket: # with self.peer.bank_socket as bank_socket:
self.peer.bank_socket.sendall(str(self.request).encode("utf-8")) self.peer.bank_socket.sendall((str(self.request) + "\r\n").encode("utf-8"))
response = self.peer.bank_socket.recv(1024).decode("utf-8") response = self.peer.bank_socket.recv(1024).decode("utf-8")
return response return response

View File

@ -26,13 +26,11 @@ class BankNodeConfig:
# Port validation # Port validation
if not scan_port_range or scan_port_range == "": if not scan_port_range or scan_port_range == "":
self.logger.error("Scan port range not defined")
raise ConfigError("Scan port range not defined") raise ConfigError("Scan port range not defined")
range_split = scan_port_range.split(":") range_split = scan_port_range.split(":")
if len(range_split) != 2 or not range_split[0].isdigit() or not range_split[1].isdigit(): if len(range_split) != 2 or not range_split[0].isdigit() or not range_split[1].isdigit():
self.logger.error("Scan port range is not in valid format")
raise ConfigError("Scan port range is not in valid format") raise ConfigError("Scan port range is not in valid format")
# Timeout validation # Timeout validation