Author: atagar Date: 2011-03-13 04:58:18 +0000 (Sun, 13 Mar 2011) New Revision: 24349
Added: arm/trunk/src/interface/connections/connEntry.py arm/trunk/src/interface/connections/entries.py Removed: arm/trunk/src/interface/connections/listings.py Modified: arm/trunk/src/interface/connections/__init__.py arm/trunk/src/interface/connections/connPanel.py arm/trunk/src/interface/controller.py arm/trunk/src/util/connections.py arm/trunk/src/util/enum.py arm/trunk/src/util/uiTools.py Log: Rearranging connection panel resources, abstracting the content away from the panel itself. This is to make it more extendable and supporting of multi-line entries (pre-reqs for my plans to display client circuits).
Modified: arm/trunk/src/interface/connections/__init__.py =================================================================== --- arm/trunk/src/interface/connections/__init__.py 2011-03-13 01:38:32 UTC (rev 24348) +++ arm/trunk/src/interface/connections/__init__.py 2011-03-13 04:58:18 UTC (rev 24349) @@ -2,5 +2,5 @@ Panels, popups, and handlers comprising the arm user interface. """
-__all__ = ["connPanel", "entry"] +__all__ = ["connEntry", "connPanel", "entries"]
Added: arm/trunk/src/interface/connections/connEntry.py =================================================================== --- arm/trunk/src/interface/connections/connEntry.py (rev 0) +++ arm/trunk/src/interface/connections/connEntry.py 2011-03-13 04:58:18 UTC (rev 24349) @@ -0,0 +1,694 @@ +""" +Connection panel entries related to actual connections to or from the system +(ie, results seen by netstat, lsof, etc). +""" + +import time +import curses + +from util import connections, enum, torTools, uiTools +from interface.connections import entries + +# Connection Categories: +# Inbound Relay connection, coming to us. +# Outbound Relay connection, leaving us. +# Exit Outbound relay connection leaving the Tor network. +# Client Circuits for our client traffic. +# Application Socks connections using Tor. +# Directory Fetching tor consensus information. +# Control Tor controller (arm, vidalia, etc). + +Category = enum.Enum("INBOUND", "OUTBOUND", "EXIT", "CLIENT", "APPLICATION", "DIRECTORY", "CONTROL") +CATEGORY_COLOR = {Category.INBOUND: "green", Category.OUTBOUND: "blue", + Category.EXIT: "red", Category.CLIENT: "cyan", + Category.APPLICATION: "yellow", Category.DIRECTORY: "magenta", + Category.CONTROL: "red"} + +# static data for listing format +# <src> --> <dst> <etc><padding> +LABEL_FORMAT = "%s --> %s %s%s" +LABEL_MIN_PADDING = 2 # min space between listing label and following data + +CONFIG = {"features.connection.showColumn.fingerprint": True, + "features.connection.showColumn.nickname": True, + "features.connection.showColumn.destination": True, + "features.connection.showColumn.expanedIp": True} + +def loadConfig(config): + config.update(CONFIG) + +class Endpoint: + """ + Collection of attributes associated with a connection endpoint. This is a + thin wrapper for torUtil functions, making use of its caching for + performance. + """ + + def __init__(self, ipAddr, port): + self.ipAddr = ipAddr + self.port = port + + # if true, we treat the port as an ORPort when searching for matching + # fingerprints (otherwise the ORPort is assumed to be unknown) + self.isORPort = False + + def getIpAddr(self): + """ + Provides the IP address of the endpoint. + """ + + return self.ipAddr + + def getPort(self): + """ + Provides the port of the endpoint. + """ + + return self.port + + def getHostname(self, default = None): + """ + Provides the hostname associated with the relay's address. This is a + non-blocking call and returns None if the address either can't be resolved + or hasn't been resolved yet. + + Arguments: + default - return value if no hostname is available + """ + + # TODO: skipping all hostname resolution to be safe for now + #try: + # myHostname = hostnames.resolve(self.ipAddr) + #except: + # # either a ValueError or IOError depending on the source of the lookup failure + # myHostname = None + # + #if not myHostname: return default + #else: return myHostname + + return default + + def getLocale(self): + """ + Provides the two letter country code for the IP address' locale. This + proivdes None if it can't be determined. + """ + + conn = torTools.getConn() + return conn.getInfo("ip-to-country/%s" % self.ipAddr) + + def getFingerprint(self): + """ + Provides the fingerprint of the relay, returning "UNKNOWN" if it can't be + determined. + """ + + conn = torTools.getConn() + orPort = self.port if self.isORPort else None + myFingerprint = conn.getRelayFingerprint(self.ipAddr, orPort) + + if myFingerprint: return myFingerprint + else: return "UNKNOWN" + + def getNickname(self): + """ + Provides the nickname of the relay, retuning "UNKNOWN" if it can't be + determined. + """ + + conn = torTools.getConn() + orPort = self.port if self.isORPort else None + myFingerprint = conn.getRelayFingerprint(self.ipAddr, orPort) + + if myFingerprint: return conn.getRelayNickname(myFingerprint) + else: return "UNKNOWN" + +class ConnectionEntry(entries.ConnectionPanelEntry): + """ + Represents a connection being made to or from this system. These only + concern real connections so it includes the inbound, outbound, directory, + application, and controller categories. + """ + + def __init__(self, lIpAddr, lPort, fIpAddr, fPort): + entries.ConnectionPanelEntry.__init__(self) + self.lines = [ConnectionLine(lIpAddr, lPort, fIpAddr, fPort)] + + def getSortValue(self, attr, listingType): + """ + Provides the value of a single attribute used for sorting purposes. + """ + + if attr == entries.SortAttr.IP_ADDRESS: + return self.lines[0].sortIpAddr + elif attr == entries.SortAttr.PORT: + return self.lines[0].sortPort + elif attr == entries.SortAttr.HOSTNAME: + return self.lines[0].foreign.getHostname("") + elif attr == entries.SortAttr.FINGERPRINT: + return self.lines[0].foreign.getFingerprint() + elif attr == entries.SortAttr.NICKNAME: + myNickname = self.lines[0].foreign.getNickname() + if myNickname == "UNKNOWN": return "z" * 20 # orders at the end + else: return myNickname.lower() + elif attr == entries.SortAttr.CATEGORY: + return Category.indexOf(self.lines[0].getType()) + elif attr == entries.SortAttr.UPTIME: + return self.lines[0].startTime + elif attr == entries.SortAttr.COUNTRY: + if connections.isIpAddressPrivate(self.lines[0].foreign.getIpAddr()): return "" + else: return self.lines[0].foreign.getLocale() + else: + return entries.ConnectionPanelEntry.getSortValue(self, attr, listingType) + +class ConnectionLine(entries.ConnectionPanelLine): + """ + Display component of the ConnectionEntry. + """ + + def __init__(self, lIpAddr, lPort, fIpAddr, fPort): + entries.ConnectionPanelLine.__init__(self) + + self.local = Endpoint(lIpAddr, lPort) + self.foreign = Endpoint(fIpAddr, fPort) + self.startTime = time.time() + + # True if the connection has matched the properties of a client/directory + # connection every time we've checked. The criteria we check is... + # client - first hop in an established circuit + # directory - matches an established single-hop circuit (probably a + # directory mirror) + + self._possibleClient = True + self._possibleDirectory = True + + conn = torTools.getConn() + myOrPort = conn.getOption("ORPort") + myDirPort = conn.getOption("DirPort") + mySocksPort = conn.getOption("SocksPort", "9050") + myCtlPort = conn.getOption("ControlPort") + + # the ORListenAddress can overwrite the ORPort + listenAddr = conn.getOption("ORListenAddress") + if listenAddr and ":" in listenAddr: + myOrPort = listenAddr[listenAddr.find(":") + 1:] + + if lPort in (myOrPort, myDirPort): + self.baseType = Category.INBOUND + self.local.isORPort = True + elif lPort == mySocksPort: + self.baseType = Category.APPLICATION + elif lPort == myCtlPort: + self.baseType = Category.CONTROL + else: + self.baseType = Category.OUTBOUND + self.foreign.isORPort = True + + self.cachedType = None + + # cached immutable values used for sorting + self.sortIpAddr = connections.ipToInt(self.foreign.getIpAddr()) + self.sortPort = int(self.foreign.getPort()) + + def getListingEntry(self, width, currentTime, listingType): + """ + Provides the DrawEntry for this connection's listing. The line is made up + of six components: + <src> --> <dst> <etc> <uptime> (<type>) + + ListingType.IP_ADDRESS: + src - <internal addr:port> --> <external addr:port> + dst - <destination addr:port> + etc - <fingerprint> <nickname> + + ListingType.HOSTNAME: + src - localhost:<port> + dst - <destination hostname:port> + etc - <destination addr:port> <fingerprint> <nickname> + + ListingType.FINGERPRINT: + src - localhost + dst - <destination fingerprint> + etc - <nickname> <destination addr:port> + + ListingType.NICKNAME: + src - <source nickname> + dst - <destination nickname> + etc - <fingerprint> <destination addr:port> + + Arguments: + width - maximum length of the line + currentTime - unix timestamp for what the results should consider to be + the current time + listingType - primary attribute we're listing connections by + """ + + # fetch our (most likely cached) display entry for the listing + myListing = entries.ConnectionPanelLine.getListingEntry(self, width, currentTime, listingType) + + # fill in the current uptime and return the results + timeEntry = myListing.getNext() + timeEntry.text = "%5s" % uiTools.getTimeLabel(currentTime - self.startTime, 1) + + return myListing + + def _getListingEntry(self, width, currentTime, listingType): + entryType = self.getType() + + # Lines are split into the following components in reverse: + # content - "<src> --> <dst> <etc> " + # time - "<uptime>" + # preType - " (" + # category - "<type>" + # postType - ") " + + lineFormat = uiTools.getColor(CATEGORY_COLOR[entryType]) + + drawEntry = uiTools.DrawEntry(")" + " " * (9 - len(entryType)), lineFormat) + drawEntry = uiTools.DrawEntry(entryType.upper(), lineFormat | curses.A_BOLD, drawEntry) + drawEntry = uiTools.DrawEntry(" (", lineFormat, drawEntry) + drawEntry = uiTools.DrawEntry(" " * 5, lineFormat, drawEntry) + drawEntry = uiTools.DrawEntry(self._getListingContent(width - 17, listingType), lineFormat, drawEntry) + return drawEntry + + def _getDetails(self, width): + """ + Provides details on the connection, correlated against available consensus + data. + + Arguments: + width - available space to display in + """ + + detailFormat = curses.A_BOLD | uiTools.getColor(CATEGORY_COLOR[self.getType()]) + return [uiTools.DrawEntry(line, detailFormat) for line in self._getDetailContent(width)] + + def resetDisplay(self): + entries.ConnectionPanelLine.resetDisplay(self) + self.cachedType = None + + def isPrivate(self): + """ + Returns true if the endpoint is private, possibly belonging to a client + connection or exit traffic. + """ + + myType = self.getType() + + if myType == Category.INBOUND: + # if the connection doesn't belong to a known relay then it might be + # client traffic + + return self.foreign.getFingerprint() == "UNKNOWN" + elif myType == Category.EXIT: + # DNS connections exiting us aren't private (since they're hitting our + # resolvers). Everything else, however, is. + + # TODO: Ideally this would also double check that it's a UDP connection + # (since DNS is the only UDP connections Tor will relay), however this + # will take a bit more work to propagate the information up from the + # connection resolver. + return self.foreign.getPort() != "53" + + # for everything else this isn't a concern + return False + + def getType(self): + """ + Provides our best guess at the current type of the connection. This + depends on consensus results, our current client circuts, etc. Results + are cached until this entry's display is reset. + """ + + # caches both to simplify the calls and to keep the type consistent until + # we want to reflect changes + if not self.cachedType: + if self.baseType == Category.OUTBOUND: + # Currently the only non-static categories are OUTBOUND vs... + # - EXIT since this depends on the current consensus + # - CLIENT if this is likely to belong to our guard usage + # - DIRECTORY if this is a single-hop circuit (directory mirror?) + # + # The exitability, circuits, and fingerprints are all cached by the + # torTools util keeping this a quick lookup. + + conn = torTools.getConn() + destFingerprint = self.foreign.getFingerprint() + + if destFingerprint == "UNKNOWN": + # Not a known relay. This might be an exit connection. + + if conn.isExitingAllowed(self.foreign.getIpAddr(), self.foreign.getPort()): + self.cachedType = Category.EXIT + elif self._possibleClient or self._possibleDirectory: + # This belongs to a known relay. If we haven't eliminated ourselves as + # a possible client or directory connection then check if it still + # holds true. + + myCircuits = conn.getCircuits() + + if self._possibleClient: + # Checks that this belongs to the first hop in a circuit that's + # either unestablished or longer than a single hop (ie, anything but + # a built 1-hop connection since those are most likely a directory + # mirror). + + for status, _, path in myCircuits: + if path[0] == destFingerprint and (status != "BUILT" or len(path) > 1): + self.cachedType = Category.CLIENT # matched a probable guard connection + + # if we fell through, we can eliminate ourselves as a guard in the future + if not self.cachedType: + self._possibleClient = False + + if self._possibleDirectory: + # Checks if we match a built, single hop circuit. + + for status, _, path in myCircuits: + if path[0] == destFingerprint and status == "BUILT" and len(path) == 1: + self.cachedType = Category.DIRECTORY + + # if we fell through, eliminate ourselves as a directory connection + if not self.cachedType: + self._possibleDirectory = False + + if not self.cachedType: + self.cachedType = self.baseType + + return self.cachedType + + 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 + + src, dst, etc = "", "", "" + if listingType == entries.ListingType.IP_ADDRESS: + myExternalIpAddr = conn.getInfo("address", self.local.getIpAddr()) + addrDiffer = myExternalIpAddr != self.local.getIpAddr() + + srcAddress = "%s:%s" % (myExternalIpAddr, self.local.getPort()) + 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 + + if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]: + # show fingerprint (column width: 42 characters) + etc += "%-40s " % self.foreign.getFingerprint() + usedSpace += 42 + + if addrDiffer and width > usedSpace + 28 and CONFIG["features.connection.showColumn.expanedIp"]: + # include the internal address in the src (extra 28 characters) + internalAddress = "%s:%s" % (self.local.getIpAddr(), self.local.getPort()) + src = "%-21s --> %s" % (internalAddress, src) + usedSpace += 28 + + 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: + # 15 characters for source, and a min of 40 reserved for the destination + src = "localhost:%-5s" % self.local.getPort() + usedSpace += len(src) + minHostnameSpace = 40 + + if width > usedSpace + minHostnameSpace + 28 and CONFIG["features.connection.showColumn.destination"]: + # show destination ip/port/locale (column width: 28 characters) + etc += "%-26s " % dstAddress + usedSpace += 28 + + if width > usedSpace + minHostnameSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]: + # show fingerprint (column width: 42 characters) + etc += "%-40s " % self.foreign.getFingerprint() + usedSpace += 42 + + if width > usedSpace + minHostnameSpace + 17 and CONFIG["features.connection.showColumn.nickname"]: + # show nickname (column width: min 17 characters, uses half of the remainder) + nicknameSpace = 15 + (width - (usedSpace + minHostnameSpace + 17)) / 2 + nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0) + etc += ("%%-%is " % nicknameSpace) % nicknameLabel + usedSpace += (nicknameSpace + 2) + + 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()) + port = self.foreign.getPort() + + # truncates long hostnames and sets dst to <hostname>:<port> + hostname = uiTools.cropStr(hostname, hostnameSpace, 0) + dst = "%s:%-5s" % (hostname, port) + dst = ("%%-%is" % hostnameSpace) % dst + 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 + + if width > usedSpace + 17: + # show nickname (column width: min 17 characters, consumes any remaining space) + nicknameSpace = width - usedSpace + + # 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"]: + nicknameSpace = width - usedSpace - 28 if isIpLocaleIncluded else width - usedSpace + nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0) + etc += ("%%-%is " % nicknameSpace) % nicknameLabel + usedSpace += nicknameSpace + 2 + + if isIpLocaleIncluded: + etc += "%-26s " % dstAddress + usedSpace += 28 + 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 + + if width > usedSpace + minBaseSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]: + # show fingerprint (column width: 42 characters) + etc += "%-40s " % self.foreign.getFingerprint() + usedSpace += 42 + + if width > usedSpace + minBaseSpace + 28 and CONFIG["features.connection.showColumn.destination"]: + # show destination ip/port/locale (column width: 28 characters) + etc += "%-26s " % dstAddress + usedSpace += 28 + + 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 connectoin. + + 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 potenial 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 = nsLines[1][2:] + microExit = nsLines[3][2:] + + dirPortLabel = "" if dirPort == "0" else "dirport: %s" % dirPort + lines[2] = "nickname: %-25s orport: %-10s %s" % (nickname, orPort, dirPortLabel) + lines[3] = "published: %s %s" % (pubDate, pubTime) + lines[4] = "flags: %s" % flags.replace(" ", ", ") + lines[5] = "exit policy: %s" % microExit.replace(",", ", ") + + 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] = "Muliple 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 _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 + """ + + # destination of the connection + if self.isPrivate(): + dstAddress = "<scrubbed>:%s" % self.foreign.getPort() + else: + dstAddress = "%s:%s" % (self.foreign.getIpAddr(), self.foreign.getPort()) + + # 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: + 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 = [] + + if includeLocale: + foreignLocale = self.foreign.getLocale() + extraInfo.append(foreignLocale) + spaceAvailable -= len(foreignLocale) + 2 + + if includeHostname: + dstHostname = self.foreign.getHostname() + + if dstHostname: + # determines the full space availabe, taking into account the ", " + # dividers if there's multipe 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] +
Modified: arm/trunk/src/interface/connections/connPanel.py =================================================================== --- arm/trunk/src/interface/connections/connPanel.py 2011-03-13 01:38:32 UTC (rev 24348) +++ arm/trunk/src/interface/connections/connPanel.py 2011-03-13 04:58:18 UTC (rev 24349) @@ -6,8 +6,8 @@ import curses import threading
-from interface.connections import listings -from util import connections, enum, log, panel, torTools, uiTools +from interface.connections import entries, connEntry +from util import connections, enum, panel, uiTools
DEFAULT_CONFIG = {"features.connection.listingType": 0, "features.connection.refreshRate": 10} @@ -18,7 +18,7 @@ # listing types Listing = enum.Enum(("IP_ADDRESS", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME")
-DEFAULT_SORT_ORDER = (listings.SortAttr.CATEGORY, listings.SortAttr.LISTING, listings.SortAttr.UPTIME) +DEFAULT_SORT_ORDER = (entries.SortAttr.CATEGORY, entries.SortAttr.LISTING, entries.SortAttr.UPTIME)
class ConnectionPanel(panel.Panel, threading.Thread): """ @@ -33,12 +33,13 @@
self._sortOrdering = DEFAULT_SORT_ORDER self._config = dict(DEFAULT_CONFIG) + if config: config.update(self._config, { "features.connection.listingType": (0, len(Listing.values()) - 1), "features.connection.refreshRate": 1})
- sortFields = listings.SortAttr.values() + sortFields = entries.SortAttr.values() customOrdering = config.getIntCSV("features.connection.order", None, 3, 0, len(sortFields))
if customOrdering: @@ -48,6 +49,7 @@ self._scroller = uiTools.Scroller(True) self._title = "Connections:" # title line of the panel self._connections = [] # last fetched connections + self._connectionLines = [] # individual lines in the connection listing self._showDetails = False # presents the details panel if true
self._lastUpdate = -1 # time the content was last revised @@ -55,13 +57,12 @@ self._pauseTime = None # time when the panel was paused self._halt = False # terminates thread if true self._cond = threading.Condition() # used for pausing the thread + self.valsLock = threading.RLock()
# Last sampling received from the ConnectionResolver, used to detect when # it changes. self._lastResourceFetch = -1
- self.valsLock = threading.RLock() - self._update() # populates initial entries
# TODO: should listen for tor shutdown @@ -95,6 +96,10 @@ self.valsLock.acquire() if ordering: self._sortOrdering = ordering self._connections.sort(key=lambda i: (i.getSortValues(self._sortOrdering, self._listingType))) + + self._connectionLines = [] + for entry in self._connections: + self._connectionLines += entry.getLines() self.valsLock.release()
def setListingType(self, listingType): @@ -109,7 +114,7 @@ self._listingType = listingType
# if we're sorting by the listing then we need to resort - if listings.SortAttr.LISTING in self._sortOrdering: + if entries.SortAttr.LISTING in self._sortOrdering: self.setSortOrder()
self.valsLock.release() @@ -120,7 +125,7 @@ if uiTools.isScrollKey(key): pageHeight = self.getPreferredSize()[0] - 1 if self._showDetails: pageHeight -= (DETAILS_HEIGHT + 1) - isChanged = self._scroller.handleKey(key, self._connections, pageHeight) + isChanged = self._scroller.handleKey(key, self._connectionLines, pageHeight) if isChanged: self.redraw(True) elif uiTools.isSelectionKey(key): self._showDetails = not self._showDetails @@ -152,14 +157,21 @@
# extra line when showing the detail panel is for the bottom border detailPanelOffset = DETAILS_HEIGHT + 1 if self._showDetails else 0 - isScrollbarVisible = len(self._connections) > height - detailPanelOffset - 1 + isScrollbarVisible = len(self._connectionLines) > height - detailPanelOffset - 1
- scrollLoc = self._scroller.getScrollLoc(self._connections, height - detailPanelOffset - 1) - cursorSelection = self._scroller.getCursorSelection(self._connections) + scrollLoc = self._scroller.getScrollLoc(self._connectionLines, height - detailPanelOffset - 1) + cursorSelection = self._scroller.getCursorSelection(self._connectionLines)
# draws the detail panel if currently displaying it if self._showDetails: - self._drawSelectionPanel(cursorSelection, width, isScrollbarVisible) + # 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, DETAILS_HEIGHT + 2) + if isScrollbarVisible: self.addch(DETAILS_HEIGHT + 1, 1, curses.ACS_TTEE) + + drawEntries = cursorSelection.getDetails(width) + for i in range(min(len(drawEntries), DETAILS_HEIGHT)): + drawEntries[i].render(self, 1 + i, 2)
# title label with connection counts title = "Connection Details:" if self._showDetails else self._title @@ -168,38 +180,18 @@ scrollOffset = 0 if isScrollbarVisible: scrollOffset = 3 - self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelOffset - 1, len(self._connections), 1 + detailPanelOffset) + self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelOffset - 1, len(self._connectionLines), 1 + detailPanelOffset)
currentTime = self._pauseTime if self._pauseTime else time.time() - for lineNum in range(scrollLoc, len(self._connections)): - entry = self._connections[lineNum] - drawLine = lineNum + detailPanelOffset + 1 - scrollLoc + for lineNum in range(scrollLoc, len(self._connectionLines)): + entryLine = self._connectionLines[lineNum]
- entryType = entry.getType() - lineFormat = uiTools.getColor(listings.CATEGORY_COLOR[entryType]) - if entry == cursorSelection: lineFormat |= curses.A_STANDOUT + # hilighting if this is the selected line + extraFormat = curses.A_STANDOUT if entryLine == cursorSelection else curses.A_NORMAL
- # Lines are split into three components (prefix, category, and suffix) - # since the category includes the bold attribute (otherwise, all use - # lineFormat). - xLoc = scrollOffset - - # prefix (entry data which is largely static, plus the time label) - # the right content (time and type) takes seventeen columns - entryLabel = entry.getLabel(self._listingType, width - scrollOffset - 17) - timeLabel = uiTools.getTimeLabel(currentTime - entry.startTime, 1) - prefixLabel = "%s%5s (" % (entryLabel, timeLabel) - - self.addstr(drawLine, xLoc, prefixLabel, lineFormat) - xLoc += len(prefixLabel) - - # category - self.addstr(drawLine, xLoc, entryType.upper(), lineFormat | curses.A_BOLD) - xLoc += len(entryType) - - # suffix (ending parentheses plus padding so lines are the same length) - self.addstr(drawLine, xLoc, ")" + " " * (9 - len(entryType)), lineFormat) - + drawEntry = entryLine.getListingEntry(width - scrollOffset, currentTime, self._listingType) + drawLine = lineNum + detailPanelOffset + 1 - scrollLoc + drawEntry.render(self, drawLine, scrollOffset, extraFormat) if drawLine >= height: break
self.valsLock.release() @@ -232,25 +224,33 @@ newConnections = []
# preserves any ConnectionEntries they already exist - for conn in self._connections: - connAttr = (conn.local.getIpAddr(), conn.local.getPort(), - conn.foreign.getIpAddr(), conn.foreign.getPort()) - - if connAttr in currentConnections: - newConnections.append(conn) - currentConnections.remove(connAttr) + for entry in self._connections: + if isinstance(entry, connEntry.ConnectionEntry): + connLine = entry.getLines()[0] + connAttr = (connLine.local.getIpAddr(), connLine.local.getPort(), + connLine.foreign.getIpAddr(), connLine.foreign.getPort()) + + if connAttr in currentConnections: + newConnections.append(entry) + currentConnections.remove(connAttr)
+ # reset any display attributes for the entries we're keeping + for entry in newConnections: + entry.resetDisplay() + # add new entries for any additions for lIp, lPort, fIp, fPort in currentConnections: - newConnections.append(listings.ConnectionEntry(lIp, lPort, fIp, fPort)) + newConnections.append(connEntry.ConnectionEntry(lIp, lPort, fIp, fPort))
# Counts the relays in each of the categories. This also flushes the # type cache for all of the connections (in case its changed since last # fetched).
- categoryTypes = listings.Category.values() + categoryTypes = connEntry.Category.values() typeCounts = dict((type, 0) for type in categoryTypes) - for conn in newConnections: typeCounts[conn.getType(True)] += 1 + for entry in newConnections: + if isinstance(entry, connEntry.ConnectionEntry): + typeCounts[entry.getLines()[0].getType()] += 1
# makes labels for all the categories with connections (ie, # "21 outbound", "1 control", etc) @@ -264,116 +264,12 @@ else: self._title = "Connections:"
self._connections = newConnections + + self._connectionLines = [] + for entry in self._connections: + self._connectionLines += entry.getLines() + self.setSortOrder() self._lastResourceFetch = currentResolutionCount self.valsLock.release() - - def _drawSelectionPanel(self, selection, width, isScrollbarVisible): - """ - Renders a panel for details on the selected connnection. - """ - - # 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, DETAILS_HEIGHT + 2) - if isScrollbarVisible: self.addch(DETAILS_HEIGHT + 1, 1, curses.ACS_TTEE) - - selectionFormat = curses.A_BOLD | uiTools.getColor(listings.CATEGORY_COLOR[selection.getType()]) - lines = [""] * 7 - - lines[0] = "address: %s" % selection.getDestinationLabel(width - 11, listings.DestAttr.NONE) - lines[1] = "locale: %s" % ("??" if selection.isPrivate() else selection.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 potenial 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 = selection.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 = nsLines[1][2:] - microExit = nsLines[3][2:] - - dirPortLabel = "" if dirPort == "0" else "dirport: %s" % dirPort - lines[2] = "nickname: %-25s orport: %-10s %s" % (nickname, orPort, dirPortLabel) - lines[3] = "published: %s %s" % (pubDate, pubTime) - lines[4] = "flags: %s" % flags.replace(" ", ", ") - lines[5] = "exit policy: %s" % microExit.replace(",", ", ") - - if descEntry: - torVersion, patform, 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] = "%-36s 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(selection.foreign.getIpAddr(), getAllMatches = True) - - if allMatches: - # multiple matches - lines[2] = "Muliple 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" - - for i in range(len(lines)): - lineText = uiTools.cropStr(lines[i], width - 2) - self.addstr(1 + i, 2, lineText, selectionFormat)
Added: arm/trunk/src/interface/connections/entries.py =================================================================== --- arm/trunk/src/interface/connections/entries.py (rev 0) +++ arm/trunk/src/interface/connections/entries.py 2011-03-13 04:58:18 UTC (rev 24349) @@ -0,0 +1,155 @@ +""" +Interface for entries in the connection panel. These consist of two parts: the +entry itself (ie, Tor connection, client circuit, etc) and the lines it +consists of in the listing. +""" + +from util import enum + +# attributes we can list entries by +ListingType = enum.Enum(("IP_ADDRESS", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME") + +SortAttr = enum.Enum("CATEGORY", "UPTIME", "LISTING", "IP_ADDRESS", "PORT", + "HOSTNAME", "FINGERPRINT", "NICKNAME", "COUNTRY") + +SORT_COLORS = {SortAttr.CATEGORY: "red", SortAttr.UPTIME: "yellow", + SortAttr.LISTING: "green", SortAttr.IP_ADDRESS: "blue", + SortAttr.PORT: "blue", SortAttr.HOSTNAME: "magenta", + SortAttr.FINGERPRINT: "cyan", SortAttr.NICKNAME: "cyan", + SortAttr.COUNTRY: "blue"} + +class ConnectionPanelEntry: + """ + Common parent for connection panel entries. This consists of a list of lines + in the panel listing. This caches results until the display indicates that + they should be flushed. + """ + + def __init__(self): + self.lines = [] + self.flushCache = True + + def getLines(self): + """ + Provides the individual lines in the connection listing. + """ + + if self.flushCache: + self.lines = self._getLines(self.lines) + self.flushCache = False + + return self.lines + + def _getLines(self, oldResults): + # implementation of getLines + + for line in oldResults: + line.resetDisplay() + + return oldResults + + def getSortValues(self, sortAttrs, listingType): + """ + Provides the value used in comparisons to sort based on the given + attribute. + + Arguments: + sortAttrs - list of SortAttr values for the field being sorted on + listingType - ListingType enumeration for the attribute we're listing + entries by + """ + + return [self.getSortValue(attr, listingType) for attr in sortAttrs] + + def getSortValue(self, attr, listingType): + """ + Provides the value of a single attribute used for sorting purposes. + + Arguments: + attr - list of SortAttr values for the field being sorted on + listingType - ListingType enumeration for the attribute we're listing + entries by + """ + + if attr == SortAttr.LISTING: + if listingType == ListingType.IP_ADDRESS: + return self.getSortValue(SortAttr.IP_ADDRESS, listingType) + elif listingType == ListingType.HOSTNAME: + return self.getSortValue(SortAttr.HOSTNAME, listingType) + elif listingType == ListingType.FINGERPRINT: + return self.getSortValue(SortAttr.FINGERPRINT, listingType) + elif listingType == ListingType.NICKNAME: + return self.getSortValue(SortAttr.NICKNAME, listingType) + + return "" + + def resetDisplay(self): + """ + Flushes cached display results. + """ + + self.flushCache = True + +class ConnectionPanelLine: + """ + Individual line in the connection panel listing. + """ + + def __init__(self): + # cache for displayed information + self._listingCache = None + self._listingCacheArgs = (None, None) + + self._detailsCache = None + self._detailsCacheArgs = None + + def getListingEntry(self, width, currentTime, listingType): + """ + Provides a DrawEntry instance for contents to be displayed in the + connection panel listing. + + Arguments: + width - available space to display in + currentTime - unix timestamp for what the results should consider to be + the current time (this may be ignored due to caching) + listingType - ListingType enumeration for the highest priority content + to be displayed + """ + + if self._listingCacheArgs != (width, listingType): + self._listingCache = self._getListingEntry(width, currentTime, listingType) + self._listingCacheArgs = (width, listingType) + + return self._listingCache + + def _getListingEntry(self, width, currentTime, listingType): + # implementation of getListingEntry + return None + + def getDetails(self, width): + """ + Provides a list of DrawEntry instances with detailed information for this + connection. + + Arguments: + width - available space to display in + """ + + if self._detailsCacheArgs != width: + self._detailsCache = self._getDetails(width) + self._detailsCacheArgs = width + + return self._detailsCache + + def _getDetails(self, width): + # implementation of getListing + return [] + + def resetDisplay(self): + """ + Flushes cached display results. + """ + + self._listingCacheArgs = (None, None) + self._detailsCacheArgs = None +
Deleted: arm/trunk/src/interface/connections/listings.py =================================================================== --- arm/trunk/src/interface/connections/listings.py 2011-03-13 01:38:32 UTC (rev 24348) +++ arm/trunk/src/interface/connections/listings.py 2011-03-13 04:58:18 UTC (rev 24349) @@ -1,570 +0,0 @@ -""" -Entries for connections related to the Tor process. -""" - -import time - -from util import connections, enum, hostnames, torTools, uiTools - -# Connection Categories: -# Inbound Relay connection, coming to us. -# Outbound Relay connection, leaving us. -# Exit Outbound relay connection leaving the Tor network. -# Client Circuits for our client traffic. -# Application Socks connections using Tor. -# Directory Fetching tor consensus information. -# Control Tor controller (arm, vidalia, etc). - -DestAttr = enum.Enum("NONE", "LOCALE", "HOSTNAME") -Category = enum.Enum("INBOUND", "OUTBOUND", "EXIT", "CLIENT", "APPLICATION", "DIRECTORY", "CONTROL") -CATEGORY_COLOR = {Category.INBOUND: "green", Category.OUTBOUND: "blue", - Category.EXIT: "red", Category.CLIENT: "cyan", - Category.APPLICATION: "yellow", Category.DIRECTORY: "magenta", - Category.CONTROL: "red"} - -SortAttr = enum.Enum("CATEGORY", "UPTIME", "LISTING", "IP_ADDRESS", "PORT", - "HOSTNAME", "FINGERPRINT", "NICKNAME", "COUNTRY") -SORT_COLORS = {SortAttr.CATEGORY: "red", SortAttr.UPTIME: "yellow", - SortAttr.LISTING: "green", SortAttr.IP_ADDRESS: "blue", - SortAttr.PORT: "blue", SortAttr.HOSTNAME: "magenta", - SortAttr.FINGERPRINT: "cyan", SortAttr.NICKNAME: "cyan", - SortAttr.COUNTRY: "blue"} - -# static data for listing format -# <src> --> <dst> <etc><padding> -LABEL_FORMAT = "%s --> %s %s%s" -LABEL_MIN_PADDING = 2 # min space between listing label and following data - -CONFIG = {"features.connection.showColumn.fingerprint": True, - "features.connection.showColumn.nickname": True, - "features.connection.showColumn.destination": True, - "features.connection.showColumn.expanedIp": True} - -def loadConfig(config): - config.update(CONFIG) - -class Endpoint: - """ - Collection of attributes associated with a connection endpoint. This is a - thin wrapper for torUtil functions, making use of its caching for - performance. - """ - - def __init__(self, ipAddr, port): - self.ipAddr = ipAddr - self.port = port - - # if true, we treat the port as an ORPort when searching for matching - # fingerprints (otherwise the ORPort is assumed to be unknown) - self.isORPort = False - - def getIpAddr(self): - """ - Provides the IP address of the endpoint. - """ - - return self.ipAddr - - def getPort(self): - """ - Provides the port of the endpoint. - """ - - return self.port - - def getHostname(self, default = None): - """ - Provides the hostname associated with the relay's address. This is a - non-blocking call and returns None if the address either can't be resolved - or hasn't been resolved yet. - - Arguments: - default - return value if no hostname is available - """ - - # TODO: skipping all hostname resolution to be safe for now - #try: - # myHostname = hostnames.resolve(self.ipAddr) - #except: - # # either a ValueError or IOError depending on the source of the lookup failure - # myHostname = None - # - #if not myHostname: return default - #else: return myHostname - - return default - - def getLocale(self): - """ - Provides the two letter country code for the IP address' locale. This - proivdes None if it can't be determined. - """ - - conn = torTools.getConn() - return conn.getInfo("ip-to-country/%s" % self.ipAddr) - - def getFingerprint(self): - """ - Provides the fingerprint of the relay, returning "UNKNOWN" if it can't be - determined. - """ - - conn = torTools.getConn() - orPort = self.port if self.isORPort else None - myFingerprint = conn.getRelayFingerprint(self.ipAddr, orPort) - - if myFingerprint: return myFingerprint - else: return "UNKNOWN" - - def getNickname(self): - """ - Provides the nickname of the relay, retuning "UNKNOWN" if it can't be - determined. - """ - - conn = torTools.getConn() - orPort = self.port if self.isORPort else None - myFingerprint = conn.getRelayFingerprint(self.ipAddr, orPort) - - if myFingerprint: return conn.getRelayNickname(myFingerprint) - else: return "UNKNOWN" - -class ConnectionEntry: - """ - Represents a connection being made to or from this system. These only - concern real connections so it only includes the inbound, outbound, - directory, application, and controller categories. - """ - - def __init__(self, lIpAddr, lPort, fIpAddr, fPort): - self.local = Endpoint(lIpAddr, lPort) - self.foreign = Endpoint(fIpAddr, fPort) - self.startTime = time.time() - - self._labelCache = "" - self._labelCacheArgs = (None, None) - - # True if the connection has matched the properties of a client/directory - # connection every time we've checked. The criteria we check is... - # client - first hop in an established circuit - # directory - matches an established single-hop circuit (probably a - # directory mirror) - - self._possibleClient = True - self._possibleDirectory = True - - conn = torTools.getConn() - myOrPort = conn.getOption("ORPort") - myDirPort = conn.getOption("DirPort") - mySocksPort = conn.getOption("SocksPort", "9050") - myCtlPort = conn.getOption("ControlPort") - myAuthorities = conn.getMyDirAuthorities() - - # the ORListenAddress can overwrite the ORPort - listenAddr = conn.getOption("ORListenAddress") - if listenAddr and ":" in listenAddr: - myOrPort = listenAddr[listenAddr.find(":") + 1:] - - if lPort in (myOrPort, myDirPort): - self.baseType = Category.INBOUND - self.local.isORPort = True - elif lPort == mySocksPort: - self.baseType = Category.APPLICATION - elif lPort == myCtlPort: - self.baseType = Category.CONTROL - elif (fIpAddr, fPort) in myAuthorities: - self.baseType = Category.DIRECTORY - else: - self.baseType = Category.OUTBOUND - self.foreign.isORPort = True - - self.cachedType = None - - # cached immutable values used for sorting - self.sortIpAddr = _ipToInt(self.foreign.getIpAddr()) - self.sortPort = int(self.foreign.getPort()) - - def getType(self, reset=False): - """ - Provides the category this connection belongs to. This isn't always static - since it can rely on dynamic information (like the current consensus). - - Arguments: - reset - determines if the type has changed if true, otherwise this - provides the same result as the last call - """ - - # caches both to simplify the calls and to keep the type consistent until - # we want to reflect changes - if reset or not self.cachedType: - self.cachedType = self._getType() - - return self.cachedType - - def getDestinationLabel(self, maxLength, extraAttr=DestAttr.NONE): - """ - 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 extraAttr if the address isn't private and isn't on the local network - - nothing otherwise - - Arguments: - maxLength - maximum length of the string returned - """ - - # destination of the connection - if self.isPrivate(): - dstAddress = "<scrubbed>:%s" % self.foreign.getPort() - else: - dstAddress = "%s:%s" % (self.foreign.getIpAddr(), self.foreign.getPort()) - - # 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: - 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()): - if extraAttr == DestAttr.LOCALE: - dstAddress += " (%s)" % self.foreign.getLocale() - elif extraAttr == DestAttr.HOSTNAME: - dstHostname = self.foreign.getHostname() - - if dstHostname: - dstAddress += " (%s)" % uiTools.cropStr(dstHostname, spaceAvailable) - - return dstAddress[:maxLength] - - def isPrivate(self): - """ - Returns true if the endpoint is private, possibly belonging to a client - connection or exit traffic. - """ - - myType = self.getType() - - if myType == Category.INBOUND: - # if the connection doesn't belong to a known relay then it might be - # client traffic - - return self.foreign.getFingerprint() == "UNKNOWN" - elif myType == Category.EXIT: - # DNS connections exiting us aren't private (since they're hitting our - # resolvers). Everything else, however, is. - - # TODO: Ideally this would also double check that it's a UDP connection - # (since DNS is the only UDP connections Tor will relay), however this - # will take a bit more work to propagate the information up from the - # connection resolver. - return self.foreign.getPort() != "53" - - # for everything else this isn't a concern - return False - - def getSortValues(self, sortAttrs, listingType): - """ - Provides the value used in comparisons to sort based on the given - attribute. - - Arguments: - sortAttrs - list of SortAttr values for the field being sorted on - listingType - primary attribute we're listing connections by - """ - - return [self._getSortValue(attr, listingType) for attr in sortAttrs] - - def getLabel(self, listingType, width): - """ - Provides the formatted display string for this entry in the listing with - the given constraints. Labels are made up of six components: - <src> --> <dst> <etc> <uptime> (<type>) - this provides the first three components padded to fill up to the uptime. - - Listing.IP_ADDRESS: - src - <internal addr:port> --> <external addr:port> - dst - <destination addr:port> - etc - <fingerprint> <nickname> - - Listing.HOSTNAME: - src - localhost:<port> - dst - <destination hostname:port> - etc - <destination addr:port> <fingerprint> <nickname> - - Listing.FINGERPRINT: - src - localhost - dst - <destination fingerprint> - etc - <nickname> <destination addr:port> - - Listing.NICKNAME: - src - <source nickname> - dst - <destination nickname> - etc - <fingerprint> <destination addr:port> - - Arguments: - listingType - primary attribute we're listing connections by - width - maximum length of the entry - """ - - # late import for the Listing enum (doing it in the header errors due to a - # circular import) - from interface.connections import connPanel - - # if our cached entries are still valid then use that - if self._labelCacheArgs == (listingType, width): - return self._labelCache - - conn = torTools.getConn() - myType = self.getType() - dstAddress = self.getDestinationLabel(26, DestAttr.LOCALE) - - # 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 - - src, dst, etc = "", "", "" - if listingType == connPanel.Listing.IP_ADDRESS: - myExternalIpAddr = conn.getInfo("address", self.local.getIpAddr()) - addrDiffer = myExternalIpAddr != self.local.getIpAddr() - - srcAddress = "%s:%s" % (myExternalIpAddr, self.local.getPort()) - 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 - - if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]: - # show fingerprint (column width: 42 characters) - etc += "%-40s " % self.foreign.getFingerprint() - usedSpace += 42 - - if addrDiffer and width > usedSpace + 28 and CONFIG["features.connection.showColumn.expanedIp"]: - # include the internal address in the src (extra 28 characters) - internalAddress = "%s:%s" % (self.local.getIpAddr(), self.local.getPort()) - src = "%-21s --> %s" % (internalAddress, src) - usedSpace += 28 - - 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 == connPanel.Listing.HOSTNAME: - # 15 characters for source, and a min of 40 reserved for the destination - src = "localhost:%-5s" % self.local.getPort() - usedSpace += len(src) - minHostnameSpace = 40 - - if width > usedSpace + minHostnameSpace + 28 and CONFIG["features.connection.showColumn.destination"]: - # show destination ip/port/locale (column width: 28 characters) - etc += "%-26s " % dstAddress - usedSpace += 28 - - if width > usedSpace + minHostnameSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]: - # show fingerprint (column width: 42 characters) - etc += "%-40s " % self.foreign.getFingerprint() - usedSpace += 42 - - if width > usedSpace + minHostnameSpace + 17 and CONFIG["features.connection.showColumn.nickname"]: - # show nickname (column width: min 17 characters, uses half of the remainder) - nicknameSpace = 15 + (width - (usedSpace + minHostnameSpace + 17)) / 2 - nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0) - etc += ("%%-%is " % nicknameSpace) % nicknameLabel - usedSpace += (nicknameSpace + 2) - - 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()) - port = self.foreign.getPort() - - # truncates long hostnames and sets dst to <hostname>:<port> - hostname = uiTools.cropStr(hostname, hostnameSpace, 0) - dst = "%s:%-5s" % (hostname, port) - dst = ("%%-%is" % hostnameSpace) % dst - elif listingType == connPanel.Listing.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 - - if width > usedSpace + 17: - # show nickname (column width: min 17 characters, consumes any remaining space) - nicknameSpace = width - usedSpace - - # 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"]: - nicknameSpace = width - usedSpace - 28 if isIpLocaleIncluded else width - usedSpace - nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0) - etc += ("%%-%is " % nicknameSpace) % nicknameLabel - usedSpace += nicknameSpace + 2 - - if isIpLocaleIncluded: - etc += "%-26s " % dstAddress - usedSpace += 28 - 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 - - if width > usedSpace + minBaseSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]: - # show fingerprint (column width: 42 characters) - etc += "%-40s " % self.foreign.getFingerprint() - usedSpace += 42 - - if width > usedSpace + minBaseSpace + 28 and CONFIG["features.connection.showColumn.destination"]: - # show destination ip/port/locale (column width: 28 characters) - etc += "%-26s " % dstAddress - usedSpace += 28 - - 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) - self._labelCache = LABEL_FORMAT % (src, dst, etc, padding) - self._labelCacheArgs = (listingType, width) - - return self._labelCache - - def _getType(self): - """ - Provides our best guess at the current type of the connection. This - depends on consensus results, our current client circuts, etc. - """ - - if self.baseType == Category.OUTBOUND: - # Currently the only non-static categories are OUTBOUND vs... - # - EXIT since this depends on the current consensus - # - CLIENT if this is likely to belong to our guard usage - # - DIRECTORY if this is a single-hop circuit (directory mirror?) - # - # The exitability, circuits, and fingerprints are all cached by the - # torTools util keeping this a quick lookup. - - conn = torTools.getConn() - destFingerprint = self.foreign.getFingerprint() - - if destFingerprint == "UNKNOWN": - # Not a known relay. This might be an exit connection. - - if conn.isExitingAllowed(self.foreign.getIpAddr(), self.foreign.getPort()): - return Category.EXIT - elif self._possibleClient or self._possibleDirectory: - # This belongs to a known relay. If we haven't eliminated ourselves as - # a possible client or directory connection then check if it still - # holds true. - - myCircuits = conn.getCircuits() - - if self._possibleClient: - # Checks that this belongs to the first hop in a circuit that's - # either unestablished or longer than a single hop (ie, anything but - # a built 1-hop connection since those are most likely a directory - # mirror). - - for status, _, path in myCircuits: - if path[0] == destFingerprint and (status != "BUILT" or len(path) > 1): - return Category.CLIENT # matched a probable guard connection - - # fell through, we can eliminate ourselves as a guard in the future - self._possibleClient = False - - if self._possibleDirectory: - # Checks if we match a built, single hop circuit. - - for status, _, path in myCircuits: - if path[0] == destFingerprint and status == "BUILT" and len(path) == 1: - return Category.DIRECTORY - - # fell through, eliminate ourselves as a directory connection - self._possibleDirectory = False - - return self.baseType - - def _getSortValue(self, sortAttr, listingType): - """ - Provides the value of a single attribute used for sorting purposes. - """ - - from interface.connections import connPanel - - if sortAttr == SortAttr.IP_ADDRESS: return self.sortIpAddr - elif sortAttr == SortAttr.PORT: return self.sortPort - elif sortAttr == SortAttr.HOSTNAME: return self.foreign.getHostname("") - elif sortAttr == SortAttr.FINGERPRINT: return self.foreign.getFingerprint() - elif sortAttr == SortAttr.NICKNAME: - myNickname = self.foreign.getNickname() - - if myNickname == "UNKNOWN": return "z" * 20 # orders at the end - else: return myNickname.lower() - elif sortAttr == SortAttr.CATEGORY: return Category.indexOf(self.getType()) - elif sortAttr == SortAttr.UPTIME: return self.startTime - elif sortAttr == SortAttr.COUNTRY: - if connections.isIpAddressPrivate(self.foreign.getIpAddr()): return "" - else: return self.foreign.getLocale() - elif sortAttr == SortAttr.LISTING: - if listingType == connPanel.Listing.IP_ADDRESS: - return self._getSortValue(SortAttr.IP_ADDRESS, listingType) - elif listingType == connPanel.Listing.HOSTNAME: - return self._getSortValue(SortAttr.HOSTNAME, listingType) - elif listingType == connPanel.Listing.FINGERPRINT: - return self._getSortValue(SortAttr.FINGERPRINT, listingType) - elif listingType == connPanel.Listing.NICKNAME: - return self._getSortValue(SortAttr.NICKNAME, listingType) - - return "" - -def _ipToInt(ipAddr): - """ - Provides an integer representation of the ip address, suitable for sorting. - - Arguments: - ipAddr - ip address to be converted - """ - - total = 0 - - for comp in ipAddr.split("."): - total *= 255 - total += int(comp) - - return total -
Modified: arm/trunk/src/interface/controller.py =================================================================== --- arm/trunk/src/interface/controller.py 2011-03-13 01:38:32 UTC (rev 24348) +++ arm/trunk/src/interface/controller.py 2011-03-13 04:58:18 UTC (rev 24349) @@ -25,7 +25,8 @@ import fileDescriptorPopup
import interface.connections.connPanel -import interface.connections.listings +import interface.connections.connEntry +import interface.connections.entries from util import conf, log, connections, hostnames, panel, sysTools, torConfig, torTools, uiTools import graphing.bandwidthStats import graphing.connStats @@ -425,7 +426,7 @@ config = conf.getConfig("arm") config.update(CONFIG) graphing.graphPanel.loadConfig(config) - interface.connections.listings.loadConfig(config) + interface.connections.connEntry.loadConfig(config)
# adds events needed for arm functionality to the torTools REQ_EVENTS mapping # (they're then included with any setControllerEvents call, and log a more @@ -1602,7 +1603,7 @@ panel.CURSES_LOCK.release() elif page == 2 and (key == ord('l') or key == ord('L')): # provides a menu to pick the primary information we list connections by - options = interface.connections.connPanel.Listing.values() + options = interface.connections.entries.ListingType.values() initialSelection = options.index(panels["conn2"]._listingType)
# hides top label of connection panel and pauses the display @@ -1624,9 +1625,9 @@ elif page == 2 and (key == ord('s') or key == ord('S')): # set ordering for connection options titleLabel = "Connection Ordering:" - options = interface.connections.listings.SortAttr.values() + options = interface.connections.entries.SortAttr.values() oldSelection = panels["conn2"]._sortOrdering - optionColors = dict([(attr, interface.connections.listings.SORT_COLORS[attr]) for attr in options]) + optionColors = dict([(attr, interface.connections.entries.SORT_COLORS[attr]) for attr in options]) results = showSortDialog(stdscr, panels, isPaused, page, titleLabel, options, oldSelection, optionColors)
if results:
Modified: arm/trunk/src/util/connections.py =================================================================== --- arm/trunk/src/util/connections.py 2011-03-13 01:38:32 UTC (rev 24348) +++ arm/trunk/src/util/connections.py 2011-03-13 04:58:18 UTC (rev 24349) @@ -153,6 +153,22 @@
return False
+def ipToInt(ipAddr): + """ + Provides an integer representation of the ip address, suitable for sorting. + + Arguments: + ipAddr - ip address to be converted + """ + + total = 0 + + for comp in ipAddr.split("."): + total *= 255 + total += int(comp) + + return total + def getPortUsage(port): """ Provides the common use of a given port. If no useage is known then this
Modified: arm/trunk/src/util/enum.py =================================================================== --- arm/trunk/src/util/enum.py 2011-03-13 01:38:32 UTC (rev 24348) +++ arm/trunk/src/util/enum.py 2011-03-13 04:58:18 UTC (rev 24349) @@ -12,12 +12,12 @@
pets.DOG
'Skippy'
pets.CAT
-"Cat" +'Cat'
or with entirely custom string components as an unordered enum with:
pets = LEnum(DOG="Skippy", CAT="Kitty", FISH="Nemo") pets.CAT
-"Kitty" +'Kitty' """
def toCamelCase(label):
Modified: arm/trunk/src/util/uiTools.py =================================================================== --- arm/trunk/src/util/uiTools.py 2011-03-13 01:38:32 UTC (rev 24348) +++ arm/trunk/src/util/uiTools.py 2011-03-13 04:58:18 UTC (rev 24349) @@ -409,6 +409,53 @@ except ValueError: raise ValueError(errorMsg)
+class DrawEntry: + """ + Renderable content, encapsulating the text and formatting. These can be + chained together to compose lines with multiple types of formatting. + """ + + def __init__(self, text, format=curses.A_NORMAL, nextEntry=None): + self.text = text + self.format = format + self.nextEntry = nextEntry + + def getNext(self): + """ + Provides the next DrawEntry in the chain. + """ + + return self.nextEntry + + def setNext(self, nextEntry): + """ + Sets additional content to be drawn after this entry. If None then + rendering is terminated after this entry. + + Arguments: + nextEntry - DrawEntry instance to be rendered after this one + """ + + self.nextEntry = nextEntry + + def render(self, drawPanel, y, x, extraFormat=curses.A_NORMAL): + """ + Draws this content at the given position. + + Arguments: + drawPanel - context in which to be drawn + y - vertical location + x - horizontal location + extraFormat - additional formatting + """ + + drawFormat = self.format | extraFormat + drawPanel.addstr(y, x, self.text, drawFormat) + + # if there's additional content to show then render it too + if self.nextEntry: + self.nextEntry.render(drawPanel, y, x + len(self.text), extraFormat) + class Scroller: """ Tracks the scrolling position when there might be a visible cursor. This