[tor-commits] [stem/master] Handler for interpretor commands

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


commit 2fad86f212566b456c1f66a159ef11714ec730d3
Author: Damian Johnson <atagar at torproject.org>
Date:   Thu Apr 10 09:12:33 2014 -0700

    Handler for interpretor commands
    
    Using a more sophisticated handler rather than simply run msg(). This adds
    '/help' and '/quit' interpretor commands as well as takes advantage of Stem's
    caching.
---
 stem/interpretor/__init__.py |    7 +-
 stem/interpretor/commands.py |  433 ++++++++++++++++++++++++++++++++++++++++++
 stem/response/__init__.py    |    1 +
 3 files changed, 439 insertions(+), 2 deletions(-)

diff --git a/stem/interpretor/__init__.py b/stem/interpretor/__init__.py
index 9d08676..d1a5359 100644
--- a/stem/interpretor/__init__.py
+++ b/stem/interpretor/__init__.py
@@ -10,6 +10,7 @@ __all__ = ['arguments']
 
 import sys
 
+import stem
 import stem.connection
 import stem.interpretor.arguments
 import stem.interpretor.commands
@@ -58,10 +59,12 @@ def main():
     readline.set_completer(tab_completer.complete)
     readline.set_completer_delims('\n')
 
+    interpretor = stem.interpretor.commands.ControlInterpretor(controller)
+
     while True:
       try:
         user_input = raw_input(PROMPT)
-        print controller.msg(user_input)
-      except (KeyboardInterrupt, EOFError) as exc:
+        print interpretor.run_command(user_input)
+      except (KeyboardInterrupt, EOFError, stem.SocketClosed) as exc:
         print  # move cursor to the following line
         break
diff --git a/stem/interpretor/commands.py b/stem/interpretor/commands.py
index 3b809b4..b326cc0 100644
--- a/stem/interpretor/commands.py
+++ b/stem/interpretor/commands.py
@@ -2,6 +2,16 @@
 Handles making requests and formatting the responses.
 """
 
+import re
+
+import stem
+
+from stem.util.term import Attr, Color, format
+
+OUTPUT_FORMAT = (Color.BLUE, )
+BOLD_OUTPUT_FORMAT = (Color.BLUE, Attr.BOLD)
+ERROR_FORMAT = (Attr.BOLD, Color.RED)
+
 TOR_CONTROLLER_COMMANDS = [
   'SAVECONF',
   'MAPADDRESS',
@@ -22,6 +32,173 @@ TOR_CONTROLLER_COMMANDS = [
   'DROPGUARDS',
 ]
 
+MULTILINE_UNIMPLEMENTED_NOTICE = "Multi-line control options like this are not yet implemented."
+
+GENERAL_HELP = """Interpretor commands include:
+  /help   - provides information for interpretor and tor commands/config options
+  /quit   - shuts down the interpretor
+
+Tor commands include:
+  GETINFO - queries information from tor
+  GETCONF, SETCONF, RESETCONF - show or edit a configuration option
+  SIGNAL - issues control signal to the process (for resetting, stopping, etc)
+  SETEVENTS - configures the events tor will notify us of
+
+  USEFEATURE - enables custom behavior for the controller
+  SAVECONF - writes tor's current configuration to our torrc
+  LOADCONF - loads the given input like it was part of our torrc
+  MAPADDRESS - replaces requests for one address with another
+  POSTDESCRIPTOR - adds a relay descriptor to our cache
+  EXTENDCIRCUIT - create or extend a tor circuit
+  SETCIRCUITPURPOSE - configures the purpose associated with a circuit
+  CLOSECIRCUIT - closes the given circuit
+  ATTACHSTREAM - associates an application's stream with a tor circuit
+  REDIRECTSTREAM - sets a stream's destination
+  CLOSESTREAM - closes the given stream
+  RESOLVE - issues an asynchronous dns or rdns request over tor
+  TAKEOWNERSHIP - instructs tor to quit when this control connection is closed
+  PROTOCOLINFO - queries version and controller authentication information
+  QUIT - disconnect the control connection
+
+For more information use '/help [OPTION]'."""
+
+HELP_HELP = """Provides usage information for the given interpretor, tor command, or tor
+configuration option.
+
+Example:
+  /help GETINFO     # usage information for tor's GETINFO controller option
+"""
+
+HELP_QUIT = """Terminates the interpretor."""
+
+HELP_GETINFO = """Queries the tor process for information. Options are...
+"""
+
+HELP_GETCONF = """Provides the current value for a given configuration value. Options include...
+"""
+
+HELP_SETCONF = """Sets the given configuration parameters. Values can be quoted or non-quoted
+strings, and reverts the option to 0 or NULL if not provided.
+
+Examples:
+  * Sets a contact address and resets our family to NULL
+    SETCONF MyFamily ContactInfo=foo at bar.com
+
+  * Sets an exit policy that only includes port 80/443
+    SETCONF ExitPolicy=\"accept *:80, accept *:443, reject *:*\"\
+"""
+
+HELP_RESETCONF = """Reverts the given configuration options to their default values. If a value
+is provided then this behaves in the same way as SETCONF.
+
+Examples:
+  * Returns both of our accounting parameters to their defaults
+    RESETCONF AccountingMax AccountingStart
+
+  * Uses the default exit policy and sets our nickname to be 'Goomba'
+    RESETCONF ExitPolicy Nickname=Goomba"""
+
+HELP_SIGNAL = """Issues a signal that tells the tor process to reload its torrc, dump its
+stats, halt, etc.
+"""
+
+SIGNAL_DESCRIPTIONS = (
+  ("RELOAD / HUP", "reload our torrc"),
+  ("SHUTDOWN / INT", "gracefully shut down, waiting 30 seconds if we're a relay"),
+  ("DUMP / USR1", "logs information about open connections and circuits"),
+  ("DEBUG / USR2", "makes us log at the DEBUG runlevel"),
+  ("HALT / TERM", "immediately shut down"),
+  ("CLEARDNSCACHE", "clears any cached DNS results"),
+  ("NEWNYM", "clears the DNS cache and uses new circuits for future connections")
+)
+
+HELP_SETEVENTS = """Sets the events that we will receive. This turns off any events that aren't
+listed so sending 'SETEVENTS' without any values will turn off all event reporting.
+
+For Tor versions between 0.1.1.9 and 0.2.2.1 adding 'EXTENDED' causes some
+events to give us additional information. After version 0.2.2.1 this is
+always on.
+
+Events include...
+
+"""
+
+HELP_USEFEATURE = """Customizes the behavior of the control port. Options include...
+"""
+
+HELP_SAVECONF = """Writes Tor's current configuration to its torrc."""
+
+HELP_LOADCONF = """Reads the given text like it belonged to our torrc.
+
+Example:
+  +LOADCONF
+  # sets our exit policy to just accept ports 80 and 443
+  ExitPolicy accept *:80
+  ExitPolicy accept *:443
+  ExitPolicy reject *:*
+  ."""
+
+HELP_MAPADDRESS = """Replaces future requests for one address with another.
+
+Example:
+  MAPADDRESS 0.0.0.0=torproject.org 1.2.3.4=tor.freehaven.net"""
+
+HELP_POSTDESCRIPTOR = """Simulates getting a new relay descriptor."""
+
+HELP_EXTENDCIRCUIT = """Extends the given circuit or create a new one if the CircuitID is zero. The
+PATH is a comma separated list of fingerprints. If it isn't set then this
+uses Tor's normal path selection."""
+
+HELP_SETCIRCUITPURPOSE = """Sets the purpose attribute for a circuit."""
+
+HELP_CLOSECIRCUIT = """Closes the given circuit. If "IfUnused" is included then this only closes
+the circuit if it isn't currently being used."""
+
+HELP_ATTACHSTREAM = """Attaches a stream with the given built circuit (tor picks one on its own if
+CircuitID is zero). If HopNum is given then this hop is used to exit the
+circuit, otherwise the last relay is used."""
+
+HELP_REDIRECTSTREAM = """Sets the destination for a given stream. This can only be done after a
+stream is created but before it's attached to a circuit."""
+
+HELP_CLOSESTREAM = """Closes the given stream, the reason being an integer matching a reason as
+per section 6.3 of the tor-spec."""
+
+HELP_RESOLVE = """Performs IPv4 DNS resolution over tor, doing a reverse lookup instead if
+"mode=reverse" is included. This request is processed in the background and
+results in a ADDRMAP event with the response."""
+
+HELP_TAKEOWNERSHIP = """Instructs Tor to gracefully shut down when this control connection is closed."""
+
+HELP_PROTOCOLINFO = """Provides bootstrapping information that a controller might need when first
+starting, like Tor's version and controller authentication. This can be done
+before authenticating to the control port."""
+
+HELP_OPTIONS = {
+  "HELP": ("/help [OPTION]", HELP_HELP),
+  "QUIT": ("/quit", HELP_QUIT),
+  "GETINFO": ("GETINFO OPTION", HELP_GETINFO),
+  "GETCONF": ("GETCONF OPTION", HELP_GETCONF),
+  "SETCONF": ("SETCONF PARAM[=VALUE]", HELP_SETCONF),
+  "RESETCONF": ("RESETCONF PARAM[=VALUE]", HELP_RESETCONF),
+  "SIGNAL": ("SIGNAL SIG", HELP_SIGNAL),
+  "SETEVENTS": ("SETEVENTS [EXTENDED] [EVENTS]", HELP_SETEVENTS),
+  "USEFEATURE": ("USEFEATURE OPTION", HELP_USEFEATURE),
+  "SAVECONF": ("SAVECONF", HELP_SAVECONF),
+  "LOADCONF": ("LOADCONF...", HELP_LOADCONF),
+  "MAPADDRESS": ("MAPADDRESS SOURCE_ADDR=DESTINATION_ADDR", HELP_MAPADDRESS),
+  "POSTDESCRIPTOR": ("POSTDESCRIPTOR [purpose=general/controller/bridge] [cache=yes/no]...", HELP_POSTDESCRIPTOR),
+  "EXTENDCIRCUIT": ("EXTENDCIRCUIT CircuitID [PATH] [purpose=general/controller]", HELP_EXTENDCIRCUIT),
+  "SETCIRCUITPURPOSE": ("SETCIRCUITPURPOSE CircuitID purpose=general/controller", HELP_SETCIRCUITPURPOSE),
+  "CLOSECIRCUIT": ("CLOSECIRCUIT CircuitID [IfUnused]", HELP_CLOSECIRCUIT),
+  "ATTACHSTREAM": ("ATTACHSTREAM StreamID CircuitID [HOP=HopNum]", HELP_ATTACHSTREAM),
+  "REDIRECTSTREAM": ("REDIRECTSTREAM StreamID Address [Port]", HELP_REDIRECTSTREAM),
+  "CLOSESTREAM": ("CLOSESTREAM StreamID Reason [Flag]", HELP_CLOSESTREAM),
+  "RESOLVE": ("RESOLVE [mode=reverse] address", HELP_RESOLVE),
+  "TAKEOWNERSHIP": ("TAKEOWNERSHIP", HELP_TAKEOWNERSHIP),
+  "PROTOCOLINFO": ("PROTOCOLINFO [ProtocolVersion]", HELP_PROTOCOLINFO),
+}
+
 
 def _get_commands(controller):
   """
@@ -86,6 +263,19 @@ def _get_commands(controller):
   else:
     commands.append('SIGNAL ')
 
+  # adds interpretor commands
+
+  for cmd in HELP_OPTIONS:
+    if HELP_OPTIONS[cmd][0].startswith('/'):
+      commands.append('/' + cmd.lower())
+
+  # adds help options for the previous commands
+
+  base_cmd = set([cmd.split(' ')[0].replace('+', '').replace('/', '') for cmd in commands])
+
+  for cmd in base_cmd:
+    commands.append('/help ' + cmd)
+
   return commands
 
 
@@ -106,3 +296,246 @@ class Autocomplete(object):
       return prefix_matches[state]
     else:
       return None
+
+
+class ControlInterpretor(object):
+  """
+  Handles issuing requests and providing nicely formed responses, with support
+  for special irc style subcommands.
+  """
+
+  def __init__(self, controller):
+    self.controller = controller
+
+  def do_help(self, arg):
+    """
+    Performs the '/help' operation, giving usage information for the given
+    argument or a general summary if there wasn't one.
+    """
+
+    arg = arg.upper()
+
+    # If there's multiple arguments then just take the first. This is
+    # particularly likely if they're trying to query a full command (for
+    # instance "/help GETINFO version")
+
+    arg = arg.split(' ')[0]
+
+    # strip slash if someone enters an interpretor command (ex. "/help /help")
+
+    if arg.startswith('/'):
+      arg = arg[1:]
+
+    output = ''
+
+    if not arg:
+      # provides the GENERAL_HELP with everything bolded except descriptions
+
+      for line in GENERAL_HELP.splitlines():
+        cmd_start = line.find(' - ')
+
+        if cmd_start != -1:
+          output += format(line[:cmd_start], *BOLD_OUTPUT_FORMAT)
+          output += format(line[cmd_start:] + '\n', *OUTPUT_FORMAT)
+        else:
+          output += format(line + '\n', *BOLD_OUTPUT_FORMAT)
+    elif arg in HELP_OPTIONS:
+      # Provides information for the tor or interpretor argument. This bolds
+      # the usage information and indents the description after it.
+
+      usage, description = HELP_OPTIONS[arg]
+
+      output = format(usage + '\n', *BOLD_OUTPUT_FORMAT)
+
+      for line in description.splitlines():
+        output += format('  ' + line + '\n', *OUTPUT_FORMAT)
+
+      if arg == 'GETINFO':
+        # if this is the GETINFO option then also list the valid options
+
+        info_options = self.controller.get_info('info/names', None)
+
+        if info_options:
+          for line in info_options.splitlines():
+            line_match = re.match("^(.+) -- (.+)$", line)
+
+            if line_match:
+              opt, description = line_match.groups()
+
+              output += format("%-33s" % opt, *BOLD_OUTPUT_FORMAT)
+              output += format(" - %s\n" % description, *OUTPUT_FORMAT)
+      elif arg == 'GETCONF':
+        # lists all of the configuration options
+        # TODO: integrate tor man page output when stem supports that
+
+        conf_options = self.controller.get_info('config/names', None)
+
+        if conf_options:
+          conf_entries = [opt.split(' ', 1)[0] for opt in conf_options.split('\n')]
+
+          # displays two columns of 42 characters
+
+          for i in range(0, len(conf_entries), 2):
+            line_entries = conf_entries[i:i + 2]
+
+            line_content = ''
+
+            for entry in line_entries:
+              line_content += '%-42s' % entry
+
+            output += format(line_content + '\n', *OUTPUT_FORMAT)
+
+          output += format("For more information use '/help [CONFIG OPTION]'.", *BOLD_OUTPUT_FORMAT)
+      elif arg == 'SIGNAL':
+        # lists descriptions for all of the signals
+
+        for signal, description in SIGNAL_DESCRIPTIONS:
+          output += format('%-15s' % signal, *BOLD_OUTPUT_FORMAT)
+          output += format(' - %s\n' % description, *OUTPUT_FORMAT)
+      elif arg == 'SETEVENTS':
+        # lists all of the event types
+
+        event_options = self.controller.get_info('events/names', None)
+
+        if event_options:
+          event_entries = event_options.split()
+
+          # displays four columns of 20 characters
+
+          for i in range(0, len(event_entries), 4):
+            line_entries = event_entries[i:i + 4]
+
+            line_content = ''
+
+            for entry in line_entries:
+              line_content += '%-20s' % entry
+
+            output += format(line_content + '\n', *OUTPUT_FORMAT)
+      elif arg == 'USEFEATURE':
+        # lists the feature options
+
+        feature_options = self.controller.get_info('features/names', None)
+
+        if feature_options:
+          output += format(feature_options + '\n', *OUTPUT_FORMAT)
+      elif arg in ('LOADCONF', 'POSTDESCRIPTOR'):
+        # gives a warning that this option isn't yet implemented
+        output += format('\n' + MULTILINE_UNIMPLEMENTED_NOTICE + '\n', *ERROR_FORMAT)
+    else:
+      output += format("No help information available for '%s'..." % arg, *ERROR_FORMAT)
+
+    return output
+
+  def run_command(self, command):
+    """
+    Runs the given command. Requests starting with a '/' are special commands
+    to the interpretor, and anything else is sent to the control port.
+
+    :param stem.control.Controller controller: tor control connection
+    :param str command: command to be processed
+
+    :returns: **list** out output lines, each line being a list of
+      (msg, format) tuples
+
+    :raises: **stem.SocketClosed** if the control connection has been severed
+    """
+
+    if not self.controller.is_alive():
+      raise stem.SocketClosed()
+
+    command = command.strip()
+
+    # Commands fall into three categories:
+    #
+    # * Interpretor commands. These start with a '/'.
+    #
+    # * Controller commands stem knows how to handle. We use our Controller's
+    #   methods for these to take advantage of caching and present nicer
+    #   output.
+    #
+    # * Other tor commands. We pass these directly on to the control port.
+
+    if ' ' in command:
+      cmd, arg = command.split(' ', 1)
+    else:
+      cmd, arg = command, ''
+
+    output = ''
+
+    if cmd.startswith('/'):
+      if cmd == "/quit":
+        raise stem.SocketClosed()
+      elif cmd == "/help":
+        output = self.do_help(arg)
+      else:
+        output = format("'%s' isn't a recognized command" % command, *ERROR_FORMAT)
+
+      output += '\n'  # give ourselves an extra line before the next prompt
+    else:
+      cmd = cmd.upper()  # makes commands uppercase to match the spec
+
+      if cmd == 'GETINFO':
+        try:
+          response = self.controller.get_info(arg.split())
+          output = format('\n'.join(response.values()), *OUTPUT_FORMAT)
+        except stem.ControllerError as exc:
+          output = format(str(exc), *ERROR_FORMAT)
+      elif cmd in ('SETCONF', 'RESETCONF'):
+        # arguments can either be '<param>', '<param>=<value>', or
+        # '<param>="<value>"' entries
+
+        param_list = []
+
+        while arg:
+          # TODO: I'm a little dubious of this for LineList values (like the
+          # ExitPolicy) since they're parsed as a single value. However, tor
+          # seems to be happy to get a single comma separated string (though it
+          # echos back faithfully rather than being parsed) so leaving this
+          # alone for now.
+
+          quoted_match = re.match(r'^(\S+)=\"([^"]+)\"', arg)
+          nonquoted_match = re.match(r'^(\S+)=(\S+)', arg)
+
+          if quoted_match:
+            # we're dealing with a '<param>="<value>"' entry
+            param, value = quoted_match.groups()
+
+            param_list.append((param, value))
+            arg = arg[len(param) + len(value) + 3:].strip()
+          elif nonquoted_match:
+            # we're dealing with a '<param>=<value>' entry
+            param, value = nonquoted_match.groups()
+
+            param_list.append((param, value))
+            arg = arg[len(param) + len(value) + 1:].strip()
+          else:
+            # starts with just a param
+            param = arg.split()[0]
+            param_list.append((param, None))
+            arg = arg[len(param):].strip()
+
+        try:
+          is_reset = cmd == 'RESETCONF'
+          self.controller.set_options(param_list, is_reset)
+        except stem.ControllerError as exc:
+          output = format(str(exc), *ERROR_FORMAT)
+      elif cmd == 'SETEVENTS':
+        pass  # TODO: implement
+      elif cmd.replace('+', '') in ('LOADCONF', 'POSTDESCRIPTOR'):
+        # provides a notice that multi-line controller input isn't yet implemented
+        output = format(MULTILINE_UNIMPLEMENTED_NOTICE, *ERROR_FORMAT)
+      else:
+        try:
+          response = self.controller.msg(command)
+
+          if cmd == 'QUIT':
+            raise stem.SocketClosed()
+
+          output = format(str(response), *OUTPUT_FORMAT)
+        except stem.ControllerError as exc:
+          if isinstance(exc, stem.SocketClosed):
+            raise exc
+          else:
+            output = format(str(exc), *ERROR_FORMAT)
+
+    return output
diff --git a/stem/response/__init__.py b/stem/response/__init__.py
index 39e39cb..31f96e3 100644
--- a/stem/response/__init__.py
+++ b/stem/response/__init__.py
@@ -12,6 +12,7 @@ Parses replies from the control socket.
 
   ControlMessage - Message that's read from the control socket.
     |- from_str - provides a ControlMessage for the given string
+    |- is_ok - response had a 250 status
     |- content - provides the parsed message content
     |- raw_content - unparsed socket data
     |- __str__ - content stripped of protocol formatting





More information about the tor-commits mailing list