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 get_bsd_jail_id(pid): """ Gets the jail id for a process. These seem to only exist for FreeBSD (this style for jails does not exist on Linux, OSX, or OpenBSD). :param int pid: process id of the jail id to be queried :returns: **int** for the jail id, zero if this can't be determined """ # Output when called from a FreeBSD jail or when Tor isn't jailed: # JID # 0 # # Otherwise it's something like: # JID # 1 ps_output = call(GET_BSD_JAIL_ID_PS % pid) if ps_output and len(ps_output) == 2 and len(ps_output[1].split()) == 1: jid = ps_output[1].strip() if jid.isdigit(): return int(jid) os_name = platform.system() if os_name == "FreeBSD": log.warn("Unable to get the jail id for process %s." % pid) else: log.debug("get_bsd_jail_id(%s): jail ids do not exist on %s" % (pid, os_name)) return 0
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 __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 __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 load(self, logFailure=False): """ Loads or reloads the torrc contents, raising an IOError if there's a problem. Arguments: logFailure - if the torrc fails to load and we've never provided a warning for this before then logs a warning """ self.valsLock.acquire() # clears contents and caches self.contents, self.configLocation = None, None self.displayableContents = None self.strippedContents = None self.corrections = None try: self.configLocation = getConfigLocation() configFile = open(self.configLocation, "r") self.contents = configFile.readlines() configFile.close() except IOError, exc: if logFailure and not self.isLoadFailWarned: log.warn("Unable to load torrc (%s)" % sysTools.getFileErrorMsg(exc)) self.isLoadFailWarned = True self.valsLock.release() raise exc
def load(self, log_failure = False): """ Loads or reloads the torrc contents, raising an IOError if there's a problem. Arguments: log_failure - if the torrc fails to load and we've never provided a warning for this before then logs a warning """ with self._vals_lock: # clears contents and caches self.contents, self.config_location = None, None self.displayable_contents = None self.stripped_contents = None self.corrections = None try: self.config_location = get_config_location() config_file = open(self.config_location, 'r') self.contents = config_file.readlines() config_file.close() except IOError as exc: if log_failure and not self.is_foad_fail_warned: log.warn('Unable to load torrc (%s)' % exc.strerror) self.is_foad_fail_warned = True raise exc
def load(self, logFailure = False): """ Loads or reloads the torrc contents, raising an IOError if there's a problem. Arguments: logFailure - if the torrc fails to load and we've never provided a warning for this before then logs a warning """ self.valsLock.acquire() # clears contents and caches self.contents, self.configLocation = None, None self.displayableContents = None self.strippedContents = None self.corrections = None try: self.configLocation = getConfigLocation() configFile = open(self.configLocation, "r") self.contents = configFile.readlines() configFile.close() except IOError, exc: if logFailure and not self.isLoadFailWarned: log.warn("Unable to load torrc (%s)" % sysTools.getFileErrorMsg(exc)) self.isLoadFailWarned = True self.valsLock.release() raise exc
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 _warn_if_unable_to_get_pid(controller): """ Provide a warning if we're unable to determine tor's pid. This in turn will limit our ability to query information about the process later. """ try: controller.get_pid() except ValueError: log.warn('setup.unable_to_determine_pid')
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 port_usage(port): """ Provides the common use of a given port. For example, 'HTTP' for port 80 or 'SSH' for 22. .. versionadded:: 1.2.0 :param int port: port number to look up :returns: **str** with a description for the port, **None** if none is known """ global PORT_USES if PORT_USES is None: config = conf.Config() config_path = os.path.join(os.path.dirname(__file__), 'ports.cfg') try: config.load(config_path) port_uses = {} for key, value in config.get('port', {}).items(): if key.isdigit(): port_uses[int(key)] = value elif '-' in key: min_port, max_port = key.split('-', 1) for port_entry in range(int(min_port), int(max_port) + 1): port_uses[port_entry] = value else: raise ValueError("'%s' is an invalid key" % key) PORT_USES = port_uses except Exception as exc: log.warn( "BUG: stem failed to load its internal port descriptions from '%s': %s" % (config_path, exc)) if not PORT_USES: return None if isinstance(port, str) and port.isdigit(): port = int(port) return PORT_USES.get(port)
def port_usage(port): """ Provides the common use of a given port. For example, 'HTTP' for port 80 or 'SSH' for 22. .. versionadded:: 1.2.0 :param int port: port number to look up :returns: **str** with a description for the port, **None** if none is known """ global PORT_USES if PORT_USES is None: config = conf.Config() config_path = os.path.join(os.path.dirname(__file__), 'ports.cfg') try: config.load(config_path) port_uses = {} for key, value in config.get('port', {}).items(): if key.isdigit(): port_uses[int(key)] = value elif '-' in key: min_port, max_port = key.split('-', 1) for port_entry in range(int(min_port), int(max_port) + 1): port_uses[port_entry] = value else: raise ValueError("'%s' is an invalid key" % key) PORT_USES = port_uses except Exception as exc: log.warn("BUG: stem failed to load its internal port descriptions from '%s': %s" % (config_path, exc)) if not PORT_USES: return None if isinstance(port, str) and port.isdigit(): port = int(port) return PORT_USES.get(port)
def _validate_content(self): """ Validates that the descriptor content matches the signature. :raises: ValueError if the signature does not match the content """ key_as_bytes = RelayDescriptor._get_key_bytes(self.signing_key) # ensure the fingerprint is a hash of the signing key if self.fingerprint: # calculate the signing key hash key_der_as_hash = hashlib.sha1(stem.util.str_tools._to_bytes(key_as_bytes)).hexdigest() if key_der_as_hash != self.fingerprint.lower(): log.warn('Signing key hash: %s != fingerprint: %s' % (key_der_as_hash, self.fingerprint.lower())) raise ValueError('Fingerprint does not match hash') self._verify_digest(key_as_bytes)
def _validate_content(self): """ Validates that the descriptor content matches the signature. :raises: ValueError if the signature does not match the content """ key_as_bytes = RelayDescriptor._get_key_bytes(self.signing_key) # ensure the fingerprint is a hash of the signing key if self.fingerprint: # calculate the signing key hash key_der_as_hash = hashlib.sha1(stem.util.str_tools._to_bytes(key_as_bytes)).hexdigest() if key_der_as_hash != self.fingerprint.lower(): log.warn("Signing key hash: %s != fingerprint: %s" % (key_der_as_hash, self.fingerprint.lower())) raise ValueError("Fingerprint does not match hash") self._verify_digest(key_as_bytes)
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 __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 "<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['show_private_options']: continue elif value_type == 'Virtual' and not CONFIG[ 'show_virtual_options']: continue entry = ConfigEntry(name, value_type) self._all_content.append(entry) if stem.manual.is_important(entry.name): self._important_content.append(entry) self._sort_content() except stem.ControllerError as exc: log.warn( 'Unable to determine the configuration options tor supports: %s' % exc)
def _load_user_nyxrc(path, config): """ Loads user's personal nyxrc if it's available. """ if os.path.exists(path): try: config.load(path) # 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): log.notice('setup.chroot_doesnt_exist', path = chroot) config.set('tor.chroot', '') else: config.set('tor.chroot', chroot) # use the normalized path except IOError as exc: log.warn('config.unable_to_read_file', error = exc.strerror) else: log.notice('config.nothing_loaded', path = path)
def conf_handler(key, value): if key == 'graph_height': return max(1, value) elif key == 'max_graph_width': return max(1, value) elif key == 'graph_stat': if value != 'none' and value not in GraphStat: log.warn("'%s' isn't a valid graph type, options are: none, %s" % (CONFIG['graph_stat'], ', '.join(GraphStat))) return CONFIG['graph_stat'] # keep the default elif key == 'graph_interval': if value not in Interval: log.warn("'%s' isn't a valid graphing interval, options are: %s" % (value, ', '.join(Interval))) return CONFIG['graph_interval'] # keep the default elif key == 'graph_bound': if value not in Bounds: log.warn("'%s' isn't a valid graph bounds, options are: %s" % (value, ', '.join(Bounds))) return CONFIG['graph_bound'] # keep the default
def conf_handler(key, value): if key == 'features.graph.height': return max(1, value) elif key == 'features.graph.max_width': return max(1, value) elif key == 'features.graph.type': if value != 'none' and value not in GraphStat: log.warn("'%s' isn't a valid graph type, options are: none, %s" % (CONFIG['features.graph.type'], ', '.join(GraphStat))) return CONFIG['features.graph.type'] # keep the default elif key == 'features.graph.interval': if value not in Interval: log.warn("'%s' isn't a valid graphing interval, options are: %s" % (value, ', '.join(Interval))) return CONFIG['features.graph.interval'] # keep the default elif key == 'features.graph.bound': if value not in Bounds: log.warn("'%s' isn't a valid graph bounds, options are: %s" % (value, ', '.join(Bounds))) return CONFIG['features.graph.bound'] # keep the default
def logValidationIssues(self): """ Performs validation on the loaded contents, and logs warnings for issues that are found. """ corrections = self.getCorrections() if corrections: duplicateOptions, defaultOptions, mismatchLines, missingOptions = [], [], [], [] for lineNum, issue, msg in corrections: if issue == ValidationError.DUPLICATE: duplicateOptions.append("%s (line %i)" % (msg, lineNum + 1)) elif issue == ValidationError.IS_DEFAULT: defaultOptions.append("%s (line %i)" % (msg, lineNum + 1)) elif issue == ValidationError.MISMATCH: mismatchLines.append(lineNum + 1) elif issue == ValidationError.MISSING: missingOptions.append(msg) if duplicateOptions or defaultOptions: msg = "Unneeded torrc entries found. They've been highlighted in blue on the torrc page." if duplicateOptions: if len(duplicateOptions) > 1: msg += "\n- entries ignored due to having duplicates: " else: msg += "\n- entry ignored due to having a duplicate: " duplicateOptions.sort() msg += ", ".join(duplicateOptions) if defaultOptions: if len(defaultOptions) > 1: msg += "\n- entries match their default values: " else: msg += "\n- entry matches its default value: " defaultOptions.sort() msg += ", ".join(defaultOptions) log.notice(msg) if mismatchLines or missingOptions: msg = "The torrc differs from what tor's using. You can issue a sighup to reload the torrc values by pressing x." if mismatchLines: if len(mismatchLines) > 1: msg += "\n- torrc values differ on lines: " else: msg += "\n- torrc value differs on line: " mismatchLines.sort() msg += ", ".join([str(val + 1) for val in mismatchLines]) if missingOptions: if len(missingOptions) > 1: msg += "\n- configuration values are missing from the torrc: " else: msg += "\n- configuration value is missing from the torrc: " missingOptions.sort() msg += ", ".join(missingOptions) log.warn(msg)
def recv_message(control_file): """ Pulls from a control socket until we either have a complete message or encounter a problem. :param file control_file: file derived from the control socket (see the socket's makefile() method for more information) :returns: :class:`~stem.response.ControlMessage` read from the socket :raises: * :class:`stem.ProtocolError` the content from the socket is malformed * :class:`stem.SocketClosed` if the socket closes before we receive a complete message """ parsed_content, raw_content = [], [] logging_prefix = 'Error while receiving a control message (%s): ' while True: try: # From a real socket readline() would always provide bytes, but during # tests we might be given a StringIO in which case it's unicode under # python 3.x. line = stem.util.str_tools._to_bytes(control_file.readline()) except AttributeError: # if the control_file has been closed then we will receive: # AttributeError: 'NoneType' object has no attribute 'recv' prefix = logging_prefix % 'SocketClosed' log.info(prefix + 'socket file has been closed') raise stem.SocketClosed('socket file has been closed') except (socket.error, ValueError) as exc: # When disconnected we get... # # Python 2: # socket.error: [Errno 107] Transport endpoint is not connected # # Python 3: # ValueError: I/O operation on closed file. prefix = logging_prefix % 'SocketClosed' log.info(prefix + 'received exception "%s"' % exc) raise stem.SocketClosed(exc) raw_content.append(line) # Parses the tor control lines. These are of the form... # <status code><divider><content>\r\n if len(line) == 0: # if the socket is disconnected then the readline() method will provide # empty content prefix = logging_prefix % 'SocketClosed' log.info(prefix + 'empty socket content') raise stem.SocketClosed('Received empty socket content.') elif len(line) < 4: prefix = logging_prefix % 'ProtocolError' log.info(prefix + 'line too short, "%s"' % log.escape(line)) raise stem.ProtocolError('Badly formatted reply line: too short') elif not re.match(b'^[a-zA-Z0-9]{3}[-+ ]', line): prefix = logging_prefix % 'ProtocolError' log.info(prefix + 'malformed status code/divider, "%s"' % log.escape(line)) raise stem.ProtocolError( 'Badly formatted reply line: beginning is malformed') elif not line.endswith(b'\r\n'): prefix = logging_prefix % 'ProtocolError' log.info(prefix + 'no CRLF linebreak, "%s"' % log.escape(line)) raise stem.ProtocolError('All lines should end with CRLF') line = line[:-2] # strips off the CRLF status_code, divider, content = line[:3], line[3:4], line[4:] content_lines = [content] if stem.prereq.is_python_3(): status_code = stem.util.str_tools._to_unicode(status_code) divider = stem.util.str_tools._to_unicode(divider) if divider == '-': # mid-reply line, keep pulling for more content parsed_content.append((status_code, divider, content)) elif divider == ' ': # end of the message, return the message parsed_content.append((status_code, divider, content)) raw_content_str = b''.join(raw_content) log_message = stem.util.str_tools._to_unicode( raw_content_str.replace(b'\r\n', b'\n').rstrip()) log_message_lines = log_message.split('\n') if TRUNCATE_LOGS and len(log_message_lines) > TRUNCATE_LOGS: log_message = '\n'.join(log_message_lines[:TRUNCATE_LOGS] + [ '... %i more lines...' % (len(log_message_lines) - TRUNCATE_LOGS) ]) if len(log_message_lines) > 2: log.trace('Received from tor:\n%s' % log_message) else: log.trace('Received from tor: %s' % log_message.replace('\n', '\\n')) return stem.response.ControlMessage(parsed_content, raw_content_str) elif divider == '+': # data entry, all of the following lines belong to the content until we # get a line with just a period while True: try: line = stem.util.str_tools._to_bytes( control_file.readline()) except socket.error as exc: prefix = logging_prefix % 'SocketClosed' log.info( prefix + 'received an exception while mid-way through a data reply (exception: "%s", read content: "%s")' % (exc, log.escape(b''.join(raw_content)))) raise stem.SocketClosed(exc) raw_content.append(line) if not line.endswith(b'\r\n'): prefix = logging_prefix % 'ProtocolError' log.info( prefix + 'CRLF linebreaks missing from a data reply, "%s"' % log.escape(b''.join(raw_content))) raise stem.ProtocolError('All lines should end with CRLF') elif line == b'.\r\n': break # data block termination line = line[:-2] # strips off the CRLF # lines starting with a period are escaped by a second period (as per # section 2.4 of the control-spec) if line.startswith(b'..'): line = line[1:] content_lines.append(line) # joins the content using a newline rather than CRLF separator (more # conventional for multi-line string content outside the windows world) parsed_content.append( (status_code, divider, b'\n'.join(content_lines))) else: # this should never be reached due to the prefix regex, but might as well # be safe... prefix = logging_prefix % 'ProtocolError' log.warn(prefix + "\"%s\" isn't a recognized divider type" % divider) raise stem.ProtocolError( "Unrecognized divider type '%s': %s" % (divider, stem.util.str_tools._to_unicode(line)))
if line.startswith(".."): line = line[1:] # appends to previous content, using a newline rather than CRLF # separator (more conventional for multi-line string content outside # the windows world) content += "\n" + line parsed_content.append((status_code, divider, content)) else: # this should never be reached due to the prefix regex, but might as well # be safe... prefix = logging_prefix % "ProtocolError" log.warn(prefix + "\"%s\" isn't a recognized divider type" % line) raise stem.ProtocolError("Unrecognized divider type '%s': %s" % (divider, line)) def send_formatting(message): """ Performs the formatting expected from sent control messages. For more information see the :func:`~stem.socket.send_message` function. :param str message: message to be formatted :returns: **str** of the message wrapped by the formatting expected from controllers """ # From control-spec section 2.2...
def recv_message(control_file, arrived_at = None): """ Pulls from a control socket until we either have a complete message or encounter a problem. :param file control_file: file derived from the control socket (see the socket's makefile() method for more information) :returns: :class:`~stem.response.ControlMessage` read from the socket :raises: * :class:`stem.ProtocolError` the content from the socket is malformed * :class:`stem.SocketClosed` if the socket closes before we receive a complete message """ parsed_content, raw_content, first_line = None, None, True while True: try: line = control_file.readline() except AttributeError: # if the control_file has been closed then we will receive: # AttributeError: 'NoneType' object has no attribute 'recv' log.info(ERROR_MSG % ('SocketClosed', 'socket file has been closed')) raise stem.SocketClosed('socket file has been closed') except (OSError, ValueError) as exc: # when disconnected this errors with... # # * ValueError: I/O operation on closed file # * OSError: [Errno 107] Transport endpoint is not connected # * OSError: [Errno 9] Bad file descriptor log.info(ERROR_MSG % ('SocketClosed', 'received exception "%s"' % exc)) raise stem.SocketClosed(exc) # Parses the tor control lines. These are of the form... # <status code><divider><content>\r\n if not line: # if the socket is disconnected then the readline() method will provide # empty content log.info(ERROR_MSG % ('SocketClosed', 'empty socket content')) raise stem.SocketClosed('Received empty socket content.') elif not MESSAGE_PREFIX.match(line): log.info(ERROR_MSG % ('ProtocolError', 'malformed status code/divider, "%s"' % log.escape(line))) raise stem.ProtocolError('Badly formatted reply line: beginning is malformed') elif not line.endswith(b'\r\n'): log.info(ERROR_MSG % ('ProtocolError', 'no CRLF linebreak, "%s"' % log.escape(line))) raise stem.ProtocolError('All lines should end with CRLF') status_code, divider, content = line[:3], line[3:4], line[4:-2] # strip CRLF off content status_code = stem.util.str_tools._to_unicode(status_code) divider = stem.util.str_tools._to_unicode(divider) # Most controller responses are single lines, in which case we don't need # so much overhead. if first_line: if divider == ' ': _log_trace(line) return stem.response.ControlMessage([(status_code, divider, content)], line, arrived_at = arrived_at) else: parsed_content, raw_content, first_line = [], bytearray(), False raw_content += line if divider == '-': # mid-reply line, keep pulling for more content parsed_content.append((status_code, divider, content)) elif divider == ' ': # end of the message, return the message parsed_content.append((status_code, divider, content)) _log_trace(bytes(raw_content)) return stem.response.ControlMessage(parsed_content, bytes(raw_content), arrived_at = arrived_at) elif divider == '+': # data entry, all of the following lines belong to the content until we # get a line with just a period content_block = bytearray(content) while True: try: line = control_file.readline() raw_content += line except socket.error as exc: log.info(ERROR_MSG % ('SocketClosed', 'received an exception while mid-way through a data reply (exception: "%s", read content: "%s")' % (exc, log.escape(bytes(raw_content))))) raise stem.SocketClosed(exc) if not line.endswith(b'\r\n'): log.info(ERROR_MSG % ('ProtocolError', 'CRLF linebreaks missing from a data reply, "%s"' % log.escape(bytes(raw_content)))) raise stem.ProtocolError('All lines should end with CRLF') elif line == b'.\r\n': break # data block termination line = line[:-2] # strips off the CRLF # lines starting with a period are escaped by a second period (as per # section 2.4 of the control-spec) if line.startswith(b'..'): line = line[1:] content_block += b'\n' + line # joins the content using a newline rather than CRLF separator (more # conventional for multi-line string content outside the windows world) parsed_content.append((status_code, divider, bytes(content_block))) else: # this should never be reached due to the prefix regex, but might as well # be safe... log.warn(ERROR_MSG % ('ProtocolError', "\"%s\" isn't a recognized divider type" % divider)) raise stem.ProtocolError("Unrecognized divider type '%s': %s" % (divider, stem.util.str_tools._to_unicode(line)))
def showCountDialog(countType, counts): """ Provides a dialog with bar graphs and percentages for the given set of counts. Pressing any key closes the dialog. Arguments: countType - type of counts being presented counts - mapping of labels to counts """ isNoStats = not counts noStatsMsg = "Usage stats aren't available yet, press any key..." if isNoStats: popup, width, height = cli.popups.init(3, len(noStatsMsg) + 4) else: popup, width, height = cli.popups.init(4 + max(1, len(counts)), 80) if not popup: return try: control = cli.controller.getController() popup.win.box() # dialog title if countType == CountType.CLIENT_LOCALE: title = "Client Locales" elif countType == CountType.EXIT_PORT: title = "Exiting Port Usage" else: title = "" log.warn("Unrecognized count type: %s" % countType) popup.addstr(0, 0, title, curses.A_STANDOUT) if isNoStats: popup.addstr(1, 2, noStatsMsg, curses.A_BOLD | uiTools.getColor("cyan")) else: sortedCounts = sorted(counts.iteritems(), key=operator.itemgetter(1)) sortedCounts.reverse() # constructs string formatting for the max key and value display width keyWidth, valWidth, valueTotal = 3, 1, 0 for k, v in sortedCounts: keyWidth = max(keyWidth, len(k)) valWidth = max(valWidth, len(str(v))) valueTotal += v # extra space since we're adding usage informaion if countType == CountType.EXIT_PORT: keyWidth += EXIT_USAGE_WIDTH labelFormat = "%%-%is %%%ii (%%%%%%-2i)" % (keyWidth, valWidth) for i in range(height - 4): k, v = sortedCounts[i] # includes a port usage column if countType == CountType.EXIT_PORT: usage = connections.getPortUsage(k) if usage: keyFormat = "%%-%is %%s" % (keyWidth - EXIT_USAGE_WIDTH) k = keyFormat % (k, usage[:EXIT_USAGE_WIDTH - 3]) label = labelFormat % (k, v, v * 100 / valueTotal) popup.addstr(i + 1, 2, label, curses.A_BOLD | uiTools.getColor("green")) # All labels have the same size since they're based on the max widths. # If this changes then this'll need to be the max label width. labelWidth = len(label) # draws simple bar graph for percentages fillWidth = v * (width - 4 - labelWidth) / valueTotal for j in range(fillWidth): popup.addstr(i + 1, 3 + labelWidth + j, " ", curses.A_STANDOUT | uiTools.getColor("red")) popup.addstr(height - 2, 2, "Press any key...") popup.win.refresh() curses.cbreak() control.getScreen().getch() finally: cli.popups.finalize()
def recv_message(control_file): """ Pulls from a control socket until we either have a complete message or encounter a problem. :param file control_file: file derived from the control socket (see the socket's makefile() method for more information) :returns: :class:`~stem.response.ControlMessage` read from the socket :raises: * :class:`stem.ProtocolError` the content from the socket is malformed * :class:`stem.SocketClosed` if the socket closes before we receive a complete message """ parsed_content, raw_content = [], "" logging_prefix = "Error while receiving a control message (%s): " while True: try: line = control_file.readline() if stem.prereq.is_python_3(): line = stem.util.str_tools._to_unicode(line) except AttributeError: # if the control_file has been closed then we will receive: # AttributeError: 'NoneType' object has no attribute 'recv' prefix = logging_prefix % "SocketClosed" log.info(prefix + "socket file has been closed") raise stem.SocketClosed("socket file has been closed") except (socket.error, ValueError) as exc: # When disconnected we get... # # Python 2: # socket.error: [Errno 107] Transport endpoint is not connected # # Python 3: # ValueError: I/O operation on closed file. prefix = logging_prefix % "SocketClosed" log.info(prefix + "received exception \"%s\"" % exc) raise stem.SocketClosed(exc) raw_content += line # Parses the tor control lines. These are of the form... # <status code><divider><content>\r\n if len(line) == 0: # if the socket is disconnected then the readline() method will provide # empty content prefix = logging_prefix % "SocketClosed" log.info(prefix + "empty socket content") raise stem.SocketClosed("Received empty socket content.") elif len(line) < 4: prefix = logging_prefix % "ProtocolError" log.info(prefix + "line too short, \"%s\"" % log.escape(line)) raise stem.ProtocolError("Badly formatted reply line: too short") elif not re.match(r'^[a-zA-Z0-9]{3}[-+ ]', line): prefix = logging_prefix % "ProtocolError" log.info(prefix + "malformed status code/divider, \"%s\"" % log.escape(line)) raise stem.ProtocolError("Badly formatted reply line: beginning is malformed") elif not line.endswith("\r\n"): prefix = logging_prefix % "ProtocolError" log.info(prefix + "no CRLF linebreak, \"%s\"" % log.escape(line)) raise stem.ProtocolError("All lines should end with CRLF") line = line[:-2] # strips off the CRLF status_code, divider, content = line[:3], line[3], line[4:] if divider == "-": # mid-reply line, keep pulling for more content parsed_content.append((status_code, divider, content)) elif divider == " ": # end of the message, return the message parsed_content.append((status_code, divider, content)) log_message = raw_content.replace("\r\n", "\n").rstrip() log.trace("Received from tor:\n" + log_message) return stem.response.ControlMessage(parsed_content, raw_content) elif divider == "+": # data entry, all of the following lines belong to the content until we # get a line with just a period while True: try: line = control_file.readline() if stem.prereq.is_python_3(): line = stem.util.str_tools._to_unicode(line) except socket.error as exc: prefix = logging_prefix % "SocketClosed" log.info(prefix + "received an exception while mid-way through a data reply (exception: \"%s\", read content: \"%s\")" % (exc, log.escape(raw_content))) raise stem.SocketClosed(exc) raw_content += line if not line.endswith("\r\n"): prefix = logging_prefix % "ProtocolError" log.info(prefix + "CRLF linebreaks missing from a data reply, \"%s\"" % log.escape(raw_content)) raise stem.ProtocolError("All lines should end with CRLF") elif line == ".\r\n": break # data block termination line = line[:-2] # strips off the CRLF # lines starting with a period are escaped by a second period (as per # section 2.4 of the control-spec) if line.startswith(".."): line = line[1:] # appends to previous content, using a newline rather than CRLF # separator (more conventional for multi-line string content outside # the windows world) content += "\n" + line parsed_content.append((status_code, divider, content)) else: # this should never be reached due to the prefix regex, but might as well # be safe... prefix = logging_prefix % "ProtocolError" log.warn(prefix + "\"%s\" isn't a recognized divider type" % line) raise stem.ProtocolError("Unrecognized divider type '%s': %s" % (divider, line))
def startTorMonitor(startTime): """ Initializes the interface and starts the main draw loop. Arguments: startTime - unix time for when arm was started """ # attempts to fetch the tor pid, warning if unsuccessful (this is needed for # checking its resource usage, among other things) conn = torTools.getConn() torPid = conn.getMyPid() if not torPid and conn.isAlive(): log.warn("Unable to determine Tor's pid. Some information, like its resource usage will be unavailable.") # adds events needed for arm functionality to the torTools REQ_EVENTS # mapping (they're then included with any setControllerEvents call, and log # a more helpful error if unavailable) torTools.REQ_EVENTS["BW"] = "bandwidth graph won't function" if not CONFIG["startup.blindModeEnabled"]: # The DisableDebuggerAttachment will prevent our connection panel from really # functioning. It'll have circuits, but little else. If this is the case then # notify the user and tell them what they can do to fix it. if conn.getOption("DisableDebuggerAttachment", None) == "1": log.notice( "Tor is preventing system utilities like netstat and lsof from working. This means that arm can't provide you with connection information. You can change this by adding 'DisableDebuggerAttachment 0' to your torrc and restarting tor. For more information see...\nhttps://trac.torproject.org/3313" ) connections.getResolver("tor").setPaused(True) else: torTools.REQ_EVENTS["CIRC"] = "may cause issues in identifying client connections" # Configures connection resoultions. This is paused/unpaused according to # if Tor's connected or not. conn.addStatusListener(connResetListener) if torPid: # use the tor pid to help narrow connection results torCmdName = sysTools.getProcessName(torPid, "tor") connections.getResolver(torCmdName, torPid, "tor") else: # constructs singleton resolver and, if tor isn't connected, initizes # it to be paused connections.getResolver("tor").setPaused(not conn.isAlive()) # hack to display a better (arm specific) notice if all resolvers fail connections.RESOLVER_FINAL_FAILURE_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 arm with the same user as tor (ie, \"sudo -u <tor user> arm\")." # provides a notice about any event types tor supports but arm doesn't missingEventTypes = cli.logPanel.getMissingEventTypes() if missingEventTypes: pluralLabel = "s" if len(missingEventTypes) > 1 else "" log.info( "arm doesn't recognize the following event type%s: %s (log 'UNKNOWN' events to see them)" % (pluralLabel, ", ".join(missingEventTypes)) ) try: curses.wrapper(drawTorMonitor, startTime) except KeyboardInterrupt: # Skip printing stack trace in case of keyboard interrupt. The # HALT_ACTIVITY attempts to prevent daemons from triggering a curses redraw # (which would leave the user's terminal in a screwed up state). There is # still a tiny timing issue here (after the exception but before the flag # is set) but I've never seen it happen in practice. panel.HALT_ACTIVITY = True shutdownDaemons()
def recv_message(control_file): """ Pulls from a control socket until we either have a complete message or encounter a problem. :param file control_file: file derived from the control socket (see the socket's makefile() method for more information) :returns: :class:`~stem.response.ControlMessage` read from the socket :raises: * :class:`stem.ProtocolError` the content from the socket is malformed * :class:`stem.SocketClosed` if the socket closes before we receive a complete message """ parsed_content, raw_content = [], b"" logging_prefix = "Error while receiving a control message (%s): " while True: try: # From a real socket readline() would always provide bytes, but during # tests we might be given a StringIO in which case it's unicode under # python 3.x. line = stem.util.str_tools._to_bytes(control_file.readline()) except AttributeError: # if the control_file has been closed then we will receive: # AttributeError: 'NoneType' object has no attribute 'recv' prefix = logging_prefix % "SocketClosed" log.info(prefix + "socket file has been closed") raise stem.SocketClosed("socket file has been closed") except (socket.error, ValueError) as exc: # When disconnected we get... # # Python 2: # socket.error: [Errno 107] Transport endpoint is not connected # # Python 3: # ValueError: I/O operation on closed file. prefix = logging_prefix % "SocketClosed" log.info(prefix + "received exception \"%s\"" % exc) raise stem.SocketClosed(exc) raw_content += line # Parses the tor control lines. These are of the form... # <status code><divider><content>\r\n if len(line) == 0: # if the socket is disconnected then the readline() method will provide # empty content prefix = logging_prefix % "SocketClosed" log.info(prefix + "empty socket content") raise stem.SocketClosed("Received empty socket content.") elif len(line) < 4: prefix = logging_prefix % "ProtocolError" log.info(prefix + "line too short, \"%s\"" % log.escape(line)) raise stem.ProtocolError("Badly formatted reply line: too short") elif not re.match(b'^[a-zA-Z0-9]{3}[-+ ]', line): prefix = logging_prefix % "ProtocolError" log.info(prefix + "malformed status code/divider, \"%s\"" % log.escape(line)) raise stem.ProtocolError( "Badly formatted reply line: beginning is malformed") elif not line.endswith(b"\r\n"): prefix = logging_prefix % "ProtocolError" log.info(prefix + "no CRLF linebreak, \"%s\"" % log.escape(line)) raise stem.ProtocolError("All lines should end with CRLF") line = line[:-2] # strips off the CRLF status_code, divider, content = line[:3], line[3:4], line[4:] if stem.prereq.is_python_3(): status_code = stem.util.str_tools._to_unicode(status_code) divider = stem.util.str_tools._to_unicode(divider) if divider == "-": # mid-reply line, keep pulling for more content parsed_content.append((status_code, divider, content)) elif divider == " ": # end of the message, return the message parsed_content.append((status_code, divider, content)) log_message = raw_content.replace(b"\r\n", b"\n").rstrip() log.trace("Received from tor:\n" + stem.util.str_tools._to_unicode(log_message)) return stem.response.ControlMessage(parsed_content, raw_content) elif divider == "+": # data entry, all of the following lines belong to the content until we # get a line with just a period while True: try: line = stem.util.str_tools._to_bytes( control_file.readline()) except socket.error as exc: prefix = logging_prefix % "SocketClosed" log.info( prefix + "received an exception while mid-way through a data reply (exception: \"%s\", read content: \"%s\")" % (exc, log.escape(raw_content))) raise stem.SocketClosed(exc) raw_content += line if not line.endswith(b"\r\n"): prefix = logging_prefix % "ProtocolError" log.info( prefix + "CRLF linebreaks missing from a data reply, \"%s\"" % log.escape(raw_content)) raise stem.ProtocolError("All lines should end with CRLF") elif line == b".\r\n": break # data block termination line = line[:-2] # strips off the CRLF # lines starting with a period are escaped by a second period (as per # section 2.4 of the control-spec) if line.startswith(b".."): line = line[1:] # appends to previous content, using a newline rather than CRLF # separator (more conventional for multi-line string content outside # the windows world) content += b"\n" + line parsed_content.append((status_code, divider, content)) else: # this should never be reached due to the prefix regex, but might as well # be safe... prefix = logging_prefix % "ProtocolError" log.warn(prefix + "\"%s\" isn't a recognized divider type" % divider) raise stem.ProtocolError( "Unrecognized divider type '%s': %s" % (divider, stem.util.str_tools._to_unicode(line)))
def get_info(self, params, default=stem.control.UNDEFINED, get_bytes=False, cache_miss_warning=True): """ get_info(params, default = UNDEFINED, get_bytes = False) Queries the control socket for the given GETINFO option. If provided a default then that's returned if the GETINFO option is undefined or the call fails for any reason (error response, control port closed, initiated, etc). .. versionchanged:: 1.1.0 Added the get_bytes argument. :param str,list params: GETINFO option or options to be queried :param object default: response if the query fails :param bool get_bytes: provides **bytes** values rather than a **str** under python 3.x :param bool cache_miss_warning: Emit a warning if a cache miss happens :returns: Response depends upon how we were called as follows... * **str** with the response if our param was a **str** * **dict** with the 'param => response' mapping if our param was a **list** * default if one was provided and our call failed :raises: * :class:`stem.ControllerError` if the call fails and we weren't provided a default response * :class:`stem.InvalidArguments` if the 'params' requested was invalid * :class:`stem.ProtocolError` if the geoip database is known to be unavailable """ start_time = time.time() reply = {} if isinstance(params, (bytes, str_type)): is_multiple = False params = set([params]) else: if not params: return {} is_multiple = True params = set(params) # check for cached results from_cache = [param.lower() for param in params] cached_results = self._get_cache_map(from_cache, 'getinfo') for key in cached_results: user_expected_key = stem.control._case_insensitive_lookup( params, key) reply[user_expected_key] = cached_results[key] params.remove(user_expected_key) for param in params: if param.startswith( 'ip-to-country/') and self.is_geoip_unavailable(): # the geoip database already looks to be unavailable - abort the request raise stem.ProtocolError('Tor geoip database is unavailable') # if everything was cached then short circuit making the query if not params: if stem.control.LOG_CACHE_FETCHES: log.trace('GETINFO %s (cache fetch)' % ' '.join(reply.keys())) if is_multiple: return reply else: return list(reply.values())[0] if cache_miss_warning is True: # As this should only happen when we intentionally refresh the cache (when the warning should be disabled) # we warn rather than debug to ensure we get the issue! lgr = logging.getLogger('theonionbox') lgr.info( 'Cache miss for the following parameter(s): {}'.format(params)) try: response = self.msg('GETINFO %s' % ' '.join(params)) stem.response.convert('GETINFO', response) response._assert_matches(params) # usually we want unicode values under python 3.x if stem.prereq.is_python_3() and not get_bytes: response.entries = dict((k, stem.util.str_tools._to_unicode(v)) for (k, v) in response.entries.items()) reply.update(response.entries) if self.is_caching_enabled(): to_cache = {} for key, value in response.entries.items(): key = key.lower() # make case insensitive # To allow The Onion Box smooth response cycles even when connecting to a relay / bridge # via Tor socks proxy / hidden service, we cache *all* parameters and 'manually' update them with a # single call every once in a while. to_cache[key] = value if key.startswith('ip-to-country/'): # both cache-able and means that we should reset the geoip failure count self._geoip_failure_count = -1 self._set_cache(to_cache, 'getinfo') log.debug('GETINFO %s (runtime: %0.4f)' % (' '.join(params), time.time() - start_time)) if is_multiple: return reply else: return list(reply.values())[0] except stem.ControllerError as exc: # bump geoip failure count if... # * we're caching results # * this was soley a geoip lookup # * we've never had a successful geoip lookup (failure count isn't -1) is_geoip_request = len(params) == 1 and list(params)[0].startswith( 'ip-to-country/') if is_geoip_request and self.is_caching_enabled( ) and self._geoip_failure_count != -1: self._geoip_failure_count += 1 if self.is_geoip_unavailable(): log.warn("Tor's geoip database is unavailable.") log.debug('GETINFO %s (failed: %s)' % (' '.join(params), exc)) raise
def startTorMonitor(startTime): """ Initializes the interface and starts the main draw loop. Arguments: startTime - unix time for when arm was started """ # attempts to fetch the tor pid, warning if unsuccessful (this is needed for # checking its resource usage, among other things) conn = torTools.getConn() torPid = conn.getMyPid() if not torPid and conn.isAlive(): log.warn( "Unable to determine Tor's pid. Some information, like its resource usage will be unavailable." ) # adds events needed for arm functionality to the torTools REQ_EVENTS # mapping (they're then included with any setControllerEvents call, and log # a more helpful error if unavailable) torTools.REQ_EVENTS["BW"] = "bandwidth graph won't function" if not CONFIG["startup.blindModeEnabled"]: # The DisableDebuggerAttachment will prevent our connection panel from really # functioning. It'll have circuits, but little else. If this is the case then # notify the user and tell them what they can do to fix it. if conn.getOption("DisableDebuggerAttachment", None) == "1": log.notice( "Tor is preventing system utilities like netstat and lsof from working. This means that arm can't provide you with connection information. You can change this by adding 'DisableDebuggerAttachment 0' to your torrc and restarting tor. For more information see...\nhttps://trac.torproject.org/3313" ) connections.getResolver("tor").setPaused(True) else: torTools.REQ_EVENTS[ "CIRC"] = "may cause issues in identifying client connections" # Configures connection resoultions. This is paused/unpaused according to # if Tor's connected or not. conn.addStatusListener(connResetListener) if torPid: # use the tor pid to help narrow connection results torCmdName = sysTools.getProcessName(torPid, "tor") connections.getResolver(torCmdName, torPid, "tor") else: # constructs singleton resolver and, if tor isn't connected, initizes # it to be paused connections.getResolver("tor").setPaused(not conn.isAlive()) # hack to display a better (arm specific) notice if all resolvers fail connections.RESOLVER_FINAL_FAILURE_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 arm with the same user as tor (ie, \"sudo -u <tor user> arm\")." # provides a notice about any event types tor supports but arm doesn't missingEventTypes = cli.logPanel.getMissingEventTypes() if missingEventTypes: pluralLabel = "s" if len(missingEventTypes) > 1 else "" log.info( "arm doesn't recognize the following event type%s: %s (log 'UNKNOWN' events to see them)" % (pluralLabel, ", ".join(missingEventTypes))) try: curses.wrapper(drawTorMonitor, startTime) except KeyboardInterrupt: # Skip printing stack trace in case of keyboard interrupt. The # HALT_ACTIVITY attempts to prevent daemons from triggering a curses redraw # (which would leave the user's terminal in a screwed up state). There is # still a tiny timing issue here (after the exception but before the flag # is set) but I've never seen it happen in practice. panel.HALT_ACTIVITY = True shutdownDaemons()
if line.startswith(".."): line = line[1:] # appends to previous content, using a newline rather than CRLF # separator (more conventional for multi-line string content outside # the windows world) content += "\n" + line parsed_content.append((status_code, divider, content)) else: # this should never be reached due to the prefix regex, but might as well # be safe... prefix = logging_prefix % "ProtocolError" log.warn(prefix + "\"%s\" isn't a recognized divider type" % line) raise stem.ProtocolError("Unrecognized divider type '%s': %s" % (divider, line)) def send_formatting(message): """ Performs the formatting expected from sent control messages. For more information see the :func:`~stem.socket.send_message` function. :param str message: message to be formatted :returns: **str** of the message wrapped by the formatting expected from controllers """
def recv_message(control_file): """ Pulls from a control socket until we either have a complete message or encounter a problem. :param file control_file: file derived from the control socket (see the socket's makefile() method for more information) :returns: :class:`~stem.response.ControlMessage` read from the socket :raises: * :class:`stem.ProtocolError` the content from the socket is malformed * :class:`stem.SocketClosed` if the socket closes before we receive a complete message """ parsed_content, raw_content = [], [] logging_prefix = 'Error while receiving a control message (%s): ' while True: try: # From a real socket readline() would always provide bytes, but during # tests we might be given a StringIO in which case it's unicode under # python 3.x. line = stem.util.str_tools._to_bytes(control_file.readline()) except AttributeError: # if the control_file has been closed then we will receive: # AttributeError: 'NoneType' object has no attribute 'recv' prefix = logging_prefix % 'SocketClosed' log.info(prefix + 'socket file has been closed') raise stem.SocketClosed('socket file has been closed') except (socket.error, ValueError) as exc: # When disconnected we get... # # Python 2: # socket.error: [Errno 107] Transport endpoint is not connected # # Python 3: # ValueError: I/O operation on closed file. prefix = logging_prefix % 'SocketClosed' log.info(prefix + 'received exception "%s"' % exc) raise stem.SocketClosed(exc) raw_content.append(line) # Parses the tor control lines. These are of the form... # <status code><divider><content>\r\n if len(line) == 0: # if the socket is disconnected then the readline() method will provide # empty content prefix = logging_prefix % 'SocketClosed' log.info(prefix + 'empty socket content') raise stem.SocketClosed('Received empty socket content.') elif len(line) < 4: prefix = logging_prefix % 'ProtocolError' log.info(prefix + 'line too short, "%s"' % log.escape(line)) raise stem.ProtocolError('Badly formatted reply line: too short') elif not re.match(b'^[a-zA-Z0-9]{3}[-+ ]', line): prefix = logging_prefix % 'ProtocolError' log.info(prefix + 'malformed status code/divider, "%s"' % log.escape(line)) raise stem.ProtocolError('Badly formatted reply line: beginning is malformed') elif not line.endswith(b'\r\n'): prefix = logging_prefix % 'ProtocolError' log.info(prefix + 'no CRLF linebreak, "%s"' % log.escape(line)) raise stem.ProtocolError('All lines should end with CRLF') line = line[:-2] # strips off the CRLF status_code, divider, content = line[:3], line[3:4], line[4:] content_lines = [content] if stem.prereq.is_python_3(): status_code = stem.util.str_tools._to_unicode(status_code) divider = stem.util.str_tools._to_unicode(divider) if divider == '-': # mid-reply line, keep pulling for more content parsed_content.append((status_code, divider, content)) elif divider == ' ': # end of the message, return the message parsed_content.append((status_code, divider, content)) raw_content_str = b''.join(raw_content) log_message = raw_content_str.replace(b'\r\n', b'\n').rstrip() log.trace('Received from tor:\n' + stem.util.str_tools._to_unicode(log_message)) return stem.response.ControlMessage(parsed_content, raw_content_str) elif divider == '+': # data entry, all of the following lines belong to the content until we # get a line with just a period while True: try: line = stem.util.str_tools._to_bytes(control_file.readline()) except socket.error as exc: prefix = logging_prefix % 'SocketClosed' log.info(prefix + 'received an exception while mid-way through a data reply (exception: "%s", read content: "%s")' % (exc, log.escape(b''.join(raw_content)))) raise stem.SocketClosed(exc) raw_content.append(line) if not line.endswith(b'\r\n'): prefix = logging_prefix % 'ProtocolError' log.info(prefix + 'CRLF linebreaks missing from a data reply, "%s"' % log.escape(b''.join(raw_content))) raise stem.ProtocolError('All lines should end with CRLF') elif line == b'.\r\n': break # data block termination line = line[:-2] # strips off the CRLF # lines starting with a period are escaped by a second period (as per # section 2.4 of the control-spec) if line.startswith(b'..'): line = line[1:] content_lines.append(line) # joins the content using a newline rather than CRLF separator (more # conventional for multi-line string content outside the windows world) parsed_content.append((status_code, divider, b'\n'.join(content_lines))) else: # this should never be reached due to the prefix regex, but might as well # be safe... prefix = logging_prefix % 'ProtocolError' log.warn(prefix + "\"%s\" isn't a recognized divider type" % divider) raise stem.ProtocolError("Unrecognized divider type '%s': %s" % (divider, stem.util.str_tools._to_unicode(line)))
def _update(self, setStatic=False): """ Updates stats in the vals mapping. By default this just revises volatile attributes. Arguments: setStatic - resets all parameters, including relatively static values """ self.valsLock.acquire() conn = torTools.getConn() if setStatic: # version is truncated to first part, for instance: # 0.2.2.13-alpha (git-feb8c1b5f67f2c6f) -> 0.2.2.13-alpha self.vals["tor/version"] = conn.getInfo("version", "Unknown").split()[0] self.vals["tor/versionStatus"] = conn.getInfo( "status/version/current", "Unknown") self.vals["tor/nickname"] = conn.getOption("Nickname", "") self.vals["tor/orPort"] = conn.getOption("ORPort", "0") self.vals["tor/dirPort"] = conn.getOption("DirPort", "0") self.vals["tor/controlPort"] = conn.getOption("ControlPort", "0") self.vals["tor/socketPath"] = conn.getOption("ControlSocket", "") self.vals["tor/isAuthPassword"] = conn.getOption( "HashedControlPassword", None) != None self.vals["tor/isAuthCookie"] = conn.getOption( "CookieAuthentication", None) == "1" # orport is reported as zero if unset if self.vals["tor/orPort"] == "0": self.vals["tor/orPort"] = "" # overwrite address if ORListenAddress is set (and possibly orPort too) self.vals["tor/orListenAddr"] = "" listenAddr = conn.getOption("ORListenAddress", None) if listenAddr: if ":" in listenAddr: # both ip and port overwritten self.vals["tor/orListenAddr"] = listenAddr[:listenAddr. find(":")] self.vals["tor/orPort"] = listenAddr[listenAddr.find(":") + 1:] else: self.vals["tor/orListenAddr"] = listenAddr # fetch exit policy (might span over multiple lines) policyEntries = [] for exitPolicy in conn.getOption("ExitPolicy", [], True): policyEntries += [ policy.strip() for policy in exitPolicy.split(",") ] self.vals["tor/exitPolicy"] = ", ".join(policyEntries) # file descriptor limit for the process, if this can't be determined # then the limit is None fdLimit, fdIsEstimate = conn.getMyFileDescriptorLimit() self.vals["tor/fdLimit"] = fdLimit self.vals["tor/isFdLimitEstimate"] = fdIsEstimate # system information unameVals = os.uname() self.vals["sys/hostname"] = unameVals[1] self.vals["sys/os"] = unameVals[0] self.vals["sys/version"] = unameVals[2] pid = conn.getMyPid() self.vals["tor/pid"] = pid if pid else "" startTime = conn.getStartTime() self.vals["tor/startTime"] = startTime if startTime else "" # reverts volatile parameters to defaults self.vals["tor/fingerprint"] = "Unknown" self.vals["tor/flags"] = [] self.vals["tor/fdUsed"] = 0 self.vals["stat/%torCpu"] = "0" self.vals["stat/%armCpu"] = "0" self.vals["stat/rss"] = "0" self.vals["stat/%mem"] = "0" # sets volatile parameters # TODO: This can change, being reported by STATUS_SERVER -> EXTERNAL_ADDRESS # events. Introduce caching via torTools? self.vals["tor/address"] = conn.getInfo("address", "") self.vals["tor/fingerprint"] = conn.getInfo( "fingerprint", self.vals["tor/fingerprint"]) self.vals["tor/flags"] = conn.getMyFlags(self.vals["tor/flags"]) # Updates file descriptor usage and logs if the usage is high. If we don't # have a known limit or it's obviously faulty (being lower than our # current usage) then omit file descriptor functionality. if self.vals["tor/fdLimit"]: fdUsed = conn.getMyFileDescriptorUsage() if fdUsed and fdUsed <= self.vals["tor/fdLimit"]: self.vals["tor/fdUsed"] = fdUsed else: self.vals["tor/fdUsed"] = 0 if self.vals["tor/fdUsed"] and self.vals["tor/fdLimit"]: fdPercent = 100 * self.vals["tor/fdUsed"] / self.vals["tor/fdLimit"] estimatedLabel = " estimated" if self.vals[ "tor/isFdLimitEstimate"] else "" msg = "Tor's%s file descriptor usage is at %i%%." % ( estimatedLabel, fdPercent) if fdPercent >= 90 and not self._isFdNinetyPercentWarned: self._isFdSixtyPercentWarned, self._isFdNinetyPercentWarned = True, True msg += " If you run out Tor will be unable to continue functioning." log.warn(msg) elif fdPercent >= 60 and not self._isFdSixtyPercentWarned: self._isFdSixtyPercentWarned = True log.notice(msg) # ps or proc derived resource usage stats if self.vals["tor/pid"]: resourceTracker = sysTools.getResourceTracker(self.vals["tor/pid"]) if resourceTracker.lastQueryFailed(): self.vals["stat/%torCpu"] = "0" self.vals["stat/rss"] = "0" self.vals["stat/%mem"] = "0" else: cpuUsage, _, memUsage, memUsagePercent = resourceTracker.getResourceUsage( ) self._lastResourceFetch = resourceTracker.getRunCount() self.vals["stat/%torCpu"] = "%0.1f" % (100 * cpuUsage) self.vals["stat/rss"] = str(memUsage) self.vals["stat/%mem"] = "%0.1f" % (100 * memUsagePercent) # determines the cpu time for the arm process (including user and system # time of both the primary and child processes) totalArmCpuTime, currentTime = sum(os.times()[:3]), time.time() armCpuDelta = totalArmCpuTime - self._armCpuSampling[0] armTimeDelta = currentTime - self._armCpuSampling[1] pythonCpuTime = armCpuDelta / armTimeDelta sysCallCpuTime = sysTools.getSysCpuUsage() self.vals["stat/%armCpu"] = "%0.1f" % ( 100 * (pythonCpuTime + sysCallCpuTime)) self._armCpuSampling = (totalArmCpuTime, currentTime) self._lastUpdate = currentTime self.valsLock.release()