REFACT dmrproto
Этот коммит содержится в:
@@ -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
|
||||
|
||||
Executable
+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()
|
||||
Executable
@@ -0,0 +1,2 @@
|
||||
from .mmdvm_l1 import *
|
||||
from .exceptions import *
|
||||
Executable
+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"))
|
||||
|
||||
|
||||
Executable
+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
|
||||
@@ -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:
|
||||
Ссылка в новой задаче
Block a user