Ozmo930 #9

Merged
bmartin5692 merged 5 commits from Ozmo930 into master 2019-02-27 15:10:14 +01:00
5 changed files with 374 additions and 195 deletions

View file

@ -20,6 +20,7 @@ As work to reverse the protocols and provide a self-hosted central server is sti
| Model | Protocol Used | Bumper Version | | Model | Protocol Used | Bumper Version |
|--|--|--| |--|--|--|
| Deebot 900/901 | MQTT | master | | Deebot 900/901 | MQTT | master |
| Deebot Ozmo 930 | XMPP | master |
| Deebot M81 Pro | XMPP | v0.1.0 | | 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. 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.

View file

@ -89,7 +89,7 @@ class VacBotDevice(object):
resource="", resource="",
name="", name="",
nick="", nick="",
company="eco-ng", company="",
): ):
self.vac_bot_device_class = vac_bot_device_class self.vac_bot_device_class = vac_bot_device_class
self.company = company self.company = company
@ -132,13 +132,14 @@ def check_authcode(uid, authcode):
return False return False
def add_bot(sn, did, devclass, resource): def add_bot(sn, did, devclass, resource, company):
newbot = VacBotDevice() newbot = VacBotDevice()
newbot.did = did newbot.did = did
newbot.name = sn newbot.name = sn
newbot.vac_bot_device_class = devclass newbot.vac_bot_device_class = devclass
newbot.resource = resource newbot.resource = resource
newbot.company = company
bots = bumper_bots_var.get() bots = bumper_bots_var.get()
existingbot = False existingbot = False

View file

@ -581,9 +581,28 @@ class ConfServer:
self.bumper_bots.set(bots) self.bumper_bots.set(bots)
body = {"result": "ok", "todo": "result"} 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( confserverlog.debug(
"\r\n POST: {} \r\n Response: {}".format(postbody, body) "\r\n POST: {} \r\n Response: {}".format(postbody, body)
) )
return web.json_response(body) return web.json_response(body)
except Exception as e: except Exception as e:
@ -606,11 +625,17 @@ class ConfServer:
if todo == "FindBest": if todo == "FindBest":
service = postbody["service"] service = postbody["service"]
if service == "EcoMsgNew": if service == "EcoMsgNew":
body = {
"result": "ok", srvip = socket.gethostbyname(socket.gethostname())
"ip": socket.gethostbyname(socket.gethostname()), msgserver = {"ip":srvip,"port":5223,"result":"ok"}
"port": 5223, 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, msgserver)
)
return web.json_response(text=msgserver)
elif service == "EcoUpdate": elif service == "EcoUpdate":
body = {"result": "ok", "ip": "47.88.66.164", "port": 8005} body = {"result": "ok", "ip": "47.88.66.164", "port": 8005}
@ -627,22 +652,32 @@ class ConfServer:
json_body = json.loads(await request.text()) json_body = json.loads(await request.text())
randomid = "".join(random.sample(string.ascii_letters, 6)) randomid = "".join(random.sample(string.ascii_letters, 6))
bots = self.bumper_bots.get() bots = self.bumper_bots.get()
for bot in bots: if "toId" in json_body: #Its a command
if bot.did == json_body["toId"] and bot.mqtt_connection == True: for bot in bots:
retcmd = await self.helperbot.send_command(json_body, randomid) if bot.company == 'eco-ng':
body = retcmd if bot.did == json_body["toId"] and bot.mqtt_connection == True:
confserverlog.debug( retcmd = await self.helperbot.send_command(json_body, randomid)
"\r\n POST: {} \r\n Response: {}".format(json_body, body) 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: body = {"id": randomid, "errno": bumper.ERR_COMMON, "ret": "fail"}
confserverlog.error( return web.json_response(body)
"No bots with DID: {} connected to MQTT".format( else:
json_body["toId"] if "td" in json_body: #Seen when doing initial wifi config
) if json_body["td"] == "PollSCResult":
) body = {
body = {"id": randomid, "errno": bumper.ERR_COMMON, "ret": "fail"} "ret": "ok"
return web.json_response(body) }
return web.json_response(body)
except Exception as e: except Exception as e:
confserverlog.exception("{}".format(e)) confserverlog.exception("{}".format(e))

View file

@ -333,7 +333,7 @@ class BumperMQTTServer_Plugin:
): ):
tmpbotdetail = str(didsplit[1]).split("/") tmpbotdetail = str(didsplit[1]).split("/")
bumper.add_bot( bumper.add_bot(
username, didsplit[0], tmpbotdetail[0], tmpbotdetail[1] username, didsplit[0], tmpbotdetail[0], tmpbotdetail[1], "eco-ng"
) )
mqttserverlog.debug( mqttserverlog.debug(
"new bot authenticated SN: {} DID: {}".format( "new bot authenticated SN: {} DID: {}".format(

View file

@ -11,8 +11,7 @@ xmppserverlog = logging.getLogger("xmppserver")
class XMPPServer: class XMPPServer:
server_id = "bumper" server_id = "ecouser.net"
bot_id = "bumpy"
client_id = None client_id = None
clients = [] clients = []
exit_flag = False exit_flag = False
@ -193,6 +192,8 @@ class Client(threading.Thread):
self.connection = connection self.connection = connection
self.address = client_address[0] self.address = client_address[0]
self.clientresource = "" self.clientresource = ""
self.devclass = ""
self.bumper_jid = ""
self.uid = "" self.uid = ""
self.log_sent_message = False # Set to true to log sends self.log_sent_message = False # Set to true to log sends
self.log_incoming_data = True # Set to true to log sends self.log_incoming_data = True # Set to true to log sends
@ -216,7 +217,7 @@ class Client(threading.Thread):
except BrokenPipeError as e: except BrokenPipeError as e:
xmppserverlog.error("{}".format(e)) xmppserverlog.error("{}".format(e))
# self._set_state('DISCONNECT') self._set_state('DISCONNECT')
except ConnectionResetError as e: except ConnectionResetError as e:
xmppserverlog.error("{}".format(e)) xmppserverlog.error("{}".format(e))
@ -280,7 +281,7 @@ class Client(threading.Thread):
def _handle_ctl(self, xml, data): def _handle_ctl(self, xml, data):
try: try:
if data.decode("utf-8").find("roster") > -1: if "roster" in data:
# Return not-implemented for roster # Return not-implemented for roster
self.send( self.send(
'<iq type="error" id="{}"><error type="cancel" code="501"><feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/></error></iq>'.format( '<iq type="error" id="{}"><error type="cancel" code="501"><feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/></error></iq>'.format(
@ -291,23 +292,18 @@ class Client(threading.Thread):
if xml.get("type") == "set": if xml.get("type") == "set":
if ( if (
data.decode("utf-8").find("com:sf") > -1 "com:sf" in data
and xml.get("to") == "rl.ecorobot.net" and xml.get("to") == "rl.ecorobot.net"
): # Android bind? Not sure what this does yet. ): # Android bind? Not sure what this does yet.
self.send( self.send(
'<iq id="{}" to="{}@{}/{}" from="rl.ecorobot.net" type="result"/>'.format( '<iq id="{}" to="{}@{}/{}" from="rl.ecorobot.net" type="result"/>'.format(
xml.get("id"), xml.get("id"),
self.uid, self.uid,
XMPPServer.bot_id, XMPPServer.server_id,
self.clientresource, self.clientresource,
) )
) )
else:
xmppserverlog.debug(
"Unknown set type: {}".format(data.decode("utf-8"))
)
if xml[0][0]: if xml[0][0]:
ctl = xml[0][0] ctl = xml[0][0]
if ctl.get("admin") and self.type == self.BOT: if ctl.get("admin") and self.type == self.BOT:
@ -319,100 +315,178 @@ class Client(threading.Thread):
# forward # forward
for client in XMPPServer.clients: 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:
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('<query','<query xmlns="com:ctl"')
if client.type == self.BOT: if client.type == self.BOT:
data = data.decode("utf-8") if client.uid.lower() in ctl_to.lower():
id_index = data.find("id") xmppserverlog.info("Sending ctl to bot: {}".format(rxmlstring))
if id_index > -1: client.send(rxmlstring)
data = (
data[:id_index]
+ 'from="'
+ XMPPServer.client_id
+ '" '
+ data[id_index:]
)
data = data.encode()
client.send(data.decode("utf-8"))
except Exception as e: except Exception as e:
xmppserverlog.exception("{}".format(e)) xmppserverlog.exception("{}".format(e))
def _handle_ping(self, xml, data): def _handle_ping(self, xml, data):
try: try:
if xml.get("to").find("@") == -1: if xml.get("to").find("@") == -1: #No to address
# Ping to server - respond # Ping to server - respond
self.send( pingresp = '<iq type="result" id="{}" from="{}" />'.format(
'<iq type="result" id="{}" from="{}" />'.format(
xml.get("id"), xml.get("to") xml.get("id"), xml.get("to")
) )
) #xmppserverlog.debug("Server Ping resp: {}".format(pingresp))
self.send(pingresp)
else: 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('<query','<query xmlns="com:ctl"')
for client in XMPPServer.clients: 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:
client.send(data.decode("utf-8")) if pingto.lower() in client.bumper_jid.lower():
pingsend = '<iq type="result" id="{}" from="{}" to="{}" />'.format(
xml.get("id"), pingfrom, pingto
)
xmppserverlog.debug("ping from {} to {}".format(pingfrom, pingto))
client.send(pingstring)
except Exception as e: except Exception as e:
xmppserverlog.exception("{}".format(e)) xmppserverlog.exception("{}".format(e))
def _handle_result(self, data): def _handle_result(self, xml, data):
# forward
try: try:
for client in XMPPServer.clients: ctl_to = xml.get("to")
if client.address != self.address and client.state == client.READY: xml.attrib["from"] = self.bumper_jid
client.send(data.decode("utf-8")) if "errno='103' error='permission denied," in data: #No permissions, usually if bot was last on Ecovac network
if self.type == self.BOT:
xquery = xml.getchildren()
ctl = xquery[0].getchildren()
ctlerr = ctl[0].attrib["error"]
adminuser = ctlerr.replace("permission denied, please contact ","")
adminuser = adminuser.replace(" ","")
if not (adminuser.startswith("fuid_") or bumper.use_auth): #if not fuid_ then its ecovacs OR ignore bumper auth
#TODO: Implement auth later, should this user have access to bot?
#Add user jid to bot
newuser = ctl_to.split("/")[0]
adduser = '<iq type="set" id="{}" from="{}" to="{}"><query xmlns="com:ctl"><ctl td="AddUser" id="0000" jid="{}" /></query></iq>'.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 = '<iq type="set" id="{}" from="{}" to="{}"><query xmlns="com:ctl"><ctl td="SetAC" id="1111" jid="{}"><acs><ac name="userman" allow="1"/><ac name="setting" allow="1"/><ac name="clean" allow="1"/></acs></ctl></query></iq>'.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(
'<iq type="set" id="{}" from="{}" to="{}"><query xmlns="com:ctl"><ctl td="GetUserInfo" id="4444" /><UserInfos/></query></iq>'.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('<query','<query xmlns="com:ctl"')
if self.type == self.BOT:
if ctl_to == "de.ecorobot.net": #Send to all clients
xmppserverlog.debug("Sending to all clients because of de: {}".format(rxmlstring))
for client in XMPPServer.clients:
client.send(rxmlstring)
if xml.get("to").find("@") == -1: #No to address
ctl_to = xml.get("to")
else:
ctl_to = "{}@ecouser.net".format(ctl_to.split("@")[0])
for client in XMPPServer.clients:
if client.bumper_jid != self.bumper_jid and client.state == client.READY:
if not "@" in ctl_to: #No user@, send to all clients?
#TODO: Revisit later, this may be wrong
client.send(rxmlstring)
elif client.uid.lower() in ctl_to.lower(): #If client matches TO=
xmppserverlog.debug("Sending from {} to client {}: {}".format(self.uid, client.uid, rxmlstring))
client.send(rxmlstring)
except Exception as e: except Exception as e:
xmppserverlog.exception("{}".format(e)) xmppserverlog.exception("{}".format(e))
def _handle_connect(self, data): def _handle_connect(self, data, xml=None):
try: try:
if self.state == self.CONNECT: if self.state == self.CONNECT:
# Client first connecting, send our features if xml == None:
# Client first connecting, send our features
if data.decode("utf-8").find("jabber:client") > -1: if data.decode("utf-8").find("jabber:client") > -1:
# ack jabbr:client sc = data.decode("utf-8").find("to=")
# no STARTTLS ec = data.decode("utf-8").find(".ecorobot.net")
self.send( if ec > -1:
'<stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" version="1.0" id="1" from="{}">'.format( self.devclass = data.decode("utf-8")[sc+4:ec]
XMPPServer.server_id # ack jabbr:client
# no STARTTLS
self.send(
'<stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" version="1.0" id="1" from="{}">'.format(
XMPPServer.server_id
)
) )
) # with STARTTLS
# with STARTTLS # self.send('<stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns:tls="http://www.ietf.org/rfc/rfc2595.txt" xmlns="jabber:client" version="1.0" id="1" from="{}">'.format(XMPPServer.server_id))
# self.send('<stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns:tls="http://www.ietf.org/rfc/rfc2595.txt" xmlns="jabber:client" version="1.0" id="1" from="{}">'.format(XMPPServer.server_id)) time.sleep(0.25)
time.sleep(0.25) # send authentication support for iq-auth (fallback) and SASL
# send authentication support for iq-auth (fallback) and SASL self.send(
self.send( '<stream:features><auth xmlns="http://jabber.org/features/iq-auth"/><mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><mechanism>PLAIN</mechanism></mechanisms></stream:features>'
'<stream:features><auth xmlns="http://jabber.org/features/iq-auth"/><mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><mechanism>PLAIN</mechanism></mechanisms></stream:features>' )
) # self.send('<stream:features><auth xmlns="http://jabber.org/features/iq-auth"/></stream:features>')
# self.send('<stream:features><auth xmlns="http://jabber.org/features/iq-auth"/></stream:features>')
elif data.decode("utf-8").find("jabber:iq:auth") > -1: # Handle iq-auth else:
self._handle_iq_auth(data) self.send("</stream>")
elif ( else:
data.decode("utf-8").find("urn:ietf:params:xml:ns:xmpp-sasl") > -1 if "jabber:iq:auth" in xml.tag: # Handle iq-auth
): # Handle SASL auth self._handle_iq_auth(xml)
self._handle_sasl_auth(data) 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: elif self.state == self.INIT:
# Client getting session after authentication if xml == None:
if data.decode("utf-8").find("jabber:client") > -1: # Client getting session after authentication
# ack jabbr:client if data.decode("utf-8").find("jabber:client") > -1:
self.send( # ack jabbr:client
'<stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" version="1.0" id="1" from="{}">'.format( self.send(
XMPPServer.server_id '<stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" version="1.0" id="1" from="{}">'.format(
XMPPServer.server_id
)
)
time.sleep(0.25)
# session
self.send(
'<stream:features><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/><session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></stream:features>'
) )
)
time.sleep(0.25)
# session
self.send(
'<stream:features><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/><session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></stream:features>'
)
else: # Handle init bind else: # Handle init bind
xml = ET.fromstring(data.decode("utf-8"))
if len(xml): if len(xml):
child = self._tag_strip_uri(xml[0].tag) child = self._tag_strip_uri(xml[0].tag)
else: else:
@ -421,6 +495,10 @@ class Client(threading.Thread):
if xml.tag == "iq": if xml.tag == "iq":
if child == "bind": if child == "bind":
self._handle_bind(xml) self._handle_bind(xml)
else:
xmppserverlog.error("Couldn't handle: {}".format(xml))
except Exception as e: except Exception as e:
xmppserverlog.exception("{}".format(e)) xmppserverlog.exception("{}".format(e))
@ -504,7 +582,7 @@ class Client(threading.Thread):
except ET.ParseError as e: except ET.ParseError as e:
if "no element found" in e.msg: if "no element found" in e.msg:
xmppserverlog.debug( xmppserverlog.debug(
"xml parse error - {} - {} - this is common with ecovac protocol".format( "xml parse error - {} - {}".format(
data.decode("utf-8"), e data.decode("utf-8"), e
) )
) )
@ -520,20 +598,27 @@ class Client(threading.Thread):
except Exception as e: except Exception as e:
xmppserverlog.exception("{}".format(e)) xmppserverlog.exception("{}".format(e))
def _handle_sasl_auth(self, data): def _handle_sasl_auth(self, xml):
try: try:
xml = ET.fromstring(data.decode("utf-8"))
saslauth = base64.b64decode(xml.text).decode("utf-8").split("/") saslauth = base64.b64decode(xml.text).decode("utf-8").split("/")
username = saslauth[0] username = saslauth[0]
username = saslauth[0].split("\x00")[1] username = saslauth[0].split("\x00")[1]
self.uid = username self.uid = username
resource = saslauth[1] if len(saslauth) > 1:
self.clientresource = resource resource = saslauth[1]
authcode = saslauth[2] 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"): if not self.uid.startswith("fuid"):
# Need sample data to see details here # 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")
self.type = self.BOT
xmppserverlog.info("bot authenticated {}".format(self.uid)) xmppserverlog.info("bot authenticated {}".format(self.uid))
# Send response # Send response
self.send( self.send(
@ -551,6 +636,7 @@ class Client(threading.Thread):
auth = True auth = True
if auth: if auth:
self.type = self.CONTROLLER
bumper.add_client(self.uid, "bumper", self.clientresource) bumper.add_client(self.uid, "bumper", self.clientresource)
xmppserverlog.debug("client authenticated {}".format(self.uid)) xmppserverlog.debug("client authenticated {}".format(self.uid))
@ -568,22 +654,6 @@ class Client(threading.Thread):
'<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl"/>' '<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl"/>'
) # Fail ) # 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: except Exception as e:
xmppserverlog.exception("{}".format(e)) xmppserverlog.exception("{}".format(e))
@ -606,21 +676,31 @@ class Client(threading.Thread):
clientbindxml = xml.getchildren() clientbindxml = xml.getchildren()
clientresourcexml = clientbindxml[0].getchildren() clientresourcexml = clientbindxml[0].getchildren()
if len(clientresourcexml) > 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 = '<iq type="result" id="{}"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>{}</jid></bind></iq>'.format(
xml.get("id"), self.bumper_jid
)
elif len(clientresourcexml) > 0:
self.clientresource = clientresourcexml[0].text self.clientresource = clientresourcexml[0].text
self.name = "XMPP_Client_{}".format(self.clientresource) self.name = "XMPP_Client_{}".format(self.clientresource)
self.bumper_jid = "{}@{}/{}".format(self.uid, XMPPServer.server_id, self.clientresource)
xmppserverlog.debug( xmppserverlog.debug(
"new client {} using resource {}".format( "new client {} using resource {}".format(
self.address, self.clientresource self.uid, self.clientresource
) )
) )
res = '<iq type="result" id="{}"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>{}@{}/{}</jid></bind></iq>'.format( res = '<iq type="result" id="{}"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>{}</jid></bind></iq>'.format(
xml.get("id"), self.uid, XMPPServer.bot_id, self.clientresource xml.get("id"), self.bumper_jid
) )
else: else:
xmppserverlog.debug("new client {}".format(self.address)) self.name = "XMPP_Client_{}_{}".format(self.uid,self.address)
res = '<iq type="result" id="{}"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>{}@{}</jid></bind></iq>'.format( self.bumper_jid = "{}@{}".format(self.uid, XMPPServer.server_id)
xml.get("id"), self.uid, XMPPServer.bot_id xmppserverlog.debug("new client {}".format(self.uid))
res = '<iq type="result" id="{}"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>{}</jid></bind></iq>'.format(
xml.get("id"), self.bumper_jid
) )
self._set_state("BIND") self._set_state("BIND")
@ -640,93 +720,148 @@ class Client(threading.Thread):
def _handle_presence(self, xml): def _handle_presence(self, xml):
try: 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
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( self.send(
'<iq type="set" id="{}" from="{}" to="{}"><query xmlns="com:ctl"><ctl td="GetCleanState" /></query></iq>'.format( '<presence to="{}"> dummy </presence>'.format(
uuid.uuid4(), "unknown@ecouser.net", XMPPServer.bot_id self.bumper_jid
) )
) )
#If it is a BOT, send extras
if self.type == self.BOT:
#get device info
self.send(
'<iq type="set" id="14" to="{}" from="{}"><query xmlns="com:ctl"><ctl td="GetDeviceInfo"/></query></iq>'.format(
self.bumper_jid, XMPPServer.server_id
)
)
else: else:
self.type = self.CONTROLLER
xmppserverlog.debug( xmppserverlog.debug(
"{} type set to CONTROLLER (based on presence tag)".format( "client presence - {} ".format(ET.tostring(xml, encoding="utf-8"))
self.address
)
) )
self.send(
'<presence to="{}@{}/{}"> dummy </presence>'.format( if xml.get("type") == "available":
self.uid, XMPPServer.bot_id, self.clientresource xmppserverlog.debug(
"client presence available - {} ".format(ET.tostring(xml, encoding="utf-8"))
) )
) #Send dummy return
self.send(
'<presence to="{}"> dummy </presence>'.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(
'<presence to="{}"> dummy </presence>'.format(
self.bumper_jid
)
)
except Exception as e: except Exception as e:
xmppserverlog.exception("{}".format(e)) xmppserverlog.exception("{}".format(e))
def _parse_data(self, data): def _parse_data(self, data):
if self.log_incoming_data:
xmppserverlog.debug(
"from {} - {}".format(self.address, data.decode("utf-8")) if data.decode("utf-8").startswith("<?xml"): #Strip <?xml and add artificial root
) newdata = re.sub(r"(<\?xml[^>]+\?>)", r"<root>",data.decode("utf-8")) + "</root>"
else:
newdata = "<root>{}</root>".format(data.decode("utf-8")) #Add artificial root
try: try:
xml = ET.fromstring(data.decode("utf-8")) root = ET.fromstring(newdata)
self._handle_xml(xml, data) 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: except ET.ParseError as e:
if ( if (
"no element found" in e.msg "no element found" in e.msg
): # Element not closed or not all bytes received ): # Element not closed or not all bytes received
# Happens wth connect stream often # Happens wth connect stream often
if "<stream:stream " in data.decode("utf-8"): if "<stream:stream " in newdata:
if self.state == self.CONNECT or self.state == self.INIT: if self.state == self.CONNECT or self.state == self.INIT:
self._handle_connect(data) self._handle_connect(newdata.encode("utf-8"))
else: else:
if not (data.decode("utf-8") == "" or data.decode("utf-8") == " "): if not (newdata == "" or newdata == " "):
xmppserverlog.error( xmppserverlog.error(
"xml parse error - {} - {}".format(data.decode("utf-8"), e) "xml parse error - {} - {}".format(newdata, e)
) )
elif "not well-formed (invalid token)" in e.msg: elif "not well-formed (invalid token)" in e.msg:
# If a lone </stream:stream> - client is signalling end of session/disconnect # If a lone </stream:stream> - client is signalling end of session/disconnect
if not "</stream:stream>" in data.decode("utf-8"): if not "</stream:stream>" in newdata:
xmppserverlog.error( xmppserverlog.error(
"xml parse error - {} - {}".format(data.decode("utf-8"), e) "xml parse error - {} - {}".format(newdata, e)
) )
else: else:
self.send("</stream:stream>") # Close stream self.send("</stream:stream>") # 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: else:
xmppserverlog.debug( if "<stream:stream" in newdata: #Handle start stream and connect
"xml parse error - {} - {}".format(data.decode("utf-8"), e) if self.state == self.CONNECT or self.state == self.INIT:
) xmppserverlog.debug(
"Handling connect data - {}".format(newdata)
)
self._handle_connect(newdata.encode("utf-8"))
else:
if not "</stream:stream>" in newdata:
xmppserverlog.error(
"xml parse error - {} - {}".format(newdata, e)
)
else:
self.send("</stream:stream>") # Close stream
self._set_state("DISCONNECT")
except Exception as e: except Exception as e:
xmppserverlog.exception("{}".format(e)) xmppserverlog.exception("{}".format(e))
def _handle_xml(self, xml, data): def _handle_iq(self, xml, data):
try: try:
if self.state == self.CONNECT or self.state == self.INIT:
self._handle_connect(data)
if len(xml): if len(xml):
child = self._tag_strip_uri(xml[0].tag) child = self._tag_strip_uri(xml[0].tag)
else: else:
@ -737,14 +872,23 @@ class Client(threading.Thread):
self._handle_bind(xml) self._handle_bind(xml)
elif child == "session": elif child == "session":
self._handle_session(xml) self._handle_session(xml)
elif child == "query":
self._handle_ctl(xml, data)
elif child == "ping": elif child == "ping":
self._handle_ping(xml, data) 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": elif xml.get("type") == "result":
self._handle_result(data) if self.type == self.BOT:
elif xml.tag == "presence": self._handle_result(xml, data)
self._handle_presence(xml) 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: except Exception as e:
xmppserverlog.exception("{}".format(e)) xmppserverlog.exception("{}".format(e))
@ -752,15 +896,14 @@ class Client(threading.Thread):
def run(self): def run(self):
# xmppserverlog.info('client connected - {}'.format(self.address)) # xmppserverlog.info('client connected - {}'.format(self.address))
self._set_state("CONNECT") self._set_state("CONNECT")
while not self.state == self.DISCONNECT and not self.connection._closed: while not self.state == self.DISCONNECT and not self.connection._closed:
data = b"" data = b""
time.sleep(0.1)
time.sleep(0.2)
if not self.connection._closed: if not self.connection._closed:
try: try:
data = self.connection.recv(4096) data = self.connection.recv(4096)
if data != b"":
self._parse_data(data)
except ConnectionResetError as e: except ConnectionResetError as e:
xmppserverlog.error("{}".format(e)) xmppserverlog.error("{}".format(e))
except OSError as e: except OSError as e:
@ -768,6 +911,5 @@ class Client(threading.Thread):
except Exception as e: except Exception as e:
xmppserverlog.exception("{}".format(e)) xmppserverlog.exception("{}".format(e))
if data != b"":
self._parse_data(data)