class Connection(Thread): """Thread to handle a single TCP conection.""" def __init__(self, conn, mbox, cdr, password): """Initialize.""" # pylint: disable=too-many-arguments Thread.__init__(self) self.conn = conn self.mbox = mbox self.cdr = cdr self.mboxq = PollableQueue() self.password = password self.accept_pw = False def queue(self): """Fetch queue object.""" return self.mboxq def _build_msg_list(self): """Send a sanitized list to the client.""" result = [] blacklist = ["wav"] status = self.mbox.get_mbox_status() for _k, ref in status.items(): result.append( {key: val for key, val in ref.items() if key not in blacklist}) return result @staticmethod def _build_cdr(entries, start=0, count=-1, sha=None): """Extract requested info from cdr list.""" if sha is not None: msg = decode_from_sha(sha) start, count = [int(item) for item in msg.split(b',')] if not start or start < 0: start = 0 if start >= len(entries) or count == 0: return msg if count < 0 or count + start > len(entries): end = len(entries) else: end = start + count return entries[start:end] def _send(self, command, msg): """Send message prefixed by length.""" print('SEND command %s' % (command)) msglen = len(msg) msglen = bytes([ msglen >> 24, 0xff & (msglen >> 16), 0xff & (msglen >> 8), 0xff & (msglen >> 0) ]) self.conn.send(bytes([command]) + bytes(msglen) + bytes(msg)) def _handle_request(self, request): print(request) if request['cmd'] == cmd.CMD_MESSAGE_PASSWORD: self.accept_pw, msg = compare_password(self.password, request['sha']) if self.accept_pw: self._send(cmd.CMD_MESSAGE_VERSION, __version__.encode('utf-8')) logging.info("Password accepted") else: self._send(cmd.CMD_MESSAGE_ERROR, msg.encode('utf-8')) logging.warning("Password rejected: %s", msg) elif not self.accept_pw: logging.warning("Bad Password") self._send(cmd.CMD_MESSAGE_ERROR, b'bad password') elif request['cmd'] == cmd.CMD_MESSAGE_LIST: logging.debug("Requested Message List") self._send(cmd.CMD_MESSAGE_LIST, json.dumps(self._build_msg_list()).encode('utf-8')) elif request['cmd'] == cmd.CMD_MESSAGE_MP3: msg = self.mbox.mp3(request['sha']) if msg: self._send(cmd.CMD_MESSAGE_MP3, msg) else: logging.warning("Couldn't find message for %s", request['sha']) self._send(cmd.CMD_MESSAGE_ERROR, "Could not find requested message") elif request['cmd'] == cmd.CMD_MESSAGE_DELETE: msg = self.mbox.delete(request['sha']) self._send(cmd.CMD_MESSAGE_LIST, json.dumps(self._build_msg_list()).encode('utf-8')) elif request['cmd'] == cmd.CMD_MESSAGE_CDR_AVAILABLE: if not self.cdr: self._send(cmd.CMD_MESSAGE_ERROR, b'CDR Not enabled') return self._send(cmd.CMD_MESSAGE_CDR_AVAILABLE, json.dumps({ 'count': self.cdr.count() }).encode('utf-8')) elif request['cmd'] == cmd.CMD_MESSAGE_CDR: if not self.cdr: self._send(cmd.CMD_MESSAGE_ERROR, b'CDR Not enabled') return entries = self._build_cdr(self.cdr.entries(), sha=request['sha']) try: msg = { 'keys': self.cdr.keys(), 'entries': entries.decode('utf-8') } except: msg = {'keys': self.cdr.keys(), 'entries': entries} try: for entry in entries: if entry['time'] == '': logging.exception('No timestamp for this: %s' % (entry)) except: asdf = '' self._send(cmd.CMD_MESSAGE_CDR, zlib.compress(json.dumps(msg).encode('utf-8'))) def run(self): """Thread main loop.""" while True: readable, _w, _e = select.select([self.conn, self.mboxq], [], []) if self.conn in readable: try: request = _parse_request(recv_blocking(self.conn, 66)) except RuntimeError: logging.warning("Connection closed") break logging.debug(request) self._handle_request(request) if self.mboxq in readable: msgtype = self.mboxq.get() self.mboxq.task_done() if msgtype == 'mbox': self._send( cmd.CMD_MESSAGE_LIST, json.dumps(self._build_msg_list()).encode('utf-8')) elif msgtype == 'cdr': self._send( cmd.CMD_MESSAGE_CDR_AVAILABLE, json.dumps({ 'count': self.cdr.count() }).encode('utf-8'))
class Client: """asterisk_mbox client.""" def __init__(self, ipaddr, port, password, callback=None, **kwargs): """constructor.""" self._ipaddr = ipaddr self._port = port self._password = encode_password(password).encode('utf-8') self._callback = callback self._soc = None self._thread = None self._status = {} # Stop thread self.signal = PollableQueue() # Send data to the server self.request_queue = PollableQueue() # Receive data from the server self.result_queue = PollableQueue() if 'autostart' not in kwargs or kwargs['autostart']: self.start() def start(self): """Start thread.""" if not self._thread: logging.info("Starting asterisk mbox thread") # Ensure signal queue is empty try: while True: self.signal.get(False) except queue.Empty: pass self._thread = threading.Thread(target=self._loop) self._thread.setDaemon(True) self._thread.start() def stop(self): """Stop thread.""" if self._thread: self.signal.put("Stop") self._thread.join() if self._soc: self._soc.shutdown() self._soc.close() self._thread = None def _connect(self): """Connect to server.""" self._soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._soc.connect((self._ipaddr, self._port)) self._soc.send( _build_request({ 'cmd': cmd.CMD_MESSAGE_PASSWORD, 'sha': self._password })) def _recv_msg(self): """Read a message from the server.""" command = ord(recv_blocking(self._soc, 1)) msglen = recv_blocking(self._soc, 4) msglen = ((msglen[0] << 24) + (msglen[1] << 16) + (msglen[2] << 8) + msglen[3]) msg = recv_blocking(self._soc, msglen) return command, msg def _handle_msg(self, command, msg, request): if command == cmd.CMD_MESSAGE_ERROR: logging.warning("Received error: %s", msg.decode('utf-8')) elif command == cmd.CMD_MESSAGE_VERSION: min_ver = StrictVersion(__min_server_version__) server_ver = StrictVersion(msg.decode('utf-8')) if server_ver < min_ver: raise ServerError("Server version is too low: {} < {}".format( msg.decode('utf-8'), __min_server_version__)) elif command == cmd.CMD_MESSAGE_LIST: self._status = json.loads(msg.decode('utf-8')) msg = self._status elif command == cmd.CMD_MESSAGE_CDR: self._status = json.loads(zlib.decompress(msg).decode('utf-8')) msg = self._status if self._callback and 'sync' not in request: self._callback(command, msg) elif request and (command == request.get('cmd') or command == cmd.CMD_MESSAGE_ERROR): logging.debug("Got command: %s", cmd.commandstr(command)) self.result_queue.put([command, msg]) request.clear() else: logging.debug("Got unhandled command: %s", cmd.commandstr(command)) def _clear_request(self, request): if not self._callback or 'sync' in request: self.result_queue.put( [cmd.CMD_MESSAGE_ERROR, "Not connected to server"]) request.clear() def _loop(self): """Handle data.""" request = {} connected = False while True: timeout = None sockets = [self.request_queue, self.signal] if not connected: try: self._clear_request(request) self._connect() self._soc.send( _build_request({'cmd': cmd.CMD_MESSAGE_LIST})) self._soc.send( _build_request({'cmd': cmd.CMD_MESSAGE_CDR_AVAILABLE})) connected = True except ConnectionRefusedError: timeout = 5.0 if connected: sockets.append(self._soc) readable, _writable, _errored = select.select( sockets, [], [], timeout) if self.signal in readable: break if self._soc in readable: # We have incoming data try: command, msg = self._recv_msg() self._handle_msg(command, msg, request) except (RuntimeError, ConnectionResetError): logging.warning("Lost connection") connected = False self._clear_request(request) if self.request_queue in readable: request = self.request_queue.get() self.request_queue.task_done() if not connected: self._clear_request(request) else: if (request['cmd'] == cmd.CMD_MESSAGE_LIST and self._status and (not self._callback or 'sync' in request)): self.result_queue.put( [cmd.CMD_MESSAGE_LIST, self._status]) request = {} else: self._soc.send(_build_request(request)) def _queue_msg(self, item, **kwargs): if not self._thread: raise ServerError("Client not running") if not self._callback or kwargs.get('sync'): item['sync'] = True self.request_queue.put(item) command, msg = self.result_queue.get() if command == cmd.CMD_MESSAGE_ERROR: raise ServerError(msg) return msg else: self.request_queue.put(item) def messages(self, **kwargs): """Get list of messages with metadata.""" return self._queue_msg({'cmd': cmd.CMD_MESSAGE_LIST}, **kwargs) def mp3(self, sha, **kwargs): """Get raw MP3 of a message.""" return self._queue_msg( { 'cmd': cmd.CMD_MESSAGE_MP3, 'sha': _get_bytes(sha) }, **kwargs) def delete(self, sha, **kwargs): """Delete a message.""" return self._queue_msg( { 'cmd': cmd.CMD_MESSAGE_DELETE, 'sha': _get_bytes(sha) }, **kwargs) def cdr_count(self, **kwargs): """Request count of CDR entries""" return self._queue_msg({'cmd': cmd.CMD_MESSAGE_CDR_AVAILABLE}, **kwargs) def get_cdr(self, start=0, count=-1, **kwargs): """Request range of CDR messages""" sha = encode_to_sha("{:d},{:d}".format(start, count)) return self._queue_msg({ 'cmd': cmd.CMD_MESSAGE_CDR, 'sha': sha }, **kwargs)