[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] r24349: {arm} Rearranging connection panel resources, abstracting the cont (in arm/trunk/src: interface interface/connections util)
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
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits