def __init__(self, clone = None): GraphCategory.__init__(self, clone) self._title_last_updated = None if not clone: # fill in past bandwidth information controller = tor_controller() bw_entries, is_successful = controller.get_info('bw-event-cache', None), True if bw_entries: for entry in bw_entries.split(): entry_comp = entry.split(',') if len(entry_comp) != 2 or not entry_comp[0].isdigit() or not entry_comp[1].isdigit(): log.warn("Tor's 'GETINFO bw-event-cache' provided malformed output: %s" % bw_entries) is_successful = False break self.primary.update(int(entry_comp[0])) self.secondary.update(int(entry_comp[1])) if is_successful: log.info('Bandwidth graph has information for the last %s' % str_tools.time_label(len(bw_entries.split()), is_long = True)) read_total = controller.get_info('traffic/read', None) write_total = controller.get_info('traffic/written', None) start_time = system.start_time(controller.get_pid(None)) if read_total and write_total and start_time: self.primary.total = int(read_total) self.secondary.total = int(write_total) self.start_time = start_time
def getCorrections(self): """ Performs validation on the loaded contents and provides back the corrections. If validation is disabled then this won't provide any results. """ self.valsLock.acquire() if not self.isLoaded(): returnVal = None else: torVersion = torTools.getConn().getVersion() skipValidation = not CONFIG["features.torrc.validate"] skipValidation |= (torVersion is None or not torVersion.meets_requirements(stem.version.Requirement.GETINFO_CONFIG_TEXT)) if skipValidation: log.info("Skipping torrc validation (requires tor 0.2.2.7-alpha)") returnVal = {} else: if self.corrections == None: self.corrections = validate(self.contents) returnVal = list(self.corrections) self.valsLock.release() return returnVal
def getCorrections(self): """ Performs validation on the loaded contents and provides back the corrections. If validation is disabled then this won't provide any results. """ self.valsLock.acquire() if not self.isLoaded(): returnVal = None else: torVersion = torTools.getConn().getVersion() skipValidation = not CONFIG["features.torrc.validate"] skipValidation |= ( torVersion is None or not torVersion.meets_requirements( stem.version.Requirement.GETINFO_CONFIG_TEXT)) if skipValidation: log.info( "Skipping torrc validation (requires tor 0.2.2.7-alpha)") returnVal = {} else: if self.corrections == None: self.corrections = validate(self.contents) returnVal = list(self.corrections) self.valsLock.release() return returnVal
def send_message(control_file, message, raw=False): """ Sends a message to the control socket, adding the expected formatting for single verses multi-line messages. Neither message type should contain an ending newline (if so it'll be treated as a multi-line message with a blank line at the end). If the message doesn't contain a newline then it's sent as... :: <message>\\r\\n and if it does contain newlines then it's split on ``\\n`` and sent as... :: +<line 1>\\r\\n <line 2>\\r\\n <line 3>\\r\\n .\\r\\n :param file control_file: file derived from the control socket (see the socket's makefile() method for more information) :param str message: message to be sent on the control socket :param bool raw: leaves the message formatting untouched, passing it to the socket as-is :raises: * :class:`stem.SocketError` if a problem arises in using the socket * :class:`stem.SocketClosed` if the socket is known to be shut down """ if not raw: message = send_formatting(message) try: control_file.write(stem.util.str_tools._to_bytes(message)) control_file.flush() if log.is_tracing(): log_message = message.replace('\r\n', '\n').rstrip() msg_div = '\n' if '\n' in log_message else ' ' log.trace('Sent to tor:%s%s' % (msg_div, log_message)) except socket.error as exc: log.info('Failed to send message: %s' % exc) # When sending there doesn't seem to be a reliable method for # distinguishing between failures from a disconnect verses other things. # Just accounting for known disconnection responses. if str(exc) == '[Errno 32] Broken pipe': raise stem.SocketClosed(exc) else: raise stem.SocketError(exc) except AttributeError: # if the control_file has been closed then flush will receive: # AttributeError: 'NoneType' object has no attribute 'sendall' log.info('Failed to send message: file has been closed') raise stem.SocketClosed('file has been closed')
def __init__(self, clone = None): GraphCategory.__init__(self, clone) if not clone: # fill in past bandwidth information controller = tor_controller() bw_entries, is_successful = controller.get_info('bw-event-cache', None), True if bw_entries: for entry in bw_entries.split(): entry_comp = entry.split(',') if len(entry_comp) != 2 or not entry_comp[0].isdigit() or not entry_comp[1].isdigit(): log.warn(msg('panel.graphing.bw_event_cache_malformed', response = bw_entries)) is_successful = False break self.primary.update(int(entry_comp[0])) self.secondary.update(int(entry_comp[1])) if is_successful: log.info(msg('panel.graphing.prepopulation_successful', duration = str_tools.time_label(len(bw_entries.split()), is_long = True))) read_total = controller.get_info('traffic/read', None) write_total = controller.get_info('traffic/written', None) start_time = system.start_time(controller.get_pid(None)) if read_total and write_total and start_time: self.primary.total = int(read_total) self.secondary.total = int(write_total) self.start_time = start_time
def send_message(control_file, message, raw = False): """ Sends a message to the control socket, adding the expected formatting for single verses multi-line messages. Neither message type should contain an ending newline (if so it'll be treated as a multi-line message with a blank line at the end). If the message doesn't contain a newline then it's sent as... :: <message>\\r\\n and if it does contain newlines then it's split on ``\\n`` and sent as... :: +<line 1>\\r\\n <line 2>\\r\\n <line 3>\\r\\n .\\r\\n :param file control_file: file derived from the control socket (see the socket's makefile() method for more information) :param str message: message to be sent on the control socket :param bool raw: leaves the message formatting untouched, passing it to the socket as-is :raises: * :class:`stem.SocketError` if a problem arises in using the socket * :class:`stem.SocketClosed` if the socket is known to be shut down """ if not raw: message = send_formatting(message) try: control_file.write(stem.util.str_tools._to_bytes(message)) control_file.flush() log_message = message.replace('\r\n', '\n').rstrip() log.trace('Sent to tor:\n' + log_message) except socket.error as exc: log.info('Failed to send message: %s' % exc) # When sending there doesn't seem to be a reliable method for # distinguishing between failures from a disconnect verses other things. # Just accounting for known disconnection responses. if str(exc) == '[Errno 32] Broken pipe': raise stem.SocketClosed(exc) else: raise stem.SocketError(exc) except AttributeError: # if the control_file has been closed then flush will receive: # AttributeError: 'NoneType' object has no attribute 'sendall' log.info('Failed to send message: file has been closed') raise stem.SocketClosed('file has been closed')
def _workerLoop(self): """ Simple producer-consumer loop followed by worker threads. This takes addresses from the unresolvedQueue, attempts to look up its hostname, and adds its results or the error to the resolved cache. Resolver reference provides shared resources used by the thread pool. """ while not self.halt: # if resolver is paused then put a hold on further resolutions if self.isPaused: self.cond.acquire() if not self.halt: self.cond.wait(1) self.cond.release() continue # snags next available ip, timeout is because queue can't be woken up # when 'halt' is set try: ipAddr = self.unresolvedQueue.get_nowait() except Queue.Empty: # no elements ready, wait a little while and try again self.cond.acquire() if not self.halt: self.cond.wait(1) self.cond.release() continue if self.halt: break try: if self.useSocketResolution: result = _resolveViaSocket(ipAddr) else: result = _resolveViaHost(ipAddr) except IOError, exc: result = exc # lookup failed except ValueError, exc: result = exc # dns error self.resolvedLock.acquire() self.resolvedCache[ipAddr] = (result, RESOLVER_COUNTER.next()) # trim cache if excessively large (clearing out oldest entries) if len(self.resolvedCache) > CONFIG["cache.hostnames.size"]: # Providing for concurrent, non-blocking calls require that entries are # never removed from the cache, so this creates a new, trimmed version # instead. # determines minimum age of entries to be kept currentCount = RESOLVER_COUNTER.next() newCacheSize = CONFIG["cache.hostnames.size"] - CONFIG["cache.hostnames.trimSize"] threshold = currentCount - newCacheSize newCache = {} msg = "trimming hostname cache from %i entries to %i" % (len(self.resolvedCache), newCacheSize) log.info(msg) # checks age of each entry, adding to toDelete if too old for ipAddr, entry in self.resolvedCache.iteritems(): if entry[1] >= threshold: newCache[ipAddr] = entry self.resolvedCache = newCache self.resolvedLock.release()
def _notify_of_unknown_events(): """ Provides a notice about any event types tor supports but nyx doesn't. """ missing_events = nyx.arguments.missing_event_types() if missing_events: log.info('setup.unknown_event_types', event_types = ', '.join(missing_events))
def manual(option): result = stem.manual.query( 'SELECT category, usage, summary, description, position FROM torrc WHERE key=?', option.upper()).fetchone() if result: return ManualEntry(*result) else: log.info("No manual information found for '%s'" % option) return None
def _setup_freebsd_chroot(controller, config): """ If we're running under FreeBSD then check the system for a chroot path. """ if not config.get('tor.chroot', None) and platform.system() == 'FreeBSD': jail_chroot = stem.util.system.bsd_jail_path(controller.get_pid(0)) if jail_chroot and os.path.exists(jail_chroot): log.info('setup.set_freebsd_chroot', path = jail_chroot) config.set('tor.chroot', jail_chroot)
def __init__(self, processName, processPid = "", resolveRate = None, handle = None): """ Initializes a new resolver daemon. When no longer needed it's suggested that this is stopped. Arguments: processName - name of the process being resolved processPid - pid of the process being resolved resolveRate - time between resolving connections (in seconds, None if chosen dynamically) handle - name used to query this resolver, this is the processName if undefined """ threading.Thread.__init__(self) self.setDaemon(True) self.processName = processName self.processPid = processPid self.resolveRate = resolveRate self.handle = handle if handle else processName self.defaultRate = CONFIG["queries.connections.minRate"] self.lastLookup = -1 self.overwriteResolver = None self.defaultResolver = Resolver.PROC osType = os.uname()[0] self.resolverOptions = getSystemResolvers(osType) log.info("Operating System: %s, Connection Resolvers: %s" % (osType, ", ".join(self.resolverOptions))) # sets the default resolver to be the first found in the system's PATH # (left as netstat if none are found) for resolver in self.resolverOptions: # Resolver strings correspond to their command with the exception of bsd # resolvers. resolverCmd = resolver.replace(" (bsd)", "") if resolver == Resolver.PROC or system.is_available(resolverCmd): self.defaultResolver = resolver break self._connections = [] # connection cache (latest results) self._resolutionCounter = 0 # number of successful connection resolutions self._isPaused = False self._halt = False # terminates thread if true self._cond = threading.Condition() # used for pausing the thread self._subsiquentFailures = 0 # number of failed resolutions with the default in a row self._resolverBlacklist = [] # resolvers that have failed to resolve # Number of sequential times the threshold rate's been too low. This is to # avoid having stray spikes up the rate. self._rateThresholdBroken = 0
def send_message(control_file, message, raw=False): """ Sends a message to the control socket, adding the expected formatting for single verses multi-line messages. Neither message type should contain an ending newline (if so it'll be treated as a multi-line message with a blank line at the end). If the message doesn't contain a newline then it's sent as... :: <message>\\r\\n and if it does contain newlines then it's split on ``\\n`` and sent as... :: +<line 1>\\r\\n <line 2>\\r\\n <line 3>\\r\\n .\\r\\n :param file control_file: file derived from the control socket (see the socket's makefile() method for more information) :param str message: message to be sent on the control socket :param bool raw: leaves the message formatting untouched, passing it to the socket as-is :raises: * :class:`stem.SocketError` if a problem arises in using the socket * :class:`stem.SocketClosed` if the socket is known to be shut down """ if not raw: message = send_formatting(message) try: control_file.write(message) control_file.flush() log_message = message.replace("\r\n", "\n").rstrip() log.trace("Sent to tor:\n" + log_message) except socket.error, exc: log.info("Failed to send message: %s" % exc) # When sending there doesn't seem to be a reliable method for # distinguishing between failures from a disconnect verses other things. # Just accounting for known disconnection responses. if str(exc) == "[Errno 32] Broken pipe": raise stem.SocketClosed(exc) else: raise stem.SocketError(exc)
def _initColors(): """ Initializes color mappings usable by curses. This can only be done after calling curses.initscr(). """ global COLOR_ATTR_INITIALIZED, COLOR_IS_SUPPORTED if not COLOR_ATTR_INITIALIZED: # hack to replace all ACS characters with '+' if ACS support has been # manually disabled if not CONFIG["features.acsSupport"]: for item in curses.__dict__: if item.startswith("ACS_"): curses.__dict__[item] = ord('+') # replace a few common border pipes that are better rendered as '|' or # '-' instead curses.ACS_SBSB = ord('|') curses.ACS_VLINE = ord('|') curses.ACS_BSBS = ord('-') curses.ACS_HLINE = ord('-') COLOR_ATTR_INITIALIZED = True COLOR_IS_SUPPORTED = False if not CONFIG["features.colorInterface"]: return try: COLOR_IS_SUPPORTED = curses.has_colors() except curses.error: return # initscr hasn't been called yet # initializes color mappings if color support is available if COLOR_IS_SUPPORTED: colorpair = 0 log.info("Terminal color support detected and enabled") for colorName in COLOR_LIST: fgColor = COLOR_LIST[colorName] bgColor = -1 # allows for default (possibly transparent) background colorpair += 1 curses.init_pair(colorpair, fgColor, bgColor) COLOR_ATTR[colorName] = curses.color_pair(colorpair) else: log.info("Terminal color support unavailable")
def _write_to_socket(socket_file, message): try: socket_file.write(stem.util.str_tools._to_bytes(message)) socket_file.flush() except socket.error as exc: log.info('Failed to send: %s' % exc) # When sending there doesn't seem to be a reliable method for # distinguishing between failures from a disconnect verses other things. # Just accounting for known disconnection responses. if str(exc) == '[Errno 32] Broken pipe': raise stem.SocketClosed(exc) else: raise stem.SocketError(exc) except AttributeError: # if the control_file has been closed then flush will receive: # AttributeError: 'NoneType' object has no attribute 'sendall' log.info('Failed to send: file has been closed') raise stem.SocketClosed('file has been closed')
def __init__(self, clone=None): GraphCategory.__init__(self, clone) if not clone: # fill in past bandwidth information controller = tor_controller() bw_entries, is_successful = controller.get_info( 'bw-event-cache', None), True if bw_entries: for entry in bw_entries.split(): entry_comp = entry.split(',') if len(entry_comp) != 2 or not entry_comp[0].isdigit( ) or not entry_comp[1].isdigit(): log.warn( msg('panel.graphing.bw_event_cache_malformed', response=bw_entries)) is_successful = False break self.primary.update(int(entry_comp[0])) self.secondary.update(int(entry_comp[1])) if is_successful: log.info( msg('panel.graphing.prepopulation_successful', duration=str_tools.time_label(len( bw_entries.split()), is_long=True))) read_total = controller.get_info('traffic/read', None) write_total = controller.get_info('traffic/written', None) start_time = system.start_time(controller.get_pid(None)) if read_total and write_total and start_time: self.primary.total = int(read_total) self.secondary.total = int(write_total) self.start_time = start_time
async def _write_to_socket(writer: asyncio.StreamWriter, message: Union[str, bytes]) -> None: try: writer.write(stem.util.str_tools._to_bytes(message)) await writer.drain() except socket.error as exc: log.info('Failed to send: %s' % exc) # When sending there doesn't seem to be a reliable method for # distinguishing between failures from a disconnect verses other things. # Just accounting for known disconnection responses. if str(exc) == 'Connection lost': raise stem.SocketClosed(exc) else: raise stem.SocketError(exc) except AttributeError: # if the control_file has been closed then flush will receive: # AttributeError: 'NoneType' object has no attribute 'sendall' log.info('Failed to send: file has been closed') raise stem.SocketClosed('file has been closed')
def get_corrections(self): """ Performs validation on the loaded contents and provides back the corrections. If validation is disabled then this won't provide any results. """ with self._vals_lock: if not self.is_loaded(): return None else: tor_version = tor_controller().get_version(None) skip_validation = not CONFIG['features.torrc.validate'] skip_validation |= (tor_version is None or not tor_version >= stem.version.Requirement.GETINFO_CONFIG_TEXT) if skip_validation: log.info('Skipping torrc validation (requires tor 0.2.2.7-alpha)') return {} else: if self.corrections is None: self.corrections = validate(self.contents) return list(self.corrections)
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 recv_message(control_file): """ Pulls from a control socket until we either have a complete message or encounter a problem. :param file control_file: file derived from the control socket (see the socket's makefile() method for more information) :returns: :class:`~stem.response.ControlMessage` read from the socket :raises: * :class:`stem.ProtocolError` the content from the socket is malformed * :class:`stem.SocketClosed` if the socket closes before we receive a complete message """ parsed_content, raw_content = [], b"" logging_prefix = "Error while receiving a control message (%s): " while True: try: # From a real socket readline() would always provide bytes, but during # tests we might be given a StringIO in which case it's unicode under # python 3.x. line = stem.util.str_tools._to_bytes(control_file.readline()) except AttributeError: # if the control_file has been closed then we will receive: # AttributeError: 'NoneType' object has no attribute 'recv' prefix = logging_prefix % "SocketClosed" log.info(prefix + "socket file has been closed") raise stem.SocketClosed("socket file has been closed") except (socket.error, ValueError) as exc: # When disconnected we get... # # Python 2: # socket.error: [Errno 107] Transport endpoint is not connected # # Python 3: # ValueError: I/O operation on closed file. prefix = logging_prefix % "SocketClosed" log.info(prefix + "received exception \"%s\"" % exc) raise stem.SocketClosed(exc) raw_content += line # Parses the tor control lines. These are of the form... # <status code><divider><content>\r\n if len(line) == 0: # if the socket is disconnected then the readline() method will provide # empty content prefix = logging_prefix % "SocketClosed" log.info(prefix + "empty socket content") raise stem.SocketClosed("Received empty socket content.") elif len(line) < 4: prefix = logging_prefix % "ProtocolError" log.info(prefix + "line too short, \"%s\"" % log.escape(line)) raise stem.ProtocolError("Badly formatted reply line: too short") elif not re.match(b'^[a-zA-Z0-9]{3}[-+ ]', line): prefix = logging_prefix % "ProtocolError" log.info(prefix + "malformed status code/divider, \"%s\"" % log.escape(line)) raise stem.ProtocolError( "Badly formatted reply line: beginning is malformed") elif not line.endswith(b"\r\n"): prefix = logging_prefix % "ProtocolError" log.info(prefix + "no CRLF linebreak, \"%s\"" % log.escape(line)) raise stem.ProtocolError("All lines should end with CRLF") line = line[:-2] # strips off the CRLF status_code, divider, content = line[:3], line[3:4], line[4:] if stem.prereq.is_python_3(): status_code = stem.util.str_tools._to_unicode(status_code) divider = stem.util.str_tools._to_unicode(divider) if divider == "-": # mid-reply line, keep pulling for more content parsed_content.append((status_code, divider, content)) elif divider == " ": # end of the message, return the message parsed_content.append((status_code, divider, content)) log_message = raw_content.replace(b"\r\n", b"\n").rstrip() log.trace("Received from tor:\n" + stem.util.str_tools._to_unicode(log_message)) return stem.response.ControlMessage(parsed_content, raw_content) elif divider == "+": # data entry, all of the following lines belong to the content until we # get a line with just a period while True: try: line = stem.util.str_tools._to_bytes( control_file.readline()) except socket.error as exc: prefix = logging_prefix % "SocketClosed" log.info( prefix + "received an exception while mid-way through a data reply (exception: \"%s\", read content: \"%s\")" % (exc, log.escape(raw_content))) raise stem.SocketClosed(exc) raw_content += line if not line.endswith(b"\r\n"): prefix = logging_prefix % "ProtocolError" log.info( prefix + "CRLF linebreaks missing from a data reply, \"%s\"" % log.escape(raw_content)) raise stem.ProtocolError("All lines should end with CRLF") elif line == b".\r\n": break # data block termination line = line[:-2] # strips off the CRLF # lines starting with a period are escaped by a second period (as per # section 2.4 of the control-spec) if line.startswith(b".."): line = line[1:] # appends to previous content, using a newline rather than CRLF # separator (more conventional for multi-line string content outside # the windows world) content += b"\n" + line parsed_content.append((status_code, divider, content)) else: # this should never be reached due to the prefix regex, but might as well # be safe... prefix = logging_prefix % "ProtocolError" log.warn(prefix + "\"%s\" isn't a recognized divider type" % divider) raise stem.ProtocolError( "Unrecognized divider type '%s': %s" % (divider, stem.util.str_tools._to_unicode(line)))
return # success! except OpenAuthRejected, exc: auth_exceptions.append(exc) except IncorrectPassword, exc: auth_exceptions.append(exc) except PasswordAuthRejected, exc: # Since the PROTOCOLINFO says password auth is available we can assume # that if PasswordAuthRejected is raised it's being raised in error. log.debug( "The authenticate_password method raised a PasswordAuthRejected when password auth should be available. Stem may need to be corrected to recognize this response: %s" % exc) auth_exceptions.append(IncorrectPassword(str(exc))) except AuthSecurityFailure, exc: log.info( "Tor failed to provide the nonce expected for safecookie authentication. (%s)" % exc) auth_exceptions.append(exc) except (InvalidClientNonce, UnrecognizedAuthChallengeMethod, AuthChallengeFailed), exc: auth_exceptions.append(exc) except (IncorrectCookieSize, UnreadableCookieFile, IncorrectCookieValue), exc: auth_exceptions.append(exc) except CookieAuthRejected, exc: auth_func = "authenticate_safecookie" if exc.is_safecookie else "authenticate_cookie" log.debug( "The %s method raised a CookieAuthRejected when cookie auth should be available. Stem may need to be corrected to recognize this response: %s" % (auth_func, exc)) auth_exceptions.append(
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()
class _Resolver(): """ Performs reverse DNS resolutions. Lookups are a network bound operation so this spawns a pool of worker threads to do several at a time in parallel. """ def __init__(self): # IP Address => (hostname/error, age), resolution failures result in a # ValueError with the lookup's status self.resolvedCache = {} self.resolvedLock = threading.RLock( ) # governs concurrent access when modifying resolvedCache self.unresolvedQueue = Queue.Queue() # unprocessed lookup requests self.recentQueries = [ ] # recent resolution requests to prevent duplicate requests self.threadPool = [] # worker threads that process requests self.totalResolves = 0 # counter for the total number of addresses queried to be resolved self.isPaused = False # prevents further resolutions if true self.halt = False # if true, tells workers to stop self.cond = threading.Condition() # used for pausing threads # Determines if resolutions are made using os 'host' calls or python's # 'socket.gethostbyaddr'. The following checks if the system has the # gethostbyname_r function, which determines if python resolutions can be # done in parallel or not. If so, this is preferable. isSocketResolutionParallel = distutils.sysconfig.get_config_var( "HAVE_GETHOSTBYNAME_R") self.useSocketResolution = CONFIG[ "queries.hostnames.useSocketModule"] and isSocketResolutionParallel for _ in range(CONFIG["queries.hostnames.poolSize"]): t = threading.Thread(target=self._workerLoop) t.setDaemon(True) t.start() self.threadPool.append(t) def getHostname(self, ipAddr, timeout, flushCache=False): """ Provides the hostname, queuing the request and returning None if the timeout is reached before resolution. If a problem's encountered then this either raises an IOError (for os and network issues) or ValueError (for DNS resolution errors). Arguments: ipAddr - ip address to be resolved timeout - maximum duration to wait for a resolution (blocks to completion if None) flushCache - if true the cache is skipped and address re-resolved """ # if outstanding requests are done then clear recentQueries to allow # entries removed from the cache to be re-run if self.unresolvedQueue.empty(): self.recentQueries = [] # copies reference cache (this is important in case the cache is trimmed # during this call) cacheRef = self.resolvedCache if not flushCache and ipAddr in cacheRef: # cached response is available - raise if an error, return if a hostname response = cacheRef[ipAddr][0] if isinstance(response, Exception): raise response else: return response elif flushCache or ipAddr not in self.recentQueries: # new request - queue for resolution self.totalResolves += 1 self.recentQueries.append(ipAddr) self.unresolvedQueue.put(ipAddr) # periodically check cache if requester is willing to wait if timeout == None or timeout > 0: startTime = time.time() while timeout == None or time.time() - startTime < timeout: if ipAddr in cacheRef: # address was resolved - raise if an error, return if a hostname response = cacheRef[ipAddr][0] if isinstance(response, Exception): raise response else: return response else: time.sleep(0.1) return None # timeout reached without resolution def stop(self): """ Halts further resolutions and terminates the thread. """ self.cond.acquire() self.halt = True self.cond.notifyAll() self.cond.release() def _workerLoop(self): """ Simple producer-consumer loop followed by worker threads. This takes addresses from the unresolvedQueue, attempts to look up its hostname, and adds its results or the error to the resolved cache. Resolver reference provides shared resources used by the thread pool. """ while not self.halt: # if resolver is paused then put a hold on further resolutions if self.isPaused: self.cond.acquire() if not self.halt: self.cond.wait(1) self.cond.release() continue # snags next available ip, timeout is because queue can't be woken up # when 'halt' is set try: ipAddr = self.unresolvedQueue.get_nowait() except Queue.Empty: # no elements ready, wait a little while and try again self.cond.acquire() if not self.halt: self.cond.wait(1) self.cond.release() continue if self.halt: break try: if self.useSocketResolution: result = _resolveViaSocket(ipAddr) else: result = _resolveViaHost(ipAddr) except IOError, exc: result = exc # lookup failed except ValueError, exc: result = exc # dns error self.resolvedLock.acquire() self.resolvedCache[ipAddr] = (result, RESOLVER_COUNTER.next()) # trim cache if excessively large (clearing out oldest entries) if len(self.resolvedCache) > CONFIG["cache.hostnames.size"]: # Providing for concurrent, non-blocking calls require that entries are # never removed from the cache, so this creates a new, trimmed version # instead. # determines minimum age of entries to be kept currentCount = RESOLVER_COUNTER.next() newCacheSize = CONFIG["cache.hostnames.size"] - CONFIG[ "cache.hostnames.trimSize"] threshold = currentCount - newCacheSize newCache = {} msg = "trimming hostname cache from %i entries to %i" % (len( self.resolvedCache), newCacheSize) log.info(msg) # checks age of each entry, adding to toDelete if too old for ipAddr, entry in self.resolvedCache.iteritems(): if entry[1] >= threshold: newCache[ipAddr] = entry self.resolvedCache = newCache self.resolvedLock.release()
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 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 recv_message(control_file, arrived_at = None): """ Pulls from a control socket until we either have a complete message or encounter a problem. :param file control_file: file derived from the control socket (see the socket's makefile() method for more information) :returns: :class:`~stem.response.ControlMessage` read from the socket :raises: * :class:`stem.ProtocolError` the content from the socket is malformed * :class:`stem.SocketClosed` if the socket closes before we receive a complete message """ parsed_content, raw_content, first_line = None, None, True while True: try: line = control_file.readline() except AttributeError: # if the control_file has been closed then we will receive: # AttributeError: 'NoneType' object has no attribute 'recv' log.info(ERROR_MSG % ('SocketClosed', 'socket file has been closed')) raise stem.SocketClosed('socket file has been closed') except (OSError, ValueError) as exc: # when disconnected this errors with... # # * ValueError: I/O operation on closed file # * OSError: [Errno 107] Transport endpoint is not connected # * OSError: [Errno 9] Bad file descriptor log.info(ERROR_MSG % ('SocketClosed', 'received exception "%s"' % exc)) raise stem.SocketClosed(exc) # Parses the tor control lines. These are of the form... # <status code><divider><content>\r\n if not line: # if the socket is disconnected then the readline() method will provide # empty content log.info(ERROR_MSG % ('SocketClosed', 'empty socket content')) raise stem.SocketClosed('Received empty socket content.') elif not MESSAGE_PREFIX.match(line): log.info(ERROR_MSG % ('ProtocolError', 'malformed status code/divider, "%s"' % log.escape(line))) raise stem.ProtocolError('Badly formatted reply line: beginning is malformed') elif not line.endswith(b'\r\n'): log.info(ERROR_MSG % ('ProtocolError', 'no CRLF linebreak, "%s"' % log.escape(line))) raise stem.ProtocolError('All lines should end with CRLF') status_code, divider, content = line[:3], line[3:4], line[4:-2] # strip CRLF off content status_code = stem.util.str_tools._to_unicode(status_code) divider = stem.util.str_tools._to_unicode(divider) # Most controller responses are single lines, in which case we don't need # so much overhead. if first_line: if divider == ' ': _log_trace(line) return stem.response.ControlMessage([(status_code, divider, content)], line, arrived_at = arrived_at) else: parsed_content, raw_content, first_line = [], bytearray(), False raw_content += line if divider == '-': # mid-reply line, keep pulling for more content parsed_content.append((status_code, divider, content)) elif divider == ' ': # end of the message, return the message parsed_content.append((status_code, divider, content)) _log_trace(bytes(raw_content)) return stem.response.ControlMessage(parsed_content, bytes(raw_content), arrived_at = arrived_at) elif divider == '+': # data entry, all of the following lines belong to the content until we # get a line with just a period content_block = bytearray(content) while True: try: line = control_file.readline() raw_content += line except socket.error as exc: log.info(ERROR_MSG % ('SocketClosed', 'received an exception while mid-way through a data reply (exception: "%s", read content: "%s")' % (exc, log.escape(bytes(raw_content))))) raise stem.SocketClosed(exc) if not line.endswith(b'\r\n'): log.info(ERROR_MSG % ('ProtocolError', 'CRLF linebreaks missing from a data reply, "%s"' % log.escape(bytes(raw_content)))) raise stem.ProtocolError('All lines should end with CRLF') elif line == b'.\r\n': break # data block termination line = line[:-2] # strips off the CRLF # lines starting with a period are escaped by a second period (as per # section 2.4 of the control-spec) if line.startswith(b'..'): line = line[1:] content_block += b'\n' + line # joins the content using a newline rather than CRLF separator (more # conventional for multi-line string content outside the windows world) parsed_content.append((status_code, divider, bytes(content_block))) else: # this should never be reached due to the prefix regex, but might as well # be safe... log.warn(ERROR_MSG % ('ProtocolError', "\"%s\" isn't a recognized divider type" % divider)) raise stem.ProtocolError("Unrecognized divider type '%s': %s" % (divider, stem.util.str_tools._to_unicode(line)))
def recv_message(control_file): """ Pulls from a control socket until we either have a complete message or encounter a problem. :param file control_file: file derived from the control socket (see the socket's makefile() method for more information) :returns: :class:`~stem.response.ControlMessage` read from the socket :raises: * :class:`stem.ProtocolError` the content from the socket is malformed * :class:`stem.SocketClosed` if the socket closes before we receive a complete message """ parsed_content, raw_content = [], "" logging_prefix = "Error while receiving a control message (%s): " while True: try: line = control_file.readline() except AttributeError: # if the control_file has been closed then we will receive: # AttributeError: 'NoneType' object has no attribute 'recv' prefix = logging_prefix % "SocketClosed" log.info(prefix + "socket file has been closed") raise stem.SocketClosed("socket file has been closed") except socket.error, exc: # when disconnected we get... # socket.error: [Errno 107] Transport endpoint is not connected prefix = logging_prefix % "SocketClosed" log.info(prefix + "received exception \"%s\"" % exc) raise stem.SocketClosed(exc) raw_content += line # Parses the tor control lines. These are of the form... # <status code><divider><content>\r\n if len(line) == 0: # if the socket is disconnected then the readline() method will provide # empty content prefix = logging_prefix % "SocketClosed" log.info(prefix + "empty socket content") raise stem.SocketClosed("Received empty socket content.") elif len(line) < 4: prefix = logging_prefix % "ProtocolError" log.info(prefix + "line too short, \"%s\"" % log.escape(line)) raise stem.ProtocolError("Badly formatted reply line: too short") elif not re.match(r'^[a-zA-Z0-9]{3}[-+ ]', line): prefix = logging_prefix % "ProtocolError" log.info(prefix + "malformed status code/divider, \"%s\"" % log.escape(line)) raise stem.ProtocolError("Badly formatted reply line: beginning is malformed") elif not line.endswith("\r\n"): prefix = logging_prefix % "ProtocolError" log.info(prefix + "no CRLF linebreak, \"%s\"" % log.escape(line)) raise stem.ProtocolError("All lines should end with CRLF") line = line[:-2] # strips off the CRLF status_code, divider, content = line[:3], line[3], line[4:] if divider == "-": # mid-reply line, keep pulling for more content parsed_content.append((status_code, divider, content)) elif divider == " ": # end of the message, return the message parsed_content.append((status_code, divider, content)) log_message = raw_content.replace("\r\n", "\n").rstrip() log.trace("Received from tor:\n" + log_message) return stem.response.ControlMessage(parsed_content, raw_content) elif divider == "+": # data entry, all of the following lines belong to the content until we # get a line with just a period while True: try: line = control_file.readline() except socket.error, exc: prefix = logging_prefix % "SocketClosed" log.info(prefix + "received an exception while mid-way through a data reply (exception: \"%s\", read content: \"%s\")" % (exc, log.escape(raw_content))) raise stem.SocketClosed(exc) raw_content += line if not line.endswith("\r\n"): prefix = logging_prefix % "ProtocolError" log.info(prefix + "CRLF linebreaks missing from a data reply, \"%s\"" % log.escape(raw_content)) raise stem.ProtocolError("All lines should end with CRLF") elif line == ".\r\n": break # data block termination line = line[:-2] # strips off the CRLF # lines starting with a period are escaped by a second period (as per # section 2.4 of the control-spec) if line.startswith(".."): line = line[1:] # appends to previous content, using a newline rather than CRLF # separator (more conventional for multi-line string content outside # the windows world) content += "\n" + line parsed_content.append((status_code, divider, content))
def renderTorrc(template, options, commentIndent = 30): """ Uses the given template to generate a nicely formatted torrc with the given options. The tempating language this recognizes is a simple one, recognizing the following options: [IF <option>] # if <option> maps to true or a non-empty string [IF NOT <option>] # logical inverse [IF <opt1> | <opt2>] # logical or of the options [ELSE] # if the prior conditional evaluated to false [END IF] # ends the control block [<option>] # inputs the option value, omitting the line if it maps # to a boolean or empty string [NEWLINE] # empty line, otherwise templating white space is ignored Arguments: template - torrc template lines used to generate the results options - mapping of keywords to their given values, with values being booleans or strings (possibly multi-line) commentIndent - minimum column that comments align on """ results = [] templateIter = iter(template) commentLineFormat = "%%-%is%%s" % commentIndent try: while True: line = templateIter.next().strip() if line.startswith("[IF ") and line.endswith("]"): # checks if any of the conditional options are true or a non-empty string evaluatesTrue = False for cond in line[4:-1].split("|"): isInverse = False if cond.startswith("NOT "): isInverse = True cond = cond[4:] if isInverse != bool(options.get(cond.strip())): evaluatesTrue = True break if evaluatesTrue: continue else: # skips lines until we come to an else or the end of the block depth = 0 while depth != -1: line = templateIter.next().strip() if line.startswith("[IF ") and line.endswith("]"): depth += 1 elif line == "[END IF]": depth -= 1 elif depth == 0 and line == "[ELSE]": depth -= 1 elif line == "[ELSE]": # an else block we aren't using - skip to the end of it depth = 0 while depth != -1: line = templateIter.next().strip() if line.startswith("[IF "): depth += 1 elif line == "[END IF]": depth -= 1 elif line == "[NEWLINE]": # explicit newline results.append("") elif line.startswith("#"): # comment only results.append(line) elif line.startswith("[") and line.endswith("]"): # completely dynamic entry optValue = options.get(line[1:-1]) if optValue: results.append(optValue) else: # torrc option line option, arg, comment = "", "", "" parsedLine = line if "#" in parsedLine: parsedLine, comment = parsedLine.split("#", 1) parsedLine = parsedLine.strip() comment = "# %s" % comment.strip() # parses the argument from the option if " " in parsedLine.strip(): option, arg = parsedLine.split(" ", 1) option = option.strip() else: log.info("torrc template option lacks an argument: '%s'" % line) continue # inputs dynamic arguments if arg.startswith("[") and arg.endswith("]"): arg = options.get(arg[1:-1]) # skips argument if it's false or an empty string if not arg: continue torrcEntry = "%s %s" % (option, arg) if comment: results.append(commentLineFormat % (torrcEntry + " ", comment)) else: results.append(torrcEntry) except StopIteration: pass return "\n".join(results)
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 startTorMonitor(startTime): """ Initializes the interface and starts the main draw loop. Arguments: startTime - unix time for when arm was started """ # attempts to fetch the tor pid, warning if unsuccessful (this is needed for # checking its resource usage, among other things) conn = torTools.getConn() torPid = conn.getMyPid() if not torPid and conn.isAlive(): log.warn("Unable to determine Tor's pid. Some information, like its resource usage will be unavailable.") # adds events needed for arm functionality to the torTools REQ_EVENTS # mapping (they're then included with any setControllerEvents call, and log # a more helpful error if unavailable) torTools.REQ_EVENTS["BW"] = "bandwidth graph won't function" if not CONFIG["startup.blindModeEnabled"]: # The DisableDebuggerAttachment will prevent our connection panel from really # functioning. It'll have circuits, but little else. If this is the case then # notify the user and tell them what they can do to fix it. if conn.getOption("DisableDebuggerAttachment", None) == "1": log.notice( "Tor is preventing system utilities like netstat and lsof from working. This means that arm can't provide you with connection information. You can change this by adding 'DisableDebuggerAttachment 0' to your torrc and restarting tor. For more information see...\nhttps://trac.torproject.org/3313" ) connections.getResolver("tor").setPaused(True) else: torTools.REQ_EVENTS["CIRC"] = "may cause issues in identifying client connections" # Configures connection resoultions. This is paused/unpaused according to # if Tor's connected or not. conn.addStatusListener(connResetListener) if torPid: # use the tor pid to help narrow connection results torCmdName = sysTools.getProcessName(torPid, "tor") connections.getResolver(torCmdName, torPid, "tor") else: # constructs singleton resolver and, if tor isn't connected, initizes # it to be paused connections.getResolver("tor").setPaused(not conn.isAlive()) # hack to display a better (arm specific) notice if all resolvers fail connections.RESOLVER_FINAL_FAILURE_MSG = "We were unable to use any of your system's resolvers to get tor's connections. This is fine, but means that the connections page will be empty. This is usually permissions related so if you would like to fix this then run arm with the same user as tor (ie, \"sudo -u <tor user> arm\")." # provides a notice about any event types tor supports but arm doesn't missingEventTypes = cli.logPanel.getMissingEventTypes() if missingEventTypes: pluralLabel = "s" if len(missingEventTypes) > 1 else "" log.info( "arm doesn't recognize the following event type%s: %s (log 'UNKNOWN' events to see them)" % (pluralLabel, ", ".join(missingEventTypes)) ) try: curses.wrapper(drawTorMonitor, startTime) except KeyboardInterrupt: # Skip printing stack trace in case of keyboard interrupt. The # HALT_ACTIVITY attempts to prevent daemons from triggering a curses redraw # (which would leave the user's terminal in a screwed up state). There is # still a tiny timing issue here (after the exception but before the flag # is set) but I've never seen it happen in practice. panel.HALT_ACTIVITY = True shutdownDaemons()
def recv_message(control_file): """ Pulls from a control socket until we either have a complete message or encounter a problem. :param file control_file: file derived from the control socket (see the socket's makefile() method for more information) :returns: :class:`~stem.response.ControlMessage` read from the socket :raises: * :class:`stem.ProtocolError` the content from the socket is malformed * :class:`stem.SocketClosed` if the socket closes before we receive a complete message """ parsed_content, raw_content = [], "" logging_prefix = "Error while receiving a control message (%s): " while True: try: line = control_file.readline() except AttributeError: # if the control_file has been closed then we will receive: # AttributeError: 'NoneType' object has no attribute 'recv' prefix = logging_prefix % "SocketClosed" log.info(prefix + "socket file has been closed") raise stem.SocketClosed("socket file has been closed") except socket.error, exc: # when disconnected we get... # socket.error: [Errno 107] Transport endpoint is not connected prefix = logging_prefix % "SocketClosed" log.info(prefix + "received exception \"%s\"" % exc) raise stem.SocketClosed(exc) raw_content += line # Parses the tor control lines. These are of the form... # <status code><divider><content>\r\n if len(line) == 0: # if the socket is disconnected then the readline() method will provide # empty content prefix = logging_prefix % "SocketClosed" log.info(prefix + "empty socket content") raise stem.SocketClosed("Received empty socket content.") elif len(line) < 4: prefix = logging_prefix % "ProtocolError" log.info(prefix + "line too short, \"%s\"" % log.escape(line)) raise stem.ProtocolError("Badly formatted reply line: too short") elif not re.match(r'^[a-zA-Z0-9]{3}[-+ ]', line): prefix = logging_prefix % "ProtocolError" log.info(prefix + "malformed status code/divider, \"%s\"" % log.escape(line)) raise stem.ProtocolError( "Badly formatted reply line: beginning is malformed") elif not line.endswith("\r\n"): prefix = logging_prefix % "ProtocolError" log.info(prefix + "no CRLF linebreak, \"%s\"" % log.escape(line)) raise stem.ProtocolError("All lines should end with CRLF") line = line[:-2] # strips off the CRLF status_code, divider, content = line[:3], line[3], line[4:] if divider == "-": # mid-reply line, keep pulling for more content parsed_content.append((status_code, divider, content)) elif divider == " ": # end of the message, return the message parsed_content.append((status_code, divider, content)) log_message = raw_content.replace("\r\n", "\n").rstrip() log.trace("Received from tor:\n" + log_message) return stem.response.ControlMessage(parsed_content, raw_content) elif divider == "+": # data entry, all of the following lines belong to the content until we # get a line with just a period while True: try: line = control_file.readline() except socket.error, exc: prefix = logging_prefix % "SocketClosed" log.info( prefix + "received an exception while mid-way through a data reply (exception: \"%s\", read content: \"%s\")" % (exc, log.escape(raw_content))) raise stem.SocketClosed(exc) raw_content += line if not line.endswith("\r\n"): prefix = logging_prefix % "ProtocolError" log.info( prefix + "CRLF linebreaks missing from a data reply, \"%s\"" % log.escape(raw_content)) raise stem.ProtocolError("All lines should end with CRLF") elif line == ".\r\n": break # data block termination line = line[:-2] # strips off the CRLF # lines starting with a period are escaped by a second period (as per # section 2.4 of the control-spec) if line.startswith(".."): line = line[1:] # appends to previous content, using a newline rather than CRLF # separator (more conventional for multi-line string content outside # the windows world) content += "\n" + line parsed_content.append((status_code, divider, content))
def recv_message(control_file): """ Pulls from a control socket until we either have a complete message or encounter a problem. :param file control_file: file derived from the control socket (see the socket's makefile() method for more information) :returns: :class:`~stem.response.ControlMessage` read from the socket :raises: * :class:`stem.ProtocolError` the content from the socket is malformed * :class:`stem.SocketClosed` if the socket closes before we receive a complete message """ parsed_content, raw_content = [], [] logging_prefix = 'Error while receiving a control message (%s): ' while True: try: # From a real socket readline() would always provide bytes, but during # tests we might be given a StringIO in which case it's unicode under # python 3.x. line = stem.util.str_tools._to_bytes(control_file.readline()) except AttributeError: # if the control_file has been closed then we will receive: # AttributeError: 'NoneType' object has no attribute 'recv' prefix = logging_prefix % 'SocketClosed' log.info(prefix + 'socket file has been closed') raise stem.SocketClosed('socket file has been closed') except (socket.error, ValueError) as exc: # When disconnected we get... # # Python 2: # socket.error: [Errno 107] Transport endpoint is not connected # # Python 3: # ValueError: I/O operation on closed file. prefix = logging_prefix % 'SocketClosed' log.info(prefix + 'received exception "%s"' % exc) raise stem.SocketClosed(exc) raw_content.append(line) # Parses the tor control lines. These are of the form... # <status code><divider><content>\r\n if len(line) == 0: # if the socket is disconnected then the readline() method will provide # empty content prefix = logging_prefix % 'SocketClosed' log.info(prefix + 'empty socket content') raise stem.SocketClosed('Received empty socket content.') elif len(line) < 4: prefix = logging_prefix % 'ProtocolError' log.info(prefix + 'line too short, "%s"' % log.escape(line)) raise stem.ProtocolError('Badly formatted reply line: too short') elif not re.match(b'^[a-zA-Z0-9]{3}[-+ ]', line): prefix = logging_prefix % 'ProtocolError' log.info(prefix + 'malformed status code/divider, "%s"' % log.escape(line)) raise stem.ProtocolError( 'Badly formatted reply line: beginning is malformed') elif not line.endswith(b'\r\n'): prefix = logging_prefix % 'ProtocolError' log.info(prefix + 'no CRLF linebreak, "%s"' % log.escape(line)) raise stem.ProtocolError('All lines should end with CRLF') line = line[:-2] # strips off the CRLF status_code, divider, content = line[:3], line[3:4], line[4:] content_lines = [content] if stem.prereq.is_python_3(): status_code = stem.util.str_tools._to_unicode(status_code) divider = stem.util.str_tools._to_unicode(divider) if divider == '-': # mid-reply line, keep pulling for more content parsed_content.append((status_code, divider, content)) elif divider == ' ': # end of the message, return the message parsed_content.append((status_code, divider, content)) raw_content_str = b''.join(raw_content) log_message = stem.util.str_tools._to_unicode( raw_content_str.replace(b'\r\n', b'\n').rstrip()) log_message_lines = log_message.split('\n') if TRUNCATE_LOGS and len(log_message_lines) > TRUNCATE_LOGS: log_message = '\n'.join(log_message_lines[:TRUNCATE_LOGS] + [ '... %i more lines...' % (len(log_message_lines) - TRUNCATE_LOGS) ]) if len(log_message_lines) > 2: log.trace('Received from tor:\n%s' % log_message) else: log.trace('Received from tor: %s' % log_message.replace('\n', '\\n')) return stem.response.ControlMessage(parsed_content, raw_content_str) elif divider == '+': # data entry, all of the following lines belong to the content until we # get a line with just a period while True: try: line = stem.util.str_tools._to_bytes( control_file.readline()) except socket.error as exc: prefix = logging_prefix % 'SocketClosed' log.info( prefix + 'received an exception while mid-way through a data reply (exception: "%s", read content: "%s")' % (exc, log.escape(b''.join(raw_content)))) raise stem.SocketClosed(exc) raw_content.append(line) if not line.endswith(b'\r\n'): prefix = logging_prefix % 'ProtocolError' log.info( prefix + 'CRLF linebreaks missing from a data reply, "%s"' % log.escape(b''.join(raw_content))) raise stem.ProtocolError('All lines should end with CRLF') elif line == b'.\r\n': break # data block termination line = line[:-2] # strips off the CRLF # lines starting with a period are escaped by a second period (as per # section 2.4 of the control-spec) if line.startswith(b'..'): line = line[1:] content_lines.append(line) # joins the content using a newline rather than CRLF separator (more # conventional for multi-line string content outside the windows world) parsed_content.append( (status_code, divider, b'\n'.join(content_lines))) else: # this should never be reached due to the prefix regex, but might as well # be safe... prefix = logging_prefix % 'ProtocolError' log.warn(prefix + "\"%s\" isn't a recognized divider type" % divider) raise stem.ProtocolError( "Unrecognized divider type '%s': %s" % (divider, stem.util.str_tools._to_unicode(line)))
def run(self): while not self._halt: timeSinceReset = time.time() - self.lastLookup if self.resolveRate == 0: self._cond.acquire() if not self._halt: self._cond.wait(0.2) self._cond.release() continue elif timeSinceReset < self.resolveRate: sleepTime = max(0.2, self.resolveRate - timeSinceReset) self._cond.acquire() if not self._halt: self._cond.wait(sleepTime) self._cond.release() continue # done waiting, try again newValues = {} try: if self._useProc: utime, stime, startTime = proc.get_stats(self.processPid, proc.Stat.CPU_UTIME, proc.Stat.CPU_STIME, proc.Stat.START_TIME) totalCpuTime = float(utime) + float(stime) cpuDelta = totalCpuTime - self._lastCpuTotal newValues["cpuSampling"] = cpuDelta / timeSinceReset newValues["cpuAvg"] = totalCpuTime / (time.time() - float(startTime)) newValues["_lastCpuTotal"] = totalCpuTime memUsage = int(proc.get_memory_usage(self.processPid)[0]) totalMemory = proc.get_physical_memory() newValues["memUsage"] = memUsage newValues["memUsagePercentage"] = float(memUsage) / totalMemory else: # the ps call formats results as: # # TIME ELAPSED RSS %MEM # 3-08:06:32 21-00:00:12 121844 23.5 # # or if Tor has only recently been started: # # TIME ELAPSED RSS %MEM # 0:04.40 37:57 18772 0.9 psCall = system.call("ps -p %s -o cputime,etime,rss,%%mem" % self.processPid) isSuccessful = False if psCall and len(psCall) >= 2: stats = psCall[1].strip().split() if len(stats) == 4: try: totalCpuTime = str_tools.parse_short_time_label(stats[0]) uptime = str_tools.parse_short_time_label(stats[1]) cpuDelta = totalCpuTime - self._lastCpuTotal newValues["cpuSampling"] = cpuDelta / timeSinceReset newValues["cpuAvg"] = totalCpuTime / uptime newValues["_lastCpuTotal"] = totalCpuTime newValues["memUsage"] = int(stats[2]) * 1024 # ps size is in kb newValues["memUsagePercentage"] = float(stats[3]) / 100.0 isSuccessful = True except ValueError, exc: pass if not isSuccessful: raise IOError("unrecognized output from ps: %s" % psCall) except IOError, exc: newValues = {} self._failureCount += 1 if self._useProc: if self._failureCount >= 3: # We've failed three times resolving via proc. Warn, and fall back # to ps resolutions. log.info("Failed three attempts to get process resource usage from proc, falling back to ps (%s)" % exc) self._useProc = False self._failureCount = 1 # prevents lastQueryFailed() from thinking that we succeeded else: # wait a bit and try again log.debug("Unable to query process resource usage from proc (%s)" % exc) self._cond.acquire() if not self._halt: self._cond.wait(0.5) self._cond.release() else: # exponential backoff on making failed ps calls sleepTime = 0.01 * (2 ** self._failureCount) + self._failureCount log.debug("Unable to query process resource usage from ps, waiting %0.2f seconds (%s)" % (sleepTime, exc)) self._cond.acquire() if not self._halt: self._cond.wait(sleepTime) self._cond.release() # sets the new values if newValues: # If this is the first run then the cpuSampling stat is meaningless # (there isn't a previous tick to sample from so it's zero at this # point). Setting it to the average, which is a fairer estimate. if self.lastLookup == -1: newValues["cpuSampling"] = newValues["cpuAvg"] self._valLock.acquire() self.cpuSampling = newValues["cpuSampling"] self.cpuAvg = newValues["cpuAvg"] self.memUsage = newValues["memUsage"] self.memUsagePercentage = newValues["memUsagePercentage"] self._lastCpuTotal = newValues["_lastCpuTotal"] self.lastLookup = time.time() self._runCount += 1 self._failureCount = 0 self._valLock.release()
except socket.error, exc: log.info("Failed to send message: %s" % exc) # When sending there doesn't seem to be a reliable method for # distinguishing between failures from a disconnect verses other things. # Just accounting for known disconnection responses. if str(exc) == "[Errno 32] Broken pipe": raise stem.SocketClosed(exc) else: raise stem.SocketError(exc) except AttributeError: # if the control_file has been closed then flush will receive: # AttributeError: 'NoneType' object has no attribute 'sendall' log.info("Failed to send message: file has been closed") raise stem.SocketClosed("file has been closed") def recv_message(control_file): """ Pulls from a control socket until we either have a complete message or encounter a problem. :param file control_file: file derived from the control socket (see the socket's makefile() method for more information) :returns: :class:`~stem.response.ControlMessage` read from the socket :raises: * :class:`stem.ProtocolError` the content from the socket is malformed
def _parse_message(self): # Example: # 250-PROTOCOLINFO 1 # 250-AUTH METHODS=COOKIE COOKIEFILE="/home/atagar/.tor/control_auth_cookie" # 250-VERSION Tor="0.2.1.30" # 250 OK self.protocol_version = None self.tor_version = None self.auth_methods = () self.unknown_auth_methods = () self.cookie_path = None auth_methods, unknown_auth_methods = [], [] remaining_lines = list(self) if not self.is_ok() or not remaining_lines.pop() == "OK": raise stem.ProtocolError("PROTOCOLINFO response didn't have an OK status:\n%s" % self) # sanity check that we're a PROTOCOLINFO response if not remaining_lines[0].startswith("PROTOCOLINFO"): raise stem.ProtocolError("Message is not a PROTOCOLINFO response:\n%s" % self) while remaining_lines: line = remaining_lines.pop(0) line_type = line.pop() if line_type == "PROTOCOLINFO": # Line format: # FirstLine = "PROTOCOLINFO" SP PIVERSION CRLF # PIVERSION = 1*DIGIT if line.is_empty(): raise stem.ProtocolError("PROTOCOLINFO response's initial line is missing the protocol version: %s" % line) try: self.protocol_version = int(line.pop()) except ValueError: raise stem.ProtocolError("PROTOCOLINFO response version is non-numeric: %s" % line) # The piversion really should be "1" but, according to the spec, tor # does not necessarily need to provide the PROTOCOLINFO version that we # requested. Log if it's something we aren't expecting but still make # an effort to parse like a v1 response. if self.protocol_version != 1: log.info("We made a PROTOCOLINFO version 1 query but got a version %i response instead. We'll still try to use it, but this may cause problems." % self.protocol_version) elif line_type == "AUTH": # Line format: # AuthLine = "250-AUTH" SP "METHODS=" AuthMethod *("," AuthMethod) # *(SP "COOKIEFILE=" AuthCookieFile) CRLF # AuthMethod = "NULL" / "HASHEDPASSWORD" / "COOKIE" # AuthCookieFile = QuotedString # parse AuthMethod mapping if not line.is_next_mapping("METHODS"): raise stem.ProtocolError("PROTOCOLINFO response's AUTH line is missing its mandatory 'METHODS' mapping: %s" % line) for method in line.pop_mapping()[1].split(","): if method == "NULL": auth_methods.append(AuthMethod.NONE) elif method == "HASHEDPASSWORD": auth_methods.append(AuthMethod.PASSWORD) elif method == "COOKIE": auth_methods.append(AuthMethod.COOKIE) elif method == "SAFECOOKIE": auth_methods.append(AuthMethod.SAFECOOKIE) else: unknown_auth_methods.append(method) message_id = "stem.response.protocolinfo.unknown_auth_%s" % method log.log_once(message_id, log.INFO, "PROTOCOLINFO response included a type of authentication that we don't recognize: %s" % method) # our auth_methods should have a single AuthMethod.UNKNOWN entry if # any unknown authentication methods exist if not AuthMethod.UNKNOWN in auth_methods: auth_methods.append(AuthMethod.UNKNOWN) # parse optional COOKIEFILE mapping (quoted and can have escapes) if line.is_next_mapping("COOKIEFILE", True, True): self.cookie_path = line.pop_mapping(True, True)[1] elif line_type == "VERSION": # Line format: # VersionLine = "250-VERSION" SP "Tor=" TorVersion OptArguments CRLF # TorVersion = QuotedString if not line.is_next_mapping("Tor", True): raise stem.ProtocolError("PROTOCOLINFO response's VERSION line is missing its mandatory tor version mapping: %s" % line) try: self.tor_version = stem.version.Version(line.pop_mapping(True)[1]) except ValueError as exc: raise stem.ProtocolError(exc) else: log.debug("Unrecognized PROTOCOLINFO line type '%s', ignoring it: %s" % (line_type, line)) self.auth_methods = tuple(auth_methods) self.unknown_auth_methods = tuple(unknown_auth_methods)
def renderTorrc(template, options, commentIndent=30): """ Uses the given template to generate a nicely formatted torrc with the given options. The tempating language this recognizes is a simple one, recognizing the following options: [IF <option>] # if <option> maps to true or a non-empty string [IF NOT <option>] # logical inverse [IF <opt1> | <opt2>] # logical or of the options [ELSE] # if the prior conditional evaluated to false [END IF] # ends the control block [<option>] # inputs the option value, omitting the line if it maps # to a boolean or empty string [NEWLINE] # empty line, otherwise templating white space is ignored Arguments: template - torrc template lines used to generate the results options - mapping of keywords to their given values, with values being booleans or strings (possibly multi-line) commentIndent - minimum column that comments align on """ results = [] templateIter = iter(template) commentLineFormat = "%%-%is%%s" % commentIndent try: while True: line = templateIter.next().strip() if line.startswith("[IF ") and line.endswith("]"): # checks if any of the conditional options are true or a non-empty string evaluatesTrue = False for cond in line[4:-1].split("|"): isInverse = False if cond.startswith("NOT "): isInverse = True cond = cond[4:] if isInverse != bool(options.get(cond.strip())): evaluatesTrue = True break if evaluatesTrue: continue else: # skips lines until we come to an else or the end of the block depth = 0 while depth != -1: line = templateIter.next().strip() if line.startswith("[IF ") and line.endswith("]"): depth += 1 elif line == "[END IF]": depth -= 1 elif depth == 0 and line == "[ELSE]": depth -= 1 elif line == "[ELSE]": # an else block we aren't using - skip to the end of it depth = 0 while depth != -1: line = templateIter.next().strip() if line.startswith("[IF "): depth += 1 elif line == "[END IF]": depth -= 1 elif line == "[NEWLINE]": # explicit newline results.append("") elif line.startswith("#"): # comment only results.append(line) elif line.startswith("[") and line.endswith("]"): # completely dynamic entry optValue = options.get(line[1:-1]) if optValue: results.append(optValue) else: # torrc option line option, arg, comment = "", "", "" parsedLine = line if "#" in parsedLine: parsedLine, comment = parsedLine.split("#", 1) parsedLine = parsedLine.strip() comment = "# %s" % comment.strip() # parses the argument from the option if " " in parsedLine.strip(): option, arg = parsedLine.split(" ", 1) option = option.strip() else: log.info("torrc template option lacks an argument: '%s'" % line) continue # inputs dynamic arguments if arg.startswith("[") and arg.endswith("]"): arg = options.get(arg[1:-1]) # skips argument if it's false or an empty string if not arg: continue torrcEntry = "%s %s" % (option, arg) if comment: results.append(commentLineFormat % (torrcEntry + " ", comment)) else: results.append(torrcEntry) except StopIteration: pass return "\n".join(results)
def recv_message(control_file): """ Pulls from a control socket until we either have a complete message or encounter a problem. :param file control_file: file derived from the control socket (see the socket's makefile() method for more information) :returns: :class:`~stem.response.ControlMessage` read from the socket :raises: * :class:`stem.ProtocolError` the content from the socket is malformed * :class:`stem.SocketClosed` if the socket closes before we receive a complete message """ parsed_content, raw_content = [], [] logging_prefix = 'Error while receiving a control message (%s): ' while True: try: # From a real socket readline() would always provide bytes, but during # tests we might be given a StringIO in which case it's unicode under # python 3.x. line = stem.util.str_tools._to_bytes(control_file.readline()) except AttributeError: # if the control_file has been closed then we will receive: # AttributeError: 'NoneType' object has no attribute 'recv' prefix = logging_prefix % 'SocketClosed' log.info(prefix + 'socket file has been closed') raise stem.SocketClosed('socket file has been closed') except (socket.error, ValueError) as exc: # When disconnected we get... # # Python 2: # socket.error: [Errno 107] Transport endpoint is not connected # # Python 3: # ValueError: I/O operation on closed file. prefix = logging_prefix % 'SocketClosed' log.info(prefix + 'received exception "%s"' % exc) raise stem.SocketClosed(exc) raw_content.append(line) # Parses the tor control lines. These are of the form... # <status code><divider><content>\r\n if len(line) == 0: # if the socket is disconnected then the readline() method will provide # empty content prefix = logging_prefix % 'SocketClosed' log.info(prefix + 'empty socket content') raise stem.SocketClosed('Received empty socket content.') elif len(line) < 4: prefix = logging_prefix % 'ProtocolError' log.info(prefix + 'line too short, "%s"' % log.escape(line)) raise stem.ProtocolError('Badly formatted reply line: too short') elif not re.match(b'^[a-zA-Z0-9]{3}[-+ ]', line): prefix = logging_prefix % 'ProtocolError' log.info(prefix + 'malformed status code/divider, "%s"' % log.escape(line)) raise stem.ProtocolError('Badly formatted reply line: beginning is malformed') elif not line.endswith(b'\r\n'): prefix = logging_prefix % 'ProtocolError' log.info(prefix + 'no CRLF linebreak, "%s"' % log.escape(line)) raise stem.ProtocolError('All lines should end with CRLF') line = line[:-2] # strips off the CRLF status_code, divider, content = line[:3], line[3:4], line[4:] content_lines = [content] if stem.prereq.is_python_3(): status_code = stem.util.str_tools._to_unicode(status_code) divider = stem.util.str_tools._to_unicode(divider) if divider == '-': # mid-reply line, keep pulling for more content parsed_content.append((status_code, divider, content)) elif divider == ' ': # end of the message, return the message parsed_content.append((status_code, divider, content)) raw_content_str = b''.join(raw_content) log_message = raw_content_str.replace(b'\r\n', b'\n').rstrip() log.trace('Received from tor:\n' + stem.util.str_tools._to_unicode(log_message)) return stem.response.ControlMessage(parsed_content, raw_content_str) elif divider == '+': # data entry, all of the following lines belong to the content until we # get a line with just a period while True: try: line = stem.util.str_tools._to_bytes(control_file.readline()) except socket.error as exc: prefix = logging_prefix % 'SocketClosed' log.info(prefix + 'received an exception while mid-way through a data reply (exception: "%s", read content: "%s")' % (exc, log.escape(b''.join(raw_content)))) raise stem.SocketClosed(exc) raw_content.append(line) if not line.endswith(b'\r\n'): prefix = logging_prefix % 'ProtocolError' log.info(prefix + 'CRLF linebreaks missing from a data reply, "%s"' % log.escape(b''.join(raw_content))) raise stem.ProtocolError('All lines should end with CRLF') elif line == b'.\r\n': break # data block termination line = line[:-2] # strips off the CRLF # lines starting with a period are escaped by a second period (as per # section 2.4 of the control-spec) if line.startswith(b'..'): line = line[1:] content_lines.append(line) # joins the content using a newline rather than CRLF separator (more # conventional for multi-line string content outside the windows world) parsed_content.append((status_code, divider, b'\n'.join(content_lines))) else: # this should never be reached due to the prefix regex, but might as well # be safe... prefix = logging_prefix % 'ProtocolError' log.warn(prefix + "\"%s\" isn't a recognized divider type" % divider) raise stem.ProtocolError("Unrecognized divider type '%s': %s" % (divider, stem.util.str_tools._to_unicode(line)))
import base64 import hashlib import datetime import stem.descriptor import stem.descriptor.extrainfo_descriptor import stem.version import stem.util.log as log import stem.util.connection import stem.util.tor_tools try: import rsa IS_RSA_AVAILABLE = True except ImportError: log.info("Unable to import the rsa module. Because of this we'll be unable to verify descriptor signature integrity.") IS_RSA_AVAILABLE = False # relay descriptors must have exactly one of the following REQUIRED_FIELDS = ( "router", "bandwidth", "published", "onion-key", "signing-key", "router-signature", ) # optional entries that can appear at most once SINGLE_FIELDS = ( "platform",
authenticate_safecookie(controller, cookie_path, False) else: authenticate_cookie(controller, cookie_path, False) return # success! except OpenAuthRejected, exc: auth_exceptions.append(exc) except IncorrectPassword, exc: auth_exceptions.append(exc) except PasswordAuthRejected, exc: # Since the PROTOCOLINFO says password auth is available we can assume # that if PasswordAuthRejected is raised it's being raised in error. log.debug("The authenticate_password method raised a PasswordAuthRejected when password auth should be available. Stem may need to be corrected to recognize this response: %s" % exc) auth_exceptions.append(IncorrectPassword(str(exc))) except AuthSecurityFailure, exc: log.info("Tor failed to provide the nonce expected for safecookie authentication. (%s)" % exc) auth_exceptions.append(exc) except (InvalidClientNonce, UnrecognizedAuthChallengeMethod, AuthChallengeFailed), exc: auth_exceptions.append(exc) except (IncorrectCookieSize, UnreadableCookieFile, IncorrectCookieValue), exc: auth_exceptions.append(exc) except CookieAuthRejected, exc: auth_func = "authenticate_safecookie" if exc.is_safecookie else "authenticate_cookie" log.debug("The %s method raised a CookieAuthRejected when cookie auth should be available. Stem may need to be corrected to recognize this response: %s" % (auth_func, exc)) auth_exceptions.append(IncorrectCookieValue(str(exc), exc.cookie_path, exc.is_safecookie)) except stem.ControllerError, exc: auth_exceptions.append(AuthenticationFailure(str(exc))) # All authentication attempts failed. Raise the exception that takes priority # according to our pydocs.
def run(self): while not self._halt: timeSinceReset = time.time() - self.lastLookup if self.resolveRate == 0: self._cond.acquire() if not self._halt: self._cond.wait(0.2) self._cond.release() continue elif timeSinceReset < self.resolveRate: sleepTime = max(0.2, self.resolveRate - timeSinceReset) self._cond.acquire() if not self._halt: self._cond.wait(sleepTime) self._cond.release() continue # done waiting, try again newValues = {} try: if self._useProc: utime, stime, startTime = proc.get_stats( self.processPid, proc.Stat.CPU_UTIME, proc.Stat.CPU_STIME, proc.Stat.START_TIME) totalCpuTime = float(utime) + float(stime) cpuDelta = totalCpuTime - self._lastCpuTotal newValues["cpuSampling"] = cpuDelta / timeSinceReset newValues["cpuAvg"] = totalCpuTime / (time.time() - float(startTime)) newValues["_lastCpuTotal"] = totalCpuTime memUsage = int(proc.get_memory_usage(self.processPid)[0]) totalMemory = proc.get_physical_memory() newValues["memUsage"] = memUsage newValues["memUsagePercentage"] = float( memUsage) / totalMemory else: # the ps call formats results as: # # TIME ELAPSED RSS %MEM # 3-08:06:32 21-00:00:12 121844 23.5 # # or if Tor has only recently been started: # # TIME ELAPSED RSS %MEM # 0:04.40 37:57 18772 0.9 psCall = system.call( "ps -p %s -o cputime,etime,rss,%%mem" % self.processPid) isSuccessful = False if psCall and len(psCall) >= 2: stats = psCall[1].strip().split() if len(stats) == 4: try: totalCpuTime = str_tools.parse_short_time_label( stats[0]) uptime = str_tools.parse_short_time_label( stats[1]) cpuDelta = totalCpuTime - self._lastCpuTotal newValues[ "cpuSampling"] = cpuDelta / timeSinceReset newValues["cpuAvg"] = totalCpuTime / uptime newValues["_lastCpuTotal"] = totalCpuTime newValues["memUsage"] = int( stats[2]) * 1024 # ps size is in kb newValues["memUsagePercentage"] = float( stats[3]) / 100.0 isSuccessful = True except ValueError, exc: pass if not isSuccessful: raise IOError("unrecognized output from ps: %s" % psCall) except IOError, exc: newValues = {} self._failureCount += 1 if self._useProc: if self._failureCount >= 3: # We've failed three times resolving via proc. Warn, and fall back # to ps resolutions. log.info( "Failed three attempts to get process resource usage from proc, falling back to ps (%s)" % exc) self._useProc = False self._failureCount = 1 # prevents lastQueryFailed() from thinking that we succeeded else: # wait a bit and try again log.debug( "Unable to query process resource usage from proc (%s)" % exc) self._cond.acquire() if not self._halt: self._cond.wait(0.5) self._cond.release() else: # exponential backoff on making failed ps calls sleepTime = 0.01 * ( 2**self._failureCount) + self._failureCount log.debug( "Unable to query process resource usage from ps, waiting %0.2f seconds (%s)" % (sleepTime, exc)) self._cond.acquire() if not self._halt: self._cond.wait(sleepTime) self._cond.release() # sets the new values if newValues: # If this is the first run then the cpuSampling stat is meaningless # (there isn't a previous tick to sample from so it's zero at this # point). Setting it to the average, which is a fairer estimate. if self.lastLookup == -1: newValues["cpuSampling"] = newValues["cpuAvg"] self._valLock.acquire() self.cpuSampling = newValues["cpuSampling"] self.cpuAvg = newValues["cpuAvg"] self.memUsage = newValues["memUsage"] self.memUsagePercentage = newValues["memUsagePercentage"] self._lastCpuTotal = newValues["_lastCpuTotal"] self.lastLookup = time.time() self._runCount += 1 self._failureCount = 0 self._valLock.release()
def authenticate(controller, password = None, chroot_path = None, protocolinfo_response = None): """ Authenticates to a control socket using the information provided by a PROTOCOLINFO response. In practice this will often be all we need to authenticate, raising an exception if all attempts to authenticate fail. All exceptions are subclasses of AuthenticationFailure so, in practice, callers should catch the types of authentication failure that they care about, then have a :class:`~stem.connection.AuthenticationFailure` catch-all at the end. This can authenticate to either a :class:`~stem.control.BaseController` or :class:`~stem.socket.ControlSocket`. :param controller: tor controller or socket to be authenticated :param str password: passphrase to present to the socket if it uses password authentication (skips password auth if **None**) :param str chroot_path: path prefix if in a chroot environment :param stem.response.protocolinfo.ProtocolInfoResponse protocolinfo_response: tor protocolinfo response, this is retrieved on our own if **None** :raises: If all attempts to authenticate fails then this will raise a :class:`~stem.connection.AuthenticationFailure` subclass. Since this may try multiple authentication methods it may encounter multiple exceptions. If so then the exception this raises is prioritized as follows... * :class:`stem.connection.IncorrectSocketType` The controller does not speak the tor control protocol. Most often this happened because the user confused the SocksPort or ORPort with the ControlPort. * :class:`stem.connection.UnrecognizedAuthMethods` All of the authentication methods tor will accept are new and unrecognized. Please upgrade stem and, if that doesn't work, file a ticket on 'trac.torproject.org' and I'd be happy to add support. * :class:`stem.connection.MissingPassword` We were unable to authenticate but didn't attempt password authentication because none was provided. You should prompt the user for a password and try again via 'authenticate_password'. * :class:`stem.connection.IncorrectPassword` We were provided with a password but it was incorrect. * :class:`stem.connection.IncorrectCookieSize` Tor allows for authentication by reading it a cookie file, but that file is the wrong size to be an authentication cookie. * :class:`stem.connection.UnreadableCookieFile` Tor allows for authentication by reading it a cookie file, but we can't read that file (probably due to permissions). * **\***:class:`stem.connection.IncorrectCookieValue` Tor allows for authentication by reading it a cookie file, but rejected the contents of that file. * **\***:class:`stem.connection.AuthChallengeUnsupported` Tor doesn't recognize the AUTHCHALLENGE command. This is probably a Tor version prior to SAFECOOKIE being implement, but this exception shouldn't arise because we won't attempt SAFECOOKIE auth unless Tor claims to support it. * **\***:class:`stem.connection.UnrecognizedAuthChallengeMethod` Tor couldn't recognize the AUTHCHALLENGE method Stem sent to it. This shouldn't happen at all. * **\***:class:`stem.connection.InvalidClientNonce` Tor says that the client nonce provided by Stem during the AUTHCHALLENGE process is invalid. * **\***:class:`stem.connection.AuthSecurityFailure` Nonce value provided by the server was invalid. * **\***:class:`stem.connection.OpenAuthRejected` Tor says that it allows for authentication without any credentials, but then rejected our authentication attempt. * **\***:class:`stem.connection.MissingAuthInfo` Tor provided us with a PROTOCOLINFO reply that is technically valid, but missing the information we need to authenticate. * **\***:class:`stem.connection.AuthenticationFailure` There are numerous other ways that authentication could have failed including socket failures, malformed controller responses, etc. These mostly constitute transient failures or bugs. **\*** In practice it is highly unusual for this to occur, being more of a theoretical possibility rather than something you should expect. It's fine to treat these as errors. If you have a use case where this commonly happens, please file a ticket on 'trac.torproject.org'. In the future new :class:`~stem.connection.AuthenticationFailure` subclasses may be added to allow for better error handling. """ if not protocolinfo_response: try: protocolinfo_response = get_protocolinfo(controller) except stem.ProtocolError: raise IncorrectSocketType('unable to use the control socket') except stem.SocketError as exc: raise AuthenticationFailure('socket connection failed (%s)' % exc) auth_methods = list(protocolinfo_response.auth_methods) auth_exceptions = [] if len(auth_methods) == 0: raise NoAuthMethods('our PROTOCOLINFO response did not have any methods for authenticating') # remove authentication methods that are either unknown or for which we don't # have an input if AuthMethod.UNKNOWN in auth_methods: auth_methods.remove(AuthMethod.UNKNOWN) unknown_methods = protocolinfo_response.unknown_auth_methods plural_label = 's' if len(unknown_methods) > 1 else '' methods_label = ', '.join(unknown_methods) # we... er, can't do anything with only unrecognized auth types if not auth_methods: exc_msg = 'unrecognized authentication method%s (%s)' % (plural_label, methods_label) auth_exceptions.append(UnrecognizedAuthMethods(exc_msg, unknown_methods)) else: log.debug('Authenticating to a socket with unrecognized auth method%s, ignoring them: %s' % (plural_label, methods_label)) if protocolinfo_response.cookie_path is None: for cookie_auth_method in (AuthMethod.COOKIE, AuthMethod.SAFECOOKIE): if cookie_auth_method in auth_methods: auth_methods.remove(cookie_auth_method) exc_msg = 'our PROTOCOLINFO response did not have the location of our authentication cookie' auth_exceptions.append(NoAuthCookie(exc_msg, cookie_auth_method == AuthMethod.SAFECOOKIE)) if AuthMethod.PASSWORD in auth_methods and password is None: auth_methods.remove(AuthMethod.PASSWORD) auth_exceptions.append(MissingPassword('no passphrase provided')) # iterating over AuthMethods so we can try them in this order for auth_type in (AuthMethod.NONE, AuthMethod.PASSWORD, AuthMethod.SAFECOOKIE, AuthMethod.COOKIE): if auth_type not in auth_methods: continue try: if auth_type == AuthMethod.NONE: authenticate_none(controller, False) elif auth_type == AuthMethod.PASSWORD: authenticate_password(controller, password, False) elif auth_type in (AuthMethod.COOKIE, AuthMethod.SAFECOOKIE): cookie_path = protocolinfo_response.cookie_path if chroot_path: cookie_path = os.path.join(chroot_path, cookie_path.lstrip(os.path.sep)) if auth_type == AuthMethod.SAFECOOKIE: authenticate_safecookie(controller, cookie_path, False) else: authenticate_cookie(controller, cookie_path, False) return # success! except OpenAuthRejected as exc: auth_exceptions.append(exc) except IncorrectPassword as exc: auth_exceptions.append(exc) except PasswordAuthRejected as exc: # Since the PROTOCOLINFO says password auth is available we can assume # that if PasswordAuthRejected is raised it's being raised in error. log.debug('The authenticate_password method raised a PasswordAuthRejected when password auth should be available. Stem may need to be corrected to recognize this response: %s' % exc) auth_exceptions.append(IncorrectPassword(str(exc))) except AuthSecurityFailure as exc: log.info('Tor failed to provide the nonce expected for safecookie authentication. (%s)' % exc) auth_exceptions.append(exc) except (InvalidClientNonce, UnrecognizedAuthChallengeMethod, AuthChallengeFailed) as exc: auth_exceptions.append(exc) except (IncorrectCookieSize, UnreadableCookieFile, IncorrectCookieValue) as exc: auth_exceptions.append(exc) except CookieAuthRejected as exc: auth_func = 'authenticate_safecookie' if exc.is_safecookie else 'authenticate_cookie' log.debug('The %s method raised a CookieAuthRejected when cookie auth should be available. Stem may need to be corrected to recognize this response: %s' % (auth_func, exc)) auth_exceptions.append(IncorrectCookieValue(str(exc), exc.cookie_path, exc.is_safecookie)) except stem.ControllerError as exc: auth_exceptions.append(AuthenticationFailure(str(exc))) # All authentication attempts failed. Raise the exception that takes priority # according to our pydocs. for exc_type in AUTHENTICATE_EXCEPTIONS: for auth_exc in auth_exceptions: if isinstance(auth_exc, exc_type): raise auth_exc # We really, really shouldn't get here. It means that auth_exceptions is # either empty or contains something that isn't an AuthenticationFailure. raise AssertionError('BUG: Authentication failed without providing a recognized exception: %s' % str(auth_exceptions))
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)