Beispiel #1
0
  def handle_key(self, key):
    if key.match('n'):
      self.send_newnym()
    elif key.match('r') and not self._vals.is_connected:
      # TODO: This is borked. Not quite sure why but our attempt to call
      # PROTOCOLINFO fails with a socket error, followed by completely freezing
      # nyx. This is exposing two bugs...
      #
      # * This should be working. That's a stem issue.
      # * Our interface shouldn't be locking up. That's an nyx issue.

      return True

      controller = tor_controller()

      try:
        controller.connect()

        try:
          controller.authenticate()  # TODO: should account for our chroot
        except stem.connection.MissingPassword:
          password = nyx.popups.input_prompt('Controller Password: '******'s control port")
        nyx.popups.show_msg('Tor reconnected', 1)
      except Exception as exc:
        nyx.popups.show_msg('Unable to reconnect (%s)' % exc, 3)
        controller.close()
    else:
      return False

    return True
Beispiel #2
0
    def _update(self):
        self._vals = Sampling.create(self._vals)

        if self._vals.fd_used and self._vals.fd_limit != -1:
            fd_percent = 100 * self._vals.fd_used / self._vals.fd_limit

            if fd_percent >= 90:
                log_msg = msg('panel.header.fd_used_at_ninety_percent',
                              percentage=fd_percent)
                log.log_once('fd_used_at_ninety_percent', log.WARN, log_msg)
                log.DEDUPLICATION_MESSAGE_IDS.add('fd_used_at_sixty_percent')
            elif fd_percent >= 60:
                log_msg = msg('panel.header.fd_used_at_sixty_percent',
                              percentage=fd_percent)
                log.log_once('fd_used_at_sixty_percent', log.NOTICE, log_msg)

        if self._vals.is_connected:
            if not self._reported_inactive and (
                    time.time() - self._vals.last_heartbeat) >= 10:
                self._reported_inactive = True
                log.notice('Relay unresponsive (last heartbeat: %s)' %
                           time.ctime(self._vals.last_heartbeat))
            elif self._reported_inactive and (time.time() -
                                              self._vals.last_heartbeat) < 10:
                self._reported_inactive = False
                log.notice('Relay resumed')

        self.redraw()
Beispiel #3
0
    def _update(self):
        self._vals = Sampling.create(self._vals)

        if self._vals.fd_used and self._vals.fd_limit != -1:
            fd_percent = 100 * self._vals.fd_used // self._vals.fd_limit

            if fd_percent >= 90:
                log_msg = "Tor's file descriptor usage is at %s%%. If you run out Tor will be unable to continue functioning." % fd_percent
                log.log_once('fd_used_at_ninety_percent', log.WARN, log_msg)
                log.DEDUPLICATION_MESSAGE_IDS.add('fd_used_at_sixty_percent')
            elif fd_percent >= 60:
                log_msg = "Tor's file descriptor usage is at %s%%." % fd_percent
                log.log_once('fd_used_at_sixty_percent', log.NOTICE, log_msg)

        if self._vals.is_connected:
            if not self._reported_inactive and (
                    time.time() - self._vals.last_heartbeat) >= 10:
                self._reported_inactive = True
                log.notice('Relay unresponsive (last heartbeat: %s)' %
                           time.ctime(self._vals.last_heartbeat))
            elif self._reported_inactive and (time.time() -
                                              self._vals.last_heartbeat) < 10:
                self._reported_inactive = False
                log.notice('Relay resumed')

        self.redraw()
Beispiel #4
0
def _warn_if_root(controller):
  """
  Give a notice if tor or nyx are running with root.
  """

  if controller.get_user(None) == 'root':
    log.notice('setup.tor_is_running_as_root')
  elif os.getuid() == 0:
    log.notice('setup.nyx_is_running_as_root')
Beispiel #5
0
def _warn_if_root(controller):
  """
  Give a notice if tor or nyx are running with root.
  """

  if controller.get_user(None) == 'root':
    log.notice('setup.tor_is_running_as_root')
  elif os.getuid() == 0:
    log.notice('setup.nyx_is_running_as_root')
Beispiel #6
0
def heartbeatCheck(isUnresponsive):
    """
  Logs if its been ten seconds since the last BW event.
  
  Arguments:
    isUnresponsive - flag for if we've indicated to be responsive or not
  """

    conn = torTools.getConn()
    lastHeartbeat = conn.getHeartbeat()
    if conn.isAlive() and "BW" in conn.getControllerEvents():
        if not isUnresponsive and (time.time() - lastHeartbeat) >= 10:
            isUnresponsive = True
            log.notice("Relay unresponsive (last heartbeat: %s)" % time.ctime(lastHeartbeat))
        elif isUnresponsive and (time.time() - lastHeartbeat) < 10:
            # really shouldn't happen (meant Tor froze for a bit)
            isUnresponsive = False
            log.notice("Relay resumed")

    return isUnresponsive
Beispiel #7
0
def conn_reset_listener(controller, event_type, _):
  """
  Pauses connection resolution when tor's shut down, and resumes with the new
  pid if started again.
  """

  resolver = nyx.util.tracker.get_connection_tracker()

  if resolver.is_alive():
    resolver.set_paused(event_type == State.CLOSED)

    if event_type == State.CLOSED:
      log.notice('Tor control port closed')
    elif event_type in (State.INIT, State.RESET):
      # Reload the torrc contents. If the torrc panel is present then it will
      # do this instead since it wants to do validation and redraw _after_ the
      # new contents are loaded.

      if get_controller().get_panel('torrc') is None:
        tor_config.get_torrc().load(True)
Beispiel #8
0
    def _reconnect():
      if self._vals.is_connected:
        return

      controller = tor_controller()
      self.show_message('Reconnecting...', HIGHLIGHT)

      try:
        try:
          controller.reconnect(chroot_path = CONFIG['tor.chroot'])
        except stem.connection.MissingPassword:
          password = nyx.controller.input_prompt('Controller Password: '******'s control port")
        self.show_message('Tor reconnected', HIGHLIGHT, max_wait = 1)
      except Exception as exc:
        self.show_message('Unable to reconnect (%s)' % exc, HIGHLIGHT, max_wait = 3)
        controller.close()
Beispiel #9
0
def heartbeat_check(is_unresponsive):
  """
  Logs if its been ten seconds since the last BW event.

  Arguments:
    is_unresponsive - flag for if we've indicated to be responsive or not
  """

  controller = tor_controller()
  last_heartbeat = controller.get_latest_heartbeat()

  if controller.is_alive():
    if not is_unresponsive and (time.time() - last_heartbeat) >= 10:
      is_unresponsive = True
      log.notice('Relay unresponsive (last heartbeat: %s)' % time.ctime(last_heartbeat))
    elif is_unresponsive and (time.time() - last_heartbeat) < 10:
      # really shouldn't happen (meant Tor froze for a bit)
      is_unresponsive = False
      log.notice('Relay resumed')

  return is_unresponsive
Beispiel #10
0
def heartbeatCheck(isUnresponsive):
    """
  Logs if its been ten seconds since the last BW event.
  
  Arguments:
    isUnresponsive - flag for if we've indicated to be responsive or not
  """

    conn = torTools.getConn()
    lastHeartbeat = conn.getHeartbeat()
    if conn.isAlive() and "BW" in conn.getControllerEvents():
        if not isUnresponsive and (time.time() - lastHeartbeat) >= 10:
            isUnresponsive = True
            log.notice("Relay unresponsive (last heartbeat: %s)" %
                       time.ctime(lastHeartbeat))
        elif isUnresponsive and (time.time() - lastHeartbeat) < 10:
            # really shouldn't happen (meant Tor froze for a bit)
            isUnresponsive = False
            log.notice("Relay resumed")

    return isUnresponsive
Beispiel #11
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 #12
0
        def _reconnect():
            if self._vals.is_connected:
                return

            controller = tor_controller()
            self.show_message('Reconnecting...', HIGHLIGHT)

            try:
                try:
                    controller.reconnect(chroot_path=nyx.chroot())
                except stem.connection.MissingPassword:
                    password = nyx.input_prompt('Controller Password: '******'s control port")
                self.show_message('Tor reconnected', HIGHLIGHT, max_wait=1)
            except Exception as exc:
                self.show_message('Unable to reconnect (%s)' % exc,
                                  HIGHLIGHT,
                                  max_wait=3)
                controller.close()
Beispiel #13
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 #14
0
  def _update(self):
    self._vals = Sampling.create(self._vals)

    if self._vals.fd_used and self._vals.fd_limit != -1:
      fd_percent = 100 * self._vals.fd_used / self._vals.fd_limit

      if fd_percent >= 90:
        log_msg = msg('panel.header.fd_used_at_ninety_percent', percentage = fd_percent)
        log.log_once('fd_used_at_ninety_percent', log.WARN, log_msg)
        log.DEDUPLICATION_MESSAGE_IDS.add('fd_used_at_sixty_percent')
      elif fd_percent >= 60:
        log_msg = msg('panel.header.fd_used_at_sixty_percent', percentage = fd_percent)
        log.log_once('fd_used_at_sixty_percent', log.NOTICE, log_msg)

    if self._vals.is_connected:
      if not self._reported_inactive and (time.time() - self._vals.last_heartbeat) >= 10:
        self._reported_inactive = True
        log.notice('Relay unresponsive (last heartbeat: %s)' % time.ctime(self._vals.last_heartbeat))
      elif self._reported_inactive and (time.time() - self._vals.last_heartbeat) < 10:
        self._reported_inactive = False
        log.notice('Relay resumed')

    self.redraw()
Beispiel #15
0
def conf_handler(key, value):
  if key.startswith("port.label."):
    portEntry = key[11:]
    
    divIndex = portEntry.find("-")
    if divIndex == -1:
      # single port
      if portEntry.isdigit():
        PORT_USAGE[portEntry] = value
      else:
        msg = "Port value isn't numeric for entry: %s" % key
        log.notice(msg)
    else:
      try:
        # range of ports (inclusive)
        minPort = int(portEntry[:divIndex])
        maxPort = int(portEntry[divIndex + 1:])
        if minPort > maxPort: raise ValueError()
        
        for port in range(minPort, maxPort + 1):
          PORT_USAGE[str(port)] = value
      except ValueError:
        msg = "Unable to parse port range for entry: %s" % key
        log.notice(msg)
Beispiel #16
0
  def reset_listener(self, controller, event_type, _):
    self._update()

    if event_type == stem.control.State.CLOSED:
      log.notice('Tor control port closed')
Beispiel #17
0
def drawTorMonitor(stdscr, startTime):
    """
  Main draw loop context.
  
  Arguments:
    stdscr    - curses window
    startTime - unix time for when arm was started
  """

    initController(stdscr, startTime)
    control = getController()

    # provides notice about any unused config keys
    for key in conf.get_config("arm").unused_keys():
        log.notice("Unused configuration entry: %s" % key)

    # tells daemon panels to start
    for panelImpl in control.getDaemonPanels():
        panelImpl.start()

    # allows for background transparency
    try:
        curses.use_default_colors()
    except curses.error:
        pass

    # makes the cursor invisible
    try:
        curses.curs_set(0)
    except curses.error:
        pass

    # logs the initialization time
    log.info("arm started (initialization took %0.3f seconds)" %
             (time.time() - startTime))

    # main draw loop
    overrideKey = None  # uses this rather than waiting on user input
    isUnresponsive = False  # flag for heartbeat responsiveness check

    while not control.isDone():
        displayPanels = control.getDisplayPanels()
        isUnresponsive = heartbeatCheck(isUnresponsive)

        # sets panel visability
        for panelImpl in control.getAllPanels():
            panelImpl.setVisible(panelImpl in displayPanels)

        # redraws the interface if it's needed
        control.redraw(False)
        stdscr.refresh()

        # wait for user keyboard input until timeout, unless an override was set
        if overrideKey:
            key, overrideKey = overrideKey, None
        else:
            curses.halfdelay(CONFIG["features.redrawRate"] * 10)
            key = stdscr.getch()

        if key == curses.KEY_RIGHT:
            control.nextPage()
        elif key == curses.KEY_LEFT:
            control.prevPage()
        elif key == ord('p') or key == ord('P'):
            control.setPaused(not control.isPaused())
        elif key == ord('m') or key == ord('M'):
            cli.menu.menu.showMenu()
        elif key == ord('q') or key == ord('Q'):
            # provides prompt to confirm that arm should exit
            if CONFIG["features.confirmQuit"]:
                msg = "Are you sure (q again to confirm)?"
                confirmationKey = cli.popups.showMsg(msg, attr=curses.A_BOLD)
                quitConfirmed = confirmationKey in (ord('q'), ord('Q'))
            else:
                quitConfirmed = True

            if quitConfirmed: control.quit()
        elif key == ord('x') or key == ord('X'):
            # provides prompt to confirm that arm should issue a sighup
            msg = "This will reset Tor's internal state. Are you sure (x again to confirm)?"
            confirmationKey = cli.popups.showMsg(msg, attr=curses.A_BOLD)

            if confirmationKey in (ord('x'), ord('X')):
                try:
                    torTools.getConn().reload()
                except IOError, exc:
                    log.error("Error detected when reloading tor: %s" %
                              sysTools.getFileErrorMsg(exc))
        elif key == ord('h') or key == ord('H'):
            overrideKey = cli.popups.showHelpPopup()
Beispiel #18
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 #19
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 #20
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 #21
0
def drawTorMonitor(stdscr, startTime):
    """
  Main draw loop context.
  
  Arguments:
    stdscr    - curses window
    startTime - unix time for when arm was started
  """

    initController(stdscr, startTime)
    control = getController()

    # provides notice about any unused config keys
    for key in conf.get_config("arm").unused_keys():
        log.notice("Unused configuration entry: %s" % key)

    # tells daemon panels to start
    for panelImpl in control.getDaemonPanels():
        panelImpl.start()

    # allows for background transparency
    try:
        curses.use_default_colors()
    except curses.error:
        pass

    # makes the cursor invisible
    try:
        curses.curs_set(0)
    except curses.error:
        pass

    # logs the initialization time
    log.info("arm started (initialization took %0.3f seconds)" % (time.time() - startTime))

    # main draw loop
    overrideKey = None  # uses this rather than waiting on user input
    isUnresponsive = False  # flag for heartbeat responsiveness check

    while not control.isDone():
        displayPanels = control.getDisplayPanels()
        isUnresponsive = heartbeatCheck(isUnresponsive)

        # sets panel visability
        for panelImpl in control.getAllPanels():
            panelImpl.setVisible(panelImpl in displayPanels)

        # redraws the interface if it's needed
        control.redraw(False)
        stdscr.refresh()

        # wait for user keyboard input until timeout, unless an override was set
        if overrideKey:
            key, overrideKey = overrideKey, None
        else:
            curses.halfdelay(CONFIG["features.redrawRate"] * 10)
            key = stdscr.getch()

        if key == curses.KEY_RIGHT:
            control.nextPage()
        elif key == curses.KEY_LEFT:
            control.prevPage()
        elif key == ord("p") or key == ord("P"):
            control.setPaused(not control.isPaused())
        elif key == ord("m") or key == ord("M"):
            cli.menu.menu.showMenu()
        elif key == ord("q") or key == ord("Q"):
            # provides prompt to confirm that arm should exit
            if CONFIG["features.confirmQuit"]:
                msg = "Are you sure (q again to confirm)?"
                confirmationKey = cli.popups.showMsg(msg, attr=curses.A_BOLD)
                quitConfirmed = confirmationKey in (ord("q"), ord("Q"))
            else:
                quitConfirmed = True

            if quitConfirmed:
                control.quit()
        elif key == ord("x") or key == ord("X"):
            # provides prompt to confirm that arm should issue a sighup
            msg = "This will reset Tor's internal state. Are you sure (x again to confirm)?"
            confirmationKey = cli.popups.showMsg(msg, attr=curses.A_BOLD)

            if confirmationKey in (ord("x"), ord("X")):
                try:
                    torTools.getConn().reload()
                except IOError, exc:
                    log.error("Error detected when reloading tor: %s" % sysTools.getFileErrorMsg(exc))
        elif key == ord("h") or key == ord("H"):
            overrideKey = cli.popups.showHelpPopup()
Beispiel #22
0
def conf_handler(key, value):
    if key == "features.colorOverride" and value != "none":
        try:
            setColorOverride(value)
        except ValueError, exc:
            log.notice(exc)
Beispiel #23
0
    def prepopulateFromState(self):
        """
    Attempts to use tor's state file to prepopulate values for the 15 minute
    interval via the BWHistoryReadValues/BWHistoryWriteValues values. This
    returns True if successful and False otherwise.
    """

        # checks that this is a relay (if ORPort is unset, then skip)
        conn = torTools.getConn()
        orPort = conn.getOption("ORPort", None)
        if orPort == "0": return

        # gets the uptime (using the same parameters as the header panel to take
        # advantage of caching)
        # TODO: stem dropped system caching support so we'll need to think of
        # something else
        uptime = None
        queryPid = conn.getMyPid()
        if queryPid:
            queryParam = ["%cpu", "rss", "%mem", "etime"]
            queryCmd = "ps -p %s -o %s" % (queryPid, ",".join(queryParam))
            psCall = system.call(queryCmd, None)

            if psCall and len(psCall) == 2:
                stats = psCall[1].strip().split()
                if len(stats) == 4: uptime = stats[3]

        # checks if tor has been running for at least a day, the reason being that
        # the state tracks a day's worth of data and this should only prepopulate
        # results associated with this tor instance
        if not uptime or not "-" in uptime:
            msg = PREPOPULATE_FAILURE_MSG % "insufficient uptime"
            log.notice(msg)
            return False

        # get the user's data directory (usually '~/.tor')
        dataDir = conn.getOption("DataDirectory", None)
        if not dataDir:
            msg = PREPOPULATE_FAILURE_MSG % "data directory not found"
            log.notice(msg)
            return False

        # attempt to open the state file
        try:
            stateFile = open("%s%s/state" % (conn.getPathPrefix(), dataDir),
                             "r")
        except IOError:
            msg = PREPOPULATE_FAILURE_MSG % "unable to read the state file"
            log.notice(msg)
            return False

        # get the BWHistory entries (ordered oldest to newest) and number of
        # intervals since last recorded
        bwReadEntries, bwWriteEntries = None, None
        missingReadEntries, missingWriteEntries = None, None

        # converts from gmt to local with respect to DST
        tz_offset = time.altzone if time.localtime()[8] else time.timezone

        for line in stateFile:
            line = line.strip()

            # According to the rep_hist_update_state() function the BWHistory*Ends
            # correspond to the start of the following sampling period. Also, the
            # most recent values of BWHistory*Values appear to be an incremental
            # counter for the current sampling period. Hence, offsets are added to
            # account for both.

            if line.startswith("BWHistoryReadValues"):
                bwReadEntries = line[20:].split(",")
                bwReadEntries = [
                    int(entry) / 1024.0 / 900 for entry in bwReadEntries
                ]
                bwReadEntries.pop()
            elif line.startswith("BWHistoryWriteValues"):
                bwWriteEntries = line[21:].split(",")
                bwWriteEntries = [
                    int(entry) / 1024.0 / 900 for entry in bwWriteEntries
                ]
                bwWriteEntries.pop()
            elif line.startswith("BWHistoryReadEnds"):
                lastReadTime = time.mktime(
                    time.strptime(line[18:], "%Y-%m-%d %H:%M:%S")) - tz_offset
                lastReadTime -= 900
                missingReadEntries = int((time.time() - lastReadTime) / 900)
            elif line.startswith("BWHistoryWriteEnds"):
                lastWriteTime = time.mktime(
                    time.strptime(line[19:], "%Y-%m-%d %H:%M:%S")) - tz_offset
                lastWriteTime -= 900
                missingWriteEntries = int((time.time() - lastWriteTime) / 900)

        if not bwReadEntries or not bwWriteEntries or not lastReadTime or not lastWriteTime:
            msg = PREPOPULATE_FAILURE_MSG % "bandwidth stats missing from state file"
            log.notice(msg)
            return False

        # fills missing entries with the last value
        bwReadEntries += [bwReadEntries[-1]] * missingReadEntries
        bwWriteEntries += [bwWriteEntries[-1]] * missingWriteEntries

        # crops starting entries so they're the same size
        entryCount = min(len(bwReadEntries), len(bwWriteEntries), self.maxCol)
        bwReadEntries = bwReadEntries[len(bwReadEntries) - entryCount:]
        bwWriteEntries = bwWriteEntries[len(bwWriteEntries) - entryCount:]

        # gets index for 15-minute interval
        intervalIndex = 0
        for indexEntry in graphPanel.UPDATE_INTERVALS:
            if indexEntry[1] == 900: break
            else: intervalIndex += 1

        # fills the graphing parameters with state information
        for i in range(entryCount):
            readVal, writeVal = bwReadEntries[i], bwWriteEntries[i]

            self.lastPrimary, self.lastSecondary = readVal, writeVal

            self.prepopulatePrimaryTotal += readVal * 900
            self.prepopulateSecondaryTotal += writeVal * 900
            self.prepopulateTicks += 900

            self.primaryCounts[intervalIndex].insert(0, readVal)
            self.secondaryCounts[intervalIndex].insert(0, writeVal)

        self.maxPrimary[intervalIndex] = max(self.primaryCounts)
        self.maxSecondary[intervalIndex] = max(self.secondaryCounts)
        del self.primaryCounts[intervalIndex][self.maxCol + 1:]
        del self.secondaryCounts[intervalIndex][self.maxCol + 1:]

        msg = PREPOPULATE_SUCCESS_MSG
        missingSec = time.time() - min(lastReadTime, lastWriteTime)
        if missingSec:
            msg += " (%s is missing)" % str_tools.get_time_label(
                missingSec, 0, True)
        log.notice(msg)

        return True
Beispiel #24
0
def conf_handler(key, value):
  if key == "features.colorOverride" and value != "none":
    try: setColorOverride(value)
    except ValueError, exc:
      log.notice(exc)
Beispiel #25
0
def load_option_descriptions(load_path = None, check_version = True):
  """
  Fetches and parses descriptions for tor's configuration options from its man
  page. This can be a somewhat lengthy call, and raises an IOError if issues
  occure. When successful loading from a file this returns the version for the
  contents loaded.

  If available, this can load the configuration descriptions from a file where
  they were previously persisted to cut down on the load time (latency for this
  is around 200ms).

  Arguments:
    load_path     - if set, this attempts to fetch the configuration
                   descriptions from the given path instead of the man page
    check_version - discards the results if true and tor's version doens't
                   match the cached descriptors, otherwise accepts anyway
  """

  with CONFIG_DESCRIPTIONS_LOCK:
    CONFIG_DESCRIPTIONS.clear()

    raised_exc = None
    loaded_version = ''

    try:
      if load_path:
        # Input file is expected to be of the form:
        # <option>
        # <arg description>
        # <description, possibly multiple lines>
        # <PERSIST_ENTRY_DIVIDER>
        input_file = open(load_path, 'r')
        input_file_contents = input_file.readlines()
        input_file.close()

        try:
          version_line = input_file_contents.pop(0).rstrip()

          if version_line.startswith('Tor Version '):
            file_version = version_line[12:]
            loaded_version = file_version
            tor_version = tor_controller().get_info('version', '')

            if check_version and file_version != tor_version:
              msg = "wrong version, tor is %s but the file's from %s" % (tor_version, file_version)
              raise IOError(msg)
          else:
            raise IOError('unable to parse version')

          while input_file_contents:
            # gets category enum, failing if it doesn't exist
            category = input_file_contents.pop(0).rstrip()

            if category not in Category:
              base_msg = "invalid category in input file: '%s'"
              raise IOError(base_msg % category)

            # gets the position in the man page
            index_arg, index_str = -1, input_file_contents.pop(0).rstrip()

            if index_str.startswith('index: '):
              index_str = index_str[7:]

              if index_str.isdigit():
                index_arg = int(index_str)
              else:
                raise IOError('non-numeric index value: %s' % index_str)
            else:
              raise IOError('malformed index argument: %s' % index_str)

            option = input_file_contents.pop(0).rstrip()
            argument = input_file_contents.pop(0).rstrip()

            description, loaded_line = '', input_file_contents.pop(0)

            while loaded_line != PERSIST_ENTRY_DIVIDER:
              description += loaded_line

              if input_file_contents:
                loaded_line = input_file_contents.pop(0)
              else:
                break

            CONFIG_DESCRIPTIONS[option.lower()] = ManPageEntry(option, index_arg, category, argument, description.rstrip())
        except IndexError:
          CONFIG_DESCRIPTIONS.clear()
          raise IOError('input file format is invalid')
      else:
        man_call_results = system.call('man tor', None)

        if not man_call_results:
          raise IOError('man page not found')

        # Fetches all options available with this tor instance. This isn't
        # vital, and the valid_options are left empty if the call fails.

        controller, valid_options = tor_controller(), []
        config_option_query = controller.get_info('config/names', None)

        if config_option_query:
          for line in config_option_query.strip().split('\n'):
            valid_options.append(line[:line.find(' ')].lower())

        option_count, last_option, last_arg = 0, None, None
        last_category, last_description = Category.GENERAL, ''

        for line in man_call_results:
          line = codecs.latin_1_encode(line, 'replace')[0]
          line = ui_tools.get_printable(line)
          stripped_line = line.strip()

          # we have content, but an indent less than an option (ignore line)
          # if stripped_line and not line.startswith(' ' * MAN_OPT_INDENT): continue

          # line starts with an indent equivilant to a new config option

          is_opt_indent = line.startswith(' ' * MAN_OPT_INDENT) and line[MAN_OPT_INDENT] != ' '

          is_category_line = not line.startswith(' ') and 'OPTIONS' in line

          # if this is a category header or a new option, add an entry using the
          # buffered results

          if is_opt_indent or is_category_line:
            # Filters the line based on if the option is recognized by tor or
            # not. This isn't necessary for nyx, so if unable to make the check
            # then we skip filtering (no loss, the map will just have some extra
            # noise).

            stripped_description = last_description.strip()

            if last_option and (not valid_options or last_option.lower() in valid_options):
              CONFIG_DESCRIPTIONS[last_option.lower()] = ManPageEntry(last_option, option_count, last_category, last_arg, stripped_description)
              option_count += 1

            last_description = ''

            # parses the option and argument

            line = line.strip()
            div_index = line.find(' ')

            if div_index != -1:
              last_option, last_arg = line[:div_index], line[div_index + 1:]

            # if this is a category header then switch it

            if is_category_line:
              if line.startswith('OPTIONS'):
                last_category = Category.GENERAL
              elif line.startswith('CLIENT'):
                last_category = Category.CLIENT
              elif line.startswith('SERVER'):
                last_category = Category.RELAY
              elif line.startswith('DIRECTORY SERVER'):
                last_category = Category.DIRECTORY
              elif line.startswith('DIRECTORY AUTHORITY SERVER'):
                last_category = Category.AUTHORITY
              elif line.startswith('HIDDEN SERVICE'):
                last_category = Category.HIDDEN_SERVICE
              elif line.startswith('TESTING NETWORK'):
                last_category = Category.TESTING
              else:
                log.notice('Unrecognized category in the man page: %s' % line.strip())
          else:
            # Appends the text to the running description. Empty lines and lines
            # starting with a specific indentation are used for formatting, for
            # instance the ExitPolicy and TestingTorNetwork entries.

            if last_description and last_description[-1] != '\n':
              last_description += ' '

            if not stripped_line:
              last_description += '\n\n'
            elif line.startswith(' ' * MAN_EX_INDENT):
              last_description += '    %s\n' % stripped_line
            else:
              last_description += stripped_line
    except IOError as exc:
      raised_exc = exc

  if raised_exc:
    raise raised_exc
  else:
    return loaded_version
Beispiel #26
0
 def msg(self, message):
   """
   Sends a message to our control socket and provides back its reply.
   
   :param str message: message to be formatted and sent to tor
   
   :returns: :class:`stem.response.ControlMessage` with the response
   
   :raises:
     * :class:`stem.socket.ProtocolError` the content from the socket is malformed
     * :class:`stem.socket.SocketError` if a problem arises in using the socket
     * :class:`stem.socket.SocketClosed` if the socket is shut down
   """
   
   with self._msg_lock:
     # If our _reply_queue isn't empty then one of a few things happened...
     #
     # - Our connection was closed and probably re-restablished. This was
     #   in reply to pulling for an asynchronous event and getting this is
     #   expected - ignore it.
     #
     # - Pulling for asynchronous events produced an error. If this was a
     #   ProtocolError then it's a tor bug, and if a non-closure SocketError
     #   then it was probably a socket glitch. Deserves an INFO level log
     #   message.
     #
     # - This is a leftover response for a msg() call. We can't tell who an
     #   exception was airmarked for, so we only know that this was the case
     #   if it's a ControlMessage. This should not be possable and indicates
     #   a stem bug. This deserves a NOTICE level log message since it
     #   indicates that one of our callers didn't get their reply.
     
     while not self._reply_queue.empty():
       try:
         response = self._reply_queue.get_nowait()
         
         if isinstance(response, stem.socket.SocketClosed):
           pass # this is fine
         elif isinstance(response, stem.socket.ProtocolError):
           log.info("Tor provided a malformed message (%s)" % response)
         elif isinstance(response, stem.socket.ControllerError):
           log.info("Socket experienced a problem (%s)" % response)
         elif isinstance(response, stem.response.ControlMessage):
           log.notice("BUG: the msg() function failed to deliver a response: %s" % response)
       except Queue.Empty:
         # the empty() method is documented to not be fully reliable so this
         # isn't entirely surprising
         
         break
     
     try:
       self._socket.send(message)
       response = self._reply_queue.get()
       
       # If the message we received back had an exception then re-raise it to the
       # caller. Otherwise return the response.
       
       if isinstance(response, stem.socket.ControllerError):
         raise response
       else:
         return response
     except stem.socket.SocketClosed, exc:
       # If the recv() thread caused the SocketClosed then we could still be
       # in the process of closing. Calling close() here so that we can
       # provide an assurance to the caller that when we raise a SocketClosed
       # exception we are shut down afterward for realz.
       
       self.close()
       raise exc
Beispiel #27
0
 def run(self):
   while not self._halt:
     minWait = self.resolveRate if self.resolveRate else self.defaultRate
     timeSinceReset = time.time() - self.lastLookup
     
     if self._isPaused or timeSinceReset < minWait:
       sleepTime = max(0.2, minWait - timeSinceReset)
       
       self._cond.acquire()
       if not self._halt: self._cond.wait(sleepTime)
       self._cond.release()
       
       continue # done waiting, try again
     
     isDefault = self.overwriteResolver == None
     resolver = self.defaultResolver if isDefault else self.overwriteResolver
     
     # checks if there's nothing to resolve with
     if not resolver:
       self.lastLookup = time.time() # avoids a busy wait in this case
       continue
     
     try:
       resolveStart = time.time()
       connResults = getConnections(resolver, self.processName, self.processPid)
       lookupTime = time.time() - resolveStart
       
       self._connections = connResults
       self._resolutionCounter += 1
       
       newMinDefaultRate = 100 * lookupTime
       if self.defaultRate < newMinDefaultRate:
         if self._rateThresholdBroken >= 3:
           # adding extra to keep the rate from frequently changing
           self.defaultRate = newMinDefaultRate + 0.5
           
           log.trace("connection lookup time increasing to %0.1f seconds per call" % self.defaultRate)
         else: self._rateThresholdBroken += 1
       else: self._rateThresholdBroken = 0
       
       if isDefault: self._subsiquentFailures = 0
     except (ValueError, IOError), exc:
       # this logs in a couple of cases:
       # - special failures noted by getConnections (most cases are already
       # logged via system)
       # - note fail-overs for default resolution methods
       if str(exc).startswith("No results found using:"):
         log.info(exc)
       
       if isDefault:
         self._subsiquentFailures += 1
         
         if self._subsiquentFailures >= RESOLVER_FAILURE_TOLERANCE:
           # failed several times in a row - abandon resolver and move on to another
           self._resolverBlacklist.append(resolver)
           self._subsiquentFailures = 0
           
           # pick another (non-blacklisted) resolver
           newResolver = None
           for r in self.resolverOptions:
             if not r in self._resolverBlacklist:
               newResolver = r
               break
           
           if newResolver:
             # provide notice that failures have occurred and resolver is changing
             log.notice(RESOLVER_SERIAL_FAILURE_MSG % (resolver, newResolver))
           else:
             # exhausted all resolvers, give warning
             log.notice(RESOLVER_FINAL_FAILURE_MSG)
           
           self.defaultResolver = newResolver
     finally:
Beispiel #28
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 #29
0
def start_nyx(stdscr):
  """
  Main draw loop context.

  Arguments:
    stdscr    - curses window
  """

  init_controller(stdscr, CONFIG['start_time'])
  control = get_controller()

  if not CONFIG['features.acsSupport']:
    ui_tools.disable_acs()

  # provides notice about any unused config keys

  for key in conf.get_config('nyx').unused_keys():
    log.notice('Unused configuration entry: %s' % key)

  # tells daemon panels to start

  for panel_impl in control.get_daemon_panels():
    panel_impl.start()

  # allows for background transparency

  try:
    curses.use_default_colors()
  except curses.error:
    pass

  # makes the cursor invisible

  try:
    curses.curs_set(0)
  except curses.error:
    pass

  # logs the initialization time

  log.info('nyx started (initialization took %0.3f seconds)' % (time.time() - CONFIG['start_time']))

  # main draw loop

  override_key = None      # uses this rather than waiting on user input
  is_unresponsive = False  # flag for heartbeat responsiveness check

  while not control.quit_signal:
    display_panels = control.get_display_panels()
    is_unresponsive = heartbeat_check(is_unresponsive)

    # sets panel visability

    for panel_impl in control.get_all_panels():
      panel_impl.set_visible(panel_impl in display_panels)

    # redraws the interface if it's needed

    control.redraw(False)
    stdscr.refresh()

    # wait for user keyboard input until timeout, unless an override was set

    if override_key:
      key, override_key = override_key, None
    else:
      curses.halfdelay(CONFIG['features.redrawRate'] * 10)
      key = panel.KeyInput(stdscr.getch())

    if key.match('right'):
      control.next_page()
    elif key.match('left'):
      control.prev_page()
    elif key.match('p'):
      control.set_paused(not control.is_paused())
    elif key.match('m'):
      nyx.menu.menu.show_menu()
    elif key.match('q'):
      # provides prompt to confirm that nyx should exit

      if CONFIG['features.confirmQuit']:
        msg = 'Are you sure (q again to confirm)?'
        confirmation_key = nyx.popups.show_msg(msg, attr = curses.A_BOLD)
        quit_confirmed = confirmation_key.match('q')
      else:
        quit_confirmed = True

      if quit_confirmed:
        break
    elif key.match('x'):
      # provides prompt to confirm that nyx should issue a sighup

      msg = "This will reset Tor's internal state. Are you sure (x again to confirm)?"
      confirmation_key = nyx.popups.show_msg(msg, attr = curses.A_BOLD)

      if confirmation_key in (ord('x'), ord('X')):
        try:
          tor_controller().signal(stem.Signal.RELOAD)
        except IOError as exc:
          log.error('Error detected when reloading tor: %s' % exc.strerror)
    elif key.match('h'):
      override_key = nyx.popups.show_help_popup()
    elif key == ord('l') - 96:
      # force redraw when ctrl+l is pressed
      control.redraw(True)
    else:
      for panel_impl in display_panels:
        is_keystroke_consumed = panel_impl.handle_key(key)

        if is_keystroke_consumed:
          break
Beispiel #30
0
    def reset_listener(self, controller, event_type, _):
        self._update()

        if event_type == stem.control.State.CLOSED:
            log.notice('Tor control port closed')
Beispiel #31
0
def init_controller(stdscr, start_time):
  """
  Spawns the controller, and related panels for it.

  Arguments:
    stdscr - curses window
  """

  global NYX_CONTROLLER

  # initializes the panels

  sticky_panels = [
    nyx.header_panel.HeaderPanel(stdscr, start_time),
    LabelPanel(stdscr),
  ]

  page_panels, first_page_panels = [], []

  # first page: graph and log
  if CONFIG['features.panels.show.graph']:
    first_page_panels.append(nyx.graph_panel.GraphPanel(stdscr))

  if CONFIG['features.panels.show.log']:
    expanded_events = nyx.arguments.expand_events(CONFIG['startup.events'])
    first_page_panels.append(nyx.log_panel.LogPanel(stdscr, expanded_events))

  if first_page_panels:
    page_panels.append(first_page_panels)

  # second page: connections
  if CONFIG['features.panels.show.connection']:
    page_panels.append([nyx.connection_panel.ConnectionPanel(stdscr)])

    # 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.

    controller = tor_controller()

    if controller.get_conf('DisableDebuggerAttachment', None) == '1':
      log.notice("Tor is preventing system utilities like netstat and lsof from working. This means that nyx 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")
      nyx.util.tracker.get_connection_tracker().set_paused(True)
    else:
      # Configures connection resoultions. This is paused/unpaused according to
      # if Tor's connected or not.

      controller.add_status_listener(conn_reset_listener)

      tor_pid = controller.get_pid(None)

      if tor_pid:
        # use the tor pid to help narrow connection results
        tor_cmd = system.name_by_pid(tor_pid)

        if tor_cmd is None:
          tor_cmd = 'tor'

        resolver = nyx.util.tracker.get_connection_tracker()
        log.info('Operating System: %s, Connection Resolvers: %s' % (os.uname()[0], ', '.join(resolver._resolvers)))
      else:
        # constructs singleton resolver and, if tor isn't connected, initizes
        # it to be paused

        nyx.util.tracker.get_connection_tracker().set_paused(not controller.is_alive())

  # third page: config

  if CONFIG['features.panels.show.config']:
    page_panels.append([nyx.config_panel.ConfigPanel(stdscr, nyx.config_panel.State.TOR)])

  # fourth page: torrc

  if CONFIG['features.panels.show.torrc']:
    page_panels.append([nyx.torrc_panel.TorrcPanel(stdscr, nyx.torrc_panel.Config.TORRC)])

  # initializes the controller

  NYX_CONTROLLER = Controller(stdscr, sticky_panels, page_panels)
Beispiel #32
0
def loadOptionDescriptions(loadPath=None, checkVersion=True):
    """
  Fetches and parses descriptions for tor's configuration options from its man
  page. This can be a somewhat lengthy call, and raises an IOError if issues
  occure. When successful loading from a file this returns the version for the
  contents loaded.
  
  If available, this can load the configuration descriptions from a file where
  they were previously persisted to cut down on the load time (latency for this
  is around 200ms).
  
  Arguments:
    loadPath     - if set, this attempts to fetch the configuration
                   descriptions from the given path instead of the man page
    checkVersion - discards the results if true and tor's version doens't
                   match the cached descriptors, otherwise accepts anyway
  """

    CONFIG_DESCRIPTIONS_LOCK.acquire()
    CONFIG_DESCRIPTIONS.clear()

    raisedExc = None
    loadedVersion = ""
    try:
        if loadPath:
            # Input file is expected to be of the form:
            # <option>
            # <arg description>
            # <description, possibly multiple lines>
            # <PERSIST_ENTRY_DIVIDER>
            inputFile = open(loadPath, "r")
            inputFileContents = inputFile.readlines()
            inputFile.close()

            try:
                versionLine = inputFileContents.pop(0).rstrip()

                if versionLine.startswith("Tor Version "):
                    fileVersion = versionLine[12:]
                    loadedVersion = fileVersion
                    torVersion = torTools.getConn().getInfo("version", "")

                    if checkVersion and fileVersion != torVersion:
                        msg = "wrong version, tor is %s but the file's from %s" % (
                            torVersion, fileVersion)
                        raise IOError(msg)
                else:
                    raise IOError("unable to parse version")

                while inputFileContents:
                    # gets category enum, failing if it doesn't exist
                    category = inputFileContents.pop(0).rstrip()
                    if not category in Category:
                        baseMsg = "invalid category in input file: '%s'"
                        raise IOError(baseMsg % category)

                    # gets the position in the man page
                    indexArg, indexStr = -1, inputFileContents.pop(0).rstrip()

                    if indexStr.startswith("index: "):
                        indexStr = indexStr[7:]

                        if indexStr.isdigit(): indexArg = int(indexStr)
                        else:
                            raise IOError("non-numeric index value: %s" %
                                          indexStr)
                    else:
                        raise IOError("malformed index argument: %s" %
                                      indexStr)

                    option = inputFileContents.pop(0).rstrip()
                    argument = inputFileContents.pop(0).rstrip()

                    description, loadedLine = "", inputFileContents.pop(0)
                    while loadedLine != PERSIST_ENTRY_DIVIDER:
                        description += loadedLine

                        if inputFileContents:
                            loadedLine = inputFileContents.pop(0)
                        else:
                            break

                    CONFIG_DESCRIPTIONS[option.lower()] = ManPageEntry(
                        option, indexArg, category, argument,
                        description.rstrip())
            except IndexError:
                CONFIG_DESCRIPTIONS.clear()
                raise IOError("input file format is invalid")
        else:
            manCallResults = system.call("man tor")

            if not manCallResults:
                raise IOError("man page not found")

            # Fetches all options available with this tor instance. This isn't
            # vital, and the validOptions are left empty if the call fails.
            conn, validOptions = torTools.getConn(), []
            configOptionQuery = conn.getInfo("config/names", None)
            if configOptionQuery:
                for line in configOptionQuery.strip().split("\n"):
                    validOptions.append(line[:line.find(" ")].lower())

            optionCount, lastOption, lastArg = 0, None, None
            lastCategory, lastDescription = Category.GENERAL, ""
            for line in manCallResults:
                line = uiTools.getPrintable(line)
                strippedLine = line.strip()

                # we have content, but an indent less than an option (ignore line)
                #if strippedLine and not line.startswith(" " * MAN_OPT_INDENT): continue

                # line starts with an indent equivilant to a new config option
                isOptIndent = line.startswith(
                    " " * MAN_OPT_INDENT) and line[MAN_OPT_INDENT] != " "

                isCategoryLine = not line.startswith(" ") and "OPTIONS" in line

                # if this is a category header or a new option, add an entry using the
                # buffered results
                if isOptIndent or isCategoryLine:
                    # Filters the line based on if the option is recognized by tor or
                    # not. This isn't necessary for arm, so if unable to make the check
                    # then we skip filtering (no loss, the map will just have some extra
                    # noise).
                    strippedDescription = lastDescription.strip()
                    if lastOption and (not validOptions
                                       or lastOption.lower() in validOptions):
                        CONFIG_DESCRIPTIONS[lastOption.lower()] = ManPageEntry(
                            lastOption, optionCount, lastCategory, lastArg,
                            strippedDescription)
                        optionCount += 1
                    lastDescription = ""

                    # parses the option and argument
                    line = line.strip()
                    divIndex = line.find(" ")
                    if divIndex != -1:
                        lastOption, lastArg = line[:divIndex], line[divIndex +
                                                                    1:]

                    # if this is a category header then switch it
                    if isCategoryLine:
                        if line.startswith("OPTIONS"):
                            lastCategory = Category.GENERAL
                        elif line.startswith("CLIENT"):
                            lastCategory = Category.CLIENT
                        elif line.startswith("SERVER"):
                            lastCategory = Category.RELAY
                        elif line.startswith("DIRECTORY SERVER"):
                            lastCategory = Category.DIRECTORY
                        elif line.startswith("DIRECTORY AUTHORITY SERVER"):
                            lastCategory = Category.AUTHORITY
                        elif line.startswith("HIDDEN SERVICE"):
                            lastCategory = Category.HIDDEN_SERVICE
                        elif line.startswith("TESTING NETWORK"):
                            lastCategory = Category.TESTING
                        else:
                            log.notice(
                                "Unrecognized category in the man page: %s" %
                                line.strip())
                else:
                    # Appends the text to the running description. Empty lines and lines
                    # starting with a specific indentation are used for formatting, for
                    # instance the ExitPolicy and TestingTorNetwork entries.
                    if lastDescription and lastDescription[-1] != "\n":
                        lastDescription += " "

                    if not strippedLine:
                        lastDescription += "\n\n"
                    elif line.startswith(" " * MAN_EX_INDENT):
                        lastDescription += "    %s\n" % strippedLine
                    else:
                        lastDescription += strippedLine
    except IOError, exc:
        raisedExc = exc
Beispiel #33
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 #34
0
 def prepopulateFromState(self):
   """
   Attempts to use tor's state file to prepopulate values for the 15 minute
   interval via the BWHistoryReadValues/BWHistoryWriteValues values. This
   returns True if successful and False otherwise.
   """
   
   # checks that this is a relay (if ORPort is unset, then skip)
   conn = torTools.getConn()
   orPort = conn.getOption("ORPort", None)
   if orPort == "0": return
   
   # gets the uptime (using the same parameters as the header panel to take
   # advantage of caching)
   # TODO: stem dropped system caching support so we'll need to think of
   # something else
   uptime = None
   queryPid = conn.getMyPid()
   if queryPid:
     queryParam = ["%cpu", "rss", "%mem", "etime"]
     queryCmd = "ps -p %s -o %s" % (queryPid, ",".join(queryParam))
     psCall = system.call(queryCmd, None)
     
     if psCall and len(psCall) == 2:
       stats = psCall[1].strip().split()
       if len(stats) == 4: uptime = stats[3]
   
   # checks if tor has been running for at least a day, the reason being that
   # the state tracks a day's worth of data and this should only prepopulate
   # results associated with this tor instance
   if not uptime or not "-" in uptime:
     msg = PREPOPULATE_FAILURE_MSG % "insufficient uptime"
     log.notice(msg)
     return False
   
   # get the user's data directory (usually '~/.tor')
   dataDir = conn.getOption("DataDirectory", None)
   if not dataDir:
     msg = PREPOPULATE_FAILURE_MSG % "data directory not found"
     log.notice(msg)
     return False
   
   # attempt to open the state file
   try: stateFile = open("%s%s/state" % (conn.getPathPrefix(), dataDir), "r")
   except IOError:
     msg = PREPOPULATE_FAILURE_MSG % "unable to read the state file"
     log.notice(msg)
     return False
   
   # get the BWHistory entries (ordered oldest to newest) and number of
   # intervals since last recorded
   bwReadEntries, bwWriteEntries = None, None
   missingReadEntries, missingWriteEntries = None, None
   
   # converts from gmt to local with respect to DST
   tz_offset = time.altzone if time.localtime()[8] else time.timezone
   
   for line in stateFile:
     line = line.strip()
     
     # According to the rep_hist_update_state() function the BWHistory*Ends
     # correspond to the start of the following sampling period. Also, the
     # most recent values of BWHistory*Values appear to be an incremental
     # counter for the current sampling period. Hence, offsets are added to
     # account for both.
     
     if line.startswith("BWHistoryReadValues"):
       bwReadEntries = line[20:].split(",")
       bwReadEntries = [int(entry) / 1024.0 / 900 for entry in bwReadEntries]
       bwReadEntries.pop()
     elif line.startswith("BWHistoryWriteValues"):
       bwWriteEntries = line[21:].split(",")
       bwWriteEntries = [int(entry) / 1024.0 / 900 for entry in bwWriteEntries]
       bwWriteEntries.pop()
     elif line.startswith("BWHistoryReadEnds"):
       lastReadTime = time.mktime(time.strptime(line[18:], "%Y-%m-%d %H:%M:%S")) - tz_offset
       lastReadTime -= 900
       missingReadEntries = int((time.time() - lastReadTime) / 900)
     elif line.startswith("BWHistoryWriteEnds"):
       lastWriteTime = time.mktime(time.strptime(line[19:], "%Y-%m-%d %H:%M:%S")) - tz_offset
       lastWriteTime -= 900
       missingWriteEntries = int((time.time() - lastWriteTime) / 900)
   
   if not bwReadEntries or not bwWriteEntries or not lastReadTime or not lastWriteTime:
     msg = PREPOPULATE_FAILURE_MSG % "bandwidth stats missing from state file"
     log.notice(msg)
     return False
   
   # fills missing entries with the last value
   bwReadEntries += [bwReadEntries[-1]] * missingReadEntries
   bwWriteEntries += [bwWriteEntries[-1]] * missingWriteEntries
   
   # crops starting entries so they're the same size
   entryCount = min(len(bwReadEntries), len(bwWriteEntries), self.maxCol)
   bwReadEntries = bwReadEntries[len(bwReadEntries) - entryCount:]
   bwWriteEntries = bwWriteEntries[len(bwWriteEntries) - entryCount:]
   
   # gets index for 15-minute interval
   intervalIndex = 0
   for indexEntry in graphPanel.UPDATE_INTERVALS:
     if indexEntry[1] == 900: break
     else: intervalIndex += 1
   
   # fills the graphing parameters with state information
   for i in range(entryCount):
     readVal, writeVal = bwReadEntries[i], bwWriteEntries[i]
     
     self.lastPrimary, self.lastSecondary = readVal, writeVal
     
     self.prepopulatePrimaryTotal += readVal * 900
     self.prepopulateSecondaryTotal += writeVal * 900
     self.prepopulateTicks += 900
     
     self.primaryCounts[intervalIndex].insert(0, readVal)
     self.secondaryCounts[intervalIndex].insert(0, writeVal)
   
   self.maxPrimary[intervalIndex] = max(self.primaryCounts)
   self.maxSecondary[intervalIndex] = max(self.secondaryCounts)
   del self.primaryCounts[intervalIndex][self.maxCol + 1:]
   del self.secondaryCounts[intervalIndex][self.maxCol + 1:]
   
   msg = PREPOPULATE_SUCCESS_MSG
   missingSec = time.time() - min(lastReadTime, lastWriteTime)
   if missingSec: msg += " (%s is missing)" % str_tools.get_time_label(missingSec, 0, True)
   log.notice(msg)
   
   return True
Beispiel #35
0
def loadOptionDescriptions(loadPath = None, checkVersion = True):
  """
  Fetches and parses descriptions for tor's configuration options from its man
  page. This can be a somewhat lengthy call, and raises an IOError if issues
  occure. When successful loading from a file this returns the version for the
  contents loaded.
  
  If available, this can load the configuration descriptions from a file where
  they were previously persisted to cut down on the load time (latency for this
  is around 200ms).
  
  Arguments:
    loadPath     - if set, this attempts to fetch the configuration
                   descriptions from the given path instead of the man page
    checkVersion - discards the results if true and tor's version doens't
                   match the cached descriptors, otherwise accepts anyway
  """
  
  CONFIG_DESCRIPTIONS_LOCK.acquire()
  CONFIG_DESCRIPTIONS.clear()
  
  raisedExc = None
  loadedVersion = ""
  try:
    if loadPath:
      # Input file is expected to be of the form:
      # <option>
      # <arg description>
      # <description, possibly multiple lines>
      # <PERSIST_ENTRY_DIVIDER>
      inputFile = open(loadPath, "r")
      inputFileContents = inputFile.readlines()
      inputFile.close()
      
      try:
        versionLine = inputFileContents.pop(0).rstrip()
        
        if versionLine.startswith("Tor Version "):
          fileVersion = versionLine[12:]
          loadedVersion = fileVersion
          torVersion = torTools.getConn().getInfo("version", "")
          
          if checkVersion and fileVersion != torVersion:
            msg = "wrong version, tor is %s but the file's from %s" % (torVersion, fileVersion)
            raise IOError(msg)
        else:
          raise IOError("unable to parse version")
        
        while inputFileContents:
          # gets category enum, failing if it doesn't exist
          category = inputFileContents.pop(0).rstrip()
          if not category in Category:
            baseMsg = "invalid category in input file: '%s'"
            raise IOError(baseMsg % category)
          
          # gets the position in the man page
          indexArg, indexStr = -1, inputFileContents.pop(0).rstrip()
          
          if indexStr.startswith("index: "):
            indexStr = indexStr[7:]
            
            if indexStr.isdigit(): indexArg = int(indexStr)
            else: raise IOError("non-numeric index value: %s" % indexStr)
          else: raise IOError("malformed index argument: %s"% indexStr)
          
          option = inputFileContents.pop(0).rstrip()
          argument = inputFileContents.pop(0).rstrip()
          
          description, loadedLine = "", inputFileContents.pop(0)
          while loadedLine != PERSIST_ENTRY_DIVIDER:
            description += loadedLine
            
            if inputFileContents: loadedLine = inputFileContents.pop(0)
            else: break
          
          CONFIG_DESCRIPTIONS[option.lower()] = ManPageEntry(option, indexArg, category, argument, description.rstrip())
      except IndexError:
        CONFIG_DESCRIPTIONS.clear()
        raise IOError("input file format is invalid")
    else:
      manCallResults = system.call("man tor")
      
      if not manCallResults:
        raise IOError("man page not found")
      
      # Fetches all options available with this tor instance. This isn't
      # vital, and the validOptions are left empty if the call fails.
      conn, validOptions = torTools.getConn(), []
      configOptionQuery = conn.getInfo("config/names", None)
      if configOptionQuery:
        for line in configOptionQuery.strip().split("\n"):
          validOptions.append(line[:line.find(" ")].lower())
      
      optionCount, lastOption, lastArg = 0, None, None
      lastCategory, lastDescription = Category.GENERAL, ""
      for line in manCallResults:
        line = uiTools.getPrintable(line)
        strippedLine = line.strip()
        
        # we have content, but an indent less than an option (ignore line)
        #if strippedLine and not line.startswith(" " * MAN_OPT_INDENT): continue
        
        # line starts with an indent equivilant to a new config option
        isOptIndent = line.startswith(" " * MAN_OPT_INDENT) and line[MAN_OPT_INDENT] != " "
        
        isCategoryLine = not line.startswith(" ") and "OPTIONS" in line
        
        # if this is a category header or a new option, add an entry using the
        # buffered results
        if isOptIndent or isCategoryLine:
          # Filters the line based on if the option is recognized by tor or
          # not. This isn't necessary for arm, so if unable to make the check
          # then we skip filtering (no loss, the map will just have some extra
          # noise).
          strippedDescription = lastDescription.strip()
          if lastOption and (not validOptions or lastOption.lower() in validOptions):
            CONFIG_DESCRIPTIONS[lastOption.lower()] = ManPageEntry(lastOption, optionCount, lastCategory, lastArg, strippedDescription)
            optionCount += 1
          lastDescription = ""
          
          # parses the option and argument
          line = line.strip()
          divIndex = line.find(" ")
          if divIndex != -1:
            lastOption, lastArg = line[:divIndex], line[divIndex + 1:]
          
          # if this is a category header then switch it
          if isCategoryLine:
            if line.startswith("OPTIONS"): lastCategory = Category.GENERAL
            elif line.startswith("CLIENT"): lastCategory = Category.CLIENT
            elif line.startswith("SERVER"): lastCategory = Category.RELAY
            elif line.startswith("DIRECTORY SERVER"): lastCategory = Category.DIRECTORY
            elif line.startswith("DIRECTORY AUTHORITY SERVER"): lastCategory = Category.AUTHORITY
            elif line.startswith("HIDDEN SERVICE"): lastCategory = Category.HIDDEN_SERVICE
            elif line.startswith("TESTING NETWORK"): lastCategory = Category.TESTING
            else:
              log.notice("Unrecognized category in the man page: %s" % line.strip())
        else:
          # Appends the text to the running description. Empty lines and lines
          # starting with a specific indentation are used for formatting, for
          # instance the ExitPolicy and TestingTorNetwork entries.
          if lastDescription and lastDescription[-1] != "\n":
            lastDescription += " "
          
          if not strippedLine:
            lastDescription += "\n\n"
          elif line.startswith(" " * MAN_EX_INDENT):
            lastDescription += "    %s\n" % strippedLine
          else: lastDescription += strippedLine
  except IOError, exc:
    raisedExc = exc
Beispiel #36
0
class HeaderPanel(panel.Panel, threading.Thread):
    """
  Top area contenting tor settings and system information. Stats are stored in
  the vals mapping, keys including:
    tor/  version, versionStatus, nickname, orPort, dirPort, controlPort,
          socketPath, exitPolicy, isAuthPassword (bool), isAuthCookie (bool),
          orListenAddr, *address, *fingerprint, *flags, pid, startTime,
          *fdUsed, fdLimit, isFdLimitEstimate
    sys/  hostname, os, version
    stat/ *%torCpu, *%armCpu, *rss, *%mem
  
  * volatile parameter that'll be reset on each update
  """
    def __init__(self, stdscr, startTime):
        panel.Panel.__init__(self, stdscr, "header", 0)
        threading.Thread.__init__(self)
        self.setDaemon(True)

        self._isTorConnected = torTools.getConn().isAlive()
        self._lastUpdate = -1  # time the content was last revised
        self._halt = False  # terminates thread if true
        self._cond = threading.Condition()  # used for pausing the thread

        # Time when the panel was paused or tor was stopped. This is used to
        # freeze the uptime statistic (uptime increments normally when None).
        self._haltTime = None

        # The last arm cpu usage sampling taken. This is a tuple of the form:
        # (total arm cpu time, sampling timestamp)
        #
        # The initial cpu total should be zero. However, at startup the cpu time
        # in practice is often greater than the real time causing the initially
        # reported cpu usage to be over 100% (which shouldn't be possible on
        # single core systems).
        #
        # Setting the initial cpu total to the value at this panel's init tends to
        # give smoother results (staying in the same ballpark as the second
        # sampling) so fudging the numbers this way for now.

        self._armCpuSampling = (sum(os.times()[:3]), startTime)

        # Last sampling received from the ResourceTracker, used to detect when it
        # changes.
        self._lastResourceFetch = -1

        # flag to indicate if we've already given file descriptor warnings
        self._isFdSixtyPercentWarned = False
        self._isFdNinetyPercentWarned = False

        self.vals = {}
        self.valsLock = threading.RLock()
        self._update(True)

        # listens for tor reload (sighup) events
        torTools.getConn().addStatusListener(self.resetListener)

    def getHeight(self):
        """
    Provides the height of the content, which is dynamically determined by the
    panel's maximum width.
    """

        isWide = self.getParent().getmaxyx()[1] >= MIN_DUAL_COL_WIDTH
        if self.vals["tor/orPort"]: return 4 if isWide else 6
        else: return 3 if isWide else 4

    def sendNewnym(self):
        """
    Requests a new identity and provides a visual queue.
    """

        torTools.getConn().sendNewnym()

        # If we're wide then the newnym label in this panel will give an
        # indication that the signal was sent. Otherwise use a msg.
        isWide = self.getParent().getmaxyx()[1] >= MIN_DUAL_COL_WIDTH
        if not isWide: cli.popups.showMsg("Requesting a new identity", 1)

    def handleKey(self, key):
        isKeystrokeConsumed = True

        if key in (ord('n'),
                   ord('N')) and torTools.getConn().isNewnymAvailable():
            self.sendNewnym()
        elif key in (ord('r'), ord('R')) and not self._isTorConnected:
            controller = None
            allowPortConnection, allowSocketConnection, _ = starter.allowConnectionTypes(
            )

            if os.path.exists(CONFIG["startup.interface.socket"]
                              ) and allowSocketConnection:
                try:
                    # TODO: um... what about passwords?
                    controller = Controller.from_socket_file(
                        CONFIG["startup.interface.socket"])
                    controller.authenticate()
                except (IOError, stem.SocketError), exc:
                    controller = None

                    if not allowPortConnection:
                        cli.popups.showMsg("Unable to reconnect (%s)" % exc, 3)
            elif not allowPortConnection:
                cli.popups.showMsg(
                    "Unable to reconnect (socket '%s' doesn't exist)" %
                    CONFIG["startup.interface.socket"], 3)

            if not controller and allowPortConnection:
                # TODO: This has diverged from starter.py's connection, for instance it
                # doesn't account for relative cookie paths or multiple authentication
                # methods. We can't use the starter.py's connection function directly
                # due to password prompts, but we could certainly make this mess more
                # manageable.

                try:
                    ctlAddr, ctlPort = CONFIG[
                        "startup.interface.ipAddress"], CONFIG[
                            "startup.interface.port"]
                    controller = Controller.from_port(ctlAddr, ctlPort)

                    try:
                        controller.authenticate()
                    except stem.connection.MissingPassword:
                        controller.authenticate(
                            authValue)  # already got the password above
                except Exception, exc:
                    controller = None

            if controller:
                torTools.getConn().init(controller)
                log.notice("Reconnected to Tor's control port")
                cli.popups.showMsg("Tor reconnected", 1)
Beispiel #37
0
def load_configuration_descriptions(path_prefix):
  """
  Attempts to load descriptions for tor's configuration options, fetching them
  from the man page and persisting them to a file to speed future startups.
  """

  # It is important that this is loaded before entering the curses context,
  # otherwise the man call pegs the cpu for around a minute (I'm not sure
  # why... curses must mess the terminal in a way that's important to man).

  if CONFIG['features.config.descriptions.enabled']:
    is_config_descriptions_loaded = False

    # determines the path where cached descriptions should be persisted (left
    # undefined if caching is disabled)

    descriptor_path = None

    if CONFIG['features.config.descriptions.persist']:
      data_dir = CONFIG['startup.data_directory']

      if not data_dir.endswith('/'):
        data_dir += '/'

      descriptor_path = os.path.expanduser(data_dir + 'cache/') + CONFIG_DESC_FILENAME

    # attempts to load configuration descriptions cached in the data directory

    if descriptor_path:
      try:
        load_start_time = time.time()
        load_option_descriptions(descriptor_path)
        is_config_descriptions_loaded = True

        log.info(DESC_LOAD_SUCCESS_MSG % (descriptor_path, time.time() - load_start_time))
      except IOError as exc:
        log.info(DESC_LOAD_FAILED_MSG % exc.strerror)

    # fetches configuration options from the man page

    if not is_config_descriptions_loaded:
      try:
        load_start_time = time.time()
        load_option_descriptions()
        is_config_descriptions_loaded = True

        log.info(DESC_READ_MAN_SUCCESS_MSG % (time.time() - load_start_time))
      except IOError as exc:
        log.notice(DESC_READ_MAN_FAILED_MSG % exc.strerror)

      # persists configuration descriptions

      if is_config_descriptions_loaded and descriptor_path:
        try:
          load_start_time = time.time()
          save_option_descriptions(descriptor_path)
          log.info(DESC_SAVE_SUCCESS_MSG % (descriptor_path, time.time() - load_start_time))
        except IOError as exc:
          log.notice(DESC_SAVE_FAILED_MSG % exc.strerror)
        except OSError as exc:
          log.notice(DESC_SAVE_FAILED_MSG % exc)

    # finally fall back to the cached descriptors provided with nyx (this is
    # often the case for tbb and manual builds)

    if not is_config_descriptions_loaded:
      try:
        load_start_time = time.time()
        loaded_version = load_option_descriptions('%sresources/%s' % (path_prefix, CONFIG_DESC_FILENAME), False)
        is_config_descriptions_loaded = True
        log.notice(DESC_INTERNAL_LOAD_SUCCESS_MSG % loaded_version)
      except IOError as exc:
        log.error(DESC_INTERNAL_LOAD_FAILED_MSG % exc.strerror)