def _edit_selected_value(): selected = self._scroller.selection(self._get_config_options()) initial_value = selected.value() if selected.is_set() else '' new_value = nyx.controller.input_prompt( '%s Value (esc to cancel): ' % selected.name, initial_value) if new_value != initial_value: try: if selected.value_type == 'Boolean': # if the value's a boolean then allow for 'true' and 'false' inputs if new_value.lower() == 'true': new_value = '1' elif new_value.lower() == 'false': new_value = '0' elif selected.value_type == 'LineList': new_value = new_value.split( ',') # set_conf accepts list inputs tor_controller().set_conf(selected.name, new_value) self.redraw() except Exception as exc: nyx.controller.show_message('%s (press any key)' % exc, HIGHLIGHT, max_wait=30)
def _draw_address_column(subwindow, x, y, line, attr): src = tor_controller().get_info('address', line.connection.local_address) src += ':%s' % line.connection.local_port if line.line_type == LineType.CONNECTION else '' if line.line_type == LineType.CIRCUIT_HEADER and line.circuit.status != 'BUILT': dst = 'Building...' else: dst = '<scrubbed>' if line.entry.is_private() else line.connection.remote_address dst += ':%s' % line.connection.remote_port if line.entry.get_type() == Category.EXIT: purpose = connection.port_usage(line.connection.remote_port) if purpose: dst += ' (%s)' % str_tools.crop(purpose, 26 - len(dst) - 3) elif not tor_controller().is_geoip_unavailable() and not line.entry.is_private(): dst += ' (%s)' % (line.locale if line.locale else '??') if line.entry.get_type() in (Category.INBOUND, Category.SOCKS, Category.CONTROL): dst, src = src, dst if line.line_type == LineType.CIRCUIT: subwindow.addstr(x, y, dst, *attr) else: subwindow.addstr(x, y, '%-21s --> %-26s' % (src, dst), *attr)
def _draw_address_column(subwindow, x, y, line, attr): src = tor_controller().get_info('address', line.connection.local_address) if line.line_type == LineType.CONNECTION: src = '%s:%s' % (src, line.connection.local_port) if line.line_type == LineType.CIRCUIT_HEADER and line.circuit.status != 'BUILT': dst = 'Building...' else: dst = '<scrubbed>' if line.entry.is_private( ) else line.connection.remote_address dst += ':%s' % line.connection.remote_port if line.entry.get_type() == Category.EXIT: purpose = connection.port_usage(line.connection.remote_port) if purpose: dst += ' (%s)' % str_tools.crop(purpose, 26 - len(dst) - 3) elif not tor_controller().is_geoip_unavailable( ) and not line.entry.is_private(): dst += ' (%s)' % (line.locale if line.locale else '??') src = '%-21s' % src dst = '%-21s' % dst if tor_controller().is_geoip_unavailable( ) else '%-26s' % dst if line.entry.get_type() in (Category.INBOUND, Category.SOCKS, Category.CONTROL): dst, src = src, dst if line.line_type == LineType.CIRCUIT: return subwindow.addstr(x, y, dst, *attr) else: return subwindow.addstr(x, y, '%s --> %s' % (src, dst), *attr)
def __init__(self): nyx.panel.DaemonPanel.__init__(self, UPDATE_RATE) self._vals = Sampling.create() self._last_width = nyx.curses.screen_size().width self._reported_inactive = False self._message = None self._message_attr = [] tor_controller().add_status_listener(self.reset_listener)
def __init__(self): nyx.panel.DaemonPanel.__init__(self, UPDATE_RATE) self._vals = Sampling.create() self._last_width = nyx.curses.screen_size().width self._reported_inactive = False self._message = None self._message_attr = [] tor_controller().add_status_listener(self.reset_listener)
def __init__(self): nyx.panel.Panel.__init__(self) self._displayed_stat = None if CONFIG['features.graph.type'] == 'none' else CONFIG['features.graph.type'] self._update_interval = CONFIG['features.graph.interval'] self._bounds = CONFIG['features.graph.bound'] self._graph_height = CONFIG['features.graph.height'] self._accounting_stats = None self._accounting_stats_paused = None self._stats = { GraphStat.BANDWIDTH: BandwidthStats(), GraphStat.SYSTEM_RESOURCES: ResourceStats(), } self._stats_paused = None if CONFIG['features.panels.show.connection']: self._stats[GraphStat.CONNECTIONS] = ConnectionStats() elif self._displayed_stat == GraphStat.CONNECTIONS: log.warn("The connection graph is unavailble when you set 'features.panels.show.connection false'.") self._displayed_stat = GraphStat.BANDWIDTH controller = tor_controller() controller.add_event_listener(self._update_accounting, EventType.BW) controller.add_event_listener(self._update_stats, EventType.BW) controller.add_status_listener(lambda *args: self.redraw())
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 = [CustomResolver.INFERENCE ] if stem.util.proc.is_available() else [] if tor_controller().get_conf('DisableDebuggerAttachment', None) == '0': self._resolvers = self._resolvers + connection.system_resolvers() elif not self._resolvers: stem.util.log.notice( "Tor connection information is unavailable. This is fine, but if you would like to have it please see https://nyx.torproject.org/#no_connections" ) stem.util.log.info('Operating System: %s, Connection Resolvers: %s' % (os.uname()[0], ', '.join(self._resolvers)))
def __init__(self, clone = None): GraphCategory.__init__(self, clone) if not clone: # fill in past bandwidth information controller = tor_controller() bw_entries, is_successful = controller.get_info('bw-event-cache', None), True if bw_entries: for entry in bw_entries.split(): entry_comp = entry.split(',') if len(entry_comp) != 2 or not entry_comp[0].isdigit() or not entry_comp[1].isdigit(): log.warn(msg('panel.graphing.bw_event_cache_malformed', response = bw_entries)) is_successful = False break self.primary.update(int(entry_comp[0])) self.secondary.update(int(entry_comp[1])) if is_successful: log.info(msg('panel.graphing.prepopulation_successful', duration = str_tools.time_label(len(bw_entries.split()), is_long = True))) read_total = controller.get_info('traffic/read', None) write_total = controller.get_info('traffic/written', None) start_time = system.start_time(controller.get_pid(None)) if read_total and write_total and start_time: self.primary.total = int(read_total) self.secondary.total = int(write_total) self.start_time = start_time
def listen_for_events(listener, events): """ Configures tor to notify a function of these event types. If tor is configured to notify this listener then the old listener is replaced. :param function listener: listener to be notified :param list events: event types to attempt to set :returns: **list** of event types we're successfully now listening to """ events = set(events) # drops duplicates nyx_events = events.intersection(set(NYX_RUNLEVELS)) tor_events = events.difference(nyx_events) controller = nyx.tor_controller() controller.remove_event_listener(listener) for event_type in list(tor_events): try: controller.add_event_listener(listener, event_type) except stem.ProtocolError: warn('panel.log.unsupported_event', event = event_type) tor_events.remove(event_type) return sorted(tor_events.union(nyx_events))
def listen_for_events(listener, events): """ Configures tor to notify a function of these event types. If tor is configured to notify this listener then the old listener is replaced. :param function listener: listener to be notified :param list events: event types to attempt to set :returns: **list** of event types we're successfully now listening to """ import nyx.arguments events = set(events) # drops duplicates # accounts for runlevel naming difference tor_events = events.intersection(set(nyx.arguments.TOR_EVENT_TYPES.values())) nyx_events = events.intersection(set(NYX_RUNLEVELS)) # adds events unrecognized by nyx if we're listening to the 'UNKNOWN' type if 'UNKNOWN' in events: tor_events.update(set(nyx.arguments.missing_event_types())) controller = nyx.tor_controller() controller.remove_event_listener(listener) for event_type in list(tor_events): try: controller.add_event_listener(listener, event_type) except stem.ProtocolError: tor_events.remove(event_type) return sorted(tor_events.union(nyx_events))
def bandwidth_event(self, event): inbound_count, outbound_count = 0, 0 controller = tor_controller() or_ports = controller.get_ports(Listener.OR, []) dir_ports = controller.get_ports(Listener.DIR, []) control_ports = controller.get_ports(Listener.CONTROL, []) for entry in nyx.tracker.get_connection_tracker().get_value(): if entry.local_port in or_ports or entry.local_port in dir_ports: inbound_count += 1 elif entry.local_port in control_ports: pass # control connection else: outbound_count += 1 self.primary.update(inbound_count) self.secondary.update(outbound_count) self._primary_header_stats = [ str(self.primary.latest_value), ', avg: %i' % self.primary.average() ] self._secondary_header_stats = [ str(self.secondary.latest_value), ', avg: %i' % self.secondary.average() ]
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 _draw_details(self, selected, width, is_scrollbar_visible): """ Shows detailed information about the selected connection. """ attr = (CONFIG['attr.connection.category_color'].get(selected.entry.get_type(), WHITE), BOLD) if selected.line_type == LineType.CIRCUIT_HEADER and selected.circuit.status != 'BUILT': self.addstr(1, 2, 'Building Circuit...', *attr) else: address = '<scrubbed>' if selected.entry.is_private() else selected.connection.remote_address self.addstr(1, 2, 'address: %s:%s' % (address, selected.connection.remote_port), *attr) self.addstr(2, 2, 'locale: %s' % ('??' if selected.entry.is_private() else (selected.locale if selected.locale else '??')), *attr) matches = nyx.tracker.get_consensus_tracker().get_relay_fingerprints(selected.connection.remote_address) if not matches: self.addstr(3, 2, 'No consensus data found', *attr) elif len(matches) == 1 or selected.connection.remote_port in matches: controller = tor_controller() fingerprint = matches.values()[0] if len(matches) == 1 else matches[selected.connection.remote_port] router_status_entry = controller.get_network_status(fingerprint, None) self.addstr(2, 15, 'fingerprint: %s' % fingerprint, *attr) if router_status_entry: dir_port_label = 'dirport: %s' % router_status_entry.dir_port if router_status_entry.dir_port else '' self.addstr(3, 2, 'nickname: %-25s orport: %-10s %s' % (router_status_entry.nickname, router_status_entry.or_port, dir_port_label), *attr) self.addstr(4, 2, 'published: %s' % router_status_entry.published.strftime("%H:%M %m/%d/%Y"), *attr) self.addstr(5, 2, 'flags: %s' % ', '.join(router_status_entry.flags), *attr) server_descriptor = controller.get_server_descriptor(fingerprint, None) if server_descriptor: policy_label = server_descriptor.exit_policy.summary() if server_descriptor.exit_policy else 'unknown' self.addstr(6, 2, 'exit policy: %s' % policy_label, *attr) self.addstr(4, 38, 'os: %-14s version: %s' % (server_descriptor.operating_system, server_descriptor.tor_version), *attr) if server_descriptor.contact: self.addstr(7, 2, 'contact: %s' % server_descriptor.contact, *attr) else: self.addstr(3, 2, 'Multiple matches, possible fingerprints are:', *attr) for i, port in enumerate(sorted(matches.keys())): is_last_line, remaining_relays = i == 3, len(matches) - i if not is_last_line or remaining_relays == 1: self.addstr(4 + i, 2, '%i. or port: %-5s fingerprint: %s' % (i + 1, port, matches[port]), *attr) else: self.addstr(4 + i, 2, '... %i more' % remaining_relays, *attr) if is_last_line: break # draw the border, with a 'T' pipe if connecting with the scrollbar self.draw_box(0, 0, width, DETAILS_HEIGHT + 2) if is_scrollbar_visible: self.addch(DETAILS_HEIGHT + 1, 1, curses.ACS_TTEE)
def draw(self, width, height): controller = tor_controller() entries = self._entries lines = list(itertools.chain.from_iterable([entry.get_lines() for entry in entries])) is_showing_details = self._show_details and lines details_offset = DETAILS_HEIGHT + 1 if is_showing_details else 0 selected, scroll = self._scroller.selection(lines, height - details_offset - 1) if self.is_paused(): current_time = self.get_pause_time() elif not controller.is_alive(): current_time = controller.connection_time() else: current_time = time.time() is_scrollbar_visible = len(lines) > height - details_offset - 1 scroll_offset = 2 if is_scrollbar_visible else 0 self._draw_title(entries, self._show_details) if is_showing_details: self._draw_details(selected, width, is_scrollbar_visible) if is_scrollbar_visible: self.add_scroll_bar(scroll, scroll + height - details_offset - 1, len(lines), 1 + details_offset) for line_number in range(scroll, len(lines)): y = line_number + details_offset + 1 - scroll self._draw_line(scroll_offset, y, lines[line_number], lines[line_number] == selected, width - scroll_offset, current_time) if y >= height: break
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 get_type(self): controller = tor_controller() if self._connection.local_port in controller.get_ports(Listener.OR, []): return Category.INBOUND elif self._connection.local_port in controller.get_ports(Listener.DIR, []): return Category.INBOUND elif self._connection.local_port in controller.get_ports(Listener.SOCKS, []): return Category.SOCKS elif self._connection.local_port in controller.get_ports(Listener.CONTROL, []): return Category.CONTROL if LAST_RETRIEVED_HS_CONF: for hs_config in LAST_RETRIEVED_HS_CONF.values(): if self._connection.remote_port == hs_config['HiddenServicePort']: return Category.HIDDEN fingerprint = nyx.tracker.get_consensus_tracker().get_relay_fingerprints(self._connection.remote_address).get(self._connection.remote_port) if fingerprint and LAST_RETRIEVED_CIRCUITS: for circ in LAST_RETRIEVED_CIRCUITS: if circ.path and len(circ.path) == 1 and circ.path[0][0] == fingerprint and circ.status == 'BUILT': return Category.DIRECTORY # one-hop circuit to retrieve directory information else: # not a known relay, might be an exit connection exit_policy = controller.get_exit_policy(None) if exit_policy and exit_policy.can_exit_to(self._connection.remote_address, self._connection.remote_port): return Category.EXIT return Category.OUTBOUND
def _get_type(self): controller = tor_controller() if self._connection.local_port in controller.get_ports(Listener.OR, []): return Category.INBOUND elif self._connection.local_port in controller.get_ports(Listener.DIR, []): return Category.INBOUND elif self._connection.local_port in controller.get_ports(Listener.SOCKS, []): return Category.SOCKS elif self._connection.local_port in controller.get_ports(Listener.CONTROL, []): return Category.CONTROL if LAST_RETRIEVED_HS_CONF: for hs_config in LAST_RETRIEVED_HS_CONF.values(): if self._connection.remote_port == hs_config['HiddenServicePort']: return Category.HIDDEN fingerprint = nyx.tracker.get_consensus_tracker().get_relay_fingerprints(self._connection.remote_address).get(self._connection.remote_port) exit_policy = controller.get_exit_policy(None) if fingerprint and LAST_RETRIEVED_CIRCUITS: for circ in LAST_RETRIEVED_CIRCUITS: if circ.path and len(circ.path) == 1 and circ.path[0][0] == fingerprint and circ.status == 'BUILT': return Category.DIRECTORY # one-hop circuit to retrieve directory information elif not fingerprint and exit_policy and exit_policy.can_exit_to(self._connection.remote_address, self._connection.remote_port): return Category.EXIT return Category.OUTBOUND
def listen_for_events(listener, events): """ Configures tor to notify a function of these event types. If tor is configured to notify this listener then the old listener is replaced. :param function listener: listener to be notified :param list events: event types to attempt to set :returns: **list** of event types we're successfully now listening to """ events = set(events) # drops duplicates nyx_events = events.intersection(set(NYX_RUNLEVELS)) tor_events = events.difference(nyx_events) controller = nyx.tor_controller() controller.remove_event_listener(listener) for event_type in list(tor_events): try: controller.add_event_listener(listener, event_type) except stem.ProtocolError: stem.util.log.warn("%s isn't an event tor supports" % event_type) tor_events.remove(event_type) return sorted(tor_events.union(nyx_events))
def _bandwidth_title_stats(): controller = tor_controller() stats = [] bw_rate = controller.get_effective_rate(None) bw_burst = controller.get_effective_rate(None, burst = True) if bw_rate and bw_burst: bw_rate_label = _size_label(bw_rate) bw_burst_label = _size_label(bw_burst) # if both are using rounded values then strip off the '.0' decimal if '.0' in bw_rate_label and '.0' in bw_burst_label: bw_rate_label = bw_rate_label.replace('.0', '') bw_burst_label = bw_burst_label.replace('.0', '') stats.append('limit: %s/s' % bw_rate_label) stats.append('burst: %s/s' % bw_burst_label) my_server_descriptor = controller.get_server_descriptor(default = None) observed_bw = getattr(my_server_descriptor, 'observed_bandwidth', None) if observed_bw: stats.append('observed: %s/s' % _size_label(observed_bw)) return stats
def __init__(self, clone = None): GraphCategory.__init__(self, clone) self._title_last_updated = None if not clone: # fill in past bandwidth information controller = tor_controller() bw_entries, is_successful = controller.get_info('bw-event-cache', None), True if bw_entries: for entry in bw_entries.split(): entry_comp = entry.split(',') if len(entry_comp) != 2 or not entry_comp[0].isdigit() or not entry_comp[1].isdigit(): log.warn("Tor's 'GETINFO bw-event-cache' provided malformed output: %s" % bw_entries) is_successful = False break self.primary.update(int(entry_comp[0])) self.secondary.update(int(entry_comp[1])) if is_successful: log.info('Bandwidth graph has information for the last %s' % str_tools.time_label(len(bw_entries.split()), is_long = True)) read_total = controller.get_info('traffic/read', None) write_total = controller.get_info('traffic/written', None) start_time = system.start_time(controller.get_pid(None)) if read_total and write_total and start_time: self.primary.total = int(read_total) self.secondary.total = int(write_total) self.start_time = start_time
def __init__(self): nyx.panel.Panel.__init__(self) self._displayed_stat = None if CONFIG['graph_stat'] == 'none' else CONFIG['graph_stat'] self._update_interval = CONFIG['graph_interval'] self._bounds_type = CONFIG['graph_bound'] self._graph_height = CONFIG['graph_height'] self._accounting_stats = None self._accounting_stats_paused = None self._stats = { GraphStat.BANDWIDTH: BandwidthStats(), GraphStat.SYSTEM_RESOURCES: ResourceStats(), } self._stats_lock = threading.RLock() self._stats_paused = None if CONFIG['show_connections']: self._stats[GraphStat.CONNECTIONS] = ConnectionStats() elif self._displayed_stat == GraphStat.CONNECTIONS: log.warn("The connection graph is unavailble when you set 'show_connections false'.") self._displayed_stat = GraphStat.BANDWIDTH controller = tor_controller() controller.add_event_listener(self._update_accounting, EventType.BW) controller.add_event_listener(self._update_stats, EventType.BW) controller.add_status_listener(lambda *args: self.redraw())
def make_actions_menu(): """ Submenu consisting of... Close Menu New Identity Reset Tor Pause / Unpause Exit """ control = nyx.controller.get_controller() controller = tor_controller() header_panel = control.header_panel() actions_menu = Submenu('Actions') actions_menu.add(MenuItem('Close Menu', None)) actions_menu.add(MenuItem('New Identity', header_panel.send_newnym)) actions_menu.add(MenuItem('Reset Tor', functools.partial(controller.signal, stem.Signal.RELOAD))) if control.is_paused(): label, arg = 'Unpause', False else: label, arg = 'Pause', True actions_menu.add(MenuItem(label, functools.partial(control.set_paused, arg))) actions_menu.add(MenuItem('Exit', control.quit)) return actions_menu
def _show_write_dialog(self): """ Confirmation dialog for saving tor's configuration. """ controller = tor_controller() torrc = controller.get_info('config-text', None) if nyx.popups.confirm_save_torrc(torrc): try: controller.save_conf() show_message('Saved configuration to %s' % controller.get_info('config-file', '<unknown>'), HIGHLIGHT, max_wait=2) except stem.OperationFailed as exc: show_message('Unable to save configuration ([%s] %s)' % (exc.code, exc.message), HIGHLIGHT, max_wait=2) except stem.ControllerError as exc: show_message('Unable to save configuration (%s)' % exc, HIGHLIGHT, max_wait=2) self.redraw()
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 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 __init__(self): nyx.panel.Panel.__init__(self) self._contents = [] self._scroller = nyx.curses.CursorScroller() self._sort_order = CONFIG['features.config.order'] self._show_all = False # show all options, or just the important ones cached_manual_path = os.path.join(DATA_DIR, 'manual') if os.path.exists(cached_manual_path): manual = stem.manual.Manual.from_cache(cached_manual_path) else: try: manual = stem.manual.Manual.from_man() try: manual.save(cached_manual_path) except IOError as exc: log.debug( "Unable to cache manual information to '%s'. This is fine, but means starting Nyx takes a little longer than usual: " % (cached_manual_path, exc)) except IOError as exc: log.debug( "Unable to use 'man tor' to get information about config options (%s), using bundled information instead" % exc) manual = stem.manual.Manual.from_cache() try: for line in tor_controller().get_info('config/names').splitlines(): # Lines of the form "<option> <type>[ <documentation>]". Documentation # was apparently only in old tor versions like 0.2.1.25. if ' ' not in line: continue line_comp = line.split() name, value_type = line_comp[0], line_comp[1] # skips private and virtual entries if not configured to show them if name.startswith('__') and not CONFIG[ 'features.config.state.showPrivateOptions']: continue elif value_type == 'Virtual' and not CONFIG[ 'features.config.state.showVirtualOptions']: continue self._contents.append(ConfigEntry(name, value_type, manual)) self._contents = sorted( self._contents, key=lambda entry: [entry.sort_value(field) for field in self._sort_order]) except stem.ControllerError as exc: log.warn( 'Unable to determine the configuration options tor supports: %s' % exc)
def __init__(self): nyx.panel.DaemonPanel.__init__(self, UPDATE_RATE) logged_events = CONFIG['startup.events'].split(',') tor_events = tor_controller().get_info('events/names', '').split() invalid_events = filter(lambda event: not event.startswith('NYX_') and event not in tor_events, logged_events) if invalid_events: logged_events = ['NOTICE', 'WARN', 'ERR', 'NYX_NOTICE', 'NYX_WARNING', 'NYX_ERROR'] log.warn("Your --log argument had the following events tor doesn't recognize: %s" % ', '.join(invalid_events)) self._event_log = nyx.log.LogGroup(CONFIG['cache.log_panel.size'], group_by_day = True) self._event_log_paused = None self._event_types = nyx.log.listen_for_events(self._register_tor_event, logged_events) self._log_file = nyx.log.LogFileOutput(CONFIG['features.logFile']) self._filter = nyx.log.LogFilters(initial_filters = CONFIG['features.log.regex']) self._show_duplicates = CONFIG['features.log.showDuplicateEntries'] self._scroller = nyx.curses.Scroller() self._has_new_event = False self._last_day = nyx.log.day_count(time.time()) # fetches past tor events from log file, if available if CONFIG['features.log.prepopulate']: log_location = nyx.log.log_file_path(tor_controller()) if log_location: try: for entry in reversed(list(nyx.log.read_tor_log(log_location, CONFIG['features.log.prepopulateReadLimit']))): if entry.type in self._event_types: self._event_log.add(entry) except IOError as exc: log.info('Unable to read log located at %s: %s' % (log_location, exc)) except ValueError as exc: log.info(str(exc)) self._last_content_height = len(self._event_log) # height of the rendered content when last drawn # merge NYX_LOGGER into us, and listen for its future events for event in NYX_LOGGER: self._register_nyx_event(event) NYX_LOGGER.emit = self._register_nyx_event
def _update(self): """ Fetches the newest resolved connections. """ global LAST_RETRIEVED_CIRCUITS, LAST_RETRIEVED_HS_CONF controller = tor_controller() LAST_RETRIEVED_CIRCUITS = controller.get_circuits([]) LAST_RETRIEVED_HS_CONF = controller.get_hidden_service_conf({}) conn_resolver = nyx.tracker.get_connection_tracker() current_resolution_count = conn_resolver.run_counter() if not conn_resolver.is_alive(): return # if we're not fetching connections then this is a no-op elif current_resolution_count == self._last_resource_fetch: return # no new connections to process new_entries = [Entry.from_connection(conn) for conn in conn_resolver.get_value()] for circ in LAST_RETRIEVED_CIRCUITS: # Skips established single-hop circuits (these are for directory # fetches, not client circuits) if not (circ.status == 'BUILT' and len(circ.path) == 1): new_entries.append(Entry.from_circuit(circ)) # update stats for client and exit connections for entry in new_entries: line = entry.get_lines()[0] if entry.is_private() and line.connection not in self._counted_connections: if entry.get_type() == Category.INBOUND and line.locale: self._client_locale_usage[line.locale] = self._client_locale_usage.get(line.locale, 0) + 1 elif entry.get_type() == Category.EXIT: self._exit_port_usage[line.connection.remote_port] = self._exit_port_usage.get(line.connection.remote_port, 0) + 1 self._counted_connections.add(line.connection) self._entries = sorted(new_entries, key = lambda entry: [entry.sort_value(attr) for attr in self._sort_order]) self._last_resource_fetch = current_resolution_count if CONFIG['features.connection.resolveApps']: local_ports, remote_ports = [], [] for entry in new_entries: line = entry.get_lines()[0] if entry.get_type() in (Category.SOCKS, Category.CONTROL): local_ports.append(line.connection.remote_port) elif entry.get_type() == Category.HIDDEN: remote_ports.append(line.connection.local_port) nyx.tracker.get_port_usage_tracker().query(local_ports, remote_ports)
def _draw_details(subwindow, selected): """ Shows detailed information about the selected connection. """ attr = (CONFIG['attr.connection.category_color'].get(selected.entry.get_type(), WHITE), BOLD) if selected.line_type == LineType.CIRCUIT_HEADER and selected.circuit.status != 'BUILT': subwindow.addstr(2, 1, 'Building Circuit...', *attr) else: address = '<scrubbed>' if selected.entry.is_private() else selected.connection.remote_address subwindow.addstr(2, 1, 'address: %s:%s' % (address, selected.connection.remote_port), *attr) subwindow.addstr(2, 2, 'locale: %s' % (selected.locale if selected.locale and not selected.entry.is_private() else '??'), *attr) matches = nyx.tracker.get_consensus_tracker().get_relay_fingerprints(selected.connection.remote_address) if not matches: subwindow.addstr(2, 3, 'No consensus data found', *attr) elif len(matches) == 1 or selected.connection.remote_port in matches: controller = tor_controller() fingerprint = list(matches.values())[0] if len(matches) == 1 else matches[selected.connection.remote_port] router_status_entry = controller.get_network_status(fingerprint, None) subwindow.addstr(15, 2, 'fingerprint: %s' % fingerprint, *attr) if router_status_entry: dir_port_label = 'dirport: %s' % router_status_entry.dir_port if router_status_entry.dir_port else '' subwindow.addstr(2, 3, 'nickname: %-25s orport: %-10s %s' % (router_status_entry.nickname, router_status_entry.or_port, dir_port_label), *attr) subwindow.addstr(2, 4, 'published: %s' % router_status_entry.published.strftime("%H:%M %m/%d/%Y"), *attr) subwindow.addstr(2, 5, 'flags: %s' % ', '.join(router_status_entry.flags), *attr) server_descriptor = controller.get_server_descriptor(fingerprint, None) if server_descriptor: policy_label = server_descriptor.exit_policy.summary() if server_descriptor.exit_policy else 'unknown' subwindow.addstr(2, 6, 'exit policy: %s' % policy_label, *attr) subwindow.addstr(38, 4, 'os: %-14s version: %s' % (server_descriptor.operating_system, server_descriptor.tor_version), *attr) if server_descriptor.contact: subwindow.addstr(2, 7, 'contact: %s' % server_descriptor.contact, *attr) else: subwindow.addstr(2, 3, 'Multiple matches, possible fingerprints are:', *attr) for i, port in enumerate(sorted(matches.keys())): is_last_line, remaining_relays = i == 3, len(matches) - i if not is_last_line or remaining_relays == 1: subwindow.addstr(2, 4 + i, '%i. or port: %-5s fingerprint: %s' % (i + 1, port, matches[port]), *attr) else: subwindow.addstr(2, 4 + i, '... %i more' % remaining_relays, *attr) if is_last_line: break subwindow.box(0, 0, subwindow.width, DETAILS_HEIGHT + 2)
def line(fingerprint, line_type): address, port, nickname = '0.0.0.0', 0, None consensus_tracker = nyx.tracker.get_consensus_tracker() if fingerprint is not None: address, port = consensus_tracker.get_relay_address(fingerprint, ('192.168.0.1', 0)) nickname = consensus_tracker.get_relay_nickname(fingerprint) locale = tor_controller().get_info('ip-to-country/%s' % address, None) connection = nyx.tracker.Connection(datetime_to_unix(self._circuit.created), False, '127.0.0.1', 0, address, port, 'tcp', False) return Line(self, line_type, connection, self._circuit, fingerprint, nickname, locale)
def get_lines(self): fingerprint, nickname, locale = None, None, None if self.get_type() in (Category.OUTBOUND, Category.CIRCUIT, Category.DIRECTORY, Category.EXIT): fingerprint = nyx.tracker.get_consensus_tracker().get_relay_fingerprints(self._connection.remote_address).get(self._connection.remote_port) if fingerprint: nickname = nyx.tracker.get_consensus_tracker().get_relay_nickname(fingerprint) locale = tor_controller().get_info('ip-to-country/%s' % self._connection.remote_address, None) return [Line(self, LineType.CONNECTION, self._connection, None, fingerprint, nickname, locale)]
def _get_lines(self): fingerprint, nickname = None, None if self.get_type() in (Category.OUTBOUND, Category.CIRCUIT, Category.DIRECTORY, Category.EXIT): fingerprint = nyx.tracker.get_consensus_tracker().get_relay_fingerprints(self._connection.remote_address).get(self._connection.remote_port) if fingerprint: nickname = nyx.tracker.get_consensus_tracker().get_relay_nickname(fingerprint) locale = tor_controller().get_info('ip-to-country/%s' % self._connection.remote_address, None) return [Line(self, LineType.CONNECTION, self._connection, None, fingerprint, nickname, locale)]
def line(fingerprint, line_type): address, port, nickname, locale = '0.0.0.0', 0, None, None consensus_tracker = nyx.tracker.get_consensus_tracker() if fingerprint is not None: address, port = consensus_tracker.get_relay_address(fingerprint, ('192.168.0.1', 0)) nickname = consensus_tracker.get_relay_nickname(fingerprint) locale = tor_controller().get_info('ip-to-country/%s' % address, None) connection = nyx.tracker.Connection(datetime_to_unix(self._circuit.created), False, '127.0.0.1', 0, address, port, 'tcp', False) return Line(self, line_type, connection, self._circuit, fingerprint, nickname, locale)
def _edit_selected_value(): selected = self._scroller.selection(self._get_config_options()) initial_value = selected.value() if selected.is_set() else '' new_value = nyx.controller.input_prompt('%s Value (esc to cancel): ' % selected.name, initial_value) if new_value != initial_value: try: if selected.value_type == 'Boolean': # if the value's a boolean then allow for 'true' and 'false' inputs if new_value.lower() == 'true': new_value = '1' elif new_value.lower() == 'false': new_value = '0' elif selected.value_type == 'LineList': new_value = new_value.split(',') # set_conf accepts list inputs tor_controller().set_conf(selected.name, new_value) self.redraw() except Exception as exc: nyx.controller.show_message('%s (press any key)' % exc, HIGHLIGHT, max_wait = 30)
def bandwidth_event(self, event): self.primary.update(event.read) self.secondary.update(event.written) self._primary_header_stats = [ '%-14s' % ('%s/sec' % _size_label(self.primary.latest_value)), '- avg: %s/sec' % _size_label(self.primary.total / (time.time() - self.start_time)), ', total: %s' % _size_label(self.primary.total), ] self._secondary_header_stats = [ '%-14s' % ('%s/sec' % _size_label(self.secondary.latest_value)), '- avg: %s/sec' % _size_label(self.secondary.total / (time.time() - self.start_time)), ', total: %s' % _size_label(self.secondary.total), ] controller = tor_controller() stats = [] bw_rate = controller.get_effective_rate(None) bw_burst = controller.get_effective_rate(None, burst=True) if bw_rate and bw_burst: bw_rate_label = _size_label(bw_rate) bw_burst_label = _size_label(bw_burst) # if both are using rounded values then strip off the '.0' decimal if '.0' in bw_rate_label and '.0' in bw_burst_label: bw_rate_label = bw_rate_label.split('.', 1)[0] bw_burst_label = bw_burst_label.split('.', 1)[0] stats.append('limit: %s/s' % bw_rate_label) stats.append('burst: %s/s' % bw_burst_label) my_router_status_entry = controller.get_network_status(default=None) measured_bw = getattr(my_router_status_entry, 'bandwidth', None) if measured_bw: stats.append('measured: %s/s' % _size_label(measured_bw)) else: my_server_descriptor = controller.get_server_descriptor( default=None) observed_bw = getattr(my_server_descriptor, 'observed_bandwidth', None) if observed_bw: stats.append('observed: %s/s' % _size_label(observed_bw)) self._title_stats = stats
def _update_accounting(self, event): if not CONFIG['show_accounting']: self._accounting_stats = None elif not self._accounting_stats or time.time() - self._accounting_stats.retrieved >= ACCOUNTING_RATE: old_accounting_stats = self._accounting_stats self._accounting_stats = tor_controller().get_accounting_stats(None) if not nyx_interface().is_paused(): # if we either added or removed accounting info then redraw the whole # screen to account for resizing if bool(old_accounting_stats) != bool(self._accounting_stats): nyx_interface().redraw()
def _update_accounting(self, event): if not CONFIG['features.graph.bw.accounting.show']: self._accounting_stats = None elif not self._accounting_stats or time.time() - self._accounting_stats.retrieved >= ACCOUNTING_RATE: nyx_controller = nyx.controller.get_controller() old_accounting_stats = self._accounting_stats self._accounting_stats = tor_controller().get_accounting_stats(None) if not nyx_controller.is_paused(): # if we either added or removed accounting info then redraw the whole # screen to account for resizing if bool(old_accounting_stats) != bool(self._accounting_stats): nyx.controller.get_controller().redraw()
def __init__(self): nyx.panel.Panel.__init__(self) self._is_input_mode = False self._x_offset = 0 self._scroller = nyx.curses.Scroller() self._lines = [] self._user_inputs = [] # previous user inputs controller = tor_controller() self._autocompleter = stem.interpreter.autocomplete.Autocompleter( controller) self._interpreter = stem.interpreter.commands.ControlInterpreter( controller)
def _draw_accounting_stats(subwindow, y, accounting): if tor_controller().is_alive(): hibernate_color = CONFIG['attr.hibernate_color'].get(accounting.status, RED) x = subwindow.addstr(0, y, 'Accounting (', BOLD) x = subwindow.addstr(x, y, accounting.status, BOLD, hibernate_color) x = subwindow.addstr(x, y, ')', BOLD) subwindow.addstr(35, y, 'Time to reset: %s' % str_tools.short_time_label(accounting.time_until_reset)) subwindow.addstr(2, y + 1, '%s / %s' % (_size_label(accounting.read_bytes), _size_label(accounting.read_limit)), PRIMARY_COLOR) subwindow.addstr(37, y + 1, '%s / %s' % (_size_label(accounting.written_bytes), _size_label(accounting.write_limit)), SECONDARY_COLOR) else: subwindow.addstr(0, y, 'Accounting:', BOLD) subwindow.addstr(12, y, 'Connection Closed...')
def __init__(self): panel.Panel.__init__(self) self._scroller = nyx.curses.Scroller() self._show_line_numbers = True # shows left aligned line numbers self._show_comments = True # shows comments and extra whitespace self._last_content_height = 0 self._torrc_location = None self._torrc_content = None self._torrc_load_error = None controller = tor_controller() controller.add_status_listener(self._reset_listener) self._reset_listener(controller, State.RESET, None)
def __init__(self): panel.Panel.__init__(self, 'torrc') self._scroller = nyx.curses.Scroller() self._show_line_numbers = True # shows left aligned line numbers self._show_comments = True # shows comments and extra whitespace self._last_content_height = 0 self._torrc_location = None self._torrc_content = None self._torrc_load_error = None controller = tor_controller() controller.add_status_listener(self.reset_listener) self.reset_listener(controller, State.RESET, None)
def _draw_accounting_stats(subwindow, y, accounting): if tor_controller().is_alive(): hibernate_color = CONFIG['attr.hibernate_color'].get(accounting.status, RED) x = subwindow.addstr(0, y, 'Accounting (', BOLD) x = subwindow.addstr(x, y, accounting.status, BOLD, hibernate_color) x = subwindow.addstr(x, y, ')', BOLD) subwindow.addstr(35, y, 'Time to reset: %s' % str_tools.short_time_label(accounting.time_until_reset)) subwindow.addstr(2, y + 1, '%s / %s' % (_size_label(accounting.read_bytes), _size_label(accounting.read_limit)), PRIMARY_COLOR) subwindow.addstr(37, y + 1, '%s / %s' % (_size_label(accounting.written_bytes), _size_label(accounting.write_limit)), SECONDARY_COLOR) else: subwindow.addstr(0, y, 'Accounting:', BOLD) subwindow.addstr(12, y, 'Connection Closed...')
def bandwidth_event(self, event): self.primary.update(event.read) self.secondary.update(event.written) self._primary_header_stats = [ '%-14s' % ('%s/sec' % _size_label(self.primary.latest_value)), '- avg: %s/sec' % _size_label(self.primary.total / (time.time() - self.start_time)), ', total: %s' % _size_label(self.primary.total), ] self._secondary_header_stats = [ '%-14s' % ('%s/sec' % _size_label(self.secondary.latest_value)), '- avg: %s/sec' % _size_label(self.secondary.total / (time.time() - self.start_time)), ', total: %s' % _size_label(self.secondary.total), ] controller = tor_controller() stats = [] bw_rate = controller.get_effective_rate(None) bw_burst = controller.get_effective_rate(None, burst = True) if bw_rate and bw_burst: bw_rate_label = _size_label(bw_rate) bw_burst_label = _size_label(bw_burst) # if both are using rounded values then strip off the '.0' decimal if '.0' in bw_rate_label and '.0' in bw_burst_label: bw_rate_label = bw_rate_label.split('.', 1)[0] bw_burst_label = bw_burst_label.split('.', 1)[0] stats.append('limit: %s/s' % bw_rate_label) stats.append('burst: %s/s' % bw_burst_label) my_router_status_entry = controller.get_network_status(default = None) measured_bw = getattr(my_router_status_entry, 'bandwidth', None) if measured_bw: stats.append('measured: %s/s' % _size_label(measured_bw)) else: my_server_descriptor = controller.get_server_descriptor(default = None) observed_bw = getattr(my_server_descriptor, 'observed_bandwidth', None) if observed_bw: stats.append('observed: %s/s' % _size_label(observed_bw)) self._title_stats = stats
def is_private(self): if not CONFIG['features.connection.showIps']: return True if self.get_type() == Category.INBOUND: controller = tor_controller() if controller.is_user_traffic_allowed().inbound: return len(nyx.tracker.get_consensus_tracker().get_relay_fingerprints(self._connection.remote_address)) == 0 elif self.get_type() == Category.EXIT: # DNS connections exiting us aren't private (since they're hitting our # resolvers). Everything else is. return not (self._connection.remote_port == 53 and self._connection.protocol == 'udp') return False # for everything else this isn't a concern
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 show_write_dialog(self): """ Confirmation dialog for saving tor's configuration. """ controller = tor_controller() torrc = controller.get_info('config-text', None) if nyx.popups.confirm_save_torrc(torrc): try: controller.save_conf() nyx.controller.show_message('Saved configuration to %s' % controller.get_info('config-file', '<unknown>'), HIGHLIGHT, max_wait = 2) except IOError as exc: nyx.controller.show_message('Unable to save configuration (%s)' % exc.strerror, HIGHLIGHT, max_wait = 2) self.redraw()
def __init__(self): nyx.panel.Panel.__init__(self) self._contents = [] self._scroller = nyx.curses.CursorScroller() self._sort_order = CONFIG['features.config.order'] self._show_all = False # show all options, or just the important ones cached_manual_path = os.path.join(DATA_DIR, 'manual') if os.path.exists(cached_manual_path): manual = stem.manual.Manual.from_cache(cached_manual_path) else: try: manual = stem.manual.Manual.from_man() try: manual.save(cached_manual_path) except IOError as exc: log.debug("Unable to cache manual information to '%s'. This is fine, but means starting Nyx takes a little longer than usual: " % (cached_manual_path, exc)) except IOError as exc: log.debug("Unable to use 'man tor' to get information about config options (%s), using bundled information instead" % exc) manual = stem.manual.Manual.from_cache() try: for line in tor_controller().get_info('config/names').splitlines(): # Lines of the form "<option> <type>[ <documentation>]". Documentation # was apparently only in old tor versions like 0.2.1.25. if ' ' not in line: continue line_comp = line.split() name, value_type = line_comp[0], line_comp[1] # skips private and virtual entries if not configured to show them if name.startswith('__') and not CONFIG['features.config.state.showPrivateOptions']: continue elif value_type == 'Virtual' and not CONFIG['features.config.state.showVirtualOptions']: continue self._contents.append(ConfigEntry(name, value_type, manual)) self._contents = sorted(self._contents, key = lambda entry: [entry.sort_value(field) for field in self._sort_order]) except stem.ControllerError as exc: log.warn('Unable to determine the configuration options tor supports: %s' % exc)
def missing_event_types(): """ Provides the event types the current tor connection supports but nyx doesn't. This provides an empty list if no event types are missing or the GETINFO query fails. :returns: **list** of missing event types """ response = tor_controller().get_info('events/names', None) if response is None: return [] # GETINFO query failed tor_event_types = response.split(' ') recognized_types = TOR_EVENT_TYPES.values() return list(filter(lambda x: x not in recognized_types, tor_event_types))
def send_newnym(self): """ Requests a new identity and provides a visual queue. """ controller = tor_controller() if not controller.is_newnym_available(): return controller.signal(stem.Signal.NEWNYM) # If we're wide then the newnym label in this panel will give an # indication that the signal was sent. Otherwise use a msg. if not self.is_wide(): self.show_message('Requesting a new identity', HIGHLIGHT, max_wait = 1)
def _draw(self, subwindow): controller = tor_controller() interface = nyx_interface() entries = self._entries lines = list( itertools.chain.from_iterable( [entry.get_lines() for entry in entries])) is_showing_details = self._show_details and lines details_offset = DETAILS_HEIGHT + 1 if is_showing_details else 0 selected, scroll = self._scroller.selection( lines, subwindow.height - details_offset - 1) if interface.is_paused(): current_time = self._pause_time elif not controller.is_alive(): current_time = controller.connection_time() else: current_time = time.time() is_scrollbar_visible = len( lines) > subwindow.height - details_offset - 1 scroll_offset = 2 if is_scrollbar_visible else 0 _draw_title(subwindow, entries, self._show_details) if is_showing_details: _draw_details(subwindow, selected) # draw a 'T' pipe if connecting with the scrollbar if is_scrollbar_visible: subwindow._addch(1, DETAILS_HEIGHT + 1, curses.ACS_TTEE) if is_scrollbar_visible: subwindow.scrollbar(1 + details_offset, scroll, len(lines)) for line_number in range(scroll, len(lines)): y = line_number + details_offset + 1 - scroll _draw_line(subwindow, scroll_offset, y, lines[line_number], lines[line_number] == selected, subwindow.width - scroll_offset, current_time) if y >= subwindow.height: break
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 self._nickname_cache.get(fingerprint)
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 __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 value(self): """ Provides the value of this configuration option. :returns: **str** representation of the current config value """ values = tor_controller().get_conf(self.name, [], True) if not values: return '<none>' elif self.value_type == 'Boolean' and values[0] in ('0', '1'): return 'False' if values[0] == '0' else 'True' elif self.value_type == 'DataSize' and values[0].isdigit(): return str_tools.size_label(int(values[0])) elif self.value_type == 'TimeInterval' and values[0].isdigit(): return str_tools.time_label(int(values[0]), is_long = True) else: return ', '.join(values)
def __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._pause_condition = threading.Condition() 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 send_newnym(self): """ Requests a new identity and provides a visual queue. """ controller = tor_controller() if not controller.is_newnym_available(): return controller.signal(stem.Signal.NEWNYM) # If we're wide then the newnym label in this panel will give an # indication that the signal was sent. Otherwise use a msg. if not self.is_wide(): self.show_message('Requesting a new identity', HIGHLIGHT, max_wait=1)
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 address == controller.get_info('address', None): 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 dict([(port, fp) for (port, fp) in self._fingerprint_cache.get(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 self._address_cache.get(fingerprint, default)