[tor-commits] r24349: {arm} Rearranging connection panel resources, abstracting the cont (in arm/trunk/src: interface interface/connections util)

Damian Johnson atagar1 at gmail.com
Sun Mar 13 04:58:19 UTC 2011


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



More information about the tor-commits mailing list