class TestPacketList(unittest.TestCase): def setUp(self): self.contact_list = ContactList(nicks=['Alice', 'Bob']) self.settings = Settings() self.onion_pub_key = nick_to_pub_key('Alice') packet = Packet(self.onion_pub_key, ORIGIN_CONTACT_HEADER, MESSAGE, self.contact_list.get_contact_by_address_or_nick('Alice'), self.settings) self.packet_list = PacketList(self.settings, self.contact_list) self.packet_list.packets = [packet] def test_packet_list_iterates_over_contact_objects(self): for p in self.packet_list: self.assertIsInstance(p, Packet) def test_len_returns_number_of_contacts(self): self.assertEqual(len(self.packet_list), 1) def test_has_packet(self): self.assertTrue(self.packet_list.has_packet(self.onion_pub_key, ORIGIN_CONTACT_HEADER, MESSAGE)) self.assertFalse(self.packet_list.has_packet(self.onion_pub_key, ORIGIN_USER_HEADER, MESSAGE)) def test_get_packet(self): packet = self.packet_list.get_packet(self.onion_pub_key, ORIGIN_CONTACT_HEADER, MESSAGE) self.assertEqual(packet.onion_pub_key, self.onion_pub_key) self.assertEqual(packet.origin, ORIGIN_CONTACT_HEADER) self.assertEqual(packet.type, MESSAGE) packet = self.packet_list.get_packet(self.onion_pub_key, ORIGIN_CONTACT_HEADER, MESSAGE) self.assertEqual(packet.onion_pub_key, self.onion_pub_key) self.assertEqual(packet.origin, ORIGIN_CONTACT_HEADER) self.assertEqual(packet.type, MESSAGE)
def setUp(self): self.contact_list = ContactList(nicks=['Alice', 'Bob']) self.settings = Settings() self.onion_pub_key = nick_to_pub_key('Alice') packet = Packet(self.onion_pub_key, ORIGIN_CONTACT_HEADER, MESSAGE, self.contact_list.get_contact_by_address_or_nick('Alice'), self.settings) self.packet_list = PacketList(self.settings, self.contact_list) self.packet_list.packets = [packet]
def access_logs(window: Union['TxWindow', 'RxWindow'], contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', master_key: 'MasterKey', msg_to_load: int = 0, export: bool = False) -> None: """\ Load 'msg_to_load' last messages from log database and display or export them. The default value of zero for `msg_to_load` means all messages for the window will be retrieved from the log database. """ file_name = f'{DIR_USER_DATA}{settings.software_operation}_logs' packet_list = PacketList(settings, contact_list) message_list = [] # type: List[MsgTuple] group_msg_id = b'' check_log_file_exists(file_name) message_log = MessageLog(file_name, master_key.master_key) for log_entry in message_log: onion_pub_key, timestamp, origin, assembly_packet \ = separate_headers(log_entry, [ONION_SERVICE_PUBLIC_KEY_LENGTH, TIMESTAMP_LENGTH, ORIGIN_HEADER_LENGTH]) if window.type == WIN_TYPE_CONTACT and onion_pub_key != window.uid: continue packet = packet_list.get_packet(onion_pub_key, origin, MESSAGE, log_access=True) try: packet.add_packet(assembly_packet) except SoftError: continue if not packet.is_complete: continue group_msg_id = add_complete_message_to_message_list( timestamp, onion_pub_key, group_msg_id, packet, message_list, window) message_log.close_database() print_logs(message_list[-msg_to_load:], export, msg_to_load, window, contact_list, group_list, settings)
def print_logs(message_list: List[MsgTuple], export: bool, msg_to_load: int, window: Union['TxWindow', 'RxWindow'], contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings' ) -> None: """Print list of logged messages to screen or export them to file.""" terminal_width = get_terminal_width() system, m_dir = {TX: ("Transmitter", "sent to"), RX: ("Receiver", "to/from")}[settings.software_operation] f_name = open(f"{system} - Plaintext log ({window.name})", 'w+') if export else sys.stdout subset = '' if msg_to_load == 0 else f"{msg_to_load} most recent " title = textwrap.fill(f"Log file of {subset}message(s) {m_dir} {window.type} {window.name}", terminal_width) packet_list = PacketList(settings, contact_list) log_window = RxWindow(window.uid, contact_list, group_list, settings, packet_list) log_window.is_active = True log_window.message_log = message_list if message_list: if not export: clear_screen() print(title, file=f_name) print(terminal_width * '═', file=f_name) log_window.redraw( file=f_name) print("<End of log file>\n", file=f_name) else: raise FunctionReturn(f"No logged messages for {window.type} '{window.name}'.", head_clear=True) if export: f_name.close()
def setUp(self): self.unittest_dir = cd_unittest() self.msg = ("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean condimentum consectetur purus quis" " dapibus. Fusce venenatis lacus ut rhoncus faucibus. Cras sollicitudin commodo sapien, sed bibendu" "m velit maximus in. Aliquam ac metus risus. Sed cursus ornare luctus. Integer aliquet lectus id ma" "ssa blandit imperdiet. Ut sed massa eget quam facilisis rutrum. Mauris eget luctus nisl. Sed ut el" "it iaculis, faucibus lacus eget, sodales magna. Nunc sed commodo arcu. In hac habitasse platea dic" "tumst. Integer luctus aliquam justo, at vestibulum dolor iaculis ac. Etiam laoreet est eget odio r" "utrum, vel malesuada lorem rhoncus. Cras finibus in neque eu euismod. Nulla facilisi. Nunc nec ali" "quam quam, quis ullamcorper leo. Nunc egestas lectus eget est porttitor, in iaculis felis sceleris" "que. In sem elit, fringilla id viverra commodo, sagittis varius purus. Pellentesque rutrum loborti" "s neque a facilisis. Mauris id tortor placerat, aliquam dolor ac, venenatis arcu.") self.ts = datetime.now() self.master_key = MasterKey() self.settings = Settings(log_file_masking=True) self.file_name = f'{DIR_USER_DATA}{self.settings.software_operation}_logs' self.contact_list = ContactList(nicks=['Alice', 'Bob', 'Charlie', LOCAL_ID]) self.key_list = KeyList( nicks=['Alice', 'Bob', 'Charlie', LOCAL_ID]) self.group_list = GroupList( groups=['test_group']) self.packet_list = PacketList(contact_list=self.contact_list, settings=self.settings) self.window_list = WindowList(contact_list=self.contact_list, settings=self.settings, group_list=self.group_list, packet_list=self.packet_list) self.group_id = group_name_to_group_id('test_group') self.file_keys = dict() self.group_list.get_group('test_group').log_messages = True self.args = (self.window_list, self.packet_list, self.contact_list, self.key_list, self.group_list, self.settings, self.master_key, self.file_keys) ensure_dir(DIR_USER_DATA)
def setUp(self): self.unittest_dir = cd_unittest() self.ts = datetime.now() self.settings = Settings() self.master_key = MasterKey() self.group_list = GroupList() self.exit_queue = Queue() self.gateway = Gateway() self.window_list = WindowList(nicks=[LOCAL_ID]) self.contact_list = ContactList(nicks=[LOCAL_ID]) self.packet_list = PacketList(self.settings, self.contact_list) self.key_list = KeyList(nicks=[LOCAL_ID]) self.key_set = self.key_list.get_keyset(LOCAL_PUBKEY) self.args = (self.window_list, self.packet_list, self.contact_list, self.key_list, self.group_list, self.settings, self.master_key, self.gateway, self.exit_queue)
def remove_logs(contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', master_key: 'MasterKey', selector: bytes) -> None: """\ Remove log entries for selector (public key of an account/group ID). If the selector is a public key, all messages (both the private conversation and any associated group messages) sent to and received from the associated contact are removed. If the selector is a group ID, only messages for the group matching that group ID are removed. """ ensure_dir(DIR_USER_DATA) file_name = f'{DIR_USER_DATA}{settings.software_operation}_logs' temp_name = file_name + TEMP_SUFFIX packet_list = PacketList(settings, contact_list) entries_to_keep = [] # type: List[bytes] removed = False contact = len(selector) == ONION_SERVICE_PUBLIC_KEY_LENGTH check_log_file_exists(file_name) message_log = MessageLog(file_name, master_key.master_key) for log_entry in message_log: onion_pub_key, _, origin, assembly_packet = separate_headers( log_entry, [ ONION_SERVICE_PUBLIC_KEY_LENGTH, TIMESTAMP_LENGTH, ORIGIN_HEADER_LENGTH ]) if contact: if onion_pub_key == selector: removed = True else: entries_to_keep.append(log_entry) else: # Group packet = packet_list.get_packet(onion_pub_key, origin, MESSAGE, log_access=True) try: packet.add_packet(assembly_packet, log_entry) except SoftError: continue if not packet.is_complete: continue removed = check_packet_fate(entries_to_keep, packet, removed, selector) message_log.close_database() message_log_temp = MessageLog(temp_name, master_key.master_key) for log_entry in entries_to_keep: message_log_temp.insert_log_entry(log_entry) message_log_temp.close_database() os.replace(temp_name, file_name) try: name = contact_list.get_nick_by_pub_key( selector) if contact else group_list.get_group_by_id(selector).name except StopIteration: name = pub_key_to_short_address(selector) if contact else b58encode( selector) action = "Removed" if removed else "Found no" win_type = "contact" if contact else "group" raise SoftError(f"{action} log entries for {win_type} '{name}'.")
def remove_logs(contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', master_key: 'MasterKey', selector: bytes) -> None: """\ Remove log entries for selector (public key of an account/group ID). If the selector is a public key, all messages (both the private conversation and any associated group messages) sent to and received from the associated contact are removed. If the selector is a group ID, only messages for group determined by that group ID are removed. """ ensure_dir(DIR_USER_DATA) file_name = f'{DIR_USER_DATA}{settings.software_operation}_logs' temp_name = f'{file_name}_temp' log_file = get_logfile(file_name) packet_list = PacketList(settings, contact_list) ct_to_keep = [] # type: List[bytes] removed = False contact = len(selector) == ONION_SERVICE_PUBLIC_KEY_LENGTH for ct in iter(lambda: log_file.read(LOG_ENTRY_LENGTH), b''): plaintext = auth_and_decrypt(ct, master_key.master_key, database=file_name) onion_pub_key, _, origin, assembly_packet = separate_headers( plaintext, [ ONION_SERVICE_PUBLIC_KEY_LENGTH, TIMESTAMP_LENGTH, ORIGIN_HEADER_LENGTH ]) if contact: if onion_pub_key == selector: removed = True else: ct_to_keep.append(ct) else: # Group packet = packet_list.get_packet(onion_pub_key, origin, MESSAGE, log_access=True) try: packet.add_packet(assembly_packet, ct) except FunctionReturn: continue if not packet.is_complete: continue _, header, message = separate_headers( packet.assemble_message_packet(), [WHISPER_FIELD_LENGTH, MESSAGE_HEADER_LENGTH]) if header == PRIVATE_MESSAGE_HEADER: ct_to_keep.extend(packet.log_ct_list) packet.clear_assembly_packets() elif header == GROUP_MESSAGE_HEADER: group_id, _ = separate_header(message, GROUP_ID_LENGTH) if group_id == selector: removed = True else: ct_to_keep.extend(packet.log_ct_list) packet.clear_assembly_packets() log_file.close() if os.path.isfile(temp_name): os.remove(temp_name) with open(temp_name, 'wb+') as f: if ct_to_keep: f.write(b''.join(ct_to_keep)) os.remove(file_name) os.rename(temp_name, file_name) try: name = contact_list.get_contact_by_pub_key(selector).nick \ if contact else group_list.get_group_by_id(selector).name except StopIteration: name = pub_key_to_short_address(selector) \ if contact else b58encode(selector) action = "Removed" if removed else "Found no" win_type = "contact" if contact else "group" raise FunctionReturn(f"{action} log entries for {win_type} '{name}'.")
def access_logs(window: Union['TxWindow', 'RxWindow'], contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings', master_key: 'MasterKey', msg_to_load: int = 0, export: bool = False) -> None: """\ Load 'msg_to_load' last messages from log database and display or export them. The default value of zero for `msg_to_load` means all messages for the window will be retrieved from the log database. """ file_name = f'{DIR_USER_DATA}{settings.software_operation}_logs' log_file = get_logfile(file_name) packet_list = PacketList(settings, contact_list) message_log = [] # type: List[MsgTuple] group_msg_id = b'' for ct in iter(lambda: log_file.read(LOG_ENTRY_LENGTH), b''): plaintext = auth_and_decrypt(ct, master_key.master_key, database=file_name) onion_pub_key, timestamp, origin, assembly_packet = separate_headers( plaintext, [ ONION_SERVICE_PUBLIC_KEY_LENGTH, TIMESTAMP_LENGTH, ORIGIN_HEADER_LENGTH ]) if window.type == WIN_TYPE_CONTACT and onion_pub_key != window.uid: continue packet = packet_list.get_packet(onion_pub_key, origin, MESSAGE, log_access=True) try: packet.add_packet(assembly_packet) except FunctionReturn: continue if not packet.is_complete: continue whisper_byte, header, message = separate_headers( packet.assemble_message_packet(), [WHISPER_FIELD_LENGTH, MESSAGE_HEADER_LENGTH]) whisper = bytes_to_bool(whisper_byte) if header == PRIVATE_MESSAGE_HEADER and window.type == WIN_TYPE_CONTACT: message_log.append( (bytes_to_timestamp(timestamp), message.decode(), onion_pub_key, packet.origin, whisper, False)) elif header == GROUP_MESSAGE_HEADER and window.type == WIN_TYPE_GROUP: purp_group_id, message = separate_header(message, GROUP_ID_LENGTH) if window.group is not None and purp_group_id != window.group.group_id: continue purp_msg_id, message = separate_header(message, GROUP_MSG_ID_LENGTH) if packet.origin == ORIGIN_USER_HEADER: if purp_msg_id == group_msg_id: continue group_msg_id = purp_msg_id message_log.append( (bytes_to_timestamp(timestamp), message.decode(), onion_pub_key, packet.origin, whisper, False)) log_file.close() print_logs(message_log[-msg_to_load:], export, msg_to_load, window, contact_list, group_list, settings)
def output_loop(queues: Dict[bytes, 'Queue[Any]'], gateway: 'Gateway', settings: 'Settings', contact_list: 'ContactList', key_list: 'KeyList', group_list: 'GroupList', master_key: 'MasterKey', message_log: 'MessageLog', stdin_fd: int, unit_test: bool = False) -> None: """Process packets in message queues according to their priority.""" sys.stdin = os.fdopen(stdin_fd) packet_buffer = dict() # type: packet_buffer_type file_buffer = dict() # type: file_buffer_type file_keys = dict() # type: file_keys_type kdk_hashes = [] # type: List[bytes] packet_hashes = [] # type: List[bytes] packet_list = PacketList(settings, contact_list) window_list = WindowList(settings, contact_list, group_list, packet_list) clear_screen() while True: try: # Local key packets process_local_key_queue(queues, window_list, contact_list, key_list, settings, kdk_hashes, packet_hashes) # Commands process_command_queue(queues, window_list, contact_list, group_list, settings, key_list, packet_list, master_key, gateway) # File window refresh window_list.refresh_file_window_check() # Cached messages process_cached_messages(window_list, contact_list, group_list, key_list, settings, packet_list, message_log, file_keys, packet_buffer) # New messages process_message_queue(queues, window_list, contact_list, group_list, key_list, settings, packet_list, message_log, file_keys, packet_buffer) # Cached files process_cached_files(window_list, contact_list, settings, file_keys, file_buffer) # New files process_file_queue(queues, window_list, contact_list, settings, file_keys, file_buffer) time.sleep(0.01) if unit_test and queues[UNIT_TEST_QUEUE].qsize() != 0: break except (KeyError, KeyboardInterrupt, SoftError): pass
def output_loop(queues: Dict[bytes, 'Queue[Any]'], gateway: 'Gateway', settings: 'Settings', contact_list: 'ContactList', key_list: 'KeyList', group_list: 'GroupList', master_key: 'MasterKey', stdin_fd: int, unit_test: bool = False) -> None: """Process packets in message queues according to their priority.""" local_key_queue = queues[LOCAL_KEY_DATAGRAM_HEADER] message_queue = queues[MESSAGE_DATAGRAM_HEADER] file_queue = queues[FILE_DATAGRAM_HEADER] command_queue = queues[COMMAND_DATAGRAM_HEADER] exit_queue = queues[EXIT_QUEUE] sys.stdin = os.fdopen(stdin_fd) packet_buffer = dict() # type: Dict[bytes, List[Tuple[datetime, bytes]]] file_buffer = dict() # type: Dict[bytes, Tuple[datetime, bytes]] file_keys = dict() # type: Dict[bytes, bytes] kdk_hashes = [] # type: List[bytes] packet_hashes = [] # type: List[bytes] packet_list = PacketList(settings, contact_list) window_list = WindowList(settings, contact_list, group_list, packet_list) clear_screen() while True: try: if local_key_queue.qsize() != 0: ts, packet = local_key_queue.get() process_local_key(ts, packet, window_list, contact_list, key_list, settings, kdk_hashes, packet_hashes, local_key_queue) continue if not contact_list.has_local_contact(): time.sleep(0.1) continue # Commands if command_queue.qsize() != 0: ts, packet = command_queue.get() process_command(ts, packet, window_list, packet_list, contact_list, key_list, group_list, settings, master_key, gateway, exit_queue) continue # File window refresh if window_list.active_win is not None and window_list.active_win.uid == WIN_UID_FILE: window_list.active_win.redraw_file_win() # Cached message packets for onion_pub_key in packet_buffer: if (contact_list.has_pub_key(onion_pub_key) and key_list.has_rx_mk(onion_pub_key) and packet_buffer[onion_pub_key]): ts, packet = packet_buffer[onion_pub_key].pop(0) process_message(ts, packet, window_list, packet_list, contact_list, key_list, group_list, settings, master_key, file_keys) continue # New messages if message_queue.qsize() != 0: ts, packet = message_queue.get() onion_pub_key = packet[:ONION_SERVICE_PUBLIC_KEY_LENGTH] if contact_list.has_pub_key( onion_pub_key) and key_list.has_rx_mk(onion_pub_key): process_message(ts, packet, window_list, packet_list, contact_list, key_list, group_list, settings, master_key, file_keys) else: packet_buffer.setdefault(onion_pub_key, []).append( (ts, packet)) continue # Cached files if file_buffer: for k in file_buffer: key_to_remove = b'' try: if k in file_keys: key_to_remove = k ts_, file_ct = file_buffer[k] dec_key = file_keys[k] onion_pub_key = k[:ONION_SERVICE_PUBLIC_KEY_LENGTH] process_file(ts_, onion_pub_key, file_ct, dec_key, contact_list, window_list, settings) finally: if key_to_remove: file_buffer.pop(k) file_keys.pop(k) break # New files if file_queue.qsize() != 0: ts, packet = file_queue.get() new_file(ts, packet, file_keys, file_buffer, contact_list, window_list, settings) time.sleep(0.01) if unit_test and queues[UNIT_TEST_QUEUE].qsize() != 0: break except (FunctionReturn, KeyError, KeyboardInterrupt): pass