tor-commits
Threads by month
- ----- 2025 -----
- July
- June
- May
- April
- March
- February
- January
- ----- 2024 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2023 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2022 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2021 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2020 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2019 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2018 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2017 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2016 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2015 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2014 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2013 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2012 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2011 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
March 2016
- 16 participants
- 1259 discussions

08 Mar '16
commit 7de1f92ffee74b1692edbcf217a1136d18ee04c5
Author: Nicolas Vigier <boklm(a)torproject.org>
Date: Tue Mar 8 19:34:42 2016 +0100
Add new Tor Browser versions: 5.5.3, 6.0a3
---
include/versions.wmi | 46 +++++++++++++++---------------
projects/torbrowser/RecommendedTBBVersions | 12 +++++++-
2 files changed, 34 insertions(+), 24 deletions(-)
diff --git a/include/versions.wmi b/include/versions.wmi
index bc0041e..88ddb98 100644
--- a/include/versions.wmi
+++ b/include/versions.wmi
@@ -4,29 +4,29 @@
<define-tag version-win32-stable whitespace=delete>0.2.7.6</define-tag>
<define-tag version-torbrowserdevelopbranch whitespace=delete>maint-5.5</define-tag>
-<define-tag version-torbrowserbundledir whitespace=delete>5.5.2</define-tag>
-<define-tag version-torbrowserbundlebetadir whitespace=delete>6.0a2</define-tag>
-
-<define-tag version-torbrowserbundle whitespace=delete>5.5.2</define-tag>
-<define-tag releasedate-torbrowserbundle whitespace=delete>2016-02-12</define-tag>
-<define-tag version-torbrowserbundlebeta whitespace=delete>6.0a2</define-tag>
-<define-tag releasedate-torbrowserbundlebeta whitespace=delete>2016-02-5</define-tag>
-
-<define-tag version-torbrowserbundlelinux32 whitespace=delete>5.5.2</define-tag>
-<define-tag releasedate-torbrowserbundlelinux32 whitespace=delete>2016-02-12</define-tag>
-<define-tag version-torbrowserbundlelinux64 whitespace=delete>5.5.2</define-tag>
-<define-tag releasedate-torbrowserbundlelinux64 whitespace=delete>2016-02-12</define-tag>
-<define-tag version-torbrowserbundlelinux32beta whitespace=delete>6.0a2</define-tag>
-<define-tag releasedate-torbrowserbundlelinux32beta whitespace=delete>2016-02-5</define-tag>
-<define-tag version-torbrowserbundlelinux64beta whitespace=delete>6.0a2</define-tag>
-<define-tag releasedate-torbrowserbundlelinux64beta whitespace=delete>2016-02-5</define-tag>
-
-<define-tag version-torbrowserbundleosx32 whitespace=delete>5.5.2</define-tag>
-<define-tag releasedate-torbrowserbundleosx32 whitespace=delete>2016-02-12</define-tag>
-<define-tag version-torbrowserbundleosx64 whitespace=delete>5.5.2</define-tag>
-<define-tag releasedate-torbrowserbundleosx64 whitespace=delete>2016-02-12</define-tag>
-<define-tag version-torbrowserbundleosx64beta whitespace=delete>6.0a2</define-tag>
-<define-tag releasedate-torbrowserbundleosx64beta whitespace=delete>2016-02-5</define-tag>
+<define-tag version-torbrowserbundledir whitespace=delete>5.5.3</define-tag>
+<define-tag version-torbrowserbundlebetadir whitespace=delete>6.0a3</define-tag>
+
+<define-tag version-torbrowserbundle whitespace=delete>5.5.3</define-tag>
+<define-tag releasedate-torbrowserbundle whitespace=delete>2016-03-08</define-tag>
+<define-tag version-torbrowserbundlebeta whitespace=delete>6.0a3</define-tag>
+<define-tag releasedate-torbrowserbundlebeta whitespace=delete>2016-03-08</define-tag>
+
+<define-tag version-torbrowserbundlelinux32 whitespace=delete>5.5.3</define-tag>
+<define-tag releasedate-torbrowserbundlelinux32 whitespace=delete>2016-03-08</define-tag>
+<define-tag version-torbrowserbundlelinux64 whitespace=delete>5.5.3</define-tag>
+<define-tag releasedate-torbrowserbundlelinux64 whitespace=delete>2016-03-08</define-tag>
+<define-tag version-torbrowserbundlelinux32beta whitespace=delete>6.0a3</define-tag>
+<define-tag releasedate-torbrowserbundlelinux32beta whitespace=delete>2016-03-08</define-tag>
+<define-tag version-torbrowserbundlelinux64beta whitespace=delete>6.0a3</define-tag>
+<define-tag releasedate-torbrowserbundlelinux64beta whitespace=delete>2016-03-08</define-tag>
+
+<define-tag version-torbrowserbundleosx32 whitespace=delete>5.5.3</define-tag>
+<define-tag releasedate-torbrowserbundleosx32 whitespace=delete>2016-03-08</define-tag>
+<define-tag version-torbrowserbundleosx64 whitespace=delete>5.5.3</define-tag>
+<define-tag releasedate-torbrowserbundleosx64 whitespace=delete>2016-03-08</define-tag>
+<define-tag version-torbrowserbundleosx64beta whitespace=delete>6.0a3</define-tag>
+<define-tag releasedate-torbrowserbundleosx64beta whitespace=delete>2016-03-08</define-tag>
<define-tag package-win32-stable whitespace=delete>../dist/torbrowser/<version-torbrowserbundledir>/tor-win32-<version-win32-stable>.zip</define-tag>
diff --git a/projects/torbrowser/RecommendedTBBVersions b/projects/torbrowser/RecommendedTBBVersions
index 17aaaff..aff56d1 100644
--- a/projects/torbrowser/RecommendedTBBVersions
+++ b/projects/torbrowser/RecommendedTBBVersions
@@ -3,10 +3,20 @@
"5.5.2-Linux",
"5.5.2-MacOS",
"5.5.2-Windows",
+"5.5.3",
+"5.5.3-Linux",
+"5.5.3-MacOS",
+"5.5.3-Windows",
"6.0a2",
"6.0a2-Linux",
"6.0a2-MacOS",
"6.0a2-Windows",
+"6.0a3",
+"6.0a3-Linux",
+"6.0a3-MacOS",
+"6.0a3-Windows",
"6.0a2-hardened",
-"6.0a2-hardened-Linux"
+"6.0a2-hardened-Linux",
+"6.0a3-hardened",
+"6.0a3-hardened-Linux"
]
1
0
commit fa3a3f0eb11eec7d5af69148f1fc1deff0f6d9bd
Author: Damian Johnson <atagar(a)torproject.org>
Date: Tue Mar 8 09:40:12 2016 -0800
Move panels into their own module
Now that we're done overhauling the panels lets move them into their own
module. They've all had the same naming convention ('foo_panel.py') so this
just makes the grouping more explicit.
---
nyx/__init__.py | 5 -
nyx/config_panel.py | 357 -----------------------
nyx/connection_panel.py | 706 ---------------------------------------------
nyx/controller.py | 25 +-
nyx/graph_panel.py | 753 ------------------------------------------------
nyx/header_panel.py | 496 -------------------------------
nyx/log_panel.py | 454 -----------------------------
nyx/menu/actions.py | 8 +-
nyx/panel/__init__.py | 11 +
nyx/panel/config.py | 357 +++++++++++++++++++++++
nyx/panel/connection.py | 706 +++++++++++++++++++++++++++++++++++++++++++++
nyx/panel/graph.py | 753 ++++++++++++++++++++++++++++++++++++++++++++++++
nyx/panel/header.py | 496 +++++++++++++++++++++++++++++++
nyx/panel/log.py | 454 +++++++++++++++++++++++++++++
nyx/panel/torrc.py | 171 +++++++++++
nyx/torrc_panel.py | 171 -----------
nyx/util/log.py | 2 +-
setup.py | 2 +-
18 files changed, 2967 insertions(+), 2960 deletions(-)
diff --git a/nyx/__init__.py b/nyx/__init__.py
index 4d58435..25da9dd 100644
--- a/nyx/__init__.py
+++ b/nyx/__init__.py
@@ -14,14 +14,9 @@ __license__ = 'GPLv3'
__all__ = [
'arguments',
- 'config_panel',
- 'connection_panel',
'controller',
- 'header_panel',
- 'log_panel',
'popups',
'starter',
- 'torrc_panel',
]
diff --git a/nyx/config_panel.py b/nyx/config_panel.py
deleted file mode 100644
index 2352fe7..0000000
--- a/nyx/config_panel.py
+++ /dev/null
@@ -1,357 +0,0 @@
-"""
-Panel presenting the configuration state for tor or nyx. Options can be edited
-and the resulting configuration files saved.
-"""
-
-import curses
-import os
-
-import nyx.controller
-import nyx.popups
-
-import stem.control
-import stem.manual
-
-from nyx.util import DATA_DIR, panel, tor_controller, ui_tools
-
-from stem.util import conf, enum, log, str_tools
-
-SortAttr = enum.Enum('NAME', 'VALUE', 'VALUE_TYPE', 'CATEGORY', 'USAGE', 'SUMMARY', 'DESCRIPTION', 'MAN_PAGE_ENTRY', 'IS_SET')
-
-DETAILS_HEIGHT = 8
-NAME_WIDTH = 25
-VALUE_WIDTH = 15
-
-
-def conf_handler(key, value):
- if key == 'features.config.order':
- return conf.parse_enum_csv(key, value[0], SortAttr, 3)
-
-
-CONFIG = conf.config_dict('nyx', {
- 'attr.config.category_color': {},
- 'attr.config.sort_color': {},
- 'features.config.order': [SortAttr.MAN_PAGE_ENTRY, SortAttr.NAME, SortAttr.IS_SET],
- 'features.config.state.showPrivateOptions': False,
- 'features.config.state.showVirtualOptions': False,
-}, conf_handler)
-
-
-class ConfigEntry(object):
- """
- Configuration option presented in the panel.
-
- :var str name: name of the configuration option
- :var str value_type: type of value
- :var stem.manual.ConfigOption manual: manual information about the option
- """
-
- def __init__(self, name, value_type, manual):
- self.name = name
- self.value_type = value_type
- self.manual = manual.config_options.get(name, stem.manual.ConfigOption(name))
- self._index = manual.config_options.keys().index(name) if name in manual.config_options else 99999
-
- def value(self):
- """
- Provides the value of this configuration option.
-
- :returns: **str** representation of the current config value
- """
-
- values = tor_controller().get_conf(self.name, [], True)
-
- if not values:
- return '<none>'
- elif self.value_type == 'Boolean' and values[0] in ('0', '1'):
- return 'False' if values[0] == '0' else 'True'
- elif self.value_type == 'DataSize' and values[0].isdigit():
- return str_tools.size_label(int(values[0]))
- elif self.value_type == 'TimeInterval' and values[0].isdigit():
- return str_tools.time_label(int(values[0]), is_long = True)
- else:
- return ', '.join(values)
-
- def is_set(self):
- """
- Checks if the configuration option has a custom value.
-
- :returns: **True** if the option has a custom value, **False** otherwise
- """
-
- return tor_controller().is_set(self.name, False)
-
- def sort_value(self, attr):
- """
- Provides a heuristic for sorting by a given value.
-
- :param SortAttr attr: sort attribute to provide a heuristic for
-
- :returns: comparable value for sorting
- """
-
- if attr == SortAttr.CATEGORY:
- return self.manual.category
- elif attr == SortAttr.NAME:
- return self.name
- elif attr == SortAttr.VALUE:
- return self.value()
- elif attr == SortAttr.VALUE_TYPE:
- return self.value_type
- elif attr == SortAttr.USAGE:
- return self.manual.usage
- elif attr == SortAttr.SUMMARY:
- return self.manual.summary
- elif attr == SortAttr.DESCRIPTION:
- return self.manual.description
- elif attr == SortAttr.MAN_PAGE_ENTRY:
- return self._index
- elif attr == SortAttr.IS_SET:
- return not self.is_set()
-
-
-class ConfigPanel(panel.Panel):
- """
- Editor for tor's configuration.
- """
-
- def __init__(self, stdscr):
- panel.Panel.__init__(self, stdscr, 'configuration', 0)
-
- self._contents = []
- self._scroller = ui_tools.Scroller(True)
- self._sort_order = CONFIG['features.config.order']
- self._show_all = False # show all options, or just the important ones
-
- cached_manual_path = os.path.join(DATA_DIR, 'manual')
-
- if os.path.exists(cached_manual_path):
- manual = stem.manual.Manual.from_cache(cached_manual_path)
- else:
- try:
- manual = stem.manual.Manual.from_man()
-
- try:
- manual.save(cached_manual_path)
- except IOError as exc:
- log.debug("Unable to cache manual information to '%s'. This is fine, but means starting Nyx takes a little longer than usual: " % (cached_manual_path, exc))
- except IOError as exc:
- log.debug("Unable to use 'man tor' to get information about config options (%s), using bundled information instead" % exc)
- manual = stem.manual.Manual.from_cache()
-
- try:
- for line in tor_controller().get_info('config/names').splitlines():
- # Lines of the form "<option> <type>[ <documentation>]". Documentation
- # was apparently only in old tor versions like 0.2.1.25.
-
- if ' ' not in line:
- continue
-
- line_comp = line.split()
- name, value_type = line_comp[0], line_comp[1]
-
- # skips private and virtual entries if not configured to show them
-
- if name.startswith('__') and not CONFIG['features.config.state.showPrivateOptions']:
- continue
- elif value_type == 'Virtual' and not CONFIG['features.config.state.showVirtualOptions']:
- continue
-
- self._contents.append(ConfigEntry(name, value_type, manual))
-
- self._contents = sorted(self._contents, key = lambda entry: [entry.sort_value(field) for field in self._sort_order])
- except stem.ControllerError as exc:
- log.warn('Unable to determine the configuration options tor supports: %s' % exc)
-
- def show_sort_dialog(self):
- """
- Provides the dialog for sorting our configuration options.
- """
-
- sort_colors = dict([(attr, CONFIG['attr.config.sort_color'].get(attr, 'white')) for attr in SortAttr])
- results = nyx.popups.show_sort_dialog('Config Option Ordering:', SortAttr, self._sort_order, sort_colors)
-
- if results:
- self._sort_order = results
- self._contents = sorted(self._contents, key = lambda entry: [entry.sort_value(field) for field in self._sort_order])
-
- def show_write_dialog(self):
- """
- Confirmation dialog for saving tor's configuration.
- """
-
- selection, controller = 1, tor_controller()
- config_text = controller.get_info('config-text', None)
- config_lines = config_text.splitlines() if config_text else []
-
- with nyx.popups.popup_window(len(config_lines) + 2) as (popup, width, height):
- if not popup or height <= 2:
- return
-
- while True:
- height, width = popup.get_preferred_size() # allow us to be resized
- popup.win.erase()
-
- for i, full_line in enumerate(config_lines):
- line = str_tools.crop(full_line, width - 2)
- option, arg = line.split(' ', 1) if ' ' in line else (line, '')
-
- popup.addstr(i + 1, 1, option, curses.A_BOLD, 'green')
- popup.addstr(i + 1, len(option) + 2, arg, curses.A_BOLD, 'cyan')
-
- x = width - 16
-
- for i, option in enumerate(['Save', 'Cancel']):
- x = popup.addstr(height - 2, x, '[')
- x = popup.addstr(height - 2, x, option, curses.A_BOLD, curses.A_STANDOUT if i == selection else curses.A_NORMAL)
- x = popup.addstr(height - 2, x, '] ')
-
- popup.win.box()
- popup.addstr(0, 0, 'Torrc to save:', curses.A_STANDOUT)
- popup.win.refresh()
-
- key = nyx.controller.get_controller().key_input()
-
- if key.match('left'):
- selection = max(0, selection - 1)
- elif key.match('right'):
- selection = min(1, selection + 1)
- elif key.is_selection():
- if selection == 0:
- try:
- controller.save_conf()
- nyx.popups.show_msg('Saved configuration to %s' % controller.get_info('config-file', '<unknown>'), 2)
- except IOError as exc:
- nyx.popups.show_msg('Unable to save configuration (%s)' % exc.strerror, 2)
-
- break
- elif key.match('esc'):
- break # esc - cancel
-
- def handle_key(self, key):
- if key.is_scroll():
- page_height = self.get_preferred_size()[0] - DETAILS_HEIGHT
- is_changed = self._scroller.handle_key(key, self._get_config_options(), page_height)
-
- if is_changed:
- self.redraw(True)
- elif key.is_selection():
- selection = self._scroller.get_cursor_selection(self._get_config_options())
- initial_value = selection.value() if selection.is_set() else ''
- new_value = nyx.popups.input_prompt('%s Value (esc to cancel): ' % selection.name, initial_value)
-
- if new_value != initial_value:
- try:
- if selection.value_type == 'Boolean':
- # if the value's a boolean then allow for 'true' and 'false' inputs
-
- if new_value.lower() == 'true':
- new_value = '1'
- elif new_value.lower() == 'false':
- new_value = '0'
- elif selection.value_type == 'LineList':
- new_value = new_value.split(',') # set_conf accepts list inputs
-
- tor_controller().set_conf(selection.name, new_value)
- self.redraw(True)
- except Exception as exc:
- nyx.popups.show_msg('%s (press any key)' % exc)
- elif key.match('a'):
- self._show_all = not self._show_all
- self.redraw(True)
- elif key.match('s'):
- self.show_sort_dialog()
- elif key.match('w'):
- self.show_write_dialog()
- else:
- return False
-
- return True
-
- def get_help(self):
- return [
- ('up arrow', 'scroll up a line', None),
- ('down arrow', 'scroll down a line', None),
- ('page up', 'scroll up a page', None),
- ('page down', 'scroll down a page', None),
- ('enter', 'edit configuration option', None),
- ('w', 'write torrc', None),
- ('a', 'toggle filtering', None),
- ('s', 'sort ordering', None),
- ]
-
- def draw(self, width, height):
- contents = self._get_config_options()
- selection = self._scroller.get_cursor_selection(contents)
- scroll_location = self._scroller.get_scroll_location(contents, height - DETAILS_HEIGHT)
- is_scrollbar_visible = len(contents) > height - DETAILS_HEIGHT
-
- if selection is not None:
- self._draw_selection_details(selection, width)
-
- if self.is_title_visible():
- hidden_msg = "press 'a' to hide most options" if self._show_all else "press 'a' to show all options"
- self.addstr(0, 0, 'Tor Configuration (%s):' % hidden_msg, curses.A_STANDOUT)
-
- scroll_offset = 1
-
- if is_scrollbar_visible:
- scroll_offset = 3
- self.add_scroll_bar(scroll_location, scroll_location + height - DETAILS_HEIGHT, len(contents), DETAILS_HEIGHT)
-
- if selection is not None:
- self.addch(DETAILS_HEIGHT - 1, 1, curses.ACS_TTEE)
-
- # Description column can grow up to eighty characters. After that any extra
- # space goes to the value.
-
- description_width = max(0, width - scroll_offset - NAME_WIDTH - VALUE_WIDTH - 2)
-
- if description_width > 80:
- value_width = VALUE_WIDTH + (description_width - 80)
- description_width = 80
- else:
- value_width = VALUE_WIDTH
-
- for i, entry in enumerate(contents[scroll_location:]):
- attr = nyx.util.ui_tools.get_color(CONFIG['attr.config.category_color'].get(entry.manual.category, 'white'))
- attr |= curses.A_BOLD if entry.is_set() else curses.A_NORMAL
- attr |= curses.A_STANDOUT if entry == selection else curses.A_NORMAL
-
- option_label = str_tools.crop(entry.name, NAME_WIDTH).ljust(NAME_WIDTH + 1)
- value_label = str_tools.crop(entry.value(), value_width).ljust(value_width + 1)
- summary_label = str_tools.crop(entry.manual.summary, description_width).ljust(description_width)
-
- self.addstr(DETAILS_HEIGHT + i, scroll_offset, option_label + value_label + summary_label, attr)
-
- if DETAILS_HEIGHT + i >= height:
- break
-
- def _get_config_options(self):
- return self._contents if self._show_all else filter(lambda entry: stem.manual.is_important(entry.name) or entry.is_set(), self._contents)
-
- def _draw_selection_details(self, selection, width):
- """
- Shows details of the currently selected option.
- """
-
- description = 'Description: %s' % (selection.manual.description)
- attr = ', '.join(('custom' if selection.is_set() else 'default', selection.value_type, 'usage: %s' % selection.manual.usage))
- selected_color = CONFIG['attr.config.category_color'].get(selection.manual.category, 'white')
- ui_tools.draw_box(self, 0, 0, width, DETAILS_HEIGHT)
-
- self.addstr(1, 2, '%s (%s Option)' % (selection.name, selection.manual.category), curses.A_BOLD, selected_color)
- self.addstr(2, 2, 'Value: %s (%s)' % (selection.value(), str_tools.crop(attr, width - len(selection.value()) - 13)), curses.A_BOLD, selected_color)
-
- for i in range(DETAILS_HEIGHT - 4):
- if not description:
- break # done writing description
-
- line, description = description.split('\n', 1) if '\n' in description else (description, '')
-
- if i < DETAILS_HEIGHT - 5:
- line, remainder = str_tools.crop(line, width - 3, 4, 4, str_tools.Ending.HYPHEN, True)
- description = ' ' + remainder.strip() + description
- self.addstr(3 + i, 2, line, curses.A_BOLD, selected_color)
- else:
- self.addstr(3 + i, 2, str_tools.crop(line, width - 3, 4, 4), curses.A_BOLD, selected_color)
diff --git a/nyx/connection_panel.py b/nyx/connection_panel.py
deleted file mode 100644
index e28f531..0000000
--- a/nyx/connection_panel.py
+++ /dev/null
@@ -1,706 +0,0 @@
-"""
-Listing of the currently established connections tor has made.
-"""
-
-import re
-import time
-import collections
-import curses
-import itertools
-import threading
-
-import nyx.popups
-import nyx.util.tracker
-import nyx.util.ui_tools
-
-from nyx.util import panel, tor_controller, ui_tools
-
-from stem.control import Listener
-from stem.util import datetime_to_unix, conf, connection, enum, str_tools
-
-try:
- # added in python 3.2
- from functools import lru_cache
-except ImportError:
- from stem.util.lru_cache import lru_cache
-
-# height of the detail panel content, not counting top and bottom border
-
-DETAILS_HEIGHT = 7
-
-EXIT_USAGE_WIDTH = 15
-UPDATE_RATE = 5 # rate in seconds at which we refresh
-
-# cached information from our last _update() call
-
-LAST_RETRIEVED_HS_CONF = None
-LAST_RETRIEVED_CIRCUITS = None
-
-# Connection Categories:
-# Inbound Relay connection, coming to us.
-# Outbound Relay connection, leaving us.
-# Exit Outbound relay connection leaving the Tor network.
-# Hidden Connections to a hidden service we're providing.
-# Socks Socks connections for applications using Tor.
-# Circuit Circuits our tor client has created.
-# Directory Fetching tor consensus information.
-# Control Tor controller (nyx, vidalia, etc).
-
-Category = enum.Enum('INBOUND', 'OUTBOUND', 'EXIT', 'HIDDEN', 'SOCKS', 'CIRCUIT', 'DIRECTORY', 'CONTROL')
-SortAttr = enum.Enum('CATEGORY', 'UPTIME', 'IP_ADDRESS', 'PORT', 'FINGERPRINT', 'NICKNAME', 'COUNTRY')
-LineType = enum.Enum('CONNECTION', 'CIRCUIT_HEADER', 'CIRCUIT')
-
-Line = collections.namedtuple('Line', [
- 'entry',
- 'line_type',
- 'connection',
- 'circuit',
- 'fingerprint',
- 'nickname',
- 'locale',
-])
-
-
-def conf_handler(key, value):
- if key == 'features.connection.order':
- return conf.parse_enum_csv(key, value[0], SortAttr, 3)
-
-
-CONFIG = conf.config_dict('nyx', {
- 'attr.connection.category_color': {},
- 'attr.connection.sort_color': {},
- 'features.connection.resolveApps': True,
- 'features.connection.order': [SortAttr.CATEGORY, SortAttr.IP_ADDRESS, SortAttr.UPTIME],
- 'features.connection.showIps': True,
-}, conf_handler)
-
-
-class Entry(object):
- @staticmethod
- @lru_cache()
- def from_connection(connection):
- return ConnectionEntry(connection)
-
- @staticmethod
- @lru_cache()
- def from_circuit(circuit):
- return CircuitEntry(circuit)
-
- def get_lines(self):
- """
- Provides individual lines of connection information.
-
- :returns: **list** of **ConnectionLine** concerning this entry
- """
-
- raise NotImplementedError('should be implemented by subclasses')
-
- def get_type(self):
- """
- Provides our best guess at the type of connection this is.
-
- :returns: **Category** for the connection's type
- """
-
- raise NotImplementedError('should be implemented by subclasses')
-
- def is_private(self):
- """
- Checks if information about this endpoint should be scrubbed. Relaying
- etiquette (and wiretapping laws) say these are bad things to look at so
- DON'T CHANGE THIS UNLESS YOU HAVE A DAMN GOOD REASON!
-
- :returns: **bool** indicating if connection information is sensive or not
- """
-
- raise NotImplementedError('should be implemented by subclasses')
-
- def sort_value(self, attr):
- """
- Provides a heuristic for sorting by a given value.
-
- :param SortAttr attr: sort attribute to provide a heuristic for
-
- :returns: comparable value for sorting
- """
-
- line = self.get_lines()[0]
- at_end = 'z' * 20
-
- if attr == SortAttr.IP_ADDRESS:
- if self.is_private():
- return 255 ** 4 # orders at the end
-
- ip_value = 0
-
- for octet in line.connection.remote_address.split('.'):
- ip_value = ip_value * 255 + int(octet)
-
- return ip_value * 65536 + line.connection.remote_port
- elif attr == SortAttr.PORT:
- return line.connection.remote_port
- elif attr == SortAttr.FINGERPRINT:
- return line.fingerprint if line.fingerprint else at_end
- elif attr == SortAttr.NICKNAME:
- return line.nickname if line.nickname else at_end
- elif attr == SortAttr.CATEGORY:
- return Category.index_of(self.get_type())
- elif attr == SortAttr.UPTIME:
- return line.connection.start_time
- elif attr == SortAttr.COUNTRY:
- return line.locale if (line.locale and not self.is_private()) else at_end
- else:
- return ''
-
-
-class ConnectionEntry(Entry):
- def __init__(self, connection):
- self._connection = connection
-
- @lru_cache()
- def get_lines(self):
- fingerprint, nickname, locale = None, None, None
-
- if self.get_type() in (Category.OUTBOUND, Category.CIRCUIT, Category.DIRECTORY, Category.EXIT):
- fingerprint = nyx.util.tracker.get_consensus_tracker().get_relay_fingerprints(self._connection.remote_address).get(self._connection.remote_port)
-
- if fingerprint:
- nickname = nyx.util.tracker.get_consensus_tracker().get_relay_nickname(fingerprint)
- locale = tor_controller().get_info('ip-to-country/%s' % self._connection.remote_address, None)
-
- return [Line(self, LineType.CONNECTION, self._connection, None, fingerprint, nickname, locale)]
-
- @lru_cache()
- def get_type(self):
- controller = tor_controller()
-
- if self._connection.local_port in controller.get_ports(Listener.OR, []):
- return Category.INBOUND
- elif self._connection.local_port in controller.get_ports(Listener.DIR, []):
- return Category.INBOUND
- elif self._connection.local_port in controller.get_ports(Listener.SOCKS, []):
- return Category.SOCKS
- elif self._connection.local_port in controller.get_ports(Listener.CONTROL, []):
- return Category.CONTROL
-
- if LAST_RETRIEVED_HS_CONF:
- for hs_config in LAST_RETRIEVED_HS_CONF.values():
- if self._connection.remote_port == hs_config['HiddenServicePort']:
- return Category.HIDDEN
-
- fingerprint = nyx.util.tracker.get_consensus_tracker().get_relay_fingerprints(self._connection.remote_address).get(self._connection.remote_port)
-
- if fingerprint and LAST_RETRIEVED_CIRCUITS:
- for circ in LAST_RETRIEVED_CIRCUITS:
- if circ.path and len(circ.path) == 1 and circ.path[0][0] == fingerprint and circ.status == 'BUILT':
- return Category.DIRECTORY # one-hop circuit to retrieve directory information
- else:
- # not a known relay, might be an exit connection
-
- exit_policy = controller.get_exit_policy(None)
-
- if exit_policy and exit_policy.can_exit_to(self._connection.remote_address, self._connection.remote_port):
- return Category.EXIT
-
- return Category.OUTBOUND
-
- @lru_cache()
- def is_private(self):
- if not CONFIG['features.connection.showIps']:
- return True
-
- if self.get_type() == Category.INBOUND:
- controller = tor_controller()
-
- if controller.is_user_traffic_allowed().inbound:
- return len(nyx.util.tracker.get_consensus_tracker().get_relay_fingerprints(self._connection.remote_address)) == 0
- elif self.get_type() == Category.EXIT:
- # DNS connections exiting us aren't private (since they're hitting our
- # resolvers). Everything else is.
-
- return not (self._connection.remote_port == 53 and self._connection.protocol == 'udp')
-
- return False # for everything else this isn't a concern
-
-
-class CircuitEntry(Entry):
- def __init__(self, circuit):
- self._circuit = circuit
-
- @lru_cache()
- def get_lines(self):
- def line(fingerprint, line_type):
- address, port, nickname, locale = '0.0.0.0', 0, None, None
- consensus_tracker = nyx.util.tracker.get_consensus_tracker()
-
- if fingerprint is not None:
- address, port = consensus_tracker.get_relay_address(fingerprint, ('192.168.0.1', 0))
- nickname = consensus_tracker.get_relay_nickname(fingerprint)
- locale = tor_controller().get_info('ip-to-country/%s' % address, None)
-
- connection = nyx.util.tracker.Connection(datetime_to_unix(self._circuit.created), False, '127.0.0.1', 0, address, port, 'tcp', False)
- return Line(self, line_type, connection, self._circuit, fingerprint, nickname, locale)
-
- header_line = line(self._circuit.path[-1][0] if self._circuit.status == 'BUILT' else None, LineType.CIRCUIT_HEADER)
- return [header_line] + [line(fp, LineType.CIRCUIT) for fp, _ in self._circuit.path]
-
- def get_type(self):
- return Category.CIRCUIT
-
- def is_private(self):
- return False
-
-
-class ConnectionPanel(panel.Panel, threading.Thread):
- """
- Listing of connections tor is making, with information correlated against
- the current consensus and other data sources.
- """
-
- def __init__(self, stdscr):
- panel.Panel.__init__(self, stdscr, 'connections', 0)
- threading.Thread.__init__(self)
- self.setDaemon(True)
-
- self._scroller = ui_tools.Scroller(True)
- self._entries = [] # last fetched display entries
- self._show_details = False # presents the details panel if true
- self._sort_order = CONFIG['features.connection.order']
-
- self._last_resource_fetch = -1 # timestamp of the last ConnectionResolver results used
-
- self._pause_condition = threading.Condition()
- self._halt = False # terminates thread if true
-
- # Tracks exiting port and client country statistics
-
- self._client_locale_usage = {}
- self._exit_port_usage = {}
- self._counted_connections = set()
-
- # If we're a bridge and been running over a day then prepopulates with the
- # last day's clients.
-
- bridge_clients = tor_controller().get_info('status/clients-seen', None)
-
- if bridge_clients:
- # Response has a couple arguments...
- # TimeStarted="2011-08-17 15:50:49" CountrySummary=us=16,de=8,uk=8
-
- country_summary = None
-
- for line in bridge_clients.split():
- if line.startswith('CountrySummary='):
- country_summary = line[15:]
- break
-
- if country_summary:
- for entry in country_summary.split(','):
- if re.match('^..=[0-9]+$', entry):
- locale, count = entry.split('=', 1)
- self._client_locale_usage[locale] = int(count)
-
- def show_sort_dialog(self):
- """
- Provides a dialog for sorting our connections.
- """
-
- sort_colors = dict([(attr, CONFIG['attr.connection.sort_color'].get(attr, 'white')) for attr in SortAttr])
- results = nyx.popups.show_sort_dialog('Connection Ordering:', SortAttr, self._sort_order, sort_colors)
-
- if results:
- self._sort_order = results
- self._entries = sorted(self._entries, key = lambda entry: [entry.sort_value(attr) for attr in self._sort_order])
-
- def handle_key(self, key):
- user_traffic_allowed = tor_controller().is_user_traffic_allowed()
-
- if key.is_scroll():
- page_height = self.get_preferred_size()[0] - 1
-
- if self._show_details:
- page_height -= (DETAILS_HEIGHT + 1)
-
- lines = list(itertools.chain.from_iterable([entry.get_lines() for entry in self._entries]))
- is_changed = self._scroller.handle_key(key, lines, page_height)
-
- if is_changed:
- self.redraw(True)
- elif key.is_selection():
- self._show_details = not self._show_details
- self.redraw(True)
- elif key.match('s'):
- self.show_sort_dialog()
- elif key.match('r'):
- connection_tracker = nyx.util.tracker.get_connection_tracker()
- options = ['auto'] + list(connection.Resolver) + list(nyx.util.tracker.CustomResolver)
-
- resolver = connection_tracker.get_custom_resolver()
- selected_index = 0 if resolver is None else options.index(resolver)
- selection = nyx.popups.show_menu('Connection Resolver:', options, selected_index)
-
- if selection != -1:
- connection_tracker.set_custom_resolver(None if selection == 0 else options[selection])
- elif key.match('d'):
- self.set_title_visible(False)
- self.redraw(True)
- entries = self._entries
-
- while True:
- lines = list(itertools.chain.from_iterable([entry.get_lines() for entry in entries]))
- selection = self._scroller.get_cursor_selection(lines)
-
- if not selection:
- break
-
- def is_close_key(key):
- return key.is_selection() or key.match('d') or key.match('left') or key.match('right')
-
- color = CONFIG['attr.connection.category_color'].get(selection.entry.get_type(), 'white')
- key = nyx.popups.show_descriptor_popup(selection.fingerprint, color, self.max_x, is_close_key)
-
- if not key or key.is_selection() or key.match('d'):
- break # closes popup
- elif key.match('left'):
- self.handle_key(panel.KeyInput(curses.KEY_UP))
- elif key.match('right'):
- self.handle_key(panel.KeyInput(curses.KEY_DOWN))
-
- self.set_title_visible(True)
- self.redraw(True)
- elif key.match('c') and user_traffic_allowed.inbound:
- nyx.popups.show_count_dialog('Client Locales', self._client_locale_usage)
- elif key.match('e') and user_traffic_allowed.outbound:
- counts = {}
- key_width = max(map(len, self._exit_port_usage.keys()))
-
- for k, v in self._exit_port_usage.items():
- usage = connection.port_usage(k)
-
- if usage:
- k = k.ljust(key_width + 3) + usage.ljust(EXIT_USAGE_WIDTH)
-
- counts[k] = v
-
- nyx.popups.show_count_dialog('Exiting Port Usage', counts)
- else:
- return False
-
- return True
-
- def run(self):
- """
- Keeps connections listing updated, checking for new entries at a set rate.
- """
-
- last_ran = -1
-
- while not self._halt:
- if self.is_paused() or not tor_controller().is_alive() or (time.time() - last_ran) < UPDATE_RATE:
- with self._pause_condition:
- if not self._halt:
- self._pause_condition.wait(0.2)
-
- continue # done waiting, try again
-
- self._update()
- self.redraw(True)
-
- # If this is our first run then fill in our fingerprint tracker. This
- # requires fetching all the router status entries which takes a few
- # seconds, so best done when we're finished with the rest of the first
- # iteration to hide the lag.
-
- if last_ran == -1:
- nyx.util.tracker.get_consensus_tracker().update(tor_controller().get_network_statuses([]))
-
- last_ran = time.time()
-
- def get_help(self):
- resolver = nyx.util.tracker.get_connection_tracker().get_custom_resolver()
- user_traffic_allowed = tor_controller().is_user_traffic_allowed()
-
- options = [
- ('up arrow', 'scroll up a line', None),
- ('down arrow', 'scroll down a line', None),
- ('page up', 'scroll up a page', None),
- ('page down', 'scroll down a page', None),
- ('enter', 'show connection details', None),
- ('d', 'raw consensus descriptor', None),
- ('s', 'sort ordering', None),
- ('r', 'connection resolver', 'auto' if resolver is None else resolver),
- ]
-
- if user_traffic_allowed.inbound:
- options.append(('c', 'client locale usage summary', None))
-
- if user_traffic_allowed.outbound:
- options.append(('e', 'exit port usage summary', None))
-
- return options
-
- def draw(self, width, height):
- controller = tor_controller()
- entries = self._entries
-
- lines = list(itertools.chain.from_iterable([entry.get_lines() for entry in entries]))
- selected = self._scroller.get_cursor_selection(lines)
-
- if self.is_paused():
- current_time = self.get_pause_time()
- elif not controller.is_alive():
- current_time = controller.connection_time()
- else:
- current_time = time.time()
-
- is_showing_details = self._show_details and selected
- details_offset = DETAILS_HEIGHT + 1 if is_showing_details else 0
-
- is_scrollbar_visible = len(lines) > height - details_offset - 1
- scroll_offset = 2 if is_scrollbar_visible else 0
- scroll_location = self._scroller.get_scroll_location(lines, height - details_offset - 1)
-
- if self.is_title_visible():
- self._draw_title(entries, self._show_details)
-
- if is_showing_details:
- self._draw_details(selected, width, is_scrollbar_visible)
-
- if is_scrollbar_visible:
- self.add_scroll_bar(scroll_location, scroll_location + height - details_offset - 1, len(lines), 1 + details_offset)
-
- for line_number in range(scroll_location, len(lines)):
- y = line_number + details_offset + 1 - scroll_location
- self._draw_line(scroll_offset, y, lines[line_number], lines[line_number] == selected, width - scroll_offset, current_time)
-
- if y >= height:
- break
-
- def _draw_title(self, entries, showing_details):
- """
- Panel title with the number of connections we presently have.
- """
-
- if showing_details:
- self.addstr(0, 0, 'Connection Details:', curses.A_STANDOUT)
- elif not entries:
- self.addstr(0, 0, 'Connections:', curses.A_STANDOUT)
- else:
- counts = collections.Counter([entry.get_type() for entry in entries])
- count_labels = ['%i %s' % (counts[category], category.lower()) for category in Category if counts[category]]
- self.addstr(0, 0, 'Connections (%s):' % ', '.join(count_labels), curses.A_STANDOUT)
-
- def _draw_details(self, selected, width, is_scrollbar_visible):
- """
- Shows detailed information about the selected connection.
- """
-
- attr = (CONFIG['attr.connection.category_color'].get(selected.entry.get_type(), 'white'), curses.A_BOLD)
-
- if selected.line_type == LineType.CIRCUIT_HEADER and selected.circuit.status != 'BUILT':
- self.addstr(1, 2, 'Building Circuit...', *attr)
- else:
- address = '<scrubbed>' if selected.entry.is_private() else selected.connection.remote_address
- self.addstr(1, 2, 'address: %s:%s' % (address, selected.connection.remote_port), *attr)
- self.addstr(2, 2, 'locale: %s' % ('??' if selected.entry.is_private() else (selected.locale if selected.locale else '??')), *attr)
-
- matches = nyx.util.tracker.get_consensus_tracker().get_relay_fingerprints(selected.connection.remote_address)
-
- if not matches:
- self.addstr(3, 2, 'No consensus data found', *attr)
- elif len(matches) == 1 or selected.connection.remote_port in matches:
- controller = tor_controller()
- fingerprint = matches.values()[0] if len(matches) == 1 else matches[selected.connection.remote_port]
- router_status_entry = controller.get_network_status(fingerprint, None)
-
- self.addstr(2, 15, 'fingerprint: %s' % fingerprint, *attr)
-
- if router_status_entry:
- dir_port_label = 'dirport: %s' % router_status_entry.dir_port if router_status_entry.dir_port else ''
- self.addstr(3, 2, 'nickname: %-25s orport: %-10s %s' % (router_status_entry.nickname, router_status_entry.or_port, dir_port_label), *attr)
- self.addstr(4, 2, 'published: %s' % router_status_entry.published.strftime("%H:%M %m/%d/%Y"), *attr)
- self.addstr(5, 2, 'flags: %s' % ', '.join(router_status_entry.flags), *attr)
-
- server_descriptor = controller.get_server_descriptor(fingerprint, None)
-
- if server_descriptor:
- policy_label = server_descriptor.exit_policy.summary() if server_descriptor.exit_policy else 'unknown'
- self.addstr(6, 2, 'exit policy: %s' % policy_label, *attr)
- self.addstr(4, 38, 'os: %-14s version: %s' % (server_descriptor.operating_system, server_descriptor.tor_version), *attr)
-
- if server_descriptor.contact:
- self.addstr(7, 2, 'contact: %s' % server_descriptor.contact, *attr)
- else:
- self.addstr(3, 2, 'Multiple matches, possible fingerprints are:', *attr)
-
- for i, port in enumerate(sorted(matches.keys())):
- is_last_line, remaining_relays = i == 3, len(matches) - i
-
- if not is_last_line or remaining_relays == 1:
- self.addstr(4 + i, 2, '%i. or port: %-5s fingerprint: %s' % (i + 1, port, matches[port]), *attr)
- else:
- self.addstr(4 + i, 2, '... %i more' % remaining_relays, *attr)
-
- if is_last_line:
- break
-
- # draw the border, with a 'T' pipe if connecting with the scrollbar
-
- ui_tools.draw_box(self, 0, 0, width, DETAILS_HEIGHT + 2)
-
- if is_scrollbar_visible:
- self.addch(DETAILS_HEIGHT + 1, 1, curses.ACS_TTEE)
-
- def _draw_line(self, x, y, line, is_selected, width, current_time):
- attr = nyx.util.ui_tools.get_color(CONFIG['attr.connection.category_color'].get(line.entry.get_type(), 'white'))
- attr |= curses.A_STANDOUT if is_selected else curses.A_NORMAL
-
- self.addstr(y, x, ' ' * (width - x), attr)
-
- if line.line_type == LineType.CIRCUIT:
- if line.circuit.path[-1][0] == line.fingerprint:
- prefix = (ord(' '), curses.ACS_LLCORNER, curses.ACS_HLINE, ord(' '))
- else:
- prefix = (ord(' '), curses.ACS_VLINE, ord(' '), ord(' '))
-
- for char in prefix:
- x = self.addch(y, x, char)
- else:
- x += 1 # offset from edge
-
- self._draw_address_column(x, y, line, attr)
- self._draw_line_details(57, y, line, width - 57 - 20, attr)
- self._draw_right_column(width - 18, y, line, current_time, attr)
-
- def _draw_address_column(self, x, y, line, attr):
- src = tor_controller().get_info('address', line.connection.local_address)
- src += ':%s' % line.connection.local_port if line.line_type == LineType.CONNECTION else ''
-
- if line.line_type == LineType.CIRCUIT_HEADER and line.circuit.status != 'BUILT':
- dst = 'Building...'
- else:
- dst = '<scrubbed>' if line.entry.is_private() else line.connection.remote_address
- dst += ':%s' % line.connection.remote_port
-
- if line.entry.get_type() == Category.EXIT:
- purpose = connection.port_usage(line.connection.remote_port)
-
- if purpose:
- dst += ' (%s)' % str_tools.crop(purpose, 26 - len(dst) - 3)
- elif not tor_controller().is_geoip_unavailable() and not line.entry.is_private():
- dst += ' (%s)' % (line.locale if line.locale else '??')
-
- if line.entry.get_type() in (Category.INBOUND, Category.SOCKS, Category.CONTROL):
- dst, src = src, dst
-
- if line.line_type == LineType.CIRCUIT:
- self.addstr(y, x, dst, attr)
- else:
- self.addstr(y, x, '%-21s --> %-26s' % (src, dst), attr)
-
- def _draw_line_details(self, x, y, line, width, attr):
- if line.line_type == LineType.CIRCUIT_HEADER:
- comp = ['Purpose: %s' % line.circuit.purpose.capitalize(), ', Circuit ID: %s' % line.circuit.id]
- elif line.entry.get_type() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL):
- try:
- port = line.connection.local_port if line.entry.get_type() == Category.HIDDEN else line.connection.remote_port
- process = nyx.util.tracker.get_port_usage_tracker().fetch(port)
- comp = ['%s (%s)' % (process.name, process.pid) if process.pid else process.name]
- except nyx.util.tracker.UnresolvedResult:
- comp = ['resolving...']
- except nyx.util.tracker.UnknownApplication:
- comp = ['UNKNOWN']
- else:
- comp = ['%-40s' % (line.fingerprint if line.fingerprint else 'UNKNOWN'), ' ' + (line.nickname if line.nickname else 'UNKNOWN')]
-
- for entry in comp:
- if width >= x + len(entry):
- x = self.addstr(y, x, entry, attr)
- else:
- return
-
- def _draw_right_column(self, x, y, line, current_time, attr):
- if line.line_type == LineType.CIRCUIT:
- circ_path = [fp for fp, _ in line.circuit.path]
- circ_index = circ_path.index(line.fingerprint)
-
- if circ_index == len(circ_path) - 1:
- placement_type = 'Exit' if line.circuit.status == 'BUILT' else 'Extending'
- elif circ_index == 0:
- placement_type = 'Guard'
- else:
- placement_type = 'Middle'
-
- self.addstr(y, x + 4, '%i / %s' % (circ_index + 1, placement_type), attr)
- else:
- x = self.addstr(y, x, '+' if line.connection.is_legacy else ' ', attr)
- x = self.addstr(y, x, '%5s' % str_tools.time_label(current_time - line.connection.start_time, 1), attr)
- x = self.addstr(y, x, ' (', attr)
- x = self.addstr(y, x, line.entry.get_type().upper(), attr | curses.A_BOLD)
- x = self.addstr(y, x, ')', attr)
-
- def stop(self):
- """
- Halts further resolutions and terminates the thread.
- """
-
- with self._pause_condition:
- self._halt = True
- self._pause_condition.notifyAll()
-
- def _update(self):
- """
- Fetches the newest resolved connections.
- """
-
- global LAST_RETRIEVED_CIRCUITS, LAST_RETRIEVED_HS_CONF
-
- controller = tor_controller()
- LAST_RETRIEVED_CIRCUITS = controller.get_circuits([])
- LAST_RETRIEVED_HS_CONF = controller.get_hidden_service_conf({})
-
- conn_resolver = nyx.util.tracker.get_connection_tracker()
- current_resolution_count = conn_resolver.run_counter()
-
- if not conn_resolver.is_alive():
- return # if we're not fetching connections then this is a no-op
- elif current_resolution_count == self._last_resource_fetch:
- return # no new connections to process
-
- new_entries = [Entry.from_connection(conn) for conn in conn_resolver.get_value()]
-
- for circ in LAST_RETRIEVED_CIRCUITS:
- # Skips established single-hop circuits (these are for directory
- # fetches, not client circuits)
-
- if not (circ.status == 'BUILT' and len(circ.path) == 1):
- new_entries.append(Entry.from_circuit(circ))
-
- # update stats for client and exit connections
-
- for entry in new_entries:
- line = entry.get_lines()[0]
-
- if entry.is_private() and line.connection not in self._counted_connections:
- if entry.get_type() == Category.INBOUND and line.locale:
- self._client_locale_usage[line.locale] = self._client_locale_usage.get(line.locale, 0) + 1
- elif entry.get_type() == Category.EXIT:
- self._exit_port_usage[line.connection.remote_port] = self._exit_port_usage.get(line.connection.remote_port, 0) + 1
-
- self._counted_connections.add(line.connection)
-
- self._entries = sorted(new_entries, key = lambda entry: [entry.sort_value(attr) for attr in self._sort_order])
- self._last_resource_fetch = current_resolution_count
-
- if CONFIG['features.connection.resolveApps']:
- local_ports, remote_ports = [], []
-
- for entry in new_entries:
- line = entry.get_lines()[0]
-
- if entry.get_type() in (Category.SOCKS, Category.CONTROL):
- local_ports.append(line.connection.remote_port)
- elif entry.get_type() == Category.HIDDEN:
- remote_ports.append(line.connection.local_port)
-
- nyx.util.tracker.get_port_usage_tracker().query(local_ports, remote_ports)
diff --git a/nyx/controller.py b/nyx/controller.py
index e143b49..318df4b 100644
--- a/nyx/controller.py
+++ b/nyx/controller.py
@@ -9,14 +9,15 @@ import threading
import nyx.menu.menu
import nyx.popups
-import nyx.header_panel
-import nyx.log_panel
-import nyx.config_panel
-import nyx.torrc_panel
-import nyx.graph_panel
-import nyx.connection_panel
import nyx.util.tracker
+import nyx.panel.config
+import nyx.panel.connection
+import nyx.panel.graph
+import nyx.panel.header
+import nyx.panel.log
+import nyx.panel.torrc
+
import stem
from stem.util import conf, log
@@ -102,29 +103,29 @@ class Controller:
self._screen = stdscr
self._sticky_panels = [
- nyx.header_panel.HeaderPanel(stdscr),
+ nyx.panel.header.HeaderPanel(stdscr),
LabelPanel(stdscr),
]
self._page_panels, first_page_panels = [], []
if CONFIG['features.panels.show.graph']:
- first_page_panels.append(nyx.graph_panel.GraphPanel(stdscr))
+ first_page_panels.append(nyx.panel.graph.GraphPanel(stdscr))
if CONFIG['features.panels.show.log']:
- first_page_panels.append(nyx.log_panel.LogPanel(stdscr))
+ first_page_panels.append(nyx.panel.log.LogPanel(stdscr))
if first_page_panels:
self._page_panels.append(first_page_panels)
if CONFIG['features.panels.show.connection']:
- self._page_panels.append([nyx.connection_panel.ConnectionPanel(stdscr)])
+ self._page_panels.append([nyx.panel.connection.ConnectionPanel(stdscr)])
if CONFIG['features.panels.show.config']:
- self._page_panels.append([nyx.config_panel.ConfigPanel(stdscr)])
+ self._page_panels.append([nyx.panel.config.ConfigPanel(stdscr)])
if CONFIG['features.panels.show.torrc']:
- self._page_panels.append([nyx.torrc_panel.TorrcPanel(stdscr)])
+ self._page_panels.append([nyx.panel.torrc.TorrcPanel(stdscr)])
self.quit_signal = False
self._page = 0
diff --git a/nyx/graph_panel.py b/nyx/graph_panel.py
deleted file mode 100644
index 086d04d..0000000
--- a/nyx/graph_panel.py
+++ /dev/null
@@ -1,753 +0,0 @@
-"""
-Graphs of tor related statistics. For example...
-
-Downloaded (0.0 B/sec): Uploaded (0.0 B/sec):
- 34 30
- * *
- ** * * * **
- * * * ** ** ** *** ** ** ** **
- ********* ****** ****** ********* ****** ******
- 0 ************ **************** 0 ************ ****************
- 25s 50 1m 1.6 2.0 25s 50 1m 1.6 2.0
-"""
-
-import collections
-import copy
-import curses
-import time
-
-import nyx.controller
-import nyx.popups
-import nyx.util.tracker
-
-from stem.control import EventType, Listener
-from stem.util import conf, enum, log, str_tools, system
-from nyx.util import join, msg, panel, tor_controller
-
-GraphStat = enum.Enum(('BANDWIDTH', 'bandwidth'), ('CONNECTIONS', 'connections'), ('SYSTEM_RESOURCES', 'resources'))
-Interval = enum.Enum(('EACH_SECOND', 'each second'), ('FIVE_SECONDS', '5 seconds'), ('THIRTY_SECONDS', '30 seconds'), ('MINUTELY', 'minutely'), ('FIFTEEN_MINUTE', '15 minute'), ('THIRTY_MINUTE', '30 minute'), ('HOURLY', 'hourly'), ('DAILY', 'daily'))
-Bounds = enum.Enum(('GLOBAL_MAX', 'global_max'), ('LOCAL_MAX', 'local_max'), ('TIGHT', 'tight'))
-
-DrawAttributes = collections.namedtuple('DrawAttributes', ('stat', 'subgraph_height', 'subgraph_width', 'interval', 'bounds_type', 'accounting'))
-
-INTERVAL_SECONDS = {
- Interval.EACH_SECOND: 1,
- Interval.FIVE_SECONDS: 5,
- Interval.THIRTY_SECONDS: 30,
- Interval.MINUTELY: 60,
- Interval.FIFTEEN_MINUTE: 900,
- Interval.THIRTY_MINUTE: 1800,
- Interval.HOURLY: 3600,
- Interval.DAILY: 86400,
-}
-
-PRIMARY_COLOR, SECONDARY_COLOR = 'green', 'cyan'
-
-ACCOUNTING_RATE = 5
-DEFAULT_CONTENT_HEIGHT = 4 # space needed for labeling above and below the graph
-WIDE_LABELING_GRAPH_COL = 50 # minimum graph columns to use wide spacing for x-axis labels
-COLLAPSE_WIDTH = 135 # width at which to move optional stats from the title to x-axis label
-
-
-def conf_handler(key, value):
- if key == 'features.graph.height':
- return max(1, value)
- elif key == 'features.graph.max_width':
- return max(1, value)
- elif key == 'features.graph.type':
- if value != 'none' and value not in GraphStat:
- log.warn("'%s' isn't a valid graph type, options are: none, %s" % (CONFIG['features.graph.type'], ', '.join(GraphStat)))
- return CONFIG['features.graph.type'] # keep the default
- elif key == 'features.graph.interval':
- if value not in Interval:
- log.warn("'%s' isn't a valid graphing interval, options are: %s" % (value, ', '.join(Interval)))
- return CONFIG['features.graph.interval'] # keep the default
- elif key == 'features.graph.bound':
- if value not in Bounds:
- log.warn("'%s' isn't a valid graph bounds, options are: %s" % (value, ', '.join(Bounds)))
- return CONFIG['features.graph.bound'] # keep the default
-
-
-CONFIG = conf.config_dict('nyx', {
- 'attr.hibernate_color': {},
- 'attr.graph.title': {},
- 'attr.graph.header.primary': {},
- 'attr.graph.header.secondary': {},
- 'features.graph.height': 7,
- 'features.graph.type': GraphStat.BANDWIDTH,
- 'features.graph.interval': Interval.EACH_SECOND,
- 'features.graph.bound': Bounds.LOCAL_MAX,
- 'features.graph.max_width': 300, # we need some sort of max size so we know how much graph data to retain
- 'features.panels.show.connection': True,
- 'features.graph.bw.transferInBytes': False,
- 'features.graph.bw.accounting.show': True,
-}, conf_handler)
-
-
-class GraphData(object):
- """
- Graphable statistical information.
-
- :var int latest_value: last value we recorded
- :var int total: sum of all values we've recorded
- :var int tick: number of events we've processed
- :var dict values: mapping of intervals to an array of samplings from newest to oldest
- :var dict max_value: mapping of intervals to the maximum value it has had
- """
-
- def __init__(self, clone = None, category = None, is_primary = True):
- if clone:
- self.latest_value = clone.latest_value
- self.total = clone.total
- self.tick = clone.tick
- self.values = copy.deepcopy(clone.values)
- self.max_value = dict(clone.max_value)
-
- self._category = clone._category
- self._is_primary = clone._is_primary
- self._in_process_value = dict(clone._in_process_value)
- else:
- self.latest_value = 0
- self.total = 0
- self.tick = 0
- self.values = dict([(i, CONFIG['features.graph.max_width'] * [0]) for i in Interval])
- self.max_value = dict([(i, 0) for i in Interval])
-
- self._category = category
- self._is_primary = is_primary
- self._in_process_value = dict([(i, 0) for i in Interval])
-
- def average(self):
- return self.total / max(1, self.tick)
-
- def update(self, new_value):
- self.latest_value = new_value
- self.total += new_value
- self.tick += 1
-
- for interval in Interval:
- interval_seconds = INTERVAL_SECONDS[interval]
- self._in_process_value[interval] += new_value
-
- if self.tick % interval_seconds == 0:
- new_entry = self._in_process_value[interval] / interval_seconds
- self.values[interval] = [new_entry] + self.values[interval][:-1]
- self.max_value[interval] = max(self.max_value[interval], new_entry)
- self._in_process_value[interval] = 0
-
- def header(self, width):
- """
- Provides the description above a subgraph.
-
- :param int width: maximum length of the header
-
- :returns: **str** with our graph header
- """
-
- return self._category._header(width, self._is_primary)
-
- def y_axis_label(self, value):
- """
- Provides the label we should display on our y-axis.
-
- :param int value: value being shown on the y-axis
-
- :returns: **str** with our y-axis label
- """
-
- return self._category._y_axis_label(value, self._is_primary)
-
-
-class GraphCategory(object):
- """
- Category for the graph. This maintains two subgraphs, updating them each
- second with updated stats.
-
- :var GraphData primary: first subgraph
- :var GraphData secondary: second subgraph
- :var float start_time: unix timestamp for when we started
- """
-
- def __init__(self, clone = None):
- if clone:
- self.primary = GraphData(clone.primary)
- self.secondary = GraphData(clone.secondary)
- self.start_time = clone.start_time
- self._title_stats = list(clone._title_stats)
- self._primary_header_stats = list(clone._primary_header_stats)
- self._secondary_header_stats = list(clone._secondary_header_stats)
- else:
- self.primary = GraphData(category = self, is_primary = True)
- self.secondary = GraphData(category = self, is_primary = False)
- self.start_time = time.time()
- self._title_stats = []
- self._primary_header_stats = []
- self._secondary_header_stats = []
-
- def stat_type(self):
- """
- Provides the GraphStat this graph is for.
-
- :returns: **GraphStat** of this graph
- """
-
- raise NotImplementedError('Should be implemented by subclasses')
-
- def title(self, width):
- """
- Provides a graph title that fits in the given width.
-
- :param int width: maximum length of the title
-
- :returns: **str** with our title
- """
-
- title = CONFIG['attr.graph.title'].get(self.stat_type(), '')
- title_stats = join(self._title_stats, ', ', width - len(title) - 4)
- return '%s (%s):' % (title, title_stats) if title_stats else title + ':'
-
- def bandwidth_event(self, event):
- """
- Called when it's time to process another event. All graphs use tor BW
- events to keep in sync with each other (this happens once per second).
- """
-
- pass
-
- def _header(self, is_primary, width):
- if is_primary:
- header = CONFIG['attr.graph.header.primary'].get(self.stat_type(), '')
- header_stats = self._primary_header_stats
- else:
- header = CONFIG['attr.graph.header.secondary'].get(self.stat_type(), '')
- header_stats = self._secondary_header_stats
-
- header_stats = join(header_stats, '', width - len(header) - 4)
- return '%s (%s):' % (header, header_stats) if header_stats else '%s:' % header
-
- def _y_axis_label(self, value, is_primary):
- return str(value)
-
-
-class BandwidthStats(GraphCategory):
- """
- Tracks tor's bandwidth usage.
- """
-
- def __init__(self, clone = None):
- GraphCategory.__init__(self, clone)
-
- if not clone:
- # fill in past bandwidth information
-
- controller = tor_controller()
- bw_entries, is_successful = controller.get_info('bw-event-cache', None), True
-
- if bw_entries:
- for entry in bw_entries.split():
- entry_comp = entry.split(',')
-
- if len(entry_comp) != 2 or not entry_comp[0].isdigit() or not entry_comp[1].isdigit():
- log.warn(msg('panel.graphing.bw_event_cache_malformed', response = bw_entries))
- is_successful = False
- break
-
- self.primary.update(int(entry_comp[0]))
- self.secondary.update(int(entry_comp[1]))
-
- if is_successful:
- log.info(msg('panel.graphing.prepopulation_successful', duration = str_tools.time_label(len(bw_entries.split()), is_long = True)))
-
- read_total = controller.get_info('traffic/read', None)
- write_total = controller.get_info('traffic/written', None)
- start_time = system.start_time(controller.get_pid(None))
-
- if read_total and write_total and start_time:
- self.primary.total = int(read_total)
- self.secondary.total = int(write_total)
- self.start_time = start_time
-
- def stat_type(self):
- return GraphStat.BANDWIDTH
-
- def _y_axis_label(self, value, is_primary):
- return _size_label(value, 0)
-
- def bandwidth_event(self, event):
- self.primary.update(event.read)
- self.secondary.update(event.written)
-
- self._primary_header_stats = [
- '%-14s' % ('%s/sec' % _size_label(self.primary.latest_value)),
- '- avg: %s/sec' % _size_label(self.primary.total / (time.time() - self.start_time)),
- ', total: %s' % _size_label(self.primary.total),
- ]
-
- self._secondary_header_stats = [
- '%-14s' % ('%s/sec' % _size_label(self.secondary.latest_value)),
- '- avg: %s/sec' % _size_label(self.secondary.total / (time.time() - self.start_time)),
- ', total: %s' % _size_label(self.secondary.total),
- ]
-
- controller = tor_controller()
-
- stats = []
- bw_rate = controller.get_effective_rate(None)
- bw_burst = controller.get_effective_rate(None, burst = True)
-
- if bw_rate and bw_burst:
- bw_rate_label = _size_label(bw_rate)
- bw_burst_label = _size_label(bw_burst)
-
- # if both are using rounded values then strip off the '.0' decimal
-
- if '.0' in bw_rate_label and '.0' in bw_burst_label:
- bw_rate_label = bw_rate_label.split('.', 1)[0]
- bw_burst_label = bw_burst_label.split('.', 1)[0]
-
- stats.append('limit: %s/s' % bw_rate_label)
- stats.append('burst: %s/s' % bw_burst_label)
-
- my_router_status_entry = controller.get_network_status(default = None)
- measured_bw = getattr(my_router_status_entry, 'bandwidth', None)
-
- if measured_bw:
- stats.append('measured: %s/s' % _size_label(measured_bw))
- else:
- my_server_descriptor = controller.get_server_descriptor(default = None)
- observed_bw = getattr(my_server_descriptor, 'observed_bandwidth', None)
-
- if observed_bw:
- stats.append('observed: %s/s' % _size_label(observed_bw))
-
- self._title_stats = stats
-
-
-class ConnectionStats(GraphCategory):
- """
- Tracks number of inbound and outbound connections.
- """
-
- def stat_type(self):
- return GraphStat.CONNECTIONS
-
- def bandwidth_event(self, event):
- inbound_count, outbound_count = 0, 0
-
- controller = tor_controller()
- or_ports = controller.get_ports(Listener.OR, [])
- dir_ports = controller.get_ports(Listener.DIR, [])
- control_ports = controller.get_ports(Listener.CONTROL, [])
-
- for entry in nyx.util.tracker.get_connection_tracker().get_value():
- if entry.local_port in or_ports or entry.local_port in dir_ports:
- inbound_count += 1
- elif entry.local_port in control_ports:
- pass # control connection
- else:
- outbound_count += 1
-
- self.primary.update(inbound_count)
- self.secondary.update(outbound_count)
-
- self._primary_header_stats = [str(self.primary.latest_value), ', avg: %s' % self.primary.average()]
- self._secondary_header_stats = [str(self.secondary.latest_value), ', avg: %s' % self.secondary.average()]
-
-
-class ResourceStats(GraphCategory):
- """
- Tracks cpu and memory usage of the tor process.
- """
-
- def stat_type(self):
- return GraphStat.SYSTEM_RESOURCES
-
- def _y_axis_label(self, value, is_primary):
- return '%i%%' % value if is_primary else str_tools.size_label(value)
-
- def bandwidth_event(self, event):
- resources = nyx.util.tracker.get_resource_tracker().get_value()
- self.primary.update(resources.cpu_sample * 100) # decimal percentage to whole numbers
- self.secondary.update(resources.memory_bytes)
-
- self._primary_header_stats = ['%0.1f%%' % self.primary.latest_value, ', avg: %0.1f%%' % self.primary.average()]
- self._secondary_header_stats = [str_tools.size_label(self.secondary.latest_value, 1), ', avg: %s' % str_tools.size_label(self.secondary.average(), 1)]
-
-
-class GraphPanel(panel.Panel):
- """
- Panel displaying graphical information of GraphCategory instances.
- """
-
- def __init__(self, stdscr):
- panel.Panel.__init__(self, stdscr, 'graph', 0)
-
- self._displayed_stat = None if CONFIG['features.graph.type'] == 'none' else CONFIG['features.graph.type']
- self._update_interval = CONFIG['features.graph.interval']
- self._bounds = CONFIG['features.graph.bound']
- self._graph_height = CONFIG['features.graph.height']
-
- self._accounting_stats = None
-
- self._stats = {
- GraphStat.BANDWIDTH: BandwidthStats(),
- GraphStat.SYSTEM_RESOURCES: ResourceStats(),
- }
-
- if CONFIG['features.panels.show.connection']:
- self._stats[GraphStat.CONNECTIONS] = ConnectionStats()
- elif self._displayed_stat == GraphStat.CONNECTIONS:
- log.warn("The connection graph is unavailble when you set 'features.panels.show.connection false'.")
- self._displayed_stat = GraphStat.BANDWIDTH
-
- self.set_pause_attr('_stats')
- self.set_pause_attr('_accounting_stats')
-
- controller = tor_controller()
- controller.add_event_listener(self._update_accounting, EventType.BW)
- controller.add_event_listener(self._update_stats, EventType.BW)
- controller.add_status_listener(lambda *args: self.redraw(True))
-
- @property
- def displayed_stat(self):
- return self._displayed_stat
-
- @displayed_stat.setter
- def displayed_stat(self, value):
- if value is not None and value not in self._stats.keys():
- raise ValueError("%s isn't a graphed statistic" % value)
-
- self._displayed_stat = value
-
- def stat_options(self):
- return self._stats.keys()
-
- @property
- def update_interval(self):
- return self._update_interval
-
- @update_interval.setter
- def update_interval(self, value):
- if value not in Interval:
- raise ValueError("%s isn't a valid graphing update interval" % value)
-
- self._update_interval = value
-
- @property
- def bounds_type(self):
- return self._bounds
-
- @bounds_type.setter
- def bounds_type(self, value):
- if value not in Bounds:
- raise ValueError("%s isn't a valid type of bounds" % value)
-
- self._bounds = value
-
- def get_height(self):
- """
- Provides the height of the content.
- """
-
- if not self.displayed_stat:
- return 0
-
- height = DEFAULT_CONTENT_HEIGHT + self._graph_height
-
- if self.displayed_stat == GraphStat.BANDWIDTH and self._accounting_stats:
- height += 3
-
- return height
-
- def set_graph_height(self, new_graph_height):
- self._graph_height = max(1, new_graph_height)
-
- def resize_graph(self):
- """
- Prompts for user input to resize the graph panel. Options include...
-
- * down arrow - grow graph
- * up arrow - shrink graph
- * enter / space - set size
- """
-
- control = nyx.controller.get_controller()
-
- with panel.CURSES_LOCK:
- try:
- while True:
- msg = 'press the down/up to resize the graph, and enter when done'
- control.set_msg(msg, curses.A_BOLD, True)
- curses.cbreak()
- key = control.key_input()
-
- if key.match('down'):
- # don't grow the graph if it's already consuming the whole display
- # (plus an extra line for the graph/log gap)
-
- max_height = self.parent.getmaxyx()[0] - self.top
- current_height = self.get_height()
-
- if current_height < max_height + 1:
- self.set_graph_height(self._graph_height + 1)
- elif key.match('up'):
- self.set_graph_height(self._graph_height - 1)
- elif key.is_selection():
- break
-
- control.redraw()
- finally:
- control.set_msg()
-
- def handle_key(self, key):
- if key.match('r'):
- self.resize_graph()
- elif key.match('b'):
- # uses the next boundary type
- self.bounds_type = Bounds.next(self.bounds_type)
- self.redraw(True)
- elif key.match('s'):
- # provides a menu to pick the graphed stats
-
- available_stats = sorted(self.stat_options())
- options = ['None'] + [stat.capitalize() for stat in available_stats]
- initial_selection = available_stats.index(self.displayed_stat) + 1 if self.displayed_stat else 0
-
- selection = nyx.popups.show_menu('Graphed Stats:', options, initial_selection)
-
- # applies new setting
-
- if selection == 0:
- self.displayed_stat = None
- elif selection != -1:
- self.displayed_stat = available_stats[selection - 1]
- elif key.match('i'):
- # provides menu to pick graph panel update interval
-
- selection = nyx.popups.show_menu('Update Interval:', list(Interval), list(Interval).index(self.update_interval))
-
- if selection != -1:
- self.update_interval = list(Interval)[selection]
- else:
- return False
-
- return True
-
- def get_help(self):
- return [
- ('r', 'resize graph', None),
- ('s', 'graphed stats', self.displayed_stat if self.displayed_stat else 'none'),
- ('b', 'graph bounds', self.bounds_type.replace('_', ' ')),
- ('i', 'graph update interval', self.update_interval),
- ]
-
- def draw(self, width, height):
- if not self.displayed_stat:
- return
-
- stat = self.get_attr('_stats')[self.displayed_stat]
-
- attr = DrawAttributes(
- stat = type(stat)(stat), # clone the GraphCategory
- subgraph_height = self._graph_height + 2, # graph rows + header + x-axis label
- subgraph_width = min(width / 2, CONFIG['features.graph.max_width']),
- interval = self.update_interval,
- bounds_type = self.bounds_type,
- accounting = self.get_attr('_accounting_stats'),
- )
-
- if self.is_title_visible():
- self.addstr(0, 0, attr.stat.title(width), curses.A_STANDOUT)
-
- self._draw_subgraph(attr, attr.stat.primary, 0, PRIMARY_COLOR)
- self._draw_subgraph(attr, attr.stat.secondary, attr.subgraph_width, SECONDARY_COLOR)
-
- if attr.stat.stat_type() == GraphStat.BANDWIDTH:
- if width <= COLLAPSE_WIDTH:
- self._draw_bandwidth_stats(attr, width)
-
- if attr.accounting:
- self._draw_accounting_stats(attr)
-
- def _draw_subgraph(self, attr, data, x, color):
- # Concering our subgraph colums, the y-axis label can be at most six
- # characters, with two spaces of padding on either side of the graph.
- # Starting with the smallest size, then possibly raise it after determing
- # the y_axis_labels.
-
- subgraph_columns = attr.subgraph_width - 8
- min_bound, max_bound = self._get_graph_bounds(attr, data, subgraph_columns)
-
- x_axis_labels = self._get_x_axis_labels(attr, subgraph_columns)
- y_axis_labels = self._get_y_axis_labels(attr, data, min_bound, max_bound)
- subgraph_columns = max(subgraph_columns, attr.subgraph_width - max([len(label) for label in y_axis_labels.values()]) - 2)
- axis_offset = max([len(label) for label in y_axis_labels.values()])
-
- self.addstr(1, x, data.header(attr.subgraph_width), curses.A_BOLD, color)
-
- for x_offset, label in x_axis_labels.items():
- self.addstr(attr.subgraph_height, x + x_offset + axis_offset, label, color)
-
- for y, label in y_axis_labels.items():
- self.addstr(y, x, label, color)
-
- for col in range(subgraph_columns):
- column_count = int(data.values[attr.interval][col]) - min_bound
- column_height = int(min(attr.subgraph_height - 2, (attr.subgraph_height - 2) * column_count / (max(1, max_bound) - min_bound)))
-
- for row in range(column_height):
- self.addstr(attr.subgraph_height - 1 - row, x + col + axis_offset + 1, ' ', curses.A_STANDOUT, color)
-
- def _get_graph_bounds(self, attr, data, subgraph_columns):
- """
- Provides the range the graph shows (ie, its minimum and maximum value).
- """
-
- min_bound, max_bound = 0, 0
- values = data.values[attr.interval][:subgraph_columns]
-
- if attr.bounds_type == Bounds.GLOBAL_MAX:
- max_bound = data.max_value[attr.interval]
- elif subgraph_columns > 0:
- max_bound = max(values) # local maxima
-
- if attr.bounds_type == Bounds.TIGHT and subgraph_columns > 0:
- min_bound = min(values)
-
- # if the max = min pick zero so we still display something
-
- if min_bound == max_bound:
- min_bound = 0
-
- return min_bound, max_bound
-
- def _get_y_axis_labels(self, attr, data, min_bound, max_bound):
- """
- Provides the labels for the y-axis. This is a mapping of the position it
- should be drawn at to its text.
- """
-
- y_axis_labels = {
- 2: data.y_axis_label(max_bound),
- attr.subgraph_height - 1: data.y_axis_label(min_bound),
- }
-
- ticks = (attr.subgraph_height - 5) / 2
-
- for i in range(ticks):
- row = attr.subgraph_height - (2 * i) - 5
-
- if attr.subgraph_height % 2 == 0 and i >= (ticks / 2):
- row -= 1 # make extra gap be in the middle when we're an even size
-
- val = (max_bound - min_bound) * (attr.subgraph_height - row - 3) / (attr.subgraph_height - 3)
-
- if val not in (min_bound, max_bound):
- y_axis_labels[row + 2] = data.y_axis_label(val)
-
- return y_axis_labels
-
- def _get_x_axis_labels(self, attr, subgraph_columns):
- """
- Provides the labels for the x-axis. We include the units for only its first
- value, then bump the precision for subsequent units. For example...
-
- 10s, 20, 30, 40, 50, 1m, 1.1, 1.3, 1.5
- """
-
- x_axis_labels = {}
-
- interval_sec = INTERVAL_SECONDS[attr.interval]
- interval_spacing = 10 if subgraph_columns >= WIDE_LABELING_GRAPH_COL else 5
- units_label, decimal_precision = None, 0
-
- for i in range((subgraph_columns - 4) / interval_spacing):
- x = (i + 1) * interval_spacing
- time_label = str_tools.time_label(x * interval_sec, decimal_precision)
-
- if not units_label:
- units_label = time_label[-1]
- elif units_label != time_label[-1]:
- # upped scale so also up precision of future measurements
- units_label = time_label[-1]
- decimal_precision += 1
- else:
- # if constrained on space then strips labeling since already provided
- time_label = time_label[:-1]
-
- x_axis_labels[x] = time_label
-
- return x_axis_labels
-
- def _draw_bandwidth_stats(self, attr, width):
- """
- Replaces the x-axis labeling with bandwidth stats. This is done on small
- screens since this information otherwise wouldn't fit.
- """
-
- labeling_line = DEFAULT_CONTENT_HEIGHT + attr.subgraph_height - 4
- self.addstr(labeling_line, 0, ' ' * width) # clear line
-
- runtime = time.time() - attr.stat.start_time
- primary_footer = 'total: %s, avg: %s/sec' % (_size_label(attr.stat.primary.total), _size_label(attr.stat.primary.total / runtime))
- secondary_footer = 'total: %s, avg: %s/sec' % (_size_label(attr.stat.secondary.total), _size_label(attr.stat.secondary.total / runtime))
-
- self.addstr(labeling_line, 1, primary_footer, PRIMARY_COLOR)
- self.addstr(labeling_line, attr.subgraph_width + 1, secondary_footer, SECONDARY_COLOR)
-
- def _draw_accounting_stats(self, attr):
- y = DEFAULT_CONTENT_HEIGHT + attr.subgraph_height - 2
-
- if tor_controller().is_alive():
- hibernate_color = CONFIG['attr.hibernate_color'].get(attr.accounting.status, 'red')
-
- x = self.addstr(y, 0, 'Accounting (', curses.A_BOLD)
- x = self.addstr(y, x, attr.accounting.status, curses.A_BOLD, hibernate_color)
- x = self.addstr(y, x, ')', curses.A_BOLD)
-
- self.addstr(y, 35, 'Time to reset: %s' % str_tools.short_time_label(attr.accounting.time_until_reset))
-
- self.addstr(y + 1, 2, '%s / %s' % (attr.accounting.read_bytes, attr.accounting.read_limit), PRIMARY_COLOR)
- self.addstr(y + 1, 37, '%s / %s' % (attr.accounting.written_bytes, attr.accounting.write_limit), SECONDARY_COLOR)
- else:
- self.addstr(y, 0, 'Accounting:', curses.A_BOLD)
- self.addstr(y, 12, 'Connection Closed...')
-
- def copy_attr(self, attr):
- if attr == '_stats':
- return dict([(key, type(self._stats[key])(self._stats[key])) for key in self._stats])
- else:
- return panel.Panel.copy_attr(self, attr)
-
- def _update_accounting(self, event):
- if not CONFIG['features.graph.bw.accounting.show']:
- self._accounting_stats = None
- elif not self._accounting_stats or time.time() - self._accounting_stats.retrieved >= ACCOUNTING_RATE:
- old_accounting_stats = self._accounting_stats
- self._accounting_stats = tor_controller().get_accounting_stats(None)
-
- # if we either added or removed accounting info then redraw the whole
- # screen to account for resizing
-
- if bool(old_accounting_stats) != bool(self._accounting_stats):
- nyx.controller.get_controller().redraw()
-
- def _update_stats(self, event):
- for stat in self._stats.values():
- stat.bandwidth_event(event)
-
- if self.displayed_stat:
- param = self.get_attr('_stats')[self.displayed_stat]
- update_rate = INTERVAL_SECONDS[self.update_interval]
-
- if param.primary.tick % update_rate == 0:
- self.redraw(True)
-
-
-def _size_label(byte_count, decimal = 1):
- """
- Alias for str_tools.size_label() that accounts for if the user prefers bits
- or bytes.
- """
-
- return str_tools.size_label(byte_count, decimal, is_bytes = CONFIG['features.graph.bw.transferInBytes'])
diff --git a/nyx/header_panel.py b/nyx/header_panel.py
deleted file mode 100644
index 0d6591a..0000000
--- a/nyx/header_panel.py
+++ /dev/null
@@ -1,496 +0,0 @@
-"""
-Top panel for every page, containing basic system and tor related information.
-This expands the information it presents to two columns if there's room
-available.
-"""
-
-import collections
-import os
-import time
-import curses
-import threading
-
-import stem
-
-import nyx.controller
-import nyx.popups
-
-from stem.control import Listener, State
-from stem.util import conf, log, proc, str_tools, system
-from nyx.util import msg, tor_controller, panel, tracker
-
-MIN_DUAL_COL_WIDTH = 141 # minimum width where we'll show two columns
-SHOW_FD_THRESHOLD = 60 # show file descriptor usage if usage is over this percentage
-UPDATE_RATE = 5 # rate in seconds at which we refresh
-
-CONFIG = conf.config_dict('nyx', {
- 'attr.flag_colors': {},
- 'attr.version_status_colors': {},
-})
-
-
-class HeaderPanel(panel.Panel, threading.Thread):
- """
- Top area containing tor settings and system information.
- """
-
- def __init__(self, stdscr):
- panel.Panel.__init__(self, stdscr, 'header', 0)
- threading.Thread.__init__(self)
- self.setDaemon(True)
-
- self._vals = get_sampling()
-
- self._pause_condition = threading.Condition()
- self._halt = False # terminates thread if true
- self._reported_inactive = False
-
- tor_controller().add_status_listener(self.reset_listener)
-
- def is_wide(self, width = None):
- """
- True if we should show two columns of information, False otherwise.
- """
-
- if width is None:
- width = self.get_parent().getmaxyx()[1]
-
- return width >= MIN_DUAL_COL_WIDTH
-
- def get_height(self):
- """
- Provides the height of the content, which is dynamically determined by the
- panel's maximum width.
- """
-
- if self._vals.is_relay:
- return 4 if self.is_wide() else 6
- else:
- return 3 if self.is_wide() else 4
-
- def send_newnym(self):
- """
- Requests a new identity and provides a visual queue.
- """
-
- controller = tor_controller()
-
- if not controller.is_newnym_available():
- return
-
- controller.signal(stem.Signal.NEWNYM)
-
- # If we're wide then the newnym label in this panel will give an
- # indication that the signal was sent. Otherwise use a msg.
-
- if not self.is_wide():
- nyx.popups.show_msg('Requesting a new identity', 1)
-
- def handle_key(self, key):
- if key.match('n'):
- self.send_newnym()
- elif key.match('r') and not self._vals.is_connected:
- # TODO: This is borked. Not quite sure why but our attempt to call
- # PROTOCOLINFO fails with a socket error, followed by completely freezing
- # nyx. This is exposing two bugs...
- #
- # * This should be working. That's a stem issue.
- # * Our interface shouldn't be locking up. That's an nyx issue.
-
- return True
-
- controller = tor_controller()
-
- try:
- controller.connect()
-
- try:
- controller.authenticate() # TODO: should account for our chroot
- except stem.connection.MissingPassword:
- password = nyx.popups.input_prompt('Controller Password: ')
-
- if password:
- controller.authenticate(password)
-
- log.notice("Reconnected to Tor's control port")
- nyx.popups.show_msg('Tor reconnected', 1)
- except Exception as exc:
- nyx.popups.show_msg('Unable to reconnect (%s)' % exc, 3)
- controller.close()
- else:
- return False
-
- return True
-
- def draw(self, width, height):
- vals = self._vals # local reference to avoid concurrency concerns
- is_wide = self.is_wide(width)
-
- # space available for content
-
- left_width = max(width / 2, 77) if is_wide else width
- right_width = width - left_width
-
- self._draw_platform_section(0, 0, left_width, vals)
-
- if vals.is_connected:
- self._draw_ports_section(0, 1, left_width, vals)
- else:
- self._draw_disconnected(0, 1, left_width, vals)
-
- if is_wide:
- self._draw_resource_usage(left_width, 0, right_width, vals)
-
- if vals.is_relay:
- self._draw_fingerprint_and_fd_usage(left_width, 1, right_width, vals)
- self._draw_flags(0, 2, left_width, vals)
- self._draw_exit_policy(left_width, 2, right_width, vals)
- elif vals.is_connected:
- self._draw_newnym_option(left_width, 1, right_width, vals)
- else:
- self._draw_resource_usage(0, 2, left_width, vals)
-
- if vals.is_relay:
- self._draw_fingerprint_and_fd_usage(0, 3, left_width, vals)
- self._draw_flags(0, 4, left_width, vals)
-
- def _draw_platform_section(self, x, y, width, vals):
- """
- Section providing the user's hostname, platform, and version information...
-
- nyx - odin (Linux 3.5.0-52-generic) Tor 0.2.5.1-alpha-dev (unrecommended)
- |------ platform (40 characters) ------| |----------- tor version -----------|
- """
-
- initial_x, space_left = x, min(width, 40)
-
- x = self.addstr(y, x, vals.format('nyx - {hostname}', space_left))
- space_left -= x - initial_x
-
- if space_left >= 10:
- self.addstr(y, x, ' (%s)' % vals.format('{platform}', space_left - 3))
-
- x, space_left = initial_x + 43, width - 43
-
- if vals.version != 'Unknown' and space_left >= 10:
- x = self.addstr(y, x, vals.format('Tor {version}', space_left))
- space_left -= x - 43 - initial_x
-
- if space_left >= 7 + len(vals.version_status):
- version_color = CONFIG['attr.version_status_colors'].get(vals.version_status, 'white')
-
- x = self.addstr(y, x, ' (')
- x = self.addstr(y, x, vals.version_status, version_color)
- self.addstr(y, x, ')')
-
- def _draw_ports_section(self, x, y, width, vals):
- """
- Section providing our nickname, address, and port information...
-
- Unnamed - 0.0.0.0:7000, Control Port (cookie): 9051
- """
-
- if not vals.is_relay:
- x = self.addstr(y, x, 'Relaying Disabled', 'cyan')
- else:
- x = self.addstr(y, x, vals.format('{nickname} - {address}:{or_port}'))
-
- if vals.dir_port != '0':
- x = self.addstr(y, x, vals.format(', Dir Port: {dir_port}'))
-
- if vals.control_port:
- if width >= x + 19 + len(vals.control_port) + len(vals.auth_type):
- auth_color = 'red' if vals.auth_type == 'open' else 'green'
-
- x = self.addstr(y, x, ', Control Port (')
- x = self.addstr(y, x, vals.auth_type, auth_color)
- self.addstr(y, x, vals.format('): {control_port}'))
- else:
- self.addstr(y, x, vals.format(', Control Port: {control_port}'))
- elif vals.socket_path:
- self.addstr(y, x, vals.format(', Control Socket: {socket_path}'))
-
- def _draw_disconnected(self, x, y, width, vals):
- """
- Message indicating that tor is disconnected...
-
- Tor Disconnected (15:21 07/13/2014, press r to reconnect)
- """
-
- x = self.addstr(y, x, 'Tor Disconnected', curses.A_BOLD, 'red')
- last_heartbeat = time.strftime('%H:%M %m/%d/%Y', time.localtime(vals.last_heartbeat))
- self.addstr(y, x, ' (%s, press r to reconnect)' % last_heartbeat)
-
- def _draw_resource_usage(self, x, y, width, vals):
- """
- System resource usage of the tor process...
-
- cpu: 0.0% tor, 1.0% nyx mem: 0 (0.0%) pid: 16329 uptime: 12-20:42:07
- """
-
- if vals.start_time:
- if not vals.is_connected:
- now = vals.connection_time
- elif self.is_paused():
- now = self.get_pause_time()
- else:
- now = time.time()
-
- uptime = str_tools.short_time_label(now - vals.start_time)
- else:
- uptime = ''
-
- sys_fields = (
- (0, vals.format('cpu: {tor_cpu}% tor, {nyx_cpu}% nyx')),
- (27, vals.format('mem: {memory} ({memory_percent}%)')),
- (47, vals.format('pid: {pid}')),
- (59, 'uptime: %s' % uptime),
- )
-
- for (start, label) in sys_fields:
- if width >= start + len(label):
- self.addstr(y, x + start, label)
- else:
- break
-
- def _draw_fingerprint_and_fd_usage(self, x, y, width, vals):
- """
- Presents our fingerprint, and our file descriptor usage if we're running
- out...
-
- fingerprint: 1A94D1A794FCB2F8B6CBC179EF8FDD4008A98D3B, file desc: 900 / 1000 (90%)
- """
-
- initial_x, space_left = x, width
-
- x = self.addstr(y, x, vals.format('fingerprint: {fingerprint}', width))
- space_left -= x - initial_x
-
- if space_left >= 30 and vals.fd_used and vals.fd_limit != -1:
- fd_percent = 100 * vals.fd_used / vals.fd_limit
-
- if fd_percent >= SHOW_FD_THRESHOLD:
- if fd_percent >= 95:
- percentage_format = (curses.A_BOLD, 'red')
- elif fd_percent >= 90:
- percentage_format = ('red',)
- elif fd_percent >= 60:
- percentage_format = ('yellow',)
- else:
- percentage_format = ()
-
- x = self.addstr(y, x, ', file descriptors' if space_left >= 37 else ', file desc')
- x = self.addstr(y, x, vals.format(': {fd_used} / {fd_limit} ('))
- x = self.addstr(y, x, '%i%%' % fd_percent, *percentage_format)
- self.addstr(y, x, ')')
-
- def _draw_flags(self, x, y, width, vals):
- """
- Presents flags held by our relay...
-
- flags: Running, Valid
- """
-
- x = self.addstr(y, x, 'flags: ')
-
- if vals.flags:
- for i, flag in enumerate(vals.flags):
- flag_color = CONFIG['attr.flag_colors'].get(flag, 'white')
- x = self.addstr(y, x, flag, curses.A_BOLD, flag_color)
-
- if i < len(vals.flags) - 1:
- x = self.addstr(y, x, ', ')
- else:
- self.addstr(y, x, 'none', curses.A_BOLD, 'cyan')
-
- def _draw_exit_policy(self, x, y, width, vals):
- """
- Presents our exit policy...
-
- exit policy: reject *:*
- """
-
- x = self.addstr(y, x, 'exit policy: ')
-
- if not vals.exit_policy:
- return
-
- rules = list(vals.exit_policy.strip_private().strip_default())
-
- for i, rule in enumerate(rules):
- policy_color = 'green' if rule.is_accept else 'red'
- x = self.addstr(y, x, str(rule), curses.A_BOLD, policy_color)
-
- if i < len(rules) - 1:
- x = self.addstr(y, x, ', ')
-
- if vals.exit_policy.has_default():
- if rules:
- x = self.addstr(y, x, ', ')
-
- self.addstr(y, x, '<default>', curses.A_BOLD, 'cyan')
-
- def _draw_newnym_option(self, x, y, width, vals):
- """
- Provide a notice for requiesting a new identity, and time until it's next
- available if in the process of building circuits.
- """
-
- if vals.newnym_wait == 0:
- self.addstr(y, x, "press 'n' for a new identity")
- else:
- plural = 's' if vals.newnym_wait > 1 else ''
- self.addstr(y, x, 'building circuits, available again in %i second%s' % (vals.newnym_wait, plural))
-
- def run(self):
- """
- Keeps stats updated, checking for new information at a set rate.
- """
-
- last_ran = -1
-
- while not self._halt:
- if self.is_paused() or not self._vals.is_connected or (time.time() - last_ran) < UPDATE_RATE:
- with self._pause_condition:
- if not self._halt:
- self._pause_condition.wait(0.2)
-
- continue # done waiting, try again
-
- self._update()
- last_ran = time.time()
-
- def stop(self):
- """
- Halts further resolutions and terminates the thread.
- """
-
- with self._pause_condition:
- self._halt = True
- self._pause_condition.notifyAll()
-
- def reset_listener(self, controller, event_type, _):
- self._update()
-
- if event_type == State.CLOSED:
- log.notice('Tor control port closed')
-
- def _update(self):
- previous_height = self.get_height()
- self._vals = get_sampling(self._vals)
-
- if self._vals.fd_used and self._vals.fd_limit != -1:
- fd_percent = 100 * self._vals.fd_used / self._vals.fd_limit
-
- if fd_percent >= 90:
- log_msg = msg('panel.header.fd_used_at_ninety_percent', percentage = fd_percent)
- log.log_once('fd_used_at_ninety_percent', log.WARN, log_msg)
- log.DEDUPLICATION_MESSAGE_IDS.add('fd_used_at_sixty_percent')
- elif fd_percent >= 60:
- log_msg = msg('panel.header.fd_used_at_sixty_percent', percentage = fd_percent)
- log.log_once('fd_used_at_sixty_percent', log.NOTICE, log_msg)
-
- if self._vals.is_connected:
- if not self._reported_inactive and (time.time() - self._vals.last_heartbeat) >= 10:
- self._reported_inactive = True
- log.notice('Relay unresponsive (last heartbeat: %s)' % time.ctime(self._vals.last_heartbeat))
- elif self._reported_inactive and (time.time() - self._vals.last_heartbeat) < 10:
- self._reported_inactive = False
- log.notice('Relay resumed')
-
- if previous_height != self.get_height():
- # We're toggling between being a relay and client, causing the height
- # of this panel to change. Redraw all content so we don't get
- # overlapping content.
-
- nyx.controller.get_controller().redraw()
- else:
- self.redraw(True) # just need to redraw ourselves
-
-
-def get_sampling(last_sampling = None):
- controller = tor_controller()
- retrieved = time.time()
-
- pid = controller.get_pid('')
- tor_resources = tracker.get_resource_tracker().get_value()
- nyx_total_cpu_time = sum(os.times()[:3])
-
- or_listeners = controller.get_listeners(Listener.OR, [])
- control_listeners = controller.get_listeners(Listener.CONTROL, [])
-
- if controller.get_conf('HashedControlPassword', None):
- auth_type = 'password'
- elif controller.get_conf('CookieAuthentication', None) == '1':
- auth_type = 'cookie'
- else:
- auth_type = 'open'
-
- try:
- fd_used = proc.file_descriptors_used(pid)
- except IOError:
- fd_used = None
-
- if last_sampling:
- nyx_cpu_delta = nyx_total_cpu_time - last_sampling.nyx_total_cpu_time
- nyx_time_delta = retrieved - last_sampling.retrieved
-
- python_cpu_time = nyx_cpu_delta / nyx_time_delta
- sys_call_cpu_time = 0.0 # TODO: add a wrapper around call() to get this
-
- nyx_cpu = python_cpu_time + sys_call_cpu_time
- else:
- nyx_cpu = 0.0
-
- attr = {
- 'retrieved': retrieved,
- 'is_connected': controller.is_alive(),
- 'connection_time': controller.connection_time(),
- 'last_heartbeat': controller.get_latest_heartbeat(),
-
- 'fingerprint': controller.get_info('fingerprint', 'Unknown'),
- 'nickname': controller.get_conf('Nickname', ''),
- 'newnym_wait': controller.get_newnym_wait(),
- 'exit_policy': controller.get_exit_policy(None),
- 'flags': getattr(controller.get_network_status(default = None), 'flags', []),
-
- 'version': str(controller.get_version('Unknown')).split()[0],
- 'version_status': controller.get_info('status/version/current', 'Unknown'),
-
- 'address': or_listeners[0][0] if (or_listeners and or_listeners[0][0] != '0.0.0.0') else controller.get_info('address', 'Unknown'),
- 'or_port': or_listeners[0][1] if or_listeners else '',
- 'dir_port': controller.get_conf('DirPort', '0'),
- 'control_port': str(control_listeners[0][1]) if control_listeners else None,
- 'socket_path': controller.get_conf('ControlSocket', None),
- 'is_relay': bool(or_listeners),
-
- 'auth_type': auth_type,
- 'pid': pid,
- 'start_time': system.start_time(pid),
- 'fd_limit': int(controller.get_info('process/descriptor-limit', '-1')),
- 'fd_used': fd_used,
-
- 'nyx_total_cpu_time': nyx_total_cpu_time,
- 'tor_cpu': '%0.1f' % (100 * tor_resources.cpu_sample),
- 'nyx_cpu': nyx_cpu,
- 'memory': str_tools.size_label(tor_resources.memory_bytes) if tor_resources.memory_bytes > 0 else 0,
- 'memory_percent': '%0.1f' % (100 * tor_resources.memory_percent),
-
- 'hostname': os.uname()[1],
- 'platform': '%s %s' % (os.uname()[0], os.uname()[2]), # [platform name] [version]
- }
-
- class Sampling(collections.namedtuple('Sampling', attr.keys())):
- def __init__(self, **attr):
- super(Sampling, self).__init__(**attr)
- self._attr = attr
-
- def format(self, message, crop_width = None):
- formatted_msg = message.format(**self._attr)
-
- if crop_width:
- formatted_msg = str_tools.crop(formatted_msg, crop_width)
-
- return formatted_msg
-
- return Sampling(**attr)
diff --git a/nyx/log_panel.py b/nyx/log_panel.py
deleted file mode 100644
index 708a679..0000000
--- a/nyx/log_panel.py
+++ /dev/null
@@ -1,454 +0,0 @@
-"""
-Panel providing a chronological log of events its been configured to listen
-for. This provides prepopulation from the log file and supports filtering by
-regular expressions.
-"""
-
-import os
-import time
-import curses
-import threading
-
-import stem.response.events
-
-import nyx.arguments
-import nyx.popups
-import nyx.util.log
-
-from nyx.util import join, panel, tor_controller, ui_tools
-from stem.util import conf, log
-
-
-def conf_handler(key, value):
- if key == 'features.log.prepopulateReadLimit':
- return max(0, value)
- elif key == 'features.log.maxRefreshRate':
- return max(10, value)
- elif key == 'cache.log_panel.size':
- return max(1000, value)
-
-
-CONFIG = conf.config_dict('nyx', {
- 'attr.log_color': {},
- 'cache.log_panel.size': 1000,
- 'features.logFile': '',
- 'features.log.showDuplicateEntries': False,
- 'features.log.prepopulate': True,
- 'features.log.prepopulateReadLimit': 5000,
- 'features.log.maxRefreshRate': 300,
- 'features.log.regex': [],
- 'msg.misc.event_types': '',
- 'startup.events': 'N3',
-}, conf_handler)
-
-# The height of the drawn content is estimated based on the last time we redrew
-# the panel. It's chiefly used for scrolling and the bar indicating its
-# position. Letting the estimate be too inaccurate results in a display bug, so
-# redraws the display if it's off by this threshold.
-
-CONTENT_HEIGHT_REDRAW_THRESHOLD = 3
-
-# Log buffer so we start collecting stem/nyx events when imported. This is used
-# to make our LogPanel when curses initializes.
-
-stem_logger = log.get_logger()
-NYX_LOGGER = log.LogBuffer(log.Runlevel.DEBUG, yield_records = True)
-stem_logger.addHandler(NYX_LOGGER)
-
-
-class LogPanel(panel.Panel, threading.Thread):
- """
- Listens for and displays tor, nyx, and stem events. This prepopulates
- from tor's log file if it exists.
- """
-
- def __init__(self, stdscr):
- panel.Panel.__init__(self, stdscr, 'log', 0)
- threading.Thread.__init__(self)
- self.setDaemon(True)
-
- logged_events = nyx.arguments.expand_events(CONFIG['startup.events'])
- self._event_log = nyx.util.log.LogGroup(CONFIG['cache.log_panel.size'], group_by_day = True)
- self._event_types = nyx.util.log.listen_for_events(self._register_tor_event, logged_events)
- self._log_file = nyx.util.log.LogFileOutput(CONFIG['features.logFile'])
- self._filter = nyx.util.log.LogFilters(initial_filters = CONFIG['features.log.regex'])
- self._show_duplicates = CONFIG['features.log.showDuplicateEntries']
-
- self.set_pause_attr('_event_log')
-
- self._halt = False # terminates thread if true
- self._pause_condition = threading.Condition()
- self._has_new_event = False
-
- # fetches past tor events from log file, if available
-
- if CONFIG['features.log.prepopulate']:
- log_location = nyx.util.log.log_file_path(tor_controller())
-
- if log_location:
- try:
- for entry in reversed(list(nyx.util.log.read_tor_log(log_location, CONFIG['features.log.prepopulateReadLimit']))):
- if entry.type in self._event_types:
- self._event_log.add(entry)
- except IOError as exc:
- log.info('Unable to read log located at %s: %s' % (log_location, exc))
- except ValueError as exc:
- log.info(str(exc))
-
- self._last_content_height = len(self._event_log) # height of the rendered content when last drawn
- self._scroll = 0
-
- # merge NYX_LOGGER into us, and listen for its future events
-
- for event in NYX_LOGGER:
- self._register_nyx_event(event)
-
- NYX_LOGGER.emit = self._register_nyx_event
-
- def set_duplicate_visability(self, is_visible):
- """
- Sets if duplicate log entries are collaped or expanded.
-
- :param bool is_visible: if **True** all log entries are shown, otherwise
- they're deduplicated
- """
-
- self._show_duplicates = is_visible
-
- def get_filter(self):
- """
- Provides our currently selected regex filter.
- """
-
- return self._filter
-
- def show_filter_prompt(self):
- """
- Prompts the user to add a new regex filter.
- """
-
- regex_input = nyx.popups.input_prompt('Regular expression: ')
-
- if regex_input:
- self._filter.select(regex_input)
-
- def show_event_selection_prompt(self):
- """
- Prompts the user to select the events being listened for.
- """
-
- # allow user to enter new types of events to log - unchanged if left blank
-
- with nyx.popups.popup_window(16, 80) as (popup, width, height):
- if popup:
- # displays the available flags
-
- popup.win.box()
- popup.addstr(0, 0, 'Event Types:', curses.A_STANDOUT)
- event_lines = CONFIG['msg.misc.event_types'].split('\n')
-
- for i in range(len(event_lines)):
- popup.addstr(i + 1, 1, event_lines[i][6:])
-
- popup.win.refresh()
-
- user_input = nyx.popups.input_prompt('Events to log: ')
-
- if user_input:
- try:
- user_input = user_input.replace(' ', '') # strip spaces
- event_types = nyx.arguments.expand_events(user_input)
-
- if event_types != self._event_types:
- self._event_types = nyx.util.log.listen_for_events(self._register_tor_event, event_types)
- self.redraw(True)
- except ValueError as exc:
- nyx.popups.show_msg('Invalid flags: %s' % str(exc), 2)
-
- def show_snapshot_prompt(self):
- """
- Lets user enter a path to take a snapshot, canceling if left blank.
- """
-
- path_input = nyx.popups.input_prompt('Path to save log snapshot: ')
-
- if path_input:
- try:
- self.save_snapshot(path_input)
- nyx.popups.show_msg('Saved: %s' % path_input, 2)
- except IOError as exc:
- nyx.popups.show_msg('Unable to save snapshot: %s' % exc, 2)
-
- def clear(self):
- """
- Clears the contents of the event log.
- """
-
- self._event_log = nyx.util.log.LogGroup(CONFIG['cache.log_panel.size'], group_by_day = True)
- self.redraw(True)
-
- def save_snapshot(self, path):
- """
- Saves the log events currently being displayed to the given path. This
- takes filers into account. This overwrites the file if it already exists.
-
- :param str path: path where to save the log snapshot
-
- :raises: **IOError** if unsuccessful
- """
-
- path = os.path.abspath(os.path.expanduser(path))
-
- # make dir if the path doesn't already exist
-
- base_dir = os.path.dirname(path)
-
- try:
- if not os.path.exists(base_dir):
- os.makedirs(base_dir)
- except OSError as exc:
- raise IOError("unable to make directory '%s'" % base_dir)
-
- event_log = list(self._event_log)
- event_filter = self._filter.clone()
-
- with open(path, 'w') as snapshot_file:
- try:
- for entry in reversed(event_log):
- if event_filter.match(entry.display_message):
- snapshot_file.write(entry.display_message + '\n')
- except Exception as exc:
- raise IOError("unable to write to '%s': %s" % (path, exc))
-
- def handle_key(self, key):
- if key.is_scroll():
- page_height = self.get_preferred_size()[0] - 1
- new_scroll = ui_tools.get_scroll_position(key, self._scroll, page_height, self._last_content_height)
-
- if self._scroll != new_scroll:
- self._scroll = new_scroll
- self.redraw(True)
- elif key.match('u'):
- self.set_duplicate_visability(not self._show_duplicates)
- self.redraw(True)
- elif key.match('c'):
- msg = 'This will clear the log. Are you sure (c again to confirm)?'
- key_press = nyx.popups.show_msg(msg, attr = curses.A_BOLD)
-
- if key_press.match('c'):
- self.clear()
- elif key.match('f'):
- with panel.CURSES_LOCK:
- initial_selection = 1 if self._filter.selection() else 0
- options = ['None'] + self._filter.latest_selections() + ['New...']
- selection = nyx.popups.show_menu('Log Filter:', options, initial_selection)
-
- if selection == 0:
- self._filter.select(None)
- elif selection == len(options) - 1:
- # selected 'New...' option - prompt user to input regular expression
- self.show_filter_prompt()
- elif selection != -1:
- self._filter.select(self._filter.latest_selections()[selection - 1])
- elif key.match('e'):
- self.show_event_selection_prompt()
- elif key.match('a'):
- self.show_snapshot_prompt()
- else:
- return False
-
- return True
-
- def get_help(self):
- return [
- ('up arrow', 'scroll log up a line', None),
- ('down arrow', 'scroll log down a line', None),
- ('a', 'save snapshot of the log', None),
- ('e', 'change logged events', None),
- ('f', 'log regex filter', 'enabled' if self._filter.selection() else 'disabled'),
- ('u', 'duplicate log entries', 'visible' if self._show_duplicates else 'hidden'),
- ('c', 'clear event log', None),
- ]
-
- def draw(self, width, height):
- self._scroll = max(0, min(self._scroll, self._last_content_height - height + 1))
-
- event_log = list(self.get_attr('_event_log'))
- event_filter = self._filter.clone()
- event_types = list(self._event_types)
- scroll = int(self._scroll)
- last_content_height = self._last_content_height
- show_duplicates = self._show_duplicates
-
- is_scrollbar_visible = last_content_height > height - 1
-
- if is_scrollbar_visible:
- self.add_scroll_bar(scroll, scroll + height - 1, last_content_height, 1)
-
- x, y = 3 if is_scrollbar_visible else 1, 1 - scroll
-
- # group entries by date, filtering out those that aren't visible
-
- day_to_entries, today = {}, nyx.util.log.day_count(time.time())
-
- for entry in event_log:
- if entry.is_duplicate and not show_duplicates:
- continue # deduplicated message
- elif not event_filter.match(entry.display_message):
- continue # filter doesn't match log message
-
- day_to_entries.setdefault(entry.day_count(), []).append(entry)
-
- for day in sorted(day_to_entries.keys(), reverse = True):
- if day == today:
- for entry in day_to_entries[day]:
- y = self._draw_entry(x, y, width, entry, show_duplicates)
- else:
- original_y, y = y, y + 1
-
- for entry in day_to_entries[day]:
- y = self._draw_entry(x, y, width, entry, show_duplicates)
-
- ui_tools.draw_box(self, original_y, x - 1, width - x + 1, y - original_y + 1, curses.A_BOLD, 'yellow')
- time_label = time.strftime(' %B %d, %Y ', time.localtime(day_to_entries[day][0].timestamp))
- self.addstr(original_y, x + 1, time_label, curses.A_BOLD, curses.A_BOLD, 'yellow')
-
- y += 1
-
- # drawing the title after the content, so we'll clear content from the top line
-
- if self.is_title_visible():
- self._draw_title(width, event_types, event_filter)
-
- # redraw the display if...
- # - last_content_height was off by too much
- # - we're off the bottom of the page
-
- new_content_height = y + scroll - 1
- content_height_delta = abs(last_content_height - new_content_height)
- force_redraw, force_redraw_reason = True, ''
-
- if content_height_delta >= CONTENT_HEIGHT_REDRAW_THRESHOLD:
- force_redraw_reason = 'estimate was off by %i' % content_height_delta
- elif new_content_height > height and self._scroll + height - 1 > new_content_height:
- force_redraw_reason = 'scrolled off the bottom of the page'
- elif not is_scrollbar_visible and new_content_height > height - 1:
- force_redraw_reason = "scroll bar wasn't previously visible"
- elif is_scrollbar_visible and new_content_height <= height - 1:
- force_redraw_reason = "scroll bar shouldn't be visible"
- else:
- force_redraw = False
-
- self._last_content_height = new_content_height
- self._has_new_event = False
-
- if force_redraw:
- log.debug('redrawing the log panel with the corrected content height (%s)' % force_redraw_reason)
- self.redraw(True)
-
- def _draw_title(self, width, event_types, event_filter):
- """
- Panel title with the event types we're logging and our regex filter if set.
- """
-
- self.addstr(0, 0, ' ' * width) # clear line
- title_comp = list(nyx.util.log.condense_runlevels(*event_types))
-
- if event_filter.selection():
- title_comp.append('filter: %s' % event_filter.selection())
-
- title_comp_str = join(title_comp, ', ', width - 10)
- title = 'Events (%s):' % title_comp_str if title_comp_str else 'Events:'
-
- self.addstr(0, 0, title, curses.A_STANDOUT)
-
- def _draw_entry(self, x, y, width, entry, show_duplicates):
- """
- Presents a log entry with line wrapping.
- """
-
- min_x, msg = x + 2, entry.display_message
- boldness = curses.A_BOLD if 'ERR' in entry.type else curses.A_NORMAL # emphasize ERR messages
- color = CONFIG['attr.log_color'].get(entry.type, 'white')
-
- for line in msg.splitlines():
- x, y = self.addstr_wrap(y, x, line, width, min_x, boldness, color)
-
- if entry.duplicates and not show_duplicates:
- duplicate_count = len(entry.duplicates) - 1
- plural = 's' if duplicate_count > 1 else ''
- duplicate_msg = ' [%i duplicate%s hidden]' % (duplicate_count, plural)
- x, y = self.addstr_wrap(y, x, duplicate_msg, width, min_x, curses.A_BOLD, 'green')
-
- return y + 1
-
- def run(self):
- """
- Redraws the display, coalescing updates if events are rapidly logged (for
- instance running at the DEBUG runlevel) while also being immediately
- responsive if additions are less frequent.
- """
-
- last_ran, last_day = -1, nyx.util.log.day_count(time.time())
-
- while not self._halt:
- current_day = nyx.util.log.day_count(time.time())
- time_since_reset = time.time() - last_ran
- max_log_update_rate = CONFIG['features.log.maxRefreshRate'] / 1000.0
-
- sleep_time = 0
-
- if (not self._has_new_event and last_day == current_day) or self.is_paused():
- sleep_time = 5
- elif time_since_reset < max_log_update_rate:
- sleep_time = max(0.05, max_log_update_rate - time_since_reset)
-
- if sleep_time:
- with self._pause_condition:
- if not self._halt:
- self._pause_condition.wait(sleep_time)
-
- continue
-
- last_ran, last_day = time.time(), current_day
- self.redraw(True)
-
- def stop(self):
- """
- Halts further updates and terminates the thread.
- """
-
- with self._pause_condition:
- self._halt = True
- self._pause_condition.notifyAll()
-
- def _register_tor_event(self, event):
- msg = ' '.join(str(event).split(' ')[1:])
-
- if isinstance(event, stem.response.events.BandwidthEvent):
- msg = 'READ: %i, WRITTEN: %i' % (event.read, event.written)
- elif isinstance(event, stem.response.events.LogEvent):
- msg = event.message
-
- self._register_event(nyx.util.log.LogEntry(event.arrived_at, event.type, msg))
-
- def _register_nyx_event(self, record):
- if record.levelname == 'WARNING':
- record.levelname = 'WARN'
-
- self._register_event(nyx.util.log.LogEntry(int(record.created), 'NYX_%s' % record.levelname, record.msg))
-
- def _register_event(self, event):
- if event.type not in self._event_types:
- return
-
- self._event_log.add(event)
- self._log_file.write(event.display_message)
-
- # notifies the display that it has new content
-
- if self._filter.match(event.display_message):
- self._has_new_event = True
-
- with self._pause_condition:
- self._pause_condition.notifyAll()
diff --git a/nyx/menu/actions.py b/nyx/menu/actions.py
index 0105615..222e3e7 100644
--- a/nyx/menu/actions.py
+++ b/nyx/menu/actions.py
@@ -4,10 +4,10 @@ Generates the menu for nyx, binding options with their related actions.
import functools
-import nyx.popups
import nyx.controller
+import nyx.panel.graph
+import nyx.popups
import nyx.menu.item
-import nyx.graph_panel
import nyx.util.tracker
from nyx.util import tor_controller, ui_tools
@@ -167,7 +167,7 @@ def make_graph_menu(graph_panel):
interval_menu = nyx.menu.item.Submenu('Interval')
interval_group = nyx.menu.item.SelectionGroup(functools.partial(setattr, graph_panel, 'update_interval'), graph_panel.update_interval)
- for interval in nyx.graph_panel.Interval:
+ for interval in nyx.panel.graph.Interval:
interval_menu.add(nyx.menu.item.SelectionMenuItem(interval, interval_group, interval))
graph_menu.add(interval_menu)
@@ -177,7 +177,7 @@ def make_graph_menu(graph_panel):
bounds_menu = nyx.menu.item.Submenu('Bounds')
bounds_group = nyx.menu.item.SelectionGroup(functools.partial(setattr, graph_panel, 'bounds_type'), graph_panel.bounds_type)
- for bounds_type in nyx.graph_panel.Bounds:
+ for bounds_type in nyx.panel.graph.Bounds:
bounds_menu.add(nyx.menu.item.SelectionMenuItem(bounds_type, bounds_group, bounds_type))
graph_menu.add(bounds_menu)
diff --git a/nyx/panel/__init__.py b/nyx/panel/__init__.py
new file mode 100644
index 0000000..0f25fac
--- /dev/null
+++ b/nyx/panel/__init__.py
@@ -0,0 +1,11 @@
+"""
+Panels consisting the nyx interface.
+"""
+
+__all__ = [
+ 'config',
+ 'connection',
+ 'header',
+ 'log',
+ 'torrc',
+]
diff --git a/nyx/panel/config.py b/nyx/panel/config.py
new file mode 100644
index 0000000..2352fe7
--- /dev/null
+++ b/nyx/panel/config.py
@@ -0,0 +1,357 @@
+"""
+Panel presenting the configuration state for tor or nyx. Options can be edited
+and the resulting configuration files saved.
+"""
+
+import curses
+import os
+
+import nyx.controller
+import nyx.popups
+
+import stem.control
+import stem.manual
+
+from nyx.util import DATA_DIR, panel, tor_controller, ui_tools
+
+from stem.util import conf, enum, log, str_tools
+
+SortAttr = enum.Enum('NAME', 'VALUE', 'VALUE_TYPE', 'CATEGORY', 'USAGE', 'SUMMARY', 'DESCRIPTION', 'MAN_PAGE_ENTRY', 'IS_SET')
+
+DETAILS_HEIGHT = 8
+NAME_WIDTH = 25
+VALUE_WIDTH = 15
+
+
+def conf_handler(key, value):
+ if key == 'features.config.order':
+ return conf.parse_enum_csv(key, value[0], SortAttr, 3)
+
+
+CONFIG = conf.config_dict('nyx', {
+ 'attr.config.category_color': {},
+ 'attr.config.sort_color': {},
+ 'features.config.order': [SortAttr.MAN_PAGE_ENTRY, SortAttr.NAME, SortAttr.IS_SET],
+ 'features.config.state.showPrivateOptions': False,
+ 'features.config.state.showVirtualOptions': False,
+}, conf_handler)
+
+
+class ConfigEntry(object):
+ """
+ Configuration option presented in the panel.
+
+ :var str name: name of the configuration option
+ :var str value_type: type of value
+ :var stem.manual.ConfigOption manual: manual information about the option
+ """
+
+ def __init__(self, name, value_type, manual):
+ self.name = name
+ self.value_type = value_type
+ self.manual = manual.config_options.get(name, stem.manual.ConfigOption(name))
+ self._index = manual.config_options.keys().index(name) if name in manual.config_options else 99999
+
+ def value(self):
+ """
+ Provides the value of this configuration option.
+
+ :returns: **str** representation of the current config value
+ """
+
+ values = tor_controller().get_conf(self.name, [], True)
+
+ if not values:
+ return '<none>'
+ elif self.value_type == 'Boolean' and values[0] in ('0', '1'):
+ return 'False' if values[0] == '0' else 'True'
+ elif self.value_type == 'DataSize' and values[0].isdigit():
+ return str_tools.size_label(int(values[0]))
+ elif self.value_type == 'TimeInterval' and values[0].isdigit():
+ return str_tools.time_label(int(values[0]), is_long = True)
+ else:
+ return ', '.join(values)
+
+ def is_set(self):
+ """
+ Checks if the configuration option has a custom value.
+
+ :returns: **True** if the option has a custom value, **False** otherwise
+ """
+
+ return tor_controller().is_set(self.name, False)
+
+ def sort_value(self, attr):
+ """
+ Provides a heuristic for sorting by a given value.
+
+ :param SortAttr attr: sort attribute to provide a heuristic for
+
+ :returns: comparable value for sorting
+ """
+
+ if attr == SortAttr.CATEGORY:
+ return self.manual.category
+ elif attr == SortAttr.NAME:
+ return self.name
+ elif attr == SortAttr.VALUE:
+ return self.value()
+ elif attr == SortAttr.VALUE_TYPE:
+ return self.value_type
+ elif attr == SortAttr.USAGE:
+ return self.manual.usage
+ elif attr == SortAttr.SUMMARY:
+ return self.manual.summary
+ elif attr == SortAttr.DESCRIPTION:
+ return self.manual.description
+ elif attr == SortAttr.MAN_PAGE_ENTRY:
+ return self._index
+ elif attr == SortAttr.IS_SET:
+ return not self.is_set()
+
+
+class ConfigPanel(panel.Panel):
+ """
+ Editor for tor's configuration.
+ """
+
+ def __init__(self, stdscr):
+ panel.Panel.__init__(self, stdscr, 'configuration', 0)
+
+ self._contents = []
+ self._scroller = ui_tools.Scroller(True)
+ self._sort_order = CONFIG['features.config.order']
+ self._show_all = False # show all options, or just the important ones
+
+ cached_manual_path = os.path.join(DATA_DIR, 'manual')
+
+ if os.path.exists(cached_manual_path):
+ manual = stem.manual.Manual.from_cache(cached_manual_path)
+ else:
+ try:
+ manual = stem.manual.Manual.from_man()
+
+ try:
+ manual.save(cached_manual_path)
+ except IOError as exc:
+ log.debug("Unable to cache manual information to '%s'. This is fine, but means starting Nyx takes a little longer than usual: " % (cached_manual_path, exc))
+ except IOError as exc:
+ log.debug("Unable to use 'man tor' to get information about config options (%s), using bundled information instead" % exc)
+ manual = stem.manual.Manual.from_cache()
+
+ try:
+ for line in tor_controller().get_info('config/names').splitlines():
+ # Lines of the form "<option> <type>[ <documentation>]". Documentation
+ # was apparently only in old tor versions like 0.2.1.25.
+
+ if ' ' not in line:
+ continue
+
+ line_comp = line.split()
+ name, value_type = line_comp[0], line_comp[1]
+
+ # skips private and virtual entries if not configured to show them
+
+ if name.startswith('__') and not CONFIG['features.config.state.showPrivateOptions']:
+ continue
+ elif value_type == 'Virtual' and not CONFIG['features.config.state.showVirtualOptions']:
+ continue
+
+ self._contents.append(ConfigEntry(name, value_type, manual))
+
+ self._contents = sorted(self._contents, key = lambda entry: [entry.sort_value(field) for field in self._sort_order])
+ except stem.ControllerError as exc:
+ log.warn('Unable to determine the configuration options tor supports: %s' % exc)
+
+ def show_sort_dialog(self):
+ """
+ Provides the dialog for sorting our configuration options.
+ """
+
+ sort_colors = dict([(attr, CONFIG['attr.config.sort_color'].get(attr, 'white')) for attr in SortAttr])
+ results = nyx.popups.show_sort_dialog('Config Option Ordering:', SortAttr, self._sort_order, sort_colors)
+
+ if results:
+ self._sort_order = results
+ self._contents = sorted(self._contents, key = lambda entry: [entry.sort_value(field) for field in self._sort_order])
+
+ def show_write_dialog(self):
+ """
+ Confirmation dialog for saving tor's configuration.
+ """
+
+ selection, controller = 1, tor_controller()
+ config_text = controller.get_info('config-text', None)
+ config_lines = config_text.splitlines() if config_text else []
+
+ with nyx.popups.popup_window(len(config_lines) + 2) as (popup, width, height):
+ if not popup or height <= 2:
+ return
+
+ while True:
+ height, width = popup.get_preferred_size() # allow us to be resized
+ popup.win.erase()
+
+ for i, full_line in enumerate(config_lines):
+ line = str_tools.crop(full_line, width - 2)
+ option, arg = line.split(' ', 1) if ' ' in line else (line, '')
+
+ popup.addstr(i + 1, 1, option, curses.A_BOLD, 'green')
+ popup.addstr(i + 1, len(option) + 2, arg, curses.A_BOLD, 'cyan')
+
+ x = width - 16
+
+ for i, option in enumerate(['Save', 'Cancel']):
+ x = popup.addstr(height - 2, x, '[')
+ x = popup.addstr(height - 2, x, option, curses.A_BOLD, curses.A_STANDOUT if i == selection else curses.A_NORMAL)
+ x = popup.addstr(height - 2, x, '] ')
+
+ popup.win.box()
+ popup.addstr(0, 0, 'Torrc to save:', curses.A_STANDOUT)
+ popup.win.refresh()
+
+ key = nyx.controller.get_controller().key_input()
+
+ if key.match('left'):
+ selection = max(0, selection - 1)
+ elif key.match('right'):
+ selection = min(1, selection + 1)
+ elif key.is_selection():
+ if selection == 0:
+ try:
+ controller.save_conf()
+ nyx.popups.show_msg('Saved configuration to %s' % controller.get_info('config-file', '<unknown>'), 2)
+ except IOError as exc:
+ nyx.popups.show_msg('Unable to save configuration (%s)' % exc.strerror, 2)
+
+ break
+ elif key.match('esc'):
+ break # esc - cancel
+
+ def handle_key(self, key):
+ if key.is_scroll():
+ page_height = self.get_preferred_size()[0] - DETAILS_HEIGHT
+ is_changed = self._scroller.handle_key(key, self._get_config_options(), page_height)
+
+ if is_changed:
+ self.redraw(True)
+ elif key.is_selection():
+ selection = self._scroller.get_cursor_selection(self._get_config_options())
+ initial_value = selection.value() if selection.is_set() else ''
+ new_value = nyx.popups.input_prompt('%s Value (esc to cancel): ' % selection.name, initial_value)
+
+ if new_value != initial_value:
+ try:
+ if selection.value_type == 'Boolean':
+ # if the value's a boolean then allow for 'true' and 'false' inputs
+
+ if new_value.lower() == 'true':
+ new_value = '1'
+ elif new_value.lower() == 'false':
+ new_value = '0'
+ elif selection.value_type == 'LineList':
+ new_value = new_value.split(',') # set_conf accepts list inputs
+
+ tor_controller().set_conf(selection.name, new_value)
+ self.redraw(True)
+ except Exception as exc:
+ nyx.popups.show_msg('%s (press any key)' % exc)
+ elif key.match('a'):
+ self._show_all = not self._show_all
+ self.redraw(True)
+ elif key.match('s'):
+ self.show_sort_dialog()
+ elif key.match('w'):
+ self.show_write_dialog()
+ else:
+ return False
+
+ return True
+
+ def get_help(self):
+ return [
+ ('up arrow', 'scroll up a line', None),
+ ('down arrow', 'scroll down a line', None),
+ ('page up', 'scroll up a page', None),
+ ('page down', 'scroll down a page', None),
+ ('enter', 'edit configuration option', None),
+ ('w', 'write torrc', None),
+ ('a', 'toggle filtering', None),
+ ('s', 'sort ordering', None),
+ ]
+
+ def draw(self, width, height):
+ contents = self._get_config_options()
+ selection = self._scroller.get_cursor_selection(contents)
+ scroll_location = self._scroller.get_scroll_location(contents, height - DETAILS_HEIGHT)
+ is_scrollbar_visible = len(contents) > height - DETAILS_HEIGHT
+
+ if selection is not None:
+ self._draw_selection_details(selection, width)
+
+ if self.is_title_visible():
+ hidden_msg = "press 'a' to hide most options" if self._show_all else "press 'a' to show all options"
+ self.addstr(0, 0, 'Tor Configuration (%s):' % hidden_msg, curses.A_STANDOUT)
+
+ scroll_offset = 1
+
+ if is_scrollbar_visible:
+ scroll_offset = 3
+ self.add_scroll_bar(scroll_location, scroll_location + height - DETAILS_HEIGHT, len(contents), DETAILS_HEIGHT)
+
+ if selection is not None:
+ self.addch(DETAILS_HEIGHT - 1, 1, curses.ACS_TTEE)
+
+ # Description column can grow up to eighty characters. After that any extra
+ # space goes to the value.
+
+ description_width = max(0, width - scroll_offset - NAME_WIDTH - VALUE_WIDTH - 2)
+
+ if description_width > 80:
+ value_width = VALUE_WIDTH + (description_width - 80)
+ description_width = 80
+ else:
+ value_width = VALUE_WIDTH
+
+ for i, entry in enumerate(contents[scroll_location:]):
+ attr = nyx.util.ui_tools.get_color(CONFIG['attr.config.category_color'].get(entry.manual.category, 'white'))
+ attr |= curses.A_BOLD if entry.is_set() else curses.A_NORMAL
+ attr |= curses.A_STANDOUT if entry == selection else curses.A_NORMAL
+
+ option_label = str_tools.crop(entry.name, NAME_WIDTH).ljust(NAME_WIDTH + 1)
+ value_label = str_tools.crop(entry.value(), value_width).ljust(value_width + 1)
+ summary_label = str_tools.crop(entry.manual.summary, description_width).ljust(description_width)
+
+ self.addstr(DETAILS_HEIGHT + i, scroll_offset, option_label + value_label + summary_label, attr)
+
+ if DETAILS_HEIGHT + i >= height:
+ break
+
+ def _get_config_options(self):
+ return self._contents if self._show_all else filter(lambda entry: stem.manual.is_important(entry.name) or entry.is_set(), self._contents)
+
+ def _draw_selection_details(self, selection, width):
+ """
+ Shows details of the currently selected option.
+ """
+
+ description = 'Description: %s' % (selection.manual.description)
+ attr = ', '.join(('custom' if selection.is_set() else 'default', selection.value_type, 'usage: %s' % selection.manual.usage))
+ selected_color = CONFIG['attr.config.category_color'].get(selection.manual.category, 'white')
+ ui_tools.draw_box(self, 0, 0, width, DETAILS_HEIGHT)
+
+ self.addstr(1, 2, '%s (%s Option)' % (selection.name, selection.manual.category), curses.A_BOLD, selected_color)
+ self.addstr(2, 2, 'Value: %s (%s)' % (selection.value(), str_tools.crop(attr, width - len(selection.value()) - 13)), curses.A_BOLD, selected_color)
+
+ for i in range(DETAILS_HEIGHT - 4):
+ if not description:
+ break # done writing description
+
+ line, description = description.split('\n', 1) if '\n' in description else (description, '')
+
+ if i < DETAILS_HEIGHT - 5:
+ line, remainder = str_tools.crop(line, width - 3, 4, 4, str_tools.Ending.HYPHEN, True)
+ description = ' ' + remainder.strip() + description
+ self.addstr(3 + i, 2, line, curses.A_BOLD, selected_color)
+ else:
+ self.addstr(3 + i, 2, str_tools.crop(line, width - 3, 4, 4), curses.A_BOLD, selected_color)
diff --git a/nyx/panel/connection.py b/nyx/panel/connection.py
new file mode 100644
index 0000000..e28f531
--- /dev/null
+++ b/nyx/panel/connection.py
@@ -0,0 +1,706 @@
+"""
+Listing of the currently established connections tor has made.
+"""
+
+import re
+import time
+import collections
+import curses
+import itertools
+import threading
+
+import nyx.popups
+import nyx.util.tracker
+import nyx.util.ui_tools
+
+from nyx.util import panel, tor_controller, ui_tools
+
+from stem.control import Listener
+from stem.util import datetime_to_unix, conf, connection, enum, str_tools
+
+try:
+ # added in python 3.2
+ from functools import lru_cache
+except ImportError:
+ from stem.util.lru_cache import lru_cache
+
+# height of the detail panel content, not counting top and bottom border
+
+DETAILS_HEIGHT = 7
+
+EXIT_USAGE_WIDTH = 15
+UPDATE_RATE = 5 # rate in seconds at which we refresh
+
+# cached information from our last _update() call
+
+LAST_RETRIEVED_HS_CONF = None
+LAST_RETRIEVED_CIRCUITS = None
+
+# Connection Categories:
+# Inbound Relay connection, coming to us.
+# Outbound Relay connection, leaving us.
+# Exit Outbound relay connection leaving the Tor network.
+# Hidden Connections to a hidden service we're providing.
+# Socks Socks connections for applications using Tor.
+# Circuit Circuits our tor client has created.
+# Directory Fetching tor consensus information.
+# Control Tor controller (nyx, vidalia, etc).
+
+Category = enum.Enum('INBOUND', 'OUTBOUND', 'EXIT', 'HIDDEN', 'SOCKS', 'CIRCUIT', 'DIRECTORY', 'CONTROL')
+SortAttr = enum.Enum('CATEGORY', 'UPTIME', 'IP_ADDRESS', 'PORT', 'FINGERPRINT', 'NICKNAME', 'COUNTRY')
+LineType = enum.Enum('CONNECTION', 'CIRCUIT_HEADER', 'CIRCUIT')
+
+Line = collections.namedtuple('Line', [
+ 'entry',
+ 'line_type',
+ 'connection',
+ 'circuit',
+ 'fingerprint',
+ 'nickname',
+ 'locale',
+])
+
+
+def conf_handler(key, value):
+ if key == 'features.connection.order':
+ return conf.parse_enum_csv(key, value[0], SortAttr, 3)
+
+
+CONFIG = conf.config_dict('nyx', {
+ 'attr.connection.category_color': {},
+ 'attr.connection.sort_color': {},
+ 'features.connection.resolveApps': True,
+ 'features.connection.order': [SortAttr.CATEGORY, SortAttr.IP_ADDRESS, SortAttr.UPTIME],
+ 'features.connection.showIps': True,
+}, conf_handler)
+
+
+class Entry(object):
+ @staticmethod
+ @lru_cache()
+ def from_connection(connection):
+ return ConnectionEntry(connection)
+
+ @staticmethod
+ @lru_cache()
+ def from_circuit(circuit):
+ return CircuitEntry(circuit)
+
+ def get_lines(self):
+ """
+ Provides individual lines of connection information.
+
+ :returns: **list** of **ConnectionLine** concerning this entry
+ """
+
+ raise NotImplementedError('should be implemented by subclasses')
+
+ def get_type(self):
+ """
+ Provides our best guess at the type of connection this is.
+
+ :returns: **Category** for the connection's type
+ """
+
+ raise NotImplementedError('should be implemented by subclasses')
+
+ def is_private(self):
+ """
+ Checks if information about this endpoint should be scrubbed. Relaying
+ etiquette (and wiretapping laws) say these are bad things to look at so
+ DON'T CHANGE THIS UNLESS YOU HAVE A DAMN GOOD REASON!
+
+ :returns: **bool** indicating if connection information is sensive or not
+ """
+
+ raise NotImplementedError('should be implemented by subclasses')
+
+ def sort_value(self, attr):
+ """
+ Provides a heuristic for sorting by a given value.
+
+ :param SortAttr attr: sort attribute to provide a heuristic for
+
+ :returns: comparable value for sorting
+ """
+
+ line = self.get_lines()[0]
+ at_end = 'z' * 20
+
+ if attr == SortAttr.IP_ADDRESS:
+ if self.is_private():
+ return 255 ** 4 # orders at the end
+
+ ip_value = 0
+
+ for octet in line.connection.remote_address.split('.'):
+ ip_value = ip_value * 255 + int(octet)
+
+ return ip_value * 65536 + line.connection.remote_port
+ elif attr == SortAttr.PORT:
+ return line.connection.remote_port
+ elif attr == SortAttr.FINGERPRINT:
+ return line.fingerprint if line.fingerprint else at_end
+ elif attr == SortAttr.NICKNAME:
+ return line.nickname if line.nickname else at_end
+ elif attr == SortAttr.CATEGORY:
+ return Category.index_of(self.get_type())
+ elif attr == SortAttr.UPTIME:
+ return line.connection.start_time
+ elif attr == SortAttr.COUNTRY:
+ return line.locale if (line.locale and not self.is_private()) else at_end
+ else:
+ return ''
+
+
+class ConnectionEntry(Entry):
+ def __init__(self, connection):
+ self._connection = connection
+
+ @lru_cache()
+ def get_lines(self):
+ fingerprint, nickname, locale = None, None, None
+
+ if self.get_type() in (Category.OUTBOUND, Category.CIRCUIT, Category.DIRECTORY, Category.EXIT):
+ fingerprint = nyx.util.tracker.get_consensus_tracker().get_relay_fingerprints(self._connection.remote_address).get(self._connection.remote_port)
+
+ if fingerprint:
+ nickname = nyx.util.tracker.get_consensus_tracker().get_relay_nickname(fingerprint)
+ locale = tor_controller().get_info('ip-to-country/%s' % self._connection.remote_address, None)
+
+ return [Line(self, LineType.CONNECTION, self._connection, None, fingerprint, nickname, locale)]
+
+ @lru_cache()
+ def get_type(self):
+ controller = tor_controller()
+
+ if self._connection.local_port in controller.get_ports(Listener.OR, []):
+ return Category.INBOUND
+ elif self._connection.local_port in controller.get_ports(Listener.DIR, []):
+ return Category.INBOUND
+ elif self._connection.local_port in controller.get_ports(Listener.SOCKS, []):
+ return Category.SOCKS
+ elif self._connection.local_port in controller.get_ports(Listener.CONTROL, []):
+ return Category.CONTROL
+
+ if LAST_RETRIEVED_HS_CONF:
+ for hs_config in LAST_RETRIEVED_HS_CONF.values():
+ if self._connection.remote_port == hs_config['HiddenServicePort']:
+ return Category.HIDDEN
+
+ fingerprint = nyx.util.tracker.get_consensus_tracker().get_relay_fingerprints(self._connection.remote_address).get(self._connection.remote_port)
+
+ if fingerprint and LAST_RETRIEVED_CIRCUITS:
+ for circ in LAST_RETRIEVED_CIRCUITS:
+ if circ.path and len(circ.path) == 1 and circ.path[0][0] == fingerprint and circ.status == 'BUILT':
+ return Category.DIRECTORY # one-hop circuit to retrieve directory information
+ else:
+ # not a known relay, might be an exit connection
+
+ exit_policy = controller.get_exit_policy(None)
+
+ if exit_policy and exit_policy.can_exit_to(self._connection.remote_address, self._connection.remote_port):
+ return Category.EXIT
+
+ return Category.OUTBOUND
+
+ @lru_cache()
+ def is_private(self):
+ if not CONFIG['features.connection.showIps']:
+ return True
+
+ if self.get_type() == Category.INBOUND:
+ controller = tor_controller()
+
+ if controller.is_user_traffic_allowed().inbound:
+ return len(nyx.util.tracker.get_consensus_tracker().get_relay_fingerprints(self._connection.remote_address)) == 0
+ elif self.get_type() == Category.EXIT:
+ # DNS connections exiting us aren't private (since they're hitting our
+ # resolvers). Everything else is.
+
+ return not (self._connection.remote_port == 53 and self._connection.protocol == 'udp')
+
+ return False # for everything else this isn't a concern
+
+
+class CircuitEntry(Entry):
+ def __init__(self, circuit):
+ self._circuit = circuit
+
+ @lru_cache()
+ def get_lines(self):
+ def line(fingerprint, line_type):
+ address, port, nickname, locale = '0.0.0.0', 0, None, None
+ consensus_tracker = nyx.util.tracker.get_consensus_tracker()
+
+ if fingerprint is not None:
+ address, port = consensus_tracker.get_relay_address(fingerprint, ('192.168.0.1', 0))
+ nickname = consensus_tracker.get_relay_nickname(fingerprint)
+ locale = tor_controller().get_info('ip-to-country/%s' % address, None)
+
+ connection = nyx.util.tracker.Connection(datetime_to_unix(self._circuit.created), False, '127.0.0.1', 0, address, port, 'tcp', False)
+ return Line(self, line_type, connection, self._circuit, fingerprint, nickname, locale)
+
+ header_line = line(self._circuit.path[-1][0] if self._circuit.status == 'BUILT' else None, LineType.CIRCUIT_HEADER)
+ return [header_line] + [line(fp, LineType.CIRCUIT) for fp, _ in self._circuit.path]
+
+ def get_type(self):
+ return Category.CIRCUIT
+
+ def is_private(self):
+ return False
+
+
+class ConnectionPanel(panel.Panel, threading.Thread):
+ """
+ Listing of connections tor is making, with information correlated against
+ the current consensus and other data sources.
+ """
+
+ def __init__(self, stdscr):
+ panel.Panel.__init__(self, stdscr, 'connections', 0)
+ threading.Thread.__init__(self)
+ self.setDaemon(True)
+
+ self._scroller = ui_tools.Scroller(True)
+ self._entries = [] # last fetched display entries
+ self._show_details = False # presents the details panel if true
+ self._sort_order = CONFIG['features.connection.order']
+
+ self._last_resource_fetch = -1 # timestamp of the last ConnectionResolver results used
+
+ self._pause_condition = threading.Condition()
+ self._halt = False # terminates thread if true
+
+ # Tracks exiting port and client country statistics
+
+ self._client_locale_usage = {}
+ self._exit_port_usage = {}
+ self._counted_connections = set()
+
+ # If we're a bridge and been running over a day then prepopulates with the
+ # last day's clients.
+
+ bridge_clients = tor_controller().get_info('status/clients-seen', None)
+
+ if bridge_clients:
+ # Response has a couple arguments...
+ # TimeStarted="2011-08-17 15:50:49" CountrySummary=us=16,de=8,uk=8
+
+ country_summary = None
+
+ for line in bridge_clients.split():
+ if line.startswith('CountrySummary='):
+ country_summary = line[15:]
+ break
+
+ if country_summary:
+ for entry in country_summary.split(','):
+ if re.match('^..=[0-9]+$', entry):
+ locale, count = entry.split('=', 1)
+ self._client_locale_usage[locale] = int(count)
+
+ def show_sort_dialog(self):
+ """
+ Provides a dialog for sorting our connections.
+ """
+
+ sort_colors = dict([(attr, CONFIG['attr.connection.sort_color'].get(attr, 'white')) for attr in SortAttr])
+ results = nyx.popups.show_sort_dialog('Connection Ordering:', SortAttr, self._sort_order, sort_colors)
+
+ if results:
+ self._sort_order = results
+ self._entries = sorted(self._entries, key = lambda entry: [entry.sort_value(attr) for attr in self._sort_order])
+
+ def handle_key(self, key):
+ user_traffic_allowed = tor_controller().is_user_traffic_allowed()
+
+ if key.is_scroll():
+ page_height = self.get_preferred_size()[0] - 1
+
+ if self._show_details:
+ page_height -= (DETAILS_HEIGHT + 1)
+
+ lines = list(itertools.chain.from_iterable([entry.get_lines() for entry in self._entries]))
+ is_changed = self._scroller.handle_key(key, lines, page_height)
+
+ if is_changed:
+ self.redraw(True)
+ elif key.is_selection():
+ self._show_details = not self._show_details
+ self.redraw(True)
+ elif key.match('s'):
+ self.show_sort_dialog()
+ elif key.match('r'):
+ connection_tracker = nyx.util.tracker.get_connection_tracker()
+ options = ['auto'] + list(connection.Resolver) + list(nyx.util.tracker.CustomResolver)
+
+ resolver = connection_tracker.get_custom_resolver()
+ selected_index = 0 if resolver is None else options.index(resolver)
+ selection = nyx.popups.show_menu('Connection Resolver:', options, selected_index)
+
+ if selection != -1:
+ connection_tracker.set_custom_resolver(None if selection == 0 else options[selection])
+ elif key.match('d'):
+ self.set_title_visible(False)
+ self.redraw(True)
+ entries = self._entries
+
+ while True:
+ lines = list(itertools.chain.from_iterable([entry.get_lines() for entry in entries]))
+ selection = self._scroller.get_cursor_selection(lines)
+
+ if not selection:
+ break
+
+ def is_close_key(key):
+ return key.is_selection() or key.match('d') or key.match('left') or key.match('right')
+
+ color = CONFIG['attr.connection.category_color'].get(selection.entry.get_type(), 'white')
+ key = nyx.popups.show_descriptor_popup(selection.fingerprint, color, self.max_x, is_close_key)
+
+ if not key or key.is_selection() or key.match('d'):
+ break # closes popup
+ elif key.match('left'):
+ self.handle_key(panel.KeyInput(curses.KEY_UP))
+ elif key.match('right'):
+ self.handle_key(panel.KeyInput(curses.KEY_DOWN))
+
+ self.set_title_visible(True)
+ self.redraw(True)
+ elif key.match('c') and user_traffic_allowed.inbound:
+ nyx.popups.show_count_dialog('Client Locales', self._client_locale_usage)
+ elif key.match('e') and user_traffic_allowed.outbound:
+ counts = {}
+ key_width = max(map(len, self._exit_port_usage.keys()))
+
+ for k, v in self._exit_port_usage.items():
+ usage = connection.port_usage(k)
+
+ if usage:
+ k = k.ljust(key_width + 3) + usage.ljust(EXIT_USAGE_WIDTH)
+
+ counts[k] = v
+
+ nyx.popups.show_count_dialog('Exiting Port Usage', counts)
+ else:
+ return False
+
+ return True
+
+ def run(self):
+ """
+ Keeps connections listing updated, checking for new entries at a set rate.
+ """
+
+ last_ran = -1
+
+ while not self._halt:
+ if self.is_paused() or not tor_controller().is_alive() or (time.time() - last_ran) < UPDATE_RATE:
+ with self._pause_condition:
+ if not self._halt:
+ self._pause_condition.wait(0.2)
+
+ continue # done waiting, try again
+
+ self._update()
+ self.redraw(True)
+
+ # If this is our first run then fill in our fingerprint tracker. This
+ # requires fetching all the router status entries which takes a few
+ # seconds, so best done when we're finished with the rest of the first
+ # iteration to hide the lag.
+
+ if last_ran == -1:
+ nyx.util.tracker.get_consensus_tracker().update(tor_controller().get_network_statuses([]))
+
+ last_ran = time.time()
+
+ def get_help(self):
+ resolver = nyx.util.tracker.get_connection_tracker().get_custom_resolver()
+ user_traffic_allowed = tor_controller().is_user_traffic_allowed()
+
+ options = [
+ ('up arrow', 'scroll up a line', None),
+ ('down arrow', 'scroll down a line', None),
+ ('page up', 'scroll up a page', None),
+ ('page down', 'scroll down a page', None),
+ ('enter', 'show connection details', None),
+ ('d', 'raw consensus descriptor', None),
+ ('s', 'sort ordering', None),
+ ('r', 'connection resolver', 'auto' if resolver is None else resolver),
+ ]
+
+ if user_traffic_allowed.inbound:
+ options.append(('c', 'client locale usage summary', None))
+
+ if user_traffic_allowed.outbound:
+ options.append(('e', 'exit port usage summary', None))
+
+ return options
+
+ def draw(self, width, height):
+ controller = tor_controller()
+ entries = self._entries
+
+ lines = list(itertools.chain.from_iterable([entry.get_lines() for entry in entries]))
+ selected = self._scroller.get_cursor_selection(lines)
+
+ if self.is_paused():
+ current_time = self.get_pause_time()
+ elif not controller.is_alive():
+ current_time = controller.connection_time()
+ else:
+ current_time = time.time()
+
+ is_showing_details = self._show_details and selected
+ details_offset = DETAILS_HEIGHT + 1 if is_showing_details else 0
+
+ is_scrollbar_visible = len(lines) > height - details_offset - 1
+ scroll_offset = 2 if is_scrollbar_visible else 0
+ scroll_location = self._scroller.get_scroll_location(lines, height - details_offset - 1)
+
+ if self.is_title_visible():
+ self._draw_title(entries, self._show_details)
+
+ if is_showing_details:
+ self._draw_details(selected, width, is_scrollbar_visible)
+
+ if is_scrollbar_visible:
+ self.add_scroll_bar(scroll_location, scroll_location + height - details_offset - 1, len(lines), 1 + details_offset)
+
+ for line_number in range(scroll_location, len(lines)):
+ y = line_number + details_offset + 1 - scroll_location
+ self._draw_line(scroll_offset, y, lines[line_number], lines[line_number] == selected, width - scroll_offset, current_time)
+
+ if y >= height:
+ break
+
+ def _draw_title(self, entries, showing_details):
+ """
+ Panel title with the number of connections we presently have.
+ """
+
+ if showing_details:
+ self.addstr(0, 0, 'Connection Details:', curses.A_STANDOUT)
+ elif not entries:
+ self.addstr(0, 0, 'Connections:', curses.A_STANDOUT)
+ else:
+ counts = collections.Counter([entry.get_type() for entry in entries])
+ count_labels = ['%i %s' % (counts[category], category.lower()) for category in Category if counts[category]]
+ self.addstr(0, 0, 'Connections (%s):' % ', '.join(count_labels), curses.A_STANDOUT)
+
+ def _draw_details(self, selected, width, is_scrollbar_visible):
+ """
+ Shows detailed information about the selected connection.
+ """
+
+ attr = (CONFIG['attr.connection.category_color'].get(selected.entry.get_type(), 'white'), curses.A_BOLD)
+
+ if selected.line_type == LineType.CIRCUIT_HEADER and selected.circuit.status != 'BUILT':
+ self.addstr(1, 2, 'Building Circuit...', *attr)
+ else:
+ address = '<scrubbed>' if selected.entry.is_private() else selected.connection.remote_address
+ self.addstr(1, 2, 'address: %s:%s' % (address, selected.connection.remote_port), *attr)
+ self.addstr(2, 2, 'locale: %s' % ('??' if selected.entry.is_private() else (selected.locale if selected.locale else '??')), *attr)
+
+ matches = nyx.util.tracker.get_consensus_tracker().get_relay_fingerprints(selected.connection.remote_address)
+
+ if not matches:
+ self.addstr(3, 2, 'No consensus data found', *attr)
+ elif len(matches) == 1 or selected.connection.remote_port in matches:
+ controller = tor_controller()
+ fingerprint = matches.values()[0] if len(matches) == 1 else matches[selected.connection.remote_port]
+ router_status_entry = controller.get_network_status(fingerprint, None)
+
+ self.addstr(2, 15, 'fingerprint: %s' % fingerprint, *attr)
+
+ if router_status_entry:
+ dir_port_label = 'dirport: %s' % router_status_entry.dir_port if router_status_entry.dir_port else ''
+ self.addstr(3, 2, 'nickname: %-25s orport: %-10s %s' % (router_status_entry.nickname, router_status_entry.or_port, dir_port_label), *attr)
+ self.addstr(4, 2, 'published: %s' % router_status_entry.published.strftime("%H:%M %m/%d/%Y"), *attr)
+ self.addstr(5, 2, 'flags: %s' % ', '.join(router_status_entry.flags), *attr)
+
+ server_descriptor = controller.get_server_descriptor(fingerprint, None)
+
+ if server_descriptor:
+ policy_label = server_descriptor.exit_policy.summary() if server_descriptor.exit_policy else 'unknown'
+ self.addstr(6, 2, 'exit policy: %s' % policy_label, *attr)
+ self.addstr(4, 38, 'os: %-14s version: %s' % (server_descriptor.operating_system, server_descriptor.tor_version), *attr)
+
+ if server_descriptor.contact:
+ self.addstr(7, 2, 'contact: %s' % server_descriptor.contact, *attr)
+ else:
+ self.addstr(3, 2, 'Multiple matches, possible fingerprints are:', *attr)
+
+ for i, port in enumerate(sorted(matches.keys())):
+ is_last_line, remaining_relays = i == 3, len(matches) - i
+
+ if not is_last_line or remaining_relays == 1:
+ self.addstr(4 + i, 2, '%i. or port: %-5s fingerprint: %s' % (i + 1, port, matches[port]), *attr)
+ else:
+ self.addstr(4 + i, 2, '... %i more' % remaining_relays, *attr)
+
+ if is_last_line:
+ break
+
+ # draw the border, with a 'T' pipe if connecting with the scrollbar
+
+ ui_tools.draw_box(self, 0, 0, width, DETAILS_HEIGHT + 2)
+
+ if is_scrollbar_visible:
+ self.addch(DETAILS_HEIGHT + 1, 1, curses.ACS_TTEE)
+
+ def _draw_line(self, x, y, line, is_selected, width, current_time):
+ attr = nyx.util.ui_tools.get_color(CONFIG['attr.connection.category_color'].get(line.entry.get_type(), 'white'))
+ attr |= curses.A_STANDOUT if is_selected else curses.A_NORMAL
+
+ self.addstr(y, x, ' ' * (width - x), attr)
+
+ if line.line_type == LineType.CIRCUIT:
+ if line.circuit.path[-1][0] == line.fingerprint:
+ prefix = (ord(' '), curses.ACS_LLCORNER, curses.ACS_HLINE, ord(' '))
+ else:
+ prefix = (ord(' '), curses.ACS_VLINE, ord(' '), ord(' '))
+
+ for char in prefix:
+ x = self.addch(y, x, char)
+ else:
+ x += 1 # offset from edge
+
+ self._draw_address_column(x, y, line, attr)
+ self._draw_line_details(57, y, line, width - 57 - 20, attr)
+ self._draw_right_column(width - 18, y, line, current_time, attr)
+
+ def _draw_address_column(self, x, y, line, attr):
+ src = tor_controller().get_info('address', line.connection.local_address)
+ src += ':%s' % line.connection.local_port if line.line_type == LineType.CONNECTION else ''
+
+ if line.line_type == LineType.CIRCUIT_HEADER and line.circuit.status != 'BUILT':
+ dst = 'Building...'
+ else:
+ dst = '<scrubbed>' if line.entry.is_private() else line.connection.remote_address
+ dst += ':%s' % line.connection.remote_port
+
+ if line.entry.get_type() == Category.EXIT:
+ purpose = connection.port_usage(line.connection.remote_port)
+
+ if purpose:
+ dst += ' (%s)' % str_tools.crop(purpose, 26 - len(dst) - 3)
+ elif not tor_controller().is_geoip_unavailable() and not line.entry.is_private():
+ dst += ' (%s)' % (line.locale if line.locale else '??')
+
+ if line.entry.get_type() in (Category.INBOUND, Category.SOCKS, Category.CONTROL):
+ dst, src = src, dst
+
+ if line.line_type == LineType.CIRCUIT:
+ self.addstr(y, x, dst, attr)
+ else:
+ self.addstr(y, x, '%-21s --> %-26s' % (src, dst), attr)
+
+ def _draw_line_details(self, x, y, line, width, attr):
+ if line.line_type == LineType.CIRCUIT_HEADER:
+ comp = ['Purpose: %s' % line.circuit.purpose.capitalize(), ', Circuit ID: %s' % line.circuit.id]
+ elif line.entry.get_type() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL):
+ try:
+ port = line.connection.local_port if line.entry.get_type() == Category.HIDDEN else line.connection.remote_port
+ process = nyx.util.tracker.get_port_usage_tracker().fetch(port)
+ comp = ['%s (%s)' % (process.name, process.pid) if process.pid else process.name]
+ except nyx.util.tracker.UnresolvedResult:
+ comp = ['resolving...']
+ except nyx.util.tracker.UnknownApplication:
+ comp = ['UNKNOWN']
+ else:
+ comp = ['%-40s' % (line.fingerprint if line.fingerprint else 'UNKNOWN'), ' ' + (line.nickname if line.nickname else 'UNKNOWN')]
+
+ for entry in comp:
+ if width >= x + len(entry):
+ x = self.addstr(y, x, entry, attr)
+ else:
+ return
+
+ def _draw_right_column(self, x, y, line, current_time, attr):
+ if line.line_type == LineType.CIRCUIT:
+ circ_path = [fp for fp, _ in line.circuit.path]
+ circ_index = circ_path.index(line.fingerprint)
+
+ if circ_index == len(circ_path) - 1:
+ placement_type = 'Exit' if line.circuit.status == 'BUILT' else 'Extending'
+ elif circ_index == 0:
+ placement_type = 'Guard'
+ else:
+ placement_type = 'Middle'
+
+ self.addstr(y, x + 4, '%i / %s' % (circ_index + 1, placement_type), attr)
+ else:
+ x = self.addstr(y, x, '+' if line.connection.is_legacy else ' ', attr)
+ x = self.addstr(y, x, '%5s' % str_tools.time_label(current_time - line.connection.start_time, 1), attr)
+ x = self.addstr(y, x, ' (', attr)
+ x = self.addstr(y, x, line.entry.get_type().upper(), attr | curses.A_BOLD)
+ x = self.addstr(y, x, ')', attr)
+
+ def stop(self):
+ """
+ Halts further resolutions and terminates the thread.
+ """
+
+ with self._pause_condition:
+ self._halt = True
+ self._pause_condition.notifyAll()
+
+ def _update(self):
+ """
+ Fetches the newest resolved connections.
+ """
+
+ global LAST_RETRIEVED_CIRCUITS, LAST_RETRIEVED_HS_CONF
+
+ controller = tor_controller()
+ LAST_RETRIEVED_CIRCUITS = controller.get_circuits([])
+ LAST_RETRIEVED_HS_CONF = controller.get_hidden_service_conf({})
+
+ conn_resolver = nyx.util.tracker.get_connection_tracker()
+ current_resolution_count = conn_resolver.run_counter()
+
+ if not conn_resolver.is_alive():
+ return # if we're not fetching connections then this is a no-op
+ elif current_resolution_count == self._last_resource_fetch:
+ return # no new connections to process
+
+ new_entries = [Entry.from_connection(conn) for conn in conn_resolver.get_value()]
+
+ for circ in LAST_RETRIEVED_CIRCUITS:
+ # Skips established single-hop circuits (these are for directory
+ # fetches, not client circuits)
+
+ if not (circ.status == 'BUILT' and len(circ.path) == 1):
+ new_entries.append(Entry.from_circuit(circ))
+
+ # update stats for client and exit connections
+
+ for entry in new_entries:
+ line = entry.get_lines()[0]
+
+ if entry.is_private() and line.connection not in self._counted_connections:
+ if entry.get_type() == Category.INBOUND and line.locale:
+ self._client_locale_usage[line.locale] = self._client_locale_usage.get(line.locale, 0) + 1
+ elif entry.get_type() == Category.EXIT:
+ self._exit_port_usage[line.connection.remote_port] = self._exit_port_usage.get(line.connection.remote_port, 0) + 1
+
+ self._counted_connections.add(line.connection)
+
+ self._entries = sorted(new_entries, key = lambda entry: [entry.sort_value(attr) for attr in self._sort_order])
+ self._last_resource_fetch = current_resolution_count
+
+ if CONFIG['features.connection.resolveApps']:
+ local_ports, remote_ports = [], []
+
+ for entry in new_entries:
+ line = entry.get_lines()[0]
+
+ if entry.get_type() in (Category.SOCKS, Category.CONTROL):
+ local_ports.append(line.connection.remote_port)
+ elif entry.get_type() == Category.HIDDEN:
+ remote_ports.append(line.connection.local_port)
+
+ nyx.util.tracker.get_port_usage_tracker().query(local_ports, remote_ports)
diff --git a/nyx/panel/graph.py b/nyx/panel/graph.py
new file mode 100644
index 0000000..086d04d
--- /dev/null
+++ b/nyx/panel/graph.py
@@ -0,0 +1,753 @@
+"""
+Graphs of tor related statistics. For example...
+
+Downloaded (0.0 B/sec): Uploaded (0.0 B/sec):
+ 34 30
+ * *
+ ** * * * **
+ * * * ** ** ** *** ** ** ** **
+ ********* ****** ****** ********* ****** ******
+ 0 ************ **************** 0 ************ ****************
+ 25s 50 1m 1.6 2.0 25s 50 1m 1.6 2.0
+"""
+
+import collections
+import copy
+import curses
+import time
+
+import nyx.controller
+import nyx.popups
+import nyx.util.tracker
+
+from stem.control import EventType, Listener
+from stem.util import conf, enum, log, str_tools, system
+from nyx.util import join, msg, panel, tor_controller
+
+GraphStat = enum.Enum(('BANDWIDTH', 'bandwidth'), ('CONNECTIONS', 'connections'), ('SYSTEM_RESOURCES', 'resources'))
+Interval = enum.Enum(('EACH_SECOND', 'each second'), ('FIVE_SECONDS', '5 seconds'), ('THIRTY_SECONDS', '30 seconds'), ('MINUTELY', 'minutely'), ('FIFTEEN_MINUTE', '15 minute'), ('THIRTY_MINUTE', '30 minute'), ('HOURLY', 'hourly'), ('DAILY', 'daily'))
+Bounds = enum.Enum(('GLOBAL_MAX', 'global_max'), ('LOCAL_MAX', 'local_max'), ('TIGHT', 'tight'))
+
+DrawAttributes = collections.namedtuple('DrawAttributes', ('stat', 'subgraph_height', 'subgraph_width', 'interval', 'bounds_type', 'accounting'))
+
+INTERVAL_SECONDS = {
+ Interval.EACH_SECOND: 1,
+ Interval.FIVE_SECONDS: 5,
+ Interval.THIRTY_SECONDS: 30,
+ Interval.MINUTELY: 60,
+ Interval.FIFTEEN_MINUTE: 900,
+ Interval.THIRTY_MINUTE: 1800,
+ Interval.HOURLY: 3600,
+ Interval.DAILY: 86400,
+}
+
+PRIMARY_COLOR, SECONDARY_COLOR = 'green', 'cyan'
+
+ACCOUNTING_RATE = 5
+DEFAULT_CONTENT_HEIGHT = 4 # space needed for labeling above and below the graph
+WIDE_LABELING_GRAPH_COL = 50 # minimum graph columns to use wide spacing for x-axis labels
+COLLAPSE_WIDTH = 135 # width at which to move optional stats from the title to x-axis label
+
+
+def conf_handler(key, value):
+ if key == 'features.graph.height':
+ return max(1, value)
+ elif key == 'features.graph.max_width':
+ return max(1, value)
+ elif key == 'features.graph.type':
+ if value != 'none' and value not in GraphStat:
+ log.warn("'%s' isn't a valid graph type, options are: none, %s" % (CONFIG['features.graph.type'], ', '.join(GraphStat)))
+ return CONFIG['features.graph.type'] # keep the default
+ elif key == 'features.graph.interval':
+ if value not in Interval:
+ log.warn("'%s' isn't a valid graphing interval, options are: %s" % (value, ', '.join(Interval)))
+ return CONFIG['features.graph.interval'] # keep the default
+ elif key == 'features.graph.bound':
+ if value not in Bounds:
+ log.warn("'%s' isn't a valid graph bounds, options are: %s" % (value, ', '.join(Bounds)))
+ return CONFIG['features.graph.bound'] # keep the default
+
+
+CONFIG = conf.config_dict('nyx', {
+ 'attr.hibernate_color': {},
+ 'attr.graph.title': {},
+ 'attr.graph.header.primary': {},
+ 'attr.graph.header.secondary': {},
+ 'features.graph.height': 7,
+ 'features.graph.type': GraphStat.BANDWIDTH,
+ 'features.graph.interval': Interval.EACH_SECOND,
+ 'features.graph.bound': Bounds.LOCAL_MAX,
+ 'features.graph.max_width': 300, # we need some sort of max size so we know how much graph data to retain
+ 'features.panels.show.connection': True,
+ 'features.graph.bw.transferInBytes': False,
+ 'features.graph.bw.accounting.show': True,
+}, conf_handler)
+
+
+class GraphData(object):
+ """
+ Graphable statistical information.
+
+ :var int latest_value: last value we recorded
+ :var int total: sum of all values we've recorded
+ :var int tick: number of events we've processed
+ :var dict values: mapping of intervals to an array of samplings from newest to oldest
+ :var dict max_value: mapping of intervals to the maximum value it has had
+ """
+
+ def __init__(self, clone = None, category = None, is_primary = True):
+ if clone:
+ self.latest_value = clone.latest_value
+ self.total = clone.total
+ self.tick = clone.tick
+ self.values = copy.deepcopy(clone.values)
+ self.max_value = dict(clone.max_value)
+
+ self._category = clone._category
+ self._is_primary = clone._is_primary
+ self._in_process_value = dict(clone._in_process_value)
+ else:
+ self.latest_value = 0
+ self.total = 0
+ self.tick = 0
+ self.values = dict([(i, CONFIG['features.graph.max_width'] * [0]) for i in Interval])
+ self.max_value = dict([(i, 0) for i in Interval])
+
+ self._category = category
+ self._is_primary = is_primary
+ self._in_process_value = dict([(i, 0) for i in Interval])
+
+ def average(self):
+ return self.total / max(1, self.tick)
+
+ def update(self, new_value):
+ self.latest_value = new_value
+ self.total += new_value
+ self.tick += 1
+
+ for interval in Interval:
+ interval_seconds = INTERVAL_SECONDS[interval]
+ self._in_process_value[interval] += new_value
+
+ if self.tick % interval_seconds == 0:
+ new_entry = self._in_process_value[interval] / interval_seconds
+ self.values[interval] = [new_entry] + self.values[interval][:-1]
+ self.max_value[interval] = max(self.max_value[interval], new_entry)
+ self._in_process_value[interval] = 0
+
+ def header(self, width):
+ """
+ Provides the description above a subgraph.
+
+ :param int width: maximum length of the header
+
+ :returns: **str** with our graph header
+ """
+
+ return self._category._header(width, self._is_primary)
+
+ def y_axis_label(self, value):
+ """
+ Provides the label we should display on our y-axis.
+
+ :param int value: value being shown on the y-axis
+
+ :returns: **str** with our y-axis label
+ """
+
+ return self._category._y_axis_label(value, self._is_primary)
+
+
+class GraphCategory(object):
+ """
+ Category for the graph. This maintains two subgraphs, updating them each
+ second with updated stats.
+
+ :var GraphData primary: first subgraph
+ :var GraphData secondary: second subgraph
+ :var float start_time: unix timestamp for when we started
+ """
+
+ def __init__(self, clone = None):
+ if clone:
+ self.primary = GraphData(clone.primary)
+ self.secondary = GraphData(clone.secondary)
+ self.start_time = clone.start_time
+ self._title_stats = list(clone._title_stats)
+ self._primary_header_stats = list(clone._primary_header_stats)
+ self._secondary_header_stats = list(clone._secondary_header_stats)
+ else:
+ self.primary = GraphData(category = self, is_primary = True)
+ self.secondary = GraphData(category = self, is_primary = False)
+ self.start_time = time.time()
+ self._title_stats = []
+ self._primary_header_stats = []
+ self._secondary_header_stats = []
+
+ def stat_type(self):
+ """
+ Provides the GraphStat this graph is for.
+
+ :returns: **GraphStat** of this graph
+ """
+
+ raise NotImplementedError('Should be implemented by subclasses')
+
+ def title(self, width):
+ """
+ Provides a graph title that fits in the given width.
+
+ :param int width: maximum length of the title
+
+ :returns: **str** with our title
+ """
+
+ title = CONFIG['attr.graph.title'].get(self.stat_type(), '')
+ title_stats = join(self._title_stats, ', ', width - len(title) - 4)
+ return '%s (%s):' % (title, title_stats) if title_stats else title + ':'
+
+ def bandwidth_event(self, event):
+ """
+ Called when it's time to process another event. All graphs use tor BW
+ events to keep in sync with each other (this happens once per second).
+ """
+
+ pass
+
+ def _header(self, is_primary, width):
+ if is_primary:
+ header = CONFIG['attr.graph.header.primary'].get(self.stat_type(), '')
+ header_stats = self._primary_header_stats
+ else:
+ header = CONFIG['attr.graph.header.secondary'].get(self.stat_type(), '')
+ header_stats = self._secondary_header_stats
+
+ header_stats = join(header_stats, '', width - len(header) - 4)
+ return '%s (%s):' % (header, header_stats) if header_stats else '%s:' % header
+
+ def _y_axis_label(self, value, is_primary):
+ return str(value)
+
+
+class BandwidthStats(GraphCategory):
+ """
+ Tracks tor's bandwidth usage.
+ """
+
+ def __init__(self, clone = None):
+ GraphCategory.__init__(self, clone)
+
+ if not clone:
+ # fill in past bandwidth information
+
+ controller = tor_controller()
+ bw_entries, is_successful = controller.get_info('bw-event-cache', None), True
+
+ if bw_entries:
+ for entry in bw_entries.split():
+ entry_comp = entry.split(',')
+
+ if len(entry_comp) != 2 or not entry_comp[0].isdigit() or not entry_comp[1].isdigit():
+ log.warn(msg('panel.graphing.bw_event_cache_malformed', response = bw_entries))
+ is_successful = False
+ break
+
+ self.primary.update(int(entry_comp[0]))
+ self.secondary.update(int(entry_comp[1]))
+
+ if is_successful:
+ log.info(msg('panel.graphing.prepopulation_successful', duration = str_tools.time_label(len(bw_entries.split()), is_long = True)))
+
+ read_total = controller.get_info('traffic/read', None)
+ write_total = controller.get_info('traffic/written', None)
+ start_time = system.start_time(controller.get_pid(None))
+
+ if read_total and write_total and start_time:
+ self.primary.total = int(read_total)
+ self.secondary.total = int(write_total)
+ self.start_time = start_time
+
+ def stat_type(self):
+ return GraphStat.BANDWIDTH
+
+ def _y_axis_label(self, value, is_primary):
+ return _size_label(value, 0)
+
+ def bandwidth_event(self, event):
+ self.primary.update(event.read)
+ self.secondary.update(event.written)
+
+ self._primary_header_stats = [
+ '%-14s' % ('%s/sec' % _size_label(self.primary.latest_value)),
+ '- avg: %s/sec' % _size_label(self.primary.total / (time.time() - self.start_time)),
+ ', total: %s' % _size_label(self.primary.total),
+ ]
+
+ self._secondary_header_stats = [
+ '%-14s' % ('%s/sec' % _size_label(self.secondary.latest_value)),
+ '- avg: %s/sec' % _size_label(self.secondary.total / (time.time() - self.start_time)),
+ ', total: %s' % _size_label(self.secondary.total),
+ ]
+
+ controller = tor_controller()
+
+ stats = []
+ bw_rate = controller.get_effective_rate(None)
+ bw_burst = controller.get_effective_rate(None, burst = True)
+
+ if bw_rate and bw_burst:
+ bw_rate_label = _size_label(bw_rate)
+ bw_burst_label = _size_label(bw_burst)
+
+ # if both are using rounded values then strip off the '.0' decimal
+
+ if '.0' in bw_rate_label and '.0' in bw_burst_label:
+ bw_rate_label = bw_rate_label.split('.', 1)[0]
+ bw_burst_label = bw_burst_label.split('.', 1)[0]
+
+ stats.append('limit: %s/s' % bw_rate_label)
+ stats.append('burst: %s/s' % bw_burst_label)
+
+ my_router_status_entry = controller.get_network_status(default = None)
+ measured_bw = getattr(my_router_status_entry, 'bandwidth', None)
+
+ if measured_bw:
+ stats.append('measured: %s/s' % _size_label(measured_bw))
+ else:
+ my_server_descriptor = controller.get_server_descriptor(default = None)
+ observed_bw = getattr(my_server_descriptor, 'observed_bandwidth', None)
+
+ if observed_bw:
+ stats.append('observed: %s/s' % _size_label(observed_bw))
+
+ self._title_stats = stats
+
+
+class ConnectionStats(GraphCategory):
+ """
+ Tracks number of inbound and outbound connections.
+ """
+
+ def stat_type(self):
+ return GraphStat.CONNECTIONS
+
+ def bandwidth_event(self, event):
+ inbound_count, outbound_count = 0, 0
+
+ controller = tor_controller()
+ or_ports = controller.get_ports(Listener.OR, [])
+ dir_ports = controller.get_ports(Listener.DIR, [])
+ control_ports = controller.get_ports(Listener.CONTROL, [])
+
+ for entry in nyx.util.tracker.get_connection_tracker().get_value():
+ if entry.local_port in or_ports or entry.local_port in dir_ports:
+ inbound_count += 1
+ elif entry.local_port in control_ports:
+ pass # control connection
+ else:
+ outbound_count += 1
+
+ self.primary.update(inbound_count)
+ self.secondary.update(outbound_count)
+
+ self._primary_header_stats = [str(self.primary.latest_value), ', avg: %s' % self.primary.average()]
+ self._secondary_header_stats = [str(self.secondary.latest_value), ', avg: %s' % self.secondary.average()]
+
+
+class ResourceStats(GraphCategory):
+ """
+ Tracks cpu and memory usage of the tor process.
+ """
+
+ def stat_type(self):
+ return GraphStat.SYSTEM_RESOURCES
+
+ def _y_axis_label(self, value, is_primary):
+ return '%i%%' % value if is_primary else str_tools.size_label(value)
+
+ def bandwidth_event(self, event):
+ resources = nyx.util.tracker.get_resource_tracker().get_value()
+ self.primary.update(resources.cpu_sample * 100) # decimal percentage to whole numbers
+ self.secondary.update(resources.memory_bytes)
+
+ self._primary_header_stats = ['%0.1f%%' % self.primary.latest_value, ', avg: %0.1f%%' % self.primary.average()]
+ self._secondary_header_stats = [str_tools.size_label(self.secondary.latest_value, 1), ', avg: %s' % str_tools.size_label(self.secondary.average(), 1)]
+
+
+class GraphPanel(panel.Panel):
+ """
+ Panel displaying graphical information of GraphCategory instances.
+ """
+
+ def __init__(self, stdscr):
+ panel.Panel.__init__(self, stdscr, 'graph', 0)
+
+ self._displayed_stat = None if CONFIG['features.graph.type'] == 'none' else CONFIG['features.graph.type']
+ self._update_interval = CONFIG['features.graph.interval']
+ self._bounds = CONFIG['features.graph.bound']
+ self._graph_height = CONFIG['features.graph.height']
+
+ self._accounting_stats = None
+
+ self._stats = {
+ GraphStat.BANDWIDTH: BandwidthStats(),
+ GraphStat.SYSTEM_RESOURCES: ResourceStats(),
+ }
+
+ if CONFIG['features.panels.show.connection']:
+ self._stats[GraphStat.CONNECTIONS] = ConnectionStats()
+ elif self._displayed_stat == GraphStat.CONNECTIONS:
+ log.warn("The connection graph is unavailble when you set 'features.panels.show.connection false'.")
+ self._displayed_stat = GraphStat.BANDWIDTH
+
+ self.set_pause_attr('_stats')
+ self.set_pause_attr('_accounting_stats')
+
+ controller = tor_controller()
+ controller.add_event_listener(self._update_accounting, EventType.BW)
+ controller.add_event_listener(self._update_stats, EventType.BW)
+ controller.add_status_listener(lambda *args: self.redraw(True))
+
+ @property
+ def displayed_stat(self):
+ return self._displayed_stat
+
+ @displayed_stat.setter
+ def displayed_stat(self, value):
+ if value is not None and value not in self._stats.keys():
+ raise ValueError("%s isn't a graphed statistic" % value)
+
+ self._displayed_stat = value
+
+ def stat_options(self):
+ return self._stats.keys()
+
+ @property
+ def update_interval(self):
+ return self._update_interval
+
+ @update_interval.setter
+ def update_interval(self, value):
+ if value not in Interval:
+ raise ValueError("%s isn't a valid graphing update interval" % value)
+
+ self._update_interval = value
+
+ @property
+ def bounds_type(self):
+ return self._bounds
+
+ @bounds_type.setter
+ def bounds_type(self, value):
+ if value not in Bounds:
+ raise ValueError("%s isn't a valid type of bounds" % value)
+
+ self._bounds = value
+
+ def get_height(self):
+ """
+ Provides the height of the content.
+ """
+
+ if not self.displayed_stat:
+ return 0
+
+ height = DEFAULT_CONTENT_HEIGHT + self._graph_height
+
+ if self.displayed_stat == GraphStat.BANDWIDTH and self._accounting_stats:
+ height += 3
+
+ return height
+
+ def set_graph_height(self, new_graph_height):
+ self._graph_height = max(1, new_graph_height)
+
+ def resize_graph(self):
+ """
+ Prompts for user input to resize the graph panel. Options include...
+
+ * down arrow - grow graph
+ * up arrow - shrink graph
+ * enter / space - set size
+ """
+
+ control = nyx.controller.get_controller()
+
+ with panel.CURSES_LOCK:
+ try:
+ while True:
+ msg = 'press the down/up to resize the graph, and enter when done'
+ control.set_msg(msg, curses.A_BOLD, True)
+ curses.cbreak()
+ key = control.key_input()
+
+ if key.match('down'):
+ # don't grow the graph if it's already consuming the whole display
+ # (plus an extra line for the graph/log gap)
+
+ max_height = self.parent.getmaxyx()[0] - self.top
+ current_height = self.get_height()
+
+ if current_height < max_height + 1:
+ self.set_graph_height(self._graph_height + 1)
+ elif key.match('up'):
+ self.set_graph_height(self._graph_height - 1)
+ elif key.is_selection():
+ break
+
+ control.redraw()
+ finally:
+ control.set_msg()
+
+ def handle_key(self, key):
+ if key.match('r'):
+ self.resize_graph()
+ elif key.match('b'):
+ # uses the next boundary type
+ self.bounds_type = Bounds.next(self.bounds_type)
+ self.redraw(True)
+ elif key.match('s'):
+ # provides a menu to pick the graphed stats
+
+ available_stats = sorted(self.stat_options())
+ options = ['None'] + [stat.capitalize() for stat in available_stats]
+ initial_selection = available_stats.index(self.displayed_stat) + 1 if self.displayed_stat else 0
+
+ selection = nyx.popups.show_menu('Graphed Stats:', options, initial_selection)
+
+ # applies new setting
+
+ if selection == 0:
+ self.displayed_stat = None
+ elif selection != -1:
+ self.displayed_stat = available_stats[selection - 1]
+ elif key.match('i'):
+ # provides menu to pick graph panel update interval
+
+ selection = nyx.popups.show_menu('Update Interval:', list(Interval), list(Interval).index(self.update_interval))
+
+ if selection != -1:
+ self.update_interval = list(Interval)[selection]
+ else:
+ return False
+
+ return True
+
+ def get_help(self):
+ return [
+ ('r', 'resize graph', None),
+ ('s', 'graphed stats', self.displayed_stat if self.displayed_stat else 'none'),
+ ('b', 'graph bounds', self.bounds_type.replace('_', ' ')),
+ ('i', 'graph update interval', self.update_interval),
+ ]
+
+ def draw(self, width, height):
+ if not self.displayed_stat:
+ return
+
+ stat = self.get_attr('_stats')[self.displayed_stat]
+
+ attr = DrawAttributes(
+ stat = type(stat)(stat), # clone the GraphCategory
+ subgraph_height = self._graph_height + 2, # graph rows + header + x-axis label
+ subgraph_width = min(width / 2, CONFIG['features.graph.max_width']),
+ interval = self.update_interval,
+ bounds_type = self.bounds_type,
+ accounting = self.get_attr('_accounting_stats'),
+ )
+
+ if self.is_title_visible():
+ self.addstr(0, 0, attr.stat.title(width), curses.A_STANDOUT)
+
+ self._draw_subgraph(attr, attr.stat.primary, 0, PRIMARY_COLOR)
+ self._draw_subgraph(attr, attr.stat.secondary, attr.subgraph_width, SECONDARY_COLOR)
+
+ if attr.stat.stat_type() == GraphStat.BANDWIDTH:
+ if width <= COLLAPSE_WIDTH:
+ self._draw_bandwidth_stats(attr, width)
+
+ if attr.accounting:
+ self._draw_accounting_stats(attr)
+
+ def _draw_subgraph(self, attr, data, x, color):
+ # Concering our subgraph colums, the y-axis label can be at most six
+ # characters, with two spaces of padding on either side of the graph.
+ # Starting with the smallest size, then possibly raise it after determing
+ # the y_axis_labels.
+
+ subgraph_columns = attr.subgraph_width - 8
+ min_bound, max_bound = self._get_graph_bounds(attr, data, subgraph_columns)
+
+ x_axis_labels = self._get_x_axis_labels(attr, subgraph_columns)
+ y_axis_labels = self._get_y_axis_labels(attr, data, min_bound, max_bound)
+ subgraph_columns = max(subgraph_columns, attr.subgraph_width - max([len(label) for label in y_axis_labels.values()]) - 2)
+ axis_offset = max([len(label) for label in y_axis_labels.values()])
+
+ self.addstr(1, x, data.header(attr.subgraph_width), curses.A_BOLD, color)
+
+ for x_offset, label in x_axis_labels.items():
+ self.addstr(attr.subgraph_height, x + x_offset + axis_offset, label, color)
+
+ for y, label in y_axis_labels.items():
+ self.addstr(y, x, label, color)
+
+ for col in range(subgraph_columns):
+ column_count = int(data.values[attr.interval][col]) - min_bound
+ column_height = int(min(attr.subgraph_height - 2, (attr.subgraph_height - 2) * column_count / (max(1, max_bound) - min_bound)))
+
+ for row in range(column_height):
+ self.addstr(attr.subgraph_height - 1 - row, x + col + axis_offset + 1, ' ', curses.A_STANDOUT, color)
+
+ def _get_graph_bounds(self, attr, data, subgraph_columns):
+ """
+ Provides the range the graph shows (ie, its minimum and maximum value).
+ """
+
+ min_bound, max_bound = 0, 0
+ values = data.values[attr.interval][:subgraph_columns]
+
+ if attr.bounds_type == Bounds.GLOBAL_MAX:
+ max_bound = data.max_value[attr.interval]
+ elif subgraph_columns > 0:
+ max_bound = max(values) # local maxima
+
+ if attr.bounds_type == Bounds.TIGHT and subgraph_columns > 0:
+ min_bound = min(values)
+
+ # if the max = min pick zero so we still display something
+
+ if min_bound == max_bound:
+ min_bound = 0
+
+ return min_bound, max_bound
+
+ def _get_y_axis_labels(self, attr, data, min_bound, max_bound):
+ """
+ Provides the labels for the y-axis. This is a mapping of the position it
+ should be drawn at to its text.
+ """
+
+ y_axis_labels = {
+ 2: data.y_axis_label(max_bound),
+ attr.subgraph_height - 1: data.y_axis_label(min_bound),
+ }
+
+ ticks = (attr.subgraph_height - 5) / 2
+
+ for i in range(ticks):
+ row = attr.subgraph_height - (2 * i) - 5
+
+ if attr.subgraph_height % 2 == 0 and i >= (ticks / 2):
+ row -= 1 # make extra gap be in the middle when we're an even size
+
+ val = (max_bound - min_bound) * (attr.subgraph_height - row - 3) / (attr.subgraph_height - 3)
+
+ if val not in (min_bound, max_bound):
+ y_axis_labels[row + 2] = data.y_axis_label(val)
+
+ return y_axis_labels
+
+ def _get_x_axis_labels(self, attr, subgraph_columns):
+ """
+ Provides the labels for the x-axis. We include the units for only its first
+ value, then bump the precision for subsequent units. For example...
+
+ 10s, 20, 30, 40, 50, 1m, 1.1, 1.3, 1.5
+ """
+
+ x_axis_labels = {}
+
+ interval_sec = INTERVAL_SECONDS[attr.interval]
+ interval_spacing = 10 if subgraph_columns >= WIDE_LABELING_GRAPH_COL else 5
+ units_label, decimal_precision = None, 0
+
+ for i in range((subgraph_columns - 4) / interval_spacing):
+ x = (i + 1) * interval_spacing
+ time_label = str_tools.time_label(x * interval_sec, decimal_precision)
+
+ if not units_label:
+ units_label = time_label[-1]
+ elif units_label != time_label[-1]:
+ # upped scale so also up precision of future measurements
+ units_label = time_label[-1]
+ decimal_precision += 1
+ else:
+ # if constrained on space then strips labeling since already provided
+ time_label = time_label[:-1]
+
+ x_axis_labels[x] = time_label
+
+ return x_axis_labels
+
+ def _draw_bandwidth_stats(self, attr, width):
+ """
+ Replaces the x-axis labeling with bandwidth stats. This is done on small
+ screens since this information otherwise wouldn't fit.
+ """
+
+ labeling_line = DEFAULT_CONTENT_HEIGHT + attr.subgraph_height - 4
+ self.addstr(labeling_line, 0, ' ' * width) # clear line
+
+ runtime = time.time() - attr.stat.start_time
+ primary_footer = 'total: %s, avg: %s/sec' % (_size_label(attr.stat.primary.total), _size_label(attr.stat.primary.total / runtime))
+ secondary_footer = 'total: %s, avg: %s/sec' % (_size_label(attr.stat.secondary.total), _size_label(attr.stat.secondary.total / runtime))
+
+ self.addstr(labeling_line, 1, primary_footer, PRIMARY_COLOR)
+ self.addstr(labeling_line, attr.subgraph_width + 1, secondary_footer, SECONDARY_COLOR)
+
+ def _draw_accounting_stats(self, attr):
+ y = DEFAULT_CONTENT_HEIGHT + attr.subgraph_height - 2
+
+ if tor_controller().is_alive():
+ hibernate_color = CONFIG['attr.hibernate_color'].get(attr.accounting.status, 'red')
+
+ x = self.addstr(y, 0, 'Accounting (', curses.A_BOLD)
+ x = self.addstr(y, x, attr.accounting.status, curses.A_BOLD, hibernate_color)
+ x = self.addstr(y, x, ')', curses.A_BOLD)
+
+ self.addstr(y, 35, 'Time to reset: %s' % str_tools.short_time_label(attr.accounting.time_until_reset))
+
+ self.addstr(y + 1, 2, '%s / %s' % (attr.accounting.read_bytes, attr.accounting.read_limit), PRIMARY_COLOR)
+ self.addstr(y + 1, 37, '%s / %s' % (attr.accounting.written_bytes, attr.accounting.write_limit), SECONDARY_COLOR)
+ else:
+ self.addstr(y, 0, 'Accounting:', curses.A_BOLD)
+ self.addstr(y, 12, 'Connection Closed...')
+
+ def copy_attr(self, attr):
+ if attr == '_stats':
+ return dict([(key, type(self._stats[key])(self._stats[key])) for key in self._stats])
+ else:
+ return panel.Panel.copy_attr(self, attr)
+
+ def _update_accounting(self, event):
+ if not CONFIG['features.graph.bw.accounting.show']:
+ self._accounting_stats = None
+ elif not self._accounting_stats or time.time() - self._accounting_stats.retrieved >= ACCOUNTING_RATE:
+ old_accounting_stats = self._accounting_stats
+ self._accounting_stats = tor_controller().get_accounting_stats(None)
+
+ # if we either added or removed accounting info then redraw the whole
+ # screen to account for resizing
+
+ if bool(old_accounting_stats) != bool(self._accounting_stats):
+ nyx.controller.get_controller().redraw()
+
+ def _update_stats(self, event):
+ for stat in self._stats.values():
+ stat.bandwidth_event(event)
+
+ if self.displayed_stat:
+ param = self.get_attr('_stats')[self.displayed_stat]
+ update_rate = INTERVAL_SECONDS[self.update_interval]
+
+ if param.primary.tick % update_rate == 0:
+ self.redraw(True)
+
+
+def _size_label(byte_count, decimal = 1):
+ """
+ Alias for str_tools.size_label() that accounts for if the user prefers bits
+ or bytes.
+ """
+
+ return str_tools.size_label(byte_count, decimal, is_bytes = CONFIG['features.graph.bw.transferInBytes'])
diff --git a/nyx/panel/header.py b/nyx/panel/header.py
new file mode 100644
index 0000000..0d6591a
--- /dev/null
+++ b/nyx/panel/header.py
@@ -0,0 +1,496 @@
+"""
+Top panel for every page, containing basic system and tor related information.
+This expands the information it presents to two columns if there's room
+available.
+"""
+
+import collections
+import os
+import time
+import curses
+import threading
+
+import stem
+
+import nyx.controller
+import nyx.popups
+
+from stem.control import Listener, State
+from stem.util import conf, log, proc, str_tools, system
+from nyx.util import msg, tor_controller, panel, tracker
+
+MIN_DUAL_COL_WIDTH = 141 # minimum width where we'll show two columns
+SHOW_FD_THRESHOLD = 60 # show file descriptor usage if usage is over this percentage
+UPDATE_RATE = 5 # rate in seconds at which we refresh
+
+CONFIG = conf.config_dict('nyx', {
+ 'attr.flag_colors': {},
+ 'attr.version_status_colors': {},
+})
+
+
+class HeaderPanel(panel.Panel, threading.Thread):
+ """
+ Top area containing tor settings and system information.
+ """
+
+ def __init__(self, stdscr):
+ panel.Panel.__init__(self, stdscr, 'header', 0)
+ threading.Thread.__init__(self)
+ self.setDaemon(True)
+
+ self._vals = get_sampling()
+
+ self._pause_condition = threading.Condition()
+ self._halt = False # terminates thread if true
+ self._reported_inactive = False
+
+ tor_controller().add_status_listener(self.reset_listener)
+
+ def is_wide(self, width = None):
+ """
+ True if we should show two columns of information, False otherwise.
+ """
+
+ if width is None:
+ width = self.get_parent().getmaxyx()[1]
+
+ return width >= MIN_DUAL_COL_WIDTH
+
+ def get_height(self):
+ """
+ Provides the height of the content, which is dynamically determined by the
+ panel's maximum width.
+ """
+
+ if self._vals.is_relay:
+ return 4 if self.is_wide() else 6
+ else:
+ return 3 if self.is_wide() else 4
+
+ def send_newnym(self):
+ """
+ Requests a new identity and provides a visual queue.
+ """
+
+ controller = tor_controller()
+
+ if not controller.is_newnym_available():
+ return
+
+ controller.signal(stem.Signal.NEWNYM)
+
+ # If we're wide then the newnym label in this panel will give an
+ # indication that the signal was sent. Otherwise use a msg.
+
+ if not self.is_wide():
+ nyx.popups.show_msg('Requesting a new identity', 1)
+
+ def handle_key(self, key):
+ if key.match('n'):
+ self.send_newnym()
+ elif key.match('r') and not self._vals.is_connected:
+ # TODO: This is borked. Not quite sure why but our attempt to call
+ # PROTOCOLINFO fails with a socket error, followed by completely freezing
+ # nyx. This is exposing two bugs...
+ #
+ # * This should be working. That's a stem issue.
+ # * Our interface shouldn't be locking up. That's an nyx issue.
+
+ return True
+
+ controller = tor_controller()
+
+ try:
+ controller.connect()
+
+ try:
+ controller.authenticate() # TODO: should account for our chroot
+ except stem.connection.MissingPassword:
+ password = nyx.popups.input_prompt('Controller Password: ')
+
+ if password:
+ controller.authenticate(password)
+
+ log.notice("Reconnected to Tor's control port")
+ nyx.popups.show_msg('Tor reconnected', 1)
+ except Exception as exc:
+ nyx.popups.show_msg('Unable to reconnect (%s)' % exc, 3)
+ controller.close()
+ else:
+ return False
+
+ return True
+
+ def draw(self, width, height):
+ vals = self._vals # local reference to avoid concurrency concerns
+ is_wide = self.is_wide(width)
+
+ # space available for content
+
+ left_width = max(width / 2, 77) if is_wide else width
+ right_width = width - left_width
+
+ self._draw_platform_section(0, 0, left_width, vals)
+
+ if vals.is_connected:
+ self._draw_ports_section(0, 1, left_width, vals)
+ else:
+ self._draw_disconnected(0, 1, left_width, vals)
+
+ if is_wide:
+ self._draw_resource_usage(left_width, 0, right_width, vals)
+
+ if vals.is_relay:
+ self._draw_fingerprint_and_fd_usage(left_width, 1, right_width, vals)
+ self._draw_flags(0, 2, left_width, vals)
+ self._draw_exit_policy(left_width, 2, right_width, vals)
+ elif vals.is_connected:
+ self._draw_newnym_option(left_width, 1, right_width, vals)
+ else:
+ self._draw_resource_usage(0, 2, left_width, vals)
+
+ if vals.is_relay:
+ self._draw_fingerprint_and_fd_usage(0, 3, left_width, vals)
+ self._draw_flags(0, 4, left_width, vals)
+
+ def _draw_platform_section(self, x, y, width, vals):
+ """
+ Section providing the user's hostname, platform, and version information...
+
+ nyx - odin (Linux 3.5.0-52-generic) Tor 0.2.5.1-alpha-dev (unrecommended)
+ |------ platform (40 characters) ------| |----------- tor version -----------|
+ """
+
+ initial_x, space_left = x, min(width, 40)
+
+ x = self.addstr(y, x, vals.format('nyx - {hostname}', space_left))
+ space_left -= x - initial_x
+
+ if space_left >= 10:
+ self.addstr(y, x, ' (%s)' % vals.format('{platform}', space_left - 3))
+
+ x, space_left = initial_x + 43, width - 43
+
+ if vals.version != 'Unknown' and space_left >= 10:
+ x = self.addstr(y, x, vals.format('Tor {version}', space_left))
+ space_left -= x - 43 - initial_x
+
+ if space_left >= 7 + len(vals.version_status):
+ version_color = CONFIG['attr.version_status_colors'].get(vals.version_status, 'white')
+
+ x = self.addstr(y, x, ' (')
+ x = self.addstr(y, x, vals.version_status, version_color)
+ self.addstr(y, x, ')')
+
+ def _draw_ports_section(self, x, y, width, vals):
+ """
+ Section providing our nickname, address, and port information...
+
+ Unnamed - 0.0.0.0:7000, Control Port (cookie): 9051
+ """
+
+ if not vals.is_relay:
+ x = self.addstr(y, x, 'Relaying Disabled', 'cyan')
+ else:
+ x = self.addstr(y, x, vals.format('{nickname} - {address}:{or_port}'))
+
+ if vals.dir_port != '0':
+ x = self.addstr(y, x, vals.format(', Dir Port: {dir_port}'))
+
+ if vals.control_port:
+ if width >= x + 19 + len(vals.control_port) + len(vals.auth_type):
+ auth_color = 'red' if vals.auth_type == 'open' else 'green'
+
+ x = self.addstr(y, x, ', Control Port (')
+ x = self.addstr(y, x, vals.auth_type, auth_color)
+ self.addstr(y, x, vals.format('): {control_port}'))
+ else:
+ self.addstr(y, x, vals.format(', Control Port: {control_port}'))
+ elif vals.socket_path:
+ self.addstr(y, x, vals.format(', Control Socket: {socket_path}'))
+
+ def _draw_disconnected(self, x, y, width, vals):
+ """
+ Message indicating that tor is disconnected...
+
+ Tor Disconnected (15:21 07/13/2014, press r to reconnect)
+ """
+
+ x = self.addstr(y, x, 'Tor Disconnected', curses.A_BOLD, 'red')
+ last_heartbeat = time.strftime('%H:%M %m/%d/%Y', time.localtime(vals.last_heartbeat))
+ self.addstr(y, x, ' (%s, press r to reconnect)' % last_heartbeat)
+
+ def _draw_resource_usage(self, x, y, width, vals):
+ """
+ System resource usage of the tor process...
+
+ cpu: 0.0% tor, 1.0% nyx mem: 0 (0.0%) pid: 16329 uptime: 12-20:42:07
+ """
+
+ if vals.start_time:
+ if not vals.is_connected:
+ now = vals.connection_time
+ elif self.is_paused():
+ now = self.get_pause_time()
+ else:
+ now = time.time()
+
+ uptime = str_tools.short_time_label(now - vals.start_time)
+ else:
+ uptime = ''
+
+ sys_fields = (
+ (0, vals.format('cpu: {tor_cpu}% tor, {nyx_cpu}% nyx')),
+ (27, vals.format('mem: {memory} ({memory_percent}%)')),
+ (47, vals.format('pid: {pid}')),
+ (59, 'uptime: %s' % uptime),
+ )
+
+ for (start, label) in sys_fields:
+ if width >= start + len(label):
+ self.addstr(y, x + start, label)
+ else:
+ break
+
+ def _draw_fingerprint_and_fd_usage(self, x, y, width, vals):
+ """
+ Presents our fingerprint, and our file descriptor usage if we're running
+ out...
+
+ fingerprint: 1A94D1A794FCB2F8B6CBC179EF8FDD4008A98D3B, file desc: 900 / 1000 (90%)
+ """
+
+ initial_x, space_left = x, width
+
+ x = self.addstr(y, x, vals.format('fingerprint: {fingerprint}', width))
+ space_left -= x - initial_x
+
+ if space_left >= 30 and vals.fd_used and vals.fd_limit != -1:
+ fd_percent = 100 * vals.fd_used / vals.fd_limit
+
+ if fd_percent >= SHOW_FD_THRESHOLD:
+ if fd_percent >= 95:
+ percentage_format = (curses.A_BOLD, 'red')
+ elif fd_percent >= 90:
+ percentage_format = ('red',)
+ elif fd_percent >= 60:
+ percentage_format = ('yellow',)
+ else:
+ percentage_format = ()
+
+ x = self.addstr(y, x, ', file descriptors' if space_left >= 37 else ', file desc')
+ x = self.addstr(y, x, vals.format(': {fd_used} / {fd_limit} ('))
+ x = self.addstr(y, x, '%i%%' % fd_percent, *percentage_format)
+ self.addstr(y, x, ')')
+
+ def _draw_flags(self, x, y, width, vals):
+ """
+ Presents flags held by our relay...
+
+ flags: Running, Valid
+ """
+
+ x = self.addstr(y, x, 'flags: ')
+
+ if vals.flags:
+ for i, flag in enumerate(vals.flags):
+ flag_color = CONFIG['attr.flag_colors'].get(flag, 'white')
+ x = self.addstr(y, x, flag, curses.A_BOLD, flag_color)
+
+ if i < len(vals.flags) - 1:
+ x = self.addstr(y, x, ', ')
+ else:
+ self.addstr(y, x, 'none', curses.A_BOLD, 'cyan')
+
+ def _draw_exit_policy(self, x, y, width, vals):
+ """
+ Presents our exit policy...
+
+ exit policy: reject *:*
+ """
+
+ x = self.addstr(y, x, 'exit policy: ')
+
+ if not vals.exit_policy:
+ return
+
+ rules = list(vals.exit_policy.strip_private().strip_default())
+
+ for i, rule in enumerate(rules):
+ policy_color = 'green' if rule.is_accept else 'red'
+ x = self.addstr(y, x, str(rule), curses.A_BOLD, policy_color)
+
+ if i < len(rules) - 1:
+ x = self.addstr(y, x, ', ')
+
+ if vals.exit_policy.has_default():
+ if rules:
+ x = self.addstr(y, x, ', ')
+
+ self.addstr(y, x, '<default>', curses.A_BOLD, 'cyan')
+
+ def _draw_newnym_option(self, x, y, width, vals):
+ """
+ Provide a notice for requiesting a new identity, and time until it's next
+ available if in the process of building circuits.
+ """
+
+ if vals.newnym_wait == 0:
+ self.addstr(y, x, "press 'n' for a new identity")
+ else:
+ plural = 's' if vals.newnym_wait > 1 else ''
+ self.addstr(y, x, 'building circuits, available again in %i second%s' % (vals.newnym_wait, plural))
+
+ def run(self):
+ """
+ Keeps stats updated, checking for new information at a set rate.
+ """
+
+ last_ran = -1
+
+ while not self._halt:
+ if self.is_paused() or not self._vals.is_connected or (time.time() - last_ran) < UPDATE_RATE:
+ with self._pause_condition:
+ if not self._halt:
+ self._pause_condition.wait(0.2)
+
+ continue # done waiting, try again
+
+ self._update()
+ last_ran = time.time()
+
+ def stop(self):
+ """
+ Halts further resolutions and terminates the thread.
+ """
+
+ with self._pause_condition:
+ self._halt = True
+ self._pause_condition.notifyAll()
+
+ def reset_listener(self, controller, event_type, _):
+ self._update()
+
+ if event_type == State.CLOSED:
+ log.notice('Tor control port closed')
+
+ def _update(self):
+ previous_height = self.get_height()
+ self._vals = get_sampling(self._vals)
+
+ if self._vals.fd_used and self._vals.fd_limit != -1:
+ fd_percent = 100 * self._vals.fd_used / self._vals.fd_limit
+
+ if fd_percent >= 90:
+ log_msg = msg('panel.header.fd_used_at_ninety_percent', percentage = fd_percent)
+ log.log_once('fd_used_at_ninety_percent', log.WARN, log_msg)
+ log.DEDUPLICATION_MESSAGE_IDS.add('fd_used_at_sixty_percent')
+ elif fd_percent >= 60:
+ log_msg = msg('panel.header.fd_used_at_sixty_percent', percentage = fd_percent)
+ log.log_once('fd_used_at_sixty_percent', log.NOTICE, log_msg)
+
+ if self._vals.is_connected:
+ if not self._reported_inactive and (time.time() - self._vals.last_heartbeat) >= 10:
+ self._reported_inactive = True
+ log.notice('Relay unresponsive (last heartbeat: %s)' % time.ctime(self._vals.last_heartbeat))
+ elif self._reported_inactive and (time.time() - self._vals.last_heartbeat) < 10:
+ self._reported_inactive = False
+ log.notice('Relay resumed')
+
+ if previous_height != self.get_height():
+ # We're toggling between being a relay and client, causing the height
+ # of this panel to change. Redraw all content so we don't get
+ # overlapping content.
+
+ nyx.controller.get_controller().redraw()
+ else:
+ self.redraw(True) # just need to redraw ourselves
+
+
+def get_sampling(last_sampling = None):
+ controller = tor_controller()
+ retrieved = time.time()
+
+ pid = controller.get_pid('')
+ tor_resources = tracker.get_resource_tracker().get_value()
+ nyx_total_cpu_time = sum(os.times()[:3])
+
+ or_listeners = controller.get_listeners(Listener.OR, [])
+ control_listeners = controller.get_listeners(Listener.CONTROL, [])
+
+ if controller.get_conf('HashedControlPassword', None):
+ auth_type = 'password'
+ elif controller.get_conf('CookieAuthentication', None) == '1':
+ auth_type = 'cookie'
+ else:
+ auth_type = 'open'
+
+ try:
+ fd_used = proc.file_descriptors_used(pid)
+ except IOError:
+ fd_used = None
+
+ if last_sampling:
+ nyx_cpu_delta = nyx_total_cpu_time - last_sampling.nyx_total_cpu_time
+ nyx_time_delta = retrieved - last_sampling.retrieved
+
+ python_cpu_time = nyx_cpu_delta / nyx_time_delta
+ sys_call_cpu_time = 0.0 # TODO: add a wrapper around call() to get this
+
+ nyx_cpu = python_cpu_time + sys_call_cpu_time
+ else:
+ nyx_cpu = 0.0
+
+ attr = {
+ 'retrieved': retrieved,
+ 'is_connected': controller.is_alive(),
+ 'connection_time': controller.connection_time(),
+ 'last_heartbeat': controller.get_latest_heartbeat(),
+
+ 'fingerprint': controller.get_info('fingerprint', 'Unknown'),
+ 'nickname': controller.get_conf('Nickname', ''),
+ 'newnym_wait': controller.get_newnym_wait(),
+ 'exit_policy': controller.get_exit_policy(None),
+ 'flags': getattr(controller.get_network_status(default = None), 'flags', []),
+
+ 'version': str(controller.get_version('Unknown')).split()[0],
+ 'version_status': controller.get_info('status/version/current', 'Unknown'),
+
+ 'address': or_listeners[0][0] if (or_listeners and or_listeners[0][0] != '0.0.0.0') else controller.get_info('address', 'Unknown'),
+ 'or_port': or_listeners[0][1] if or_listeners else '',
+ 'dir_port': controller.get_conf('DirPort', '0'),
+ 'control_port': str(control_listeners[0][1]) if control_listeners else None,
+ 'socket_path': controller.get_conf('ControlSocket', None),
+ 'is_relay': bool(or_listeners),
+
+ 'auth_type': auth_type,
+ 'pid': pid,
+ 'start_time': system.start_time(pid),
+ 'fd_limit': int(controller.get_info('process/descriptor-limit', '-1')),
+ 'fd_used': fd_used,
+
+ 'nyx_total_cpu_time': nyx_total_cpu_time,
+ 'tor_cpu': '%0.1f' % (100 * tor_resources.cpu_sample),
+ 'nyx_cpu': nyx_cpu,
+ 'memory': str_tools.size_label(tor_resources.memory_bytes) if tor_resources.memory_bytes > 0 else 0,
+ 'memory_percent': '%0.1f' % (100 * tor_resources.memory_percent),
+
+ 'hostname': os.uname()[1],
+ 'platform': '%s %s' % (os.uname()[0], os.uname()[2]), # [platform name] [version]
+ }
+
+ class Sampling(collections.namedtuple('Sampling', attr.keys())):
+ def __init__(self, **attr):
+ super(Sampling, self).__init__(**attr)
+ self._attr = attr
+
+ def format(self, message, crop_width = None):
+ formatted_msg = message.format(**self._attr)
+
+ if crop_width:
+ formatted_msg = str_tools.crop(formatted_msg, crop_width)
+
+ return formatted_msg
+
+ return Sampling(**attr)
diff --git a/nyx/panel/log.py b/nyx/panel/log.py
new file mode 100644
index 0000000..708a679
--- /dev/null
+++ b/nyx/panel/log.py
@@ -0,0 +1,454 @@
+"""
+Panel providing a chronological log of events its been configured to listen
+for. This provides prepopulation from the log file and supports filtering by
+regular expressions.
+"""
+
+import os
+import time
+import curses
+import threading
+
+import stem.response.events
+
+import nyx.arguments
+import nyx.popups
+import nyx.util.log
+
+from nyx.util import join, panel, tor_controller, ui_tools
+from stem.util import conf, log
+
+
+def conf_handler(key, value):
+ if key == 'features.log.prepopulateReadLimit':
+ return max(0, value)
+ elif key == 'features.log.maxRefreshRate':
+ return max(10, value)
+ elif key == 'cache.log_panel.size':
+ return max(1000, value)
+
+
+CONFIG = conf.config_dict('nyx', {
+ 'attr.log_color': {},
+ 'cache.log_panel.size': 1000,
+ 'features.logFile': '',
+ 'features.log.showDuplicateEntries': False,
+ 'features.log.prepopulate': True,
+ 'features.log.prepopulateReadLimit': 5000,
+ 'features.log.maxRefreshRate': 300,
+ 'features.log.regex': [],
+ 'msg.misc.event_types': '',
+ 'startup.events': 'N3',
+}, conf_handler)
+
+# The height of the drawn content is estimated based on the last time we redrew
+# the panel. It's chiefly used for scrolling and the bar indicating its
+# position. Letting the estimate be too inaccurate results in a display bug, so
+# redraws the display if it's off by this threshold.
+
+CONTENT_HEIGHT_REDRAW_THRESHOLD = 3
+
+# Log buffer so we start collecting stem/nyx events when imported. This is used
+# to make our LogPanel when curses initializes.
+
+stem_logger = log.get_logger()
+NYX_LOGGER = log.LogBuffer(log.Runlevel.DEBUG, yield_records = True)
+stem_logger.addHandler(NYX_LOGGER)
+
+
+class LogPanel(panel.Panel, threading.Thread):
+ """
+ Listens for and displays tor, nyx, and stem events. This prepopulates
+ from tor's log file if it exists.
+ """
+
+ def __init__(self, stdscr):
+ panel.Panel.__init__(self, stdscr, 'log', 0)
+ threading.Thread.__init__(self)
+ self.setDaemon(True)
+
+ logged_events = nyx.arguments.expand_events(CONFIG['startup.events'])
+ self._event_log = nyx.util.log.LogGroup(CONFIG['cache.log_panel.size'], group_by_day = True)
+ self._event_types = nyx.util.log.listen_for_events(self._register_tor_event, logged_events)
+ self._log_file = nyx.util.log.LogFileOutput(CONFIG['features.logFile'])
+ self._filter = nyx.util.log.LogFilters(initial_filters = CONFIG['features.log.regex'])
+ self._show_duplicates = CONFIG['features.log.showDuplicateEntries']
+
+ self.set_pause_attr('_event_log')
+
+ self._halt = False # terminates thread if true
+ self._pause_condition = threading.Condition()
+ self._has_new_event = False
+
+ # fetches past tor events from log file, if available
+
+ if CONFIG['features.log.prepopulate']:
+ log_location = nyx.util.log.log_file_path(tor_controller())
+
+ if log_location:
+ try:
+ for entry in reversed(list(nyx.util.log.read_tor_log(log_location, CONFIG['features.log.prepopulateReadLimit']))):
+ if entry.type in self._event_types:
+ self._event_log.add(entry)
+ except IOError as exc:
+ log.info('Unable to read log located at %s: %s' % (log_location, exc))
+ except ValueError as exc:
+ log.info(str(exc))
+
+ self._last_content_height = len(self._event_log) # height of the rendered content when last drawn
+ self._scroll = 0
+
+ # merge NYX_LOGGER into us, and listen for its future events
+
+ for event in NYX_LOGGER:
+ self._register_nyx_event(event)
+
+ NYX_LOGGER.emit = self._register_nyx_event
+
+ def set_duplicate_visability(self, is_visible):
+ """
+ Sets if duplicate log entries are collaped or expanded.
+
+ :param bool is_visible: if **True** all log entries are shown, otherwise
+ they're deduplicated
+ """
+
+ self._show_duplicates = is_visible
+
+ def get_filter(self):
+ """
+ Provides our currently selected regex filter.
+ """
+
+ return self._filter
+
+ def show_filter_prompt(self):
+ """
+ Prompts the user to add a new regex filter.
+ """
+
+ regex_input = nyx.popups.input_prompt('Regular expression: ')
+
+ if regex_input:
+ self._filter.select(regex_input)
+
+ def show_event_selection_prompt(self):
+ """
+ Prompts the user to select the events being listened for.
+ """
+
+ # allow user to enter new types of events to log - unchanged if left blank
+
+ with nyx.popups.popup_window(16, 80) as (popup, width, height):
+ if popup:
+ # displays the available flags
+
+ popup.win.box()
+ popup.addstr(0, 0, 'Event Types:', curses.A_STANDOUT)
+ event_lines = CONFIG['msg.misc.event_types'].split('\n')
+
+ for i in range(len(event_lines)):
+ popup.addstr(i + 1, 1, event_lines[i][6:])
+
+ popup.win.refresh()
+
+ user_input = nyx.popups.input_prompt('Events to log: ')
+
+ if user_input:
+ try:
+ user_input = user_input.replace(' ', '') # strip spaces
+ event_types = nyx.arguments.expand_events(user_input)
+
+ if event_types != self._event_types:
+ self._event_types = nyx.util.log.listen_for_events(self._register_tor_event, event_types)
+ self.redraw(True)
+ except ValueError as exc:
+ nyx.popups.show_msg('Invalid flags: %s' % str(exc), 2)
+
+ def show_snapshot_prompt(self):
+ """
+ Lets user enter a path to take a snapshot, canceling if left blank.
+ """
+
+ path_input = nyx.popups.input_prompt('Path to save log snapshot: ')
+
+ if path_input:
+ try:
+ self.save_snapshot(path_input)
+ nyx.popups.show_msg('Saved: %s' % path_input, 2)
+ except IOError as exc:
+ nyx.popups.show_msg('Unable to save snapshot: %s' % exc, 2)
+
+ def clear(self):
+ """
+ Clears the contents of the event log.
+ """
+
+ self._event_log = nyx.util.log.LogGroup(CONFIG['cache.log_panel.size'], group_by_day = True)
+ self.redraw(True)
+
+ def save_snapshot(self, path):
+ """
+ Saves the log events currently being displayed to the given path. This
+ takes filers into account. This overwrites the file if it already exists.
+
+ :param str path: path where to save the log snapshot
+
+ :raises: **IOError** if unsuccessful
+ """
+
+ path = os.path.abspath(os.path.expanduser(path))
+
+ # make dir if the path doesn't already exist
+
+ base_dir = os.path.dirname(path)
+
+ try:
+ if not os.path.exists(base_dir):
+ os.makedirs(base_dir)
+ except OSError as exc:
+ raise IOError("unable to make directory '%s'" % base_dir)
+
+ event_log = list(self._event_log)
+ event_filter = self._filter.clone()
+
+ with open(path, 'w') as snapshot_file:
+ try:
+ for entry in reversed(event_log):
+ if event_filter.match(entry.display_message):
+ snapshot_file.write(entry.display_message + '\n')
+ except Exception as exc:
+ raise IOError("unable to write to '%s': %s" % (path, exc))
+
+ def handle_key(self, key):
+ if key.is_scroll():
+ page_height = self.get_preferred_size()[0] - 1
+ new_scroll = ui_tools.get_scroll_position(key, self._scroll, page_height, self._last_content_height)
+
+ if self._scroll != new_scroll:
+ self._scroll = new_scroll
+ self.redraw(True)
+ elif key.match('u'):
+ self.set_duplicate_visability(not self._show_duplicates)
+ self.redraw(True)
+ elif key.match('c'):
+ msg = 'This will clear the log. Are you sure (c again to confirm)?'
+ key_press = nyx.popups.show_msg(msg, attr = curses.A_BOLD)
+
+ if key_press.match('c'):
+ self.clear()
+ elif key.match('f'):
+ with panel.CURSES_LOCK:
+ initial_selection = 1 if self._filter.selection() else 0
+ options = ['None'] + self._filter.latest_selections() + ['New...']
+ selection = nyx.popups.show_menu('Log Filter:', options, initial_selection)
+
+ if selection == 0:
+ self._filter.select(None)
+ elif selection == len(options) - 1:
+ # selected 'New...' option - prompt user to input regular expression
+ self.show_filter_prompt()
+ elif selection != -1:
+ self._filter.select(self._filter.latest_selections()[selection - 1])
+ elif key.match('e'):
+ self.show_event_selection_prompt()
+ elif key.match('a'):
+ self.show_snapshot_prompt()
+ else:
+ return False
+
+ return True
+
+ def get_help(self):
+ return [
+ ('up arrow', 'scroll log up a line', None),
+ ('down arrow', 'scroll log down a line', None),
+ ('a', 'save snapshot of the log', None),
+ ('e', 'change logged events', None),
+ ('f', 'log regex filter', 'enabled' if self._filter.selection() else 'disabled'),
+ ('u', 'duplicate log entries', 'visible' if self._show_duplicates else 'hidden'),
+ ('c', 'clear event log', None),
+ ]
+
+ def draw(self, width, height):
+ self._scroll = max(0, min(self._scroll, self._last_content_height - height + 1))
+
+ event_log = list(self.get_attr('_event_log'))
+ event_filter = self._filter.clone()
+ event_types = list(self._event_types)
+ scroll = int(self._scroll)
+ last_content_height = self._last_content_height
+ show_duplicates = self._show_duplicates
+
+ is_scrollbar_visible = last_content_height > height - 1
+
+ if is_scrollbar_visible:
+ self.add_scroll_bar(scroll, scroll + height - 1, last_content_height, 1)
+
+ x, y = 3 if is_scrollbar_visible else 1, 1 - scroll
+
+ # group entries by date, filtering out those that aren't visible
+
+ day_to_entries, today = {}, nyx.util.log.day_count(time.time())
+
+ for entry in event_log:
+ if entry.is_duplicate and not show_duplicates:
+ continue # deduplicated message
+ elif not event_filter.match(entry.display_message):
+ continue # filter doesn't match log message
+
+ day_to_entries.setdefault(entry.day_count(), []).append(entry)
+
+ for day in sorted(day_to_entries.keys(), reverse = True):
+ if day == today:
+ for entry in day_to_entries[day]:
+ y = self._draw_entry(x, y, width, entry, show_duplicates)
+ else:
+ original_y, y = y, y + 1
+
+ for entry in day_to_entries[day]:
+ y = self._draw_entry(x, y, width, entry, show_duplicates)
+
+ ui_tools.draw_box(self, original_y, x - 1, width - x + 1, y - original_y + 1, curses.A_BOLD, 'yellow')
+ time_label = time.strftime(' %B %d, %Y ', time.localtime(day_to_entries[day][0].timestamp))
+ self.addstr(original_y, x + 1, time_label, curses.A_BOLD, curses.A_BOLD, 'yellow')
+
+ y += 1
+
+ # drawing the title after the content, so we'll clear content from the top line
+
+ if self.is_title_visible():
+ self._draw_title(width, event_types, event_filter)
+
+ # redraw the display if...
+ # - last_content_height was off by too much
+ # - we're off the bottom of the page
+
+ new_content_height = y + scroll - 1
+ content_height_delta = abs(last_content_height - new_content_height)
+ force_redraw, force_redraw_reason = True, ''
+
+ if content_height_delta >= CONTENT_HEIGHT_REDRAW_THRESHOLD:
+ force_redraw_reason = 'estimate was off by %i' % content_height_delta
+ elif new_content_height > height and self._scroll + height - 1 > new_content_height:
+ force_redraw_reason = 'scrolled off the bottom of the page'
+ elif not is_scrollbar_visible and new_content_height > height - 1:
+ force_redraw_reason = "scroll bar wasn't previously visible"
+ elif is_scrollbar_visible and new_content_height <= height - 1:
+ force_redraw_reason = "scroll bar shouldn't be visible"
+ else:
+ force_redraw = False
+
+ self._last_content_height = new_content_height
+ self._has_new_event = False
+
+ if force_redraw:
+ log.debug('redrawing the log panel with the corrected content height (%s)' % force_redraw_reason)
+ self.redraw(True)
+
+ def _draw_title(self, width, event_types, event_filter):
+ """
+ Panel title with the event types we're logging and our regex filter if set.
+ """
+
+ self.addstr(0, 0, ' ' * width) # clear line
+ title_comp = list(nyx.util.log.condense_runlevels(*event_types))
+
+ if event_filter.selection():
+ title_comp.append('filter: %s' % event_filter.selection())
+
+ title_comp_str = join(title_comp, ', ', width - 10)
+ title = 'Events (%s):' % title_comp_str if title_comp_str else 'Events:'
+
+ self.addstr(0, 0, title, curses.A_STANDOUT)
+
+ def _draw_entry(self, x, y, width, entry, show_duplicates):
+ """
+ Presents a log entry with line wrapping.
+ """
+
+ min_x, msg = x + 2, entry.display_message
+ boldness = curses.A_BOLD if 'ERR' in entry.type else curses.A_NORMAL # emphasize ERR messages
+ color = CONFIG['attr.log_color'].get(entry.type, 'white')
+
+ for line in msg.splitlines():
+ x, y = self.addstr_wrap(y, x, line, width, min_x, boldness, color)
+
+ if entry.duplicates and not show_duplicates:
+ duplicate_count = len(entry.duplicates) - 1
+ plural = 's' if duplicate_count > 1 else ''
+ duplicate_msg = ' [%i duplicate%s hidden]' % (duplicate_count, plural)
+ x, y = self.addstr_wrap(y, x, duplicate_msg, width, min_x, curses.A_BOLD, 'green')
+
+ return y + 1
+
+ def run(self):
+ """
+ Redraws the display, coalescing updates if events are rapidly logged (for
+ instance running at the DEBUG runlevel) while also being immediately
+ responsive if additions are less frequent.
+ """
+
+ last_ran, last_day = -1, nyx.util.log.day_count(time.time())
+
+ while not self._halt:
+ current_day = nyx.util.log.day_count(time.time())
+ time_since_reset = time.time() - last_ran
+ max_log_update_rate = CONFIG['features.log.maxRefreshRate'] / 1000.0
+
+ sleep_time = 0
+
+ if (not self._has_new_event and last_day == current_day) or self.is_paused():
+ sleep_time = 5
+ elif time_since_reset < max_log_update_rate:
+ sleep_time = max(0.05, max_log_update_rate - time_since_reset)
+
+ if sleep_time:
+ with self._pause_condition:
+ if not self._halt:
+ self._pause_condition.wait(sleep_time)
+
+ continue
+
+ last_ran, last_day = time.time(), current_day
+ self.redraw(True)
+
+ def stop(self):
+ """
+ Halts further updates and terminates the thread.
+ """
+
+ with self._pause_condition:
+ self._halt = True
+ self._pause_condition.notifyAll()
+
+ def _register_tor_event(self, event):
+ msg = ' '.join(str(event).split(' ')[1:])
+
+ if isinstance(event, stem.response.events.BandwidthEvent):
+ msg = 'READ: %i, WRITTEN: %i' % (event.read, event.written)
+ elif isinstance(event, stem.response.events.LogEvent):
+ msg = event.message
+
+ self._register_event(nyx.util.log.LogEntry(event.arrived_at, event.type, msg))
+
+ def _register_nyx_event(self, record):
+ if record.levelname == 'WARNING':
+ record.levelname = 'WARN'
+
+ self._register_event(nyx.util.log.LogEntry(int(record.created), 'NYX_%s' % record.levelname, record.msg))
+
+ def _register_event(self, event):
+ if event.type not in self._event_types:
+ return
+
+ self._event_log.add(event)
+ self._log_file.write(event.display_message)
+
+ # notifies the display that it has new content
+
+ if self._filter.match(event.display_message):
+ self._has_new_event = True
+
+ with self._pause_condition:
+ self._pause_condition.notifyAll()
diff --git a/nyx/panel/torrc.py b/nyx/panel/torrc.py
new file mode 100644
index 0000000..f0f66b4
--- /dev/null
+++ b/nyx/panel/torrc.py
@@ -0,0 +1,171 @@
+"""
+Panel displaying the torrc or nyxrc with the validation done against it.
+"""
+
+import math
+import curses
+
+from nyx.util import expand_path, msg, panel, tor_controller, ui_tools
+
+from stem import ControllerError
+from stem.control import State
+
+
+class TorrcPanel(panel.Panel):
+ """
+ Renders the current torrc or nyxrc with syntax highlighting in a scrollable
+ area.
+ """
+
+ def __init__(self, stdscr):
+ panel.Panel.__init__(self, stdscr, 'torrc', 0)
+
+ self._scroll = 0
+ self._show_line_numbers = True # shows left aligned line numbers
+ self._show_comments = True # shows comments and extra whitespace
+ self._last_content_height = 0
+
+ self._torrc_location = None
+ self._torrc_content = None
+ self._torrc_load_error = None
+
+ controller = tor_controller()
+ controller.add_status_listener(self.reset_listener)
+ self.reset_listener(controller, State.RESET, None)
+
+ def reset_listener(self, controller, event_type, _):
+ """
+ Reloads and displays the torrc on tor reload (sighup) events.
+ """
+
+ if event_type == State.RESET:
+ try:
+ self._torrc_location = expand_path(controller.get_info('config-file'))
+
+ with open(self._torrc_location) as torrc_file:
+ self._torrc_content = [ui_tools.get_printable(line.replace('\t', ' ')).rstrip() for line in torrc_file.readlines()]
+ except ControllerError as exc:
+ self._torrc_load_error = msg('panel.torrc.unable_to_find_torrc', error = exc)
+ self._torrc_location = None
+ self._torrc_content = None
+ except Exception as exc:
+ self._torrc_load_error = msg('panel.torrc.unable_to_load_torrc', error = exc.strerror)
+ self._torrc_content = None
+
+ def set_comments_visible(self, is_visible):
+ """
+ Sets if comments and blank lines are shown or stripped.
+
+ :var bool is_visible: shows comments if true, strips otherwise
+ """
+
+ self._show_comments = is_visible
+ self.redraw(True)
+
+ def set_line_number_visible(self, is_visible):
+ """
+ Sets if line numbers are shown or hidden.
+
+ :var bool is_visible: displays line numbers if true, hides otherwise
+ """
+
+ self._show_line_numbers = is_visible
+ self.redraw(True)
+
+ def handle_key(self, key):
+ if key.is_scroll():
+ page_height = self.get_preferred_size()[0] - 1
+ new_scroll = ui_tools.get_scroll_position(key, self._scroll, page_height, self._last_content_height)
+
+ if self._scroll != new_scroll:
+ self._scroll = new_scroll
+ self.redraw(True)
+ elif key.match('l'):
+ self.set_line_number_visible(not self._show_line_numbers)
+ elif key.match('s'):
+ self.set_comments_visible(not self._show_comments)
+ else:
+ return False
+
+ return True
+
+ def get_help(self):
+ return [
+ ('up arrow', 'scroll up a line', None),
+ ('down arrow', 'scroll down a line', None),
+ ('page up', 'scroll up a page', None),
+ ('page down', 'scroll down a page', None),
+ ('s', 'comment stripping', 'off' if self._show_comments else 'on'),
+ ('l', 'line numbering', 'on' if self._show_line_numbers else 'off'),
+ ('x', 'reset tor (issue sighup)', None),
+ ]
+
+ def draw(self, width, height):
+ if self._torrc_content is None:
+ self.addstr(1, 0, self._torrc_load_error, 'red', curses.A_BOLD)
+ new_content_height = 1
+ else:
+ self._scroll = max(0, min(self._scroll, self._last_content_height - height + 1))
+
+ if not self._show_line_numbers:
+ line_number_offset = 0
+ elif len(self._torrc_content) == 0:
+ line_number_offset = 2
+ else:
+ line_number_offset = int(math.log10(len(self._torrc_content))) + 2
+
+ scroll_offset = 0
+
+ if self._last_content_height > height - 1:
+ scroll_offset = 3
+ self.add_scroll_bar(self._scroll, self._scroll + height - 1, self._last_content_height, 1)
+
+ y = 1 - self._scroll
+ is_multiline = False # true if we're in the middle of a multiline torrc entry
+
+ for line_number, line in enumerate(self._torrc_content):
+ if not self._show_comments:
+ line = line[:line.find('#')].rstrip() if '#' in line else line
+
+ if not line:
+ continue # skip blank lines
+
+ if '#' in line:
+ line, comment = line.split('#', 1)
+ comment = '#' + comment
+ else:
+ comment = ''
+
+ if is_multiline:
+ option, argument = '', line # previous line ended with a '\'
+ elif ' ' not in line.strip():
+ option, argument = line, '' # no argument
+ else:
+ whitespace = ' ' * (len(line) - len(line.strip()))
+ option, argument = line.strip().split(' ', 1)
+ option = whitespace + option + ' '
+
+ is_multiline = line.endswith('\\') # next line's part of a multi-line entry
+
+ if self._show_line_numbers:
+ self.addstr(y, scroll_offset, str(line_number + 1).rjust(line_number_offset - 1), curses.A_BOLD, 'yellow')
+
+ x = line_number_offset + scroll_offset
+ min_x = line_number_offset + scroll_offset
+
+ x, y = self.addstr_wrap(y, x, option, width, min_x, curses.A_BOLD, 'green')
+ x, y = self.addstr_wrap(y, x, argument, width, min_x, curses.A_BOLD, 'cyan')
+ x, y = self.addstr_wrap(y, x, comment, width, min_x, 'white')
+
+ y += 1
+
+ new_content_height = y + self._scroll - 1
+
+ if self.is_title_visible():
+ self.addstr(0, 0, ' ' * width) # clear line
+ location = ' (%s)' % self._torrc_location if self._torrc_location else ''
+ self.addstr(0, 0, 'Tor Configuration File%s:' % location, curses.A_STANDOUT)
+
+ if self._last_content_height != new_content_height:
+ self._last_content_height = new_content_height
+ self.redraw(True)
diff --git a/nyx/torrc_panel.py b/nyx/torrc_panel.py
deleted file mode 100644
index f0f66b4..0000000
--- a/nyx/torrc_panel.py
+++ /dev/null
@@ -1,171 +0,0 @@
-"""
-Panel displaying the torrc or nyxrc with the validation done against it.
-"""
-
-import math
-import curses
-
-from nyx.util import expand_path, msg, panel, tor_controller, ui_tools
-
-from stem import ControllerError
-from stem.control import State
-
-
-class TorrcPanel(panel.Panel):
- """
- Renders the current torrc or nyxrc with syntax highlighting in a scrollable
- area.
- """
-
- def __init__(self, stdscr):
- panel.Panel.__init__(self, stdscr, 'torrc', 0)
-
- self._scroll = 0
- self._show_line_numbers = True # shows left aligned line numbers
- self._show_comments = True # shows comments and extra whitespace
- self._last_content_height = 0
-
- self._torrc_location = None
- self._torrc_content = None
- self._torrc_load_error = None
-
- controller = tor_controller()
- controller.add_status_listener(self.reset_listener)
- self.reset_listener(controller, State.RESET, None)
-
- def reset_listener(self, controller, event_type, _):
- """
- Reloads and displays the torrc on tor reload (sighup) events.
- """
-
- if event_type == State.RESET:
- try:
- self._torrc_location = expand_path(controller.get_info('config-file'))
-
- with open(self._torrc_location) as torrc_file:
- self._torrc_content = [ui_tools.get_printable(line.replace('\t', ' ')).rstrip() for line in torrc_file.readlines()]
- except ControllerError as exc:
- self._torrc_load_error = msg('panel.torrc.unable_to_find_torrc', error = exc)
- self._torrc_location = None
- self._torrc_content = None
- except Exception as exc:
- self._torrc_load_error = msg('panel.torrc.unable_to_load_torrc', error = exc.strerror)
- self._torrc_content = None
-
- def set_comments_visible(self, is_visible):
- """
- Sets if comments and blank lines are shown or stripped.
-
- :var bool is_visible: shows comments if true, strips otherwise
- """
-
- self._show_comments = is_visible
- self.redraw(True)
-
- def set_line_number_visible(self, is_visible):
- """
- Sets if line numbers are shown or hidden.
-
- :var bool is_visible: displays line numbers if true, hides otherwise
- """
-
- self._show_line_numbers = is_visible
- self.redraw(True)
-
- def handle_key(self, key):
- if key.is_scroll():
- page_height = self.get_preferred_size()[0] - 1
- new_scroll = ui_tools.get_scroll_position(key, self._scroll, page_height, self._last_content_height)
-
- if self._scroll != new_scroll:
- self._scroll = new_scroll
- self.redraw(True)
- elif key.match('l'):
- self.set_line_number_visible(not self._show_line_numbers)
- elif key.match('s'):
- self.set_comments_visible(not self._show_comments)
- else:
- return False
-
- return True
-
- def get_help(self):
- return [
- ('up arrow', 'scroll up a line', None),
- ('down arrow', 'scroll down a line', None),
- ('page up', 'scroll up a page', None),
- ('page down', 'scroll down a page', None),
- ('s', 'comment stripping', 'off' if self._show_comments else 'on'),
- ('l', 'line numbering', 'on' if self._show_line_numbers else 'off'),
- ('x', 'reset tor (issue sighup)', None),
- ]
-
- def draw(self, width, height):
- if self._torrc_content is None:
- self.addstr(1, 0, self._torrc_load_error, 'red', curses.A_BOLD)
- new_content_height = 1
- else:
- self._scroll = max(0, min(self._scroll, self._last_content_height - height + 1))
-
- if not self._show_line_numbers:
- line_number_offset = 0
- elif len(self._torrc_content) == 0:
- line_number_offset = 2
- else:
- line_number_offset = int(math.log10(len(self._torrc_content))) + 2
-
- scroll_offset = 0
-
- if self._last_content_height > height - 1:
- scroll_offset = 3
- self.add_scroll_bar(self._scroll, self._scroll + height - 1, self._last_content_height, 1)
-
- y = 1 - self._scroll
- is_multiline = False # true if we're in the middle of a multiline torrc entry
-
- for line_number, line in enumerate(self._torrc_content):
- if not self._show_comments:
- line = line[:line.find('#')].rstrip() if '#' in line else line
-
- if not line:
- continue # skip blank lines
-
- if '#' in line:
- line, comment = line.split('#', 1)
- comment = '#' + comment
- else:
- comment = ''
-
- if is_multiline:
- option, argument = '', line # previous line ended with a '\'
- elif ' ' not in line.strip():
- option, argument = line, '' # no argument
- else:
- whitespace = ' ' * (len(line) - len(line.strip()))
- option, argument = line.strip().split(' ', 1)
- option = whitespace + option + ' '
-
- is_multiline = line.endswith('\\') # next line's part of a multi-line entry
-
- if self._show_line_numbers:
- self.addstr(y, scroll_offset, str(line_number + 1).rjust(line_number_offset - 1), curses.A_BOLD, 'yellow')
-
- x = line_number_offset + scroll_offset
- min_x = line_number_offset + scroll_offset
-
- x, y = self.addstr_wrap(y, x, option, width, min_x, curses.A_BOLD, 'green')
- x, y = self.addstr_wrap(y, x, argument, width, min_x, curses.A_BOLD, 'cyan')
- x, y = self.addstr_wrap(y, x, comment, width, min_x, 'white')
-
- y += 1
-
- new_content_height = y + self._scroll - 1
-
- if self.is_title_visible():
- self.addstr(0, 0, ' ' * width) # clear line
- location = ' (%s)' % self._torrc_location if self._torrc_location else ''
- self.addstr(0, 0, 'Tor Configuration File%s:' % location, curses.A_STANDOUT)
-
- if self._last_content_height != new_content_height:
- self._last_content_height = new_content_height
- self.redraw(True)
diff --git a/nyx/util/log.py b/nyx/util/log.py
index bf9fe82..e4f962b 100644
--- a/nyx/util/log.py
+++ b/nyx/util/log.py
@@ -4,7 +4,7 @@ runlevels.
::
- trace - logs a message at the TRACE runlevel
+ trace - logs a message at the TRACE runlevel
debug - logs a message at the DEBUG runlevel
info - logs a message at the INFO runlevel
notice - logs a message at the NOTICE runlevel
diff --git a/setup.py b/setup.py
index 13ae6af..326cc61 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.menu', 'nyx.util'],
+ packages = ['nyx', 'nyx.panel', 'nyx.menu', 'nyx.util'],
keywords = 'tor onion controller',
install_requires = ['stem>=1.4.1'],
package_data = {'nyx': ['settings/*', 'resources/*']},
1
0

08 Mar '16
commit 6a7f03d18cd4c050f6b4ca41590a9cd4eb5f5e3c
Author: David Goulet <dgoulet(a)ev0ke.net>
Date: Tue Feb 23 12:34:50 2016 -0500
Add LizardNSA entries which is the Google Cloud
Chances are that legitimate relays will start appearing from those addresses
so the expiry time for this entry is only 3 months that is until June 1st.
bad-relays@: <20160204190530.GA8570@raoul>
Signed-off-by: David Goulet <dgoulet(a)ev0ke.net>
---
data/tracked_relays.cfg | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/data/tracked_relays.cfg b/data/tracked_relays.cfg
index e25959e..5b01e43 100644
--- a/data/tracked_relays.cfg
+++ b/data/tracked_relays.cfg
@@ -20,3 +20,18 @@
# Unnamed001.expires 2016-03-01
# Unnamed001.fingerprint 05AF83344B3787D0DCCD47DC4A6A4668142A5F8C
+# The networks below are the entire Google Cloud.
+LizardNSA.description Sybil attack on the network on 2014-12-27
+LizardNSA.expires 2016-06-01
+LizardNSA.address 104.154.0.0/15
+LizardNSA.address 107.167.160.0/19
+LizardNSA.address 107.178.192.0/18
+LizardNSA.address 108.59.80.0/20
+LizardNSA.address 130.211.0.0/16
+LizardNSA.address 146.148.0.0/17
+LizardNSA.address 162.222.176.0/21
+LizardNSA.address 173.255.112.0/20
+LizardNSA.address 192.158.28.0/22
+LizardNSA.address 199.223.232.0/21
+LizardNSA.address 23.236.48.0/20
+LizardNSA.address 23.251.128.0/19
1
0
commit 66eb29a2b8bf349c69dbd33a59db2d99e89e8eea
Author: Damian Johnson <atagar(a)torproject.org>
Date: Tue Mar 8 08:35:41 2016 -0800
Adding 'IP hijacking' project idea
Idea from Aaron, Donncha and Yawnbox. Aaron is the only person that spoke up
volunteering to mentor but might be worth nudging the others if we get
students.
---
getinvolved/en/volunteer.wml | 40 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 40 insertions(+)
diff --git a/getinvolved/en/volunteer.wml b/getinvolved/en/volunteer.wml
index 6748023..88b6e0d 100644
--- a/getinvolved/en/volunteer.wml
+++ b/getinvolved/en/volunteer.wml
@@ -1453,6 +1453,46 @@ implementation.
</li>
</ol>
</li>
+
+ <a id="ipHijacking"></a>
+ <li>
+ <b>IP hijacking detection for the Tor Network</b>
+ <br>
+ Likely Mentors: <i>Aaron Gibson (aagbsn)</i>
+ <br><br>
+ <p>
+ <a href="https://en.wikipedia.org/wiki/IP_hijacking">IP hijacking</a>
+ occurs when a bad actor creates false routing information to redirect
+ Internet traffic to or through themselves. This activity is straightforward
+ to detect, because the Internet routing tables are public information, but
+ currently there are no public services that monitor the Tor network. The
+ Tor Network is a dynamic set of relays, so monitoring must be Tor-aware in
+ order to keep the set of monitored relays accurate. Additionally, consensus
+ archives and historical Internet routing table snapshots are publicly
+ available, and this analysis can be performed retroactively.
+ </p>
+
+ <p>
+ The implications of IP hijacking are that Tor traffic can be redirected
+ through a network that an attacker controls, even if the attacker does not
+ normally have this capability - i.e. they are not in the network path. For
+ example, an adversary could hijack the prefix of a Tor Guard relay, in
+ order to learn who its clients are, or hijack a Tor Exit relay to tamper
+ with requests or name resolution.
+ </p>
+
+ <p>
+ This project comprises building a service that compares network prefixes of
+ relays in the consensus with present and historic routing table snapshots
+ from looking glass services such as <a
+ href="http://routeviews.org">Routeviews</a>, or aggregators such as <a
+ href="https://bgpstream.caida.org">Caida BGPStream</a> and then issues
+ email alerts to the contact-info in the relay descriptor and a mailing
+ list. Network operators are responsive to route injections, and these
+ alerts can be used to notify network operators to take immediate action, as
+ well as collect information about the occurrence of these type of attacks.
+ </p>
+ </li>
<!--
<a id=""></a>
<li>
1
0

08 Mar '16
commit 5226ad9cc8f223c3e14e8554b0eb244b971b87c0
Author: Damian Johnson <atagar(a)torproject.org>
Date: Tue Mar 8 08:31:51 2016 -0800
Adding dgoulet as mentor for the HS project
Requested by dgoulet on #tor-project.
---
getinvolved/en/volunteer.wml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/getinvolved/en/volunteer.wml b/getinvolved/en/volunteer.wml
index 8b6c894..6748023 100644
--- a/getinvolved/en/volunteer.wml
+++ b/getinvolved/en/volunteer.wml
@@ -887,7 +887,7 @@ You may contact the mentors on IRC for more information. (sukhe on #tor-dev, #to
<br>
Language: <i>C</i>
<br>
- Likely Mentors: <i>George (asn)</i>
+ Likely Mentors: <i>George (asn), David Goulet (dgoulet)</i>
<br><br>
<p>
We're working on a revamp of the entire Tor hidden service design to
1
0

08 Mar '16
commit f77ddbc5033f1d2261f3cb9bbf0ce16a8c13454c
Author: Georg Koppen <gk(a)torproject.org>
Date: Tue Mar 8 11:34:01 2016 +0000
Fix CHANGELOG (add translations update)
---
src/CHANGELOG | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/CHANGELOG b/src/CHANGELOG
index 8b2ddb1..3bb3e54 100644
--- a/src/CHANGELOG
+++ b/src/CHANGELOG
@@ -2,6 +2,7 @@
* Bug 16990: Don't mishandle multiline commands
* Bug 18144: about:tor update arrow position is wrong
* Bug 16725: Allow resizing with non-default homepage
+ * Translation updates
1.9.4.3
* Bug 16990: Show circuit display for connections using multi-party channels
1
0

08 Mar '16
commit fbcb41e87c77d2a032181cb37ee129165c1c631b
Author: Translation commit bot <translation(a)torproject.org>
Date: Tue Mar 8 11:15:02 2016 +0000
Update translations for bridgedb
---
be/LC_MESSAGES/bridgedb.po | 34 +++++++++++++++++-----------------
1 file changed, 17 insertions(+), 17 deletions(-)
diff --git a/be/LC_MESSAGES/bridgedb.po b/be/LC_MESSAGES/bridgedb.po
index 8bb4e10..538f7b0 100644
--- a/be/LC_MESSAGES/bridgedb.po
+++ b/be/LC_MESSAGES/bridgedb.po
@@ -10,7 +10,7 @@ msgstr ""
"Project-Id-Version: The Tor Project\n"
"Report-Msgid-Bugs-To: 'https://trac.torproject.org/projects/tor/newticket?component=BridgeDB&keywo…'\n"
"POT-Creation-Date: 2015-07-25 03:40+0000\n"
-"PO-Revision-Date: 2016-03-07 21:31+0000\n"
+"PO-Revision-Date: 2016-03-08 11:14+0000\n"
"Last-Translator: Сілкін Станіслаў <moonblr29(a)gmail.com>\n"
"Language-Team: Belarusian (http://www.transifex.com/otf/torproject/language/be/)\n"
"MIME-Version: 1.0\n"
@@ -49,7 +49,7 @@ msgstr "Гісторыя зменаў"
#: bridgedb/https/templates/base.html:88
msgid "Contact"
-msgstr ""
+msgstr "Сувязь"
#: bridgedb/https/templates/bridges.html:35
msgid "Select All"
@@ -61,7 +61,7 @@ msgstr "Паказаць QR-код"
#: bridgedb/https/templates/bridges.html:52
msgid "QRCode for your bridge lines"
-msgstr ""
+msgstr "QR-код для вашых ліній мастоў"
#. TRANSLATORS: Please translate this into some silly way to say
#. "There was a problem!" in your language. For example,
@@ -80,7 +80,7 @@ msgstr "Здаецца, адбылася памылка падчас атрым
msgid ""
"This QRCode contains your bridge lines. Scan it with a QRCode reader to copy"
" your bridge lines onto mobile and other devices."
-msgstr ""
+msgstr "Гэты QR-код утрымлівае вашыя лініі мастоў. Праскануйце яго пры дапамозе чытальніка QR-кодаў для таго, каб скапіяваць вашыя лініі мастоў на мабільныя ці іншыя прылады."
#: bridgedb/https/templates/bridges.html:131
msgid "There currently aren't any bridges available..."
@@ -111,7 +111,7 @@ msgstr "Крок %s2%s"
#: bridgedb/https/templates/index.html:27
#, python-format
msgid "Get %s bridges %s"
-msgstr ""
+msgstr "Атрымаць %s масты %s"
#: bridgedb/https/templates/index.html:36
#, python-format
@@ -130,7 +130,7 @@ msgstr "Цяпер %s дадайце масты ў Tor Browser %s"
#: bridgedb/https/templates/options.html:38
#, python-format
msgid "%sJ%sust give me bridges!"
-msgstr ""
+msgstr "%sП%sроста дайце мне гэтыя масты!"
#: bridgedb/https/templates/options.html:51
msgid "Advanced Options"
@@ -142,7 +142,7 @@ msgstr "Не"
#: bridgedb/https/templates/options.html:87
msgid "none"
-msgstr ""
+msgstr "няма"
#. TRANSLATORS: Please make sure the '%s' surrounding single letters at the
#. beginning of words are present in your final translation. Thanks!
@@ -158,11 +158,11 @@ msgstr "%sТ%sак!"
#: bridgedb/https/templates/options.html:147
#, python-format
msgid "%sG%set Bridges"
-msgstr ""
+msgstr "%sА%sтрымаць Масты"
#: bridgedb/strings.py:43
msgid "[This is an automated message; please do not reply.]"
-msgstr "[Гэта аўтаматычнае паведамленне; каліласка, не адказвайце на яго.]"
+msgstr "[Гэта аўтаматычнае паведамленне; калі ласка, не адказвайце на яго.]"
#: bridgedb/strings.py:45
msgid "Here are your bridges:"
@@ -188,7 +188,7 @@ msgstr "Рады вітаць вас у BridgeDB!"
#. TRANSLATORS: Please DO NOT translate the words "transport" or "TYPE".
#: bridgedb/strings.py:55
msgid "Currently supported transport TYPEs:"
-msgstr ""
+msgstr "Transport TYPEs, што зараз падтрымліваюцца:"
#: bridgedb/strings.py:56
#, python-format
@@ -211,7 +211,7 @@ msgstr "Публічныя Ключы"
msgid ""
"This email was generated with rainbows, unicorns, and sparkles\n"
"for %s on %s at %s."
-msgstr "Гэты імэйл быў створаны чароўнымі медзведзянятамі, маленькімі знічкамі і зіхатлівымі агеньчыкамі для %s у(ў) %s у(ў) %s ."
+msgstr "Гэтая паштовая скрыня была створаная чароўнымі медзведзянятамі, маленькімі знічкамі і зіхатлівымі агеньчыкамі для %s у(ў) %s у(ў) %s ."
#. TRANSLATORS: Please DO NOT translate "BridgeDB".
#. TRANSLATORS: Please DO NOT translate "Pluggable Transports".
@@ -225,7 +225,7 @@ msgid ""
"difficult for anyone watching your internet traffic to determine that you are\n"
"using Tor.\n"
"\n"
-msgstr ""
+msgstr "BridgeDB можа забяспечваць масты рознымі %sтыпамі Pluggable Transports%s,\nшто могуць дапамагчы схаваць вашыя падлучэнні да Tor Network. Для тых, хто назірае за вашым Інтэрнэт-трафікам становіцца больш складаным\nвызначыць, што вы\nкарыстаецеся Tor.\n\n"
#. TRANSLATORS: Please DO NOT translate "Pluggable Transports".
#: bridgedb/strings.py:79
@@ -233,7 +233,7 @@ msgid ""
"Some bridges with IPv6 addresses are also available, though some Pluggable\n"
"Transports aren't IPv6 compatible.\n"
"\n"
-msgstr ""
+msgstr "Даступныя таксама некаторыя масты з IPv6 адрасамі,\nхоць і не ўсе Pluggable\nTransports сумяшчальныя з IPv6.\n\n"
#. TRANSLATORS: Please DO NOT translate "BridgeDB".
#. TRANSLATORS: The phrase "plain-ol'-vanilla" means "plain, boring,
@@ -251,16 +251,16 @@ msgstr ""
#: bridgedb/strings.py:101
msgid "What are bridges?"
-msgstr ""
+msgstr "Што за масты?"
#: bridgedb/strings.py:102
#, python-format
msgid "%s Bridges %s are Tor relays that help you circumvent censorship."
-msgstr ""
+msgstr "%s Масты %s - гэта рэтранслятары Tor, якія дапамогуць вам абмінуць цэнзуру."
#: bridgedb/strings.py:107
msgid "I need an alternative way of getting bridges!"
-msgstr ""
+msgstr "Мне трэба атрымаць масты як-небудзь яшчэ!"
#: bridgedb/strings.py:108
#, python-format
@@ -292,7 +292,7 @@ msgstr ""
#: bridgedb/strings.py:128
msgid "Here are your bridge lines:"
-msgstr ""
+msgstr "Вось вашыя лініі мастоў:"
#: bridgedb/strings.py:129
msgid "Get Bridges!"
1
0

[snowflake/master] multiple arbitrary ice servers can be passed as client flag (close #24)
by serene@torproject.org 08 Mar '16
by serene@torproject.org 08 Mar '16
08 Mar '16
commit 9daa6c4b71bf4a366aa65a65f1551b8febd7a1df
Author: Serene Han <keroserene+git(a)gmail.com>
Date: Sat Mar 5 17:01:30 2016 -0800
multiple arbitrary ice servers can be passed as client flag (close #24)
---
client/snowflake.go | 10 +++++-----
client/torrc | 2 +-
client/util.go | 2 ++
3 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/client/snowflake.go b/client/snowflake.go
index 4287425..f10c50e 100644
--- a/client/snowflake.go
+++ b/client/snowflake.go
@@ -64,7 +64,6 @@ func dialWebRTC() (*webRTCConn, error) {
// TODO: [#3] Fetch ICE server information from Broker.
// TODO: [#18] Consider TURN servers here too.
config := webrtc.NewConfiguration(iceServers...)
-
broker := NewBrokerChannel(brokerURL, frontDomain)
if nil == broker {
return nil, errors.New("Failed to prepare BrokerChannel")
@@ -160,10 +159,6 @@ func readSignalingMessages(f *os.File) {
func main() {
// var err error
webrtc.SetLoggingVerbosity(1)
- flag.StringVar(&brokerURL, "url", "", "URL of signaling broker")
- flag.StringVar(&frontDomain, "front", "", "front domain")
- flag.Var(&iceServers, "ice", "comma-separated list of ICE servers")
- flag.Parse()
logFile, err := os.OpenFile("snowflake.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
log.Fatal(err)
@@ -172,6 +167,11 @@ func main() {
log.SetOutput(logFile)
log.Println("\nStarting Snowflake Client...")
+ flag.StringVar(&brokerURL, "url", "", "URL of signaling broker")
+ flag.StringVar(&frontDomain, "front", "", "front domain")
+ flag.Var(&iceServers, "ice", "comma-separated list of ICE servers")
+ flag.Parse()
+
// Expect user to copy-paste if
// TODO: Maybe just get rid of copy-paste entirely.
if "" != brokerURL {
diff --git a/client/torrc b/client/torrc
index c4af61f..b6d0f61 100644
--- a/client/torrc
+++ b/client/torrc
@@ -4,6 +4,6 @@ DataDirectory datadir
ClientTransportPlugin snowflake exec ./client \
-url https://snowflake-reg.appspot.com/ \
-front www.google.com \
--ice stun:stun.l.google.com:19302
+-ice stun:stun.l.google.com:19302,stun:s1.taraba.net
Bridge snowflake 0.0.3.0:1
diff --git a/client/util.go b/client/util.go
index 02132f2..6a2b6de 100644
--- a/client/util.go
+++ b/client/util.go
@@ -19,8 +19,10 @@ func (i *IceServerList) String() string {
}
func (i *IceServerList) Set(s string) error {
+ log.Println("IceServerList:")
for _, server := range strings.Split(s, ",") {
// TODO: STUN / TURN url format validation?
+ log.Println(server)
option := webrtc.OptionIceServer(server)
*i = append(*i, option)
}
1
0

[snowflake/master] provide 'silent' param on snowflake proxy to disable confirmation dialog
by serene@torproject.org 08 Mar '16
by serene@torproject.org 08 Mar '16
08 Mar '16
commit 39be8403a430abab13c77cded2b13bf06324e25c
Author: Serene Han <keroserene+git(a)gmail.com>
Date: Mon Mar 7 22:58:23 2016 -0800
provide 'silent' param on snowflake proxy to disable confirmation dialog
---
proxy/snowflake.coffee | 4 +++-
proxy/spec/snowflake.spec.coffee | 8 ++++++++
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/proxy/snowflake.coffee b/proxy/snowflake.coffee
index 90150ad..4a98472 100644
--- a/proxy/snowflake.coffee
+++ b/proxy/snowflake.coffee
@@ -14,6 +14,7 @@ DEFAULT_RELAY =
COPY_PASTE_ENABLED = false
DEBUG = false
+silenceNotifications = false
query = null
if 'undefined' != typeof window && window.location
query = Query.parse(window.location.search.substr(1))
@@ -190,6 +191,7 @@ dbg = (msg) -> log msg if true == snowflake.ui.debug
init = ->
ui = new UI()
+ silenceNotifications = Params.getBool(query, 'silent', false)
brokerUrl = Params.getString(query, 'broker', DEFAULT_BROKER)
broker = new Broker brokerUrl
snowflake = new Snowflake broker, ui
@@ -205,7 +207,7 @@ init = ->
# Notification of closing tab with active proxy.
# TODO: Opt-in/out parameter or cookie
window.onbeforeunload = ->
- if MODE.WEBRTC_READY == snowflake.state
+ if !silenceNotifications && MODE.WEBRTC_READY == snowflake.state
return CONFIRMATION_MESSAGE
null
diff --git a/proxy/spec/snowflake.spec.coffee b/proxy/spec/snowflake.spec.coffee
index f3fbae8..9b63419 100644
--- a/proxy/spec/snowflake.spec.coffee
+++ b/proxy/spec/snowflake.spec.coffee
@@ -61,6 +61,7 @@ describe 'Snowflake', ->
expect(s.proxyPairs.length).toBe 2
it 'gives a dialog when closing, only while active', ->
+ silenceNotifications = false
snowflake.state = MODE.WEBRTC_READY
msg = window.onbeforeunload()
expect(snowflake.state).toBe MODE.WEBRTC_READY
@@ -70,3 +71,10 @@ describe 'Snowflake', ->
msg = window.onbeforeunload()
expect(snowflake.state).toBe MODE.INIT
expect(msg).toBe null
+
+ it 'does not give a dialog when silent flag is on', ->
+ silenceNotifications = true
+ snowflake.state = MODE.WEBRTC_READY
+ msg = window.onbeforeunload()
+ expect(snowflake.state).toBe MODE.WEBRTC_READY
+ expect(msg).toBe null
1
0

[snowflake/master] fix datachannel remote vs. local closing during client's peerconnection cleanup
by serene@torproject.org 08 Mar '16
by serene@torproject.org 08 Mar '16
08 Mar '16
commit 91673a4abe85e61cc397c5c4c359c80382958e46
Author: Serene Han <keroserene+git(a)gmail.com>
Date: Sun Mar 6 11:40:00 2016 -0800
fix datachannel remote vs. local closing during client's peerconnection cleanup
---
client/webrtc.go | 46 +++++++++++++++++++++++++++++-----------------
1 file changed, 29 insertions(+), 17 deletions(-)
diff --git a/client/webrtc.go b/client/webrtc.go
index 2388e9a..e2e8280 100644
--- a/client/webrtc.go
+++ b/client/webrtc.go
@@ -48,17 +48,7 @@ func (c *webRTCConn) Write(b []byte) (int, error) {
func (c *webRTCConn) Close() error {
var err error = nil
log.Printf("WebRTC: Closing")
- if nil != c.snowflake {
- s := c.snowflake
- c.snowflake = nil
- log.Printf("WebRTC: closing DataChannel")
- s.Close()
- }
- if nil != c.pc {
- log.Printf("WebRTC: closing PeerConnection")
- err = c.pc.Close()
- c.pc = nil
- }
+ c.cleanup()
close(c.offerChannel)
close(c.answerChannel)
close(c.errorChannel)
@@ -127,9 +117,12 @@ func (c *webRTCConn) ConnectLoop() {
<-c.reset
log.Println(" --- snowflake connection reset ---")
}
+ <-time.After(time.Second * 1)
+ c.cleanup()
}
}
+// Create and prepare callbacks on a new WebRTC PeerConnection.
func (c *webRTCConn) preparePeerConnection() {
if nil != c.pc {
c.pc.Close()
@@ -178,6 +171,9 @@ func (c *webRTCConn) preparePeerConnection() {
// Create a WebRTC DataChannel locally.
func (c *webRTCConn) establishDataChannel() error {
+ if c.snowflake != nil {
+ panic("Unexpected datachannel already exists!")
+ }
dc, err := c.pc.CreateDataChannel("snowflake", webrtc.Init{})
// Triggers "OnNegotiationNeeded" on the PeerConnection, which will prepare
// an SDP offer while other goroutines operating on this struct handle the
@@ -201,20 +197,20 @@ func (c *webRTCConn) establishDataChannel() error {
c.snowflake = dc
}
dc.OnClose = func() {
- // Disable the DataChannel as a write destination.
// Future writes will go to the buffer until a new DataChannel is available.
- log.Println("WebRTC: DataChannel.OnClose")
- // Only reset if this OnClose was triggered remotely.
if nil == c.snowflake {
- panic("Should not have nil snowflake before closing. ")
+ // Closed locally, as part of a reset.
+ log.Println("WebRTC: DataChannel.OnClose [locally]")
+ return
}
+ // Closed remotely, need to reset everything.
+ // Disable the DataChannel as a write destination.
+ log.Println("WebRTC: DataChannel.OnClose [remotely]")
c.snowflake = nil
// TODO: Need a way to update the circuit so that when a new WebRTC
// data channel is available, the relay actually recognizes the new
// snowflake?
c.Reset()
- // c.Close()
- // c.endChannel <- struct{}{}
}
dc.OnMessage = func(msg []byte) {
// log.Println("ONMESSAGE: ", len(msg))
@@ -290,3 +286,19 @@ func (c *webRTCConn) Reset() {
log.Println("WebRTC resetting...")
}()
}
+
+func (c *webRTCConn) cleanup() {
+ if nil != c.snowflake {
+ s := c.snowflake
+ log.Printf("WebRTC: closing DataChannel")
+ // Setting snowflak to nil *before* Close indicates to OnClose that it
+ // was locally triggered.
+ c.snowflake = nil
+ s.Close()
+ }
+ if nil != c.pc {
+ log.Printf("WebRTC: closing PeerConnection")
+ c.pc.Close()
+ c.pc = nil
+ }
+}
1
0