commit 4e61f4660c8c5bd5b5fa8a045dd2a39deada65cc Author: Damian Johnson atagar@torproject.org Date: Wed Aug 17 10:16:13 2011 -0700
Adding a dialog for showing usage stats
Dialog is enabled for bridges, guards, and exits to show client locale and exit port usage statistics. --- README | 1 + src/cli/connections/__init__.py | 2 +- src/cli/connections/connPanel.py | 74 ++++++++++++++++++++++++++++++- src/cli/connections/countPopup.py | 86 +++++++++++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+), 4 deletions(-)
diff --git a/README b/README index 119d24e..cbaf31a 100644 --- a/README +++ b/README @@ -174,6 +174,7 @@ Layout: connPanel.py - (page 2) lists the active tor connections circEntry.py - circuit entries in the connection panel connEntry.py - individual connections to or from the system + countPopup.py - displays client locale or exit port counts descriptorPopup.py - displays raw descriptor and consensus entries entries.py - common parent for connPanel display entries
diff --git a/src/cli/connections/__init__.py b/src/cli/connections/__init__.py index 0f29d23..abd3410 100644 --- a/src/cli/connections/__init__.py +++ b/src/cli/connections/__init__.py @@ -2,5 +2,5 @@ Connection panel related resources. """
-__all__ = ["circEntry", "connEntry", "connPanel", "descriptorPopup", "entries"] +__all__ = ["circEntry", "connEntry", "connPanel", "countPopup", "descriptorPopup", "entries"]
diff --git a/src/cli/connections/connPanel.py b/src/cli/connections/connPanel.py index 923902c..9745176 100644 --- a/src/cli/connections/connPanel.py +++ b/src/cli/connections/connPanel.py @@ -2,13 +2,14 @@ Listing of the currently established connections tor has made. """
+import re import time import curses import threading
import cli.popups
-from cli.connections import descriptorPopup, entries, connEntry, circEntry +from cli.connections import countPopup, descriptorPopup, entries, connEntry, circEntry from util import connections, enum, panel, torTools, uiTools
DEFAULT_CONFIG = {"features.connection.resolveApps": True, @@ -68,6 +69,32 @@ class ConnectionPanel(panel.Panel, threading.Thread): self._cond = threading.Condition() # used for pausing the thread self.valsLock = threading.RLock()
+ # Tracks exiting port and client country statistics + self._clientLocaleUsage = {} + self._exitPortUsage = {} + + # If we're a bridge and been running over a day then prepopulates with the + # last day's clients. + + conn = torTools.getConn() + bridgeClients = conn.getInfo("status/clients-seen") + + if bridgeClients: + # Response has a couple arguments... + # TimeStarted="2011-08-17 15:50:49" CountrySummary=us=16,de=8,uk=8 + + countrySummary = None + for arg in bridgeClients.split(): + if arg.startswith("CountrySummary="): + countrySummary = arg[15:] + break + + if countrySummary: + for entry in countrySummary.split(","): + if re.match("^..=[0-9]+$", entry): + locale, count = entry.split("=", 1) + self._clientLocaleUsage[locale] = int(count) + # Last sampling received from the ConnectionResolver, used to detect when # it changes. self._lastResourceFetch = -1 @@ -84,7 +111,7 @@ class ConnectionPanel(panel.Panel, threading.Thread): entry.getLines()[0].isInitialConnection = True
# listens for when tor stops so we know to stop reflecting changes - torTools.getConn().addStatusListener(self.torStateListener) + conn.addStatusListener(self.torStateListener)
def torStateListener(self, conn, eventType): """ @@ -155,6 +182,22 @@ class ConnectionPanel(panel.Panel, threading.Thread):
self.valsLock.release()
+ def isClientsAllowed(self): + """ + True if client connections are permissable, false otherwise. + """ + + conn = torTools.getConn() + return "Guard" in conn.getMyFlags([]) or conn.getOption("BridgeRelay") == "1" + + def isExitsAllowed(self): + """ + True if exit connections are permissable, false otherwise. + """ + + policy = torTools.getConn().getExitPolicy() + return policy and policy.isExitingAllowed() + def showSortDialog(self): """ Provides the sort dialog for our connections. @@ -214,6 +257,10 @@ class ConnectionPanel(panel.Panel, threading.Thread): elif key == ord('d') or key == ord('D'): # presents popup for raw consensus data descriptorPopup.showDescriptorPopup(self) + elif (key == ord('c') or key == ord('C')) and self.isClientsAllowed(): + countPopup.showCountDialog(countPopup.CountType.CLIENT_LOCALE, self._clientLocaleUsage) + elif (key == ord('e') or key == ord('E')) and self.isExitsAllowed(): + countPopup.showCountDialog(countPopup.CountType.EXIT_PORT, self._exitPortUsage) else: isKeystrokeConsumed = False
self.valsLock.release() @@ -263,6 +310,13 @@ class ConnectionPanel(panel.Panel, threading.Thread): options.append(("page down", "scroll down a page", None)) options.append(("enter", "edit configuration option", None)) options.append(("d", "raw consensus descriptor", None)) + + if self.isClientsAllowed(): + options.append(("c", "client locale usage summary", None)) + + if self.isExitsAllowed(): + options.append(("e", "exit port usage summary", None)) + options.append(("l", "listed identity", self._listingType.lower())) options.append(("s", "sort ordering", None)) options.append(("u", "resolving utility", resolverUtil)) @@ -411,8 +465,22 @@ class ConnectionPanel(panel.Panel, threading.Thread): # Adds any new connection and circuit entries. for lIp, lPort, fIp, fPort in newConnections: newConnEntry = connEntry.ConnectionEntry(lIp, lPort, fIp, fPort) - if newConnEntry.getLines()[0].getType() != connEntry.Category.CIRCUIT: + newConnLine = newConnEntry.getLines()[0] + + if newConnLine.getType() != connEntry.Category.CIRCUIT: newEntries.append(newConnEntry) + + # updates exit port and client locale usage information + if newConnLine.isPrivate(): + if newConnLine.getType() == connEntry.Category.INBOUND: + # client connection, update locale information + clientLocale = newConnLine.foreign.getLocale() + + if clientLocale: + self._clientLocaleUsage[clientLocale] = self._clientLocaleUsage.get(clientLocale, 0) + 1 + elif newConnLine.getType() == connEntry.Category.EXIT: + exitPort = newConnLine.foreign.getPort() + self._exitPortUsage[exitPort] = self._exitPortUsage.get(exitPort, 0) + 1
for circuitID in newCircuits: status, purpose, path = newCircuits[circuitID] diff --git a/src/cli/connections/countPopup.py b/src/cli/connections/countPopup.py new file mode 100644 index 0000000..341087f --- /dev/null +++ b/src/cli/connections/countPopup.py @@ -0,0 +1,86 @@ +""" +Provides a dialog with client locale or exiting port counts. +""" + +import curses +import operator + +import cli.controller +import cli.popups + +from util import enum, log, uiTools + +CountType = enum.Enum("CLIENT_LOCALE", "EXIT_PORT") + +def showCountDialog(countType, counts): + """ + Provides a dialog with bar graphs and percentages for the given set of + counts. Pressing any key closes the dialog. + + Arguments: + countType - type of counts being presented + counts - mapping of labels to counts + """ + + isNoStats = not counts + noStatsMsg = "Usage stats aren't available yet, press any key..." + + if isNoStats: + popup, width, height = cli.popups.init(3, len(noStatsMsg) + 4) + else: + popup, width, height = cli.popups.init(4 + max(1, len(counts)), 80) + if not popup: return + + try: + control = cli.controller.getController() + + popup.win.box() + + # dialog title + if countType == CountType.CLIENT_LOCALE: + title = "Client Locales" + elif countType == CountType.EXIT_PORT: + title = "Exiting Port Usage" + else: + title = "" + log.log(log.WARN, "Unrecognized count type: %s" % countType) + + popup.addstr(0, 0, title, curses.A_STANDOUT) + + if isNoStats: + popup.addstr(1, 2, noStatsMsg, curses.A_BOLD | uiTools.getColor("cyan")) + else: + sortedCounts = sorted(counts.iteritems(), key=operator.itemgetter(1)) + sortedCounts.reverse() + + # constructs string formatting for the max key and value display width + keyWidth, valWidth, valueTotal = 3, 1, 0 + for k, v in sortedCounts: + keyWidth = max(keyWidth, len(k)) + valWidth = max(valWidth, len(str(v))) + valueTotal += v + + labelFormat = "%%-%is %%%ii (%%%%%%-2i)" % (keyWidth, valWidth) + + for i in range(height - 4): + k, v = sortedCounts[i] + label = labelFormat % (k, v, v * 100 / valueTotal) + popup.addstr(i + 1, 2, label, curses.A_BOLD | uiTools.getColor("green")) + + # All labels have the same size since they're based on the max widths. + # If this changes then this'll need to be the max label width. + labelWidth = len(label) + + # draws simple bar graph for percentages + fillWidth = v * (width - 4 - labelWidth) / valueTotal + for j in range(fillWidth): + popup.addstr(i + 1, 3 + labelWidth + j, " ", curses.A_STANDOUT | uiTools.getColor("red")) + + popup.addstr(height - 2, 2, "Press any key...") + + popup.win.refresh() + + curses.cbreak() + control.getScreen().getch() + finally: cli.popups.finalize() +
tor-commits@lists.torproject.org