#!/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 contextvars import bumper xmppserverlog = logging.getLogger("xmppserver") class XMPPServer: server_id = "ecouser.net" client_id = None clients = [] 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: 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]: 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) 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) 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") self.socket.close() def disconnect(self): try: xmppserverlog.debug("waiting for all client threads to exit") for client in self.clients: 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 INIT = 2 BIND = 3 READY = 4 DISCONNECT = 5 UNKNOWN = 0 BOT = 1 CONTROLLER = 2 def __init__(self, thread_id, connection, client_address): threading.Thread.__init__(self) self.id = thread_id 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.devclass = "" self.bumper_jid = "" 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: if not self.connection._closed: if self.log_sent_message: xmppserverlog.debug("send {} - {}".format(self.address, command)) self.connection.send(command.encode()) except BrokenPipeError as e: xmppserverlog.debug("{}".format(e)) self._set_state("DISCONNECT") except ConnectionResetError as e: xmppserverlog.debug("{}".format(e)) self._set_state("DISCONNECT") except ConnectionAbortedError as e: xmppserverlog.debug("{}".format(e)) self._set_state("DISCONNECT") except OSError as e: xmppserverlog.debug("{}".format(e)) except Exception as e: xmppserverlog.exception("{}".format(e)) def _disconnect(self): try: bot = bumper.bot_get(self.uid) if bot: bumper.bot_set_xmpp(bot["did"], False) client = bumper.client_get(self.clientresource) if client: bumper.client_set_xmpp(client["resource"], False) self.connection.close() except Exception as e: xmppserverlog.exception("{}".format(e)) def _tag_strip_uri(self, tag): try: if tag[0] == "{": _, _, tag = tag[1:].partition("}") return tag except Exception as e: xmppserverlog.exception("{}".format(e)) def _set_state(self, state): try: 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._disconnect() except Exception as e: xmppserverlog.exception("{}".format(e)) def _handle_ctl(self, xml, data): try: if "roster" in data: # Return not-implemented for roster self.send( ''.format( xml.get("id") ) ) return if xml.get("type") == "set": if ( "com:sf" in data and xml.get("to") == "rl.ecorobot.net" ): # Android bind? Not sure what this does yet. self.send( ''.format( xml.get("id"), self.uid, XMPPServer.server_id, self.clientresource, ) ) 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.bumper_jid != self.bumper_jid and client.state == client.READY ): ctl_to = xml.get("to") xml.attrib["from"] = "{}".format(self.bumper_jid) rxmlstring = ET.tostring(xml).decode("utf-8") # clean up string to remove namespaces added by ET rxmlstring = rxmlstring.replace("xmlns:ns0=", "xmlns=") rxmlstring = rxmlstring.replace("ns0:", "") rxmlstring = rxmlstring.replace('iq xmlns="com:ctl"', "iq") rxmlstring = rxmlstring.replace("'.format( uuid.uuid4(), adminuser, self.bumper_jid, newuser ) xmppserverlog.debug("Add User: {}".format(adduser)) self.send(adduser) # Add user ACs - Manage users, settings, and clean (full access) adduseracs = ''.format( uuid.uuid4(), adminuser, self.bumper_jid, newuser ) xmppserverlog.debug("Add User ACs: {}".format(adduseracs)) self.send(adduseracs) # GetUserInfo - Just to confirm it set correctly self.send( ''.format( uuid.uuid4(), adminuser, self.bumper_jid ) ) else: rxmlstring = ET.tostring(xml).decode("utf-8") # clean up string to remove namespaces added by ET rxmlstring = rxmlstring.replace("xmlns:ns0=", "xmlns=") rxmlstring = rxmlstring.replace("ns0:", "") rxmlstring = rxmlstring.replace('iq xmlns="com:ctl"', "iq") rxmlstring = rxmlstring.replace(" -1: sc = data.decode("utf-8").find("to=") ec = data.decode("utf-8").find(".ecorobot.net") if ec > -1: self.devclass = data.decode("utf-8")[sc + 4 : ec] # 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('') else: self.send("") else: if "jabber:iq:auth" in xml.tag: # Handle iq-auth self._handle_iq_auth(xml) elif ( "urn:ietf:params:xml:ns:xmpp-sasl" in xml.tag ): # Handle SASL Auth self._handle_sasl_auth(xml) else: xmppserverlog.error("Couldn't handle: {}".format(xml)) elif self.state == self.INIT: if xml == None: # 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 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) else: xmppserverlog.error("Couldn't handle: {}".format(xml)) except Exception as e: xmppserverlog.exception("{}".format(e)) 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 elif "password" in aitem.tag: password = aitem.text.split("/")[2] authcode = password elif "resource" in aitem.tag: self.clientresource = aitem.text resource = self.clientresource if not self.uid.startswith("fuid"): # Need sample data to see details here bumper.bot_add("", self.uid, "", resource, "eco-legacy") xmppserverlog.info("bot authenticated {}".format(self.uid)) # Client authenticated, move to next state self._set_state("INIT") # Successful auth self.send(''.format(xml.get("id"))) else: auth = False if bumper.check_authcode(self.uid, authcode): auth = True elif bumper.use_auth == False: auth = True if auth: bumper.client_add(self.uid, "bumper", self.clientresource) xmppserverlog.debug("client authenticated {}".format(self.uid)) # 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 - {} - {}".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, xml): try: saslauth = base64.b64decode(xml.text).decode("utf-8").split("/") username = saslauth[0] username = saslauth[0].split("\x00")[1] self.uid = username if len(saslauth) > 1: resource = saslauth[1] self.clientresource = resource elif len(saslauth[0].split("\x00")) > 2: resource = saslauth[0].split("\x00")[2] self.clientresource = resource if len(saslauth) > 2: authcode = saslauth[2] if not self.uid.startswith("fuid"): # Need sample data to see details here bumper.bot_add(self.uid, self.uid, self.devclass, "atom", "eco-legacy") self.type = self.BOT xmppserverlog.info("bot authenticated {}".format(self.uid)) # Send response self.send( '' ) # Success # Client authenticated, move to next state self._set_state("INIT") else: auth = False if bumper.check_authcode(self.uid, authcode): auth = True elif bumper.use_auth == False: auth = True if auth: self.type = self.CONTROLLER bumper.client_add(self.uid, "bumper", self.clientresource) xmppserverlog.debug("client authenticated {}".format(self.uid)) # Client authenticated, move to next state self._set_state("INIT") # Send response self.send( '' ) # Success else: # Failed to authenticate self.send( '' ) # Fail except Exception as e: xmppserverlog.exception("{}".format(e)) def _handle_bind(self, xml): try: bot = bumper.bot_get(self.uid) if bot: bumper.bot_set_xmpp(bot["did"], True) client = bumper.client_get(self.clientresource) if client: bumper.client_set_xmpp(client["resource"], True) clientbindxml = xml.getchildren() clientresourcexml = clientbindxml[0].getchildren() if self.devclass: # its a bot self.name = "XMPP_Client_{}_{}".format(self.uid, self.devclass) self.bumper_jid = "{}@{}.ecorobot.net/atom".format( self.uid, self.devclass ) xmppserverlog.debug("new bot {}".format(self.uid)) res = '{}'.format( xml.get("id"), self.bumper_jid ) elif len(clientresourcexml) > 0: self.clientresource = clientresourcexml[0].text self.name = "XMPP_Client_{}".format(self.clientresource) self.bumper_jid = "{}@{}/{}".format( self.uid, XMPPServer.server_id, self.clientresource ) xmppserverlog.debug( "new client {} using resource {}".format( self.uid, self.clientresource ) ) res = '{}'.format( xml.get("id"), self.bumper_jid ) else: self.name = "XMPP_Client_{}_{}".format(self.uid, self.address) self.bumper_jid = "{}@{}".format(self.uid, XMPPServer.server_id) xmppserverlog.debug("new client {}".format(self.uid)) res = '{}'.format( xml.get("id"), self.bumper_jid ) 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": xmppserverlog.debug( "bot presence {} ".format(ET.tostring(xml, encoding="utf-8")) ) # Most likely a bot, possibly hello world in text # Send dummy return self.send( ' dummy '.format(self.bumper_jid) ) # If it is a BOT, send extras if self.type == self.BOT: # get device info self.send( ''.format( self.bumper_jid, XMPPServer.server_id ) ) else: xmppserverlog.debug( "client presence - {} ".format(ET.tostring(xml, encoding="utf-8")) ) if xml.get("type") == "available": xmppserverlog.debug( "client presence available - {} ".format( ET.tostring(xml, encoding="utf-8") ) ) # Send dummy return self.send( ' dummy '.format(self.bumper_jid) ) elif xml.get("type") == "unavailable": xmppserverlog.debug( "client presence unavailable (DISCONNECT) - {} ".format( ET.tostring(xml, encoding="utf-8") ) ) self._set_state("DISCONNECT") else: # Sometimes the android app sends these xmppserverlog.debug( "client presence (UNKNOWN) - {} ".format( ET.tostring(xml, encoding="utf-8") ) ) # Send dummy return self.send( ' dummy '.format(self.bumper_jid) ) except Exception as e: xmppserverlog.exception("{}".format(e)) def _parse_data(self, data): if data.decode("utf-8").startswith( "]+\?>)", r"", data.decode("utf-8")) + "" ) else: newdata = "{}".format( data.decode("utf-8") ) # Add artificial root try: root = ET.fromstring(newdata) for item in root.iter(): if item.tag != "root": if item.tag == "iq": if self.log_incoming_data: xmppserverlog.debug( "from {} - {}".format( self.address, str( ET.tostring(item, encoding="utf-8").decode( "utf-8" ) ).replace("ns0:", ""), ) ) self._handle_iq(item, newdata) item.clear() elif "auth" in item.tag: if "urn:ietf:params:xml:ns:xmpp-sasl" in item.tag: # SASL Auth self._handle_sasl_auth(item) item.clear() elif "presence" in item.tag: self._handle_presence(item) item.clear() else: if self.log_incoming_data: xmppserverlog.debug( "Unparsed Item - {}".format( str( ET.tostring(item, encoding="utf-8").decode( "utf-8" ) ).replace("ns0:", "") ) ) 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 newdata: xmppserverlog.error("xml parse error - {} - {}".format(newdata, e)) else: self.send("") # Close stream else: if "" in newdata: xmppserverlog.error( "xml parse error - {} - {}".format(newdata, e) ) else: self.send("") # Close stream self._set_state("DISCONNECT") except Exception as e: xmppserverlog.exception("{}".format(e)) def _handle_iq(self, xml, data): try: 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 == "ping": self._handle_ping(xml, data) elif child == "query": if self.type == self.BOT: self._handle_result(xml, data) else: self._handle_ctl(xml, data) elif xml.get("type") == "result": if self.type == self.BOT: self._handle_result(xml, data) else: self._handle_result(xml, data) elif xml.get("type") == "set": if self.type == self.BOT: self._handle_result(xml, data) else: self._handle_result(xml, data) 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.1) if not self.connection._closed: try: data = self.connection.recv(4096) if data != b"": self._parse_data(data) except ConnectionResetError as e: xmppserverlog.debug("{}".format(e)) except OSError as e: xmppserverlog.debug("{}".format(e)) except Exception as e: xmppserverlog.exception("{}".format(e))