Added code for a server to maybe run and respond to requests based on handlers registered with the server.
This commit is contained in:
@@ -2,3 +2,5 @@ pyham_pe
|
|||||||
msgpack
|
msgpack
|
||||||
pyham_ax25
|
pyham_ax25
|
||||||
ZODB
|
ZODB
|
||||||
|
BTrees
|
||||||
|
transaction
|
||||||
@@ -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 ""
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
5
src/packetserver/server/constants.py
Normal file
5
src/packetserver/server/constants.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
default_server_config = {
|
||||||
|
"motd": "Welcome to this PacketServer BBS!",
|
||||||
|
"operator": "placeholder",
|
||||||
|
}
|
||||||
66
src/packetserver/server/requests.py
Normal file
66
src/packetserver/server/requests.py
Normal 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.")
|
||||||
Reference in New Issue
Block a user