[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[or-cvs] r20087: {arm} Last substantial feature on my to-do list. added: connection (in arm/trunk: . interface)



Author: atagar
Date: 2009-07-19 04:09:48 -0400 (Sun, 19 Jul 2009)
New Revision: 20087

Modified:
   arm/trunk/arm
   arm/trunk/interface/connPanel.py
   arm/trunk/interface/controller.py
   arm/trunk/interface/hostnameResolver.py
Log:
Last substantial feature on my to-do list.
added: connections can be selected to view consensus details (very spiffy!)
added: listing selection is by menu rather than cycling



Modified: arm/trunk/arm
===================================================================
--- arm/trunk/arm	2009-07-19 04:00:11 UTC (rev 20086)
+++ arm/trunk/arm	2009-07-19 08:09:48 UTC (rev 20087)
@@ -1,3 +1,3 @@
 #!/bin/sh
-python arm.py $*
+python -W ignore::DeprecationWarning arm.py $*
 

Modified: arm/trunk/interface/connPanel.py
===================================================================
--- arm/trunk/interface/connPanel.py	2009-07-19 04:00:11 UTC (rev 20086)
+++ arm/trunk/interface/connPanel.py	2009-07-19 08:09:48 UTC (rev 20087)
@@ -87,6 +87,8 @@
     self.logger = logger            # notified in case of problems
     self.listingType = LIST_IP      # information used in listing entries
     self.allowDNS = True            # permits hostname resolutions if true
+    self.showLabel = True           # shows top label if true, hides otherwise
+    self.showingDetails = False     # augments display to accomidate details window if true
     self.sortOrdering = [ORD_TYPE, ORD_FOREIGN_LISTING, ORD_FOREIGN_PORT]
     self.isPaused = False
     self.resolver = hostnameResolver.HostnameResolver()
@@ -95,6 +97,10 @@
     self.fingerprintMappings = _getFingerprintMappings(self.conn) # mappings of ip -> [(port, OR identity), ...]
     self.nickname = self.conn.get_option("Nickname")[0][1]
     
+    self.isCursorEnabled = True
+    self.cursorSelection = None
+    self.cursorLoc = 0              # fallback cursor location if selection disappears
+    
     # gets process id to make sure we get the correct netstat data
     psCall = os.popen('ps -C tor -o pid')
     try: self.pid = psCall.read().strip().split()[1]
@@ -206,12 +212,34 @@
     if self.listingType != LIST_HOSTNAME: self.sortConnections()
   
   def handleKey(self, key):
-    self._resetBounds()
-    pageHeight = self.maxY - 1
-    if key == curses.KEY_UP: self.scroll = max(self.scroll - 1, 0)
-    elif key == curses.KEY_DOWN: self.scroll = max(0, self.scroll + 1)
-    elif key == curses.KEY_PPAGE: self.scroll = max(self.scroll - pageHeight, 0)
-    elif key == curses.KEY_NPAGE: self.scroll = max(0, self.scroll + pageHeight)
+    # cursor or scroll movement
+    if key in (curses.KEY_UP, curses.KEY_DOWN, curses.KEY_PPAGE, curses.KEY_NPAGE):
+      self._resetBounds()
+      pageHeight = self.maxY - 1
+      if self.showingDetails: pageHeight -= 8
+      
+      # determines location parameter to use
+      if self.isCursorEnabled:
+        try: currentLoc = self.connections.index(self.cursorSelection)
+        except ValueError: currentLoc = self.cursorLoc # fall back to nearby entry
+      else: currentLoc = self.scroll
+      
+      # location offset
+      if key == curses.KEY_UP: shift = -1
+      elif key == curses.KEY_DOWN: shift = 1
+      elif key == curses.KEY_PPAGE: shift = -pageHeight + 1 if self.isCursorEnabled else -pageHeight
+      elif key == curses.KEY_NPAGE: shift = pageHeight - 1 if self.isCursorEnabled else pageHeight
+      newLoc = currentLoc + shift
+      
+      # restricts to valid bounds
+      maxLoc = len(self.connections) - 1 if self.isCursorEnabled else len(self.connections) - pageHeight
+      newLoc = max(0, min(newLoc, maxLoc))
+      
+      # applies to proper parameter
+      if self.isCursorEnabled: self.cursorSelection, self.cursorLoc = self.connections[newLoc], newLoc
+      else: self.scroll = newLoc
+    elif key == ord('c') or key == ord('C'):
+      self.isCursorEnabled = not self.isCursorEnabled
     elif key == ord('r') or key == ord('R'):
       self.allowDNS = not self.allowDNS
       if not self.allowDNS: self.resolver.setPaused(True)
@@ -227,9 +255,25 @@
         if self.listingType == LIST_HOSTNAME: self.sortConnections()
         
         self.clear()
-        self.addstr(0, 0, "Connections (%i inbound, %i outbound, %i control):" % tuple(self.connectionCount), util.LABEL_ATTR)
+        if self.showLabel: self.addstr(0, 0, "Connections (%i inbound, %i outbound, %i control):" % tuple(self.connectionCount), util.LABEL_ATTR)
         
-        self.scroll = max(min(self.scroll, len(self.connections) - self.maxY + 1), 0)
+        listingHeight = self.maxY - 1
+        if self.showingDetails: listingHeight -= 8
+        
+        # ensure cursor location and scroll top are within bounds
+        self.cursorLoc = max(min(self.cursorLoc, len(self.connections) - 1), 0)
+        self.scroll = max(min(self.scroll, len(self.connections) - listingHeight), 0)
+        
+        if self.isCursorEnabled:
+          # update cursorLoc with selection (or vice versa if selection not found)
+          if self.cursorSelection not in self.connections:
+            self.cursorSelection = self.connections[self.cursorLoc]
+          else: self.cursorLoc = self.connections.index(self.cursorSelection)
+          
+          # shift scroll if necessary for cursor to be visible
+          if self.cursorLoc < self.scroll: self.scroll = self.cursorLoc
+          elif self.cursorLoc - listingHeight + 1 > self.scroll: self.scroll = self.cursorLoc - listingHeight + 1
+        
         lineNum = (-1 * self.scroll) + 1
         for entry in self.connections:
           if lineNum >= 1:
@@ -262,7 +306,12 @@
               dst = "%-41s" % dst
             
             if type == "inbound": src, dst = dst, src
-            self.addfstr(lineNum, 0, "<%s>%s -->   %s   (<b>%s</b>)</%s>" % (color, src, dst, type.upper(), color))
+            lineEntry = "<%s>%s -->   %s   (<b>%s</b>)</%s>" % (color, src, dst, type.upper(), color)
+            if self.isCursorEnabled and entry == self.cursorSelection:
+              lineEntry = "<h>%s</h>" % lineEntry
+            
+            offset = 0 if not self.showingDetails else 8
+            self.addfstr(lineNum + offset, 0, lineEntry)
           lineNum += 1
         
         self.refresh()
@@ -302,7 +351,11 @@
       return self.nicknameLookupCache[(ipAddr, port)]
     else:
       match = self.getFingerprint(ipAddr, port)
-      if match != "UNKNOWN": match = self.conn.get_network_status("id/%s" % match)[0].nickname
+      
+      try:
+        if match != "UNKNOWN": match = self.conn.get_network_status("id/%s" % match)[0].nickname
+      except TorCtl.ErrorReply: return "UNKNOWN" # don't cache result
+      
       self.nicknameLookupCache[(ipAddr, port)] = match
       return match
   

Modified: arm/trunk/interface/controller.py
===================================================================
--- arm/trunk/interface/controller.py	2009-07-19 04:00:11 UTC (rev 20086)
+++ arm/trunk/interface/controller.py	2009-07-19 08:09:48 UTC (rev 20087)
@@ -76,7 +76,9 @@
           else:
             batchSize = self.resolver.totalResolves - self.resolvingCounter
             entryCount = batchSize - self.resolver.unresolvedQueue.qsize()
-            progress = 100 * entryCount / batchSize
+            if batchSize > 0: progress = 100 * entryCount / batchSize
+            else: progress = 0
+            
             additive = "(or l) " if self.page == 2 else ""
             msgText = "Resolving hostnames (%i / %i, %i%%) - press esc %sto cancel" % (entryCount, batchSize, progress, additive)
         
@@ -183,7 +185,7 @@
           panels[panelKey].recreate(stdscr, startY)
           startY += panels[panelKey].height
         
-        isChanged = panels["popup"].recreate(stdscr, startY, 80)
+        panels["popup"].recreate(stdscr, startY, 80)
         
         for panelSet in PAGES:
           tmpStartY = startY
@@ -257,14 +259,16 @@
           popup.addstr(1, 41, "down arrow: scroll down a line")
           popup.addstr(2, 2, "page up: scroll up a page")
           popup.addstr(2, 41, "page down: scroll down a page")
+          popup.addstr(3, 2, "enter: connection details")
+          popup.addfstr(3, 41, "c: toggle cursor (<b>%s</b>)" % ("on" if panels["conn"].isCursorEnabled else "off"))
           
           listingType = connPanel.LIST_LABEL[panels["conn"].listingType].lower()
-          popup.addfstr(3, 2, "l: listed identity (<b>%s</b>)" % listingType)
+          popup.addfstr(4, 2, "l: listed identity (<b>%s</b>)" % listingType)
           
           allowDnsLabel = "allow" if panels["conn"].allowDNS else "disallow"
-          popup.addfstr(3, 41, "r: permit DNS resolution (<b>%s</b>)" % allowDnsLabel)
+          popup.addfstr(4, 41, "r: permit DNS resolution (<b>%s</b>)" % allowDnsLabel)
           
-          popup.addstr(4, 2, "s: sort ordering")
+          popup.addstr(5, 2, "s: sort ordering")
         elif page == 2:
           popup.addstr(1, 2, "up arrow: scroll up a line")
           popup.addstr(1, 41, "down arrow: scroll down a line")
@@ -343,22 +347,206 @@
         for key in PAUSEABLE: panels[key].setPaused(isPaused or key not in PAGES[page])
       finally:
         cursesLock.release()
-    elif (page == 1 and (key == ord('l') or key == ord('L'))) or (key == 27 and panels["conn"].listingType == connPanel.LIST_HOSTNAME and panels["control"].resolvingCounter != -1):
-      # either pressed 'l' on connection listing or canceling hostname resolution (esc on any page)
-      panels["conn"].listingType = (panels["conn"].listingType + 1) % len(connPanel.LIST_LABEL)
-      
-      if panels["conn"].listingType == connPanel.LIST_HOSTNAME:
-        curses.halfdelay(10) # refreshes display every second until done resolving
-        panels["control"].resolvingCounter = panels["conn"].resolver.totalResolves
+    elif key == 27 and panels["conn"].listingType == connPanel.LIST_HOSTNAME and panels["control"].resolvingCounter != -1:
+      # canceling hostname resolution (esc on any page)
+      panels["conn"].listingType = connPanel.LIST_IP
+      panels["control"].resolvingCounter = -1
+      panels["conn"].resolver.setPaused(True)
+      panels["conn"].sortConnections()
+    elif page == 1 and (key == ord('l') or key == ord('L')):
+      # provides menu to pick identification info listed for connections
+      cursesLock.acquire()
+      try:
+        for key in PAUSEABLE: panels[key].setPaused(True)
+        curses.cbreak() # wait indefinitely for key presses (no timeout)
+        popup = panels["popup"]
         
+        # uses smaller dimentions more fitting for small content
+        panels["popup"].height = 6
+        panels["popup"].recreate(stdscr, startY, 20)
+        
+        # hides top label of conn panel
+        panels["conn"].showLabel = False
+        panels["conn"].redraw()
+        
+        selection = panels["conn"].listingType    # starts with current option selected
+        options = [connPanel.LIST_IP, connPanel.LIST_HOSTNAME, connPanel.LIST_FINGERPRINT, connPanel.LIST_NICKNAME]
+        key = 0
+        
+        while key not in (curses.KEY_ENTER, 10, ord(' ')):
+          popup.clear()
+          popup.win.box()
+          popup.addstr(0, 0, "List By:", util.LABEL_ATTR)
+          
+          for i in range(len(options)):
+            sortType = options[i]
+            format = curses.A_STANDOUT if i == selection else curses.A_NORMAL
+            
+            if panels["conn"].listingType == sortType: tab = "> "
+            else: tab = "  "
+            sortLabel = connPanel.LIST_LABEL[sortType]
+            
+            popup.addstr(i + 1, 2, tab)
+            popup.addstr(i + 1, 4, sortLabel, format)
+          
+          popup.refresh()
+          key = stdscr.getch()
+          if key == curses.KEY_UP: selection = max(0, selection - 1)
+          elif key == curses.KEY_DOWN: selection = min(len(options) - 1, selection + 1)
+          elif key == 27:
+            # esc - cancel
+            selection = panels["conn"].listingType
+            key = curses.KEY_ENTER
+        
+        # reverts popup dimensions and conn panel label
+        panels["popup"].height = 9
+        panels["popup"].recreate(stdscr, startY, 80)
+        panels["conn"].showLabel = True
+        
+        # applies new setting
+        pickedOption = options[selection]
+        if pickedOption != panels["conn"].listingType:
+          panels["conn"].listingType = pickedOption
+          
+          if panels["conn"].listingType == connPanel.LIST_HOSTNAME:
+            curses.halfdelay(10) # refreshes display every second until done resolving
+            panels["control"].resolvingCounter = panels["conn"].resolver.totalResolves - panels["conn"].resolver.unresolvedQueue.qsize()
+            
+            resolver = panels["conn"].resolver
+            resolver.setPaused(not panels["conn"].allowDNS)
+            for connEntry in panels["conn"].connections: resolver.resolve(connEntry[connPanel.CONN_F_IP])
+          else:
+            panels["control"].resolvingCounter = -1
+            panels["conn"].resolver.setPaused(True)
+          
+          panels["conn"].sortConnections()
+        
+        curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
+      finally:
+        cursesLock.release()
+    elif page == 1 and panels["conn"].isCursorEnabled and key in (curses.KEY_ENTER, 10, ord(' ')):
+      # provides details on selected connection
+      cursesLock.acquire()
+      try:
+        for key in PAUSEABLE: panels[key].setPaused(True)
+        popup = panels["popup"]
+        
+        # reconfigures connection panel to accomidate details dialog
+        panels["conn"].showLabel = False
+        panels["conn"].showingDetails = True
+        panels["conn"].redraw()
+        
         resolver = panels["conn"].resolver
         resolver.setPaused(not panels["conn"].allowDNS)
-        for connEntry in panels["conn"].connections: resolver.resolve(connEntry[connPanel.CONN_F_IP])
-      else:
-        panels["control"].resolvingCounter = -1
-        resolver.setPaused(True)
+        relayLookupCache = {} # temporary cache of entry -> (ns data, desc data)
+        
+        while key not in (curses.KEY_ENTER, 10, ord(' ')):
+          key = 0
+          curses.cbreak() # wait indefinitely for key presses (no timeout)
+          popup.clear()
+          popup.win.box()
+          popup.addstr(0, 0, "Connection Details:", util.LABEL_ATTR)
+          
+          selection = panels["conn"].cursorSelection
+          if not selection: break
+          selectionColor = connPanel.TYPE_COLORS[selection[connPanel.CONN_TYPE]]
+          format = util.getColor(selectionColor) | curses.A_BOLD
+          
+          selectedIp = selection[connPanel.CONN_F_IP]
+          selectedPort = selection[connPanel.CONN_F_PORT]
+          addrLabel = "address: %s:%s" % (selectedIp, selectedPort)
+          
+          hostname = resolver.resolve(selectedIp)
+          if hostname == None:
+            if resolver.isPaused: hostname = "DNS resolution disallowed"
+            elif selectedIp not in resolver.resolvedCache.keys():
+              # if hostname is still being resolved refresh panel every half-second until it's completed
+              curses.halfdelay(5)
+              hostname = "resolving..."
+            else:
+              # hostname couldn't be resolved
+              hostname = "unknown"
+          elif len(hostname) > 73 - len(addrLabel):
+            # hostname too long - truncate
+            hostname = "%s..." % hostname[:70 - len(addrLabel)]
+          
+          popup.addstr(1, 2, "%s (%s)" % (addrLabel, hostname), format)
+          
+          locale = selection[connPanel.CONN_COUNTRY]
+          popup.addstr(2, 2, "locale: %s" % locale, format)
+          
+          # provides consensus data for selection (needs fingerprint to get anywhere...)
+          fingerprint = panels["conn"].getFingerprint(selectedIp, selectedPort)
+          
+          if fingerprint == "UNKNOWN":
+            if selectedIp not in panels["conn"].fingerprintMappings.keys():
+              # no consensus entry for this ip address
+              popup.addstr(3, 2, "No consensus data found", format)
+            else:
+              # couldn't resolve due to multiple matches - list them all
+              popup.addstr(3, 2, "Muliple matching IPs, possible fingerprints are:", format)
+              matchings = panels["conn"].fingerprintMappings[selectedIp]
+              
+              line = 4
+              for (matchPort, matchFingerprint) in matchings:
+                popup.addstr(line, 2, "%i. or port: %-5s fingerprint: %s" % (line - 3, matchPort, matchFingerprint), format)
+                line += 1
+                
+                if line == 7 and len(matchings) > 4:
+                  popup.addstr(8, 2, "... %i more" % len(matchings) - 3, format)
+                  break
+          else:
+            # fingerprint found - retrieve related data
+            if selection in relayLookupCache.keys(): nsEntry, descEntry = relayLookupCache[selection]
+            else:
+              nsData = conn.get_network_status("id/%s" % fingerprint)
+              
+              if len(nsData) > 1:
+                # multiple records for fingerprint (shouldn't happen)
+                panels["log"].monitor_event("WARN", "Multiple consensus entries for fingerprint: %s" % fingerprint)
+              
+              nsEntry = nsData[0]
+              descLookupCmd = "desc/id/%s" % fingerprint
+              descEntry = TorCtl.Router.build_from_desc(conn.get_info(descLookupCmd)[descLookupCmd].split("\n"), nsEntry)
+              relayLookupCache[selection] = (nsEntry, descEntry)
+            
+            popup.addstr(2, 15, "fingerprint: %s" % fingerprint, format)
+            
+            nickname = panels["conn"].getNickname(selectedIp, selectedPort)
+            dirPortLabel = "dirport: %i" % nsEntry.dirport if nsEntry.dirport else ""
+            popup.addstr(3, 2, "nickname: %-25s orport: %-10i %s" % (nickname, nsEntry.orport, dirPortLabel), format)
+            
+            popup.addstr(4, 2, "published: %-24s os: %-14s version: %s" % (descEntry.published, descEntry.os, descEntry.version), format)
+            popup.addstr(5, 2, "flags: %s" % ", ".join(nsEntry.flags), format)
+            
+            exitLine = ", ".join([str(k) for k in descEntry.exitpolicy])
+            if len(exitLine) > 63: exitLine = "%s..." % exitLine[:60]
+            popup.addstr(6, 2, "exit policy: %s" % exitLine, format)
+            
+            if descEntry.contact:
+              # clears up some common obscuring
+              contactAddr = descEntry.contact
+              obscuring = [(" at ", "@"), (" AT ", "@"), ("AT", "@"), (" dot ", "."), (" DOT ", ".")]
+              for match, replace in obscuring: contactAddr = contactAddr.replace(match, replace)
+              if len(contactAddr) > 67: contactAddr = "%s..." % contactAddr[:64]
+              popup.addstr(7, 2, "contact: %s" % contactAddr, format)
+          
+          popup.refresh()
+          key = stdscr.getch()
+          
+          if key == curses.KEY_RIGHT: key = curses.KEY_DOWN
+          elif key == curses.KEY_LEFT: key = curses.KEY_UP
+          
+          if key in (curses.KEY_DOWN, curses.KEY_UP, curses.KEY_PPAGE, curses.KEY_NPAGE):
+            panels["conn"].handleKey(key)
+        
+        panels["conn"].showLabel = True
+        panels["conn"].showingDetails = False
+        resolver.setPaused(not panels["conn"].allowDNS and panels["conn"].listingType == connPanel.LIST_HOSTNAME)
+        curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
+      finally:
+        cursesLock.release()
       
-      panels["conn"].sortConnections()
     elif page == 1 and (key == ord('s') or key == ord('S')):
       # set ordering for connection listing
       cursesLock.acquire()
@@ -368,8 +556,8 @@
         
         # lists event types
         popup = panels["popup"]
-        selections = []    # new ordering
-        cursorLoc = 0     # index of highlighted option
+        selections = []     # new ordering
+        cursorLoc = 0       # index of highlighted option
         
         # listing of inital ordering
         prevOrdering = "<b>Current Order: "
@@ -417,6 +605,7 @@
               selections.append(connPanel.getSortType(selection.replace("Tor ID", "Fingerprint")))
               options.remove(selection)
               cursorLoc = min(cursorLoc, len(options) - 1)
+          elif key == 27: break # esc - cancel
           
         if len(selections) == 3:
           panels["conn"].sortOrdering = selections

Modified: arm/trunk/interface/hostnameResolver.py
===================================================================
--- arm/trunk/interface/hostnameResolver.py	2009-07-19 04:00:11 UTC (rev 20086)
+++ arm/trunk/interface/hostnameResolver.py	2009-07-19 08:09:48 UTC (rev 20087)
@@ -46,11 +46,12 @@
       t.start()
       self.threadPool.append(t)
   
-  def resolve(self, ipAddr):
+  def resolve(self, ipAddr, blockTime = 0):
     """
     Provides hostname associated with an IP address. If not found this returns
     None and performs a reverse DNS lookup for future reference. This also
-    provides None if the address couldn't be resolved.
+    provides None if the address couldn't be resolved. This can be made to block
+    if some delay is tolerable.
     """
     
     # if outstanding requests are done then clear recentQueries so we can run erronious requests again
@@ -73,6 +74,16 @@
           if entryAge < threshold: toDelete.append(entryAddr)
         
         for entryAddr in toDelete: del self.resolvedCache[entryAddr]
+      
+      if blockTime > 0 and not self.isPaused:
+        timeWaited = 0
+        
+        while ipAddr not in self.resolvedCache.keys() and timeWaited < blockTime:
+          time.sleep(0.1)
+          timeWaited += 0.1
+        
+        if ipAddr in self.resolvedCache.keys(): return self.resolvedCache[ipAddr][0]
+        else: return None
   
   def setPaused(self, isPause):
     """