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
# Anything else results in an exit
SCAN_PORT_RANGE=65525:65535
# Manually set the IP address
# IP=127.0.0.1

View File

@ -1,5 +1,90 @@
# 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
### 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"
python-dotenv==1.0.1 ; python_version >= "3.12" and python_version < "4.0"
sqlalchemy==2.0.37 ; python_version >= "3.12" and python_version < "4.0"
typing-extensions==4.12.2 ; python_version >= "3.12" and python_version < "4.0"
greenlet==3.1.1
python-dotenv==1.0.1
sqlalchemy==2.0.37
typing-extensions==4.12.2

View File

@ -2,6 +2,7 @@ import socket
import signal
import sys
import logging
import errno
from core.config import BankNodeConfig
from core.exceptions import ConfigError
@ -66,7 +67,7 @@ class BankNode():
self.logger.error("All ports are in use")
self.exit_with_error()
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)
else:
self.logger.critical("Unknown error: %s", e)

View File

@ -4,12 +4,13 @@ import logging
from typing import Tuple
import signal
import sys
import time
from bank_protocol.command_handler import CommandHandler
from core import Request, Response, BankNodeConfig
from core.exceptions import BankNodeError
from utils.logger import setup_logger
from database import DatabaseManager
class BankWorker(multiprocessing.Process):
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
# -> Set it up again in the fresh process
if sys.platform == "win32":
DatabaseManager()
setup_logger(self.config.verbosity)
self.logger = logging.getLogger(__name__)
self.command_handler = CommandHandler(self.config)
self.client_socket.setblocking(True)
self.client_socket.settimeout(self.config.client_idle_timeout)
self.__setup_signals()
with self.client_socket:
@ -55,7 +57,7 @@ class BankWorker(multiprocessing.Process):
def serve_client(self):
buffer = ""
ending = "\r\n"
ending = None
while True:
try:
@ -78,34 +80,52 @@ class BankWorker(multiprocessing.Process):
ending = "\r"
self.logger.debug("CR detected")
request_data, buffer = buffer.split(ending, 1)
self.logger.debug("Processing request: %r", request_data)
elif len(buffer) == 2 or len(buffer) >= 16:
# 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)
response: Response = self.command_handler.execute(request) + ending
if ending is None:
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.logger.debug("Response sent to %s", self.client_address[0])
buffer = ""
ending = None
except socket.timeout:
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.shutdown(socket.SHUT_RDWR)
self.client_socket.close()
break
except UnicodeDecodeError:
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"))
break
except BankNodeError as e:
response = "ER " + e.message + ending
response = "ER " + e.message + (ending or "\n")
self.client_socket.sendall(response.encode("utf-8"))
buffer = ""
except socket.error as e:
self.logger.error(e)
response = "ER Internal server error" + ending
response = "ER Internal server error" + (ending or "\n")
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, _):
"""Log the signal caught and exit with status 0"""

View File

@ -1,6 +1,7 @@
import socket
import threading
import logging
import errno
from core.peer import BankPeer
@ -31,7 +32,7 @@ class BankScanner(threading.Thread):
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 or e.errno == errno.WSAECONNREFUSED: # Connection refused
self.logger.debug("Port %d not open", port)
else:
self.logger.debug("Unknown error occurred when probing port: %s", e)

View File

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

View File

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

View File

@ -9,7 +9,7 @@ def account_create(request: Request, config: BankNodeConfig) -> Response:
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)
return None
return "AD"
__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"]

View File

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

View File

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

View File

@ -7,6 +7,6 @@ def bank_number_of_clients(request: Request, _) -> Response:
raise InvalidRequest("Incorrect usage")
number_of_clients = get_account_count()
return number_of_clients
return f"BN {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 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:
raise InvalidRequest("Incorrect usage")
total_balace = get_total_balance()
return total_balace
return f"BA {total_balace}"
__all__ = ["bank_total_amount"]

View File

@ -39,7 +39,7 @@ class BankProxy():
raise NoPortsOpenError("Bank is unreachable")
# 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")
return response

View File

@ -26,13 +26,11 @@ class BankNodeConfig:
# Port validation
if not scan_port_range or scan_port_range == "":
self.logger.error("Scan port range not defined")
raise ConfigError("Scan port range not defined")
range_split = scan_port_range.split(":")
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")
# Timeout validation