Beispiel #1
0
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'
Beispiel #2
0
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
Beispiel #3
0
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
Beispiel #4
0
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])
Beispiel #5
0
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
Beispiel #11
0
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()