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
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()
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()
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')
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
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)
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()
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
def _load_user_nyxrc(path, config): """ Loads user's personal nyxrc if it's available. """ if os.path.exists(path): try: config.load(path) # If the user provided us with a chroot then validate and normalize the # path. chroot = config.get('tor.chroot', '').strip().rstrip(os.path.sep) if chroot and not os.path.exists(chroot): log.notice('setup.chroot_doesnt_exist', path = chroot) config.set('tor.chroot', '') else: config.set('tor.chroot', chroot) # use the normalized path except IOError as exc: log.warn('config.unable_to_read_file', error = exc.strerror) else: log.notice('config.nothing_loaded', path = path)
def _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()
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()
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)
def reset_listener(self, controller, event_type, _): self._update() if event_type == stem.control.State.CLOSED: log.notice('Tor control port closed')
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()
def startTorMonitor(startTime): """ Initializes the interface and starts the main draw loop. Arguments: startTime - unix time for when arm was started """ # attempts to fetch the tor pid, warning if unsuccessful (this is needed for # checking its resource usage, among other things) conn = torTools.getConn() torPid = conn.getMyPid() if not torPid and conn.isAlive(): log.warn( "Unable to determine Tor's pid. Some information, like its resource usage will be unavailable." ) # adds events needed for arm functionality to the torTools REQ_EVENTS # mapping (they're then included with any setControllerEvents call, and log # a more helpful error if unavailable) torTools.REQ_EVENTS["BW"] = "bandwidth graph won't function" if not CONFIG["startup.blindModeEnabled"]: # The DisableDebuggerAttachment will prevent our connection panel from really # functioning. It'll have circuits, but little else. If this is the case then # notify the user and tell them what they can do to fix it. if conn.getOption("DisableDebuggerAttachment", None) == "1": log.notice( "Tor is preventing system utilities like netstat and lsof from working. This means that arm can't provide you with connection information. You can change this by adding 'DisableDebuggerAttachment 0' to your torrc and restarting tor. For more information see...\nhttps://trac.torproject.org/3313" ) connections.getResolver("tor").setPaused(True) else: torTools.REQ_EVENTS[ "CIRC"] = "may cause issues in identifying client connections" # Configures connection resoultions. This is paused/unpaused according to # if Tor's connected or not. conn.addStatusListener(connResetListener) if torPid: # use the tor pid to help narrow connection results torCmdName = sysTools.getProcessName(torPid, "tor") connections.getResolver(torCmdName, torPid, "tor") else: # constructs singleton resolver and, if tor isn't connected, initizes # it to be paused connections.getResolver("tor").setPaused(not conn.isAlive()) # hack to display a better (arm specific) notice if all resolvers fail connections.RESOLVER_FINAL_FAILURE_MSG = "We were unable to use any of your system's resolvers to get tor's connections. This is fine, but means that the connections page will be empty. This is usually permissions related so if you would like to fix this then run arm with the same user as tor (ie, \"sudo -u <tor user> arm\")." # provides a notice about any event types tor supports but arm doesn't missingEventTypes = cli.logPanel.getMissingEventTypes() if missingEventTypes: pluralLabel = "s" if len(missingEventTypes) > 1 else "" log.info( "arm doesn't recognize the following event type%s: %s (log 'UNKNOWN' events to see them)" % (pluralLabel, ", ".join(missingEventTypes))) try: curses.wrapper(drawTorMonitor, startTime) except KeyboardInterrupt: # Skip printing stack trace in case of keyboard interrupt. The # HALT_ACTIVITY attempts to prevent daemons from triggering a curses redraw # (which would leave the user's terminal in a screwed up state). There is # still a tiny timing issue here (after the exception but before the flag # is set) but I've never seen it happen in practice. panel.HALT_ACTIVITY = True shutdownDaemons()
def _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()
def logValidationIssues(self): """ Performs validation on the loaded contents, and logs warnings for issues that are found. """ corrections = self.getCorrections() if corrections: duplicateOptions, defaultOptions, mismatchLines, missingOptions = [], [], [], [] for lineNum, issue, msg in corrections: if issue == ValidationError.DUPLICATE: duplicateOptions.append("%s (line %i)" % (msg, lineNum + 1)) elif issue == ValidationError.IS_DEFAULT: defaultOptions.append("%s (line %i)" % (msg, lineNum + 1)) elif issue == ValidationError.MISMATCH: mismatchLines.append(lineNum + 1) elif issue == ValidationError.MISSING: missingOptions.append(msg) if duplicateOptions or defaultOptions: msg = "Unneeded torrc entries found. They've been highlighted in blue on the torrc page." if duplicateOptions: if len(duplicateOptions) > 1: msg += "\n- entries ignored due to having duplicates: " else: msg += "\n- entry ignored due to having a duplicate: " duplicateOptions.sort() msg += ", ".join(duplicateOptions) if defaultOptions: if len(defaultOptions) > 1: msg += "\n- entries match their default values: " else: msg += "\n- entry matches its default value: " defaultOptions.sort() msg += ", ".join(defaultOptions) log.notice(msg) if mismatchLines or missingOptions: msg = "The torrc differs from what tor's using. You can issue a sighup to reload the torrc values by pressing x." if mismatchLines: if len(mismatchLines) > 1: msg += "\n- torrc values differ on lines: " else: msg += "\n- torrc value differs on line: " mismatchLines.sort() msg += ", ".join([str(val + 1) for val in mismatchLines]) if missingOptions: if len(missingOptions) > 1: msg += "\n- configuration values are missing from the torrc: " else: msg += "\n- configuration value is missing from the torrc: " missingOptions.sort() msg += ", ".join(missingOptions) log.warn(msg)
def 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()
def conf_handler(key, value): if key == "features.colorOverride" and value != "none": try: setColorOverride(value) except ValueError, exc: log.notice(exc)
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
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
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
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:
def startTorMonitor(startTime): """ Initializes the interface and starts the main draw loop. Arguments: startTime - unix time for when arm was started """ # attempts to fetch the tor pid, warning if unsuccessful (this is needed for # checking its resource usage, among other things) conn = torTools.getConn() torPid = conn.getMyPid() if not torPid and conn.isAlive(): log.warn("Unable to determine Tor's pid. Some information, like its resource usage will be unavailable.") # adds events needed for arm functionality to the torTools REQ_EVENTS # mapping (they're then included with any setControllerEvents call, and log # a more helpful error if unavailable) torTools.REQ_EVENTS["BW"] = "bandwidth graph won't function" if not CONFIG["startup.blindModeEnabled"]: # The DisableDebuggerAttachment will prevent our connection panel from really # functioning. It'll have circuits, but little else. If this is the case then # notify the user and tell them what they can do to fix it. if conn.getOption("DisableDebuggerAttachment", None) == "1": log.notice( "Tor is preventing system utilities like netstat and lsof from working. This means that arm can't provide you with connection information. You can change this by adding 'DisableDebuggerAttachment 0' to your torrc and restarting tor. For more information see...\nhttps://trac.torproject.org/3313" ) connections.getResolver("tor").setPaused(True) else: torTools.REQ_EVENTS["CIRC"] = "may cause issues in identifying client connections" # Configures connection resoultions. This is paused/unpaused according to # if Tor's connected or not. conn.addStatusListener(connResetListener) if torPid: # use the tor pid to help narrow connection results torCmdName = sysTools.getProcessName(torPid, "tor") connections.getResolver(torCmdName, torPid, "tor") else: # constructs singleton resolver and, if tor isn't connected, initizes # it to be paused connections.getResolver("tor").setPaused(not conn.isAlive()) # hack to display a better (arm specific) notice if all resolvers fail connections.RESOLVER_FINAL_FAILURE_MSG = "We were unable to use any of your system's resolvers to get tor's connections. This is fine, but means that the connections page will be empty. This is usually permissions related so if you would like to fix this then run arm with the same user as tor (ie, \"sudo -u <tor user> arm\")." # provides a notice about any event types tor supports but arm doesn't missingEventTypes = cli.logPanel.getMissingEventTypes() if missingEventTypes: pluralLabel = "s" if len(missingEventTypes) > 1 else "" log.info( "arm doesn't recognize the following event type%s: %s (log 'UNKNOWN' events to see them)" % (pluralLabel, ", ".join(missingEventTypes)) ) try: curses.wrapper(drawTorMonitor, startTime) except KeyboardInterrupt: # Skip printing stack trace in case of keyboard interrupt. The # HALT_ACTIVITY attempts to prevent daemons from triggering a curses redraw # (which would leave the user's terminal in a screwed up state). There is # still a tiny timing issue here (after the exception but before the flag # is set) but I've never seen it happen in practice. panel.HALT_ACTIVITY = True shutdownDaemons()
def 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
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)
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
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
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
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)
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)