class WebRequestHandler(Plugin): """Default handler for the /web subpath""" authObservers = ObserverCollection(IWebRequestAuthenticationHandler) implements(IWebRequestHandler) @staticmethod def getTemplatesDirs(): return [resource_filename('web', 'templates')] def handleRequest(self, plugin, path, __params, **__kwargs): if plugin != 'web': return None if path == 'authFailed': return 'authFailed.html', {} if path == 'login': providers = [] for observer in self.authObservers: provider = observer.loginProvider() if provider is not None: providers.append(provider) if len(providers) == 1: # Only one provider. Use it raise cherrypy.HTTPRedirect(providers[0]['url']) return 'login.html', {'providers': providers} return None def isUrlAuthorized(self, request): if len(self.authObservers) == 0: raise cherrypy.HTTPRedirect( '/web/authFailed?reason=noAuthHandlersConfigured') for observer in self.authObservers: ret = observer.isUrlAuthorized(request) if ret is True: return True request.setSession( 'returnTo', '%s?%s' % (cherrypy.request.path_info, cherrypy.request.query_string)) raise cherrypy.HTTPRedirect('/web/login') @staticmethod def matchRequest(plugin, path): if plugin != 'web': return False if path in ['authFailed', 'login']: return True return False @staticmethod def requireAuthentication(plugin, __path): return plugin != 'web'
class React(Plugin): implements(IWebRequestHandler) observers = ObserverCollection(IWebReactHandler) def __init__(self): pass def components(self): retval = {} for o in self.observers: components = o.getReactComponents() if type(components) != dict: continue # Make sure defaults exists for name in components: tags = components[name].setdefault('tags', []) if 'menu' in tags: components[name].setdefault('path', '/%s' % name) retval.update(components) return retval def getTemplatesDirs(self): return [resource_filename('telldus', 'templates')] def handleRequest(self, plugin, path, params, request): if path == '' and plugin == '': return WebResponseHtml('react.html') if plugin == 'telldus': if path == 'reactComponents': return WebResponseJson(self.components()) if path in ['settings']: return WebResponseHtml('react.html') fullPath = '/%s/%s' % (plugin, path) if path is not '' else '/%s' % plugin components = self.components() for name in components: if components[name].get('path', None) == fullPath: return WebResponseHtml('react.html') return None def matchRequest(self, plugin, path): if path == '' and plugin == '': return True if plugin == 'telldus' and path in ['reactComponents', 'settings']: return True # Check if we match a react route fullPath = '/%s/%s' % (plugin, path) if path is not '' else '/%s' % plugin components = self.components() for name in components: if components[name].get('path', None) == fullPath: return True return False
class React(Plugin): implements(IWebRequestHandler) observers = ObserverCollection(IWebReactHandler) def __init__(self): pass def getTemplatesDirs(self): return [resource_filename('telldus', 'templates')] def handleRequest(self, plugin, path, params, request): if path == '' and plugin == '': return WebResponseHtml('react.html') if plugin == 'telldus': if path == 'reactPlugins': return WebResponseJson(self.routes()) fullPath = '/%s/%s' % (plugin, path) if path is not '' else '/%s' % plugin for route in self.routes(): if route['path'] == fullPath: return WebResponseHtml('react.html') return None def matchRequest(self, plugin, path): if path == '' and plugin == '': return True if plugin == 'telldus' and path in ['reactPlugins']: return True # Check if we match a react route fullPath = '/%s/%s' % (plugin, path) if path is not '' else '/%s' % plugin for route in self.routes(): if route['path'] == fullPath: return True return False def routes(self): plugins = [] for o in self.observers: routes = o.getReactRoutes() if type(routes) != list: continue for route in routes: if 'path' not in route: route['path'] = '/%s' % route['name'] plugins.extend(routes) return plugins
class SSDP(Plugin): observers = ObserverCollection(ISSDPNotifier) def __init__(self): self.rootDevices = {} self.devices = {} Application().registerScheduledTask(fn=self.startDiscover, minutes=10, runAtOnce=True) def startDiscover(self): t = Thread(target=self.discover, name='SSDP discoverer') t.daemon = True t.start() def discover(self): service = "ssdp:all" group = ("239.255.255.250", 1900) message = "\r\n".join([ 'M-SEARCH * HTTP/1.1', 'HOST: {0}:{1}', 'MAN: "ssdp:discover"', 'ST: {st}', 'MX: 3', '', '' ]) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.settimeout(5) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) sock.sendto(message.format(*group, st=service), group) while True: try: response = SSDPResponse(sock.recv(1024)) if response.type == SSDPResponse.ST_ROOT_DEVICE: pass elif response.type == SSDPResponse.ST_DEVICE: device = Device.fromSSDPResponse(response) self.devices[response.uuid] = device except socket.timeout: break self.__discoveryDone() @mainthread def __discoveryDone(self): for i in self.devices: self.observers.ssdpDeviceFound(self.devices[i])
class RequestHandler(object): observers = ObserverCollection(IWebRequestHandler) def __init__(self, context): self.templates = None self.context = context @staticmethod def loadTemplate(filename, dirs): if not isinstance(dirs, list): dirs = [] dirs.append(resource_filename('web', 'templates')) templates = TemplateLoader(dirs) return templates.load(filename) def handle(self, plugin, path, **params): path = '/'.join(path) # First check for the file in htdocs try: if plugin != '' and \ resource_exists(plugin, 'htdocs/' + path) and \ resource_isdir(plugin, 'htdocs/' + path) is False: mimetype, __encoding = mimetypes.guess_type(path, strict=False) if mimetype is not None: cherrypy.response.headers['Content-Type'] = mimetype return resource_stream(plugin, 'htdocs/' + path) except Exception as __error: pass menu = [] for observer in self.observers: arr = observer.getMenuItems() if isinstance(arr, list): menu.extend(arr) template = None templateDirs = [] response = None request = WebRequest(cherrypy.request) for observer in self.observers: if not observer.matchRequest(plugin, path): continue requireAuth = observer.requireAuthentication(plugin, path) if requireAuth != False: WebRequestHandler(self.context).isUrlAuthorized(request) response = observer.handleRequest(plugin, path, params, request=request) templateDirs = observer.getTemplatesDirs() break if response is None: raise cherrypy.NotFound() if isinstance(response, WebResponseRedirect): if response.url[:4] == 'http': raise cherrypy.HTTPRedirect(response.url) raise cherrypy.HTTPRedirect( '/%s%s%s' % (plugin, '' if response.url[0] == '/' else '/', response.url)) elif isinstance(response, WebResponse): if isinstance(response, WebResponseHtml): response.setDirs(plugin, templateDirs) cherrypy.response.status = response.statusCode response.output(cherrypy.response) return response.data template, data = response if template is None: raise cherrypy.NotFound() tmpl = self.loadTemplate(template, templateDirs) data['menu'] = menu stream = tmpl.generate(title='TellStick ZNet', **data) return stream.render('html', doctype='html') def __call__(self, plugin='', *args, **kwargs): if plugin == 'ws': # Ignore, this is for websocket return path = [x for x in args] return self.handle(plugin, path, **kwargs)
class TelldusLive(Plugin): observers = ObserverCollection(ITelldusLiveObserver) def __init__(self): logging.info("Telldus Live! loading") self.email = '' self.supportedMethods = 0 self.connected = False self.registered = False self.serverList = ServerList() Application().registerShutdown(self.stop) self.s = Settings('tellduslive.config') self.uuid = self.s['uuid'] self.conn = ServerConnection() self.pingTimer = 0 self.thread = threading.Thread(target=self.run) if self.conn.publicKey != '': # Only connect if the keys has been set. self.thread.start() @mainthread def handleMessage(self, message): if (message.name() == "notregistered"): self.email = '' self.connected = True self.registered = False params = message.argument(0).dictVal self.s['uuid'] = params['uuid'].stringVal logging.info( "This client isn't activated, please activate it using this url:\n%s", params['url'].stringVal) self.observers.liveConnected() return if (message.name() == "registered"): self.connected = True self.registered = True data = message.argument(0).toNative() if 'email' in data: self.email = data['email'] self.observers.liveRegistered(data) return if (message.name() == "command"): # Extract ACK and handle it args = message.argument(0).dictVal if 'ACK' in args: msg = LiveMessage("ACK") msg.append(args['ACK'].intVal) self.send(msg) if (message.name() == "pong"): return if (message.name() == "disconnect"): self.conn.close() self.__disconnected() return handled = False for o in self.observers: for f in getattr(o, '_telldusLiveHandlers', {}).get(message.name(), []): f(o, message) handled = True if not handled: logging.warning("Did not understand: %s", message.toByteArray()) def isConnected(self): return self.connected def isRegistered(self): return self.registered def run(self): self.running = True wait = 0 pongTimer, self.pingTimer = (0, 0) while self.running: if wait > 0: wait = wait - 1 time.sleep(1) continue state = self.conn.process() if state == ServerConnection.CLOSED: server = self.serverList.popServer() if not server: wait = random.randint(60, 300) logging.warning("No servers found, retry in %i seconds", wait) continue if not self.conn.connect(server['address'], int( server['port'])): wait = random.randint(60, 300) logging.warning("Could not connect, retry in %i seconds", wait) elif state == ServerConnection.CONNECTED: pongTimer, self.pingTimer = (time.time(), time.time()) self.__sendRegisterMessage() elif state == ServerConnection.MSG_RECEIVED: msg = self.conn.popMessage() if msg is None: continue pongTimer = time.time() self.handleMessage(msg) elif state == ServerConnection.DISCONNECTED: wait = random.randint(10, 50) logging.warning("Disconnected, reconnect in %i seconds", wait) self.__disconnected() else: if (time.time() - pongTimer >= 360): # No pong received self.conn.close() wait = random.randint(10, 50) logging.warning( "No pong received, disconnecting. Reconnect in %i seconds", wait) self.__disconnected() elif (time.time() - self.pingTimer >= 120): # Time to ping self.conn.send(LiveMessage("Ping")) self.pingTimer = time.time() def stop(self): self.running = False def send(self, message): self.conn.send(message) self.pingTimer = time.time() def pushToWeb(self, module, action, data): msg = LiveMessage("sendToWeb") msg.append(module) msg.append(action) msg.append(data) self.send(msg) def __disconnected(self): self.email = '' self.connected = False self.registered = False def sendNotification(): self.observers.liveDisconnected() # Syncronize signal with main thread Application().queue(sendNotification) @staticmethod def handler(message): def call(fn): import sys frame = sys._getframe(1) frame.f_locals.setdefault('_telldusLiveHandlers', {}).setdefault(message, []).append(fn) return fn return call def __sendRegisterMessage(self): print("Send register") msg = LiveMessage('Register') msg.append({ 'key': self.conn.publicKey, 'mac': TelldusLive.getMacAddr(Board.networkInterface()), 'secret': Board.secret(), 'hash': 'sha1' }) msg.append({ 'protocol': 3, 'version': Board.firmwareVersion(), 'os': 'linux', 'os-version': 'telldus' }) self.conn.send(msg) @staticmethod def getMacAddr(ifname): addrs = netifaces.ifaddresses(ifname) try: mac = addrs[netifaces.AF_LINK][0]['addr'] except (IndexError, KeyError) as e: return '' return mac.upper().replace(':', '')
class DeviceManager(Plugin): """The devicemanager holds and manages all the devices in the server""" implements(ITelldusLiveObserver) observers = ObserverCollection(IDeviceChange) def __init__(self): self.devices = [] self.s = Settings('telldus.devicemanager') self.nextId = self.s.get('nextId', 0) self.live = TelldusLive(self.context) self.registered = False self.__load() @mainthread def addDevice(self, device): """Call this function to register a new device to the device manager. .. note:: The :func:`localId` function in the device must return a unique id for the transport type returned by :func:`typeString` """ cachedDevice = None for i, delDevice in enumerate(self.devices): # Delete the cached device from loaded devices, since it is replaced by a confirmed/specialised one if delDevice.localId() == device.localId() and device.typeString( ) == delDevice.typeString() and not delDevice.confirmed(): cachedDevice = delDevice del self.devices[i] break self.devices.append(device) device.setManager(self) if not cachedDevice: # New device, not stored in local cache self.nextId = self.nextId + 1 device.setId(self.nextId) else: # Transfer parameters from the loaded one device.loadCached(cachedDevice) self.save() if not cachedDevice: self.__deviceAdded(device) if self.live.registered and device.isDevice(): (state, stateValue) = device.state() deviceDict = { 'id': device.id(), 'name': device.name(), 'methods': device.methods(), 'state': state, 'stateValue': stateValue, 'protocol': device.protocol(), 'model': device.model(), 'transport': device.typeString() } msg = LiveMessage("DeviceAdded") msg.append(deviceDict) self.live.send(msg) else: # Previously cached device is now confirmed, TODO notify Live! about this too? self.observers.deviceConfirmed(device) def device(self, deviceId): """Retrieves a device. Returns: the device specified by `deviceId` or None of no device was found """ for d in self.devices: if d.id() == deviceId: return d return None def findByName(self, name): for d in self.devices: if d.name() == name: return d return None def finishedLoading(self, type): """ Finished loading all devices of this type. If there are any unconfirmed, these should be deleted """ for device in self.devices: if device.typeString() == type and not device.confirmed(): self.removeDevice(device.id()) def removeDevice(self, deviceId): """Removes a device. .. warning:: This function may only be called by the module supplying the device since removing of a device may be transport specific. """ isDevice = True for i, device in enumerate(self.devices): if device.id() == deviceId: self.__deviceRemoved(deviceId) isDevice = self.devices[i].isDevice() del self.devices[i] break self.save() if self.live.registered and isDevice: msg = LiveMessage("DeviceRemoved") msg.append({'id': deviceId}) self.live.send(msg) def retrieveDevices(self, deviceType=None): """Retrieve a list of devices. Args: :deviceType: If this parameter is set only devices with this type is returned Returns: Returns a list of devices """ l = [] for d in self.devices: if deviceType is not None and d.typeString() != deviceType: continue l.append(d) return l @signal def sensorValueUpdated(self, device, valueType, value, scale): """ Called every time a sensors value is updated. """ if device.isSensor() == False: return self.observers.sensorValueUpdated(device, valueType, value, scale) if not self.live.registered or device.ignored(): # don't send if ignored return msg = LiveMessage("SensorEvent") sensor = { 'name': device.name(), 'protocol': device.protocol(), 'model': device.model(), 'sensor_id': device.id(), } battery = device.battery() if battery is not None: sensor['battery'] = battery msg.append(sensor) values = device.sensorValues() valueList = [] for valueType in values: for value in values[valueType]: valueList.append({ 'type': valueType, 'lastUp': str(int(time.time())), 'value': str(value['value']), 'scale': value['scale'] }) msg.append(valueList) self.live.send(msg) def stateUpdated(self, device, ackId=None, origin=None): if device.isDevice() == False: return extras = {} if ackId: extras['ACK'] = ackId if origin: extras['origin'] = origin else: extras['origin'] = 'Incoming signal' (state, stateValue) = device.state() self.__deviceStateChanged(device, state, stateValue) self.save() if not self.live.registered: return msg = LiveMessage("DeviceEvent") msg.append(device.id()) msg.append(state) msg.append(str(stateValue)) msg.append(extras) self.live.send(msg) def stateUpdatedFail(self, device, state, stateValue, reason, origin): if not self.live.registered: return if device.isDevice() == False: return extras = { 'reason': reason, } if origin: extras['origin'] = origin else: extras['origin'] = 'Unknown' (state, stateValue) = device.state() self.__deviceStateChanged(device, state, stateValue) msg = LiveMessage('DeviceFailEvent') msg.append(device.id()) msg.append(state) msg.append(stateValue) msg.append(extras) self.live.send(msg) @TelldusLive.handler('command') def __handleCommand(self, msg): args = msg.argument(0).toNative() action = args['action'] value = args['value'] if 'value' in args else None id = args['id'] device = None for dev in self.devices: if dev.id() == id: device = dev break def success(state, stateValue): if 'ACK' in args: device.setState(state, stateValue, ack=args['ACK']) # Abort the DeviceEvent this triggered raise DeviceAbortException() def fail(reason): # We failed to set status for some reason, nack the server if 'ACK' in args: msg = LiveMessage('NACK') msg.append({ 'ackid': args['ACK'], 'reason': reason, }) self.live.send(msg) # Abort the DeviceEvent this triggered raise DeviceAbortException() device.command(action, value, success=success, failure=fail) @TelldusLive.handler('device') def __handleDeviceCommand(self, msg): args = msg.argument(0).toNative() if 'action' not in args: return if args['action'] == 'setName': if 'name' not in args or args['name'] == '': return for dev in self.devices: if dev.id() != args['device']: continue if type(args['name']) is int: dev.setName(str(args['name'])) else: dev.setName(args['name'].decode('UTF-8')) if dev.isDevice(): self.__sendDeviceReport() if dev.isSensor: self.__sendSensorReport( True) # force name change even for ignored sensor return @TelldusLive.handler('reload') def __handleSensorUpdate(self, msg): reloadType = msg.argument(0).toNative() if reloadType != 'sensor': # not for us return data = msg.argument(1).toNative() if not msg.argument(2) or 'sensorId' not in msg.argument(2).toNative(): # nothing to do, might be an orphaned zwave sensor return sensorId = msg.argument(2).toNative()['sensorId'] updateType = data['type'] for dev in self.devices: if dev.id() == sensorId: if updateType == 'updateignored': value = data['ignored'] if dev.ignored() == value: return dev.setIgnored(value) self.__sendSensorChange(sensorId, updateType, value) return def liveRegistered(self, msg): self.registered = True self.__sendDeviceReport() self.__sendSensorReport() def __load(self): self.store = self.s.get('devices', []) for dev in self.store: if 'type' not in dev or 'localId' not in dev: continue # This should not be possible d = CachedDevice(dev) # If we have loaded this device from cache 5 times in a row it's # considered dead if d.loadCount() < 5: self.devices.append(d) @signal('deviceAdded') def __deviceAdded(self, device): """ Called every time a device is added/created """ self.observers.deviceAdded(device) @signal('deviceRemoved') def __deviceRemoved(self, deviceId): """ Called every time a device is removed. The parameter deviceId is the old device id. The ref to the device is no longer available """ self.observers.deviceRemoved(deviceId) @signal('deviceStateChanged') def __deviceStateChanged(self, device, state, stateValue): """ Called every time the state of a device is changed. """ self.observers.stateChanged(device, state, stateValue) def save(self): data = [] for d in self.devices: (state, stateValue) = d.state() data.append({ "id": d.id(), "loadCount": d.loadCount(), "localId": d.localId(), "type": d.typeString(), "name": d.name(), "params": d.params(), "methods": d.methods(), "state": state, "stateValue": stateValue, "ignored": d.ignored() }) self.s['devices'] = data self.s['nextId'] = self.nextId def __sendDeviceReport(self): if not self.live.registered: return l = [] for d in self.devices: if not d.isDevice(): continue (state, stateValue) = d.state() device = { 'id': d.id(), 'name': d.name(), 'methods': d.methods(), 'state': state, 'stateValue': stateValue, 'protocol': d.protocol(), 'model': d.model(), 'transport': d.typeString(), 'ignored': d.ignored() } battery = d.battery() if battery is not None: device['battery'] = battery l.append(device) msg = LiveMessage("DevicesReport") msg.append(l) self.live.send(msg) def __sendSensorChange(self, sensorid, valueType, value): msg = LiveMessage("SensorChange") device = None for d in self.devices: if d.id() == sensorid: device = d break if not device: return sensor = { 'protocol': device.typeString(), 'model': device.model(), 'sensor_id': device.id(), } msg.append(sensor) msg.append(valueType) msg.append(value) self.live.send(msg) def __sendSensorReport(self, forceIgnored=False): if not self.live.registered: return l = [] for d in self.devices: if d.isSensor() == False or (d.ignored() and not forceIgnored): continue sensorFrame = [] sensor = { 'name': d.name(), 'protocol': d.protocol(), 'model': d.model(), 'sensor_id': d.id(), } battery = d.battery() if battery is not None: sensor['battery'] = battery sensorFrame.append(sensor) valueList = [] # TODO(micke): Add current values sensorFrame.append(valueList) l.append(sensorFrame) msg = LiveMessage("SensorsReport") msg.append(l) self.live.send(msg)
class ApiManager(Plugin): implements(IWebRequestHandler) observers = ObserverCollection(IApiCallHandler) def __init__(self): self.tokens = {} self.tokenKey = None def getTemplatesDirs(self): return [resource_filename('api', 'templates')] def matchRequest(self, plugin, path): if plugin != 'api': return False return True def handleRequest(self, plugin, path, params, request, **kwargs): if path == '': methods = {} for o in self.observers: for module, actions in getattr(o, '_apicalls', {}).iteritems(): for action, fn in actions.iteritems(): methods.setdefault(module, {})[action] = { 'doc': fn.__doc__ } return 'index.html', {'methods': methods} if path == 'token': if request.method() == 'PUT': token = uuid.uuid4().hex self.tokens[token] = { 'app': request.post('app'), 'authorized': False, } return WebResponseJson({ 'authUrl': '%s/api/authorize?token=%s' % (request.base(), token), 'token': token }) elif request.method() == 'GET': token = params.get('token', None) if token is None: return WebResponseJson({'error': 'No token specified'}, statusCode=400) if token not in self.tokens: return WebResponseJson({'error': 'No such token'}, statusCode=404) if self.tokens[token]['authorized'] is not True: return WebResponseJson( {'error': 'Token is not authorized'}, statusCode=403) claims = { 'aud': self.tokens[token]['app'], 'exp': int(time.time() + self.tokens[token]['ttl']), } body = {} if self.tokens[token]['allowRenew'] == True: body['renew'] = True body['ttl'] = self.tokens[token]['ttl'] accessToken = jwt.encode(body, self.__tokenKey(), algorithm='HS256', headers=claims) resp = WebResponseJson({ 'token': accessToken, 'expires': claims['exp'], 'allowRenew': self.tokens[token]['allowRenew'], }) del self.tokens[token] return resp if path == 'authorize': if 'token' not in params: return WebResponseJson({'error': 'No token specified'}, statusCode=400) token = params['token'] if token not in self.tokens: return WebResponseJson({'error': 'No such token'}, statusCode=404) if request.method() == 'POST': self.tokens[token]['authorized'] = True self.tokens[token]['allowRenew'] = bool( request.post('extend', False)) self.tokens[token]['ttl'] = int(request.post('ttl', 0)) * 60 return 'authorize.html', {'token': self.tokens[token]} # Check authorization token = request.header('Authorization') if token is None: return WebResponseJson( {'error': 'No token was found in the request'}, statusCode=401) if not token.startswith('Bearer '): return WebResponseJson( { 'error': 'The autorization token must be supplied as a bearer token' }, statusCode=401) token = token[7:] try: body = jwt.decode(token, self.__tokenKey(), algorithms='HS256') except JWSError as e: return WebResponseJson({'error': str(e)}, statusCode=401) claims = jwt.get_unverified_headers(token) if 'exp' not in claims or claims['exp'] < time.time(): return WebResponseJson({'error': 'The token has expired'}, statusCode=401) if 'aud' not in claims or claims['aud'] is None: return WebResponseJson( {'error': 'No app was configured in the token'}, statusCode=401) aud = claims['aud'] if path == 'refreshToken': if 'renew' not in body or body['renew'] != True: return WebResponseJson( {'error': 'The token is not authorized for refresh'}, statusCode=403) if 'ttl' not in body: return WebResponseJson( {'error': 'No TTL was specified in the token'}, statusCode=401) ttl = body['ttl'] exp = int(time.time() + ttl) accessToken = jwt.encode({ 'renew': True, 'ttl': ttl }, self.__tokenKey(), algorithm='HS256', headers={ 'aud': aud, 'exp': exp, }) return WebResponseJson({ 'token': accessToken, 'expires': exp, }) paths = path.split('/') if len(paths) < 2: return None module = paths[0] action = paths[1] for o in self.observers: fn = getattr(o, '_apicalls', {}).get(module, {}).get(action, None) if fn is None: continue try: params['app'] = aud retval = fn(o, **params) except Exception as e: logging.exception(e) return WebResponseJson({'error': str(e)}) if retval == True: retval = {'status': 'success'} return WebResponseJson(retval) return WebResponseJson( {'error': 'The method %s/%s does not exist' % (module, action)}, statusCode=404) def requireAuthentication(self, plugin, path): if plugin != 'api': return if path in ['', 'authorize']: return True return False def __tokenKey(self): if self.tokenKey is not None: return self.tokenKey password = Board.secret() s = Settings('telldus.api') tokenKey = s.get('tokenKey', '') if tokenKey == '': self.tokenKey = os.urandom(32) # Store it salt = os.urandom(16) key = PBKDF2(password, salt).read(32) pwhash = crypt(password) s['salt'] = base64.b64encode(salt) s['pw'] = pwhash # Encrypt token key cipher = AES.new(key, AES.MODE_ECB, '') s['tokenKey'] = base64.b64encode(cipher.encrypt(self.tokenKey)) else: # Decode it salt = base64.b64decode(s.get('salt', '')) pwhash = s.get('pw', '') if crypt(password, pwhash) != pwhash: logging.warning('Could not decrypt token key, wrong password') return None key = PBKDF2(password, salt).read(32) enc = base64.b64decode(tokenKey) cipher = AES.new(key, AES.MODE_ECB, '') self.tokenKey = cipher.decrypt(enc) return self.tokenKey @staticmethod def apicall(module, action): def call(fn): import sys frame = sys._getframe(1) frame.f_locals.setdefault('_apicalls', {}).setdefault(module, {})[action] = fn return fn return call
class DeviceManager(Plugin): """The devicemanager holds and manages all the devices in the server""" implements(ITelldusLiveObserver) observers = ObserverCollection(IDeviceChange) public = True def __init__(self): self.devices = [] self.settings = Settings('telldus.devicemanager') self.nextId = self.settings.get('nextId', 0) self.live = TelldusLive(self.context) self.registered = False self.__load() @mainthread def addDevice(self, device): """ Call this function to register a new device to the device manager. .. note:: The :func:`Device.localId() <telldus.Device.localId>` function in the device must return a unique id for the transport type returned by :func:`Device.typeString() <telldus.Device.localId>` """ cachedDevice = None for i, delDevice in enumerate(self.devices): # Delete the cached device from loaded devices, since it is replaced # by a confirmed/specialised one if delDevice.localId() == device.localId() \ and device.typeString() == delDevice.typeString() \ and not delDevice.confirmed(): cachedDevice = delDevice del self.devices[i] break self.devices.append(device) device.setManager(self) if not cachedDevice: # New device, not stored in local cache self.nextId = self.nextId + 1 device.setId(self.nextId) else: # Transfer parameters from the loaded one device.loadCached(cachedDevice) self.save() if not cachedDevice: self.__deviceAdded(device) if self.live.registered and device.isDevice(): (state, stateValue) = device.state() deviceDict = { 'id': device.id(), 'name': device.name(), 'methods': device.methods(), 'state': state, 'stateValue': stateValue, 'protocol': device.protocol(), 'model': device.model(), 'transport': device.typeString() } msg = LiveMessage("DeviceAdded") msg.append(deviceDict) self.live.send(msg) else: # Previously cached device is now confirmed, TODO notify Live! about this too? self.observers.deviceConfirmed(device) def device(self, deviceId): """Retrieves a device. :param int deviceId: The id of the device to be returned. :returns: the device specified by `deviceId` or None of no device was found """ for device in self.devices: if device.id() == deviceId: return device return None def deviceParamUpdated(self, device, param): self.save() if param == 'name': if device.isDevice(): self.__sendDeviceReport() if device.isSensor: self.__sendSensorReport() def findByName(self, name): for device in self.devices: if device.name() == name: return device return None @mainthread def finishedLoading(self, deviceType): """ Finished loading all devices of this type. If there are any unconfirmed, these should be deleted """ for device in self.devices: if device.typeString() == deviceType and not device.confirmed(): self.removeDevice(device.id()) @mainthread def removeDevice(self, deviceId): """ Removes a device. .. warning:: This function may only be called by the module supplying the device since removing of a device may be transport specific. """ isDevice = True for i, device in enumerate(self.devices): if device.id() == deviceId: self.__deviceRemoved(deviceId) isDevice = self.devices[i].isDevice() del self.devices[i] break self.save() if self.live.registered and isDevice: msg = LiveMessage("DeviceRemoved") msg.append({'id': deviceId}) self.live.send(msg) @mainthread def removeDevicesByType(self, deviceType): """ .. versionadded:: 1.1.0 Remove all devices of a specific device type :param str deviceType: The type of devices to remove """ deviceIds = [] for device in self.devices: if device.typeString() == deviceType: deviceIds.append(device.id()) for deviceId in deviceIds: self.removeDevice(deviceId) def retrieveDevices(self, deviceType=None): """Retrieve a list of devices. :param deviceType: If this parameter is set only devices with this type is returned :type deviceType: str or None :returns: a list of devices """ lst = [] for device in self.devices: if deviceType is not None and device.typeString() != deviceType: continue lst.append(device) return lst @signal def sensorValueUpdated(self, device, valueType, value, scale): """ Called every time a sensors value is updated. """ if device.isSensor() is False: return self.observers.sensorValueUpdated(device, valueType, value, scale) if not self.live.registered or device.ignored(): # don't send if not connected to live or sensor is ignored return if valueType in device.lastUpdatedLive \ and (valueType in device.valueChangedTime \ and device.valueChangedTime[valueType] < device.lastUpdatedLive[valueType]) \ and device.lastUpdatedLive[valueType] > (int(time.time()) - 300): # no values have changed since the last live-update, and the last # time this sensor was sent to live was less than 5 minutes ago return msg = LiveMessage("SensorEvent") # pcc = packageCountChecked - already checked package count, # just accept it server side directly sensor = { 'name': device.name(), 'protocol': device.protocol(), 'model': device.model(), 'sensor_id': device.id(), 'pcc': 1, } battery = device.battery() if battery is not None: sensor['battery'] = battery msg.append(sensor) # small clarification: valueType etc that is sent in here is only used for sending # information about what have changed on to observers, below is instead all the values # of the sensor picked up and sent in a sensor event-message (the sensor values # have already been updated in other words) values = device.sensorValues() valueList = [] for valueType in values: for value in values[valueType]: valueList.append({ 'type': valueType, 'lastUp': str(value['lastUpdated']), 'value': str(value['value']), 'scale': value['scale'] }) msg.append(valueList) device.lastUpdatedLive[valueType] = int(time.time()) self.live.send(msg) def stateUpdated(self, device, ackId=None, origin=None): if device.isDevice() is False: return extras = {} if ackId: extras['ACK'] = ackId if origin: extras['origin'] = origin else: extras['origin'] = 'Incoming signal' (state, stateValue) = device.state() self.__deviceStateChanged(device, state, stateValue, extras['origin']) self.save() if not self.live.registered: return msg = LiveMessage("DeviceEvent") msg.append(device.id()) msg.append(state) msg.append(str(stateValue)) msg.append(extras) self.live.send(msg) def stateUpdatedFail(self, device, state, stateValue, reason, origin): if not self.live.registered: return if device.isDevice() is False: return extras = { 'reason': reason, } if origin: extras['origin'] = origin else: extras['origin'] = 'Unknown' (state, stateValue) = device.state() self.__deviceStateChanged(device, state, stateValue, extras['origin']) msg = LiveMessage('DeviceFailEvent') msg.append(device.id()) msg.append(state) msg.append(stateValue) msg.append(extras) self.live.send(msg) @TelldusLive.handler('command') def __handleCommand(self, msg): args = msg.argument(0).toNative() action = args['action'] value = args['value'] if 'value' in args else None deviceId = args['id'] device = None for dev in self.devices: if dev.id() == deviceId: device = dev break def success(state, stateValue): if 'ACK' in args: device.setState(state, stateValue, ack=args['ACK']) # Abort the DeviceEvent this triggered raise DeviceAbortException() def fail(reason): # We failed to set status for some reason, nack the server if 'ACK' in args: msg = LiveMessage('NACK') msg.append({ 'ackid': args['ACK'], 'reason': reason, }) self.live.send(msg) # Abort the DeviceEvent this triggered raise DeviceAbortException() device.command(action, value, success=success, failure=fail) @TelldusLive.handler('device') def __handleDeviceCommand(self, msg): args = msg.argument(0).toNative() if 'action' not in args: return if args['action'] == 'setName': if 'name' not in args or args['name'] == '': return for dev in self.devices: if dev.id() != args['device']: continue if isinstance(args['name'], int): dev.setName(str(args['name'])) else: dev.setName(args['name'].decode('UTF-8')) return @TelldusLive.handler('device-requestdata') def __handleDeviceParametersRequest(self, msg): args = msg.argument(0).toNative() device = self.device(args.get('id', 0)) if not device: return reply = LiveMessage('device-datareport') data = {'id': args['id']} if args.get('parameters', 0) == 1: parameters = json.dumps(device.allParameters(), separators=(',', ':'), sort_keys=True) data['parameters'] = parameters data['parametersHash'] = hashlib.sha1(parameters).hexdigest() if args.get('metadata', 0) == 1: metadata = json.dumps(device.metadata(), separators=(',', ':'), sort_keys=True) data['metadata'] = metadata data['metadataHash'] = hashlib.sha1(metadata).hexdigest() reply.append(data) self.live.send(reply) @TelldusLive.handler('reload') def __handleSensorUpdate(self, msg): reloadType = msg.argument(0).toNative() if reloadType != 'sensor': # not for us return data = msg.argument(1).toNative() if not msg.argument(2) or 'sensorId' not in msg.argument(2).toNative(): # nothing to do, might be an orphaned zwave sensor return sensorId = msg.argument(2).toNative()['sensorId'] updateType = data['type'] for dev in self.devices: if dev.id() == sensorId: if updateType == 'updateignored': value = data['ignored'] if dev.ignored() == value: return dev.setIgnored(value) self.__sendSensorChange(sensorId, updateType, value) return if updateType == 'updateignored' and len(self.devices) > 0: # we don't have this sensor, do something! (can't send sensor change # back (__sendSensorChange), because can't create message when # sensor is unknown (could create special workaround, but only do # that if it's still a problem in the future)) logging.warning( 'Requested ignore change for non-existing sensor %s', str(sensorId)) # send an updated sensor report, so that this sensor is hopefully # cleaned up self.__sendSensorReport() def liveRegistered(self, __msg): self.registered = True self.__sendDeviceReport() self.__sendSensorReport() def __load(self): self.store = self.settings.get('devices', []) for dev in self.store: if 'type' not in dev or 'localId' not in dev: continue # This should not be possible device = CachedDevice(dev) # If we have loaded this device from cache 5 times in a row it's # considered dead if device.loadCount() < 5: self.devices.append(device) @signal('deviceAdded') def __deviceAdded(self, device): """ Called every time a device is added/created """ self.observers.deviceAdded(device) @signal('deviceRemoved') def __deviceRemoved(self, deviceId): """ Called every time a device is removed. The parameter deviceId is the old device id. The ref to the device is no longer available """ self.observers.deviceRemoved(deviceId) @signal('deviceStateChanged') def __deviceStateChanged(self, device, state, stateValue, origin): """ Called every time the state of a device is changed. """ del origin # Remove pylint warning self.observers.stateChanged(device, state, stateValue) def save(self): data = [] for device in self.devices: (state, stateValue) = device.state() dev = { "id": device.id(), "loadCount": device.loadCount(), "localId": device.localId(), "type": device.typeString(), "name": device.name(), "params": device.params(), "methods": device.methods(), "state": state, "stateValue": stateValue, "ignored": device.ignored(), "isSensor": device.isSensor() } if len(device.sensorValues()) > 0: dev['sensorValues'] = device.sensorValues() battery = device.battery() if battery is not None: dev['battery'] = battery if hasattr(device, 'declaredDead') and device.declaredDead: dev['declaredDead'] = device.declaredDead data.append(dev) self.settings['devices'] = data self.settings['nextId'] = self.nextId def __sendDeviceReport(self): logging.warning("Send Devices Report") if not self.live.registered: return lst = [] for device in self.devices: if not device.isDevice(): continue (state, stateValue) = device.state() parametersHash = hashlib.sha1( json.dumps(device.allParameters(), separators=(',', ':'), sort_keys=True)) metadataHash = hashlib.sha1( json.dumps(device.metadata(), separators=(',', ':'), sort_keys=True)) dev = { 'id': device.id(), 'name': device.name(), 'methods': device.methods(), 'state': state, 'stateValue': str(stateValue), 'protocol': device.protocol(), 'model': device.model(), 'parametersHash': parametersHash.hexdigest(), 'metadataHash': metadataHash.hexdigest(), 'transport': device.typeString(), 'ignored': device.ignored() } battery = device.battery() if battery is not None: dev['battery'] = battery lst.append(dev) msg = LiveMessage("DevicesReport") logging.warning("DR %s", lst) msg.append(lst) self.live.send(msg) def __sendSensorChange(self, sensorid, valueType, value): msg = LiveMessage("SensorChange") device = None for dev in self.devices: if dev.id() == sensorid: device = dev break if not device: return sensor = { 'protocol': device.typeString(), 'model': device.model(), 'sensor_id': device.id(), } msg.append(sensor) msg.append(valueType) msg.append(value) self.live.send(msg) def __sendSensorReport(self): if not self.live.registered: return lst = [] for device in self.devices: if device.isSensor() is False: continue sensorFrame = [] sensor = { 'name': device.name(), 'protocol': device.protocol(), 'model': device.model(), 'sensor_id': device.id(), } if device.params() and 'sensorId' in device.params(): sensor['channelId'] = device.params()['sensorId'] battery = device.battery() if battery is not None: sensor['battery'] = battery if hasattr(device, 'declaredDead') and device.declaredDead: # Sensor shouldn't be removed for a while, but don't update it on server side sensor['declaredDead'] = 1 sensorFrame.append(sensor) valueList = [] values = device.sensorValues() for valueType in values: for value in values[valueType]: valueList.append({ 'type': valueType, 'lastUp': str(value['lastUpdated']), 'value': str(value['value']), 'scale': value['scale'] }) # Telldus Live! does not aknowledge sensorreportupdates yet, # so don't count this yet (wait for Cassandra only) # device.lastUpdatedLive[valueType] = int(time.time()) sensorFrame.append(valueList) lst.append(sensorFrame) msg = LiveMessage("SensorsReport") msg.append(lst) self.live.send(msg) def sensorsUpdated(self): self.__sendSensorReport()
class EventManager(Plugin): implements(ITelldusLiveObserver) observers = ObserverCollection(IEventFactory) def __init__(self): self.events = {} self.settings = Settings('telldus.event') self.schedulersettings = Settings('telldus.scheduler') self.live = TelldusLive(self.context) self.timezone = self.schedulersettings.get('tz', 'UTC') self.latitude = self.schedulersettings.get('latitude', '55.699592') self.longitude = self.schedulersettings.get('longitude', '13.187836') self.loadLocalEvents() def loadEvent(self, eventId, data, storeddata): event = Event(self, eventId, data['minRepeatInterval'], data['description']) event.loadActions(data['actions'], storeddata) event.loadConditions(data['conditions']) event.loadTriggers(data['triggers']) self.events[eventId] = event def liveRegistered(self, msg, refreshRequired): changed = False if 'latitude' in msg and msg['latitude'] != self.latitude: changed = True self.latitude = msg['latitude'] self.schedulersettings['latitude'] = self.latitude if 'longitude' in msg and msg['longitude'] != self.longitude: changed = True self.longitude = msg['longitude'] self.schedulersettings['longitude'] = self.longitude if 'tz' in msg and msg['tz'] != self.timezone: changed = True self.timezone = msg['tz'] self.schedulersettings['tz'] = self.timezone if changed: self.recalcTriggers() @mainthread def loadLocalEvents(self): if len(self.events) == 0: # only load local events if no report has been received (highly improbable though) data = self.settings.get('events', {}) for eventId in data: if eventId not in self.events and data[eventId] != "": self.loadEvent(eventId, data[eventId], {}) def recalcTriggers(self): for observer in self.observers: observer.recalcTrigger() def requestAction(self, type, **kwargs): for observer in self.observers: if type == 'url': return UrlAction(type=type, **kwargs) action = observer.createAction(type=type, **kwargs) if action is not None: return action return None def requestCondition(self, type, **kwargs): for observer in self.observers: condition = observer.createCondition(type=type, **kwargs) if condition is not None: return condition return None def requestTrigger(self, type, **kwargs): for observer in self.observers: trigger = observer.createTrigger(type=type, **kwargs) if trigger is not None: return trigger return None @TelldusLive.handler('events-report') def receiveEventsFromServer(self, msg): data = msg.argument(0).toNative() for eventId in self.events: # clear old timers self.events[eventId].close() self.events = {} storeddata = self.settings.get('events', {}) self.settings['events'] = data for observer in self.observers: observer.clearAll() for eventId in data: if eventId not in self.events: self.loadEvent(eventId, data[eventId], storeddata) @TelldusLive.handler('one-event-deleted') def receiveDeletedEventFromServer(self, msg): eventId = msg.argument(0).toNative()['eventId'] if eventId in self.events: self.events[eventId].close() del self.events[eventId] storeddata = self.settings.get('events', {}) storeddata[str(eventId)] = "" self.settings['events'] = storeddata @TelldusLive.handler('one-event-report') def receiveEventFromServer(self, msg): data = msg.argument(0).toNative() eventId = data['eventId'] if eventId in self.events: self.events[eventId].close() del self.events[eventId] storeddata = self.settings.get('events', {}) newstoreddata = storeddata.copy() newstoreddata[str(eventId)] = data self.settings['events'] = newstoreddata self.loadEvent(eventId, data, storeddata) @TelldusLive.handler('event-conditionresult') def receiveConditionResultFromServer(self, msg): data = msg.argument(0).toNative() for eid in self.events: event = self.events[eid] for cgid in event.conditions: conditionGroup = event.conditions[cgid] for cid in conditionGroup.conditions: if cid == data['condition']: try: conditionGroup.conditions[ cid].receivedResultFromServer(data['status']) except AttributeError: # Not a RemoteCondition pass return
class ApiManager(Plugin): implements(IWebRequestHandler) implements(ITelldusLiveObserver) observers = ObserverCollection(IApiCallHandler) def __init__(self): self.tokens = {} self.tokenKey = None @staticmethod def getTemplatesDirs(): return [resource_filename('api', 'templates')] @staticmethod def matchRequest(plugin, __path): if plugin != 'api': return False return True def handleRequest(self, plugin, path, params, request, **__kwargs): del plugin if path == '': methods = {} for observer in self.observers: for module, actions in getattr(observer, '_apicalls', {}).iteritems(): for action, func in actions.iteritems(): methods.setdefault(module, {})[action] = {'doc': func.__doc__} return 'index.html', {'methods': methods} if path == 'token': if request.method() == 'PUT': token = uuid.uuid4().hex self.tokens[token] = { 'app': request.post('app'), 'authorized': False, } return WebResponseJson({ 'authUrl': '%s/api/authorize?token=%s' % (request.base(), token), 'token': token }) elif request.method() == 'GET': token = params.get('token', None) if token is None: return WebResponseJson({'error': 'No token specified'}, statusCode=400) if token not in self.tokens: return WebResponseJson({'error': 'No such token'}, statusCode=404) if self.tokens[token]['authorized'] is not True: return WebResponseJson({'error': 'Token is not authorized'}, statusCode=403) claims = { 'aud': self.tokens[token]['app'], 'exp': int(time.time()+self.tokens[token]['ttl']), } body = {} if self.tokens[token]['allowRenew'] is True: body['renew'] = True body['ttl'] = self.tokens[token]['ttl'] accessToken = self.__generateToken(body, claims) resp = WebResponseJson({ 'token': accessToken, 'expires': claims['exp'], 'allowRenew': self.tokens[token]['allowRenew'], }) del self.tokens[token] return resp if path == 'authorize': if 'token' not in params: return WebResponseJson({'error': 'No token specified'}, statusCode=400) token = params['token'] if token not in self.tokens: return WebResponseJson({'error': 'No such token'}, statusCode=404) if request.method() == 'POST': self.tokens[token]['authorized'] = True self.tokens[token]['allowRenew'] = bool(request.post('extend', False)) self.tokens[token]['ttl'] = int(request.post('ttl', 0))*60 return 'authorize.html', {'token': self.tokens[token]} # Check authorization token = request.header('Authorization') if token is None: return WebResponseJson({'error': 'No token was found in the request'}, statusCode=401) if not token.startswith('Bearer '): return WebResponseJson( { 'error': 'The autorization token must be supplied as a bearer token' }, statusCode=401 ) token = token[7:] try: body = jwt.decode(token, self.__tokenKey(), algorithms='HS256') except JWSError as error: return WebResponseJson({'error': str(error)}, statusCode=401) except JWTError as error: return WebResponseJson({'error': str(error)}, statusCode=401) claims = jwt.get_unverified_headers(token) if 'exp' not in claims or claims['exp'] < time.time(): return WebResponseJson({'error': 'The token has expired'}, statusCode=401) if 'aud' not in claims or claims['aud'] is None: return WebResponseJson({'error': 'No app was configured in the token'}, statusCode=401) aud = claims['aud'] if path == 'refreshToken': if 'renew' not in body or body['renew'] != True: return WebResponseJson({'error': 'The token is not authorized for refresh'}, statusCode=403) if 'ttl' not in body: return WebResponseJson({'error': 'No TTL was specified in the token'}, statusCode=401) ttl = body['ttl'] exp = int(time.time()+ttl) accessToken = self.__generateToken({ 'renew': True, 'ttl': ttl }, { 'aud': aud, 'exp': exp, }) return WebResponseJson({ 'token': accessToken, 'expires': exp, }) paths = path.split('/') if len(paths) < 2: return None module = paths[0] action = paths[1] for observer in self.observers: func = getattr(observer, '_apicalls', {}).get(module, {}).get(action, None) if func is None: continue try: params['app'] = aud retval = func(observer, **params) except Exception as error: logging.exception(error) return WebResponseJson({'error': str(error)}) if retval is True: retval = {'status': 'success'} return WebResponseJson(retval) return WebResponseJson( {'error': 'The method %s/%s does not exist' % (module, action)}, statusCode=404 ) @staticmethod def requireAuthentication(plugin, path): if plugin != 'api': return if path in ['', 'authorize']: return True return False @TelldusLive.handler('requestlocalkey') def __requestLocalKey(self, msg): args = msg.argument(0).toNative() live = TelldusLive(self.context) try: publicKey = serialization.load_pem_public_key( args.get('publicKey', ''), backend=default_backend(), ) ttl = int(time.time()+2629743) # One month accessToken = self.__generateToken({}, { 'aud': args.get('app', 'Unknown'), 'exp': ttl, }) ciphertext = publicKey.encrypt( str(accessToken), padding.OAEP( mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None ) ) except Exception as error: live.pushToWeb('api', 'localkey', { 'success': False, 'error': str(error) }) return live.pushToWeb('api', 'localkey', { 'key': base64.b64encode(ciphertext), 'ttl': ttl, 'uuid': args.get('uuid', ''), 'client': live.uuid, }) def __tokenKey(self): if self.tokenKey is not None: return self.tokenKey password = Board.secret() settings = Settings('telldus.api') tokenKey = settings.get('tokenKey', '') backend = default_backend() if tokenKey == '': self.tokenKey = os.urandom(32) # Store it salt = os.urandom(16) kdf = PBKDF2HMAC( algorithm=hashes.SHA1(), length=32, salt=salt, iterations=1000, backend=backend ) key = kdf.derive(password) pwhash = ApiManager.pbkdf2crypt(password) settings['salt'] = base64.b64encode(salt) settings['pw'] = pwhash # Encrypt token key cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend) encryptor = cipher.encryptor() settings['tokenKey'] = base64.b64encode(bytes(encryptor.update(self.tokenKey))) else: # Decode it salt = base64.b64decode(settings.get('salt', '')) pwhash = settings.get('pw', '') if ApiManager.pbkdf2crypt(password, pwhash) != pwhash: logging.warning('Could not decrypt token key, wrong password') return None kdf = PBKDF2HMAC( algorithm=hashes.SHA1(), length=32, salt=salt, iterations=1000, backend=backend ) key = kdf.derive(password) enc = base64.b64decode(tokenKey) cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend) decryptor = cipher.decryptor() self.tokenKey = bytes(decryptor.update(enc)) return self.tokenKey def __generateToken(self, body, claims): return jwt.encode(body, self.__tokenKey(), algorithm='HS256', headers=claims) @staticmethod def apicall(module, action): def call(func): import sys frame = sys._getframe(1) # pylint: disable=W0212 frame.f_locals.setdefault('_apicalls', {}).setdefault(module, {})[action] = func return func return call @staticmethod def pbkdf2crypt(password, salt=None): if salt is None: binarysalt = b''.join([struct.pack("@H", random.randint(0, 0xffff)) for _i in range(3)]) salt = "$p5k2$$" + base64.b64encode(binarysalt, "./") elif salt.startswith("$p5k2$"): salt = "$p5k2$$" + salt.split("$")[3] kdf = PBKDF2HMAC( algorithm=hashes.SHA1(), length=24, salt=salt, iterations=400, backend=default_backend() ) rawhash = kdf.derive(password) return salt + "$" + base64.b64encode(rawhash, "./")
class TelldusLive(Plugin): implements(ISignalObserver) observers = ObserverCollection(ITelldusLiveObserver) def __init__(self): logging.info("Telldus Live! loading") self.email = '' self.supportedMethods = 0 self.connected = False self.registered = False self.running = False self.serverList = ServerList() self.lastBackedUpConfig = 0 Application().registerShutdown(self.stop) self.settings = Settings('tellduslive.config') self.uuid = self.settings['uuid'] self.conn = ServerConnection() self.pingTimer = 0 self.thread = threading.Thread(target=self.run) if self.conn.publicKey != '': # Only connect if the keys has been set. self.thread.start() @slot('configurationWritten') def configurationWritten(self, path): if time.time() - self.lastBackedUpConfig < 86400: # Only send the backup once per day return self.lastBackedUpConfig = time.time() uploadPath = 'http://%s/upload/config' % Board.liveServer() logging.info('Upload backup to %s', uploadPath) with open(path, 'rb') as fd: fileData = fd.read() fileData = bz2.compress(fileData) # Compress it fileData = TelldusLive.deviceSpecificEncrypt(fileData) # Encrypt it requests.post( uploadPath, data={'mac': TelldusLive.getMacAddr(Board.networkInterface())}, files={'Telldus.conf.bz2': fileData} ) @mainthread def handleMessage(self, message): if (message.name() == "notregistered"): self.email = '' self.connected = True self.registered = False params = message.argument(0).dictVal self.uuid = params['uuid'].stringVal self.settings['uuid'] = self.uuid logging.info( "This client isn't activated, please activate it using this url:\n%s", params['url'].stringVal ) self.liveConnected() return if (message.name() == "registered"): self.connected = True self.registered = True data = message.argument(0).toNative() if 'email' in data: self.email = data['email'] if 'uuid' in data and data['uuid'] != self.uuid: self.uuid = data['uuid'] self.settings['uuid'] = self.uuid self.liveRegistered(data) return if (message.name() == "command"): # Extract ACK and handle it args = message.argument(0).dictVal if 'ACK' in args: msg = LiveMessage("ACK") msg.append(args['ACK'].intVal) self.send(msg) if (message.name() == "pong"): return if (message.name() == "disconnect"): self.conn.close() self.__disconnected() return handled = False for observer in self.observers: for func in getattr(observer, '_telldusLiveHandlers', {}).get(message.name(), []): func(observer, message) handled = True if not handled: logging.warning("Did not understand: %s", message.toByteArray()) def isConnected(self): return self.connected def isRegistered(self): return self.registered @signal def liveConnected(self): """This signal is sent when we have succesfully connected to a Live! server""" self.observers.liveConnected() @signal def liveDisconnected(self): """ This signal is sent when we are disconnected. Please note that it is normal for it di be disconnected and happens regularly """ self.observers.liveDisconnected() @signal def liveRegistered(self, options): """This signal is sent when we have succesfully registered with a Live! server""" self.observers.liveRegistered(options) def run(self): self.running = True wait = 0 pongTimer, self.pingTimer = (0, 0) while self.running: if wait > 0: wait = wait - 1 time.sleep(1) continue state = self.conn.process() if state == ServerConnection.CLOSED: server = self.serverList.popServer() if not server: wait = random.randint(60, 300) logging.warning("No servers found, retry in %i seconds", wait) continue if not self.conn.connect(server['address'], int(server['port'])): wait = random.randint(60, 300) logging.warning("Could not connect, retry in %i seconds", wait) elif state == ServerConnection.CONNECTED: pongTimer, self.pingTimer = (time.time(), time.time()) self.__sendRegisterMessage() elif state == ServerConnection.MSG_RECEIVED: msg = self.conn.popMessage() if msg is None: continue pongTimer = time.time() self.handleMessage(msg) elif state == ServerConnection.DISCONNECTED: wait = random.randint(10, 50) logging.warning("Disconnected, reconnect in %i seconds", wait) self.__disconnected() else: if (time.time() - pongTimer >= 360): # No pong received self.conn.close() wait = random.randint(10, 50) logging.warning("No pong received, disconnecting. Reconnect in %i seconds", wait) self.__disconnected() elif (time.time() - self.pingTimer >= 120): # Time to ping self.conn.send(LiveMessage("Ping")) self.pingTimer = time.time() def stop(self): self.running = False def send(self, message): self.conn.send(message) self.pingTimer = time.time() def pushToWeb(self, module, action, data): msg = LiveMessage("sendToWeb") msg.append(module) msg.append(action) msg.append(data) self.send(msg) def __disconnected(self): self.email = '' self.connected = False self.registered = False def sendNotification(): self.liveDisconnected() # Syncronize signal with main thread Application().queue(sendNotification) @staticmethod def handler(message): def call(func): import sys frame = sys._getframe(1) # pylint: disable=W0212 frame.f_locals.setdefault('_telldusLiveHandlers', {}).setdefault(message, []).append(func) return func return call def __sendRegisterMessage(self): print("Send register") msg = LiveMessage('Register') msg.append({ 'key': self.conn.publicKey, 'mac': TelldusLive.getMacAddr(Board.networkInterface()), 'secret': Board.secret(), 'hash': 'sha1' }) msg.append({ 'protocol': 3, 'version': Board.firmwareVersion(), 'os': 'linux', 'os-version': 'telldus' }) self.conn.send(msg) @staticmethod def getMacAddr(ifname): addrs = netifaces.ifaddresses(ifname) try: mac = addrs[netifaces.AF_LINK][0]['addr'] except (IndexError, KeyError) as __error: return '' return mac.upper().replace(':', '') @staticmethod def deviceSpecificEncrypt(payload): # TODO: Use security plugin once available password = Board.secret() iv = os.urandom(16) # pylint: disable=C0103 key = PBKDF2(password, iv).read(32) encryptor = AES.new(key, AES.MODE_CBC, iv) buff = StringIO() buff.write(struct.pack('<Q', len(payload))) buff.write(iv) if len(payload) % 16 != 0: # Pad payload payload += ' ' * (16 - len(payload) % 16) buff.write(encryptor.encrypt(payload)) return buff.getvalue()