0DMRMaster version 0.4
Этот коммит содержится в:
@@ -1,2 +1,5 @@
|
||||
from .dispatcher import Dispatcher
|
||||
from .dmrproto import DMRPPacketFactory
|
||||
from .network import IDatagramSender, IDatagramReceiver
|
||||
from .pphex import hexdump
|
||||
from .auth import AllowAllPeerAuth, DenyAllPeerAuth, ListPeerAuth
|
||||
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from .call import Call
|
||||
from .dmrproto import DMRPPacketData
|
||||
|
||||
|
||||
class AppException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class IAppDispatcher(ABC):
|
||||
"""
|
||||
Dispatcher interface for app
|
||||
"""
|
||||
@abstractmethod
|
||||
def inject_packet(self, p: DMRPPacketData) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class IAppCallInterceptor(ABC):
|
||||
@abstractmethod
|
||||
def process_call_packet(self, call: Call, p: DMRPPacketData) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class App(ABC):
|
||||
def __init__(self) -> None:
|
||||
self.dispatcher: IAppDispatcher|None = None
|
||||
|
||||
@abstractmethod
|
||||
def get_name(self) -> str:
|
||||
pass
|
||||
|
||||
|
||||
class AppKeeper:
|
||||
def __init__(self, dispatcher: IAppDispatcher):
|
||||
self.dispatcher: IAppDispatcher = dispatcher
|
||||
self.apps: list[App] = []
|
||||
|
||||
def register(self, app: App) -> bool:
|
||||
logging.info(f"Registered app '{app.get_name()}' ")
|
||||
app.dispatcher = self.dispatcher
|
||||
self.apps.append(app)
|
||||
return True
|
||||
|
||||
def process_call_packet(self, call: Call, p: DMRPPacketData) -> None:
|
||||
for app in self.apps:
|
||||
if isinstance(app, IAppCallInterceptor):
|
||||
app.process_call_packet(call, p)
|
||||
|
||||
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from typing import Any
|
||||
|
||||
from .network import IDatagramReceiver, IDatagramSender
|
||||
|
||||
|
||||
class AsyncDatagramServer(asyncio.DatagramProtocol, IDatagramSender):
|
||||
def __init__(self) -> None:
|
||||
self.receiver: IDatagramReceiver|None = None
|
||||
self.transport: Any|None = None
|
||||
|
||||
def connection_made(self, transport: Any) -> None:
|
||||
# bug in linux implementation - _SelectorDatagramTransport is
|
||||
# not inherited from DatagramTransport. That's why hasattr is used
|
||||
if (hasattr(transport, 'sendto') or
|
||||
isinstance(transport, asyncio.DatagramTransport)):
|
||||
self.transport = transport
|
||||
else:
|
||||
logging.error("AsyncDatagramServer.connection_made(): "
|
||||
"Unexpected transport received")
|
||||
|
||||
def set_receiver(self, receiver: IDatagramReceiver):
|
||||
self.receiver = receiver
|
||||
|
||||
def datagram_received(self, data: bytes, addr: tuple) -> None:
|
||||
if self.receiver is not None:
|
||||
self.receiver.recv_dg(data, addr)
|
||||
|
||||
def send_dg(self, data: bytes, addr: tuple) -> None:
|
||||
if self.transport is not None:
|
||||
self.transport.sendto(data, addr)
|
||||
|
||||
# def error_received(self, exc: Exception|None) -> None:
|
||||
# logging.error(f"Client protocol error: {exc}")
|
||||
|
||||
def connection_lost(self, exc: Exception|None) -> None:
|
||||
logging.info(f"Client connection closed: {exc}")
|
||||
self.transport = None
|
||||
|
||||
def close(self) -> None:
|
||||
if self.transport:
|
||||
self.transport.close()
|
||||
|
||||
Executable
+69
@@ -0,0 +1,69 @@
|
||||
import logging
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from .dmrproto import calc_password_hash
|
||||
|
||||
|
||||
class IPeerAuth(ABC):
|
||||
@abstractmethod
|
||||
def allow_peer_id(self, peer_id: int) -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def check_password(self, peer_id: int, salt: bytes,
|
||||
pass_hash: bytes) -> bool:
|
||||
pass
|
||||
|
||||
|
||||
class AllowAllPeerAuth(IPeerAuth):
|
||||
"""
|
||||
Auth agent to allow any peer id and any password
|
||||
"""
|
||||
def allow_peer_id(self, peer_id: int) -> bool:
|
||||
return True
|
||||
|
||||
def check_password(self, peer_id: int, salt: bytes,
|
||||
pass_hash: bytes) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class DenyAllPeerAuth(IPeerAuth):
|
||||
"""
|
||||
Auth agent to deny any peer id and any password
|
||||
"""
|
||||
def allow_peer_id(self, peer_id: int) -> bool:
|
||||
logging.warning("Deny all policy active")
|
||||
return False
|
||||
|
||||
def check_password(self, peer_id: int, salt: bytes,
|
||||
pass_hash: bytes) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class ListPeerAuth(IPeerAuth):
|
||||
"""
|
||||
Auth agent with list of allowed peers and their passwords
|
||||
use empty password to accept any password
|
||||
"""
|
||||
def __init__(self, allowed_peers: dict[int, str]|None = None) -> None:
|
||||
"""
|
||||
allowed_peers in format peer_id -> password
|
||||
"""
|
||||
self.allowed_peers: dict[int, str] = (
|
||||
dict() if allowed_peers is None else allowed_peers)
|
||||
|
||||
def allow_peer_id(self, peer_id: int) -> bool:
|
||||
return peer_id in self.allowed_peers
|
||||
|
||||
def check_password(self, peer_id: int, salt: bytes,
|
||||
pass_hash: bytes) -> bool:
|
||||
if peer_id not in self.allowed_peers:
|
||||
return False
|
||||
|
||||
valid_password = self.allowed_peers[peer_id]
|
||||
if valid_password == '':
|
||||
logging.debug(f"Any password accepted for {peer_id}")
|
||||
return True # Accept any password if empty in config
|
||||
|
||||
return pass_hash == calc_password_hash(salt, valid_password)
|
||||
Executable
+118
@@ -0,0 +1,118 @@
|
||||
import logging
|
||||
|
||||
from time import time
|
||||
|
||||
from .dmrproto import DMRPPacketData
|
||||
from .peer import Peer
|
||||
|
||||
|
||||
class Call:
|
||||
DEAD_TIMEOUT = 5
|
||||
CLEAN_TIMEOUT = 60
|
||||
CLEAN_LOG_TIMEOUT = 3600 * 6 # 6h
|
||||
|
||||
def __init__(self, call_id: int,
|
||||
src_id: int, dst_id: int, peer_id: int,
|
||||
call_type: DMRPPacketData.CallType) -> None:
|
||||
self.call_id: int = call_id
|
||||
self.src_id: int = src_id
|
||||
self.dst_id: int = dst_id
|
||||
self.peer_id: int = peer_id
|
||||
self.call_type: DMRPPacketData.CallType = call_type
|
||||
self.start_time: float = time()
|
||||
self.last_packet_time: float = time()
|
||||
self.end_time: float|None = None
|
||||
self.packets: int = 0
|
||||
self.route_to: set[Peer]|None = None
|
||||
logging.info(f"Voice call beg {str(self)}")
|
||||
|
||||
@property
|
||||
def dst_hr(self) -> str:
|
||||
dst: str = f"{self.dst_id}"
|
||||
if self.call_type == DMRPPacketData.CallType.GROUP:
|
||||
dst = f"TG-{self.dst_id}"
|
||||
return dst
|
||||
|
||||
@property
|
||||
def is_ended(self) -> bool:
|
||||
return self.end_time is not None
|
||||
|
||||
@property
|
||||
def is_dead(self) -> bool:
|
||||
return (not self.is_ended and
|
||||
not self.check_timeout(self.DEAD_TIMEOUT))
|
||||
|
||||
@property
|
||||
def time(self) -> float:
|
||||
if self.end_time is None:
|
||||
return self.last_packet_time - self.start_time
|
||||
return self.end_time - self.start_time
|
||||
|
||||
@property
|
||||
def to_be_cleaned(self) -> bool:
|
||||
return self.is_ended and not self.check_timeout(self.CLEAN_TIMEOUT)
|
||||
|
||||
@property
|
||||
def to_be_cleaned_log(self) -> bool:
|
||||
return self.is_ended and not self.check_timeout(self.CLEAN_LOG_TIMEOUT)
|
||||
|
||||
def packet_received(self) -> None:
|
||||
self.last_packet_time = time()
|
||||
self.packets += 1
|
||||
|
||||
def check_timeout(self, timeout: float) -> bool:
|
||||
return time() - self.last_packet_time < timeout
|
||||
|
||||
def end(self, by_timeout: bool = False) -> None:
|
||||
self.end_time = self.last_packet_time if by_timeout else time()
|
||||
action = "t/o" if by_timeout else "end"
|
||||
logging.info(f"Voice call {action} {str(self)}")
|
||||
|
||||
def __str__(self) -> str:
|
||||
duration = f"dur:{self.time:.1f}s" if self.is_ended else "running"
|
||||
res = (f"id:{self.call_id} {self.src_id}->{self.dst_hr} "
|
||||
f"peer:{self.peer_id} {duration}")
|
||||
return res
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Call '{self.call_id}'>"
|
||||
|
||||
|
||||
class CallKeeper:
|
||||
def __init__(self) -> None:
|
||||
self.calls: set[Call] = set()
|
||||
self.calls_log: set[Call] = set()
|
||||
|
||||
def maintain(self) -> None:
|
||||
# end dead calls
|
||||
dead_calls: set[Call] = set(call for call in self.calls
|
||||
if call.is_dead)
|
||||
|
||||
if len(dead_calls) > 0:
|
||||
logging.debug(f"Ending dead calls {dead_calls}")
|
||||
for call in dead_calls:
|
||||
call.end(by_timeout=True)
|
||||
|
||||
logging.debug(
|
||||
"Upkeep calls" +
|
||||
("".join(["\n - " + str(call) for call in self.calls])))
|
||||
|
||||
clean_calls: set[Call] = set(call for call in self.calls
|
||||
if call.to_be_cleaned)
|
||||
|
||||
if len(clean_calls) > 0:
|
||||
logging.debug(f"Removing ended calls {clean_calls}")
|
||||
self.calls -= clean_calls
|
||||
|
||||
clean_log: set[Call] = set(call for call in self.calls_log
|
||||
if call.to_be_cleaned_log)
|
||||
self.calls_log -= clean_log
|
||||
|
||||
def by_call_id(self, call_id: int) -> Call|None:
|
||||
call_id_map: dict[int, Call] = {call.call_id: call
|
||||
for call in self.calls}
|
||||
return call_id_map[call_id] if call_id in call_id_map else None
|
||||
|
||||
def add(self, call: Call) -> None:
|
||||
self.calls.add(call)
|
||||
self.calls_log.add(call)
|
||||
Executable
+138
@@ -0,0 +1,138 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
from time import time
|
||||
|
||||
from .app import AppKeeper, IAppDispatcher
|
||||
from .call import Call, CallKeeper
|
||||
from .dmrproto import DMRPPacketData, DMRPPacketTalkerAlias
|
||||
from .dmrproto import DMRPPacketFactory, DMRPBasePacket, DMRPBasePeerPacket
|
||||
from .network import IDatagramReceiver, IDatagramSender
|
||||
from .peer import Peer, PeerKeeper
|
||||
from .peer_controller import IPCDispatcher, PeerController
|
||||
from .auth import IPeerAuth, DenyAllPeerAuth
|
||||
from .pphex import hexdump
|
||||
|
||||
|
||||
class Dispatcher(IDatagramReceiver, IAppDispatcher, IPCDispatcher):
|
||||
MAINTENANCE_PERIOD = 10
|
||||
|
||||
def __init__(self, sender: IDatagramSender) -> None:
|
||||
self.sender: IDatagramSender = sender
|
||||
self.peer_auth: IPeerAuth = DenyAllPeerAuth() # Default policy to deny
|
||||
self.peer_keeper: PeerKeeper = PeerKeeper()
|
||||
self.call_keeper: CallKeeper = CallKeeper()
|
||||
self.app_keeper: AppKeeper = AppKeeper(self)
|
||||
|
||||
sender.set_receiver(self)
|
||||
|
||||
asyncio.create_task(self.maintain_task())
|
||||
|
||||
def maintain(self) -> None:
|
||||
self.peer_keeper.maintain()
|
||||
self.call_keeper.maintain()
|
||||
|
||||
async def maintain_task(self) -> None:
|
||||
logging.debug("Dispatcher: periodic maintenance task scheduled")
|
||||
while True:
|
||||
await asyncio.sleep(self.MAINTENANCE_PERIOD)
|
||||
self.maintain()
|
||||
|
||||
def shutdown(self) -> None:
|
||||
logging.info("Dispatcher: shutting down")
|
||||
for peer in self.peer_keeper.get_all():
|
||||
controller: PeerController = PeerController(peer, self)
|
||||
controller.send_close()
|
||||
|
||||
def dispatch_data_packet(self, p: DMRPPacketData, orig_addr: tuple) -> None:
|
||||
logging.debug(f"Dispatching packet from {orig_addr}:\n{p}\n")
|
||||
|
||||
call_id = p.stream_id
|
||||
|
||||
if (call := self.call_keeper.by_call_id(call_id)) is None:
|
||||
call = Call(p.stream_id, p.src_id, p.dst_id,
|
||||
p.peer_id, p.call_type)
|
||||
self.call_keeper.add(call)
|
||||
|
||||
if call.call_type == DMRPPacketData.CallType.UNIT:
|
||||
# get peers location and rout unit call
|
||||
peers = self.peer_keeper.get_by_unit(p.dst_id)
|
||||
if len(peers) > 0:
|
||||
call.route_to = peers
|
||||
|
||||
call.packet_received()
|
||||
|
||||
if p.is_voice_term:
|
||||
call.end()
|
||||
|
||||
self.app_keeper.process_call_packet(call, p)
|
||||
|
||||
if isinstance(p, DMRPBasePeerPacket):
|
||||
self.distribute_by_peers(p, orig_addr, call.route_to)
|
||||
|
||||
def dispatch_ta_packet(self, p: DMRPPacketTalkerAlias,
|
||||
orig_addr: tuple) -> None:
|
||||
self.distribute_by_peers(p, orig_addr)
|
||||
|
||||
def distribute_by_peers(self, p: DMRPBasePeerPacket,
|
||||
orig_addr: tuple,
|
||||
peers: set[Peer]|None = None) -> None:
|
||||
sendp = p.copy()
|
||||
|
||||
if peers is None:
|
||||
peers = self.peer_keeper.get_active()
|
||||
for peer in peers:
|
||||
if orig_addr == peer.addr: # skip myself
|
||||
continue
|
||||
sendp.peer_id = peer.peer_id
|
||||
# logging.debug(f" - sending to {peer.logname}"
|
||||
# f"\n{hexdump(sendp.get_data())}\n{sendp}\n")
|
||||
self.sender.send_dg(sendp.get_data(), peer.addr)
|
||||
|
||||
#-------------------------------
|
||||
# IDatagramReceiver implementation
|
||||
def recv_dg(self, data: bytes, addr: tuple) -> None:
|
||||
peer: Peer = self.peer_keeper.get_by_addr(addr)
|
||||
p: DMRPBasePacket|None = None
|
||||
|
||||
try:
|
||||
p = DMRPPacketFactory.fd(data)
|
||||
logging.debug(f"Got packet "
|
||||
f"from {peer.logname} | {len(data)} bytes:\n"
|
||||
f"{hexdump(data)}\n"
|
||||
f"{str(p)}\n")
|
||||
except Exception as e:
|
||||
logging.error(f"Exception with packet "
|
||||
f"from {peer.logname} | {len(data)} bytes:\n"
|
||||
f"{hexdump(data)}\n"
|
||||
f"{traceback.format_exc()}")
|
||||
return
|
||||
|
||||
# process in context of the peer by controller
|
||||
controller: PeerController = PeerController(peer, self)
|
||||
if not controller.process_packet(p):
|
||||
return
|
||||
|
||||
if type(p) is DMRPPacketData:
|
||||
self.dispatch_data_packet(p, addr)
|
||||
|
||||
if type(p) is DMRPPacketTalkerAlias:
|
||||
self.dispatch_ta_packet(p, addr)
|
||||
|
||||
#-------------------------------
|
||||
# IAppDispatcher implementation
|
||||
def inject_packet(self, p: DMRPPacketData) -> None:
|
||||
logging.debug(f"Injecting packet from app")
|
||||
self.dispatch_data_packet(p, (None, None))
|
||||
|
||||
#-------------------------------
|
||||
# IPCDispatcher implementation
|
||||
def send_dg(self, data: bytes, addr: tuple) -> None:
|
||||
self.sender.send_dg(data, addr)
|
||||
|
||||
def get_peer_keeper(self) -> PeerKeeper:
|
||||
return self.peer_keeper
|
||||
|
||||
def get_peer_auth(self) -> IPeerAuth:
|
||||
return self.peer_auth
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class IDatagramReceiver(ABC):
|
||||
@abstractmethod
|
||||
def recv_dg(self, data: bytes, addr: tuple) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class IDatagramSender(ABC):
|
||||
@abstractmethod
|
||||
def send_dg(self, data: bytes, addr: tuple) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_receiver(self, receiver: IDatagramReceiver) -> None:
|
||||
pass
|
||||
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
|
||||
from .app import App, IAppCallInterceptor, AppException
|
||||
from .call import Call
|
||||
from .dmrproto import DMRPPacketData
|
||||
|
||||
|
||||
class ParrotApp(App, IAppCallInterceptor):
|
||||
"""
|
||||
Simple parrot application
|
||||
"""
|
||||
def __init__(self, parrot_id: int = 9990, repeat_delay: int = 5,
|
||||
enable_unit: bool = True, enable_group: bool = True) -> None:
|
||||
# settings
|
||||
self.parrot_id: int = parrot_id
|
||||
self.repeat_delay: int = repeat_delay
|
||||
self.enable_unit: bool = enable_unit
|
||||
self.enable_group: bool = enable_group
|
||||
|
||||
super().__init__()
|
||||
|
||||
# internal state
|
||||
self._mycalls: set[int] = set()
|
||||
self._records: dict[int, list[DMRPPacketData]] = dict()
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.get_name()
|
||||
|
||||
async def repeat(self, packets: list[DMRPPacketData]) -> None:
|
||||
if self.dispatcher is None:
|
||||
raise AppException(
|
||||
"send_packets called before dispatcher had been set")
|
||||
|
||||
if len(packets) == 0:
|
||||
logging.debug(f"{self.name}: nothing to repeat, 0 packets in call")
|
||||
return
|
||||
|
||||
await asyncio.sleep(self.repeat_delay)
|
||||
|
||||
logging.info(f"{self.name}: repeating call")
|
||||
|
||||
stream_id = packets[0].stream_id
|
||||
self._mycalls.add(stream_id)
|
||||
|
||||
start_time = time.monotonic()
|
||||
for i, p in enumerate(packets):
|
||||
self.dispatcher.inject_packet(p)
|
||||
if (delay := start_time + (i + 1) * 0.06 - time.monotonic()) > 0:
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
self._mycalls.discard(stream_id)
|
||||
|
||||
def record(self, call: Call, p: DMRPPacketData) -> None:
|
||||
if p.src_id == self.parrot_id or p.dst_id != self.parrot_id:
|
||||
return
|
||||
|
||||
if (p.call_type == DMRPPacketData.CallType.UNIT
|
||||
and not self.enable_unit):
|
||||
logging.debug(f"{self.name}: unit calls disabled")
|
||||
return
|
||||
|
||||
if (p.call_type == DMRPPacketData.CallType.GROUP
|
||||
and not self.enable_group):
|
||||
logging.debug(f"{self.name}: group calls disabled")
|
||||
return
|
||||
|
||||
if p.stream_id in self._mycalls:
|
||||
logging.debug(f"{self.name}: skipping my own call")
|
||||
return
|
||||
|
||||
if p.stream_id not in self._records:
|
||||
logging.info(f"{self.name}: recording {call.call_id}")
|
||||
self._records[p.stream_id] = []
|
||||
|
||||
ans_p = p.copy()
|
||||
|
||||
# invert source and dest for unit call
|
||||
if p.call_type == DMRPPacketData.CallType.UNIT:
|
||||
ans_p.src_id = self.parrot_id
|
||||
ans_p.dst_id = p.src_id
|
||||
|
||||
ans_p.stream_id = p.stream_id + 1
|
||||
|
||||
self._records[p.stream_id].append(ans_p)
|
||||
|
||||
if call.is_ended:
|
||||
packets: list[DMRPPacketData] = self._records[p.stream_id]
|
||||
asyncio.create_task(self.repeat(packets))
|
||||
del self._records[p.stream_id]
|
||||
|
||||
#------------------------------------
|
||||
# App implementation
|
||||
def get_name(self) -> str:
|
||||
return f"Parrot {self.parrot_id}"
|
||||
|
||||
#------------------------------------
|
||||
# IAppCallInterceptor implementation
|
||||
def process_call_packet(self, call: Call, p: DMRPPacketData) -> None:
|
||||
self.record(call, p)
|
||||
Executable
+158
@@ -0,0 +1,158 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import logging
|
||||
|
||||
from time import time
|
||||
|
||||
|
||||
class Unit:
|
||||
TIMEOUT = 3600
|
||||
|
||||
def __init__(self, unit_id: int) -> None:
|
||||
self.unit_id: int = unit_id
|
||||
self.active_time: float = time()
|
||||
|
||||
def update_active(self) -> None:
|
||||
self.active_time = time()
|
||||
|
||||
def check_timeout(self) -> bool:
|
||||
return time() - self.active_time < self.TIMEOUT
|
||||
|
||||
|
||||
class Peer:
|
||||
class Status(enum.IntEnum):
|
||||
LOGIN = enum.auto()
|
||||
AUTH = enum.auto()
|
||||
CONFIG = enum.auto()
|
||||
ACTIVE = enum.auto()
|
||||
DEAD = enum.auto()
|
||||
|
||||
def is_applicable(self, test_status: Peer.Status) -> bool:
|
||||
"""
|
||||
Loose check, if test_status is applicable for the current one,
|
||||
like we can treat any status except DEAD as LOGIN and accept login
|
||||
packet in it
|
||||
"""
|
||||
applicable_map: dict[Peer.Status, set[Peer.Status]] = {
|
||||
# current status --> Applicable test statuses
|
||||
Peer.Status.AUTH: {Peer.Status.LOGIN,
|
||||
Peer.Status.AUTH},
|
||||
Peer.Status.CONFIG: {Peer.Status.LOGIN,
|
||||
Peer.Status.AUTH,
|
||||
Peer.Status.CONFIG},
|
||||
Peer.Status.ACTIVE: {Peer.Status.LOGIN,
|
||||
Peer.Status.AUTH,
|
||||
Peer.Status.CONFIG,
|
||||
Peer.Status.ACTIVE},
|
||||
}
|
||||
|
||||
# if not in map, return exact match
|
||||
if self not in applicable_map:
|
||||
return self == test_status
|
||||
|
||||
# return if applicable
|
||||
return test_status in applicable_map[self]
|
||||
|
||||
|
||||
PING_TIMEOUT = 130
|
||||
|
||||
def __init__(self, addr: tuple) -> None:
|
||||
self.addr: tuple = addr
|
||||
self.auth_salt: bytes|None = None
|
||||
self.status: Peer.Status = Peer.Status.LOGIN
|
||||
self.peer_id: int = 0
|
||||
self.connect_time: float = time()
|
||||
self.active_time: float = time()
|
||||
self.config: dict[str, str] = dict()
|
||||
self.units: dict[int, Unit] = dict()
|
||||
|
||||
@property
|
||||
def logname(self) -> str:
|
||||
return (self.addr_str if self.peer_id == 0
|
||||
else f"{self.peer_id}/{self.addr_str}")
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return str(self.peer_id) if self.peer_id != 0 else self.addr_str
|
||||
|
||||
@property
|
||||
def addr_str(self) -> str:
|
||||
return f"{self.addr[0]}:{self.addr[1]}"
|
||||
|
||||
def check_timeout(self) -> bool:
|
||||
return time() - self.active_time < self.PING_TIMEOUT
|
||||
|
||||
def update_active(self) -> None:
|
||||
self.active_time = time()
|
||||
|
||||
def update_unit(self, unit_id: int) -> None:
|
||||
if unit_id not in self.units:
|
||||
logging.info(f"Unit {unit_id} added to {self.logname}")
|
||||
self.units[unit_id] = Unit(unit_id)
|
||||
self.units[unit_id].update_active()
|
||||
|
||||
def die(self) -> None:
|
||||
self.status = Peer.Status.DEAD
|
||||
self.peer_id = 0
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.logname} status:{self.status.name}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Peer '{self.logname}'>"
|
||||
|
||||
|
||||
class PeerKeeper:
|
||||
def __init__(self) -> None:
|
||||
self.peers: set[Peer] = set()
|
||||
|
||||
def maintain(self) -> None:
|
||||
# Check timeouts
|
||||
for peer in self.peers:
|
||||
# check unit timeouts:
|
||||
tout_unit_ids = [unit_id for unit_id, unit in peer.units.items()
|
||||
if not unit.check_timeout()]
|
||||
|
||||
for unit_id in tout_unit_ids:
|
||||
logging.info(f"Unit {unit_id} removed from {peer.logname}")
|
||||
del peer.units[unit_id]
|
||||
|
||||
if not peer.check_timeout():
|
||||
logging.info(f"Peer {peer.logname} timed out")
|
||||
peer.die()
|
||||
|
||||
# remove dead peers
|
||||
dead_peers = set(peer for peer in self.peers
|
||||
if peer.status == Peer.Status.DEAD)
|
||||
|
||||
if len(dead_peers) > 0:
|
||||
logging.debug(f"Removing dead peers {dead_peers}")
|
||||
self.peers -= dead_peers
|
||||
|
||||
logging.debug(
|
||||
"Upkeep peers" +
|
||||
("".join(["\n - " + str(peer) for peer in self.peers])))
|
||||
|
||||
def get_by_addr(self, addr: tuple) -> Peer:
|
||||
addr_map: dict[tuple, Peer] = {peer.addr: peer for peer in self.peers}
|
||||
if addr in addr_map:
|
||||
return addr_map[addr]
|
||||
|
||||
peer = Peer(addr)
|
||||
logging.info(f"Peer {peer.logname} connected")
|
||||
self.peers.add(peer)
|
||||
return peer
|
||||
|
||||
def get_by_id(self, peer_id: int) -> set[Peer]:
|
||||
return {peer for peer in self.peers if peer.peer_id == peer_id}
|
||||
|
||||
def get_by_unit(self, unit_id: int) -> set[Peer]:
|
||||
return {peer for peer in self.peers if unit_id in peer.units}
|
||||
|
||||
def get_all(self) -> set[Peer]:
|
||||
return self.peers
|
||||
|
||||
def get_active(self) -> set[Peer]:
|
||||
return {peer for peer in self.peers
|
||||
if peer.status == Peer.Status.ACTIVE}
|
||||
Executable
+210
@@ -0,0 +1,210 @@
|
||||
import logging
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from .auth import IPeerAuth
|
||||
from .dmrproto import DMRPBasePacket
|
||||
from .dmrproto import DMRPPacketConfig, DMRPPacketPing, DMRPPacketPong
|
||||
from .dmrproto import DMRPPacketData, DMRPPacketTalkerAlias
|
||||
from .dmrproto import DMRPPacketLogin, DMRPPacketSalt, DMRPPacketAuth
|
||||
from .dmrproto import DMRPPacketMasterClose, DMRPPacketRepeaterClose
|
||||
from .dmrproto import DMRPPacketMasterNoAck, DMRPPacketAck
|
||||
from .peer import Peer, PeerKeeper
|
||||
from .pphex import hexdump
|
||||
|
||||
|
||||
class IPCDispatcher(ABC):
|
||||
"""
|
||||
Dispatcher interface to pass into controller to call back
|
||||
"""
|
||||
@abstractmethod
|
||||
def send_dg(self, data: bytes, addr: tuple) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_peer_keeper(self) -> PeerKeeper:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_peer_auth(self) -> IPeerAuth:
|
||||
pass
|
||||
|
||||
|
||||
class PeerController:
|
||||
def __init__(self, peer: Peer, dispatcher: IPCDispatcher) -> None:
|
||||
self.peer: Peer = peer
|
||||
self.dispatcher: IPCDispatcher = dispatcher
|
||||
self.peer_auth: IPeerAuth = dispatcher.get_peer_auth()
|
||||
|
||||
def process_packet(self, p: DMRPBasePacket) -> bool:
|
||||
"""
|
||||
Process packet in context of the peer
|
||||
|
||||
Returns:
|
||||
True - continue processing with dispatcher (for data)
|
||||
False - stop processing (peer cmds)
|
||||
"""
|
||||
# CLOSE packet
|
||||
if type(p) is DMRPPacketRepeaterClose:
|
||||
logging.info(f"Peer {self.peer.logname} disconnected")
|
||||
self.peer.die()
|
||||
return False
|
||||
|
||||
# DATA packet
|
||||
if type(p) is DMRPPacketData:
|
||||
if not self.peer.status.is_applicable(Peer.Status.ACTIVE):
|
||||
logging.debug(
|
||||
f"Got DMRPPacketData "
|
||||
f"while status is {self.peer.status.name}")
|
||||
logging.error(f"Data from inactive peer {self.peer.logname}. "
|
||||
f"Closing connection.")
|
||||
self.peer.die()
|
||||
self.send_close(p.peer_id)
|
||||
return False
|
||||
|
||||
self.peer.update_active()
|
||||
self.peer.update_unit(p.src_id)
|
||||
return True
|
||||
|
||||
# PING packet
|
||||
if type(p) is DMRPPacketPing:
|
||||
if not self.peer.status.is_applicable(Peer.Status.ACTIVE):
|
||||
logging.debug(
|
||||
f"Got DMRPPacketPing "
|
||||
f"while status is {self.peer.status.name}")
|
||||
logging.error(f"Ping from inactive peer {self.peer.logname}. "
|
||||
f"Closing connection.")
|
||||
self.peer.die()
|
||||
self.send_close(p.peer_id)
|
||||
return False
|
||||
|
||||
logging.debug(f"Ping-pong {self.peer.logname}.")
|
||||
self.peer.update_active()
|
||||
self.send_pong()
|
||||
return False
|
||||
|
||||
# LOGIN sequence
|
||||
if type(p) is DMRPPacketLogin:
|
||||
if not self.peer.status.is_applicable(Peer.Status.LOGIN):
|
||||
logging.debug(
|
||||
f"Got DMRPPacketLogin "
|
||||
f"while status is {self.peer.status.name}")
|
||||
logging.error(f"Bad login sequence from {self.peer.logname}. "
|
||||
f"Closing connection.")
|
||||
self.peer.die()
|
||||
self.send_close(p.peer_id)
|
||||
return False
|
||||
|
||||
peer_keeper: PeerKeeper = self.dispatcher.get_peer_keeper()
|
||||
if len(peer_keeper.get_by_id(p.peer_id)) > 0:
|
||||
logging.error(f"Auth {self.peer.logname}: "
|
||||
f"peer id {p.peer_id} is already connected")
|
||||
self.peer.die()
|
||||
self.send_close(p.peer_id)
|
||||
return False
|
||||
|
||||
if not self.peer_auth.allow_peer_id(p.peer_id):
|
||||
logging.error(f"Auth {self.peer.logname}: "
|
||||
f"peer id {p.peer_id} disabled")
|
||||
self.peer.die()
|
||||
self.send_close(p.peer_id)
|
||||
return False
|
||||
|
||||
self.peer.status = Peer.Status.AUTH
|
||||
logging.info(f"Login request from {self.peer.logname}"
|
||||
f" ({p.peer_id})")
|
||||
self.send_salt()
|
||||
return False
|
||||
|
||||
if type(p) is DMRPPacketAuth:
|
||||
if not self.peer.status.is_applicable(Peer.Status.AUTH):
|
||||
logging.debug(
|
||||
f"Got DMRPPacketAuth "
|
||||
f"while status is {self.peer.status.name}")
|
||||
logging.error(f"Bad login sequence from {self.peer.logname}. "
|
||||
f"Closing connection.")
|
||||
self.peer.die()
|
||||
self.send_close(p.peer_id)
|
||||
return False
|
||||
|
||||
if (self.peer.auth_salt is None or
|
||||
not self.peer_auth.check_password(p.peer_id,
|
||||
self.peer.auth_salt,
|
||||
p.pass_hash)):
|
||||
logging.error(
|
||||
f"Auth {self.peer.logname}: password incorrect")
|
||||
self.peer.die()
|
||||
self.send_close(p.peer_id)
|
||||
return False
|
||||
|
||||
self.peer.peer_id = p.peer_id
|
||||
self.peer.status = Peer.Status.CONFIG
|
||||
logging.info(f"Auth success {self.peer.logname}")
|
||||
self.send_ack_ok()
|
||||
return False
|
||||
|
||||
if type(p) is DMRPPacketConfig:
|
||||
if not self.peer.status.is_applicable(Peer.Status.CONFIG):
|
||||
logging.debug(
|
||||
f"Got DMRPPacketConfig "
|
||||
f"while status is {self.peer.status.name}")
|
||||
logging.error(f"Bad login sequence from {self.peer.logname}. "
|
||||
f"Closing connection.")
|
||||
self.peer.die()
|
||||
self.send_close(p.peer_id)
|
||||
return False
|
||||
|
||||
config = {
|
||||
'callsign': p.callsign,
|
||||
'rx_freq': p.rx_freq,
|
||||
'tx_freq': p.tx_freq,
|
||||
'power': p.power,
|
||||
'color_code': p.color_code,
|
||||
'lat': p.lat,
|
||||
'lon': p.lon,
|
||||
'height': p.height,
|
||||
'location': p.location,
|
||||
'description': p.description,
|
||||
'slots': p.slots,
|
||||
'url': p.url,
|
||||
'software_id': p.software_id,
|
||||
'package_id': p.package_id,
|
||||
}
|
||||
logging.info(f"Config from {self.peer.logname}: " +
|
||||
", ".join(f"{k}={v}" for k, v in config.items()));
|
||||
|
||||
self.peer.status = Peer.Status.ACTIVE
|
||||
self.peer.config = config
|
||||
self.send_ack_ok()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def send_data(self, p: DMRPBasePacket) -> None:
|
||||
data: bytes = p.get_data()
|
||||
logging.debug(
|
||||
f"Sending packet to {self.peer.logname} | {len(data)} bytes:\n"
|
||||
f"{hexdump(data)}\n"
|
||||
f"{str(p)}\n")
|
||||
self.dispatcher.send_dg(p.get_data(), self.peer.addr)
|
||||
|
||||
def send_close(self, peer_id: int = 0) -> None:
|
||||
p = DMRPPacketMasterClose()
|
||||
p.peer_id = peer_id if peer_id != 0 else self.peer.peer_id
|
||||
self.send_data(p)
|
||||
|
||||
def send_salt(self) -> None:
|
||||
p = DMRPPacketSalt()
|
||||
p.set_random_salt()
|
||||
self.peer.auth_salt = p.salt
|
||||
self.send_data(p)
|
||||
|
||||
def send_ack_ok(self) -> None:
|
||||
p = DMRPPacketAck()
|
||||
p.peer_id = self.peer.peer_id
|
||||
self.send_data(p)
|
||||
|
||||
def send_pong(self) -> None:
|
||||
p = DMRPPacketPong()
|
||||
p.peer_id = self.peer.peer_id
|
||||
self.send_data(p)
|
||||
Ссылка в новой задаче
Block a user