Ozmo930 #9
5 changed files with 374 additions and 195 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -581,9 +581,28 @@ 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)
|
||||
)
|
||||
|
||||
|
||||
return web.json_response(body)
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -606,11 +625,17 @@ class ConfServer:
|
|||
if todo == "FindBest":
|
||||
service = postbody["service"]
|
||||
if service == "EcoMsgNew":
|
||||
body = {
|
||||
"result": "ok",
|
||||
"ip": socket.gethostbyname(socket.gethostname()),
|
||||
"port": 5223,
|
||||
}
|
||||
|
||||
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, msgserver)
|
||||
)
|
||||
return web.json_response(text=msgserver)
|
||||
|
||||
elif service == "EcoUpdate":
|
||||
body = {"result": "ok", "ip": "47.88.66.164", "port": 8005}
|
||||
|
||||
|
|
@ -627,7 +652,9 @@ class ConfServer:
|
|||
json_body = json.loads(await request.text())
|
||||
randomid = "".join(random.sample(string.ascii_letters, 6))
|
||||
bots = self.bumper_bots.get()
|
||||
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
|
||||
|
|
@ -635,7 +662,8 @@ class ConfServer:
|
|||
"\r\n POST: {} \r\n Response: {}".format(json_body, body)
|
||||
)
|
||||
return web.json_response(body)
|
||||
else:
|
||||
|
||||
#No response, send error back
|
||||
confserverlog.error(
|
||||
"No bots with DID: {} connected to MQTT".format(
|
||||
json_body["toId"]
|
||||
|
|
@ -643,6 +671,13 @@ class ConfServer:
|
|||
)
|
||||
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))
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -193,6 +192,8 @@ class Client(threading.Thread):
|
|||
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
|
||||
|
|
@ -216,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))
|
||||
|
|
@ -280,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(
|
||||
'<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 (
|
||||
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(
|
||||
'<iq id="{}" to="{}@{}/{}" from="rl.ecorobot.net" type="result"/>'.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"))
|
||||
)
|
||||
|
||||
if xml[0][0]:
|
||||
ctl = xml[0][0]
|
||||
if ctl.get("admin") and self.type == self.BOT:
|
||||
|
|
@ -319,59 +315,134 @@ 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:
|
||||
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:
|
||||
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"))
|
||||
if client.uid.lower() in ctl_to.lower():
|
||||
xmppserverlog.info("Sending ctl to bot: {}".format(rxmlstring))
|
||||
client.send(rxmlstring)
|
||||
|
||||
|
||||
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(
|
||||
'<iq type="result" id="{}" from="{}" />'.format(
|
||||
pingresp = '<iq type="result" id="{}" from="{}" />'.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('<query','<query xmlns="com:ctl"')
|
||||
|
||||
for client in XMPPServer.clients:
|
||||
if client.bumper_jid != self.bumper_jid and client.state == client.READY:
|
||||
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:
|
||||
xmppserverlog.exception("{}".format(e))
|
||||
|
||||
def _handle_result(self, xml, data):
|
||||
try:
|
||||
ctl_to = xml.get("to")
|
||||
xml.attrib["from"] = self.bumper_jid
|
||||
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:
|
||||
if client.address != self.address and client.state == client.READY:
|
||||
client.send(data.decode("utf-8"))
|
||||
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:
|
||||
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))
|
||||
|
||||
def _handle_connect(self, data):
|
||||
def _handle_connect(self, data, xml=None):
|
||||
try:
|
||||
|
||||
if self.state == self.CONNECT:
|
||||
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(
|
||||
|
|
@ -388,15 +459,19 @@ class Client(threading.Thread):
|
|||
)
|
||||
# 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
|
||||
self._handle_iq_auth(data)
|
||||
else:
|
||||
self.send("</stream>")
|
||||
|
||||
elif (
|
||||
data.decode("utf-8").find("urn:ietf:params:xml:ns:xmpp-sasl") > -1
|
||||
): # Handle SASL auth
|
||||
self._handle_sasl_auth(data)
|
||||
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
|
||||
|
|
@ -412,7 +487,6 @@ class Client(threading.Thread):
|
|||
)
|
||||
|
||||
else: # Handle init bind
|
||||
xml = ET.fromstring(data.decode("utf-8"))
|
||||
if len(xml):
|
||||
child = self._tag_strip_uri(xml[0].tag)
|
||||
else:
|
||||
|
|
@ -421,6 +495,10 @@ class Client(threading.Thread):
|
|||
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))
|
||||
|
|
@ -504,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
|
||||
)
|
||||
)
|
||||
|
|
@ -520,20 +598,27 @@ class Client(threading.Thread):
|
|||
except Exception as e:
|
||||
xmppserverlog.exception("{}".format(e))
|
||||
|
||||
def _handle_sasl_auth(self, data):
|
||||
def _handle_sasl_auth(self, xml):
|
||||
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
|
||||
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")
|
||||
self.type = self.BOT
|
||||
xmppserverlog.info("bot authenticated {}".format(self.uid))
|
||||
# Send response
|
||||
self.send(
|
||||
|
|
@ -551,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))
|
||||
|
||||
|
|
@ -568,22 +654,6 @@ class Client(threading.Thread):
|
|||
'<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl"/>'
|
||||
) # 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))
|
||||
|
||||
|
|
@ -606,21 +676,31 @@ class Client(threading.Thread):
|
|||
|
||||
clientbindxml = xml.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.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 = '<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
|
||||
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
|
||||
)
|
||||
else:
|
||||
xmppserverlog.debug("new client {}".format(self.address))
|
||||
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.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 = '<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")
|
||||
|
|
@ -640,93 +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)
|
||||
)
|
||||
# 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(
|
||||
'<iq type="set" id="{}" from="{}" to="{}"><query xmlns="com:ctl"><ctl td="GetCleanState" /></query></iq>'.format(
|
||||
uuid.uuid4(), "unknown@ecouser.net", XMPPServer.bot_id
|
||||
'<presence to="{}"> dummy </presence>'.format(
|
||||
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:
|
||||
self.type = self.CONTROLLER
|
||||
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"))
|
||||
)
|
||||
#Send dummy return
|
||||
self.send(
|
||||
'<presence to="{}@{}/{}"> dummy </presence>'.format(
|
||||
self.uid, XMPPServer.bot_id, self.clientresource
|
||||
'<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:
|
||||
xmppserverlog.exception("{}".format(e))
|
||||
|
||||
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:
|
||||
xml = ET.fromstring(data.decode("utf-8"))
|
||||
self._handle_xml(xml, data)
|
||||
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 "<stream:stream " in data.decode("utf-8"):
|
||||
if "<stream:stream " in newdata:
|
||||
if self.state == self.CONNECT or self.state == self.INIT:
|
||||
self._handle_connect(data)
|
||||
self._handle_connect(newdata.encode("utf-8"))
|
||||
else:
|
||||
if not (data.decode("utf-8") == "" or data.decode("utf-8") == " "):
|
||||
if not (newdata == "" or newdata == " "):
|
||||
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:
|
||||
# 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(
|
||||
"xml parse error - {} - {}".format(data.decode("utf-8"), e)
|
||||
"xml parse error - {} - {}".format(newdata, e)
|
||||
)
|
||||
else:
|
||||
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:
|
||||
if "<stream:stream" in newdata: #Handle start stream and connect
|
||||
if self.state == self.CONNECT or self.state == self.INIT:
|
||||
xmppserverlog.debug(
|
||||
"xml parse error - {} - {}".format(data.decode("utf-8"), e)
|
||||
"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:
|
||||
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:
|
||||
|
|
@ -737,14 +872,23 @@ 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":
|
||||
self._handle_presence(xml)
|
||||
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))
|
||||
|
|
@ -752,15 +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)
|
||||
|
||||
if data != b"":
|
||||
self._parse_data(data)
|
||||
except ConnectionResetError as e:
|
||||
xmppserverlog.error("{}".format(e))
|
||||
except OSError as e:
|
||||
|
|
@ -768,6 +911,5 @@ class Client(threading.Thread):
|
|||
except Exception as e:
|
||||
xmppserverlog.exception("{}".format(e))
|
||||
|
||||
if data != b"":
|
||||
self._parse_data(data)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue