[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