diff --git a/README.md b/README.md index 15c68b3..dbb8492 100644 --- a/README.md +++ b/README.md @@ -149,9 +149,13 @@ So far two protocols have been identified that various models of EcoVacs robots ---- ### Login/Authentication/RestAPI -***Bumper*** provides a fully simulated central server that handles login/authentication for the app/clients. At this time no authentication layer is implemented and you can use any e-mail/password when logging in. Future versions should add additional options here for security. +***Bumper*** provides a fully simulated central server that handles login/authentication for the app/clients. -This means however, that once a robot has been configured to access your WiFi it never needs to communicate with EcoVac's servers again. +The EcoVacs app encrypts the username/password with the public key of EcoVacs when authenticating. Since we don't have the private key to decrypt, there is no way to provide true security and authentication. + +Future versions may add additional options here for security. + +Bots have no authentication and once a robot has been configured to access your WiFi it never needs to communicate with EcoVac's servers again. ---- ### XMPP diff --git a/bumper.py b/bumper.py index 0f86cbc..be48215 100644 --- a/bumper.py +++ b/bumper.py @@ -37,13 +37,12 @@ def main(): conf_server_2 = bumper.ConfServer(conf_address_8007, usessl=False,bumper_users=bumper.bumper_users_var, bumper_bots=bumper.bumper_bots_var,bumper_clients=bumper.bumper_clients_var, helperbot=mqtt_helperbot) #add user - users = bumper.bumper_users_var.get() - user1 = bumper.BumperUser('user1') - user1.add_device('devid') - user1.add_bot('bot_did') - users.append(user1) - bumper.bumper_users_var.set(users) - + # users = bumper.bumper_users_var.get() + # user1 = bumper.BumperUser('user1') + # user1.add_device('devid') + # user1.add_bot('bot_did') + # users.append(user1) + # bumper.bumper_users_var.set(users) # start xmpp server on port 5223 (sync) xmpp_server.run(run_async=True) #Start in new thread diff --git a/bumper/__init__.py b/bumper/__init__.py index 45691ae..6ae059c 100644 --- a/bumper/__init__.py +++ b/bumper/__init__.py @@ -18,6 +18,8 @@ ca_cert = './certs/CA/cacert.pem' server_cert = './certs/cert.pem' server_key = './certs/key.pem' +use_auth = False + #Logs bumperlog = logging.getLogger("bumper") confserverlog = logging.getLogger("confserver") @@ -105,12 +107,51 @@ class VacBotClient(object): return {"userid": self.userid,"realm": self.realm,"resource": self.resource} def check_authcode(uid, authcode): - users = bumper_users_var.get() - for user in users: - if uid == "fuid_{}".format(user.userid) and authcode in user.authcodes: - return True + users = bumper_users_var.get() + for user in users: + if uid == "fuid_{}".format(user.userid) and authcode in user.authcodes: + return True + + return False + +def add_bot(sn, did, devclass, resource): + + newbot = VacBotDevice() + newbot.did = did + newbot.name = sn + newbot.vac_bot_device_class = devclass + newbot.resource = resource + + bots = bumper_bots_var.get() + existingbot = False + for bot in bots: + if bot.did == newbot.did: + existingbot = True + + if existingbot == False: + bots.append(newbot) + bumperlog.info("new bot added SN: {} DID: {}".format(newbot.name, newbot.did)) + bumper_bots_var.set(bots) + +def add_client(userid, realm, resource): + + newclient = VacBotClient() + newclient.userid = userid + newclient.realm = realm + newclient.resource = resource + + clients = bumper_clients_var.get() + + existingclient = False + for client in clients: + if client.userid == newclient.userid: + existingclient = True + + if existingclient == False: + clients.append(newclient) + bumperlog.info("new client added {}".format(newclient.userid)) + bumper_clients_var.set(clients) - return False RETURN_API_SUCCESS = "0000" ERR_ACTIVATE_TOKEN_TIMEOUT = "1006" diff --git a/bumper/confserver.py b/bumper/confserver.py index 87726cb..8f8d5db 100644 --- a/bumper/confserver.py +++ b/bumper/confserver.py @@ -134,73 +134,134 @@ class ConfServer(): except Exception as e: confserverlog.exception('{}'.format(e)) - async def handle_login(self, request): - + async def handle_login(self, request): try: user_devid = request.match_info.get('devid', "") countrycode = request.match_info.get('country', "us") - if not user_devid == "": #Performing basic "auth" using devid, super insecure - users = self.bumper_users.get() - for user in users: - if user_devid in user.devices: - tmpaccesstoken = '' - if 'checkLogin' in request.path: - if request.query['accessToken'] in user.tokens and request.query['uid'] == "fuid_{}".format(user.userid): - tmpaccesstoken = request.query['accessToken'] - body = { - "code": bumper.RETURN_API_SUCCESS, - "data": { - "accessToken": tmpaccesstoken, #Random chars 32 length - "country": countrycode, - "email": "null@null.com", - "uid": "fuid_{}".format(user.userid), - "username": "fusername_{}".format(user.userid), - }, - "msg": "操作成功", + confserverlog.info('client with devid {} attempting login'.format(user_devid)) + if bumper.use_auth: + if not user_devid == "": #Performing basic "auth" using devid, super insecure + users = self.bumper_users.get() + for user in users: + if user_devid in user.devices: + tmpaccesstoken = '' + if 'checkLogin' in request.path: + if request.query['accessToken'] in user.tokens and request.query['uid'] == "fuid_{}".format(user.userid): + tmpaccesstoken = request.query['accessToken'] + body = { + "code": bumper.RETURN_API_SUCCESS, + "data": { + "accessToken": tmpaccesstoken, #Random chars 32 length + "country": countrycode, + "email": "null@null.com", + "uid": "fuid_{}".format(user.userid), + "username": "fusername_{}".format(user.userid), + }, + "msg": "操作成功", + "time": bumper.get_milli_time(time.time()) + } + else: + body = { + "code": bumper.ERR_TOKEN_INVALID, + "data": None, + "msg": "当前密码错误", "time": bumper.get_milli_time(time.time()) - } + } else: + if tmpaccesstoken == '': + tmpaccesstoken = uuid.uuid4().hex + user.add_token(tmpaccesstoken) + body = { - "code": bumper.ERR_TOKEN_INVALID, - "data": None, - "msg": "当前密码错误", - "time": bumper.get_milli_time(time.time()) - } - else: - if tmpaccesstoken == '': - tmpaccesstoken = uuid.uuid4().hex - user.add_token(tmpaccesstoken) - - body = { - "code": bumper.RETURN_API_SUCCESS, - "data": { - "accessToken": tmpaccesstoken, #Random chars 32 length - "country": countrycode, - "email": "null@null.com", - "uid": "fuid_{}".format(user.userid), - "username": "fusername_{}".format(user.userid), - }, - "msg": "操作成功", - "time": bumper.get_milli_time(time.time()) - } - self.bumper_users.set(users) - - return web.json_response(body) - - body = { - "code": bumper.ERR_USER_NOT_ACTIVATED, - "data": None, - "msg": "当前密码错误", - "time": bumper.get_milli_time(time.time()) - } - - return web.json_response(body) - + "code": bumper.RETURN_API_SUCCESS, + "data": { + "accessToken": tmpaccesstoken, #Random chars 32 length + "country": countrycode, + "email": "null@null.com", + "uid": "fuid_{}".format(user.userid), + "username": "fusername_{}".format(user.userid), + }, + "msg": "操作成功", + "time": bumper.get_milli_time(time.time()) + } + self.bumper_users.set(users) + + return web.json_response(body) + + body = { + "code": bumper.ERR_USER_NOT_ACTIVATED, + "data": None, + "msg": "当前密码错误", + "time": bumper.get_milli_time(time.time()) + } + + return web.json_response(body) + else: + return web.json_response(self._auth_any(user_devid, countrycode, request)) except Exception as e: confserverlog.exception('{}'.format(e)) + def _auth_any(self, devid, country, request): + try: + user_devid = devid + countrycode = country + + tmpaccesstoken = '' + users = self.bumper_users.get() + bots = self.bumper_bots.get() + + if len(users) > 0: + tmpuser = users[0] + tmpuser.add_device(user_devid) + else: + tmpuser = bumper.BumperUser('tmpuser') + users.append(tmpuser) + tmpuser.add_device(user_devid) + + for bot in bots: + tmpuser.add_bot(bot.did) + + if 'checkLogin' in request.path: + tmpaccesstoken = request.query['accessToken'] + tmpuser.add_token(tmpaccesstoken) + body = { + "code": bumper.RETURN_API_SUCCESS, + "data": { + "accessToken": tmpaccesstoken, #Random chars 32 length + "country": countrycode, + "email": "null@null.com", + "uid": "fuid_{}".format(tmpuser.userid), + "username": "fusername_{}".format(tmpuser.userid), + }, + "msg": "操作成功", + "time": bumper.get_milli_time(time.time()) + } + else: + if tmpaccesstoken == '': + tmpaccesstoken = uuid.uuid4().hex + tmpuser.add_token(tmpaccesstoken) + + body = { + "code": bumper.RETURN_API_SUCCESS, + "data": { + "accessToken": tmpaccesstoken, #Random chars 32 length + "country": countrycode, + "email": "null@null.com", + "uid": "fuid_{}".format(tmpuser.userid), + "username": "fusername_{}".format(tmpuser.userid), + }, + "msg": "操作成功", + "time": bumper.get_milli_time(time.time()) + } + self.bumper_users.set(users) + + return body + + except Exception as e: + confserverlog.exception('{}'.format(e)) + async def handle_logout(self, request): try: user_devid = request.match_info.get('devid', "") @@ -225,30 +286,31 @@ class ConfServer(): user_devid = request.match_info.get('devid', "") if not user_devid == "": users = self.bumper_users.get() - for user in users: - if user_devid in user.devices and request.query['accessToken'] in user.tokens: - countrycode = request.match_info.get('country', "us") - tmpauthcode = "{}_{}".format(countrycode,uuid.uuid4().hex) - user.add_authcode(tmpauthcode) - - body = { - "code": bumper.RETURN_API_SUCCESS, - "data": { - "authCode": tmpauthcode, - "ecovacsUid": request.query['uid'] - }, - "msg": "操作成功", - "time": bumper.get_milli_time(time.time()) - } - self.bumper_users.set(users) - return web.json_response(body) + if len(users) > 0: + for user in users: + if user_devid in user.devices and request.query['accessToken'] in user.tokens: + countrycode = request.match_info.get('country', "us") + tmpauthcode = "{}_{}".format(countrycode,uuid.uuid4().hex) + user.add_authcode(tmpauthcode) + + body = { + "code": bumper.RETURN_API_SUCCESS, + "data": { + "authCode": tmpauthcode, + "ecovacsUid": request.query['uid'] + }, + "msg": "操作成功", + "time": bumper.get_milli_time(time.time()) + } + self.bumper_users.set(users) + return web.json_response(body) body = { "code": bumper.ERR_TOKEN_INVALID, "data": None, "msg": "当前密码错误", "time": bumper.get_milli_time(time.time()) - } + } return web.json_response(body) @@ -412,12 +474,18 @@ class ConfServer(): async def handle_devmanager_botcommand(self, request): try: json_body = json.loads(await request.text()) - randomid = ''.join(random.sample(string.ascii_letters,6)) - 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) + 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)) + 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) except Exception as e: confserverlog.exception('{}'.format(e)) diff --git a/bumper/mqttserver.py b/bumper/mqttserver.py index d3246bf..91e733c 100644 --- a/bumper/mqttserver.py +++ b/bumper/mqttserver.py @@ -284,51 +284,36 @@ class BumperMQTTServer_Plugin: didsplit = str(client_id).split("@") #If this isn't a fake user (fuid) then add as a bot if not (str(didsplit[0]).startswith("fuid") or str(didsplit[0]).startswith("helper")): - tmpbotdetail = str(didsplit[1]).split("/") - newbot = bumper.VacBotDevice() - newbot.did = didsplit[0] - newbot.name = username - newbot.vac_bot_device_class = tmpbotdetail[0] - newbot.resource = tmpbotdetail[1] - existingbot = False - for bot in bumper_bots: - if bot.did == newbot.did: - existingbot = True - - if existingbot == False: - bumper_bots.append(newbot) - mqttserverlog.info("new bot authenticated {}".format(newbot.name)) - self.bumper_config['bumper_bots'].set(bumper_bots) - + tmpbotdetail = str(didsplit[1]).split("/") + bumper.add_bot(username, didsplit[0], tmpbotdetail[0], tmpbotdetail[1]) + mqttserverlog.debug("new bot authenticated SN: {} DID: {}".format(username, didsplit[0])) authenticated = True else: - if didsplit[0] == "helper1": - authenticated = True + tmpclientdetail = str(didsplit[1]).split("/") + userid = didsplit[0] + realm = tmpclientdetail[0] + resource = tmpclientdetail[1] - elif bumper.check_authcode(didsplit[0], password): - tmpclientdetail = str(didsplit[1]).split("/") - newclient = bumper.VacBotClient() - newclient.userid = didsplit[0] - newclient.realm = tmpclientdetail[0] - newclient.resource = tmpclientdetail[1] - existingclient = False - for client in bumper_clients: - if client.userid == newclient.userid: - existingclient = True - - if existingclient == False: - bumper_clients.append(newclient) - mqttserverlog.info("new client authenticated {}".format(newclient.userid)) - self.bumper_config['bumper_clients'].set(bumper_clients) - + if userid == "helper1": authenticated = True - else: - authenticated = False + auth = False + if bumper.check_authcode(didsplit[0], password): + auth = True + elif bumper.use_auth == False: + auth = True + + if auth: + bumper.add_client(userid, realm, resource) + mqttserverlog.debug("client authenticated {}".format(userid)) + authenticated = True + + else: + authenticated = False except KeyError: - self.context.logger.warning("Session informations not available") + self.context.logger.warning("Session information not available") authenticated = False return authenticated @@ -343,13 +328,13 @@ class BumperMQTTServer_Plugin: for bot in bumper_bots: if didsplit[0] == bot.did: bot.mqtt_connection = True - mqttserverlog.info("bot connected {}".format(bot.did)) + #mqttserverlog.info("bot connected {}".format(bot.did)) self.bumper_config['bumper_bots'].set(bumper_bots) for client in bumper_clients: if didsplit[0] == client.userid and client.userid != 'helper1': client.mqtt_connection = True - mqttserverlog.info("client connected {}".format(client.userid)) + #mqttserverlog.info("client connected {}".format(client.userid)) self.bumper_config['bumper_clients'].set(bumper_clients) except Exception as e: @@ -366,13 +351,13 @@ class BumperMQTTServer_Plugin: for bot in bumper_bots: if didsplit[0] == bot.did: bot.mqtt_connection = False - mqttserverlog.info("bot disconnected {}".format(bot.did)) + #mqttserverlog.info("bot disconnected {}".format(bot.did)) self.bumper_config['bumper_bots'].set(bumper_bots) for client in bumper_clients: if didsplit[0] == client.userid and client.userid != 'helper1': client.mqtt_connection = False - mqttserverlog.info("client disconnected {}".format(client.userid)) + #mqttserverlog.info("client disconnected {}".format(client.userid)) self.bumper_config['bumper_clients'].set(bumper_clients) except Exception as e: diff --git a/bumper/xmppserver.py b/bumper/xmppserver.py index 552da52..a71b7c9 100644 --- a/bumper/xmppserver.py +++ b/bumper/xmppserver.py @@ -185,14 +185,14 @@ class Client(threading.Thread): for bot in bumper_bots: if self.uid == bot.did: bot.xmpp_connection = False - xmppserverlog.info("bot disconnected {}".format(bot.did)) + #xmppserverlog.info("bot disconnected {}".format(bot.did)) self.bumper_bots.set(bumper_bots) for client in bumper_clients: if self.uid == client.userid and client.userid != 'helper1': client.xmpp_connection = False - xmppserverlog.info("client disconnected {}".format(client.userid)) + #xmppserverlog.info("client disconnected {}".format(client.userid)) self.bumper_clients.set(bumper_clients) #xmppserverlog.debug('client {} with resource {} disconnecting'.format(self.address, self.clientresource)) @@ -356,37 +356,46 @@ class Client(threading.Thread): elif 'password' in aitem.tag: password = aitem.text.split("/")[2] + authcode = password elif 'resource' in aitem.tag: self.clientresource = aitem.text - - if bumper.check_authcode(self.uid, password): - bumper_bots = self.bumper_bots.get() - bumper_clients = self.bumper_clients.get() - for bot in bumper_bots: - if self.uid == bot.did: - bot.xmpp_connection = True - xmppserverlog.info("bot connected {}".format(bot.did)) - - self.bumper_config['bumper_bots'].set(bumper_bots) + resource = self.clientresource - for client in bumper_clients: - if self.uid == client.userid and client.userid != 'helper1': - client.xmpp_connection = True - xmppserverlog.info("client connected {}".format(client.userid)) + if not self.uid.startswith("fuid"): + + #Need sample data to see details here + bumper.add_bot('',self.uid, '', resource) + xmppserverlog.info("bot authenticated {}".format(self.uid)) - self.bumper_config['bumper_clients'].set(bumper_clients) - #Client authenticated, move to next state self._set_state('INIT') #Successful auth - self.send(''.format(xml.get('id'))) + 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 - else: - #Failed auth - self.send(''.format(xml.get('id'))) - + if auth: + bumper.add_client(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 - {} - {} - this is common with ecovac protocol'.format(data.decode('utf-8'), e)) @@ -409,17 +418,36 @@ class Client(threading.Thread): self.clientresource = resource authcode = saslauth[2] - if bumper.check_authcode(self.uid, authcode): - + if not self.uid.startswith("fuid"): + #Need sample data to see details here + bumper.add_bot('',self.uid, '', resource) + xmppserverlog.info("bot authenticated {}".format(self.uid)) #Send response self.send('') #Success #Client authenticated, move to next state - self._set_state('INIT') + 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: + bumper.add_client(self.uid, 'bumper', self.clientresource) + xmppserverlog.debug("client authenticated {}".format(self.uid)) - else: - #Failed to authenticate - self.send('') #Fail + #Client authenticated, move to next state + self._set_state('INIT') + + #Send response + self.send('') #Success + + else: + #Failed to authenticate + self.send('') #Fail except ET.ParseError as e: if "no element found" in e.msg: @@ -433,7 +461,22 @@ class Client(threading.Thread): xmppserverlog.exception('{}'.format(e)) def _handle_bind(self, xml): - try: + try: + bumper_bots = self.bumper_bots.get() + bumper_clients = self.bumper_clients.get() + + for bot in bumper_bots: + if self.uid == bot.did: + bot.xmpp_connection = True + #xmppserverlog.info("bot connected {}".format(bot.did)) + self.bumper_bots.set(bumper_bots) + + for client in bumper_clients: + if self.uid == client.userid: + client.xmpp_connection = True + #xmppserverlog.info("client connected {}".format(client.userid)) + self.bumper_clients.set(bumper_clients) + clientbindxml = xml.getchildren() clientresourcexml = clientbindxml[0].getchildren() if len(clientresourcexml) > 0: @@ -546,7 +589,7 @@ class Client(threading.Thread): def run(self): - xmppserverlog.info('client connected - {}'.format(self.address)) + #xmppserverlog.info('client connected - {}'.format(self.address)) self._set_state('CONNECT') while not self.state == self.DISCONNECT and not self.connection._closed: