Author: atagar Date: 2011-04-04 15:22:31 +0000 (Mon, 04 Apr 2011) New Revision: 24555
Added: arm/release/src/interface/connections/ arm/release/src/interface/connections/__init__.py arm/release/src/interface/connections/circEntry.py arm/release/src/interface/connections/connEntry.py arm/release/src/interface/connections/connPanel.py arm/release/src/interface/connections/entries.py arm/release/src/util/enum.py Removed: arm/release/TODO arm/release/src/interface/connections/__init__.py arm/release/src/interface/connections/circEntry.py arm/release/src/interface/connections/connEntry.py arm/release/src/interface/connections/connPanel.py arm/release/src/interface/connections/entries.py Modified: arm/release/ arm/release/ChangeLog arm/release/README arm/release/armrc.sample arm/release/src/interface/configPanel.py arm/release/src/interface/connPanel.py arm/release/src/interface/controller.py arm/release/src/interface/descriptorPopup.py arm/release/src/interface/graphing/__init__.py arm/release/src/interface/graphing/bandwidthStats.py arm/release/src/interface/graphing/connStats.py arm/release/src/interface/graphing/graphPanel.py arm/release/src/interface/headerPanel.py arm/release/src/interface/logPanel.py arm/release/src/interface/torrcPanel.py arm/release/src/settings.cfg arm/release/src/starter.py arm/release/src/test.py arm/release/src/util/__init__.py arm/release/src/util/conf.py arm/release/src/util/connections.py arm/release/src/util/log.py arm/release/src/util/panel.py arm/release/src/util/procTools.py arm/release/src/util/sysTools.py arm/release/src/util/torConfig.py arm/release/src/util/torTools.py arm/release/src/util/uiTools.py arm/release/src/version.py Log: Arm version 1.4.2 release.
Property changes on: arm/release ___________________________________________________________________ Modified: svn:mergeinfo - /arm/trunk:22227-24074 + /arm/trunk:22227-24554
Modified: arm/release/ChangeLog =================================================================== --- arm/release/ChangeLog 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/ChangeLog 2011-04-04 15:22:31 UTC (rev 24555) @@ -1,5 +1,53 @@ CHANGE LOG
+4/4/11 - version 1.4.2 +This release chiefly consists of a fully reimplemented connection panel. Besides being a sane, maintainable implementation this includes numerous new features and improvements like full circuit paths, applications involved for local connections, and better type identification. + + * added: full rewrite of the connection panel, providing: + o listing the full paths involved in active circuits + o identification of socks, hidden service, and controller applications (arm, vidalia, polipo, etc) + o identification of exit connections with the common usage for the port they're using + o display of the local -> internal -> external address when room is available (original patch by Fabian Keil) + o better accuracy and performance in identifying client and directory connections + o marking the uptimes for initial connections (arm only tracks connection uptimes since starting, so these entries are just minimum durations) + o lazily loading the initial IP -> fingerprint mappings to improve the startup time + o using the circuit-status to disambiguating multiple relays on the same IP address + o smarter space utilization, filling in smaller columns if there isn't room for higher priority but larger entries + o connection details popup changes: + + using the consensus exit policies rather than the longer descriptor versions when available + + displaying connection details no longer freezes the rest of the display + + detail panel uses the full screen width and is dynamically resizable + + more resilient to missing descriptors + * change: hiding most tor config values by default (idea by arma) + * change: dropping warning suggesting that users set the FetchUselessDescriptors option (suggestion by Sebastian and others) + * change: always starting the bandwidth field from zero rather than using the state file total, which only contains the last day's worth of data (thanks to guilhem) + * change: suggesting authentication and giving steps for it in the readme (suggestion by Sebastian) + * change: caching config display lines, which reduces the CPU usage when scrolling by around 40% + * change: added summaries for the remaining tor configuration options + * change: using a dedicated enum class rather than tuple sets + * fix: torrc validation requires 'GETINFO config-text' which was introduced in Tor verison 0.2.2.7 (caught by Sjon, talion, and torland, https://trac.torproject.org/projects/tor/ticket/2501) + * fix: off-by-one issue with the displayed line numbers for torrc errors (caught by Sjon) + * fix: bin function wasn't available before python 2.6 (caught by Paul Menzel) + * fix: mis-parsing family entries when there's no entry after the comma (caught by StrangeCharm, https://trac.torproject.org/projects/tor/ticket/2414) + * fix: preventing SOCKS and CONTROL connections from being expanded (patch by Fabian Keil) + * fix: disabling name resolution for application queries to avoid leaking to resolvers (patch by Fabian Keil) + * fix: reversing src and dst addresses of SOCKS and CONTROL connections (caught by Fabian Keil) + * fix: changing the 'APPLICATION' type to 'SOCKS' since the previous label was too long (caught by Fabian Keil) + * fix: crashing issue from unknown relay nicknames (caught by krkhan) + * fix: concurrency bug occasionally causing "syshook" stacktraces when shutting down + * fix: header panel displayed the wrong IP address if it changed since we first started (https://trac.torproject.org/projects/tor/ticket/2776) + * fix: unchecked OSError could cause us to crash when making directories (for instance if there was a permissions issue) + * fix: the availability check for bsd resolvers was broken, probably causing resolution to fail for a few seconds on that platform + * fix: dropping the pointless 'Log notice stdout' entry provided by config-text queries (https://trac.torproject.org/projects/tor/ticket/2362) + * fix: taking DirServer and AlternateDirAuthority into account when determining the directory authorities + * fix: consuming a little extra space in the connection panel when scrollbars aren't visible + * fix: dropping the deprecated 'features.config.descriptions.persistPath' config option + * fix: failed connection attempts to the control port were generating zombie connections (https://trac.torproject.org/projects/tor/ticket/2812) + * fix: concurrency bug in joining on the TorCtl thread when tor shut down + * fix: the 'startup.dataDirectory' config option was being ignored + * fix: recognizing the proper private ip ranges of the 172.* block + * fix: missing 'is default' option from config sort ordering + 1/7/11 - version 1.4.1 (r24054) Platform specific enhancements including BSD compatibility and vastly improved performance on Linux.
Modified: arm/release/README =================================================================== --- arm/release/README 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/README 2011-04-04 15:22:31 UTC (rev 24555) @@ -25,6 +25,22 @@ ... starting Tor with '--controlport <PORT>' ... or including 'ControlPort <PORT>' in your torrc
+It's also highly suggested for the control port to require authentication. +This can be done either with a cookie or password: + * Cookie Authentication - Controllers authenticate to Tor by providing the + contents of the control_auth_cookie file. To set this up... + - add "CookieAuthentication 1" to your torrc + - either restart Tor or run "pkill -sighup tor" + - this method of authentication is automatically handled by arm, so you + can still start arm as you normally would + + * Password Authentication - Attaching to the control port requires a + password. To set this up... + - run "tor --hash-password <your password>" + - add "HashedControlPassword <hashed password>" to your torrc + - either restart Tor or run "pkill -sighup tor" + - when starting up arm will prompt you for this password + For full functionality this also needs: - To be ran with the same user as tor to avoid permission issues with connection resolution and reading the torrc. @@ -111,7 +127,6 @@ ChangeLog - revision history LICENSE - copy of the gpl v3 README - um... guess you figured this one out - TODO - known issues, future plans, etc setup.py - distutils installation script for arm
src/ @@ -125,6 +140,13 @@ uninstall - removal script
interface/ + connections/ + __init__.py + connPanel.py - (page 2) lists the active tor connections + circEntry.py - circuit entries in the connection panel + connEntry.py - individual connections to or from the system + entries.py - common parent for connPanel display entries + graphing/ __init__.py graphPanel.py - (page 1) presents graphs for data instances @@ -139,7 +161,7 @@ logPanel.py - (page 1) displays tor, arm, and torctl events fileDescriptorPopup.py - (popup) displays file descriptors used by tor
- connPanel.py - (page 2) displays information on tor connections + connPanel.py - (page 2) deprecated counterpart for connections/* descriptorPopup.py - (popup) displays connection descriptor data
configPanel.py - (page 3) editor panel for the tor configuration @@ -149,6 +171,7 @@ __init__.py conf.py - loading and persistence for user configuration connections.py - service providing periodic connection lookups + enum.py - enumerations for ordered collections hostnames.py - service providing nonblocking reverse dns lookups log.py - aggregator for application events panel.py - wrapper for safely working with curses subwindows
Deleted: arm/release/TODO =================================================================== --- arm/release/TODO 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/TODO 2011-04-04 15:22:31 UTC (rev 24555) @@ -1,281 +0,0 @@ -TODO - -- Roadmap and completed work for next release (1.4.2) - [ ] refactor panels - Currently the interface is a bit of a rat's nest (especially the - controller). The goal is to use better modularization to both simplify - the codebase and make it possible to use smarter caching to improve - performance (far too much is done in the ui logic). This work is in - progress - /init and /util are done and /interface is partly done. Known - bugs are being fixed while refactoring. - - * conn panel - - expand client connections and note location in circuit (entry-exit) - - for clients give an option to list all connections, to tell which are - going through tor and which might be leaking - - check family members to see if they're alive (VERSION cell - handshake?) - - fallback when pid or connection querying via pid is unavailable - List all connections listed both by netstat and the consensus - - note when connection times are estimates (color?), ie connection - was established before arm - - connection uptime to associate inbound/outbound connections? - - identify controller connections (if it's arm, vidalia, etc) with - special detail page for them - - provide bridge / client country / exiting port statistics - Include bridge related data via GETINFO option (feature request - by waltman and ioerror). - - note the common port usage along with the exit statistics - - show the port used in scrubbed exit connections - - pick apart applications like iftop and pktstat to see how they get - per-connection bandwidth usage. Forum thread discussing it: - https://bbs.archlinux.org/viewtopic.php?pid=715906 - - include an option to show both the internal and external ips for the - local connection, ie: - myInternal --> myExternal --> foreign - idea and initial patch by Fabian Keil - - give a warning if family relays don't name us - * classify config options as useful (defaultly shown), standard, and - deprecated (configured to be hidden by default) - * check tor source for deprecated options like 'group' (are they - ignored? idea is thanks to NightMonkey) - * elaborate on the password prompt (suggestion by weasel) - * release prep - * pylint --indent-string=" " --disable=C,R interface/foo.py | less - * double check __init__.py and README for added or removed files - * wait a week, then bump package versions - * Debian - Contact: weasel (Peter Palfrader) - Initial Release: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=603056 - Update Instructions: - * TBD - - * Gentoo - Contact: NightMonkey (Jesse Adelman) - Initial Release: https://bugs.gentoo.org/show_bug.cgi?id=341731 - Update Instructions: - * go to https://bugs.gentoo.org - * make a generic bug with "net-misc/arm-X.X.X version bump, please" - - * ArchLinux - Contact: Spider.007 - Initial Release: http://aur.archlinux.org/packages.php?ID=44172 - Update Instructions: - * go to aur.archlinux.org - * select "Out-of-date" for the package - -- Roadmap for version 1.4.3 - [ ] refactor panels - [ ] controller and popup panels - [ ] attempt to clear controller password from memory - - http://www.codexon.com/posts/clearing-passwords-in-memory-with-python - * release prep - * pylint --indent-string=" " --disable=C,R interface/foo.py | less - * double check __init__.py and README for changes - -- Roadmap for version 1.3.9 - [ ] refactor panels - [ ] controller and popup panels - - allow arm to resume after restarting tor - This requires a full move to the torTools controller. - - improve on performance bottlenecks for startup time and cpu usage - - intermittent concurrency bugs during shutdown, one possible source: - https://trac.torproject.org/projects/tor/ticket/2144 - [ ] setup scripts for arm - [ ] updater (checks for a new tarball and installs it automatically) - - attempt to verify download signature, providing a warning if unable - to do so - [ ] look into CAPs to get around permission issues for connection - listing sudo wrapper for arm to help arm run as the same user as - tor? Irc suggestions: - - man capabilities - - http://www.linuxjournal.com/article/5737 - -- Bugs - * The default resolver isn't configurable. - * When saving the config the Log entry should be filtered out if unnecessary. - * The config write dialog (ie, the one for saving the config) has its a - misaligned border when it's smaller than the top detail section. - * The arm header panel doesn't properly reflect when the ip address - changes. This provides a notice event saying: - "Our IP Address has changed from X to Y; rebuilding descriptor (source Z)." - * The cpu usage spikes for scrollable content when the key's held. Try - coalescing the events. - * The manpage layout is system dependent, so the scraper needs to be more - resilient against being confused by whitespace. Another improvement is - including fallback results if the man page can't be parsed (suggested by - rransom, issue caught by NightMonkey). - * Log deduplication is currently an n^2 operation. Hence it can't handle - large logs (for instance, when at the DEBUG runlevel). Currently we're - timing out the function if it takes too long, but a more efficient method - for deduplication would be preferable. - * when in client mode and tor stops the header panel doesn't say so - * util/torTools.py: effective bandwidth rate/burst measurements don't take - SETCONF into consideration, blocked on: - https://trac.torproject.org/projects/tor/ticket/1692 - * log prepopulation fails to limit entries to the current tor instance if - the file isn't logged to at the NOTICE level. A fix is to use the - timestamps to see if it belongs to this tor instance. This requires - tor's uptime - blocked on implementation of the following proposal: - https://gitweb.torproject.org/tor.git/blob/HEAD:/doc/spec/proposals/173-geti... - * the STATUS_SERVER event may not be supported - 18:52 < mikeperry> atagar: I believe there is no event parsing for STATUS_SERVER - 18:53 < mikeperry> atagar: see TorCtl.EventSink and classes that inherit from it - 18:54 < mikeperry> specifically, TorCtl.EventHandler._decode1, _handle1, and _map1 - - * conn panel: - * *never* do reverse dns lookups for first hops (could be resolving via - tor and hence leaking to the exit) - * If there's duplicate family entries (and harder case: both nickname and - fingerprint entries for the same relay) then the duplicate should be - removed. This is also causing a bad scrolling bug where the cursor can't - get past the pair of duplicate entries. - * revise multikey sort of connections - Currently using a pretty ugly hack. Look at: - http://www.velocityreviews.com/forums/ - t356461-sorting-a-list-of-objects-by-multiple-attributes.html - and check for performance difference. - * replace checks against exit policy with Mike's torctl version - My version still isn't handling all inputs anyway (still need to handle - masks, private keyword, and prepended policy). Parse it from the rest - of the router if too heavy ("TorCtl.Router.will_exit_to instead"). - * avoid hostname lookups of private connections - Stripped most of them but suspect there might be others (have assertions - check for this in a debug mode?) - * connection uptimes shouldn't show fractions of a second - * connections aren't cleared when control port closes - -- Packaging - * OpenWrt - OpenWrt uses the opkg packaging format which could make use of - arm's current deb packages. Packaging for this platform would help with - the Torouter project: - https://trac.torproject.org/projects/tor/wiki/TheOnionRouter/Torouter - * Mac - Couple of options include macport and dmg... - * macport (http://guide.macports.org/#development) - Build-from-source distribution method (like BSD portinstall). This has - been suggested by several people. - - * dmg (http://en.wikipedia.org/wiki/Apple_Disk_Image) - Most conventional method of software distribution on mac. This is just - a container (no updating/removal support), but could contain an icon - for the dock that starts a terminal with arm. This might include a pkg - installer. - - * mpkg (http://pypi.python.org/pypi/bdist_mpkg/) - Plugin for distutils. Like most mac packaging, this can only run on a - mac. It also requires setuptools: - http://www.errorhelp.com/search/details/74034/importerror-no-module-named-se... - -- Future Features - * client mode use cases - * not sure what sort of information would be useful in the header (to - replace the orport, fingerprint, flags, etc) - * one idea by velope: - "whether you configured a dnsport, transport, etc. and whether they - were successfully opened. might be nice to know this after the log - messages might be gone." - [notice] Opening Socks listener on 127.0.0.1:9050 - [notice] Opening Transparent pf/netfilter listener on 127.0.0.1:9040 - [notice] Opening DNS listener on 127.0.0.1:53 - * rdns and whois lookups (to find ISP, country, and jurisdiction, etc) - To avoid disclosing connection data to third parties this needs to be - an all-or-nothing operation (ie, needs to fetch information on all - relays or none of them). Plan is something like: - * add resolving/caching capabilities to fetch information on all relays - and distil whois entries to just what we care about (hosting provider - or ISP), by default updating the cache on a daily basis - * construct tarball and make this available for download rather than - fetching everything at each client - * possibly make these archives downloadable from peer relays (this is a - no-go for clients) via torrents or some dirport like scheme - * look at Vidalia and TorK for ideas - * need to solicit for ideas on what would be most helpful to clients - * dialog with bridge statuses (idea by mikeperry) - https://trac.vidalia-project.net/ticket/570 - https://trac.torproject.org/projects/tor/ticket/2068 - * menus - * http://gnosis.cx/publish/programming/charming_python_6.html ? - * additional options: - * make update rates configurable via the ui - * dialog with flag descriptions and other help - * menu with all torrc options (making them editable/toggleable) - * control port interpreter (interactive prompt) - Panel and startup option (-t maybe?) for providing raw control port - access along with usability improvements (piggybacking on the arm - connection): - * irc like help (ex "/help GETINFO" could provide a summary of - getinfo commands, partly using the results from - "GETINFO info/names") - * tab completion and up/down for previous commands - * warn and get confirmation if command would disrupt arm (for - instance 'SETEVENTS') - * 'safe' option that restricts to read-only access (start with this) - * issue sighup reset - * make use of the new process/* GETINFO options - They'll be available in the next tor release, as per: - https://trac.torproject.org/projects/tor/ticket/2291 - * feature parity for arm's config values (armrc entries) - * editability - * parse descriptions from the man page? autogeneration of the man page from - something storing the descriptions - * handle mutiple tor instances - * screen style (dialog for switching between instances) - * extra window with whatever stats can be aggregated over all instances, - or a config option to aggregate stats for bw, resource usage, etc - * option to save the current settings to the config - * provide warning at startup if the armrc doesn't exist, with instructions - for generating it - * email alerts for changes to the relay's status, similar to tor-weather - * simple alert if tor shuts down - * accounting and alerts for if the bandwidth drops to zero - * daily/weekly/etc alerts for basic status (log output, bandwidth history, - etc), borrowing from the consensus tracker for some of the formatting - * tab completion for input fields that expect a filesystem path - * look through vidalia's tickets for more ideas - https://trac.vidalia-project.net/ - * look into additions to the used apis - * curses (python 2.6 extended?): http://docs.python.org/library/curses.html - * new control options (like "desc-annotations/id/<OR identity>")? - * look into better supporting hidden services (what could be useful here?) - * provide option for a consensus page - Shows full consensus with an interface similar to the connection panel. - For this Mike's ConsensusTracker would be helpful (though boost the - startup time by several seconds) - * show qos stats - Take a look at 'linux-tor-prio.sh' to see if any of the stats are - available and interesting. - * escaping function for uiTools' formatted strings - * switch check of ip address validity to regex? - match = re.match("(\d*).(\d*).(\d*).(\d*)", ip) - http://wang.yuxuan.org/blog/2009/4/2/python_script_to_convert_from_ip_range_... - * setup wizard for new relays - Setting the password and such for torrc generation. Maybe a netinstaller - that fetches the right package for the plagform, verifies signatures, etc? - Another alternative would be that when arm is started and tor isn't - running offer to start tor as a client, relay, or bridge. (idea by ioerror) - * audit what tor does - * Provide warnings if tor connections misbehaves, for instance: - * ensuring ExitPolicyRejectPrivate is being obeyed - * check that ExitPolicy violations don't occur (not possible yet since - not all relays aren't identified) - * check that all connections are properly related to a circuit, for - instance no outbound connections without a corresponding inbound (not - possible yet due to being unable to correlate connections to circuits) - * check file descriptors being accessed by tor to see if they're outside a - known pattern - * script that dumps relay stats to stdout - Derived from an idea by StrangeCharm. Django has a small terminal coloring - module that could be nice for formatting. Could possibly include: - * desc / ns information for our relay - * ps / netstat stats like load, uptime, and connection counts, etc - * implement control-spec proposals: - * https://gitweb.torproject.org/tor.git/blob/HEAD:/doc/spec/proposals/172-circ... - * https://gitweb.torproject.org/tor.git/blob/HEAD:/doc/spec/proposals/173-geti... - * gui frontend (gtk?) - Look into if the arm utilities and codebase would fit nicely for a gui - controller like Vidalia and TorK. - * unit tests - Primarily for util, for instance 'addfstr' would be a good candidate. - * python 3 compatibility - Currently blocked on TorCtl support. -
Modified: arm/release/armrc.sample =================================================================== --- arm/release/armrc.sample 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/armrc.sample 2011-04-04 15:22:31 UTC (rev 24555) @@ -62,12 +62,11 @@
# Paremters for the config panel # --------------------------- -# type -# 0 -> tor state, 1 -> torrc, 2 -> arm state, 3 -> armrc # order # three comma separated configuration attributes, options including: -# 0 -> Category, 1 -> Option Name, 2 -> Value, 3 -> Arg Type, -# 4 -> Arg Usage, 5 -> Description, 6 -> Man Entry, 7 -> Is Default +# 0 -> Category, 1 -> Option Name, 2 -> Value, 3 -> Arg Type, +# 4 -> Arg Usage, 5 -> Summary, 6 -> Description, 7 -> Man Entry, +# 8 -> Is Default # selectionDetails.height # rows of data for the panel showing details on the current selection, this # is disabled entirely if zero @@ -87,12 +86,11 @@ # file.maxLinesPerEntry # max number of lines to display for a single entry in the torrc
-features.config.type 0 -features.config.order 0, 6, 7 +features.config.order 7, 1, 8 features.config.selectionDetails.height 6 features.config.prepopulateEditValues true features.config.state.colWidth.option 25 -features.config.state.colWidth.value 10 +features.config.state.colWidth.value 15 features.config.state.showPrivateOptions false features.config.state.showVirtualOptions false features.config.file.showScrollbars true @@ -104,12 +102,11 @@ # --------------------------- # enabled # allows the descriptions to be fetched from the man page if true -# persistPath -# location descriptions should be loaded from and saved to (this feature is -# disabled if unset) +# persist +# caches the descriptions (substantially saving on future startup times)
features.config.descriptions.enabled true -features.config.descriptions.persistPath /tmp/arm/torConfigDescriptions.txt +features.config.descriptions.persist true
# General graph parameters # ------------------------ @@ -139,6 +136,9 @@ # prepopulate # attempts to use tor's state file to prepopulate the bandwidth graph at the # 15-minute interval (this requires the minimum of a day's worth of uptime) +# prepopulateTotal +# populates the total stat from the state file if true (this only contains +# the last day's worth of information, so this metric isn't the true total) # transferInBystes # shows rate measurments in bytes if true, bits otherwise # accounting.show @@ -149,11 +149,57 @@ # provides verbose measurements of time if true
features.graph.bw.prepopulate true +features.graph.bw.prepopulateTotal false features.graph.bw.transferInBytes false features.graph.bw.accounting.show true features.graph.bw.accounting.rate 10 features.graph.bw.accounting.isTimeLong false
+# Parameters for connection display +# --------------------------------- +# oldPanel +# includes the old connection panel in the interface +# newPanel +# includes the new connection panel in the interface +# listingType +# the primary category of information shown by default, options including: +# 0 -> IP Address / Port 1 -> Hostname +# 2 -> Fingerprint 3 -> Nickname +# order +# three comma separated configuration attributes, options including: +# 0 -> Category, 1 -> Uptime, 2 -> Listing, 3 -> IP Address, +# 4 -> Port, 5 -> Hostname, 6 -> Fingerprint, 7 -> Nickname, +# 8 -> Country +# refreshRate +# rate at which the connection panel contents is redrawn (if higher than the +# connection resolution rate then reducing this won't casue new data to +# appear more frequently - just increase the rate at which the uptime field +# is updated) +# resolveApps +# issues lsof queries to determining the applications involved in local +# SOCKS and CONTROL connections +# markInitialConnections +# if true, the uptime of the initial connections when we start are marked +# with a '+' (these uptimes are estimates since arm can only track a +# connection's duration while it runs) +# showExitPort +# shows port related information of exit connections we relay if true +# showColumn.* +# toggles the visability of the connection table columns + +features.connection.oldPanel false +features.connection.newPanel true +features.connection.listingType 0 +features.connection.order 0, 2, 1 +features.connection.refreshRate 5 +features.connection.resolveApps true +features.connection.markInitialConnections true +features.connection.showExitPort true +features.connection.showColumn.fingerprint true +features.connection.showColumn.nickname true +features.connection.showColumn.destination true +features.connection.showColumn.expandedIp true + # Thread pool size for hostname resolutions # Determines the maximum number of concurrent requests. Upping this to around # thirty or so seems to be problematic, causing intermittently seizing. @@ -187,6 +233,7 @@ log.torGetInfo DEBUG log.torGetInfoCache NONE log.torGetConf DEBUG +log.torGetConfCache NONE log.torSetConf INFO log.torEventTypeUnrecognized NOTICE log.torPrefixPathInvalid NOTICE @@ -224,6 +271,7 @@ log.cursesColorSupport INFO log.bsdJailFound INFO log.unknownBsdJailId WARN +log.geoipUnavailable WARN log.stats.failedProcResolution DEBUG log.stats.procResolutionFailover INFO log.stats.failedPsResolution INFO
Modified: arm/release/src/interface/configPanel.py =================================================================== --- arm/release/src/interface/configPanel.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/interface/configPanel.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -6,65 +6,76 @@ import curses import threading
-from util import conf, panel, torTools, torConfig, uiTools +from util import conf, enum, panel, torTools, torConfig, uiTools
DEFAULT_CONFIG = {"features.config.selectionDetails.height": 6, "features.config.state.showPrivateOptions": False, "features.config.state.showVirtualOptions": False, "features.config.state.colWidth.option": 25, - "features.config.state.colWidth.value": 10} + "features.config.state.colWidth.value": 15}
# TODO: The arm use cases are incomplete since they currently can't be # modified, have their descriptions fetched, or even get a complete listing # of what's available. -TOR_STATE, ARM_STATE = range(1, 3) # state to be presented +State = enum.Enum("TOR", "ARM") # state to be presented
# mappings of option categories to the color for their entries -CATEGORY_COLOR = {torConfig.GENERAL: "green", - torConfig.CLIENT: "blue", - torConfig.SERVER: "yellow", - torConfig.DIRECTORY: "magenta", - torConfig.AUTHORITY: "red", - torConfig.HIDDEN_SERVICE: "cyan", - torConfig.TESTING: "white", - torConfig.UNKNOWN: "white"} +CATEGORY_COLOR = {torConfig.Category.GENERAL: "green", + torConfig.Category.CLIENT: "blue", + torConfig.Category.RELAY: "yellow", + torConfig.Category.DIRECTORY: "magenta", + torConfig.Category.AUTHORITY: "red", + torConfig.Category.HIDDEN_SERVICE: "cyan", + torConfig.Category.TESTING: "white", + torConfig.Category.UNKNOWN: "white"}
# attributes of a ConfigEntry -FIELD_CATEGORY, FIELD_OPTION, FIELD_VALUE, FIELD_TYPE, FIELD_ARG_USAGE, FIELD_SUMMARY, FIELD_DESCRIPTION, FIELD_MAN_ENTRY, FIELD_IS_DEFAULT = range(9) -DEFAULT_SORT_ORDER = (FIELD_CATEGORY, FIELD_MAN_ENTRY, FIELD_IS_DEFAULT) -FIELD_ATTR = {FIELD_CATEGORY: ("Category", "red"), - FIELD_OPTION: ("Option Name", "blue"), - FIELD_VALUE: ("Value", "cyan"), - FIELD_TYPE: ("Arg Type", "green"), - FIELD_ARG_USAGE: ("Arg Usage", "yellow"), - FIELD_SUMMARY: ("Summary", "green"), - FIELD_DESCRIPTION: ("Description", "white"), - FIELD_MAN_ENTRY: ("Man Page Entry", "blue"), - FIELD_IS_DEFAULT: ("Is Default", "magenta")} +Field = enum.Enum("CATEGORY", "OPTION", "VALUE", "TYPE", "ARG_USAGE", + "SUMMARY", "DESCRIPTION", "MAN_ENTRY", "IS_DEFAULT") +DEFAULT_SORT_ORDER = (Field.MAN_ENTRY, Field.OPTION, Field.IS_DEFAULT) +FIELD_ATTR = {Field.CATEGORY: ("Category", "red"), + Field.OPTION: ("Option Name", "blue"), + Field.VALUE: ("Value", "cyan"), + Field.TYPE: ("Arg Type", "green"), + Field.ARG_USAGE: ("Arg Usage", "yellow"), + Field.SUMMARY: ("Summary", "green"), + Field.DESCRIPTION: ("Description", "white"), + Field.MAN_ENTRY: ("Man Page Entry", "blue"), + Field.IS_DEFAULT: ("Is Default", "magenta")}
class ConfigEntry(): """ Configuration option in the panel. """
- def __init__(self, option, type, isDefault, summary, manEntry): + def __init__(self, option, type, isDefault): self.fields = {} - self.fields[FIELD_OPTION] = option - self.fields[FIELD_TYPE] = type - self.fields[FIELD_IS_DEFAULT] = isDefault + self.fields[Field.OPTION] = option + self.fields[Field.TYPE] = type + self.fields[Field.IS_DEFAULT] = isDefault
+ # Fetches extra infromation from external sources (the arm config and tor + # man page). These are None if unavailable for this config option. + summary = torConfig.getConfigSummary(option) + manEntry = torConfig.getConfigDescription(option) + if manEntry: - self.fields[FIELD_MAN_ENTRY] = manEntry.index - self.fields[FIELD_CATEGORY] = manEntry.category - self.fields[FIELD_ARG_USAGE] = manEntry.argUsage - self.fields[FIELD_DESCRIPTION] = manEntry.description + self.fields[Field.MAN_ENTRY] = manEntry.index + self.fields[Field.CATEGORY] = manEntry.category + self.fields[Field.ARG_USAGE] = manEntry.argUsage + self.fields[Field.DESCRIPTION] = manEntry.description else: - self.fields[FIELD_MAN_ENTRY] = 99999 # sorts non-man entries last - self.fields[FIELD_CATEGORY] = torConfig.UNKNOWN - self.fields[FIELD_ARG_USAGE] = "" - self.fields[FIELD_DESCRIPTION] = "" + self.fields[Field.MAN_ENTRY] = 99999 # sorts non-man entries last + self.fields[Field.CATEGORY] = torConfig.Category.UNKNOWN + self.fields[Field.ARG_USAGE] = "" + self.fields[Field.DESCRIPTION] = ""
- self.fields[FIELD_SUMMARY] = summary if summary != None else self.fields[FIELD_DESCRIPTION] + # uses the full man page description if a summary is unavailable + self.fields[Field.SUMMARY] = summary if summary != None else self.fields[Field.DESCRIPTION] + + # cache of what's displayed for this configuration option + self.labelCache = None + self.labelCacheArgs = None
def get(self, field): """ @@ -74,9 +85,44 @@ field - enum for the field to be provided back """
- if field == FIELD_VALUE: return self._getValue() + if field == Field.VALUE: return self._getValue() else: return self.fields[field]
+ def getAll(self, fields): + """ + Provides back a list with the given field values. + + Arguments: + field - enums for the fields to be provided back + """ + + return [self.get(field) for field in fields] + + def getLabel(self, optionWidth, valueWidth, summaryWidth): + """ + Provides display string of the configuration entry with the given + constraints on the width of the contents. + + Arguments: + optionWidth - width of the option column + valueWidth - width of the value column + summaryWidth - width of the summary column + """ + + # Fetching the display entries is very common so this caches the values. + # Doing this substantially drops cpu usage when scrolling (by around 40%). + + argSet = (optionWidth, valueWidth, summaryWidth) + if not self.labelCache or self.labelCacheArgs != argSet: + optionLabel = uiTools.cropStr(self.get(Field.OPTION), optionWidth) + valueLabel = uiTools.cropStr(self.get(Field.VALUE), valueWidth) + summaryLabel = uiTools.cropStr(self.get(Field.SUMMARY), summaryWidth, None) + lineTextLayout = "%%-%is %%-%is %%-%is" % (optionWidth, valueWidth, summaryWidth) + self.labelCache = lineTextLayout % (optionLabel, valueLabel, summaryLabel) + self.labelCacheArgs = argSet + + return self.labelCache + def _getValue(self): """ Provides the current value of the configuration entry, taking advantage of @@ -84,28 +130,18 @@ value's type to provide a user friendly representation if able. """
- confValue = ", ".join(torTools.getConn().getOption(self.get(FIELD_OPTION), [], True)) + confValue = ", ".join(torTools.getConn().getOption(self.get(Field.OPTION), [], True))
# provides nicer values for recognized types if not confValue: confValue = "<none>" - elif self.get(FIELD_TYPE) == "Boolean" and confValue in ("0", "1"): + elif self.get(Field.TYPE) == "Boolean" and confValue in ("0", "1"): confValue = "False" if confValue == "0" else "True" - elif self.get(FIELD_TYPE) == "DataSize" and confValue.isdigit(): + elif self.get(Field.TYPE) == "DataSize" and confValue.isdigit(): confValue = uiTools.getSizeLabel(int(confValue)) - elif self.get(FIELD_TYPE) == "TimeInterval" and confValue.isdigit(): + elif self.get(Field.TYPE) == "TimeInterval" and confValue.isdigit(): confValue = uiTools.getTimeLabel(int(confValue), isLong = True)
return confValue - - def getAttr(self, argTypes): - """ - Provides back a list with the given parameters. - - Arguments: - argTypes - list of enums for the arguments to be provided back - """ - - return [self.get(field) for field in argTypes]
class ConfigPanel(panel.Panel): """ @@ -124,14 +160,22 @@ "features.config.state.colWidth.option": 5, "features.config.state.colWidth.value": 5})
- self.sortOrdering = config.getIntCSV("features.config.order", self.sortOrdering, 3, 0, 6) + sortFields = Field.values() + customOrdering = config.getIntCSV("features.config.order", None, 3, 0, len(sortFields)) + + if customOrdering: + self.sortOrdering = [sortFields[i] for i in customOrdering]
self.configType = configType self.confContents = [] self.scroller = uiTools.Scroller(True) self.valsLock = threading.RLock()
- if self.configType == TOR_STATE: + # shows all configuration options if true, otherwise only the ones with + # the 'important' flag are shown + self.showAll = False + + if self.configType == State.TOR: conn = torTools.getConn() customOptions = torConfig.getCustomOptions() configOptionLines = conn.getInfo("config/names", "").strip().split("\n") @@ -141,29 +185,37 @@ # UseEntryGuards Boolean confOption, confType = line.strip().split(" ", 1)
- # skips private and virtual entries if not set to show them + # skips private and virtual entries if not configured to show them if not self._config["features.config.state.showPrivateOptions"] and confOption.startswith("__"): continue elif not self._config["features.config.state.showVirtualOptions"] and confType == "Virtual": continue
- summary = torConfig.getConfigSummary(confOption) - manEntry = torConfig.getConfigDescription(confOption) - self.confContents.append(ConfigEntry(confOption, confType, not confOption in customOptions, summary, manEntry)) - - self.setSortOrder() # initial sorting of the contents - elif self.configType == ARM_STATE: + self.confContents.append(ConfigEntry(confOption, confType, not confOption in customOptions)) + elif self.configType == State.ARM: # loaded via the conf utility armConf = conf.getConfig("arm") for key in armConf.getKeys(): pass # TODO: implement + + # mirror listing with only the important configuration options + self.confImportantContents = [] + for entry in self.confContents: + if torConfig.isImportant(entry.get(Field.OPTION)): + self.confImportantContents.append(entry) + + # if there aren't any important options then show everything + if not self.confImportantContents: + self.confImportantContents = self.confContents + + self.setSortOrder() # initial sorting of the contents
def getSelection(self): """ Provides the currently selected entry. """
- return self.scroller.getCursorSelection(self.confContents) + return self.scroller.getCursorSelection(self._getConfigOptions())
def setSortOrder(self, ordering = None): """ @@ -177,7 +229,8 @@
self.valsLock.acquire() if ordering: self.sortOrdering = ordering - self.confContents.sort(key=lambda i: (i.getAttr(self.sortOrdering))) + self.confContents.sort(key=lambda i: (i.getAll(self.sortOrdering))) + self.confImportantContents.sort(key=lambda i: (i.getAll(self.sortOrdering))) self.valsLock.release()
def handleKey(self, key): @@ -188,107 +241,104 @@ if detailPanelHeight > 0 and detailPanelHeight + 2 <= pageHeight: pageHeight -= (detailPanelHeight + 1)
- isChanged = self.scroller.handleKey(key, self.confContents, pageHeight) + isChanged = self.scroller.handleKey(key, self._getConfigOptions(), pageHeight) if isChanged: self.redraw(True) + elif key == ord('a') or key == ord('A'): + self.showAll = not self.showAll + self.redraw(True) self.valsLock.release()
- def draw(self, subwindow, width, height): + def draw(self, width, height): self.valsLock.acquire()
# draws the top label - titleLabel = "%s Configuration:" % ("Tor" if self.configType == TOR_STATE else "Arm") - self.addstr(0, 0, titleLabel, curses.A_STANDOUT) + configType = "Tor" if self.configType == State.TOR else "Arm" + hiddenMsg = "press 'a' to hide most options" if self.showAll else "press 'a' to show all options"
# panel with details for the current selection detailPanelHeight = self._config["features.config.selectionDetails.height"] + isScrollbarVisible = False if detailPanelHeight == 0 or detailPanelHeight + 2 >= height: # no detail panel detailPanelHeight = 0 - scrollLoc = self.scroller.getScrollLoc(self.confContents, height - 1) + scrollLoc = self.scroller.getScrollLoc(self._getConfigOptions(), height - 1) cursorSelection = self.getSelection() + isScrollbarVisible = len(self._getConfigOptions()) > height - 1 else: # Shrink detail panel if there isn't sufficient room for the whole # thing. The extra line is for the bottom border. detailPanelHeight = min(height - 1, detailPanelHeight + 1) - scrollLoc = self.scroller.getScrollLoc(self.confContents, height - 1 - detailPanelHeight) + scrollLoc = self.scroller.getScrollLoc(self._getConfigOptions(), height - 1 - detailPanelHeight) cursorSelection = self.getSelection() + isScrollbarVisible = len(self._getConfigOptions()) > height - detailPanelHeight - 1
- self._drawSelectionPanel(cursorSelection, width, detailPanelHeight, titleLabel) + self._drawSelectionPanel(cursorSelection, width, detailPanelHeight, isScrollbarVisible)
+ titleLabel = "%s Configuration (%s):" % (configType, hiddenMsg) + self.addstr(0, 0, titleLabel, curses.A_STANDOUT) + # draws left-hand scroll bar if content's longer than the height - scrollOffset = 0 - if len(self.confContents) > height - detailPanelHeight - 1: + scrollOffset = 1 + if isScrollbarVisible: scrollOffset = 3 - self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelHeight - 1, len(self.confContents), 1 + detailPanelHeight) + self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelHeight - 1, len(self._getConfigOptions()), 1 + detailPanelHeight)
optionWidth = self._config["features.config.state.colWidth.option"] valueWidth = self._config["features.config.state.colWidth.value"] descriptionWidth = max(0, width - scrollOffset - optionWidth - valueWidth - 2)
- for lineNum in range(scrollLoc, len(self.confContents)): - entry = self.confContents[lineNum] + for lineNum in range(scrollLoc, len(self._getConfigOptions())): + entry = self._getConfigOptions()[lineNum] drawLine = lineNum + detailPanelHeight + 1 - scrollLoc
- optionLabel = uiTools.cropStr(entry.get(FIELD_OPTION), optionWidth) - valueLabel = uiTools.cropStr(entry.get(FIELD_VALUE), valueWidth) - summaryLabel = uiTools.cropStr(entry.get(FIELD_SUMMARY), descriptionWidth, None) - - lineFormat = curses.A_NORMAL if entry.get(FIELD_IS_DEFAULT) else curses.A_BOLD - if entry.get(FIELD_CATEGORY): lineFormat |= uiTools.getColor(CATEGORY_COLOR[entry.get(FIELD_CATEGORY)]) + lineFormat = curses.A_NORMAL if entry.get(Field.IS_DEFAULT) else curses.A_BOLD + if entry.get(Field.CATEGORY): lineFormat |= uiTools.getColor(CATEGORY_COLOR[entry.get(Field.CATEGORY)]) if entry == cursorSelection: lineFormat |= curses.A_STANDOUT
- lineTextLayout = "%%-%is %%-%is %%-%is" % (optionWidth, valueWidth, descriptionWidth) - lineText = lineTextLayout % (optionLabel, valueLabel, summaryLabel) + lineText = entry.getLabel(optionWidth, valueWidth, descriptionWidth) self.addstr(drawLine, scrollOffset, lineText, lineFormat)
if drawLine >= height: break
self.valsLock.release()
- def _drawSelectionPanel(self, cursorSelection, width, detailPanelHeight, titleLabel): + def _getConfigOptions(self): + return self.confContents if self.showAll else self.confImportantContents + + def _drawSelectionPanel(self, selection, width, detailPanelHeight, isScrollbarVisible): """ Renders a panel for the selected configuration option. """
- # border (top) - if width >= len(titleLabel): - self.win.hline(0, len(titleLabel), curses.ACS_HLINE, width - len(titleLabel)) - self.win.addch(0, width, curses.ACS_URCORNER) + # 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, detailPanelHeight + 1) + if isScrollbarVisible: self.addch(detailPanelHeight, 1, curses.ACS_TTEE)
- # border (sides) - self.win.vline(1, 0, curses.ACS_VLINE, detailPanelHeight - 1) - self.win.vline(1, width, curses.ACS_VLINE, detailPanelHeight - 1) + selectionFormat = curses.A_BOLD | uiTools.getColor(CATEGORY_COLOR[selection.get(Field.CATEGORY)])
- # border (bottom) - self.win.addch(detailPanelHeight, 0, curses.ACS_LLCORNER) - if width >= 2: self.win.addch(detailPanelHeight, 1, curses.ACS_TTEE) - if width >= 3: self.win.hline(detailPanelHeight, 2, curses.ACS_HLINE, width - 2) - self.win.addch(detailPanelHeight, width, curses.ACS_LRCORNER) - - selectionFormat = curses.A_BOLD | uiTools.getColor(CATEGORY_COLOR[cursorSelection.get(FIELD_CATEGORY)]) - # first entry: # <option> (<category> Option) - optionLabel =" (%s Option)" % torConfig.OPTION_CATEGORY_STR[cursorSelection.get(FIELD_CATEGORY)] - self.addstr(1, 2, cursorSelection.get(FIELD_OPTION) + optionLabel, selectionFormat) + optionLabel =" (%s Option)" % selection.get(Field.CATEGORY) + self.addstr(1, 2, selection.get(Field.OPTION) + optionLabel, selectionFormat)
# second entry: # Value: <value> ([default|custom], <type>, usage: <argument usage>) if detailPanelHeight >= 3: valueAttr = [] - valueAttr.append("default" if cursorSelection.get(FIELD_IS_DEFAULT) else "custom") - valueAttr.append(cursorSelection.get(FIELD_TYPE)) - valueAttr.append("usage: %s" % (cursorSelection.get(FIELD_ARG_USAGE))) + valueAttr.append("default" if selection.get(Field.IS_DEFAULT) else "custom") + valueAttr.append(selection.get(Field.TYPE)) + valueAttr.append("usage: %s" % (selection.get(Field.ARG_USAGE))) valueAttrLabel = ", ".join(valueAttr)
valueLabelWidth = width - 12 - len(valueAttrLabel) - valueLabel = uiTools.cropStr(cursorSelection.get(FIELD_VALUE), valueLabelWidth) + valueLabel = uiTools.cropStr(selection.get(Field.VALUE), valueLabelWidth)
self.addstr(2, 2, "Value: %s (%s)" % (valueLabel, valueAttrLabel), selectionFormat)
# remainder is filled with the man page description descriptionHeight = max(0, detailPanelHeight - 3) - descriptionContent = "Description: " + cursorSelection.get(FIELD_DESCRIPTION) + descriptionContent = "Description: " + selection.get(Field.DESCRIPTION)
for i in range(descriptionHeight): # checks if we're done writing the description @@ -304,7 +354,7 @@
if i != descriptionHeight - 1: # there's more lines to display - msg, remainder = uiTools.cropStr(lineContent, width - 2, 4, 4, uiTools.END_WITH_HYPHEN, True) + msg, remainder = uiTools.cropStr(lineContent, width - 2, 4, 4, uiTools.Ending.HYPHEN, True) descriptionContent = remainder.strip() + descriptionContent else: # this is the last line, end it with an ellipse
Modified: arm/release/src/interface/connPanel.py =================================================================== --- arm/release/src/interface/connPanel.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/interface/connPanel.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -508,7 +508,7 @@ else: return # skip following redraw self.redraw(True)
- def draw(self, subwindow, width, height): + def draw(self, width, height): self.connectionsLock.acquire() try: # hostnames frequently get updated so frequent sorting needed @@ -529,7 +529,7 @@ if self.showingDetails: listingHeight -= 8 isScrollBarVisible = len(self.connections) > height - 9 - if width > 80: subwindow.hline(8, 80, curses.ACS_HLINE, width - 81) + if width > 80: self.win.hline(8, 80, curses.ACS_HLINE, width - 81) else: isScrollBarVisible = len(self.connections) > height - 1 xOffset = 3 if isScrollBarVisible else 0 # content offset for scroll bar @@ -878,6 +878,8 @@ self.familyFingerprints = {}
for familyEntry in self.family: + if not familyEntry: continue + if familyEntry[0] == "$": # relay identified by fingerprint self.familyFingerprints[familyEntry] = familyEntry[1:]
Deleted: arm/release/src/interface/connections/__init__.py =================================================================== --- arm/trunk/src/interface/connections/__init__.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/interface/connections/__init__.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -1,6 +0,0 @@ -""" -Panels, popups, and handlers comprising the arm user interface. -""" - -__all__ = ["circEntry", "connEntry", "connPanel", "entries"] -
Copied: arm/release/src/interface/connections/__init__.py (from rev 24554, arm/trunk/src/interface/connections/__init__.py) =================================================================== --- arm/release/src/interface/connections/__init__.py (rev 0) +++ arm/release/src/interface/connections/__init__.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -0,0 +1,6 @@ +""" +Panels, popups, and handlers comprising the arm user interface. +""" + +__all__ = ["circEntry", "connEntry", "connPanel", "entries"] +
Deleted: arm/release/src/interface/connections/circEntry.py =================================================================== --- arm/trunk/src/interface/connections/circEntry.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/interface/connections/circEntry.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -1,216 +0,0 @@ -""" -Connection panel entries for client circuits. This includes a header entry -followed by an entry for each hop in the circuit. For instance: - -89.188.20.246:42667 --> 217.172.182.26 (de) General / Built 8.6m (CIRCUIT) -| 85.8.28.4 (se) 98FBC3B2B93897A78CDD797EF549E6B62C9A8523 1 / Guard -| 91.121.204.76 (fr) 546387D93F8D40CFF8842BB9D3A8EC477CEDA984 2 / Middle -+- 217.172.182.26 (de) 5CFA9EA136C0EA0AC096E5CEA7EB674F1207CF86 3 / Exit -""" - -import curses - -from interface.connections import entries, connEntry -from util import torTools, uiTools - -# cached fingerprint -> (IP Address, ORPort) results -RELAY_INFO = {} - -def getRelayInfo(fingerprint): - """ - Provides the (IP Address, ORPort) tuple for the given relay. If the lookup - fails then this returns ("192.168.0.1", "0"). - - Arguments: - fingerprint - relay to look up - """ - - if not fingerprint in RELAY_INFO: - conn = torTools.getConn() - failureResult = ("192.168.0.1", "0") - - nsEntry = conn.getConsensusEntry(fingerprint) - if not nsEntry: return failureResult - - nsLineComp = nsEntry.split("\n")[0].split(" ") - if len(nsLineComp) < 8: return failureResult - - RELAY_INFO[fingerprint] = (nsLineComp[6], nsLineComp[7]) - - return RELAY_INFO[fingerprint] - -class CircEntry(connEntry.ConnectionEntry): - def __init__(self, circuitID, status, purpose, path): - connEntry.ConnectionEntry.__init__(self, "127.0.0.1", "0", "127.0.0.1", "0") - - self.circuitID = circuitID - self.status = status - - # drops to lowercase except the first letter - if len(purpose) >= 2: - purpose = purpose[0].upper() + purpose[1:].lower() - - self.lines = [CircHeaderLine(self.circuitID, purpose)] - - # Overwrites attributes of the initial line to make it more fitting as the - # header for our listing. - - self.lines[0].baseType = connEntry.Category.CIRCUIT - - self.update(status, path) - - def update(self, status, path): - """ - Our status and path can change over time if the circuit is still in the - process of being built. Updates these attributes of our relay. - - Arguments: - status - new status of the circuit - path - list of fingerprints for the series of relays involved in the - circuit - """ - - self.status = status - self.lines = [self.lines[0]] - - if status == "BUILT" and not self.lines[0].isBuilt: - exitIp, exitORPort = getRelayInfo(path[-1]) - self.lines[0].setExit(exitIp, exitORPort, path[-1]) - - for i in range(len(path)): - relayFingerprint = path[i] - relayIp, relayOrPort = getRelayInfo(relayFingerprint) - - if i == len(path) - 1: - if status == "BUILT": placementType = "Exit" - else: placementType = "Extending" - elif i == 0: placementType = "Guard" - else: placementType = "Middle" - - placementLabel = "%i / %s" % (i + 1, placementType) - - self.lines.append(CircLine(relayIp, relayOrPort, relayFingerprint, placementLabel)) - - self.lines[-1].isLast = True - -class CircHeaderLine(connEntry.ConnectionLine): - """ - Initial line of a client entry. This has the same basic format as connection - lines except that its etc field has circuit attributes. - """ - - def __init__(self, circuitID, purpose): - connEntry.ConnectionLine.__init__(self, "127.0.0.1", "0", "0.0.0.0", "0", False, False) - self.circuitID = circuitID - self.purpose = purpose - self.isBuilt = False - - def setExit(self, exitIpAddr, exitPort, exitFingerprint): - connEntry.ConnectionLine.__init__(self, "127.0.0.1", "0", exitIpAddr, exitPort, False, False) - self.isBuilt = True - self.foreign.fingerprintOverwrite = exitFingerprint - - def getType(self): - return connEntry.Category.CIRCUIT - - def getDestinationLabel(self, maxLength, includeLocale=False, includeHostname=False): - if not self.isBuilt: return "Building..." - return connEntry.ConnectionLine.getDestinationLabel(self, maxLength, includeLocale, includeHostname) - - def getEtcContent(self, width, listingType): - """ - Attempts to provide all circuit related stats. Anything that can't be - shown completely (not enough room) is dropped. - """ - - etcAttr = ["Purpose: %s" % self.purpose, "Circuit ID: %i" % self.circuitID] - - for i in range(len(etcAttr), -1, -1): - etcLabel = ", ".join(etcAttr[:i]) - if len(etcLabel) <= width: - return ("%%-%is" % width) % etcLabel - - return "" - - def getDetails(self, width): - if not self.isBuilt: - detailFormat = curses.A_BOLD | uiTools.getColor(connEntry.CATEGORY_COLOR[self.getType()]) - return [uiTools.DrawEntry("Building Circuit...", detailFormat)] - else: return connEntry.ConnectionLine.getDetails(self, width) - -class CircLine(connEntry.ConnectionLine): - """ - An individual hop in a circuit. This overwrites the displayed listing, but - otherwise makes use of the ConnectionLine attributes (for the detail display, - caching, etc). - """ - - def __init__(self, fIpAddr, fPort, fFingerprint, placementLabel): - connEntry.ConnectionLine.__init__(self, "127.0.0.1", "0", fIpAddr, fPort) - self.foreign.fingerprintOverwrite = fFingerprint - self.placementLabel = placementLabel - self.includePort = False - - # determines the sort of left hand bracketing we use - self.isLast = False - - def getType(self): - return connEntry.Category.CIRCUIT - - def getListingEntry(self, width, currentTime, listingType): - """ - Provides the DrawEntry for this relay in the circuilt listing. Lines are - composed of the following components: - <bracket> <dst> <etc> <placement label> - - The dst and etc entries largely match their ConnectionEntry counterparts. - - Arguments: - width - maximum length of the line - currentTime - the current unix time (ignored) - listingType - primary attribute we're listing connections by - """ - - return entries.ConnectionPanelLine.getListingEntry(self, width, currentTime, listingType) - - def _getListingEntry(self, width, currentTime, listingType): - lineFormat = uiTools.getColor(connEntry.CATEGORY_COLOR[self.getType()]) - - # The required widths are the sum of the following: - # bracketing (3 characters) - # placementLabel (14 characters) - # gap between etc and placement label (5 characters) - - if self.isLast: bracket = (curses.ACS_LLCORNER, curses.ACS_HLINE, ord(' ')) - else: bracket = (curses.ACS_VLINE, ord(' '), ord(' ')) - baselineSpace = len(bracket) + 14 + 5 - - dst, etc = "", "" - if listingType == entries.ListingType.IP_ADDRESS: - # TODO: include hostname when that's available - # dst width is derived as: - # src (21) + dst (26) + divider (7) + right gap (2) - bracket (3) = 53 char - dst = "%-53s" % self.getDestinationLabel(53, includeLocale = True) - etc = self.getEtcContent(width - baselineSpace - len(dst), listingType) - elif listingType == entries.ListingType.HOSTNAME: - # min space for the hostname is 40 characters - etc = self.getEtcContent(width - baselineSpace - 40, listingType) - dstLayout = "%%-%is" % (width - baselineSpace - len(etc)) - dst = dstLayout % self.foreign.getHostname(self.foreign.getIpAddr()) - elif listingType == entries.ListingType.FINGERPRINT: - # dst width is derived as: - # src (9) + dst (40) + divider (7) + right gap (2) - bracket (3) = 55 char - dst = "%-55s" % self.foreign.getFingerprint() - etc = self.getEtcContent(width - baselineSpace - len(dst), listingType) - else: - # min space for the nickname is 56 characters - etc = self.getEtcContent(width - baselineSpace - 56, listingType) - dstLayout = "%%-%is" % (width - baselineSpace - len(etc)) - dst = dstLayout % self.foreign.getNickname() - - drawEntry = uiTools.DrawEntry("%-14s" % self.placementLabel, lineFormat) - drawEntry = uiTools.DrawEntry(" " * (width - baselineSpace - len(dst) - len(etc) + 5), lineFormat, drawEntry) - drawEntry = uiTools.DrawEntry(dst + etc, lineFormat, drawEntry) - drawEntry = uiTools.DrawEntry(bracket, curses.A_NORMAL, drawEntry, lockFormat = True) - return drawEntry -
Copied: arm/release/src/interface/connections/circEntry.py (from rev 24554, arm/trunk/src/interface/connections/circEntry.py) =================================================================== --- arm/release/src/interface/connections/circEntry.py (rev 0) +++ arm/release/src/interface/connections/circEntry.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -0,0 +1,216 @@ +""" +Connection panel entries for client circuits. This includes a header entry +followed by an entry for each hop in the circuit. For instance: + +89.188.20.246:42667 --> 217.172.182.26 (de) General / Built 8.6m (CIRCUIT) +| 85.8.28.4 (se) 98FBC3B2B93897A78CDD797EF549E6B62C9A8523 1 / Guard +| 91.121.204.76 (fr) 546387D93F8D40CFF8842BB9D3A8EC477CEDA984 2 / Middle ++- 217.172.182.26 (de) 5CFA9EA136C0EA0AC096E5CEA7EB674F1207CF86 3 / Exit +""" + +import curses + +from interface.connections import entries, connEntry +from util import torTools, uiTools + +# cached fingerprint -> (IP Address, ORPort) results +RELAY_INFO = {} + +def getRelayInfo(fingerprint): + """ + Provides the (IP Address, ORPort) tuple for the given relay. If the lookup + fails then this returns ("192.168.0.1", "0"). + + Arguments: + fingerprint - relay to look up + """ + + if not fingerprint in RELAY_INFO: + conn = torTools.getConn() + failureResult = ("192.168.0.1", "0") + + nsEntry = conn.getConsensusEntry(fingerprint) + if not nsEntry: return failureResult + + nsLineComp = nsEntry.split("\n")[0].split(" ") + if len(nsLineComp) < 8: return failureResult + + RELAY_INFO[fingerprint] = (nsLineComp[6], nsLineComp[7]) + + return RELAY_INFO[fingerprint] + +class CircEntry(connEntry.ConnectionEntry): + def __init__(self, circuitID, status, purpose, path): + connEntry.ConnectionEntry.__init__(self, "127.0.0.1", "0", "127.0.0.1", "0") + + self.circuitID = circuitID + self.status = status + + # drops to lowercase except the first letter + if len(purpose) >= 2: + purpose = purpose[0].upper() + purpose[1:].lower() + + self.lines = [CircHeaderLine(self.circuitID, purpose)] + + # Overwrites attributes of the initial line to make it more fitting as the + # header for our listing. + + self.lines[0].baseType = connEntry.Category.CIRCUIT + + self.update(status, path) + + def update(self, status, path): + """ + Our status and path can change over time if the circuit is still in the + process of being built. Updates these attributes of our relay. + + Arguments: + status - new status of the circuit + path - list of fingerprints for the series of relays involved in the + circuit + """ + + self.status = status + self.lines = [self.lines[0]] + + if status == "BUILT" and not self.lines[0].isBuilt: + exitIp, exitORPort = getRelayInfo(path[-1]) + self.lines[0].setExit(exitIp, exitORPort, path[-1]) + + for i in range(len(path)): + relayFingerprint = path[i] + relayIp, relayOrPort = getRelayInfo(relayFingerprint) + + if i == len(path) - 1: + if status == "BUILT": placementType = "Exit" + else: placementType = "Extending" + elif i == 0: placementType = "Guard" + else: placementType = "Middle" + + placementLabel = "%i / %s" % (i + 1, placementType) + + self.lines.append(CircLine(relayIp, relayOrPort, relayFingerprint, placementLabel)) + + self.lines[-1].isLast = True + +class CircHeaderLine(connEntry.ConnectionLine): + """ + Initial line of a client entry. This has the same basic format as connection + lines except that its etc field has circuit attributes. + """ + + def __init__(self, circuitID, purpose): + connEntry.ConnectionLine.__init__(self, "127.0.0.1", "0", "0.0.0.0", "0", False, False) + self.circuitID = circuitID + self.purpose = purpose + self.isBuilt = False + + def setExit(self, exitIpAddr, exitPort, exitFingerprint): + connEntry.ConnectionLine.__init__(self, "127.0.0.1", "0", exitIpAddr, exitPort, False, False) + self.isBuilt = True + self.foreign.fingerprintOverwrite = exitFingerprint + + def getType(self): + return connEntry.Category.CIRCUIT + + def getDestinationLabel(self, maxLength, includeLocale=False, includeHostname=False): + if not self.isBuilt: return "Building..." + return connEntry.ConnectionLine.getDestinationLabel(self, maxLength, includeLocale, includeHostname) + + def getEtcContent(self, width, listingType): + """ + Attempts to provide all circuit related stats. Anything that can't be + shown completely (not enough room) is dropped. + """ + + etcAttr = ["Purpose: %s" % self.purpose, "Circuit ID: %i" % self.circuitID] + + for i in range(len(etcAttr), -1, -1): + etcLabel = ", ".join(etcAttr[:i]) + if len(etcLabel) <= width: + return ("%%-%is" % width) % etcLabel + + return "" + + def getDetails(self, width): + if not self.isBuilt: + detailFormat = curses.A_BOLD | uiTools.getColor(connEntry.CATEGORY_COLOR[self.getType()]) + return [uiTools.DrawEntry("Building Circuit...", detailFormat)] + else: return connEntry.ConnectionLine.getDetails(self, width) + +class CircLine(connEntry.ConnectionLine): + """ + An individual hop in a circuit. This overwrites the displayed listing, but + otherwise makes use of the ConnectionLine attributes (for the detail display, + caching, etc). + """ + + def __init__(self, fIpAddr, fPort, fFingerprint, placementLabel): + connEntry.ConnectionLine.__init__(self, "127.0.0.1", "0", fIpAddr, fPort) + self.foreign.fingerprintOverwrite = fFingerprint + self.placementLabel = placementLabel + self.includePort = False + + # determines the sort of left hand bracketing we use + self.isLast = False + + def getType(self): + return connEntry.Category.CIRCUIT + + def getListingEntry(self, width, currentTime, listingType): + """ + Provides the DrawEntry for this relay in the circuilt listing. Lines are + composed of the following components: + <bracket> <dst> <etc> <placement label> + + The dst and etc entries largely match their ConnectionEntry counterparts. + + Arguments: + width - maximum length of the line + currentTime - the current unix time (ignored) + listingType - primary attribute we're listing connections by + """ + + return entries.ConnectionPanelLine.getListingEntry(self, width, currentTime, listingType) + + def _getListingEntry(self, width, currentTime, listingType): + lineFormat = uiTools.getColor(connEntry.CATEGORY_COLOR[self.getType()]) + + # The required widths are the sum of the following: + # bracketing (3 characters) + # placementLabel (14 characters) + # gap between etc and placement label (5 characters) + + if self.isLast: bracket = (curses.ACS_LLCORNER, curses.ACS_HLINE, ord(' ')) + else: bracket = (curses.ACS_VLINE, ord(' '), ord(' ')) + baselineSpace = len(bracket) + 14 + 5 + + dst, etc = "", "" + if listingType == entries.ListingType.IP_ADDRESS: + # TODO: include hostname when that's available + # dst width is derived as: + # src (21) + dst (26) + divider (7) + right gap (2) - bracket (3) = 53 char + dst = "%-53s" % self.getDestinationLabel(53, includeLocale = True) + etc = self.getEtcContent(width - baselineSpace - len(dst), listingType) + elif listingType == entries.ListingType.HOSTNAME: + # min space for the hostname is 40 characters + etc = self.getEtcContent(width - baselineSpace - 40, listingType) + dstLayout = "%%-%is" % (width - baselineSpace - len(etc)) + dst = dstLayout % self.foreign.getHostname(self.foreign.getIpAddr()) + elif listingType == entries.ListingType.FINGERPRINT: + # dst width is derived as: + # src (9) + dst (40) + divider (7) + right gap (2) - bracket (3) = 55 char + dst = "%-55s" % self.foreign.getFingerprint() + etc = self.getEtcContent(width - baselineSpace - len(dst), listingType) + else: + # min space for the nickname is 56 characters + etc = self.getEtcContent(width - baselineSpace - 56, listingType) + dstLayout = "%%-%is" % (width - baselineSpace - len(etc)) + dst = dstLayout % self.foreign.getNickname() + + drawEntry = uiTools.DrawEntry("%-14s" % self.placementLabel, lineFormat) + drawEntry = uiTools.DrawEntry(" " * (width - baselineSpace - len(dst) - len(etc) + 5), lineFormat, drawEntry) + drawEntry = uiTools.DrawEntry(dst + etc, lineFormat, drawEntry) + drawEntry = uiTools.DrawEntry(bracket, curses.A_NORMAL, drawEntry, lockFormat = True) + return drawEntry +
Deleted: arm/release/src/interface/connections/connEntry.py =================================================================== --- arm/trunk/src/interface/connections/connEntry.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/interface/connections/connEntry.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -1,864 +0,0 @@ -""" -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. -# Hidden Connections to a hidden service we're providing. -# Socks Socks connections for applications using Tor. -# Circuit Circuits our tor client has created. -# Directory Fetching tor consensus information. -# Control Tor controller (arm, vidalia, etc). - -Category = enum.Enum("INBOUND", "OUTBOUND", "EXIT", "HIDDEN", "SOCKS", "CIRCUIT", "DIRECTORY", "CONTROL") -CATEGORY_COLOR = {Category.INBOUND: "green", Category.OUTBOUND: "blue", - Category.EXIT: "red", Category.HIDDEN: "magenta", - Category.SOCKS: "yellow", Category.CIRCUIT: "cyan", - 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 - -# sort value for scrubbed ip addresses -SCRUBBED_IP_VAL = 255 ** 4 - -CONFIG = {"features.connection.markInitialConnections": True, - "features.connection.showExitPort": True, - "features.connection.showColumn.fingerprint": True, - "features.connection.showColumn.nickname": True, - "features.connection.showColumn.destination": True, - "features.connection.showColumn.expandedIp": 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 - - # if set then this overwrites fingerprint lookups - self.fingerprintOverwrite = None - - 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, default=None): - """ - Provides the two letter country code for the IP address' locale. - - Arguments: - default - return value if no locale information is available - """ - - conn = torTools.getConn() - return conn.getInfo("ip-to-country/%s" % self.ipAddr, default) - - def getFingerprint(self): - """ - Provides the fingerprint of the relay, returning "UNKNOWN" if it can't be - determined. - """ - - if self.fingerprintOverwrite: - return self.fingerprintOverwrite - - 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. - """ - - myFingerprint = self.getFingerprint() - - if myFingerprint != "UNKNOWN": - conn = torTools.getConn() - myNickname = conn.getRelayNickname(myFingerprint) - - if myNickname: return myNickname - else: return "UNKNOWN" - 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. - """ - - connLine = self.lines[0] - if attr == entries.SortAttr.IP_ADDRESS: - if connLine.isPrivate(): return SCRUBBED_IP_VAL # orders at the end - return connLine.sortIpAddr - elif attr == entries.SortAttr.PORT: - return connLine.sortPort - elif attr == entries.SortAttr.HOSTNAME: - if connLine.isPrivate(): return "" - return connLine.foreign.getHostname("") - elif attr == entries.SortAttr.FINGERPRINT: - return connLine.foreign.getFingerprint() - elif attr == entries.SortAttr.NICKNAME: - myNickname = connLine.foreign.getNickname() - if myNickname == "UNKNOWN": return "z" * 20 # orders at the end - else: return myNickname.lower() - elif attr == entries.SortAttr.CATEGORY: - return Category.indexOf(connLine.getType()) - elif attr == entries.SortAttr.UPTIME: - return connLine.startTime - elif attr == entries.SortAttr.COUNTRY: - if connections.isIpAddressPrivate(self.lines[0].foreign.getIpAddr()): return "" - else: return connLine.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, includePort=True, includeExpandedIpAddr=True): - entries.ConnectionPanelLine.__init__(self) - - self.local = Endpoint(lIpAddr, lPort) - self.foreign = Endpoint(fIpAddr, fPort) - self.startTime = time.time() - self.isInitialConnection = False - - # overwrite the local fingerprint with ours - conn = torTools.getConn() - self.local.fingerprintOverwrite = conn.getInfo("fingerprint") - - # 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 - - # attributes for SOCKS, HIDDEN, and CONTROL connections - self.appName = None - self.appPid = None - self.isAppResolving = False - - myOrPort = conn.getOption("ORPort") - myDirPort = conn.getOption("DirPort") - mySocksPort = conn.getOption("SocksPort", "9050") - myCtlPort = conn.getOption("ControlPort") - myHiddenServicePorts = conn.getHiddenServicePorts() - - # 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.SOCKS - elif fPort in myHiddenServicePorts: - self.baseType = Category.HIDDEN - elif lPort == myCtlPort: - self.baseType = Category.CONTROL - else: - self.baseType = Category.OUTBOUND - self.foreign.isORPort = True - - self.cachedType = None - - # includes the port or expanded ip address field when displaying listing - # information if true - self.includePort = includePort - self.includeExpandedIpAddr = includeExpandedIpAddr - - # 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. Lines are composed - of the following 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 - if CONFIG["features.connection.markInitialConnections"]: - timePrefix = "+" if self.isInitialConnection else " " - else: timePrefix = "" - - timeEntry = myListing.getNext() - timeEntry.text = timePrefix + "%5s" % uiTools.getTimeLabel(currentTime - self.startTime, 1) - - return myListing - - def isUnresolvedApp(self): - """ - True if our display uses application information that hasn't yet been resolved. - """ - - return self.appName == None and self.getType() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL) - - 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]) - timeWidth = 6 if CONFIG["features.connection.markInitialConnections"] else 5 - - 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(" " * timeWidth, lineFormat, drawEntry) - drawEntry = uiTools.DrawEntry(self._getListingContent(width - (12 + timeWidth), 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 _getDescriptors(self, width): - """ - Provides raw descriptor information for the relay. - - Arguments: - width - available space to display in - """ - - # TODO: Porting and refactoring the descriptorPopup.py functionality is - # gonna take quite a bit of work. This is a very rarely used feature and - # not worth delaying the 1.4.2 release any further, so this will be a part - # of 1.4.3. - - return [] - - 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. - """ - - # This is used to scrub private information from the interface. Relaying - # etiquette (and wiretapping laws) say these are bad things to look at so - # DON'T CHANGE THIS UNLESS YOU HAVE A DAMN GOOD REASON! - - myType = self.getType() - - if myType == Category.INBOUND: - # if we're a guard or bridge and the connection doesn't belong to a - # known relay then it might be client traffic - - conn = torTools.getConn() - if "Guard" in conn.getMyFlags() or conn.getOption("BridgeRelay") == "1": - allMatches = conn.getRelayFingerprint(self.foreign.getIpAddr(), getAllMatches = True) - return allMatches == [] - 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 circuits, 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 - # - CIRCUIT 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.CIRCUIT # 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 getEtcContent(self, width, listingType): - """ - Provides the optional content for the connection. - - Arguments: - width - maximum length of the line - listingType - primary attribute we're listing connections by - """ - - # for applications show the command/pid - if self.getType() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL): - displayLabel = "" - - if self.appName: - if self.appPid: displayLabel = "%s (%s)" % (self.appName, self.appPid) - else: displayLabel = self.appName - elif self.isAppResolving: - displayLabel = "resolving..." - else: displayLabel = "UNKNOWN" - - if len(displayLabel) < width: - return ("%%-%is" % width) % displayLabel - else: return "" - - # for everything else display connection/consensus information - dstAddress = self.getDestinationLabel(26, includeLocale = True) - etc, usedSpace = "", 0 - if listingType == entries.ListingType.IP_ADDRESS: - if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]: - # show fingerprint (column width: 42 characters) - etc += "%-40s " % self.foreign.getFingerprint() - usedSpace += 42 - - 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: - if width > usedSpace + 28 and CONFIG["features.connection.showColumn.destination"]: - # show destination ip/port/locale (column width: 28 characters) - etc += "%-26s " % dstAddress - usedSpace += 28 - - if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]: - # show fingerprint (column width: 42 characters) - etc += "%-40s " % self.foreign.getFingerprint() - usedSpace += 42 - - if width > usedSpace + 17 and CONFIG["features.connection.showColumn.nickname"]: - # show nickname (column width: min 17 characters, uses half of the remainder) - nicknameSpace = 15 + (width - (usedSpace + 17)) / 2 - nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0) - etc += ("%%-%is " % nicknameSpace) % nicknameLabel - usedSpace += (nicknameSpace + 2) - elif listingType == entries.ListingType.FINGERPRINT: - if width > usedSpace + 17: - # show nickname (column width: min 17 characters, consumes any remaining space) - nicknameSpace = width - usedSpace - 2 - - # 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"]: - nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0) - etc += ("%%-%is " % nicknameSpace) % nicknameLabel - usedSpace += nicknameSpace + 2 - - if isIpLocaleIncluded: - etc += "%-26s " % dstAddress - usedSpace += 28 - else: - if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]: - # show fingerprint (column width: 42 characters) - etc += "%-40s " % self.foreign.getFingerprint() - usedSpace += 42 - - if width > usedSpace + 28 and CONFIG["features.connection.showColumn.destination"]: - # show destination ip/port/locale (column width: 28 characters) - etc += "%-26s " % dstAddress - usedSpace += 28 - - return ("%%-%is" % width) % etc - - 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 - localPort = ":%s" % self.local.getPort() if self.includePort else "" - - src, dst, etc = "", "", "" - if listingType == entries.ListingType.IP_ADDRESS: - myExternalIpAddr = conn.getInfo("address", self.local.getIpAddr()) - addrDiffer = myExternalIpAddr != self.local.getIpAddr() - - # Expanding doesn't make sense, if the connection isn't actually - # going through Tor's external IP address. As there isn't a known - # method for checking if it is, we're checking the type instead. - # - # This isn't entirely correct. It might be a better idea to check if - # the source and destination addresses are both private, but that might - # not be perfectly reliable either. - - isExpansionType = not myType in (Category.SOCKS, Category.HIDDEN, Category.CONTROL) - - if isExpansionType: srcAddress = myExternalIpAddr + localPort - else: srcAddress = self.local.getIpAddr() + localPort - - if myType in (Category.SOCKS, Category.CONTROL): - # Like inbound connections these need their source and destination to - # be swapped. However, this only applies when listing by IP or hostname - # (their fingerprint and nickname are both for us). Reversing the - # fields here to keep the same column alignments. - - src = "%-21s" % dstAddress - dst = "%-26s" % srcAddress - else: - 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 - - # Showing the fingerprint (which has the width of 42) has priority over - # an expanded address field. Hence check if we either have space for - # both or wouldn't be showing the fingerprint regardless. - - isExpandedAddrVisible = width > usedSpace + 28 - if isExpandedAddrVisible and CONFIG["features.connection.showColumn.fingerprint"]: - isExpandedAddrVisible = width < usedSpace + 42 or width > usedSpace + 70 - - if addrDiffer and isExpansionType and isExpandedAddrVisible and self.includeExpandedIpAddr and CONFIG["features.connection.showColumn.expandedIp"]: - # include the internal address in the src (extra 28 characters) - internalAddress = self.local.getIpAddr() + localPort - - # If this is an inbound connection then reverse ordering so it's: - # <foreign> --> <external> --> <internal> - # when the src and dst are swapped later - - if myType == Category.INBOUND: src = "%-21s --> %s" % (src, internalAddress) - else: src = "%-21s --> %s" % (internalAddress, src) - - usedSpace += 28 - - etc = self.getEtcContent(width - usedSpace, listingType) - usedSpace += len(etc) - elif listingType == entries.ListingType.HOSTNAME: - # 15 characters for source, and a min of 40 reserved for the destination - # TODO: when actually functional the src and dst need to be swapped for - # SOCKS and CONTROL connections - src = "localhost%-6s" % localPort - usedSpace += len(src) - minHostnameSpace = 40 - - etc = self.getEtcContent(width - usedSpace - minHostnameSpace, listingType) - usedSpace += len(etc) - - 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()) - portLabel = ":%-5s" % self.foreign.getPort() if self.includePort else "" - - # truncates long hostnames and sets dst to <hostname>:<port> - hostname = uiTools.cropStr(hostname, hostnameSpace, 0) - dst = ("%%-%is" % hostnameSpace) % (hostname + portLabel) - 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 - - etc = self.getEtcContent(width - usedSpace, listingType) - usedSpace += len(etc) - 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 - - etc = self.getEtcContent(width - usedSpace - minBaseSpace, listingType) - usedSpace += len(etc) - - 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 connection. - - 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 potential 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 = "unknown" - if len(nsLines) >= 2 and nsLines[1].startswith("s "): - flags = nsLines[1][2:] - - # The network status exit policy doesn't exist for older tor versions. - # If unavailable we'll need the full exit policy which is on the - # descriptor (if that's available). - - exitPolicy = "unknown" - if len(nsLines) >= 4 and nsLines[3].startswith("p "): - exitPolicy = nsLines[3][2:].replace(",", ", ") - elif descEntry: - # the descriptor has an individual line for each entry in the exit policy - exitPolicyEntries = [] - - for line in descEntry.split("\n"): - if line.startswith("accept") or line.startswith("reject"): - exitPolicyEntries.append(line.strip()) - - exitPolicy = ", ".join(exitPolicyEntries) - - 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" % exitPolicy - - 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] = "Multiple 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 - """ - - # the port and port derived data can be hidden by config or without includePort - includePort = self.includePort and (CONFIG["features.connection.showExitPort"] or self.getType() != Category.EXIT) - - # destination of the connection - ipLabel = "<scrubbed>" if self.isPrivate() else self.foreign.getIpAddr() - portLabel = ":%s" % self.foreign.getPort() if includePort else "" - dstAddress = ipLabel + portLabel - - # 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 and includePort: - 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 available, taking into account the ", " - # dividers if there's multiple 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] -
Copied: arm/release/src/interface/connections/connEntry.py (from rev 24554, arm/trunk/src/interface/connections/connEntry.py) =================================================================== --- arm/release/src/interface/connections/connEntry.py (rev 0) +++ arm/release/src/interface/connections/connEntry.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -0,0 +1,864 @@ +""" +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. +# Hidden Connections to a hidden service we're providing. +# Socks Socks connections for applications using Tor. +# Circuit Circuits our tor client has created. +# Directory Fetching tor consensus information. +# Control Tor controller (arm, vidalia, etc). + +Category = enum.Enum("INBOUND", "OUTBOUND", "EXIT", "HIDDEN", "SOCKS", "CIRCUIT", "DIRECTORY", "CONTROL") +CATEGORY_COLOR = {Category.INBOUND: "green", Category.OUTBOUND: "blue", + Category.EXIT: "red", Category.HIDDEN: "magenta", + Category.SOCKS: "yellow", Category.CIRCUIT: "cyan", + 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 + +# sort value for scrubbed ip addresses +SCRUBBED_IP_VAL = 255 ** 4 + +CONFIG = {"features.connection.markInitialConnections": True, + "features.connection.showExitPort": True, + "features.connection.showColumn.fingerprint": True, + "features.connection.showColumn.nickname": True, + "features.connection.showColumn.destination": True, + "features.connection.showColumn.expandedIp": 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 + + # if set then this overwrites fingerprint lookups + self.fingerprintOverwrite = None + + 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, default=None): + """ + Provides the two letter country code for the IP address' locale. + + Arguments: + default - return value if no locale information is available + """ + + conn = torTools.getConn() + return conn.getInfo("ip-to-country/%s" % self.ipAddr, default) + + def getFingerprint(self): + """ + Provides the fingerprint of the relay, returning "UNKNOWN" if it can't be + determined. + """ + + if self.fingerprintOverwrite: + return self.fingerprintOverwrite + + 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. + """ + + myFingerprint = self.getFingerprint() + + if myFingerprint != "UNKNOWN": + conn = torTools.getConn() + myNickname = conn.getRelayNickname(myFingerprint) + + if myNickname: return myNickname + else: return "UNKNOWN" + 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. + """ + + connLine = self.lines[0] + if attr == entries.SortAttr.IP_ADDRESS: + if connLine.isPrivate(): return SCRUBBED_IP_VAL # orders at the end + return connLine.sortIpAddr + elif attr == entries.SortAttr.PORT: + return connLine.sortPort + elif attr == entries.SortAttr.HOSTNAME: + if connLine.isPrivate(): return "" + return connLine.foreign.getHostname("") + elif attr == entries.SortAttr.FINGERPRINT: + return connLine.foreign.getFingerprint() + elif attr == entries.SortAttr.NICKNAME: + myNickname = connLine.foreign.getNickname() + if myNickname == "UNKNOWN": return "z" * 20 # orders at the end + else: return myNickname.lower() + elif attr == entries.SortAttr.CATEGORY: + return Category.indexOf(connLine.getType()) + elif attr == entries.SortAttr.UPTIME: + return connLine.startTime + elif attr == entries.SortAttr.COUNTRY: + if connections.isIpAddressPrivate(self.lines[0].foreign.getIpAddr()): return "" + else: return connLine.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, includePort=True, includeExpandedIpAddr=True): + entries.ConnectionPanelLine.__init__(self) + + self.local = Endpoint(lIpAddr, lPort) + self.foreign = Endpoint(fIpAddr, fPort) + self.startTime = time.time() + self.isInitialConnection = False + + # overwrite the local fingerprint with ours + conn = torTools.getConn() + self.local.fingerprintOverwrite = conn.getInfo("fingerprint") + + # 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 + + # attributes for SOCKS, HIDDEN, and CONTROL connections + self.appName = None + self.appPid = None + self.isAppResolving = False + + myOrPort = conn.getOption("ORPort") + myDirPort = conn.getOption("DirPort") + mySocksPort = conn.getOption("SocksPort", "9050") + myCtlPort = conn.getOption("ControlPort") + myHiddenServicePorts = conn.getHiddenServicePorts() + + # 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.SOCKS + elif fPort in myHiddenServicePorts: + self.baseType = Category.HIDDEN + elif lPort == myCtlPort: + self.baseType = Category.CONTROL + else: + self.baseType = Category.OUTBOUND + self.foreign.isORPort = True + + self.cachedType = None + + # includes the port or expanded ip address field when displaying listing + # information if true + self.includePort = includePort + self.includeExpandedIpAddr = includeExpandedIpAddr + + # 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. Lines are composed + of the following 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 + if CONFIG["features.connection.markInitialConnections"]: + timePrefix = "+" if self.isInitialConnection else " " + else: timePrefix = "" + + timeEntry = myListing.getNext() + timeEntry.text = timePrefix + "%5s" % uiTools.getTimeLabel(currentTime - self.startTime, 1) + + return myListing + + def isUnresolvedApp(self): + """ + True if our display uses application information that hasn't yet been resolved. + """ + + return self.appName == None and self.getType() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL) + + 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]) + timeWidth = 6 if CONFIG["features.connection.markInitialConnections"] else 5 + + 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(" " * timeWidth, lineFormat, drawEntry) + drawEntry = uiTools.DrawEntry(self._getListingContent(width - (12 + timeWidth), 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 _getDescriptors(self, width): + """ + Provides raw descriptor information for the relay. + + Arguments: + width - available space to display in + """ + + # TODO: Porting and refactoring the descriptorPopup.py functionality is + # gonna take quite a bit of work. This is a very rarely used feature and + # not worth delaying the 1.4.2 release any further, so this will be a part + # of 1.4.3. + + return [] + + 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. + """ + + # This is used to scrub private information from the interface. Relaying + # etiquette (and wiretapping laws) say these are bad things to look at so + # DON'T CHANGE THIS UNLESS YOU HAVE A DAMN GOOD REASON! + + myType = self.getType() + + if myType == Category.INBOUND: + # if we're a guard or bridge and the connection doesn't belong to a + # known relay then it might be client traffic + + conn = torTools.getConn() + if "Guard" in conn.getMyFlags() or conn.getOption("BridgeRelay") == "1": + allMatches = conn.getRelayFingerprint(self.foreign.getIpAddr(), getAllMatches = True) + return allMatches == [] + 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 circuits, 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 + # - CIRCUIT 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.CIRCUIT # 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 getEtcContent(self, width, listingType): + """ + Provides the optional content for the connection. + + Arguments: + width - maximum length of the line + listingType - primary attribute we're listing connections by + """ + + # for applications show the command/pid + if self.getType() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL): + displayLabel = "" + + if self.appName: + if self.appPid: displayLabel = "%s (%s)" % (self.appName, self.appPid) + else: displayLabel = self.appName + elif self.isAppResolving: + displayLabel = "resolving..." + else: displayLabel = "UNKNOWN" + + if len(displayLabel) < width: + return ("%%-%is" % width) % displayLabel + else: return "" + + # for everything else display connection/consensus information + dstAddress = self.getDestinationLabel(26, includeLocale = True) + etc, usedSpace = "", 0 + if listingType == entries.ListingType.IP_ADDRESS: + if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]: + # show fingerprint (column width: 42 characters) + etc += "%-40s " % self.foreign.getFingerprint() + usedSpace += 42 + + 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: + if width > usedSpace + 28 and CONFIG["features.connection.showColumn.destination"]: + # show destination ip/port/locale (column width: 28 characters) + etc += "%-26s " % dstAddress + usedSpace += 28 + + if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]: + # show fingerprint (column width: 42 characters) + etc += "%-40s " % self.foreign.getFingerprint() + usedSpace += 42 + + if width > usedSpace + 17 and CONFIG["features.connection.showColumn.nickname"]: + # show nickname (column width: min 17 characters, uses half of the remainder) + nicknameSpace = 15 + (width - (usedSpace + 17)) / 2 + nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0) + etc += ("%%-%is " % nicknameSpace) % nicknameLabel + usedSpace += (nicknameSpace + 2) + elif listingType == entries.ListingType.FINGERPRINT: + if width > usedSpace + 17: + # show nickname (column width: min 17 characters, consumes any remaining space) + nicknameSpace = width - usedSpace - 2 + + # 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"]: + nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0) + etc += ("%%-%is " % nicknameSpace) % nicknameLabel + usedSpace += nicknameSpace + 2 + + if isIpLocaleIncluded: + etc += "%-26s " % dstAddress + usedSpace += 28 + else: + if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]: + # show fingerprint (column width: 42 characters) + etc += "%-40s " % self.foreign.getFingerprint() + usedSpace += 42 + + if width > usedSpace + 28 and CONFIG["features.connection.showColumn.destination"]: + # show destination ip/port/locale (column width: 28 characters) + etc += "%-26s " % dstAddress + usedSpace += 28 + + return ("%%-%is" % width) % etc + + 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 + localPort = ":%s" % self.local.getPort() if self.includePort else "" + + src, dst, etc = "", "", "" + if listingType == entries.ListingType.IP_ADDRESS: + myExternalIpAddr = conn.getInfo("address", self.local.getIpAddr()) + addrDiffer = myExternalIpAddr != self.local.getIpAddr() + + # Expanding doesn't make sense, if the connection isn't actually + # going through Tor's external IP address. As there isn't a known + # method for checking if it is, we're checking the type instead. + # + # This isn't entirely correct. It might be a better idea to check if + # the source and destination addresses are both private, but that might + # not be perfectly reliable either. + + isExpansionType = not myType in (Category.SOCKS, Category.HIDDEN, Category.CONTROL) + + if isExpansionType: srcAddress = myExternalIpAddr + localPort + else: srcAddress = self.local.getIpAddr() + localPort + + if myType in (Category.SOCKS, Category.CONTROL): + # Like inbound connections these need their source and destination to + # be swapped. However, this only applies when listing by IP or hostname + # (their fingerprint and nickname are both for us). Reversing the + # fields here to keep the same column alignments. + + src = "%-21s" % dstAddress + dst = "%-26s" % srcAddress + else: + 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 + + # Showing the fingerprint (which has the width of 42) has priority over + # an expanded address field. Hence check if we either have space for + # both or wouldn't be showing the fingerprint regardless. + + isExpandedAddrVisible = width > usedSpace + 28 + if isExpandedAddrVisible and CONFIG["features.connection.showColumn.fingerprint"]: + isExpandedAddrVisible = width < usedSpace + 42 or width > usedSpace + 70 + + if addrDiffer and isExpansionType and isExpandedAddrVisible and self.includeExpandedIpAddr and CONFIG["features.connection.showColumn.expandedIp"]: + # include the internal address in the src (extra 28 characters) + internalAddress = self.local.getIpAddr() + localPort + + # If this is an inbound connection then reverse ordering so it's: + # <foreign> --> <external> --> <internal> + # when the src and dst are swapped later + + if myType == Category.INBOUND: src = "%-21s --> %s" % (src, internalAddress) + else: src = "%-21s --> %s" % (internalAddress, src) + + usedSpace += 28 + + etc = self.getEtcContent(width - usedSpace, listingType) + usedSpace += len(etc) + elif listingType == entries.ListingType.HOSTNAME: + # 15 characters for source, and a min of 40 reserved for the destination + # TODO: when actually functional the src and dst need to be swapped for + # SOCKS and CONTROL connections + src = "localhost%-6s" % localPort + usedSpace += len(src) + minHostnameSpace = 40 + + etc = self.getEtcContent(width - usedSpace - minHostnameSpace, listingType) + usedSpace += len(etc) + + 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()) + portLabel = ":%-5s" % self.foreign.getPort() if self.includePort else "" + + # truncates long hostnames and sets dst to <hostname>:<port> + hostname = uiTools.cropStr(hostname, hostnameSpace, 0) + dst = ("%%-%is" % hostnameSpace) % (hostname + portLabel) + 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 + + etc = self.getEtcContent(width - usedSpace, listingType) + usedSpace += len(etc) + 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 + + etc = self.getEtcContent(width - usedSpace - minBaseSpace, listingType) + usedSpace += len(etc) + + 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 connection. + + 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 potential 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 = "unknown" + if len(nsLines) >= 2 and nsLines[1].startswith("s "): + flags = nsLines[1][2:] + + # The network status exit policy doesn't exist for older tor versions. + # If unavailable we'll need the full exit policy which is on the + # descriptor (if that's available). + + exitPolicy = "unknown" + if len(nsLines) >= 4 and nsLines[3].startswith("p "): + exitPolicy = nsLines[3][2:].replace(",", ", ") + elif descEntry: + # the descriptor has an individual line for each entry in the exit policy + exitPolicyEntries = [] + + for line in descEntry.split("\n"): + if line.startswith("accept") or line.startswith("reject"): + exitPolicyEntries.append(line.strip()) + + exitPolicy = ", ".join(exitPolicyEntries) + + 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" % exitPolicy + + 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] = "Multiple 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 + """ + + # the port and port derived data can be hidden by config or without includePort + includePort = self.includePort and (CONFIG["features.connection.showExitPort"] or self.getType() != Category.EXIT) + + # destination of the connection + ipLabel = "<scrubbed>" if self.isPrivate() else self.foreign.getIpAddr() + portLabel = ":%s" % self.foreign.getPort() if includePort else "" + dstAddress = ipLabel + portLabel + + # 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 and includePort: + 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 available, taking into account the ", " + # dividers if there's multiple 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] +
Deleted: arm/release/src/interface/connections/connPanel.py =================================================================== --- arm/trunk/src/interface/connections/connPanel.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/interface/connections/connPanel.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -1,398 +0,0 @@ -""" -Listing of the currently established connections tor has made. -""" - -import time -import curses -import threading - -from interface.connections import entries, connEntry, circEntry -from util import connections, enum, panel, torTools, uiTools - -DEFAULT_CONFIG = {"features.connection.resolveApps": True, - "features.connection.listingType": 0, - "features.connection.refreshRate": 5} - -# height of the detail panel content, not counting top and bottom border -DETAILS_HEIGHT = 7 - -# listing types -Listing = enum.Enum(("IP_ADDRESS", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME") - -DEFAULT_SORT_ORDER = (entries.SortAttr.CATEGORY, entries.SortAttr.LISTING, entries.SortAttr.UPTIME) - -class ConnectionPanel(panel.Panel, threading.Thread): - """ - Listing of connections tor is making, with information correlated against - the current consensus and other data sources. - """ - - def __init__(self, stdscr, config=None): - panel.Panel.__init__(self, stdscr, "conn", 0) - threading.Thread.__init__(self) - self.setDaemon(True) - - 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 = entries.SortAttr.values() - customOrdering = config.getIntCSV("features.connection.order", None, 3, 0, len(sortFields)) - - if customOrdering: - self._sortOrdering = [sortFields[i] for i in customOrdering] - - self._listingType = Listing.values()[self._config["features.connection.listingType"]] - self._scroller = uiTools.Scroller(True) - self._title = "Connections:" # title line of the panel - self._entries = [] # last fetched display entries - self._entryLines = [] # individual lines rendered from the entries listing - self._showDetails = False # presents the details panel if true - - self._lastUpdate = -1 # time the content was last revised - self._isTorRunning = True # indicates if tor is currently running or not - self._isPaused = True # prevents updates if true - 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 - - # resolver for the command/pid associated with SOCKS, HIDDEN, and CONTROL connections - self._appResolver = connections.AppResolver("arm") - - # rate limits appResolver queries to once per update - self.appResolveSinceUpdate = False - - self._update() # populates initial entries - self._resolveApps(False) # resolves initial applications - - # mark the initially exitsing connection uptimes as being estimates - for entry in self._entries: - if isinstance(entry, connEntry.ConnectionEntry): - entry.getLines()[0].isInitialConnection = True - - # listens for when tor stops so we know to stop reflecting changes - torTools.getConn().addStatusListener(self.torStateListener) - - def torStateListener(self, conn, eventType): - """ - Freezes the connection contents when Tor stops. - - Arguments: - conn - tor controller - eventType - type of event detected - """ - - self._isTorRunning = eventType == torTools.State.INIT - - if self._isPaused or not self._isTorRunning: - if not self._pauseTime: self._pauseTime = time.time() - else: self._pauseTime = None - - self.redraw(True) - - def setPaused(self, isPause): - """ - If true, prevents the panel from updating. - """ - - if not self._isPaused == isPause: - self._isPaused = isPause - - if isPause or not self._isTorRunning: - if not self._pauseTime: self._pauseTime = time.time() - else: self._pauseTime = None - - # redraws so the display reflects any changes between the last update - # and being paused - self.redraw(True) - - def setSortOrder(self, ordering = None): - """ - Sets the connection attributes we're sorting by and resorts the contents. - - Arguments: - ordering - new ordering, if undefined then this resorts with the last - set ordering - """ - - self.valsLock.acquire() - if ordering: self._sortOrdering = ordering - self._entries.sort(key=lambda i: (i.getSortValues(self._sortOrdering, self._listingType))) - - self._entryLines = [] - for entry in self._entries: - self._entryLines += entry.getLines() - self.valsLock.release() - - def setListingType(self, listingType): - """ - Sets the priority information presented by the panel. - - Arguments: - listingType - Listing instance for the primary information to be shown - """ - - self.valsLock.acquire() - self._listingType = listingType - - # if we're sorting by the listing then we need to resort - if entries.SortAttr.LISTING in self._sortOrdering: - self.setSortOrder() - - self.valsLock.release() - - def handleKey(self, key): - self.valsLock.acquire() - - if uiTools.isScrollKey(key): - pageHeight = self.getPreferredSize()[0] - 1 - if self._showDetails: pageHeight -= (DETAILS_HEIGHT + 1) - isChanged = self._scroller.handleKey(key, self._entryLines, pageHeight) - if isChanged: self.redraw(True) - elif uiTools.isSelectionKey(key): - self._showDetails = not self._showDetails - self.redraw(True) - - self.valsLock.release() - - def run(self): - """ - Keeps connections listing updated, checking for new entries at a set rate. - """ - - lastDraw = time.time() - 1 - while not self._halt: - currentTime = time.time() - - if self._isPaused or not self._isTorRunning or currentTime - lastDraw < self._config["features.connection.refreshRate"]: - self._cond.acquire() - if not self._halt: self._cond.wait(0.2) - self._cond.release() - else: - # updates content if their's new results, otherwise just redraws - self._update() - self.redraw(True) - - # we may have missed multiple updates due to being paused, showing - # another panel, etc so lastDraw might need to jump multiple ticks - drawTicks = (time.time() - lastDraw) / self._config["features.connection.refreshRate"] - lastDraw += self._config["features.connection.refreshRate"] * drawTicks - - def draw(self, width, height): - self.valsLock.acquire() - - # extra line when showing the detail panel is for the bottom border - detailPanelOffset = DETAILS_HEIGHT + 1 if self._showDetails else 0 - isScrollbarVisible = len(self._entryLines) > height - detailPanelOffset - 1 - - scrollLoc = self._scroller.getScrollLoc(self._entryLines, height - detailPanelOffset - 1) - cursorSelection = self._scroller.getCursorSelection(self._entryLines) - - # draws the detail panel if currently displaying it - if self._showDetails: - # 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 - self.addstr(0, 0, title, curses.A_STANDOUT) - - scrollOffset = 1 - if isScrollbarVisible: - scrollOffset = 3 - self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelOffset - 1, len(self._entryLines), 1 + detailPanelOffset) - - currentTime = self._pauseTime if self._pauseTime else time.time() - for lineNum in range(scrollLoc, len(self._entryLines)): - entryLine = self._entryLines[lineNum] - - # if this is an unresolved SOCKS, HIDDEN, or CONTROL entry then queue up - # resolution for the applicaitions they belong to - if isinstance(entryLine, connEntry.ConnectionLine) and entryLine.isUnresolvedApp(): - self._resolveApps() - - # hilighting if this is the selected line - extraFormat = curses.A_STANDOUT if entryLine == cursorSelection else curses.A_NORMAL - - 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() - - def stop(self): - """ - Halts further resolutions and terminates the thread. - """ - - self._cond.acquire() - self._halt = True - self._cond.notifyAll() - self._cond.release() - - def _update(self): - """ - Fetches the newest resolved connections. - """ - - connResolver = connections.getResolver("tor") - currentResolutionCount = connResolver.getResolutionCount() - self.appResolveSinceUpdate = False - - if self._lastResourceFetch != currentResolutionCount: - self.valsLock.acquire() - - newEntries = [] # the new results we'll display - - # Fetches new connections and client circuits... - # newConnections [(local ip, local port, foreign ip, foreign port)...] - # newCircuits {circuitID => (status, purpose, path)...} - - newConnections = connResolver.getConnections() - newCircuits = {} - - for circuitID, status, purpose, path in torTools.getConn().getCircuits(): - # Skips established single-hop circuits (these are for directory - # fetches, not client circuits) - if not (status == "BUILT" and len(path) == 1): - newCircuits[circuitID] = (status, purpose, path) - - # Populates newEntries with any of our old entries that still exist. - # This is both for performance and to keep from resetting the uptime - # attributes. Note that CircEntries are a ConnectionEntry subclass so - # we need to check for them first. - - for oldEntry in self._entries: - if isinstance(oldEntry, circEntry.CircEntry): - newEntry = newCircuits.get(oldEntry.circuitID) - - if newEntry: - oldEntry.update(newEntry[0], newEntry[2]) - newEntries.append(oldEntry) - del newCircuits[oldEntry.circuitID] - elif isinstance(oldEntry, connEntry.ConnectionEntry): - connLine = oldEntry.getLines()[0] - connAttr = (connLine.local.getIpAddr(), connLine.local.getPort(), - connLine.foreign.getIpAddr(), connLine.foreign.getPort()) - - if connAttr in newConnections: - newEntries.append(oldEntry) - newConnections.remove(connAttr) - - # Reset any display attributes for the entries we're keeping - for entry in newEntries: entry.resetDisplay() - - # Adds any new connection and circuit entries. - for lIp, lPort, fIp, fPort in newConnections: - newConnEntry = connEntry.ConnectionEntry(lIp, lPort, fIp, fPort) - if newConnEntry.getLines()[0].getType() != connEntry.Category.CIRCUIT: - newEntries.append(newConnEntry) - - for circuitID in newCircuits: - status, purpose, path = newCircuits[circuitID] - newEntries.append(circEntry.CircEntry(circuitID, status, purpose, path)) - - # 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 = connEntry.Category.values() - typeCounts = dict((type, 0) for type in categoryTypes) - for entry in newEntries: - if isinstance(entry, connEntry.ConnectionEntry): - typeCounts[entry.getLines()[0].getType()] += 1 - elif isinstance(entry, circEntry.CircEntry): - typeCounts[connEntry.Category.CIRCUIT] += 1 - - # makes labels for all the categories with connections (ie, - # "21 outbound", "1 control", etc) - countLabels = [] - - for category in categoryTypes: - if typeCounts[category] > 0: - countLabels.append("%i %s" % (typeCounts[category], category.lower())) - - if countLabels: self._title = "Connections (%s):" % ", ".join(countLabels) - else: self._title = "Connections:" - - self._entries = newEntries - - self._entryLines = [] - for entry in self._entries: - self._entryLines += entry.getLines() - - self.setSortOrder() - self._lastResourceFetch = currentResolutionCount - self.valsLock.release() - - def _resolveApps(self, flagQuery = True): - """ - Triggers an asynchronous query for all unresolved SOCKS, HIDDEN, and - CONTROL entries. - - Arguments: - flagQuery - sets a flag to prevent further call from being respected - until the next update if true - """ - - if self.appResolveSinceUpdate or not self._config["features.connection.resolveApps"]: return - unresolvedLines = [l for l in self._entryLines if isinstance(l, connEntry.ConnectionLine) and l.isUnresolvedApp()] - - # get the ports used for unresolved applications - appPorts = [] - - for line in unresolvedLines: - appConn = line.local if line.getType() == connEntry.Category.HIDDEN else line.foreign - appPorts.append(appConn.getPort()) - - # Queue up resolution for the unresolved ports (skips if it's still working - # on the last query). - if appPorts and not self._appResolver.isResolving: - self._appResolver.resolve(appPorts) - - # Fetches results. If the query finishes quickly then this is what we just - # asked for, otherwise these belong to an earlier resolution. - # - # The application resolver might have given up querying (for instance, if - # the lsof lookups aren't working on this platform or lacks permissions). - # The isAppResolving flag lets the unresolved entries indicate if there's - # a lookup in progress for them or not. - - appResults = self._appResolver.getResults(0.2) - - for line in unresolvedLines: - isLocal = line.getType() == connEntry.Category.HIDDEN - linePort = line.local.getPort() if isLocal else line.foreign.getPort() - - if linePort in appResults: - # sets application attributes if there's a result with this as the - # inbound port - for inboundPort, outboundPort, cmd, pid in appResults[linePort]: - appPort = outboundPort if isLocal else inboundPort - - if linePort == appPort: - line.appName = cmd - line.appPid = pid - line.isAppResolving = False - else: - line.isAppResolving = self._appResolver.isResolving - - if flagQuery: - self.appResolveSinceUpdate = True -
Copied: arm/release/src/interface/connections/connPanel.py (from rev 24554, arm/trunk/src/interface/connections/connPanel.py) =================================================================== --- arm/release/src/interface/connections/connPanel.py (rev 0) +++ arm/release/src/interface/connections/connPanel.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -0,0 +1,398 @@ +""" +Listing of the currently established connections tor has made. +""" + +import time +import curses +import threading + +from interface.connections import entries, connEntry, circEntry +from util import connections, enum, panel, torTools, uiTools + +DEFAULT_CONFIG = {"features.connection.resolveApps": True, + "features.connection.listingType": 0, + "features.connection.refreshRate": 5} + +# height of the detail panel content, not counting top and bottom border +DETAILS_HEIGHT = 7 + +# listing types +Listing = enum.Enum(("IP_ADDRESS", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME") + +DEFAULT_SORT_ORDER = (entries.SortAttr.CATEGORY, entries.SortAttr.LISTING, entries.SortAttr.UPTIME) + +class ConnectionPanel(panel.Panel, threading.Thread): + """ + Listing of connections tor is making, with information correlated against + the current consensus and other data sources. + """ + + def __init__(self, stdscr, config=None): + panel.Panel.__init__(self, stdscr, "conn", 0) + threading.Thread.__init__(self) + self.setDaemon(True) + + 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 = entries.SortAttr.values() + customOrdering = config.getIntCSV("features.connection.order", None, 3, 0, len(sortFields)) + + if customOrdering: + self._sortOrdering = [sortFields[i] for i in customOrdering] + + self._listingType = Listing.values()[self._config["features.connection.listingType"]] + self._scroller = uiTools.Scroller(True) + self._title = "Connections:" # title line of the panel + self._entries = [] # last fetched display entries + self._entryLines = [] # individual lines rendered from the entries listing + self._showDetails = False # presents the details panel if true + + self._lastUpdate = -1 # time the content was last revised + self._isTorRunning = True # indicates if tor is currently running or not + self._isPaused = True # prevents updates if true + 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 + + # resolver for the command/pid associated with SOCKS, HIDDEN, and CONTROL connections + self._appResolver = connections.AppResolver("arm") + + # rate limits appResolver queries to once per update + self.appResolveSinceUpdate = False + + self._update() # populates initial entries + self._resolveApps(False) # resolves initial applications + + # mark the initially exitsing connection uptimes as being estimates + for entry in self._entries: + if isinstance(entry, connEntry.ConnectionEntry): + entry.getLines()[0].isInitialConnection = True + + # listens for when tor stops so we know to stop reflecting changes + torTools.getConn().addStatusListener(self.torStateListener) + + def torStateListener(self, conn, eventType): + """ + Freezes the connection contents when Tor stops. + + Arguments: + conn - tor controller + eventType - type of event detected + """ + + self._isTorRunning = eventType == torTools.State.INIT + + if self._isPaused or not self._isTorRunning: + if not self._pauseTime: self._pauseTime = time.time() + else: self._pauseTime = None + + self.redraw(True) + + def setPaused(self, isPause): + """ + If true, prevents the panel from updating. + """ + + if not self._isPaused == isPause: + self._isPaused = isPause + + if isPause or not self._isTorRunning: + if not self._pauseTime: self._pauseTime = time.time() + else: self._pauseTime = None + + # redraws so the display reflects any changes between the last update + # and being paused + self.redraw(True) + + def setSortOrder(self, ordering = None): + """ + Sets the connection attributes we're sorting by and resorts the contents. + + Arguments: + ordering - new ordering, if undefined then this resorts with the last + set ordering + """ + + self.valsLock.acquire() + if ordering: self._sortOrdering = ordering + self._entries.sort(key=lambda i: (i.getSortValues(self._sortOrdering, self._listingType))) + + self._entryLines = [] + for entry in self._entries: + self._entryLines += entry.getLines() + self.valsLock.release() + + def setListingType(self, listingType): + """ + Sets the priority information presented by the panel. + + Arguments: + listingType - Listing instance for the primary information to be shown + """ + + self.valsLock.acquire() + self._listingType = listingType + + # if we're sorting by the listing then we need to resort + if entries.SortAttr.LISTING in self._sortOrdering: + self.setSortOrder() + + self.valsLock.release() + + def handleKey(self, key): + self.valsLock.acquire() + + if uiTools.isScrollKey(key): + pageHeight = self.getPreferredSize()[0] - 1 + if self._showDetails: pageHeight -= (DETAILS_HEIGHT + 1) + isChanged = self._scroller.handleKey(key, self._entryLines, pageHeight) + if isChanged: self.redraw(True) + elif uiTools.isSelectionKey(key): + self._showDetails = not self._showDetails + self.redraw(True) + + self.valsLock.release() + + def run(self): + """ + Keeps connections listing updated, checking for new entries at a set rate. + """ + + lastDraw = time.time() - 1 + while not self._halt: + currentTime = time.time() + + if self._isPaused or not self._isTorRunning or currentTime - lastDraw < self._config["features.connection.refreshRate"]: + self._cond.acquire() + if not self._halt: self._cond.wait(0.2) + self._cond.release() + else: + # updates content if their's new results, otherwise just redraws + self._update() + self.redraw(True) + + # we may have missed multiple updates due to being paused, showing + # another panel, etc so lastDraw might need to jump multiple ticks + drawTicks = (time.time() - lastDraw) / self._config["features.connection.refreshRate"] + lastDraw += self._config["features.connection.refreshRate"] * drawTicks + + def draw(self, width, height): + self.valsLock.acquire() + + # extra line when showing the detail panel is for the bottom border + detailPanelOffset = DETAILS_HEIGHT + 1 if self._showDetails else 0 + isScrollbarVisible = len(self._entryLines) > height - detailPanelOffset - 1 + + scrollLoc = self._scroller.getScrollLoc(self._entryLines, height - detailPanelOffset - 1) + cursorSelection = self._scroller.getCursorSelection(self._entryLines) + + # draws the detail panel if currently displaying it + if self._showDetails: + # 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 + self.addstr(0, 0, title, curses.A_STANDOUT) + + scrollOffset = 1 + if isScrollbarVisible: + scrollOffset = 3 + self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelOffset - 1, len(self._entryLines), 1 + detailPanelOffset) + + currentTime = self._pauseTime if self._pauseTime else time.time() + for lineNum in range(scrollLoc, len(self._entryLines)): + entryLine = self._entryLines[lineNum] + + # if this is an unresolved SOCKS, HIDDEN, or CONTROL entry then queue up + # resolution for the applicaitions they belong to + if isinstance(entryLine, connEntry.ConnectionLine) and entryLine.isUnresolvedApp(): + self._resolveApps() + + # hilighting if this is the selected line + extraFormat = curses.A_STANDOUT if entryLine == cursorSelection else curses.A_NORMAL + + 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() + + def stop(self): + """ + Halts further resolutions and terminates the thread. + """ + + self._cond.acquire() + self._halt = True + self._cond.notifyAll() + self._cond.release() + + def _update(self): + """ + Fetches the newest resolved connections. + """ + + connResolver = connections.getResolver("tor") + currentResolutionCount = connResolver.getResolutionCount() + self.appResolveSinceUpdate = False + + if self._lastResourceFetch != currentResolutionCount: + self.valsLock.acquire() + + newEntries = [] # the new results we'll display + + # Fetches new connections and client circuits... + # newConnections [(local ip, local port, foreign ip, foreign port)...] + # newCircuits {circuitID => (status, purpose, path)...} + + newConnections = connResolver.getConnections() + newCircuits = {} + + for circuitID, status, purpose, path in torTools.getConn().getCircuits(): + # Skips established single-hop circuits (these are for directory + # fetches, not client circuits) + if not (status == "BUILT" and len(path) == 1): + newCircuits[circuitID] = (status, purpose, path) + + # Populates newEntries with any of our old entries that still exist. + # This is both for performance and to keep from resetting the uptime + # attributes. Note that CircEntries are a ConnectionEntry subclass so + # we need to check for them first. + + for oldEntry in self._entries: + if isinstance(oldEntry, circEntry.CircEntry): + newEntry = newCircuits.get(oldEntry.circuitID) + + if newEntry: + oldEntry.update(newEntry[0], newEntry[2]) + newEntries.append(oldEntry) + del newCircuits[oldEntry.circuitID] + elif isinstance(oldEntry, connEntry.ConnectionEntry): + connLine = oldEntry.getLines()[0] + connAttr = (connLine.local.getIpAddr(), connLine.local.getPort(), + connLine.foreign.getIpAddr(), connLine.foreign.getPort()) + + if connAttr in newConnections: + newEntries.append(oldEntry) + newConnections.remove(connAttr) + + # Reset any display attributes for the entries we're keeping + for entry in newEntries: entry.resetDisplay() + + # Adds any new connection and circuit entries. + for lIp, lPort, fIp, fPort in newConnections: + newConnEntry = connEntry.ConnectionEntry(lIp, lPort, fIp, fPort) + if newConnEntry.getLines()[0].getType() != connEntry.Category.CIRCUIT: + newEntries.append(newConnEntry) + + for circuitID in newCircuits: + status, purpose, path = newCircuits[circuitID] + newEntries.append(circEntry.CircEntry(circuitID, status, purpose, path)) + + # 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 = connEntry.Category.values() + typeCounts = dict((type, 0) for type in categoryTypes) + for entry in newEntries: + if isinstance(entry, connEntry.ConnectionEntry): + typeCounts[entry.getLines()[0].getType()] += 1 + elif isinstance(entry, circEntry.CircEntry): + typeCounts[connEntry.Category.CIRCUIT] += 1 + + # makes labels for all the categories with connections (ie, + # "21 outbound", "1 control", etc) + countLabels = [] + + for category in categoryTypes: + if typeCounts[category] > 0: + countLabels.append("%i %s" % (typeCounts[category], category.lower())) + + if countLabels: self._title = "Connections (%s):" % ", ".join(countLabels) + else: self._title = "Connections:" + + self._entries = newEntries + + self._entryLines = [] + for entry in self._entries: + self._entryLines += entry.getLines() + + self.setSortOrder() + self._lastResourceFetch = currentResolutionCount + self.valsLock.release() + + def _resolveApps(self, flagQuery = True): + """ + Triggers an asynchronous query for all unresolved SOCKS, HIDDEN, and + CONTROL entries. + + Arguments: + flagQuery - sets a flag to prevent further call from being respected + until the next update if true + """ + + if self.appResolveSinceUpdate or not self._config["features.connection.resolveApps"]: return + unresolvedLines = [l for l in self._entryLines if isinstance(l, connEntry.ConnectionLine) and l.isUnresolvedApp()] + + # get the ports used for unresolved applications + appPorts = [] + + for line in unresolvedLines: + appConn = line.local if line.getType() == connEntry.Category.HIDDEN else line.foreign + appPorts.append(appConn.getPort()) + + # Queue up resolution for the unresolved ports (skips if it's still working + # on the last query). + if appPorts and not self._appResolver.isResolving: + self._appResolver.resolve(appPorts) + + # Fetches results. If the query finishes quickly then this is what we just + # asked for, otherwise these belong to an earlier resolution. + # + # The application resolver might have given up querying (for instance, if + # the lsof lookups aren't working on this platform or lacks permissions). + # The isAppResolving flag lets the unresolved entries indicate if there's + # a lookup in progress for them or not. + + appResults = self._appResolver.getResults(0.2) + + for line in unresolvedLines: + isLocal = line.getType() == connEntry.Category.HIDDEN + linePort = line.local.getPort() if isLocal else line.foreign.getPort() + + if linePort in appResults: + # sets application attributes if there's a result with this as the + # inbound port + for inboundPort, outboundPort, cmd, pid in appResults[linePort]: + appPort = outboundPort if isLocal else inboundPort + + if linePort == appPort: + line.appName = cmd + line.appPid = pid + line.isAppResolving = False + else: + line.isAppResolving = self._appResolver.isResolving + + if flagQuery: + self.appResolveSinceUpdate = True +
Deleted: arm/release/src/interface/connections/entries.py =================================================================== --- arm/trunk/src/interface/connections/entries.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/interface/connections/entries.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -1,183 +0,0 @@ -""" -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"} - -# maximum number of ports a system can have -PORT_COUNT = 65536 - -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: - # uses the IP address as the primary value, and port as secondary - sortValue = self.getSortValue(SortAttr.IP_ADDRESS, listingType) * PORT_COUNT - sortValue += self.getSortValue(SortAttr.PORT, listingType) - return sortValue - 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 - - self._descriptorCache = None - self._descriptorCacheArgs = 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 getDetails - return [] - - def getDescriptor(self, width): - """ - Provides a list of DrawEntry instances with descriptor information for - this connection. - - Arguments: - width - available space to display in - """ - - if self._descriptorCacheArgs != width: - self._descriptorCache = self._getDescriptor(width) - self._descriptorCacheArgs = width - - return self._descriptorCache - - def _getDescriptor(self, width): - # implementation of getDescriptor - return [] - - def resetDisplay(self): - """ - Flushes cached display results. - """ - - self._listingCacheArgs = (None, None) - self._detailsCacheArgs = None -
Copied: arm/release/src/interface/connections/entries.py (from rev 24554, arm/trunk/src/interface/connections/entries.py) =================================================================== --- arm/release/src/interface/connections/entries.py (rev 0) +++ arm/release/src/interface/connections/entries.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -0,0 +1,183 @@ +""" +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"} + +# maximum number of ports a system can have +PORT_COUNT = 65536 + +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: + # uses the IP address as the primary value, and port as secondary + sortValue = self.getSortValue(SortAttr.IP_ADDRESS, listingType) * PORT_COUNT + sortValue += self.getSortValue(SortAttr.PORT, listingType) + return sortValue + 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 + + self._descriptorCache = None + self._descriptorCacheArgs = 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 getDetails + return [] + + def getDescriptor(self, width): + """ + Provides a list of DrawEntry instances with descriptor information for + this connection. + + Arguments: + width - available space to display in + """ + + if self._descriptorCacheArgs != width: + self._descriptorCache = self._getDescriptor(width) + self._descriptorCacheArgs = width + + return self._descriptorCache + + def _getDescriptor(self, width): + # implementation of getDescriptor + return [] + + def resetDisplay(self): + """ + Flushes cached display results. + """ + + self._listingCacheArgs = (None, None) + self._detailsCacheArgs = None +
Modified: arm/release/src/interface/controller.py =================================================================== --- arm/release/src/interface/controller.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/interface/controller.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -24,6 +24,9 @@ import descriptorPopup import fileDescriptorPopup
+import interface.connections.connPanel +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 @@ -41,13 +44,17 @@ PAGES = [ ["graph", "log"], ["conn"], + ["conn2"], ["config"], ["torrc"]] -PAUSEABLE = ["header", "graph", "log", "conn"]
+PAUSEABLE = ["header", "graph", "log", "conn", "conn2"] + CONFIG = {"log.torrc.readFailed": log.WARN, "features.graph.type": 1, "features.config.prepopulateEditValues": True, + "features.connection.oldPanel": False, + "features.connection.newPanel": True, "queries.refreshRate.rate": 5, "log.torEventTypeUnrecognized": log.NOTICE, "features.graph.bw.prepopulate": True, @@ -78,7 +85,7 @@ self.msgText = msgText self.msgAttr = msgAttr
- def draw(self, subwindow, width, height): + def draw(self, width, height): msgText = self.msgText msgAttr = self.msgAttr barTab = 2 # space between msgText and progress bar @@ -113,7 +120,11 @@ currentPage = self.page pageCount = len(PAGES)
- if self.isBlindMode: + if not CONFIG["features.connection.newPanel"]: + if currentPage >= 3: currentPage -= 1 + pageCount -= 1 + + if self.isBlindMode or not CONFIG["features.connection.oldPanel"]: if currentPage >= 2: currentPage -= 1 pageCount -= 1
@@ -240,7 +251,7 @@ popup.recreate(stdscr, newWidth)
key = 0 - while key not in (curses.KEY_ENTER, 10, ord(' ')): + while not uiTools.isSelectionKey(key): popup.clear() popup.win.box() popup.addstr(0, 0, title, curses.A_STANDOUT) @@ -341,7 +352,7 @@ elif key == curses.KEY_RIGHT: cursorLoc = min(len(selectionOptions) - 1, cursorLoc + 1) elif key == curses.KEY_UP: cursorLoc = max(0, cursorLoc - 4) elif key == curses.KEY_DOWN: cursorLoc = min(len(selectionOptions) - 1, cursorLoc + 4) - elif key in (curses.KEY_ENTER, 10, ord(' ')): + elif uiTools.isSelectionKey(key): # selected entry (the ord of '10' seems needed to pick up enter) selection = selectionOptions[cursorLoc] if selection == "Cancel": break @@ -394,7 +405,7 @@
if connections.isResolverAlive("tor"): resolver = connections.getResolver("tor") - resolver.setPaused(eventType == torTools.TOR_CLOSED) + resolver.setPaused(eventType == torTools.State.CLOSED)
def selectiveRefresh(panels, page): """ @@ -419,6 +430,7 @@ config = conf.getConfig("arm") config.update(CONFIG) graphing.graphPanel.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 @@ -471,12 +483,12 @@ duplicateOptions, defaultOptions, mismatchLines, missingOptions = [], [], [], []
for lineNum, issue, msg in corrections: - if issue == torConfig.VAL_DUPLICATE: - duplicateOptions.append("%s (line %i)" % (msg, lineNum)) - elif issue == torConfig.VAL_IS_DEFAULT: - defaultOptions.append("%s (line %i)" % (msg, lineNum)) - elif issue == torConfig.VAL_MISMATCH: mismatchLines.append(lineNum) - elif issue == torConfig.VAL_MISSING: missingOptions.append(msg) + if issue == torConfig.ValidationError.DUPLICATE: + duplicateOptions.append("%s (line %i)" % (msg, lineNum + 1)) + elif issue == torConfig.ValidationError.IS_DEFAULT: + defaultOptions.append("%s (line %i)" % (msg, lineNum + 1)) + elif issue == torConfig.ValidationError.MISMATCH: mismatchLines.append(lineNum + 1) + elif issue == torConfig.ValidationError.MISSING: missingOptions.append(msg)
if duplicateOptions or defaultOptions: msg = "Unneeded torrc entries found. They've been highlighted in blue on the torrc page." @@ -551,10 +563,21 @@ # before being positioned - the following is a quick hack til rewritten panels["log"].setPaused(True)
- panels["conn"] = connPanel.ConnPanel(stdscr, conn, isBlindMode) + if CONFIG["features.connection.oldPanel"]: + panels["conn"] = connPanel.ConnPanel(stdscr, conn, isBlindMode) + else: + panels["conn"] = panel.Panel(stdscr, "blank", 0, 0, 0) + PAUSEABLE.remove("conn") + + if CONFIG["features.connection.newPanel"]: + panels["conn2"] = interface.connections.connPanel.ConnectionPanel(stdscr, config) + else: + panels["conn2"] = panel.Panel(stdscr, "blank", 0, 0, 0) + PAUSEABLE.remove("conn2") + panels["control"] = ControlPanel(stdscr, isBlindMode) - panels["config"] = configPanel.ConfigPanel(stdscr, configPanel.TOR_STATE, config) - panels["torrc"] = torrcPanel.TorrcPanel(stdscr, torrcPanel.TORRC, config) + panels["config"] = configPanel.ConfigPanel(stdscr, configPanel.State.TOR, config) + panels["torrc"] = torrcPanel.TorrcPanel(stdscr, torrcPanel.Config.TORRC, config)
# provides error if pid coulnd't be determined (hopefully shouldn't happen...) if not torPid: log.log(log.WARN, "Unable to resolve tor pid, abandoning connection listing") @@ -577,7 +600,8 @@ conn.add_event_listener(panels["graph"].stats["bandwidth"]) conn.add_event_listener(panels["graph"].stats["system resources"]) if not isBlindMode: conn.add_event_listener(panels["graph"].stats["connections"]) - conn.add_event_listener(panels["conn"]) + if CONFIG["features.connection.oldPanel"]: + conn.add_event_listener(panels["conn"]) conn.add_event_listener(sighupTracker)
# prepopulates bandwidth values from state file @@ -604,16 +628,18 @@ # tells revised panels to run as daemons panels["header"].start() panels["log"].start() + if CONFIG["features.connection.newPanel"]: + panels["conn2"].start()
# warns if tor isn't updating descriptors - try: - if conn.get_option("FetchUselessDescriptors")[0][1] == "0" and conn.get_option("DirPort")[0][1] == "0": - warning = """Descriptors won't be updated (causing some connection information to be stale) unless: - a. 'FetchUselessDescriptors 1' is set in your torrc - b. the directory service is provided ('DirPort' defined) - c. or tor is used as a client""" - log.log(log.WARN, warning) - except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): pass + #try: + # if conn.get_option("FetchUselessDescriptors")[0][1] == "0" and conn.get_option("DirPort")[0][1] == "0": + # warning = """Descriptors won't be updated (causing some connection information to be stale) unless: + #a. 'FetchUselessDescriptors 1' is set in your torrc + #b. the directory service is provided ('DirPort' defined) + #c. or tor is used as a client""" + # log.log(log.WARN, warning) + #except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): pass
isUnresponsive = False # true if it's been over ten seconds since the last BW event (probably due to Tor closing) isPaused = False # if true updates are frozen @@ -641,6 +667,11 @@
lastSize = None
+ # sets initial visiblity for the pages + for i in range(len(PAGES)): + isVisible = i == page + for entry in PAGES[i]: panels[entry].setVisible(isVisible) + # TODO: come up with a nice, clean method for other threads to immediately # terminate the draw loop and provide a stacktrace while True: @@ -657,7 +688,8 @@ #panels["header"]._updateParams(True)
# other panels that use torrc data - panels["conn"].resetOptions() + if CONFIG["features.connection.oldPanel"]: + panels["conn"].resetOptions() #if not isBlindMode: panels["graph"].stats["connections"].resetOptions(conn) #panels["graph"].stats["bandwidth"].resetOptions()
@@ -718,7 +750,8 @@ isUnresponsive = False log.log(log.NOTICE, "Relay resumed")
- panels["conn"].reset() + if CONFIG["features.connection.oldPanel"]: + panels["conn"].reset()
# TODO: part two of hack to prevent premature drawing by log panel if page == 0 and not isPaused: panels["log"].setPaused(False) @@ -731,7 +764,7 @@ isResize = lastSize != newSize lastSize = newSize
- if panelKey in ("header", "graph", "log", "config", "torrc"): + if panelKey in ("header", "graph", "log", "config", "torrc", "conn2"): # revised panel (manages its own content refreshing) panels[panelKey].redraw(isResize) else: @@ -808,11 +841,15 @@ # this appears to be a python bug: http://bugs.python.org/issue3014 # (haven't seen this is quite some time... mysteriously resolved?)
+ torTools.NO_SPAWN = True # prevents further worker threads from being spawned + # stops panel daemons panels["header"].stop() + if CONFIG["features.connection.newPanel"]: panels["conn2"].stop() panels["log"].stop()
panels["header"].join() + if CONFIG["features.connection.newPanel"]: panels["conn2"].join() panels["log"].join()
# joins on utility daemon threads - this might take a moment since @@ -835,15 +872,25 @@ if key == curses.KEY_LEFT: page = (page - 1) % len(PAGES) else: page = (page + 1) % len(PAGES)
- # skip connections listing if it's disabled - if page == 1 and isBlindMode: - if key == curses.KEY_LEFT: page = (page - 1) % len(PAGES) - else: page = (page + 1) % len(PAGES) + # skip connections listings if it's disabled + while True: + if page == 1 and (isBlindMode or not CONFIG["features.connection.oldPanel"]): + if key == curses.KEY_LEFT: page = (page - 1) % len(PAGES) + else: page = (page + 1) % len(PAGES) + elif page == 2 and (isBlindMode or not CONFIG["features.connection.newPanel"]): + if key == curses.KEY_LEFT: page = (page - 1) % len(PAGES) + else: page = (page + 1) % len(PAGES) + else: break
# pauses panels that aren't visible to prevent events from accumilating # (otherwise they'll wait on the curses lock which might get demanding) setPauseState(panels, isPaused, page)
+ # prevents panels on other pages from redrawing + for i in range(len(PAGES)): + isVisible = i == page + for entry in PAGES[i]: panels[entry].setVisible(isVisible) + panels["control"].page = page + 1
# TODO: this redraw doesn't seem necessary (redraws anyway after this @@ -914,7 +961,7 @@ popup.addfstr(2, 41, "<b>n</b>: decrease graph size") popup.addfstr(3, 2, "<b>s</b>: graphed stats (<b>%s</b>)" % graphedStats) popup.addfstr(3, 41, "<b>i</b>: graph update interval (<b>%s</b>)" % graphing.graphPanel.UPDATE_INTERVALS[panels["graph"].updateInterval][0]) - popup.addfstr(4, 2, "<b>b</b>: graph bounds (<b>%s</b>)" % graphing.graphPanel.BOUND_LABELS[panels["graph"].bounds]) + popup.addfstr(4, 2, "<b>b</b>: graph bounds (<b>%s</b>)" % panels["graph"].bounds.lower()) popup.addfstr(4, 41, "<b>d</b>: file descriptors") popup.addfstr(5, 2, "<b>e</b>: change logged events")
@@ -940,10 +987,11 @@
resolverUtil = connections.getResolver("tor").overwriteResolver if resolverUtil == None: resolverUtil = "auto" - else: resolverUtil = connections.CMD_STR[resolverUtil] popup.addfstr(4, 41, "<b>u</b>: resolving utility (<b>%s</b>)" % resolverUtil)
- allowDnsLabel = "allow" if panels["conn"].allowDNS else "disallow" + if CONFIG["features.connection.oldPanel"]: + allowDnsLabel = "allow" if panels["conn"].allowDNS else "disallow" + else: allowDnsLabel = "disallow" popup.addfstr(5, 2, "<b>r</b>: permit DNS resolution (<b>%s</b>)" % allowDnsLabel)
popup.addfstr(5, 41, "<b>s</b>: sort ordering") @@ -959,8 +1007,18 @@ popup.addfstr(2, 41, "<b>page down</b>: scroll down a page")
popup.addfstr(3, 2, "<b>enter</b>: edit configuration option") - popup.addfstr(3, 41, "<b>w</b>: save current configuration") - popup.addfstr(4, 2, "<b>s</b>: sort ordering") + #popup.addfstr(3, 41, "<b>d</b>: raw consensus descriptor") + + listingType = panels["conn2"]._listingType.lower() + popup.addfstr(4, 2, "<b>l</b>: listed identity (<b>%s</b>)" % listingType) + + popup.addfstr(4, 41, "<b>s</b>: sort ordering") + + resolverUtil = connections.getResolver("tor").overwriteResolver + if resolverUtil == None: resolverUtil = "auto" + popup.addfstr(3, 41, "<b>u</b>: resolving utility (<b>%s</b>)" % resolverUtil) + + pageOverrideKeys = (ord('l'), ord('s'), ord('u')) elif page == 3: popup.addfstr(1, 2, "<b>up arrow</b>: scroll up a line") popup.addfstr(1, 41, "<b>down arrow</b>: scroll down a line") @@ -975,6 +1033,12 @@
popup.addfstr(4, 2, "<b>r</b>: reload torrc") popup.addfstr(4, 41, "<b>x</b>: reset tor (issue sighup)") + elif page == 4: + popup.addfstr(1, 2, "<b>up arrow</b>: scroll up a line") + popup.addfstr(1, 41, "<b>down arrow</b>: scroll down a line") + popup.addfstr(2, 2, "<b>page up</b>: scroll up a page") + popup.addfstr(2, 41, "<b>page down</b>: scroll down a page") + popup.addfstr(3, 2, "<b>enter</b>: connection details")
popup.addstr(7, 2, "Press any key...") popup.refresh() @@ -1054,7 +1118,7 @@ selectiveRefresh(panels, page) elif page == 0 and (key == ord('b') or key == ord('B')): # uses the next boundary type for graph - panels["graph"].bounds = (panels["graph"].bounds + 1) % 3 + panels["graph"].bounds = graphing.graphPanel.Bounds.next(panels["graph"].bounds)
selectiveRefresh(panels, page) elif page == 0 and key in (ord('d'), ord('D')): @@ -1245,13 +1309,16 @@ setPauseState(panels, isPaused, page) finally: panel.CURSES_LOCK.release() - elif key == 27 and panels["conn"].listingType == connPanel.LIST_HOSTNAME and panels["control"].resolvingCounter != -1: + elif CONFIG["features.connection.oldPanel"] and 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 hostnames.setPaused(True) panels["conn"].sortConnections() - elif page == 1 and panels["conn"].isCursorEnabled and key in (curses.KEY_ENTER, 10, ord(' ')): + elif page == 1 and panels["conn"].isCursorEnabled and uiTools.isSelectionKey(key): + # TODO: deprecated when migrated to the new connection panel, thought as + # well keep around until there's a counterpart for hostname fetching + # provides details on selected connection panel.CURSES_LOCK.acquire() try: @@ -1269,7 +1336,7 @@ curses.cbreak() # wait indefinitely for key presses (no timeout) key = 0
- while key not in (curses.KEY_ENTER, 10, ord(' ')): + while not uiTools.isSelectionKey(key): popup.clear() popup.win.box() popup.addstr(0, 0, "Connection Details:", curses.A_STANDOUT) @@ -1455,13 +1522,13 @@ hostnames.setPaused(True)
panels["conn"].sortConnections() - elif page == 1 and (key == ord('u') or key == ord('U')): + elif page in (1, 2) and (key == ord('u') or key == ord('U')): # provides menu to pick identification resolving utility - optionTypes = [None, connections.CMD_PROC, connections.CMD_NETSTAT, connections.CMD_SOCKSTAT, connections.CMD_LSOF, connections.CMD_SS, connections.CMD_BSD_SOCKSTAT, connections.CMD_BSD_PROCSTAT] - options = ["auto"] + [connections.CMD_STR[util] for util in optionTypes[1:]] + options = ["auto"] + connections.Resolver.values()
- initialSelection = connections.getResolver("tor").overwriteResolver # enums correspond to indices - if initialSelection == None: initialSelection = 0 + currentOverwrite = connections.getResolver("tor").overwriteResolver # enums correspond to indices + if currentOverwrite == None: initialSelection = 0 + else: initialSelection = options.index(currentOverwrite)
# hides top label of conn panel and pauses panels panels["conn"].showLabel = False @@ -1469,14 +1536,15 @@ setPauseState(panels, isPaused, page, True)
selection = showMenu(stdscr, panels["popup"], "Resolver Util:", options, initialSelection) + selectedOption = options[selection] if selection != "auto" else None
# reverts changes made for popup panels["conn"].showLabel = True setPauseState(panels, isPaused, page)
# applies new setting - if selection != -1 and optionTypes[selection] != connections.getResolver("tor").overwriteResolver: - connections.getResolver("tor").overwriteResolver = optionTypes[selection] + if selection != -1 and selectedOption != connections.getResolver("tor").overwriteResolver: + connections.getResolver("tor").overwriteResolver = selectedOption elif page == 1 and (key == ord('s') or key == ord('S')): # set ordering for connection listing titleLabel = "Connection Ordering:" @@ -1543,7 +1611,44 @@ setPauseState(panels, isPaused, page) finally: panel.CURSES_LOCK.release() - elif page == 2 and (key == ord('c') or key == ord('C')) and False: + 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.entries.ListingType.values() + + # dropping the HOSTNAME listing type until we support displaying that content + options.remove(interface.connections.entries.ListingType.HOSTNAME) + + initialSelection = options.index(panels["conn2"]._listingType) + + # hides top label of connection panel and pauses the display + panelTitle = panels["conn2"]._title + panels["conn2"]._title = "" + panels["conn2"].redraw(True) + setPauseState(panels, isPaused, page, True) + + selection = showMenu(stdscr, panels["popup"], "List By:", options, initialSelection) + + # reverts changes made for popup + panels["conn2"]._title = panelTitle + setPauseState(panels, isPaused, page) + + # applies new setting + if selection != -1 and options[selection] != panels["conn2"]._listingType: + panels["conn2"].setListingType(options[selection]) + panels["conn2"].redraw(True) + elif page == 2 and (key == ord('s') or key == ord('S')): + # set ordering for connection options + titleLabel = "Connection Ordering:" + options = interface.connections.entries.SortAttr.values() + oldSelection = panels["conn2"]._sortOrdering + 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: + panels["conn2"].setSortOrder(results) + + panels["conn2"].redraw(True) + elif page == 3 and (key == ord('c') or key == ord('C')) and False: # TODO: disabled for now (probably gonna be going with separate pages # rather than popup menu) # provides menu to pick config being displayed @@ -1566,12 +1671,11 @@ if selection != -1: panels["torrc"].setConfigType(selection)
selectiveRefresh(panels, page) - elif page == 2 and (key == ord('w') or key == ord('W')): + elif page == 3 and (key == ord('w') or key == ord('W')): # display a popup for saving the current configuration panel.CURSES_LOCK.acquire() try: - configText = torTools.getConn().getInfo("config-text", "").strip() - configLines = configText.split("\n") + configLines = torConfig.getCustomOptions(True)
# lists event types popup = panels["popup"] @@ -1594,7 +1698,7 @@ popup.recreate(stdscr)
key, selection = 0, 2 - while key not in (curses.KEY_ENTER, 10, ord(' ')): + while not uiTools.isSelectionKey(key): # if the popup has been resized then recreate it (needed for the # proper border height) newHeight, newWidth = panels["popup"].getPreferredSize() @@ -1669,7 +1773,7 @@
# saves the configuration to the file configFile = open(configLocation, "w") - configFile.write(configText) + configFile.write("\n".join(configLines)) configFile.close()
# reloads the cached torrc if overwriting it @@ -1696,12 +1800,12 @@ panel.CURSES_LOCK.release()
panels["config"].redraw(True) - elif page == 2 and (key == ord('s') or key == ord('S')): + elif page == 3 and (key == ord('s') or key == ord('S')): # set ordering for config options titleLabel = "Config Option Ordering:" - options = [configPanel.FIELD_ATTR[i][0] for i in range(8)] - oldSelection = [configPanel.FIELD_ATTR[entry][0] for entry in panels["config"].sortOrdering] - optionColors = dict([configPanel.FIELD_ATTR[i] for i in range(8)]) + options = [configPanel.FIELD_ATTR[field][0] for field in configPanel.Field.values()] + oldSelection = [configPanel.FIELD_ATTR[field][0] for field in panels["config"].sortOrdering] + optionColors = dict([configPanel.FIELD_ATTR[field] for field in configPanel.Field.values()]) results = showSortDialog(stdscr, panels, isPaused, page, titleLabel, options, oldSelection, optionColors)
if results: @@ -1717,7 +1821,7 @@ panels["config"].setSortOrder(resultEnums)
panels["config"].redraw(True) - elif page == 2 and key in (curses.KEY_ENTER, 10, ord(' ')): + elif page == 3 and uiTools.isSelectionKey(key): # let the user edit the configuration value, unchanged if left blank panel.CURSES_LOCK.acquire() try: @@ -1725,13 +1829,13 @@
# provides prompt selection = panels["config"].getSelection() - configOption = selection.get(configPanel.FIELD_OPTION) + configOption = selection.get(configPanel.Field.OPTION) titleMsg = "%s Value (esc to cancel): " % configOption panels["control"].setMsg(titleMsg) panels["control"].redraw(True)
displayWidth = panels["control"].getPreferredSize()[1] - initialValue = selection.get(configPanel.FIELD_VALUE) + initialValue = selection.get(configPanel.Field.VALUE)
# initial input for the text field initialText = "" @@ -1745,19 +1849,19 @@ conn = torTools.getConn()
# if the value's a boolean then allow for 'true' and 'false' inputs - if selection.get(configPanel.FIELD_TYPE) == "Boolean": + if selection.get(configPanel.Field.TYPE) == "Boolean": if newConfigValue.lower() == "true": newConfigValue = "1" elif newConfigValue.lower() == "false": newConfigValue = "0"
try: - if selection.get(configPanel.FIELD_TYPE) == "LineList": + if selection.get(configPanel.Field.TYPE) == "LineList": newConfigValue = newConfigValue.split(",")
conn.setOption(configOption, newConfigValue)
# resets the isDefault flag customOptions = torConfig.getCustomOptions() - selection.fields[configPanel.FIELD_IS_DEFAULT] = not configOption in customOptions + selection.fields[configPanel.Field.IS_DEFAULT] = not configOption in customOptions
panels["config"].redraw(True) except Exception, exc: @@ -1773,7 +1877,7 @@ setPauseState(panels, isPaused, page) finally: panel.CURSES_LOCK.release() - elif page == 3 and key == ord('r') or key == ord('R'): + elif page == 4 and key == ord('r') or key == ord('R'): # reloads torrc, providing a notice if successful or not loadedTorrc = torConfig.getTorrc() loadedTorrc.getLock().acquire() @@ -1803,8 +1907,10 @@ elif page == 1: panels["conn"].handleKey(key) elif page == 2: + panels["conn2"].handleKey(key) + elif page == 3: panels["config"].handleKey(key) - elif page == 3: + elif page == 4: panels["torrc"].handleKey(key)
def startTorMonitor(startTime, loggedEvents, isBlindMode):
Modified: arm/release/src/interface/descriptorPopup.py =================================================================== --- arm/release/src/interface/descriptorPopup.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/interface/descriptorPopup.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -106,7 +106,7 @@ draw(popup, properties) key = stdscr.getch()
- if key in (curses.KEY_ENTER, 10, ord(' '), ord('d'), ord('D')): + if uiTools.isSelectionKey(key) or key in (ord('d'), ord('D')): # closes popup isVisible = False elif key in (curses.KEY_LEFT, curses.KEY_RIGHT):
Modified: arm/release/src/interface/graphing/__init__.py =================================================================== --- arm/release/src/interface/graphing/__init__.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/interface/graphing/__init__.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -2,5 +2,5 @@ Panels, popups, and handlers comprising the arm user interface. """
-__all__ = ["graphPanel.py", "bandwidthStats", "connStats", "resourceStats"] +__all__ = ["graphPanel", "bandwidthStats", "connStats", "resourceStats"]
Modified: arm/release/src/interface/graphing/bandwidthStats.py =================================================================== --- arm/release/src/interface/graphing/bandwidthStats.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/interface/graphing/bandwidthStats.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -20,7 +20,8 @@ PREPOPULATE_SUCCESS_MSG = "Read the last day of bandwidth history from the state file" PREPOPULATE_FAILURE_MSG = "Unable to prepopulate bandwidth information (%s)"
-DEFAULT_CONFIG = {"features.graph.bw.transferInBytes": False, +DEFAULT_CONFIG = {"features.graph.bw.prepopulateTotal": False, + "features.graph.bw.transferInBytes": False, "features.graph.bw.accounting.show": True, "features.graph.bw.accounting.rate": 10, "features.graph.bw.accounting.isTimeLong": False, @@ -39,6 +40,11 @@ if config: config.update(self._config, {"features.graph.bw.accounting.rate": 1})
+ # stats prepopulated from tor's state file + self.prepopulatePrimaryTotal = 0 + self.prepopulateSecondaryTotal = 0 + self.prepopulateTicks = 0 + # accounting data (set by _updateAccountingInfo method) self.accountingLastUpdated = 0 self.accountingInfo = dict([(arg, "") for arg in ACCOUNTING_ARGS]) @@ -47,7 +53,7 @@ # rate/burst and if tor's using accounting conn = torTools.getConn() self._titleStats, self.isAccounting = [], False - self.resetListener(conn, torTools.TOR_INIT) # initializes values + self.resetListener(conn, torTools.State.INIT) # initializes values conn.addStatusListener(self.resetListener)
def resetListener(self, conn, eventType): @@ -55,7 +61,7 @@ self._titleStats = [] # force reset of title self.new_desc_event(None) # updates title params
- if eventType == torTools.TOR_INIT and self._config["features.graph.bw.accounting.show"]: + if eventType == torTools.State.INIT and self._config["features.graph.bw.accounting.show"]: self.isAccounting = conn.getInfo('accounting/enabled') == '1'
def prepopulateFromState(self): @@ -164,10 +170,11 @@ readVal, writeVal = bwReadEntries[i], bwWriteEntries[i]
self.lastPrimary, self.lastSecondary = readVal, writeVal - self.primaryTotal += readVal * 900 - self.secondaryTotal += writeVal * 900 - self.tick += 900
+ self.prepopulatePrimaryTotal += readVal * 900 + self.prepopulateSecondaryTotal += writeVal * 900 + self.prepopulateTicks += 900 + self.primaryCounts[intervalIndex].insert(0, readVal) self.secondaryCounts[intervalIndex].insert(0, writeVal)
@@ -316,10 +323,15 @@
def _getAvgLabel(self, isPrimary): total = self.primaryTotal if isPrimary else self.secondaryTotal - return "avg: %s/sec" % uiTools.getSizeLabel((total / max(1, self.tick)) * 1024, 1, False, self._config["features.graph.bw.transferInBytes"]) + total += self.prepopulatePrimaryTotal if isPrimary else self.prepopulateSecondaryTotal + return "avg: %s/sec" % uiTools.getSizeLabel((total / max(1, self.tick + self.prepopulateTicks)) * 1024, 1, False, self._config["features.graph.bw.transferInBytes"])
def _getTotalLabel(self, isPrimary): total = self.primaryTotal if isPrimary else self.secondaryTotal + + if self._config["features.graph.bw.prepopulateTotal"]: + total += self.prepopulatePrimaryTotal if isPrimary else self.prepopulateSecondaryTotal + return "total: %s" % uiTools.getSizeLabel(total * 1024, 1)
def _updateAccountingInfo(self):
Modified: arm/release/src/interface/graphing/connStats.py =================================================================== --- arm/release/src/interface/graphing/connStats.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/interface/graphing/connStats.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -17,11 +17,11 @@ # listens for tor reload (sighup) events which can reset the ports tor uses conn = torTools.getConn() self.orPort, self.dirPort, self.controlPort = "0", "0", "0" - self.resetListener(conn, torTools.TOR_INIT) # initialize port values + self.resetListener(conn, torTools.State.INIT) # initialize port values conn.addStatusListener(self.resetListener)
def resetListener(self, conn, eventType): - if eventType == torTools.TOR_INIT: + if eventType == torTools.State.INIT: self.orPort = conn.getOption("ORPort", "0") self.dirPort = conn.getOption("DirPort", "0") self.controlPort = conn.getOption("ControlPort", "0")
Modified: arm/release/src/interface/graphing/graphPanel.py =================================================================== --- arm/release/src/interface/graphing/graphPanel.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/interface/graphing/graphPanel.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -20,7 +20,7 @@ import curses from TorCtl import TorCtl
-from util import panel, uiTools +from util import enum, panel, uiTools
# time intervals at which graphs can be updated UPDATE_INTERVALS = [("each second", 1), ("5 seconds", 5), ("30 seconds", 30), @@ -32,11 +32,10 @@ MIN_GRAPH_HEIGHT = 1
# enums for graph bounds: -# BOUNDS_GLOBAL_MAX - global maximum (highest value ever seen) -# BOUNDS_LOCAL_MAX - local maximum (highest value currently on the graph) -# BOUNDS_TIGHT - local maximum and minimum -BOUNDS_GLOBAL_MAX, BOUNDS_LOCAL_MAX, BOUNDS_TIGHT = range(3) -BOUND_LABELS = {BOUNDS_GLOBAL_MAX: "global max", BOUNDS_LOCAL_MAX: "local max", BOUNDS_TIGHT: "tight"} +# Bounds.GLOBAL_MAX - global maximum (highest value ever seen) +# Bounds.LOCAL_MAX - local maximum (highest value currently on the graph) +# Bounds.TIGHT - local maximum and minimum +Bounds = enum.Enum("GLOBAL_MAX", "LOCAL_MAX", "TIGHT")
WIDE_LABELING_GRAPH_COL = 50 # minimum graph columns to use wide spacing for x-axis labels
@@ -248,7 +247,7 @@ def __init__(self, stdscr): panel.Panel.__init__(self, stdscr, "graph", 0) self.updateInterval = CONFIG["features.graph.interval"] - self.bounds = CONFIG["features.graph.bound"] + self.bounds = Bounds.values()[CONFIG["features.graph.bound"]] self.graphHeight = CONFIG["features.graph.height"] self.currentDisplay = None # label of the stats currently being displayed self.stats = {} # available stats (mappings of label -> instance) @@ -276,7 +275,7 @@
self.graphHeight = max(MIN_GRAPH_HEIGHT, newGraphHeight)
- def draw(self, subwindow, width, height): + def draw(self, width, height): """ Redraws graph panel """
if self.currentDisplay: @@ -294,11 +293,11 @@ if right: self.addstr(1, graphCol + 5, right, curses.A_BOLD | secondaryColor)
# determines max/min value on the graph - if self.bounds == BOUNDS_GLOBAL_MAX: + if self.bounds == Bounds.GLOBAL_MAX: primaryMaxBound = int(param.maxPrimary[self.updateInterval]) secondaryMaxBound = int(param.maxSecondary[self.updateInterval]) else: - # both BOUNDS_LOCAL_MAX and BOUNDS_TIGHT use local maxima + # both Bounds.LOCAL_MAX and Bounds.TIGHT use local maxima if graphCol < 2: # nothing being displayed primaryMaxBound, secondaryMaxBound = 0, 0 @@ -307,7 +306,7 @@ secondaryMaxBound = int(max(param.secondaryCounts[self.updateInterval][1:graphCol + 1]))
primaryMinBound = secondaryMinBound = 0 - if self.bounds == BOUNDS_TIGHT: + if self.bounds == Bounds.TIGHT: primaryMinBound = int(min(param.primaryCounts[self.updateInterval][1:graphCol + 1])) secondaryMinBound = int(min(param.secondaryCounts[self.updateInterval][1:graphCol + 1]))
Modified: arm/release/src/interface/headerPanel.py =================================================================== --- arm/release/src/interface/headerPanel.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/interface/headerPanel.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -37,8 +37,8 @@ Top area contenting tor settings and system information. Stats are stored in the vals mapping, keys including: tor/ version, versionStatus, nickname, orPort, dirPort, controlPort, - exitPolicy, isAuthPassword (bool), isAuthCookie (bool) - *address, *fingerprint, *flags, pid, startTime + exitPolicy, isAuthPassword (bool), isAuthCookie (bool), + orListenAddr, *address, *fingerprint, *flags, pid, startTime sys/ hostname, os, version stat/ *%torCpu, *%armCpu, *rss, *%mem
@@ -95,7 +95,7 @@ if self.vals["tor/orPort"]: return 4 if isWide else 6 else: return 3 if isWide else 4
- def draw(self, subwindow, width, height): + def draw(self, width, height): self.valsLock.acquire() isWide = width + 1 >= MIN_DUAL_COL_WIDTH
@@ -127,10 +127,14 @@
# Line 2 / Line 2 Left (tor ip/port information) if self.vals["tor/orPort"]: + myAddress = "Unknown" + if self.vals["tor/orListenAddr"]: myAddress = self.vals["tor/orListenAddr"] + elif self.vals["tor/address"]: myAddress = self.vals["tor/address"] + # acting as a relay (we can assume certain parameters are set entry = "" dirPortLabel = ", Dir Port: %s" % self.vals["tor/dirPort"] if self.vals["tor/dirPort"] != "0" else "" - for label in (self.vals["tor/nickname"], " - " + self.vals["tor/address"], ":" + self.vals["tor/orPort"], dirPortLabel): + for label in (self.vals["tor/nickname"], " - " + myAddress, ":" + self.vals["tor/orPort"], dirPortLabel): if len(entry) + len(label) <= leftWidth: entry += label else: break else: @@ -239,7 +243,7 @@
def run(self): """ - Keeps stats updated, querying new information at a set rate. + Keeps stats updated, checking for new information at a set rate. """
lastDraw = time.time() - 1 @@ -288,14 +292,14 @@ eventType - type of event detected """
- if eventType == torTools.TOR_INIT: + if eventType == torTools.State.INIT: self._isTorConnected = True if self._isPaused: self._haltTime = time.time() else: self._haltTime = None
self._update(True) self.redraw(True) - elif eventType == torTools.TOR_CLOSED: + elif eventType == torTools.State.CLOSED: self._isTorConnected = False self._haltTime = time.time() self._update() @@ -329,15 +333,15 @@ if self.vals["tor/orPort"] == "0": self.vals["tor/orPort"] = ""
# overwrite address if ORListenAddress is set (and possibly orPort too) - self.vals["tor/address"] = "Unknown" + self.vals["tor/orListenAddr"] = "" listenAddr = conn.getOption("ORListenAddress") if listenAddr: if ":" in listenAddr: # both ip and port overwritten - self.vals["tor/address"] = listenAddr[:listenAddr.find(":")] + self.vals["tor/orListenAddr"] = listenAddr[:listenAddr.find(":")] self.vals["tor/orPort"] = listenAddr[listenAddr.find(":") + 1:] else: - self.vals["tor/address"] = listenAddr + self.vals["tor/orListenAddr"] = listenAddr
# fetch exit policy (might span over multiple lines) policyEntries = [] @@ -368,8 +372,7 @@ # sets volatile parameters # TODO: This can change, being reported by STATUS_SERVER -> EXTERNAL_ADDRESS # events. Introduce caching via torTools? - if self.vals["tor/address"] == "Unknown": - self.vals["tor/address"] = conn.getInfo("address", self.vals["tor/address"]) + self.vals["tor/address"] = conn.getInfo("address", "")
self.vals["tor/fingerprint"] = conn.getInfo("fingerprint", self.vals["tor/fingerprint"]) self.vals["tor/flags"] = conn.getMyFlags(self.vals["tor/flags"])
Modified: arm/release/src/interface/logPanel.py =================================================================== --- arm/release/src/interface/logPanel.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/interface/logPanel.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -32,8 +32,8 @@ 12345 arm runlevel+ X No Events 67890 torctl runlevel+ U Unknown Events"""
-RUNLEVELS = ["DEBUG", "INFO", "NOTICE", "WARN", "ERR"] -RUNLEVEL_EVENT_COLOR = {"DEBUG": "magenta", "INFO": "blue", "NOTICE": "green", "WARN": "yellow", "ERR": "red"} +RUNLEVEL_EVENT_COLOR = {log.DEBUG: "magenta", log.INFO: "blue", log.NOTICE: "green", + log.WARN: "yellow", log.ERR: "red"} DAYBREAK_EVENT = "DAYBREAK" # special event for marking when the date changes TIMEZONE_OFFSET = time.altzone if time.localtime()[8] else time.timezone
@@ -112,8 +112,8 @@
for flag in eventAbbr: if flag == "A": - armRunlevels = ["ARM_" + runlevel for runlevel in RUNLEVELS] - torctlRunlevels = ["TORCTL_" + runlevel for runlevel in RUNLEVELS] + armRunlevels = ["ARM_" + runlevel for runlevel in log.Runlevel.values()] + torctlRunlevels = ["TORCTL_" + runlevel for runlevel in log.Runlevel.values()] expandedEvents = set(TOR_EVENT_TYPES.values() + armRunlevels + torctlRunlevels + ["UNKNOWN"]) break elif flag == "X": @@ -131,7 +131,7 @@ elif flag in "W49": runlevelIndex = 3 elif flag in "E50": runlevelIndex = 4
- runlevelSet = [typePrefix + runlevel for runlevel in RUNLEVELS[runlevelIndex:]] + runlevelSet = [typePrefix + runlevel for runlevel in log.Runlevel.values()[runlevelIndex:]] expandedEvents = expandedEvents.union(set(runlevelSet)) elif flag == "U": expandedEvents.add("UNKNOWN") @@ -210,16 +210,17 @@
# if the runlevels argument is a superset of the log file then we can # limit the read contents to the addLimit + runlevels = log.Runlevel.values() loggingTypes = loggingTypes.upper() if addLimit and (not readLimit or readLimit > addLimit): if "-" in loggingTypes: divIndex = loggingTypes.find("-") - sIndex = RUNLEVELS.index(loggingTypes[:divIndex]) - eIndex = RUNLEVELS.index(loggingTypes[divIndex+1:]) - logFileRunlevels = RUNLEVELS[sIndex:eIndex+1] + sIndex = runlevels.index(loggingTypes[:divIndex]) + eIndex = runlevels.index(loggingTypes[divIndex+1:]) + logFileRunlevels = runlevels[sIndex:eIndex+1] else: - sIndex = RUNLEVELS.index(loggingTypes) - logFileRunlevels = RUNLEVELS[sIndex:] + sIndex = runlevels.index(loggingTypes) + logFileRunlevels = runlevels[sIndex:]
# checks if runlevels we're reporting are a superset of the file's contents isFileSubset = True @@ -570,7 +571,7 @@ # fetches past tor events from log file, if available torEventBacklog = [] if self._config["features.log.prepopulate"]: - setRunlevels = list(set.intersection(set(self.loggedEvents), set(RUNLEVELS))) + setRunlevels = list(set.intersection(set(self.loggedEvents), set(log.Runlevel.values()))) readLimit = self._config["features.log.prepopulateReadLimit"] addLimit = self._config["cache.logPanel.size"] torEventBacklog = getLogFileEntries(setRunlevels, readLimit, addLimit, self._config) @@ -584,13 +585,12 @@ # gets the set of arm events we're logging setRunlevels = [] for i in range(len(armRunlevels)): - if "ARM_" + RUNLEVELS[i] in self.loggedEvents: + if "ARM_" + log.Runlevel.values()[i] in self.loggedEvents: setRunlevels.append(armRunlevels[i])
armEventBacklog = [] for level, msg, eventTime in log._getEntries(setRunlevels): - runlevelStr = log.RUNLEVEL_STR[level] - armEventEntry = LogEntry(eventTime, "ARM_" + runlevelStr, msg, RUNLEVEL_EVENT_COLOR[runlevelStr]) + armEventEntry = LogEntry(eventTime, "ARM_" + level, msg, RUNLEVEL_EVENT_COLOR[level]) armEventBacklog.insert(0, armEventEntry)
# joins armEventBacklog and torEventBacklog chronologically into msgLog @@ -621,14 +621,14 @@ if self._config["features.logFile"]: logPath = self._config["features.logFile"]
- # make dir if the path doesn't already exist - baseDir = os.path.dirname(logPath) - if not os.path.exists(baseDir): os.makedirs(baseDir) - try: + # make dir if the path doesn't already exist + baseDir = os.path.dirname(logPath) + if not os.path.exists(baseDir): os.makedirs(baseDir) + self.logFile = open(logPath, "a") log.log(self._config["log.logPanel.logFileOpened"], "arm %s opening log file (%s)" % (VERSION, logPath)) - except IOError, exc: + except (IOError, OSError), exc: log.log(self._config["log.logPanel.logFileWriteFailed"], "Unable to write to log file: %s" % sysTools.getFileErrorMsg(exc)) self.logFile = None
@@ -778,7 +778,7 @@ self.redraw(True) self.valsLock.release()
- def draw(self, subwindow, width, height): + def draw(self, width, height): """ Redraws message log. Entries stretch to use available space and may contain up to two lines. Starts with newest entries. @@ -831,23 +831,22 @@ # bottom of the divider if seenFirstDateDivider: if lineCount >= 1 and lineCount < height and showDaybreaks: - self.win.vline(lineCount, dividerIndent, curses.ACS_LLCORNER | dividerAttr, 1) - self.win.hline(lineCount, dividerIndent + 1, curses.ACS_HLINE | dividerAttr, width - dividerIndent - 1) - self.win.vline(lineCount, width, curses.ACS_LRCORNER | dividerAttr, 1) + self.addch(lineCount, dividerIndent, curses.ACS_LLCORNER, dividerAttr) + self.hline(lineCount, dividerIndent + 1, width - dividerIndent - 1, dividerAttr) + self.addch(lineCount, width, curses.ACS_LRCORNER, dividerAttr)
lineCount += 1
# top of the divider if lineCount >= 1 and lineCount < height and showDaybreaks: timeLabel = time.strftime(" %B %d, %Y ", time.localtime(entry.timestamp)) - self.win.vline(lineCount, dividerIndent, curses.ACS_ULCORNER | dividerAttr, 1) - self.win.hline(lineCount, dividerIndent + 1, curses.ACS_HLINE | dividerAttr, 1) + self.addch(lineCount, dividerIndent, curses.ACS_ULCORNER, dividerAttr) + self.addch(lineCount, dividerIndent + 1, curses.ACS_HLINE, dividerAttr) self.addstr(lineCount, dividerIndent + 2, timeLabel, curses.A_BOLD | dividerAttr)
- if dividerIndent + len(timeLabel) + 2 <= width: - lineLength = width - dividerIndent - len(timeLabel) - 2 - self.win.hline(lineCount, dividerIndent + len(timeLabel) + 2, curses.ACS_HLINE | dividerAttr, lineLength) - self.win.vline(lineCount, dividerIndent + len(timeLabel) + 2 + lineLength, curses.ACS_URCORNER | dividerAttr, 1) + lineLength = width - dividerIndent - len(timeLabel) - 2 + self.hline(lineCount, dividerIndent + len(timeLabel) + 2, lineLength, dividerAttr) + self.addch(lineCount, dividerIndent + len(timeLabel) + 2 + lineLength, curses.ACS_URCORNER, dividerAttr)
seenFirstDateDivider = True lineCount += 1 @@ -879,15 +878,15 @@ if lineOffset == maxEntriesPerLine - 1: msg = uiTools.cropStr(msg, maxMsgSize) else: - msg, remainder = uiTools.cropStr(msg, maxMsgSize, 4, 4, uiTools.END_WITH_HYPHEN, True) + msg, remainder = uiTools.cropStr(msg, maxMsgSize, 4, 4, uiTools.Ending.HYPHEN, True) displayQueue.insert(0, (remainder.strip(), format, includeBreak))
includeBreak = True
if drawLine < height and drawLine >= 1: if seenFirstDateDivider and width - dividerIndent >= 3 and showDaybreaks: - self.win.vline(drawLine, dividerIndent, curses.ACS_VLINE | dividerAttr, 1) - self.win.vline(drawLine, width, curses.ACS_VLINE | dividerAttr, 1) + self.addch(drawLine, dividerIndent, curses.ACS_VLINE, dividerAttr) + self.addch(drawLine, width, curses.ACS_VLINE, dividerAttr)
self.addstr(drawLine, cursorLoc, msg, format)
@@ -902,13 +901,9 @@ # if this is the last line and there's room, then draw the bottom of the divider if not deduplicatedLog and seenFirstDateDivider: if lineCount < height and showDaybreaks: - # when resizing with a small width the following entries can be - # problematc (though I'm not sure why) - try: - self.win.vline(lineCount, dividerIndent, curses.ACS_LLCORNER | dividerAttr, 1) - self.win.hline(lineCount, dividerIndent + 1, curses.ACS_HLINE | dividerAttr, width - dividerIndent - 1) - self.win.vline(lineCount, width, curses.ACS_LRCORNER | dividerAttr, 1) - except: pass + self.addch(lineCount, dividerIndent, curses.ACS_LLCORNER, dividerAttr) + self.hline(lineCount, dividerIndent + 1, width - dividerIndent - 1, dividerAttr) + self.addch(lineCount, width, curses.ACS_LRCORNER, dividerAttr)
lineCount += 1
@@ -1019,7 +1014,7 @@ runlevelRanges = [] # tuple of type, startLevel, endLevel for ranges to be consensed
# reverses runlevels and types so they're appended in the right order - reversedRunlevels = list(RUNLEVELS) + reversedRunlevels = log.Runlevel.values() reversedRunlevels.reverse() for prefix in ("TORCTL_", "ARM_", ""): # blank ending runlevel forces the break condition to be reached at the end
Modified: arm/release/src/interface/torrcPanel.py =================================================================== --- arm/release/src/interface/torrcPanel.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/interface/torrcPanel.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -6,14 +6,14 @@ import curses import threading
-from util import conf, panel, torConfig, uiTools +from util import conf, enum, panel, torConfig, uiTools
DEFAULT_CONFIG = {"features.config.file.showScrollbars": True, "features.config.file.maxLinesPerEntry": 8}
# TODO: The armrc use case is incomplete. There should be equivilant reloading # and validation capabilities to the torrc. -TORRC, ARMRC = range(1, 3) # configuration file types that can be displayed +Config = enum.Enum("TORRC", "ARMRC") # configuration file types that can be displayed
class TorrcPanel(panel.Panel): """ @@ -60,7 +60,7 @@
self.valsLock.release()
- def draw(self, subwindow, width, height): + def draw(self, width, height): self.valsLock.acquire()
# If true, we assume that the cached value in self._lastContentHeight is @@ -74,7 +74,7 @@ self.scroll = max(0, min(self.scroll, self._lastContentHeight - height + 1))
renderedContents, corrections, confLocation = None, {}, None - if self.configType == TORRC: + if self.configType == Config.TORRC: loadedTorrc = torConfig.getTorrc() loadedTorrc.getLock().acquire() confLocation = loadedTorrc.getConfigLocation() @@ -109,7 +109,7 @@
# draws the top label if self.showLabel: - sourceLabel = "Tor" if self.configType == TORRC else "Arm" + sourceLabel = "Tor" if self.configType == Config.TORRC else "Arm" locationLabel = " (%s)" % confLocation if confLocation else "" self.addstr(0, 0, "%s Configuration File%s:" % (sourceLabel, locationLabel), curses.A_STANDOUT)
@@ -157,10 +157,10 @@ if lineNumber in corrections: lineIssue, lineIssueMsg = corrections[lineNumber]
- if lineIssue in (torConfig.VAL_DUPLICATE, torConfig.VAL_IS_DEFAULT): + if lineIssue in (torConfig.ValidationError.DUPLICATE, torConfig.ValidationError.IS_DEFAULT): lineComp["option"][1] = curses.A_BOLD | uiTools.getColor("blue") lineComp["argument"][1] = curses.A_BOLD | uiTools.getColor("blue") - elif lineIssue == torConfig.VAL_MISMATCH: + elif lineIssue == torConfig.ValidationError.MISMATCH: lineComp["argument"][1] = curses.A_BOLD | uiTools.getColor("red") lineComp["correction"][0] = " (%s)" % lineIssueMsg else: @@ -189,7 +189,7 @@ msg = uiTools.cropStr(msg, maxMsgSize) else: includeBreak = True - msg, remainder = uiTools.cropStr(msg, maxMsgSize, 4, 4, uiTools.END_WITH_HYPHEN, True) + msg, remainder = uiTools.cropStr(msg, maxMsgSize, 4, 4, uiTools.Ending.HYPHEN, True) displayQueue.insert(0, (remainder.strip(), format))
drawLine = displayLine + lineOffset
Modified: arm/release/src/settings.cfg =================================================================== --- arm/release/src/settings.cfg 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/settings.cfg 2011-04-04 15:22:31 UTC (rev 24555) @@ -1,3 +1,32 @@ +# Important tor configuration options (shown by default) +config.important BandwidthRate +config.important BandwidthBurst +config.important RelayBandwidthRate +config.important RelayBandwidthBurst +config.important ControlPort +config.important HashedControlPassword +config.important CookieAuthentication +config.important DataDirectory +config.important Log +config.important RunAsDaemon +config.important User +config.important Bridge +config.important ExcludeNodes +config.important SocksPort +config.important UseBridges +config.important BridgeRelay +config.important ContactInfo +config.important ExitPolicy +config.important MyFamily +config.important Nickname +config.important ORPort +config.important AccountingMax +config.important AccountingStart +config.important DirPortFrontPage +config.important DirPort +config.important HiddenServiceDir +config.important HiddenServicePort + # Summary descriptions for Tor configuration options # General Config Options config.summary.BandwidthRate Average bandwidth usage limit @@ -145,6 +174,58 @@ config.summary.ExitPortStatistics Toggles storing traffic and port usage data to disk config.summary.ExtraInfoStatistics Publishes statistic data in the extra-info documents
+# Directory Server Options +config.summary.AuthoritativeDirectory act as a directory authority +config.summary.DirPortFrontPage publish this html file on the DirPort +config.summary.V1AuthoritativeDirectory generates a version 1 consensus +config.summary.V2AuthoritativeDirectory generates a version 2 consensus +config.summary.V3AuthoritativeDirectory generates a version 3 consensus +config.summary.VersioningAuthoritativeDirectory provides opinions on recommended versions of tor +config.summary.NamingAuthoritativeDirectory provides opinions on fingerprint to nickname bindings +config.summary.HSAuthoritativeDir toggles accepting hidden service descriptors +config.summary.HidServDirectoryV2 toggles accepting version 2 hidden service descriptors +config.summary.BridgeAuthoritativeDir acts as a bridge authority +config.summary.MinUptimeHidServDirectoryV2 required uptime before accepting hidden service directory +config.summary.DirPort port for directory connections +config.summary.DirListenAddress address the directory service is bound to +config.summary.DirPolicy access policy for the DirPort + +# Directory Authority Server Options +config.summary.RecommendedVersions tor versions believed to be safe +config.summary.RecommendedClientVersions tor versions believed to be safe for clients +config.summary.RecommendedServerVersions tor versions believed to be safe for relays +config.summary.ConsensusParams params entry of the networkstatus vote +config.summary.DirAllowPrivateAddresses toggles allowing arbitrary input or non-public IPs in descriptors +config.summary.AuthDirBadDir relays to be flagged as bad directory caches +config.summary.AuthDirBadExit relays to be flagged as bad exits +config.summary.AuthDirInvalid relays from which the valid flag is withheld +config.summary.AuthDirReject relays to be dropped from the consensus +config.summary.AuthDirListBadDirs toggles if we provide an opinion on bad directory caches +config.summary.AuthDirListBadExits toggles if we provide an opinion on bad exits +config.summary.AuthDirRejectUnlisted rejects further relay descriptors +config.summary.AuthDirMaxServersPerAddr limit on the number of relays accepted per ip +config.summary.AuthDirMaxServersPerAuthAddr limit on the number of relays accepted per an authority's ip +config.summary.V3AuthVotingInterval consensus voting interval +config.summary.V3AuthVoteDelay wait time to collect votes of other authorities +config.summary.V3AuthDistDelay wait time to collect the signatures of other authorities +config.summary.V3AuthNIntervalsValid number of voting intervals a consensus is valid for +config.summary.V3BandwidthsFile path to a file containing measured relay bandwidths + +# Hidden Service Options +config.summary.HiddenServiceDir directory contents for the hidden service +config.summary.HiddenServicePort port the hidden service is provided on +config.summary.PublishHidServDescriptors toggles automated publishing of the hidden service to the rendezvous directory +config.summary.HiddenServiceAuthorizeClient restricts access to the hidden service +config.summary.RendPostPeriod period at which the rendezvous service descriptors are refreshed + +# Testing Network Options +config.summary.TestingTorNetwork overrides other options to be a testing network +config.summary.TestingV3AuthInitialVotingInterval overrides V3AuthVotingInterval for the first consensus +config.summary.TestingV3AuthInitialVoteDelay overrides TestingV3AuthInitialVoteDelay for the first consensus +config.summary.TestingV3AuthInitialDistDelay overrides TestingV3AuthInitialDistDelay for the first consensus +config.summary.TestingAuthDirTimeToLearnReachability delay until opinions are given about which relays are running or not +config.summary.TestingEstimatedDescriptorPropagationTime delay before clients attempt to fetch descriptors from directory caches + # Snippets from common log messages # These are static bits of log messages, used to determine when entries with # dynamic content (hostnames, numbers, etc) are the same. If this matches the @@ -285,3 +366,287 @@ torrc.label.time.day day, days torrc.label.time.week week, weeks
+# Common usages for ports based on: +# https://secure.wikimedia.org/wikipedia/en/wiki/List_of_TCP_and_UDP_port_numb... +# http://isc.sans.edu/services.html +# +# Including all the official low ports (< 1024), and higher ones I recognize. + +port.label.1 TCPMUX +port.label.2 CompressNET +port.label.3 CompressNET +port.label.5 RJE +port.label.7 Echo +port.label.9 Discard +port.label.11 SYSTAT +port.label.13 Daytime +port.label.15 netstat +port.label.17 QOTD +port.label.18 MSP +port.label.19 CHARGEN +port.label.20 FTP +port.label.21 FTP +port.label.22 SSH +port.label.23 Telnet +port.label.24 Priv-mail +port.label.25 SMTP +port.label.34 RF +port.label.35 Printer +port.label.37 TIME +port.label.39 RLP +port.label.41 Graphics +port.label.42 WINS +port.label.43 WHOIS +port.label.47 NI FTP +port.label.49 TACACS +port.label.50 Remote Mail +port.label.51 IMP +port.label.52 XNS +port.label.53 DNS +port.label.54 XNS +port.label.55 ISI-GL +port.label.56 RAP +port.label.57 MTP +port.label.58 XNS +port.label.67 BOOTP +port.label.68 BOOTP +port.label.69 TFTP +port.label.70 Gopher +port.label.79 Finger +port.label.80 HTTP +port.label.81 Torpark +port.label.82 Torpark +port.label.83 MIT ML +port.label.88 Kerberos +port.label.90 dnsix +port.label.99 WIP +port.label.101 NIC +port.label.102 ISO-TSAP +port.label.104 ACR/NEMA +port.label.105 CCSO +port.label.107 Telnet +port.label.108 SNA +port.label.109 POP2 +port.label.110 POP3 +port.label.111 ONC RPC +port.label.113 ident +port.label.115 SFTP +port.label.117 UUCP +port.label.118 SQL +port.label.119 NNTP +port.label.123 NTP +port.label.135 DCE +port.label.137 NetBIOS +port.label.138 NetBIOS +port.label.139 NetBIOS +port.label.143 IMAP +port.label.152 BFTP +port.label.153 SGMP +port.label.156 SQL +port.label.158 DMSP +port.label.161 SNMP +port.label.162 SNMPTRAP +port.label.170 Print-srv +port.label.177 XDMCP +port.label.179 BGP +port.label.194 IRC +port.label.199 SMUX +port.label.201 AppleTalk +port.label.209 QMTP +port.label.210 ANSI +port.label.213 IPX +port.label.218 MPP +port.label.220 IMAP +port.label.256 2DEV +port.label.259 ESRO +port.label.264 BGMP +port.label.308 Novastor +port.label.311 OSX Admin +port.label.318 PKIX TSP +port.label.319 PTP +port.label.320 PTP +port.label.323 IMMP +port.label.350 MATIP +port.label.351 MATIP +port.label.366 ODMR +port.label.369 Rpc2portmap +port.label.370 codaauth2 +port.label.371 ClearCase +port.label.383 HP Alarm Mgr +port.label.384 ARNS +port.label.387 AURP +port.label.389 LDAP +port.label.401 UPS +port.label.402 Altiris +port.label.427 SLP +port.label.443 HTTPS +port.label.444 SNPP +port.label.445 SMB +port.label.464 Kerberos +port.label.465 SMTP +port.label.475 tcpnethaspsrv +port.label.497 Retrospect +port.label.500 ISAKMP +port.label.501 STMF +port.label.502 Modbus +port.label.504 Citadel +port.label.510 FirstClass +port.label.512 Rexec +port.label.513 rlogin +port.label.514 rsh +port.label.515 LPD +port.label.517 Talk +port.label.518 NTalk +port.label.520 efs +port.label.524 NCP +port.label.530 RPC +port.label.531 AIM/IRC +port.label.532 netnews +port.label.533 netwall +port.label.540 UUCP +port.label.542 commerce +port.label.543 klogin +port.label.544 klogin +port.label.545 OSISoft PI +port.label.546 DHCPv6 +port.label.547 DHCPv6 +port.label.548 AFP +port.label.550 new-who +port.label.554 RTSP +port.label.556 RFS +port.label.560 rmonitor +port.label.561 monitor +port.label.563 NNTPS +port.label.587 SMTP +port.label.591 FileMaker +port.label.593 HTTP RPC +port.label.604 TUNNEL +port.label.623 ASF-RMCP +port.label.631 CUPS +port.label.635 RLZ DBase +port.label.636 LDAPS +port.label.639 MSDP +port.label.641 SupportSoft +port.label.646 LDP +port.label.647 DHCP +port.label.648 RRP +port.label.651 IEEE-MMS +port.label.652 DTCP +port.label.653 SupportSoft +port.label.654 MMS/MMP +port.label.657 RMC +port.label.660 OSX Admin +port.label.665 sun-dr +port.label.666 Doom +port.label.674 ACAP +port.label.691 MS Exchange +port.label.692 Hyperwave-ISP +port.label.694 Linux-HA +port.label.695 IEEE-MMS-SSL +port.label.698 OLSR +port.label.699 Access Network +port.label.700 EPP +port.label.701 LMP +port.label.702 IRIS +port.label.706 SILC +port.label.711 MPLS +port.label.712 TBRPF +port.label.720 SMQP +port.label.749 Kerberos +port.label.750 rfile +port.label.751 pump +port.label.752 qrh +port.label.753 rrh +port.label.754 tell send +port.label.760 ns +port.label.782 Conserver +port.label.783 spamd +port.label.829 CMP +port.label.843 Flash +port.label.847 DHCP +port.label.860 iSCSI +port.label.873 rsync +port.label.888 CDDB +port.label.901 SWAT +port.label.902 VMware +port.label.903 VMware +port.label.904 VMware +port.label.911 NCA +port.label.953 DNS RNDC +port.label.981 SofaWare +port.label.989 FTPS +port.label.990 FTPS +port.label.991 NAS +port.label.992 Telnet +port.label.993 IMAPS +port.label.995 POP3S +port.label.999 ScimoreDB +port.label.1001 JtoMB +port.label.1002 cogbot + +port.label.1080 SOCKS +port.label.1085 WebObjects +port.label.1109 KPOP +port.label.1169 Tripwire +port.label.1194 OpenVPN +port.label.1214 Kazaa +port.label.1220 QuickTime +port.label.1234 VLC +port.label.1241 Nessus +port.label.1270 SCOM +port.label.1293 IPSec +port.label.1433 MSSQL +port.label.1434 MSSQL +port.label.1503 MSN +port.label.1512 WINS +port.label.1521 Oracle +port.label.1526 Oracle +port.label.1666 Perforce +port.label.1725 Steam +port.label.1863 MSNP +port.label.2049 NFS +port.label.2086 GNUnet +port.label.2401 CVS +port.label.2525 SMTP +port.label.2710 BitTorrent +port.label.3074 XBox LIVE +port.label.3101 BlackBerry +port.label.3306 MySQL +port.label.3690 SVN +port.label.3723 Battle.net +port.label.3724 WoW +port.label.4662 eMule +port.label.5003 FileMaker +port.label.5050 Yahoo IM +port.label.5060 SIP +port.label.5061 SIP +port.label.5190 AIM/ICQ +port.label.5222 Jabber +port.label.5223 Jabber +port.label.5269 Jabber +port.label.5298 Jabber +port.label.5432 PostgreSQL +port.label.5500 VNC +port.label.5556 Freeciv +port.label.5666 NRPE +port.label.5667 NSCA +port.label.5800 VNC +port.label.5900 VNC +port.label.6346 gnutella +port.label.6347 gnutella +port.label.6660-6669 IRC +port.label.6679 IRC +port.label.6697 IRC +port.label.6881-6999 BitTorrent +port.label.8008 HTTP +port.label.8010 XMPP +port.label.8080 Tomcat +port.label.8118 Privoxy +port.label.8123 Polipo +port.label.9030 Tor +port.label.9050 Tor +port.label.9051 Tor +port.label.23399 Skype +port.label.30301 BitTorrent +port.label.33434 traceroute +
Modified: arm/release/src/starter.py =================================================================== --- arm/release/src/starter.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/starter.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -36,8 +36,9 @@ "startup.interface.port": 9051, "startup.blindModeEnabled": False, "startup.events": "N3", - "data.cache.path": "~/.arm/cache", + "startup.dataDirectory": "~/.arm", "features.config.descriptions.enabled": True, + "features.config.descriptions.persist": True, "log.configDescriptions.readManPageSuccess": util.log.INFO, "log.configDescriptions.readManPageFailed": util.log.NOTICE, "log.configDescriptions.internalLoadSuccess": util.log.NOTICE, @@ -88,29 +89,6 @@ # torrc entries that are scrubbed when dumping PRIVATE_TORRC_ENTRIES = ["HashedControlPassword", "Bridge", "HiddenServiceDir"]
-def isValidIpAddr(ipStr): - """ - Returns true if input is a valid IPv4 address, false otherwise. - """ - - for i in range(4): - if i < 3: - divIndex = ipStr.find(".") - if divIndex == -1: return False # expected a period to be valid - octetStr = ipStr[:divIndex] - ipStr = ipStr[divIndex + 1:] - else: - octetStr = ipStr - - try: - octet = int(octetStr) - if not octet >= 0 or not octet <= 255: return False - except ValueError: - # address value isn't an integer - return False - - return True - def _loadConfigurationDescriptions(pathPrefix): """ Attempts to load descriptions for tor's configuration options, fetching them @@ -125,13 +103,14 @@ isConfigDescriptionsLoaded = False
# determines the path where cached descriptions should be persisted (left - # undefined of arm caching is disabled) - cachePath, descriptorPath = CONFIG["data.cache.path"], None + # undefined if caching is disabled) + descriptorPath = None + if CONFIG["features.config.descriptions.persist"]: + dataDir = CONFIG["startup.dataDirectory"] + if not dataDir.endswith("/"): dataDir += "/" + + descriptorPath = os.path.expanduser(dataDir + "cache/") + CONFIG_DESC_FILENAME
- if cachePath: - if not cachePath.endswith("/"): cachePath += "/" - descriptorPath = os.path.expanduser(cachePath) + CONFIG_DESC_FILENAME - # attempts to load configuration descriptions cached in the data directory if descriptorPath: try: @@ -166,7 +145,7 @@
msg = DESC_SAVE_SUCCESS_MSG % (descriptorPath, time.time() - loadStartTime) util.log.log(CONFIG["log.configDescriptions.persistance.loadSuccess"], msg) - except IOError, exc: + except (IOError, OSError), exc: msg = DESC_SAVE_FAILED_MSG % util.sysTools.getFileErrorMsg(exc) util.log.log(CONFIG["log.configDescriptions.persistance.saveFailed"], msg)
@@ -324,7 +303,7 @@ controlAddr = param["startup.interface.ipAddress"] controlPort = param["startup.interface.port"]
- if not isValidIpAddr(controlAddr): + if not util.connections.isValidIpAddress(controlAddr): print "'%s' isn't a valid IP address" % controlAddr sys.exit() elif controlPort < 0 or controlPort > 65535: @@ -409,5 +388,4 @@ _dumpConfig()
interface.controller.startTorMonitor(time.time() - initTime, expandedEvents, param["startup.blindModeEnabled"]) - conn.close()
Modified: arm/release/src/test.py =================================================================== --- arm/release/src/test.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/test.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -11,6 +11,7 @@ 1. Resolver Performance Test 2. Resolver Dump 3. Glyph Demo + 4. Exit Policy Check q. Quit
Selection: """ @@ -23,7 +24,7 @@ userInput = raw_input(MENU)
# initiate the TorCtl connection if the test needs it - if userInput in ("1", "2") and not conn: + if userInput in ("1", "2", "4") and not conn: conn = torTools.getConn() conn.init()
@@ -43,7 +44,7 @@ connectionResults.sort() allConnectionResults.append(connectionResults)
- resolverLabel = "%-10s" % connections.CMD_STR[resolver] + resolverLabel = "%-10s" % resolver countLabel = "%4i results" % len(connectionResults) timeLabel = "%0.4f seconds" % (time.time() - startTime) print "%s %s %s" % (resolverLabel, countLabel, timeLabel) @@ -67,8 +68,9 @@ # provide the selection options printDivider() print("Select a resolver:") - for i in range(1, 8): - print(" %i. %s" % (i, connections.CMD_STR[i])) + availableResolvers = connections.Resolver.values() + for i in range(len(availableResolvers)): + print(" %i. %s" % (i, availableResolvers[i])) print(" q. Go back to the main menu")
userSelection = raw_input("\nSelection: ") @@ -101,6 +103,42 @@ # Switching to a curses context and back repeatedly seems to screw up the # terminal. Just to be safe this ends the process after the demo. break + elif userInput == "4": + # display the current exit policy and query if destinations are allowed by it + exitPolicy = conn.getExitPolicy() + print("Exit Policy: %s" % exitPolicy) + printDivider() + + while True: + # provide the selection options + userSelection = raw_input("\nCheck if destination is allowed (q to go back): ") + userSelection = userSelection.replace(" ", "").strip() # removes all whitespace + + isValidQuery, isExitAllowed = True, False + if userSelection == "q": + printDivider() + break + elif connections.isValidIpAddress(userSelection): + # just an ip address (use port 80) + isExitAllowed = exitPolicy.check(userSelection, 80) + elif userSelection.isdigit(): + # just a port (use a common ip like 4.2.2.2) + isExitAllowed = exitPolicy.check("4.2.2.2", userSelection) + elif ":" in userSelection: + # ip/port combination + ipAddr, port = userSelection.split(":", 1) + + if connections.isValidIpAddress(ipAddr) and port.isdigit(): + isExitAllowed = exitPolicy.check(ipAddr, port) + else: isValidQuery = False + else: isValidQuery = False # invalid input + + if isValidQuery: + resultStr = "is" if isExitAllowed else "is *not*" + print("Exiting %s allowed to that destination" % resultStr) + else: + print("'%s' isn't a valid destination (should be an ip, port, or ip:port)\n" % userSelection) + else: print("'%s' isn't a valid selection\n" % userInput)
Modified: arm/release/src/util/__init__.py =================================================================== --- arm/release/src/util/__init__.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/util/__init__.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -4,5 +4,5 @@ and safely working with curses (hiding some of the gory details). """
-__all__ = ["conf", "connections", "hostnames", "log", "panel", "procTools", "sysTools", "torConfig", "torTools", "uiTools"] +__all__ = ["conf", "connections", "enum", "hostnames", "log", "panel", "procTools", "sysTools", "torConfig", "torTools", "uiTools"]
Modified: arm/release/src/util/conf.py =================================================================== --- arm/release/src/util/conf.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/util/conf.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -105,36 +105,35 @@ default - value provided if no such key exists """
- callDefault = log.runlevelToStr(default) if key.startswith("log.") else default isMultivalue = isinstance(default, list) or isinstance(default, dict) - val = self.getValue(key, callDefault, isMultivalue) + val = self.getValue(key, default, isMultivalue) if val == default: return val
if key.startswith("log."): - if val.lower() in ("none", "debug", "info", "notice", "warn", "err"): - val = log.strToRunlevel(val) + if val.upper() == "NONE": val = None + elif val.upper() in log.Runlevel.values(): val = val.upper() else: - msg = "config entry '%s' is expected to be a runlevel" % key - if default != None: msg += ", defaulting to '%s'" % callDefault + msg = "Config entry '%s' is expected to be a runlevel" % key + if default != None: msg += ", defaulting to '%s'" % default log.log(CONFIG["log.configEntryTypeError"], msg) val = default elif isinstance(default, bool): if val.lower() == "true": val = True elif val.lower() == "false": val = False else: - msg = "config entry '%s' is expected to be a boolean, defaulting to '%s'" % (key, str(default)) + msg = "Config entry '%s' is expected to be a boolean, defaulting to '%s'" % (key, str(default)) log.log(CONFIG["log.configEntryTypeError"], msg) val = default elif isinstance(default, int): try: val = int(val) except ValueError: - msg = "config entry '%s' is expected to be an integer, defaulting to '%i'" % (key, default) + msg = "Config entry '%s' is expected to be an integer, defaulting to '%i'" % (key, default) log.log(CONFIG["log.configEntryTypeError"], msg) val = default elif isinstance(default, float): try: val = float(val) except ValueError: - msg = "config entry '%s' is expected to be a float, defaulting to '%f'" % (key, default) + msg = "Config entry '%s' is expected to be a float, defaulting to '%f'" % (key, default) log.log(CONFIG["log.configEntryTypeError"], msg) val = default elif isinstance(default, list): @@ -146,7 +145,7 @@ entryKey, entryVal = entry.split("=>", 1) valMap[entryKey.strip()] = entryVal.strip() else: - msg = "ignoring invalid %s config entry (expected a mapping, but "%s" was missing "=>")" % (key, entry) + msg = "Ignoring invalid %s config entry (expected a mapping, but "%s" was missing "=>")" % (key, entry) log.log(CONFIG["log.configEntryTypeError"], msg) val = valMap
@@ -171,7 +170,7 @@
# check if the count doesn't match if count != None and len(confComp) != count: - msg = "config entry '%s' is expected to be %i comma separated values" % (key, count) + msg = "Config entry '%s' is expected to be %i comma separated values" % (key, count) if default != None and (isinstance(default, list) or isinstance(default, tuple)): defaultStr = ", ".join([str(i) for i in default]) msg += ", defaulting to '%s'" % defaultStr @@ -201,7 +200,7 @@
# validates the input, setting the errorMsg if there's a problem errorMsg = None - baseErrorMsg = "config entry '%s' is expected to %%s" % key + baseErrorMsg = "Config entry '%s' is expected to %%s" % key if default != None and (isinstance(default, list) or isinstance(default, tuple)): defaultStr = ", ".join([str(i) for i in default]) baseErrorMsg += ", defaulting to '%s'" % defaultStr
Modified: arm/release/src/util/connections.py =================================================================== --- arm/release/src/util/connections.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/util/connections.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -21,17 +21,16 @@ import time import threading
-from util import log, procTools, sysTools +from util import enum, log, procTools, sysTools
# enums for connection resolution utilities -CMD_PROC, CMD_NETSTAT, CMD_SOCKSTAT, CMD_LSOF, CMD_SS, CMD_BSD_SOCKSTAT, CMD_BSD_PROCSTAT = range(1, 8) -CMD_STR = {CMD_PROC: "proc", - CMD_NETSTAT: "netstat", - CMD_SS: "ss", - CMD_LSOF: "lsof", - CMD_SOCKSTAT: "sockstat", - CMD_BSD_SOCKSTAT: "sockstat (bsd)", - CMD_BSD_PROCSTAT: "procstat (bsd)"} +Resolver = enum.Enum(("PROC", "proc"), + ("NETSTAT", "netstat"), + ("SS", "ss"), + ("LSOF", "lsof"), + ("SOCKSTAT", "sockstat"), + ("BSD_SOCKSTAT", "sockstat (bsd)"), + ("BSD_PROCSTAT", "procstat (bsd)"))
# If true this provides new instantiations for resolvers if the old one has # been stopped. This can make it difficult ensure all threads are terminated @@ -80,11 +79,107 @@ "log.connLookupFailed": log.INFO, "log.connLookupFailover": log.NOTICE, "log.connLookupAbandon": log.WARN, - "log.connLookupRateGrowing": None} + "log.connLookupRateGrowing": None, + "log.configEntryTypeError": log.NOTICE}
+PORT_USAGE = {} + def loadConfig(config): config.update(CONFIG) + + for configKey in config.getKeys(): + # fetches any port.label.* values + if configKey.startswith("port.label."): + portEntry = configKey[11:] + purpose = config.get(configKey) + + divIndex = portEntry.find("-") + if divIndex == -1: + # single port + if portEntry.isdigit(): + PORT_USAGE[portEntry] = purpose + else: + msg = "Port value isn't numeric for entry: %s" % configKey + log.log(CONFIG["log.configEntryTypeError"], msg) + else: + try: + # range of ports (inclusive) + minPort = int(portEntry[:divIndex]) + maxPort = int(portEntry[divIndex + 1:]) + if minPort > maxPort: raise ValueError() + + for port in range(minPort, maxPort + 1): + PORT_USAGE[str(port)] = purpose + except ValueError: + msg = "Unable to parse port range for entry: %s" % configKey + log.log(CONFIG["log.configEntryTypeError"], msg)
+def isValidIpAddress(ipStr): + """ + Returns true if input is a valid IPv4 address, false otherwise. + """ + + # checks if theres four period separated values + if not ipStr.count(".") == 3: return False + + # checks that each value in the octet are decimal values between 0-255 + for ipComp in ipStr.split("."): + if not ipComp.isdigit() or int(ipComp) < 0 or int(ipComp) > 255: + return False + + return True + +def isIpAddressPrivate(ipAddr): + """ + Provides true if the IP address belongs on the local network or belongs to + loopback, false otherwise. These include: + Private ranges: 10.*, 172.16.* - 172.31.*, 192.168.* + Loopback: 127.* + + Arguments: + ipAddr - IP address to be checked + """ + + # checks for any of the simple wildcard ranges + if ipAddr.startswith("10.") or ipAddr.startswith("192.168.") or ipAddr.startswith("127."): + return True + + # checks for the 172.16.* - 172.31.* range + if ipAddr.startswith("172.") and ipAddr.count(".") == 3: + secondOctet = ipAddr[4:ipAddr.find(".", 4)] + + if secondOctet.isdigit() and int(secondOctet) >= 16 and int(secondOctet) <= 31: + return True + + 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 + provides None. + + Arguments: + port - port number to look up + """ + + return PORT_USAGE.get(port) + def getResolverCommand(resolutionCmd, processName, processPid = ""): """ Provides the command that would be processed for the given resolver type. @@ -99,19 +194,19 @@
if not processPid: # the pid is required for procstat resolution - if resolutionCmd == CMD_BSD_PROCSTAT: + if resolutionCmd == Resolver.BSD_PROCSTAT: raise ValueError("procstat resolution requires a pid")
# if the pid was undefined then match any in that field processPid = "[0-9]*"
- if resolutionCmd == CMD_PROC: return "" - elif resolutionCmd == CMD_NETSTAT: return RUN_NETSTAT % (processPid, processName) - elif resolutionCmd == CMD_SS: return RUN_SS % (processName, processPid) - elif resolutionCmd == CMD_LSOF: return RUN_LSOF % (processName, processPid) - elif resolutionCmd == CMD_SOCKSTAT: return RUN_SOCKSTAT % (processName, processPid) - elif resolutionCmd == CMD_BSD_SOCKSTAT: return RUN_BSD_SOCKSTAT % (processName, processPid) - elif resolutionCmd == CMD_BSD_PROCSTAT: return RUN_BSD_PROCSTAT % processPid + if resolutionCmd == Resolver.PROC: return "" + elif resolutionCmd == Resolver.NETSTAT: return RUN_NETSTAT % (processPid, processName) + elif resolutionCmd == Resolver.SS: return RUN_SS % (processName, processPid) + elif resolutionCmd == Resolver.LSOF: return RUN_LSOF % (processName, processPid) + elif resolutionCmd == Resolver.SOCKSTAT: return RUN_SOCKSTAT % (processName, processPid) + elif resolutionCmd == Resolver.BSD_SOCKSTAT: return RUN_BSD_SOCKSTAT % (processName, processPid) + elif resolutionCmd == Resolver.BSD_PROCSTAT: return RUN_BSD_PROCSTAT % processPid else: raise ValueError("Unrecognized resolution type: %s" % resolutionCmd)
def getConnections(resolutionCmd, processName, processPid = ""): @@ -131,7 +226,7 @@ processPid - process ID (this helps improve accuracy) """
- if resolutionCmd == CMD_PROC: + if resolutionCmd == Resolver.PROC: # Attempts resolution via checking the proc contents. if not processPid: raise ValueError("proc resolution requires a pid") @@ -151,30 +246,30 @@ # parses results for the resolution command conn = [] for line in results: - if resolutionCmd == CMD_LSOF: + if resolutionCmd == Resolver.LSOF: # Different versions of lsof have different numbers of columns, so # stripping off the optional 'established' entry so we can just use # the last one. comp = line.replace("(ESTABLISHED)", "").strip().split() else: comp = line.split()
- if resolutionCmd == CMD_NETSTAT: + if resolutionCmd == Resolver.NETSTAT: localIp, localPort = comp[3].split(":") foreignIp, foreignPort = comp[4].split(":") - elif resolutionCmd == CMD_SS: + elif resolutionCmd == Resolver.SS: localIp, localPort = comp[4].split(":") foreignIp, foreignPort = comp[5].split(":") - elif resolutionCmd == CMD_LSOF: + elif resolutionCmd == Resolver.LSOF: local, foreign = comp[-1].split("->") localIp, localPort = local.split(":") foreignIp, foreignPort = foreign.split(":") - elif resolutionCmd == CMD_SOCKSTAT: + elif resolutionCmd == Resolver.SOCKSTAT: localIp, localPort = comp[4].split(":") foreignIp, foreignPort = comp[5].split(":") - elif resolutionCmd == CMD_BSD_SOCKSTAT: + elif resolutionCmd == Resolver.BSD_SOCKSTAT: localIp, localPort = comp[5].split(":") foreignIp, foreignPort = comp[6].split(":") - elif resolutionCmd == CMD_BSD_PROCSTAT: + elif resolutionCmd == Resolver.BSD_PROCSTAT: localIp, localPort = comp[9].split(":") foreignIp, foreignPort = comp[10].split(":")
@@ -241,13 +336,13 @@ if osType == None: osType = os.uname()[0]
if osType == "FreeBSD": - resolvers = [CMD_BSD_SOCKSTAT, CMD_BSD_PROCSTAT, CMD_LSOF] + resolvers = [Resolver.BSD_SOCKSTAT, Resolver.BSD_PROCSTAT, Resolver.LSOF] else: - resolvers = [CMD_NETSTAT, CMD_SOCKSTAT, CMD_LSOF, CMD_SS] + resolvers = [Resolver.NETSTAT, Resolver.SOCKSTAT, Resolver.LSOF, Resolver.SS]
# proc resolution, by far, outperforms the others so defaults to this is able if procTools.isProcAvailable(): - resolvers = [CMD_PROC] + resolvers + resolvers = [Resolver.PROC] + resolvers
return resolvers
@@ -317,22 +412,26 @@ self.defaultRate = CONFIG["queries.connections.minRate"] self.lastLookup = -1 self.overwriteResolver = None - self.defaultResolver = CMD_PROC + self.defaultResolver = Resolver.PROC
osType = os.uname()[0] self.resolverOptions = getSystemResolvers(osType)
- resolverLabels = ", ".join([CMD_STR[option] for option in self.resolverOptions]) - log.log(CONFIG["log.connResolverOptions"], "Operating System: %s, Connection Resolvers: %s" % (osType, resolverLabels)) + log.log(CONFIG["log.connResolverOptions"], "Operating System: %s, Connection Resolvers: %s" % (osType, ", ".join(self.resolverOptions)))
# sets the default resolver to be the first found in the system's PATH # (left as netstat if none are found) for resolver in self.resolverOptions: - if resolver == CMD_PROC or sysTools.isAvailable(CMD_STR[resolver]): + # Resolver strings correspond to their command with the exception of bsd + # resolvers. + resolverCmd = resolver.replace(" (bsd)", "") + + if resolver == Resolver.PROC or sysTools.isAvailable(resolverCmd): self.defaultResolver = resolver break
self._connections = [] # connection cache (latest results) + self._resolutionCounter = 0 # number of successful connection resolutions self._isPaused = False self._halt = False # terminates thread if true self._cond = threading.Condition() # used for pausing the thread @@ -371,6 +470,7 @@ lookupTime = time.time() - resolveStart
self._connections = connResults + self._resolutionCounter += 1
newMinDefaultRate = 100 * lookupTime if self.defaultRate < newMinDefaultRate: @@ -409,7 +509,7 @@
if newResolver: # provide notice that failures have occurred and resolver is changing - msg = RESOLVER_SERIAL_FAILURE_MSG % (CMD_STR[resolver], CMD_STR[newResolver]) + msg = RESOLVER_SERIAL_FAILURE_MSG % (resolver, newResolver) log.log(CONFIG["log.connLookupFailover"], msg) else: # exhausted all resolvers, give warning @@ -428,6 +528,14 @@ if self._halt: return [] else: return list(self._connections)
+ def getResolutionCount(self): + """ + Provides the number of successful resolutions so far. This can be used to + determine if the connection results are new for the caller or not. + """ + + return self._resolutionCounter + def setPaused(self, isPause): """ Allows or prevents further connection resolutions (this still makes use of @@ -451,3 +559,168 @@ self._cond.notifyAll() self._cond.release()
+class AppResolver: + """ + Provides the names and pids of appliations attached to the given ports. This + stops attempting to query if it fails three times without successfully + getting lsof results. + """ + + def __init__(self, scriptName = "python"): + """ + Constructs a resolver instance. + + Arguments: + scriptName - name by which to all our own entries + """ + + self.scriptName = scriptName + self.queryResults = {} + self.resultsLock = threading.RLock() + self._cond = threading.Condition() # used for pausing when waiting for results + self.isResolving = False # flag set if we're in the process of making a query + self.failureCount = 0 # -1 if we've made a successful query + + def getResults(self, maxWait=0): + """ + Provides the last queried results. If we're in the process of making a + query then we can optionally block for a time to see if it finishes. + + Arguments: + maxWait - maximum second duration to block on getting results before + returning + """ + + self._cond.acquire() + if self.isResolving and maxWait > 0: + self._cond.wait(maxWait) + self._cond.release() + + self.resultsLock.acquire() + results = dict(self.queryResults) + self.resultsLock.release() + + return results + + def resolve(self, ports): + """ + Queues the given listing of ports to be resolved. This clears the last set + of results when completed. + + Arguments: + ports - list of ports to be resolved to applications + """ + + if self.failureCount < 3: + self.isResolving = True + t = threading.Thread(target = self._queryApplications, kwargs = {"ports": ports}) + t.setDaemon(True) + t.start() + + def _queryApplications(self, ports=[]): + """ + Performs an lsof lookup on the given ports to get the command/pid tuples. + + Arguments: + ports - list of ports to be resolved to applications + """ + + # atagar@fenrir:~/Desktop/arm$ lsof -i tcp:51849 -i tcp:37277 + # COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME + # tor 2001 atagar 14u IPv4 14048 0t0 TCP localhost:9051->localhost:37277 (ESTABLISHED) + # tor 2001 atagar 15u IPv4 22024 0t0 TCP localhost:9051->localhost:51849 (ESTABLISHED) + # python 2462 atagar 3u IPv4 14047 0t0 TCP localhost:37277->localhost:9051 (ESTABLISHED) + # python 3444 atagar 3u IPv4 22023 0t0 TCP localhost:51849->localhost:9051 (ESTABLISHED) + + if not ports: + self.resultsLock.acquire() + self.queryResults = {} + self.isResolving = False + self.resultsLock.release() + + # wakes threads waiting on results + self._cond.acquire() + self._cond.notifyAll() + self._cond.release() + + return + + results = {} + lsofArgs = [] + + # Uses results from the last query if we have any, otherwise appends the + # port to the lsof command. This has the potential for persisting dirty + # results but if we're querying by the dynamic port on the local tcp + # connections then this should be very rare (and definitely worth the + # chance of being able to skip an lsof query altogether). + for port in ports: + if port in self.queryResults: + results[port] = self.queryResults[port] + else: lsofArgs.append("-i tcp:%s" % port) + + if lsofArgs: + lsofResults = sysTools.call("lsof -nP " + " ".join(lsofArgs)) + else: lsofResults = None + + if not lsofResults and self.failureCount != -1: + # lsof query failed and we aren't yet sure if it's possible to + # successfully get results on this platform + self.failureCount += 1 + self.isResolving = False + return + elif lsofResults: + # (iPort, oPort) tuple for our own process, if it was fetched + ourConnection = None + + for line in lsofResults: + lineComp = line.split() + + if len(lineComp) == 10 and lineComp[9] == "(ESTABLISHED)": + cmd, pid, _, _, _, _, _, _, portMap, _ = lineComp + + if "->" in portMap: + iPort, oPort = portMap.split("->") + iPort = iPort.split(":")[1] + oPort = oPort.split(":")[1] + + # entry belongs to our own process + if pid == str(os.getpid()): + cmd = self.scriptName + ourConnection = (iPort, oPort) + + if iPort.isdigit() and oPort.isdigit(): + newEntry = (iPort, oPort, cmd, pid) + + # adds the entry under the key of whatever we queried it with + # (this might be both the inbound _and_ outbound ports) + for portMatch in (iPort, oPort): + if portMatch in ports: + if portMatch in results: + results[portMatch].append(newEntry) + else: results[portMatch] = [newEntry] + + # making the lsof call generated an extraneous sh entry for our own connection + if ourConnection: + for ourPort in ourConnection: + if ourPort in results: + shIndex = None + + for i in range(len(results[ourPort])): + if results[ourPort][i][2] == "sh": + shIndex = i + break + + if shIndex != None: + del results[ourPort][shIndex] + + self.resultsLock.acquire() + self.failureCount = -1 + self.queryResults = results + self.isResolving = False + self.resultsLock.release() + + # wakes threads waiting on results + self._cond.acquire() + self._cond.notifyAll() + self._cond.release() +
Copied: arm/release/src/util/enum.py (from rev 24554, arm/trunk/src/util/enum.py) =================================================================== --- arm/release/src/util/enum.py (rev 0) +++ arm/release/src/util/enum.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -0,0 +1,116 @@ +""" +Basic enumeration, providing ordered types for collections. These can be +constructed as simple type listings, ie: +>>> insects = Enum("ANT", "WASP", "LADYBUG", "FIREFLY") +>>> insects.ANT +'Ant' +>>> insects.values() +['Ant', 'Wasp', 'Ladybug', 'Firefly'] + +with overwritten string counterparts: +>>> pets = Enum(("DOG", "Skippy"), "CAT", ("FISH", "Nemo")) +>>> pets.DOG +'Skippy' +>>> pets.CAT +'Cat' + +or with entirely custom string components as an unordered enum with: +>>> pets = LEnum(DOG="Skippy", CAT="Kitty", FISH="Nemo") +>>> pets.CAT +'Kitty' +""" + +def toCamelCase(label): + """ + Converts the given string to camel case, ie: + >>> toCamelCase("I_LIKE_PEPPERJACK!") + 'I Like Pepperjack!' + + Arguments: + label - input string to be converted + """ + + words = [] + for entry in label.split("_"): + if len(entry) == 0: words.append("") + elif len(entry) == 1: words.append(entry.upper()) + else: words.append(entry[0].upper() + entry[1:].lower()) + + return " ".join(words) + +class Enum: + """ + Basic enumeration. + """ + + def __init__(self, *args): + self.orderedValues = [] + + for entry in args: + if isinstance(entry, str): + key, val = entry, toCamelCase(entry) + elif isinstance(entry, tuple) and len(entry) == 2: + key, val = entry + else: raise ValueError("Unrecognized input: %s" % args) + + self.__dict__[key] = val + self.orderedValues.append(val) + + def values(self): + """ + Provides an ordered listing of the enumerations in this set. + """ + + return list(self.orderedValues) + + def indexOf(self, value): + """ + Provides the index of the given value in the collection. This raises a + ValueError if no such element exists. + + Arguments: + value - entry to be looked up + """ + + return self.orderedValues.index(value) + + def next(self, value): + """ + Provides the next enumeration after the given value, raising a ValueError + if no such enum exists. + + Arguments: + value - enumeration for which to get the next entry + """ + + if not value in self.orderedValues: + raise ValueError("No such enumeration exists: %s (options: %s)" % (value, ", ".join(self.orderedValues))) + + nextIndex = (self.orderedValues.index(value) + 1) % len(self.orderedValues) + return self.orderedValues[nextIndex] + + def previous(self, value): + """ + Provides the previous enumeration before the given value, raising a + ValueError if no such enum exists. + + Arguments: + value - enumeration for which to get the previous entry + """ + + if not value in self.orderedValues: + raise ValueError("No such enumeration exists: %s (options: %s)" % (value, ", ".join(self.orderedValues))) + + prevIndex = (self.orderedValues.index(value) - 1) % len(self.orderedValues) + return self.orderedValues[prevIndex] + +class LEnum(Enum): + """ + Enumeration that accepts custom string mappings. + """ + + def __init__(self, **args): + Enum.__init__(self) + self.__dict__.update(args) + self.orderedValues = sorted(args.values()) +
Modified: arm/release/src/util/log.py =================================================================== --- arm/release/src/util/log.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/util/log.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -11,19 +11,23 @@ from sys import maxint from threading import RLock
-# logging runlevels -DEBUG, INFO, NOTICE, WARN, ERR = range(1, 6) -RUNLEVEL_STR = {DEBUG: "DEBUG", INFO: "INFO", NOTICE: "NOTICE", WARN: "WARN", ERR: "ERR"} +from util import enum
+# Logging runlevels. These are *very* commonly used so including shorter +# aliases (so they can be referenced as log.DEBUG, log.WARN, etc). +Runlevel = enum.Enum(("DEBUG", "DEBUG"), ("INFO", "INFO"), ("NOTICE", "NOTICE"), + ("WARN", "WARN"), ("ERR", "ERR")) +DEBUG, INFO, NOTICE, WARN, ERR = Runlevel.values() + # provides thread safety for logging operations LOG_LOCK = RLock()
# chronologically ordered records of events for each runlevel, stored as tuples # consisting of: (time, message) -_backlog = dict([(level, []) for level in range(1, 6)]) +_backlog = dict([(level, []) for level in Runlevel.values()])
# mapping of runlevels to the listeners interested in receiving events from it -_listeners = dict([(level, []) for level in range(1, 6)]) +_listeners = dict([(level, []) for level in Runlevel.values()])
CONFIG = {"cache.armLog.size": 1000, "cache.armLog.trimSize": 200} @@ -55,36 +59,6 @@
DUMP_FILE = open(logPath, "w")
-def strToRunlevel(runlevelStr): - """ - Converts runlevel strings ("DEBUG", "INFO", "NOTICE", etc) to their - corresponding enumeations. This isn't case sensitive and provides None if - unrecognized. - - Arguments: - runlevelStr - string to be converted to runlevel - """ - - if not runlevelStr: return None - - runlevelStr = runlevelStr.upper() - for enum, level in RUNLEVEL_STR.items(): - if level == runlevelStr: return enum - - return None - -def runlevelToStr(runlevelEnum): - """ - Converts runlevel enumerations to corresponding string. If unrecognized then - this provides "NONE". - - Arguments: - runlevelEnum - enumeration to be converted to string - """ - - if runlevelEnum in RUNLEVEL_STR: return RUNLEVEL_STR[runlevelEnum] - else: return "NONE" - def log(level, msg, eventTime = None): """ Registers an event, directing it to interested listeners and preserving it in @@ -128,7 +102,7 @@ try: entryTime = time.localtime(eventTime) timeLabel = "%i/%i/%i %02i:%02i:%02i" % (entryTime[1], entryTime[2], entryTime[0], entryTime[3], entryTime[4], entryTime[5]) - logEntry = "%s [%s] %s\n" % (timeLabel, runlevelToStr(level), msg) + logEntry = "%s [%s] %s\n" % (timeLabel, level, msg) DUMP_FILE.write(logEntry) DUMP_FILE.flush() except IOError, exc: @@ -137,7 +111,7 @@
# notifies listeners for callback in _listeners[level]: - callback(RUNLEVEL_STR[level], msg, eventTime) + callback(level, msg, eventTime) finally: LOG_LOCK.release()
@@ -175,7 +149,7 @@
if dumpBacklog: for level, msg, eventTime in _getEntries(levels): - callback(RUNLEVEL_STR[level], msg, eventTime) + callback(level, msg, eventTime) finally: LOG_LOCK.release()
Modified: arm/release/src/util/panel.py =================================================================== --- arm/release/src/util/panel.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/util/panel.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -56,8 +56,9 @@ # implementations aren't entirely deterministic (for instance panels # might chose their height based on its parent's current width).
+ self.panelName = name self.parent = parent - self.panelName = name + self.visible = True self.top = top self.height = height self.width = width @@ -99,6 +100,23 @@ self.parent = parent self.win = None
+ def isVisible(self): + """ + Provides if the panel's configured to be visible or not. + """ + + return self.visible + + def setVisible(self, isVisible): + """ + Toggles if the panel is visible or not. + + Arguments: + isVisible - panel is redrawn when requested if true, skipped otherwise + """ + + self.visible = isVisible + def getTop(self): """ Provides the position subwindows are placed at within its parent. @@ -170,7 +188,7 @@ if setWidth != -1: newWidth = min(newWidth, setWidth) return (newHeight, newWidth)
- def draw(self, subwindow, width, height): + def draw(self, width, height): """ Draws display's content. This is meant to be overwritten by implementations and not called directly (use redraw() instead). The @@ -178,10 +196,8 @@ a column less than the actual space.
Arguments: - sudwindow - panel's current subwindow instance, providing raw access to - its curses functions - width - horizontal space available for content - height - vertical space available for content + width - horizontal space available for content + height - vertical space available for content """
pass @@ -198,6 +214,9 @@ abandoned """
+ # skipped if not currently visible + if not self.isVisible(): return + # if the panel's completely outside its parent then this is a no-op newHeight, newWidth = self.getPreferredSize() if newHeight == 0: @@ -222,11 +241,70 @@ try: if forceRedraw: self.win.erase() # clears any old contents - self.draw(self.win, self.maxX - 1, self.maxY) + self.draw(self.maxX - 1, self.maxY) self.win.refresh() finally: CURSES_LOCK.release()
+ def hline(self, y, x, length, attr=curses.A_NORMAL): + """ + Draws a horizontal line. This should only be called from the context of a + panel's draw method. + + Arguments: + y - vertical location + x - horizontal location + length - length the line spans + attr - text attributes + """ + + if self.win and self.maxX > x and self.maxY > y: + try: + drawLength = min(length, self.maxX - x) + self.win.hline(y, x, curses.ACS_HLINE | attr, drawLength) + except: + # in edge cases drawing could cause a _curses.error + pass + + def vline(self, y, x, length, attr=curses.A_NORMAL): + """ + Draws a vertical line. This should only be called from the context of a + panel's draw method. + + Arguments: + y - vertical location + x - horizontal location + length - length the line spans + attr - text attributes + """ + + if self.win and self.maxX > x and self.maxY > y: + try: + drawLength = min(length, self.maxY - y) + self.win.vline(y, x, curses.ACS_VLINE | attr, drawLength) + except: + # in edge cases drawing could cause a _curses.error + pass + + def addch(self, y, x, char, attr=curses.A_NORMAL): + """ + Draws a single character. This should only be called from the context of a + panel's draw method. + + Arguments: + y - vertical location + x - horizontal location + char - character to be drawn + attr - text attributes + """ + + if self.win and self.maxX > x and self.maxY > y: + try: + self.win.addch(y, x, char, attr) + except: + # in edge cases drawing could cause a _curses.error + pass + def addstr(self, y, x, msg, attr=curses.A_NORMAL): """ Writes string to subwindow if able. This takes into account screen bounds
Modified: arm/release/src/util/procTools.py =================================================================== --- arm/release/src/util/procTools.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/util/procTools.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -20,12 +20,12 @@ import socket import base64
-from util import log +from util import enum, log
# cached system values SYS_START_TIME, SYS_PHYSICAL_MEMORY = None, None CLOCK_TICKS = os.sysconf(os.sysconf_names["SC_CLK_TCK"]) -STAT_COMMAND, STAT_CPU_UTIME, STAT_CPU_STIME, STAT_START_TIME = range(4) +Stat = enum.Enum("COMMAND", "CPU_UTIME", "CPU_STIME", "START_TIME")
CONFIG = {"queries.useProc": True, "log.procCallMade": log.DEBUG} @@ -128,10 +128,10 @@ def getStats(pid, *statTypes): """ Provides process specific information. Options are: - STAT_COMMAND command name under which the process is running - STAT_CPU_UTIME total user time spent on the process - STAT_CPU_STIME total system time spent on the process - STAT_START_TIME when this process began, in unix time + Stat.COMMAND command name under which the process is running + Stat.CPU_UTIME total user time spent on the process + Stat.CPU_STIME total system time spent on the process + Stat.START_TIME when this process began, in unix time
Arguments: pid - queried process @@ -159,19 +159,19 @@
results, queriedStats = [], [] for statType in statTypes: - if statType == STAT_COMMAND: + if statType == Stat.COMMAND: queriedStats.append("command") if pid == 0: results.append("sched") else: results.append(statComp[1]) - elif statType == STAT_CPU_UTIME: + elif statType == Stat.CPU_UTIME: queriedStats.append("utime") if pid == 0: results.append("0") else: results.append(str(float(statComp[13]) / CLOCK_TICKS)) - elif statType == STAT_CPU_STIME: + elif statType == Stat.CPU_STIME: queriedStats.append("stime") if pid == 0: results.append("0") else: results.append(str(float(statComp[14]) / CLOCK_TICKS)) - elif statType == STAT_START_TIME: + elif statType == Stat.START_TIME: queriedStats.append("start time") if pid == 0: return getSystemStartTime() else:
Modified: arm/release/src/util/sysTools.py =================================================================== --- arm/release/src/util/sysTools.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/util/sysTools.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -126,7 +126,7 @@ # fetch it from proc contents if available if procTools.isProcAvailable(): try: - processName = procTools.getStats(pid, procTools.STAT_COMMAND)[0] + processName = procTools.getStats(pid, procTools.Stat.COMMAND)[0] except IOError, exc: raisedExc = exc
@@ -466,7 +466,7 @@ newValues = {} try: if self._useProc: - utime, stime, startTime = procTools.getStats(self.processPid, procTools.STAT_CPU_UTIME, procTools.STAT_CPU_STIME, procTools.STAT_START_TIME) + utime, stime, startTime = procTools.getStats(self.processPid, procTools.Stat.CPU_UTIME, procTools.Stat.CPU_STIME, procTools.Stat.START_TIME) totalCpuTime = float(utime) + float(stime) cpuDelta = totalCpuTime - self._lastCpuTotal newValues["cpuSampling"] = cpuDelta / timeSinceReset
Modified: arm/release/src/util/torConfig.py =================================================================== --- arm/release/src/util/torConfig.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/util/torConfig.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -5,9 +5,10 @@ import os import threading
-from util import log, sysTools, torTools, uiTools +from util import enum, log, sysTools, torTools, uiTools
CONFIG = {"features.torrc.validate": True, + "config.important": [], "torrc.alias": {}, "torrc.label.size.b": [], "torrc.label.size.kb": [], @@ -22,27 +23,23 @@ "log.configDescriptions.unrecognizedCategory": log.NOTICE}
# enums and values for numeric torrc entries -UNRECOGNIZED, SIZE_VALUE, TIME_VALUE = range(1, 4) +ValueType = enum.Enum("UNRECOGNIZED", "SIZE", "TIME") SIZE_MULT = {"b": 1, "kb": 1024, "mb": 1048576, "gb": 1073741824, "tb": 1099511627776} TIME_MULT = {"sec": 1, "min": 60, "hour": 3600, "day": 86400, "week": 604800}
# enums for issues found during torrc validation: -# VAL_DUPLICATE - entry is ignored due to being a duplicate -# VAL_MISMATCH - the value doesn't match tor's current state -# VAL_MISSING - value differs from its default but is missing from the torrc -# VAL_IS_DEFAULT - the configuration option matches tor's default -VAL_DUPLICATE, VAL_MISMATCH, VAL_MISSING, VAL_IS_DEFAULT = range(1, 5) +# DUPLICATE - entry is ignored due to being a duplicate +# MISMATCH - the value doesn't match tor's current state +# MISSING - value differs from its default but is missing from the torrc +# IS_DEFAULT - the configuration option matches tor's default +ValidationError = enum.Enum("DUPLICATE", "MISMATCH", "MISSING", "IS_DEFAULT")
# descriptions of tor's configuration options fetched from its man page CONFIG_DESCRIPTIONS_LOCK = threading.RLock() CONFIG_DESCRIPTIONS = {}
# categories for tor configuration options -GENERAL, CLIENT, SERVER, DIRECTORY, AUTHORITY, HIDDEN_SERVICE, TESTING, UNKNOWN = range(1, 9) -OPTION_CATEGORY_STR = {GENERAL: "General", CLIENT: "Client", - SERVER: "Relay", DIRECTORY: "Directory", - AUTHORITY: "Authority", HIDDEN_SERVICE: "Hidden Service", - TESTING: "Testing", UNKNOWN: "Unknown"} +Category = enum.Enum("GENERAL", "CLIENT", "RELAY", "DIRECTORY", "AUTHORITY", "HIDDEN_SERVICE", "TESTING", "UNKNOWN")
TORRC = None # singleton torrc instance MAN_OPT_INDENT = 7 # indentation before options in the man page @@ -51,10 +48,13 @@ MULTILINE_PARAM = None # cached multiline parameters (lazily loaded)
def loadConfig(config): - CONFIG["torrc.alias"] = config.get("torrc.alias", {}) + config.update(CONFIG)
- # fetches any config.summary.* values + # stores lowercase entries to drop case sensitivity + CONFIG["config.important"] = [entry.lower() for entry in CONFIG["config.important"]] + for configKey in config.getKeys(): + # fetches any config.summary.* values if configKey.startswith("config.summary."): CONFIG[configKey.lower()] = config.get(configKey)
@@ -119,9 +119,6 @@ inputFileContents = inputFile.readlines() inputFile.close()
- # constructs a reverse mapping for categories - strToCat = dict([(OPTION_CATEGORY_STR[cat], cat) for cat in OPTION_CATEGORY_STR]) - try: versionLine = inputFileContents.pop(0).rstrip()
@@ -138,10 +135,8 @@
while inputFileContents: # gets category enum, failing if it doesn't exist - categoryStr = inputFileContents.pop(0).rstrip() - if categoryStr in strToCat: - category = strToCat[categoryStr] - else: + category = inputFileContents.pop(0).rstrip() + if not category in Category.values(): baseMsg = "invalid category in input file: '%s'" raise IOError(baseMsg % categoryStr)
@@ -183,7 +178,7 @@ validOptions = [line[:line.find(" ")].lower() for line in configOptionQuery]
optionCount, lastOption, lastArg = 0, None, None - lastCategory, lastDescription = GENERAL, "" + lastCategory, lastDescription = Category.GENERAL, "" for line in manCallResults: line = uiTools.getPrintable(line) strippedLine = line.strip() @@ -217,13 +212,13 @@
# if this is a category header then switch it if isCategoryLine: - if line.startswith("OPTIONS"): lastCategory = GENERAL - elif line.startswith("CLIENT"): lastCategory = CLIENT - elif line.startswith("SERVER"): lastCategory = SERVER - elif line.startswith("DIRECTORY SERVER"): lastCategory = DIRECTORY - elif line.startswith("DIRECTORY AUTHORITY SERVER"): lastCategory = AUTHORITY - elif line.startswith("HIDDEN SERVICE"): lastCategory = HIDDEN_SERVICE - elif line.startswith("TESTING NETWORK"): lastCategory = TESTING + if line.startswith("OPTIONS"): lastCategory = Category.GENERAL + elif line.startswith("CLIENT"): lastCategory = Category.CLIENT + elif line.startswith("SERVER"): lastCategory = Category.RELAY + elif line.startswith("DIRECTORY SERVER"): lastCategory = Category.DIRECTORY + elif line.startswith("DIRECTORY AUTHORITY SERVER"): lastCategory = Category.AUTHORITY + elif line.startswith("HIDDEN SERVICE"): lastCategory = Category.HIDDEN_SERVICE + elif line.startswith("TESTING NETWORK"): lastCategory = Category.TESTING else: msg = "Unrecognized category in the man page: %s" % line.strip() log.log(CONFIG["log.configDescriptions.unrecognizedCategory"], msg) @@ -249,7 +244,7 @@ def saveOptionDescriptions(path): """ Preserves the current configuration descriptors to the given path. This - raises an IOError if unable to do so. + raises an IOError or OSError if unable to do so.
Arguments: path - location to persist configuration descriptors @@ -269,7 +264,7 @@ for i in range(len(sortedOptions)): option = sortedOptions[i] manEntry = getConfigDescription(option) - outputFile.write("%s\nindex: %i\n%s\n%s\n%s\n" % (OPTION_CATEGORY_STR[manEntry.category], manEntry.index, option, manEntry.argUsage, manEntry.description)) + outputFile.write("%s\nindex: %i\n%s\n%s\n%s\n" % (manEntry.category, manEntry.index, option, manEntry.argUsage, manEntry.description)) if i != len(sortedOptions) - 1: outputFile.write(PERSIST_ENTRY_DIVIDER)
outputFile.close() @@ -286,6 +281,17 @@
return CONFIG.get("config.summary.%s" % option.lower())
+def isImportant(option): + """ + Provides True if the option has the 'important' flag in the configuration, + False otherwise. + + Arguments: + option - tor config option + """ + + return option.lower() in CONFIG["config.important"] + def getConfigDescription(option): """ Provides ManPageEntry instances populated with information fetched from the @@ -345,19 +351,31 @@
return tuple(MULTILINE_PARAM)
-def getCustomOptions(): +def getCustomOptions(includeValue = False): """ - Provides the set of torrc parameters that differ from their defaults. + Provides the torrc parameters that differ from their defaults. + + Arguments: + includeValue - provides the current value with results if true, otherwise + this just contains the options """
- customOptions, conn = set(), torTools.getConn() - configTextQuery = conn.getInfo("config-text", "").strip().split("\n") + configText = torTools.getConn().getInfo("config-text", "").strip() + configLines = configText.split("\n")
- for entry in configTextQuery: - # tor provides a Log entry even if it matches the default - if entry != "Log notice stdout": - customOptions.add(entry[:entry.find(" ")]) - return customOptions + # removes any duplicates + configLines = list(set(configLines)) + + # The "GETINFO config-text" query only provides options that differ + # from Tor's defaults with the exception of its Log entry which, even + # if undefined, returns "Log notice stdout" as per: + # https://trac.torproject.org/projects/tor/ticket/2362 + + try: configLines.remove("Log notice stdout") + except ValueError: pass + + if includeValue: return configLines + else: return [line[:line.find(" ")] for line in configLines]
def validate(contents = None): """ @@ -403,13 +421,13 @@
# most parameters are overwritten if defined multiple times if option in seenOptions and not option in getMultilineParameters(): - issuesFound.append((lineNumber, VAL_DUPLICATE, option)) + issuesFound.append((lineNumber, ValidationError.DUPLICATE, option)) continue else: seenOptions.append(option)
# checks if the value isn't necessary due to matching the defaults if not option in customOptions: - issuesFound.append((lineNumber, VAL_IS_DEFAULT, option)) + issuesFound.append((lineNumber, ValidationError.IS_DEFAULT, option))
# replace aliases with their recognized representation if option in CONFIG["torrc.alias"]: @@ -444,17 +462,17 @@ if not isBlankMatch and not val in torValues: # converts corrections to reader friedly size values displayValues = torValues - if valueType == SIZE_VALUE: + if valueType == ValueType.SIZE: displayValues = [uiTools.getSizeLabel(int(val)) for val in torValues] - elif valueType == TIME_VALUE: + elif valueType == ValueType.TIME: displayValues = [uiTools.getTimeLabel(int(val)) for val in torValues]
- issuesFound.append((lineNumber, VAL_MISMATCH, ", ".join(displayValues))) + issuesFound.append((lineNumber, ValidationError.MISMATCH, ", ".join(displayValues)))
# checks if any custom options are missing from the torrc for option in customOptions: if not option in seenOptions: - issuesFound.append((None, VAL_MISSING, option)) + issuesFound.append((None, ValidationError.MISSING, option))
return issuesFound
@@ -470,13 +488,13 @@
if confArg.count(" ") == 1: val, unit = confArg.lower().split(" ", 1) - if not val.isdigit(): return confArg, UNRECOGNIZED + if not val.isdigit(): return confArg, ValueType.UNRECOGNIZED mult, multType = _getUnitType(unit)
if mult != None: return str(int(val) * mult), multType
- return confArg, UNRECOGNIZED + return confArg, ValueType.UNRECOGNIZED
def _getUnitType(unit): """ @@ -489,13 +507,13 @@
for label in SIZE_MULT: if unit in CONFIG["torrc.label.size." + label]: - return SIZE_MULT[label], SIZE_VALUE + return SIZE_MULT[label], ValueType.SIZE
for label in TIME_MULT: if unit in CONFIG["torrc.label.time." + label]: - return TIME_MULT[label], TIME_VALUE + return TIME_MULT[label], ValueType.TIME
- return None, UNRECOGNIZED + return None, ValueType.UNRECOGNIZED
def _stripComments(contents): """ @@ -622,13 +640,23 @@
self.valsLock.acquire()
+ # The torrc validation relies on 'GETINFO config-text' which was + # introduced in tor 0.2.2.7-alpha so if we're using an earlier version + # (or configured to skip torrc validation) then this is a no-op. For more + # information see: + # https://trac.torproject.org/projects/tor/ticket/2501 + if not self.isLoaded(): returnVal = None - elif not CONFIG["features.torrc.validate"]: returnVal = {} else: - if self.corrections == None: - self.corrections = validate(self.contents) + skipValidation = not CONFIG["features.torrc.validate"] + skipValidation |= not torTools.getConn().isVersion("0.2.2.7-alpha")
- returnVal = list(self.corrections) + if skipValidation: returnVal = {} + else: + if self.corrections == None: + self.corrections = validate(self.contents) + + returnVal = list(self.corrections)
self.valsLock.release() return returnVal
Modified: arm/release/src/util/torTools.py =================================================================== --- arm/release/src/util/torTools.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/util/torTools.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -17,13 +17,25 @@
from TorCtl import TorCtl, TorUtil
-from util import log, procTools, sysTools, uiTools +from util import enum, log, procTools, sysTools, uiTools
# enums for tor's controller state: -# TOR_INIT - attached to a new controller or restart/sighup signal received -# TOR_CLOSED - control port closed -TOR_INIT, TOR_CLOSED = range(1, 3) +# INIT - attached to a new controller or restart/sighup signal received +# CLOSED - control port closed +State = enum.Enum("INIT", "CLOSED")
+# Addresses of the default directory authorities for tor version 0.2.3.0-alpha +# (this comes from the dirservers array in src/or/config.c). +DIR_SERVERS = [("86.59.21.38", "80"), # tor26 + ("128.31.0.39", "9031"), # moria1 + ("216.224.124.114", "9030"), # ides + ("212.112.245.170", "80"), # gabelmoo + ("194.109.206.212", "80"), # dizum + ("193.23.244.244", "80"), # dannenberg + ("208.83.223.34", "443"), # urras + ("213.115.239.118", "443"), # maatuska + ("82.94.251.203", "80")] # Tonga + # message logged by default when a controller can't set an event type DEFAULT_FAILED_EVENT_MSG = "Unsupported event type: %s"
@@ -41,9 +53,21 @@ # options (unchangable, even with a SETCONF) and other useful stats CACHE_ARGS = ("version", "config-file", "exit-policy/default", "fingerprint", "config/names", "info/names", "features/names", "events/names", - "nsEntry", "descEntry", "bwRate", "bwBurst", "bwObserved", - "bwMeasured", "flags", "pid", "pathPrefix", "startTime") + "nsEntry", "descEntry", "address", "bwRate", "bwBurst", + "bwObserved", "bwMeasured", "flags", "parsedVersion", "pid", + "pathPrefix", "startTime", "authorities", "circuits", "hsPorts") +CACHE_GETINFO_PREFIX_ARGS = ("ip-to-country/", )
+# Tor has a couple messages (in or/router.c) for when our ip address changes: +# "Our IP Address has changed from <previous> to <current>; rebuilding +# descriptor (source: <source>)." +# "Guessed our IP address as <current> (source: <source>)." +# +# It would probably be preferable to use the EXTERNAL_ADDRESS event, but I'm +# not quite sure why it's not provided by check_descriptor_ipaddress_changed +# so erring on the side of inclusiveness by using the notice event instead. +ADDR_CHANGED_MSG_PREFIX = ("Our IP Address has changed from", "Guessed our IP address as") + TOR_CTL_CLOSE_MSG = "Tor closed control connection. Exiting event thread." UNKNOWN = "UNKNOWN" # value used by cached information if undefined CONFIG = {"torrc.map": {}, @@ -52,6 +76,7 @@ "log.torGetInfo": log.DEBUG, "log.torGetInfoCache": None, "log.torGetConf": log.DEBUG, + "log.torGetConfCache": None, "log.torSetConf": log.INFO, "log.torPrefixPathInvalid": log.NOTICE, "log.bsdJailFound": log.INFO, @@ -65,9 +90,22 @@ "NS": "information related to the consensus will grow stale", "NEWCONSENSUS": "information related to the consensus will grow stale"}
+# number of sequential attempts before we decide that the Tor geoip database +# is unavailable +GEOIP_FAILURE_THRESHOLD = 5 + # provides int -> str mappings for torctl event runlevels TORCTL_RUNLEVELS = dict([(val, key) for (key, val) in TorUtil.loglevels.items()])
+# ip address ranges substituted by the 'private' keyword +PRIVATE_IP_RANGES = ("0.0.0.0/8", "169.254.0.0/16", "127.0.0.0/8", "192.168.0.0/16", "10.0.0.0/8", "172.16.0.0/12") + +# This prevents controllers from spawning worker threads (and by extension +# notifying status listeners). This is important when shutting down to prevent +# rogue threads from being alive during shutdown. + +NO_SPAWN = False + def loadConfig(config): config.update(CONFIG)
@@ -185,6 +223,41 @@ log.log(CONFIG["log.unknownBsdJailId"], "Failed to figure out the FreeBSD jail id. Assuming 0.") return 0
+def parseVersion(versionStr): + """ + Parses the given version string into its expected components, for instance... + '0.2.2.13-alpha (git-feb8c1b5f67f2c6f)' + + would provide: + (0, 2, 2, 13, 'alpha') + + If the input isn't recognized then this returns None. + + Arguments: + versionStr - version string to be parsed + """ + + # crops off extra arguments, for instance: + # '0.2.2.13-alpha (git-feb8c1b5f67f2c6f)' -> '0.2.2.13-alpha' + versionStr = versionStr.split()[0] + + result = None + if versionStr.count(".") in (2, 3): + # parses the optional suffix ('alpha', 'release', etc) + if versionStr.count("-") == 1: + versionStr, versionSuffix = versionStr.split("-") + else: versionSuffix = "" + + # Parses the numeric portion of the version. This can have three or four + # entries depending on if an optional patch level was provided. + try: + versionComp = [int(entry) for entry in versionStr.split(".")] + if len(versionComp) == 3: versionComp += [0] + result = tuple(versionComp + [versionSuffix]) + except ValueError: pass + + return result + def getConn(): """ Singleton constructor for a Controller. Be aware that this starts as being @@ -210,18 +283,29 @@ self.torctlListeners = [] # callback functions for TorCtl events self.statusListeners = [] # callback functions for tor's state changes self.controllerEvents = [] # list of successfully set controller events + self._fingerprintMappings = None # mappings of ip -> [(port, fingerprint), ...] + self._fingerprintLookupCache = {} # lookup cache with (ip, port) -> fingerprint mappings + self._fingerprintsAttachedCache = None # cache of relays we're connected to + self._nicknameLookupCache = {} # lookup cache with fingerprint -> nickname mappings + self._consensusLookupCache = {} # lookup cache with network status entries + self._descriptorLookupCache = {} # lookup cache with relay descriptors self._isReset = False # internal flag for tracking resets - self._status = TOR_CLOSED # current status of the attached control port + self._status = State.CLOSED # current status of the attached control port self._statusTime = 0 # unix time-stamp for the duration of the status self.lastHeartbeat = 0 # time of the last tor event
+ self._exitPolicyChecker = None + self._isExitingAllowed = False + self._exitPolicyLookupCache = {} # mappings of ip/port tuples to if they were accepted by the policy or not + # Logs issues and notices when fetching the path prefix if true. This is # only done once for the duration of the application to avoid pointless # messages. self._pathPrefixLogging = True
- # cached GETINFO parameters (None if unset or possibly changed) - self._cachedParam = dict([(arg, "") for arg in CACHE_ARGS]) + # cached parameters for GETINFO and custom getters (None if unset or + # possibly changed) + self._cachedParam = {}
# cached GETCONF parameters, entries consisting of: # (option, fetch_type) => value @@ -230,6 +314,9 @@ # directs TorCtl to notify us of events TorUtil.logger = self TorUtil.loglevel = "DEBUG" + + # tracks the number of sequential geoip lookup failures + self.geoipFailureCount = 0
def init(self, conn=None): """ @@ -254,17 +341,30 @@ self.conn.add_event_listener(self) for listener in self.eventListeners: self.conn.add_event_listener(listener)
+ # reset caches for ip -> fingerprint lookups + self._fingerprintMappings = None + self._fingerprintLookupCache = {} + self._fingerprintsAttachedCache = None + self._nicknameLookupCache = {} + self._consensusLookupCache = {} + self._descriptorLookupCache = {} + + self._exitPolicyChecker = self.getExitPolicy() + self._isExitingAllowed = self._exitPolicyChecker.isExitingAllowed() + self._exitPolicyLookupCache = {} + # sets the events listened for by the new controller (incompatible events # are dropped with a logged warning) self.setControllerEvents(self.controllerEvents)
self.connLock.release()
- self._status = TOR_INIT + self._status = State.INIT self._statusTime = time.time()
# notifies listeners that a new controller is available - thread.start_new_thread(self._notifyStatusListeners, (TOR_INIT,)) + if not NO_SPAWN: + thread.start_new_thread(self._notifyStatusListeners, (State.INIT,))
def close(self): """ @@ -274,14 +374,29 @@ self.connLock.acquire() if self.conn: self.conn.close() + + # If we're closing due to an event from TorCtl (for instance, tor was + # stopped) then TorCtl is shutting itself down and there's no need to + # join on its thread (actually, this *is* the TorCtl thread in that + # case so joining on it causes deadlock). + # + # This poses a slight possability of shutting down with a live orphaned + # thread if Tor is shut down, then arm shuts down before TorCtl has a + # chance to terminate. However, I've never seen that occure so leaving + # that alone for now. + + if not threading.currentThread() == self.conn._thread: + self.conn._thread.join() + self.conn = None self.connLock.release()
- self._status = TOR_CLOSED + self._status = State.CLOSED self._statusTime = time.time()
# notifies listeners that the controller's been shut down - thread.start_new_thread(self._notifyStatusListeners, (TOR_CLOSED,)) + if not NO_SPAWN: + thread.start_new_thread(self._notifyStatusListeners, (State.CLOSED,)) else: self.connLock.release()
def isAlive(self): @@ -337,21 +452,44 @@
self.connLock.acquire()
+ isGeoipRequest = param.startswith("ip-to-country/") + + # checks if this is an arg caching covers + isCacheArg = param in CACHE_ARGS + + if not isCacheArg: + for prefix in CACHE_GETINFO_PREFIX_ARGS: + if param.startswith(prefix): + isCacheArg = True + break + startTime = time.time() result, raisedExc, isFromCache = default, None, False if self.isAlive(): - if param in CACHE_ARGS and self._cachedParam[param]: - result = self._cachedParam[param] + cachedValue = self._cachedParam.get(param) + + if isCacheArg and cachedValue: + result = cachedValue isFromCache = True + elif isGeoipRequest and self.geoipFailureCount == GEOIP_FAILURE_THRESHOLD: + # the geoip database aleady looks to be unavailable - abort the request + raisedExc = TorCtl.ErrorReply("Tor geoip database is unavailable.") else: try: getInfoVal = self.conn.get_info(param)[param] if getInfoVal != None: result = getInfoVal + if isGeoipRequest: self.geoipFailureCount = 0 except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed), exc: if type(exc) == TorCtl.TorCtlClosed: self.close() raisedExc = exc + + if isGeoipRequest: + self.geoipFailureCount += 1 + + if self.geoipFailureCount == GEOIP_FAILURE_THRESHOLD: + log.log(CONFIG["log.geoipUnavailable"], "Tor geoip database is unavailable.")
- if not isFromCache and result and param in CACHE_ARGS: + if isCacheArg and result and not isFromCache: self._cachedParam[param] = result
if isFromCache: @@ -439,9 +577,9 @@ result = {} if fetchType == "map" else []
if self.isAlive(): - if (param, fetchType) in self._cachedConf: + if (param.lower(), fetchType) in self._cachedConf: isFromCache = True - result = self._cachedConf[(param, fetchType)] + result = self._cachedConf[(param.lower(), fetchType)] else: try: if fetchType == "str": @@ -458,15 +596,18 @@ if type(exc) == TorCtl.TorCtlClosed: self.close() result, raisedExc = default, exc
- if not isFromCache and result: + if not isFromCache: cacheValue = result if fetchType == "list": cacheValue = list(result) elif fetchType == "map": cacheValue = dict(result) - self._cachedConf[(param, fetchType)] = cacheValue + self._cachedConf[(param.lower(), fetchType)] = cacheValue
- runtimeLabel = "cache fetch" if isFromCache else "runtime: %0.4f" % (time.time() - startTime) - msg = "GETCONF %s (%s)" % (param, runtimeLabel) - log.log(CONFIG["log.torGetConf"], msg) + if isFromCache: + msg = "GETCONF %s (cache fetch)" % param + log.log(CONFIG["log.torGetConfCache"], msg) + else: + msg = "GETCONF %s (runtime: %0.4f)" % (param, time.time() - startTime) + log.log(CONFIG["log.torGetConf"], msg)
self.connLock.release()
@@ -496,10 +637,16 @@
# flushing cached values (needed until we can detect SETCONF calls) for fetchType in ("str", "list", "map"): - entry = (param, fetchType) + entry = (param.lower(), fetchType)
if entry in self._cachedConf: del self._cachedConf[entry] + + # special caches for the exit policy + if param.lower() == "exitpolicy": + self._exitPolicyChecker = self.getExitPolicy() + self._isExitingAllowed = self._exitPolicyChecker.isExitingAllowed() + self._exitPolicyLookupCache = {} except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed), exc: if type(exc) == TorCtl.TorCtlClosed: self.close() elif type(exc) == TorCtl.ErrorReply: @@ -527,6 +674,27 @@
if raisedExc: raise raisedExc
+ def getCircuits(self, default = []): + """ + This provides a list with tuples of the form: + (circuitID, status, purpose, (fingerprint1, fingerprint2...)) + + Arguments: + default - value provided back if unable to query the circuit-status + """ + + return self._getRelayAttr("circuits", default) + + def getHiddenServicePorts(self, default = []): + """ + Provides the target ports hidden services are configured to use. + + Arguments: + default - value provided back if unable to query the hidden service ports + """ + + return self._getRelayAttr("hsPorts", default) + def getMyNetworkStatus(self, default = None): """ Provides the network status entry for this relay if available. This is @@ -615,6 +783,53 @@
return self._getRelayAttr("flags", default)
+ def isVersion(self, minVersionStr): + """ + Checks if we meet the given version. Recognized versions are of the form: + <major>.<minor>.<micro>[.<patch>][-<status_tag>] + + for instance, "0.2.2.13-alpha" or "0.2.1.5". This raises a ValueError if + the input isn't recognized, and returns False if unable to fetch our + instance's version. + + According to the spec the status_tag is purely informal, so it's ignored + in comparisons. + + Arguments: + minVersionStr - version to be compared against + """ + + minVersion = parseVersion(minVersionStr) + + if minVersion == None: + raise ValueError("unrecognized version: %s" % minVersionStr) + + self.connLock.acquire() + + result = False + if self.isAlive(): + myVersion = self._getRelayAttr("parsedVersion", None) + + if not myVersion: + result = False + elif myVersion[:4] == minVersion[:4]: + result = True # versions match + else: + # compares each of the numeric portions of the version + for i in range(4): + myVal, minVal = myVersion[i], minVersion[i] + + if myVal > minVal: + result = True + break + elif myVal < minVal: + result = False + break + + self.connLock.release() + + return result + def getMyPid(self): """ Provides the pid of the attached tor process (None if no controller exists @@ -623,6 +838,16 @@
return self._getRelayAttr("pid", None)
+ def getMyDirAuthorities(self): + """ + Provides a listing of IP/port tuples for the directory authorities we've + been configured to use. If set in the configuration then these are custom + authorities, otherwise its an estimate of what Tor has been hardcoded to + use (unfortunately, this might be out of date). + """ + + return self._getRelayAttr("authorities", []) + def getPathPrefix(self): """ Provides the path prefix that should be used for fetching tor resources. @@ -630,10 +855,7 @@ jail's path. """
- result = self._getRelayAttr("pathPrefix", "") - - if result == UNKNOWN: return "" - else: return result + return self._getRelayAttr("pathPrefix", "")
def getStartTime(self): """ @@ -641,10 +863,7 @@ can't be determined then this provides None. """
- result = self._getRelayAttr("startTime", None) - - if result == UNKNOWN: return None - else: return result + return self._getRelayAttr("startTime", None)
def getStatus(self): """ @@ -655,6 +874,199 @@
return (self._status, self._statusTime)
+ def isExitingAllowed(self, ipAddress, port): + """ + Checks if the given destination can be exited to by this relay, returning + True if so and False otherwise. + """ + + self.connLock.acquire() + + result = False + if self.isAlive(): + # query the policy if it isn't yet cached + if not (ipAddress, port) in self._exitPolicyLookupCache: + # If we allow any exiting then this could be relayed DNS queries, + # otherwise the policy is checked. Tor still makes DNS connections to + # test when exiting isn't allowed, but nothing is relayed over them. + # I'm registering these as non-exiting to avoid likely user confusion: + # https://trac.torproject.org/projects/tor/ticket/965 + + if self._isExitingAllowed and port == "53": isAccepted = True + else: isAccepted = self._exitPolicyChecker.check(ipAddress, port) + self._exitPolicyLookupCache[(ipAddress, port)] = isAccepted + + result = self._exitPolicyLookupCache[(ipAddress, port)] + + self.connLock.release() + + return result + + def getExitPolicy(self): + """ + Provides an ExitPolicy instance for the head of this relay's exit policy + chain. If there's no active connection then this provides None. + """ + + self.connLock.acquire() + + result = None + if self.isAlive(): + if self.getOption("ORPort"): + policyEntries = [] + for exitPolicy in self.getOption("ExitPolicy", [], True): + policyEntries += [policy.strip() for policy in exitPolicy.split(",")] + + # appends the default exit policy + defaultExitPolicy = self.getInfo("exit-policy/default") + + if defaultExitPolicy: + policyEntries += defaultExitPolicy.split(",") + + # construct the policy chain backwards + policyEntries.reverse() + + for entry in policyEntries: + result = ExitPolicy(entry, result) + + # Checks if we are rejecting private connections. If set, this appends + # 'reject private' and 'reject <my ip>' to the start of our policy chain. + isPrivateRejected = self.getOption("ExitPolicyRejectPrivate", True) + + if isPrivateRejected: + result = ExitPolicy("reject private", result) + + myAddress = self.getInfo("address") + if myAddress: result = ExitPolicy("reject %s" % myAddress, result) + else: + # no ORPort is set so all relaying is disabled + result = ExitPolicy("reject *:*") + + self.connLock.release() + + return result + + def getConsensusEntry(self, relayFingerprint): + """ + Provides the most recently available consensus information for the given + relay. This is none if no such information exists. + + Arguments: + relayFingerprint - fingerprint of the relay + """ + + self.connLock.acquire() + + result = None + if self.isAlive(): + if not relayFingerprint in self._consensusLookupCache: + nsEntry = self.getInfo("ns/id/%s" % relayFingerprint) + self._consensusLookupCache[relayFingerprint] = nsEntry + + result = self._consensusLookupCache[relayFingerprint] + + self.connLock.release() + + return result + + def getDescriptorEntry(self, relayFingerprint): + """ + Provides the most recently available descriptor information for the given + relay. Unless FetchUselessDescriptors is set this may frequently be + unavailable. If no such descriptor is available then this returns None. + + Arguments: + relayFingerprint - fingerprint of the relay + """ + + self.connLock.acquire() + + result = None + if self.isAlive(): + if not relayFingerprint in self._descriptorLookupCache: + descEntry = self.getInfo("desc/id/%s" % relayFingerprint) + self._descriptorLookupCache[relayFingerprint] = descEntry + + result = self._descriptorLookupCache[relayFingerprint] + + self.connLock.release() + + return result + + def getRelayFingerprint(self, relayAddress, relayPort = None, getAllMatches = False): + """ + Provides the fingerprint associated with the given address. If there's + multiple potential matches or the mapping is unknown then this returns + None. This disambiguates the fingerprint if there's multiple relays on + the same ip address by several methods, one of them being to pick relays + we have a connection with. + + Arguments: + relayAddress - address of relay to be returned + relayPort - orport of relay (to further narrow the results) + getAllMatches - ignores the relayPort and provides all of the + (port, fingerprint) tuples matching the given + address + """ + + self.connLock.acquire() + + result = None + if self.isAlive(): + if getAllMatches: + # populates the ip -> fingerprint mappings if not yet available + if self._fingerprintMappings == None: + self._fingerprintMappings = self._getFingerprintMappings() + + if relayAddress in self._fingerprintMappings: + result = self._fingerprintMappings[relayAddress] + else: result = [] + else: + # query the fingerprint if it isn't yet cached + if not (relayAddress, relayPort) in self._fingerprintLookupCache: + relayFingerprint = self._getRelayFingerprint(relayAddress, relayPort) + self._fingerprintLookupCache[(relayAddress, relayPort)] = relayFingerprint + + result = self._fingerprintLookupCache[(relayAddress, relayPort)] + + self.connLock.release() + + return result + + def getRelayNickname(self, relayFingerprint): + """ + Provides the nickname associated with the given relay. This provides None + if no such relay exists, and "Unnamed" if the name hasn't been set. + + Arguments: + relayFingerprint - fingerprint of the relay + """ + + self.connLock.acquire() + + result = None + if self.isAlive(): + # query the nickname if it isn't yet cached + if not relayFingerprint in self._nicknameLookupCache: + if relayFingerprint == self.getInfo("fingerprint"): + # this is us, simply check the config + myNickname = self.getOption("Nickname", "Unnamed") + self._nicknameLookupCache[relayFingerprint] = myNickname + else: + # check the consensus for the relay + nsEntry = self.getConsensusEntry(relayFingerprint) + + if nsEntry: relayNickname = nsEntry[2:nsEntry.find(" ", 2)] + else: relayNickname = None + + self._nicknameLookupCache[relayFingerprint] = relayNickname + + result = self._nicknameLookupCache[relayFingerprint] + + self.connLock.release() + + return result + def addEventListener(self, listener): """ Directs further tor controller events to callback functions of the @@ -825,7 +1237,7 @@ if not issueSighup: try: self.conn.send_signal("RELOAD") - self._cachedParam = dict([(arg, "") for arg in CACHE_ARGS]) + self._cachedParam = {} self._cachedConf = {} except Exception, exc: # new torrc parameters caused an error (tor's likely shut down) @@ -870,7 +1282,7 @@ if errorLine: raise IOError(" ".join(errorLine.split()[3:])) else: raise IOError("failed silently")
- self._cachedParam = dict([(arg, "") for arg in CACHE_ARGS]) + self._cachedParam = {} self._cachedConf = {} except IOError, exc: raisedException = exc @@ -888,13 +1300,15 @@ if event.level == "NOTICE" and event.msg.startswith("Received reload signal (hup)"): self._isReset = True
- self._status = TOR_INIT + self._status = State.INIT self._statusTime = time.time()
- thread.start_new_thread(self._notifyStatusListeners, (TOR_INIT,)) + if not NO_SPAWN: + thread.start_new_thread(self._notifyStatusListeners, (State.INIT,))
def ns_event(self, event): self._updateHeartbeat() + self._consensusLookupCache = {}
myFingerprint = self.getInfo("fingerprint") if myFingerprint: @@ -912,20 +1326,77 @@ def new_consensus_event(self, event): self._updateHeartbeat()
+ self.connLock.acquire() + self._cachedParam["nsEntry"] = None self._cachedParam["flags"] = None self._cachedParam["bwMeasured"] = None + + # reconstructs consensus based mappings + self._fingerprintLookupCache = {} + self._fingerprintsAttachedCache = None + self._nicknameLookupCache = {} + self._consensusLookupCache = {} + + if self._fingerprintMappings != None: + self._fingerprintMappings = self._getFingerprintMappings(event.nslist) + + self.connLock.release()
def new_desc_event(self, event): self._updateHeartbeat()
+ self.connLock.acquire() + myFingerprint = self.getInfo("fingerprint") if not myFingerprint or myFingerprint in event.idlist: self._cachedParam["descEntry"] = None self._cachedParam["bwObserved"] = None + + # If we're tracking ip address -> fingerprint mappings then update with + # the new relays. + self._fingerprintLookupCache = {} + self._fingerprintsAttachedCache = None + self._descriptorLookupCache = {} + + if self._fingerprintMappings != None: + for fingerprint in event.idlist: + # gets consensus data for the new descriptor + try: nsLookup = self.conn.get_network_status("id/%s" % fingerprint) + except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): continue + + if len(nsLookup) > 1: + # multiple records for fingerprint (shouldn't happen) + log.log(log.WARN, "Multiple consensus entries for fingerprint: %s" % fingerprint) + continue + + # updates fingerprintMappings with new data + newRelay = nsLookup[0] + if newRelay.ip in self._fingerprintMappings: + # if entry already exists with the same orport, remove it + orportMatch = None + for entryPort, entryFingerprint in self._fingerprintMappings[newRelay.ip]: + if entryPort == newRelay.orport: + orportMatch = (entryPort, entryFingerprint) + break + + if orportMatch: self._fingerprintMappings[newRelay.ip].remove(orportMatch) + + # add the new entry + self._fingerprintMappings[newRelay.ip].append((newRelay.orport, newRelay.idhex)) + else: + self._fingerprintMappings[newRelay.ip] = [(newRelay.orport, newRelay.idhex)] + + self.connLock.release()
def circ_status_event(self, event): self._updateHeartbeat() + + # CIRC events aren't required, but if one's received then flush this cache + # since it uses circuit-status results. + self._fingerprintsAttachedCache = None + + self._cachedParam["circuits"] = None
def buildtimeout_set_event(self, event): self._updateHeartbeat() @@ -959,6 +1430,13 @@
# checks if TorCtl is providing a notice that control port is closed if TOR_CTL_CLOSE_MSG in msg: self.close() + + # if the message is informing us of our ip address changing then clear + # its cached value + for prefix in ADDR_CHANGED_MSG_PREFIX: + if msg.startswith(prefix): + self._cachedParam["address"] = None + break
def _updateHeartbeat(self): """ @@ -968,6 +1446,126 @@ # alternative is to use the event's timestamp (via event.arrived_at) self.lastHeartbeat = time.time()
+ def _getFingerprintMappings(self, nsList = None): + """ + Provides IP address to (port, fingerprint) tuple mappings for all of the + currently cached relays. + + Arguments: + nsList - network status listing (fetched if not provided) + """ + + results = {} + if self.isAlive(): + # fetch the current network status if not provided + if not nsList: + try: nsList = self.conn.get_network_status() + except (socket.error, TorCtl.TorCtlClosed, TorCtl.ErrorReply): nsList = [] + + # construct mappings of ips to relay data + for relay in nsList: + if relay.ip in results: results[relay.ip].append((relay.orport, relay.idhex)) + else: results[relay.ip] = [(relay.orport, relay.idhex)] + + return results + + def _getRelayFingerprint(self, relayAddress, relayPort): + """ + Provides the fingerprint associated with the address/port combination. + + Arguments: + relayAddress - address of relay to be returned + relayPort - orport of relay (to further narrow the results) + """ + + # If we were provided with a string port then convert to an int (so + # lookups won't mismatch based on type). + if isinstance(relayPort, str): relayPort = int(relayPort) + + # checks if this matches us + if relayAddress == self.getInfo("address"): + if not relayPort or relayPort == self.getOption("ORPort"): + return self.getInfo("fingerprint") + + # if we haven't yet populated the ip -> fingerprint mappings then do so + if self._fingerprintMappings == None: + self._fingerprintMappings = self._getFingerprintMappings() + + potentialMatches = self._fingerprintMappings.get(relayAddress) + if not potentialMatches: return None # no relay matches this ip address + + if len(potentialMatches) == 1: + # There's only one relay belonging to this ip address. If the port + # matches then we're done. + match = potentialMatches[0] + + if relayPort and match[0] != relayPort: return None + else: return match[1] + elif relayPort: + # Multiple potential matches, so trying to match based on the port. + for entryPort, entryFingerprint in potentialMatches: + if entryPort == relayPort: + return entryFingerprint + + # Disambiguates based on our orconn-status and circuit-status results. + # This only includes relays we're connected to, so chances are pretty + # slim that we'll still have a problem narrowing this down. Note that we + # aren't necessarily checking for events that can create new client + # circuits (so this cache might be a little dirty). + + # populates the cache + if self._fingerprintsAttachedCache == None: + self._fingerprintsAttachedCache = [] + + # orconn-status has entries of the form: + # $33173252B70A50FE3928C7453077936D71E45C52=shiven CONNECTED + orconnResults = self.getInfo("orconn-status") + if orconnResults: + for line in orconnResults.split("\n"): + self._fingerprintsAttachedCache.append(line[1:line.find("=")]) + + # circuit-status results (we only make connections to the first hop) + for _, _, _, path in self.getCircuits(): + self._fingerprintsAttachedCache.append(path[0]) + + # narrow to only relays we have a connection to + attachedMatches = [] + for _, entryFingerprint in potentialMatches: + if entryFingerprint in self._fingerprintsAttachedCache: + attachedMatches.append(entryFingerprint) + + if len(attachedMatches) == 1: + return attachedMatches[0] + + # Highly unlikely, but still haven't found it. Last we'll use some + # tricks from Mike's ConsensusTracker, excluding possiblities that + # have... + # - lost their Running flag + # - list a bandwidth of 0 + # - have 'opt hibernating' set + # + # This involves constructing a TorCtl Router and checking its 'down' + # flag (which is set by the three conditions above). This is the last + # resort since it involves a couple GETINFO queries. + + for entryPort, entryFingerprint in list(potentialMatches): + try: + nsCall = self.conn.get_network_status("id/%s" % entryFingerprint) + if not nsCall: raise TorCtl.ErrorReply() # network consensus couldn't be fetched + nsEntry = nsCall[0] + + descEntry = self.getInfo("desc/id/%s" % entryFingerprint) + if not descEntry: raise TorCtl.ErrorReply() # relay descriptor couldn't be fetched + descLines = descEntry.split("\n") + + isDown = TorCtl.Router.build_from_desc(descLines, nsEntry).down + if isDown: potentialMatches.remove((entryPort, entryFingerprint)) + except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): pass + + if len(potentialMatches) == 1: + return potentialMatches[0][1] + else: return None + def _getRelayAttr(self, key, default, cacheUndefined = True): """ Provides information associated with this relay, using the cached value if @@ -980,15 +1578,15 @@ lookups if true """
- currentVal = self._cachedParam[key] - if currentVal: + currentVal = self._cachedParam.get(key) + if currentVal != None: if currentVal == UNKNOWN: return default else: return currentVal
self.connLock.acquire()
- currentVal, result = self._cachedParam[key], None - if not currentVal and self.isAlive(): + currentVal, result = self._cachedParam.get(key), None + if currentVal == None and self.isAlive(): # still unset - fetch value if key in ("nsEntry", "descEntry"): myFingerprint = self.getInfo("fingerprint") @@ -1043,6 +1641,8 @@ if line.startswith("s "): result = line[2:].split() break + elif key == "parsedVersion": + result = parseVersion(self.getInfo("version", "")) elif key == "pid": result = getPid(int(self.getOption("ControlPort", 9051)), self.getOption("PidFile")) elif key == "pathPrefix": @@ -1084,7 +1684,7 @@ if myPid: try: if procTools.isProcAvailable(): - result = float(procTools.getStats(myPid, procTools.STAT_START_TIME)[0]) + result = float(procTools.getStats(myPid, procTools.Stat.START_TIME)[0]) else: psCall = sysTools.call("ps -p %s -o etime" % myPid)
@@ -1092,16 +1692,83 @@ etimeEntry = psCall[1].strip() result = time.time() - uiTools.parseShortTimeLabel(etimeEntry) except: pass + elif key == "authorities": + # There's two configuration options that can overwrite the default + # authorities: DirServer and AlternateDirAuthority. + + # TODO: Both options accept a set of flags to more precisely set what they + # overwrite. Ideally this would account for these flags to more accurately + # identify authority connections from relays. + + dirServerCfg = self.getOption("DirServer", [], True) + altDirAuthCfg = self.getOption("AlternateDirAuthority", [], True) + altAuthoritiesCfg = dirServerCfg + altDirAuthCfg + + if altAuthoritiesCfg: + result = [] + + # entries are of the form: + # [nickname] [flags] address:port fingerprint + for entry in altAuthoritiesCfg: + locationComp = entry.split()[-2] # address:port component + result.append(tuple(locationComp.split(":", 1))) + else: result = list(DIR_SERVERS) + elif key == "circuits": + # Parses our circuit-status results, for instance + # 91 BUILT $E4AE6E2FE320FBBD31924E8577F3289D4BE0B4AD=Qwerty PURPOSE=GENERAL + # would belong to a single hop circuit, most likely fetching the + # consensus via a directory mirror. + circStatusResults = self.getInfo("circuit-status") + + if circStatusResults == "": + result = [] # we don't have any circuits + elif circStatusResults != None: + result = [] + + for line in circStatusResults.split("\n"): + # appends a tuple with the (status, purpose, path) + lineComp = line.split(" ") + + # skips blank lines and circuits without a path, for instance: + # 5 LAUNCHED PURPOSE=TESTING + if len(lineComp) < 4: continue + + path = tuple([hopEntry[1:41] for hopEntry in lineComp[2].split(",")]) + result.append((int(lineComp[0]), lineComp[1], lineComp[3][8:], path)) + elif key == "hsPorts": + result = [] + hsOptions = self.getOptionMap("HiddenServiceOptions") + + if hsOptions and "HiddenServicePort" in hsOptions: + for hsEntry in hsOptions["HiddenServicePort"]: + # hidden service port entries are of the form: + # VIRTPORT [TARGET] + # with the TARGET being an IP, port, or IP:Port. If the target port + # isn't defined then uses the VIRTPORT. + + hsPort = None + + if " " in hsEntry: + # parses the target, checking if it's a port or IP:Port combination + hsTarget = hsEntry.split(" ")[1] + + if ":" in hsTarget: + hsPort = hsTarget.split(":")[1] # target is the IP:Port + elif hsTarget.isdigit(): + hsPort = hsTarget # target is just the port + else: hsPort = hsEntry # just has the virtual port + + if hsPort.isdigit(): + result.append(hsPort)
# cache value - if result: self._cachedParam[key] = result + if result != None: self._cachedParam[key] = result elif cacheUndefined: self._cachedParam[key] = UNKNOWN - elif currentVal == UNKNOWN: result = currentVal
self.connLock.release()
- if result: return result - else: return default + if result == None or result == UNKNOWN: return default + else: return result
def _notifyStatusListeners(self, eventType): """ @@ -1113,13 +1780,156 @@ """
# resets cached GETINFO and GETCONF parameters - self._cachedParam = dict([(arg, "") for arg in CACHE_ARGS]) + self._cachedParam = {} self._cachedConf = {}
# gives a notice that the control port has closed - if eventType == TOR_CLOSED: + if eventType == State.CLOSED: log.log(CONFIG["log.torCtlPortClosed"], "Tor control port closed")
for callback in self.statusListeners: callback(self, eventType)
+class ExitPolicy: + """ + Single rule from the user's exit policy. These are chained together to form + complete policies. + """ + + def __init__(self, ruleEntry, nextRule): + """ + Exit policy rule constructor. + + Arguments: + ruleEntry - tor exit policy rule (for instance, "reject *:135-139") + nextRule - next rule to be checked when queries don't match this policy + """ + + # sanitize the input a bit, cleaning up tabs and stripping quotes + ruleEntry = ruleEntry.replace("\t", " ").replace(""", "") + + self.ruleEntry = ruleEntry + self.nextRule = nextRule + self.isAccept = ruleEntry.startswith("accept") + + # strips off "accept " or "reject " and extra spaces + ruleEntry = ruleEntry[7:].replace(" ", "") + + # split ip address (with mask if provided) and port + if ":" in ruleEntry: entryIp, entryPort = ruleEntry.split(":", 1) + else: entryIp, entryPort = ruleEntry, "*" + + # sets the ip address component + self.isIpWildcard = entryIp == "*" or entryIp.endswith("/0") + + # checks for the private alias (which expands this to a chain of entries) + if entryIp.lower() == "private": + entryIp = PRIVATE_IP_RANGES[0] + + # constructs the chain backwards (last first) + lastHop = self.nextRule + prefix = "accept " if self.isAccept else "reject " + suffix = ":" + entryPort + for addr in PRIVATE_IP_RANGES[-1:0:-1]: + lastHop = ExitPolicy(prefix + addr + suffix, lastHop) + + self.nextRule = lastHop # our next hop is the start of the chain + + if "/" in entryIp: + ipComp = entryIp.split("/", 1) + self.ipAddress = ipComp[0] + self.ipMask = int(ipComp[1]) + else: + self.ipAddress = entryIp + self.ipMask = 32 + + # constructs the binary address just in case of comparison with a mask + if self.ipAddress != "*": + self.ipAddressBin = "" + for octet in self.ipAddress.split("."): + # Converts the int to a binary string, padded with zeros. Source: + # http://www.daniweb.com/code/snippet216539.html + self.ipAddressBin += "".join([str((int(octet) >> y) & 1) for y in range(7, -1, -1)]) + else: + self.ipAddressBin = "0" * 32 + + # sets the port component + self.minPort, self.maxPort = 0, 0 + self.isPortWildcard = entryPort == "*" + + if entryPort != "*": + if "-" in entryPort: + portComp = entryPort.split("-", 1) + self.minPort = int(portComp[0]) + self.maxPort = int(portComp[1]) + else: + self.minPort = int(entryPort) + self.maxPort = int(entryPort) + + # if both the address and port are wildcards then we're effectively the + # last entry so cut off the remaining chain + if self.isIpWildcard and self.isPortWildcard: + self.nextRule = None + + def isExitingAllowed(self): + """ + Provides true if the policy allows exiting whatsoever, false otherwise. + """ + + if self.isAccept: return True + elif self.isIpWildcard and self.isPortWildcard: return False + elif not self.nextRule: return False # fell off policy (shouldn't happen) + else: return self.nextRule.isExitingAllowed() + + def check(self, ipAddress, port): + """ + Checks if the rule chain allows exiting to this address, returning true if + so and false otherwise. + """ + + port = int(port) + + # does the port check first since comparing ip masks is more work + isPortMatch = self.isPortWildcard or (port >= self.minPort and port <= self.maxPort) + + if isPortMatch: + isIpMatch = self.isIpWildcard or self.ipAddress == ipAddress + + # expands the check to include the mask if it has one + if not isIpMatch and self.ipMask != 32: + inputAddressBin = "" + for octet in ipAddress.split("."): + inputAddressBin += "".join([str((int(octet) >> y) & 1) for y in range(7, -1, -1)]) + + isIpMatch = self.ipAddressBin[:self.ipMask] == inputAddressBin[:self.ipMask] + + if isIpMatch: return self.isAccept + + # our policy doesn't concern this address, move on to the next one + if self.nextRule: return self.nextRule.check(ipAddress, port) + else: return True # fell off the chain without a conclusion (shouldn't happen...) + + def __str__(self): + # This provides the actual policy rather than the entry used to construct + # it so the 'private' keyword is expanded. + + acceptanceLabel = "accept" if self.isAccept else "reject" + + if self.isIpWildcard: + ipLabel = "*" + elif self.ipMask != 32: + ipLabel = "%s/%i" % (self.ipAddress, self.ipMask) + else: ipLabel = self.ipAddress + + if self.isPortWildcard: + portLabel = "*" + elif self.minPort != self.maxPort: + portLabel = "%i-%i" % (self.minPort, self.maxPort) + else: portLabel = str(self.minPort) + + myPolicy = "%s %s:%s" % (acceptanceLabel, ipLabel, portLabel) + + if self.nextRule: + return myPolicy + ", " + str(self.nextRule) + else: return myPolicy +
Modified: arm/release/src/util/uiTools.py =================================================================== --- arm/release/src/util/uiTools.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/util/uiTools.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -9,7 +9,7 @@ import curses
from curses.ascii import isprint -from util import log +from util import enum, log
# colors curses can handle COLOR_LIST = {"red": curses.COLOR_RED, "green": curses.COLOR_GREEN, @@ -32,7 +32,7 @@ TIME_UNITS = [(86400.0, "d", " day"), (3600.0, "h", " hour"), (60.0, "m", " minute"), (1.0, "s", " second")]
-END_WITH_ELLIPSE, END_WITH_HYPHEN = range(1, 3) +Ending = enum.Enum("ELLIPSE", "HYPHEN") SCROLL_KEYS = (curses.KEY_UP, curses.KEY_DOWN, curses.KEY_PPAGE, curses.KEY_NPAGE, curses.KEY_HOME, curses.KEY_END) CONFIG = {"features.colorInterface": True, "log.cursesColorSupport": log.INFO} @@ -117,7 +117,7 @@ if not COLOR_ATTR_INITIALIZED: _initColors() return COLOR_ATTR[color]
-def cropStr(msg, size, minWordLen = 4, minCrop = 0, endType = END_WITH_ELLIPSE, getRemainder = False): +def cropStr(msg, size, minWordLen = 4, minCrop = 0, endType = Ending.ELLIPSE, getRemainder = False): """ Provides the msg constrained to the given length, truncating on word breaks. If the last words is long this truncates mid-word with an ellipse. If there @@ -143,8 +143,8 @@ minCrop - minimum characters that must be dropped if a word's cropped endType - type of ending used when truncating: None - blank ending - END_WITH_ELLIPSE - includes an ellipse - END_WITH_HYPHEN - adds hyphen when breaking words + Ending.ELLIPSE - includes an ellipse + Ending.HYPHEN - adds hyphen when breaking words getRemainder - returns a tuple instead, with the second part being the cropped portion of the message """ @@ -161,11 +161,12 @@
# since we're cropping, the effective space available is less with an # ellipse, and cropping words requires an extra space for hyphens - if endType == END_WITH_ELLIPSE: size -= 3 - elif endType == END_WITH_HYPHEN and minWordLen != None: minWordLen += 1 + if endType == Ending.ELLIPSE: size -= 3 + elif endType == Ending.HYPHEN and minWordLen != None: minWordLen += 1
# checks if there isn't the minimum space needed to include anything lastWordbreak = msg.rfind(" ", 0, size + 1) + lastWordbreak = len(msg[:lastWordbreak].rstrip()) # drops extra ending whitespaces if (minWordLen != None and size < minWordLen) or (minWordLen == None and lastWordbreak < 1): if getRemainder: return ("", msg) else: return "" @@ -181,23 +182,64 @@
if includeCrop: returnMsg, remainder = msg[:size], msg[size:] - if endType == END_WITH_HYPHEN: + if endType == Ending.HYPHEN: remainder = returnMsg[-1] + remainder - returnMsg = returnMsg[:-1] + "-" + returnMsg = returnMsg[:-1].rstrip() + "-" else: returnMsg, remainder = msg[:lastWordbreak], msg[lastWordbreak:]
# if this is ending with a comma or period then strip it off if not getRemainder and returnMsg[-1] in (",", "."): returnMsg = returnMsg[:-1]
- if endType == END_WITH_ELLIPSE: returnMsg += "..." + if endType == Ending.ELLIPSE: + returnMsg = returnMsg.rstrip() + "..."
if getRemainder: return (returnMsg, remainder) else: return returnMsg
+def drawBox(panel, top, left, width, height, attr=curses.A_NORMAL): + """ + Draws a box in the panel with the given bounds. + + Arguments: + panel - panel in which to draw + top - vertical position of the box's top + left - horizontal position of the box's left side + width - width of the drawn box + height - height of the drawn box + attr - text attributes + """ + + # draws the top and bottom + panel.hline(top, left + 1, width - 1, attr) + panel.hline(top + height - 1, left + 1, width - 1, attr) + + # draws the left and right sides + panel.vline(top + 1, left, height - 2, attr) + panel.vline(top + 1, left + width, height - 2, attr) + + # draws the corners + panel.addch(top, left, curses.ACS_ULCORNER, attr) + panel.addch(top, left + width, curses.ACS_URCORNER, attr) + panel.addch(top + height - 1, left, curses.ACS_LLCORNER, attr) + panel.addch(top + height - 1, left + width, curses.ACS_LRCORNER, attr) + +def isSelectionKey(key): + """ + Returns true if the keycode matches the enter or space keys. + + Argument: + key - keycode to be checked + """ + + return key in (curses.KEY_ENTER, 10, ord(' ')) + def isScrollKey(key): """ Returns true if the keycode is recognized by the getScrollPosition function for scrolling. + + Argument: + key - keycode to be checked """
return key in SCROLL_KEYS @@ -367,6 +409,73 @@ 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, lockFormat=False): + """ + Constructor for prepared draw entries. + + Arguments: + text - content to be drawn, this can either be a string or list of + integer character codes + format - properties to apply when drawing + nextEntry - entry to be drawn after this one + lockFormat - prevents extra formatting attributes from being applied + when rendered if true + """ + + self.text = text + self.format = format + self.nextEntry = nextEntry + self.lockFormat = lockFormat + + 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 + """ + + if self.lockFormat: drawFormat = self.format + else: drawFormat = self.format | extraFormat + + if isinstance(self.text, str): + drawPanel.addstr(y, x, self.text, drawFormat) + else: + for i in range(len(self.text)): + drawChar = self.text[i] + drawPanel.addch(y, x + i, drawChar, 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 @@ -394,10 +503,16 @@ if self.isCursorEnabled: self.getCursorSelection(content) # resets the cursor location
+ # makes sure the cursor is visible if self.cursorLoc < self.scrollLoc: self.scrollLoc = self.cursorLoc elif self.cursorLoc > self.scrollLoc + pageHeight - 1: self.scrollLoc = self.cursorLoc - pageHeight + 1 + + # checks if the bottom would run off the content (this could be the + # case when the content's size is dynamic and entries are removed) + if len(content) > pageHeight: + self.scrollLoc = min(self.scrollLoc, len(content) - pageHeight)
return self.scrollLoc
Modified: arm/release/src/version.py =================================================================== --- arm/release/src/version.py 2011-04-04 15:15:20 UTC (rev 24554) +++ arm/release/src/version.py 2011-04-04 15:22:31 UTC (rev 24555) @@ -2,6 +2,6 @@ Provides arm's version and release date. """
-VERSION = '1.4.1.3' -LAST_MODIFIED = "January 15, 2011" +VERSION = '1.4.2' +LAST_MODIFIED = "April 4, 2011"
tor-commits@lists.torproject.org