Beispiel #1
0
  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
Beispiel #2
0
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
Beispiel #3
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())
Beispiel #4
0
  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
Beispiel #5
0
  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())
Beispiel #6
0
    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
Beispiel #7
0
  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
Beispiel #8
0
 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
Beispiel #9
0
    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)
Beispiel #10
0
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')
Beispiel #11
0
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')
Beispiel #12
0
  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)
Beispiel #13
0
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)
Beispiel #14
0
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)
Beispiel #16
0
  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)
Beispiel #17
0
    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
Beispiel #18
0
    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)
Beispiel #19
0
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)
Beispiel #20
0
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)
Beispiel #21
0
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
Beispiel #22
0
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
Beispiel #23
0
 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)
Beispiel #24
0
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)))
Beispiel #25
0
    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)
Beispiel #26
0
        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...
Beispiel #27
0
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)))
Beispiel #28
0
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()
Beispiel #29
0
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))
Beispiel #30
0
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()
Beispiel #31
0
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)))
Beispiel #32
0
    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
Beispiel #33
0
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()
Beispiel #34
0
                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
  """
Beispiel #35
0
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)))
Beispiel #36
0
    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()
Beispiel #37
0
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()