diff --git a/bumper.py b/bumper.py index ed7af8d..a6ba2ac 100644 --- a/bumper.py +++ b/bumper.py @@ -6,9 +6,10 @@ import sys, socket import time import platform -bumperlog = logging.getLogger("bumper") + def main(): args = sys.argv + if len(args) > 0: if '--debug' in args: logging.basicConfig(level=logging.DEBUG, @@ -18,14 +19,7 @@ def main(): format="[%(asctime)s] :: %(levelname)s :: %(name)s :: %(message)s") #format="[%(asctime)s] :: %(levelname)s :: %(name)s :: %(module)s :: %(funcName)s :: %(lineno)d :: %(message)s") - # A default bot could be set here to automatically add it as available - # dbot = bumper.VacBotDevice("did", "class", "resource", "name","nick" ) - # bclient = bumper.bumper_bots_var - # bclienttemp = bclient.get() - # bclienttemp.append(dbot.asdict()) - # bclient.set(bclienttemp) - - if platform.system() == "Darwin": + if platform.system() == "Darwin": #If a Mac, use 0.0.0.0 for listening listen_host = "0.0.0.0" else: listen_host = socket.gethostbyname(socket.gethostname()) @@ -35,36 +29,46 @@ def main(): conf_address_8007 = (listen_host, 8007) xmpp_address = (listen_host, 5223) mqtt_address = (listen_host, 8883) + + xmpp_server = bumper.XMPPServer(xmpp_address) + mqtt_server = bumper.MQTTServer(mqtt_address,bumper_bots=bumper.bumper_bots_var,bumper_clients=bumper.bumper_clients_var,remove_clients=bumper.bumper_removeclients_var) + mqtt_helperbot = bumper.MQTTHelperBot(mqtt_address, bumper_bots=bumper.bumper_bots_var,bumper_clients=bumper.bumper_clients_var) + conf_server = bumper.ConfServer(conf_address_443, usessl=True, bumper_bots=bumper.bumper_bots_var,bumper_clients=bumper.bumper_clients_var, remove_clients=bumper.bumper_removeclients_var,helperbot=mqtt_helperbot) + conf_server_2 = bumper.ConfServer(conf_address_8007, usessl=False, bumper_bots=bumper.bumper_bots_var,bumper_clients=bumper.bumper_clients_var, helperbot=mqtt_helperbot,remove_clients=bumper.bumper_removeclients_var) + + # start xmpp server on port 5223 (sync) + xmpp_server.run(run_async=True) #Start in new thread - # start mqtt server on port 8883 (async) - startmqttserver = "Starting MQTT Server at {}".format(mqtt_address) - bumperlog.info("{}".format(startmqttserver)) - print("{}".format(startmqttserver)) - mqtt_server = bumper.MQTTServer(mqtt_address, run_async=True,bumper_bots=bumper.bumper_bots_var,bumper_clients=bumper.bumper_clients_var) + # start mqtt server on port 8883 (async) + mqtt_server.run(run_async=True) #Start in new thread + time.sleep(1.5) #Wait for broker startup - # start mqtt_helperbot (async) - bumperlog.info("Starting MQTT HelperBot") - print("Starting MQTT HelperBot") - mqtt_helperbot = bumper.MQTTHelperBot(mqtt_address, run_async=True,bumper_bots=bumper.bumper_bots_var,bumper_clients=bumper.bumper_clients_var) + # start mqtt_helperbot (async) + mqtt_helperbot.run(run_async=True) #Start in new thread - # start conf server on port 443 (async) - Used for most https calls - startconf443 = "Starting Main ConfServer at {}".format(conf_address_443) - bumperlog.info("{}".format(startconf443)) - print("{}".format(startconf443)) - conf_server = bumper.ConfServer(conf_address_443, usessl=True, run_async=True,bumper_bots=bumper.bumper_bots_var,bumper_clients=bumper.bumper_clients_var, helperbot=mqtt_helperbot) + # start conf server on port 443 (async) - Used for most https calls + conf_server.run(run_async=True) #Start in new thread - # start conf server on port 8007 (async) - Used for a load balancer request - startconf8007 = "Starting LoadBalancer ConfServer at {}".format(conf_address_8007) - bumperlog.info("{}".format(startconf8007)) - print("{}".format(startconf8007)) - conf_server_2 = bumper.ConfServer(conf_address_8007, usessl=False, run_async=True,bumper_bots=bumper.bumper_bots_var,bumper_clients=bumper.bumper_clients_var, helperbot=mqtt_helperbot) + # start conf server on port 8007 (async) - Used for a load balancer request + conf_server_2.run(run_async=True) #Start in new thread - # start xmpp server on port 5223 (sync) - startxmpp = "Starting XMPP Server at {}".format(xmpp_address) - bumperlog.info("{}".format(startxmpp)) - print("{}".format(startxmpp)) - xmpp_server = bumper.XMPPServer(xmpp_address) + while True: + try: + time.sleep(0.25) + # WIP: Remove clients that have disconnected + remove_clients = bumper.bumper_removeclients_var.get() + if len(remove_clients) > 0: + for uid in remove_clients: + if uid != "": + xmpp_server.remove_client_byuid(uid) #Remove clients from xmpp server + remove_clients.remove(uid) + + bumper.bumper_removeclients_var.set(remove_clients) + + except KeyboardInterrupt: + print("Bumper Exiting") + if __name__ == "__main__": main() \ No newline at end of file diff --git a/bumper/__init__.py b/bumper/__init__.py index bfcdb9f..e38cf5b 100644 --- a/bumper/__init__.py +++ b/bumper/__init__.py @@ -7,13 +7,30 @@ from .xmppserver import XMPPServer import asyncio import contextvars import time +import logging bumper_clients_var = contextvars.ContextVar('bumper_clients', default=[]) bumper_bots_var = contextvars.ContextVar('bumper_bots', default=[]) +bumper_removeclients_var = contextvars.ContextVar('bumper_removeclients', default=[]) ca_cert = './certs/CA/cacert.pem' server_cert = './certs/cert.pem' server_key = './certs/key.pem' +#Logs +bumperlog = logging.getLogger("bumper") +confserverlog = logging.getLogger("confserver") +#Override the logging level +#confserverlog.setLevel(logging.INFO) +mqttserverlog = logging.getLogger("mqttserver") +#Override the logging level +#mqttserverlog.setLevel(logging.INFO) +helperbotlog = logging.getLogger("helperbot") +#Override the logging level +#helperbotlog.setLevel(logging.INFO) +xmppserverlog = logging.getLogger("xmppserver") +#Override the logging level +#xmppserverlog.setLevel(logging.INFO) + def get_milli_time(timetoconvert): return int(round(timetoconvert * 1000)) diff --git a/bumper/confserver.py b/bumper/confserver.py index a00aad3..501ca1f 100644 --- a/bumper/confserver.py +++ b/bumper/confserver.py @@ -32,33 +32,37 @@ class ConfServer(): bumper_clients = contextvars.ContextVar bumper_bots = contextvars.ContextVar - def __init__(self, address, usessl=False, run_async=True, bumper_bots=contextvars.ContextVar, bumper_clients=contextvars.ContextVar, helperbot=None): + def __init__(self, address, usessl=False, bumper_bots=contextvars.ContextVar, bumper_clients=contextvars.ContextVar, remove_clients=contextvars.ContextVar,helperbot=None): self.bumper_bots = bumper_bots self.bumper_clients = bumper_clients + self.remove_clients = remove_clients self.helperbot = helperbot self.usessl = usessl - self.run_async = run_async self.address = address - + self.confthread = None + + def run(self, run_async=False): try: if run_async: confserverlog.debug("Starting ConfServer Thread: 1") - confserver = Thread(name="ConfServer_Thread",target=self.run_server) - self.server = confserver - confserver.setDaemon(True) - confserver.start() + self.confthread = Thread(name="ConfServer_{}_Thread".format(self.address[1]),target=self.run_server) + self.confthread.setDaemon(True) + self.confthread.start() else: try: self.run_server() except KeyboardInterrupt: self.disconnect() + except Exception as e: confserverlog.exception('{}'.format(e)) def run_server(self): + logging.info("Starting ConfServer at {}".format(self.address)) + print("Starting ConfServer at {}".format(self.address)) try: loop = asyncio.get_event_loop() except: @@ -76,6 +80,7 @@ class ConfServer(): app = web.Application() app.add_routes([ + web.get('', self.handle_base), web.get('/{apiversion}/private/{country}/{language}/{devid}/{apptype}/{appversion}/{devtype}/{aid}/user/login', self.handle_login), # web.get('/{apiversion}/private/{country}/{language}/{devid}/{apptype}/{appversion}/{devtype}/{aid}/user/checkLogin', self.handle_checkLogin), web.get('/{apiversion}/private/{country}/{language}/{devid}/{apptype}/{appversion}/{devtype}/{aid}/user/logout', self.handle_logout), @@ -115,6 +120,16 @@ class ConfServer(): confserverlog.exception('{}'.format(e)) exit(1) + async def handle_base(self, request): + try: + + text = "Bumper!" + + return web.json_response(text) + + except Exception as e: + confserverlog.exception('{}'.format(e)) + async def handle_login(self, request): try: #Could implement basic auth if you wanted, or just accept anything @@ -162,8 +177,14 @@ class ConfServer(): async def handle_logout(self, request): try: + uid = request.query['uid'] body = {"code": "0000","data": None,"msg": "操作成功", "time": bumper.get_milli_time(time.time())} - #TODO - when logging out close out any other connections MQTT/XMPP + # QUERY String + # 'uid=fuid_CUOVIn&accessToken=tempaccesstoken&requestId=e584de79f9cca854df6fb3352c6893b6&authTimespan=1550168440575&authTimeZone=GMT-5&authAppkey=eJUWrzRv34qFSaYk&authSign=14e38f95c7316e111c5815cf15f0f972' + if not uid == "": + remove_clients = self.remove_clients.get() + remove_clients.append(request.query['uid']) + self.remove_clients.set(remove_clients) return web.json_response(body) @@ -341,9 +362,9 @@ class ConfServer(): try: confserverlog.info('shutting down') if(self.run_async): - self.server.join() + self.confthread.join() else: - self.server.disconnect() + self.confthread.disconnect() except Exception as e: confserverlog.exception('{}'.format(e)) \ No newline at end of file diff --git a/bumper/mqttserver.py b/bumper/mqttserver.py index 4d97fbc..39e610b 100644 --- a/bumper/mqttserver.py +++ b/bumper/mqttserver.py @@ -28,28 +28,28 @@ logging.getLogger("hbmqtt.client").setLevel(logging.CRITICAL + 1) #Ignore this l class MQTTHelperBot(): Client = MQTTClient() - def __init__(self, address, run_async=False, bumper_bots=contextvars.ContextVar, bumper_clients=contextvars.ContextVar): - + def __init__(self, address, bumper_bots=contextvars.ContextVar, bumper_clients=contextvars.ContextVar): self.address = address self.client_id = "helper1@bumper/helper1" self.command_responses = contextvars.ContextVar('command_responses', default=[]) - - try: - if run_async: + self.helperthread = None + + + def run(self, run_async=False): + if run_async: hloop = asyncio.new_event_loop() helperbotlog.debug("Starting MQTT HelperBot Thread: 1") - helperbot = Thread(name="MQTTHelperBot_Thread",target=self.run_helperbot, args=(hloop,)) - helperbot.setDaemon(True) - helperbot.start() + self.helperthread = Thread(name="MQTTHelperBot_Thread",target=self.run_helperbot, args=(hloop,)) + self.helperthread.setDaemon(True) + self.helperthread.start() - else: - self.run_helperbot() + else: + self.run_helperbot() - except Exception as e: - helperbotlog.exception('{}'.format(e)) - pass - def run_helperbot(self, loop): + def run_helperbot(self, loop): + logging.info("Starting MQTT HelperBot") + print("Starting MQTT HelperBot") try: asyncio.set_event_loop(loop) self.Client = MQTTClient(client_id=self.client_id, config={'check_hostname':False}) @@ -77,7 +77,10 @@ class MQTTHelperBot(): message = await self.Client.deliver_message() #helperbotlog.debug("HelperBot MQTT Received Message on Topic: {} - Message: {}".format(message.topic, str(message.payload.decode("utf-8")))) - cresp = self.command_responses.get() + cresp = self.command_responses.get() + + if (str(message.topic).split("/")[6] == "helper1"): + cresp.append({"time": time.time() ,"topic": message.topic,"payload":str(message.data.decode("utf-8"))}) #Cleanup "expired messages" > 60 seconds from time for msg in cresp: @@ -85,8 +88,7 @@ class MQTTHelperBot(): if time.time() > expire_time: #helperbotlog.debug("Pruning Message Time: {}, MsgTime: {}, MsgTime+60: {}".format(time.time(), msg['time'], expire_time)) cresp.remove(msg) - - cresp.append({"time": time.time() ,"topic": message.topic,"payload":str(message.data.decode("utf-8"))}) + self.command_responses.set(cresp) #helperbotlog.debug("MQTT Command Response List Count: %s" %len(cresp)) @@ -95,7 +97,9 @@ class MQTTHelperBot(): async def wait_for_resp(self, requestid): try: + t_end = (datetime.now() + timedelta(seconds=10)).timestamp() + while time.time() < t_end: await asyncio.sleep(0.1) responses = self.command_responses.get() @@ -118,6 +122,7 @@ class MQTTHelperBot(): self.command_responses.set(cresp) return resp + return { "id": requestid, "errno": "timeout", "ret": "fail" } except asyncio.CancelledError as e: helperbotlog.debug('wait_for_resp cancelled by asyncio') @@ -169,15 +174,21 @@ class MQTTServer(): except Exception as e: mqttserverlog.exception('{}'.format(e)) - def __init__(self, address, run_async=False, bumper_bots=contextvars.ContextVar, bumper_clients=contextvars.ContextVar): + def __init__(self, address, bumper_bots=contextvars.ContextVar, bumper_clients=contextvars.ContextVar,remove_clients=contextvars.ContextVar): try: + + self.bumper_bots = bumper_bots + self.bumper_clients = bumper_clients + self.remove_clients = remove_clients + self.mqttserverthread = None + self.address = address + #The below adds a plugin to the hbmqtt.broker.plugins without having to futz with setup.py distribution = pkg_resources.Distribution("hbmqtt.broker.plugins") - bumper_plugin = pkg_resources.EntryPoint.parse('bumper = bumper.mqttserver:BumperMQTTServer_Plugin', dist=distribution) + bumper_plugin = pkg_resources.EntryPoint.parse('bumper = bumper.mqttserver:BumperMQTTServer_Plugin', dist=distribution) distribution._ep_map = {"hbmqtt.broker.plugins": {"bumper": bumper_plugin}} pkg_resources.working_set.add(distribution) - self.bumper_bots = bumper_bots - self.bumper_clients = bumper_clients + # Initialize bot server self.default_config = { 'listeners': { @@ -203,26 +214,32 @@ class MQTTServer(): 'enabled': False }, 'clients':{ - 'connected_bots': bumper_bots, - 'connected_clients': bumper_clients - - } - } - if run_async: - sloop = asyncio.new_event_loop() - mqttserverlog.debug("Starting MQTTServer Thread: 1") - mqttserver = Thread(name="MQTTServer_Thread",target=self.run_server, args=(sloop,)) - mqttserver.setDaemon(True) - mqttserver.start() - - else: - self.run_server() + 'connected_bots': self.bumper_bots, + 'connected_clients': self.bumper_clients, + 'remove_clients': self.remove_clients + }, + } except Exception as e: mqttserverlog.exception('{}'.format(e)) - - def run_server(self, loop): + def run(self, run_async=False,): + + if run_async: + sloop = asyncio.new_event_loop() + mqttserverlog.debug("Starting MQTTServer Thread: 1") + self.mqttserverthread = Thread(name="MQTTServer_Thread",target=self.run_server, args=(sloop,)) + self.mqttserverthread.setDaemon(True) + self.mqttserverthread.start() + + else: + self.run_server() + + + def run_server(self, loop): + + logging.info("Starting MQTT Server at {}".format(self.address)) + print("Starting MQTT Server at {}".format(self.address)) try: asyncio.set_event_loop(loop) loop.run_until_complete(self.broker_coro()) @@ -237,6 +254,7 @@ class BumperMQTTServer_Plugin: self.context = context try: self.clients = self.context.config['clients'] + except KeyError: self.context.logger.warning("'clients' section not found in context configuration") except Exception as e: @@ -277,7 +295,7 @@ class BumperMQTTServer_Plugin: if client['userid'] == newuser.userid: clientactive = True - if clientactive == False: + if clientactive == False and newuser.userid != 'helper1': connected_clients.append(newuser.asdict()) mqttserverlog.info("new client {}".format(newuser.userid)) @@ -295,20 +313,25 @@ class BumperMQTTServer_Plugin: try: #mqttserverlog.debug('%s disconnected' % client_id) connected_bots = self.clients['connected_bots'].get() - connected_clients = self.clients['connected_clients'].get() + connected_clients = self.clients['connected_clients'].get() + remove_clients = self.clients['remove_clients'].get() didsplit = str(client_id).split("@") #If the did is in the list, remove it for bot in connected_bots: if didsplit[0] == bot['did']: mqttserverlog.info("bot disconnected {}".format(bot['did'])) connected_bots.remove(bot) - self.clients['connected_bots'].set(connected_bots) + #remove_clients.append(bot['did']) + self.clients['connected_bots'].set(connected_bots) for client in connected_clients: - if didsplit[0] == client['userid']: - mqttserverlog.info("client disconnected {}".format(client['userid'])) + if didsplit[0] == client['userid'] and client['userid'] != 'helper1': + mqttserverlog.info("client disconnected {}".format(client['userid'])) connected_clients.remove(client) + #remove_clients.append(client['userid']) self.clients['connected_clients'].set(connected_clients) + + #self.clients['remove_clients'].set(remove_clients) #mqttserverlog.debug('Connected Bots: %s' %self.clients['connected_bots'].get()) #mqttserverlog.debug('Connected Clients: %s' %self.clients['connected_clients'].get()) diff --git a/bumper/xmppserver.py b/bumper/xmppserver.py index f149d66..e0a3228 100644 --- a/bumper/xmppserver.py +++ b/bumper/xmppserver.py @@ -1,6 +1,10 @@ #!/usr/bin/env python3 +from threading import Thread import sys, socket, threading, re, time, logging, uuid, xml.etree.ElementTree as ET +import base64 +import ssl +import bumper xmppserverlog = logging.getLogger("xmppserver") @@ -12,51 +16,115 @@ class XMPPServer(): exit_flag = False def __init__(self, address): + # Initialize bot server + self.address = address + + + def run(self, run_async=False): + if run_async: + xmppserverlog.debug("Starting XMPPServer Thread: 1") + self.xmppthread = Thread(name="XMPPServer_Thread",target=self.run_server) + self.xmppthread.setDaemon(True) + self.xmppthread.start() + + else: + try: + self.run_server() + except KeyboardInterrupt: + self.disconnect() + + def run_server(self): + logging.info("Starting XMPP Server at {}".format(self.address)) + print("Starting XMPP Server at {}".format(self.address)) + + #xmppserverlog.setLevel(logging.DEBUG) + + #Set SSL Context + self.ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + self.ssl_ctx.load_cert_chain(certfile=bumper.server_cert,keyfile=bumper.server_key) + + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: - # Initialize bot server - self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.socket.bind(address) - self.socket.listen(1) - xmppserverlog.debug('listening on {}:{}'.format(address[0], address[1])) - while not self.exit_flag: + self.socket.bind(self.address) + self.socket.listen(5) + + xmppserverlog.debug('listening on {}:{}'.format(self.address[0], self.address[1])) + while not self.exit_flag: connection, client_address = self.socket.accept() + # disconnect any clients with this ip for client in self.clients: if client.address == client_address[0]: - client.disconnect() - client.join() - thread_id = uuid.uuid4() - client = Client(thread_id, connection, client_address) + xmppserverlog.debug('disconnecting existing client {} with resource {}'.format(client.address, client.clientresource)) + client._disconnect() + self.remove_client_byip(client.address) + xmppserverlog.debug('starting new client with ip {}'.format(client_address[0])) + thread_id = uuid.uuid4() + client = Client(thread_id, connection, client_address) + client.setDaemon(True) client.start() - self.clients.append(client) - self.socket.close() + self.clients.append(client) + except PermissionError as e: if "bind" in e.strerror: xmppserverlog.exception("Error binding XMPPServer, exiting. Try using a different hostname or IP - {}".format(e)) - exit(1) + exit(1) + except Exception as e: xmppserverlog.exception('{}'.format(e)) exit(1) + except KeyboardInterrupt as e: xmppserverlog.exception('{}'.format(e)) + finally: + connection.shutdown(socket.SHUT_RDWR) + connection.close() self.disconnect() - xmppserverlog.info('disconnecting') + xmppserverlog.info('disconnecting') + + self.socket.close() + + def disconnect(self): try: xmppserverlog.debug('waiting for all client threads to exit') for client in self.clients: - client.disconnect() - client.join() + client._disconnect() + self.exit_flag = True xmppserverlog.debug('shutting down') + except Exception as e: xmppserverlog.exception("{}".format(e)) + def remove_client_byip(self, ip): + for client in self.clients: + if client.address == ip: + xmppserverlog.debug('removing client from client list with ip {} and resource {}'.format(client.address, client.clientresource)) + client._disconnect() + self.clients.remove(client) + + def remove_client_byresource(self, resource): + for client in self.clients: + if str(client.clientresource).lower() == str(resource).lower(): + xmppserverlog.debug('removing client from client list with ip {} and resource {}'.format(client.address, client.clientresource)) + client._disconnect() + self.clients.remove(client) + + def remove_client_byuid(self, uid): + for client in self.clients: + if str(client.uid).lower() == str(uid).lower(): + xmppserverlog.debug('removing client from client list with ip {} and resource {}'.format(client.address, client.clientresource)) + client._disconnect() + self.clients.remove(client) + + class Client(threading.Thread): IDLE = 0 CONNECT = 1 @@ -68,32 +136,47 @@ class Client(threading.Thread): BOT = 1 CONTROLLER = 2 - def __init__(self, thread_id, connection, client_address): + def __init__(self, thread_id, connection, client_address): threading.Thread.__init__(self) self.id = thread_id - self.name = "XMPP Thread {}".format(client_address[0]) + self.name = "XMPP_Client_{}".format(client_address[0]) self.type = self.UNKNOWN self.state = self.IDLE self.connection = connection self.address = client_address[0] self.clientresource = "" + self.uid = "" + self.log_sent_message = False #Set to true to log sends + self.log_incoming_data = True #Set to true to log sends + + xmppserverlog.debug('new client thread init for client with ip {}'.format(self.address)) def send(self, command): try: - xmppserverlog.debug('send {} - {}'.format(self.address, command)) - self.connection.send(command.encode()) + if not self.connection._closed: + if self.log_sent_message: + xmppserverlog.debug('send {} - {}'.format(self.address, command)) + self.connection.send(command.encode()) + except OSError as e: - xmppserverlog.error('{}'.format(e)) + xmppserverlog.error('{}'.format(e)) + + except BrokenPipeError as e: + xmppserverlog.error('{}'.format(e)) + #self._set_state('DISCONNECT') + + except ConnectionResetError as e: + xmppserverlog.error('{}'.format(e)) + #self._set_state('DISCONNECT') + except Exception as e: xmppserverlog.exception("{}".format(e)) + - - - def disconnect(self): + def _disconnect(self): try: xmppserverlog.debug('client {} with resource {} disconnecting'.format(self.address, self.clientresource)) self.connection.close() - self._set_state('DISCONNECT') except Exception as e: xmppserverlog.exception("{}".format(e)) @@ -103,6 +186,7 @@ class Client(threading.Thread): if tag[0] == '{': uri, ignore, tag = tag[1:].partition('}') return tag + except Exception as e: xmppserverlog.exception("{}".format(e)) @@ -111,21 +195,40 @@ class Client(threading.Thread): new_state = getattr(Client, state) if self.state > new_state: raise Exception('{} illegal state change {}->{}'.format(self.address, self.state, new_state)) + xmppserverlog.debug('{} state: {}'.format(self.address, state)) + self.state = new_state - if new_state == '5': - self.join() + + if new_state == 5: + self._disconnect() + except Exception as e: xmppserverlog.exception("{}".format(e)) - def _handle_ctl(self, xml, data): + def _handle_ctl(self, xml, data): try: - ctl = xml[0][0] - if ctl.get('admin') and self.type == self.BOT: - xmppserverlog.debug('admin username received from bot: {}'.format(ctl.get('admin'))) - XMPPServer.client_id = ctl.get('admin') + + if data.decode('utf-8').find('roster') > -1: + #Return not-implemented for roster + self.send(''.format(xml.get('id'))) return - # forward + + if xml.get('type') == 'set': + if data.decode('utf-8').find('com:sf') > -1 and xml.get('to') == 'rl.ecorobot.net': #Android bind? Not sure what this does yet. + self.send(''.format(xml.get('id'), self.uid, XMPPServer.bot_id, self.clientresource)) + + else: + xmppserverlog.debug('Unknown set type: {}'.format(data.decode('utf-8'))) + + if xml[0][0]: + ctl = xml[0][0] + if ctl.get('admin') and self.type == self.BOT: + xmppserverlog.debug('admin username received from bot: {}'.format(ctl.get('admin'))) + XMPPServer.client_id = ctl.get('admin') + return + + #forward for client in XMPPServer.clients: if client.address != self.address and client.state == client.READY: if client.type == self.BOT: @@ -135,106 +238,298 @@ class Client(threading.Thread): data = data[:id_index] + 'from="' + XMPPServer.client_id + '" ' + data[id_index:] data = data.encode() client.send(data.decode('utf-8')) + except Exception as e: xmppserverlog.exception("{}".format(e)) def _handle_ping(self, xml, data): - if(xml.get('to').find('@') == -1): - # Ping to server - respond - self.send(''.format(xml.get('id'), xml.get('to'))) - else: - for client in XMPPServer.clients: - if client.address != self.address and client.state == client.READY: - client.send(data.decode('utf-8')) + try: + if(xml.get('to').find('@') == -1): + # Ping to server - respond + self.send(''.format(xml.get('id'), xml.get('to'))) - def _handle_result(self, data): + else: + for client in XMPPServer.clients: + if client.address != self.address and client.state == client.READY: + client.send(data.decode('utf-8')) + + except Exception as e: + xmppserverlog.exception('{}'.format(e)) + + def _handle_result(self, data): # forward try: for client in XMPPServer.clients: if client.address != self.address and client.state == client.READY: client.send(data.decode('utf-8')) + except Exception as e: - xmppserverlog.exception("{}".format(e)) + xmppserverlog.exception("{}".format(e)) + - def run(self): + def _handle_connect(self, data): try: - xmppserverlog.info('client connected - {}'.format(self.address)) - self._set_state('CONNECT') - data = "" - while True: - time.sleep(0.2) - if not self.connection._closed: - try: - data = self.connection.recv(4096) - except ConnectionResetError as e: - xmppserverlog.error('{}'.format(e)) - except Exception as e: - xmppserverlog.exception('{}'.format(e)) - if data: - xmppserverlog.debug('from {} - {}'.format(self.address, data.decode('utf-8'))) - try: - if self.state == self.CONNECT: - if data.decode('utf-8').find('jabber:client') > -1: - self._set_state('INIT') - # ack jabbr:client - self.send(''.format(XMPPServer.server_id)) - time.sleep(0.5) - # session - self.send('') - continue - xml = ET.fromstring(data) - if len(xml): - child = self._tag_strip_uri(xml[0].tag) - else: - child = None - if xml.tag == 'iq': - res = None - if child == 'bind': - clientbindxml = xml.getchildren() - clientresourcexml = clientbindxml[0].getchildren() - self.clientresource = clientresourcexml[0].text - self.name = "XMPP Thread {}".format(self.clientresource) - xmppserverlog.debug("new client {} using resource {}".format(self.address, self.clientresource)) - res = '{}'.format(xml.get('id'), XMPPServer.bot_id) - self._set_state('BIND') - elif child == 'session': - res = ''.format(xml.get('id')) - self._set_state('READY') - elif child == 'query': - self._handle_ctl(xml, data) - elif child == 'ping': - self._handle_ping(xml, data) - elif xml.get('type') == 'result': - self._handle_result(data) - if res: - self.send(res) - elif xml.tag == 'presence': - if len(xml) and xml[0].tag == 'status': - # bot announcing arrival - self.type = self.BOT - xmppserverlog.debug('{} type set to BOT (based on presence tag)'.format(self.address)) - # send a command from an unknown user - the response will contain the correct admin username - self.send(''.format(uuid.uuid4(), 'unknown@ecouser.net', XMPPServer.bot_id)) - elif xml.get('type') == 'available': - self.type = self.CONTROLLER - xmppserverlog.debug('{} type set to CONTROLLER (based on presence tag)'.format(self.address)) - except ET.ParseError as e: - xmppserverlog.debug('parse error - {}'.format(e)) - except Exception as e: - xmppserverlog.exception('{}'.format(e)) - self._set_state('DISCONNECT') - except OSError as e: - xmppserverlog.error('{}'.format(e)) - self._set_state('DISCONNECT') + + if self.state == self.CONNECT: + #Client first connecting, send our features - except ConnectionResetError as e: - xmppserverlog.error('{}'.format(e)) - self._set_state('DISCONNECT') - + if data.decode('utf-8').find('jabber:client') > -1: + # ack jabbr:client + #no STARTTLS + self.send(''.format(XMPPServer.server_id)) + #with STARTTLS + #self.send(''.format(XMPPServer.server_id)) + time.sleep(0.25) + # send authentication support for iq-auth (fallback) and SASL + self.send('PLAIN') + #self.send('') + + elif data.decode('utf-8').find('jabber:iq:auth') > -1: #Handle iq-auth + self._handle_iq_auth(data) + + elif data.decode('utf-8').find('urn:ietf:params:xml:ns:xmpp-sasl') > -1: #Handle SASL auth + self._handle_sasl_auth(data) + + elif self.state == self.INIT: + #Client getting session after authentication + if data.decode('utf-8').find('jabber:client') > -1: + # ack jabbr:client + self.send(''.format(XMPPServer.server_id)) + time.sleep(0.25) + # session + self.send('') + + else: #Handle init bind + xml = ET.fromstring(data.decode('utf-8')) + if len(xml): + child = self._tag_strip_uri(xml[0].tag) + else: + child = None + + if xml.tag == 'iq': + if child == 'bind': + self._handle_bind(xml) + except Exception as e: xmppserverlog.exception('{}'.format(e)) - self._set_state('DISCONNECT') + + + def _handle_iq_auth(self, data): + try: + xml = ET.fromstring(data.decode('utf-8')) + ctl = xml[0][0] + xmppserverlog.info("IQ AUTH XML: {}".format(xml)) + #Received username and auth tag, send username/password requirement + if xml.get('type') == 'get' and "auth}username" in ctl.tag and self.type == self.UNKNOWN: + self.send(''.format(xml.get('id'))) + + #Received username, password, resource - Handle auth here and return pass or fail + if xml.get('type') == 'set' and "auth}username" in ctl.tag and self.type == self.UNKNOWN: + xmlauth = xml[0].getchildren() + uid = '' + password = '' + resource = '' + for aitem in xmlauth: + if 'username' in aitem.tag: + self.uid = aitem.text - finally: - self.disconnect() + elif 'password' in aitem.tag: + password = aitem.text.split("/")[2] + + elif 'resource' in aitem.tag: + self.clientresource = aitem.text + + if True: + #Client authenticated, move to next state + self._set_state('INIT') + + #Successful auth + self.send(''.format(xml.get('id'))) + + else: + #Failed auth + self.send(''.format(xml.get('id'))) + + + + except ET.ParseError as e: + if "no element found" in e.msg: + xmppserverlog.debug('xml parse error - {} - {} - this is common with ecovac protocol'.format(data.decode('utf-8'), e)) + elif "not well-formed (invalid token)" in e.msg: + xmppserverlog.debug('xml parse error - {} - {}'.format(data.decode('utf-8'), e)) + else: + xmppserverlog.debug('xml parse error - {} - {}'.format(data.decode('utf-8'), e)) + + except Exception as e: + xmppserverlog.exception('{}'.format(e)) + + + def _handle_sasl_auth(self, data): + try: + xml = ET.fromstring(data.decode('utf-8')) + saslauth = base64.b64decode(xml.text).decode('utf-8').split("/") + username = saslauth[0] + username = saslauth[0].split('\x00')[1] + self.uid = username + resource = saslauth[1] + self.clientresource = resource + authcode = saslauth[2] + + if authcode == "us_tempauthcode": #Handle auth + #Send response + self.send('') #Success + + #Client authenticated, move to next state + self._set_state('INIT') + + else: + #Failed to authenticate + self.send('') #Fail + + except ET.ParseError as e: + if "no element found" in e.msg: + xmppserverlog.debug('xml parse error - {} - {} - this is common with ecovac protocol'.format(data.decode('utf-8'), e)) + elif "not well-formed (invalid token)" in e.msg: + xmppserverlog.debug('xml parse error - {} - {}'.format(data.decode('utf-8'), e)) + else: + xmppserverlog.debug('xml parse error - {} - {}'.format(data.decode('utf-8'), e)) + + except Exception as e: + xmppserverlog.exception('{}'.format(e)) + + def _handle_bind(self, xml): + try: + clientbindxml = xml.getchildren() + clientresourcexml = clientbindxml[0].getchildren() + if len(clientresourcexml) > 0: + self.clientresource = clientresourcexml[0].text + self.name = "XMPP_Client_{}".format(self.clientresource) + xmppserverlog.debug("new client {} using resource {}".format(self.address, self.clientresource)) + res = '{}@{}/{}'.format(xml.get('id'), self.uid, XMPPServer.bot_id, self.clientresource) + else: + xmppserverlog.debug("new client {}".format(self.address)) + res = '{}'.format(xml.get('id'), XMPPServer.bot_id) + + self._set_state('BIND') + self.send(res) + + except Exception as e: + xmppserverlog.exception('{}'.format(e)) + + def _handle_session(self, xml): + try: + res = ''.format(xml.get('id')) + self._set_state('READY') + self.send(res) + + except Exception as e: + xmppserverlog.exception('{}'.format(e)) + + def _handle_presence(self, xml): + try: + if len(xml) and xml[0].tag == 'status': + # bot announcing arrival + self.type = self.BOT + xmppserverlog.debug('{} type set to BOT (based on presence tag)'.format(self.address)) + # send a command from an unknown user - the response will contain the correct admin username + + self.send(''.format(uuid.uuid4(), 'unknown@ecouser.net', XMPPServer.bot_id)) + + else: + self.type = self.CONTROLLER + xmppserverlog.debug('{} type set to CONTROLLER (based on presence tag)'.format(self.address)) + self.send(' dummy '.format(self.uid, XMPPServer.bot_id, self.clientresource)) + + except Exception as e: + xmppserverlog.exception('{}'.format(e)) + + def _parse_data(self, data): + if self.log_incoming_data: + xmppserverlog.debug('from {} - {}'.format(self.address, data.decode('utf-8'))) + + try: + xml = ET.fromstring(data.decode('utf-8')) + self._handle_xml(xml, data) + + except ET.ParseError as e: + if "no element found" in e.msg: #Element not closed or not all bytes received + #Happens wth connect stream often + if ' - client is signalling end of session/disconnect + if not '' in data.decode('utf-8'): + xmppserverlog.error('xml parse error - {} - {}'.format(data.decode('utf-8'), e)) + else: + self.send("") #Close stream + + elif "junk after document element" in e.msg: #More than one xml doc in data + #try to split it + data0 = data.decode('utf-8') + data1 = data0[e.position[1]:] + data0 = data0[:e.position[1]] + #xmppserverlog.debug('xml parse error - {} - {} - split0: {} - split1: {}'.format(data.decode('utf-8'), e, data0, data1)) + self._parse_data(data0.encode('utf-8')) + self._parse_data(data1.encode('utf-8')) + + else: + xmppserverlog.debug('xml parse error - {} - {}'.format(data.decode('utf-8'), e)) + + except Exception as e: + xmppserverlog.exception('{}'.format(e)) + + def _handle_xml(self, xml, data): + try: + if self.state == self.CONNECT or self.state == self.INIT: + self._handle_connect(data) + + if len(xml): + child = self._tag_strip_uri(xml[0].tag) + else: + child = None + + if xml.tag == 'iq': + if child == 'bind': + self._handle_bind(xml) + elif child == 'session': + self._handle_session(xml) + elif child == 'query': + self._handle_ctl(xml, data) + elif child == 'ping': + self._handle_ping(xml, data) + elif xml.get('type') == 'result': + self._handle_result(data) + elif xml.tag == 'presence': + self._handle_presence(xml) + + except Exception as e: + xmppserverlog.exception('{}'.format(e)) + + + def run(self): + xmppserverlog.info('client connected - {}'.format(self.address)) + self._set_state('CONNECT') + + while not self.state == self.DISCONNECT and not self.connection._closed: + data = b'' + + time.sleep(0.2) + if not self.connection._closed: + try: + data = self.connection.recv(4096) + + except ConnectionResetError as e: + xmppserverlog.error('{}'.format(e)) + except OSError as e: + xmppserverlog.error('{}'.format(e)) + except Exception as e: + xmppserverlog.exception('{}'.format(e)) + + if data != b'': + self._parse_data(data) + \ No newline at end of file