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:
@@ -4,3 +4,4 @@ pyham_ax25
|
||||
ZODB
|
||||
BTrees
|
||||
transaction
|
||||
ZEO
|
||||
9
src/packetserver/common/util.py
Normal file
9
src/packetserver/common/util.py
Normal 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
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user