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

@@ -1,4 +1,6 @@
pyham_pe
msgpack
pyham_ax25
ZODB
ZODB
BTrees
transaction

View File

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

View File

@@ -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
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()

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