def getLabel(self, optionWidth, valueWidth, summaryWidth): """ Provides display string of the configuration entry with the given constraints on the width of the contents. Arguments: optionWidth - width of the option column valueWidth - width of the value column summaryWidth - width of the summary column """ # Fetching the display entries is very common so this caches the values. # Doing this substantially drops cpu usage when scrolling (by around 40%). argSet = (optionWidth, valueWidth, summaryWidth) if not self.labelCache or self.labelCacheArgs != argSet: optionLabel = uiTools.cropStr(self.get(Field.OPTION), optionWidth) valueLabel = uiTools.cropStr(self.get(Field.VALUE), valueWidth) summaryLabel = uiTools.cropStr(self.get(Field.SUMMARY), summaryWidth, None) lineTextLayout = "%%-%is %%-%is %%-%is" % (optionWidth, valueWidth, summaryWidth) self.labelCache = lineTextLayout % (optionLabel, valueLabel, summaryLabel) self.labelCacheArgs = argSet return self.labelCache
def _drawSelectionPanel(self, selection, width, detailPanelHeight, isScrollbarVisible): """ Renders a panel for the selected configuration option. """ # This is a solid border unless the scrollbar is visible, in which case a # 'T' pipe connects the border to the bar. uiTools.drawBox(self, 0, 0, width, detailPanelHeight + 1) if isScrollbarVisible: self.addch(detailPanelHeight, 1, curses.ACS_TTEE) selectionFormat = curses.A_BOLD | uiTools.getColor(CATEGORY_COLOR[selection.get(Field.CATEGORY)]) # first entry: # <option> (<category> Option) optionLabel = " (%s Option)" % selection.get(Field.CATEGORY) self.addstr(1, 2, selection.get(Field.OPTION) + optionLabel, selectionFormat) # second entry: # Value: <value> ([default|custom], <type>, usage: <argument usage>) if detailPanelHeight >= 3: valueAttr = [] valueAttr.append("default" if selection.get(Field.IS_DEFAULT) else "custom") valueAttr.append(selection.get(Field.TYPE)) valueAttr.append("usage: %s" % (selection.get(Field.ARG_USAGE))) valueAttrLabel = ", ".join(valueAttr) valueLabelWidth = width - 12 - len(valueAttrLabel) valueLabel = uiTools.cropStr(selection.get(Field.VALUE), valueLabelWidth) self.addstr(2, 2, "Value: %s (%s)" % (valueLabel, valueAttrLabel), selectionFormat) # remainder is filled with the man page description descriptionHeight = max(0, detailPanelHeight - 3) descriptionContent = "Description: " + selection.get(Field.DESCRIPTION) for i in range(descriptionHeight): # checks if we're done writing the description if not descriptionContent: break # there's a leading indent after the first line if i > 0: descriptionContent = " " + descriptionContent # we only want to work with content up until the next newline if "\n" in descriptionContent: lineContent, descriptionContent = descriptionContent.split("\n", 1) else: lineContent, descriptionContent = descriptionContent, "" if i != descriptionHeight - 1: # there's more lines to display msg, remainder = uiTools.cropStr(lineContent, width - 3, 4, 4, uiTools.Ending.HYPHEN, True) descriptionContent = remainder.strip() + descriptionContent else: # this is the last line, end it with an ellipse msg = uiTools.cropStr(lineContent, width - 3, 4, 4) self.addstr(3 + i, 2, msg, selectionFormat)
def _drawSelectionPanel(self, selection, width, detailPanelHeight, isScrollbarVisible): """ Renders a panel for the selected configuration option. """ # This is a solid border unless the scrollbar is visible, in which case a # 'T' pipe connects the border to the bar. uiTools.drawBox(self, 0, 0, width, detailPanelHeight + 1) if isScrollbarVisible: self.addch(detailPanelHeight, 1, curses.ACS_TTEE) selectionFormat = curses.A_BOLD | uiTools.getColor(CATEGORY_COLOR[selection.get(Field.CATEGORY)]) # first entry: # <option> (<category> Option) optionLabel =" (%s Option)" % selection.get(Field.CATEGORY) self.addstr(1, 2, selection.get(Field.OPTION) + optionLabel, selectionFormat) # second entry: # Value: <value> ([default|custom], <type>, usage: <argument usage>) if detailPanelHeight >= 3: valueAttr = [] valueAttr.append("default" if selection.get(Field.IS_DEFAULT) else "custom") valueAttr.append(selection.get(Field.TYPE)) valueAttr.append("usage: %s" % (selection.get(Field.ARG_USAGE))) valueAttrLabel = ", ".join(valueAttr) valueLabelWidth = width - 12 - len(valueAttrLabel) valueLabel = uiTools.cropStr(selection.get(Field.VALUE), valueLabelWidth) self.addstr(2, 2, "Value: %s (%s)" % (valueLabel, valueAttrLabel), selectionFormat) # remainder is filled with the man page description descriptionHeight = max(0, detailPanelHeight - 3) descriptionContent = "Description: " + selection.get(Field.DESCRIPTION) for i in range(descriptionHeight): # checks if we're done writing the description if not descriptionContent: break # there's a leading indent after the first line if i > 0: descriptionContent = " " + descriptionContent # we only want to work with content up until the next newline if "\n" in descriptionContent: lineContent, descriptionContent = descriptionContent.split("\n", 1) else: lineContent, descriptionContent = descriptionContent, "" if i != descriptionHeight - 1: # there's more lines to display msg, remainder = uiTools.cropStr(lineContent, width - 3, 4, 4, uiTools.Ending.HYPHEN, True) descriptionContent = remainder.strip() + descriptionContent else: # this is the last line, end it with an ellipse msg = uiTools.cropStr(lineContent, width - 3, 4, 4) self.addstr(3 + i, 2, msg, selectionFormat)
def draw(self, subwindow, width, height): self.valsLock.acquire() # draws the top label titleLabel = "%s Configuration:" % ("Tor" if self.configType == TOR_STATE else "Arm") self.addstr(0, 0, titleLabel, curses.A_STANDOUT) # panel with details for the current selection detailPanelHeight = self._config["features.config.selectionDetails.height"] if detailPanelHeight == 0 or detailPanelHeight + 2 >= height: # no detail panel detailPanelHeight = 0 scrollLoc = self.scroller.getScrollLoc(self.confContents, height - 1) cursorSelection = self.getSelection() else: # Shrink detail panel if there isn't sufficient room for the whole # thing. The extra line is for the bottom border. detailPanelHeight = min(height - 1, detailPanelHeight + 1) scrollLoc = self.scroller.getScrollLoc(self.confContents, height - 1 - detailPanelHeight) cursorSelection = self.getSelection() self._drawSelectionPanel(cursorSelection, width, detailPanelHeight, titleLabel) # draws left-hand scroll bar if content's longer than the height scrollOffset = 0 if len(self.confContents) > height - detailPanelHeight - 1: scrollOffset = 3 self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelHeight - 1, len(self.confContents), 1 + detailPanelHeight) optionWidth = self._config["features.config.state.colWidth.option"] valueWidth = self._config["features.config.state.colWidth.value"] descriptionWidth = max(0, width - scrollOffset - optionWidth - valueWidth - 2) for lineNum in range(scrollLoc, len(self.confContents)): entry = self.confContents[lineNum] drawLine = lineNum + detailPanelHeight + 1 - scrollLoc optionLabel = uiTools.cropStr(entry.get(FIELD_OPTION), optionWidth) valueLabel = uiTools.cropStr(entry.get(FIELD_VALUE), valueWidth) # ends description at the first newline descriptionLabel = uiTools.cropStr(entry.get(FIELD_DESCRIPTION).split("\n")[0], descriptionWidth, None) lineFormat = curses.A_NORMAL if entry.get(FIELD_IS_DEFAULT) else curses.A_BOLD if entry.get(FIELD_CATEGORY): lineFormat |= uiTools.getColor(CATEGORY_COLOR[entry.get(FIELD_CATEGORY)]) if entry == cursorSelection: lineFormat |= curses.A_STANDOUT lineTextLayout = "%%-%is %%-%is %%-%is" % (optionWidth, valueWidth, descriptionWidth) lineText = lineTextLayout % (optionLabel, valueLabel, descriptionLabel) self.addstr(drawLine, scrollOffset, lineText, lineFormat) if drawLine >= height: break self.valsLock.release()
def _splitStr(msg, width): """ Splits a string into substrings of a given length. Arguments: msg - string to be broken up width - max length of any returned substring """ results = [] while msg: msgSegment, msg = uiTools.cropStr(msg, width, None, endType = None, getRemainder = True) if not msgSegment: break # happens if the width is less than the first word results.append(msgSegment.strip()) return results
def _getListingEntry(self, width, currentTime, listingType): lineFormat = uiTools.getColor(connEntry.CATEGORY_COLOR[self.getType()]) # The required widths are the sum of the following: # initial space (1 character) # bracketing (3 characters) # placementLabel (14 characters) # gap between etc and placement label (5 characters) baselineSpace = 14 + 5 dst, etc = "", "" if listingType == entries.ListingType.IP_ADDRESS: # TODO: include hostname when that's available # dst width is derived as: # src (21) + dst (26) + divider (7) + right gap (2) - bracket (3) = 53 char dst = "%-53s" % self.getDestinationLabel(53, includeLocale=True) # fills the nickname into the empty space here dst = "%s%-25s " % ( dst[:25], uiTools.cropStr(self.foreign.getNickname(), 25, 0)) etc = self.getEtcContent(width - baselineSpace - len(dst), listingType) elif listingType == entries.ListingType.HOSTNAME: # min space for the hostname is 40 characters etc = self.getEtcContent(width - baselineSpace - 40, listingType) dstLayout = "%%-%is" % (width - baselineSpace - len(etc)) dst = dstLayout % self.foreign.getHostname( self.foreign.getIpAddr()) elif listingType == entries.ListingType.FINGERPRINT: # dst width is derived as: # src (9) + dst (40) + divider (7) + right gap (2) - bracket (3) = 55 char dst = "%-55s" % self.foreign.getFingerprint() etc = self.getEtcContent(width - baselineSpace - len(dst), listingType) else: # min space for the nickname is 56 characters etc = self.getEtcContent(width - baselineSpace - 56, listingType) dstLayout = "%%-%is" % (width - baselineSpace - len(etc)) dst = dstLayout % self.foreign.getNickname() return ((dst + etc, lineFormat), (" " * (width - baselineSpace - len(dst) - len(etc) + 5), lineFormat), ("%-14s" % self.placementLabel, lineFormat))
def _getListingEntry(self, width, currentTime, listingType): lineFormat = uiTools.getColor(connEntry.CATEGORY_COLOR[self.getType()]) # The required widths are the sum of the following: # initial space (1 character) # bracketing (3 characters) # placementLabel (14 characters) # gap between etc and placement label (5 characters) baselineSpace = 14 + 5 dst, etc = "", "" if listingType == entries.ListingType.IP_ADDRESS: # TODO: include hostname when that's available # dst width is derived as: # src (21) + dst (26) + divider (7) + right gap (2) - bracket (3) = 53 char dst = "%-53s" % self.getDestinationLabel(53, includeLocale = True) # fills the nickname into the empty space here dst = "%s%-25s " % (dst[:25], uiTools.cropStr(self.foreign.getNickname(), 25, 0)) etc = self.getEtcContent(width - baselineSpace - len(dst), listingType) elif listingType == entries.ListingType.HOSTNAME: # min space for the hostname is 40 characters etc = self.getEtcContent(width - baselineSpace - 40, listingType) dstLayout = "%%-%is" % (width - baselineSpace - len(etc)) dst = dstLayout % self.foreign.getHostname(self.foreign.getIpAddr()) elif listingType == entries.ListingType.FINGERPRINT: # dst width is derived as: # src (9) + dst (40) + divider (7) + right gap (2) - bracket (3) = 55 char dst = "%-55s" % self.foreign.getFingerprint() etc = self.getEtcContent(width - baselineSpace - len(dst), listingType) else: # min space for the nickname is 56 characters etc = self.getEtcContent(width - baselineSpace - 56, listingType) dstLayout = "%%-%is" % (width - baselineSpace - len(etc)) dst = dstLayout % self.foreign.getNickname() return ((dst + etc, lineFormat), (" " * (width - baselineSpace - len(dst) - len(etc) + 5), lineFormat), ("%-14s" % self.placementLabel, lineFormat))
def _splitStr(msg, width): """ Splits a string into substrings of a given length. Arguments: msg - string to be broken up width - max length of any returned substring """ results = [] while msg: msgSegment, msg = uiTools.cropStr(msg, width, None, endType=None, getRemainder=True) if not msgSegment: break # happens if the width is less than the first word results.append(msgSegment.strip()) return results
def doHelp(self, arg, outputEntry): """ Performs the '/help' operation, giving usage information for the given argument or a general summary if there wasn't one. """ arg = arg.upper() # If there's multiple arguments then just take the first. This is # particularly likely if they're trying to query a full command (for # instance "/help GETINFO version") arg = arg.split(" ")[0] # strip slash if someone enters an interpretor command (ex. "/help /help") if arg.startswith("/"): arg = arg[1:] if arg: if arg in HELP_OPTIONS: # Provides information for the tor or interpretor argument. This bolds # the usage information and indents the description after it. usage, description = HELP_OPTIONS[arg] outputEntry.append( (usage + "\n", OUTPUT_FORMAT + (Attr.BOLD, ))) for line in description.split("\n"): outputEntry.append((" " + line + "\n", OUTPUT_FORMAT)) if arg == "GETINFO": # if this is the GETINFO option then also list the valid options infoOptions = torTools.getConn().getInfo("info/names") if infoOptions: for line in infoOptions.split("\n"): if line.startswith("config/*") or line.startswith( "dir-usage"): continue lineMatch = re.match("^(.+) -- (.+)$", line) if lineMatch: opt, description = lineMatch.groups() outputEntry.append( ("%-33s" % opt, OUTPUT_FORMAT + (Attr.BOLD, ))) outputEntry.append( (" - %s\n" % description, OUTPUT_FORMAT)) elif arg == "GETCONF": # lists all of the configuration options confOptions = torTools.getConn().getInfo("config/names") if confOptions: confEntries = [ opt.split(" ", 1)[0] for opt in confOptions.split("\n") ] # displays two columns of 42 characters for i in range(0, len(confEntries), 2): lineEntries = confEntries[i:i + 2] lineContent = "" for entry in lineEntries: lineContent += "%-42s" % entry outputEntry.append( (lineContent + "\n", OUTPUT_FORMAT)) outputEntry.append(( "For more information use '/help [CONFIG OPTION]'.", OUTPUT_FORMAT + (Attr.BOLD, ))) elif arg == "SIGNAL": # lists descriptions for all of the signals for signal, description in SIGNAL_DESCRIPTIONS: outputEntry.append( ("%-15s" % signal, OUTPUT_FORMAT + (Attr.BOLD, ))) outputEntry.append( (" - %s\n" % description, OUTPUT_FORMAT)) elif arg == "SETEVENTS": # lists all of the event types eventOptions = torTools.getConn().getInfo("events/names") if eventOptions: eventEntries = eventOptions.split() # displays four columns of 20 characters for i in range(0, len(eventEntries), 4): lineEntries = eventEntries[i:i + 4] lineContent = "" for entry in lineEntries: lineContent += "%-20s" % entry outputEntry.append( (lineContent + "\n", OUTPUT_FORMAT)) elif arg == "USEFEATURE": # lists the feature options featureOptions = torTools.getConn().getInfo( "features/names") if featureOptions: outputEntry.append( (featureOptions + "\n", OUTPUT_FORMAT)) elif arg in ("LOADCONF", "POSTDESCRIPTOR"): # gives a warning that this option isn't yet implemented outputEntry.append( ("\n" + MULTILINE_UNIMPLEMENTED_NOTICE + "\n", ERROR_FORMAT)) else: # check if this is a configuration option manEntry = torConfig.getConfigDescription(arg) if manEntry: # provides basic usage information in bold, followed an indented # copy of the man page description (wrapped to eighty characters) helpTitle = "%s %s (category: %s)\n" % ( manEntry.option, manEntry.argUsage, manEntry.category) outputEntry.append( (helpTitle, OUTPUT_FORMAT + (Attr.BOLD, ))) descLines = manEntry.description.split("\n") for line in descLines: if not line: outputEntry.append(("\n", OUTPUT_FORMAT)) else: while line: drawPortion, line = uiTools.cropStr( line, 88, 4, 4, uiTools.Ending.HYPHEN, True) outputEntry.append( (" %s\n" % drawPortion.strip(), OUTPUT_FORMAT)) else: outputEntry.append( ("No help information available for '%s'..." % arg, ERROR_FORMAT)) else: # provides the GENERAL_HELP with everything bolded except descriptions for line in GENERAL_HELP.split("\n"): cmdStart = line.find(" - ") if cmdStart != -1: outputEntry.append( (line[:cmdStart], OUTPUT_FORMAT + (Attr.BOLD, ))) outputEntry.append((line[cmdStart:] + "\n", OUTPUT_FORMAT)) else: outputEntry.append( (line + "\n", OUTPUT_FORMAT + (Attr.BOLD, )))
def draw(self, subwindow, width, height): """ Redraws message log. Entries stretch to use available space and may contain up to two lines. Starts with newest entries. """ self.valsLock.acquire() self._lastLoggedEvents, self._lastUpdate = list( self.msgLog), time.time() # draws the top label self.addstr(0, 0, self._getTitle(width), curses.A_STANDOUT) # restricts scroll location to valid bounds self.scroll = max( 0, min(self.scroll, self.lastContentHeight - height + 1)) # draws left-hand scroll bar if content's longer than the height msgIndent, dividerIndent = 0, 0 # offsets for scroll bar isScrollBarVisible = self.lastContentHeight > height - 1 if isScrollBarVisible: msgIndent, dividerIndent = 3, 2 self.addScrollBar(self.scroll, self.scroll + height - 1, self.lastContentHeight, 1) # draws log entries lineCount = 1 - self.scroll seenFirstDateDivider = False dividerAttr, duplicateAttr = curses.A_BOLD | uiTools.getColor( "yellow"), curses.A_BOLD | uiTools.getColor("green") isDatesShown = self.regexFilter == None and self._config[ "features.log.showDateDividers"] eventLog = getDaybreaks( self.msgLog, self._isPaused) if isDatesShown else list(self.msgLog) if not self.showDuplicates: deduplicatedLog = getDuplicates(eventLog) if deduplicatedLog == None: msg = "Deduplication took too long. Its current implementation has difficulty handling large logs so disabling it to keep the interface responsive." log.log(log.WARN, msg) self.showDuplicates = True deduplicatedLog = [(entry, 0) for entry in eventLog] else: deduplicatedLog = [(entry, 0) for entry in eventLog] # determines if we have the minimum width to show date dividers showDaybreaks = width - dividerIndent >= 3 while deduplicatedLog: entry, duplicateCount = deduplicatedLog.pop(0) if self.regexFilter and not self.regexFilter.search( entry.getDisplayMessage()): continue # filter doesn't match log message - skip # checks if we should be showing a divider with the date if entry.type == DAYBREAK_EVENT: # bottom of the divider if seenFirstDateDivider: if lineCount >= 1 and lineCount < height and showDaybreaks: self.win.vline(lineCount, dividerIndent, curses.ACS_LLCORNER | dividerAttr, 1) self.win.hline(lineCount, dividerIndent + 1, curses.ACS_HLINE | dividerAttr, width - dividerIndent - 1) self.win.vline(lineCount, width, curses.ACS_LRCORNER | dividerAttr, 1) lineCount += 1 # top of the divider if lineCount >= 1 and lineCount < height and showDaybreaks: timeLabel = time.strftime(" %B %d, %Y ", time.localtime(entry.timestamp)) self.win.vline(lineCount, dividerIndent, curses.ACS_ULCORNER | dividerAttr, 1) self.win.hline(lineCount, dividerIndent + 1, curses.ACS_HLINE | dividerAttr, 1) self.addstr(lineCount, dividerIndent + 2, timeLabel, curses.A_BOLD | dividerAttr) if dividerIndent + len(timeLabel) + 2 <= width: lineLength = width - dividerIndent - len(timeLabel) - 2 self.win.hline(lineCount, dividerIndent + len(timeLabel) + 2, curses.ACS_HLINE | dividerAttr, lineLength) self.win.vline( lineCount, dividerIndent + len(timeLabel) + 2 + lineLength, curses.ACS_URCORNER | dividerAttr, 1) seenFirstDateDivider = True lineCount += 1 else: # entry contents to be displayed, tuples of the form: # (msg, formatting, includeLinebreak) displayQueue = [] msgComp = entry.getDisplayMessage().split("\n") for i in range(len(msgComp)): font = curses.A_BOLD if "ERR" in entry.type else curses.A_NORMAL # emphasizes ERR messages displayQueue.append((msgComp[i].strip(), font | uiTools.getColor(entry.color), i != len(msgComp) - 1)) if duplicateCount: pluralLabel = "s" if duplicateCount > 1 else "" duplicateMsg = DUPLICATE_MSG % (duplicateCount, pluralLabel) displayQueue.append((duplicateMsg, duplicateAttr, False)) cursorLoc, lineOffset = msgIndent, 0 maxEntriesPerLine = self._config[ "features.log.maxLinesPerEntry"] while displayQueue: msg, format, includeBreak = displayQueue.pop(0) drawLine = lineCount + lineOffset if lineOffset == maxEntriesPerLine: break maxMsgSize = width - cursorLoc if len(msg) > maxMsgSize: # message is too long - break it up if lineOffset == maxEntriesPerLine - 1: msg = uiTools.cropStr(msg, maxMsgSize) else: msg, remainder = uiTools.cropStr( msg, maxMsgSize, 4, 4, uiTools.END_WITH_HYPHEN, True) displayQueue.insert( 0, (remainder.strip(), format, includeBreak)) includeBreak = True if drawLine < height and drawLine >= 1: if seenFirstDateDivider and width - dividerIndent >= 3 and showDaybreaks: self.win.vline(drawLine, dividerIndent, curses.ACS_VLINE | dividerAttr, 1) self.win.vline(drawLine, width, curses.ACS_VLINE | dividerAttr, 1) self.addstr(drawLine, cursorLoc, msg, format) cursorLoc += len(msg) if includeBreak or not displayQueue: lineOffset += 1 cursorLoc = msgIndent + ENTRY_INDENT lineCount += lineOffset # if this is the last line and there's room, then draw the bottom of the divider if not deduplicatedLog and seenFirstDateDivider: if lineCount < height and showDaybreaks: # when resizing with a small width the following entries can be # problematc (though I'm not sure why) try: self.win.vline(lineCount, dividerIndent, curses.ACS_LLCORNER | dividerAttr, 1) self.win.hline(lineCount, dividerIndent + 1, curses.ACS_HLINE | dividerAttr, width - dividerIndent - 1) self.win.vline(lineCount, width, curses.ACS_LRCORNER | dividerAttr, 1) except: pass lineCount += 1 # redraw the display if... # - lastContentHeight was off by too much # - we're off the bottom of the page newContentHeight = lineCount + self.scroll - 1 contentHeightDelta = abs(self.lastContentHeight - newContentHeight) forceRedraw, forceRedrawReason = True, "" if contentHeightDelta >= CONTENT_HEIGHT_REDRAW_THRESHOLD: forceRedrawReason = "estimate was off by %i" % contentHeightDelta elif newContentHeight > height and self.scroll + height - 1 > newContentHeight: forceRedrawReason = "scrolled off the bottom of the page" elif not isScrollBarVisible and newContentHeight > height - 1: forceRedrawReason = "scroll bar wasn't previously visible" elif isScrollBarVisible and newContentHeight <= height - 1: forceRedrawReason = "scroll bar shouldn't be visible" else: forceRedraw = False self.lastContentHeight = newContentHeight if forceRedraw: forceRedrawReason = "redrawing the log panel with the corrected content height (%s)" % forceRedrawReason log.log(self._config["log.logPanel.forceDoubleRedraw"], forceRedrawReason) self.redraw(True) self.valsLock.release()
def draw(popup, fingerprint, displayText, displayColor, scroll, showLineNumber): popup.win.erase() popup.win.box() xOffset = 2 if fingerprint: title = "Consensus Descriptor (%s):" % fingerprint else: title = "Consensus Descriptor:" popup.addstr(0, 0, title, curses.A_STANDOUT) lineNumWidth = int(math.log10(len(displayText))) + 1 isEncryptionBlock = False # flag indicating if we're currently displaying a key # checks if first line is in an encryption block for i in range(0, scroll): lineText = displayText[i].strip() if lineText in SIG_START_KEYS: isEncryptionBlock = True elif lineText in SIG_END_KEYS: isEncryptionBlock = False drawLine, pageHeight = 1, popup.maxY - 2 for i in range(scroll, scroll + pageHeight): lineText = displayText[i].strip() xOffset = 2 if showLineNumber: lineNumLabel = ("%%%ii" % lineNumWidth) % (i + 1) lineNumFormat = curses.A_BOLD | uiTools.getColor(LINE_NUM_COLOR) popup.addstr(drawLine, xOffset, lineNumLabel, lineNumFormat) xOffset += lineNumWidth + 1 # Most consensus and descriptor lines are keyword/value pairs. Both are # shown with the same color, but the keyword is bolded. keyword, value = lineText, "" drawFormat = uiTools.getColor(displayColor) if lineText.startswith(HEADER_PREFIX[0]) or lineText.startswith(HEADER_PREFIX[1]): keyword, value = lineText, "" drawFormat = uiTools.getColor(HEADER_COLOR) elif lineText == UNRESOLVED_MSG or lineText == ERROR_MSG: keyword, value = lineText, "" elif lineText in SIG_START_KEYS: keyword, value = lineText, "" isEncryptionBlock = True drawFormat = uiTools.getColor(SIG_COLOR) elif lineText in SIG_END_KEYS: keyword, value = lineText, "" isEncryptionBlock = False drawFormat = uiTools.getColor(SIG_COLOR) elif isEncryptionBlock: keyword, value = "", lineText drawFormat = uiTools.getColor(SIG_COLOR) elif " " in lineText: divIndex = lineText.find(" ") keyword, value = lineText[:divIndex], lineText[divIndex:] displayQueue = [(keyword, drawFormat | curses.A_BOLD), (value, drawFormat)] cursorLoc = xOffset while displayQueue: msg, format = displayQueue.pop(0) if not msg: continue maxMsgSize = popup.maxX - 1 - cursorLoc if len(msg) >= maxMsgSize: # needs to split up the line msg, remainder = uiTools.cropStr(msg, maxMsgSize, None, endType = None, getRemainder = True) if xOffset == cursorLoc and msg == "": # first word is longer than the line msg = uiTools.cropStr(remainder, maxMsgSize) if " " in remainder: remainder = remainder.split(" ", 1)[1] else: remainder = "" popup.addstr(drawLine, cursorLoc, msg, format) cursorLoc = xOffset if remainder: displayQueue.insert(0, (remainder.strip(), format)) drawLine += 1 else: popup.addstr(drawLine, cursorLoc, msg, format) cursorLoc += len(msg) if drawLine > pageHeight: break drawLine += 1 if drawLine > pageHeight: break popup.win.refresh()
def draw(self, subwindow, width, height): self.valsLock.acquire() isWide = width + 1 >= MIN_DUAL_COL_WIDTH # space available for content if isWide: leftWidth = max(width / 2, 77) rightWidth = width - leftWidth else: leftWidth = rightWidth = width # Line 1 / Line 1 Left (system and tor version information) sysNameLabel = "arm - %s" % self.vals["sys/hostname"] contentSpace = min(leftWidth, 40) if len(sysNameLabel) + 10 <= contentSpace: sysTypeLabel = "%s %s" % (self.vals["sys/os"], self.vals["sys/version"]) sysTypeLabel = uiTools.cropStr( sysTypeLabel, contentSpace - len(sysNameLabel) - 3, 4) self.addstr(0, 0, "%s (%s)" % (sysNameLabel, sysTypeLabel)) else: self.addstr(0, 0, uiTools.cropStr(sysNameLabel, contentSpace)) contentSpace = leftWidth - 43 if 7 + len(self.vals["tor/version"]) + len( self.vals["tor/versionStatus"]) <= contentSpace: versionColor = VERSION_STATUS_COLORS[self.vals["tor/versionStatus"]] if \ self.vals["tor/versionStatus"] in VERSION_STATUS_COLORS else "white" versionStatusMsg = "<%s>%s</%s>" % ( versionColor, self.vals["tor/versionStatus"], versionColor) self.addfstr( 0, 43, "Tor %s (%s)" % (self.vals["tor/version"], versionStatusMsg)) elif 11 <= contentSpace: self.addstr( 0, 43, uiTools.cropStr("Tor %s" % self.vals["tor/version"], contentSpace, 4)) # Line 2 / Line 2 Left (tor ip/port information) if self.vals["tor/orPort"]: # acting as a relay (we can assume certain parameters are set entry = "" dirPortLabel = ", Dir Port: %s" % self.vals[ "tor/dirPort"] if self.vals["tor/dirPort"] != "0" else "" for label in (self.vals["tor/nickname"], " - " + self.vals["tor/address"], ":" + self.vals["tor/orPort"], dirPortLabel): if len(entry) + len(label) <= leftWidth: entry += label else: break else: # non-relay (client only) # TODO: not sure what sort of stats to provide... entry = "<red><b>Relaying Disabled</b></red>" if self.vals["tor/isAuthPassword"]: authType = "password" elif self.vals["tor/isAuthCookie"]: authType = "cookie" else: authType = "open" if len(entry) + 19 + len( self.vals["tor/controlPort"]) + len(authType) <= leftWidth: authColor = "red" if authType == "open" else "green" authLabel = "<%s>%s</%s>" % (authColor, authType, authColor) self.addfstr( 1, 0, "%s, Control Port (%s): %s" % (entry, authLabel, self.vals["tor/controlPort"])) elif len(entry) + 16 + len(self.vals["tor/controlPort"]) <= leftWidth: self.addstr( 1, 0, "%s, Control Port: %s" % (entry, self.vals["tor/controlPort"])) else: self.addstr(1, 0, entry) # Line 3 / Line 1 Right (system usage info) y, x = (0, leftWidth) if isWide else (2, 0) if self.vals["stat/rss"] != "0": memoryLabel = uiTools.getSizeLabel( int(self.vals["stat/rss"]) * 1024) else: memoryLabel = "0" sysFields = ((0, "cpu: %s%% tor, %s%% arm" % (self.vals["stat/%torCpu"], self.vals["stat/%armCpu"])), (27, "mem: %s (%s%%)" % (memoryLabel, self.vals["stat/%mem"])), (47, "pid: %s" % (self.vals["stat/pid"] if self._isTorConnected else "")), (59, "uptime: %s" % self.vals["stat/etime"])) for (start, label) in sysFields: if start + len(label) <= rightWidth: self.addstr(y, x + start, label) else: break if self.vals["tor/orPort"]: # Line 4 / Line 2 Right (fingerprint) y, x = (1, leftWidth) if isWide else (3, 0) fingerprintLabel = uiTools.cropStr( "fingerprint: %s" % self.vals["tor/fingerprint"], width) self.addstr(y, x, fingerprintLabel) # Line 5 / Line 3 Left (flags) if self._isTorConnected: flagLine = "flags: " for flag in self.vals["tor/flags"]: flagColor = FLAG_COLORS[flag] if flag in FLAG_COLORS.keys( ) else "white" flagLine += "<b><%s>%s</%s></b>, " % (flagColor, flag, flagColor) if len(self.vals["tor/flags"]) > 0: flagLine = flagLine[:-2] else: flagLine += "<b><cyan>none</cyan></b>" self.addfstr(2 if isWide else 4, 0, flagLine) else: statusTime = torTools.getConn().getStatus()[1] statusTimeLabel = time.strftime("%H:%M %m/%d/%Y", time.localtime(statusTime)) self.addfstr( 2 if isWide else 4, 0, "<b><red>Tor Disconnected</red></b> (%s)" % statusTimeLabel) # Undisplayed / Line 3 Right (exit policy) if isWide: exitPolicy = self.vals["tor/exitPolicy"] # adds note when default exit policy is appended if exitPolicy == "": exitPolicy = "<default>" elif not exitPolicy.endswith((" *:*", " *")): exitPolicy += ", <default>" # color codes accepts to be green, rejects to be red, and default marker to be cyan isSimple = len(exitPolicy) > rightWidth - 13 policies = exitPolicy.split(", ") for i in range(len(policies)): policy = policies[i].strip() displayedPolicy = policy.replace("accept", "").replace( "reject", "").strip() if isSimple else policy if policy.startswith("accept"): policy = "<green><b>%s</b></green>" % displayedPolicy elif policy.startswith("reject"): policy = "<red><b>%s</b></red>" % displayedPolicy elif policy.startswith("<default>"): policy = "<cyan><b>%s</b></cyan>" % displayedPolicy policies[i] = policy self.addfstr(2, leftWidth, "exit policy: %s" % ", ".join(policies)) else: # Client only # TODO: not sure what information to provide here... pass self._isLastDrawWide = isWide self._isChanged = False self.valsLock.release()
def _getListingContent(self, width, listingType): """ Provides the source, destination, and extra info for our listing. Arguments: width - maximum length of the line listingType - primary attribute we're listing connections by """ conn = torTools.getConn() myType = self.getType() dstAddress = self.getDestinationLabel(26, includeLocale = True) # The required widths are the sum of the following: # - room for LABEL_FORMAT and LABEL_MIN_PADDING (11 characters) # - base data for the listing # - that extra field plus any previous usedSpace = len(LABEL_FORMAT % tuple([""] * 4)) + LABEL_MIN_PADDING localPort = ":%s" % self.local.getPort() if self.includePort else "" src, dst, etc = "", "", "" if listingType == entries.ListingType.IP_ADDRESS: myExternalIpAddr = conn.getInfo("address", self.local.getIpAddr()) addrDiffer = myExternalIpAddr != self.local.getIpAddr() # Expanding doesn't make sense, if the connection isn't actually # going through Tor's external IP address. As there isn't a known # method for checking if it is, we're checking the type instead. # # This isn't entirely correct. It might be a better idea to check if # the source and destination addresses are both private, but that might # not be perfectly reliable either. isExpansionType = not myType in (Category.SOCKS, Category.HIDDEN, Category.CONTROL) if isExpansionType: srcAddress = myExternalIpAddr + localPort else: srcAddress = self.local.getIpAddr() + localPort if myType in (Category.SOCKS, Category.CONTROL): # Like inbound connections these need their source and destination to # be swapped. However, this only applies when listing by IP or hostname # (their fingerprint and nickname are both for us). Reversing the # fields here to keep the same column alignments. src = "%-21s" % dstAddress dst = "%-26s" % srcAddress else: src = "%-21s" % srcAddress # ip:port = max of 21 characters dst = "%-26s" % dstAddress # ip:port (xx) = max of 26 characters usedSpace += len(src) + len(dst) # base data requires 47 characters # Showing the fingerprint (which has the width of 42) has priority over # an expanded address field. Hence check if we either have space for # both or wouldn't be showing the fingerprint regardless. isExpandedAddrVisible = width > usedSpace + 28 if isExpandedAddrVisible and CONFIG["features.connection.showColumn.fingerprint"]: isExpandedAddrVisible = width < usedSpace + 42 or width > usedSpace + 70 if addrDiffer and isExpansionType and isExpandedAddrVisible and self.includeExpandedIpAddr and CONFIG["features.connection.showColumn.expandedIp"]: # include the internal address in the src (extra 28 characters) internalAddress = self.local.getIpAddr() + localPort # If this is an inbound connection then reverse ordering so it's: # <foreign> --> <external> --> <internal> # when the src and dst are swapped later if myType == Category.INBOUND: src = "%-21s --> %s" % (src, internalAddress) else: src = "%-21s --> %s" % (internalAddress, src) usedSpace += 28 etc = self.getEtcContent(width - usedSpace, listingType) usedSpace += len(etc) elif listingType == entries.ListingType.HOSTNAME: # 15 characters for source, and a min of 40 reserved for the destination # TODO: when actually functional the src and dst need to be swapped for # SOCKS and CONTROL connections src = "localhost%-6s" % localPort usedSpace += len(src) minHostnameSpace = 40 etc = self.getEtcContent(width - usedSpace - minHostnameSpace, listingType) usedSpace += len(etc) hostnameSpace = width - usedSpace usedSpace = width # prevents padding at the end if self.isPrivate(): dst = ("%%-%is" % hostnameSpace) % "<scrubbed>" else: hostname = self.foreign.getHostname(self.foreign.getIpAddr()) portLabel = ":%-5s" % self.foreign.getPort() if self.includePort else "" # truncates long hostnames and sets dst to <hostname>:<port> hostname = uiTools.cropStr(hostname, hostnameSpace, 0) dst = ("%%-%is" % hostnameSpace) % (hostname + portLabel) elif listingType == entries.ListingType.FINGERPRINT: src = "localhost" if myType == Category.CONTROL: dst = "localhost" else: dst = self.foreign.getFingerprint() dst = "%-40s" % dst usedSpace += len(src) + len(dst) # base data requires 49 characters etc = self.getEtcContent(width - usedSpace, listingType) usedSpace += len(etc) else: # base data requires 50 min characters src = self.local.getNickname() if myType == Category.CONTROL: dst = self.local.getNickname() else: dst = self.foreign.getNickname() minBaseSpace = 50 etc = self.getEtcContent(width - usedSpace - minBaseSpace, listingType) usedSpace += len(etc) baseSpace = width - usedSpace usedSpace = width # prevents padding at the end if len(src) + len(dst) > baseSpace: src = uiTools.cropStr(src, baseSpace / 3) dst = uiTools.cropStr(dst, baseSpace - len(src)) # pads dst entry to its max space dst = ("%%-%is" % (baseSpace - len(src))) % dst if myType == Category.INBOUND: src, dst = dst, src padding = " " * (width - usedSpace + LABEL_MIN_PADDING) return LABEL_FORMAT % (src, dst, etc, padding)
def getDestinationLabel(self, maxLength, includeLocale=False, includeHostname=False): """ Provides a short description of the destination. This is made up of two components, the base <ip addr>:<port> and an extra piece of information in parentheses. The IP address is scrubbed from private connections. Extra information is... - the port's purpose for exit connections - the locale and/or hostname if set to do so, the address isn't private, and isn't on the local network - nothing otherwise Arguments: maxLength - maximum length of the string returned includeLocale - possibly includes the locale includeHostname - possibly includes the hostname """ # the port and port derived data can be hidden by config or without includePort includePort = self.includePort and (CONFIG["features.connection.showExitPort"] or self.getType() != Category.EXIT) # destination of the connection ipLabel = "<scrubbed>" if self.isPrivate() else self.foreign.getIpAddr() portLabel = ":%s" % self.foreign.getPort() if includePort else "" dstAddress = ipLabel + portLabel # Only append the extra info if there's at least a couple characters of # space (this is what's needed for the country codes). if len(dstAddress) + 5 <= maxLength: spaceAvailable = maxLength - len(dstAddress) - 3 if self.getType() == Category.EXIT and includePort: purpose = connections.getPortUsage(self.foreign.getPort()) if purpose: # BitTorrent is a common protocol to truncate, so just use "Torrent" # if there's not enough room. if len(purpose) > spaceAvailable and purpose == "BitTorrent": purpose = "Torrent" # crops with a hyphen if too long purpose = uiTools.cropStr(purpose, spaceAvailable, endType = uiTools.Ending.HYPHEN) dstAddress += " (%s)" % purpose elif not connections.isIpAddressPrivate(self.foreign.getIpAddr()): extraInfo = [] conn = torTools.getConn() if includeLocale and not conn.isGeoipUnavailable(): foreignLocale = self.foreign.getLocale("??") extraInfo.append(foreignLocale) spaceAvailable -= len(foreignLocale) + 2 if includeHostname: dstHostname = self.foreign.getHostname() if dstHostname: # determines the full space available, taking into account the ", " # dividers if there's multiple pieces of extra data maxHostnameSpace = spaceAvailable - 2 * len(extraInfo) dstHostname = uiTools.cropStr(dstHostname, maxHostnameSpace) extraInfo.append(dstHostname) spaceAvailable -= len(dstHostname) if extraInfo: dstAddress += " (%s)" % ", ".join(extraInfo) return dstAddress[:maxLength]
def getDestinationLabel(self, maxLength, includeLocale=False, includeHostname=False): """ Provides a short description of the destination. This is made up of two components, the base <ip addr>:<port> and an extra piece of information in parentheses. The IP address is scrubbed from private connections. Extra information is... - the port's purpose for exit connections - the locale and/or hostname if set to do so, the address isn't private, and isn't on the local network - nothing otherwise Arguments: maxLength - maximum length of the string returned includeLocale - possibly includes the locale includeHostname - possibly includes the hostname """ # the port and port derived data can be hidden by config or without includePort includePort = self.includePort and ( CONFIG["features.connection.showExitPort"] or self.getType() != Category.EXIT) # destination of the connection ipLabel = "<scrubbed>" if self.isPrivate() else self.foreign.getIpAddr( ) portLabel = ":%s" % self.foreign.getPort() if includePort else "" dstAddress = ipLabel + portLabel # Only append the extra info if there's at least a couple characters of # space (this is what's needed for the country codes). if len(dstAddress) + 5 <= maxLength: spaceAvailable = maxLength - len(dstAddress) - 3 if self.getType() == Category.EXIT and includePort: purpose = connections.getPortUsage(self.foreign.getPort()) if purpose: # BitTorrent is a common protocol to truncate, so just use "Torrent" # if there's not enough room. if len(purpose ) > spaceAvailable and purpose == "BitTorrent": purpose = "Torrent" # crops with a hyphen if too long purpose = uiTools.cropStr(purpose, spaceAvailable, endType=uiTools.Ending.HYPHEN) dstAddress += " (%s)" % purpose elif not connections.isIpAddressPrivate(self.foreign.getIpAddr()): extraInfo = [] conn = torTools.getConn() if includeLocale and not conn.isGeoipUnavailable(): foreignLocale = self.foreign.getLocale("??") extraInfo.append(foreignLocale) spaceAvailable -= len(foreignLocale) + 2 if includeHostname: dstHostname = self.foreign.getHostname() if dstHostname: # determines the full space available, taking into account the ", " # dividers if there's multiple pieces of extra data maxHostnameSpace = spaceAvailable - 2 * len(extraInfo) dstHostname = uiTools.cropStr(dstHostname, maxHostnameSpace) extraInfo.append(dstHostname) spaceAvailable -= len(dstHostname) if extraInfo: dstAddress += " (%s)" % ", ".join(extraInfo) return dstAddress[:maxLength]
def _drawSelectionPanel(self, cursorSelection, width, detailPanelHeight, titleLabel): """ Renders a panel for the selected configuration option. """ # border (top) if width >= len(titleLabel): self.win.hline(0, len(titleLabel), curses.ACS_HLINE, width - len(titleLabel)) self.win.addch(0, width, curses.ACS_URCORNER) # border (sides) self.win.vline(1, 0, curses.ACS_VLINE, detailPanelHeight - 1) self.win.vline(1, width, curses.ACS_VLINE, detailPanelHeight - 1) # border (bottom) self.win.addch(detailPanelHeight, 0, curses.ACS_LLCORNER) if width >= 2: self.win.addch(detailPanelHeight, 1, curses.ACS_TTEE) if width >= 3: self.win.hline(detailPanelHeight, 2, curses.ACS_HLINE, width - 2) self.win.addch(detailPanelHeight, width, curses.ACS_LRCORNER) selectionFormat = curses.A_BOLD | uiTools.getColor( CATEGORY_COLOR[cursorSelection.get(FIELD_CATEGORY)]) # first entry: # <option> (<category> Option) optionLabel = " (%s Option)" % torConfig.OPTION_CATEGORY_STR[ cursorSelection.get(FIELD_CATEGORY)] self.addstr(1, 2, cursorSelection.get(FIELD_OPTION) + optionLabel, selectionFormat) # second entry: # Value: <value> ([default|custom], <type>, usage: <argument usage>) if detailPanelHeight >= 3: valueAttr = [] valueAttr.append("default" if cursorSelection.get(FIELD_IS_DEFAULT ) else "custom") valueAttr.append(cursorSelection.get(FIELD_TYPE)) valueAttr.append("usage: %s" % (cursorSelection.get(FIELD_ARG_USAGE))) valueAttrLabel = ", ".join(valueAttr) valueLabelWidth = width - 12 - len(valueAttrLabel) valueLabel = uiTools.cropStr(cursorSelection.get(FIELD_VALUE), valueLabelWidth) self.addstr(2, 2, "Value: %s (%s)" % (valueLabel, valueAttrLabel), selectionFormat) # remainder is filled with the man page description descriptionHeight = max(0, detailPanelHeight - 3) descriptionContent = "Description: " + cursorSelection.get( FIELD_DESCRIPTION) for i in range(descriptionHeight): # checks if we're done writing the description if not descriptionContent: break # there's a leading indent after the first line if i > 0: descriptionContent = " " + descriptionContent # we only want to work with content up until the next newline if "\n" in descriptionContent: lineContent, descriptionContent = descriptionContent.split( "\n", 1) else: lineContent, descriptionContent = descriptionContent, "" if i != descriptionHeight - 1: # there's more lines to display msg, remainder = uiTools.cropStr(lineContent, width - 2, 4, 4, uiTools.END_WITH_HYPHEN, True) descriptionContent = remainder.strip() + descriptionContent else: # this is the last line, end it with an ellipse msg = uiTools.cropStr(lineContent, width - 2, 4, 4) self.addstr(3 + i, 2, msg, selectionFormat)
def _getListingContent(self, width, listingType): """ Provides the source, destination, and extra info for our listing. Arguments: width - maximum length of the line listingType - primary attribute we're listing connections by """ conn = torTools.getConn() myType = self.getType() dstAddress = self.getDestinationLabel(26, includeLocale=True) # The required widths are the sum of the following: # - room for LABEL_FORMAT and LABEL_MIN_PADDING (11 characters) # - base data for the listing # - that extra field plus any previous usedSpace = len(LABEL_FORMAT % tuple([""] * 4)) + LABEL_MIN_PADDING localPort = ":%s" % self.local.getPort() if self.includePort else "" src, dst, etc = "", "", "" if listingType == entries.ListingType.IP_ADDRESS: myExternalIpAddr = conn.getInfo("address", self.local.getIpAddr()) addrDiffer = myExternalIpAddr != self.local.getIpAddr() # Expanding doesn't make sense, if the connection isn't actually # going through Tor's external IP address. As there isn't a known # method for checking if it is, we're checking the type instead. # # This isn't entirely correct. It might be a better idea to check if # the source and destination addresses are both private, but that might # not be perfectly reliable either. isExpansionType = not myType in (Category.SOCKS, Category.HIDDEN, Category.CONTROL) if isExpansionType: srcAddress = myExternalIpAddr + localPort else: srcAddress = self.local.getIpAddr() + localPort if myType in (Category.SOCKS, Category.CONTROL): # Like inbound connections these need their source and destination to # be swapped. However, this only applies when listing by IP or hostname # (their fingerprint and nickname are both for us). Reversing the # fields here to keep the same column alignments. src = "%-21s" % dstAddress dst = "%-26s" % srcAddress else: src = "%-21s" % srcAddress # ip:port = max of 21 characters dst = "%-26s" % dstAddress # ip:port (xx) = max of 26 characters usedSpace += len(src) + len( dst) # base data requires 47 characters # Showing the fingerprint (which has the width of 42) has priority over # an expanded address field. Hence check if we either have space for # both or wouldn't be showing the fingerprint regardless. isExpandedAddrVisible = width > usedSpace + 28 if isExpandedAddrVisible and CONFIG[ "features.connection.showColumn.fingerprint"]: isExpandedAddrVisible = width < usedSpace + 42 or width > usedSpace + 70 if addrDiffer and isExpansionType and isExpandedAddrVisible and self.includeExpandedIpAddr and CONFIG[ "features.connection.showColumn.expandedIp"]: # include the internal address in the src (extra 28 characters) internalAddress = self.local.getIpAddr() + localPort # If this is an inbound connection then reverse ordering so it's: # <foreign> --> <external> --> <internal> # when the src and dst are swapped later if myType == Category.INBOUND: src = "%-21s --> %s" % (src, internalAddress) else: src = "%-21s --> %s" % (internalAddress, src) usedSpace += 28 etc = self.getEtcContent(width - usedSpace, listingType) usedSpace += len(etc) elif listingType == entries.ListingType.HOSTNAME: # 15 characters for source, and a min of 40 reserved for the destination # TODO: when actually functional the src and dst need to be swapped for # SOCKS and CONTROL connections src = "localhost%-6s" % localPort usedSpace += len(src) minHostnameSpace = 40 etc = self.getEtcContent(width - usedSpace - minHostnameSpace, listingType) usedSpace += len(etc) hostnameSpace = width - usedSpace usedSpace = width # prevents padding at the end if self.isPrivate(): dst = ("%%-%is" % hostnameSpace) % "<scrubbed>" else: hostname = self.foreign.getHostname(self.foreign.getIpAddr()) portLabel = ":%-5s" % self.foreign.getPort( ) if self.includePort else "" # truncates long hostnames and sets dst to <hostname>:<port> hostname = uiTools.cropStr(hostname, hostnameSpace, 0) dst = ("%%-%is" % hostnameSpace) % (hostname + portLabel) elif listingType == entries.ListingType.FINGERPRINT: src = "localhost" if myType == Category.CONTROL: dst = "localhost" else: dst = self.foreign.getFingerprint() dst = "%-40s" % dst usedSpace += len(src) + len( dst) # base data requires 49 characters etc = self.getEtcContent(width - usedSpace, listingType) usedSpace += len(etc) else: # base data requires 50 min characters src = self.local.getNickname() if myType == Category.CONTROL: dst = self.local.getNickname() else: dst = self.foreign.getNickname() minBaseSpace = 50 etc = self.getEtcContent(width - usedSpace - minBaseSpace, listingType) usedSpace += len(etc) baseSpace = width - usedSpace usedSpace = width # prevents padding at the end if len(src) + len(dst) > baseSpace: src = uiTools.cropStr(src, baseSpace / 3) dst = uiTools.cropStr(dst, baseSpace - len(src)) # pads dst entry to its max space dst = ("%%-%is" % (baseSpace - len(src))) % dst if myType == Category.INBOUND: src, dst = dst, src padding = " " * (width - usedSpace + LABEL_MIN_PADDING) return LABEL_FORMAT % (src, dst, etc, padding)
def _getDetailContent(self, width): """ Provides a list with detailed information for this connection. Arguments: width - max length of lines """ lines = [""] * 7 lines[0] = "address: %s" % self.getDestinationLabel(width - 11) lines[1] = "locale: %s" % ("??" if self.isPrivate() else self.foreign.getLocale("??")) # Remaining data concerns the consensus results, with three possible cases: # - if there's a single match then display its details # - if there's multiple potential relays then list all of the combinations # of ORPorts / Fingerprints # - if no consensus data is available then say so (probably a client or # exit connection) fingerprint = self.foreign.getFingerprint() conn = torTools.getConn() if fingerprint != "UNKNOWN": # single match - display information available about it nsEntry = conn.getConsensusEntry(fingerprint) descEntry = conn.getDescriptorEntry(fingerprint) # append the fingerprint to the second line lines[1] = "%-13sfingerprint: %s" % (lines[1], fingerprint) if nsEntry: # example consensus entry: # r murble R8sCM1ar1sS2GulQYFVmvN95xsk RJr6q+wkTFG+ng5v2bdCbVVFfA4 2011-02-21 00:25:32 195.43.157.85 443 0 # s Exit Fast Guard Named Running Stable Valid # w Bandwidth=2540 # p accept 20-23,43,53,79-81,88,110,143,194,443 nsLines = nsEntry.split("\n") firstLineComp = nsLines[0].split(" ") if len(firstLineComp) >= 9: _, nickname, _, _, pubDate, pubTime, _, orPort, dirPort = firstLineComp[: 9] else: nickname, pubDate, pubTime, orPort, dirPort = "", "", "", "", "" flags = "unknown" if len(nsLines) >= 2 and nsLines[1].startswith("s "): flags = nsLines[1][2:] exitPolicy = conn.getRelayExitPolicy(fingerprint) if exitPolicy: policyLabel = exitPolicy.getSummary() else: policyLabel = "unknown" dirPortLabel = "" if dirPort == "0" else "dirport: %s" % dirPort lines[2] = "nickname: %-25s orport: %-10s %s" % ( nickname, orPort, dirPortLabel) lines[3] = "published: %s %s" % (pubTime, pubDate) lines[4] = "flags: %s" % flags.replace(" ", ", ") lines[5] = "exit policy: %s" % policyLabel if descEntry: torVersion, platform, contact = "", "", "" for descLine in descEntry.split("\n"): if descLine.startswith("platform"): # has the tor version and platform, ex: # platform Tor 0.2.1.29 (r318f470bc5f2ad43) on Linux x86_64 torVersion = descLine[13:descLine.find(" ", 13)] platform = descLine[descLine.rfind(" on ") + 4:] elif descLine.startswith("contact"): contact = descLine[8:] # clears up some highly common obscuring for alias in (" at ", " AT "): contact = contact.replace(alias, "@") for alias in (" dot ", " DOT "): contact = contact.replace(alias, ".") break # contact lines come after the platform lines[3] = "%-35s os: %-14s version: %s" % (lines[3], platform, torVersion) # contact information is an optional field if contact: lines[6] = "contact: %s" % contact else: allMatches = conn.getRelayFingerprint(self.foreign.getIpAddr(), getAllMatches=True) if allMatches: # multiple matches lines[2] = "Multiple matches, possible fingerprints are:" for i in range(len(allMatches)): isLastLine = i == 3 relayPort, relayFingerprint = allMatches[i] lineText = "%i. or port: %-5s fingerprint: %s" % ( i, relayPort, relayFingerprint) # if there's multiple lines remaining at the end then give a count remainingRelays = len(allMatches) - i if isLastLine and remainingRelays > 1: lineText = "... %i more" % remainingRelays lines[3 + i] = lineText if isLastLine: break else: # no consensus entry for this ip address lines[2] = "No consensus data found" # crops any lines that are too long for i in range(len(lines)): lines[i] = uiTools.cropStr(lines[i], width - 2) return lines
def getEtcContent(self, width, listingType): """ Provides the optional content for the connection. Arguments: width - maximum length of the line listingType - primary attribute we're listing connections by """ # for applications show the command/pid if self.getType() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL): displayLabel = "" if self.appName: if self.appPid: displayLabel = "%s (%s)" % (self.appName, self.appPid) else: displayLabel = self.appName elif self.isAppResolving: displayLabel = "resolving..." else: displayLabel = "UNKNOWN" if len(displayLabel) < width: return ("%%-%is" % width) % displayLabel else: return "" # for everything else display connection/consensus information dstAddress = self.getDestinationLabel(26, includeLocale=True) etc, usedSpace = "", 0 if listingType == entries.ListingType.IP_ADDRESS: if width > usedSpace + 42 and CONFIG[ "features.connection.showColumn.fingerprint"]: # show fingerprint (column width: 42 characters) etc += "%-40s " % self.foreign.getFingerprint() usedSpace += 42 if width > usedSpace + 10 and CONFIG[ "features.connection.showColumn.nickname"]: # show nickname (column width: remainder) nicknameSpace = width - usedSpace nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0) etc += ("%%-%is " % nicknameSpace) % nicknameLabel usedSpace += nicknameSpace + 2 elif listingType == entries.ListingType.HOSTNAME: if width > usedSpace + 28 and CONFIG[ "features.connection.showColumn.destination"]: # show destination ip/port/locale (column width: 28 characters) etc += "%-26s " % dstAddress usedSpace += 28 if width > usedSpace + 42 and CONFIG[ "features.connection.showColumn.fingerprint"]: # show fingerprint (column width: 42 characters) etc += "%-40s " % self.foreign.getFingerprint() usedSpace += 42 if width > usedSpace + 17 and CONFIG[ "features.connection.showColumn.nickname"]: # show nickname (column width: min 17 characters, uses half of the remainder) nicknameSpace = 15 + (width - (usedSpace + 17)) / 2 nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0) etc += ("%%-%is " % nicknameSpace) % nicknameLabel usedSpace += (nicknameSpace + 2) elif listingType == entries.ListingType.FINGERPRINT: if width > usedSpace + 17: # show nickname (column width: min 17 characters, consumes any remaining space) nicknameSpace = width - usedSpace - 2 # if there's room then also show a column with the destination # ip/port/locale (column width: 28 characters) isIpLocaleIncluded = width > usedSpace + 45 isIpLocaleIncluded &= CONFIG[ "features.connection.showColumn.destination"] if isIpLocaleIncluded: nicknameSpace -= 28 if CONFIG["features.connection.showColumn.nickname"]: nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0) etc += ("%%-%is " % nicknameSpace) % nicknameLabel usedSpace += nicknameSpace + 2 if isIpLocaleIncluded: etc += "%-26s " % dstAddress usedSpace += 28 else: if width > usedSpace + 42 and CONFIG[ "features.connection.showColumn.fingerprint"]: # show fingerprint (column width: 42 characters) etc += "%-40s " % self.foreign.getFingerprint() usedSpace += 42 if width > usedSpace + 28 and CONFIG[ "features.connection.showColumn.destination"]: # show destination ip/port/locale (column width: 28 characters) etc += "%-26s " % dstAddress usedSpace += 28 return ("%%-%is" % width) % etc
def draw(popup, fingerprint, displayText, displayColor, scroll, showLineNumber): popup.win.erase() popup.win.box() xOffset = 2 if fingerprint: title = "Consensus Descriptor (%s):" % fingerprint else: title = "Consensus Descriptor:" popup.addstr(0, 0, title, curses.A_STANDOUT) lineNumWidth = int(math.log10(len(displayText))) + 1 isEncryptionBlock = False # flag indicating if we're currently displaying a key # checks if first line is in an encryption block for i in range(0, scroll): lineText = displayText[i].strip() if lineText in SIG_START_KEYS: isEncryptionBlock = True elif lineText in SIG_END_KEYS: isEncryptionBlock = False drawLine, pageHeight = 1, popup.maxY - 2 for i in range(scroll, scroll + pageHeight): lineText = displayText[i].strip() xOffset = 2 if showLineNumber: lineNumLabel = ("%%%ii" % lineNumWidth) % (i + 1) lineNumFormat = curses.A_BOLD | uiTools.getColor(LINE_NUM_COLOR) popup.addstr(drawLine, xOffset, lineNumLabel, lineNumFormat) xOffset += lineNumWidth + 1 # Most consensus and descriptor lines are keyword/value pairs. Both are # shown with the same color, but the keyword is bolded. keyword, value = lineText, "" drawFormat = uiTools.getColor(displayColor) if lineText.startswith(HEADER_PREFIX[0]) or lineText.startswith( HEADER_PREFIX[1]): keyword, value = lineText, "" drawFormat = uiTools.getColor(HEADER_COLOR) elif lineText == UNRESOLVED_MSG or lineText == ERROR_MSG: keyword, value = lineText, "" elif lineText in SIG_START_KEYS: keyword, value = lineText, "" isEncryptionBlock = True drawFormat = uiTools.getColor(SIG_COLOR) elif lineText in SIG_END_KEYS: keyword, value = lineText, "" isEncryptionBlock = False drawFormat = uiTools.getColor(SIG_COLOR) elif isEncryptionBlock: keyword, value = "", lineText drawFormat = uiTools.getColor(SIG_COLOR) elif " " in lineText: divIndex = lineText.find(" ") keyword, value = lineText[:divIndex], lineText[divIndex:] displayQueue = [(keyword, drawFormat | curses.A_BOLD), (value, drawFormat)] cursorLoc = xOffset while displayQueue: msg, format = displayQueue.pop(0) if not msg: continue maxMsgSize = popup.maxX - 1 - cursorLoc if len(msg) >= maxMsgSize: # needs to split up the line msg, remainder = uiTools.cropStr(msg, maxMsgSize, None, endType=None, getRemainder=True) if xOffset == cursorLoc and msg == "": # first word is longer than the line msg = uiTools.cropStr(remainder, maxMsgSize) if " " in remainder: remainder = remainder.split(" ", 1)[1] else: remainder = "" popup.addstr(drawLine, cursorLoc, msg, format) cursorLoc = xOffset if remainder: displayQueue.insert(0, (remainder.strip(), format)) drawLine += 1 else: popup.addstr(drawLine, cursorLoc, msg, format) cursorLoc += len(msg) if drawLine > pageHeight: break drawLine += 1 if drawLine > pageHeight: break popup.win.refresh()
def draw(self, width, height): self.valsLock.acquire() # If true, we assume that the cached value in self._lastContentHeight is # still accurate, and stop drawing when there's nothing more to display. # Otherwise the self._lastContentHeight is suspect, and we'll process all # the content to check if it's right (and redraw again with the corrected # height if not). trustLastContentHeight = self._lastContentHeightArgs == (width, height) # restricts scroll location to valid bounds self.scroll = max( 0, min(self.scroll, self._lastContentHeight - height + 1)) renderedContents, corrections, confLocation = None, {}, None if self.configType == Config.TORRC: loadedTorrc = torConfig.getTorrc() loadedTorrc.getLock().acquire() confLocation = loadedTorrc.getConfigLocation() if not loadedTorrc.isLoaded(): renderedContents = ["### Unable to load the torrc ###"] else: renderedContents = loadedTorrc.getDisplayContents( self.stripComments) # constructs a mapping of line numbers to the issue on it corrections = dict( (lineNum, (issue, msg)) for lineNum, issue, msg in loadedTorrc.getCorrections()) loadedTorrc.getLock().release() else: loadedArmrc = conf.getConfig("arm") confLocation = loadedArmrc.path renderedContents = list(loadedArmrc.rawContents) # offset to make room for the line numbers lineNumOffset = 0 if self.showLineNum: if len(renderedContents) == 0: lineNumOffset = 2 else: lineNumOffset = int(math.log10(len(renderedContents))) + 2 # draws left-hand scroll bar if content's longer than the height scrollOffset = 0 if self._config[ "features.config.file.showScrollbars"] and self._lastContentHeight > height - 1: scrollOffset = 3 self.addScrollBar(self.scroll, self.scroll + height - 1, self._lastContentHeight, 1) displayLine = -self.scroll + 1 # line we're drawing on # draws the top label if self.isTitleVisible(): sourceLabel = "Tor" if self.configType == Config.TORRC else "Arm" locationLabel = " (%s)" % confLocation if confLocation else "" self.addstr( 0, 0, "%s Configuration File%s:" % (sourceLabel, locationLabel), curses.A_STANDOUT) isMultiline = False # true if we're in the middle of a multiline torrc entry for lineNumber in range(0, len(renderedContents)): lineText = renderedContents[lineNumber] lineText = lineText.rstrip() # remove ending whitespace # blank lines are hidden when stripping comments if self.stripComments and not lineText: continue # splits the line into its component (msg, format) tuples lineComp = { "option": ["", curses.A_BOLD | uiTools.getColor("green")], "argument": ["", curses.A_BOLD | uiTools.getColor("cyan")], "correction": ["", curses.A_BOLD | uiTools.getColor("cyan")], "comment": ["", uiTools.getColor("white")] } # parses the comment commentIndex = lineText.find("#") if commentIndex != -1: lineComp["comment"][0] = lineText[commentIndex:] lineText = lineText[:commentIndex] # splits the option and argument, preserving any whitespace around them strippedLine = lineText.strip() optionIndex = strippedLine.find(" ") if isMultiline: # part of a multiline entry started on a previous line so everything # is part of the argument lineComp["argument"][0] = lineText elif optionIndex == -1: # no argument provided lineComp["option"][0] = lineText else: optionText = strippedLine[:optionIndex] optionEnd = lineText.find(optionText) + len(optionText) lineComp["option"][0] = lineText[:optionEnd] lineComp["argument"][0] = lineText[optionEnd:] # flags following lines as belonging to this multiline entry if it ends # with a slash if strippedLine: isMultiline = strippedLine.endswith("\\") # gets the correction if lineNumber in corrections: lineIssue, lineIssueMsg = corrections[lineNumber] if lineIssue in (torConfig.ValidationError.DUPLICATE, torConfig.ValidationError.IS_DEFAULT): lineComp["option"][1] = curses.A_BOLD | uiTools.getColor( "blue") lineComp["argument"][1] = curses.A_BOLD | uiTools.getColor( "blue") elif lineIssue == torConfig.ValidationError.MISMATCH: lineComp["argument"][1] = curses.A_BOLD | uiTools.getColor( "red") lineComp["correction"][0] = " (%s)" % lineIssueMsg else: # For some types of configs the correction field is simply used to # provide extra data (for instance, the type for tor state fields). lineComp["correction"][0] = " (%s)" % lineIssueMsg lineComp["correction"][ 1] = curses.A_BOLD | uiTools.getColor("magenta") # draws the line number if self.showLineNum and displayLine < height and displayLine >= 1: lineNumStr = ("%%%ii" % (lineNumOffset - 1)) % (lineNumber + 1) self.addstr(displayLine, scrollOffset, lineNumStr, curses.A_BOLD | uiTools.getColor("yellow")) # draws the rest of the components with line wrap cursorLoc, lineOffset = lineNumOffset + scrollOffset, 0 maxLinesPerEntry = self._config[ "features.config.file.maxLinesPerEntry"] displayQueue = [ lineComp[entry] for entry in ("option", "argument", "correction", "comment") ] while displayQueue: msg, format = displayQueue.pop(0) maxMsgSize, includeBreak = width - cursorLoc, False if len(msg) >= maxMsgSize: # message is too long - break it up if lineOffset == maxLinesPerEntry - 1: msg = uiTools.cropStr(msg, maxMsgSize) else: includeBreak = True msg, remainder = uiTools.cropStr( msg, maxMsgSize, 4, 4, uiTools.Ending.HYPHEN, True) displayQueue.insert(0, (remainder.strip(), format)) drawLine = displayLine + lineOffset if msg and drawLine < height and drawLine >= 1: self.addstr(drawLine, cursorLoc, msg, format) # If we're done, and have added content to this line, then start # further content on the next line. cursorLoc += len(msg) includeBreak |= not displayQueue and cursorLoc != lineNumOffset + scrollOffset if includeBreak: lineOffset += 1 cursorLoc = lineNumOffset + scrollOffset displayLine += max(lineOffset, 1) if trustLastContentHeight and displayLine >= height: break if not trustLastContentHeight: self._lastContentHeightArgs = (width, height) newContentHeight = displayLine + self.scroll - 1 if self._lastContentHeight != newContentHeight: self._lastContentHeight = newContentHeight self.redraw(True) self.valsLock.release()
def _drawSelectionPanel(self, cursorSelection, width, detailPanelHeight, titleLabel): """ Renders a panel for the selected configuration option. """ # border (top) if width >= len(titleLabel): self.win.hline(0, len(titleLabel), curses.ACS_HLINE, width - len(titleLabel)) self.win.addch(0, width, curses.ACS_URCORNER) # border (sides) self.win.vline(1, 0, curses.ACS_VLINE, detailPanelHeight - 1) self.win.vline(1, width, curses.ACS_VLINE, detailPanelHeight - 1) # border (bottom) self.win.addch(detailPanelHeight, 0, curses.ACS_LLCORNER) if width >= 2: self.win.addch(detailPanelHeight, 1, curses.ACS_TTEE) if width >= 3: self.win.hline(detailPanelHeight, 2, curses.ACS_HLINE, width - 2) self.win.addch(detailPanelHeight, width, curses.ACS_LRCORNER) selectionFormat = curses.A_BOLD | uiTools.getColor(CATEGORY_COLOR[cursorSelection.get(FIELD_CATEGORY)]) # first entry: # <option> (<category> Option) optionLabel =" (%s Option)" % torConfig.OPTION_CATEGORY_STR[cursorSelection.get(FIELD_CATEGORY)] self.addstr(1, 2, cursorSelection.get(FIELD_OPTION) + optionLabel, selectionFormat) # second entry: # Value: <value> ([default|custom], <type>, usage: <argument usage>) if detailPanelHeight >= 3: valueAttr = [] valueAttr.append("default" if cursorSelection.get(FIELD_IS_DEFAULT) else "custom") valueAttr.append(cursorSelection.get(FIELD_TYPE)) valueAttr.append("usage: %s" % (cursorSelection.get(FIELD_ARG_USAGE))) valueAttrLabel = ", ".join(valueAttr) valueLabelWidth = width - 12 - len(valueAttrLabel) valueLabel = uiTools.cropStr(cursorSelection.get(FIELD_VALUE), valueLabelWidth) self.addstr(2, 2, "Value: %s (%s)" % (valueLabel, valueAttrLabel), selectionFormat) # remainder is filled with the man page description descriptionHeight = max(0, detailPanelHeight - 3) descriptionContent = "Description: " + cursorSelection.get(FIELD_DESCRIPTION) for i in range(descriptionHeight): # checks if we're done writing the description if not descriptionContent: break # there's a leading indent after the first line if i > 0: descriptionContent = " " + descriptionContent # we only want to work with content up until the next newline if "\n" in descriptionContent: lineContent, descriptionContent = descriptionContent.split("\n", 1) else: lineContent, descriptionContent = descriptionContent, "" if i != descriptionHeight - 1: # there's more lines to display msg, remainder = uiTools.cropStr(lineContent, width - 2, 4, 4, uiTools.END_WITH_HYPHEN, True) descriptionContent = remainder.strip() + descriptionContent else: # this is the last line, end it with an ellipse msg = uiTools.cropStr(lineContent, width - 2, 4, 4) self.addstr(3 + i, 2, msg, selectionFormat)
def showConfirmationDialog(torrcContents, torrcLocation): """ Shows a confirmation dialog with the given torrc contents, returning CANCEL, NEXT, or BACK based on the selection. Arguments: torrcContents - lines of torrc contents to be presented torrcLocation - path where the torrc will be placed """ torrcLines = torrcContents.split("\n") options = ["Cancel", "Back to Setup", "Start Tor"] control = cli.controller.getController() screenHeight = control.getScreen().getmaxyx()[0] stickyHeight = sum([stickyPanel.getHeight() for stickyPanel in control.getStickyPanels()]) isScrollbarVisible = len(torrcLines) + stickyHeight + 5 > screenHeight xOffset = 3 if isScrollbarVisible else 0 popup, width, height = cli.popups.init(len(torrcLines) + 5, 84 + xOffset) if not popup: return False try: scroll, selection = 0, 2 curses.cbreak() while True: popup.win.erase() popup.win.box() # renders the scrollbar if isScrollbarVisible: popup.addScrollBar(scroll, scroll + height - 5, len(torrcLines), 1, height - 4, 1) # shows the path where the torrc will be placed titleMsg = "The following will be placed at '%s':" % torrcLocation popup.addstr(0, 0, titleMsg, curses.A_STANDOUT) # renders the torrc contents for i in range(scroll, min(len(torrcLines), height - 5 + scroll)): # parses the argument and comment from options option, arg, comment = uiTools.cropStr(torrcLines[i], width - 4 - xOffset), "", "" div = option.find("#") if div != -1: option, comment = option[:div], option[div:] div = option.strip().find(" ") if div != -1: option, arg = option[:div], option[div:] drawX = 2 + xOffset popup.addstr(i + 1 - scroll, drawX, option, curses.A_BOLD | uiTools.getColor("green")) drawX += len(option) popup.addstr(i + 1 - scroll, drawX, arg, curses.A_BOLD | uiTools.getColor("cyan")) drawX += len(arg) popup.addstr(i + 1 - scroll, drawX, comment, uiTools.getColor("white")) # divider between the torrc and the options popup.addch(height - 4, 0, curses.ACS_LTEE) popup.addch(height - 4, width, curses.ACS_RTEE) popup.hline(height - 4, 1, width - 1) if isScrollbarVisible: popup.addch(height - 4, 2, curses.ACS_BTEE) # renders the selection options confirmationMsg = "Run tor with the above configuration?" popup.addstr(height - 3, width - len(confirmationMsg) - 1, confirmationMsg, uiTools.getColor("green") | curses.A_BOLD) drawX = width - 1 for i in range(len(options) - 1, -1, -1): optionLabel = " %s " % options[i] drawX -= (len(optionLabel) + 4) selectionFormat = curses.A_STANDOUT if i == selection else curses.A_NORMAL popup.addstr(height - 2, drawX, "[", uiTools.getColor("green")) popup.addstr(height - 2, drawX + 1, optionLabel, uiTools.getColor("green") | selectionFormat | curses.A_BOLD) popup.addstr(height - 2, drawX + len(optionLabel) + 1, "]", uiTools.getColor("green")) drawX -= 1 # space gap between the options popup.win.refresh() key = cli.controller.getController().getScreen().getch() if key == curses.KEY_LEFT: selection = (selection - 1) % len(options) elif key == curses.KEY_RIGHT: selection = (selection + 1) % len(options) elif uiTools.isScrollKey(key): scroll = uiTools.getScrollPosition(key, scroll, height - 5, len(torrcLines)) elif uiTools.isSelectionKey(key): if selection == 0: return CANCEL elif selection == 1: return BACK else: return NEXT elif key in (27, ord('q'), ord('Q')): return CANCEL finally: cli.popups.finalize()
def draw(self, subwindow, width, height): self.valsLock.acquire() # draws the top label titleLabel = "%s Configuration:" % ("Tor" if self.configType == TOR_STATE else "Arm") self.addstr(0, 0, titleLabel, curses.A_STANDOUT) # panel with details for the current selection detailPanelHeight = self._config[ "features.config.selectionDetails.height"] if detailPanelHeight == 0 or detailPanelHeight + 2 >= height: # no detail panel detailPanelHeight = 0 scrollLoc = self.scroller.getScrollLoc(self.confContents, height - 1) cursorSelection = self.getSelection() else: # Shrink detail panel if there isn't sufficient room for the whole # thing. The extra line is for the bottom border. detailPanelHeight = min(height - 1, detailPanelHeight + 1) scrollLoc = self.scroller.getScrollLoc( self.confContents, height - 1 - detailPanelHeight) cursorSelection = self.getSelection() self._drawSelectionPanel(cursorSelection, width, detailPanelHeight, titleLabel) # draws left-hand scroll bar if content's longer than the height scrollOffset = 0 if len(self.confContents) > height - detailPanelHeight - 1: scrollOffset = 3 self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelHeight - 1, len(self.confContents), 1 + detailPanelHeight) optionWidth = self._config["features.config.state.colWidth.option"] valueWidth = self._config["features.config.state.colWidth.value"] descriptionWidth = max( 0, width - scrollOffset - optionWidth - valueWidth - 2) for lineNum in range(scrollLoc, len(self.confContents)): entry = self.confContents[lineNum] drawLine = lineNum + detailPanelHeight + 1 - scrollLoc optionLabel = uiTools.cropStr(entry.get(FIELD_OPTION), optionWidth) valueLabel = uiTools.cropStr(entry.get(FIELD_VALUE), valueWidth) # ends description at the first newline descriptionLabel = uiTools.cropStr( entry.get(FIELD_DESCRIPTION).split("\n")[0], descriptionWidth, None) lineFormat = curses.A_NORMAL if entry.get( FIELD_IS_DEFAULT) else curses.A_BOLD if entry.get(FIELD_CATEGORY): lineFormat |= uiTools.getColor( CATEGORY_COLOR[entry.get(FIELD_CATEGORY)]) if entry == cursorSelection: lineFormat |= curses.A_STANDOUT lineTextLayout = "%%-%is %%-%is %%-%is" % (optionWidth, valueWidth, descriptionWidth) lineText = lineTextLayout % (optionLabel, valueLabel, descriptionLabel) self.addstr(drawLine, scrollOffset, lineText, lineFormat) if drawLine >= height: break self.valsLock.release()
def doHelp(self, arg, outputEntry): """ Performs the '/help' operation, giving usage information for the given argument or a general summary if there wasn't one. """ arg = arg.upper() # If there's multiple arguments then just take the first. This is # particularly likely if they're trying to query a full command (for # instance "/help GETINFO version") arg = arg.split(" ")[0] # strip slash if someone enters an interpretor command (ex. "/help /help") if arg.startswith("/"): arg = arg[1:] if arg: if arg in HELP_OPTIONS: # Provides information for the tor or interpretor argument. This bolds # the usage information and indents the description after it. usage, description = HELP_OPTIONS[arg] outputEntry.append((usage + "\n", OUTPUT_FORMAT + (Attr.BOLD, ))) for line in description.split("\n"): outputEntry.append((" " + line + "\n", OUTPUT_FORMAT)) if arg == "GETINFO": # if this is the GETINFO option then also list the valid options infoOptions = torTools.getConn().getInfo("info/names") if infoOptions: for line in infoOptions.split("\n"): if line.startswith("config/*") or line.startswith("dir-usage"): continue lineMatch = re.match("^(.+) -- (.+)$", line) if lineMatch: opt, description = lineMatch.groups() outputEntry.append(("%-33s" % opt, OUTPUT_FORMAT + (Attr.BOLD, ))) outputEntry.append((" - %s\n" % description, OUTPUT_FORMAT)) elif arg == "GETCONF": # lists all of the configuration options confOptions = torTools.getConn().getInfo("config/names") if confOptions: confEntries = [opt.split(" ", 1)[0] for opt in confOptions.split("\n")] # displays two columns of 42 characters for i in range(0, len(confEntries), 2): lineEntries = confEntries[i : i+2] lineContent = "" for entry in lineEntries: lineContent += "%-42s" % entry outputEntry.append((lineContent + "\n", OUTPUT_FORMAT)) outputEntry.append(("For more information use '/help [CONFIG OPTION]'.", OUTPUT_FORMAT + (Attr.BOLD, ))) elif arg == "SIGNAL": # lists descriptions for all of the signals for signal, description in SIGNAL_DESCRIPTIONS: outputEntry.append(("%-15s" % signal, OUTPUT_FORMAT + (Attr.BOLD, ))) outputEntry.append((" - %s\n" % description, OUTPUT_FORMAT)) elif arg == "SETEVENTS": # lists all of the event types eventOptions = torTools.getConn().getInfo("events/names") if eventOptions: eventEntries = eventOptions.split() # displays four columns of 20 characters for i in range(0, len(eventEntries), 4): lineEntries = eventEntries[i : i+4] lineContent = "" for entry in lineEntries: lineContent += "%-20s" % entry outputEntry.append((lineContent + "\n", OUTPUT_FORMAT)) elif arg == "USEFEATURE": # lists the feature options featureOptions = torTools.getConn().getInfo("features/names") if featureOptions: outputEntry.append((featureOptions + "\n", OUTPUT_FORMAT)) elif arg in ("LOADCONF", "POSTDESCRIPTOR"): # gives a warning that this option isn't yet implemented outputEntry.append(("\n" + MULTILINE_UNIMPLEMENTED_NOTICE + "\n", ERROR_FORMAT)) else: # check if this is a configuration option manEntry = torConfig.getConfigDescription(arg) if manEntry: # provides basic usage information in bold, followed an indented # copy of the man page description (wrapped to eighty characters) helpTitle = "%s %s (category: %s)\n" % (manEntry.option, manEntry.argUsage, manEntry.category) outputEntry.append((helpTitle, OUTPUT_FORMAT + (Attr.BOLD, ))) descLines = manEntry.description.split("\n") for line in descLines: if not line: outputEntry.append(("\n", OUTPUT_FORMAT)) else: while line: drawPortion, line = uiTools.cropStr(line, 88, 4, 4, uiTools.Ending.HYPHEN, True) outputEntry.append((" %s\n" % drawPortion.strip(), OUTPUT_FORMAT)) else: outputEntry.append(("No help information available for '%s'..." % arg, ERROR_FORMAT)) else: # provides the GENERAL_HELP with everything bolded except descriptions for line in GENERAL_HELP.split("\n"): cmdStart = line.find(" - ") if cmdStart != -1: outputEntry.append((line[:cmdStart], OUTPUT_FORMAT + (Attr.BOLD, ))) outputEntry.append((line[cmdStart:] + "\n", OUTPUT_FORMAT)) else: outputEntry.append((line + "\n", OUTPUT_FORMAT + (Attr.BOLD, )))
def getEtcContent(self, width, listingType): """ Provides the optional content for the connection. Arguments: width - maximum length of the line listingType - primary attribute we're listing connections by """ # for applications show the command/pid if self.getType() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL): displayLabel = "" if self.appName: if self.appPid: displayLabel = "%s (%s)" % (self.appName, self.appPid) else: displayLabel = self.appName elif self.isAppResolving: displayLabel = "resolving..." else: displayLabel = "UNKNOWN" if len(displayLabel) < width: return ("%%-%is" % width) % displayLabel else: return "" # for everything else display connection/consensus information dstAddress = self.getDestinationLabel(26, includeLocale = True) etc, usedSpace = "", 0 if listingType == entries.ListingType.IP_ADDRESS: if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]: # show fingerprint (column width: 42 characters) etc += "%-40s " % self.foreign.getFingerprint() usedSpace += 42 if width > usedSpace + 10 and CONFIG["features.connection.showColumn.nickname"]: # show nickname (column width: remainder) nicknameSpace = width - usedSpace nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0) etc += ("%%-%is " % nicknameSpace) % nicknameLabel usedSpace += nicknameSpace + 2 elif listingType == entries.ListingType.HOSTNAME: if width > usedSpace + 28 and CONFIG["features.connection.showColumn.destination"]: # show destination ip/port/locale (column width: 28 characters) etc += "%-26s " % dstAddress usedSpace += 28 if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]: # show fingerprint (column width: 42 characters) etc += "%-40s " % self.foreign.getFingerprint() usedSpace += 42 if width > usedSpace + 17 and CONFIG["features.connection.showColumn.nickname"]: # show nickname (column width: min 17 characters, uses half of the remainder) nicknameSpace = 15 + (width - (usedSpace + 17)) / 2 nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0) etc += ("%%-%is " % nicknameSpace) % nicknameLabel usedSpace += (nicknameSpace + 2) elif listingType == entries.ListingType.FINGERPRINT: if width > usedSpace + 17: # show nickname (column width: min 17 characters, consumes any remaining space) nicknameSpace = width - usedSpace - 2 # if there's room then also show a column with the destination # ip/port/locale (column width: 28 characters) isIpLocaleIncluded = width > usedSpace + 45 isIpLocaleIncluded &= CONFIG["features.connection.showColumn.destination"] if isIpLocaleIncluded: nicknameSpace -= 28 if CONFIG["features.connection.showColumn.nickname"]: nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0) etc += ("%%-%is " % nicknameSpace) % nicknameLabel usedSpace += nicknameSpace + 2 if isIpLocaleIncluded: etc += "%-26s " % dstAddress usedSpace += 28 else: if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]: # show fingerprint (column width: 42 characters) etc += "%-40s " % self.foreign.getFingerprint() usedSpace += 42 if width > usedSpace + 28 and CONFIG["features.connection.showColumn.destination"]: # show destination ip/port/locale (column width: 28 characters) etc += "%-26s " % dstAddress usedSpace += 28 return ("%%-%is" % width) % etc
def showConfirmationDialog(torrcContents, torrcLocation): """ Shows a confirmation dialog with the given torrc contents, returning CANCEL, NEXT, or BACK based on the selection. Arguments: torrcContents - lines of torrc contents to be presented torrcLocation - path where the torrc will be placed """ torrcLines = torrcContents.split("\n") options = ["Cancel", "Back to Setup", "Start Tor"] control = cli.controller.getController() screenHeight = control.getScreen().getmaxyx()[0] stickyHeight = sum( [stickyPanel.getHeight() for stickyPanel in control.getStickyPanels()]) isScrollbarVisible = len(torrcLines) + stickyHeight + 5 > screenHeight xOffset = 3 if isScrollbarVisible else 0 popup, width, height = cli.popups.init(len(torrcLines) + 5, 84 + xOffset) if not popup: return False try: scroll, selection = 0, 2 curses.cbreak() while True: popup.win.erase() popup.win.box() # renders the scrollbar if isScrollbarVisible: popup.addScrollBar(scroll, scroll + height - 5, len(torrcLines), 1, height - 4, 1) # shows the path where the torrc will be placed titleMsg = "The following will be placed at '%s':" % torrcLocation popup.addstr(0, 0, titleMsg, curses.A_STANDOUT) # renders the torrc contents for i in range(scroll, min(len(torrcLines), height - 5 + scroll)): # parses the argument and comment from options option, arg, comment = uiTools.cropStr(torrcLines[i], width - 4 - xOffset), "", "" div = option.find("#") if div != -1: option, comment = option[:div], option[div:] div = option.strip().find(" ") if div != -1: option, arg = option[:div], option[div:] drawX = 2 + xOffset popup.addstr(i + 1 - scroll, drawX, option, curses.A_BOLD | uiTools.getColor("green")) drawX += len(option) popup.addstr(i + 1 - scroll, drawX, arg, curses.A_BOLD | uiTools.getColor("cyan")) drawX += len(arg) popup.addstr(i + 1 - scroll, drawX, comment, uiTools.getColor("white")) # divider between the torrc and the options popup.addch(height - 4, 0, curses.ACS_LTEE) popup.addch(height - 4, width, curses.ACS_RTEE) popup.hline(height - 4, 1, width - 1) if isScrollbarVisible: popup.addch(height - 4, 2, curses.ACS_BTEE) # renders the selection options confirmationMsg = "Run tor with the above configuration?" popup.addstr(height - 3, width - len(confirmationMsg) - 1, confirmationMsg, uiTools.getColor("green") | curses.A_BOLD) drawX = width - 1 for i in range(len(options) - 1, -1, -1): optionLabel = " %s " % options[i] drawX -= (len(optionLabel) + 4) selectionFormat = curses.A_STANDOUT if i == selection else curses.A_NORMAL popup.addstr(height - 2, drawX, "[", uiTools.getColor("green")) popup.addstr( height - 2, drawX + 1, optionLabel, uiTools.getColor("green") | selectionFormat | curses.A_BOLD) popup.addstr(height - 2, drawX + len(optionLabel) + 1, "]", uiTools.getColor("green")) drawX -= 1 # space gap between the options popup.win.refresh() key = cli.controller.getController().getScreen().getch() if key == curses.KEY_LEFT: selection = (selection - 1) % len(options) elif key == curses.KEY_RIGHT: selection = (selection + 1) % len(options) elif uiTools.isScrollKey(key): scroll = uiTools.getScrollPosition(key, scroll, height - 5, len(torrcLines)) elif uiTools.isSelectionKey(key): if selection == 0: return CANCEL elif selection == 1: return BACK else: return NEXT elif key in (27, ord('q'), ord('Q')): return CANCEL finally: cli.popups.finalize()
def _getDetailContent(self, width): """ Provides a list with detailed information for this connection. Arguments: width - max length of lines """ lines = [""] * 7 lines[0] = "address: %s" % self.getDestinationLabel(width - 11) lines[1] = "locale: %s" % ("??" if self.isPrivate() else self.foreign.getLocale("??")) # Remaining data concerns the consensus results, with three possible cases: # - if there's a single match then display its details # - if there's multiple potential relays then list all of the combinations # of ORPorts / Fingerprints # - if no consensus data is available then say so (probably a client or # exit connection) fingerprint = self.foreign.getFingerprint() conn = torTools.getConn() if fingerprint != "UNKNOWN": # single match - display information available about it nsEntry = conn.getConsensusEntry(fingerprint) descEntry = conn.getDescriptorEntry(fingerprint) # append the fingerprint to the second line lines[1] = "%-13sfingerprint: %s" % (lines[1], fingerprint) if nsEntry: # example consensus entry: # r murble R8sCM1ar1sS2GulQYFVmvN95xsk RJr6q+wkTFG+ng5v2bdCbVVFfA4 2011-02-21 00:25:32 195.43.157.85 443 0 # s Exit Fast Guard Named Running Stable Valid # w Bandwidth=2540 # p accept 20-23,43,53,79-81,88,110,143,194,443 nsLines = nsEntry.split("\n") firstLineComp = nsLines[0].split(" ") if len(firstLineComp) >= 9: _, nickname, _, _, pubDate, pubTime, _, orPort, dirPort = firstLineComp[:9] else: nickname, pubDate, pubTime, orPort, dirPort = "", "", "", "", "" flags = "unknown" if len(nsLines) >= 2 and nsLines[1].startswith("s "): flags = nsLines[1][2:] exitPolicy = conn.getRelayExitPolicy(fingerprint) if exitPolicy: policyLabel = exitPolicy.getSummary() else: policyLabel = "unknown" dirPortLabel = "" if dirPort == "0" else "dirport: %s" % dirPort lines[2] = "nickname: %-25s orport: %-10s %s" % (nickname, orPort, dirPortLabel) lines[3] = "published: %s %s" % (pubTime, pubDate) lines[4] = "flags: %s" % flags.replace(" ", ", ") lines[5] = "exit policy: %s" % policyLabel if descEntry: torVersion, platform, contact = "", "", "" for descLine in descEntry.split("\n"): if descLine.startswith("platform"): # has the tor version and platform, ex: # platform Tor 0.2.1.29 (r318f470bc5f2ad43) on Linux x86_64 torVersion = descLine[13:descLine.find(" ", 13)] platform = descLine[descLine.rfind(" on ") + 4:] elif descLine.startswith("contact"): contact = descLine[8:] # clears up some highly common obscuring for alias in (" at ", " AT "): contact = contact.replace(alias, "@") for alias in (" dot ", " DOT "): contact = contact.replace(alias, ".") break # contact lines come after the platform lines[3] = "%-35s os: %-14s version: %s" % (lines[3], platform, torVersion) # contact information is an optional field if contact: lines[6] = "contact: %s" % contact else: allMatches = conn.getRelayFingerprint(self.foreign.getIpAddr(), getAllMatches = True) if allMatches: # multiple matches lines[2] = "Multiple matches, possible fingerprints are:" for i in range(len(allMatches)): isLastLine = i == 3 relayPort, relayFingerprint = allMatches[i] lineText = "%i. or port: %-5s fingerprint: %s" % (i, relayPort, relayFingerprint) # if there's multiple lines remaining at the end then give a count remainingRelays = len(allMatches) - i if isLastLine and remainingRelays > 1: lineText = "... %i more" % remainingRelays lines[3 + i] = lineText if isLastLine: break else: # no consensus entry for this ip address lines[2] = "No consensus data found" # crops any lines that are too long for i in range(len(lines)): lines[i] = uiTools.cropStr(lines[i], width - 2) return lines
def _getTitle(self, width): """ Provides the label used for the panel, looking like: Events (ARM NOTICE - ERR, BW - filter: prepopulate): This truncates the attributes (with an ellipse) if too long, and condenses runlevel ranges if there's three or more in a row (for instance ARM_INFO, ARM_NOTICE, and ARM_WARN becomes "ARM_INFO - WARN"). Arguments: width - width constraint the label needs to fix in """ # usually the attributes used to make the label are decently static, so # provide cached results if they're unchanged self.valsLock.acquire() currentPattern = self.regexFilter.pattern if self.regexFilter else None isUnchanged = self._titleArgs[0] == self.loggedEvents isUnchanged &= self._titleArgs[1] == currentPattern isUnchanged &= self._titleArgs[2] == width if isUnchanged: self.valsLock.release() return self._titleCache eventsList = list(self.loggedEvents) if not eventsList: if not currentPattern: panelLabel = "Events:" else: labelPattern = uiTools.cropStr(currentPattern, width - 18) panelLabel = "Events (filter: %s):" % labelPattern else: # does the following with all runlevel types (tor, arm, and torctl): # - pulls to the start of the list # - condenses range if there's three or more in a row (ex. "ARM_INFO - WARN") # - condense further if there's identical runlevel ranges for multiple # types (ex. "NOTICE - ERR, ARM_NOTICE - ERR" becomes "TOR/ARM NOTICE - ERR") tmpRunlevels = [ ] # runlevels pulled from the list (just the runlevel part) runlevelRanges = [ ] # tuple of type, startLevel, endLevel for ranges to be consensed # reverses runlevels and types so they're appended in the right order reversedRunlevels = list(RUNLEVELS) reversedRunlevels.reverse() for prefix in ("TORCTL_", "ARM_", ""): # blank ending runlevel forces the break condition to be reached at the end for runlevel in reversedRunlevels + [""]: eventType = prefix + runlevel if runlevel and eventType in eventsList: # runlevel event found, move to the tmp list eventsList.remove(eventType) tmpRunlevels.append(runlevel) elif tmpRunlevels: # adds all tmp list entries to the start of eventsList if len(tmpRunlevels) >= 3: # save condense sequential runlevels to be added later runlevelRanges.append( (prefix, tmpRunlevels[-1], tmpRunlevels[0])) else: # adds runlevels individaully for tmpRunlevel in tmpRunlevels: eventsList.insert(0, prefix + tmpRunlevel) tmpRunlevels = [] # adds runlevel ranges, condensing if there's identical ranges for i in range(len(runlevelRanges)): if runlevelRanges[i]: prefix, startLevel, endLevel = runlevelRanges[i] # check for matching ranges matches = [] for j in range(i + 1, len(runlevelRanges)): if runlevelRanges[j] and runlevelRanges[j][ 1] == startLevel and runlevelRanges[j][ 2] == endLevel: matches.append(runlevelRanges[j]) runlevelRanges[j] = None if matches: # strips underscores and replaces empty entries with "TOR" prefixes = [entry[0] for entry in matches] + [prefix] for k in range(len(prefixes)): if prefixes[k] == "": prefixes[k] = "TOR" else: prefixes[k] = prefixes[k].replace("_", "") eventsList.insert( 0, "%s %s - %s" % ("/".join(prefixes), startLevel, endLevel)) else: eventsList.insert( 0, "%s%s - %s" % (prefix, startLevel, endLevel)) # truncates to use an ellipsis if too long, for instance: attrLabel = ", ".join(eventsList) if currentPattern: attrLabel += " - filter: %s" % currentPattern attrLabel = uiTools.cropStr(attrLabel, width - 10, 1) if attrLabel: attrLabel = " (%s)" % attrLabel panelLabel = "Events%s:" % attrLabel # cache results and return self._titleCache = panelLabel self._titleArgs = (list(self.loggedEvents), currentPattern, width) self.valsLock.release() return panelLabel
def draw(self, subwindow, width, height): self.valsLock.acquire() isWide = width + 1 >= MIN_DUAL_COL_WIDTH # space available for content if isWide: leftWidth = max(width / 2, 77) rightWidth = width - leftWidth else: leftWidth = rightWidth = width # Line 1 / Line 1 Left (system and tor version information) sysNameLabel = "arm - %s" % self.vals["sys/hostname"] contentSpace = min(leftWidth, 40) if len(sysNameLabel) + 10 <= contentSpace: sysTypeLabel = "%s %s" % (self.vals["sys/os"], self.vals["sys/version"]) sysTypeLabel = uiTools.cropStr(sysTypeLabel, contentSpace - len(sysNameLabel) - 3, 4) self.addstr(0, 0, "%s (%s)" % (sysNameLabel, sysTypeLabel)) else: self.addstr(0, 0, uiTools.cropStr(sysNameLabel, contentSpace)) contentSpace = leftWidth - 43 if 7 + len(self.vals["tor/version"]) + len(self.vals["tor/versionStatus"]) <= contentSpace: versionColor = VERSION_STATUS_COLORS[self.vals["tor/versionStatus"]] if \ self.vals["tor/versionStatus"] in VERSION_STATUS_COLORS else "white" versionStatusMsg = "<%s>%s</%s>" % (versionColor, self.vals["tor/versionStatus"], versionColor) self.addfstr(0, 43, "Tor %s (%s)" % (self.vals["tor/version"], versionStatusMsg)) elif 11 <= contentSpace: self.addstr(0, 43, uiTools.cropStr("Tor %s" % self.vals["tor/version"], contentSpace, 4)) # Line 2 / Line 2 Left (tor ip/port information) if self.vals["tor/orPort"]: # acting as a relay (we can assume certain parameters are set entry = "" dirPortLabel = ", Dir Port: %s" % self.vals["tor/dirPort"] if self.vals["tor/dirPort"] != "0" else "" for label in (self.vals["tor/nickname"], " - " + self.vals["tor/address"], ":" + self.vals["tor/orPort"], dirPortLabel): if len(entry) + len(label) <= leftWidth: entry += label else: break else: # non-relay (client only) # TODO: not sure what sort of stats to provide... entry = "<red><b>Relaying Disabled</b></red>" if self.vals["tor/isAuthPassword"]: authType = "password" elif self.vals["tor/isAuthCookie"]: authType = "cookie" else: authType = "open" if len(entry) + 19 + len(self.vals["tor/controlPort"]) + len(authType) <= leftWidth: authColor = "red" if authType == "open" else "green" authLabel = "<%s>%s</%s>" % (authColor, authType, authColor) self.addfstr(1, 0, "%s, Control Port (%s): %s" % (entry, authLabel, self.vals["tor/controlPort"])) elif len(entry) + 16 + len(self.vals["tor/controlPort"]) <= leftWidth: self.addstr(1, 0, "%s, Control Port: %s" % (entry, self.vals["tor/controlPort"])) else: self.addstr(1, 0, entry) # Line 3 / Line 1 Right (system usage info) y, x = (0, leftWidth) if isWide else (2, 0) if self.vals["stat/rss"] != "0": memoryLabel = uiTools.getSizeLabel(int(self.vals["stat/rss"]) * 1024) else: memoryLabel = "0" sysFields = ((0, "cpu: %s%% tor, %s%% arm" % (self.vals["stat/%torCpu"], self.vals["stat/%armCpu"])), (27, "mem: %s (%s%%)" % (memoryLabel, self.vals["stat/%mem"])), (47, "pid: %s" % (self.vals["stat/pid"] if self._isTorConnected else "")), (59, "uptime: %s" % self.vals["stat/etime"])) for (start, label) in sysFields: if start + len(label) <= rightWidth: self.addstr(y, x + start, label) else: break if self.vals["tor/orPort"]: # Line 4 / Line 2 Right (fingerprint) y, x = (1, leftWidth) if isWide else (3, 0) fingerprintLabel = uiTools.cropStr("fingerprint: %s" % self.vals["tor/fingerprint"], width) self.addstr(y, x, fingerprintLabel) # Line 5 / Line 3 Left (flags) if self._isTorConnected: flagLine = "flags: " for flag in self.vals["tor/flags"]: flagColor = FLAG_COLORS[flag] if flag in FLAG_COLORS.keys() else "white" flagLine += "<b><%s>%s</%s></b>, " % (flagColor, flag, flagColor) if len(self.vals["tor/flags"]) > 0: flagLine = flagLine[:-2] else: flagLine += "<b><cyan>none</cyan></b>" self.addfstr(2 if isWide else 4, 0, flagLine) else: statusTime = torTools.getConn().getStatus()[1] statusTimeLabel = time.strftime("%H:%M %m/%d/%Y", time.localtime(statusTime)) self.addfstr(2 if isWide else 4, 0, "<b><red>Tor Disconnected</red></b> (%s)" % statusTimeLabel) # Undisplayed / Line 3 Right (exit policy) if isWide: exitPolicy = self.vals["tor/exitPolicy"] # adds note when default exit policy is appended if exitPolicy == "": exitPolicy = "<default>" elif not exitPolicy.endswith((" *:*", " *")): exitPolicy += ", <default>" # color codes accepts to be green, rejects to be red, and default marker to be cyan isSimple = len(exitPolicy) > rightWidth - 13 policies = exitPolicy.split(", ") for i in range(len(policies)): policy = policies[i].strip() displayedPolicy = policy.replace("accept", "").replace("reject", "").strip() if isSimple else policy if policy.startswith("accept"): policy = "<green><b>%s</b></green>" % displayedPolicy elif policy.startswith("reject"): policy = "<red><b>%s</b></red>" % displayedPolicy elif policy.startswith("<default>"): policy = "<cyan><b>%s</b></cyan>" % displayedPolicy policies[i] = policy self.addfstr(2, leftWidth, "exit policy: %s" % ", ".join(policies)) else: # Client only # TODO: not sure what information to provide here... pass self._isLastDrawWide = isWide self._isChanged = False self.valsLock.release()
def showWriteDialog(self): """ Provies an interface to confirm if the configuration is saved and, if so, where. """ # display a popup for saving the current configuration configLines = torConfig.getCustomOptions(True) popup, width, height = popups.init(len(configLines) + 2) if not popup: return try: # displayed options (truncating the labels if there's limited room) if width >= 30: selectionOptions = ("Save", "Save As...", "Cancel") else: selectionOptions = ("Save", "Save As", "X") # checks if we can show options beside the last line of visible content isOptionLineSeparate = False lastIndex = min(height - 2, len(configLines) - 1) # if we don't have room to display the selection options and room to # grow then display the selection options on its own line if width < (30 + len(configLines[lastIndex])): popup.setHeight(height + 1) popup.redraw(True) # recreates the window instance newHeight, _ = popup.getPreferredSize() if newHeight > height: height = newHeight isOptionLineSeparate = True key, selection = 0, 2 while not uiTools.isSelectionKey(key): # if the popup has been resized then recreate it (needed for the # proper border height) newHeight, newWidth = popup.getPreferredSize() if (height, width) != (newHeight, newWidth): height, width = newHeight, newWidth popup.redraw(True) # if there isn't room to display the popup then cancel it if height <= 2: selection = 2 break popup.win.erase() popup.win.box() popup.addstr(0, 0, "Configuration being saved:", curses.A_STANDOUT) visibleConfigLines = height - 3 if isOptionLineSeparate else height - 2 for i in range(visibleConfigLines): line = uiTools.cropStr(configLines[i], width - 2) if " " in line: option, arg = line.split(" ", 1) popup.addstr(i + 1, 1, option, curses.A_BOLD | uiTools.getColor("green")) popup.addstr(i + 1, len(option) + 2, arg, curses.A_BOLD | uiTools.getColor("cyan")) else: popup.addstr(i + 1, 1, line, curses.A_BOLD | uiTools.getColor("green")) # draws selection options (drawn right to left) drawX = width - 1 for i in range(len(selectionOptions) - 1, -1, -1): optionLabel = selectionOptions[i] drawX -= (len(optionLabel) + 2) # if we've run out of room then drop the option (this will only # occure on tiny displays) if drawX < 1: break selectionFormat = curses.A_STANDOUT if i == selection else curses.A_NORMAL popup.addstr(height - 2, drawX, "[") popup.addstr(height - 2, drawX + 1, optionLabel, selectionFormat | curses.A_BOLD) popup.addstr(height - 2, drawX + len(optionLabel) + 1, "]") drawX -= 1 # space gap between the options popup.win.refresh() key = cli.controller.getController().getScreen().getch() if key == curses.KEY_LEFT: selection = max(0, selection - 1) elif key == curses.KEY_RIGHT: selection = min(len(selectionOptions) - 1, selection + 1) if selection in (0, 1): loadedTorrc, promptCanceled = torConfig.getTorrc(), False try: configLocation = loadedTorrc.getConfigLocation() except IOError: configLocation = "" if selection == 1: # prompts user for a configuration location configLocation = popups.inputPrompt("Save to (esc to cancel): ", configLocation) if not configLocation: promptCanceled = True if not promptCanceled: try: torConfig.saveConf(configLocation, configLines) msg = "Saved configuration to %s" % configLocation except IOError, exc: msg = "Unable to save configuration (%s)" % sysTools.getFileErrorMsg(exc) popups.showMsg(msg, 2) finally: popups.finalize()
def draw(self, width, height): """ Redraws message log. Entries stretch to use available space and may contain up to two lines. Starts with newest entries. """ currentLog = self.getAttr("msgLog") self.valsLock.acquire() self._lastLoggedEvents, self._lastUpdate = list(currentLog), time.time() # draws the top label if self.isTitleVisible(): self.addstr(0, 0, self._getTitle(width), curses.A_STANDOUT) # restricts scroll location to valid bounds self.scroll = max(0, min(self.scroll, self.lastContentHeight - height + 1)) # draws left-hand scroll bar if content's longer than the height msgIndent, dividerIndent = 1, 0 # offsets for scroll bar isScrollBarVisible = self.lastContentHeight > height - 1 if isScrollBarVisible: msgIndent, dividerIndent = 3, 2 self.addScrollBar(self.scroll, self.scroll + height - 1, self.lastContentHeight, 1) # draws log entries lineCount = 1 - self.scroll seenFirstDateDivider = False dividerAttr, duplicateAttr = curses.A_BOLD | uiTools.getColor("yellow"), curses.A_BOLD | uiTools.getColor("green") isDatesShown = self.regexFilter == None and self._config["features.log.showDateDividers"] eventLog = getDaybreaks(currentLog, self.isPaused()) if isDatesShown else list(currentLog) if not self.showDuplicates: deduplicatedLog = getDuplicates(eventLog) if deduplicatedLog == None: msg = "Deduplication took too long. Its current implementation has difficulty handling large logs so disabling it to keep the interface responsive." log.log(log.WARN, msg) self.showDuplicates = True deduplicatedLog = [(entry, 0) for entry in eventLog] else: deduplicatedLog = [(entry, 0) for entry in eventLog] # determines if we have the minimum width to show date dividers showDaybreaks = width - dividerIndent >= 3 while deduplicatedLog: entry, duplicateCount = deduplicatedLog.pop(0) if self.regexFilter and not self.regexFilter.search(entry.getDisplayMessage()): continue # filter doesn't match log message - skip # checks if we should be showing a divider with the date if entry.type == DAYBREAK_EVENT: # bottom of the divider if seenFirstDateDivider: if lineCount >= 1 and lineCount < height and showDaybreaks: self.addch(lineCount, dividerIndent, curses.ACS_LLCORNER, dividerAttr) self.hline(lineCount, dividerIndent + 1, width - dividerIndent - 2, dividerAttr) self.addch(lineCount, width - 1, curses.ACS_LRCORNER, dividerAttr) lineCount += 1 # top of the divider if lineCount >= 1 and lineCount < height and showDaybreaks: timeLabel = time.strftime(" %B %d, %Y ", time.localtime(entry.timestamp)) self.addch(lineCount, dividerIndent, curses.ACS_ULCORNER, dividerAttr) self.addch(lineCount, dividerIndent + 1, curses.ACS_HLINE, dividerAttr) self.addstr(lineCount, dividerIndent + 2, timeLabel, curses.A_BOLD | dividerAttr) lineLength = width - dividerIndent - len(timeLabel) - 3 self.hline(lineCount, dividerIndent + len(timeLabel) + 2, lineLength, dividerAttr) self.addch(lineCount, dividerIndent + len(timeLabel) + 2 + lineLength, curses.ACS_URCORNER, dividerAttr) seenFirstDateDivider = True lineCount += 1 else: # entry contents to be displayed, tuples of the form: # (msg, formatting, includeLinebreak) displayQueue = [] msgComp = entry.getDisplayMessage().split("\n") for i in range(len(msgComp)): font = curses.A_BOLD if "ERR" in entry.type else curses.A_NORMAL # emphasizes ERR messages displayQueue.append((msgComp[i].strip(), font | uiTools.getColor(entry.color), i != len(msgComp) - 1)) if duplicateCount: pluralLabel = "s" if duplicateCount > 1 else "" duplicateMsg = DUPLICATE_MSG % (duplicateCount, pluralLabel) displayQueue.append((duplicateMsg, duplicateAttr, False)) cursorLoc, lineOffset = msgIndent, 0 maxEntriesPerLine = self._config["features.log.maxLinesPerEntry"] while displayQueue: msg, format, includeBreak = displayQueue.pop(0) drawLine = lineCount + lineOffset if lineOffset == maxEntriesPerLine: break maxMsgSize = width - cursorLoc - 1 if len(msg) > maxMsgSize: # message is too long - break it up if lineOffset == maxEntriesPerLine - 1: msg = uiTools.cropStr(msg, maxMsgSize) else: msg, remainder = uiTools.cropStr(msg, maxMsgSize, 4, 4, uiTools.Ending.HYPHEN, True) displayQueue.insert(0, (remainder.strip(), format, includeBreak)) includeBreak = True if drawLine < height and drawLine >= 1: if seenFirstDateDivider and width - dividerIndent >= 3 and showDaybreaks: self.addch(drawLine, dividerIndent, curses.ACS_VLINE, dividerAttr) self.addch(drawLine, width - 1, curses.ACS_VLINE, dividerAttr) self.addstr(drawLine, cursorLoc, msg, format) cursorLoc += len(msg) if includeBreak or not displayQueue: lineOffset += 1 cursorLoc = msgIndent + ENTRY_INDENT lineCount += lineOffset # if this is the last line and there's room, then draw the bottom of the divider if not deduplicatedLog and seenFirstDateDivider: if lineCount < height and showDaybreaks: self.addch(lineCount, dividerIndent, curses.ACS_LLCORNER, dividerAttr) self.hline(lineCount, dividerIndent + 1, width - dividerIndent - 2, dividerAttr) self.addch(lineCount, width - 1, curses.ACS_LRCORNER, dividerAttr) lineCount += 1 # redraw the display if... # - lastContentHeight was off by too much # - we're off the bottom of the page newContentHeight = lineCount + self.scroll - 1 contentHeightDelta = abs(self.lastContentHeight - newContentHeight) forceRedraw, forceRedrawReason = True, "" if contentHeightDelta >= CONTENT_HEIGHT_REDRAW_THRESHOLD: forceRedrawReason = "estimate was off by %i" % contentHeightDelta elif newContentHeight > height and self.scroll + height - 1 > newContentHeight: forceRedrawReason = "scrolled off the bottom of the page" elif not isScrollBarVisible and newContentHeight > height - 1: forceRedrawReason = "scroll bar wasn't previously visible" elif isScrollBarVisible and newContentHeight <= height - 1: forceRedrawReason = "scroll bar shouldn't be visible" else: forceRedraw = False self.lastContentHeight = newContentHeight if forceRedraw: forceRedrawReason = "redrawing the log panel with the corrected content height (%s)" % forceRedrawReason log.log(self._config["log.logPanel.forceDoubleRedraw"], forceRedrawReason) self.redraw(True) self.valsLock.release()
def draw(self, width, height): self.valsLock.acquire() isWide = width + 1 >= MIN_DUAL_COL_WIDTH # space available for content if isWide: leftWidth = max(width / 2, 77) rightWidth = width - leftWidth else: leftWidth = rightWidth = width # Line 1 / Line 1 Left (system and tor version information) sysNameLabel = "arm - %s" % self.vals["sys/hostname"] contentSpace = min(leftWidth, 40) if len(sysNameLabel) + 10 <= contentSpace: sysTypeLabel = "%s %s" % (self.vals["sys/os"], self.vals["sys/version"]) sysTypeLabel = uiTools.cropStr(sysTypeLabel, contentSpace - len(sysNameLabel) - 3, 4) self.addstr(0, 0, "%s (%s)" % (sysNameLabel, sysTypeLabel)) else: self.addstr(0, 0, uiTools.cropStr(sysNameLabel, contentSpace)) contentSpace = leftWidth - 43 if 7 + len(self.vals["tor/version"]) + len(self.vals["tor/versionStatus"]) <= contentSpace: if self.vals["tor/version"] != "Unknown": versionColor = VERSION_STATUS_COLORS[self.vals["tor/versionStatus"]] if \ self.vals["tor/versionStatus"] in VERSION_STATUS_COLORS else "white" labelPrefix = "Tor %s (" % self.vals["tor/version"] self.addstr(0, 43, labelPrefix) self.addstr(0, 43 + len(labelPrefix), self.vals["tor/versionStatus"], uiTools.getColor(versionColor)) self.addstr(0, 43 + len(labelPrefix) + len(self.vals["tor/versionStatus"]), ")") elif 11 <= contentSpace: self.addstr(0, 43, uiTools.cropStr("Tor %s" % self.vals["tor/version"], contentSpace, 4)) # Line 2 / Line 2 Left (tor ip/port information) x, includeControlPort = 0, True if self.vals["tor/orPort"]: myAddress = "Unknown" if self.vals["tor/orListenAddr"]: myAddress = self.vals["tor/orListenAddr"] elif self.vals["tor/address"]: myAddress = self.vals["tor/address"] # acting as a relay (we can assume certain parameters are set dirPortLabel = ", Dir Port: %s" % self.vals["tor/dirPort"] if self.vals["tor/dirPort"] != "0" else "" for label in (self.vals["tor/nickname"], " - " + myAddress, ":" + self.vals["tor/orPort"], dirPortLabel): if x + len(label) <= leftWidth: self.addstr(1, x, label) x += len(label) else: break else: # non-relay (client only) if self._isTorConnected: self.addstr(1, x, "Relaying Disabled", uiTools.getColor("cyan")) x += 17 else: statusTime = torTools.getConn().getStatus()[1] if statusTime: statusTimeLabel = time.strftime("%H:%M %m/%d/%Y, ", time.localtime(statusTime)) else: statusTimeLabel = "" # never connected to tor self.addstr(1, x, "Tor Disconnected", curses.A_BOLD | uiTools.getColor("red")) self.addstr(1, x + 16, " (%spress r to reconnect)" % statusTimeLabel) x += 39 + len(statusTimeLabel) includeControlPort = False if includeControlPort: if self.vals["tor/controlPort"] == "0": # connected via a control socket self.addstr(1, x, ", Control Socket: %s" % self.vals["tor/socketPath"]) else: if self.vals["tor/isAuthPassword"]: authType = "password" elif self.vals["tor/isAuthCookie"]: authType = "cookie" else: authType = "open" if x + 19 + len(self.vals["tor/controlPort"]) + len(authType) <= leftWidth: authColor = "red" if authType == "open" else "green" self.addstr(1, x, ", Control Port (") self.addstr(1, x + 16, authType, uiTools.getColor(authColor)) self.addstr(1, x + 16 + len(authType), "): %s" % self.vals["tor/controlPort"]) elif x + 16 + len(self.vals["tor/controlPort"]) <= leftWidth: self.addstr(1, 0, ", Control Port: %s" % self.vals["tor/controlPort"]) # Line 3 / Line 1 Right (system usage info) y, x = (0, leftWidth) if isWide else (2, 0) if self.vals["stat/rss"] != "0": memoryLabel = uiTools.getSizeLabel(int(self.vals["stat/rss"])) else: memoryLabel = "0" uptimeLabel = "" if self.vals["tor/startTime"]: if self.isPaused() or not self._isTorConnected: # freeze the uptime when paused or the tor process is stopped uptimeLabel = uiTools.getShortTimeLabel(self.getPauseTime() - self.vals["tor/startTime"]) else: uptimeLabel = uiTools.getShortTimeLabel(time.time() - self.vals["tor/startTime"]) sysFields = ((0, "cpu: %s%% tor, %s%% arm" % (self.vals["stat/%torCpu"], self.vals["stat/%armCpu"])), (27, "mem: %s (%s%%)" % (memoryLabel, self.vals["stat/%mem"])), (47, "pid: %s" % (self.vals["tor/pid"] if self._isTorConnected else "")), (59, "uptime: %s" % uptimeLabel)) for (start, label) in sysFields: if start + len(label) <= rightWidth: self.addstr(y, x + start, label) else: break if self.vals["tor/orPort"]: # Line 4 / Line 2 Right (fingerprint, and possibly file descriptor usage) y, x = (1, leftWidth) if isWide else (3, 0) fingerprintLabel = uiTools.cropStr("fingerprint: %s" % self.vals["tor/fingerprint"], width) self.addstr(y, x, fingerprintLabel) # if there's room and we're able to retrieve both the file descriptor # usage and limit then it might be presented if width - x - 59 >= 20 and self.vals["tor/fdUsed"] and self.vals["tor/fdLimit"]: # display file descriptor usage if we're either configured to do so or # running out fdPercent = 100 * self.vals["tor/fdUsed"] / self.vals["tor/fdLimit"] if fdPercent >= 60 or self._config["features.showFdUsage"]: fdPercentLabel, fdPercentFormat = "%i%%" % fdPercent, curses.A_NORMAL if fdPercent >= 95: fdPercentFormat = curses.A_BOLD | uiTools.getColor("red") elif fdPercent >= 90: fdPercentFormat = uiTools.getColor("red") elif fdPercent >= 60: fdPercentFormat = uiTools.getColor("yellow") estimateChar = "?" if self.vals["tor/isFdLimitEstimate"] else "" baseLabel = "file desc: %i / %i%s (" % (self.vals["tor/fdUsed"], self.vals["tor/fdLimit"], estimateChar) self.addstr(y, x + 59, baseLabel) self.addstr(y, x + 59 + len(baseLabel), fdPercentLabel, fdPercentFormat) self.addstr(y, x + 59 + len(baseLabel) + len(fdPercentLabel), ")") # Line 5 / Line 3 Left (flags) if self._isTorConnected: y, x = (2 if isWide else 4, 0) self.addstr(y, x, "flags: ") x += 7 if len(self.vals["tor/flags"]) > 0: for i in range(len(self.vals["tor/flags"])): flag = self.vals["tor/flags"][i] flagColor = FLAG_COLORS[flag] if flag in FLAG_COLORS.keys() else "white" self.addstr(y, x, flag, curses.A_BOLD | uiTools.getColor(flagColor)) x += len(flag) if i < len(self.vals["tor/flags"]) - 1: self.addstr(y, x, ", ") x += 2 else: self.addstr(y, x, "none", curses.A_BOLD | uiTools.getColor("cyan")) else: y = 2 if isWide else 4 statusTime = torTools.getConn().getStatus()[1] statusTimeLabel = time.strftime("%H:%M %m/%d/%Y", time.localtime(statusTime)) self.addstr(y, 0, "Tor Disconnected", curses.A_BOLD | uiTools.getColor("red")) self.addstr(y, 16, " (%s) - press r to reconnect" % statusTimeLabel) # Undisplayed / Line 3 Right (exit policy) if isWide: exitPolicy = self.vals["tor/exitPolicy"] # adds note when default exit policy is appended if exitPolicy == "": exitPolicy = "<default>" elif not exitPolicy.endswith((" *:*", " *")): exitPolicy += ", <default>" self.addstr(2, leftWidth, "exit policy: ") x = leftWidth + 13 # color codes accepts to be green, rejects to be red, and default marker to be cyan isSimple = len(exitPolicy) > rightWidth - 13 policies = exitPolicy.split(", ") for i in range(len(policies)): policy = policies[i].strip() policyLabel = policy.replace("accept", "").replace("reject", "").strip() if isSimple else policy policyColor = "white" if policy.startswith("accept"): policyColor = "green" elif policy.startswith("reject"): policyColor = "red" elif policy.startswith("<default>"): policyColor = "cyan" self.addstr(2, x, policyLabel, curses.A_BOLD | uiTools.getColor(policyColor)) x += len(policyLabel) if i < len(policies) - 1: self.addstr(2, x, ", ") x += 2 else: # (Client only) Undisplayed / Line 2 Right (new identity option) if isWide: conn = torTools.getConn() newnymWait = conn.getNewnymWait() msg = "press 'n' for a new identity" if newnymWait > 0: pluralLabel = "s" if newnymWait > 1 else "" msg = "building circuits, available again in %i second%s" % (newnymWait, pluralLabel) self.addstr(1, leftWidth, msg) self.valsLock.release()
def draw(self, subwindow, width, height): self.valsLock.acquire() # If true, we assume that the cached value in self._lastContentHeight is # still accurate, and stop drawing when there's nothing more to display. # Otherwise the self._lastContentHeight is suspect, and we'll process all # the content to check if it's right (and redraw again with the corrected # height if not). trustLastContentHeight = self._lastContentHeightArgs == (width, height) # restricts scroll location to valid bounds self.scroll = max(0, min(self.scroll, self._lastContentHeight - height + 1)) renderedContents, corrections, confLocation = None, {}, None if self.configType == TORRC: loadedTorrc = torConfig.getTorrc() loadedTorrc.getLock().acquire() confLocation = loadedTorrc.getConfigLocation() if not loadedTorrc.isLoaded(): renderedContents = ["### Unable to load the torrc ###"] else: renderedContents = loadedTorrc.getDisplayContents(self.stripComments) # constructs a mapping of line numbers to the issue on it corrections = dict((lineNum, (issue, msg)) for lineNum, issue, msg in loadedTorrc.getCorrections()) loadedTorrc.getLock().release() else: loadedArmrc = conf.getConfig("arm") confLocation = loadedArmrc.path renderedContents = list(loadedArmrc.rawContents) # offset to make room for the line numbers lineNumOffset = 0 if self.showLineNum: if len(renderedContents) == 0: lineNumOffset = 2 else: lineNumOffset = int(math.log10(len(renderedContents))) + 2 # draws left-hand scroll bar if content's longer than the height scrollOffset = 0 if self._config["features.config.file.showScrollbars"] and self._lastContentHeight > height - 1: scrollOffset = 3 self.addScrollBar(self.scroll, self.scroll + height - 1, self._lastContentHeight, 1) displayLine = -self.scroll + 1 # line we're drawing on # draws the top label if self.showLabel: sourceLabel = "Tor" if self.configType == TORRC else "Arm" locationLabel = " (%s)" % confLocation if confLocation else "" self.addstr(0, 0, "%s Configuration File%s:" % (sourceLabel, locationLabel), curses.A_STANDOUT) isMultiline = False # true if we're in the middle of a multiline torrc entry for lineNumber in range(0, len(renderedContents)): lineText = renderedContents[lineNumber] lineText = lineText.rstrip() # remove ending whitespace # blank lines are hidden when stripping comments if self.stripComments and not lineText: continue # splits the line into its component (msg, format) tuples lineComp = {"option": ["", curses.A_BOLD | uiTools.getColor("green")], "argument": ["", curses.A_BOLD | uiTools.getColor("cyan")], "correction": ["", curses.A_BOLD | uiTools.getColor("cyan")], "comment": ["", uiTools.getColor("white")]} # parses the comment commentIndex = lineText.find("#") if commentIndex != -1: lineComp["comment"][0] = lineText[commentIndex:] lineText = lineText[:commentIndex] # splits the option and argument, preserving any whitespace around them strippedLine = lineText.strip() optionIndex = strippedLine.find(" ") if isMultiline: # part of a multiline entry started on a previous line so everything # is part of the argument lineComp["argument"][0] = lineText elif optionIndex == -1: # no argument provided lineComp["option"][0] = lineText else: optionText = strippedLine[:optionIndex] optionEnd = lineText.find(optionText) + len(optionText) lineComp["option"][0] = lineText[:optionEnd] lineComp["argument"][0] = lineText[optionEnd:] # flags following lines as belonging to this multiline entry if it ends # with a slash if strippedLine: isMultiline = strippedLine.endswith("\\") # gets the correction if lineNumber in corrections: lineIssue, lineIssueMsg = corrections[lineNumber] if lineIssue in (torConfig.VAL_DUPLICATE, torConfig.VAL_IS_DEFAULT): lineComp["option"][1] = curses.A_BOLD | uiTools.getColor("blue") lineComp["argument"][1] = curses.A_BOLD | uiTools.getColor("blue") elif lineIssue == torConfig.VAL_MISMATCH: lineComp["argument"][1] = curses.A_BOLD | uiTools.getColor("red") lineComp["correction"][0] = " (%s)" % lineIssueMsg else: # For some types of configs the correction field is simply used to # provide extra data (for instance, the type for tor state fields). lineComp["correction"][0] = " (%s)" % lineIssueMsg lineComp["correction"][1] = curses.A_BOLD | uiTools.getColor("magenta") # draws the line number if self.showLineNum and displayLine < height and displayLine >= 1: lineNumStr = ("%%%ii" % (lineNumOffset - 1)) % (lineNumber + 1) self.addstr(displayLine, scrollOffset, lineNumStr, curses.A_BOLD | uiTools.getColor("yellow")) # draws the rest of the components with line wrap cursorLoc, lineOffset = lineNumOffset + scrollOffset, 0 maxLinesPerEntry = self._config["features.config.file.maxLinesPerEntry"] displayQueue = [lineComp[entry] for entry in ("option", "argument", "correction", "comment")] while displayQueue: msg, format = displayQueue.pop(0) maxMsgSize, includeBreak = width - cursorLoc, False if len(msg) >= maxMsgSize: # message is too long - break it up if lineOffset == maxLinesPerEntry - 1: msg = uiTools.cropStr(msg, maxMsgSize) else: includeBreak = True msg, remainder = uiTools.cropStr(msg, maxMsgSize, 4, 4, uiTools.END_WITH_HYPHEN, True) displayQueue.insert(0, (remainder.strip(), format)) drawLine = displayLine + lineOffset if msg and drawLine < height and drawLine >= 1: self.addstr(drawLine, cursorLoc, msg, format) # If we're done, and have added content to this line, then start # further content on the next line. cursorLoc += len(msg) includeBreak |= not displayQueue and cursorLoc != lineNumOffset + scrollOffset if includeBreak: lineOffset += 1 cursorLoc = lineNumOffset + scrollOffset displayLine += max(lineOffset, 1) if trustLastContentHeight and displayLine >= height: break if not trustLastContentHeight: self._lastContentHeightArgs = (width, height) newContentHeight = displayLine + self.scroll - 1 if self._lastContentHeight != newContentHeight: self._lastContentHeight = newContentHeight self.redraw(True) self.valsLock.release()
def draw(self, width, height): self.valsLock.acquire() isWide = width + 1 >= MIN_DUAL_COL_WIDTH # space available for content if isWide: leftWidth = max(width / 2, 77) rightWidth = width - leftWidth else: leftWidth = rightWidth = width # Line 1 / Line 1 Left (system and tor version information) sysNameLabel = "arm - %s" % self.vals["sys/hostname"] contentSpace = min(leftWidth, 40) if len(sysNameLabel) + 10 <= contentSpace: sysTypeLabel = "%s %s" % (self.vals["sys/os"], self.vals["sys/version"]) sysTypeLabel = uiTools.cropStr( sysTypeLabel, contentSpace - len(sysNameLabel) - 3, 4) self.addstr(0, 0, "%s (%s)" % (sysNameLabel, sysTypeLabel)) else: self.addstr(0, 0, uiTools.cropStr(sysNameLabel, contentSpace)) contentSpace = leftWidth - 43 if 7 + len(self.vals["tor/version"]) + len( self.vals["tor/versionStatus"]) <= contentSpace: if self.vals["tor/version"] != "Unknown": versionColor = VERSION_STATUS_COLORS[self.vals["tor/versionStatus"]] if \ self.vals["tor/versionStatus"] in VERSION_STATUS_COLORS else "white" labelPrefix = "Tor %s (" % self.vals["tor/version"] self.addstr(0, 43, labelPrefix) self.addstr(0, 43 + len(labelPrefix), self.vals["tor/versionStatus"], uiTools.getColor(versionColor)) self.addstr( 0, 43 + len(labelPrefix) + len(self.vals["tor/versionStatus"]), ")") elif 11 <= contentSpace: self.addstr( 0, 43, uiTools.cropStr("Tor %s" % self.vals["tor/version"], contentSpace, 4)) # Line 2 / Line 2 Left (tor ip/port information) x, includeControlPort = 0, True if self.vals["tor/orPort"]: myAddress = "Unknown" if self.vals["tor/orListenAddr"]: myAddress = self.vals["tor/orListenAddr"] elif self.vals["tor/address"]: myAddress = self.vals["tor/address"] # acting as a relay (we can assume certain parameters are set dirPortLabel = ", Dir Port: %s" % self.vals[ "tor/dirPort"] if self.vals["tor/dirPort"] != "0" else "" for label in (self.vals["tor/nickname"], " - " + myAddress, ":" + self.vals["tor/orPort"], dirPortLabel): if x + len(label) <= leftWidth: self.addstr(1, x, label) x += len(label) else: break else: # non-relay (client only) if self._isTorConnected: self.addstr(1, x, "Relaying Disabled", uiTools.getColor("cyan")) x += 17 else: statusTime = torTools.getConn().getHeartbeat() if statusTime: statusTimeLabel = time.strftime("%H:%M %m/%d/%Y, ", time.localtime(statusTime)) else: statusTimeLabel = "" # never connected to tor self.addstr(1, x, "Tor Disconnected", curses.A_BOLD | uiTools.getColor("red")) self.addstr(1, x + 16, " (%spress r to reconnect)" % statusTimeLabel) x += 39 + len(statusTimeLabel) includeControlPort = False if includeControlPort: if self.vals["tor/controlPort"] == "0": # connected via a control socket self.addstr( 1, x, ", Control Socket: %s" % self.vals["tor/socketPath"]) else: if self.vals["tor/isAuthPassword"]: authType = "password" elif self.vals["tor/isAuthCookie"]: authType = "cookie" else: authType = "open" if x + 19 + len(self.vals["tor/controlPort"]) + len( authType) <= leftWidth: authColor = "red" if authType == "open" else "green" self.addstr(1, x, ", Control Port (") self.addstr(1, x + 16, authType, uiTools.getColor(authColor)) self.addstr(1, x + 16 + len(authType), "): %s" % self.vals["tor/controlPort"]) elif x + 16 + len(self.vals["tor/controlPort"]) <= leftWidth: self.addstr( 1, 0, ", Control Port: %s" % self.vals["tor/controlPort"]) # Line 3 / Line 1 Right (system usage info) y, x = (0, leftWidth) if isWide else (2, 0) if self.vals["stat/rss"] != "0": memoryLabel = str_tools.get_size_label(int(self.vals["stat/rss"])) else: memoryLabel = "0" uptimeLabel = "" if self.vals["tor/startTime"]: if self.isPaused() or not self._isTorConnected: # freeze the uptime when paused or the tor process is stopped uptimeLabel = str_tools.get_short_time_label( self.getPauseTime() - self.vals["tor/startTime"]) else: uptimeLabel = str_tools.get_short_time_label( time.time() - self.vals["tor/startTime"]) sysFields = ((0, "cpu: %s%% tor, %s%% arm" % (self.vals["stat/%torCpu"], self.vals["stat/%armCpu"])), (27, "mem: %s (%s%%)" % (memoryLabel, self.vals["stat/%mem"])), (47, "pid: %s" % (self.vals["tor/pid"] if self._isTorConnected else "")), (59, "uptime: %s" % uptimeLabel)) for (start, label) in sysFields: if start + len(label) <= rightWidth: self.addstr(y, x + start, label) else: break if self.vals["tor/orPort"]: # Line 4 / Line 2 Right (fingerprint, and possibly file descriptor usage) y, x = (1, leftWidth) if isWide else (3, 0) fingerprintLabel = uiTools.cropStr( "fingerprint: %s" % self.vals["tor/fingerprint"], width) self.addstr(y, x, fingerprintLabel) # if there's room and we're able to retrieve both the file descriptor # usage and limit then it might be presented if width - x - 59 >= 20 and self.vals["tor/fdUsed"] and self.vals[ "tor/fdLimit"]: # display file descriptor usage if we're either configured to do so or # running out fdPercent = 100 * self.vals["tor/fdUsed"] / self.vals[ "tor/fdLimit"] if fdPercent >= 60 or CONFIG["features.showFdUsage"]: fdPercentLabel, fdPercentFormat = "%i%%" % fdPercent, curses.A_NORMAL if fdPercent >= 95: fdPercentFormat = curses.A_BOLD | uiTools.getColor( "red") elif fdPercent >= 90: fdPercentFormat = uiTools.getColor("red") elif fdPercent >= 60: fdPercentFormat = uiTools.getColor("yellow") estimateChar = "?" if self.vals[ "tor/isFdLimitEstimate"] else "" baseLabel = "file desc: %i / %i%s (" % ( self.vals["tor/fdUsed"], self.vals["tor/fdLimit"], estimateChar) self.addstr(y, x + 59, baseLabel) self.addstr(y, x + 59 + len(baseLabel), fdPercentLabel, fdPercentFormat) self.addstr(y, x + 59 + len(baseLabel) + len(fdPercentLabel), ")") # Line 5 / Line 3 Left (flags) if self._isTorConnected: y, x = (2 if isWide else 4, 0) self.addstr(y, x, "flags: ") x += 7 if len(self.vals["tor/flags"]) > 0: for i in range(len(self.vals["tor/flags"])): flag = self.vals["tor/flags"][i] flagColor = FLAG_COLORS[ flag] if flag in FLAG_COLORS.keys() else "white" self.addstr( y, x, flag, curses.A_BOLD | uiTools.getColor(flagColor)) x += len(flag) if i < len(self.vals["tor/flags"]) - 1: self.addstr(y, x, ", ") x += 2 else: self.addstr(y, x, "none", curses.A_BOLD | uiTools.getColor("cyan")) else: y = 2 if isWide else 4 statusTime = torTools.getConn().getHeartbeat() statusTimeLabel = time.strftime("%H:%M %m/%d/%Y", time.localtime(statusTime)) self.addstr(y, 0, "Tor Disconnected", curses.A_BOLD | uiTools.getColor("red")) self.addstr(y, 16, " (%s) - press r to reconnect" % statusTimeLabel) # Undisplayed / Line 3 Right (exit policy) if isWide: exitPolicy = self.vals["tor/exitPolicy"] # adds note when default exit policy is appended if exitPolicy == "": exitPolicy = "<default>" elif not exitPolicy.endswith((" *:*", " *")): exitPolicy += ", <default>" self.addstr(2, leftWidth, "exit policy: ") x = leftWidth + 13 # color codes accepts to be green, rejects to be red, and default marker to be cyan isSimple = len(exitPolicy) > rightWidth - 13 policies = exitPolicy.split(", ") for i in range(len(policies)): policy = policies[i].strip() policyLabel = policy.replace("accept", "").replace( "reject", "").strip() if isSimple else policy policyColor = "white" if policy.startswith("accept"): policyColor = "green" elif policy.startswith("reject"): policyColor = "red" elif policy.startswith("<default>"): policyColor = "cyan" self.addstr(2, x, policyLabel, curses.A_BOLD | uiTools.getColor(policyColor)) x += len(policyLabel) if i < len(policies) - 1: self.addstr(2, x, ", ") x += 2 else: # (Client only) Undisplayed / Line 2 Right (new identity option) if isWide: conn = torTools.getConn() newnymWait = conn.getNewnymWait() msg = "press 'n' for a new identity" if newnymWait > 0: pluralLabel = "s" if newnymWait > 1 else "" msg = "building circuits, available again in %i second%s" % ( newnymWait, pluralLabel) self.addstr(1, leftWidth, msg) self.valsLock.release()
def _getTitle(self, width): """ Provides the label used for the panel, looking like: Events (ARM NOTICE - ERR, BW - filter: prepopulate): This truncates the attributes (with an ellipse) if too long, and condenses runlevel ranges if there's three or more in a row (for instance ARM_INFO, ARM_NOTICE, and ARM_WARN becomes "ARM_INFO - WARN"). Arguments: width - width constraint the label needs to fix in """ # usually the attributes used to make the label are decently static, so # provide cached results if they're unchanged self.valsLock.acquire() currentPattern = self.regexFilter.pattern if self.regexFilter else None isUnchanged = self._titleArgs[0] == self.loggedEvents isUnchanged &= self._titleArgs[1] == currentPattern isUnchanged &= self._titleArgs[2] == width if isUnchanged: self.valsLock.release() return self._titleCache eventsList = list(self.loggedEvents) if not eventsList: if not currentPattern: panelLabel = "Events:" else: labelPattern = uiTools.cropStr(currentPattern, width - 18) panelLabel = "Events (filter: %s):" % labelPattern else: # does the following with all runlevel types (tor, arm, and stem): # - pulls to the start of the list # - condenses range if there's three or more in a row (ex. "ARM_INFO - WARN") # - condense further if there's identical runlevel ranges for multiple # types (ex. "NOTICE - ERR, ARM_NOTICE - ERR" becomes "TOR/ARM NOTICE - ERR") tmpRunlevels = [] # runlevels pulled from the list (just the runlevel part) runlevelRanges = [] # tuple of type, startLevel, endLevel for ranges to be consensed # reverses runlevels and types so they're appended in the right order reversedRunlevels = log.Runlevel.values() reversedRunlevels.reverse() for prefix in ("STEM_", "ARM_", ""): # blank ending runlevel forces the break condition to be reached at the end for runlevel in reversedRunlevels + [""]: eventType = prefix + runlevel if runlevel and eventType in eventsList: # runlevel event found, move to the tmp list eventsList.remove(eventType) tmpRunlevels.append(runlevel) elif tmpRunlevels: # adds all tmp list entries to the start of eventsList if len(tmpRunlevels) >= 3: # save condense sequential runlevels to be added later runlevelRanges.append((prefix, tmpRunlevels[-1], tmpRunlevels[0])) else: # adds runlevels individaully for tmpRunlevel in tmpRunlevels: eventsList.insert(0, prefix + tmpRunlevel) tmpRunlevels = [] # adds runlevel ranges, condensing if there's identical ranges for i in range(len(runlevelRanges)): if runlevelRanges[i]: prefix, startLevel, endLevel = runlevelRanges[i] # check for matching ranges matches = [] for j in range(i + 1, len(runlevelRanges)): if runlevelRanges[j] and runlevelRanges[j][1] == startLevel and runlevelRanges[j][2] == endLevel: matches.append(runlevelRanges[j]) runlevelRanges[j] = None if matches: # strips underscores and replaces empty entries with "TOR" prefixes = [entry[0] for entry in matches] + [prefix] for k in range(len(prefixes)): if prefixes[k] == "": prefixes[k] = "TOR" else: prefixes[k] = prefixes[k].replace("_", "") eventsList.insert(0, "%s %s - %s" % ("/".join(prefixes), startLevel, endLevel)) else: eventsList.insert(0, "%s%s - %s" % (prefix, startLevel, endLevel)) # truncates to use an ellipsis if too long, for instance: attrLabel = ", ".join(eventsList) if currentPattern: attrLabel += " - filter: %s" % currentPattern attrLabel = uiTools.cropStr(attrLabel, width - 10, 1) if attrLabel: attrLabel = " (%s)" % attrLabel panelLabel = "Events%s:" % attrLabel # cache results and return self._titleCache = panelLabel self._titleArgs = (list(self.loggedEvents), currentPattern, width) self.valsLock.release() return panelLabel