[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [nyx/master] Consolidate remaining nyx/connections/* module
commit 02327e0bed85ad2e87ce3c0ecb3bb62bcce117dd
Author: Damian Johnson <atagar@xxxxxxxxxxxxxx>
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@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits