示例#1
0
class WSGIServer(_WSGIServer):
    def initialize_websockets_manager(self):
        """
        Call thos to start the underlying websockets
        manager. Make sure to call it once your server
        is created.
        """
        self.manager = WebSocketManager()
        self.manager.start()

    def shutdown_request(self, request):
        """
        The base class would close our socket
        if we didn't override it.
        """
        pass

    def link_websocket_to_server(self, ws):
        """
        Call this from your WSGI handler when a websocket
        has been created.
        """
        self.manager.add(ws)

    def server_close(self):
        """
        Properly initiate closing handshakes on
        all websockets when the WSGI server terminates.
        """
        if hasattr(self, 'manager'):
            self.manager.close_all()
            self.manager.stop()
            self.manager.join()
            delattr(self, 'manager')
        _WSGIServer.server_close(self)
    def test_websocket_close_all(self, MockSelectPoller):
        m = WebSocketManager(poller=MockSelectPoller())

        ws = MagicMock()
        m.add(ws)
        m.close_all()
        ws.terminate.assert_call_once_with(1001, 'Server is shutting down')
    def test_websocket_close_all(self, MockSelectPoller):
        m = WebSocketManager(poller=MockSelectPoller())

        ws = MagicMock()
        m.add(ws)
        m.close_all()
        ws.terminate.assert_call_once_with(1001, 'Server is shutting down')
    def test_websocket_close_all(self, MockSelectPoller):
        m = WebSocketManager(poller=MockSelectPoller())

        ws = MagicMock()
        m.add(ws)
        m.close_all()
        ws.close.assert_called_once_with(code=1001, reason='Server is shutting down')
    def test_websocket_close_all(self, MockSelectPoller):
        m = WebSocketManager(poller=MockSelectPoller())

        ws = MagicMock()
        m.add(ws)
        m.close_all()
        ws.close.assert_called_once_with(code=1001,
                                         reason='Server is shutting down')
    def test_broadcast(self, MockSelectPoller):
        m = WebSocketManager(poller=MockSelectPoller())

        ws = MagicMock()
        ws.terminated = False
        m.add(ws)

        m.broadcast(b'hello there')
        ws.send.assert_call_once_with(b'hello there')
    def test_broadcast(self, MockSelectPoller):
        m = WebSocketManager(poller=MockSelectPoller())

        ws = MagicMock()
        ws.terminated = False
        m.add(ws)

        m.broadcast(b'hello there')
        ws.send.assert_called_once_with(b'hello there', False)
    def test_add_and_remove_websocket(self, MockSelectPoller):
        m = WebSocketManager(poller=MockSelectPoller())

        ws = MagicMock()
        ws.sock.fileno.return_value = 1
        
        m.add(ws)
        m.poller.register.assert_call_once_with(ws)

        m.remove(ws)
        m.poller.unregister.assert_call_once_with(ws)
    def test_cannot_add_websocket_more_than_once(self, MockSelectPoller):
        m = WebSocketManager(poller=MockSelectPoller())

        ws = MagicMock()
        ws.sock.fileno.return_value = 1

        m.add(ws)
        self.assertEqual(len(m), 1)

        m.add(ws)
        self.assertEqual(len(m), 1)
    def test_add_and_remove_websocket(self, MockSelectPoller):
        m = WebSocketManager(poller=MockSelectPoller())

        ws = MagicMock()
        ws.sock.fileno.return_value = 1

        m.add(ws)
        m.poller.register.assert_called_once_with(1)

        m.remove(ws)
        m.poller.unregister.assert_called_once_with(1)
    def test_cannot_add_websocket_more_than_once(self, MockSelectPoller):
        m = WebSocketManager(poller=MockSelectPoller())

        ws = MagicMock()
        ws.sock.fileno.return_value = 1
        
        m.add(ws)
        self.assertEqual(len(m), 1)
        
        m.add(ws)
        self.assertEqual(len(m), 1)
    def test_broadcast_failure_must_not_break_caller(self, MockSelectPoller):
        m = WebSocketManager(poller=MockSelectPoller())

        ws = MagicMock()
        ws.terminated = False
        ws.send.side_effect = RuntimeError
        m.add(ws)

        try:
                m.broadcast(b'hello there')
        except:
                self.fail("Broadcasting shouldn't have failed")
    def test_broadcast_failure_must_not_break_caller(self, MockSelectPoller):
        m = WebSocketManager(poller=MockSelectPoller())

        ws = MagicMock()
        ws.terminated = False
        ws.send.side_effect = RuntimeError
        m.add(ws)

        try:
            m.broadcast(b'hello there')
        except:
            self.fail("Broadcasting shouldn't have failed")
示例#14
0
    def test_websocket_terminated_from_mainloop(self, MockSelectPoller):
        m = WebSocketManager(poller=MockSelectPoller())
        m.poller.poll.return_value = [1]

        ws = MagicMock()

        ws.terminated = False
        ws.sock.fileno.return_value = 1
        ws.once.return_value = False

        m.add(ws)
        m.start()

        ws.terminate.assert_call_once_with()

        m.stop()
    def test_websocket_terminated_from_mainloop(self, MockSelectPoller):
        m = WebSocketManager(poller=MockSelectPoller())
        m.poller.poll.return_value = [1]

        ws = MagicMock()
        
        ws.terminated = False
        ws.sock.fileno.return_value = 1
        ws.once.return_value = False
        
        m.add(ws)
        m.start()
        
        ws.terminate.assert_call_once_with()
        
        m.stop()
class WebSocketPlugin(plugins.SimplePlugin):
    def __init__(self, bus):
        plugins.SimplePlugin.__init__(self, bus)
        self.manager = WebSocketManager()

    def start(self):
        self.bus.log("Starting WebSocket processing")
        self.bus.subscribe('stop', self.cleanup)
        self.bus.subscribe('handle-websocket', self.handle)
        self.bus.subscribe('websocket-broadcast', self.broadcast)
        self.manager.start()

    def stop(self):
        self.bus.log("Terminating WebSocket processing")
        self.bus.unsubscribe('stop', self.cleanup)
        self.bus.unsubscribe('handle-websocket', self.handle)
        self.bus.unsubscribe('websocket-broadcast', self.broadcast)

    def handle(self, ws_handler, peer_addr):
        """
        Tracks the provided handler.

        :param ws_handler: websocket handler instance
        :param peer_addr: remote peer address for tracing purpose
        """
        self.manager.add(ws_handler)

    def cleanup(self):
        """
        Terminate all connections and clear the pool. Executed when the engine stops.
        """
        self.manager.close_all()
        self.manager.stop()
        self.manager.join()

    def broadcast(self, message, binary=False):
        """
        Broadcasts a message to all connected clients known to
        the server.

        :param message: a message suitable to pass to the send() method
          of the connected handler.
        :param binary: whether or not the message is a binary one
        """
        self.manager.broadcast(message, binary)
示例#17
0
class WebSocketPlugin(plugins.SimplePlugin):
    def __init__(self, bus):
        plugins.SimplePlugin.__init__(self, bus)
        self.manager = WebSocketManager()

    def start(self):
        self.bus.log("Starting WebSocket processing")
        self.bus.subscribe('stop', self.cleanup)
        self.bus.subscribe('handle-websocket', self.handle)
        self.bus.subscribe('websocket-broadcast', self.broadcast)
        self.manager.start()

    def stop(self):
        self.bus.log("Terminating WebSocket processing")
        self.bus.unsubscribe('stop', self.cleanup)
        self.bus.unsubscribe('handle-websocket', self.handle)
        self.bus.unsubscribe('websocket-broadcast', self.broadcast)

    def handle(self, ws_handler, peer_addr):
        """
        Tracks the provided handler.

        :param ws_handler: websocket handler instance
        :param peer_addr: remote peer address for tracing purpose
        """
        self.manager.add(ws_handler)

    def cleanup(self):
        """
        Terminate all connections and clear the pool. Executed when the engine stops.
        """
        self.manager.close_all()
        self.manager.stop()
        self.manager.join()

    def broadcast(self, message, binary=False):
        """
        Broadcasts a message to all connected clients known to
        the server.

        :param message: a message suitable to pass to the send() method
          of the connected handler.
        :param binary: whether or not the message is a binary one
        """
        self.manager.broadcast(message, binary)
    def test_cannot_remove_unregistered_websocket(self, MockSelectPoller):
        m = WebSocketManager(poller=MockSelectPoller())

        ws = MagicMock()
        ws.sock.fileno.return_value = 1
        
        m.remove(ws)
        self.assertEqual(len(m), 0)
        self.assertFalse(m.poller.unregister.called)
        
        m.add(ws)
        self.assertEqual(len(m), 1)
        m.remove(ws)
        self.assertEqual(len(m), 0)
        m.poller.unregister.assert_call_once_with(ws)
        m.poller.reset_mock()
        
        m.remove(ws)
        self.assertEqual(len(m), 0)
        self.assertFalse(m.poller.unregister.called)
    def test_cannot_remove_unregistered_websocket(self, MockSelectPoller):
        m = WebSocketManager(poller=MockSelectPoller())

        ws = MagicMock()
        ws.sock.fileno.return_value = 1

        m.remove(ws)
        self.assertEqual(len(m), 0)
        self.assertFalse(m.poller.unregister.called)

        m.add(ws)
        self.assertEqual(len(m), 1)
        m.remove(ws)
        self.assertEqual(len(m), 0)
        m.poller.unregister.assert_called_once_with(1)
        m.poller.reset_mock()

        m.remove(ws)
        self.assertEqual(len(m), 0)
        self.assertFalse(m.poller.unregister.called)
class FileServer(object):
    """ Serves static files from a directory.
    """

    def __init__(self, path):
        """ path is directory where static files are stored
        """
        self.path = path
        self.wsapp = WebSocketWSGIApplication(handler_cls=EchoWebSocket)
        self.manager = WebSocketManager()
        self.manager.start()

    def __call__(self, environ, start_response):
        """ WSGI entry point
        """
        # Upgrade header means websockets...
        upgrade_header = environ.get('HTTP_UPGRADE', '').lower()
        if upgrade_header:
            environ['ws4py.socket'] = get_connection(environ['wsgi.input'])
            # This will make a websocket, hopefully!
            ret = self.wsapp(environ, start_response)
            if 'ws4py.websocket' in environ:
                self.manager.add(environ.pop('ws4py.websocket'))
            return ret

        # Find path to file to server
        path_info = environ["PATH_INFO"]

        if not path_info or path_info == "/":
            path_info = "/index.html"

        file_path = os.path.join(self.path, path_info[1:])

        # If file does not exist, return 404
        if not os.path.exists(file_path):
            return self._not_found(start_response)

        # Guess mimetype of file based on file extension
        mimetype = mimetypes.guess_type(file_path)[0]

        # If we can't guess mimetype, return a 403 Forbidden
        if mimetype is None:
            return self._forbidden(start_response)

        # Get size of file
        size = os.path.getsize(file_path)

        # Create headers and start response
        headers = [
            ("Content-type", mimetype),
            ("Content-length", str(size)),
        ]

        start_response("200 OK", headers)

        # Send file
        return self._send_file(file_path, size)

    def _send_file(self, file_path, size):
        """ A generator function which returns the blocks in a file, one at
        a time.

        """
        with open(file_path) as f:
            block = f.read(BLOCK_SIZE)
            while block:
                yield block
                block = f.read(BLOCK_SIZE)

    def _not_found(self, start_response):
        start_response("404 NOT FOUND", [("Content-type", "text/plain")])
        return ["Not found", ]

    def _forbidden(self, start_response):
        start_response("403 FORBIDDEN", [("Content-type", "text/plain")])
        return ["Forbidden", ]
示例#21
0
文件: pushbullet.py 项目: lsta/pai
class PushBulletWSClient(WebSocketBaseClient):
    name = "pushbullet"

    def __init__(self, interface, url):
        """ Initializes the PB WS Client"""
        super().__init__(url)

        self.pb = Pushbullet(cfg.PUSHBULLET_KEY)
        self.manager = WebSocketManager()
        self.interface = interface

        self.device = None
        for i, device in enumerate(self.pb.devices):
            if device.nickname == 'pai':
                logger.debug("Device found")
                self.device = device
                break
        else:
            logger.exception("Device not found. Creating 'pai' device")
            self.device = self.pb.new_device(nickname='pai', icon='system')

    def stop(self):
        self.terminate()
        self.manager.stop()

    def handshake_ok(self):
        """ Callback trigger when connection succeeded"""
        logger.info("Handshake OK")
        self.manager.add(self)
        self.manager.start()
        for chat in self.pb.chats:
            logger.debug("Associated contacts: {}".format(chat))

        # Receiving pending messages
        self.received_message(json.dumps({
            "type": "tickle",
            "subtype": "push"
        }))

        self.send_message("Active")

    def received_message(self, message):
        """ Handle Pushbullet message. It should be a command """
        logger.debug("Received Message {}".format(message))

        try:
            message = json.loads(str(message))
        except:
            logger.exception("Unable to parse message")
            return

        if message['type'] == 'tickle' and message['subtype'] == 'push':
            now = time.time()
            pushes = self.pb.get_pushes(modified_after=int(now) - 20,
                                        limit=1,
                                        filter_inactive=True)
            for p in pushes:

                # Ignore messages send by us
                if p.get('direction') == 'self' and p.get('title') == 'pai':
                    #logger.debug('Ignoring message sent')
                    continue

                if p.get('direction') == 'outgoing' or p.get('dismissed'):
                    #logger.debug('Ignoring outgoing dismissed')
                    continue

                if p.get('sender_email_normalized'
                         ) in cfg.PUSHBULLET_CONTACTS or p.get(
                             'direction') == 'self':
                    ret = self.interface.handle_command(p.get('body'))

                    m = "PB {}: {}".format(p.get('sender_email_normalized'),
                                           ret)
                    logger.info(m)
                else:
                    m = "PB {} (UNK): {}".format(
                        p.get('sender_email_normalized'), p.get('body'))
                    logger.warning(m)

                self.send_message(m)
                ps.sendNotification(
                    Notification(sender=self.name,
                                 message=m,
                                 level=EventLevel.INFO))

    def unhandled_error(self, error):
        logger.error("{}".format(error))

        try:
            self.terminate()
        except Exception:
            logger.exception("Closing Pushbullet WS")

        self.close()

    def send_message(self, msg, dstchat=None):
        if dstchat is None:
            dstchat = self.pb.chats

        if not isinstance(dstchat, list):
            dstchat = [dstchat]
        # Push to self
        self.device.push_note("pai", msg)

        for chat in dstchat:
            if chat.email in cfg.PUSHBULLET_CONTACTS:
                try:
                    self.pb.push_note("pai", msg, chat=chat)
                except Exception:
                    logger.exception("Sending message")
                    time.sleep(5)
示例#22
0
class PushBulletWSClient(WebSocketBaseClient):

    def init(self, interface):
        """ Initializes the PB WS Client"""

        self.logger = logging.getLogger('PAI').getChild(__name__)
        self.pb = Pushbullet(cfg.PUSHBULLET_KEY, cfg.PUSHBULLET_SECRET)
        self.manager = WebSocketManager()
        self.alarm = None
        self.interface = interface

    def stop(self):
        self.terminate()
        self.manager.stop()

    def set_alarm(self, alarm):
        """ Sets the paradox alarm object """
        self.alarm = alarm

    def handshake_ok(self):
        """ Callback trigger when connection succeeded"""
        self.logger.info("Handshake OK")
        self.manager.add(self)
        self.manager.start()
        for chat in self.pb.chats:
            self.logger.debug("Associated contacts: {}".format(chat))

        # Receiving pending messages
        self.received_message(json.dumps({"type": "tickle", "subtype": "push"}))

        self.send_message("Active")

    def received_message(self, message):
        """ Handle Pushbullet message. It should be a command """
        self.logger.debug("Received Message {}".format(message))

        try:
            message = json.loads(str(message))
        except:
            self.logger.exception("Unable to parse message")
            return

        if self.alarm is None:
            return
        if message['type'] == 'tickle' and message['subtype'] == 'push':
            now = time.time()
            pushes = self.pb.get_pushes(modified_after=int(now) - 20, limit=1, filter_inactive=True)
            self.logger.debug("got pushes {}".format(pushes))
            for p in pushes:
                self.pb.dismiss_push(p.get("iden"))
                self.pb.delete_push(p.get("iden"))

                if p.get('direction') == 'outgoing' or p.get('dismissed'):
                    continue

                if p.get('sender_email_normalized') in cfg.PUSHBULLET_CONTACTS:
                    ret = self.interface.send_command(p.get('body'))

                    if ret:
                        self.logger.info("From {} ACCEPTED: {}".format(p.get('sender_email_normalized'), p.get('body')))
                    else:
                        self.logger.warning("From {} UNKNOWN: {}".format(p.get('sender_email_normalized'), p.get('body')))
                else:
                    self.logger.warning("Command from INVALID SENDER {}: {}".format(p.get('sender_email_normalized'), p.get('body')))

    def unhandled_error(self, error):
        self.logger.error("{}".format(error))

        try:
            self.terminate()
        except Exception:
            self.logger.exception("Closing Pushbullet WS")

        self.close()

    def send_message(self, msg, dstchat=None):
        if dstchat is None:
            dstchat = self.pb.chats

        if not isinstance(dstchat, list):
            dstchat = [dstchat]

        for chat in dstchat:
            if chat.email in cfg.PUSHBULLET_CONTACTS:
                try:
                    self.pb.push_note("paradox", msg, chat=chat)
                except Exception:
                    self.logger.exception("Sending message")
                    time.sleep(5)

    def notify(self, source, message, level):
        try:
            if level.value >= EventLevel.WARN.value:
                self.send_message("{}".format(message))
        except Exception:
            logging.exception("Pushbullet notify")
示例#23
0
class PushBulletWSClient(WebSocketBaseClient):
    def init(self):
        """ Initializes the PB WS Client"""

        self.pb = Pushbullet(cfg.PUSHBULLET_KEY, cfg.PUSHBULLET_SECRET)
        self.manager = WebSocketManager()
        self.alarm = None

    def set_alarm(self, alarm):
        """ Sets the paradox alarm object """
        self.alarm = alarm

    def handshake_ok(self):
        """ Callback trigger when connection succeeded"""
        logger.info("Handshake OK")
        self.manager.add(self)
        for chat in self.pb.chats:
            logger.debug("Associated contacts: {}".format(chat))

        # Receiving pending messages
        self.received_message(json.dumps({
            "type": "tickle",
            "subtype": "push"
        }))

        self.send_message("Active")

    def handle_message(self, message):
        """ Handle Pushbullet message. It should be a command """

        logger.debug("Received Message {}".format(message))
        try:
            message = json.loads(str(message))
        except:
            logger.exception("Unable to parse message")
            return

        if self.alarm == None:
            return

        if message['type'] == 'tickle' and msg['subtype'] == 'push':
            now = time.time()
            pushes = self.pb.get_pushes(modified_after=int(now) - 10,
                                        limit=1,
                                        filter_inactive=True)

            for p in pushes:
                self.pb.dismiss_push(p.get("iden"))
                self.pb.delete_push(p.get("iden"))

                if p.get('direction') == 'outgoing' or p.get('dismissed'):
                    continue

                if p.get('sender_email_normalized') in PUSHBULLET_CONTACTS:
                    ret = self.send_command(p.get('body'))

                    if ret:
                        logger.info("From {} ACCEPTED: {}".format(
                            p.get('sender_email_normalized'), p.get('body')))
                    else:
                        logger.warning("From {} UNKNOWN: {}".format(
                            p.get('sender_email_normalized'), p.get('body')))
                else:
                    logger.warning("Command from INVALID SENDER {}: {}".format(
                        p.get('sender_email_normalized'), p.get('body')))

    def unhandled_error(self, error):
        logger.error("{}".format(error))

        try:
            self.terminate()
        except:
            logger.exception("Closing Pushbullet WS")

        self.close()

    def send_message(self, msg, dstchat=None):
        for chat in self.pb.chats:
            if chat.email in PUSHBULLET_CONTACTS:
                try:
                    self.pb.push_note("paradox", msg, chat=chat)
                except:
                    logger.exception("Sending message")
                    time.sleep(5)

    def send_command(self, message):
        """Handle message received from the MQTT broker"""
        """Format TYPE LABEL COMMAND """
        tokens = message.split(" ")

        if len(tokens) != 3:
            logger.warning("Message format is invalid")
            return

        if self.alarm == None:
            logger.error("No alarm registered")
            return

        element_type = tokens[0].lower()
        element = tokens[1]
        command = self.normalize_payload(tokens[2])

        # Process a Zone Command
        if element_type == 'zone':
            if command not in ['bypass', 'clear_bypass']:
                logger.error("Invalid command for Zone {}".format(command))
                return

            if not self.alarm.control_zone(element, command):
                logger.warning("Zone command refused: {}={}".format(
                    element, command))

        # Process a Partition Command
        elif element_type == 'partition':
            if command not in ['arm', 'disarm', 'arm_stay', 'arm_sleep']:
                logger.error(
                    "Invalid command for Partition {}".format(command))
                return

            if not self.alarm.control_partition(element, command):
                logger.warning("Partition command refused: {}={}".format(
                    element, command))

        # Process an Output Command
        elif element_type == 'output':
            if command not in ['on', 'off', 'pulse']:
                logger.error("Invalid command for Output {}".format(command))
                return

            if not self.alarm.control_output(element, command):
                logger.warning("Output command refused: {}={}".format(
                    element, command))
        else:
            logger.error("Invalid control property {}".format(element))

    def normalize_payload(self, message):
        message = message.strip().lower()

        if message in ['true', 'on', '1', 'enable']:
            return 'on'
        elif message in ['false', 'off', '0', 'disable']:
            return 'off'
        elif message in [
                'pulse', 'arm', 'disarm', 'arm_stay', 'arm_sleep', 'bypass',
                'clear_bypass'
        ]:
            return message

        return None

    def notify(self, source, message, level):
        if level < logging.INFO:
            return

        try:
            self.send_message("{}".format(message))
        except:
            logging.exception("Pushbullet notify")

    def event(self, raw):
        """Handle Live Event"""
        return

    def change(self, element, label, property, value):
        """Handle Property Change"""
        #logger.debug("Property Change: element={}, label={}, property={}, value={}".format(
        #    element,
        #    label,
        #    property,
        #    value))
        return