From 97558099298ab2601e6e034ab7a57e32430700f8 Mon Sep 17 00:00:00 2001 From: Michael Woods Date: Fri, 3 Jan 2025 23:09:31 -0500 Subject: [PATCH] Added code for a server to maybe run and respond to requests based on handlers registered with the server. --- requirements.txt | 4 +- src/packetserver/common/__init__.py | 50 ++++++++++++++++----- src/packetserver/server/__init__.py | 29 +++++++++--- src/packetserver/server/constants.py | 5 +++ src/packetserver/server/requests.py | 66 ++++++++++++++++++++++++++++ 5 files changed, 138 insertions(+), 16 deletions(-) create mode 100644 src/packetserver/server/constants.py create mode 100644 src/packetserver/server/requests.py diff --git a/requirements.txt b/requirements.txt index 5c25280..5fb1257 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ pyham_pe msgpack pyham_ax25 -ZODB \ No newline at end of file +ZODB +BTrees +transaction \ No newline at end of file diff --git a/src/packetserver/common/__init__.py b/src/packetserver/common/__init__.py index 7cff2b5..b0cbd33 100644 --- a/src/packetserver/common/__init__.py +++ b/src/packetserver/common/__init__.py @@ -5,6 +5,7 @@ from msgpack import packb, unpackb from enum import Enum import bz2 from typing import Union, Self +import datetime class PacketServerConnection(Connection): @@ -17,6 +18,23 @@ class PacketServerConnection(Connection): # Now perform any initialization of your own that you might need self.data = Unpacker() self.data_lock = Lock() + self.connection_created = datetime.datetime.now(datetime.UTC) + self.connection_last_activity = datetime.datetime.now(datetime.UTC) + + + @property + def local_callsign(self): + if self.incoming: + return self.call_to + else: + return self.call_from + + @property + def remote_callsign(self): + if self.incoming: + return self.call_from + else: + return self.call_to def connected(self): print("connected") @@ -27,11 +45,16 @@ class PacketServerConnection(Connection): pass def data_received(self, pid, data): + self.connection_last_activity = datetime.datetime.now(datetime.UTC) with self.data_lock: self.data.feed(data) for fn in PacketServerConnection.receive_subscribers: fn(self) + def send_data(self, data: Union[bytes, bytearray]): + self.connection_last_activity = datetime.datetime.now(datetime.UTC) + super().send_data(data) + @classmethod def query_accept(cls, port, call_from, call_to): return True @@ -117,15 +140,8 @@ class Message: self.data['d'] = str(payload) @classmethod - def unpack(cls, msg_bytes: bytes) -> Self: - try: - unpacked = unpackb(msg_bytes) - except Exception as e: - raise ValueError("ERROR: msg_bytes didn't contain a valid msgpack object.\n" + str(e)) - for i in ('t', 'c', 'd'): - if i not in unpacked: - raise ValueError("ERROR: unpacked bytes do not contain a valid Message object.") - + def partial_unpack(cls, msg: dict) -> Self: + unpacked = msg comp = Message.CompressionType(unpacked['c']) msg_type = Message.MessageType(unpacked['t']) raw_data = unpacked['d'] @@ -136,8 +152,22 @@ class Message: data = unpackb(bz2.decompress(raw_data)) else: raise NotImplementedError(f"Compression type {comp.name} is not implemented yet.") + return Message(msg_type, comp, data) + @classmethod + def unpack(cls, msg_bytes: bytes) -> Self: + try: + unpacked = unpackb(msg_bytes) + except Exception as e: + raise ValueError("ERROR: msg_bytes didn't contain a valid msgpack object.\n" + str(e)) + if type(unpacked) is not dict: + raise ValueError("ERROR: unpacked message was not a packetserver message.") + for i in ('t', 'c', 'd'): + if i not in unpacked: + raise ValueError("ERROR: unpacked message was not a packetserver message.") + return Message.partial_unpack(unpacked) + class Request(Message): class Method(Enum): GET = 0 @@ -162,7 +192,7 @@ class Request(Message): @property def path(self): if 'p' in self.data: - return str(self.data['p']) + return str(self.data['p']).lower().strip() else: return "" diff --git a/src/packetserver/server/__init__.py b/src/packetserver/server/__init__.py index a24c8ac..d518e08 100644 --- a/src/packetserver/server/__init__.py +++ b/src/packetserver/server/__init__.py @@ -1,8 +1,14 @@ -import pe +import pe.app from ..common import PacketServerConnection +from .constants import default_server_config +from copy import deepcopy import ax25 from pathlib import Path -import ZODB, transaction, ZODB.FileStorage +import ZODB, ZODB.FileStorage +from BTrees.OOBTree import OOBTree +from .requests import process_incoming_data +from .requests import standard_handlers + class Server: def __init__(self, pe_server: str, port: int, server_callsign: str, data_dir: str = None): @@ -11,6 +17,7 @@ class Server: self.callsign = server_callsign self.pe_server = pe_server self.pe_port = port + self.handlers = deepcopy(standard_handlers) if data_dir: data_path = Path(data_dir) else: @@ -28,13 +35,25 @@ class Server: self.home_dir = data_dir self.storage = ZODB.FileStorage.FileStorage(self.data_file) self.db = ZODB.DB(self.storage) + with self.db.transaction() as conn: + if 'config' not in conn.root(): + conn.root.config = deepcopy(default_server_config) + if 'users' not in conn.root(): + conn.root.users = OOBTree() + self.app = pe.app.Application() + PacketServerConnection.receive_subscribers.append(lambda x: self.server_receiver(x)) @property def data_file(self) -> str: return str(Path(self.home_dir).joinpath('data.zopedb')) - def server_receiver(self, conn: PacketServerConnection): - pass - pass \ No newline at end of file + process_incoming_data(conn, self) + + def start(self): + self.app.start(self.pe_server, self.pe_port) + self.app.register_callsigns(self.callsign) + + def stop(self): + self.app.stop() \ No newline at end of file diff --git a/src/packetserver/server/constants.py b/src/packetserver/server/constants.py new file mode 100644 index 0000000..48fee2b --- /dev/null +++ b/src/packetserver/server/constants.py @@ -0,0 +1,5 @@ + +default_server_config = { + "motd": "Welcome to this PacketServer BBS!", + "operator": "placeholder", +} \ No newline at end of file diff --git a/src/packetserver/server/requests.py b/src/packetserver/server/requests.py new file mode 100644 index 0000000..f0b5831 --- /dev/null +++ b/src/packetserver/server/requests.py @@ -0,0 +1,66 @@ +"""Module for handling requests as they arrive to connection objects and servers.""" + +from . import PacketServerConnection +from . import Server +from msgpack.exceptions import OutOfData +from ..common import Message, Request, Response + +def handle_root_get(req: Request, conn: PacketServerConnection, server: Server): + response = Response.blank() + response.compression = Message.CompressionType.BZIP2 + operator = "" + motd = "" + with server.db.transaction() as storage: + if 'motd' in storage.root.config: + motd = storage.root.config['motd'] + if 'operator' in storage.root.config: + operator = storage.root.config['operator'] + + response.payload = { + 'operator': operator, + 'motd': motd + } + + if conn.state.name == "CONNECTED": + conn.send_data(response.pack()) + +standard_handlers = { + "": { + "GET": handle_root_get + } +} + +def handle_request(req: Request, conn: PacketServerConnection, server: Server): + """Handles a proper request by handing off to the appropriate function depending on method and Path.""" + if req.path in server.handlers: + if req.method.name in server.handlers[req.path]: + server.handlers[req.path][req.method.name](req, conn, server) + return + response_404 = Response.blank() + response_404.status_code = 404 + if conn.state.name == "CONNECTED": + conn.send_data(response_404.pack()) + +def process_incoming_data(connection: PacketServerConnection, server: Server): + """Handles incoming data.""" + with connection.data_lock: + while True: + try: + msg = Message.partial_unpack(connection.data.unpack()) + except OutOfData: + break + except ValueError: + r = Response.blank() + r.status_code = 400 + r.payload = "BAD REQUEST. COULD NOT PARSE INCOMING DATA AS PACKETSERVER MESSAGE" + connection.send_data(r.pack()) + connection.send_data(b"BAD REQUEST. COULD NOT PARSE INCOMING DATA AS PACKETSERVER MESSAGE") + + try: + request = Request(msg) + except ValueError: + r = Response.blank() + r.status_code = 400 + r.payload = "BAD REQUEST. DID NOT RECEIVE A REQUEST MESSAGE." + connection.send_data(r.pack()) + connection.send_data(b"BAD REQUEST. DID NOT RECEIVE A REQUEST MESSAGE.")