diff --git a/boswatch/network/broadcast.py b/boswatch/network/broadcast.py new file mode 100644 index 0000000..cfaf0d4 --- /dev/null +++ b/boswatch/network/broadcast.py @@ -0,0 +1,173 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""! + ____ ____ ______ __ __ __ _____ + / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / + / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < + / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / +/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ + German BOS Information Script + by Bastian Schroll + +@file: broadcast.py +@date: 21.09.2018 +@author: Bastian Schroll +@description: UDP broadcast server and client class +""" +import logging +import socket +import threading + +logging.debug("- %s loaded", __name__) + + +class BroadcastClient: + """!BroadcastClient class""" + + def __init__(self, port=5000): + """!Create an BroadcastClient instance + + @param port: port to send broadcast packets (5000)""" + self._broadcastPort = port + + self._serverIP = "" + self._serverPort = 0 + + self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + self._socket.settimeout(3) + + def getConnInfo(self, retry=0): + """!Get the connection info from server over udp broadcast + + This function will send broadcast package(s) + to get connection info from the server. + + - send the magic packet on broadcast address. + - wait for a magic packet. + - extract the connection data from the magic packet and return + + @param retry: Count of retry - 0 is infinite (0) + + @return True or False""" + sendPackages = 0 + while sendPackages < retry or retry == 0: + try: + logging.debug("send magic as broadcast - Try: %d", sendPackages) + self._socket.sendto("".encode(), ('255.255.255.255', self._broadcastPort)) + sendPackages += 1 + payload, address = self._socket.recvfrom(1024) + payload = str(payload, "UTF-8") + + if payload.startswith(""): + logging.debug("received magic from: %s", address[0]) + self._serverIP = address[0] + self._serverPort = int(payload.split(";")[1]) + logging.info("got connection info from server: %s:%d", self._serverIP, self._serverPort) + return True + except socket.timeout: # nothing received - retry + logging.debug("no magic packet received") + except: # pragma: no cover + logging.exception("error on getting connection info") + logging.warning("cannot fetch connection info after %d tries", sendPackages) + return False + + @property + def serverIP(self): + """!Property to get the server IP after successful broadcast""" + return self._serverIP + + @property + def serverPort(self): + """!Property to get the server Port after successful broadcast""" + return self._serverPort + + +class BroadcastServer: + """!BroadcastServer class""" + + def __init__(self, servePort=8080, listenPort=5000): + """!Create an BroadcastServer instance + + @param servePort: port to serve as connection info (8080) + @param listenPort: port to listen for broadcast packets (5000)""" + self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + self._socket.settimeout(2) + self._socket.bind(('', listenPort)) + self._serverThread = None + self._serverShutdown = False + self._servePort = servePort + + def start(self): + """!Start the broadcast server in a new thread + + @return True or False""" + try: + if not self._serverThread: + logging.debug("start udp broadcast server") + self._serverThread = threading.Thread(target=self._listen) + self._serverThread.name = "BroadServ" + self._serverThread.daemon = True + self._serverShutdown = False + self._serverThread.start() + return True + else: + logging.warning("udp broadcast server always started") + return True + except: # pragma: no cover + logging.exception("cannot start udp broadcast server thread") + return False + + def stop(self): + """!Stop the broadcast server + + Due to the timeout of the socket, + stopping the thread can be delayed by two seconds. + But function returns immediately. + + @return True or False""" + try: + if self._serverThread: + logging.debug("stop udp broadcast server") + self._serverShutdown = True + return True + else: + logging.warning("udp broadcast server always stopped") + return True + except: # pragma: no cover + logging.exception("cannot stop udp broadcast server thread") + return False + + def _listen(self): + """!Broadcast server worker thread + + This function listen for magic packets on broadcast + address and send the connection info to the clients. + + - listen for the magic packet + - send connection info in an macig packet""" + logging.debug("start listening for magic") + while not self._serverShutdown: + try: + payload, address = self._socket.recvfrom(1024) + payload = str(payload, "UTF-8") + if payload == "": + logging.debug("received magic from: %s", address[0]) + logging.info("send connection info in magic to: %s", address[0]) + self._socket.sendto(";".encode() + str(self._servePort).encode(), address) + except socket.timeout: + continue # timeout is accepted (not block at recvfrom()) + except: # pragma: no cover + logging.exception("error while listening for clients") + self._serverThread = None + logging.debug("udp broadcast server stopped") + + @property + def isRunning(self): + """!Property of broadcast server running state""" + if self._serverThread: + return True + return False diff --git a/bw_server.py b/bw_server.py index 414d78b..fcdab23 100644 --- a/bw_server.py +++ b/bw_server.py @@ -51,13 +51,27 @@ try: from boswatch.descriptor.descriptor import Descriptor from boswatch.filter.doubeFilter import DoubleFilter from boswatch.utils import header + from boswatch.network.broadcast import BroadcastClient + from boswatch.network.broadcast import BroadcastServer except Exception as e: # pragma: no cover logging.exception("cannot import modules") print("cannot import modules") print(e) exit(1) +# Test for the broadcast connection info function +server = BroadcastServer() +client = BroadcastClient() +server.start() +print(server.isRunning) +client.getConnInfo() +print(client.serverIP, client.serverPort) +server.stop() +print(server.isRunning) +time.sleep(2) +print(server.isRunning) +# test for the timer class from boswatch.utils.timer import RepeatedTimer from boswatch.network.netCheck import NetCheck net = NetCheck() diff --git a/test/test_broadcast.py b/test/test_broadcast.py new file mode 100644 index 0000000..c8e561b --- /dev/null +++ b/test/test_broadcast.py @@ -0,0 +1,70 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""! + ____ ____ ______ __ __ __ _____ + / __ )/ __ \/ ___/ | / /___ _/ /______/ /_ |__ / + / __ / / / /\__ \| | /| / / __ `/ __/ ___/ __ \ /_ < + / /_/ / /_/ /___/ /| |/ |/ / /_/ / /_/ /__/ / / / ___/ / +/_____/\____//____/ |__/|__/\__,_/\__/\___/_/ /_/ /____/ + German BOS Information Script + by Bastian Schroll + +@file: test_broadcast.py +@date: 25.09.2018 +@author: Bastian Schroll +@description: Unittests for BOSWatch. File must be _run as "pytest" unittest +""" +import logging +import time +import pytest + +from boswatch.network.broadcast import BroadcastServer +from boswatch.network.broadcast import BroadcastClient + + +class Test_Timer: + """!Unittest for the timer class""" + + def setup_method(self, method): + logging.debug("[TEST] %s.%s", type(self).__name__, method.__name__) + + @pytest.fixture(scope="function") + def useBroadcastServer(self): + """!Server a BroadcastServer instance""" + self.broadcastServer = BroadcastServer() + yield 1 # server the server instance + if self.broadcastServer.isRunning: + assert self.broadcastServer.stop() + while self.broadcastServer.isRunning: + pass + + @pytest.fixture(scope="function") + def useBroadcastClient(self): + """!Server a BroadcastClient instance""" + self.broadcastClient = BroadcastClient() + yield 1 # server the server instance + + # tests start here + + def test_serverStartStop(self, useBroadcastServer): + assert self.broadcastServer.start() + assert self.broadcastServer.isRunning + assert self.broadcastServer.stop() + + def test_serverDoubleStart(self, useBroadcastServer): + assert self.broadcastServer.start() + assert self.broadcastServer.start() + assert self.broadcastServer.stop() + + def test_serverStopNotStarted(self, useBroadcastServer): + assert self.broadcastServer.stop() + + def test_clientWithoutServer(self, useBroadcastClient): + assert not self.broadcastClient.getConnInfo(1) + + def test_serverClientFetchConnInfo(self, useBroadcastServer, useBroadcastClient): + assert self.broadcastServer.start() + assert self.broadcastClient.getConnInfo() + assert self.broadcastServer.stop() + assert self.broadcastClient.serverIP + assert self.broadcastClient.serverPort