Added ZEO to requirements. Server by default maintains a ZEO storage process in addition to the server process to allow live editing of configuration and DB from other processes.

This commit is contained in:
Michael Woods
2025-01-05 14:20:23 -05:00
parent 7a38f4aa6b
commit f57cdbadd8
6 changed files with 132 additions and 14 deletions

View File

@@ -4,3 +4,4 @@ pyham_ax25
ZODB
BTrees
transaction
ZEO

View File

@@ -0,0 +1,9 @@
import re
def email_valid(email: str) -> bool:
"""Taken from https://www.geeksforgeeks.org/check-if-email-address-valid-or-not-in-python/"""
regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b'
if re.fullmatch(regex, email):
return True
else:
return False

View File

@@ -15,6 +15,7 @@ import signal
import time
from msgpack.exceptions import OutOfData
from typing import Callable, Self, Union
from traceback import format_exc
def init_bulletins(root: PersistentMapping):
@@ -24,13 +25,16 @@ def init_bulletins(root: PersistentMapping):
root['bulletin_counter'] = 0
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, zeo: bool = True):
if not ax25.Address.valid_call(server_callsign):
raise ValueError(f"Provided callsign '{server_callsign}' is invalid.")
self.callsign = server_callsign
self.pe_server = pe_server
self.pe_port = port
self.handlers = deepcopy(standard_handlers)
self.zeo_addr = None
self.zeo_stop = None
self.zeo = zeo
if data_dir:
data_path = Path(data_dir)
else:
@@ -68,6 +72,8 @@ class Server:
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)
self.db.close()
self.storage.close()
@property
@@ -145,12 +151,25 @@ class Server:
def server_receiver(self, conn: PacketServerConnection):
logging.debug("running server receiver")
try:
self.process_incoming_data(conn)
except Exception:
logging.debug(f"Unhandled exception while processing incoming data:\n{format_exc()}")
def register_path_handler(self, path_root: str, fn: Callable):
self.handlers[path_root.strip().lower()] = fn
def start(self):
if not self.zeo:
self.storage = ZODB.FileStorage.FileStorage(self.data_file)
self.db = ZODB.DB(self.storage)
else:
import ZEO
address, stop = ZEO.server(path=self.data_file)
self.zeo_addr = address
self.zeo_stop = stop
self.db = ZEO.DB(self.zeo_addr)
logging.info(f"Starting ZEO server with address {self.zeo_addr}")
self.app.start(self.pe_server, self.pe_port)
self.app.register_callsigns(self.callsign)
@@ -164,3 +183,6 @@ class Server:
self.app.stop()
self.storage.close()
self.db.close()
if self.zeo:
logging.info("Stopping ZEO.")
self.zeo_stop()

View File

@@ -7,6 +7,7 @@ from typing import Self,Union,Optional
from packetserver.common import PacketServerConnection, Request, Response, Message, send_response, send_blank_response
import ZODB
import logging
from packetserver.server.users import user_authorized
def get_new_bulletin_id(root: PersistentMapping) -> int:
if 'bulletin_counter' not in root:
@@ -144,6 +145,11 @@ def handle_bulletin_delete(req: Request, conn: PacketServerConnection, db: ZODB.
def bulletin_root_handler(req: Request, conn: PacketServerConnection, db: ZODB.DB):
logging.debug(f"{req} being processed by bulletin_root_handler")
if not user_authorized(conn, db):
logging.debug(f"user {conn.remote_callsign} not authorized")
send_blank_response(conn, req, status_code=401)
return
logging.debug("user is authorized")
if req.method is Request.Method.GET:
handle_bulletin_get(req, conn, db)
elif req.method is Request.Method.POST:

View File

@@ -3,7 +3,7 @@
from msgpack.exceptions import OutOfData
from packetserver.common import Message, Request, Response, PacketServerConnection, send_response, send_blank_response
from .bulletin import bulletin_root_handler
from .users import user_root_handler
from .users import user_root_handler, user_authorized
import logging
from typing import Union
import ZODB
@@ -21,9 +21,15 @@ def handle_root_get(req: Request, conn: PacketServerConnection,
if 'operator' in storage.root.config:
operator = storage.root.config['operator']
if user_authorized(conn, db):
user_message = f"User {conn.remote_callsign} is enabled."
else:
user_message = f"User {conn.remote_callsign} is not enabled."
response.payload = {
'operator': operator,
'motd': motd
'motd': motd,
'user': user_message
}
send_response(conn, response, req)

View File

@@ -11,23 +11,28 @@ import ZODB
import logging
import uuid
from uuid import UUID
from packetserver.common.util import email_valid
class User(persistent.Persistent):
def __init__(self, username: str, enabled: bool = True, hidden: bool = False, bio: str = "", status: str = ""):
def __init__(self, username: str, enabled: bool = True, hidden: bool = False, bio: str = "", status: str = "",
email: str = None, location: str = "", socials: list[str] = None):
self._username = username.upper().strip()
self.enabled = enabled
self.hidden = hidden
self.created_at = datetime.datetime.now(datetime.UTC)
self.last_seen = self.created_at
self._email = ""
if email:
self.email = email
self._location = ""
self.location = location
self._socials = []
if socials:
self.socials = socials
self._uuid = None
if len(bio) > 4000:
self._bio = bio[:4000]
else:
self._bio = bio
if len(status) > 300:
self._status = status[:300]
else:
self._status = status
self.bio = bio
self._status = ""
self.status = status
def write_new(self, db_root: PersistentMapping):
all_uuids = [db_root['users'][x].uuid for x in db_root['users']]
@@ -38,6 +43,47 @@ class User(persistent.Persistent):
if self.username not in db_root['users']:
db_root['users'][self.username] = self
@property
def location(self) -> str:
return self._location
@location.setter
def location(self, location: str):
if len(location) > 1000:
self._location = location[:1000]
else:
self._location = location
@property
def email(self) -> str:
return self._email
@email.setter
def email(self, email: str):
if email_valid(email.strip().lower()):
self._email = email.strip().lower()
else:
raise ValueError(f"Invalid e-mail given: {email}")
@property
def socials(self) -> list[str]:
return []
@socials.setter
def socials(self, socials: list[str]):
for social in socials:
if len(social) > 300:
social = social[:300]
self._socials.append(social)
def add_social(self, social: str):
if len(social) > 300:
social = social[:300]
self._socials.append(social)
def remove_social(self, social: str):
self.socials.remove(social)
@property
def uuid(self):
return self._uuid
@@ -80,6 +126,14 @@ class User(persistent.Persistent):
else:
return all_users[:limit]
@classmethod
def is_authorized(cls, username: str, db_root: PersistentMapping) -> bool:
user = User.get_user_by_username(username, db_root)
if user:
if user.enabled:
return True
return False
def seen(self):
self.last_seen = datetime.datetime.now(datetime.UTC)
@@ -114,10 +168,25 @@ class User(persistent.Persistent):
"username": self.username,
"status": self.status,
"bio": self.bio,
"socials": self.socials,
"email": self.email,
"location": self.location,
"last_seen": self.last_seen.isoformat(),
"created_at": self.created_at.isoformat()
}
def __repr__(self):
return f"<User: {self.username} - {self.uuid}>"
def user_authorized(conn: PacketServerConnection, db: ZODB.DB) -> bool:
username = ax25.Address(conn.remote_callsign).call
logging.debug(f"Running authcheck for user {username}")
result = False
with db.transaction() as db:
result = User.is_authorized(username, db.root())
logging.debug(f"User is authorized? {result}")
return result
def handle_user_get(req: Request, conn: PacketServerConnection, db: ZODB.DB):
sp = req.path.split("/")
logging.debug("handle_user_get working")
@@ -161,6 +230,11 @@ def handle_user_update(req: Request, conn: PacketServerConnection, db: ZODB.DB):
def user_root_handler(req: Request, conn: PacketServerConnection, db: ZODB.DB):
logging.debug(f"{req} being processed by user_root_handler")
if not user_authorized(conn, db):
logging.debug(f"user {conn.remote_callsign} not authorized")
send_blank_response(conn, req, status_code=401)
return
logging.debug("user is authorized")
if req.method is Request.Method.GET:
handle_user_get(req, conn, db)
else: