Этот коммит содержится в:
Alexander UR6LKW
2025-05-23 16:07:52 +03:00
родитель 70ed9938b2
Коммит 9a34e930dd
8 изменённых файлов: 134 добавлений и 110 удалений
+2 -1
Просмотреть файл
@@ -1,5 +1,6 @@
from .auth import AllowAllPeerAuth, DenyAllPeerAuth, ListPeerAuth
from .dispatcher import Dispatcher
from .dmrmaster import DMRMaster
from .dmrproto import DMRPPacketFactory
from .network import IDatagramSender, IDatagramReceiver
from .pphex import hexdump
from .auth import AllowAllPeerAuth, DenyAllPeerAuth, ListPeerAuth
+140
Просмотреть файл
@@ -0,0 +1,140 @@
import asyncio
import logging
from argparse import ArgumentParser
from api.dmrapi import start_api
from dmrtools import Dispatcher
from dmrtools.app import App
from dmrtools.asyncnetwork import AsyncDatagramServer
from dmrtools.auth import IPeerAuth
class DMRMaster:
def __init__(self) -> None:
self.interface: str = '0.0.0.0'
self.port: int = 62031
self.web_interface: str = '0.0.0.0'
self.web_port: int = 8000
self.dispatcher: Dispatcher|None = None
self.dg_server: AsyncDatagramServer|None = None
def setup(self) -> None:
ap = ArgumentParser(
prog="dmrmaster.py",
description='0DMRMaster Server by Alexander Mokrov (UR6LKW)')
ap.add_argument('-i', '--interface', type=str,
help='Interface to listen on. Defaults to 0.0.0.0.')
ap.add_argument('-p', '--port', type=int,
help='UDP port to listen on. Defaults to 62031.')
ap.add_argument('--web-interface', type=str,
help='Interface to run API on. Defaults to 0.0.0.0.')
ap.add_argument('--web-port', type=int,
help='TCP port to run API on. Defaults to 8000.')
ap.add_argument('-l', '--log-file', type=str, help='Log filename')
ap.add_argument('-d', '--ll-debug', action='store_true',
help='Log level (INFO/DEBUG)')
args = ap.parse_args()
# print(args)
if args.interface is not None:
self.interface = args.interface
if args.port is not None:
self.port = args.port
if args.web_interface is not None:
self.web_interface = args.web_interface
if args.web_port is not None:
self.web_port = args.web_port
log_level = logging.DEBUG if args.ll_debug else logging.INFO
self.setup_log(log_level, args.log_file)
def setup_log(self, log_level = logging.DEBUG, log_file = None) -> None:
handlers = [logging.StreamHandler()]
if log_file:
file_handler = logging.FileHandler(log_file)
handlers.append(file_handler) # type: ignore[arg-type]
logging.basicConfig(
level=log_level,
format='%(asctime)s [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
handlers=handlers
)
def set_peer_auth(self, peer_auth: IPeerAuth) -> None:
if self.dispatcher is None:
logging.error("Can't set auth: no dispatcher")
return
logging.debug(f"Peer auth set to {peer_auth}")
self.dispatcher.peer_auth = peer_auth
def register_app(self, app: App) -> None:
if self.dispatcher is None:
logging.error("Can't register app: no dispatcher")
return
self.dispatcher.app_keeper.register(app)
def config(self) -> None:
"""
Local config
"""
pass
def run(self) -> None:
"""
Run, go async and handle KeyboardInterupt
"""
try:
asyncio.run(self.__async_run())
except KeyboardInterrupt:
logging.info("Interrupted by user (Ctrl+C). Exiting gracefully.")
async def __async_run(self) -> None:
await self.__start_udp()
if not isinstance(self.dg_server, AsyncDatagramServer):
logging.critical(f"Can't listen on {self.interface}:{self.port}")
return
self.dispatcher = Dispatcher(self.dg_server)
self.config() # local config for apps
logging.info(f"Starting API on {self.web_interface}:{self.web_port}")
try:
await start_api(self.web_interface, self.web_port, self.dispatcher)
# await asyncio.Future() # Run forever
finally:
self.stop()
async def __start_udp(self) -> None:
logging.info(
f"Starting server listening on {self.interface}:{self.port}")
loop = asyncio.get_running_loop()
transport, self.dg_server = await loop.create_datagram_endpoint(
lambda: AsyncDatagramServer(),
local_addr=(self.interface, self.port))
def stop(self) -> None:
if self.dispatcher is not None:
self.dispatcher.shutdown()
self.dispatcher = None
if self.dg_server is not None:
self.dg_server.close()
self.dg_server = None
def main():
master = DMRMaster();
master.setup()
master.run()
if __name__ == '__main__':
main()
+2
Просмотреть файл
@@ -0,0 +1,2 @@
from .mmdvm_l1 import *
from .exceptions import *
+70
Просмотреть файл
@@ -0,0 +1,70 @@
from abc import ABC, abstractmethod
from typing import Any
from .exceptions import DMRPFieldOutOfRangeException
class DMRPFieldBase(ABC):
def __init__(self, name: str, offset: int, bytelen: int) -> None:
self.name, self.offset, self.bytelen = name, offset, bytelen
self.eoffset = offset + bytelen
self.typename = f"bytes{self.bytelen}"
def get(self, obj) -> bytes:
return bytes(obj._data[self.offset:self.eoffset])
def set(self, obj, value: bytes):
if not isinstance(value, bytes) or self.bytelen != len(value):
raise DMRPFieldOutOfRangeException(self.name, self.typename)
obj._data[self.offset:self.eoffset] = value
@abstractmethod
def __get__(self, obj, cls = None) -> Any:
pass
@abstractmethod
def __set__(self, obj, value: Any) -> None:
pass
class DMRPFieldBytes(DMRPFieldBase):
def __get__(self, obj, cls = None) -> bytes:
return self.get(obj)
def __set__(self, obj, value: bytes) -> None:
self.set(obj, value)
class DMRPFieldStr(DMRPFieldBase):
def __init__(self, name: str, offset: int, bytelen: int,
pad_with: bytes = b'\x20') -> None:
super().__init__(name, offset, bytelen)
self.typename = 'str'
self.pad_with = pad_with
def __get__(self, obj, cls = None) -> str:
return (self.get(obj).strip(b'\x20\x00')
.decode(encoding='ascii', errors='ignore'))
def __set__(self, obj, value: str) -> None:
bvalue = value.encode()
if len(bvalue) > self.bytelen:
bvalue = bvalue[:self.bytelen]
bvalue = bvalue.ljust(self.bytelen, self.pad_with)
self.set(obj, bvalue)
class DMRPFieldInt(DMRPFieldBase):
def __init__(self, name: str, offset: int, bytelen: int) -> None:
super().__init__(name, offset, bytelen)
self.typename = f"uint{str(bytelen * 8)}"
def __get__(self, obj, cls = None) -> int:
return int.from_bytes(self.get(obj), byteorder="big")
def __set__(self, obj, value: int) -> None:
if not (0 <= value < 1<<(self.bytelen * 8)):
raise DMRPFieldOutOfRangeException(self.name, self.typename)
self.set(obj, value.to_bytes(self.bytelen, byteorder="big"))
+25
Просмотреть файл
@@ -0,0 +1,25 @@
class DMRPFieldOutOfRangeException(Exception):
"""
Exception raised when a packet field value is out of the allowed or expected range.
Attributes:
field: The name or identifier of the problematic field.
typename: The expected type or range description for the field.
"""
def __init__(self, field, typename) -> None:
super().__init__(f"Field out of range: {field} must be {typename}")
class DMRPUnknownPacketTypeException(Exception):
"""
Exception raised when the packet factory cannot recognize the packet type
from the given input data.
"""
pass
class DMRPBadPacket(Exception):
"""
Exception raised when a packet is structurally invalid or corrupted.
"""
pass
+25 -106
Просмотреть файл
@@ -5,37 +5,12 @@ import random
from abc import ABC
from hashlib import sha256
from typing import Type, Self, Any
from typing import Type, Self
#############################
# Local exception classes
#############################
class DMRPFieldOutOfRangeException(Exception):
"""
Exception raised when a packet field value is out of the allowed or expected range.
Attributes:
field: The name or identifier of the problematic field.
typename: The expected type or range description for the field.
"""
def __init__(self, field, typename) -> None:
super().__init__(f"Field out of range: {field} must be {typename}")
class DMRPUnknownPacketTypeException(Exception):
"""
Exception raised when the packet factory cannot recognize the packet type
from the given input data.
"""
pass
class DMRPBadPacket(Exception):
"""
Exception raised when a packet is structurally invalid or corrupted.
"""
pass
from .base_fields import DMRPFieldInt, DMRPFieldBytes, DMRPFieldStr
from .exceptions import DMRPBadPacket
from .exceptions import DMRPFieldOutOfRangeException
from .exceptions import DMRPUnknownPacketTypeException
#############################
@@ -45,81 +20,6 @@ def calc_password_hash(salt: bytes, password: str) -> bytes:
return sha256(salt + password.encode()).digest()
#############################
# Field classes
#############################
class DMRPFieldBase(ABC):
def __init__(self, name: str, offset: int, bytelen: int) -> None:
self.name, self.offset, self.bytelen = name, offset, bytelen
self.eoffset = offset + bytelen
self.typename = f"bytes{self.bytelen}"
def __get__(self, obj, cls = None) -> Any:
return bytes(obj._data[self.offset:self.eoffset])
def __set__(self, obj, value: Any) -> None:
if not isinstance(value, bytes) or self.bytelen != len(value):
raise DMRPFieldOutOfRangeException(self.name, self.typename)
obj._data[self.offset:self.eoffset] = value
class DMRPFieldBytes(DMRPFieldBase):
def __get__(self, obj, cls = None) -> bytes:
return super().__get__(obj, cls)
def __set__(self, obj, value: bytes) -> None:
super().__set__(obj, value)
class DMRPFieldStr(DMRPFieldBase):
def __init__(self, name: str, offset: int, bytelen: int,
pad_with: bytes = b'\x20') -> None:
super().__init__(name, offset, bytelen)
self.typename = 'str'
self.pad_with = pad_with
def __get__(self, obj, cls = None) -> str:
return (super().__get__(obj, cls).strip(b'\x20\x00')
.decode(encoding='ascii', errors='ignore'))
def __set__(self, obj, value: str) -> None:
bvalue = value.encode()
if len(bvalue) > self.bytelen:
bvalue = bvalue[:self.bytelen]
bvalue = bvalue.ljust(self.bytelen, self.pad_with)
super().__set__(obj, bvalue)
class DMRPFieldInt(DMRPFieldBase):
def __init__(self, name: str, offset: int, bytelen: int) -> None:
super().__init__(name, offset, bytelen)
self.typename = f"uint{str(bytelen * 8)}"
def __get__(self, obj, cls = None) -> int:
return int.from_bytes(super().__get__(obj, cls), byteorder="big")
def __set__(self, obj, value: int) -> None:
if not (0 <= value < 1<<(self.bytelen * 8)):
raise DMRPFieldOutOfRangeException(self.name, self.typename)
super().__set__(obj, value.to_bytes(self.bytelen, byteorder="big"))
class DMRPFieldPeerAuto(DMRPFieldInt):
def fixoffset(self, offset: int) -> None:
self.offset, self.eoffset = offset, offset + self.bytelen
def __init__(self, name: str) -> None:
super().__init__(name, 4, 4)
def __get__(self, obj, cls = None) -> int:
self.fixoffset(len(obj.PKT_TYPE))
return super().__get__(obj, cls)
def __set__(self, obj, value: int) -> None:
self.fixoffset(len(obj.PKT_TYPE))
super().__set__(obj, value)
#############################
# Packet classes hierarchy
#############################
@@ -206,6 +106,25 @@ class DMRPBasePeerPacket(DMRPBasePacket, ABC):
Abstract base class (more specific), which handles typical packets with
type header and peer_id field
"""
class DMRPFieldPeerAuto(DMRPFieldInt):
"""
Int field, which autodetects header length (PKT_TYPE) and
offsets peer_id for its len
"""
def fixoffset(self, offset: int) -> None:
self.offset, self.eoffset = offset, offset + self.bytelen
def __init__(self, name: str) -> None:
super().__init__(name, 4, 4)
def __get__(self, obj, cls = None) -> int:
self.fixoffset(len(obj.PKT_TYPE))
return super().__get__(obj, cls)
def __set__(self, obj, value: int) -> None:
self.fixoffset(len(obj.PKT_TYPE))
super().__set__(obj, value)
# peer_id
peer_id: DMRPFieldInt = DMRPFieldPeerAuto('peer_id')
@@ -494,7 +413,7 @@ class DMRPPacketFactory:
and user-registered custom packet types.
"""
__instance: Self|None = None
__instance: DMRPPacketFactory|None = None
@classmethod
def fd(cls, data: bytes) -> DMRPBasePacket: