[or-cvs] r20087: {arm} Last substantial feature on my to-do list. added: connection (in arm/trunk: . interface)
atagar at seul.org
atagar at seul.org
Sun Jul 19 08:09:49 UTC 2009
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):
"""
More information about the tor-commits
mailing list