Final commit
This commit is contained in:
parent
9e62d2517c
commit
0991aad105
@ -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
|
||||||
|
85
README.md
85
README.md
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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"""
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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:
|
||||||
|
@ -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"]
|
||||||
|
@ -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}"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -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"]
|
||||||
|
@ -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"]
|
||||||
|
@ -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"]
|
||||||
|
@ -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"]
|
||||||
|
@ -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"]
|
||||||
|
@ -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"]
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user