diff --git a/src/packetserver/common/__init__.py b/src/packetserver/common/__init__.py index 89cdd0a..12e8e69 100644 --- a/src/packetserver/common/__init__.py +++ b/src/packetserver/common/__init__.py @@ -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: @@ -281,4 +282,35 @@ class Response(Message): self.data['c'] = code.to_bytes(2) def __repr__(self): - return f"" \ No newline at end of file + return f"" + +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) \ No newline at end of file diff --git a/src/packetserver/server/__init__.py b/src/packetserver/server/__init__.py index a299e57..a938762 100644 --- a/src/packetserver/server/__init__.py +++ b/src/packetserver/server/__init__.py @@ -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 diff --git a/src/packetserver/server/bulletin.py b/src/packetserver/server/bulletin.py index 0a256c2..f7b8bad 100644 --- a/src/packetserver/server/bulletin.py +++ b/src/packetserver/server/bulletin.py @@ -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) diff --git a/src/packetserver/server/requests.py b/src/packetserver/server/requests.py index b632e29..623f1e4 100644 --- a/src/packetserver/server/requests.py +++ b/src/packetserver/server/requests.py @@ -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") \ No newline at end of file