commit 02327e0bed85ad2e87ce3c0ecb3bb62bcce117dd Author: Damian Johnson <ata...@torproject.org> Date: Sat Sep 5 15:00:57 2015 -0700
Consolidate remaining nyx/connections/* module Ok, trimmed the remaining bits enough that we can consolidate in connection_panel.py. Still a mess, but now a manageable mess. --- nyx/connection_panel.py | 594 ++++++++++++++++++++++++++++++++++- nyx/connections/__init__.py | 10 - nyx/connections/circ_entry.py | 177 ----------- nyx/connections/conn_entry.py | 453 -------------------------- nyx/connections/descriptor_popup.py | 174 ---------- nyx/popups.py | 167 +++++++++- setup.py | 2 +- 7 files changed, 752 insertions(+), 825 deletions(-) diff --git a/nyx/connection_panel.py b/nyx/connection_panel.py index c583a00..9fcacae 100644 --- a/nyx/connection_panel.py +++ b/nyx/connection_panel.py @@ -6,17 +6,18 @@ import re import time import collections import curses +import datetime import itertools import threading import nyx.popups import nyx.util.tracker +import nyx.util.ui_tools -from nyx.connections import descriptor_popup from nyx.util import panel, tor_controller, ui_tools from stem.control import Listener, State -from stem.util import conf, connection, enum +from stem.util import conf, connection, enum, str_tools try: # added in python 3.2 @@ -71,6 +72,12 @@ SORT_COLORS = { SortAttr.COUNTRY: 'blue', } +# static data for listing format +# <src> --> <dst> <etc><padding> + +LABEL_FORMAT = '%s --> %s %s%s' +LABEL_MIN_PADDING = 2 # min space between listing label and following data + def conf_handler(key, value): if key == 'features.connection.listing_type': @@ -90,6 +97,10 @@ CONFIG = conf.config_dict('nyx', { }, conf_handler) +def to_unix_time(dt): + return (dt - datetime.datetime(1970, 1, 1)).total_seconds() + + class Entry(object): @staticmethod @lru_cache() @@ -137,8 +148,7 @@ class ConnectionEntry(Entry): @lru_cache() def get_lines(self): - import nyx.connections.conn_entry - return [nyx.connections.conn_entry.ConnectionLine(self, self._connection)] + return [ConnectionLine(self, self._connection)] @lru_cache() def get_type(self): @@ -201,7 +211,6 @@ class CircuitEntry(Entry): @lru_cache() def get_lines(self): - from nyx.connections.circ_entry import CircHeaderLine, CircLine return [CircHeaderLine(self, self._circuit)] + [CircLine(self, self._circuit, fp) for fp, _ in self._circuit.path] def get_type(self): @@ -211,6 +220,575 @@ class CircuitEntry(Entry): return False +class ConnectionLine(object): + """ + Display component of the ConnectionEntry. + """ + + def __init__(self, entry, conn, include_port = True): + self._entry = entry + self.connection = conn + + # includes the port or expanded ip address field when displaying listing + # information if true + + self.include_port = include_port + + def get_listing_prefix(self): + """ + Provides a list of characters to be appended before the listing entry. + """ + + return () + + def get_locale(self, default = None): + """ + Provides the two letter country code for the remote endpoint. + """ + + return tor_controller().get_info('ip-to-country/%s' % self.connection.remote_address, default) + + def get_fingerprint(self, default = None): + """ + Provides the fingerprint of this relay. + """ + + if self._entry.get_type() in (Category.OUTBOUND, Category.CIRCUIT, Category.DIRECTORY, Category.EXIT): + my_fingerprint = nyx.util.tracker.get_consensus_tracker().get_relay_fingerprint(self.connection.remote_address, self.connection.remote_port) + return my_fingerprint if my_fingerprint else default + else: + return default # inbound connections don't have an ORPort we can resolve + + def get_nickname(self, default = None): + """ + Provides the nickname of this relay. + """ + + nickname = nyx.util.tracker.get_consensus_tracker().get_relay_nickname(self.get_fingerprint()) + return nickname if nickname else default + + def get_listing_entry(self, width, current_time, listing_type): + """ + Provides the tuple list for this connection's listing. Lines are composed + of the following components: + <src> --> <dst> <etc> <uptime> (<type>) + + Listing.IP_ADDRESS: + src - <internal addr:port> --> <external addr:port> + dst - <destination addr:port> + etc - <fingerprint> <nickname> + + Listing.FINGERPRINT: + src - localhost + dst - <destination fingerprint> + etc - <nickname> <destination addr:port> + + Listing.NICKNAME: + src - <source nickname> + dst - <destination nickname> + etc - <fingerprint> <destination addr:port> + + Arguments: + width - maximum length of the line + current_time - unix timestamp for what the results should consider to be + the current time + listing_type - primary attribute we're listing connections by + """ + + # fetch our (most likely cached) display entry for the listing + + my_listing = self._get_listing_entry(width, listing_type) + + # fill in the current uptime and return the results + + time_prefix = '+' if self.connection.is_legacy else ' ' + + time_label = time_prefix + '%5s' % str_tools.time_label(current_time - self.connection.start_time, 1) + my_listing[2] = (time_label, my_listing[2][1]) + + return my_listing + + @lru_cache() + def _get_listing_entry(self, width, listing_type): + entry_type = self._entry.get_type() + + # Lines are split into the following components in reverse: + # init gap - " " + # content - "<src> --> <dst> <etc> " + # time - "<uptime>" + # preType - " (" + # category - "<type>" + # postType - ") " + + line_format = nyx.util.ui_tools.get_color(CATEGORY_COLOR[entry_type]) + + draw_entry = [(' ', line_format), + (self._get_listing_content(width - 19, listing_type), line_format), + (' ', line_format), + (' (', line_format), + (entry_type.upper(), line_format | curses.A_BOLD), + (')' + ' ' * (9 - len(entry_type)), line_format)] + + return draw_entry + + @lru_cache() + def get_details(self, width): + """ + Provides details on the connection, correlated against available consensus + data. + + Arguments: + width - available space to display in + """ + + detail_format = (curses.A_BOLD, CATEGORY_COLOR[self._entry.get_type()]) + return [(line, detail_format) for line in self._get_detail_content(width)] + + def get_etc_content(self, width, listing_type): + """ + Provides the optional content for the connection. + + Arguments: + width - maximum length of the line + listing_type - primary attribute we're listing connections by + """ + + # for applications show the command/pid + + if self._entry.get_type() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL): + port = self.connection.local_port if self._entry.get_type() == Category.HIDDEN else self.connection.remote_port + + try: + process = nyx.util.tracker.get_port_usage_tracker().fetch(port) + display_label = '%s (%s)' % (process.name, process.pid) if process.pid else process.name + except nyx.util.tracker.UnresolvedResult: + display_label = 'resolving...' + except nyx.util.tracker.UnknownApplication: + display_label = 'UNKNOWN' + + if len(display_label) < width: + return ('%%-%is' % width) % display_label + else: + return '' + + # for everything else display connection/consensus information + + destination_address = self.get_destination_label(26, include_locale = True) + etc, used_space = '', 0 + + if listing_type == Listing.IP_ADDRESS: + if width > used_space + 42: + # show fingerprint (column width: 42 characters) + + etc += '%-40s ' % self.get_fingerprint('UNKNOWN') + used_space += 42 + + if width > used_space + 10: + # show nickname (column width: remainder) + + nickname_space = width - used_space + nickname_label = str_tools.crop(self.get_nickname('UNKNOWN'), nickname_space, 0) + etc += ('%%-%is ' % nickname_space) % nickname_label + used_space += nickname_space + 2 + elif listing_type == Listing.FINGERPRINT: + if width > used_space + 17: + # show nickname (column width: min 17 characters, consumes any remaining space) + + nickname_space = width - used_space - 2 + + # if there's room then also show a column with the destination + # ip/port/locale (column width: 28 characters) + + is_locale_included = width > used_space + 45 + + if is_locale_included: + nickname_space -= 28 + + nickname_label = str_tools.crop(self.get_nickname('UNKNOWN'), nickname_space, 0) + etc += ('%%-%is ' % nickname_space) % nickname_label + used_space += nickname_space + 2 + + if is_locale_included: + etc += '%-26s ' % destination_address + used_space += 28 + else: + if width > used_space + 42: + # show fingerprint (column width: 42 characters) + etc += '%-40s ' % self.get_fingerprint('UNKNOWN') + used_space += 42 + + if width > used_space + 28: + # show destination ip/port/locale (column width: 28 characters) + etc += '%-26s ' % destination_address + used_space += 28 + + return ('%%-%is' % width) % etc + + def _get_listing_content(self, width, listing_type): + """ + Provides the source, destination, and extra info for our listing. + + Arguments: + width - maximum length of the line + listing_type - primary attribute we're listing connections by + """ + + controller = tor_controller() + my_type = self._entry.get_type() + destination_address = self.get_destination_label(26, include_locale = True) + + # The required widths are the sum of the following: + # - room for LABEL_FORMAT and LABEL_MIN_PADDING (11 characters) + # - base data for the listing + # - that extra field plus any previous + + used_space = len(LABEL_FORMAT % tuple([''] * 4)) + LABEL_MIN_PADDING + local_port = ':%s' % self.connection.local_port if self.include_port else '' + + src, dst, etc = '', '', '' + + if listing_type == Listing.IP_ADDRESS: + my_external_address = controller.get_info('address', self.connection.local_address) + + # Show our external address if it's going through tor. + + if my_type not in (Category.SOCKS, Category.HIDDEN, Category.CONTROL): + src_address = my_external_address + local_port + else: + src_address = self.connection.local_address + local_port + + if my_type in (Category.SOCKS, Category.CONTROL): + # Like inbound connections these need their source and destination to + # be swapped. However, this only applies when listing by IP (their + # fingerprint and nickname are both for us). Reversing the fields here + # to keep the same column alignments. + + src = '%-21s' % destination_address + dst = '%-26s' % src_address + else: + src = '%-21s' % src_address # ip:port = max of 21 characters + dst = '%-26s' % destination_address # ip:port (xx) = max of 26 characters + + used_space += len(src) + len(dst) # base data requires 47 characters + + etc = self.get_etc_content(width - used_space, listing_type) + used_space += len(etc) + elif listing_type == Listing.FINGERPRINT: + src = 'localhost' + dst = '%-40s' % ('localhost' if my_type == Category.CONTROL else self.get_fingerprint('UNKNOWN')) + + used_space += len(src) + len(dst) # base data requires 49 characters + + etc = self.get_etc_content(width - used_space, listing_type) + used_space += len(etc) + else: + # base data requires 50 min characters + src = controller.get_conf('nickname', 'UNKNOWN') + dst = controller.get_conf('nickname', 'UNKNOWN') if my_type == Category.CONTROL else self.get_nickname('UNKNOWN') + + min_base_space = 50 + + etc = self.get_etc_content(width - used_space - min_base_space, listing_type) + used_space += len(etc) + + base_space = width - used_space + used_space = width # prevents padding at the end + + if len(src) + len(dst) > base_space: + src = str_tools.crop(src, base_space / 3) + dst = str_tools.crop(dst, base_space - len(src)) + + # pads dst entry to its max space + + dst = ('%%-%is' % (base_space - len(src))) % dst + + if my_type == Category.INBOUND: + src, dst = dst, src + + padding = ' ' * (width - used_space + LABEL_MIN_PADDING) + + return LABEL_FORMAT % (src, dst, etc, padding) + + def _get_detail_content(self, width): + """ + Provides a list with detailed information for this connection. + + Arguments: + width - max length of lines + """ + + lines = [''] * 7 + lines[0] = 'address: %s' % self.get_destination_label(width - 11) + lines[1] = 'locale: %s' % ('??' if self._entry.is_private() else self.get_locale('??')) + + # Remaining data concerns the consensus results, with three possible cases: + # - if there's a single match then display its details + # - if there's multiple potential relays then list all of the combinations + # of ORPorts / Fingerprints + # - if no consensus data is available then say so (probably a client or + # exit connection) + + fingerprint = self.get_fingerprint() + controller = tor_controller() + + if fingerprint: + lines[1] = '%-13sfingerprint: %s' % (lines[1], fingerprint) # append fingerprint to second line + + router_status_entry = controller.get_network_status(fingerprint, None) + server_descriptor = controller.get_server_descriptor(fingerprint, None) + + if router_status_entry: + dir_port_label = 'dirport: %s' % router_status_entry.dir_port if router_status_entry.dir_port else '' + lines[2] = 'nickname: %-25s orport: %-10s %s' % (router_status_entry.nickname, router_status_entry.or_port, dir_port_label) + lines[3] = 'published: %s' % router_status_entry.published.strftime("%H:%M %m/%d/%Y") + lines[4] = 'flags: %s' % ', '.join(router_status_entry.flags) + + if server_descriptor: + policy_label = server_descriptor.exit_policy.summary() if server_descriptor.exit_policy else 'unknown' + lines[5] = 'exit policy: %s' % policy_label + lines[3] = '%-35s os: %-14s version: %s' % (lines[3], server_descriptor.operating_system, server_descriptor.tor_version) + + if server_descriptor.contact: + lines[6] = 'contact: %s' % server_descriptor.contact + else: + all_matches = nyx.util.tracker.get_consensus_tracker().get_all_relay_fingerprints(self.connection.remote_address) + + if all_matches: + # multiple matches + lines[2] = 'Multiple matches, possible fingerprints are:' + + for i in range(len(all_matches)): + is_last_line = i == 3 + + relay_port, relay_fingerprint = all_matches[i] + line_text = '%i. or port: %-5s fingerprint: %s' % (i + 1, relay_port, relay_fingerprint) + + # if there's multiple lines remaining at the end then give a count + + remaining_relays = len(all_matches) - i + + if is_last_line and remaining_relays > 1: + line_text = '... %i more' % remaining_relays + + lines[3 + i] = line_text + + if is_last_line: + break + else: + # no consensus entry for this ip address + lines[2] = 'No consensus data found' + + # crops any lines that are too long + + for i in range(len(lines)): + lines[i] = str_tools.crop(lines[i], width - 2) + + return lines + + def get_destination_label(self, max_length, include_locale = False): + """ + Provides a short description of the destination. This is made up of two + components, the base <ip addr>:<port> and an extra piece of information in + parentheses. The IP address is scrubbed from private connections. + + Extra information is... + - the port's purpose for exit connections + - the locale, the address isn't private and isn't on the local network + - nothing otherwise + + Arguments: + max_length - maximum length of the string returned + include_locale - possibly includes the locale + """ + + # destination of the connection + + address_label = '<scrubbed>' if self._entry.is_private() else self.connection.remote_address + port_label = ':%s' % self.connection.remote_port + destination_address = address_label + port_label + + # Only append the extra info if there's at least a couple characters of + # space (this is what's needed for the country codes). + + if len(destination_address) + 5 <= max_length: + space_available = max_length - len(destination_address) - 3 + + if self._entry.get_type() == Category.EXIT: + purpose = connection.port_usage(self.connection.remote_port) + + if purpose: + # BitTorrent is a common protocol to truncate, so just use "Torrent" + # if there's not enough room. + + if len(purpose) > space_available and purpose == 'BitTorrent': + purpose = 'Torrent' + + # crops with a hyphen if too long + + purpose = str_tools.crop(purpose, space_available, ending = str_tools.Ending.HYPHEN) + + destination_address += ' (%s)' % purpose + elif not connection.is_private_address(self.connection.remote_address): + extra_info = [] + + if include_locale and not tor_controller().is_geoip_unavailable(): + foreign_locale = self.get_locale('??') + extra_info.append(foreign_locale) + space_available -= len(foreign_locale) + 2 + + if extra_info: + destination_address += ' (%s)' % ', '.join(extra_info) + + return destination_address[:max_length] + + +class CircHeaderLine(ConnectionLine): + """ + Initial line of a client entry. This has the same basic format as connection + lines except that its etc field has circuit attributes. + """ + + def __init__(self, entry, circ): + if circ.status == 'BUILT': + self._remote_fingerprint = circ.path[-1][0] + exit_address, exit_port = nyx.util.tracker.get_consensus_tracker().get_relay_address(self._remote_fingerprint, ('192.168.0.1', 0)) + self.is_built = True + else: + exit_address, exit_port = '0.0.0.0', 0 + self.is_built = False + self._remote_fingerprint = None + + ConnectionLine.__init__(self, entry, nyx.util.tracker.Connection(to_unix_time(circ.created), False, '127.0.0.1', 0, exit_address, exit_port, 'tcp'), include_port = False) + self.circuit = circ + + def get_fingerprint(self, default = None): + return self._remote_fingerprint if self._remote_fingerprint else ConnectionLine.get_fingerprint(self, default) + + def get_destination_label(self, max_length, include_locale = False): + if not self.is_built: + return 'Building...' + + return ConnectionLine.get_destination_label(self, max_length, include_locale) + + def get_etc_content(self, width, listing_type): + """ + Attempts to provide all circuit related stats. Anything that can't be + shown completely (not enough room) is dropped. + """ + + etc_attr = ['Purpose: %s' % self.circuit.purpose.capitalize(), 'Circuit ID: %s' % self.circuit.id] + + for i in range(len(etc_attr), -1, -1): + etc_label = ', '.join(etc_attr[:i]) + + if len(etc_label) <= width: + return ('%%-%is' % width) % etc_label + + return '' + + @lru_cache() + def get_details(self, width): + if not self.is_built: + detail_format = (curses.A_BOLD, CATEGORY_COLOR[self._entry.get_type()]) + return [('Building Circuit...', detail_format)] + else: + return ConnectionLine.get_details(self, width) + + +class CircLine(ConnectionLine): + """ + An individual hop in a circuit. This overwrites the displayed listing, but + otherwise makes use of the ConnectionLine attributes (for the detail display, + caching, etc). + """ + + def __init__(self, entry, circ, fingerprint): + relay_ip, relay_port = nyx.util.tracker.get_consensus_tracker().get_relay_address(fingerprint, ('192.168.0.1', 0)) + ConnectionLine.__init__(self, entry, nyx.util.tracker.Connection(to_unix_time(circ.created), False, '127.0.0.1', 0, relay_ip, relay_port, 'tcp'), include_port = False) + self._fingerprint = fingerprint + self._is_last = False + + circ_path = [path_entry[0] for path_entry in circ.path] + circ_index = circ_path.index(fingerprint) + + if circ_index == len(circ_path) - 1: + placement_type = 'Exit' if circ.status == 'BUILT' else 'Extending' + self._is_last = True + elif circ_index == 0: + placement_type = 'Guard' + else: + placement_type = 'Middle' + + self.placement_label = '%i / %s' % (circ_index + 1, placement_type) + + def get_fingerprint(self, default = None): + self._fingerprint + + def get_listing_prefix(self): + if self._is_last: + return (ord(' '), curses.ACS_LLCORNER, curses.ACS_HLINE, ord(' ')) + else: + return (ord(' '), curses.ACS_VLINE, ord(' '), ord(' ')) + + def get_listing_entry(self, width, current_time, listing_type): + """ + Provides the [(msg, attr)...] listing for this relay in the circuilt + listing. Lines are composed of the following components: + <bracket> <dst> <etc> <placement label> + + The dst and etc entries largely match their ConnectionEntry counterparts. + + Arguments: + width - maximum length of the line + current_time - the current unix time (ignored) + listing_type - primary attribute we're listing connections by + """ + + return self._get_listing_entry(width, listing_type) + + @lru_cache() + def _get_listing_entry(self, width, listing_type): + line_format = nyx.util.ui_tools.get_color(CATEGORY_COLOR[self._entry.get_type()]) + + # The required widths are the sum of the following: + # initial space (1 character) + # bracketing (3 characters) + # placement_label (14 characters) + # gap between etc and placement label (5 characters) + + baseline_space = 14 + 5 + + dst, etc = '', '' + + if listing_type == Listing.IP_ADDRESS: + # dst width is derived as: + # src (21) + dst (26) + divider (7) + right gap (2) - bracket (3) = 53 char + + dst = '%-53s' % self.get_destination_label(53, include_locale = True) + + # fills the nickname into the empty space here + + dst = '%s%-25s ' % (dst[:25], str_tools.crop(self.get_nickname('UNKNOWN'), 25, 0)) + + etc = self.get_etc_content(width - baseline_space - len(dst), listing_type) + elif listing_type == Listing.FINGERPRINT: + # dst width is derived as: + # src (9) + dst (40) + divider (7) + right gap (2) - bracket (3) = 55 char + + dst = '%-55s' % self.get_fingerprint('UNKNOWN') + etc = self.get_etc_content(width - baseline_space - len(dst), listing_type) + else: + # min space for the nickname is 56 characters + + etc = self.get_etc_content(width - baseline_space - 56, listing_type) + dst_layout = '%%-%is' % (width - baseline_space - len(etc)) + dst = dst_layout % self.get_nickname('UNKNOWN') + + return ((dst + etc, line_format), + (' ' * (width - baseline_space - len(dst) - len(etc) + 5), line_format), + ('%-14s' % self.placement_label, line_format)) + + class ConnectionPanel(panel.Panel, threading.Thread): """ Listing of connections tor is making, with information correlated against @@ -280,10 +858,8 @@ class ConnectionPanel(panel.Panel, threading.Thread): # mark the initially exitsing connection uptimes as being estimates - from nyx.connections import conn_entry - for entry in self._entries: - if isinstance(entry, conn_entry.ConnectionEntry): + if isinstance(entry, ConnectionEntry): entry.get_lines()[0].is_initial_connection = True # listens for when tor stops so we know to stop reflecting changes @@ -471,7 +1047,7 @@ class ConnectionPanel(panel.Panel, threading.Thread): color = CATEGORY_COLOR[selection.get_type()] fingerprint = selection.get_fingerprint() is_close_key = lambda key: key.is_selection() or key.match('d') or key.match('left') or key.match('right') - key = descriptor_popup.show_descriptor_popup(fingerprint, color, self.max_x, is_close_key) + key = nyx.popups.show_descriptor_popup(fingerprint, color, self.max_x, is_close_key) if not key or key.is_selection() or key.match('d'): break # closes popup diff --git a/nyx/connections/__init__.py b/nyx/connections/__init__.py deleted file mode 100644 index 577823a..0000000 --- a/nyx/connections/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -Resources related to our connection panel. -""" - -__all__ = [ - 'circ_entry', - 'conn_entry', - 'descriptor_popup', - 'entries', -] diff --git a/nyx/connections/circ_entry.py b/nyx/connections/circ_entry.py deleted file mode 100644 index 920d599..0000000 --- a/nyx/connections/circ_entry.py +++ /dev/null @@ -1,177 +0,0 @@ -""" -Connection panel entries for client circuits. This includes a header entry -followed by an entry for each hop in the circuit. For instance: - -89.188.20.246:42667 --> 217.172.182.26 (de) General / Built 8.6m (CIRCUIT) -| 85.8.28.4 (se) 98FBC3B2B93897A78CDD797EF549E6B62C9A8523 1 / Guard -| 91.121.204.76 (fr) 546387D93F8D40CFF8842BB9D3A8EC477CEDA984 2 / Middle -+- 217.172.182.26 (de) 5CFA9EA136C0EA0AC096E5CEA7EB674F1207CF86 3 / Exit -""" - -import curses -import datetime - -import nyx.util.tracker -import nyx.util.ui_tools -import nyx.connection_panel - -from nyx.connections import conn_entry - -from stem.util import str_tools - -try: - # added in python 3.2 - from functools import lru_cache -except ImportError: - from stem.util.lru_cache import lru_cache - - -def to_unix_time(dt): - return (dt - datetime.datetime(1970, 1, 1)).total_seconds() - - -class CircHeaderLine(conn_entry.ConnectionLine): - """ - Initial line of a client entry. This has the same basic format as connection - lines except that its etc field has circuit attributes. - """ - - def __init__(self, entry, circ): - if circ.status == 'BUILT': - self._remote_fingerprint = circ.path[-1][0] - exit_address, exit_port = nyx.util.tracker.get_consensus_tracker().get_relay_address(self._remote_fingerprint, ('192.168.0.1', 0)) - self.is_built = True - else: - exit_address, exit_port = '0.0.0.0', 0 - self.is_built = False - self._remote_fingerprint = None - - conn_entry.ConnectionLine.__init__(self, entry, nyx.util.tracker.Connection(to_unix_time(circ.created), False, '127.0.0.1', 0, exit_address, exit_port, 'tcp'), include_port = False) - self.circuit = circ - - def get_fingerprint(self, default = None): - return self._remote_fingerprint if self._remote_fingerprint else conn_entry.ConnectionLine.get_fingerprint(self, default) - - def get_destination_label(self, max_length, include_locale = False): - if not self.is_built: - return 'Building...' - - return conn_entry.ConnectionLine.get_destination_label(self, max_length, include_locale) - - def get_etc_content(self, width, listing_type): - """ - Attempts to provide all circuit related stats. Anything that can't be - shown completely (not enough room) is dropped. - """ - - etc_attr = ['Purpose: %s' % self.circuit.purpose.capitalize(), 'Circuit ID: %s' % self.circuit.id] - - for i in range(len(etc_attr), -1, -1): - etc_label = ', '.join(etc_attr[:i]) - - if len(etc_label) <= width: - return ('%%-%is' % width) % etc_label - - return '' - - @lru_cache() - def get_details(self, width): - if not self.is_built: - detail_format = (curses.A_BOLD, nyx.connection_panel.CATEGORY_COLOR[self._entry.get_type()]) - return [('Building Circuit...', detail_format)] - else: - return conn_entry.ConnectionLine.get_details(self, width) - - -class CircLine(conn_entry.ConnectionLine): - """ - An individual hop in a circuit. This overwrites the displayed listing, but - otherwise makes use of the ConnectionLine attributes (for the detail display, - caching, etc). - """ - - def __init__(self, entry, circ, fingerprint): - relay_ip, relay_port = nyx.util.tracker.get_consensus_tracker().get_relay_address(fingerprint, ('192.168.0.1', 0)) - conn_entry.ConnectionLine.__init__(self, entry, nyx.util.tracker.Connection(to_unix_time(circ.created), False, '127.0.0.1', 0, relay_ip, relay_port, 'tcp'), include_port = False) - self._fingerprint = fingerprint - self._is_last = False - - circ_path = [path_entry[0] for path_entry in circ.path] - circ_index = circ_path.index(fingerprint) - - if circ_index == len(circ_path) - 1: - placement_type = 'Exit' if circ.status == 'BUILT' else 'Extending' - self._is_last = True - elif circ_index == 0: - placement_type = 'Guard' - else: - placement_type = 'Middle' - - self.placement_label = '%i / %s' % (circ_index + 1, placement_type) - - def get_fingerprint(self, default = None): - self._fingerprint - - def get_listing_prefix(self): - if self._is_last: - return (ord(' '), curses.ACS_LLCORNER, curses.ACS_HLINE, ord(' ')) - else: - return (ord(' '), curses.ACS_VLINE, ord(' '), ord(' ')) - - def get_listing_entry(self, width, current_time, listing_type): - """ - Provides the [(msg, attr)...] listing for this relay in the circuilt - listing. Lines are composed of the following components: - <bracket> <dst> <etc> <placement label> - - The dst and etc entries largely match their ConnectionEntry counterparts. - - Arguments: - width - maximum length of the line - current_time - the current unix time (ignored) - listing_type - primary attribute we're listing connections by - """ - - return self._get_listing_entry(width, listing_type) - - @lru_cache() - def _get_listing_entry(self, width, listing_type): - line_format = nyx.util.ui_tools.get_color(nyx.connection_panel.CATEGORY_COLOR[self._entry.get_type()]) - - # The required widths are the sum of the following: - # initial space (1 character) - # bracketing (3 characters) - # placement_label (14 characters) - # gap between etc and placement label (5 characters) - - baseline_space = 14 + 5 - - dst, etc = '', '' - - if listing_type == nyx.connection_panel.Listing.IP_ADDRESS: - # dst width is derived as: - # src (21) + dst (26) + divider (7) + right gap (2) - bracket (3) = 53 char - - dst = '%-53s' % self.get_destination_label(53, include_locale = True) - - # fills the nickname into the empty space here - - dst = '%s%-25s ' % (dst[:25], str_tools.crop(self.get_nickname('UNKNOWN'), 25, 0)) - - etc = self.get_etc_content(width - baseline_space - len(dst), listing_type) - elif listing_type == nyx.connection_panel.Listing.FINGERPRINT: - # dst width is derived as: - # src (9) + dst (40) + divider (7) + right gap (2) - bracket (3) = 55 char - - dst = '%-55s' % self.get_fingerprint('UNKNOWN') - etc = self.get_etc_content(width - baseline_space - len(dst), listing_type) - else: - # min space for the nickname is 56 characters - - etc = self.get_etc_content(width - baseline_space - 56, listing_type) - dst_layout = '%%-%is' % (width - baseline_space - len(etc)) - dst = dst_layout % self.get_nickname('UNKNOWN') - - return ((dst + etc, line_format), - (' ' * (width - baseline_space - len(dst) - len(etc) + 5), line_format), - ('%-14s' % self.placement_label, line_format)) diff --git a/nyx/connections/conn_entry.py b/nyx/connections/conn_entry.py deleted file mode 100644 index df36dd8..0000000 --- a/nyx/connections/conn_entry.py +++ /dev/null @@ -1,453 +0,0 @@ -""" -Connection panel entries related to actual connections to or from the system -(ie, results seen by netstat, lsof, etc). -""" - -import curses - -import nyx.connection_panel -import nyx.util.tracker -import nyx.util.ui_tools - -from nyx.util import tor_controller -from nyx.connection_panel import Category - -from stem.util import conf, connection, str_tools - -try: - # added in python 3.2 - from functools import lru_cache -except ImportError: - from stem.util.lru_cache import lru_cache - -# static data for listing format -# <src> --> <dst> <etc><padding> - -LABEL_FORMAT = '%s --> %s %s%s' -LABEL_MIN_PADDING = 2 # min space between listing label and following data - -CONFIG = conf.config_dict('nyx', { - 'features.connection.showIps': True, -}) - - -class ConnectionLine(object): - """ - Display component of the ConnectionEntry. - """ - - def __init__(self, entry, conn, include_port = True): - self._entry = entry - self.connection = conn - - # includes the port or expanded ip address field when displaying listing - # information if true - - self.include_port = include_port - - def get_listing_prefix(self): - """ - Provides a list of characters to be appended before the listing entry. - """ - - return () - - def get_locale(self, default = None): - """ - Provides the two letter country code for the remote endpoint. - """ - - return tor_controller().get_info('ip-to-country/%s' % self.connection.remote_address, default) - - def get_fingerprint(self, default = None): - """ - Provides the fingerprint of this relay. - """ - - if self._entry.get_type() in (Category.OUTBOUND, Category.CIRCUIT, Category.DIRECTORY, Category.EXIT): - my_fingerprint = nyx.util.tracker.get_consensus_tracker().get_relay_fingerprint(self.connection.remote_address, self.connection.remote_port) - return my_fingerprint if my_fingerprint else default - else: - return default # inbound connections don't have an ORPort we can resolve - - def get_nickname(self, default = None): - """ - Provides the nickname of this relay. - """ - - nickname = nyx.util.tracker.get_consensus_tracker().get_relay_nickname(self.get_fingerprint()) - return nickname if nickname else default - - def get_listing_entry(self, width, current_time, listing_type): - """ - Provides the tuple list for this connection's listing. Lines are composed - of the following components: - <src> --> <dst> <etc> <uptime> (<type>) - - Listing.IP_ADDRESS: - src - <internal addr:port> --> <external addr:port> - dst - <destination addr:port> - etc - <fingerprint> <nickname> - - Listing.FINGERPRINT: - src - localhost - dst - <destination fingerprint> - etc - <nickname> <destination addr:port> - - Listing.NICKNAME: - src - <source nickname> - dst - <destination nickname> - etc - <fingerprint> <destination addr:port> - - Arguments: - width - maximum length of the line - current_time - unix timestamp for what the results should consider to be - the current time - listing_type - primary attribute we're listing connections by - """ - - # fetch our (most likely cached) display entry for the listing - - my_listing = self._get_listing_entry(width, listing_type) - - # fill in the current uptime and return the results - - time_prefix = '+' if self.connection.is_legacy else ' ' - - time_label = time_prefix + '%5s' % str_tools.time_label(current_time - self.connection.start_time, 1) - my_listing[2] = (time_label, my_listing[2][1]) - - return my_listing - - @lru_cache() - def _get_listing_entry(self, width, listing_type): - entry_type = self._entry.get_type() - - # Lines are split into the following components in reverse: - # init gap - " " - # content - "<src> --> <dst> <etc> " - # time - "<uptime>" - # preType - " (" - # category - "<type>" - # postType - ") " - - line_format = nyx.util.ui_tools.get_color(nyx.connection_panel.CATEGORY_COLOR[entry_type]) - - draw_entry = [(' ', line_format), - (self._get_listing_content(width - 19, listing_type), line_format), - (' ', line_format), - (' (', line_format), - (entry_type.upper(), line_format | curses.A_BOLD), - (')' + ' ' * (9 - len(entry_type)), line_format)] - - return draw_entry - - @lru_cache() - def get_details(self, width): - """ - Provides details on the connection, correlated against available consensus - data. - - Arguments: - width - available space to display in - """ - - detail_format = (curses.A_BOLD, nyx.connection_panel.CATEGORY_COLOR[self._entry.get_type()]) - return [(line, detail_format) for line in self._get_detail_content(width)] - - def get_etc_content(self, width, listing_type): - """ - Provides the optional content for the connection. - - Arguments: - width - maximum length of the line - listing_type - primary attribute we're listing connections by - """ - - # for applications show the command/pid - - if self._entry.get_type() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL): - port = self.connection.local_port if self._entry.get_type() == Category.HIDDEN else self.connection.remote_port - - try: - process = nyx.util.tracker.get_port_usage_tracker().fetch(port) - display_label = '%s (%s)' % (process.name, process.pid) if process.pid else process.name - except nyx.util.tracker.UnresolvedResult: - display_label = 'resolving...' - except nyx.util.tracker.UnknownApplication: - display_label = 'UNKNOWN' - - if len(display_label) < width: - return ('%%-%is' % width) % display_label - else: - return '' - - # for everything else display connection/consensus information - - destination_address = self.get_destination_label(26, include_locale = True) - etc, used_space = '', 0 - - if listing_type == nyx.connection_panel.Listing.IP_ADDRESS: - if width > used_space + 42: - # show fingerprint (column width: 42 characters) - - etc += '%-40s ' % self.get_fingerprint('UNKNOWN') - used_space += 42 - - if width > used_space + 10: - # show nickname (column width: remainder) - - nickname_space = width - used_space - nickname_label = str_tools.crop(self.get_nickname('UNKNOWN'), nickname_space, 0) - etc += ('%%-%is ' % nickname_space) % nickname_label - used_space += nickname_space + 2 - elif listing_type == nyx.connection_panel.Listing.FINGERPRINT: - if width > used_space + 17: - # show nickname (column width: min 17 characters, consumes any remaining space) - - nickname_space = width - used_space - 2 - - # if there's room then also show a column with the destination - # ip/port/locale (column width: 28 characters) - - is_locale_included = width > used_space + 45 - - if is_locale_included: - nickname_space -= 28 - - nickname_label = str_tools.crop(self.get_nickname('UNKNOWN'), nickname_space, 0) - etc += ('%%-%is ' % nickname_space) % nickname_label - used_space += nickname_space + 2 - - if is_locale_included: - etc += '%-26s ' % destination_address - used_space += 28 - else: - if width > used_space + 42: - # show fingerprint (column width: 42 characters) - etc += '%-40s ' % self.get_fingerprint('UNKNOWN') - used_space += 42 - - if width > used_space + 28: - # show destination ip/port/locale (column width: 28 characters) - etc += '%-26s ' % destination_address - used_space += 28 - - return ('%%-%is' % width) % etc - - def _get_listing_content(self, width, listing_type): - """ - Provides the source, destination, and extra info for our listing. - - Arguments: - width - maximum length of the line - listing_type - primary attribute we're listing connections by - """ - - controller = tor_controller() - my_type = self._entry.get_type() - destination_address = self.get_destination_label(26, include_locale = True) - - # The required widths are the sum of the following: - # - room for LABEL_FORMAT and LABEL_MIN_PADDING (11 characters) - # - base data for the listing - # - that extra field plus any previous - - used_space = len(LABEL_FORMAT % tuple([''] * 4)) + LABEL_MIN_PADDING - local_port = ':%s' % self.connection.local_port if self.include_port else '' - - src, dst, etc = '', '', '' - - if listing_type == nyx.connection_panel.Listing.IP_ADDRESS: - my_external_address = controller.get_info('address', self.connection.local_address) - - # Show our external address if it's going through tor. - - if my_type not in (Category.SOCKS, Category.HIDDEN, Category.CONTROL): - src_address = my_external_address + local_port - else: - src_address = self.connection.local_address + local_port - - if my_type in (Category.SOCKS, Category.CONTROL): - # Like inbound connections these need their source and destination to - # be swapped. However, this only applies when listing by IP (their - # fingerprint and nickname are both for us). Reversing the fields here - # to keep the same column alignments. - - src = '%-21s' % destination_address - dst = '%-26s' % src_address - else: - src = '%-21s' % src_address # ip:port = max of 21 characters - dst = '%-26s' % destination_address # ip:port (xx) = max of 26 characters - - used_space += len(src) + len(dst) # base data requires 47 characters - - etc = self.get_etc_content(width - used_space, listing_type) - used_space += len(etc) - elif listing_type == nyx.connection_panel.Listing.FINGERPRINT: - src = 'localhost' - dst = '%-40s' % ('localhost' if my_type == Category.CONTROL else self.get_fingerprint('UNKNOWN')) - - used_space += len(src) + len(dst) # base data requires 49 characters - - etc = self.get_etc_content(width - used_space, listing_type) - used_space += len(etc) - else: - # base data requires 50 min characters - src = controller.get_conf('nickname', 'UNKNOWN') - dst = controller.get_conf('nickname', 'UNKNOWN') if my_type == Category.CONTROL else self.get_nickname('UNKNOWN') - - min_base_space = 50 - - etc = self.get_etc_content(width - used_space - min_base_space, listing_type) - used_space += len(etc) - - base_space = width - used_space - used_space = width # prevents padding at the end - - if len(src) + len(dst) > base_space: - src = str_tools.crop(src, base_space / 3) - dst = str_tools.crop(dst, base_space - len(src)) - - # pads dst entry to its max space - - dst = ('%%-%is' % (base_space - len(src))) % dst - - if my_type == Category.INBOUND: - src, dst = dst, src - - padding = ' ' * (width - used_space + LABEL_MIN_PADDING) - - return LABEL_FORMAT % (src, dst, etc, padding) - - def _get_detail_content(self, width): - """ - Provides a list with detailed information for this connection. - - Arguments: - width - max length of lines - """ - - lines = [''] * 7 - lines[0] = 'address: %s' % self.get_destination_label(width - 11) - lines[1] = 'locale: %s' % ('??' if self._entry.is_private() else self.get_locale('??')) - - # Remaining data concerns the consensus results, with three possible cases: - # - if there's a single match then display its details - # - if there's multiple potential relays then list all of the combinations - # of ORPorts / Fingerprints - # - if no consensus data is available then say so (probably a client or - # exit connection) - - fingerprint = self.get_fingerprint() - controller = tor_controller() - - if fingerprint: - lines[1] = '%-13sfingerprint: %s' % (lines[1], fingerprint) # append fingerprint to second line - - router_status_entry = controller.get_network_status(fingerprint, None) - server_descriptor = controller.get_server_descriptor(fingerprint, None) - - if router_status_entry: - dir_port_label = 'dirport: %s' % router_status_entry.dir_port if router_status_entry.dir_port else '' - lines[2] = 'nickname: %-25s orport: %-10s %s' % (router_status_entry.nickname, router_status_entry.or_port, dir_port_label) - lines[3] = 'published: %s' % router_status_entry.published.strftime("%H:%M %m/%d/%Y") - lines[4] = 'flags: %s' % ', '.join(router_status_entry.flags) - - if server_descriptor: - policy_label = server_descriptor.exit_policy.summary() if server_descriptor.exit_policy else 'unknown' - lines[5] = 'exit policy: %s' % policy_label - lines[3] = '%-35s os: %-14s version: %s' % (lines[3], server_descriptor.operating_system, server_descriptor.tor_version) - - if server_descriptor.contact: - lines[6] = 'contact: %s' % server_descriptor.contact - else: - all_matches = nyx.util.tracker.get_consensus_tracker().get_all_relay_fingerprints(self.connection.remote_address) - - if all_matches: - # multiple matches - lines[2] = 'Multiple matches, possible fingerprints are:' - - for i in range(len(all_matches)): - is_last_line = i == 3 - - relay_port, relay_fingerprint = all_matches[i] - line_text = '%i. or port: %-5s fingerprint: %s' % (i + 1, relay_port, relay_fingerprint) - - # if there's multiple lines remaining at the end then give a count - - remaining_relays = len(all_matches) - i - - if is_last_line and remaining_relays > 1: - line_text = '... %i more' % remaining_relays - - lines[3 + i] = line_text - - if is_last_line: - break - else: - # no consensus entry for this ip address - lines[2] = 'No consensus data found' - - # crops any lines that are too long - - for i in range(len(lines)): - lines[i] = str_tools.crop(lines[i], width - 2) - - return lines - - def get_destination_label(self, max_length, include_locale = False): - """ - Provides a short description of the destination. This is made up of two - components, the base <ip addr>:<port> and an extra piece of information in - parentheses. The IP address is scrubbed from private connections. - - Extra information is... - - the port's purpose for exit connections - - the locale, the address isn't private and isn't on the local network - - nothing otherwise - - Arguments: - max_length - maximum length of the string returned - include_locale - possibly includes the locale - """ - - # destination of the connection - - address_label = '<scrubbed>' if self._entry.is_private() else self.connection.remote_address - port_label = ':%s' % self.connection.remote_port - destination_address = address_label + port_label - - # Only append the extra info if there's at least a couple characters of - # space (this is what's needed for the country codes). - - if len(destination_address) + 5 <= max_length: - space_available = max_length - len(destination_address) - 3 - - if self._entry.get_type() == Category.EXIT: - purpose = connection.port_usage(self.connection.remote_port) - - if purpose: - # BitTorrent is a common protocol to truncate, so just use "Torrent" - # if there's not enough room. - - if len(purpose) > space_available and purpose == 'BitTorrent': - purpose = 'Torrent' - - # crops with a hyphen if too long - - purpose = str_tools.crop(purpose, space_available, ending = str_tools.Ending.HYPHEN) - - destination_address += ' (%s)' % purpose - elif not connection.is_private_address(self.connection.remote_address): - extra_info = [] - - if include_locale and not tor_controller().is_geoip_unavailable(): - foreign_locale = self.get_locale('??') - extra_info.append(foreign_locale) - space_available -= len(foreign_locale) + 2 - - if extra_info: - destination_address += ' (%s)' % ', '.join(extra_info) - - return destination_address[:max_length] diff --git a/nyx/connections/descriptor_popup.py b/nyx/connections/descriptor_popup.py deleted file mode 100644 index 358784c..0000000 --- a/nyx/connections/descriptor_popup.py +++ /dev/null @@ -1,174 +0,0 @@ -""" -Popup providing the raw descriptor and consensus information for a relay. -""" - -import math -import curses - -import nyx.popups - -from nyx.util import tor_controller, ui_tools - -from stem.util import str_tools - -HEADERS = ['Consensus:', 'Microdescriptor:', 'Server Descriptor:'] -HEADER_COLOR = 'cyan' -LINE_NUMBER_COLOR = 'yellow' - -BLOCK_START, BLOCK_END = '-----BEGIN ', '-----END ' - -UNRESOLVED_MSG = 'No consensus data available' -ERROR_MSG = 'Unable to retrieve data' - - -def show_descriptor_popup(fingerprint, color, max_width, is_close_key): - """ - Provides a dialog showing the descriptors for a given relay. - - :param str fingerprint: fingerprint of the relay to be shown - :param str color: text color of the dialog - :param int max_width: maximum width of the dialog - :param function is_close_key: method to indicate if a key should close the - dialog or not - - :returns: :class:`~nyx.util.panel.KeyInput` for the keyboard input that - closed the dialog - """ - - if fingerprint: - title = 'Consensus Descriptor:' - lines = _display_text(fingerprint) - show_line_numbers = True - else: - title = 'Consensus Descriptor (%s):' % fingerprint - lines = [UNRESOLVED_MSG] - show_line_numbers = False - - popup_height, popup_width = _preferred_size(lines, max_width, show_line_numbers) - - with nyx.popups.popup_window(popup_height, popup_width) as (popup, _, height): - if not popup: - return None - - scroll, redraw = 0, True - - while True: - if redraw: - _draw(popup, title, lines, color, scroll, show_line_numbers) - redraw = False - - key = nyx.controller.get_controller().key_input() - - if key.is_scroll(): - new_scroll = ui_tools.get_scroll_position(key, scroll, height - 2, len(lines)) - - if scroll != new_scroll: - scroll, redraw = new_scroll, True - elif is_close_key(key): - return key - - -def _display_text(fingerprint): - """ - Provides the descriptors for a relay. - - :param str fingerprint: relay fingerprint to be looked up - - :returns: **list** with the lines that should be displayed in the dialog - """ - - controller = tor_controller() - router_status_entry = controller.get_network_status(fingerprint, None) - microdescriptor = controller.get_microdescriptor(fingerprint, None) - server_descriptor = controller.get_server_descriptor(fingerprint, None) - - description = 'Consensus:\n\n%s' % (router_status_entry if router_status_entry else ERROR_MSG) - - if server_descriptor: - description += '\n\nServer Descriptor:\n\n%s' % server_descriptor - - if microdescriptor: - description += '\n\nMicrodescriptor:\n\n%s' % microdescriptor - - return description.split('\n') - - -def _preferred_size(text, max_width, show_line_numbers): - """ - Provides the preferred dimensions of our dialog. - - :param list text: lines of text to be shown - :param int max_width: maximum width the dialog can be - :param bool show_line_numbers: if we should leave room for line numbers - - :returns: **tuple** of the preferred (height, width) - """ - - width, height = 0, len(text) + 2 - line_number_width = int(math.log10(len(text))) + 2 if show_line_numbers else 0 - max_content_width = max_width - line_number_width - 4 - - for line in text: - width = min(max_width, max(width, len(line) + line_number_width + 4)) - height += len(line) / max_content_width # extra lines due to text wrap - - return (height, width) - - -def _draw(popup, title, lines, entry_color, scroll, show_line_numbers): - def draw_msg(popup, min_x, x, y, width, msg, *attr): - while msg: - draw_msg, msg = str_tools.crop(msg, width - x, None, ending = None, get_remainder = True) - - if not draw_msg: - draw_msg, msg = str_tools.crop(msg, width - x), '' # first word is longer than the line - - x = popup.addstr(y, x, draw_msg, *attr) - - if msg: - x, y = min_x, y + 1 - - return x, y - - popup.win.erase() - - line_number_width = int(math.log10(len(lines))) + 1 - in_block = False # flag indicating if we're currently in crypto content - width = popup.max_x - 2 # leave space on the right for the border and an empty line - height = popup.max_y - 2 # height of the dialog without the top and bottom border - offset = line_number_width + 3 if show_line_numbers else 2 - - y = 1 - - for i, line in enumerate(lines): - keyword, value = line, '' - color = entry_color - - if line in HEADERS: - color = HEADER_COLOR - elif line.startswith(BLOCK_START): - in_block = True - elif line.startswith(BLOCK_END): - in_block = False - elif in_block: - keyword, value = '', line - elif ' ' in line and line != UNRESOLVED_MSG and line != ERROR_MSG: - keyword, value = line.split(' ', 1) - - if i < scroll: - continue - - if show_line_numbers: - popup.addstr(y, 2, str(i + 1).rjust(line_number_width), curses.A_BOLD, LINE_NUMBER_COLOR) - - x, y = draw_msg(popup, offset, offset, y, width, keyword, color, curses.A_BOLD) - x, y = draw_msg(popup, offset, x + 1, y, width, value, color) - - y += 1 - - if y > height: - break - - popup.win.box() - popup.addstr(0, 0, title, curses.A_STANDOUT) - popup.win.refresh() diff --git a/nyx/popups.py b/nyx/popups.py index 351467f..d2d5ded 100644 --- a/nyx/popups.py +++ b/nyx/popups.py @@ -2,16 +2,28 @@ Functions for displaying popups in the interface. """ +import math import curses import operator import nyx.controller from nyx import __version__, __release_date__ -from nyx.util import panel, ui_tools +from nyx.util import tor_controller, panel, ui_tools + +from stem.util import str_tools NO_STATS_MSG = "Usage stats aren't available yet, press any key..." +HEADERS = ['Consensus:', 'Microdescriptor:', 'Server Descriptor:'] +HEADER_COLOR = 'cyan' +LINE_NUMBER_COLOR = 'yellow' + +BLOCK_START, BLOCK_END = '-----BEGIN ', '-----END ' + +UNRESOLVED_MSG = 'No consensus data available' +ERROR_MSG = 'Unable to retrieve data' + def popup_window(height = -1, width = -1, top = 0, left = 0, below_static = True): """ @@ -418,3 +430,156 @@ def show_menu(title, options, old_selection): top_panel.set_title_visible(True) return selection + + +def show_descriptor_popup(fingerprint, color, max_width, is_close_key): + """ + Provides a dialog showing the descriptors for a given relay. + + :param str fingerprint: fingerprint of the relay to be shown + :param str color: text color of the dialog + :param int max_width: maximum width of the dialog + :param function is_close_key: method to indicate if a key should close the + dialog or not + + :returns: :class:`~nyx.util.panel.KeyInput` for the keyboard input that + closed the dialog + """ + + if fingerprint: + title = 'Consensus Descriptor:' + lines = _display_text(fingerprint) + show_line_numbers = True + else: + title = 'Consensus Descriptor (%s):' % fingerprint + lines = [UNRESOLVED_MSG] + show_line_numbers = False + + popup_height, popup_width = _preferred_size(lines, max_width, show_line_numbers) + + with popup_window(popup_height, popup_width) as (popup, _, height): + if not popup: + return None + + scroll, redraw = 0, True + + while True: + if redraw: + _draw(popup, title, lines, color, scroll, show_line_numbers) + redraw = False + + key = nyx.controller.get_controller().key_input() + + if key.is_scroll(): + new_scroll = ui_tools.get_scroll_position(key, scroll, height - 2, len(lines)) + + if scroll != new_scroll: + scroll, redraw = new_scroll, True + elif is_close_key(key): + return key + + +def _display_text(fingerprint): + """ + Provides the descriptors for a relay. + + :param str fingerprint: relay fingerprint to be looked up + + :returns: **list** with the lines that should be displayed in the dialog + """ + + controller = tor_controller() + router_status_entry = controller.get_network_status(fingerprint, None) + microdescriptor = controller.get_microdescriptor(fingerprint, None) + server_descriptor = controller.get_server_descriptor(fingerprint, None) + + description = 'Consensus:\n\n%s' % (router_status_entry if router_status_entry else ERROR_MSG) + + if server_descriptor: + description += '\n\nServer Descriptor:\n\n%s' % server_descriptor + + if microdescriptor: + description += '\n\nMicrodescriptor:\n\n%s' % microdescriptor + + return description.split('\n') + + +def _preferred_size(text, max_width, show_line_numbers): + """ + Provides the preferred dimensions of our dialog. + + :param list text: lines of text to be shown + :param int max_width: maximum width the dialog can be + :param bool show_line_numbers: if we should leave room for line numbers + + :returns: **tuple** of the preferred (height, width) + """ + + width, height = 0, len(text) + 2 + line_number_width = int(math.log10(len(text))) + 2 if show_line_numbers else 0 + max_content_width = max_width - line_number_width - 4 + + for line in text: + width = min(max_width, max(width, len(line) + line_number_width + 4)) + height += len(line) / max_content_width # extra lines due to text wrap + + return (height, width) + + +def _draw(popup, title, lines, entry_color, scroll, show_line_numbers): + def draw_msg(popup, min_x, x, y, width, msg, *attr): + while msg: + draw_msg, msg = str_tools.crop(msg, width - x, None, ending = None, get_remainder = True) + + if not draw_msg: + draw_msg, msg = str_tools.crop(msg, width - x), '' # first word is longer than the line + + x = popup.addstr(y, x, draw_msg, *attr) + + if msg: + x, y = min_x, y + 1 + + return x, y + + popup.win.erase() + + line_number_width = int(math.log10(len(lines))) + 1 + in_block = False # flag indicating if we're currently in crypto content + width = popup.max_x - 2 # leave space on the right for the border and an empty line + height = popup.max_y - 2 # height of the dialog without the top and bottom border + offset = line_number_width + 3 if show_line_numbers else 2 + + y = 1 + + for i, line in enumerate(lines): + keyword, value = line, '' + color = entry_color + + if line in HEADERS: + color = HEADER_COLOR + elif line.startswith(BLOCK_START): + in_block = True + elif line.startswith(BLOCK_END): + in_block = False + elif in_block: + keyword, value = '', line + elif ' ' in line and line != UNRESOLVED_MSG and line != ERROR_MSG: + keyword, value = line.split(' ', 1) + + if i < scroll: + continue + + if show_line_numbers: + popup.addstr(y, 2, str(i + 1).rjust(line_number_width), curses.A_BOLD, LINE_NUMBER_COLOR) + + x, y = draw_msg(popup, offset, offset, y, width, keyword, color, curses.A_BOLD) + x, y = draw_msg(popup, offset, x + 1, y, width, value, color) + + y += 1 + + if y > height: + break + + popup.win.box() + popup.addstr(0, 0, title, curses.A_STANDOUT) + popup.win.refresh() diff --git a/setup.py b/setup.py index f5399ef..f5b1c05 100644 --- a/setup.py +++ b/setup.py @@ -99,7 +99,7 @@ setup( author = nyx.__author__, author_email = nyx.__contact__, url = nyx.__url__, - packages = ['nyx', 'nyx.connections', 'nyx.menu', 'nyx.util'], + packages = ['nyx', 'nyx.menu', 'nyx.util'], keywords = 'tor onion controller', install_requires = ['stem>=1.4.1'], package_data = {'nyx': ['config/*', 'resources/*']}, _______________________________________________ tor-commits mailing list tor-commits@lists.torproject.org https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits