nyx-2.0.4/0000775000175000017500000000000013200142726013052 5ustar atagaratagar00000000000000nyx-2.0.4/test/0000775000175000017500000000000013200142726014031 5ustar atagaratagar00000000000000nyx-2.0.4/test/settings.cfg0000664000175000017500000000175513146406365016375 0ustar atagaratagar00000000000000# Pycodestyle compliance issues that we're ignoreing. pycodestyle.ignore E111 pycodestyle.ignore E114 pycodestyle.ignore E121 pycodestyle.ignore E501 pycodestyle.ignore E251 pycodestyle.ignore E127 pycodestyle.ignore E722 # False positives from pyflakes. These are mappings between the path and the # issue. pycodestyle.ignore nyx/__init__.py => E402: import nyx.curses pycodestyle.ignore nyx/__init__.py => E402: import nyx.menu pycodestyle.ignore nyx/__init__.py => E402: import nyx.panel pycodestyle.ignore nyx/__init__.py => E402: import nyx.panel.config pycodestyle.ignore nyx/__init__.py => E402: import nyx.panel.connection pycodestyle.ignore nyx/__init__.py => E402: import nyx.popups pycodestyle.ignore nyx/__init__.py => E402: import nyx.starter pyflakes.ignore nyx/prereq.py => 'stem' imported but unused pyflakes.ignore nyx/prereq.py => 'curses' imported but unused pyflakes.ignore run_tests.py => 'pyflakes' imported but unused pyflakes.ignore run_tests.py => 'pep8' imported but unused nyx-2.0.4/test/__init__.py0000664000175000017500000001117613177120634016157 0ustar atagaratagar00000000000000""" Unit tests for nyx. """ import collections import inspect import time import unittest import nyx.curses from nyx import expand_path, chroot, join, uses_settings try: # added in python 3.3 from unittest.mock import Mock, patch except ImportError: from mock import Mock, patch __all__ = [ 'arguments', 'curses', 'installation', 'log', 'menu', 'panel', 'popups', 'tracker', ] OUR_SCREEN_SIZE = None TEST_SCREEN_SIZE = nyx.curses.Dimensions(80, 25) RenderResult = collections.namedtuple('RenderResult', ['content', 'return_value', 'runtime']) def require_curses(func): """ Skips the test unless curses is available with a minimal dimension needed by our tests. """ if OUR_SCREEN_SIZE is None: def _check_screen_size(): global OUR_SCREEN_SIZE OUR_SCREEN_SIZE = nyx.curses.screen_size() nyx.curses.start(_check_screen_size) def wrapped(self, *args, **kwargs): if OUR_SCREEN_SIZE.width < TEST_SCREEN_SIZE.width: self.skipTest("screen isn't wide enough") elif OUR_SCREEN_SIZE.height < TEST_SCREEN_SIZE.height: self.skipTest("screen isn't tall enough") else: with patch('nyx.curses.screen_size', Mock(return_value = TEST_SCREEN_SIZE)): return func(self, *args, **kwargs) return wrapped class mock_keybindings(object): """ Mocks the given keyboard inputs. """ def __init__(self, *keys): self._mock = patch('nyx.curses.key_input', side_effect = [nyx.curses.KeyInput(key) for key in keys]) def __enter__(self, *args): self._mock.__enter__(*args) def __exit__(self, *args): self._mock.__exit__(*args) def render(func, *args, **kwargs): """ Runs the given curses function, providing content that's rendered on the screen. If the function starts with an argument named 'subwindow' then it's provided one through :func:`~nyx.curses.draw`. :param function func: draw function to be invoked :returns: :data:`~test.RenderResult` with information about what was rendered """ attr = {} def draw_func(): nyx.curses._disable_acs() nyx.curses.CURSES_SCREEN.erase() start_time = time.time() func_args = inspect.getargspec(func).args if func_args[:1] == ['subwindow'] or func_args[:2] == ['self', 'subwindow']: def _draw(subwindow): return func(subwindow, *args, **kwargs) attr['return_value'] = nyx.curses.draw(_draw) else: attr['return_value'] = func(*args, **kwargs) attr['runtime'] = time.time() - start_time attr['content'] = nyx.curses.screenshot() with patch('nyx.curses.key_input', return_value = nyx.curses.KeyInput(27)): nyx.curses.start(draw_func, transparent_background = True, cursor = False) return RenderResult(attr.get('content'), attr.get('return_value'), attr.get('runtime')) class TestBaseUtil(unittest.TestCase): def setUp(self): nyx.CHROOT = None def tearDown(self): nyx.CHROOT = None @patch('nyx.chroot', Mock(return_value = '')) @patch('nyx.tor_controller', Mock()) @patch('stem.util.system.cwd', Mock(return_value = '/your_cwd')) def test_expand_path(self): self.assertEqual('/absolute/path/to/torrc', expand_path('/absolute/path/to/torrc')) self.assertEqual('/your_cwd/torrc', expand_path('torrc')) @patch('nyx.chroot', Mock(return_value = '/chroot')) @patch('nyx.tor_controller', Mock()) @patch('stem.util.system.cwd', Mock(return_value = '/your_cwd')) def test_expand_path_with_chroot(self): self.assertEqual('/chroot/absolute/path/to/torrc', expand_path('/absolute/path/to/torrc')) self.assertEqual('/chroot/your_cwd/torrc', expand_path('torrc')) @patch('platform.system', Mock(return_value = 'Linux')) @patch('os.path.exists', Mock(return_value = True)) @uses_settings def test_chroot_uses_config(self, config): config.set('tor_chroot', '/chroot/path') self.assertEqual('/chroot/path', chroot()) config.set('tor_chroot', None) @patch('platform.system', Mock(return_value = 'Linux')) @patch('os.path.exists', Mock(return_value = False)) @uses_settings def test_chroot_requires_path_to_exist(self, config): config.set('tor_chroot', '/chroot/path') self.assertEqual('', chroot()) config.set('tor_chroot', None) def test_join(self): # check our pydoc examples self.assertEqual('This is a looooong', join(['This', 'is', 'a', 'looooong', 'message'], size = 18)) self.assertEqual('This is a', join(['This', 'is', 'a', 'looooong', 'message'], size = 17)) self.assertEqual('', join(['This', 'is', 'a', 'looooong', 'message'], size = 2)) # include a joining character self.assertEqual('Download: 5 MB, Upload: 3 MB', join(['Download: 5 MB', 'Upload: 3 MB', 'Other: 2 MB'], ', ', 30)) nyx-2.0.4/test/menu.py0000664000175000017500000001575513146406365015377 0ustar atagaratagar00000000000000""" Unit tests for nyx.menu. """ import curses import unittest import nyx.curses from nyx.menu import MenuItem, Submenu, RadioMenuItem, RadioGroup, MenuCursor class Container(object): value = False def __nonzero__(self): return bool(self.value) # for python 2.x def __bool__(self): return bool(self.value) # for python 3.x def action(*args): IS_CALLED.value = args if args else True def menu_cursor(*key_inputs): cursor = MenuCursor(INITIAL_SELECTION) for key in key_inputs: cursor.handle_key(nyx.curses.KeyInput(key)) return cursor def no_op(): pass IS_CALLED = Container() TEST_MENU = Submenu('Root Submenu', [ Submenu('Submenu 1', [ MenuItem('Item 1', action, 'selected 1'), MenuItem('Item 2', action, 'selected 2'), Submenu('Inner Submenu', [ MenuItem('Item 3', action, 'selected 3'), ]), Submenu('Empty Submenu', []), ]), Submenu('Submenu 2', [ MenuItem('Item 4', action, 'selected 1'), MenuItem('Item 5', action, 'selected 2'), ]) ]) INITIAL_SELECTION = TEST_MENU.children[0].children[0] class TestMenuItem(unittest.TestCase): def setUp(self): IS_CALLED.value = False def test_parameters(self): menu_item = MenuItem('Test Item', no_op) self.assertEqual('', menu_item.prefix) self.assertEqual('Test Item', menu_item.label) self.assertEqual('', menu_item.suffix) self.assertEqual(None, menu_item.next) self.assertEqual(None, menu_item.prev) self.assertEqual(None, menu_item.parent) self.assertEqual(menu_item, menu_item.submenu) def test_selection(self): menu_item = MenuItem('Test Item', action) menu_item.select() self.assertTrue(IS_CALLED) def test_selection_with_value(self): menu_item = MenuItem('Test Item', action, 'hi') menu_item.select() self.assertEqual(('hi',), IS_CALLED.value) def test_menu_item_hierarchy(self): root_submenu = Submenu('Root Submenu') middle_submenu = Submenu('Middle Submenu') root_submenu.add(MenuItem('Middle Item 1', no_op)) root_submenu.add(MenuItem('Middle Item 2', no_op)) root_submenu.add(middle_submenu) bottom_item = MenuItem('Bottom Item', no_op) middle_submenu.add(bottom_item) self.assertEqual(middle_submenu, bottom_item.parent) self.assertEqual(middle_submenu, bottom_item.submenu) self.assertEqual(bottom_item, bottom_item.next) self.assertEqual(bottom_item, bottom_item.prev) self.assertEqual(root_submenu, middle_submenu.parent) self.assertEqual(middle_submenu, middle_submenu.submenu) self.assertEqual('Middle Item 1', middle_submenu.next.label) self.assertEqual('Middle Item 2', middle_submenu.prev.label) class TestSubmenu(unittest.TestCase): def test_parameters(self): menu_item = Submenu('Test Item') self.assertEqual('', menu_item.prefix) self.assertEqual('Test Item', menu_item.label) self.assertEqual(' >', menu_item.suffix) self.assertEqual(None, menu_item.next) self.assertEqual(None, menu_item.prev) self.assertEqual(None, menu_item.parent) self.assertEqual(menu_item, menu_item.submenu) self.assertEqual([], menu_item.children) menu_item = Submenu('Test Item', [ MenuItem('Test Item 1', no_op), MenuItem('Test Item 2', no_op), ]) self.assertEqual(2, len(menu_item.children)) def test_add(self): submenu = Submenu('Menu') item_1 = MenuItem('Test Item 1', no_op) item_2 = MenuItem('Test Item 2', no_op) self.assertEqual([], submenu.children) submenu.add(item_1) self.assertEqual([item_1], submenu.children) submenu.add(item_2) self.assertEqual([item_1, item_2], submenu.children) def test_add_raises_when_already_in_menu(self): submenu_1 = Submenu('Menu 1') submenu_2 = Submenu('Menu 2') item = MenuItem('Test Item', no_op) submenu_1.add(item) self.assertRaises(ValueError, submenu_2.add, item) class TestRadioMenuItem(unittest.TestCase): def setUp(self): IS_CALLED.value = False def test_parameters(self): group = RadioGroup(no_op, 'selected_item') menu_item = RadioMenuItem('Test Item', group, 'selected_item') self.assertEqual('[X] ', menu_item.prefix) self.assertEqual('Test Item', menu_item.label) self.assertEqual('', menu_item.suffix) self.assertEqual(None, menu_item.next) self.assertEqual(None, menu_item.prev) self.assertEqual(None, menu_item.parent) self.assertEqual(menu_item, menu_item.submenu) def test_selection(self): group = RadioGroup(action, 'other_item') menu_item = RadioMenuItem('Test Item', group, 'selected_item') menu_item.select() self.assertTrue(IS_CALLED) def test_when_already_selected(self): group = RadioGroup(action, 'selected_item') menu_item = RadioMenuItem('Test Item', group, 'selected_item') menu_item.select() self.assertFalse(IS_CALLED) class TestMenuCursor(unittest.TestCase): def setUp(self): IS_CALLED.value = False def test_selection_of_item(self): cursor = menu_cursor(curses.KEY_ENTER) self.assertEqual(('selected 1',), IS_CALLED.value) self.assertTrue(cursor.is_done) def test_selection_of_submenu(self): cursor = menu_cursor(curses.KEY_DOWN, curses.KEY_DOWN, curses.KEY_ENTER) self.assertEqual('Item 3', cursor.selection.label) self.assertFalse(IS_CALLED.value) self.assertFalse(cursor.is_done) def test_up(self): cursor = menu_cursor() for expected in ('Item 1', 'Empty Submenu', 'Inner Submenu', 'Item 2', 'Item 1'): self.assertEqual(expected, cursor.selection.label) cursor.handle_key(nyx.curses.KeyInput(curses.KEY_UP)) def test_down(self): cursor = menu_cursor() for expected in ('Item 1', 'Item 2', 'Inner Submenu', 'Empty Submenu', 'Item 1'): self.assertEqual(expected, cursor.selection.label) cursor.handle_key(nyx.curses.KeyInput(curses.KEY_DOWN)) def test_left(self): cursor = menu_cursor() for expected in ('Item 1', 'Item 4', 'Item 1'): self.assertEqual(expected, cursor.selection.label) cursor.handle_key(nyx.curses.KeyInput(curses.KEY_LEFT)) def test_left_when_inner_submenu(self): cursor = menu_cursor(curses.KEY_DOWN, curses.KEY_DOWN, curses.KEY_RIGHT) for expected in ('Item 3', 'Inner Submenu', 'Item 4'): self.assertEqual(expected, cursor.selection.label) cursor.handle_key(nyx.curses.KeyInput(curses.KEY_LEFT)) def test_right(self): cursor = menu_cursor() for expected in ('Item 1', 'Item 4', 'Item 1'): self.assertEqual(expected, cursor.selection.label) cursor.handle_key(nyx.curses.KeyInput(curses.KEY_RIGHT)) def test_right_when_inner_submenu(self): cursor = menu_cursor(curses.KEY_DOWN, curses.KEY_DOWN) for expected in ('Inner Submenu', 'Item 3', 'Item 4', 'Item 1'): self.assertEqual(expected, cursor.selection.label) cursor.handle_key(nyx.curses.KeyInput(curses.KEY_RIGHT)) def test_esc(self): cursor = menu_cursor(27) self.assertTrue(cursor.is_done) # pressing 'm' closes the menu too cursor = menu_cursor(ord('m')) self.assertTrue(cursor.is_done) nyx-2.0.4/test/panel/0000775000175000017500000000000013200142726015130 5ustar atagaratagar00000000000000nyx-2.0.4/test/panel/config.py0000664000175000017500000000363213177120634016762 0ustar atagaratagar00000000000000""" Unit tests for nyx.panel.config. """ import unittest import nyx.panel.config import test from test import require_curses try: # added in python 3.3 from unittest.mock import patch except ImportError: from mock import patch EXPECTED_LINE = 'ControlPort 9051 Port providing access to tor...' EXPECTED_DETAIL_DIALOG = """ +------------------------------------------------------------------------------+ | ControlPort (General Option) | | Value: 9051 (custom, LineList, usage: PORT|unix:path|auto [flags]) | | Description: If set, Tor will accept connections on this port and allow those| | connections to control the Tor process using the Tor Control Protocol (des-| | cribed in control-spec.txt in torspec). Note: unless you also specify one | | or more of HashedControlPassword or CookieAuthentication, setting this... | +------------------------------------------------------------------------------+ """.strip() class TestConfigPanel(unittest.TestCase): @require_curses @patch('nyx.panel.config.tor_controller') def test_draw_line(self, tor_controller_mock): tor_controller_mock().get_info.return_value = True tor_controller_mock().get_conf.return_value = ['9051'] entry = nyx.panel.config.ConfigEntry('ControlPort', 'LineList') rendered = test.render(nyx.panel.config._draw_line, 0, 0, entry, False, 10, 35) self.assertEqual(EXPECTED_LINE, rendered.content) @require_curses @patch('nyx.panel.config.tor_controller') def test_draw_selection_details(self, tor_controller_mock): tor_controller_mock().get_info.return_value = True tor_controller_mock().get_conf.return_value = ['9051'] selected = nyx.panel.config.ConfigEntry('ControlPort', 'LineList') rendered = test.render(nyx.panel.config._draw_selection_details, selected) self.assertEqual(EXPECTED_DETAIL_DIALOG, rendered.content) nyx-2.0.4/test/panel/__init__.py0000664000175000017500000000022313141704000017227 0ustar atagaratagar00000000000000""" Unit tests for nyx's panel modules. """ __all__ = [ 'header', 'graph', 'interpreter', 'log', 'connection', 'config', 'torrc', ] nyx-2.0.4/test/panel/graph.py0000664000175000017500000001146313177120634016617 0ustar atagaratagar00000000000000""" Unit tests for nyx.panel.graph. """ import datetime import unittest import stem.control import nyx.curses import nyx.panel.graph import test from test import require_curses try: # added in python 3.3 from unittest.mock import patch except ImportError: from mock import patch EXPECTED_BLANK_GRAPH = """ Download: 0 B 0 B 5s 10 15 """.rstrip() EXPECTED_ACCOUNTING = """ Accounting (awake) Time to reset: 01:02 4.7 KB / 105.3 KB 2.0 KB / 9.3 KB """.strip() EXPECTED_GRAPH = """ Download: 7 KB * * 3 KB ** * * **** 0 B ********* 5s 10 15 """.rstrip() class TestGraphPanel(unittest.TestCase): def test_x_axis_labels(self): test_inputs = { 0: {}, 7: {}, 10: {5: '25s'}, 15: {5: '25s', 10: '50'}, 20: {5: '25s', 10: '50', 15: '1m'}, 25: {5: '25s', 10: '50', 15: '1m', 20: '1.6'}, 45: {5: '25s', 10: '50', 15: '1m', 20: '1.6', 25: '2.0', 30: '2.5', 35: '2.9', 40: '3.3'}, 80: {10: '50s', 20: '1m', 30: '2.5', 40: '3.3', 50: '4.1', 60: '5.0', 70: '5.8'}, # spaced more since wide } for width, expected in test_inputs.items(): self.assertEqual(expected, nyx.panel.graph._x_axis_labels(nyx.panel.graph.Interval.FIVE_SECONDS, width)) test_inputs = { nyx.panel.graph.Interval.EACH_SECOND: { 10: '10s', 20: '20', 30: '30', 40: '40', 50: '50', 60: '1m', 70: '1.1' }, nyx.panel.graph.Interval.FIVE_SECONDS: { 10: '50s', 20: '1m', 30: '2.5', 40: '3.3', 50: '4.1', 60: '5.0', 70: '5.8' }, nyx.panel.graph.Interval.THIRTY_SECONDS: { 10: '5m', 20: '10', 30: '15', 40: '20', 50: '25', 60: '30', 70: '35' }, nyx.panel.graph.Interval.MINUTELY: { 10: '10m', 20: '20', 30: '30', 40: '40', 50: '50', 60: '1h', 70: '1.1' }, nyx.panel.graph.Interval.FIFTEEN_MINUTE: { 10: '2h', 20: '5', 30: '7', 40: '10', 50: '12', 60: '15', 70: '17' }, nyx.panel.graph.Interval.THIRTY_MINUTE: { 10: '5h', 20: '10', 30: '15', 40: '20', 50: '1d', 60: '1.2', 70: '1.4' }, nyx.panel.graph.Interval.HOURLY: { 10: '10h', 20: '20', 30: '1d', 40: '1.6', 50: '2.0', 60: '2.5', 70: '2.9' }, nyx.panel.graph.Interval.DAILY: { 10: '10d', 20: '20', 30: '30', 40: '40', 50: '50', 60: '60', 70: '70' }, } for interval, expected in test_inputs.items(): self.assertEqual(expected, nyx.panel.graph._x_axis_labels(interval, 80)) def test_y_axis_labels(self): data = nyx.panel.graph.ConnectionStats() # check with both even and odd height since that determines an offset in the middle self.assertEqual({2: '10', 4: '7', 6: '5', 9: '2', 11: '0'}, nyx.panel.graph._y_axis_labels(12, data.primary, 0, 10)) self.assertEqual({2: '10', 4: '6', 6: '3', 8: '0'}, nyx.panel.graph._y_axis_labels(9, data.primary, 0, 10)) # check where the min and max are the same self.assertEqual({2: '0', 11: '0'}, nyx.panel.graph._y_axis_labels(12, data.primary, 0, 0)) @require_curses @patch('nyx.panel.graph.tor_controller') def test_draw_subgraph_blank(self, tor_controller_mock): tor_controller_mock().get_info.return_value = None data = nyx.panel.graph.BandwidthStats() rendered = test.render(nyx.panel.graph._draw_subgraph, data.primary, 0, 30, 7, nyx.panel.graph.Bounds.LOCAL_MAX, nyx.panel.graph.Interval.EACH_SECOND, nyx.curses.Color.CYAN, '*') self.assertEqual(EXPECTED_BLANK_GRAPH, rendered.content) @require_curses @patch('nyx.panel.graph.tor_controller') def test_draw_subgraph(self, tor_controller_mock): tor_controller_mock().get_info.return_value = '5430,5430 4210,4210 5510,5510 7100,7100 2000,2000 1750,1750 1880,1880 2500,2500 3770,3770' data = nyx.panel.graph.BandwidthStats() rendered = test.render(nyx.panel.graph._draw_subgraph, data.primary, 0, 30, 7, nyx.panel.graph.Bounds.LOCAL_MAX, nyx.panel.graph.Interval.EACH_SECOND, nyx.curses.Color.CYAN, '*') self.assertEqual(EXPECTED_GRAPH, rendered.content) @require_curses @patch('nyx.panel.graph.tor_controller') def test_draw_accounting_stats(self, tor_controller_mock): tor_controller_mock().is_alive.return_value = True accounting_stat = stem.control.AccountingStats( 1410723598.276578, 'awake', datetime.datetime(2014, 9, 14, 19, 41), 62, 4837, 102944, 107781, 2050, 7440, 9490, ) rendered = test.render(nyx.panel.graph._draw_accounting_stats, 0, accounting_stat) self.assertEqual(EXPECTED_ACCOUNTING, rendered.content) @require_curses @patch('nyx.panel.graph.tor_controller') def test_draw_accounting_stats_disconnected(self, tor_controller_mock): tor_controller_mock().is_alive.return_value = False rendered = test.render(nyx.panel.graph._draw_accounting_stats, 0, None) self.assertEqual('Accounting: Connection Closed...', rendered.content) nyx-2.0.4/test/panel/header.py0000664000175000017500000003262313177120634016747 0ustar atagaratagar00000000000000""" Unit tests for nyx.panel.header. """ import time import unittest import stem.control import stem.exit_policy import stem.version import stem.util.system import nyx.panel.header import test from test import require_curses try: # added in python 3.3 from unittest.mock import Mock, patch except ImportError: from mock import Mock, patch EXPECTED_PANEL = """ nyx - odin (Linux 3.5.0-54-generic) Tor 0.2.8.1-alpha-dev Unnamed - 174.21.17.28:7000, Dir Port: 7001, Control Port (cookie): 7002 cpu: 12.3% tor, 5.7% nyx mem: 11 MB (2.1%) pid: 765 fingerprint: 1A94D1A794FCB2F8B6CBC179EF8FDD4008A98D3B flags: Running, Exit page 2 / 4 - m: menu, p: pause, h: page help, q: quit """.strip() def test_sampling(): return nyx.panel.header.Sampling( retrieved = 1234.5, is_connected = True, connection_time = 2345.6, last_heartbeat = 3456.7, fingerprint = '1A94D1A794FCB2F8B6CBC179EF8FDD4008A98D3B', nickname = 'Unnamed', newnym_wait = None, exit_policy = stem.exit_policy.ExitPolicy('reject *:*'), flags = ['Running', 'Exit'], version = '0.2.8.1-alpha-dev', version_status = 'unrecommended', address = '174.21.17.28', or_port = '7000', dir_port = '7001', control_port = '7002', socket_path = None, is_relay = True, auth_type = 'cookie', pid = '765', start_time = 4567.8, fd_limit = 100, fd_used = 40, nyx_total_cpu_time = 100, tor_cpu = '12.3', nyx_cpu = '5.7', memory = '11 MB', memory_percent = '2.1', hostname = 'odin', platform = 'Linux 3.5.0-54-generic', ) class TestHeaderPanel(unittest.TestCase): @require_curses @patch('nyx.panel.header.nyx_interface') @patch('nyx.panel.header.tor_controller') @patch('nyx.panel.header.Sampling.create') def test_rendering_panel(self, sampling_mock, tor_controller_mock, nyx_interface_mock): nyx_interface_mock().is_paused.return_value = False nyx_interface_mock().get_page.return_value = 1 nyx_interface_mock().page_count.return_value = 4 sampling_mock.return_value = test_sampling() panel = nyx.panel.header.HeaderPanel() self.assertEqual(EXPECTED_PANEL, test.render(panel._draw).content) @patch('nyx.panel.header.tor_controller') @patch('nyx.tracker.get_resource_tracker') @patch('nyx.tracker.get_consensus_tracker') @patch('time.time', Mock(return_value = 1234.5)) @patch('os.times', Mock(return_value = (0.08, 0.03, 0.0, 0.0, 18759021.31))) @patch('os.uname', Mock(return_value = ('Linux', 'odin', '3.5.0-54-generic', '#81~precise1-Ubuntu SMP Tue Jul 15 04:05:58 UTC 2014', 'i686'))) @patch('stem.util.system.start_time', Mock(return_value = 5678)) @patch('stem.util.proc.file_descriptors_used', Mock(return_value = 89)) def test_sample(self, consensus_tracker_mock, resource_tracker_mock, tor_controller_mock): tor_controller_mock().is_alive.return_value = True tor_controller_mock().connection_time.return_value = 567.8 tor_controller_mock().get_latest_heartbeat.return_value = 89.0 tor_controller_mock().get_newnym_wait.return_value = 0 tor_controller_mock().get_exit_policy.return_value = stem.exit_policy.ExitPolicy('reject *:*') tor_controller_mock().get_version.return_value = stem.version.Version('0.1.2.3-tag') tor_controller_mock().get_pid.return_value = '123' tor_controller_mock().get_info.side_effect = lambda param, default = None: { 'address': '174.21.17.28', 'fingerprint': '1A94D1A794FCB2F8B6CBC179EF8FDD4008A98D3B', 'status/version/current': 'recommended', 'process/descriptor-limit': 678, }[param] tor_controller_mock().get_conf.side_effect = lambda param, default = None: { 'Nickname': 'Unnamed', 'HashedControlPassword': None, 'CookieAuthentication': '1', 'DirPort': '7001', 'ControlSocket': None, }[param] tor_controller_mock().get_listeners.side_effect = lambda param, default = None: { stem.control.Listener.OR: [('0.0.0.0', 7000)], stem.control.Listener.CONTROL: [('0.0.0.0', 9051)], }[param] resources = Mock() resources.cpu_sample = 6.7 resources.memory_bytes = 62464 resources.memory_percent = .125 resource_tracker_mock().get_value.return_value = resources stem.util.system.SYSTEM_CALL_TIME = 0.0 consensus_tracker_mock().my_router_status_entry.return_value = None vals = nyx.panel.header.Sampling.create() self.assertEqual(1234.5, vals.retrieved) self.assertEqual(True, vals.is_connected) self.assertEqual(567.8, vals.connection_time) self.assertEqual(89.0, vals.last_heartbeat) self.assertEqual('1A94D1A794FCB2F8B6CBC179EF8FDD4008A98D3B', vals.fingerprint) self.assertEqual('Unnamed', vals.nickname) self.assertEqual(0, vals.newnym_wait) self.assertEqual(stem.exit_policy.ExitPolicy('reject *:*'), vals.exit_policy) self.assertEqual([], vals.flags) self.assertEqual('0.1.2.3-tag', vals.version) self.assertEqual('recommended', vals.version_status) self.assertEqual('174.21.17.28', vals.address) self.assertEqual(7000, vals.or_port) self.assertEqual('7001', vals.dir_port) self.assertEqual('9051', vals.control_port) self.assertEqual(None, vals.socket_path) self.assertEqual(True, vals.is_relay) self.assertEqual('cookie', vals.auth_type) self.assertEqual('123', vals.pid) self.assertEqual(5678, vals.start_time) self.assertEqual(678, vals.fd_limit) self.assertEqual(89, vals.fd_used) self.assertEqual(0.11, vals.nyx_total_cpu_time) self.assertEqual('670.0', vals.tor_cpu) self.assertEqual('0.0', vals.nyx_cpu) self.assertEqual('61 KB', vals.memory) self.assertEqual('12.5', vals.memory_percent) self.assertEqual('odin', vals.hostname) self.assertEqual('Linux 3.5.0-54-generic', vals.platform) def test_sample_format(self): vals = nyx.panel.header.Sampling( version = '0.2.8.1', version_status = 'unrecommended', ) self.assertEqual('0.2.8.1 is unrecommended', vals.format('{version} is {version_status}')) test_input = { 25: '0.2.8.1 is unrecommended', 20: '0.2.8.1 is unreco...', 15: '0.2.8.1 is...', 10: '0.2.8.1...', 5: '', 0: '', } for width, expected in test_input.items(): self.assertEqual(expected, vals.format('{version} is {version_status}', width)) @require_curses def test_draw_platform_section(self): vals = nyx.panel.header.Sampling( hostname = 'odin', platform = 'Linux 3.5.0-54-generic', version = '0.2.8.1-alpha-dev', version_status = 'unrecommended', ) test_input = { 80: 'nyx - odin (Linux 3.5.0-54-generic) Tor 0.2.8.1-alpha-dev', 70: 'nyx - odin (Linux 3.5.0-54-generic) Tor 0.2.8.1-alpha-dev', 60: 'nyx - odin (Linux 3.5.0-54-generic) Tor 0.2.8.1-al...', 50: 'nyx - odin (Linux 3.5.0-54-generic)', 40: 'nyx - odin (Linux 3.5.0-54-generic)', 30: 'nyx - odin (Linux 3.5.0-54...)', 20: 'nyx - odin (Linu...)', 10: 'nyx - odin', 0: '', } for width, expected in test_input.items(): self.assertEqual(expected, test.render(nyx.panel.header._draw_platform_section, 0, 0, width, vals).content) @require_curses def test_draw_platform_section_without_version(self): vals = nyx.panel.header.Sampling( hostname = 'odin', platform = 'Linux 3.5.0-54-generic', version = 'Unknown', ) rendered = test.render(nyx.panel.header._draw_platform_section, 0, 0, 80, vals) self.assertEqual('nyx - odin (Linux 3.5.0-54-generic)', rendered.content) @require_curses def test_draw_ports_section(self): vals = nyx.panel.header.Sampling( nickname = 'Unnamed', address = '174.21.17.28', or_port = '7000', dir_port = '7001', control_port = '9051', auth_type = 'cookie', is_relay = True, ) test_input = { 80: 'Unnamed - 174.21.17.28:7000, Dir Port: 7001, Control Port (cookie): 9051', 50: 'Unnamed - 174.21.17.28:7000, Dir Port: 7001, Control Port: 9051', 0: 'Unnamed - 174.21.17.28:7000, Dir Port: 7001, Control Port: 9051', } for width, expected in test_input.items(): self.assertEqual(expected, test.render(nyx.panel.header._draw_ports_section, 0, 0, width, vals).content) @require_curses def test_draw_ports_section_with_relaying(self): vals = nyx.panel.header.Sampling( control_port = None, socket_path = '/path/to/control/socket', is_relay = False, ) self.assertEqual('Relaying Disabled, Control Socket: /path/to/control/socket', test.render(nyx.panel.header._draw_ports_section, 0, 0, 80, vals).content) @require_curses @patch('time.localtime') def test_draw_disconnected(self, localtime_mock): localtime_mock.return_value = time.strptime('22:43 04/09/2016', '%H:%M %m/%d/%Y') self.assertEqual('Tor Disconnected (22:43 04/09/2016, press r to reconnect)', test.render(nyx.panel.header._draw_disconnected, 0, 0, 1460267022.231895).content) @require_curses def test_draw_resource_usage(self): vals = nyx.panel.header.Sampling( start_time = 1460166022.231895, connection_time = 1460267022.231895, is_connected = False, tor_cpu = '2.1', nyx_cpu = '5.4', memory = '118 MB', memory_percent = '3.0', pid = '22439', ) test_input = { 80: 'cpu: 2.1% tor, 5.4% nyx mem: 118 MB (3.0%) pid: 22439 uptime: 1-04:03:20', 70: 'cpu: 2.1% tor, 5.4% nyx mem: 118 MB (3.0%) pid: 22439', 60: 'cpu: 2.1% tor, 5.4% nyx mem: 118 MB (3.0%) pid: 22439', 50: 'cpu: 2.1% tor, 5.4% nyx mem: 118 MB (3.0%)', 40: 'cpu: 2.1% tor, 5.4% nyx', 30: 'cpu: 2.1% tor, 5.4% nyx', 20: '', 10: '', 0: '', } for width, expected in test_input.items(): self.assertEqual(expected, test.render(nyx.panel.header._draw_resource_usage, 0, 0, width, vals, None).content) @require_curses def test_draw_fingerprint_and_fd_usage(self): vals = nyx.panel.header.Sampling( fingerprint = '1A94D1A794FCB2F8B6CBC179EF8FDD4008A98D3B', fd_used = None, ) test_input = { 80: 'fingerprint: 1A94D1A794FCB2F8B6CBC179EF8FDD4008A98D3B', 70: 'fingerprint: 1A94D1A794FCB2F8B6CBC179EF8FDD4008A98D3B', 60: 'fingerprint: 1A94D1A794FCB2F8B6CBC179EF8FDD4008A98D3B', 50: 'fingerprint: 1A94D1A794FCB2F8B6CBC179EF8FDD4008...', 40: 'fingerprint: 1A94D1A794FCB2F8B6CBC179...', 30: 'fingerprint: 1A94D1A794FCB2...', 20: 'fingerprint: 1A94...', 10: 'fingerp...', 0: '', } for width, expected in test_input.items(): self.assertEqual(expected, test.render(nyx.panel.header._draw_fingerprint_and_fd_usage, 0, 0, width, vals).content) @require_curses def test_draw_fingerprint_and_fd_usage_with_fd_count(self): test_input = { 59: 'fingerprint: ', 60: 'fingerprint: , file descriptors: 60 / 100 (60%)', 75: 'fingerprint: , file descriptors: 75 / 100 (75%)', 89: 'fingerprint: , file descriptors: 89 / 100 (89%)', 90: 'fingerprint: , file descriptors: 90 / 100 (90%)', 95: 'fingerprint: , file descriptors: 95 / 100 (95%)', 99: 'fingerprint: , file descriptors: 99 / 100 (99%)', 100: 'fingerprint: , file descriptors: 100 / 100 (100%)', } for fd_used, expected in test_input.items(): vals = nyx.panel.header.Sampling( fingerprint = '', fd_used = fd_used, fd_limit = 100, ) self.assertEqual(expected, test.render(nyx.panel.header._draw_fingerprint_and_fd_usage, 0, 0, 80, vals).content) @require_curses def test_draw_flags(self): self.assertEqual('flags: none', test.render(nyx.panel.header._draw_flags, 0, 0, []).content) self.assertEqual('flags: Guard', test.render(nyx.panel.header._draw_flags, 0, 0, ['Guard']).content) self.assertEqual('flags: Running, Exit', test.render(nyx.panel.header._draw_flags, 0, 0, ['Running', 'Exit']).content) @require_curses def test_draw_exit_policy(self): self.assertEqual('exit policy:', test.render(nyx.panel.header._draw_exit_policy, 0, 0, None).content) self.assertEqual('exit policy: reject *:*', test.render(nyx.panel.header._draw_exit_policy, 0, 0, stem.exit_policy.ExitPolicy('reject *:*')).content) self.assertEqual('exit policy: accept *:80, accept *:443, reject *:*', test.render(nyx.panel.header._draw_exit_policy, 0, 0, stem.exit_policy.ExitPolicy('accept *:80', 'accept *:443', 'reject *:*')).content) @require_curses def test_draw_newnym_option(self): self.assertEqual("press 'n' for a new identity", test.render(nyx.panel.header._draw_newnym_option, 0, 0, 0).content) self.assertEqual('building circuits, available again in 1 second', test.render(nyx.panel.header._draw_newnym_option, 0, 0, 1).content) self.assertEqual('building circuits, available again in 5 seconds', test.render(nyx.panel.header._draw_newnym_option, 0, 0, 5).content) @require_curses @patch('nyx.panel.header.nyx_interface') def test_draw_status(self, nyx_interface_mock): nyx_interface_mock().get_page.return_value = 1 nyx_interface_mock().page_count.return_value = 4 self.assertEqual('page 2 / 4 - m: menu, p: pause, h: page help, q: quit', test.render(nyx.panel.header._draw_status, 0, 0, False, None).content) self.assertEqual('Paused', test.render(nyx.panel.header._draw_status, 0, 0, True, None).content) self.assertEqual('pepperjack is wonderful!', test.render(nyx.panel.header._draw_status, 0, 0, False, 'pepperjack is wonderful!').content) nyx-2.0.4/test/panel/connection.py0000664000175000017500000003134013177440725017657 0ustar atagaratagar00000000000000""" Unit tests for nyx.panel.connection. """ import datetime import unittest import stem.exit_policy import stem.version import nyx.panel.connection import test from nyx.tracker import Connection from nyx.panel.connection import Category, LineType, Line, Entry from test import require_curses try: # added in python 3.3 from unittest.mock import Mock, patch except ImportError: from mock import Mock, patch TIMESTAMP = 1468170303.7 CONNECTION = Connection(TIMESTAMP, False, '127.0.0.1', 3531, '75.119.206.243', 22, 'tcp', False) DETAILS_BUILDING_CIRCUIT = """ +------------------------------------------------------------------------------+ | Building Circuit... | | | | | | | | | | | | | +------------------------------------------------------------------------------+ """.strip() DETAILS_NO_CONSENSUS_DATA = """ +------------------------------------------------------------------------------+ | address: 75.119.206.243:22 | | locale: de | | No consensus data found | | | | | | | | | +------------------------------------------------------------------------------+ """.strip() DETAILS_WHEN_PRIVATE = """ +------------------------------------------------------------------------------+ | address: :22 | | locale: ?? | | No consensus data found | | | | | | | | | +------------------------------------------------------------------------------+ """.strip() DETAILS_FOR_RELAY = """ +------------------------------------------------------------------------------+ | address: 75.119.206.243:22 | | locale: de fingerprint: B6D83EC2D9E18B0A7A33428F8CFA9C536769E209 | | nickname: caerSidi orport: 9051 dirport: 9052 | | published: 17:15 03/01/2012 os: Debian version: 0.2.1.30 | | flags: Fast, HSDir | | exit policy: reject 1-65535 | | contact: spiffy_person@torproject.org | +------------------------------------------------------------------------------+ """.strip() DETAILS_FOR_MULTIPLE_MATCHES = """ +------------------------------------------------------------------------------+ | address: 75.119.206.243:22 | | locale: de | | Multiple matches, possible fingerprints are: | | 1. or port: 52 fingerprint: 1F43EE37A0670301AD9CB555D94AFEC2C89FDE86 | | 2. or port: 80 fingerprint: B6D83EC2D9E18B0A7A33428F8CFA9C536769E209 | | 3. or port: 443 fingerprint: E0BD57A11F00041A9789577C53A1B784473669E4 | | | +------------------------------------------------------------------------------+ """.strip() class MockEntry(Entry): def __init__(self, lines = [], entry_type = Category.INBOUND, is_private = False): self._lines = lines self._type = entry_type self._is_private = is_private def lines(self): return self._lines def get_type(self): return self._type def is_private(self): return self._is_private class MockCircuit(object): def __init__(self, circ_id = 7, status = 'BUILT', purpose = 'GENERAL', path = None): self.id = circ_id self.status = status self.purpose = purpose if path: self.path = path else: self.path = [ ('1F43EE37A0670301AD9CB555D94AFEC2C89FDE86', 'Unnamed'), ('B6D83EC2D9E18B0A7A33428F8CFA9C536769E209', 'moria1'), ('E0BD57A11F00041A9789577C53A1B784473669E4', 'caerSidi'), ] def line(entry = MockEntry(), line_type = LineType.CONNECTION, connection = CONNECTION, circ = MockCircuit(), fingerprint = '1F43EE37A0670301AD9CB555D94AFEC2C89FDE86', nickname = 'Unnamed', locale = 'de'): return Line(entry, line_type, connection, circ, fingerprint, nickname, locale) class TestConnectionPanel(unittest.TestCase): @require_curses def test_draw_title(self): rendered = test.render(nyx.panel.connection._draw_title, [], True) self.assertEqual('Connection Details:', rendered.content) rendered = test.render(nyx.panel.connection._draw_title, [], False) self.assertEqual('Connections:', rendered.content) entries = [MockEntry(entry_type = category) for category in (Category.INBOUND, Category.INBOUND, Category.OUTBOUND, Category.INBOUND, Category.CONTROL)] rendered = test.render(nyx.panel.connection._draw_title, entries, False) self.assertEqual('Connections (3 inbound, 1 outbound, 1 control):', rendered.content) @require_curses def test_draw_details_incomplete_circuit(self): selected = line(line_type = LineType.CIRCUIT_HEADER, circ = MockCircuit(status = 'EXTENDING')) rendered = test.render(nyx.panel.connection._draw_details, selected) self.assertEqual(DETAILS_BUILDING_CIRCUIT, rendered.content) @require_curses @patch('nyx.tracker.get_consensus_tracker') def test_draw_details_no_consensus_data(self, consensus_tracker_mock): consensus_tracker_mock().get_relay_fingerprints.return_value = None rendered = test.render(nyx.panel.connection._draw_details, line()) self.assertEqual(DETAILS_NO_CONSENSUS_DATA, rendered.content) @require_curses @patch('nyx.tracker.get_consensus_tracker') def test_draw_details_when_private(self, consensus_tracker_mock): consensus_tracker_mock().get_relay_fingerprints.return_value = None selected = line(entry = MockEntry(is_private = True)) rendered = test.render(nyx.panel.connection._draw_details, selected) self.assertEqual(DETAILS_WHEN_PRIVATE, rendered.content) @require_curses @patch('nyx.panel.connection.tor_controller') @patch('nyx.tracker.get_consensus_tracker') def test_draw_details_for_relay(self, consensus_tracker_mock, tor_controller_mock): router_status_entry = Mock() router_status_entry.or_port = 9051 router_status_entry.dir_port = 9052 router_status_entry.nickname = 'caerSidi' router_status_entry.flags = ['Fast', 'HSDir'] router_status_entry.published = datetime.datetime(2012, 3, 1, 17, 15, 27) tor_controller_mock().get_network_status.return_value = router_status_entry server_descriptor = Mock() server_descriptor.exit_policy = stem.exit_policy.ExitPolicy('reject *:*') server_descriptor.tor_version = stem.version.Version('0.2.1.30') server_descriptor.operating_system = 'Debian' server_descriptor.contact = 'spiffy_person@torproject.org' tor_controller_mock().get_server_descriptor.return_value = server_descriptor consensus_tracker_mock().get_relay_fingerprints.return_value = { 22: 'B6D83EC2D9E18B0A7A33428F8CFA9C536769E209' } rendered = test.render(nyx.panel.connection._draw_details, line()) self.assertEqual(DETAILS_FOR_RELAY, rendered.content) @require_curses @patch('nyx.tracker.get_consensus_tracker') def test_draw_details_with_multiple_matches(self, consensus_tracker_mock): consensus_tracker_mock().get_relay_fingerprints.return_value = { 52: '1F43EE37A0670301AD9CB555D94AFEC2C89FDE86', 80: 'B6D83EC2D9E18B0A7A33428F8CFA9C536769E209', 443: 'E0BD57A11F00041A9789577C53A1B784473669E4', } rendered = test.render(nyx.panel.connection._draw_details, line()) self.assertEqual(DETAILS_FOR_MULTIPLE_MATCHES, rendered.content) @require_curses @patch('nyx.panel.connection.tor_controller') def test_draw_line(self, tor_controller_mock): tor_controller_mock().is_geoip_unavailable.return_value = False tor_controller_mock().get_info.return_value = '82.121.9.9' test_data = (( line(), ' 75.119.206.243:22 (de) --> 82.121.9.9:3531 15.4s (INBOUND)', ), ( line(entry = MockEntry(entry_type = Category.CIRCUIT), line_type = LineType.CIRCUIT_HEADER), ' 82.121.9.9 --> 75.119.206.243:22 (de) 15.4s (CIRCUIT)', ), ( line(line_type = LineType.CIRCUIT, fingerprint = '1F43EE37A0670301AD9CB555D94AFEC2C89FDE86'), ' | 82.121.9.9 1 / Guard', ), ( line(line_type = LineType.CIRCUIT, fingerprint = 'B6D83EC2D9E18B0A7A33428F8CFA9C536769E209'), ' | 82.121.9.9 2 / Middle', ), ( line(line_type = LineType.CIRCUIT, fingerprint = 'E0BD57A11F00041A9789577C53A1B784473669E4'), ' +- 82.121.9.9 3 / End', )) for test_line, expected in test_data: rendered = test.render(nyx.panel.connection._draw_line, 0, 0, test_line, False, 80, TIMESTAMP + 15.4) self.assertEqual(expected, rendered.content) @require_curses @patch('nyx.panel.connection.tor_controller') def test_draw_address_column(self, tor_controller_mock): tor_controller_mock().is_geoip_unavailable.return_value = False tor_controller_mock().get_info.return_value = '82.121.9.9' test_data = (( line(), '75.119.206.243:22 (de) --> 82.121.9.9:3531', ), ( line(entry = MockEntry(entry_type = Category.EXIT)), '82.121.9.9:3531 --> 75.119.206.243:22 (SSH)', ), ( line(line_type = LineType.CIRCUIT_HEADER, circ = MockCircuit(status = 'EXTENDING')), 'Building... --> 82.121.9.9', ), ( line(line_type = LineType.CIRCUIT), '82.121.9.9', )) for test_line, expected in test_data: rendered = test.render(nyx.panel.connection._draw_address_column, 0, 0, test_line, ()) self.assertEqual(expected, rendered.content) @require_curses @patch('nyx.tracker.get_port_usage_tracker') def test_draw_line_details(self, port_usage_tracker_mock): process = Mock() process.name = 'firefox' process.pid = 722 port_usage_tracker_mock().fetch.return_value = process test_data = (( line(), '1F43EE37A0670301AD9CB555D94AFEC2C89FDE86 Unnamed', ), ( line(line_type = LineType.CIRCUIT_HEADER), 'Purpose: General, Circuit ID: 7', ), ( line(entry = MockEntry(entry_type = Category.CONTROL)), 'firefox (722)', )) for test_line, expected in test_data: rendered = test.render(nyx.panel.connection._draw_line_details, 0, 0, test_line, 80, ()) self.assertEqual(expected, rendered.content) @require_curses def test_draw_right_column(self): rendered = test.render(nyx.panel.connection._draw_right_column, 0, 0, line(), TIMESTAMP + 62, ()) self.assertEqual(' 1.0m (INBOUND)', rendered.content) legacy_connection = Connection(TIMESTAMP, True, '127.0.0.1', 3531, '75.119.206.243', 22, 'tcp', False) test_line = line(entry = MockEntry(entry_type = Category.CONTROL), connection = legacy_connection) rendered = test.render(nyx.panel.connection._draw_right_column, 0, 0, test_line, TIMESTAMP + 68, ()) self.assertEqual('+ 1.1m (CONTROL)', rendered.content) test_data = { '1F43EE37A0670301AD9CB555D94AFEC2C89FDE86': ' 1 / Guard', 'B6D83EC2D9E18B0A7A33428F8CFA9C536769E209': ' 2 / Middle', 'E0BD57A11F00041A9789577C53A1B784473669E4': ' 3 / End', } for fp, expected in test_data.items(): test_line = line(line_type = LineType.CIRCUIT, fingerprint = fp) rendered = test.render(nyx.panel.connection._draw_right_column, 0, 0, test_line, TIMESTAMP + 62, ()) self.assertEqual(expected, rendered.content) nyx-2.0.4/test/panel/torrc.py0000664000175000017500000000536013177120634016646 0ustar atagaratagar00000000000000""" Unit tests for nyx.panel.torrc. """ import unittest import nyx.panel.torrc import test from test import require_curses try: # added in python 3.3 from unittest.mock import Mock, patch except ImportError: from mock import Mock, patch TORRC = """ ORPort 9050 ControlPort 9051 Exitpolicy reject *:* # non-exit relay CookieAuthentication 1 """.strip() RENDERED_DEFAULT = """ Tor Configuration File (/path/to/torrc): 1 ORPort 9050 2 ControlPort 9051 3 Exitpolicy reject *:* # non-exit relay 4 CookieAuthentication 1 """.strip() RENDERED_WITHOUT_COMMENTS = """ Tor Configuration File (/path/to/torrc): 1 ORPort 9050 2 ControlPort 9051 3 Exitpolicy reject *:* 4 CookieAuthentication 1 """.strip() RENDERED_WITHOUT_LINE_NUMBERS = """ Tor Configuration File (/path/to/torrc): ORPort 9050 ControlPort 9051 Exitpolicy reject *:* # non-exit relay CookieAuthentication 1 """.strip() RENDERED_WITH_ERROR = """ Tor Configuration File (/path/to/torrc): Unable to read our torrc: [Errno 2] No such file or directory: '/path/to/torrc' """.strip() class TestGraphPanel(unittest.TestCase): @require_curses @patch('nyx.panel.torrc._read_torrc', Mock(return_value = TORRC.splitlines())) @patch('nyx.panel.torrc.expand_path', Mock(return_value = '/path/to/torrc')) @patch('nyx.panel.torrc.tor_controller', Mock()) def test_draw_with_content(self): panel = nyx.panel.torrc.TorrcPanel() self.assertEqual(RENDERED_DEFAULT, test.render(panel._draw).content) @require_curses @patch('nyx.panel.torrc._read_torrc', Mock(return_value = TORRC.splitlines())) @patch('nyx.panel.torrc.expand_path', Mock(return_value = '/path/to/torrc')) @patch('nyx.panel.torrc.tor_controller', Mock()) def test_draw_without_comments(self): panel = nyx.panel.torrc.TorrcPanel() panel._show_comments = False self.assertEqual(RENDERED_WITHOUT_COMMENTS, test.render(panel._draw).content) @require_curses @patch('nyx.panel.torrc._read_torrc', Mock(return_value = TORRC.splitlines())) @patch('nyx.panel.torrc.expand_path', Mock(return_value = '/path/to/torrc')) @patch('nyx.panel.torrc.tor_controller', Mock()) def test_draw_without_line_numbers(self): panel = nyx.panel.torrc.TorrcPanel() panel._show_line_numbers = False self.assertEqual(RENDERED_WITHOUT_LINE_NUMBERS, test.render(panel._draw).content) @require_curses @patch('nyx.panel.torrc._read_torrc', Mock(side_effect = IOError("[Errno 2] No such file or directory: '/path/to/torrc'"))) @patch('nyx.panel.torrc.expand_path', Mock(return_value = '/path/to/torrc')) @patch('nyx.panel.torrc.tor_controller', Mock()) def test_draw_with_error(self): panel = nyx.panel.torrc.TorrcPanel() panel._show_line_numbers = False self.assertEqual(RENDERED_WITH_ERROR, test.render(panel._draw).content) nyx-2.0.4/test/panel/log.py0000664000175000017500000001475513177120634016306 0ustar atagaratagar00000000000000""" Unit tests for nyx.panel.log. """ import time import unittest import nyx.panel.log import test from nyx.log import LogEntry, LogFilters from test import require_curses try: # added in python 3.3 from unittest.mock import Mock, patch except ImportError: from mock import Mock, patch EXPECTED_WRAPPED_MSG = """\ 16:41:37 [NOTICE] ho hum, ho hum, ho hum, ho hum, ho hum, ho hum, ho hum, ho hum, ho hum, ho hum, ho hum, ho hum, ho hum, ho hum, ho hum, ho hum, ho hum, ho hum, ho hum, ho hum, ho hum... """.rstrip() EXPECTED_ENTRIES = """\ 16:41:37 [NYX_WARNING] Tor's geoip database is unavailable. 16:41:37 [NYX_NOTICE] No nyxrc loaded, using defaults. You can customize nyx by placing a configuration file at /home/atagar/.nyx/nyxrc (see the nyxrc.sample for its options). 16:41:37 [NOTICE] New control connection opened from 127.0.0.1. 16:41:37 [NOTICE] Opening OR listener on 0.0.0.0:7000 16:41:37 [NOTICE] Opening Control listener on 127.0.0.1:9051 16:41:37 [NOTICE] Opening Socks listener on 127.0.0.1:9050 16:41:37 [NOTICE] Tor v0.2.9.0-alpha-dev (git-44ea3dc3311564a9) running on Linux with Libevent 2.0.16-stable, OpenSSL 1.0.1 and Zlib 1.2.3.4. 16:41:37 [NOTICE] Tor 0.2.9.0-alpha-dev (git-44ea3dc3311564a9) opening log file. """.rstrip() EXPECTED_ENTRIES_WITH_BORDER = """\ +-October 26, 2011-------------------------------------------------------------+ |16:41:37 [NYX_WARNING] Tor's geoip database is unavailable. | |16:41:37 [NYX_NOTICE] No nyxrc loaded, using defaults. You can customize nyx | | by placing a configuration file at /home/atagar/.nyx/nyxrc (see the | | nyxrc.sample for its options). | |16:41:37 [NOTICE] New control connection opened from 127.0.0.1. | |16:41:37 [NOTICE] Opening OR listener on 0.0.0.0:7000 | |16:41:37 [NOTICE] Opening Control listener on 127.0.0.1:9051 | |16:41:37 [NOTICE] Opening Socks listener on 127.0.0.1:9050 | |16:41:37 [NOTICE] Tor v0.2.9.0-alpha-dev (git-44ea3dc3311564a9) running on | | Linux with Libevent 2.0.16-stable, OpenSSL 1.0.1 and Zlib 1.2.3.4. | |16:41:37 [NOTICE] Tor 0.2.9.0-alpha-dev (git-44ea3dc3311564a9) opening log | | file. | +------------------------------------------------------------------------------+ """.rstrip() NOW = 467656897.08663 TIME_STRUCT = time.gmtime(NOW) def entries(): return [ LogEntry(NOW, 'NYX_WARNING', "Tor's geoip database is unavailable."), LogEntry(NOW, 'NYX_NOTICE', 'No nyxrc loaded, using defaults. You can customize nyx by placing a configuration file at /home/atagar/.nyx/nyxrc (see the nyxrc.sample for its options).'), LogEntry(NOW, 'NOTICE', 'New control connection opened from 127.0.0.1.'), LogEntry(NOW, 'NOTICE', 'Opening OR listener on 0.0.0.0:7000'), LogEntry(NOW, 'NOTICE', 'Opening Control listener on 127.0.0.1:9051'), LogEntry(NOW, 'NOTICE', 'Opening Socks listener on 127.0.0.1:9050'), LogEntry(NOW, 'NOTICE', 'Tor v0.2.9.0-alpha-dev (git-44ea3dc3311564a9) running on Linux with Libevent 2.0.16-stable, OpenSSL 1.0.1 and Zlib 1.2.3.4.'), LogEntry(NOW, 'NOTICE', 'Tor 0.2.9.0-alpha-dev (git-44ea3dc3311564a9) opening log file.'), ] class TestLogPanel(unittest.TestCase): @require_curses def test_draw_title(self): rendered = test.render(nyx.panel.log._draw_title, ['NOTICE', 'WARN', 'ERR'], LogFilters()) self.assertEqual('Events (NOTICE-ERR):', rendered.content) rendered = test.render(nyx.panel.log._draw_title, ['NYX_NOTICE', 'NYX_WARNING', 'NYX_ERROR', 'NOTICE', 'WARN', 'ERR'], LogFilters()) self.assertEqual('Events (TOR/NYX NOTICE-ERR):', rendered.content) rendered = test.render(nyx.panel.log._draw_title, ['NYX_DEBUG', 'NYX_INFO', 'NYX_NOTICE', 'NYX_WARNING', 'NYX_ERROR', 'NOTICE', 'WARN', 'ERR'], LogFilters()) self.assertEqual('Events (NOTICE-ERR, NYX DEBUG-ERR):', rendered.content) @require_curses def test_draw_title_with_filter(self): log_filter = LogFilters() log_filter.select('stuff*') rendered = test.render(nyx.panel.log._draw_title, ['NOTICE', 'WARN', 'ERR'], log_filter) self.assertEqual('Events (NOTICE-ERR, filter: stuff*):', rendered.content) @require_curses @patch('time.localtime', Mock(return_value = TIME_STRUCT)) def test_draw_entry(self): entry = LogEntry(NOW, 'NOTICE', 'feeding sulfur to baby dragons is just mean...') rendered = test.render(nyx.panel.log._draw_entry, 0, 0, 80, entry, True) self.assertEqual('16:41:37 [NOTICE] feeding sulfur to baby dragons is just mean...', rendered.content) @require_curses @patch('time.localtime', Mock(return_value = TIME_STRUCT)) def test_draw_entry_that_wraps(self): entry = LogEntry(NOW, 'NOTICE', 'ho hum%s...' % (', ho hum' * 20)) rendered = test.render(nyx.panel.log._draw_entry, 0, 0, 80, entry, True) self.assertEqual(EXPECTED_WRAPPED_MSG, rendered.content) @require_curses @patch('time.localtime', Mock(return_value = TIME_STRUCT)) def test_draw_entry_with_duplicates(self): entry = LogEntry(NOW, 'NOTICE', 'feeding sulfur to baby dragons is just mean...') entry.duplicates = [1, 2] # only care about the count, not the content rendered = test.render(nyx.panel.log._draw_entry, 0, 0, 80, entry, True) self.assertEqual('16:41:37 [NOTICE] feeding sulfur to baby dragons is just mean...', rendered.content) rendered = test.render(nyx.panel.log._draw_entry, 0, 0, 80, entry, False) self.assertEqual('16:41:37 [NOTICE] feeding sulfur to baby dragons is just mean... [1 duplicate\n hidden]', rendered.content) entry.duplicates = [1, 2, 3, 4, 5, 6] rendered = test.render(nyx.panel.log._draw_entry, 0, 0, 80, entry, False) self.assertEqual('16:41:37 [NOTICE] feeding sulfur to baby dragons is just mean... [5 duplicates\n hidden]', rendered.content) @require_curses @patch('time.localtime', Mock(return_value = TIME_STRUCT)) @patch('nyx.log.day_count', Mock(return_value = 5)) def test_draw_entries(self): rendered = test.render(nyx.panel.log._draw_entries, 0, 0, entries(), True) self.assertEqual(EXPECTED_ENTRIES, rendered.content) @require_curses @patch('time.localtime', Mock(return_value = TIME_STRUCT)) @patch('time.strftime', Mock(return_value = 'October 26, 2011')) def test_draw_entries_day_dividers(self): rendered = test.render(nyx.panel.log._draw_entries, 0, 0, entries(), True) self.assertEqual(EXPECTED_ENTRIES_WITH_BORDER, rendered.content) nyx-2.0.4/test/panel/interpreter.py0000664000175000017500000000631313177120634020057 0ustar atagaratagar00000000000000""" Unit tests for nyx.panel.interpreter. """ import unittest import nyx.curses import nyx.panel.interpreter import test from test import require_curses try: # added in python 3.3 from unittest.mock import patch except ImportError: from mock import patch EXPECTED_PANEL = """ Control Interpreter: >>> to use this panel press enter """.strip() EXPECTED_PANEL_INPUT_MODE = """ Control Interpreter (enter "/help" for usage or a blank line to stop): >>> """.strip() EXPECTED_MULTILINE_PANEL = """ Control Interpreter: >>> GETINFO version 250-version=0.2.4.27 (git-412e3f7dc9c6c01a) >>> to use this panel press enter """.strip() EXPECTED_WITH_SCROLLBAR = """ Control Interpreter: |>>> GETINFO version |250-version=0.2.4.27 (git-412e3f7dc9c6c01a) | | | | | | | | | | | | | | | | | | | | | -+ """.strip() class TestInterpreter(unittest.TestCase): def test_format_prompt_input_with_interperter_command(self): output = nyx.panel.interpreter._format_prompt_input('/help') self.assertEqual(2, len(output)) self.assertEqual(('>>> ', ('Green', 'Bold')), output[0]) self.assertEqual(('/help', ('Magenta', 'Bold')), output[1]) def test_format_prompt_input_with_command(self): output = nyx.panel.interpreter._format_prompt_input('GETINFO') self.assertEqual(2, len(output)) self.assertEqual(('>>> ', ('Green', 'Bold')), output[0]) self.assertEqual(('GETINFO ', ('Green', 'Bold')), output[1]) def test_format_prompt_input_with_command_and_arg(self): output = nyx.panel.interpreter._format_prompt_input('GETINFO version') self.assertEqual(3, len(output)) self.assertEqual(('>>> ', ('Green', 'Bold')), output[0]) self.assertEqual(('GETINFO ', ('Green', 'Bold')), output[1]) self.assertEqual(('version', ('Cyan', 'Bold')), output[2]) @require_curses @patch('nyx.panel.interpreter.tor_controller') def test_blank_panel(self, tor_controller_mock): tor_controller_mock()._handle_event = lambda event: None panel = nyx.panel.interpreter.InterpreterPanel() self.assertEqual(EXPECTED_PANEL, test.render(panel._draw).content) panel._is_input_mode = True self.assertEqual(EXPECTED_PANEL_INPUT_MODE, test.render(panel._draw).content) @require_curses @patch('nyx.panel.interpreter.tor_controller') def test_multiline_panel(self, tor_controller_mock): tor_controller_mock()._handle_event = lambda event: None panel = nyx.panel.interpreter.InterpreterPanel() panel._lines = [ [('>>> ', ('Green', 'Bold')), ('GETINFO', ('Green', 'Bold')), (' version', ('Cyan',))], [('250-version=0.2.4.27 (git-412e3f7dc9c6c01a)', ('Blue',))] ] self.assertEqual(EXPECTED_MULTILINE_PANEL, test.render(panel._draw).content) @require_curses @patch('nyx.panel.interpreter.tor_controller') def test_scrollbar(self, tor_controller_mock): tor_controller_mock()._handle_event = lambda event: None panel = nyx.panel.interpreter.InterpreterPanel() panel._lines = [ [('>>> ', ('Green', 'Bold')), ('GETINFO', ('Green', 'Bold')), (' version', ('Cyan',))], [('250-version=0.2.4.27 (git-412e3f7dc9c6c01a)', ('Blue',))] ] + [()] * (panel.get_height() - 2) self.assertEqual(EXPECTED_WITH_SCROLLBAR, test.render(panel._draw).content) nyx-2.0.4/test/popups.py0000664000175000017500000005320513177120634015745 0ustar atagaratagar00000000000000""" Unit tests for nyx.popups. """ import curses import unittest import nyx.curses import nyx.panel import nyx.popups import test from test import require_curses, mock_keybindings try: # added in python 3.3 from unittest.mock import Mock, patch except ImportError: from mock import Mock, patch EXPECTED_HELP_POPUP = """ Page 1 Commands:---------------------------------------------------------------+ | arrows: scroll up and down a: save snapshot of the log | | e: change logged events f: log regex filter (disabled) | | u: duplicate log entries (hidden) c: clear event log | | r: resize graph s: graphed stats (bandwidth) | | b: graph bounds (local max) i: graph update interval (each second)| | | | Press any key... | +------------------------------------------------------------------------------+ """.strip() EXPECTED_ABOUT_POPUP = """ About:-------------------------------------------------------------------------+ | Nyx, version 1.4.6-dev (released April 28, 2011) | | Written by Damian Johnson (atagar@torproject.org) | | Project page: http://www.atagar.com/arm/ | | | | Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html) | | | | Press any key... | +------------------------------------------------------------------------------+ """.strip() EXPECTED_EMPTY_COUNTS = """ Client Locales---------------------------------------+ | Usage stats aren't available yet, press any key... | +----------------------------------------------------+ """.strip() EXPECTED_COUNTS = """ Client Locales-----------------------------------------------------------------+ | de 41 (43%) *************************** | | ru 32 (33%) ********************* | | ca 11 (11%) ******* | | us 6 (6 %) **** | | fr 5 (5 %) *** | | | | Press any key... | +------------------------------------------------------------------------------+ """.strip() EXPECTED_LIST_SELECTOR = """ Update Interval:---+ | > each second | | 5 seconds | | 30 seconds | | minutely | | 15 minute | | 30 minute | | hourly | | daily | +------------------+ """.strip() EXPECTED_SORT_DIALOG_START = """ Config Option Ordering:--------------------------------------------------------+ | Current Order: Man Page Entry, Name, Is Set | | New Order: | | | | Name Value Value Type Category | | Usage Summary Description Man Page Entry | | Is Set Cancel | | | +------------------------------------------------------------------------------+ """.strip() EXPECTED_SORT_DIALOG_END = """ Config Option Ordering:--------------------------------------------------------+ | Current Order: Man Page Entry, Name, Is Set | | New Order: Name, Summary | | | | Value Value Type Category Usage | | Description Man Page Entry Is Set Cancel | | | | | +------------------------------------------------------------------------------+ """.strip() EXPECTED_EVENT_SELECTOR = """ Event Types:-------------------------------------------------------------------+ | | | | +------------------------------------------------------------------------------| |Tor Runlevel: [ ] DEBUG [ ] INFO [ ] NOTICE [ ] WARN [ ] ERR | |Nyx Runlevel: [ ] DEBUG [ ] INFO [ ] NOTICE [ ] WARN [ ] ERR | +------------------------------------------------------------------------------+ |[ ] CIRC [ ] CIRC_MINOR | | [Ok] [Cancel]| +------------------------------------------------------------------------------+ """.strip() EXPECTED_EVENT_SELECTOR_UP_DOWN = """ Event Types:-------------------------------------------------------------------+ | | | | +------------------------------------------------------------------------------| |Tor Runlevel: [X] DEBUG [ ] INFO [ ] NOTICE [ ] WARN [ ] ERR | |Nyx Runlevel: [ ] DEBUG [ ] INFO [ ] NOTICE [ ] WARN [ ] ERR | +------------------------------------------------------------------------------+ |[ ] CIRC [ ] CIRC_MINOR [ ] STREAM | |[ ] ORCONN [ ] BW | | [Ok] [Cancel]| +------------------------------------------------------------------------------+ """.strip() EXPECTED_EVENT_SELECTOR_LEFT_RIGHT = """ Event Types:-------------------------------------------------------------------+ | | | | +------------------------------------------------------------------------------| |Tor Runlevel: [ ] DEBUG [ ] INFO [ ] NOTICE [ ] WARN [ ] ERR | |Nyx Runlevel: [X] DEBUG [ ] INFO [ ] NOTICE [ ] WARN [ ] ERR | +------------------------------------------------------------------------------+ |[ ] CIRC [ ] CIRC_MINOR [ ] STREAM | |[ ] ORCONN [ ] BW | | [Ok] [Cancel]| +------------------------------------------------------------------------------+ """.strip() EXPECTED_EVENT_SELECTOR_CANCEL = """ Event Types:-------------------------------------------------------------------+ | | | | +------------------------------------------------------------------------------| |Tor Runlevel: [ ] DEBUG [ ] INFO [ ] NOTICE [ ] WARN [ ] ERR | |Nyx Runlevel: [ ] DEBUG [ ] INFO [ ] NOTICE [ ] WARN [ ] ERR | +------------------------------------------------------------------------------+ |[ ] CIRC [ ] CIRC_MINOR [ ] STREAM | |[ ] ORCONN [ ] BW | | [Ok] [Cancel]| +------------------------------------------------------------------------------+ """.strip() EXPECTED_EVENT_SELECTOR_INITIAL_SELECTION = """ Event Types:-------------------------------------------------------------------+ | | | | +------------------------------------------------------------------------------| |Tor Runlevel: [ ] DEBUG [ ] INFO [ ] NOTICE [ ] WARN [ ] ERR | |Nyx Runlevel: [ ] DEBUG [ ] INFO [ ] NOTICE [ ] WARN [ ] ERR | +------------------------------------------------------------------------------+ |[ ] CIRC [X] CIRC_MINOR [ ] STREAM | |[ ] ORCONN [ ] BW | | [Ok] [Cancel]| +------------------------------------------------------------------------------+ """.strip() EXPECTED_DESCRIPTOR_WITHOUT_FINGERPRINT = """ Consensus Descriptor:----------+ | No consensus data available | +------------------------------+ """.strip() EXPECTED_DESCRIPTOR = """ Consensus Descriptor (29787760145CD1A473552A2FC64C72A9A130820E):---------------+ | 1 Consensus: | | 2 | | 3 r cyberphunk KXh3YBRc0aRzVSovxkxyqaEwgg4 VjdJThHuYj0jDY2tkkDJkCa8s1s | | 2016-04-04 19:03:16 94.23.150.191 8080 0 | | 4 s Fast Guard Running Stable Valid | | 5 w Bandwidth=8410 | | 6 p reject 1-65535 | | 7 | | 8 Server Descriptor: | | 9 | | 10 router cyberphunk 94.23.150.191 8080 0 0 | | 11 platform Tor 0.2.4.27 on Linux | | 12 protocols Link 1 2 Circuit 1 | | 13 published 2016-04-04 19:03:16 | | 14 fingerprint 2978 7760 145C D1A4 7355 2A2F C64C 72A9 A130 820E | | 15 uptime 3899791 | | 16 bandwidth 10240000 10444800 6482376 | | 17 extra-info-digest 9DC532664DDFD238A4119D623D30F136A3B851BF | | 18 reject *:* | | 19 router-signature | | 20 -----BEGIN SIGNATURE----- | | 21 EUFm38gONCoDuY7ZWHyJtBKuvk6Xi1MPuKuecS5frP3fX0wiZSrOVcpX0X8J+4Hr | | 22 Fb5i+yuMIAXeEn6UhtjqhhZBbY9PW9GdZOMTH8hJpG+evURyr+10PZq6UElg86rA | +------------------------------------------------------------------------------+ """.strip() TORRC = """ ControlPort 9051 CookieAuthentication 1 ExitPolicy reject *:* DataDirectory /home/atagar/.tor Log notice file /home/atagar/.tor/log ORPort 7000 """.strip() EXPECTED_SAVE_TORRC_CONFIRMATION = """ Torrc to save:-----------------------------------------------------------------+ |ControlPort 9051 | |CookieAuthentication 1 | |ExitPolicy reject *:* | |DataDirectory /home/atagar/.tor | |Log notice file /home/atagar/.tor/log | |ORPort 7000 [Save] [Cancel]| +------------------------------------------------------------------------------+ """.strip() DESCRIPTOR_TEXT = """ Consensus: r cyberphunk KXh3YBRc0aRzVSovxkxyqaEwgg4 VjdJThHuYj0jDY2tkkDJkCa8s1s 2016-04-04 19:03:16 94.23.150.191 8080 0 s Fast Guard Running Stable Valid w Bandwidth=8410 p reject 1-65535 Server Descriptor: router cyberphunk 94.23.150.191 8080 0 0 platform Tor 0.2.4.27 on Linux protocols Link 1 2 Circuit 1 published 2016-04-04 19:03:16 fingerprint 2978 7760 145C D1A4 7355 2A2F C64C 72A9 A130 820E uptime 3899791 bandwidth 10240000 10444800 6482376 extra-info-digest 9DC532664DDFD238A4119D623D30F136A3B851BF reject *:* router-signature -----BEGIN SIGNATURE----- EUFm38gONCoDuY7ZWHyJtBKuvk6Xi1MPuKuecS5frP3fX0wiZSrOVcpX0X8J+4Hr Fb5i+yuMIAXeEn6UhtjqhhZBbY9PW9GdZOMTH8hJpG+evURyr+10PZq6UElg86rA NCGI042p6+7UgCVT1x3WcLnq3ScV//s1wXHrUXa7vi0= -----END SIGNATURE----- """.strip().split('\n') class TestPopups(unittest.TestCase): @require_curses @patch('nyx.popups._top', Mock(return_value = 0)) @patch('nyx.popups.nyx_interface') def test_help(self, nyx_interface_mock): header_panel = Mock() header_panel.key_handlers.return_value = ( nyx.panel.KeyHandler('n'), nyx.panel.KeyHandler('r'), ) graph_panel = Mock() graph_panel.key_handlers.return_value = ( nyx.panel.KeyHandler('r', 'resize graph'), nyx.panel.KeyHandler('s', 'graphed stats', current = 'bandwidth'), nyx.panel.KeyHandler('b', 'graph bounds', current = 'local max'), nyx.panel.KeyHandler('i', 'graph update interval', current = 'each second'), ) log_panel = Mock() log_panel.key_handlers.return_value = ( nyx.panel.KeyHandler('arrows', 'scroll up and down'), nyx.panel.KeyHandler('a', 'save snapshot of the log'), nyx.panel.KeyHandler('e', 'change logged events'), nyx.panel.KeyHandler('f', 'log regex filter', current = 'disabled'), nyx.panel.KeyHandler('u', 'duplicate log entries', current = 'hidden'), nyx.panel.KeyHandler('c', 'clear event log'), ) nyx_interface_mock().page_panels.return_value = [header_panel, graph_panel, log_panel] rendered = test.render(nyx.popups.show_help) self.assertEqual(EXPECTED_HELP_POPUP, rendered.content) @require_curses @patch('nyx.popups._top', Mock(return_value = 0)) def test_about(self): rendered = test.render(nyx.popups.show_about) self.assertEqual(EXPECTED_ABOUT_POPUP, rendered.content) @require_curses @patch('nyx.popups._top', Mock(return_value = 0)) def test_counts_when_empty(self): rendered = test.render(nyx.popups.show_counts, 'Client Locales', {}) self.assertEqual(EXPECTED_EMPTY_COUNTS, rendered.content) @require_curses @patch('nyx.popups._top', Mock(return_value = 0)) def test_counts(self): clients = { 'fr': 5, 'us': 6, 'ca': 11, 'ru': 32, 'de': 41, } rendered = test.render(nyx.popups.show_counts, 'Client Locales', clients, fill_char = '*') self.assertEqual(EXPECTED_COUNTS, rendered.content) @require_curses @patch('nyx.popups._top', Mock(return_value = 0)) def test_select_from_list(self): options = ['each second', '5 seconds', '30 seconds', 'minutely', '15 minute', '30 minute', 'hourly', 'daily'] rendered = test.render(nyx.popups.select_from_list, 'Update Interval:', options, 'each second') self.assertEqual(EXPECTED_LIST_SELECTOR, rendered.content) self.assertEqual('each second', rendered.return_value) @require_curses @patch('nyx.popups._top', Mock(return_value = 0)) def test_select_sort_order(self): previous_order = ['Man Page Entry', 'Name', 'Is Set'] options = ['Name', 'Value', 'Value Type', 'Category', 'Usage', 'Summary', 'Description', 'Man Page Entry', 'Is Set'] rendered = test.render(nyx.popups.select_sort_order, 'Config Option Ordering:', options, previous_order, {}) self.assertEqual(EXPECTED_SORT_DIALOG_START, rendered.content) self.assertEqual(None, rendered.return_value) @require_curses @patch('nyx.popups._top', Mock(return_value = 0)) def test_select_sort_order_usage(self): # Use the dialog to make a selection. At the end we render two options as # being selected (rather than three) because the act of selecing the third # closed the popup. def draw_func(): with mock_keybindings(curses.KEY_ENTER, curses.KEY_DOWN, curses.KEY_ENTER, curses.KEY_ENTER): return nyx.popups.select_sort_order('Config Option Ordering:', options, previous_order, {}) previous_order = ['Man Page Entry', 'Name', 'Is Set'] options = ['Name', 'Value', 'Value Type', 'Category', 'Usage', 'Summary', 'Description', 'Man Page Entry', 'Is Set'] rendered = test.render(draw_func) self.assertEqual(EXPECTED_SORT_DIALOG_END, rendered.content) self.assertEqual(['Name', 'Summary', 'Description'], rendered.return_value) @require_curses @patch('nyx.popups._top', Mock(return_value = 0)) @patch('nyx.tor_controller') def test_select_event_types(self, controller_mock): controller = Mock() controller.get_info.return_value = 'DEBUG INFO NOTICE WARN ERR CIRC CIRC_MINOR' controller_mock.return_value = controller def draw_func(): with mock_keybindings(curses.KEY_DOWN, curses.KEY_DOWN, curses.KEY_DOWN, curses.KEY_ENTER): return nyx.popups.select_event_types([]) rendered = test.render(draw_func) self.assertEqual(EXPECTED_EVENT_SELECTOR, rendered.content) self.assertEqual(set([]), rendered.return_value) @require_curses @patch('nyx.popups._top', Mock(return_value = 0)) @patch('nyx.tor_controller') def test_select_event_types_up_down(self, controller_mock): controller = Mock() controller.get_info.return_value = 'DEBUG INFO NOTICE WARN ERR CIRC CIRC_MINOR STREAM ORCONN BW' controller_mock.return_value = controller def draw_func(): with mock_keybindings(curses.KEY_UP, curses.KEY_ENTER, curses.KEY_DOWN, curses.KEY_DOWN, curses.KEY_DOWN, curses.KEY_DOWN, curses.KEY_ENTER): return nyx.popups.select_event_types([]) rendered = test.render(draw_func) self.assertEqual(EXPECTED_EVENT_SELECTOR_UP_DOWN, rendered.content) self.assertEqual(set(['DEBUG']), rendered.return_value) @require_curses @patch('nyx.popups._top', Mock(return_value = 0)) @patch('nyx.tor_controller') def test_select_event_types_left_right(self, controller_mock): controller = Mock() controller.get_info.return_value = 'DEBUG INFO NOTICE WARN ERR CIRC CIRC_MINOR STREAM ORCONN BW' controller_mock.return_value = controller def draw_func(): with mock_keybindings(curses.KEY_LEFT, curses.KEY_DOWN, curses.KEY_ENTER, curses.KEY_DOWN, curses.KEY_DOWN, curses.KEY_DOWN, curses.KEY_RIGHT, curses.KEY_RIGHT, curses.KEY_LEFT, curses.KEY_ENTER): return nyx.popups.select_event_types([]) rendered = test.render(draw_func) self.assertEqual(EXPECTED_EVENT_SELECTOR_LEFT_RIGHT, rendered.content) self.assertEqual(set(['NYX_DEBUG']), rendered.return_value) @require_curses @patch('nyx.popups._top', Mock(return_value = 0)) @patch('nyx.tor_controller') def test_select_event_types_cancel(self, controller_mock): controller = Mock() controller.get_info.return_value = 'DEBUG INFO NOTICE WARN ERR CIRC CIRC_MINOR STREAM ORCONN BW' controller_mock.return_value = controller def draw_func(): with mock_keybindings(curses.KEY_DOWN, curses.KEY_DOWN, curses.KEY_DOWN, curses.KEY_DOWN, curses.KEY_RIGHT, curses.KEY_ENTER): return nyx.popups.select_event_types([]) rendered = test.render(draw_func) self.assertEqual(EXPECTED_EVENT_SELECTOR_CANCEL, rendered.content) self.assertEqual(None, rendered.return_value) @require_curses @patch('nyx.popups._top', Mock(return_value = 0)) @patch('nyx.tor_controller') def test_select_event_types_initial_selection(self, controller_mock): controller = Mock() controller.get_info.return_value = 'DEBUG INFO NOTICE WARN ERR CIRC CIRC_MINOR STREAM ORCONN BW' controller_mock.return_value = controller def draw_func(): with mock_keybindings(curses.KEY_DOWN, curses.KEY_DOWN, curses.KEY_DOWN, curses.KEY_DOWN, curses.KEY_ENTER): return nyx.popups.select_event_types(['CIRC_MINOR']) rendered = test.render(draw_func) self.assertEqual(EXPECTED_EVENT_SELECTOR_INITIAL_SELECTION, rendered.content) self.assertEqual(set(['CIRC_MINOR']), rendered.return_value) @require_curses @patch('nyx.popups._top', Mock(return_value = 0)) def test_confirm_save_torrc(self): rendered = test.render(nyx.popups.confirm_save_torrc, TORRC) self.assertEqual(EXPECTED_SAVE_TORRC_CONFIRMATION, rendered.content) self.assertEqual(False, rendered.return_value) def draw_func(): with mock_keybindings(curses.KEY_LEFT, curses.KEY_ENTER): return nyx.popups.confirm_save_torrc(TORRC) rendered = test.render(draw_func) self.assertEqual(EXPECTED_SAVE_TORRC_CONFIRMATION, rendered.content) self.assertEqual(True, rendered.return_value) @require_curses @patch('nyx.popups._top', Mock(return_value = 0)) def test_descriptor_without_fingerprint(self): rendered = test.render(nyx.popups.show_descriptor, None, nyx.curses.Color.RED, lambda key: key.match('esc')) self.assertEqual(EXPECTED_DESCRIPTOR_WITHOUT_FINGERPRINT, rendered.content) self.assertEqual(nyx.curses.KeyInput(27), rendered.return_value) @require_curses @patch('nyx.popups._top', Mock(return_value = 0)) @patch('nyx.popups._descriptor_text', Mock(return_value = DESCRIPTOR_TEXT)) def test_descriptor(self): rendered = test.render(nyx.popups.show_descriptor, '29787760145CD1A473552A2FC64C72A9A130820E', nyx.curses.Color.RED, lambda key: key.match('esc')) self.assertEqual(EXPECTED_DESCRIPTOR, rendered.content) self.assertEqual(nyx.curses.KeyInput(27), rendered.return_value) nyx-2.0.4/test/log/0000775000175000017500000000000013200142726014612 5ustar atagaratagar00000000000000nyx-2.0.4/test/log/log_group.py0000664000175000017500000001645413173140703017175 0ustar atagaratagar00000000000000import os import unittest import nyx.log from nyx.log import LogGroup, LogEntry class TestLogGroup(unittest.TestCase): def setUp(self): nyx.log.GROUP_BY_DAY = False def tearDown(self): nyx.log.GROUP_BY_DAY = True def test_maintains_certain_size(self): group = LogGroup(5) self.assertEqual(0, len(group)) group.add(LogEntry(1333738410, 'INFO', 'tor_lockfile_lock(): Locking "/home/atagar/.tor/lock"')) self.assertEqual([LogEntry(1333738410, 'INFO', 'tor_lockfile_lock(): Locking "/home/atagar/.tor/lock"')], list(group)) self.assertEqual(1, len(group)) group.add(LogEntry(1333738420, 'NYX_DEBUG', 'GETCONF MyFamily (runtime: 0.0007)')) group.add(LogEntry(1333738430, 'NOTICE', 'Bootstrapped 72%: Loading relay descriptors.')) group.add(LogEntry(1333738440, 'NOTICE', 'Bootstrapped 75%: Loading relay descriptors.')) group.add(LogEntry(1333738450, 'NOTICE', 'Bootstrapped 78%: Loading relay descriptors.')) self.assertEqual(5, len(group)) # group should now be full, adding more entries pops others off group.add(LogEntry(1333738460, 'NOTICE', 'Bootstrapped 80%: Loading relay descriptors.')) self.assertFalse(LogEntry(1333738410, 'INFO', 'tor_lockfile_lock(): Locking "/home/atagar/.tor/lock"') in list(group)) self.assertEqual(5, len(group)) # try adding a bunch that will be deduplicated, and make sure we still maintain the size group.add(LogEntry(1333738510, 'NOTICE', 'Bootstrapped 80%: Loading relay descriptors.')) group.add(LogEntry(1333738520, 'NOTICE', 'Bootstrapped 80%: Loading relay descriptors.')) group.add(LogEntry(1333738530, 'NOTICE', 'Bootstrapped 80%: Loading relay descriptors.')) group.add(LogEntry(1333738540, 'NOTICE', 'Bootstrapped 80%: Loading relay descriptors.')) group.add(LogEntry(1333738550, 'NOTICE', 'Bootstrapped 80%: Loading relay descriptors.')) group.add(LogEntry(1333738560, 'NOTICE', 'Bootstrapped 80%: Loading relay descriptors.')) group.add(LogEntry(1333738570, 'NOTICE', 'Bootstrapped 80%: Loading relay descriptors.')) self.assertEqual([1333738570, 1333738560, 1333738550, 1333738540, 1333738530], [e.timestamp for e in group]) self.assertEqual(5, len(group)) def test_deduplication(self): group = LogGroup(5) group.add(LogEntry(1333738410, 'NOTICE', 'Bootstrapped 72%: Loading relay descriptors.')) group.add(LogEntry(1333738420, 'NOTICE', 'Bootstrapped 75%: Loading relay descriptors.')) group.add(LogEntry(1333738430, 'NYX_DEBUG', 'GETCONF MyFamily (runtime: 0.0007)')) group.add(LogEntry(1333738440, 'NOTICE', 'Bootstrapped 78%: Loading relay descriptors.')) group.add(LogEntry(1333738450, 'NOTICE', 'Bootstrapped 80%: Loading relay descriptors.')) self.assertEqual([1333738450, 1333738440, 1333738430, 1333738420, 1333738410], [e.timestamp for e in group]) bootstrap_messages = [ 'Bootstrapped 80%: Loading relay descriptors.', 'Bootstrapped 78%: Loading relay descriptors.', 'Bootstrapped 75%: Loading relay descriptors.', 'Bootstrapped 72%: Loading relay descriptors.', ] group_items = list(group) self.assertEqual(bootstrap_messages, [e.message for e in group_items[0].duplicates]) self.assertEqual([False, True, False, True, True], [e.is_duplicate for e in group_items]) # add another duplicate message that pops the last group.add(LogEntry(1333738460, 'NOTICE', 'Bootstrapped 90%: Loading relay descriptors.')) bootstrap_messages = [ 'Bootstrapped 90%: Loading relay descriptors.', 'Bootstrapped 80%: Loading relay descriptors.', 'Bootstrapped 78%: Loading relay descriptors.', 'Bootstrapped 75%: Loading relay descriptors.', ] group_items = list(group) self.assertEqual(bootstrap_messages, [e.message for e in group_items[0].duplicates]) self.assertEqual([False, True, True, False, True], [e.is_duplicate for e in group_items]) # add another non-duplicate message that pops the last group.add(LogEntry(1333738470, 'INFO', 'tor_lockfile_lock(): Locking "/home/atagar/.tor/lock"')) bootstrap_messages = [ 'Bootstrapped 90%: Loading relay descriptors.', 'Bootstrapped 80%: Loading relay descriptors.', 'Bootstrapped 78%: Loading relay descriptors.', ] group_items = list(group) self.assertEqual(None, group_items[0].duplicates) self.assertEqual(bootstrap_messages, [e.message for e in group_items[1].duplicates]) self.assertEqual([False, False, True, True, False], [e.is_duplicate for e in group_items]) def test_deduplication_with_daybreaks(self): nyx.log.GROUP_BY_DAY = True group = LogGroup(100) test_log_path = os.path.join(os.path.dirname(__file__), 'data', 'daybreak_deduplication') for entry in reversed(list(nyx.log.read_tor_log(test_log_path))): group.add(entry) # Entries should consist of two days of results... # # Day 1: # 10:24:27 [NOTICE] New control connection opened from 127.0.0.1. # 10:21:31 [NOTICE] New control connection opened from 127.0.0.1. # 10:19:24 [NOTICE] New control connection opened from 127.0.0.1. # 10:16:38 [NOTICE] New control connection opened from 127.0.0.1. # 10:16:38 [NOTICE] New control connection opened from 127.0.0.1. # 05:44:40 [NOTICE] Heartbeat: Tor's uptime is 18:00 hours, with 0 circuits open. I've sent 862 kB and received 9.05 MB. # # Day 2: # 23:44:40 [NOTICE] Heartbeat: Tor's uptime is 12:00 hours, with 1 circuits open. I've sent 794 kB and received 7.32 MB. # 19:02:44 [NOTICE] New control connection opened from 127.0.0.1. # 18:52:47 [NOTICE] New control connection opened from 127.0.0.1. # 18:11:56 [NOTICE] New control connection opened from 127.0.0.1. # 17:44:40 [NOTICE] Heartbeat: Tor's uptime is 6:00 hours, with 0 circuits open. I've sent 539 kB and received 4.25 MB. # 11:45:03 [NOTICE] New control connection opened from 127.0.0.1. # 11:44:49 [NOTICE] Bootstrapped 100%: Done # ... etc... group_items = list(group) # First day self.assertEqual('New control connection opened from 127.0.0.1.', group_items[0].message) self.assertEqual(5, len(group_items[0].duplicates)) self.assertFalse(group_items[0].is_duplicate) for entry in group_items[1:5]: self.assertEqual('New control connection opened from 127.0.0.1.', entry.message) self.assertEqual(5, len(entry.duplicates)) self.assertTrue(entry.is_duplicate) self.assertEqual("Heartbeat: Tor's uptime is 18:00 hours, with 0 circuits open. I've sent 862 kB and received 9.05 MB.", group_items[5].message) self.assertEqual(None, group_items[5].duplicates) self.assertFalse(group_items[5].is_duplicate) # Second day self.assertEqual("Heartbeat: Tor's uptime is 12:00 hours, with 1 circuits open. I've sent 794 kB and received 7.32 MB.", group_items[6].message) self.assertEqual(2, len(group_items[6].duplicates)) self.assertFalse(group_items[6].is_duplicate) self.assertEqual('New control connection opened from 127.0.0.1.', group_items[8].message) self.assertEqual(4, len(group_items[8].duplicates)) self.assertTrue(group_items[8].is_duplicate) self.assertEqual("Heartbeat: Tor's uptime is 6:00 hours, with 0 circuits open. I've sent 539 kB and received 4.25 MB.", group_items[10].message) self.assertEqual(2, len(group_items[10].duplicates)) self.assertTrue(group_items[10].is_duplicate) nyx-2.0.4/test/log/__init__.py0000664000175000017500000000014013141704000016707 0ustar atagaratagar00000000000000""" Unit tests for nyx's log utilities. """ __all__ = [ 'deduplication', 'read_tor_log', ] nyx-2.0.4/test/log/condense_runlevels.py0000664000175000017500000000137313141704000021056 0ustar atagaratagar00000000000000import unittest from nyx.log import condense_runlevels class TestCondenseRunlevels(unittest.TestCase): def test_condense_runlevels(self): self.assertEqual([], condense_runlevels()) self.assertEqual(['BW'], condense_runlevels('BW')) self.assertEqual(['DEBUG', 'NOTICE', 'ERR'], condense_runlevels('DEBUG', 'NOTICE', 'ERR')) self.assertEqual(['DEBUG-NOTICE', 'NYX DEBUG-INFO'], condense_runlevels('DEBUG', 'NYX_DEBUG', 'INFO', 'NYX_INFO', 'NOTICE')) self.assertEqual(['TOR/NYX NOTICE-ERR'], condense_runlevels('NOTICE', 'WARN', 'ERR', 'NYX_NOTICE', 'NYX_WARNING', 'NYX_ERROR')) self.assertEqual(['DEBUG', 'TOR/NYX NOTICE-ERR', 'BW'], condense_runlevels('DEBUG', 'NOTICE', 'WARN', 'ERR', 'NYX_NOTICE', 'NYX_WARNING', 'NYX_ERROR', 'BW')) nyx-2.0.4/test/log/read_tor_log.py0000664000175000017500000000456213141704000017624 0ustar atagaratagar00000000000000import os import unittest from nyx.log import read_tor_log def data_path(filename): return os.path.join(os.path.dirname(__file__), 'data', filename) class TestReadTorLog(unittest.TestCase): def test_general_log(self): entries = list(read_tor_log(data_path('tor_log'))) self.assertEqual(21, len(entries)) self.assertEqual('Interrupt: exiting cleanly.', entries[0].message) self.assertEqual('Tor 0.2.7.0-alpha-dev (git-4247ce99e5d9b7b2) opening new log file.', entries[-1].message) def test_with_multiple_tor_instances(self): entries = list(read_tor_log(data_path('multiple_tor_instances'))) self.assertEqual(12, len(entries)) self.assertEqual('parse_dir_authority_line(): Trusted 100 dirserver at 128.31.0.39:9131 (9695)', entries[0].message) self.assertEqual('tor_lockfile_lock(): Locking "/home/atagar/.tor/lock"', entries[1].message) self.assertEqual('Tor 0.2.7.0-alpha-dev (git-4247ce99e5d9b7b2) opening log file.', entries[-1].message) def test_with_read_limit(self): entries = list(read_tor_log(data_path('tor_log'), 5)) self.assertEqual(5, len(entries)) self.assertEqual('Interrupt: exiting cleanly.', entries[0].message) self.assertEqual('Bootstrapped 90%: Establishing a Tor circuit', entries[-1].message) def test_with_empty_file(self): entries = list(read_tor_log(data_path('empty_file'))) self.assertEqual(0, len(entries)) def test_with_missing_path(self): self.assertRaises(IOError, list, read_tor_log(data_path('no_such_path'))) def test_with_malformed_line(self): try: list(read_tor_log(data_path('malformed_line'))) self.fail("Malformed content should've raised a ValueError") except ValueError as exc: self.assertTrue("has a line that doesn't match the format we expect: Apr 06 11:03:53.000" in str(exc)) def test_with_malformed_runlevel(self): try: list(read_tor_log(data_path('malformed_runlevel'))) self.fail("Malformed content should've raised a ValueError") except ValueError as exc: self.assertTrue('has an unrecognized runlevel: [unrecognized]' in str(exc)) def test_with_malformed_date(self): try: list(read_tor_log(data_path('malformed_date'))) self.fail("Malformed content should've raised a ValueError") except ValueError as exc: self.assertTrue("has a timestamp we don't recognize: Zed 06 11:03:52.000" in str(exc)) nyx-2.0.4/test/log/data/0000775000175000017500000000000013200142726015523 5ustar atagaratagar00000000000000nyx-2.0.4/test/log/data/empty_file0000664000175000017500000000000013141704000017562 0ustar atagaratagar00000000000000nyx-2.0.4/test/log/data/malformed_date0000664000175000017500000000331013141704000020377 0ustar atagaratagar00000000000000Apr 06 11:03:39.000 [notice] Tor 0.2.7.0-alpha-dev (git-4247ce99e5d9b7b2) opening new log file. Apr 06 11:03:39.832 [notice] Tor v0.2.7.0-alpha-dev (git-4247ce99e5d9b7b2) running on Linux with Libevent 2.0.16-stable, OpenSSL 1.0.1 and Zlib 1.2.3.4. Apr 06 11:03:39.832 [notice] Tor can't help you if you use it wrong! Learn how to be safe at https://www.torproject.org/download/download#warning Apr 06 11:03:39.833 [notice] This version is not a stable Tor release. Expect more bugs than usual. Apr 06 11:03:39.833 [notice] Read configuration file "/home/atagar/.tor/torrc". Apr 06 11:03:39.838 [notice] Opening Socks listener on 127.0.0.1:9050 Apr 06 11:03:39.838 [notice] Opening Control listener on 127.0.0.1:9051 Apr 06 11:03:39.000 [notice] Bootstrapped 0%: Starting Apr 06 11:03:42.000 [notice] Bootstrapped 45%: Asking for relay descriptors Apr 06 11:03:43.000 [notice] Bootstrapped 50%: Loading relay descriptors Apr 06 11:03:49.000 [notice] New control connection opened from 127.0.0.1. Apr 06 11:03:51.000 [notice] Bootstrapped 55%: Loading relay descriptors Apr 06 11:03:51.000 [notice] Bootstrapped 63%: Loading relay descriptors Apr 06 11:03:51.000 [notice] Bootstrapped 69%: Loading relay descriptors Apr 06 11:03:51.000 [notice] Bootstrapped 74%: Loading relay descriptors Apr 06 11:03:52.000 [notice] Bootstrapped 80%: Connecting to the Tor network Zed 06 11:03:52.000 [notice] Bootstrapped 90%: Establishing a Tor circuit Apr 06 11:03:53.000 [notice] Tor has successfully opened a circuit. Looks like client functionality is working. Apr 06 11:03:53.000 [notice] Bootstrapped 100%: Done Apr 06 11:03:55.000 [notice] New control connection opened from 127.0.0.1. Apr 06 11:53:46.000 [notice] Interrupt: exiting cleanly. nyx-2.0.4/test/log/data/multiple_tor_instances0000664000175000017500000000576713141704000022244 0ustar atagaratagar00000000000000Apr 06 11:03:39.000 [notice] Tor 0.2.7.0-alpha-dev (git-4247ce99e5d9b7b2) opening new log file. Apr 06 11:03:39.832 [notice] Tor v0.2.7.0-alpha-dev (git-4247ce99e5d9b7b2) running on Linux with Libevent 2.0.16-stable, OpenSSL 1.0.1 and Zlib 1.2.3.4. Apr 06 11:03:39.832 [notice] Tor can't help you if you use it wrong! Learn how to be safe at https://www.torproject.org/download/download#warning Apr 06 11:03:39.833 [notice] This version is not a stable Tor release. Expect more bugs than usual. Apr 06 11:03:39.833 [notice] Read configuration file "/home/atagar/.tor/torrc". Apr 06 11:03:39.838 [notice] Opening Socks listener on 127.0.0.1:9050 Apr 06 11:03:39.838 [notice] Opening Control listener on 127.0.0.1:9051 Apr 06 11:03:39.000 [notice] Bootstrapped 0%: Starting Apr 06 11:03:42.000 [notice] Bootstrapped 45%: Asking for relay descriptors Apr 06 11:03:43.000 [notice] Bootstrapped 50%: Loading relay descriptors Apr 06 11:03:49.000 [notice] New control connection opened from 127.0.0.1. Apr 06 11:03:51.000 [notice] Bootstrapped 55%: Loading relay descriptors Apr 06 11:03:51.000 [notice] Bootstrapped 63%: Loading relay descriptors Apr 06 11:03:51.000 [notice] Bootstrapped 69%: Loading relay descriptors Apr 06 11:03:51.000 [notice] Bootstrapped 74%: Loading relay descriptors Apr 06 11:03:52.000 [notice] Bootstrapped 80%: Connecting to the Tor network Apr 06 11:03:52.000 [notice] Bootstrapped 90%: Establishing a Tor circuit Apr 06 11:03:53.000 [notice] Tor has successfully opened a circuit. Looks like client functionality is working. Apr 06 11:03:53.000 [notice] Bootstrapped 100%: Done Apr 06 11:03:55.000 [notice] New control connection opened from 127.0.0.1. Apr 06 11:53:46.000 [notice] Interrupt: exiting cleanly. Apr 06 11:53:54.000 [notice] Tor 0.2.7.0-alpha-dev (git-4247ce99e5d9b7b2) opening log file. Apr 06 11:53:54.392 [notice] Tor v0.2.7.0-alpha-dev (git-4247ce99e5d9b7b2) running on Linux with Libevent 2.0.16-stable, OpenSSL 1.0.1 and Zlib 1.2.3.4. Apr 06 11:53:54.392 [notice] Tor can't help you if you use it wrong! Learn how to be safe at https://www.torproject.org/download/download#warning Apr 06 11:53:54.392 [notice] This version is not a stable Tor release. Expect more bugs than usual. Apr 06 11:53:54.392 [notice] Read configuration file "/home/atagar/.tor/torrc". Apr 06 11:53:54.396 [notice] Opening Socks listener on 127.0.0.1:9050 Apr 06 11:53:54.396 [notice] Opening Control listener on 127.0.0.1:9051 Apr 06 11:53:54.000 [warn] Your log may contain sensitive information - you're logging more than "notice". Don't log unless it serves an important reason. Overwrite the log afterwards. Apr 06 11:53:54.000 [debug] tor_disable_debugger_attach(): Attemping to disable debugger attachment to Tor for unprivileged users. Apr 06 11:53:54.000 [debug] tor_disable_debugger_attach(): Debugger attachment disabled for unprivileged users. Apr 06 11:53:54.000 [info] tor_lockfile_lock(): Locking "/home/atagar/.tor/lock" Apr 06 11:53:54.000 [debug] parse_dir_authority_line(): Trusted 100 dirserver at 128.31.0.39:9131 (9695) nyx-2.0.4/test/log/data/daybreak_deduplication0000664000175000017500000000642713141704000022136 0ustar atagaratagar00000000000000Apr 24 19:50:42.000 [notice] Heartbeat: Tor's uptime is 12 days 0:00 hours, with 0 circuits open. I've sent 4.94 MB and received 130.17 MB. Apr 24 19:50:42.000 [notice] Average packaged cell fullness: 65.101%. TLS write overhead: 11% Apr 25 01:50:42.000 [notice] Heartbeat: Tor's uptime is 12 days 6:00 hours, with 0 circuits open. I've sent 5.00 MB and received 131.87 MB. Apr 25 01:50:42.000 [notice] Average packaged cell fullness: 64.927%. TLS write overhead: 11% Apr 25 07:50:42.000 [notice] Heartbeat: Tor's uptime is 12 days 12:00 hours, with 0 circuits open. I've sent 5.08 MB and received 134.19 MB. Apr 25 07:50:42.000 [notice] Average packaged cell fullness: 64.587%. TLS write overhead: 11% Apr 25 11:44:21.000 [notice] New control connection opened from 127.0.0.1. Apr 25 11:44:33.000 [notice] Interrupt: exiting cleanly. Apr 25 11:44:36.000 [notice] Tor 0.2.7.0-alpha-dev (git-63a90f2df4dcd7ff) opening log file. Apr 25 11:44:36.492 [notice] Tor v0.2.7.0-alpha-dev (git-63a90f2df4dcd7ff) running on Linux with Libevent 2.0.16-stable, OpenSSL 1.0.1 and Zlib 1.2.3.4. Apr 25 11:44:36.492 [notice] Tor can't help you if you use it wrong! Learn how to be safe at https://www.torproject.org/download/download#warning Apr 25 11:44:36.492 [notice] This version is not a stable Tor release. Expect more bugs than usual. Apr 25 11:44:36.525 [notice] Read configuration file "/home/atagar/.tor/torrc". Apr 25 11:44:36.530 [notice] Opening Socks listener on 127.0.0.1:9050 Apr 25 11:44:36.530 [notice] Opening Control listener on 127.0.0.1:9051 Apr 25 11:44:36.000 [notice] Bootstrapped 0%: Starting Apr 25 11:44:39.000 [notice] Bootstrapped 45%: Asking for relay descriptors Apr 25 11:44:40.000 [notice] Bootstrapped 50%: Loading relay descriptors Apr 25 11:44:47.000 [notice] Bootstrapped 55%: Loading relay descriptors Apr 25 11:44:47.000 [notice] Bootstrapped 62%: Loading relay descriptors Apr 25 11:44:48.000 [notice] Bootstrapped 72%: Loading relay descriptors Apr 25 11:44:48.000 [notice] Bootstrapped 80%: Connecting to the Tor network Apr 25 11:44:48.000 [notice] Bootstrapped 90%: Establishing a Tor circuit Apr 25 11:44:49.000 [notice] Tor has successfully opened a circuit. Looks like client functionality is working. Apr 25 11:44:49.000 [notice] Bootstrapped 100%: Done Apr 25 11:45:03.000 [notice] New control connection opened from 127.0.0.1. Apr 25 17:44:40.000 [notice] Heartbeat: Tor's uptime is 6:00 hours, with 0 circuits open. I've sent 539 kB and received 4.25 MB. Apr 25 18:11:56.000 [notice] New control connection opened from 127.0.0.1. Apr 25 18:52:47.000 [notice] New control connection opened from 127.0.0.1. Apr 25 19:02:44.000 [notice] New control connection opened from 127.0.0.1. Apr 25 23:44:40.000 [notice] Heartbeat: Tor's uptime is 12:00 hours, with 1 circuits open. I've sent 794 kB and received 7.32 MB. Apr 26 05:44:40.000 [notice] Heartbeat: Tor's uptime is 18:00 hours, with 0 circuits open. I've sent 862 kB and received 9.05 MB. Apr 26 10:16:38.000 [notice] New control connection opened from 127.0.0.1. Apr 26 10:19:24.000 [notice] New control connection opened from 127.0.0.1. Apr 26 10:21:31.000 [notice] New control connection opened from 127.0.0.1. Apr 26 10:24:27.000 [notice] New control connection opened from 127.0.0.1. Apr 26 10:24:46.000 [notice] New control connection opened from 127.0.0.1. nyx-2.0.4/test/log/data/malformed_runlevel0000664000175000017500000000331613141704000021324 0ustar atagaratagar00000000000000Apr 06 11:03:39.000 [notice] Tor 0.2.7.0-alpha-dev (git-4247ce99e5d9b7b2) opening new log file. Apr 06 11:03:39.832 [notice] Tor v0.2.7.0-alpha-dev (git-4247ce99e5d9b7b2) running on Linux with Libevent 2.0.16-stable, OpenSSL 1.0.1 and Zlib 1.2.3.4. Apr 06 11:03:39.832 [notice] Tor can't help you if you use it wrong! Learn how to be safe at https://www.torproject.org/download/download#warning Apr 06 11:03:39.833 [notice] This version is not a stable Tor release. Expect more bugs than usual. Apr 06 11:03:39.833 [notice] Read configuration file "/home/atagar/.tor/torrc". Apr 06 11:03:39.838 [notice] Opening Socks listener on 127.0.0.1:9050 Apr 06 11:03:39.838 [notice] Opening Control listener on 127.0.0.1:9051 Apr 06 11:03:39.000 [notice] Bootstrapped 0%: Starting Apr 06 11:03:42.000 [notice] Bootstrapped 45%: Asking for relay descriptors Apr 06 11:03:43.000 [notice] Bootstrapped 50%: Loading relay descriptors Apr 06 11:03:49.000 [notice] New control connection opened from 127.0.0.1. Apr 06 11:03:51.000 [notice] Bootstrapped 55%: Loading relay descriptors Apr 06 11:03:51.000 [notice] Bootstrapped 63%: Loading relay descriptors Apr 06 11:03:51.000 [notice] Bootstrapped 69%: Loading relay descriptors Apr 06 11:03:51.000 [notice] Bootstrapped 74%: Loading relay descriptors Apr 06 11:03:52.000 [notice] Bootstrapped 80%: Connecting to the Tor network Apr 06 11:03:52.000 [unrecognized] Bootstrapped 90%: Establishing a Tor circuit Apr 06 11:03:53.000 [notice] Tor has successfully opened a circuit. Looks like client functionality is working. Apr 06 11:03:53.000 [notice] Bootstrapped 100%: Done Apr 06 11:03:55.000 [notice] New control connection opened from 127.0.0.1. Apr 06 11:53:46.000 [notice] Interrupt: exiting cleanly. nyx-2.0.4/test/log/data/tor_log0000664000175000017500000000331013141704000017101 0ustar atagaratagar00000000000000Apr 06 11:03:39.000 [notice] Tor 0.2.7.0-alpha-dev (git-4247ce99e5d9b7b2) opening new log file. Apr 06 11:03:39.832 [notice] Tor v0.2.7.0-alpha-dev (git-4247ce99e5d9b7b2) running on Linux with Libevent 2.0.16-stable, OpenSSL 1.0.1 and Zlib 1.2.3.4. Apr 06 11:03:39.832 [notice] Tor can't help you if you use it wrong! Learn how to be safe at https://www.torproject.org/download/download#warning Apr 06 11:03:39.833 [notice] This version is not a stable Tor release. Expect more bugs than usual. Apr 06 11:03:39.833 [notice] Read configuration file "/home/atagar/.tor/torrc". Apr 06 11:03:39.838 [notice] Opening Socks listener on 127.0.0.1:9050 Apr 06 11:03:39.838 [notice] Opening Control listener on 127.0.0.1:9051 Apr 06 11:03:39.000 [notice] Bootstrapped 0%: Starting Apr 06 11:03:42.000 [notice] Bootstrapped 45%: Asking for relay descriptors Apr 06 11:03:43.000 [notice] Bootstrapped 50%: Loading relay descriptors Apr 06 11:03:49.000 [notice] New control connection opened from 127.0.0.1. Apr 06 11:03:51.000 [notice] Bootstrapped 55%: Loading relay descriptors Apr 06 11:03:51.000 [notice] Bootstrapped 63%: Loading relay descriptors Apr 06 11:03:51.000 [notice] Bootstrapped 69%: Loading relay descriptors Apr 06 11:03:51.000 [notice] Bootstrapped 74%: Loading relay descriptors Apr 06 11:03:52.000 [notice] Bootstrapped 80%: Connecting to the Tor network Apr 06 11:03:52.000 [notice] Bootstrapped 90%: Establishing a Tor circuit Apr 06 11:03:53.000 [notice] Tor has successfully opened a circuit. Looks like client functionality is working. Apr 06 11:03:53.000 [notice] Bootstrapped 100%: Done Apr 06 11:03:55.000 [notice] New control connection opened from 127.0.0.1. Apr 06 11:53:46.000 [notice] Interrupt: exiting cleanly. nyx-2.0.4/test/log/data/malformed_line0000664000175000017500000000315413141704000020417 0ustar atagaratagar00000000000000Apr 06 11:03:39.000 [notice] Tor 0.2.7.0-alpha-dev (git-4247ce99e5d9b7b2) opening new log file. Apr 06 11:03:39.832 [notice] Tor v0.2.7.0-alpha-dev (git-4247ce99e5d9b7b2) running on Linux with Libevent 2.0.16-stable, OpenSSL 1.0.1 and Zlib 1.2.3.4. Apr 06 11:03:39.832 [notice] Tor can't help you if you use it wrong! Learn how to be safe at https://www.torproject.org/download/download#warning Apr 06 11:03:39.833 [notice] This version is not a stable Tor release. Expect more bugs than usual. Apr 06 11:03:39.833 [notice] Read configuration file "/home/atagar/.tor/torrc". Apr 06 11:03:39.838 [notice] Opening Socks listener on 127.0.0.1:9050 Apr 06 11:03:39.838 [notice] Opening Control listener on 127.0.0.1:9051 Apr 06 11:03:39.000 [notice] Bootstrapped 0%: Starting Apr 06 11:03:42.000 [notice] Bootstrapped 45%: Asking for relay descriptors Apr 06 11:03:43.000 [notice] Bootstrapped 50%: Loading relay descriptors Apr 06 11:03:49.000 [notice] New control connection opened from 127.0.0.1. Apr 06 11:03:51.000 [notice] Bootstrapped 55%: Loading relay descriptors Apr 06 11:03:51.000 [notice] Bootstrapped 63%: Loading relay descriptors Apr 06 11:03:51.000 [notice] Bootstrapped 69%: Loading relay descriptors Apr 06 11:03:51.000 [notice] Bootstrapped 74%: Loading relay descriptors Apr 06 11:03:52.000 [notice] Bootstrapped 80%: Connecting to the Tor network Apr 06 11:03:52.000 [notice] Bootstrapped 90%: Establishing a Tor circuit Apr 06 11:03:53.000 Apr 06 11:03:53.000 [notice] Bootstrapped 100%: Done Apr 06 11:03:55.000 [notice] New control connection opened from 127.0.0.1. Apr 06 11:53:46.000 [notice] Interrupt: exiting cleanly. nyx-2.0.4/test/log/log_entry.py0000664000175000017500000000170013173140703017166 0ustar atagaratagar00000000000000import unittest import nyx.log from nyx.log import LogEntry class TestLogEntry(unittest.TestCase): def setUp(self): nyx.log.GROUP_BY_DAY = False def tearDown(self): nyx.log.GROUP_BY_DAY = True def test_dedup_key_by_messages(self): entry = LogEntry(1333738434, 'INFO', 'tor_lockfile_lock(): Locking "/home/atagar/.tor/lock"') self.assertEqual('INFO:tor_lockfile_lock(): Locking "/home/atagar/.tor/lock"', entry.dedup_key) def test_dedup_key_by_prefix(self): # matches using a prefix specified in dedup.cfg entry = LogEntry(1333738434, 'NYX_DEBUG', 'GETCONF MyFamily (runtime: 0.0007)') self.assertEqual('NYX_DEBUG:GETCONF MyFamily (', entry.dedup_key) def test_dedup_key_with_wildcard(self): # matches using a wildcard specified in dedup.cfg entry = LogEntry(1333738434, 'NOTICE', 'Bootstrapped 72%: Loading relay descriptors.') self.assertEqual('NOTICE:*Loading relay descriptors.', entry.dedup_key) nyx-2.0.4/test/subwindow.py0000664000175000017500000001661013177120634016437 0ustar atagaratagar00000000000000""" Unit tests for nyx.curses. Not entirely sure why this file can't be called 'curses.py' but doing so causes the unittest module to fail internally. """ import unittest import curses import curses.ascii import nyx.curses import nyx.panel.interpreter import test from test import require_curses from nyx.curses import Color, Attr try: # added in python 3.3 from unittest.mock import call, Mock except ImportError: from mock import call, Mock EXPECTED_ADDSTR_WRAP = """ 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 0123456789 """.strip() EXPECTED_BOX = """ +---+ | | +---+ """.strip() EXPECTED_SCROLLBAR_TOP = """ *| *| *| *| | | | | | -+ """.strip() EXPECTED_SCROLLBAR_MIDDLE = """ | *| *| *| *| | | | | -+ """.strip() EXPECTED_SCROLLBAR_BOTTOM = """ | | | | | *| *| *| *| -+ """.strip() DIMENSIONS = (40, 80) def no_op_handler(textbox, key): return key def _textbox(x = 0, text = ''): textbox = Mock() textbox.win.getyx.return_value = (0, x) textbox.win.getmaxyx.return_value = (0, 40) # allow up to forty characters textbox.gather.return_value = text return textbox class TestCurses(unittest.TestCase): def test_asci_to_curses(self): self.assertEqual([], nyx.curses.asci_to_curses('')) self.assertEqual([('hi!', ())], nyx.curses.asci_to_curses('hi!')) self.assertEqual([('hi!', (Color.RED,))], nyx.curses.asci_to_curses('\x1b[31mhi!\x1b[0m')) self.assertEqual([('boo', ()), ('hi!', (Color.RED, Attr.BOLD))], nyx.curses.asci_to_curses('boo\x1b[31;1mhi!\x1b[0m')) self.assertEqual([('boo', ()), ('hi', (Color.RED,)), (' dami!', (Color.RED, Attr.BOLD))], nyx.curses.asci_to_curses('boo\x1b[31mhi\x1b[1m dami!\x1b[0m')) self.assertEqual([('boo', ()), ('hi', (Color.RED,)), (' dami!', (Color.BLUE,))], nyx.curses.asci_to_curses('boo\x1b[31mhi\x1b[34m dami!\x1b[0m')) self.assertEqual([('boo', ()), ('hi!', (Color.RED, Attr.BOLD)), ('and bye!', ())], nyx.curses.asci_to_curses('boo\x1b[31;1mhi!\x1b[0mand bye!')) @require_curses def test_addstr(self): def _draw(subwindow): subwindow.addstr(0, 0, '0123456789' * 10) # should be trimmed to the subwindow width (80 columns) self.assertEqual('01234567890123456789012345678901234567890123456789012345678901234567890123456789', test.render(_draw).content) @require_curses def test_addstr_wrap(self): def _draw(subwindow): subwindow.addstr_wrap(0, 0, '0123456789 ' * 10, 25) self.assertEqual(EXPECTED_ADDSTR_WRAP, test.render(_draw).content) @require_curses def test_addstr_wrap_single_long_word(self): def _draw(subwindow): subwindow.addstr_wrap(0, 0, '0123456789' * 10, 20) self.assertEqual('01234567890123456...', test.render(_draw).content) @require_curses def test_box(self): def _draw(subwindow): subwindow.box(width = 5, height = 3) self.assertEqual(EXPECTED_BOX, test.render(_draw).content) @require_curses def test_scrollbar_top(self): def _draw(subwindow): subwindow.scrollbar(15, 0, 30, fill_char = '*') self.assertEqual(EXPECTED_SCROLLBAR_TOP, test.render(_draw).content.strip()) @require_curses def test_scrollbar_middle(self): def _draw(subwindow): subwindow.scrollbar(15, 1, 30, fill_char = '*') # even scrolling down just one index should be visible self.assertEqual(EXPECTED_SCROLLBAR_MIDDLE, test.render(_draw).content.strip()) @require_curses def test_scrollbar_bottom(self): def _draw(subwindow): subwindow.scrollbar(15, 20, 30, fill_char = '*') self.assertEqual(EXPECTED_SCROLLBAR_BOTTOM, test.render(_draw).content.strip()) def test_handle_key_with_text(self): self.assertEqual(ord('a'), nyx.curses._handle_key(_textbox(), ord('a'))) def test_handle_key_with_esc(self): self.assertEqual(curses.ascii.BEL, nyx.curses._handle_key(_textbox(), 27)) def test_handle_key_with_home(self): textbox = _textbox() nyx.curses._handle_key(textbox, curses.KEY_HOME) self.assertEquals(call(0, 0), textbox.win.move.call_args) def test_handle_key_with_end(self): textbox = _textbox() textbox.gather.return_value = 'Sample Text' nyx.curses._handle_key(textbox, curses.KEY_END) self.assertEquals(call(0, 10), textbox.win.move.call_args) def test_handle_key_with_right_arrow(self): textbox = _textbox() textbox.gather.return_value = 'Sample Text' nyx.curses._handle_key(textbox, curses.KEY_RIGHT) # move is called twice, to revert the gather() and move the cursor self.assertEquals(2, textbox.win.move.call_count) self.assertEquals(call(0, 1), textbox.win.move.call_args) def test_handle_key_with_right_arrow_at_end(self): textbox = _textbox(x = 10) textbox.gather.return_value = 'Sample Text' nyx.curses._handle_key(textbox, curses.KEY_RIGHT) # move is only called to revert the gather() self.assertEquals(1, textbox.win.move.call_count) self.assertEquals(call(0, 10), textbox.win.move.call_args) def test_handle_key_when_resized(self): self.assertEqual(curses.ascii.BEL, nyx.curses._handle_key(_textbox(), 410)) def test_handle_tab_completion_no_op(self): result = nyx.curses._handle_tab_completion(no_op_handler, lambda txt_input: ['GETINFO version'], _textbox(), ord('a')) self.assertEqual(ord('a'), result) def test_handle_tab_completion_no_matches(self): textbox = _textbox(text = 'GETINF') result = nyx.curses._handle_tab_completion(no_op_handler, lambda txt_input: [], textbox, 9) self.assertEqual(None, result) # consumes input self.assertFalse(textbox.win.addstr.called) def test_handle_tab_completion_single_match(self): textbox = _textbox(text = 'GETINF') result = nyx.curses._handle_tab_completion(no_op_handler, lambda txt_input: ['GETINFO version'], textbox, 9) self.assertEqual(None, result) # consumes input self.assertEquals(call(0, 15), textbox.win.move.call_args) # move cursor to end self.assertEqual(call(0, 0, 'GETINFO version'), textbox.win.addstr.call_args) def test_handle_tab_completion_multiple_matches(self): textbox = _textbox(text = 'GETINF') result = nyx.curses._handle_tab_completion(no_op_handler, lambda txt_input: ['GETINFO version', 'GETINFO info/events'], textbox, 9) self.assertEqual(None, result) # consumes input self.assertEquals(call(0, 8), textbox.win.move.call_args) # move cursor to end self.assertEqual(call(0, 0, 'GETINFO '), textbox.win.addstr.call_args) def test_text_backlog_no_op(self): backlog = nyx.curses._TextBacklog(['GETINFO version']) textbox = _textbox() self.assertEqual(ord('a'), backlog._handler(no_op_handler, textbox, ord('a'))) self.assertFalse(textbox.win.addstr.called) def test_text_backlog_fills_history(self): backlog = nyx.curses._TextBacklog(['GETINFO version']) textbox = _textbox() self.assertEqual(None, backlog._handler(no_op_handler, textbox, curses.KEY_UP)) self.assertEqual(call(0, 0, 'GETINFO version'), textbox.win.addstr.call_args) def test_text_backlog_remembers_custom_input(self): backlog = nyx.curses._TextBacklog(['GETINFO version']) textbox = _textbox(text = 'hello') self.assertEqual(None, backlog._handler(no_op_handler, textbox, curses.KEY_UP)) self.assertEqual(call(0, 0, 'GETINFO version'), textbox.win.addstr.call_args) self.assertEqual(None, backlog._handler(no_op_handler, textbox, curses.KEY_DOWN)) self.assertEqual(call(0, 0, 'hello'), textbox.win.addstr.call_args) nyx-2.0.4/test/cache.py0000664000175000017500000001427313177120634015464 0ustar atagaratagar00000000000000""" Unit tests for nyx.cache. """ import re import tempfile import time import unittest import nyx try: # added in python 3.3 from unittest.mock import Mock, patch except ImportError: from mock import Mock, patch class TestCache(unittest.TestCase): def setUp(self): nyx.CACHE = None # drop cached database reference @patch('nyx.data_directory', Mock(return_value = None)) def test_memory_cache(self): """ Create a cache in memory. """ cache = nyx.cache() self.assertEqual((0, 'main', ''), cache._query('PRAGMA database_list').fetchone()) with cache.write() as writer: writer.record_relay('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '208.113.165.162', 1443, 'caersidi') self.assertEqual('caersidi', cache.relay_nickname('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66')) def test_file_cache(self): """ Create a new cache file, and ensure we can reload cached results. """ with tempfile.NamedTemporaryFile(suffix = '.sqlite') as tmp: with patch('nyx.data_directory', Mock(return_value = tmp.name)): cache = nyx.cache() self.assertEqual((0, 'main', tmp.name), cache._query('PRAGMA database_list').fetchone()) with cache.write() as writer: writer.record_relay('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '208.113.165.162', 1443, 'caersidi') nyx.CACHE = None cache = nyx.cache() self.assertEqual('caersidi', cache.relay_nickname('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66')) @patch('nyx.data_directory', Mock(return_value = None)) def test_relays_for_address(self): """ Basic checks for fetching relays by their address. """ cache = nyx.cache() with cache.write() as writer: writer.record_relay('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '208.113.165.162', 1443, 'caersidi1') writer.record_relay('9695DFC35FFEB861329B9F1AB04C46397020CE31', '128.31.0.34', 9101, 'moria1') writer.record_relay('74A910646BCEEFBCD2E874FC1DC997430F968145', '208.113.165.162', 1543, 'caersidi2') self.assertEqual({9101: '9695DFC35FFEB861329B9F1AB04C46397020CE31'}, cache.relays_for_address('128.31.0.34')) self.assertEqual({1443: '3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', 1543: '74A910646BCEEFBCD2E874FC1DC997430F968145'}, cache.relays_for_address('208.113.165.162')) self.assertEqual({}, cache.relays_for_address('199.254.238.53')) @patch('nyx.data_directory', Mock(return_value = None)) def test_relay_nickname(self): """ Basic checks for registering and fetching nicknames. """ cache = nyx.cache() with cache.write() as writer: writer.record_relay('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '208.113.165.162', 1443, 'caersidi') writer.record_relay('9695DFC35FFEB861329B9F1AB04C46397020CE31', '128.31.0.34', 9101, 'moria1') writer.record_relay('74A910646BCEEFBCD2E874FC1DC997430F968145', '199.254.238.53', 443, 'longclaw') self.assertEqual('moria1', cache.relay_nickname('9695DFC35FFEB861329B9F1AB04C46397020CE31')) self.assertEqual('longclaw', cache.relay_nickname('74A910646BCEEFBCD2E874FC1DC997430F968145')) self.assertEqual('caersidi', cache.relay_nickname('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66')) self.assertEqual(None, cache.relay_nickname('66E1D8F00C49820FE8AA26003EC49B6F069E8AE3')) @patch('nyx.data_directory', Mock(return_value = None)) def test_relay_address(self): """ Basic checks for registering and fetching nicknames. """ cache = nyx.cache() with cache.write() as writer: writer.record_relay('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '208.113.165.162', 1443, 'caersidi') writer.record_relay('9695DFC35FFEB861329B9F1AB04C46397020CE31', '128.31.0.34', 9101, 'moria1') writer.record_relay('74A910646BCEEFBCD2E874FC1DC997430F968145', '199.254.238.53', 443, 'longclaw') self.assertEqual(('128.31.0.34', 9101), cache.relay_address('9695DFC35FFEB861329B9F1AB04C46397020CE31')) self.assertEqual(('199.254.238.53', 443), cache.relay_address('74A910646BCEEFBCD2E874FC1DC997430F968145')) self.assertEqual(('208.113.165.162', 1443), cache.relay_address('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66')) self.assertEqual(None, cache.relay_address('66E1D8F00C49820FE8AA26003EC49B6F069E8AE3')) @patch('nyx.data_directory', Mock(return_value = None)) def test_relays_updated_at(self): """ Basic checks for getting when relay information was last updated. """ before = time.time() time.sleep(0.01) cache = nyx.cache() with cache.write() as writer: writer.record_relay('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '208.113.165.162', 1443, 'caersidi1') time.sleep(0.01) after = time.time() self.assertTrue(before < cache.relays_updated_at() < after) @patch('nyx.data_directory', Mock(return_value = None)) def test_record_relay_when_updating(self): cache = nyx.cache() with cache.write() as writer: writer.record_relay('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '208.113.165.162', 1443, 'caersidi') self.assertEqual('caersidi', cache.relay_nickname('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66')) with cache.write() as writer: writer.record_relay('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '128.31.0.34', 9101, 'moria1') self.assertEqual('moria1', cache.relay_nickname('3EA8E960F6B94CE30062AA8EF02894C00F8D1E66')) @patch('nyx.data_directory', Mock(return_value = None)) def test_record_relay_when_invalid(self): """ Provide malformed information to record_relay. """ with nyx.cache().write() as writer: self.assertRaisesRegexp(ValueError, re.escape("'blarg' isn't a valid fingerprint"), writer.record_relay, 'blarg', '208.113.165.162', 1443, 'caersidi') self.assertRaisesRegexp(ValueError, re.escape("'blarg' isn't a valid address"), writer.record_relay, '3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', 'blarg', 1443, 'caersidi') self.assertRaisesRegexp(ValueError, re.escape("'blarg' isn't a valid port"), writer.record_relay, '3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '208.113.165.162', 'blarg', 'caersidi') self.assertRaisesRegexp(ValueError, re.escape("'~blarg' isn't a valid nickname"), writer.record_relay, '3EA8E960F6B94CE30062AA8EF02894C00F8D1E66', '208.113.165.162', 1443, '~blarg') nyx-2.0.4/test/installation.py0000664000175000017500000000266513200140316017106 0ustar atagaratagar00000000000000import glob import os import shutil import subprocess import sys import unittest import nyx import stem.util.system class TestInstallation(unittest.TestCase): def test_installing_stem(self): base_directory = os.path.sep.join(__file__.split(os.path.sep)[:-2]) if not os.path.exists(os.path.sep.join([base_directory, 'setup.py'])): self.skipTest('(only for git checkout)') original_cwd = os.getcwd() try: os.chdir(base_directory) stem.util.system.call(sys.executable + ' setup.py install --prefix /tmp/nyx_test') stem.util.system.call(sys.executable + ' setup.py clean --all') # tidy up the build directory site_packages_paths = glob.glob('/tmp/nyx_test/lib*/*/site-packages') if len(site_packages_paths) != 1: self.fail('We should only have a single site-packages directory, but instead had: %s' % site_packages_paths) self.assertEqual(nyx.__version__, stem.util.system.call([sys.executable, '-c', "import sys;sys.path.insert(0, '%s');import nyx;print(nyx.__version__)" % site_packages_paths[0]])[0]) process_path = [site_packages_paths[0]] + sys.path process = subprocess.Popen(['/tmp/nyx_test/bin/nyx', '--help'], stdout = subprocess.PIPE, env = {'PYTHONPATH': ':'.join(process_path)}) stdout = process.communicate()[0] self.assertTrue(stdout.startswith(b'Usage nyx [OPTION]')) finally: shutil.rmtree('/tmp/nyx_test') os.chdir(original_cwd) nyx-2.0.4/test/tracker/0000775000175000017500000000000013200142726015464 5ustar atagaratagar00000000000000nyx-2.0.4/test/tracker/port_usage_tracker.py0000664000175000017500000001061713177120634021735 0ustar atagaratagar00000000000000import time import unittest from nyx.tracker import Process, PortUsageTracker, _process_for_ports try: # added in python 3.3 from unittest.mock import Mock, patch except ImportError: from mock import Mock, patch LSOF_OUTPUT = """\ COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME tor 2001 atagar 14u IPv4 14048 0t0 TCP localhost:9051->localhost:37277 (ESTABLISHED) tor 2001 atagar 15u IPv4 22024 0t0 TCP localhost:9051->localhost:51849 (ESTABLISHED) python 2462 atagar 3u IPv4 14047 0t0 TCP localhost:37277->localhost:9051 (ESTABLISHED) python 3444 atagar 3u IPv4 22023 0t0 TCP localhost:51849->localhost:9051 (ESTABLISHED) """ BAD_LSOF_OUTPUT_NO_ENTRY = """\ COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME """ BAD_LSOF_OUTPUT_NOT_ESTABLISHED = """\ COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME tor 2001 atagar 14u IPv4 14048 0t0 TCP localhost:9051->localhost:37277 (CLOSE_WAIT) """ BAD_LSOF_OUTPUT_MISSING_FIELD = """\ COMMAND PID USER TYPE DEVICE SIZE/OFF NODE NAME tor 2001 atagar IPv4 14048 0t0 TCP localhost:9051->localhost:37277 (ESTABLISHED) """ BAD_LSOF_OUTPUT_UNRECOGNIZED_MAPPING = """\ COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME tor 2001 atagar 14u IPv4 14048 0t0 TCP localhost:9051=>localhost:37277 (ESTABLISHED) """ BAD_LSOF_OUTPUT_NO_ADDRESS = """\ COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME tor 2001 atagar 14u IPv4 14048 0t0 TCP 9051->localhost:37277 (ESTABLISHED) """ BAD_LSOF_OUTPUT_INVALID_PORT = """\ COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME tor 2001 atagar 14u IPv4 14048 0t0 TCP localhost:9037351->localhost:37277 (ESTABLISHED) """ class TestPortUsageTracker(unittest.TestCase): @patch('nyx.tracker.system.call', Mock(return_value = LSOF_OUTPUT.split('\n'))) def test_process_for_ports(self): self.assertEqual({}, _process_for_ports([], [])) self.assertEqual({80: None, 443: None}, _process_for_ports([80, 443], [])) self.assertEqual({80: None, 443: None}, _process_for_ports([], [80, 443])) self.assertEqual({37277: Process(2462, 'python'), 51849: Process(2001, 'tor')}, _process_for_ports([37277], [51849])) @patch('nyx.tracker.system.call') def test_process_for_ports_malformed(self, call_mock): # Issues that are valid, but should result in us not having any content. test_inputs = ( BAD_LSOF_OUTPUT_NO_ENTRY, BAD_LSOF_OUTPUT_NOT_ESTABLISHED, ) for test_input in test_inputs: call_mock.return_value = test_input.split('\n') self.assertEqual({80: None, 443: None}, _process_for_ports([80], [443])) # Isuses that are reported as errors. call_mock.return_value = [] self.assertRaises(IOError, _process_for_ports, [80], [443]) test_inputs = ( BAD_LSOF_OUTPUT_MISSING_FIELD, BAD_LSOF_OUTPUT_UNRECOGNIZED_MAPPING, BAD_LSOF_OUTPUT_NO_ADDRESS, BAD_LSOF_OUTPUT_INVALID_PORT, ) for test_input in test_inputs: call_mock.return_value = test_input.split('\n') self.assertRaises(IOError, _process_for_ports, [80], [443]) @patch('nyx.tracker.tor_controller') @patch('nyx.tracker._process_for_ports') @patch('nyx.tracker.system', Mock(return_value = Mock())) def test_fetching_samplings(self, process_for_ports_mock, tor_controller_mock): tor_controller_mock().get_pid.return_value = 12345 process_for_ports_mock.return_value = {37277: 'python', 51849: 'tor'} with PortUsageTracker(0.02) as daemon: time.sleep(0.01) self.assertEqual({}, daemon.query([37277, 51849], [])) time.sleep(0.04) self.assertEqual({37277: 'python', 51849: 'tor'}, daemon.query([37277, 51849], [])) @patch('nyx.tracker.tor_controller') @patch('nyx.tracker._process_for_ports') @patch('nyx.tracker.system', Mock(return_value = Mock())) def test_resolver_failover(self, process_for_ports_mock, tor_controller_mock): tor_controller_mock().get_pid.return_value = 12345 process_for_ports_mock.side_effect = IOError() with PortUsageTracker(0.01) as daemon: # We shouldn't attempt lookups (nor encounter failures) without ports to # query. time.sleep(0.05) self.assertEqual(0, daemon._failure_count) daemon.query([37277, 51849], []) time.sleep(0.015) self.assertTrue(daemon.is_alive()) time.sleep(0.1) self.assertFalse(daemon.is_alive()) nyx-2.0.4/test/tracker/__init__.py0000664000175000017500000000022113141704000017561 0ustar atagaratagar00000000000000""" Unit tests for nyx's tracker utilities. """ __all__ = [ 'connection_tracker', 'daemon', 'port_usage_tracker', 'resource_tracker', ] nyx-2.0.4/test/tracker/resource_tracker.py0000664000175000017500000001322113177120634021406 0ustar atagaratagar00000000000000import time import unittest from nyx.tracker import ResourceTracker, _resources_via_ps, _resources_via_proc try: # added in python 3.3 from unittest.mock import Mock, patch except ImportError: from mock import Mock, patch PS_OUTPUT = """\ TIME ELAPSED RSS %MEM 00:00:02 00:18 18848 0.4 """ class TestResourceTracker(unittest.TestCase): @patch('nyx.tracker.tor_controller') @patch('nyx.tracker._resources_via_proc') @patch('nyx.tracker.system', Mock(return_value = Mock())) @patch('nyx.tracker.proc.is_available', Mock(return_value = True)) def test_fetching_samplings(self, resources_via_proc_mock, tor_controller_mock): tor_controller_mock().get_pid.return_value = 12345 resources_via_proc_mock.return_value = (105.3, 2.4, 8072, 0.3) with ResourceTracker(0.04) as daemon: time.sleep(0.01) resources = daemon.get_value() self.assertEqual(1, daemon.run_counter()) self.assertEqual(0.0, resources.cpu_sample) self.assertEqual(43.875, resources.cpu_average) self.assertEqual(105.3, resources.cpu_total) self.assertEqual(8072, resources.memory_bytes) self.assertEqual(0.3, resources.memory_percent) self.assertTrue((time.time() - resources.timestamp) < 0.5) resources_via_proc_mock.return_value = (800.3, 3.2, 6020, 0.26) time.sleep(0.05) resources = daemon.get_value() self.assertEqual(2, daemon.run_counter()) self.assertTrue(17000 < resources.cpu_sample < 18000) self.assertEqual(250.09374999999997, resources.cpu_average) self.assertEqual(800.3, resources.cpu_total) self.assertEqual(6020, resources.memory_bytes) self.assertEqual(0.26, resources.memory_percent) self.assertTrue((time.time() - resources.timestamp) < 0.5) resources_via_proc_mock.assert_called_with(12345) @patch('nyx.tracker.tor_controller') @patch('nyx.tracker.proc.is_available') @patch('nyx.tracker._resources_via_ps', Mock(return_value = (105.3, 2.4, 8072, 0.3))) @patch('nyx.tracker._resources_via_proc', Mock(return_value = (340.3, 3.2, 6020, 0.26))) @patch('nyx.tracker.system', Mock(return_value = Mock())) def test_picking_proc_or_ps(self, is_proc_available_mock, tor_controller_mock): tor_controller_mock().get_pid.return_value = 12345 is_proc_available_mock.return_value = True with ResourceTracker(0.04) as daemon: time.sleep(0.01) resources = daemon.get_value() self.assertEqual(1, daemon.run_counter()) self.assertEqual(0.0, resources.cpu_sample) self.assertEqual(106.34375, resources.cpu_average) self.assertEqual(340.3, resources.cpu_total) self.assertEqual(6020, resources.memory_bytes) self.assertEqual(0.26, resources.memory_percent) self.assertTrue((time.time() - resources.timestamp) < 0.5) is_proc_available_mock.return_value = False with ResourceTracker(0.04) as daemon: time.sleep(0.01) resources = daemon.get_value() self.assertEqual(1, daemon.run_counter()) self.assertEqual(0.0, resources.cpu_sample) self.assertEqual(43.875, resources.cpu_average) self.assertEqual(105.3, resources.cpu_total) self.assertEqual(8072, resources.memory_bytes) self.assertEqual(0.3, resources.memory_percent) self.assertTrue((time.time() - resources.timestamp) < 0.5) @patch('nyx.tracker.tor_controller') @patch('nyx.tracker._resources_via_ps', Mock(return_value = (105.3, 2.4, 8072, 0.3))) @patch('nyx.tracker._resources_via_proc', Mock(side_effect = IOError())) @patch('nyx.tracker.system', Mock(return_value = Mock())) @patch('nyx.tracker.proc.is_available', Mock(return_value = True)) def test_failing_over_to_ps(self, tor_controller_mock): tor_controller_mock().get_pid.return_value = 12345 with ResourceTracker(0.01) as daemon: time.sleep(0.015) self.assertEqual(True, daemon._use_proc) resources = daemon.get_value() self.assertEqual(0, daemon.run_counter()) self.assertEqual(0.0, resources.cpu_sample) self.assertEqual(0.0, resources.cpu_average) self.assertEqual(0, resources.cpu_total) self.assertEqual(0, resources.memory_bytes) self.assertEqual(0.0, resources.memory_percent) self.assertEqual(0.0, resources.timestamp) while daemon.run_counter() < 1: time.sleep(0.01) self.assertEqual(False, daemon._use_proc) resources = daemon.get_value() self.assertEqual(0.0, resources.cpu_sample) self.assertEqual(43.875, resources.cpu_average) self.assertEqual(105.3, resources.cpu_total) self.assertEqual(8072, resources.memory_bytes) self.assertEqual(0.3, resources.memory_percent) self.assertTrue((time.time() - resources.timestamp) < 0.5) @patch('nyx.tracker.system.call', Mock(return_value = PS_OUTPUT.split('\n'))) def test_resources_via_ps(self): total_cpu_time, uptime, memory_in_bytes, memory_in_percent = _resources_via_ps(12345) self.assertEqual(2.0, total_cpu_time) self.assertEqual(18, uptime) self.assertEqual(19300352, memory_in_bytes) self.assertEqual(0.004, memory_in_percent) @patch('time.time', Mock(return_value = 1388967218.973117)) @patch('nyx.tracker.proc.stats', Mock(return_value = (1.5, 0.5, 1388967200.9))) @patch('nyx.tracker.proc.memory_usage', Mock(return_value = (19300352, 6432))) @patch('nyx.tracker.proc.physical_memory', Mock(return_value = 4825088000)) def test_resources_via_proc(self): total_cpu_time, uptime, memory_in_bytes, memory_in_percent = _resources_via_proc(12345) self.assertEqual(2.0, total_cpu_time) self.assertEqual(18, int(uptime)) self.assertEqual(19300352, memory_in_bytes) self.assertEqual(0.004, memory_in_percent) nyx-2.0.4/test/tracker/daemon.py0000664000175000017500000000506713177120634017320 0ustar atagaratagar00000000000000import time import unittest from nyx.tracker import Daemon try: # added in python 3.3 from unittest.mock import Mock, patch except ImportError: from mock import Mock, patch class TestDaemon(unittest.TestCase): @patch('nyx.tracker.tor_controller') @patch('nyx.tracker.system') def test_init(self, system_mock, tor_controller_mock): # Check that we register ourselves to listen for status changes, and # properly retrieve the process' pid and name. tor_controller_mock().get_pid.return_value = 12345 system_mock.name_by_pid.return_value = 'local_tor' daemon = Daemon(0.05) self.assertEqual(0.05, daemon.get_rate()) self.assertEqual(12345, daemon._process_pid) self.assertEqual('local_tor', daemon._process_name) tor_controller_mock().add_status_listener.assert_called_with(daemon._tor_status_listener) system_mock.name_by_pid.assert_called_with(12345) @patch('nyx.tracker.tor_controller') @patch('nyx.tracker.system') def test_init_without_name(self, system_mock, tor_controller_mock): # Check when we default to 'tor' if unable to determine the process' name. tor_controller_mock().get_pid.return_value = 12345 system_mock.name_by_pid.return_value = None daemon = Daemon(0.05) self.assertEqual('tor', daemon._process_name) @patch('nyx.tracker.tor_controller') @patch('nyx.tracker.system') def test_init_without_pid(self, system_mock, tor_controller_mock): # Check when we can't determine tor's pid. tor_controller_mock().get_pid.return_value = None daemon = Daemon(0.05) self.assertEqual(None, daemon._process_pid) self.assertEqual('tor', daemon._process_name) self.assertEqual(0, system_mock.call_count) @patch('nyx.tracker.tor_controller', Mock(return_value = Mock())) @patch('nyx.tracker.system', Mock(return_value = Mock())) def test_daemon_calls_task(self): # Check that our Daemon calls the task method at the given rate. with Daemon(0.01) as daemon: time.sleep(0.05) self.assertTrue(2 < daemon.run_counter()) @patch('nyx.tracker.tor_controller', Mock(return_value = Mock())) @patch('nyx.tracker.system', Mock(return_value = Mock())) def test_pausing_daemon(self): # Check that we can pause and unpause daemon. with Daemon(0.01) as daemon: time.sleep(0.2) self.assertTrue(2 < daemon.run_counter()) daemon.set_paused(True) daemon._run_counter = 0 time.sleep(0.05) self.assertEqual(0, daemon.run_counter()) daemon.set_paused(False) time.sleep(0.2) self.assertTrue(2 < daemon.run_counter()) nyx-2.0.4/test/tracker/connection_tracker.py0000664000175000017500000001142613177440474021732 0ustar atagaratagar00000000000000import time import unittest from nyx.tracker import ConnectionTracker from stem.util import connection try: # added in python 3.3 from unittest.mock import Mock, patch except ImportError: from mock import Mock, patch STEM_CONNECTIONS = [ connection.Connection('127.0.0.1', 3531, '75.119.206.243', 22, 'tcp', False), connection.Connection('127.0.0.1', 1766, '86.59.30.40', 443, 'tcp', False), connection.Connection('127.0.0.1', 1059, '74.125.28.106', 80, 'tcp', False) ] class TestConnectionTracker(unittest.TestCase): @patch('nyx.tracker.tor_controller') @patch('nyx.tracker.connection.get_connections') @patch('nyx.tracker.system', Mock(return_value = Mock())) @patch('stem.util.proc.is_available', Mock(return_value = False)) @patch('nyx.tracker.connection.system_resolvers', Mock(return_value = [connection.Resolver.NETSTAT])) def test_fetching_connections(self, get_value_mock, tor_controller_mock): tor_controller_mock().get_pid.return_value = 12345 tor_controller_mock().get_conf.return_value = '0' get_value_mock.return_value = STEM_CONNECTIONS with ConnectionTracker(0.04) as daemon: time.sleep(0.01) connections = daemon.get_value() self.assertEqual(1, daemon.run_counter()) self.assertEqual([conn.remote_address for conn in STEM_CONNECTIONS], [conn.remote_address for conn in connections]) get_value_mock.return_value = [] # no connection results time.sleep(0.05) connections = daemon.get_value() self.assertEqual(2, daemon.run_counter()) self.assertEqual([], connections) @patch('nyx.tracker.tor_controller') @patch('nyx.tracker.connection.get_connections') @patch('nyx.tracker.system', Mock(return_value = Mock())) @patch('stem.util.proc.is_available', Mock(return_value = False)) @patch('nyx.tracker.connection.system_resolvers', Mock(return_value = [connection.Resolver.NETSTAT, connection.Resolver.LSOF])) def test_resolver_failover(self, get_value_mock, tor_controller_mock): tor_controller_mock().get_pid.return_value = 12345 tor_controller_mock().get_conf.return_value = '0' get_value_mock.side_effect = IOError() with ConnectionTracker(0.01) as daemon: time.sleep(0.015) self.assertEqual([connection.Resolver.NETSTAT, connection.Resolver.LSOF], daemon._resolvers) self.assertEqual([], daemon.get_value()) time.sleep(0.025) self.assertEqual([connection.Resolver.LSOF], daemon._resolvers) self.assertEqual([], daemon.get_value()) time.sleep(0.035) self.assertEqual([], daemon._resolvers) self.assertEqual([], daemon.get_value()) # Now make connection resolution work. We still shouldn't provide any # results since we stopped looking. get_value_mock.return_value = STEM_CONNECTIONS[:2] get_value_mock.side_effect = None time.sleep(0.05) self.assertEqual([], daemon.get_value()) # Finally, select a custom resolver. This should cause us to query again # reguardless of our prior failures. daemon.set_custom_resolver(connection.Resolver.NETSTAT) time.sleep(0.05) self.assertEqual([conn.remote_address for conn in STEM_CONNECTIONS[:2]], [conn.remote_address for conn in daemon.get_value()]) @patch('nyx.tracker.tor_controller') @patch('nyx.tracker.connection.get_connections') @patch('nyx.tracker.system', Mock(return_value = Mock())) @patch('stem.util.proc.is_available', Mock(return_value = False)) @patch('nyx.tracker.connection.system_resolvers', Mock(return_value = [connection.Resolver.NETSTAT])) def test_tracking_uptime(self, get_value_mock, tor_controller_mock): tor_controller_mock().get_pid.return_value = 12345 tor_controller_mock().get_conf.return_value = '0' get_value_mock.return_value = [STEM_CONNECTIONS[0]] first_start_time = time.time() with ConnectionTracker(0.04) as daemon: time.sleep(0.01) connections = daemon.get_value() self.assertEqual(1, len(connections)) self.assertEqual(STEM_CONNECTIONS[0].remote_address, connections[0].remote_address) self.assertTrue(first_start_time <= connections[0].start_time <= time.time()) self.assertTrue(connections[0].is_legacy) second_start_time = time.time() get_value_mock.return_value = STEM_CONNECTIONS[:2] time.sleep(0.05) connections = daemon.get_value() self.assertEqual(2, len(connections)) self.assertEqual(STEM_CONNECTIONS[0].remote_address, connections[0].remote_address) self.assertTrue(first_start_time < connections[0].start_time < time.time()) self.assertTrue(connections[0].is_legacy) self.assertEqual(STEM_CONNECTIONS[1].remote_address, connections[1].remote_address) self.assertTrue(second_start_time < connections[1].start_time < time.time()) self.assertFalse(connections[1].is_legacy) nyx-2.0.4/test/arguments.py0000664000175000017500000000554413173140703016422 0ustar atagaratagar00000000000000""" Unit tests for nyx.arguments. """ import unittest from nyx.arguments import DEFAULT_ARGS, parse, get_help, get_version class TestArgumentParsing(unittest.TestCase): def test_that_we_get_default_values(self): args = parse([]) for attr in DEFAULT_ARGS: self.assertEqual(DEFAULT_ARGS[attr], getattr(args, attr)) def test_that_we_load_arguments(self): args = parse(['--interface', '10.0.0.25:80']) self.assertEqual(('10.0.0.25', 80), args.control_port) args = parse(['--interface', '80']) self.assertEqual((DEFAULT_ARGS['control_port'][0], 80), args.control_port) args = parse(['--socket', '/tmp/my_socket', '--config', '/tmp/my_config']) self.assertEqual('/tmp/my_socket', args.control_socket) self.assertEqual('/tmp/my_config', args.config) args = parse(['--debug', '/tmp/dump']) self.assertEqual('/tmp/dump', args.debug_path) args = parse(['--log', 'DEBUG,NYX_DEBUG']) self.assertEqual('DEBUG,NYX_DEBUG', args.logged_events) args = parse(['--version']) self.assertEqual(True, args.print_version) args = parse(['--help']) self.assertEqual(True, args.print_help) def test_examples(self): args = parse(['-i', '1643']) self.assertEqual((DEFAULT_ARGS['control_port'][0], 1643), args.control_port) args = parse(['-l', 'WARN,ERR', '-c', '/tmp/cfg']) self.assertEqual('WARN,ERR', args.logged_events) self.assertEqual('/tmp/cfg', args.config) def test_that_we_reject_unrecognized_arguments(self): self.assertRaises(ValueError, parse, ['--blarg', 'stuff']) def test_port_and_socket_unset_other(self): args = parse([]) self.assertEqual(DEFAULT_ARGS['control_port'], args.control_port) self.assertEqual(DEFAULT_ARGS['control_socket'], args.control_socket) args = parse(['--interface', '10.0.0.25:80']) self.assertEqual(('10.0.0.25', 80), args.control_port) self.assertEqual(None, args.control_socket) args = parse(['--socket', '/tmp/my_socket']) self.assertEqual(None, args.control_port) self.assertEqual('/tmp/my_socket', args.control_socket) args = parse(['--interface', '10.0.0.25:80', '--socket', '/tmp/my_socket']) self.assertEqual(('10.0.0.25', 80), args.control_port) self.assertEqual('/tmp/my_socket', args.control_socket) def test_that_we_reject_invalid_interfaces(self): invalid_inputs = ( '', ' ', 'blarg', '127.0.0.1', '127.0.0.1:', ':80', '400.0.0.1:80', '127.0.0.1:-5', '127.0.0.1:500000', ) for invalid_input in invalid_inputs: self.assertRaises(ValueError, parse, ['--interface', invalid_input]) def test_help(self): self.assertTrue(get_help().startswith('Usage nyx [OPTION]')) self.assertTrue('change control interface from 127.0.0.1:9051' in get_help()) def test_version(self): self.assertTrue(get_version().startswith('nyx version')) nyx-2.0.4/nyx/0000775000175000017500000000000013200142726013670 5ustar atagaratagar00000000000000nyx-2.0.4/nyx/tracker.py0000664000175000017500000007011313177437350015714 0ustar atagaratagar00000000000000# Copyright 2013-2017, Damian Johnson and The Tor Project # See LICENSE for licensing information """ Background tasks for gathering information about the tor process. :: get_connection_tracker - provides a ConnectionTracker for our tor process get_resource_tracker - provides a ResourceTracker for our tor process get_port_usage_tracker - provides a PortUsageTracker for our system get_consensus_tracker - provides a ConsensusTracker for our tor process stop_trackers - halts any active trackers Daemon - common parent for resolvers |- ConnectionTracker - periodically checks the connections established by tor | |- get_custom_resolver - provide the custom conntion resolver we're using | |- set_custom_resolver - overwrites automatic resolver selecion with a custom resolver | +- get_value - provides our latest connection results | |- ResourceTracker - periodically checks the resource usage of tor | +- get_value - provides our latest resource usage results | |- PortUsageTracker - provides information about port usage on the local system | +- get_processes_using_ports - mapping of ports to the processes using it | |- run_counter - number of successful runs |- get_rate - provides the rate at which we run |- set_rate - sets the rate at which we run |- set_paused - pauses or continues work +- stop - stops further work by the daemon ConsensusTracker - performant lookups for consensus related information |- update - updates the consensus information we're based on |- my_router_status_entry - provides the router status entry for ourselves |- get_relay_nickname - provides the nickname for a given relay |- get_relay_fingerprints - provides relays running at a location +- get_relay_address - provides the address a relay is running at .. data:: Resources Resource usage information retrieved about the tor process. :var float cpu_sample: average cpu usage since we last checked :var float cpu_average: average cpu usage since we first started tracking the process :var float cpu_total: total cpu time the process has used since starting :var int memory_bytes: memory usage of the process in bytes :var float memory_percent: percentage of our memory used by this process :var float timestamp: unix timestamp for when this information was fetched """ import collections import os import time import threading import nyx import stem.control import stem.descriptor.router_status_entry import stem.util.log from nyx import tor_controller from stem.util import conf, connection, enum, proc, str_tools, system CONFIG = conf.config_dict('nyx', { 'connection_rate': 5, 'resource_rate': 5, 'port_usage_rate': 5, }) UNABLE_TO_USE_ANY_RESOLVER_MSG = """ We were unable to use any of your system's resolvers to get tor's connections. This is fine, but means that the connections page will be empty. This is usually permissions related so if you would like to fix this then run nyx with the same user as tor (ie, "sudo -u nyx"). """.strip() CONNECTION_TRACKER = None RESOURCE_TRACKER = None PORT_USAGE_TRACKER = None CONSENSUS_TRACKER = None CustomResolver = enum.Enum( ('INFERENCE', 'by inference'), ) # Extending stem's Connection tuple with attributes for the uptime of the # connection. Connection = collections.namedtuple('Connection', [ 'start_time', 'is_legacy', # boolean to indicate if the connection predated us ] + list(stem.util.connection.Connection._fields)) Resources = collections.namedtuple('Resources', [ 'cpu_sample', 'cpu_average', 'cpu_total', 'memory_bytes', 'memory_percent', 'timestamp', ]) Process = collections.namedtuple('Process', [ 'pid', 'name', ]) class UnresolvedResult(Exception): 'Indicates the application being used by a port is still being determined.' class UnknownApplication(Exception): 'No application could be determined for this port.' def get_connection_tracker(): """ Singleton for tracking the connections established by tor. """ global CONNECTION_TRACKER if CONNECTION_TRACKER is None: CONNECTION_TRACKER = ConnectionTracker(CONFIG['connection_rate']) CONNECTION_TRACKER.start() return CONNECTION_TRACKER def get_resource_tracker(): """ Singleton for tracking the resource usage of our tor process. """ global RESOURCE_TRACKER if RESOURCE_TRACKER is None: RESOURCE_TRACKER = ResourceTracker(CONFIG['resource_rate']) RESOURCE_TRACKER.start() return RESOURCE_TRACKER def get_port_usage_tracker(): """ Singleton for tracking the process using a set of ports. """ global PORT_USAGE_TRACKER if PORT_USAGE_TRACKER is None: PORT_USAGE_TRACKER = PortUsageTracker(CONFIG['port_usage_rate']) PORT_USAGE_TRACKER.start() return PORT_USAGE_TRACKER def get_consensus_tracker(): """ Singleton for tracking the connections established by tor. """ global CONSENSUS_TRACKER if CONSENSUS_TRACKER is None: CONSENSUS_TRACKER = ConsensusTracker() return CONSENSUS_TRACKER def stop_trackers(): """ Halts active trackers, providing back the thread shutting them down. :returns: **threading.Thread** shutting down the daemons """ def halt_trackers(): trackers = filter(lambda t: t and t.is_alive(), [ CONNECTION_TRACKER, RESOURCE_TRACKER, PORT_USAGE_TRACKER, ]) for tracker in trackers: tracker.stop() for tracker in trackers: tracker.join() halt_thread = threading.Thread(target = halt_trackers) halt_thread.setDaemon(True) halt_thread.start() return halt_thread def _resources_via_ps(pid): """ Fetches resource usage information about a given process via ps. This returns a tuple of the form... (total_cpu_time, uptime, memory_in_bytes, memory_in_percent) :param int pid: process to be queried :returns: **tuple** with the resource usage information :raises: **IOError** if unsuccessful """ # ps results are of the form... # # TIME ELAPSED RSS %MEM # 3-08:06:32 21-00:00:12 121844 23.5 # # ... or if Tor has only recently been started... # # TIME ELAPSED RSS %MEM # 0:04.40 37:57 18772 0.9 try: ps_call = system.call('ps -p {pid} -o cputime,etime,rss,%mem'.format(pid = pid)) except OSError as exc: raise IOError(exc) if ps_call and len(ps_call) >= 2: stats = ps_call[1].strip().split() if len(stats) == 4: try: total_cpu_time = str_tools.parse_short_time_label(stats[0]) uptime = str_tools.parse_short_time_label(stats[1]) memory_bytes = int(stats[2]) * 1024 # ps size is in kb memory_percent = float(stats[3]) / 100.0 return (total_cpu_time, uptime, memory_bytes, memory_percent) except ValueError: pass raise IOError('unrecognized output from ps: %s' % ps_call) def _resources_via_proc(pid): """ Fetches resource usage information about a given process via proc. This returns a tuple of the form... (total_cpu_time, uptime, memory_in_bytes, memory_in_percent) :param int pid: process to be queried :returns: **tuple** with the resource usage information :raises: **IOError** if unsuccessful """ utime, stime, start_time = proc.stats( pid, proc.Stat.CPU_UTIME, proc.Stat.CPU_STIME, proc.Stat.START_TIME, ) total_cpu_time = float(utime) + float(stime) memory_in_bytes = proc.memory_usage(pid)[0] total_memory = proc.physical_memory() uptime = time.time() - float(start_time) memory_in_percent = float(memory_in_bytes) / total_memory return (total_cpu_time, uptime, memory_in_bytes, memory_in_percent) def _process_for_ports(local_ports, remote_ports): """ Provides the name of the process using the given ports. :param list local_ports: local port numbers to look up :param list remote_ports: remote port numbers to look up :returns: **dict** mapping the ports to the associated **Process**, or **None** if it can't be determined :raises: **IOError** if unsuccessful """ def _parse_lsof_line(line): line_comp = line.split() if not line: return None, None, None, None # blank line elif len(line_comp) != 10: raise ValueError('lines are expected to have ten fields: %s' % line) elif line_comp[9] != '(ESTABLISHED)': return None, None, None, None # connection isn't established elif not line_comp[1].isdigit(): raise ValueError('expected the pid (which is the second value) to be an integer: %s' % line) pid = int(line_comp[1]) cmd = line_comp[0] port_map = line_comp[8] if '->' not in port_map: raise ValueError("'%s' is expected to be a '->' separated mapping" % port_map) local, remote = port_map.split('->', 1) if ':' not in local or ':' not in remote: raise ValueError("'%s' is expected to be 'address:port' entries" % port_map) local_port = local.split(':', 1)[1] remote_port = remote.split(':', 1)[1] if not connection.is_valid_port(local_port): raise ValueError("'%s' isn't a valid port" % local_port) elif not connection.is_valid_port(remote_port): raise ValueError("'%s' isn't a valid port" % remote_port) return int(local_port), int(remote_port), pid, cmd # atagar@fenrir:~/Desktop/nyx$ lsof -i tcp:51849 -i tcp:37277 # COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME # tor 2001 atagar 14u IPv4 14048 0t0 TCP localhost:9051->localhost:37277 (ESTABLISHED) # tor 2001 atagar 15u IPv4 22024 0t0 TCP localhost:9051->localhost:51849 (ESTABLISHED) # python 2462 atagar 3u IPv4 14047 0t0 TCP localhost:37277->localhost:9051 (ESTABLISHED) # python 3444 atagar 3u IPv4 22023 0t0 TCP localhost:51849->localhost:9051 (ESTABLISHED) try: lsof_cmd = 'lsof -nP ' + ' '.join(['-i tcp:%s' % port for port in (local_ports + remote_ports)]) lsof_call = system.call(lsof_cmd) except OSError as exc: raise IOError(exc) if lsof_call: results = {} if lsof_call[0].startswith('COMMAND'): lsof_call = lsof_call[1:] # strip the title line for line in lsof_call: try: local_port, remote_port, pid, cmd = _parse_lsof_line(line) if local_port in local_ports: results[local_port] = Process(pid, cmd) elif remote_port in remote_ports: results[remote_port] = Process(pid, cmd) except ValueError as exc: raise IOError('unrecognized output from lsof (%s): %s' % (exc, line)) for unknown_port in set(local_ports).union(remote_ports).difference(results.keys()): results[unknown_port] = None return results raise IOError('no results from lsof') class Daemon(threading.Thread): """ Daemon that can perform a given action at a set rate. Subclasses are expected to implement our _task() method with the work to be done. """ def __init__(self, rate): super(Daemon, self).__init__() self.setDaemon(True) self._process_lock = threading.RLock() self._process_pid = None self._process_name = None self._rate = rate self._last_ran = -1 # time when we last ran self._run_counter = 0 # counter for the number of successful runs self._is_paused = False self._halt = False # terminates thread if true controller = tor_controller() controller.add_status_listener(self._tor_status_listener) self._tor_status_listener(controller, stem.control.State.INIT, None) def run(self): while not self._halt: if self._is_paused or time.time() - self._last_ran < self._rate: sleep_until = self._last_ran + self._rate while not self._halt and time.time() < sleep_until: time.sleep(nyx.PAUSE_TIME) continue # done waiting, try again with self._process_lock: is_successful = False if self._process_pid is not None: try: is_successful = self._task(self._process_pid, self._process_name) except Exception as exc: stem.util.log.notice('BUG: Unexpected exception from %s: %s' % (type(self).__name__, exc)) if is_successful: self._run_counter += 1 self._last_ran = time.time() def _task(self, process_pid, process_name): """ Task the resolver is meant to perform. This should be implemented by subclasses. :param int process_pid: pid of the process we're tracking :param str process_name: name of the process we're tracking :returns: **bool** indicating if our run was successful or not """ return True def run_counter(self): """ Provides the number of times we've successful runs so far. This can be used by callers to determine if our results have been seen by them before or not. :returns: **int** for the run count we're on """ return self._run_counter def get_rate(self): """ Provides the rate at which we perform our task. :returns: **float** for the rate in seconds at which we perform our task """ return self._rate def set_rate(self, rate): """ Sets the rate at which we perform our task in seconds. :param float rate: rate at which to perform work in seconds """ self._rate = rate def set_paused(self, pause): """ Either resumes or holds off on doing further work. :param bool pause: halts work if **True**, resumes otherwise """ self._is_paused = pause def stop(self): """ Halts further work and terminates the thread. """ self._halt = True def _tor_status_listener(self, controller, event_type, _): with self._process_lock: if not self._halt and event_type in (stem.control.State.INIT, stem.control.State.RESET): tor_pid = controller.get_pid(None) tor_cmd = system.name_by_pid(tor_pid) if tor_pid else None self._process_pid = tor_pid self._process_name = tor_cmd if tor_cmd else 'tor' elif event_type == stem.control.State.CLOSED: self._process_pid = None self._process_name = None def __enter__(self): self.start() return self def __exit__(self, exit_type, value, traceback): self.stop() self.join() class ConnectionTracker(Daemon): """ Periodically retrieves the connections established by tor. """ def __init__(self, rate): super(ConnectionTracker, self).__init__(rate) self._connections = [] self._start_times = {} # connection => (unix_timestamp, is_legacy) self._custom_resolver = None self._is_first_run = True # Number of times in a row we've either failed with our current resolver or # concluded that our rate is too low. self._failure_count = 0 self._rate_too_low_count = 0 # If 'DisableDebuggerAttachment 0' is set we can do normal connection # resolution. Otherwise connection resolution by inference is the only game # in town. self._resolvers = [] if tor_controller().get_conf('DisableDebuggerAttachment', None) == '0': self._resolvers = connection.system_resolvers() if stem.util.proc.is_available(): self._resolvers = [CustomResolver.INFERENCE] + self._resolvers stem.util.log.info('Operating System: %s, Connection Resolvers: %s' % (os.uname()[0], ', '.join(self._resolvers))) def _task(self, process_pid, process_name): if self._custom_resolver: resolver = self._custom_resolver is_default_resolver = False elif self._resolvers: resolver = self._resolvers[0] is_default_resolver = True else: return False # nothing to resolve with try: start_time = time.time() new_connections, new_start_times = [], {} if resolver == CustomResolver.INFERENCE: # provide connections going to a relay or one of our tor ports connections = [] controller = tor_controller() consensus_tracker = get_consensus_tracker() relay_ports = set(controller.get_ports(stem.control.Listener.OR, [])) relay_ports.update(controller.get_ports(stem.control.Listener.DIR, [])) relay_ports.update(controller.get_ports(stem.control.Listener.CONTROL, [])) for conn in proc.connections(user = controller.get_user(None)): if conn.remote_port in consensus_tracker.get_relay_fingerprints(conn.remote_address): connections.append(conn) # outbound to another relay elif conn.local_port in relay_ports: connections.append(conn) else: connections = connection.get_connections(resolver, process_pid = process_pid, process_name = process_name) for conn in connections: conn_start_time, is_legacy = self._start_times.get(conn, (start_time, self._is_first_run)) new_start_times[conn] = (conn_start_time, is_legacy) new_connections.append(Connection(conn_start_time, is_legacy, *conn)) self._connections = new_connections self._start_times = new_start_times self._is_first_run = False runtime = time.time() - start_time if is_default_resolver: self._failure_count = 0 # Reduce our rate if connection resolution is taking a long time. This is # most often an issue for extremely busy relays. min_rate = 100 * runtime if self.get_rate() < min_rate: self._rate_too_low_count += 1 if self._rate_too_low_count >= 3: min_rate += 1 # little extra padding so we don't frequently update this self.set_rate(min_rate) self._rate_too_low_count = 0 stem.util.log.debug('connection lookup time increasing to %0.1f seconds per call' % min_rate) else: self._rate_too_low_count = 0 return True except IOError as exc: stem.util.log.info(str(exc)) # Fail over to another resolver if we've repeatedly been unable to use # this one. if is_default_resolver: self._failure_count += 1 if self._failure_count >= 3: self._resolvers.pop(0) self._failure_count = 0 if self._resolvers: stem.util.log.notice('Unable to query connections with %s, trying %s' % (resolver, self._resolvers[0])) else: stem.util.log.notice(UNABLE_TO_USE_ANY_RESOLVER_MSG) return False def get_custom_resolver(self): """ Provides the custom resolver the user has selected. This is **None** if we're picking resolvers dynamically. :returns: :data:`~stem.util.connection.Resolver` we're overwritten to use """ return self._custom_resolver def set_custom_resolver(self, resolver): """ Sets the resolver used for connection resolution. If **None** then this is automatically determined based on what is available. :param stem.util.connection.Resolver resolver: resolver to use """ self._custom_resolver = resolver def get_value(self): """ Provides a listing of tor's latest connections. :returns: **list** of :class:`~nyx.tracker.Connection` we last retrieved, an empty list if our tracker's been stopped """ if self._halt: return [] else: return list(self._connections) class ResourceTracker(Daemon): """ Periodically retrieves the resource usage of tor. """ def __init__(self, rate): super(ResourceTracker, self).__init__(rate) self._resources = None self._use_proc = proc.is_available() # determines if we use proc or ps for lookups self._failure_count = 0 # number of times in a row we've failed to get results def get_value(self): """ Provides tor's latest resource usage. :returns: latest :data:`~nyx.tracker.Resources` we've polled """ result = self._resources return result if result else Resources(0.0, 0.0, 0.0, 0, 0.0, 0.0) def _task(self, process_pid, process_name): try: resolver = _resources_via_proc if self._use_proc else _resources_via_ps total_cpu_time, uptime, memory_in_bytes, memory_in_percent = resolver(process_pid) now = time.time() if self._resources: cpu_sample = (total_cpu_time - self._resources.cpu_total) / (now - self._resources.timestamp) else: cpu_sample = 0.0 # we need a prior datapoint to give a sampling self._resources = Resources( cpu_sample = cpu_sample, cpu_average = total_cpu_time / uptime, cpu_total = total_cpu_time, memory_bytes = memory_in_bytes, memory_percent = memory_in_percent, timestamp = now, ) self._failure_count = 0 return True except IOError as exc: self._failure_count += 1 if self._use_proc: if self._failure_count >= 3: # We've failed three times resolving via proc. Warn, and fall back # to ps resolutions. self._use_proc = False self._failure_count = 0 stem.util.log.info('Failed three attempts to get process resource usage from proc, falling back to ps (%s)' % exc) else: stem.util.log.debug('Unable to query process resource usage from proc (%s)' % exc) else: if self._failure_count >= 3: # Give up on further attempts. stem.util.log.info('Failed three attempts to get process resource usage from ps, giving up on getting resource usage information (%s)' % exc) self.stop() else: stem.util.log.debug('Unable to query process resource usage from ps (%s)' % exc) return False class PortUsageTracker(Daemon): """ Periodically retrieves the processes using a set of ports. """ def __init__(self, rate): super(PortUsageTracker, self).__init__(rate) self._last_requested_local_ports = [] self._last_requested_remote_ports = [] self._processes_for_ports = {} self._failure_count = 0 # number of times in a row we've failed to get results def fetch(self, port): """ Provides the process running on the given port. This retrieves the results from our cache, so it only works if we've already issued a query() request for it and gotten results. :param int port: port number to look up :returns: **Process** using the given port :raises: * :class:`nyx.tracker.UnresolvedResult` if the application is still being determined * :class:`nyx.tracker.UnknownApplication` if the we tried to resolve the application but it couldn't be determined """ try: result = self._processes_for_ports[port] if result is None: raise UnknownApplication() else: return result except KeyError: raise UnresolvedResult() def query(self, local_ports, remote_ports): """ Registers a given set of ports for further lookups, and returns the last set of 'port => process' mappings we retrieved. Note that this means that we will not return the requested ports unless they're requested again after a successful lookup has been performed. :param list local_ports: local port numbers to look up :param list remote_ports: remote port numbers to look up :returns: **dict** mapping port numbers to the **Process** using it """ self._last_requested_local_ports = local_ports self._last_requested_remote_ports = remote_ports return self._processes_for_ports def _task(self, process_pid, process_name): local_ports = self._last_requested_local_ports remote_ports = self._last_requested_remote_ports if not local_ports and not remote_ports: return True result = {} # Use cached results from our last lookup if available. for port, process in self._processes_for_ports.items(): if port in local_ports: result[port] = process local_ports.remove(port) elif port in remote_ports: result[port] = process remote_ports.remove(port) try: if local_ports or remote_ports: result.update(_process_for_ports(local_ports, remote_ports)) self._processes_for_ports = result self._failure_count = 0 return True except IOError as exc: self._failure_count += 1 if self._failure_count >= 3: stem.util.log.info('Failed three attempts to determine the process using active ports (%s)' % exc) self.stop() else: stem.util.log.debug('Unable to query the processes using ports usage lsof (%s)' % exc) return False class ConsensusTracker(object): """ Provides performant lookups of consensus information. """ def __init__(self): self._my_router_status_entry = None self._my_router_status_entry_time = 0 # Stem's get_network_statuses() is slow, and overkill for what we need # here. Just parsing the raw GETINFO response to cut startup time down. # # Only fetching this if our cache is at least an hour old (and hence a new # consensus available). cache_age = time.time() - nyx.cache().relays_updated_at() controller = tor_controller() if cache_age < 3600: stem.util.log.info('Cache is only %s old, no need to refresh it.' % str_tools.time_label(cache_age, is_long = True)) else: stem.util.log.info('Cache is %s old, refreshing relay information.' % str_tools.time_label(cache_age, is_long = True)) ns_response = controller.get_info('ns/all', None) if ns_response: self._update(ns_response) controller.add_event_listener(lambda event: self._update(event.consensus_content), stem.control.EventType.NEWCONSENSUS) def _update(self, consensus_content): start_time = time.time() our_fingerprint = tor_controller().get_info('fingerprint', None) with nyx.cache().write() as writer: for line in consensus_content.splitlines(): if line.startswith('r '): r_comp = line.split(' ') address = r_comp[6] or_port = int(r_comp[7]) fingerprint = stem.descriptor.router_status_entry._base64_to_hex(r_comp[2]) nickname = r_comp[1] if fingerprint == our_fingerprint: self._my_router_status_entry = None self._my_router_status_entry_time = 0 writer.record_relay(fingerprint, address, or_port, nickname) stem.util.log.info('Updated consensus cache, took %0.2fs.' % (time.time() - start_time)) def my_router_status_entry(self): """ Provides the router status entry of ourselves. Descriptors are published hourly, and results are cached for five minutes. :returns: :class:`~stem.descriptor.router_status_entry.RouterStatusEntryV3` for ourselves, **None** if it cannot be retrieved """ if self._my_router_status_entry is None or (time.time() - self._my_router_status_entry_time) > 300: self._my_router_status_entry = tor_controller().get_network_status(default = None) self._my_router_status_entry_time = time.time() return self._my_router_status_entry def get_relay_nickname(self, fingerprint): """ Provides the nickname associated with the given relay. :param str fingerprint: relay to look up :returns: **str** with the nickname ("Unnamed" if unset), and **None** if no such relay exists """ controller = tor_controller() if not fingerprint: return None elif fingerprint == controller.get_info('fingerprint', None): return controller.get_conf('Nickname', 'Unnamed') else: return nyx.cache().relay_nickname(fingerprint) def get_relay_fingerprints(self, address): """ Provides the relays running at a given location. :param str address: address to be checked :returns: **dict** of ORPorts to their fingerprint """ controller = tor_controller() if controller.get_info('address', None) == address: fingerprint = controller.get_info('fingerprint', None) ports = controller.get_ports(stem.control.Listener.OR, None) if fingerprint and ports: return dict([(port, fingerprint) for port in ports]) return nyx.cache().relays_for_address(address) def get_relay_address(self, fingerprint, default): """ Provides the (address, port) tuple where a relay is running. :param str fingerprint: fingerprint to be checked :returns: **tuple** with a **str** address and **int** port """ controller = tor_controller() if fingerprint == controller.get_info('fingerprint', None): my_address = controller.get_info('address', None) my_or_ports = controller.get_ports(stem.control.Listener.OR, []) if my_address and len(my_or_ports) == 1: return (my_address, my_or_ports[0]) return nyx.cache().relay_address(fingerprint, default) nyx-2.0.4/nyx/__init__.py0000664000175000017500000005006213200142326016000 0ustar atagaratagar00000000000000# Copyright 2009-2017, Damian Johnson and The Tor Project # See LICENSE for licensing information """ Tor curses monitoring application. :: nyx_interface - nyx interface singleton tor_controller - tor connection singleton cache - provides our application cache show_message - shows a message to the user input_prompt - prompts the user for text input init_controller - initializes our connection to tor expand_path - expands path with respect to our chroot chroot - provides the chroot path we reside within join - joins a series of strings up to a set length Cache - application cache |- write - provides a content where we can write to the cache | |- relay_nickname - provides the nickname of a relay +- relay_address - provides the address and orport of a relay CacheWriter - context in which we can write to the cache +- record_relay - caches information about a relay Interface - overall nyx interface |- get_page - page we're showing |- set_page - sets the page we're showing |- page_count - pages within our interface | |- header_panel - provides the header panel |- page_panels - provides panels on a page | |- is_paused - checks if the interface is paused |- set_paused - sets paused state | |- redraw - renders our content |- quit - quits our application +- halt - stops daemon panels """ import contextlib import distutils.spawn import os import platform import sys import threading import time import stem import stem.connection import stem.control import stem.util.conf import stem.util.connection import stem.util.log import stem.util.system import stem.util.tor_tools SQLITE_UNAVAILABLE = """\ Python's sqlite3 module is unavailable. Unfortunately some platforms don't bundle this with the interpreter. Please let us know at... https://trac.torproject.org/projects/tor/wiki/doc/nyx/bugs """ SQLITE_UNAVAILABLE_GENTOO = """\ Python's sqlite3 module is unavailable. For Gentoo please run emerge dev-lang/python with USE=sqlite. """ SQLITE_UNAVAILABLE_FREEBSD = """\ Python's sqlite3 module is unavailable. Please run... sudo pkg install py%i%i-sqlite3 """ % (sys.version_info[:2]) try: import sqlite3 except ImportError: if stem.util.system.is_gentoo(): print(SQLITE_UNAVAILABLE_GENTOO) elif stem.util.system.is_bsd(): print(SQLITE_UNAVAILABLE_FREEBSD) else: print(SQLITE_UNAVAILABLE) sys.exit(1) __version__ = '2.0.4' __release_date__ = 'November 5, 2017' __author__ = 'Damian Johnson' __contact__ = 'atagar@torproject.org' __url__ = 'https://nyx.torproject.org/' __license__ = 'GPLv3' __all__ = [ 'arguments', 'cache', 'controller', 'curses', 'log', 'menu', 'panel', 'popups', 'starter', 'tracker', ] def conf_handler(key, value): if key == 'redraw_rate': return max(1, value) CONFIG = stem.util.conf.config_dict('nyx', { 'confirm_quit': True, 'redraw_rate': 5, 'show_graph': True, 'show_log': True, 'show_connections': True, 'show_config': True, 'show_torrc': True, 'show_interpreter': True, 'start_time': 0, }, conf_handler) NYX_INTERFACE = None TOR_CONTROLLER = None CACHE = None CHROOT = None BASE_DIR = os.path.sep.join(__file__.split(os.path.sep)[:-1]) # our address infrequently changes so we can cache it for a while stem.control.CACHE_ADDRESS_FOR = 30 # disable trace level messages about cache hits stem.control.LOG_CACHE_FETCHES = False # don't parse NEWCONSENSUS events since we just care about the content stem.response.events.PARSE_NEWCONSENSUS_EVENTS = False # Duration for threads to pause when there's no work left to do. This is a # compromise - lower means faster shutdown when quit but higher means lower # cpu usage when running. PAUSE_TIME = 0.4 SCHEMA_VERSION = 2 # version of our scheme, bump this if you change the following SCHEMA = ( 'CREATE TABLE schema(version INTEGER)', 'INSERT INTO schema(version) VALUES (%i)' % SCHEMA_VERSION, 'CREATE TABLE metadata(relays_updated_at REAL)', 'INSERT INTO metadata(relays_updated_at) VALUES (0.0)', 'CREATE TABLE relays(fingerprint TEXT PRIMARY KEY, address TEXT, or_port INTEGER, nickname TEXT)', 'CREATE INDEX addresses ON relays(address)', ) try: uses_settings = stem.util.conf.uses_settings('nyx', os.path.join(BASE_DIR, 'settings'), lazy_load = False) except IOError as exc: print("Unable to load nyx's internal configurations: %s" % exc) sys.exit(1) def main(): try: nyx.starter.main() except ImportError as exc: if exc.message == 'No module named stem': if distutils.spawn.find_executable('pip') is not None: advice = ", try running 'sudo pip install stem'" elif distutils.spawn.find_executable('apt-get') is not None: advice = ", try running 'sudo apt-get install python-stem'" else: advice = ', you can find it at https://stem.torproject.org/download.html' print('nyx requires stem' + advice) else: print('Unable to start nyx: %s' % exc) sys.exit(1) def draw_loop(): interface = nyx_interface() next_key = None # use this as the next user input # Redrawing before starting daemons is important so our interface is rendered # right away and the 'top' positions are set for our panels. interface.redraw() for panel in interface: if isinstance(panel, nyx.panel.DaemonPanel): panel.start() stem.util.log.info('nyx started (initialization took %0.1f seconds)' % (time.time() - CONFIG['start_time'])) while not interface._quit: if next_key: key, next_key = next_key, None else: key = nyx.curses.key_input(CONFIG['redraw_rate']) if key.match('right'): interface.set_page((interface.get_page() + 1) % interface.page_count()) elif key.match('left'): interface.set_page((interface.get_page() - 1) % interface.page_count()) elif key.match('p'): interface.set_paused(not interface.is_paused()) elif key.match('m'): nyx.menu.show_menu() elif key.match('q'): if CONFIG['confirm_quit']: confirmation_key = show_message('Are you sure (q again to confirm)?', nyx.curses.BOLD, max_wait = 30) if not confirmation_key.match('q'): continue break elif key.match('x'): confirmation_key = show_message("This will reset Tor's internal state. Are you sure (x again to confirm)?", nyx.curses.BOLD, max_wait = 30) if confirmation_key.match('x'): try: tor_controller().signal(stem.Signal.RELOAD) except stem.ControllerError as exc: stem.util.log.error('Error detected when reloading tor: %s' % exc.strerror) elif key.match('h'): next_key = nyx.popups.show_help() elif not key.is_null(): for panel in interface.page_panels(): for keybinding in panel.key_handlers(): keybinding.handle(key) interface.redraw(force = not key.is_null()) def nyx_interface(): """ Singleton controller for our interface. :returns: :class:`~nyx.Interface` controller """ if NYX_INTERFACE is None: Interface() # constructor sets NYX_INTERFACE return NYX_INTERFACE def tor_controller(): """ Singleton for getting our tor controller connection. :returns: :class:`~stem.control.Controller` nyx is using """ return TOR_CONTROLLER def cache(): """ Provides the sqlite cache for application data. :returns: :class:`~nyx.cache.Cache` for our applicaion """ global CACHE if CACHE is None: CACHE = Cache() return CACHE def show_message(message = None, *attr, **kwargs): """ Shows a message in our header. :param str message: message to be shown """ return nyx_interface().header_panel().show_message(message, *attr, **kwargs) def input_prompt(msg, initial_value = ''): """ Prompts the user for input. :param str message: prompt for user input :param str initial_value: initial value of the prompt :returns: **str** with the user input, this is **None** if the prompt is canceled """ header_panel = nyx_interface().header_panel() header_panel.show_message(msg) user_input = nyx.curses.str_input(len(msg), header_panel.get_height() - 1, initial_value) header_panel.show_message() return user_input def init_controller(*args, **kwargs): """ Sets the Controller used by nyx. This is a passthrough for Stem's :func:`~stem.connection.connect` function. :returns: :class:`~stem.control.Controller` nyx is using """ global TOR_CONTROLLER TOR_CONTROLLER = stem.connection.connect(*args, **kwargs) return TOR_CONTROLLER @uses_settings def data_directory(filename, config): path = config.get('data_directory', '~/.nyx') if path == 'disabled': return None data_dir = os.path.expanduser(path) if not os.path.exists(data_dir): try: os.mkdir(data_dir) except OSError as exc: stem.util.log.log_once('nyx.data_directory_unavailable', stem.util.log.NOTICE, 'Unable to create a data directory at %s (%s). This is fine, but caching is disabled meaning performance will be diminished a bit.' % (data_dir, exc)) return None return os.path.join(data_dir, filename) def expand_path(path): """ Expands relative paths and include our chroot if one was set. :param str path: path to be expanded :returns: **str** with the expanded path """ if path is None: return None try: tor_cwd = stem.util.system.cwd(tor_controller().get_pid(None)) if not os.path.isabs(path) else None return chroot() + stem.util.system.expand_path(path, tor_cwd) except IOError as exc: stem.util.log.info('Unable to expand a relative path (%s): %s' % (path, exc)) return path @uses_settings def chroot(config): global CHROOT if CHROOT is None: # If the user provided us with a chroot then validate and normalize the # path. chroot = config.get('tor_chroot', '').strip().rstrip(os.path.sep) if chroot and not os.path.exists(chroot): stem.util.log.notice("The chroot path set in your config (%s) doesn't exist." % chroot) chroot = '' if not chroot and platform.system() == 'FreeBSD': controller = tor_controller() pid = controller.get_pid(None) if controller else stem.util.system.pid_by_name('tor') jail_chroot = stem.util.system.bsd_jail_path(pid) if pid else None if jail_chroot and os.path.exists(jail_chroot): stem.util.log.info('Adjusting paths to account for Tor running in a FreeBSD jail at: %s' % jail_chroot) chroot = jail_chroot CHROOT = chroot return CHROOT def join(entries, joiner = ' ', size = None): """ Joins a series of strings similar to str.join(), but only up to a given size. This returns an empty string if none of the entries will fit. For example... >>> join(['This', 'is', 'a', 'looooong', 'message'], size = 18) 'This is a looooong' >>> join(['This', 'is', 'a', 'looooong', 'message'], size = 17) 'This is a' >>> join(['This', 'is', 'a', 'looooong', 'message'], size = 2) '' :param list entries: strings to be joined :param str joiner: strings to join the entries with :param int size: maximum length the result can be, there's no length limitation if **None** :returns: **str** of the joined entries up to the given length """ if size is None: return joiner.join(entries) result = '' for entry in entries: new_result = joiner.join((result, entry)) if result else entry if len(new_result) > size: break else: result = new_result return result class Cache(object): """ Cache for frequently needed information. This persists to disk if we can, and otherwise is an in-memory cache. """ def __init__(self): self._conn_lock = threading.RLock() cache_path = nyx.data_directory('cache.sqlite') if cache_path: try: self._conn = sqlite3.connect(cache_path, check_same_thread = False) schema = self._conn.execute('SELECT version FROM schema').fetchone()[0] except: schema = None if schema == SCHEMA_VERSION: stem.util.log.info('Cache loaded from %s' % cache_path) else: if schema is None: stem.util.log.info('Cache at %s is missing a schema, clearing it.' % cache_path) else: stem.util.log.info('Cache at %s has schema version %s but the current version is %s, clearing it.' % (cache_path, schema, SCHEMA_VERSION)) self._conn.close() os.remove(cache_path) self._conn = sqlite3.connect(cache_path, check_same_thread = False) for cmd in SCHEMA: self._conn.execute(cmd) else: stem.util.log.info('Unable to cache to disk. Using an in-memory cache instead.') self._conn = sqlite3.connect(':memory:', check_same_thread = False) for cmd in SCHEMA: self._conn.execute(cmd) @contextlib.contextmanager def write(self): """ Provides a context in which we can modify the cache. :returns: :class:`~nyx.CacheWriter` that can modify the cache """ with self._conn: yield CacheWriter(self) def relays_for_address(self, address): """ Provides the relays running at a given location. :param str address: address to be checked :returns: **dict** of ORPorts to their fingerprint """ result = {} for entry in self._query('SELECT or_port, fingerprint FROM relays WHERE address=?', address).fetchall(): result[entry[0]] = entry[1] return result def relay_nickname(self, fingerprint, default = None): """ Provides the nickname associated with the given relay. :param str fingerprint: relay to look up :param str default: response if no such relay exists :returns: **str** with the nickname ("Unnamed" if unset) """ result = self._query('SELECT nickname FROM relays WHERE fingerprint=?', fingerprint).fetchone() return result[0] if result else default def relay_address(self, fingerprint, default = None): """ Provides the (address, port) tuple where a relay is running. :param str fingerprint: fingerprint to be checked :param str default: response if no such relay exists :returns: **tuple** with a **str** address and **int** port """ result = self._query('SELECT address, or_port FROM relays WHERE fingerprint=?', fingerprint).fetchone() return result if result else default def relays_updated_at(self): """ Provides the unix timestamp when relay information was last updated. :returns: **float** with the unix timestamp when relay information was last updated, zero if it has never been set """ return self._query('SELECT relays_updated_at FROM metadata').fetchone()[0] def _query(self, query, *param): """ Performs a query on our cache. """ with self._conn_lock: return self._conn.execute(query, param) class CacheWriter(object): def __init__(self, cache): self._cache = cache def record_relay(self, fingerprint, address, or_port, nickname): """ Records relay metadata. :param str fingerprint: relay fingerprint :param str address: ipv4 or ipv6 address :param int or_port: ORPort of the relay :param str nickname: relay nickname :raises: **ValueError** if provided data is malformed """ if not stem.util.tor_tools.is_valid_fingerprint(fingerprint): raise ValueError("'%s' isn't a valid fingerprint" % fingerprint) elif not stem.util.tor_tools.is_valid_nickname(nickname): raise ValueError("'%s' isn't a valid nickname" % nickname) elif not stem.util.connection.is_valid_ipv4_address(address) and not stem.util.connection.is_valid_ipv6_address(address): raise ValueError("'%s' isn't a valid address" % address) elif not stem.util.connection.is_valid_port(or_port): raise ValueError("'%s' isn't a valid port" % or_port) self._cache._query('INSERT OR REPLACE INTO relays(fingerprint, address, or_port, nickname) VALUES (?,?,?,?)', fingerprint, address, or_port, nickname) self._cache._query('UPDATE metadata SET relays_updated_at=?', time.time()) class Interface(object): """ Overall state of the nyx interface. """ def __init__(self): global NYX_INTERFACE self._page = 0 self._page_panels = [] self._header_panel = None self._paused = False self._quit = False NYX_INTERFACE = self self._header_panel = nyx.panel.header.HeaderPanel() first_page_panels = [] if CONFIG['show_graph']: first_page_panels.append(nyx.panel.graph.GraphPanel()) if CONFIG['show_log']: first_page_panels.append(nyx.panel.log.LogPanel()) if first_page_panels: self._page_panels.append(first_page_panels) if CONFIG['show_connections']: self._page_panels.append([nyx.panel.connection.ConnectionPanel()]) if CONFIG['show_config']: self._page_panels.append([nyx.panel.config.ConfigPanel()]) if CONFIG['show_torrc']: self._page_panels.append([nyx.panel.torrc.TorrcPanel()]) if CONFIG['show_interpreter']: self._page_panels.append([nyx.panel.interpreter.InterpreterPanel()]) visible_panels = self.page_panels() for panel in self: panel.set_visible(panel in visible_panels) def get_page(self): """ Provides the page we're showing. :return: **int** of the page we're showing """ return self._page def set_page(self, page_number): """ Sets the selected page. :param int page_number: page to be shown :raises: **ValueError** if the page_number is invalid """ if page_number < 0 or page_number >= self.page_count(): raise ValueError('Invalid page number: %i' % page_number) if page_number != self._page: self._page = page_number self.header_panel().redraw() visible_panels = self.page_panels() for panel in self: panel.set_visible(panel in visible_panels) def page_count(self): """ Provides the number of pages the interface has. :returns: **int** number of pages in the interface """ return len(self._page_panels) def header_panel(self): """ Provides our interface's header. :returns: :class:`~nyx.panel.header.HeaderPanel` of our interface """ return self._header_panel def page_panels(self, page_number = None): """ Provides panels belonging to a page, ordered top to bottom. :param int page_number: page to provide the panels of, current page if **None** :returns: **list** of panels on that page """ if not self._page_panels: return [self._header_panel] # all panels disabled page_number = self._page if page_number is None else page_number return [self._header_panel] + self._page_panels[page_number] def is_paused(self): """ Checks if the interface is configured to be paused. :returns: **True** if the interface is paused, **False** otherwise """ return self._paused def set_paused(self, is_pause): """ Pauses or unpauses the interface. :param bool is_pause: suspends the interface if **True**, resumes it otherwise """ if is_pause != self._paused: self._paused = is_pause for panel in self: panel.set_paused(is_pause) for panel in self.page_panels(): panel.redraw() def redraw(self, force = False): """ Renders our displayed content. """ occupied = 0 for panel in self.page_panels(): panel.redraw(force = force, top = occupied) occupied += panel.get_height() def quit(self): """ Quits our application. """ self._quit = True def halt(self): """ Stops curses panels in our interface. :returns: **threading.Thread** terminating daemons """ def halt_panels(): daemons = [panel for panel in self if isinstance(panel, nyx.panel.DaemonPanel)] for panel in daemons: panel.stop() for panel in daemons: panel.join() halt_thread = threading.Thread(target = halt_panels) halt_thread.start() return halt_thread def __iter__(self): yield self._header_panel for page in self._page_panels: for panel in page: yield panel import nyx.curses import nyx.menu import nyx.panel import nyx.panel.config import nyx.panel.connection import nyx.panel.graph import nyx.panel.header import nyx.panel.interpreter import nyx.panel.log import nyx.panel.torrc import nyx.popups import nyx.starter nyx-2.0.4/nyx/menu.py0000664000175000017500000002112313177120634015214 0ustar atagaratagar00000000000000# Copyright 2011-2017, Damian Johnson and The Tor Project # See LICENSE for licensing information """ Menu for controlling nyx. """ import functools import nyx.curses import nyx.popups import stem from nyx import nyx_interface, tor_controller, show_message from nyx.curses import RED, WHITE, NORMAL, BOLD, UNDERLINE from stem.util import str_tools class MenuItem(object): """ Drop-down menu item. :var str prefix: text before our label :var str label: text we display :var str suffix: text after our label :var MenuItem next: menu item after this one :var MenuItem prev: menu item before this one :var Submenu parent: submenu we reside within :var Submenu submenu: top-level submenu we reside within """ def __init__(self, label, callback, *args): self.label = label self.suffix = '' self._parent = None if args: self._callback = functools.partial(callback, *args) else: self._callback = callback @property def prefix(self): return '' @property def next(self): return self._sibling(1) @property def prev(self): return self._sibling(-1) @property def parent(self): return self._parent @property def submenu(self): return self._parent.submenu if (self._parent and self._parent._parent) else self def select(self): """ Performs the callback for the menu item. """ if self._callback: self._callback() def _sibling(self, offset): """ Provides sibling with a given offset from us. """ if not self._parent: return None my_siblings = self._parent.children try: my_index = my_siblings.index(self) return my_siblings[(my_index + offset) % len(my_siblings)] except ValueError: # submenus and children should have bidirectional references raise ValueError("BUG: The '%s' submenu doesn't contain '%s' (children: '%s')" % (self._parent, self.label, "', '".join(my_siblings))) class Submenu(MenuItem): """ Menu item that lists other menu options. :var list children: menu items this contains """ def __init__(self, label, children = None): MenuItem.__init__(self, label, None) self.suffix = ' >' self.children = [] if children: for child in children: if isinstance(child, list): self.add(*child) else: self.add(child) def add(self, *menu_items): """ Adds the given menu item to our listing. :param list menu_items: menu item to be added :raises: **ValueError** if the item is already in a submenu """ for menu_item in menu_items: if menu_item.parent: raise ValueError("Menu option '%s' already has a parent" % menu_item) menu_item._parent = self self.children.append(menu_item) class RadioMenuItem(MenuItem): """ Menu item with an associated group which determines the selection. """ def __init__(self, label, group, arg): MenuItem.__init__(self, label, lambda: group.action(arg)) self._group = group self._arg = arg @property def prefix(self): return '[X] ' if self._arg == self._group.selected_arg else '[ ] ' class RadioGroup(object): """ Radio button groups that RadioMenuItems can belong to. """ def __init__(self, action, selected_arg): self.action = lambda arg: action(arg) if arg != self.selected_arg else None self.selected_arg = selected_arg class MenuCursor(object): """ Tracks selection and movement through the menu. :var MenuItem selection: presently selected menu item :var bool is_done: **True** if a selection indicates we should close the menu, **False** otherwise """ def __init__(self, initial_selection): self.selection = initial_selection self.is_done = False def handle_key(self, key): if key.is_selection(): if isinstance(self.selection, Submenu): if self.selection.children: self.selection = self.selection.children[0] else: self.selection.select() self.is_done = True elif key.match('up'): self.selection = self.selection.prev elif key.match('down'): self.selection = self.selection.next elif key.match('left'): if self.selection.parent == self.selection.submenu: # shift to the previous main submenu prev_submenu = self.selection.submenu.prev self.selection = prev_submenu.children[0] else: # go up a submenu level self.selection = self.selection.parent elif key.match('right'): if isinstance(self.selection, Submenu): # open submenu (same as making a selection) if self.selection.children: self.selection = self.selection.children[0] else: # shift to the next main submenu next_submenu = self.selection.submenu.next self.selection = next_submenu.children[0] elif key.match('esc', 'm'): self.is_done = True def show_menu(): menu = _make_menu() cursor = MenuCursor(menu.children[0].children[0]) with nyx.curses.CURSES_LOCK: show_message('Press m or esc to close the menu.', BOLD) while not cursor.is_done: selection_x = _draw_top_menubar(menu, cursor.selection) _draw_submenu(cursor.selection, cursor.selection.submenu, 1, selection_x) cursor.handle_key(nyx.curses.key_input()) nyx_interface().redraw(True) show_message() def _make_menu(): """ Constructs the base menu and all of its contents. """ interface = nyx_interface() if not interface.is_paused(): pause_item = MenuItem('Pause', interface.set_paused, True) else: pause_item = MenuItem('Unpause', interface.set_paused, False) root_menu = Submenu('') root_menu.add(Submenu('Actions', [ MenuItem('Close Menu', None), MenuItem('New Identity', interface.header_panel().send_newnym), MenuItem('Reset Tor', tor_controller().signal, stem.Signal.RELOAD), pause_item, MenuItem('Exit', interface.quit), ])) root_menu.add(_view_menu()) for panel in interface.page_panels(): submenu = panel.submenu() if submenu: root_menu.add(submenu) root_menu.add(Submenu('Help', [ MenuItem('Hotkeys', nyx.popups.show_help), MenuItem('About', nyx.popups.show_about), ])) return root_menu def _view_menu(): """ Submenu consisting of... [X] [ ] [ ] etc... Color (Submenu) """ interface = nyx_interface() view_menu = Submenu('View') page_group = RadioGroup(interface.set_page, interface.get_page()) for i in range(interface.page_count()): page_panels = interface.page_panels(page_number = i)[1:] label = ' / '.join([type(panel).__name__.replace('Panel', '') for panel in page_panels]) view_menu.add(RadioMenuItem(label, page_group, i)) if nyx.curses.is_color_supported(): color_group = RadioGroup(nyx.curses.set_color_override, nyx.curses.get_color_override()) view_menu.add(Submenu('Color', [ RadioMenuItem('All', color_group, None), [RadioMenuItem(str_tools._to_camel_case(opt), color_group, opt) for opt in nyx.curses.Color], ])) return view_menu def _draw_top_menubar(menu, selection): def _render(subwindow): x = 0 for submenu in menu.children: x = subwindow.addstr(x, 0, ' %s ' % submenu.label, BOLD, UNDERLINE if submenu == selection.submenu else NORMAL) subwindow.vline(x, 0, 1) x += 1 nyx.curses.draw(_render, height = 1, background = RED) selection_index = menu.children.index(selection.submenu) return 3 * selection_index + sum([len(entry.label) for entry in menu.children[:selection_index]]) def _draw_submenu(selection, submenu, top, left): # find the item from within this submenu that's selected submenu_selection = selection while submenu_selection.parent != submenu: submenu_selection = submenu_selection.parent prefix_size = max([len(entry.prefix) for entry in submenu.children]) middle_size = max([len(entry.label) for entry in submenu.children]) suffix_size = max([len(entry.suffix) for entry in submenu.children]) menu_width = prefix_size + middle_size + suffix_size + 2 label_format = ' %%-%is%%-%is%%-%is ' % (prefix_size, middle_size, suffix_size) def _render(subwindow): for y, menu_item in enumerate(submenu.children): label = label_format % (menu_item.prefix, menu_item.label, menu_item.suffix) attr = (WHITE, BOLD) if menu_item == submenu_selection else (NORMAL,) subwindow.addstr(0, y, label, *attr) nyx.curses.draw(_render, top = top, left = left, width = menu_width, height = len(submenu.children), background = RED) if submenu != selection.parent: _draw_submenu(selection, submenu_selection, top + submenu.children.index(submenu_selection), left + menu_width) nyx-2.0.4/nyx/panel/0000775000175000017500000000000013200142726014767 5ustar atagaratagar00000000000000nyx-2.0.4/nyx/panel/config.py0000664000175000017500000002754413173140703016624 0ustar atagaratagar00000000000000# Copyright 2010-2017, Damian Johnson and The Tor Project # See LICENSE for licensing information """ Panel presenting the configuration state for tor or nyx. Options can be edited and the resulting configuration files saved. """ import collections import curses import nyx.curses import nyx.panel import nyx.popups import stem.control import stem.manual import stem.util.connection from nyx import tor_controller, input_prompt, show_message from nyx.curses import WHITE, NORMAL, BOLD, HIGHLIGHT from nyx.menu import MenuItem, Submenu from stem.util import conf, enum, log, str_tools try: # added in python 3.2 from functools import lru_cache except ImportError: from stem.util.lru_cache import lru_cache SortAttr = enum.Enum('NAME', 'VALUE', 'VALUE_TYPE', 'CATEGORY', 'USAGE', 'SUMMARY', 'DESCRIPTION', 'MAN_PAGE_ENTRY', 'IS_SET') ManualEntry = collections.namedtuple('ManualEntry', ['category', 'usage', 'summary', 'description', 'position']) DETAILS_HEIGHT = 8 NAME_WIDTH = 25 VALUE_WIDTH = 15 def conf_handler(key, value): if key == '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': {}, 'config_order': [SortAttr.MAN_PAGE_ENTRY, SortAttr.NAME, SortAttr.IS_SET], 'show_private_options': False, 'show_virtual_options': False, }, conf_handler) @lru_cache() def manual(option): result = stem.manual.query('SELECT category, usage, summary, description, position FROM torrc WHERE key=?', option.upper()).fetchone() if result: return ManualEntry(*result) else: log.info("No manual information found for '%s'" % option) return None class ConfigEntry(object): """ Configuration option presented in the panel. :var str name: name of the configuration option :var str value_type: type of value """ def __init__(self, name, value_type): self.name = name self.value_type = value_type 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 '' 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.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.usage elif attr == SortAttr.SUMMARY: return self.summary elif attr == SortAttr.DESCRIPTION: return self.description elif attr == SortAttr.MAN_PAGE_ENTRY: return self.position elif attr == SortAttr.IS_SET: return not self.is_set() @property def category(self): return getattr(manual(self.name), 'category') @property def usage(self): return getattr(manual(self.name), 'usage') @property def summary(self): return getattr(manual(self.name), 'summary') @property def description(self): return getattr(manual(self.name), 'description') @property def position(self): return getattr(manual(self.name), 'position', 99999) class ConfigPanel(nyx.panel.Panel): """ Editor for tor's configuration. """ def __init__(self): nyx.panel.Panel.__init__(self) self._all_content = [] self._important_content = [] self._scroller = nyx.curses.CursorScroller() self._sort_order = CONFIG['config_order'] self._show_all = False # show all options, or just the important ones try: for line in tor_controller().get_info('config/names').splitlines(): # Lines of the form "