Added code for a server to maybe run and respond to requests based on handlers registered with the server.

This commit is contained in:
Michael Woods
2025-01-03 23:09:31 -05:00
parent 44226376c9
commit 9755809929
5 changed files with 138 additions and 16 deletions

View File

@@ -2,3 +2,5 @@ pyham_pe
msgpack msgpack
pyham_ax25 pyham_ax25
ZODB ZODB
BTrees
transaction

View File

@@ -5,6 +5,7 @@ from msgpack import packb, unpackb
from enum import Enum from enum import Enum
import bz2 import bz2
from typing import Union, Self from typing import Union, Self
import datetime
class PacketServerConnection(Connection): class PacketServerConnection(Connection):
@@ -17,6 +18,23 @@ class PacketServerConnection(Connection):
# Now perform any initialization of your own that you might need # Now perform any initialization of your own that you might need
self.data = Unpacker() self.data = Unpacker()
self.data_lock = Lock() 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): def connected(self):
print("connected") print("connected")
@@ -27,11 +45,16 @@ class PacketServerConnection(Connection):
pass pass
def data_received(self, pid, data): def data_received(self, pid, data):
self.connection_last_activity = datetime.datetime.now(datetime.UTC)
with self.data_lock: with self.data_lock:
self.data.feed(data) self.data.feed(data)
for fn in PacketServerConnection.receive_subscribers: for fn in PacketServerConnection.receive_subscribers:
fn(self) fn(self)
def send_data(self, data: Union[bytes, bytearray]):
self.connection_last_activity = datetime.datetime.now(datetime.UTC)
super().send_data(data)
@classmethod @classmethod
def query_accept(cls, port, call_from, call_to): def query_accept(cls, port, call_from, call_to):
return True return True
@@ -117,15 +140,8 @@ class Message:
self.data['d'] = str(payload) self.data['d'] = str(payload)
@classmethod @classmethod
def unpack(cls, msg_bytes: bytes) -> Self: def partial_unpack(cls, msg: dict) -> Self:
try: unpacked = msg
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.")
comp = Message.CompressionType(unpacked['c']) comp = Message.CompressionType(unpacked['c'])
msg_type = Message.MessageType(unpacked['t']) msg_type = Message.MessageType(unpacked['t'])
raw_data = unpacked['d'] raw_data = unpacked['d']
@@ -136,8 +152,22 @@ class Message:
data = unpackb(bz2.decompress(raw_data)) data = unpackb(bz2.decompress(raw_data))
else: else:
raise NotImplementedError(f"Compression type {comp.name} is not implemented yet.") raise NotImplementedError(f"Compression type {comp.name} is not implemented yet.")
return Message(msg_type, comp, data) 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 Request(Message):
class Method(Enum): class Method(Enum):
GET = 0 GET = 0
@@ -162,7 +192,7 @@ class Request(Message):
@property @property
def path(self): def path(self):
if 'p' in self.data: if 'p' in self.data:
return str(self.data['p']) return str(self.data['p']).lower().strip()
else: else:
return "" return ""

View File

@@ -1,8 +1,14 @@
import pe import pe.app
from ..common import PacketServerConnection from ..common import PacketServerConnection
from .constants import default_server_config
from copy import deepcopy
import ax25 import ax25
from pathlib import Path 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: class Server:
def __init__(self, pe_server: str, port: int, server_callsign: str, data_dir: str = None): 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.callsign = server_callsign
self.pe_server = pe_server self.pe_server = pe_server
self.pe_port = port self.pe_port = port
self.handlers = deepcopy(standard_handlers)
if data_dir: if data_dir:
data_path = Path(data_dir) data_path = Path(data_dir)
else: else:
@@ -28,13 +35,25 @@ class Server:
self.home_dir = data_dir self.home_dir = data_dir
self.storage = ZODB.FileStorage.FileStorage(self.data_file) self.storage = ZODB.FileStorage.FileStorage(self.data_file)
self.db = ZODB.DB(self.storage) 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 @property
def data_file(self) -> str: def data_file(self) -> str:
return str(Path(self.home_dir).joinpath('data.zopedb')) return str(Path(self.home_dir).joinpath('data.zopedb'))
def server_receiver(self, conn: PacketServerConnection): def server_receiver(self, conn: PacketServerConnection):
pass process_incoming_data(conn, self)
pass
def start(self):
self.app.start(self.pe_server, self.pe_port)
self.app.register_callsigns(self.callsign)
def stop(self):
self.app.stop()

View File

@@ -0,0 +1,5 @@
default_server_config = {
"motd": "Welcome to this PacketServer BBS!",
"operator": "placeholder",
}

View File

@@ -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.")