Refactored to remove some circular dependencies, fixed bug in packing method. Added a basic compression decision.

This commit is contained in:
Michael Woods
2025-01-04 18:52:30 -05:00
parent 8feadeef42
commit 6e6be5ebb0
4 changed files with 167 additions and 109 deletions

View File

@@ -113,9 +113,10 @@ class Message:
def pack(self) -> bytes:
output = {'t': self.type.value, 'c': self.compression.value}
data_bytes = self.data_bytes
logging.debug("Packing Message")
if (self.compression is self.CompressionType.NONE) or (len(data_bytes) < 30):
output['d'] = data_bytes
output['c'] = self.CompressionType.NONE.value
return packb(output)
if self.compression is self.CompressionType.BZIP2:
@@ -282,3 +283,34 @@ class Response(Message):
def __repr__(self):
return f"<Response: {self.status_code}>"
def send_response(conn: PacketServerConnection, response: Response, original_request: Request,
compression: Message.CompressionType = Message.CompressionType.BZIP2):
if conn.state.name == "CONNECTED" and not conn.closing:
# figure out compression setting based on request
comp = compression
if 'C' in original_request.vars:
val = original_request.vars['C']
for i in Message.CompressionType:
if str(val).strip().upper() == i.name:
comp = i
break
try:
if int(val) == i.value:
comp = i
except ValueError:
pass
response.compression = comp
logging.debug(f"sending response: {response}, {response.compression}, {response.payload}")
conn.send_data(response.pack())
logging.debug("response sent successfully")
def send_blank_response(conn: PacketServerConnection, original_request: Request, status_code: int = 200,
payload: Union[bytes, bytearray, str, dict] = ""):
response = Response.blank()
response.status_code = status_code
response.payload = payload
send_response(conn, response, original_request)

View File

@@ -1,7 +1,6 @@
import pe.app
import packetserver.common
from packetserver.common import Response, Message, Request, PacketServerConnection, send_response, send_blank_response
from packetserver.server.constants import default_server_config
from packetserver.server.bulletin import init_bulletins
from copy import deepcopy
import ax25
from pathlib import Path
@@ -9,14 +8,20 @@ import ZODB, ZODB.FileStorage
from BTrees.OOBTree import OOBTree
from persistent.mapping import PersistentMapping
from persistent.list import PersistentList
from packetserver.server.requests import process_incoming_data
from packetserver.server.requests import standard_handlers
import logging
import signal
import time
from typing import Callable
from msgpack.exceptions import OutOfData
from typing import Callable, Self, Union
def init_bulletins(root: PersistentMapping):
if 'bulletins' not in root:
root['bulletins'] = PersistentList()
if 'bulletin_counter' not in root:
root['bulletin_counter'] = 0
class Server:
def __init__(self, pe_server: str, port: int, server_callsign: str, data_dir: str = None):
if not ax25.Address.valid_call(server_callsign):
@@ -50,8 +55,8 @@ class Server:
conn.root.users = OOBTree()
init_bulletins(conn.root())
self.app = pe.app.Application()
packetserver.common.PacketServerConnection.receive_subscribers.append(lambda x: self.server_receiver(x))
packetserver.common.PacketServerConnection.connection_subscribers.append(lambda x: self.server_connection_bouncer(x))
PacketServerConnection.receive_subscribers.append(lambda x: self.server_receiver(x))
PacketServerConnection.connection_subscribers.append(lambda x: self.server_connection_bouncer(x))
signal.signal(signal.SIGINT, self.exit_gracefully)
signal.signal(signal.SIGTERM, self.exit_gracefully)
@@ -60,7 +65,7 @@ class Server:
def data_file(self) -> str:
return str(Path(self.home_dir).joinpath('data.zopedb'))
def server_connection_bouncer(self, conn: packetserver.common.PacketServerConnection):
def server_connection_bouncer(self, conn: PacketServerConnection):
logging.debug("new connection bouncer checking for blacklist")
# blacklist check
blacklisted = False
@@ -82,9 +87,46 @@ class Server:
break
conn.close()
def server_receiver(self, conn: packetserver.common.PacketServerConnection):
def handle_request(self, req: Request, conn: PacketServerConnection):
"""Handles a proper request by handing off to the appropriate function depending on method and Path."""
logging.debug(f"asked to handle request: {req}")
if conn.closing:
logging.debug("Connection marked as closing. Ignoring it.")
return
req_root_path = req.path.split("/")[0]
if req_root_path in self.handlers:
logging.debug(f"found handler for req {req}")
self.handlers[req_root_path](req, conn, self.db)
return
logging.warning(f"unhandled request found: {req}")
send_blank_response(conn, req, status_code=404)
def process_incoming_data(self, connection: PacketServerConnection):
"""Handles incoming data."""
logging.debug("Running process_incoming_data on connection")
with connection.data_lock:
logging.debug("Data lock acquired")
while True:
try:
msg = Message.partial_unpack(connection.data.unpack())
logging.debug(f"parsed a Message from data received")
except OutOfData:
logging.debug("no complete message yet, done until more data arrives")
break
except ValueError:
connection.send_data(b"BAD REQUEST. COULD NOT PARSE INCOMING DATA AS PACKETSERVER MESSAGE")
try:
request = Request(msg)
logging.debug(f"parsed Message into request {request}")
except ValueError:
connection.send_data(b"BAD REQUEST. DID NOT RECEIVE A REQUEST MESSAGE.")
logging.debug(f"attempting to handle request {request}")
self.handle_request(request, connection)
logging.debug("request handled")
def server_receiver(self, conn: PacketServerConnection):
logging.debug("running server receiver")
process_incoming_data(conn, self)
self.process_incoming_data(conn)
def register_path_handler(self, path_root: str, fn: Callable):
self.handlers[path_root.strip().lower()] = fn

View File

@@ -1,20 +1,14 @@
import ax25
import persistent
import persistent.list
from persistent.mapping import PersistentMapping
import datetime
from typing import Self,Union,Optional
from packetserver.common import PacketServerConnection, Request, Response, Message
from packetserver.server import Server
from packetserver.server.requests import send_404
from packetserver.server.requests import send_response, send_blank_response
import ZODB
import logging
def init_bulletins(root: PersistentMapping):
if 'bulletins' not in root:
root['bulletins'] = persistent.list.PersistentList()
if 'bulletin_counter' not in root:
root['bulletin_counter'] = 0
def get_new_bulletin_id(root: PersistentMapping) -> int:
if 'bulletin_counter' not in root:
root['bulletin_counter'] = 1
@@ -32,6 +26,17 @@ class Bulletin(persistent.Persistent):
return bull
return None
@classmethod
def get_recent_bulletins(cls, db_root: PersistentMapping, limit: int = None) -> list:
all_bulletins = sorted(db_root['bulletins'], key=lambda bulletin: bulletin.updated_at, reverse=True)
if not limit:
return all_bulletins
else:
if len(all_bulletins) < limit:
return all_bulletins
else:
return all_bulletins[:limit]
def __init__(self, author: str, subject: str, text: str):
self.author = author
self.subject = subject
@@ -69,33 +74,74 @@ class Bulletin(persistent.Persistent):
"updated_at": self.updated_at.isoformat()
}
def handle_bulletin_get(req: Request, conn: PacketServerConnection, server: Server):
response = Response.blank()
with server.db.transaction() as db:
pass
return response
def handle_bulletin_post(req: Request, conn: PacketServerConnection, server: Server):
def handle_bulletin_get(req: Request, conn: PacketServerConnection, db: ZODB.DB):
response = Response.blank()
with server.db.transaction() as db:
pass
return response
sp = req.path.split("/")
bid = None
limit = None
if 'limit' in req.vars:
try:
limit = int(req.vars['limit'])
except ValueError:
pass
if 'id' in req.vars:
try:
bid = int(req.vars['id'])
except ValueError:
pass
if len(sp) > 2:
try:
bid = int(sp[2].strip())
except ValueError:
pass
def handle_bulletin_update(req: Request, conn: PacketServerConnection, server: Server):
with db.transaction() as db:
if bid:
bull = Bulletin.get_bulletin_by_id(bid, db.root())
if bull:
response.payload = bull.to_dict()
response.status_code = 200
else:
response.status_code = 404
else:
bulls = Bulletin.get_recent_bulletins(db.root(), limit=limit)
response.payload = [bulletin.to_dict() for bulletin in bulls]
response.status_code = 200
send_response(conn, response, req)
def handle_bulletin_post(req: Request, conn: PacketServerConnection, db: ZODB.DB):
author = ax25.Address(conn.remote_callsign).call
if type(req.payload) is not dict:
send_blank_response(conn, req, 400, payload="Include dict in payload with subject and body")
if 'subject' not in req.payload:
send_blank_response(conn, req, 400, payload="Include dict in payload with subject and body")
if 'body' not in req.payload:
send_blank_response(conn, req, 400, payload="Include dict in payload with subject and body")
b = Bulletin(author, str(req.payload['subject']), str(req.payload['body']))
response = Response.blank()
with server.db.transaction() as db:
pass
return response
with db.transaction() as db:
b.write_new(db.root())
send_blank_response(conn, req, status_code=201)
def handle_bulletin_delete(req: Request, conn: PacketServerConnection, server: Server):
def handle_bulletin_update(req: Request, conn: PacketServerConnection, db: ZODB.DB):
response = Response.blank()
with server.db.transaction() as db:
with db.transaction() as db:
pass
return response
send_response(conn, response, req)
def bulletin_root_handler(req: Request, conn: PacketServerConnection, server: Server):
def handle_bulletin_delete(req: Request, conn: PacketServerConnection, db: ZODB.DB):
response = Response.blank()
with db.transaction() as db:
pass
send_response(conn, response, req)
def bulletin_root_handler(req: Request, conn: PacketServerConnection, db: ZODB.DB):
logging.debug(f"{req} being processed by bulletin_root_handler")
if req.method is Request.Method.GET:
handle_bulletin_get(req, conn, server)
handle_bulletin_get(req, conn, db)
elif req.method is Request.Method.POST:
handle_bulletin_post(req, conn, db)
else:
send_404(conn)
send_blank_response(conn, req, status_code=404)

View File

@@ -1,26 +1,20 @@
"""Module for handling requests as they arrive to connection objects and servers."""
from msgpack.exceptions import OutOfData
from packetserver.common import Message, Request, Response, PacketServerConnection
from packetserver.common import Message, Request, Response, PacketServerConnection, send_response, send_blank_response
from .bulletin import bulletin_root_handler
import logging
from typing import Union
def send_404(conn: PacketServerConnection, payload: Union[bytes, bytearray, str, dict] = ""):
response_404 = Response.blank()
response_404.status_code = 404
response_404.payload = payload
if conn.state.name == "CONNECTED":
conn.send_data(response_404.pack())
import ZODB
def handle_root_get(req: Request, conn: PacketServerConnection,
server: 'packetserver.server.Server'):
logging.debug(f"Received request: {req}")
db: ZODB.DB):
logging.debug(f"Root get handler received request: {req}")
response = Response.blank()
response.compression = Message.CompressionType.BZIP2
operator = ""
motd = ""
with server.db.transaction() as storage:
with db.transaction() as storage:
if 'motd' in storage.root.config:
motd = storage.root.config['motd']
if 'operator' in storage.root.config:
@@ -31,76 +25,20 @@ def handle_root_get(req: Request, conn: PacketServerConnection,
'motd': motd
}
if conn.state.name == "CONNECTED" and not conn.closing:
logging.debug(f"sending response: {response}, {response.compression}, {response.payload}")
conn.send_data(response.pack())
logging.debug("response sent successfully")
send_response(conn, response, req)
def root_root_handler(req: Request, conn: PacketServerConnection,
server: 'packetserver.server.Server'):
db: ZODB.DB):
logging.debug(f"{req} got to root_root_handler")
if req.method is Request.Method.GET:
handle_root_get(req, conn, server)
handle_root_get(req, conn, db)
else:
logging.warning(f"unhandled request found: {req}")
response_404 = Response.blank()
response_404.status_code = 404
if (conn.state.name == "CONNECTED") and not conn.closing:
conn.send_data(response_404.pack())
logging.debug(f"Sent 404 in response to {req}")
send_blank_response(conn, req, status_code=404)
standard_handlers = {
"": root_root_handler,
"bulletin": bulletin_root_handler
}
def handle_request(req: Request, conn: PacketServerConnection,
server: 'packetserver.server.Server'):
"""Handles a proper request by handing off to the appropriate function depending on method and Path."""
logging.debug(f"asked to handle request: {req}")
if conn.closing:
logging.debug("Connection marked as closing. Ignoring it.")
return
req_root_path = req.path.split("/")[0]
if req_root_path in server.handlers:
logging.debug(f"found handler for req {req}")
server.handlers[req_root_path](req, conn, server)
return
logging.warning(f"unhandled request found: {req}")
response_404 = Response.blank()
response_404.status_code = 404
if conn.state.name == "CONNECTED":
conn.send_data(response_404.pack())
logging.debug(f"Sent 404 in response to {req}")
def process_incoming_data(connection: 'packetserver.common.PacketServerConnection',
server: 'packetserver.server.Server'):
"""Handles incoming data."""
logging.debug("Running process_incoming_data on connection")
with connection.data_lock:
logging.debug("Data lock acquired")
while True:
try:
msg = Message.partial_unpack(connection.data.unpack())
logging.debug(f"parsed a Message from data received")
except OutOfData:
logging.debug("no complete message yet, done until more data arrives")
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)
logging.debug(f"parsed Message into request {request}")
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.")
logging.debug(f"attempting to handle request {request}")
handle_request(request, connection, server)
logging.debug("request handled")