tfc-mirror/src/transmitter/windows.py

272 lines
11 KiB
Python
Executable File

#!/usr/bin/env python3.7
# -*- coding: utf-8 -*-
"""
TFC - Onion-routed, endpoint secure messaging system
Copyright (C) 2013-2020 Markus Ottela
This file is part of TFC.
TFC is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.
TFC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with TFC. If not, see <https://www.gnu.org/licenses/>.
"""
import typing
from typing import Any, Dict, Iterable, Iterator, List, Optional, Sized
from src.common.db_contacts import Contact
from src.common.exceptions import SoftError
from src.common.input import yes
from src.common.output import clear_screen
from src.common.statics import KEX_STATUS_PENDING, WINDOW_SELECT_QUEUE, WIN_SELECT, WIN_TYPE_CONTACT, WIN_TYPE_GROUP
from src.transmitter.contact import add_new_contact
from src.transmitter.key_exchanges import export_onion_service_data, start_key_exchange
from src.transmitter.packet import queue_command
if typing.TYPE_CHECKING:
from multiprocessing import Queue
from src.common.db_contacts import ContactList
from src.common.db_groups import Group, GroupList
from src.common.db_onion import OnionService
from src.common.db_settings import Settings
from src.common.gateway import Gateway
from src.transmitter.user_input import UserInput
QueueDict = Dict[bytes, Queue[Any]]
class TxWindow(Iterable[Contact], Sized):
"""\
TxWindow object contains data about the active recipient (contact or
group).
"""
def __init__(self,
contact_list: 'ContactList',
group_list: 'GroupList'
) -> None:
"""Create a new TxWindow object."""
self.contact_list = contact_list
self.group_list = group_list
self.window_contacts = [] # type: List[Contact]
self.contact = None # type: Optional[Contact]
self.group = None # type: Optional[Group]
self.name = '' # type: str
self.uid = b'' # type: bytes
self.group_id = None # type: Optional[bytes]
self.log_messages = None # type: Optional[bool]
self.type = '' # type: str
self.type_print = None # type: Optional[str]
def __iter__(self) -> Iterator[Contact]:
"""Iterate over Contact objects in the window."""
yield from self.window_contacts
def __len__(self) -> int:
"""Return the number of contacts in the window."""
return len(self.window_contacts)
def select_tx_window(self,
settings: 'Settings', # Settings object
queues: 'QueueDict', # Dictionary of Queues
onion_service: 'OnionService', # OnionService object
gateway: 'Gateway', # Gateway object
selection: Optional[str] = None, # Selector for window
cmd: bool = False # True when `/msg` command is used to switch window
) -> None:
"""Select specified window or ask the user to specify one."""
if selection is None:
selection = self.select_recipient()
if selection in self.group_list.get_list_of_group_names():
self.select_group(selection, cmd, settings)
elif selection in self.contact_list.contact_selectors():
self.select_contact(selection, cmd, queues, settings)
elif selection.startswith("/"):
self.window_selection_command(selection, settings, queues, onion_service, gateway)
else:
raise SoftError("Error: No contact/group was found.")
if settings.traffic_masking:
queues[WINDOW_SELECT_QUEUE].put(self.window_contacts)
packet = WIN_SELECT + self.uid
queue_command(packet, settings, queues)
clear_screen()
def select_recipient(self) -> str:
"""Select recipient."""
self.contact_list.print_contacts()
self.group_list.print_groups()
if self.contact_list.has_only_pending_contacts():
print("\n'/connect' sends Onion Service/contact data to Relay"
"\n'/add' adds another contact."
"\n'/rm <Nick>' removes an existing contact.\n")
selection = input("Select recipient: ").strip()
return selection
def select_contact(self,
selection: str,
cmd: bool,
queues: 'QueueDict',
settings: 'Settings'
) -> None:
"""Select contact."""
if cmd and settings.traffic_masking:
contact = self.contact_list.get_contact_by_address_or_nick(selection)
if contact.onion_pub_key != self.uid:
raise SoftError("Error: Can't change window during traffic masking.", head_clear=True)
self.contact = self.contact_list.get_contact_by_address_or_nick(selection)
if self.contact.kex_status == KEX_STATUS_PENDING:
start_key_exchange(self.contact.onion_pub_key,
self.contact.nick,
self.contact_list,
settings, queues)
self.group = None
self.group_id = None
self.window_contacts = [self.contact]
self.name = self.contact.nick
self.uid = self.contact.onion_pub_key
self.log_messages = self.contact.log_messages
self.type = WIN_TYPE_CONTACT
self.type_print = "contact"
def select_group(self,
selection: str,
cmd: bool,
settings: 'Settings'
) -> None:
"""Select group."""
if cmd and settings.traffic_masking and selection != self.name:
raise SoftError("Error: Can't change window during traffic masking.", head_clear=True)
self.contact = None
self.group = self.group_list.get_group(selection)
self.window_contacts = self.group.members
self.name = self.group.name
self.uid = self.group.group_id
self.group_id = self.group.group_id
self.log_messages = self.group.log_messages
self.type = WIN_TYPE_GROUP
self.type_print = "group"
clear_screen()
def window_selection_command(self,
selection: str,
settings: 'Settings',
queues: 'QueueDict',
onion_service: 'OnionService',
gateway: 'Gateway'
) -> None:
"""Commands for adding and removing contacts from contact selection menu.
In situations where only pending contacts are available and
those contacts are not online, these commands prevent the user
from not being able to add new contacts.
"""
if selection == '/add':
add_new_contact(self.contact_list, self.group_list, settings, queues, onion_service)
raise SoftError("New contact added.", output=False)
if selection == '/connect':
export_onion_service_data(self.contact_list, settings, onion_service, gateway)
elif selection.startswith('/rm'):
try:
selection = selection.split()[1]
except IndexError:
raise SoftError("Error: No account specified.", delay=1)
if not yes(f"Remove contact '{selection}'?", abort=False, head=1):
raise SoftError("Removal of contact aborted.", head=0, delay=1)
if selection in self.contact_list.contact_selectors():
onion_pub_key = self.contact_list.get_contact_by_address_or_nick(selection).onion_pub_key
self.contact_list.remove_contact_by_pub_key(onion_pub_key)
self.contact_list.store_contacts()
raise SoftError(f"Removed contact '{selection}'.", delay=1)
raise SoftError(f"Error: Unknown contact '{selection}'.", delay=1)
else:
raise SoftError("Error: Invalid command.", delay=1)
def deselect(self) -> None:
"""Deselect active window."""
self.window_contacts = []
self.contact = None
self.group = None
self.name = ''
self.uid = b''
self.log_messages = None
self.type = ''
self.type_print = None
def is_selected(self) -> bool:
"""Return True if a window is selected, else False."""
return self.name != ''
def update_log_messages(self) -> None:
"""Update window's logging setting."""
if self.type == WIN_TYPE_CONTACT and self.contact is not None:
self.log_messages = self.contact.log_messages
if self.type == WIN_TYPE_GROUP and self.group is not None:
self.log_messages = self.group.log_messages
def update_window(self, group_list: 'GroupList') -> None:
"""Update window.
Since previous input may have changed the window data, reload
window data before prompting for UserInput.
"""
if self.type == WIN_TYPE_GROUP:
if self.group_id is not None and group_list.has_group_id(self.group_id):
self.group = group_list.get_group_by_id(self.group_id)
self.window_contacts = self.group.members
self.name = self.group.name
self.uid = self.group.group_id
else:
self.deselect()
elif self.type == WIN_TYPE_CONTACT:
if self.contact is not None and self.contact_list.has_pub_key(self.contact.onion_pub_key):
# Reload window contact in case keys were re-exchanged.
self.contact = self.contact_list.get_contact_by_pub_key(self.contact.onion_pub_key)
self.window_contacts = [self.contact]
def select_window(user_input: 'UserInput',
window: 'TxWindow',
settings: 'Settings',
queues: 'QueueDict',
onion_service: 'OnionService',
gateway: 'Gateway'
) -> None:
"""Select a new window to send messages/files."""
try:
selection = user_input.plaintext.split()[1]
except (IndexError, TypeError):
raise SoftError("Error: Invalid recipient.", head_clear=True)
window.select_tx_window(settings, queues, onion_service, gateway, selection=selection, cmd=True)