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.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_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_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)
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", ]
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)
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")
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