[tor-commits] [stem/master] Supporting the /info option

atagar at torproject.org atagar at torproject.org
Tue May 6 01:21:13 UTC 2014


commit ca331a6fba5ead083c4a010b72e5d4886396e7a1
Author: Damian Johnson <atagar at torproject.org>
Date:   Wed Apr 16 10:15:18 2014 -0700

    Supporting the /info option
    
    Adding support for a /info option that provides a simple summary of a relay's
    information. This accepts a fingerprint, nickname, or IP address. If there's a
    match this provides basic information in a similar fashion to arm.
---
 stem/interpretor/commands.py  |  132 ++++++++++++++++++++++++++++++++++++++++-
 stem/interpretor/settings.cfg |    6 ++
 2 files changed, 135 insertions(+), 3 deletions(-)

diff --git a/stem/interpretor/commands.py b/stem/interpretor/commands.py
index c711ade..2d2d0a5 100644
--- a/stem/interpretor/commands.py
+++ b/stem/interpretor/commands.py
@@ -6,8 +6,10 @@ import os
 import re
 
 import stem
+import stem.util.connection
 import stem.util.conf
 import stem.util.log
+import stem.util.tor_tools
 
 from stem.util.term import Attr, Color, format
 
@@ -48,6 +50,7 @@ SIGNAL_DESCRIPTIONS = (
 HELP_OPTIONS = {
   'HELP': ("/help [OPTION]", 'help.help'),
   'EVENTS': ("/events [types]", 'help.events'),
+  'INFO': ("/info [relay fingerprint, nickname, or IP address]", 'help.info'),
   'QUIT': ("/quit", 'help.quit'),
   'GETINFO': ("GETINFO OPTION", 'help.getinfo'),
   'GETCONF': ("GETCONF OPTION", 'help.getconf'),
@@ -341,6 +344,127 @@ class ControlInterpretor(object):
 
     return '\n'.join([format(str(event), *OUTPUT_FORMAT) for event in events])
 
+  def do_info(self, arg):
+    """
+    Performs the '/info' operation, looking up a relay by fingerprint, IP
+    address, or nickname and printing its descriptor and consensus entries in a
+    pretty fashion.
+    """
+
+    output, fingerprint = '', None
+
+    # determines the fingerprint, leaving it unset and adding an error message
+    # if unsuccessful
+
+    if not arg:
+      # uses our fingerprint if we're a relay, otherwise gives an error
+
+      fingerprint = self.controller.get_info('fingerprint', None)
+
+      if not fingerprint:
+        output += format("We aren't a relay, no information to provide", *ERROR_FORMAT)
+    elif stem.util.tor_tools.is_valid_fingerprint(arg):
+      fingerprint = arg
+    elif stem.util.tor_tools.is_valid_nickname(arg):
+      desc = self.controller.get_network_status(arg, None)
+
+      if desc:
+        fingerprint = desc.fingerprint
+      else:
+        return format("Unable to find a relay with the nickname of '%s'" % arg, *ERROR_FORMAT)
+    elif ':' in arg or stem.util.connection.is_valid_ipv4_address(arg):
+      # we got an address, so looking up the fingerprint
+
+      if ':' in arg:
+        address, port = arg.split(':', 1)
+
+        if not stem.util.connection.is_valid_ipv4_address(address):
+          return format("'%s' isn't a valid IPv4 address" % address, *ERROR_FORMAT)
+        elif port and not stem.util.connection.is_valid_port(port):
+          return format("'%s' isn't a valid port" % port, *ERROR_FORMAT)
+
+        port = int(port)
+      else:
+        address, port = arg, None
+
+      matches = {}
+
+      for desc in self.controller.get_network_statuses():
+        if desc.address == address:
+          if not port or desc.or_port == port:
+            matches[desc.or_port] = desc.fingerprint
+
+      if len(matches) == 0:
+        output += format('No relays found at %s' % arg, *ERROR_FORMAT)
+      elif len(matches) == 1:
+        fingerprint = matches.values()[0]
+      else:
+        output += format("There's multiple relays at %s, include a port to specify which.\n\n" % arg, *ERROR_FORMAT)
+
+        for i, or_port in enumerate(matches):
+          output += format("  %i. %s:%s, fingerprint: %s\n" % (i + 1, address, or_port, matches[or_port]), *ERROR_FORMAT)
+    else:
+      return format("'%s' isn't a fingerprint, nickname, or IP address" % arg, *ERROR_FORMAT)
+
+    if fingerprint:
+      micro_desc = self.controller.get_microdescriptor(fingerprint, None)
+      server_desc = self.controller.get_server_descriptor(fingerprint, None)
+      ns_desc = self.controller.get_network_status(fingerprint, None)
+
+      # We'll mostly rely on the router status entry. Either the server
+      # descriptor or microdescriptor will be missing, so we'll treat them as
+      # being optional.
+
+      if not ns_desc:
+        return format("Unable to find consensus information for %s" % fingerprint, *ERROR_FORMAT)
+
+      locale = self.controller.get_info('ip-to-country/%s' % ns_desc.address, None)
+      locale_label = ' (%s)' % locale if locale else ''
+
+      if server_desc:
+        exit_policy_label = server_desc.exit_policy.summary()
+      elif micro_desc:
+        exit_policy_label = micro_desc.exit_policy.summary()
+      else:
+        exit_policy_label = 'Unknown'
+
+      output += '%s (%s)\n' % (ns_desc.nickname, fingerprint)
+
+      output += format('address: ', *BOLD_OUTPUT_FORMAT)
+      output += '%s:%s%s\n' % (ns_desc.address, ns_desc.or_port, locale_label)
+
+      output += format('published: ', *BOLD_OUTPUT_FORMAT)
+      output += ns_desc.published.strftime('%H:%M:%S %d/%m/%Y') + '\n'
+
+      if server_desc:
+        output += format('os: ', *BOLD_OUTPUT_FORMAT)
+        output += server_desc.platform.decode('utf-8', 'replace') + '\n'
+
+        output += format('version: ', *BOLD_OUTPUT_FORMAT)
+        output += str(server_desc.tor_version) + '\n'
+
+      output += format('flags: ', *BOLD_OUTPUT_FORMAT)
+      output += ', '.join(ns_desc.flags) + '\n'
+
+      output += format('exit policy: ', *BOLD_OUTPUT_FORMAT)
+      output += exit_policy_label + '\n'
+
+      if server_desc:
+        contact = server_desc.contact
+
+        # clears up some highly common obscuring
+
+        for alias in (' at ', ' AT '):
+          contact = contact.replace(alias, '@')
+
+        for alias in (' dot ', ' DOT '):
+          contact = contact.replace(alias, '.')
+
+        output += format('contact: ', *BOLD_OUTPUT_FORMAT)
+        output += contact + '\n'
+
+    return output.strip()
+
   def run_command(self, command):
     """
     Runs the given command. Requests starting with a '/' are special commands
@@ -378,11 +502,13 @@ class ControlInterpretor(object):
     output = ''
 
     if cmd.startswith('/'):
-      if cmd == "/quit":
+      if cmd == '/quit':
         raise stem.SocketClosed()
-      elif cmd == "/events":
+      elif cmd == '/events':
         output = self.do_events(arg)
-      elif cmd == "/help":
+      elif cmd == '/info':
+        output = self.do_info(arg)
+      elif cmd == '/help':
         output = self.do_help(arg)
       else:
         output = format("'%s' isn't a recognized command" % command, *ERROR_FORMAT)
diff --git a/stem/interpretor/settings.cfg b/stem/interpretor/settings.cfg
index e93b0e8..9430748 100644
--- a/stem/interpretor/settings.cfg
+++ b/stem/interpretor/settings.cfg
@@ -16,6 +16,7 @@ help.general
 |Interpretor commands include:
 |  /help   - provides information for interpretor and tor commands/config options
 |  /events - prints events that we've received
+|  /info   - general information for a relay
 |  /quit   - shuts down the interpretor
 |
 |Tor commands include:
@@ -47,6 +48,7 @@ help.help
 |configuration option.
 |
 |Example:
+|  /help info        # provides a description of the '/info' option
 |  /help GETINFO     # usage information for tor's GETINFO controller option
 
 help.events
@@ -54,6 +56,10 @@ help.events
 |no types are specified then this provides all the messages that we've
 |received.
 
+help.info
+|Provides general information for a relay that's currently in the consensus.
+|If no relay is specified then this provides information on ourselves.
+
 help.getinfo
 |Queries the tor process for information. Options are...
 |





More information about the tor-commits mailing list