From e97b28a5c8ee114d16245d6890e3d882011d4131 Mon Sep 17 00:00:00 2001 From: Brian Martin Date: Sun, 24 Feb 2019 02:29:40 -0500 Subject: [PATCH 1/5] ozmo xmpp authentication ozmo auths and shows up in the app --- bumper/__init__.py | 5 +++-- bumper/confserver.py | 14 ++++++++------ bumper/mqttserver.py | 2 +- bumper/xmppserver.py | 19 +++++++++++++++---- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/bumper/__init__.py b/bumper/__init__.py index 77e54be..3ae2598 100644 --- a/bumper/__init__.py +++ b/bumper/__init__.py @@ -89,7 +89,7 @@ class VacBotDevice(object): resource="", name="", nick="", - company="eco-ng", + company="", ): self.vac_bot_device_class = vac_bot_device_class self.company = company @@ -132,13 +132,14 @@ def check_authcode(uid, authcode): return False -def add_bot(sn, did, devclass, resource): +def add_bot(sn, did, devclass, resource, company): newbot = VacBotDevice() newbot.did = did newbot.name = sn newbot.vac_bot_device_class = devclass newbot.resource = resource + newbot.company = company bots = bumper_bots_var.get() existingbot = False diff --git a/bumper/confserver.py b/bumper/confserver.py index 70c37d8..62a4cca 100644 --- a/bumper/confserver.py +++ b/bumper/confserver.py @@ -584,6 +584,8 @@ class ConfServer: confserverlog.debug( "\r\n POST: {} \r\n Response: {}".format(postbody, body) ) + + return web.json_response(body) except Exception as e: @@ -606,18 +608,18 @@ class ConfServer: if todo == "FindBest": service = postbody["service"] if service == "EcoMsgNew": - body = { - "result": "ok", - "ip": socket.gethostbyname(socket.gethostname()), - "port": 5223, - } + confserverlog.debug( + "\r\n POST: {} \r\n Response: {}".format(postbody, body) + ) + msgserver = '{"ip":"{}","port":5223,"result":"ok"}'.format(socket.gethostbyname(socket.gethostname())) #bot seems to be very picky about having no spaces, only way was with text + return web.json_response(text=msgserver) elif service == "EcoUpdate": body = {"result": "ok", "ip": "47.88.66.164", "port": 8005} confserverlog.debug( "\r\n POST: {} \r\n Response: {}".format(postbody, body) ) - return web.json_response(body) + return web.json_response(body) except Exception as e: confserverlog.exception("{}".format(e)) diff --git a/bumper/mqttserver.py b/bumper/mqttserver.py index 35285b2..6dc3f10 100644 --- a/bumper/mqttserver.py +++ b/bumper/mqttserver.py @@ -333,7 +333,7 @@ class BumperMQTTServer_Plugin: ): tmpbotdetail = str(didsplit[1]).split("/") bumper.add_bot( - username, didsplit[0], tmpbotdetail[0], tmpbotdetail[1] + username, didsplit[0], tmpbotdetail[0], tmpbotdetail[1], "eco-ng" ) mqttserverlog.debug( "new bot authenticated SN: {} DID: {}".format( diff --git a/bumper/xmppserver.py b/bumper/xmppserver.py index f20081f..366715a 100644 --- a/bumper/xmppserver.py +++ b/bumper/xmppserver.py @@ -193,6 +193,7 @@ class Client(threading.Thread): self.connection = connection self.address = client_address[0] self.clientresource = "" + self.devclass = "" self.uid = "" self.log_sent_message = False # Set to true to log sends self.log_incoming_data = True # Set to true to log sends @@ -372,6 +373,10 @@ class Client(threading.Thread): # Client first connecting, send our features if data.decode("utf-8").find("jabber:client") > -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( @@ -527,13 +532,19 @@ class Client(threading.Thread): username = saslauth[0] username = saslauth[0].split("\x00")[1] self.uid = username - resource = saslauth[1] - self.clientresource = resource - authcode = saslauth[2] + 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.add_bot("", self.uid, "", resource) + bumper.add_bot(self.uid, self.uid, self.devclass, "atom","eco-legacy") xmppserverlog.info("bot authenticated {}".format(self.uid)) # Send response self.send( From 3ec51e754ebfa43832e73fb6a7996f613f1c7318 Mon Sep 17 00:00:00 2001 From: Brian Martin Date: Sun, 24 Feb 2019 21:18:58 -0500 Subject: [PATCH 2/5] ozmo working app works - changed a number of things on XMPP side to support --- bumper/confserver.py | 29 ++++++- bumper/xmppserver.py | 182 +++++++++++++++++++++++++++++++++---------- 2 files changed, 165 insertions(+), 46 deletions(-) diff --git a/bumper/confserver.py b/bumper/confserver.py index 62a4cca..dc6cf22 100644 --- a/bumper/confserver.py +++ b/bumper/confserver.py @@ -581,6 +581,23 @@ class ConfServer: self.bumper_bots.set(bots) body = {"result": "ok", "todo": "result"} + + elif todo == "AddOneDevice": + bots = self.bumper_bots.get() + for bot in bots: + if postbody["did"] == bot.did: + bot.nick = postbody["nick"] + self.bumper_bots.set(bots) + body = {"result": "ok", "todo": "result"} + + elif todo == "DeleteOneDevice": + bots = self.bumper_bots.get() + for bot in bots: + if postbody["did"] == bot.did: + #bots.remove(bot) + #self.bumper_bots.set(bots) + body = {"result": "ok", "todo": "result"} + confserverlog.debug( "\r\n POST: {} \r\n Response: {}".format(postbody, body) ) @@ -608,11 +625,17 @@ class ConfServer: if todo == "FindBest": service = postbody["service"] if service == "EcoMsgNew": + + srvip = socket.gethostbyname(socket.gethostname()) + msgserver = {"ip":srvip,"port":5223,"result":"ok"} + msgserver = json.dumps(msgserver) + msgserver = msgserver.replace(" ","") #bot seems to be very picky about having no spaces, only way was with text + confserverlog.debug( - "\r\n POST: {} \r\n Response: {}".format(postbody, body) - ) - msgserver = '{"ip":"{}","port":5223,"result":"ok"}'.format(socket.gethostbyname(socket.gethostname())) #bot seems to be very picky about having no spaces, only way was with text + "\r\n POST: {} \r\n Response: {}".format(postbody, msgserver) + ) return web.json_response(text=msgserver) + elif service == "EcoUpdate": body = {"result": "ok", "ip": "47.88.66.164", "port": 8005} diff --git a/bumper/xmppserver.py b/bumper/xmppserver.py index 366715a..4deeadf 100644 --- a/bumper/xmppserver.py +++ b/bumper/xmppserver.py @@ -194,6 +194,7 @@ class Client(threading.Thread): 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 @@ -299,15 +300,15 @@ class Client(threading.Thread): ''.format( xml.get("id"), self.uid, - XMPPServer.bot_id, + XMPPServer.server_id, self.clientresource, ) ) - else: - xmppserverlog.debug( - "Unknown set type: {}".format(data.decode("utf-8")) - ) + #else: + # xmppserverlog.debug( + # "Unknown set type: {}".format(data.decode("utf-8")) + # ) if xml[0][0]: ctl = xml[0][0] @@ -320,48 +321,111 @@ class Client(threading.Thread): # forward for client in XMPPServer.clients: - if client.address != self.address and client.state == client.READY: + if client.bumper_jid != self.bumper_jid and client.state == client.READY: + #if client.address != self.address and client.state == client.READY: + ctl_to = xml.get("to") + + #xml.attrib["from"] = self.bumper_jid#.replace("@{}".format(XMPPServer.server_id),"@ecouser.net") + xml.attrib["from"] = "{}@ecouser.net".format(self.uid) + 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: - data = ( - data[:id_index] - + 'from="' - + XMPPServer.client_id - + '" ' - + data[id_index:] - ) - data = data.encode() - client.send(data.decode("utf-8")) + if client.uid.lower() in ctl_to.lower(): + xmppserverlog.info("Sending ctl to bot: {}".format(rxmlstring)) + client.send(rxmlstring) + #client.send(data.decode("utf-8")) + + # data = data.decode("utf-8") + # id_index = data.find("id") + # if id_index > -1: + # 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): try: - if xml.get("to").find("@") == -1: + if xml.get("to").find("@") == -1: #No to address # Ping to server - respond - self.send( - ''.format( + pingresp = ''.format( xml.get("id"), xml.get("to") ) - ) + #xmppserverlog.debug("Server Ping resp: {}".format(pingresp)) + self.send(pingresp) else: + pingto = xml.get("to") + pingfrom = self.bumper_jid + + xml.attrib["from"] = pingfrom + pingstring = ET.tostring(xml).decode("utf-8") + #clean up string to remove namespaces added by ET + pingstring = pingstring.replace("xmlns:ns0=", "xmlns=") + pingstring = pingstring.replace("ns0:", "") + pingstring = pingstring.replace('iq xmlns="com:ctl"', "iq") + pingstring = pingstring.replace(' 0: + 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.address, self.clientresource + self.uid, self.clientresource ) ) - res = '{}@{}/{}'.format( - xml.get("id"), self.uid, XMPPServer.bot_id, self.clientresource + res = '{}'.format( + xml.get("id"), self.bumper_jid ) - else: - xmppserverlog.debug("new client {}".format(self.address)) - res = '{}@{}'.format( - xml.get("id"), self.uid, XMPPServer.bot_id + 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") @@ -658,10 +732,20 @@ class Client(threading.Thread): "{} 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( + ' dummy '.format( + self.bumper_jid + ) + ) + # self.send( + # ''.format( + # self.uid, self.devclass, XMPPServer.server_id + # ) + # ) + self.send( ''.format( - uuid.uuid4(), "unknown@ecouser.net", XMPPServer.bot_id + uuid.uuid4(), "unknown@ecouser.net", XMPPServer.server_id ) ) @@ -674,7 +758,7 @@ class Client(threading.Thread): ) self.send( ' dummy '.format( - self.uid, XMPPServer.bot_id, self.clientresource + self.uid, XMPPServer.server_id, self.clientresource ) ) @@ -748,13 +832,25 @@ class Client(threading.Thread): 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 child == "query": + if self.type == self.BOT: + self._handle_result(xml, data) + else: + self._handle_ctl(xml, data) elif xml.get("type") == "result": - self._handle_result(data) - elif xml.tag == "presence": + 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) + + if xml.tag == "presence": self._handle_presence(xml) except Exception as e: From af29f69a999b9cb4d935d7d5298fabeca8079636 Mon Sep 17 00:00:00 2001 From: Brian Martin Date: Sun, 24 Feb 2019 21:48:37 -0500 Subject: [PATCH 3/5] optimize data increase buffer and split 1: + for s in splitdata: + if not s.startswith(" Date: Wed, 27 Feb 2019 09:05:23 -0500 Subject: [PATCH 4/5] additional optimizations and permission handling Optimizations for handling XMPP data and commands Basic permission handling - Add fuid users to bot - Ozmo has ACLs and an "owner", if bot comes from eco to bumper the fuid_user won't have permissions - Bumper will add the fuid_ user to bot ACLs and grant full - This can be used later when bumper adds proper auth to allow/block users --- bumper/confserver.py | 40 ++-- bumper/xmppserver.py | 423 +++++++++++++++++++++++-------------------- 2 files changed, 249 insertions(+), 214 deletions(-) diff --git a/bumper/confserver.py b/bumper/confserver.py index dc6cf22..6cb58e4 100644 --- a/bumper/confserver.py +++ b/bumper/confserver.py @@ -652,22 +652,32 @@ class ConfServer: json_body = json.loads(await request.text()) randomid = "".join(random.sample(string.ascii_letters, 6)) bots = self.bumper_bots.get() - for bot in bots: - if bot.did == json_body["toId"] and bot.mqtt_connection == True: - retcmd = await self.helperbot.send_command(json_body, randomid) - body = retcmd - confserverlog.debug( - "\r\n POST: {} \r\n Response: {}".format(json_body, body) + if "toId" in json_body: #Its a command + for bot in bots: + if bot.company == 'eco-ng': + if bot.did == json_body["toId"] and bot.mqtt_connection == True: + retcmd = await self.helperbot.send_command(json_body, randomid) + body = retcmd + confserverlog.debug( + "\r\n POST: {} \r\n Response: {}".format(json_body, body) + ) + return web.json_response(body) + + #No response, send error back + confserverlog.error( + "No bots with DID: {} connected to MQTT".format( + json_body["toId"] ) - return web.json_response(body) - else: - confserverlog.error( - "No bots with DID: {} connected to MQTT".format( - json_body["toId"] - ) - ) - body = {"id": randomid, "errno": bumper.ERR_COMMON, "ret": "fail"} - return web.json_response(body) + ) + body = {"id": randomid, "errno": bumper.ERR_COMMON, "ret": "fail"} + return web.json_response(body) + else: + if "td" in json_body: #Seen when doing initial wifi config + if json_body["td"] == "PollSCResult": + body = { + "ret": "ok" + } + return web.json_response(body) except Exception as e: confserverlog.exception("{}".format(e)) diff --git a/bumper/xmppserver.py b/bumper/xmppserver.py index 19f2a0d..78cf78b 100644 --- a/bumper/xmppserver.py +++ b/bumper/xmppserver.py @@ -11,8 +11,7 @@ xmppserverlog = logging.getLogger("xmppserver") class XMPPServer: - server_id = "bumper" - bot_id = "bumpy" + server_id = "ecouser.net" client_id = None clients = [] exit_flag = False @@ -218,7 +217,7 @@ class Client(threading.Thread): except BrokenPipeError as e: xmppserverlog.error("{}".format(e)) - # self._set_state('DISCONNECT') + self._set_state('DISCONNECT') except ConnectionResetError as e: xmppserverlog.error("{}".format(e)) @@ -282,7 +281,7 @@ class Client(threading.Thread): def _handle_ctl(self, xml, data): try: - if data.decode("utf-8").find("roster") > -1: + if "roster" in data: # Return not-implemented for roster self.send( ''.format( @@ -293,7 +292,7 @@ class Client(threading.Thread): if xml.get("type") == "set": if ( - data.decode("utf-8").find("com:sf") > -1 + "com:sf" in data and xml.get("to") == "rl.ecorobot.net" ): # Android bind? Not sure what this does yet. self.send( @@ -305,11 +304,6 @@ class Client(threading.Thread): ) ) - #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: @@ -321,12 +315,9 @@ class Client(threading.Thread): # forward for client in XMPPServer.clients: - if client.bumper_jid != self.bumper_jid and client.state == client.READY: - #if client.address != self.address and client.state == client.READY: - ctl_to = xml.get("to") - - #xml.attrib["from"] = self.bumper_jid#.replace("@{}".format(XMPPServer.server_id),"@ecouser.net") - xml.attrib["from"] = "{}@ecouser.net".format(self.uid) + 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=") @@ -338,20 +329,7 @@ class Client(threading.Thread): if client.uid.lower() in ctl_to.lower(): xmppserverlog.info("Sending ctl to bot: {}".format(rxmlstring)) client.send(rxmlstring) - #client.send(data.decode("utf-8")) - - # data = data.decode("utf-8") - # id_index = data.find("id") - # if id_index > -1: - # 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)) @@ -391,105 +369,136 @@ class Client(threading.Thread): xmppserverlog.exception("{}".format(e)) def _handle_result(self, xml, data): - # forward try: ctl_to = xml.get("to") xml.attrib["from"] = 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 + if xml == None: + # Client first connecting, send our features + if data.decode("utf-8").find("jabber:client") > -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('') + # 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) + 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: - # Client getting session after authentication - if data.decode("utf-8").find("jabber:client") > -1: - # ack jabbr:client - self.send( - ''.format( - XMPPServer.server_id + 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( + '' ) - ) - time.sleep(0.25) - # session - self.send( - '' - ) - else: # Handle init bind - xml = ET.fromstring(data.decode("utf-8")) + else: # Handle init bind if len(xml): - child = self._tag_strip_uri(xml[0].tag) + child = self._tag_strip_uri(xml[0].tag) else: child = None if xml.tag == "iq": if child == "bind": - self._handle_bind(xml) + self._handle_bind(xml) + else: + xmppserverlog.error("Couldn't handle: {}".format(xml)) + + except Exception as e: xmppserverlog.exception("{}".format(e)) @@ -573,7 +582,7 @@ class Client(threading.Thread): except ET.ParseError as e: if "no element found" in e.msg: xmppserverlog.debug( - "xml parse error - {} - {} - this is common with ecovac protocol".format( + "xml parse error - {} - {}".format( data.decode("utf-8"), e ) ) @@ -589,10 +598,10 @@ class Client(threading.Thread): 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("/") + 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 @@ -609,6 +618,7 @@ class Client(threading.Thread): if not self.uid.startswith("fuid"): # Need sample data to see details here bumper.add_bot(self.uid, self.uid, self.devclass, "atom","eco-legacy") + self.type = self.BOT xmppserverlog.info("bot authenticated {}".format(self.uid)) # Send response self.send( @@ -626,6 +636,7 @@ class Client(threading.Thread): auth = True if auth: + self.type = self.CONTROLLER bumper.add_client(self.uid, "bumper", self.clientresource) xmppserverlog.debug("client authenticated {}".format(self.uid)) @@ -643,22 +654,6 @@ class Client(threading.Thread): '' ) # 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)) @@ -725,103 +720,148 @@ class Client(threading.Thread): 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) + "bot presence {} ".format(ET.tostring(xml, encoding="utf-8")) ) - # send a command from an unknown user - the response will contain the correct admin username + #Most likely a bot, possibly hello world in text + + #Send dummy return self.send( ' dummy '.format( self.bumper_jid ) ) - # self.send( - # ''.format( - # self.uid, self.devclass, XMPPServer.server_id - # ) - # ) - - self.send( - ''.format( - uuid.uuid4(), "unknown@ecouser.net", XMPPServer.server_id - ) - ) - else: - self.type = self.CONTROLLER + #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( - "{} type set to CONTROLLER (based on presence tag)".format( - self.address + "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")) ) - ) - self.send( - ' dummy '.format( - self.uid, XMPPServer.server_id, self.clientresource + #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 self.log_incoming_data: - xmppserverlog.debug( - "from {} - {}".format(self.address, data.decode("utf-8")) - ) + def _parse_data(self, data): - try: - xml = ET.fromstring(data.decode("utf-8")) - self._handle_xml(xml, 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:","")) + ) + print("e") + 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"): + if not "" in newdata: xmppserverlog.error( - "xml parse error - {} - {}".format(data.decode("utf-8"), e) + "xml parse error - {} - {}".format(newdata, 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) - ) + 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_xml(self, xml, data): + def _handle_iq(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: @@ -848,10 +888,7 @@ class Client(threading.Thread): if self.type == self.BOT: self._handle_result(xml, data) else: - self._handle_result(xml, data) - - if xml.tag == "presence": - self._handle_presence(xml) + self._handle_result(xml, data) except Exception as e: xmppserverlog.exception("{}".format(e)) @@ -859,16 +896,14 @@ class Client(threading.Thread): 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) + time.sleep(0.1) if not self.connection._closed: try: - #data = self.connection.recv(4096) - data = self.connection.recv(8192) - + data = self.connection.recv(4096) + if data != b"": + self._parse_data(data) except ConnectionResetError as e: xmppserverlog.error("{}".format(e)) except OSError as e: @@ -876,15 +911,5 @@ class Client(threading.Thread): except Exception as e: xmppserverlog.exception("{}".format(e)) - if data != b"": - splitdata = data.decode("utf-8") - splitdata = splitdata.split(" 1: - for s in splitdata: - if not s.startswith(" Date: Wed, 27 Feb 2019 09:07:02 -0500 Subject: [PATCH 5/5] Add Ozmo to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b81c99c..c318109 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ As work to reverse the protocols and provide a self-hosted central server is sti | Model | Protocol Used | Bumper Version | |--|--|--| | Deebot 900/901 | MQTT | master | +| Deebot Ozmo 930 | XMPP | master | | Deebot M81 Pro | XMPP | v0.1.0 | For more information about the protocols and how it works, see the [How does it work?](#how-does-it-work) section at the end. If you test against another model and it works, please report it so it can be added to the list.