[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [stem/master] Fixing interpretor misspelling
commit c5a7bbe46555f9a922639942561786c7155ca4f0
Author: Damian Johnson <atagar@xxxxxxxxxxxxxx>
Date: Thu May 22 09:15:51 2014 -0700
Fixing interpretor misspelling
Huh. Not sure why my spell checker thought 'interpretor' was ok. Thanks to
Yawning for pointing this out.
---
docs/change_log.rst | 2 +-
setup.py | 2 +-
stem/interpreter/__init__.py | 129 ++++++++++++++
stem/interpreter/arguments.py | 96 +++++++++++
stem/interpreter/autocomplete.py | 112 ++++++++++++
stem/interpreter/commands.py | 299 +++++++++++++++++++++++++++++++++
stem/interpreter/help.py | 142 ++++++++++++++++
stem/interpreter/settings.cfg | 295 ++++++++++++++++++++++++++++++++
stem/interpretor/__init__.py | 129 --------------
stem/interpretor/arguments.py | 96 -----------
stem/interpretor/autocomplete.py | 112 ------------
stem/interpretor/commands.py | 299 ---------------------------------
stem/interpretor/help.py | 142 ----------------
stem/interpretor/settings.cfg | 295 --------------------------------
stem/util/system.py | 2 +-
test/settings.cfg | 8 +-
test/unit/interpreter/__init__.py | 39 +++++
test/unit/interpreter/arguments.py | 57 +++++++
test/unit/interpreter/autocomplete.py | 112 ++++++++++++
test/unit/interpreter/commands.py | 198 ++++++++++++++++++++++
test/unit/interpreter/help.py | 54 ++++++
test/unit/interpretor/__init__.py | 39 -----
test/unit/interpretor/arguments.py | 57 -------
test/unit/interpretor/autocomplete.py | 112 ------------
test/unit/interpretor/commands.py | 198 ----------------------
test/unit/interpretor/help.py | 54 ------
tor-prompt | 4 +-
27 files changed, 1542 insertions(+), 1542 deletions(-)
diff --git a/docs/change_log.rst b/docs/change_log.rst
index f89de76..ef9005d 100644
--- a/docs/change_log.rst
+++ b/docs/change_log.rst
@@ -66,7 +66,7 @@ The following are only available within Stem's `git repository
* **Interpretor**
- * Initial release of a Tor interactive interpretor. This included...
+ * Initial release of a Tor interactive interpreter. This included...
* irc-style functions such as '/help' and '/info'
* history scroll-back by pressing up/down
diff --git a/setup.py b/setup.py
index 47eb478..45071e5 100644
--- a/setup.py
+++ b/setup.py
@@ -49,7 +49,7 @@ setup(
author = module_info['author'],
author_email = module_info['contact'],
url = module_info['url'],
- packages = ['stem', 'stem.descriptor', 'stem.interpretor', 'stem.response', 'stem.util'],
+ packages = ['stem', 'stem.descriptor', 'stem.interpreter', 'stem.response', 'stem.util'],
provides = ['stem'],
cmdclass = {'build_py': build_py},
keywords = 'tor onion controller',
diff --git a/stem/interpreter/__init__.py b/stem/interpreter/__init__.py
new file mode 100644
index 0000000..23453a3
--- /dev/null
+++ b/stem/interpreter/__init__.py
@@ -0,0 +1,129 @@
+# Copyright 2014, Damian Johnson and The Tor Project
+# See LICENSE for licensing information
+
+"""
+Interactive interpreter for interacting with Tor directly. This adds usability
+features such as tab completion, history, and IRC-style functions (like /help).
+"""
+
+__all__ = ['arguments', 'autocomplete', 'commands', 'help', 'msg']
+
+import os
+import sys
+
+import stem
+import stem.connection
+import stem.process
+import stem.util.conf
+import stem.util.system
+import stem.util.term
+
+from stem.util.term import RESET, Attr, Color, format
+
+# Our color prompt triggers a bug between raw_input() and readline history,
+# where scrolling through history widens our prompt. Widening our prompt via
+# invisible characters (like resets) seems to sidestep this bug for short
+# inputs. Contrary to the ticket, this still manifests with python 2.7.1...
+#
+# http://bugs.python.org/issue12972
+
+PROMPT = format('>>> ', Color.GREEN, Attr.BOLD) + RESET * 10
+
+STANDARD_OUTPUT = (Color.BLUE, )
+BOLD_OUTPUT = (Color.BLUE, Attr.BOLD)
+HEADER_OUTPUT = (Color.GREEN, )
+HEADER_BOLD_OUTPUT = (Color.GREEN, Attr.BOLD)
+ERROR_OUTPUT = (Attr.BOLD, Color.RED)
+
+settings_path = os.path.join(os.path.dirname(__file__), 'settings.cfg')
+uses_settings = stem.util.conf.uses_settings('stem_interpreter', settings_path)
+
+
+@uses_settings
+def msg(message, config, **attr):
+ return config.get(message).format(**attr)
+
+
+def main():
+ import readline
+
+ import stem.interpreter.arguments
+ import stem.interpreter.autocomplete
+ import stem.interpreter.commands
+
+ try:
+ args = stem.interpreter.arguments.parse(sys.argv[1:])
+ except ValueError as exc:
+ print exc
+ sys.exit(1)
+
+ if args.print_help:
+ print stem.interpreter.arguments.get_help()
+ sys.exit()
+
+ if args.disable_color:
+ global PROMPT
+ stem.util.term.DISABLE_COLOR_SUPPORT = True
+ PROMPT = '>>> '
+
+ # If the user isn't connecting to something in particular then offer to start
+ # tor if it isn't running.
+
+ if not (args.user_provided_port or args.user_provided_socket):
+ is_tor_running = stem.util.system.is_running('tor') or stem.util.system.is_running('tor.real')
+
+ if not is_tor_running:
+ if not stem.util.system.is_available('tor'):
+ print format(msg('msg.tor_unavailable'), *ERROR_OUTPUT)
+ sys.exit(1)
+ else:
+ print format(msg('msg.starting_tor'), *HEADER_OUTPUT)
+
+ stem.process.launch_tor_with_config(
+ config = {
+ 'SocksPort': '0',
+ 'ControlPort': str(args.control_port),
+ 'CookieAuthentication': '1',
+ 'ExitPolicy': 'reject *:*',
+ },
+ completion_percent = 5,
+ take_ownership = True,
+ )
+
+ control_port = None if args.user_provided_socket else (args.control_address, args.control_port)
+ control_socket = None if args.user_provided_port else args.control_socket
+
+ controller = stem.connection.connect(
+ control_port = control_port,
+ control_socket = control_socket,
+ password_prompt = True,
+ )
+
+ if controller is None:
+ sys.exit(1)
+
+ with controller:
+ autocompleter = stem.interpreter.autocomplete.Autocompleter(controller)
+ readline.parse_and_bind('tab: complete')
+ readline.set_completer(autocompleter.complete)
+ readline.set_completer_delims('\n')
+
+ interpreter = stem.interpreter.commands.ControlInterpretor(controller)
+
+ for line in msg('msg.startup_banner').splitlines():
+ line_format = HEADER_BOLD_OUTPUT if line.startswith(' ') else HEADER_OUTPUT
+ print format(line, *line_format)
+
+ print
+
+ while True:
+ try:
+ prompt = '... ' if interpreter.is_multiline_context else PROMPT
+ user_input = raw_input(prompt)
+ response = interpreter.run_command(user_input)
+
+ if response is not None:
+ print response
+ except (KeyboardInterrupt, EOFError, stem.SocketClosed) as exc:
+ print # move cursor to the following line
+ break
diff --git a/stem/interpreter/arguments.py b/stem/interpreter/arguments.py
new file mode 100644
index 0000000..d62a386
--- /dev/null
+++ b/stem/interpreter/arguments.py
@@ -0,0 +1,96 @@
+# Copyright 2014, Damian Johnson and The Tor Project
+# See LICENSE for licensing information
+
+"""
+Commandline argument parsing for our interpreter prompt.
+"""
+
+import collections
+import getopt
+
+import stem.interpreter
+import stem.util.connection
+
+DEFAULT_ARGS = {
+ 'control_address': '127.0.0.1',
+ 'control_port': 9051,
+ 'user_provided_port': False,
+ 'control_socket': '/var/run/tor/control',
+ 'user_provided_socket': False,
+ 'disable_color': False,
+ 'print_help': False,
+}
+
+OPT = 'i:s:h'
+
+OPT_EXPANDED = [
+ 'interface=',
+ 'socket=',
+ 'no-color',
+ 'help',
+]
+
+
+def parse(argv):
+ """
+ Parses our arguments, providing a named tuple with their values.
+
+ :param list argv: input arguments to be parsed
+
+ :returns: a **named tuple** with our parsed arguments
+
+ :raises: **ValueError** if we got an invalid argument
+ """
+
+ args = dict(DEFAULT_ARGS)
+
+ try:
+ getopt_results = getopt.getopt(argv, OPT, OPT_EXPANDED)[0]
+ except getopt.GetoptError as exc:
+ raise ValueError('%s (for usage provide --help)' % exc)
+
+ for opt, arg in getopt_results:
+ if opt in ('-i', '--interface'):
+ if ':' in arg:
+ address, port = arg.split(':', 1)
+ else:
+ address, port = None, arg
+
+ if address is not None:
+ if not stem.util.connection.is_valid_ipv4_address(address):
+ raise ValueError("'%s' isn't a valid IPv4 address" % address)
+
+ args['control_address'] = address
+
+ if not stem.util.connection.is_valid_port(port):
+ raise ValueError("'%s' isn't a valid port number" % port)
+
+ args['control_port'] = int(port)
+ args['user_provided_port'] = True
+ elif opt in ('-s', '--socket'):
+ args['control_socket'] = arg
+ args['user_provided_socket'] = True
+ elif opt == '--no-color':
+ args['disable_color'] = True
+ elif opt in ('-h', '--help'):
+ args['print_help'] = True
+
+ # translates our args dict into a named tuple
+
+ Args = collections.namedtuple('Args', args.keys())
+ return Args(**args)
+
+
+def get_help():
+ """
+ Provides our --help usage information.
+
+ :returns: **str** with our usage information
+ """
+
+ return stem.interpreter.msg(
+ 'msg.help',
+ address = DEFAULT_ARGS['control_address'],
+ port = DEFAULT_ARGS['control_port'],
+ socket = DEFAULT_ARGS['control_socket'],
+ )
diff --git a/stem/interpreter/autocomplete.py b/stem/interpreter/autocomplete.py
new file mode 100644
index 0000000..3a9b40b
--- /dev/null
+++ b/stem/interpreter/autocomplete.py
@@ -0,0 +1,112 @@
+"""
+Tab completion for our interpreter prompt.
+"""
+
+from stem.interpreter import uses_settings
+
+try:
+ # added in python 3.2
+ from functools import lru_cache
+except ImportError:
+ from stem.util.lru_cache import lru_cache
+
+
+@uses_settings
+def _get_commands(controller, config):
+ """
+ Provides commands recognized by tor.
+ """
+
+ commands = config.get('autocomplete', [])
+
+ if controller is None:
+ return commands
+
+ # GETINFO commands. Lines are of the form '[option] -- [description]'. This
+ # strips '*' from options that accept values.
+
+ results = controller.get_info('info/names', None)
+
+ if results:
+ for line in results.splitlines():
+ option = line.split(' ', 1)[0].rstrip('*')
+ commands.append('GETINFO %s' % option)
+ else:
+ commands.append('GETINFO ')
+
+ # GETCONF, SETCONF, and RESETCONF commands. Lines are of the form
+ # '[option] [type]'.
+
+ results = controller.get_info('config/names', None)
+
+ if results:
+ for line in results.splitlines():
+ option = line.split(' ', 1)[0]
+
+ commands.append('GETCONF %s' % option)
+ commands.append('SETCONF %s' % option)
+ commands.append('RESETCONF %s' % option)
+ else:
+ commands += ['GETCONF ', 'SETCONF ', 'RESETCONF ']
+
+ # SETEVENT, USEFEATURE, and SIGNAL commands. For each of these the GETINFO
+ # results are simply a space separated lists of the values they can have.
+
+ options = (
+ ('SETEVENTS ', 'events/names'),
+ ('USEFEATURE ', 'features/names'),
+ ('SIGNAL ', 'signal/names'),
+ )
+
+ for prefix, getinfo_cmd in options:
+ results = controller.get_info(getinfo_cmd, None)
+
+ if results:
+ commands += [prefix + value for value in results.split()]
+ else:
+ commands.append(prefix)
+
+ # Adds /help commands.
+
+ usage_info = config.get('help.usage', {})
+
+ for cmd in usage_info.keys():
+ commands.append('/help ' + cmd)
+
+ return commands
+
+
+class Autocompleter(object):
+ def __init__(self, controller):
+ self._commands = _get_commands(controller)
+
+ @lru_cache()
+ def matches(self, text):
+ """
+ Provides autocompletion matches for the given text.
+
+ :param str text: text to check for autocompletion matches with
+
+ :returns: **list** with possible matches
+ """
+
+ lowercase_text = text.lower()
+ return [cmd for cmd in self._commands if cmd.lower().startswith(lowercase_text)]
+
+ def complete(self, text, state):
+ """
+ Provides case insensetive autocompletion options, acting as a functor for
+ the readlines set_completer function.
+
+ :param str text: text to check for autocompletion matches with
+ :param int state: index of result to be provided, readline fetches matches
+ until this function provides None
+
+ :returns: **str** with the autocompletion match, **None** if eithe none
+ exists or state is higher than our number of matches
+ """
+
+ try:
+ return self.matches(text)[state]
+ except IndexError:
+ return None
diff --git a/stem/interpreter/commands.py b/stem/interpreter/commands.py
new file mode 100644
index 0000000..28b3e59
--- /dev/null
+++ b/stem/interpreter/commands.py
@@ -0,0 +1,299 @@
+"""
+Handles making requests and formatting the responses.
+"""
+
+import code
+
+import stem
+import stem.control
+import stem.interpreter.help
+import stem.util.connection
+import stem.util.tor_tools
+
+from stem.interpreter import STANDARD_OUTPUT, BOLD_OUTPUT, ERROR_OUTPUT, uses_settings, msg
+from stem.util.term import format
+
+
+def _get_fingerprint(arg, controller):
+ """
+ Resolves user input into a relay fingerprint. This accepts...
+
+ * Fingerprints
+ * Nicknames
+ * IPv4 addresses, either with or without an ORPort
+ * Empty input, which is resolved to ourselves if we're a relay
+
+ :param str arg: input to be resolved to a relay fingerprint
+ :param stem.control.Controller controller: tor control connection
+
+ :returns: **str** for the relay fingerprint
+
+ :raises: **ValueError** if we're unable to resolve the input to a relay
+ """
+
+ if not arg:
+ try:
+ return controller.get_info('fingerprint')
+ except:
+ raise ValueError("We aren't a relay, no information to provide")
+ elif stem.util.tor_tools.is_valid_fingerprint(arg):
+ return arg
+ elif stem.util.tor_tools.is_valid_nickname(arg):
+ try:
+ return controller.get_network_status(arg).fingerprint
+ except:
+ raise ValueError("Unable to find a relay with the nickname of '%s'" % arg)
+ elif ':' in arg or stem.util.connection.is_valid_ipv4_address(arg):
+ if ':' in arg:
+ address, port = arg.split(':', 1)
+
+ if not stem.util.connection.is_valid_ipv4_address(address):
+ raise ValueError("'%s' isn't a valid IPv4 address" % address)
+ elif port and not stem.util.connection.is_valid_port(port):
+ raise ValueError("'%s' isn't a valid port" % port)
+
+ port = int(port)
+ else:
+ address, port = arg, None
+
+ matches = {}
+
+ for desc in 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:
+ raise ValueError('No relays found at %s' % arg)
+ elif len(matches) == 1:
+ return matches.values()[0]
+ else:
+ response = "There's multiple relays at %s, include a port to specify which.\n\n" % arg
+
+ for i, or_port in enumerate(matches):
+ response += ' %i. %s:%s, fingerprint: %s\n' % (i + 1, address, or_port, matches[or_port])
+
+ raise ValueError(response)
+ else:
+ raise ValueError("'%s' isn't a fingerprint, nickname, or IP address" % arg)
+
+
+class ControlInterpretor(code.InteractiveConsole):
+ """
+ Handles issuing requests and providing nicely formed responses, with support
+ for special irc style subcommands.
+ """
+
+ def __init__(self, controller):
+ self._received_events = []
+
+ code.InteractiveConsole.__init__(self, {
+ 'stem': stem,
+ 'stem.control': stem.control,
+ 'controller': controller,
+ 'events': self._received_events
+ })
+
+ self._controller = controller
+ self._run_python_commands = True
+
+ # Indicates if we're processing a multiline command, such as conditional
+ # block or loop.
+
+ self.is_multiline_context = False
+
+ # Intercept events our controller hears about at a pretty low level since
+ # the user will likely be requesting them by direct 'SETEVENTS' calls.
+
+ handle_event_real = self._controller._handle_event
+
+ def handle_event_wrapper(event_message):
+ handle_event_real(event_message)
+ self._received_events.append(event_message)
+
+ self._controller._handle_event = handle_event_wrapper
+
+ 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.
+ """
+
+ return stem.interpreter.help.response(self._controller, arg)
+
+ def do_events(self, arg):
+ """
+ Performs the '/events' operation, dumping the events that we've received
+ belonging to the given types. If no types are specified then this provides
+ all buffered events.
+ """
+
+ events = self._received_events
+ event_types = arg.upper().split()
+
+ if event_types:
+ events = filter(lambda event: event.type in event_types, events)
+
+ return '\n'.join([format(str(event), *STANDARD_OUTPUT) 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.
+ """
+
+ try:
+ fingerprint = _get_fingerprint(arg, self._controller)
+ except ValueError as exc:
+ return format(str(exc), *ERROR_OUTPUT)
+
+ 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_OUTPUT)
+
+ 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'
+
+ lines = [
+ '%s (%s)' % (ns_desc.nickname, fingerprint),
+ format('address: ', *BOLD_OUTPUT) + '%s:%s%s' % (ns_desc.address, ns_desc.or_port, locale_label),
+ format('published: ', *BOLD_OUTPUT) + ns_desc.published.strftime('%H:%M:%S %d/%m/%Y'),
+ ]
+
+ if server_desc:
+ lines.append(format('os: ', *BOLD_OUTPUT) + server_desc.platform.decode('utf-8', 'replace'))
+ lines.append(format('version: ', *BOLD_OUTPUT) + str(server_desc.tor_version))
+
+ lines.append(format('flags: ', *BOLD_OUTPUT) + ', '.join(ns_desc.flags))
+ lines.append(format('exit policy: ', *BOLD_OUTPUT) + exit_policy_label)
+
+ 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, '.')
+
+ lines.append(format('contact: ', *BOLD_OUTPUT) + contact)
+
+ return '\n'.join(lines)
+
+ def do_python(self, arg):
+ """
+ Performs the '/python' operation, toggling if we accept python commands or
+ not.
+ """
+
+ if not arg:
+ status = 'enabled' if self._run_python_commands else 'disabled'
+ return format('Python support is presently %s.' % status, *STANDARD_OUTPUT)
+ elif arg.lower() == 'enable':
+ self._run_python_commands = True
+ elif arg.lower() == 'disable':
+ self._run_python_commands = False
+ else:
+ return format("'%s' is not recognized. Please run either '/python enable' or '/python disable'." % arg, *ERROR_OUTPUT)
+
+ if self._run_python_commands:
+ response = "Python support enabled, we'll now run non-interpreter commands as python."
+ else:
+ response = "Python support disabled, we'll now pass along all commands to tor."
+
+ return format(response, *STANDARD_OUTPUT)
+
+ @uses_settings
+ def run_command(self, command, config):
+ """
+ Runs the given command. Requests starting with a '/' are special commands
+ to the interpreter, 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()
+
+ # 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.
+
+ cmd, arg = command.strip(), ''
+
+ if ' ' in cmd:
+ cmd, arg = cmd.split(' ', 1)
+
+ output = ''
+
+ if cmd.startswith('/'):
+ cmd = cmd.lower()
+
+ if cmd == '/quit':
+ raise stem.SocketClosed()
+ elif cmd == '/events':
+ output = self.do_events(arg)
+ elif cmd == '/info':
+ output = self.do_info(arg)
+ elif cmd == '/python':
+ output = self.do_python(arg)
+ elif cmd == '/help':
+ output = self.do_help(arg)
+ else:
+ output = format("'%s' isn't a recognized command" % command, *ERROR_OUTPUT)
+ else:
+ cmd = cmd.upper() # makes commands uppercase to match the spec
+
+ if cmd.replace('+', '') in ('LOADCONF', 'POSTDESCRIPTOR'):
+ # provides a notice that multi-line controller input isn't yet implemented
+ output = format(msg('msg.multiline_unimplemented_notice'), *ERROR_OUTPUT)
+ elif cmd == 'QUIT':
+ self._controller.msg(command)
+ raise stem.SocketClosed()
+ else:
+ is_tor_command = cmd in config.get('help.usage', {}) and cmd.lower() != 'events'
+
+ if self._run_python_commands and not is_tor_command:
+ self.is_multiline_context = code.InteractiveConsole.push(self, command)
+ return
+ else:
+ try:
+ output = format(self._controller.msg(command).raw_content().strip(), *STANDARD_OUTPUT)
+ except stem.ControllerError as exc:
+ if isinstance(exc, stem.SocketClosed):
+ raise exc
+ else:
+ output = format(str(exc), *ERROR_OUTPUT)
+
+ output += '\n' # give ourselves an extra line before the next prompt
+
+ return output
diff --git a/stem/interpreter/help.py b/stem/interpreter/help.py
new file mode 100644
index 0000000..e17fe3f
--- /dev/null
+++ b/stem/interpreter/help.py
@@ -0,0 +1,142 @@
+"""
+Provides our /help responses.
+"""
+
+from stem.interpreter import (
+ STANDARD_OUTPUT,
+ BOLD_OUTPUT,
+ ERROR_OUTPUT,
+ msg,
+ uses_settings,
+)
+
+from stem.util.term import format
+
+try:
+ # added in python 3.2
+ from functools import lru_cache
+except ImportError:
+ from stem.util.lru_cache import lru_cache
+
+
+def response(controller, arg):
+ """
+ Provides our /help response.
+
+ :param stem.control.Controller controller: tor control connection
+ :param str arg: controller or interpreter command to provide help output for
+
+ :returns: **str** with our help response
+ """
+
+ # Normalizing inputs first so we can better cache responses.
+
+ return _response(controller, _normalize(arg))
+
+
+def _normalize(arg):
+ 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 interpreter command (ex. "/help /help")
+
+ if arg.startswith('/'):
+ arg = arg[1:]
+
+ return arg
+
+
+@lru_cache()
+@uses_settings
+def _response(controller, arg, config):
+ if not arg:
+ return _general_help()
+
+ usage_info = config.get('help.usage', {})
+
+ if not arg in usage_info:
+ return format("No help information available for '%s'..." % arg, *ERROR_OUTPUT)
+
+ output = format(usage_info[arg] + '\n', *BOLD_OUTPUT)
+
+ description = config.get('help.description.%s' % arg.lower(), '')
+
+ for line in description.splitlines():
+ output += format(' ' + line, *STANDARD_OUTPUT) + '\n'
+
+ output += '\n'
+
+ if arg == 'GETINFO':
+ results = controller.get_info('info/names', None)
+
+ if results:
+ for line in results.splitlines():
+ if ' -- ' in line:
+ opt, summary = line.split(' -- ', 1)
+
+ output += format('%-33s' % opt, *BOLD_OUTPUT)
+ output += format(' - %s' % summary, *STANDARD_OUTPUT) + '\n'
+ elif arg == 'GETCONF':
+ results = controller.get_info('config/names', None)
+
+ if results:
+ options = [opt.split(' ', 1)[0] for opt in results.splitlines()]
+
+ for i in range(0, len(options), 2):
+ line = ''
+
+ for entry in options[i:i + 2]:
+ line += '%-42s' % entry
+
+ output += format(line.rstrip(), *STANDARD_OUTPUT) + '\n'
+ elif arg == 'SIGNAL':
+ signal_options = config.get('help.signal.options', {})
+
+ for signal, summary in signal_options.items():
+ output += format('%-15s' % signal, *BOLD_OUTPUT)
+ output += format(' - %s' % summary, *STANDARD_OUTPUT) + '\n'
+ elif arg == 'SETEVENTS':
+ results = controller.get_info('events/names', None)
+
+ if results:
+ entries = results.split()
+
+ # displays four columns of 20 characters
+
+ for i in range(0, len(entries), 4):
+ line = ''
+
+ for entry in entries[i:i + 4]:
+ line += '%-20s' % entry
+
+ output += format(line.rstrip(), *STANDARD_OUTPUT) + '\n'
+ elif arg == 'USEFEATURE':
+ results = controller.get_info('features/names', None)
+
+ if results:
+ output += format(results, *STANDARD_OUTPUT) + '\n'
+ elif arg in ('LOADCONF', 'POSTDESCRIPTOR'):
+ # gives a warning that this option isn't yet implemented
+ output += format(msg('msg.multiline_unimplemented_notice'), *ERROR_OUTPUT) + '\n'
+
+ return output.rstrip()
+
+
+def _general_help():
+ lines = []
+
+ for line in msg('help.general').splitlines():
+ div = line.find(' - ')
+
+ if div != -1:
+ cmd, description = line[:div], line[div:]
+ lines.append(format(cmd, *BOLD_OUTPUT) + format(description, *STANDARD_OUTPUT))
+ else:
+ lines.append(format(line, *BOLD_OUTPUT))
+
+ return '\n'.join(lines)
diff --git a/stem/interpreter/settings.cfg b/stem/interpreter/settings.cfg
new file mode 100644
index 0000000..0ddf080
--- /dev/null
+++ b/stem/interpreter/settings.cfg
@@ -0,0 +1,295 @@
+################################################################################
+#
+# Configuration data used by Stem's interpreter prompt.
+#
+################################################################################
+
+ ##################
+# GENERAL MESSAGES #
+ ##################
+
+msg.multiline_unimplemented_notice Multi-line control options like this are not yet implemented.
+
+msg.help
+|Interactive interpreter for Tor. This provides you with direct access
+|to Tor's control interface via either python or direct requests.
+|
+| -i, --interface [ADDRESS:]PORT change control interface from {address}:{port}
+| -s, --socket SOCKET_PATH attach using unix domain socket if present,
+| SOCKET_PATH defaults to: {socket}
+| --no-color disables colorized output
+| -h, --help presents this help
+|
+
+msg.startup_banner
+|Welcome to Stem's interpreter prompt. This provides you with direct access to
+|Tor's control interface.
+|
+|This acts like a standard python interpreter with a Tor connection available
+|via your 'controller' variable...
+|
+| >>> controller.get_info('version')
+| '0.2.5.1-alpha-dev (git-245ecfff36c0cecc)'
+|
+|You can also issue requests directly to Tor...
+|
+| >>> GETINFO version
+| 250-version=0.2.5.1-alpha-dev (git-245ecfff36c0cecc)
+| 250 OK
+|
+|For more information run '/help'.
+|
+
+msg.tor_unavailable Tor isn't running and the command presently isn't in your PATH.
+
+msg.starting_tor
+|Tor isn't running. Starting a temporary Tor instance for our interpreter to
+|interact with. This will have a minimal non-relaying configuration, and be
+|shut down when you're done.
+|
+|--------------------------------------------------------------------------------
+|
+
+ #################
+# OUTPUT OF /HELP #
+ #################
+
+# Response for the '/help' command without any arguments.
+
+help.general
+|Interpretor commands include:
+| /help - provides information for interpreter and tor commands
+| /events - prints events that we've received
+| /info - general information for a relay
+| /python - enable or disable support for running python commands
+| /quit - shuts down the interpreter
+|
+|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]'.
+
+# Usage of tor and interpreter commands.
+
+help.usage HELP => /help [OPTION]
+help.usage EVENTS => /events [types]
+help.usage INFO => /info [relay fingerprint, nickname, or IP address]
+help.usage PYTHON => /python [enable,disable]
+help.usage QUIT => /quit
+help.usage GETINFO => GETINFO OPTION
+help.usage GETCONF => GETCONF OPTION
+help.usage SETCONF => SETCONF PARAM[=VALUE]
+help.usage RESETCONF => RESETCONF PARAM[=VALUE]
+help.usage SIGNAL => SIGNAL SIG
+help.usage SETEVENTS => SETEVENTS [EXTENDED] [EVENTS]
+help.usage USEFEATURE => USEFEATURE OPTION
+help.usage SAVECONF => SAVECONF
+help.usage LOADCONF => LOADCONF...
+help.usage MAPADDRESS => MAPADDRESS SOURCE_ADDR=DESTINATION_ADDR
+help.usage POSTDESCRIPTOR => POSTDESCRIPTOR [purpose=general/controller/bridge] [cache=yes/no]...
+help.usage EXTENDCIRCUIT => EXTENDCIRCUIT CircuitID [PATH] [purpose=general/controller]
+help.usage SETCIRCUITPURPOSE => SETCIRCUITPURPOSE CircuitID purpose=general/controller
+help.usage CLOSECIRCUIT => CLOSECIRCUIT CircuitID [IfUnused]
+help.usage ATTACHSTREAM => ATTACHSTREAM StreamID CircuitID [HOP=HopNum]
+help.usage REDIRECTSTREAM => REDIRECTSTREAM StreamID Address [Port]
+help.usage CLOSESTREAM => CLOSESTREAM StreamID Reason [Flag]
+help.usage RESOLVE => RESOLVE [mode=reverse] address
+help.usage TAKEOWNERSHIP => TAKEOWNERSHIP
+help.usage PROTOCOLINFO => PROTOCOLINFO [ProtocolVersion]
+
+# Longer description of what tor and interpreter commands do.
+
+help.description.help
+|Provides usage information for the given interpreter, tor command, or tor
+|configuration option.
+|
+|Example:
+| /help info # provides a description of the '/info' option
+| /help GETINFO # usage information for tor's GETINFO controller option
+
+help.description.events
+|Provides events that we've received belonging to the given event types. If
+|no types are specified then this provides all the messages that we've
+|received.
+
+help.description.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.description.python
+|Enables or disables support for running python commands. This determines how
+|we treat commands this interpreter doesn't recognize...
+|
+|* If enabled then unrecognized commands are executed as python.
+|* If disabled then unrecognized commands are passed along to tor.
+
+help.description.quit
+|Terminates the interpreter.
+
+help.description.getinfo
+|Queries the tor process for information. Options are...
+|
+
+help.description.getconf
+|Provides the current value for a given configuration value. Options include...
+|
+
+help.description.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@xxxxxxx
+|
+| * Sets an exit policy that only includes port 80/443
+| SETCONF ExitPolicy=\"accept *:80, accept *:443, reject *:*\"\
+
+help.description.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.description.signal
+|Issues a signal that tells the tor process to reload its torrc, dump its
+|stats, halt, etc.
+
+help.description.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.description.usefeature
+|Customizes the behavior of the control port. Options include...
+|
+
+help.description.saveconf
+|Writes Tor's current configuration to its torrc.
+
+help.description.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.description.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.description.postdescriptor
+|Simulates getting a new relay descriptor.
+
+help.description.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.description.setcircuitpurpose
+|Sets the purpose attribute for a circuit.
+
+help.description.closecircuit
+|Closes the given circuit. If "IfUnused" is included then this only closes
+|the circuit if it isn't currently being used.
+
+help.description.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.description.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.description.closestream
+|Closes the given stream, the reason being an integer matching a reason as
+|per section 6.3 of the tor-spec.
+
+help.description.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.description.takeownership
+|Instructs Tor to gracefully shut down when this control connection is closed.
+
+help.description.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.signal.options RELOAD / HUP => reload our torrc
+help.signal.options SHUTDOWN / INT => gracefully shut down, waiting 30 seconds if we're a relay
+help.signal.options DUMP / USR1 => logs information about open connections and circuits
+help.signal.options DEBUG / USR2 => makes us log at the DEBUG runlevel
+help.signal.options HALT / TERM => immediately shut down
+help.signal.options CLEARDNSCACHE => clears any cached DNS results
+help.signal.options NEWNYM => clears the DNS cache and uses new circuits for future connections
+
+ ################
+# TAB COMPLETION #
+ ################
+
+# Commands we'll autocomplete when the user hits tab. This is just the start of
+# our autocompletion list - more are determined dynamically by checking what
+# tor supports.
+
+autocomplete /help
+autocomplete /events
+autocomplete /info
+autocomplete /quit
+autocomplete SAVECONF
+autocomplete MAPADDRESS
+autocomplete EXTENDCIRCUIT
+autocomplete SETCIRCUITPURPOSE
+autocomplete SETROUTERPURPOSE
+autocomplete ATTACHSTREAM
+#autocomplete +POSTDESCRIPTOR # TODO: needs multi-line support
+autocomplete REDIRECTSTREAM
+autocomplete CLOSESTREAM
+autocomplete CLOSECIRCUIT
+autocomplete QUIT
+autocomplete RESOLVE
+autocomplete PROTOCOLINFO
+#autocomplete +LOADCONF # TODO: needs multi-line support
+autocomplete TAKEOWNERSHIP
+autocomplete AUTHCHALLENGE
+autocomplete DROPGUARDS
+
diff --git a/stem/interpretor/__init__.py b/stem/interpretor/__init__.py
deleted file mode 100644
index 5ecd356..0000000
--- a/stem/interpretor/__init__.py
+++ /dev/null
@@ -1,129 +0,0 @@
-# Copyright 2014, Damian Johnson and The Tor Project
-# See LICENSE for licensing information
-
-"""
-Interactive interpretor for interacting with Tor directly. This adds usability
-features such as tab completion, history, and IRC-style functions (like /help).
-"""
-
-__all__ = ['arguments', 'autocomplete', 'commands', 'help', 'msg']
-
-import os
-import sys
-
-import stem
-import stem.connection
-import stem.process
-import stem.util.conf
-import stem.util.system
-import stem.util.term
-
-from stem.util.term import RESET, Attr, Color, format
-
-# Our color prompt triggers a bug between raw_input() and readline history,
-# where scrolling through history widens our prompt. Widening our prompt via
-# invisible characters (like resets) seems to sidestep this bug for short
-# inputs. Contrary to the ticket, this still manifests with python 2.7.1...
-#
-# http://bugs.python.org/issue12972
-
-PROMPT = format('>>> ', Color.GREEN, Attr.BOLD) + RESET * 10
-
-STANDARD_OUTPUT = (Color.BLUE, )
-BOLD_OUTPUT = (Color.BLUE, Attr.BOLD)
-HEADER_OUTPUT = (Color.GREEN, )
-HEADER_BOLD_OUTPUT = (Color.GREEN, Attr.BOLD)
-ERROR_OUTPUT = (Attr.BOLD, Color.RED)
-
-settings_path = os.path.join(os.path.dirname(__file__), 'settings.cfg')
-uses_settings = stem.util.conf.uses_settings('stem_interpretor', settings_path)
-
-
-@uses_settings
-def msg(message, config, **attr):
- return config.get(message).format(**attr)
-
-
-def main():
- import readline
-
- import stem.interpretor.arguments
- import stem.interpretor.autocomplete
- import stem.interpretor.commands
-
- try:
- args = stem.interpretor.arguments.parse(sys.argv[1:])
- except ValueError as exc:
- print exc
- sys.exit(1)
-
- if args.print_help:
- print stem.interpretor.arguments.get_help()
- sys.exit()
-
- if args.disable_color:
- global PROMPT
- stem.util.term.DISABLE_COLOR_SUPPORT = True
- PROMPT = '>>> '
-
- # If the user isn't connecting to something in particular then offer to start
- # tor if it isn't running.
-
- if not (args.user_provided_port or args.user_provided_socket):
- is_tor_running = stem.util.system.is_running('tor') or stem.util.system.is_running('tor.real')
-
- if not is_tor_running:
- if not stem.util.system.is_available('tor'):
- print format(msg('msg.tor_unavailable'), *ERROR_OUTPUT)
- sys.exit(1)
- else:
- print format(msg('msg.starting_tor'), *HEADER_OUTPUT)
-
- stem.process.launch_tor_with_config(
- config = {
- 'SocksPort': '0',
- 'ControlPort': str(args.control_port),
- 'CookieAuthentication': '1',
- 'ExitPolicy': 'reject *:*',
- },
- completion_percent = 5,
- take_ownership = True,
- )
-
- control_port = None if args.user_provided_socket else (args.control_address, args.control_port)
- control_socket = None if args.user_provided_port else args.control_socket
-
- controller = stem.connection.connect(
- control_port = control_port,
- control_socket = control_socket,
- password_prompt = True,
- )
-
- if controller is None:
- sys.exit(1)
-
- with controller:
- autocompleter = stem.interpretor.autocomplete.Autocompleter(controller)
- readline.parse_and_bind('tab: complete')
- readline.set_completer(autocompleter.complete)
- readline.set_completer_delims('\n')
-
- interpretor = stem.interpretor.commands.ControlInterpretor(controller)
-
- for line in msg('msg.startup_banner').splitlines():
- line_format = HEADER_BOLD_OUTPUT if line.startswith(' ') else HEADER_OUTPUT
- print format(line, *line_format)
-
- print
-
- while True:
- try:
- prompt = '... ' if interpretor.is_multiline_context else PROMPT
- user_input = raw_input(prompt)
- response = interpretor.run_command(user_input)
-
- if response is not None:
- print response
- except (KeyboardInterrupt, EOFError, stem.SocketClosed) as exc:
- print # move cursor to the following line
- break
diff --git a/stem/interpretor/arguments.py b/stem/interpretor/arguments.py
deleted file mode 100644
index 278d2a0..0000000
--- a/stem/interpretor/arguments.py
+++ /dev/null
@@ -1,96 +0,0 @@
-# Copyright 2014, Damian Johnson and The Tor Project
-# See LICENSE for licensing information
-
-"""
-Commandline argument parsing for our interpretor prompt.
-"""
-
-import collections
-import getopt
-
-import stem.interpretor
-import stem.util.connection
-
-DEFAULT_ARGS = {
- 'control_address': '127.0.0.1',
- 'control_port': 9051,
- 'user_provided_port': False,
- 'control_socket': '/var/run/tor/control',
- 'user_provided_socket': False,
- 'disable_color': False,
- 'print_help': False,
-}
-
-OPT = 'i:s:h'
-
-OPT_EXPANDED = [
- 'interface=',
- 'socket=',
- 'no-color',
- 'help',
-]
-
-
-def parse(argv):
- """
- Parses our arguments, providing a named tuple with their values.
-
- :param list argv: input arguments to be parsed
-
- :returns: a **named tuple** with our parsed arguments
-
- :raises: **ValueError** if we got an invalid argument
- """
-
- args = dict(DEFAULT_ARGS)
-
- try:
- getopt_results = getopt.getopt(argv, OPT, OPT_EXPANDED)[0]
- except getopt.GetoptError as exc:
- raise ValueError('%s (for usage provide --help)' % exc)
-
- for opt, arg in getopt_results:
- if opt in ('-i', '--interface'):
- if ':' in arg:
- address, port = arg.split(':', 1)
- else:
- address, port = None, arg
-
- if address is not None:
- if not stem.util.connection.is_valid_ipv4_address(address):
- raise ValueError("'%s' isn't a valid IPv4 address" % address)
-
- args['control_address'] = address
-
- if not stem.util.connection.is_valid_port(port):
- raise ValueError("'%s' isn't a valid port number" % port)
-
- args['control_port'] = int(port)
- args['user_provided_port'] = True
- elif opt in ('-s', '--socket'):
- args['control_socket'] = arg
- args['user_provided_socket'] = True
- elif opt == '--no-color':
- args['disable_color'] = True
- elif opt in ('-h', '--help'):
- args['print_help'] = True
-
- # translates our args dict into a named tuple
-
- Args = collections.namedtuple('Args', args.keys())
- return Args(**args)
-
-
-def get_help():
- """
- Provides our --help usage information.
-
- :returns: **str** with our usage information
- """
-
- return stem.interpretor.msg(
- 'msg.help',
- address = DEFAULT_ARGS['control_address'],
- port = DEFAULT_ARGS['control_port'],
- socket = DEFAULT_ARGS['control_socket'],
- )
diff --git a/stem/interpretor/autocomplete.py b/stem/interpretor/autocomplete.py
deleted file mode 100644
index f42084e..0000000
--- a/stem/interpretor/autocomplete.py
+++ /dev/null
@@ -1,112 +0,0 @@
-"""
-Tab completion for our interpretor prompt.
-"""
-
-from stem.interpretor import uses_settings
-
-try:
- # added in python 3.2
- from functools import lru_cache
-except ImportError:
- from stem.util.lru_cache import lru_cache
-
-
-@uses_settings
-def _get_commands(controller, config):
- """
- Provides commands recognized by tor.
- """
-
- commands = config.get('autocomplete', [])
-
- if controller is None:
- return commands
-
- # GETINFO commands. Lines are of the form '[option] -- [description]'. This
- # strips '*' from options that accept values.
-
- results = controller.get_info('info/names', None)
-
- if results:
- for line in results.splitlines():
- option = line.split(' ', 1)[0].rstrip('*')
- commands.append('GETINFO %s' % option)
- else:
- commands.append('GETINFO ')
-
- # GETCONF, SETCONF, and RESETCONF commands. Lines are of the form
- # '[option] [type]'.
-
- results = controller.get_info('config/names', None)
-
- if results:
- for line in results.splitlines():
- option = line.split(' ', 1)[0]
-
- commands.append('GETCONF %s' % option)
- commands.append('SETCONF %s' % option)
- commands.append('RESETCONF %s' % option)
- else:
- commands += ['GETCONF ', 'SETCONF ', 'RESETCONF ']
-
- # SETEVENT, USEFEATURE, and SIGNAL commands. For each of these the GETINFO
- # results are simply a space separated lists of the values they can have.
-
- options = (
- ('SETEVENTS ', 'events/names'),
- ('USEFEATURE ', 'features/names'),
- ('SIGNAL ', 'signal/names'),
- )
-
- for prefix, getinfo_cmd in options:
- results = controller.get_info(getinfo_cmd, None)
-
- if results:
- commands += [prefix + value for value in results.split()]
- else:
- commands.append(prefix)
-
- # Adds /help commands.
-
- usage_info = config.get('help.usage', {})
-
- for cmd in usage_info.keys():
- commands.append('/help ' + cmd)
-
- return commands
-
-
-class Autocompleter(object):
- def __init__(self, controller):
- self._commands = _get_commands(controller)
-
- @lru_cache()
- def matches(self, text):
- """
- Provides autocompletion matches for the given text.
-
- :param str text: text to check for autocompletion matches with
-
- :returns: **list** with possible matches
- """
-
- lowercase_text = text.lower()
- return [cmd for cmd in self._commands if cmd.lower().startswith(lowercase_text)]
-
- def complete(self, text, state):
- """
- Provides case insensetive autocompletion options, acting as a functor for
- the readlines set_completer function.
-
- :param str text: text to check for autocompletion matches with
- :param int state: index of result to be provided, readline fetches matches
- until this function provides None
-
- :returns: **str** with the autocompletion match, **None** if eithe none
- exists or state is higher than our number of matches
- """
-
- try:
- return self.matches(text)[state]
- except IndexError:
- return None
diff --git a/stem/interpretor/commands.py b/stem/interpretor/commands.py
deleted file mode 100644
index b66c87b..0000000
--- a/stem/interpretor/commands.py
+++ /dev/null
@@ -1,299 +0,0 @@
-"""
-Handles making requests and formatting the responses.
-"""
-
-import code
-
-import stem
-import stem.control
-import stem.interpretor.help
-import stem.util.connection
-import stem.util.tor_tools
-
-from stem.interpretor import STANDARD_OUTPUT, BOLD_OUTPUT, ERROR_OUTPUT, uses_settings, msg
-from stem.util.term import format
-
-
-def _get_fingerprint(arg, controller):
- """
- Resolves user input into a relay fingerprint. This accepts...
-
- * Fingerprints
- * Nicknames
- * IPv4 addresses, either with or without an ORPort
- * Empty input, which is resolved to ourselves if we're a relay
-
- :param str arg: input to be resolved to a relay fingerprint
- :param stem.control.Controller controller: tor control connection
-
- :returns: **str** for the relay fingerprint
-
- :raises: **ValueError** if we're unable to resolve the input to a relay
- """
-
- if not arg:
- try:
- return controller.get_info('fingerprint')
- except:
- raise ValueError("We aren't a relay, no information to provide")
- elif stem.util.tor_tools.is_valid_fingerprint(arg):
- return arg
- elif stem.util.tor_tools.is_valid_nickname(arg):
- try:
- return controller.get_network_status(arg).fingerprint
- except:
- raise ValueError("Unable to find a relay with the nickname of '%s'" % arg)
- elif ':' in arg or stem.util.connection.is_valid_ipv4_address(arg):
- if ':' in arg:
- address, port = arg.split(':', 1)
-
- if not stem.util.connection.is_valid_ipv4_address(address):
- raise ValueError("'%s' isn't a valid IPv4 address" % address)
- elif port and not stem.util.connection.is_valid_port(port):
- raise ValueError("'%s' isn't a valid port" % port)
-
- port = int(port)
- else:
- address, port = arg, None
-
- matches = {}
-
- for desc in 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:
- raise ValueError('No relays found at %s' % arg)
- elif len(matches) == 1:
- return matches.values()[0]
- else:
- response = "There's multiple relays at %s, include a port to specify which.\n\n" % arg
-
- for i, or_port in enumerate(matches):
- response += ' %i. %s:%s, fingerprint: %s\n' % (i + 1, address, or_port, matches[or_port])
-
- raise ValueError(response)
- else:
- raise ValueError("'%s' isn't a fingerprint, nickname, or IP address" % arg)
-
-
-class ControlInterpretor(code.InteractiveConsole):
- """
- Handles issuing requests and providing nicely formed responses, with support
- for special irc style subcommands.
- """
-
- def __init__(self, controller):
- self._received_events = []
-
- code.InteractiveConsole.__init__(self, {
- 'stem': stem,
- 'stem.control': stem.control,
- 'controller': controller,
- 'events': self._received_events
- })
-
- self._controller = controller
- self._run_python_commands = True
-
- # Indicates if we're processing a multiline command, such as conditional
- # block or loop.
-
- self.is_multiline_context = False
-
- # Intercept events our controller hears about at a pretty low level since
- # the user will likely be requesting them by direct 'SETEVENTS' calls.
-
- handle_event_real = self._controller._handle_event
-
- def handle_event_wrapper(event_message):
- handle_event_real(event_message)
- self._received_events.append(event_message)
-
- self._controller._handle_event = handle_event_wrapper
-
- 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.
- """
-
- return stem.interpretor.help.response(self._controller, arg)
-
- def do_events(self, arg):
- """
- Performs the '/events' operation, dumping the events that we've received
- belonging to the given types. If no types are specified then this provides
- all buffered events.
- """
-
- events = self._received_events
- event_types = arg.upper().split()
-
- if event_types:
- events = filter(lambda event: event.type in event_types, events)
-
- return '\n'.join([format(str(event), *STANDARD_OUTPUT) 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.
- """
-
- try:
- fingerprint = _get_fingerprint(arg, self._controller)
- except ValueError as exc:
- return format(str(exc), *ERROR_OUTPUT)
-
- 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_OUTPUT)
-
- 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'
-
- lines = [
- '%s (%s)' % (ns_desc.nickname, fingerprint),
- format('address: ', *BOLD_OUTPUT) + '%s:%s%s' % (ns_desc.address, ns_desc.or_port, locale_label),
- format('published: ', *BOLD_OUTPUT) + ns_desc.published.strftime('%H:%M:%S %d/%m/%Y'),
- ]
-
- if server_desc:
- lines.append(format('os: ', *BOLD_OUTPUT) + server_desc.platform.decode('utf-8', 'replace'))
- lines.append(format('version: ', *BOLD_OUTPUT) + str(server_desc.tor_version))
-
- lines.append(format('flags: ', *BOLD_OUTPUT) + ', '.join(ns_desc.flags))
- lines.append(format('exit policy: ', *BOLD_OUTPUT) + exit_policy_label)
-
- 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, '.')
-
- lines.append(format('contact: ', *BOLD_OUTPUT) + contact)
-
- return '\n'.join(lines)
-
- def do_python(self, arg):
- """
- Performs the '/python' operation, toggling if we accept python commands or
- not.
- """
-
- if not arg:
- status = 'enabled' if self._run_python_commands else 'disabled'
- return format('Python support is presently %s.' % status, *STANDARD_OUTPUT)
- elif arg.lower() == 'enable':
- self._run_python_commands = True
- elif arg.lower() == 'disable':
- self._run_python_commands = False
- else:
- return format("'%s' is not recognized. Please run either '/python enable' or '/python disable'." % arg, *ERROR_OUTPUT)
-
- if self._run_python_commands:
- response = "Python support enabled, we'll now run non-interpretor commands as python."
- else:
- response = "Python support disabled, we'll now pass along all commands to tor."
-
- return format(response, *STANDARD_OUTPUT)
-
- @uses_settings
- def run_command(self, command, config):
- """
- 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()
-
- # 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.
-
- cmd, arg = command.strip(), ''
-
- if ' ' in cmd:
- cmd, arg = cmd.split(' ', 1)
-
- output = ''
-
- if cmd.startswith('/'):
- cmd = cmd.lower()
-
- if cmd == '/quit':
- raise stem.SocketClosed()
- elif cmd == '/events':
- output = self.do_events(arg)
- elif cmd == '/info':
- output = self.do_info(arg)
- elif cmd == '/python':
- output = self.do_python(arg)
- elif cmd == '/help':
- output = self.do_help(arg)
- else:
- output = format("'%s' isn't a recognized command" % command, *ERROR_OUTPUT)
- else:
- cmd = cmd.upper() # makes commands uppercase to match the spec
-
- if cmd.replace('+', '') in ('LOADCONF', 'POSTDESCRIPTOR'):
- # provides a notice that multi-line controller input isn't yet implemented
- output = format(msg('msg.multiline_unimplemented_notice'), *ERROR_OUTPUT)
- elif cmd == 'QUIT':
- self._controller.msg(command)
- raise stem.SocketClosed()
- else:
- is_tor_command = cmd in config.get('help.usage', {}) and cmd.lower() != 'events'
-
- if self._run_python_commands and not is_tor_command:
- self.is_multiline_context = code.InteractiveConsole.push(self, command)
- return
- else:
- try:
- output = format(self._controller.msg(command).raw_content().strip(), *STANDARD_OUTPUT)
- except stem.ControllerError as exc:
- if isinstance(exc, stem.SocketClosed):
- raise exc
- else:
- output = format(str(exc), *ERROR_OUTPUT)
-
- output += '\n' # give ourselves an extra line before the next prompt
-
- return output
diff --git a/stem/interpretor/help.py b/stem/interpretor/help.py
deleted file mode 100644
index b7909a9..0000000
--- a/stem/interpretor/help.py
+++ /dev/null
@@ -1,142 +0,0 @@
-"""
-Provides our /help responses.
-"""
-
-from stem.interpretor import (
- STANDARD_OUTPUT,
- BOLD_OUTPUT,
- ERROR_OUTPUT,
- msg,
- uses_settings,
-)
-
-from stem.util.term import format
-
-try:
- # added in python 3.2
- from functools import lru_cache
-except ImportError:
- from stem.util.lru_cache import lru_cache
-
-
-def response(controller, arg):
- """
- Provides our /help response.
-
- :param stem.control.Controller controller: tor control connection
- :param str arg: controller or interpretor command to provide help output for
-
- :returns: **str** with our help response
- """
-
- # Normalizing inputs first so we can better cache responses.
-
- return _response(controller, _normalize(arg))
-
-
-def _normalize(arg):
- 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:]
-
- return arg
-
-
-@lru_cache()
-@uses_settings
-def _response(controller, arg, config):
- if not arg:
- return _general_help()
-
- usage_info = config.get('help.usage', {})
-
- if not arg in usage_info:
- return format("No help information available for '%s'..." % arg, *ERROR_OUTPUT)
-
- output = format(usage_info[arg] + '\n', *BOLD_OUTPUT)
-
- description = config.get('help.description.%s' % arg.lower(), '')
-
- for line in description.splitlines():
- output += format(' ' + line, *STANDARD_OUTPUT) + '\n'
-
- output += '\n'
-
- if arg == 'GETINFO':
- results = controller.get_info('info/names', None)
-
- if results:
- for line in results.splitlines():
- if ' -- ' in line:
- opt, summary = line.split(' -- ', 1)
-
- output += format('%-33s' % opt, *BOLD_OUTPUT)
- output += format(' - %s' % summary, *STANDARD_OUTPUT) + '\n'
- elif arg == 'GETCONF':
- results = controller.get_info('config/names', None)
-
- if results:
- options = [opt.split(' ', 1)[0] for opt in results.splitlines()]
-
- for i in range(0, len(options), 2):
- line = ''
-
- for entry in options[i:i + 2]:
- line += '%-42s' % entry
-
- output += format(line.rstrip(), *STANDARD_OUTPUT) + '\n'
- elif arg == 'SIGNAL':
- signal_options = config.get('help.signal.options', {})
-
- for signal, summary in signal_options.items():
- output += format('%-15s' % signal, *BOLD_OUTPUT)
- output += format(' - %s' % summary, *STANDARD_OUTPUT) + '\n'
- elif arg == 'SETEVENTS':
- results = controller.get_info('events/names', None)
-
- if results:
- entries = results.split()
-
- # displays four columns of 20 characters
-
- for i in range(0, len(entries), 4):
- line = ''
-
- for entry in entries[i:i + 4]:
- line += '%-20s' % entry
-
- output += format(line.rstrip(), *STANDARD_OUTPUT) + '\n'
- elif arg == 'USEFEATURE':
- results = controller.get_info('features/names', None)
-
- if results:
- output += format(results, *STANDARD_OUTPUT) + '\n'
- elif arg in ('LOADCONF', 'POSTDESCRIPTOR'):
- # gives a warning that this option isn't yet implemented
- output += format(msg('msg.multiline_unimplemented_notice'), *ERROR_OUTPUT) + '\n'
-
- return output.rstrip()
-
-
-def _general_help():
- lines = []
-
- for line in msg('help.general').splitlines():
- div = line.find(' - ')
-
- if div != -1:
- cmd, description = line[:div], line[div:]
- lines.append(format(cmd, *BOLD_OUTPUT) + format(description, *STANDARD_OUTPUT))
- else:
- lines.append(format(line, *BOLD_OUTPUT))
-
- return '\n'.join(lines)
diff --git a/stem/interpretor/settings.cfg b/stem/interpretor/settings.cfg
deleted file mode 100644
index 028188f..0000000
--- a/stem/interpretor/settings.cfg
+++ /dev/null
@@ -1,295 +0,0 @@
-################################################################################
-#
-# Configuration data used by Stem's interpretor prompt.
-#
-################################################################################
-
- ##################
-# GENERAL MESSAGES #
- ##################
-
-msg.multiline_unimplemented_notice Multi-line control options like this are not yet implemented.
-
-msg.help
-|Interactive interpretor for Tor. This provides you with direct access
-|to Tor's control interface via either python or direct requests.
-|
-| -i, --interface [ADDRESS:]PORT change control interface from {address}:{port}
-| -s, --socket SOCKET_PATH attach using unix domain socket if present,
-| SOCKET_PATH defaults to: {socket}
-| --no-color disables colorized output
-| -h, --help presents this help
-|
-
-msg.startup_banner
-|Welcome to Stem's interpretor prompt. This provides you with direct access to
-|Tor's control interface.
-|
-|This acts like a standard python interpretor with a Tor connection available
-|via your 'controller' variable...
-|
-| >>> controller.get_info('version')
-| '0.2.5.1-alpha-dev (git-245ecfff36c0cecc)'
-|
-|You can also issue requests directly to Tor...
-|
-| >>> GETINFO version
-| 250-version=0.2.5.1-alpha-dev (git-245ecfff36c0cecc)
-| 250 OK
-|
-|For more information run '/help'.
-|
-
-msg.tor_unavailable Tor isn't running and the command presently isn't in your PATH.
-
-msg.starting_tor
-|Tor isn't running. Starting a temporary Tor instance for our interpretor to
-|interact with. This will have a minimal non-relaying configuration, and be
-|shut down when you're done.
-|
-|--------------------------------------------------------------------------------
-|
-
- #################
-# OUTPUT OF /HELP #
- #################
-
-# Response for the '/help' command without any arguments.
-
-help.general
-|Interpretor commands include:
-| /help - provides information for interpretor and tor commands
-| /events - prints events that we've received
-| /info - general information for a relay
-| /python - enable or disable support for running python commands
-| /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]'.
-
-# Usage of tor and interpretor commands.
-
-help.usage HELP => /help [OPTION]
-help.usage EVENTS => /events [types]
-help.usage INFO => /info [relay fingerprint, nickname, or IP address]
-help.usage PYTHON => /python [enable,disable]
-help.usage QUIT => /quit
-help.usage GETINFO => GETINFO OPTION
-help.usage GETCONF => GETCONF OPTION
-help.usage SETCONF => SETCONF PARAM[=VALUE]
-help.usage RESETCONF => RESETCONF PARAM[=VALUE]
-help.usage SIGNAL => SIGNAL SIG
-help.usage SETEVENTS => SETEVENTS [EXTENDED] [EVENTS]
-help.usage USEFEATURE => USEFEATURE OPTION
-help.usage SAVECONF => SAVECONF
-help.usage LOADCONF => LOADCONF...
-help.usage MAPADDRESS => MAPADDRESS SOURCE_ADDR=DESTINATION_ADDR
-help.usage POSTDESCRIPTOR => POSTDESCRIPTOR [purpose=general/controller/bridge] [cache=yes/no]...
-help.usage EXTENDCIRCUIT => EXTENDCIRCUIT CircuitID [PATH] [purpose=general/controller]
-help.usage SETCIRCUITPURPOSE => SETCIRCUITPURPOSE CircuitID purpose=general/controller
-help.usage CLOSECIRCUIT => CLOSECIRCUIT CircuitID [IfUnused]
-help.usage ATTACHSTREAM => ATTACHSTREAM StreamID CircuitID [HOP=HopNum]
-help.usage REDIRECTSTREAM => REDIRECTSTREAM StreamID Address [Port]
-help.usage CLOSESTREAM => CLOSESTREAM StreamID Reason [Flag]
-help.usage RESOLVE => RESOLVE [mode=reverse] address
-help.usage TAKEOWNERSHIP => TAKEOWNERSHIP
-help.usage PROTOCOLINFO => PROTOCOLINFO [ProtocolVersion]
-
-# Longer description of what tor and interpretor commands do.
-
-help.description.help
-|Provides usage information for the given interpretor, tor command, or tor
-|configuration option.
-|
-|Example:
-| /help info # provides a description of the '/info' option
-| /help GETINFO # usage information for tor's GETINFO controller option
-
-help.description.events
-|Provides events that we've received belonging to the given event types. If
-|no types are specified then this provides all the messages that we've
-|received.
-
-help.description.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.description.python
-|Enables or disables support for running python commands. This determines how
-|we treat commands this interpretor doesn't recognize...
-|
-|* If enabled then unrecognized commands are executed as python.
-|* If disabled then unrecognized commands are passed along to tor.
-
-help.description.quit
-|Terminates the interpretor.
-
-help.description.getinfo
-|Queries the tor process for information. Options are...
-|
-
-help.description.getconf
-|Provides the current value for a given configuration value. Options include...
-|
-
-help.description.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@xxxxxxx
-|
-| * Sets an exit policy that only includes port 80/443
-| SETCONF ExitPolicy=\"accept *:80, accept *:443, reject *:*\"\
-
-help.description.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.description.signal
-|Issues a signal that tells the tor process to reload its torrc, dump its
-|stats, halt, etc.
-
-help.description.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.description.usefeature
-|Customizes the behavior of the control port. Options include...
-|
-
-help.description.saveconf
-|Writes Tor's current configuration to its torrc.
-
-help.description.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.description.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.description.postdescriptor
-|Simulates getting a new relay descriptor.
-
-help.description.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.description.setcircuitpurpose
-|Sets the purpose attribute for a circuit.
-
-help.description.closecircuit
-|Closes the given circuit. If "IfUnused" is included then this only closes
-|the circuit if it isn't currently being used.
-
-help.description.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.description.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.description.closestream
-|Closes the given stream, the reason being an integer matching a reason as
-|per section 6.3 of the tor-spec.
-
-help.description.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.description.takeownership
-|Instructs Tor to gracefully shut down when this control connection is closed.
-
-help.description.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.signal.options RELOAD / HUP => reload our torrc
-help.signal.options SHUTDOWN / INT => gracefully shut down, waiting 30 seconds if we're a relay
-help.signal.options DUMP / USR1 => logs information about open connections and circuits
-help.signal.options DEBUG / USR2 => makes us log at the DEBUG runlevel
-help.signal.options HALT / TERM => immediately shut down
-help.signal.options CLEARDNSCACHE => clears any cached DNS results
-help.signal.options NEWNYM => clears the DNS cache and uses new circuits for future connections
-
- ################
-# TAB COMPLETION #
- ################
-
-# Commands we'll autocomplete when the user hits tab. This is just the start of
-# our autocompletion list - more are determined dynamically by checking what
-# tor supports.
-
-autocomplete /help
-autocomplete /events
-autocomplete /info
-autocomplete /quit
-autocomplete SAVECONF
-autocomplete MAPADDRESS
-autocomplete EXTENDCIRCUIT
-autocomplete SETCIRCUITPURPOSE
-autocomplete SETROUTERPURPOSE
-autocomplete ATTACHSTREAM
-#autocomplete +POSTDESCRIPTOR # TODO: needs multi-line support
-autocomplete REDIRECTSTREAM
-autocomplete CLOSESTREAM
-autocomplete CLOSECIRCUIT
-autocomplete QUIT
-autocomplete RESOLVE
-autocomplete PROTOCOLINFO
-#autocomplete +LOADCONF # TODO: needs multi-line support
-autocomplete TAKEOWNERSHIP
-autocomplete AUTHCHALLENGE
-autocomplete DROPGUARDS
-
diff --git a/stem/util/system.py b/stem/util/system.py
index 7827eb6..c940578 100644
--- a/stem/util/system.py
+++ b/stem/util/system.py
@@ -910,7 +910,7 @@ def get_process_name():
#
# ' '.join(['python'] + sys.argv)
#
- # ... doesn't do the trick since this will miss interpretor arguments.
+ # ... doesn't do the trick since this will miss interpreter arguments.
#
# python -W ignore::DeprecationWarning my_script.py
diff --git a/test/settings.cfg b/test/settings.cfg
index 79852b0..2ef3736 100644
--- a/test/settings.cfg
+++ b/test/settings.cfg
@@ -183,10 +183,10 @@ test.unit_tests
|test.unit.connection.authentication.TestAuthenticate
|test.unit.connection.connect.TestConnect
|test.unit.control.controller.TestControl
-|test.unit.interpretor.arguments.TestArgumentParsing
-|test.unit.interpretor.autocomplete.TestAutocompletion
-|test.unit.interpretor.help.TestHelpResponses
-|test.unit.interpretor.commands.TestInterpretorCommands
+|test.unit.interpreter.arguments.TestArgumentParsing
+|test.unit.interpreter.autocomplete.TestAutocompletion
+|test.unit.interpreter.help.TestHelpResponses
+|test.unit.interpreter.commands.TestInterpretorCommands
|test.unit.doctest.TestDocumentation
test.integ_tests
diff --git a/test/unit/interpreter/__init__.py b/test/unit/interpreter/__init__.py
new file mode 100644
index 0000000..7c10768
--- /dev/null
+++ b/test/unit/interpreter/__init__.py
@@ -0,0 +1,39 @@
+"""
+Unit tests for the stem's interpreter prompt.
+"""
+
+__all__ = [
+ 'arguments',
+ 'autocomplete',
+ 'commands',
+ 'help',
+]
+
+try:
+ # added in python 3.3
+ from unittest.mock import Mock
+except ImportError:
+ from mock import Mock
+
+GETINFO_NAMES = """
+info/names -- List of GETINFO options, types, and documentation.
+ip-to-country/* -- Perform a GEOIP lookup
+md/id/* -- Microdescriptors by ID
+""".strip()
+
+GETCONF_NAMES = """
+ExitNodes RouterList
+ExitPolicy LineList
+ExitPolicyRejectPrivate Boolean
+""".strip()
+
+
+CONTROLLER = Mock()
+
+CONTROLLER.get_info.side_effect = lambda arg, _: {
+ 'info/names': GETINFO_NAMES,
+ 'config/names': GETCONF_NAMES,
+ 'events/names': 'BW DEBUG INFO NOTICE',
+ 'features/names': 'VERBOSE_NAMES EXTENDED_EVENTS',
+ 'signal/names': 'RELOAD HUP SHUTDOWN',
+}[arg]
diff --git a/test/unit/interpreter/arguments.py b/test/unit/interpreter/arguments.py
new file mode 100644
index 0000000..60fda3d
--- /dev/null
+++ b/test/unit/interpreter/arguments.py
@@ -0,0 +1,57 @@
+import unittest
+
+from stem.interpreter.arguments import DEFAULT_ARGS, parse, get_help
+
+
+class TestArgumentParsing(unittest.TestCase):
+ def test_that_we_get_default_values(self):
+ args = parse([])
+
+ for attr in DEFAULT_ARGS:
+ self.assertEqual(DEFAULT_ARGS[attr], getattr(args, attr))
+
+ def test_that_we_load_arguments(self):
+ args = parse(['--interface', '10.0.0.25:80'])
+ self.assertEqual('10.0.0.25', args.control_address)
+ self.assertEqual(80, args.control_port)
+
+ args = parse(['--interface', '80'])
+ self.assertEqual(DEFAULT_ARGS['control_address'], args.control_address)
+ self.assertEqual(80, args.control_port)
+
+ args = parse(['--socket', '/tmp/my_socket'])
+ self.assertEqual('/tmp/my_socket', args.control_socket)
+
+ args = parse(['--help'])
+ self.assertEqual(True, args.print_help)
+
+ def test_examples(self):
+ args = parse(['-i', '1643'])
+ self.assertEqual(1643, args.control_port)
+
+ args = parse(['-s', '~/.tor/socket'])
+ self.assertEqual('~/.tor/socket', args.control_socket)
+
+ def test_that_we_reject_unrecognized_arguments(self):
+ self.assertRaises(ValueError, parse, ['--blarg', 'stuff'])
+
+ def test_that_we_reject_invalid_interfaces(self):
+ invalid_inputs = (
+ '',
+ ' ',
+ 'blarg',
+ '127.0.0.1',
+ '127.0.0.1:',
+ ':80',
+ '400.0.0.1:80',
+ '127.0.0.1:-5',
+ '127.0.0.1:500000',
+ )
+
+ for invalid_input in invalid_inputs:
+ self.assertRaises(ValueError, parse, ['--interface', invalid_input])
+
+ def test_get_help(self):
+ help_text = get_help()
+ self.assertTrue('Interactive interpreter for Tor.' in help_text)
+ self.assertTrue('change control interface from 127.0.0.1:9051' in help_text)
diff --git a/test/unit/interpreter/autocomplete.py b/test/unit/interpreter/autocomplete.py
new file mode 100644
index 0000000..40bcab4
--- /dev/null
+++ b/test/unit/interpreter/autocomplete.py
@@ -0,0 +1,112 @@
+import unittest
+
+from stem.interpreter.autocomplete import _get_commands, Autocompleter
+
+from test.unit.interpreter import CONTROLLER
+
+try:
+ # added in python 3.3
+ from unittest.mock import Mock
+except ImportError:
+ from mock import Mock
+
+
+class TestAutocompletion(unittest.TestCase):
+ def test_autocomplete_results_from_config(self):
+ """
+ Check that we load autocompletion results from our configuration.
+ """
+
+ commands = _get_commands(None)
+ self.assertTrue('PROTOCOLINFO' in commands)
+ self.assertTrue('/quit' in commands)
+
+ def test_autocomplete_results_from_tor(self):
+ """
+ Check our ability to determine autocompletion results based on our tor
+ instance's capabilities.
+ """
+
+ # Check that when GETINFO requests fail we have base commands, but nothing
+ # with arguments.
+
+ controller = Mock()
+ controller.get_info.return_value = None
+ commands = _get_commands(controller)
+
+ self.assertTrue('GETINFO ' in commands)
+ self.assertTrue('GETCONF ' in commands)
+ self.assertTrue('SIGNAL ' in commands)
+
+ self.assertFalse('GETINFO info/names' in commands)
+ self.assertFalse('GETCONF ExitPolicy' in commands)
+ self.assertFalse('SIGNAL SHUTDOWN' in commands)
+
+ # Now check where we should be able to determine tor's capabilities.
+
+ commands = _get_commands(CONTROLLER)
+
+ expected = (
+ 'GETINFO info/names',
+ 'GETINFO ip-to-country/',
+ 'GETINFO md/id/',
+
+ 'GETCONF ExitNodes',
+ 'GETCONF ExitPolicy',
+ 'SETCONF ExitPolicy',
+ 'RESETCONF ExitPolicy',
+
+ 'SETEVENTS BW',
+ 'SETEVENTS INFO',
+ 'USEFEATURE VERBOSE_NAMES',
+ 'USEFEATURE EXTENDED_EVENTS',
+ 'SIGNAL RELOAD',
+ 'SIGNAL SHUTDOWN',
+ )
+
+ for result in expected:
+ self.assertTrue(result in commands)
+
+ # We shouldn't include the base commands since we have results with
+ # their arguments.
+
+ self.assertFalse('GETINFO ' in commands)
+ self.assertFalse('GETCONF ' in commands)
+ self.assertFalse('SIGNAL ' in commands)
+
+ def test_autocompleter_match(self):
+ """
+ Exercise our Autocompleter's match method.
+ """
+
+ autocompleter = Autocompleter(None)
+
+ self.assertEqual(['/help'], autocompleter.matches('/help'))
+ self.assertEqual(['/help'], autocompleter.matches('/hel'))
+ self.assertEqual(['/help'], autocompleter.matches('/he'))
+ self.assertEqual(['/help'], autocompleter.matches('/h'))
+ self.assertEqual(['/help', '/events', '/info', '/quit'], autocompleter.matches('/'))
+
+ # check case sensitivity
+
+ self.assertEqual(['/help'], autocompleter.matches('/HELP'))
+ self.assertEqual(['/help'], autocompleter.matches('/HeLp'))
+
+ # check when we shouldn't have any matches
+
+ self.assertEqual([], autocompleter.matches('blarg'))
+
+ def test_autocompleter_complete(self):
+ """
+ Exercise our Autocompleter's complete method.
+ """
+
+ autocompleter = Autocompleter(None)
+
+ self.assertEqual('/help', autocompleter.complete('/', 0))
+ self.assertEqual('/events', autocompleter.complete('/', 1))
+ self.assertEqual('/info', autocompleter.complete('/', 2))
+ self.assertEqual('/quit', autocompleter.complete('/', 3))
+ self.assertEqual(None, autocompleter.complete('/', 4))
+
+ self.assertEqual(None, autocompleter.complete('blarg', 0))
diff --git a/test/unit/interpreter/commands.py b/test/unit/interpreter/commands.py
new file mode 100644
index 0000000..9afab59
--- /dev/null
+++ b/test/unit/interpreter/commands.py
@@ -0,0 +1,198 @@
+import datetime
+import unittest
+
+import stem
+import stem.response
+import stem.version
+
+from stem.interpreter.commands import ControlInterpretor, _get_fingerprint
+
+from test import mocking
+from test.unit.interpreter import CONTROLLER
+
+try:
+ # added in python 3.3
+ from unittest.mock import Mock
+except ImportError:
+ from mock import Mock
+
+EXPECTED_EVENTS_RESPONSE = """\
+\x1b[34mBW 15 25\x1b[0m
+\x1b[34mBW 758 570\x1b[0m
+\x1b[34mDEBUG connection_edge_process_relay_cell(): Got an extended cell! Yay.\x1b[0m
+"""
+
+EXPECTED_INFO_RESPONSE = """\
+moria1 (9695DFC35FFEB861329B9F1AB04C46397020CE31)
+\x1b[34;1maddress: \x1b[0m128.31.0.34:9101 (us)
+\x1b[34;1mpublished: \x1b[0m05:52:05 05/05/2014
+\x1b[34;1mos: \x1b[0mLinux
+\x1b[34;1mversion: \x1b[0m0.2.5.3-alpha-dev
+\x1b[34;1mflags: \x1b[0mAuthority, Fast, Guard, HSDir, Named, Running, Stable, V2Dir, Valid
+\x1b[34;1mexit policy: \x1b[0mreject 1-65535
+\x1b[34;1mcontact: \x1b[0m1024D/28988BF5 arma mit edu
+"""
+
+EXPECTED_GETCONF_RESPONSE = """\
+\x1b[34;1mlog\x1b[0m\x1b[34m => notice stdout\x1b[0m
+\x1b[34;1maddress\x1b[0m\x1b[34m => \x1b[0m
+
+"""
+
+FINGERPRINT = '9695DFC35FFEB861329B9F1AB04C46397020CE31'
+
+
+class TestInterpretorCommands(unittest.TestCase):
+ def test_get_fingerprint_for_ourselves(self):
+ controller = Mock()
+
+ controller.get_info.side_effect = lambda arg: {
+ 'fingerprint': FINGERPRINT,
+ }[arg]
+
+ self.assertEqual(FINGERPRINT, _get_fingerprint('', controller))
+
+ controller.get_info.side_effect = stem.ControllerError
+ self.assertRaises(ValueError, _get_fingerprint, '', controller)
+
+ def test_get_fingerprint_for_fingerprint(self):
+ self.assertEqual(FINGERPRINT, _get_fingerprint(FINGERPRINT, Mock()))
+
+ def test_get_fingerprint_for_nickname(self):
+ controller, descriptor = Mock(), Mock()
+ descriptor.fingerprint = FINGERPRINT
+
+ controller.get_network_status.side_effect = lambda arg: {
+ 'moria1': descriptor,
+ }[arg]
+
+ self.assertEqual(FINGERPRINT, _get_fingerprint('moria1', controller))
+
+ controller.get_network_status.side_effect = stem.ControllerError
+ self.assertRaises(ValueError, _get_fingerprint, 'moria1', controller)
+
+ def test_get_fingerprint_for_address(self):
+ controller = Mock()
+
+ self.assertRaises(ValueError, _get_fingerprint, '127.0.0.1:-1', controller)
+ self.assertRaises(ValueError, _get_fingerprint, '127.0.0.901:80', controller)
+
+ descriptor = Mock()
+ descriptor.address = '127.0.0.1'
+ descriptor.or_port = 80
+ descriptor.fingerprint = FINGERPRINT
+
+ controller.get_network_statuses.return_value = [descriptor]
+
+ self.assertEqual(FINGERPRINT, _get_fingerprint('127.0.0.1', controller))
+ self.assertEqual(FINGERPRINT, _get_fingerprint('127.0.0.1:80', controller))
+ self.assertRaises(ValueError, _get_fingerprint, '127.0.0.1:81', controller)
+ self.assertRaises(ValueError, _get_fingerprint, '127.0.0.2', controller)
+
+ def test_get_fingerprint_for_unrecognized_inputs(self):
+ self.assertRaises(ValueError, _get_fingerprint, 'blarg!', Mock())
+
+ def test_when_disconnected(self):
+ controller = Mock()
+ controller.is_alive.return_value = False
+
+ interpreter = ControlInterpretor(controller)
+ self.assertRaises(stem.SocketClosed, interpreter.run_command, '/help')
+
+ def test_quit(self):
+ interpreter = ControlInterpretor(CONTROLLER)
+ self.assertRaises(stem.SocketClosed, interpreter.run_command, '/quit')
+ self.assertRaises(stem.SocketClosed, interpreter.run_command, 'QUIT')
+
+ def test_help(self):
+ interpreter = ControlInterpretor(CONTROLLER)
+
+ self.assertTrue('Interpretor commands include:' in interpreter.run_command('/help'))
+ self.assertTrue('Queries the tor process for information.' in interpreter.run_command('/help GETINFO'))
+ self.assertTrue('Queries the tor process for information.' in interpreter.run_command('/help GETINFO version'))
+
+ def test_events(self):
+ interpreter = ControlInterpretor(CONTROLLER)
+
+ # no received events
+
+ self.assertEqual('\n', interpreter.run_command('/events'))
+
+ # with enqueued events
+
+ event_contents = (
+ '650 BW 15 25',
+ '650 BW 758 570',
+ '650 DEBUG connection_edge_process_relay_cell(): Got an extended cell! Yay.',
+ )
+
+ for content in event_contents:
+ event = mocking.get_message(content)
+ stem.response.convert('EVENT', event)
+ interpreter._received_events.append(event)
+
+ self.assertEqual(EXPECTED_EVENTS_RESPONSE, interpreter.run_command('/events'))
+
+ def test_info(self):
+ controller, server_desc, ns_desc = Mock(), Mock(), Mock()
+
+ controller.get_microdescriptor.return_value = None
+ controller.get_server_descriptor.return_value = server_desc
+ controller.get_network_status.return_value = ns_desc
+
+ controller.get_info.side_effect = lambda arg, _: {
+ 'ip-to-country/128.31.0.34': 'us',
+ }[arg]
+
+ ns_desc.address = '128.31.0.34'
+ ns_desc.or_port = 9101
+ ns_desc.published = datetime.datetime(2014, 5, 5, 5, 52, 5)
+ ns_desc.nickname = 'moria1'
+ ns_desc.flags = ['Authority', 'Fast', 'Guard', 'HSDir', 'Named', 'Running', 'Stable', 'V2Dir', 'Valid']
+
+ server_desc.exit_policy.summary.return_value = 'reject 1-65535'
+ server_desc.platform = 'Linux'
+ server_desc.tor_version = stem.version.Version('0.2.5.3-alpha-dev')
+ server_desc.contact = '1024D/28988BF5 arma mit edu'
+
+ interpreter = ControlInterpretor(controller)
+ self.assertEqual(EXPECTED_INFO_RESPONSE, interpreter.run_command('/info ' + FINGERPRINT))
+
+ def test_unrecognized_interpreter_command(self):
+ interpreter = ControlInterpretor(CONTROLLER)
+
+ expected = "\x1b[1;31m'/unrecognized' isn't a recognized command\x1b[0m\n"
+ self.assertEqual(expected, interpreter.run_command('/unrecognized'))
+
+ def test_getinfo(self):
+ response = '250-version=0.2.5.1-alpha-dev (git-245ecfff36c0cecc)\r\n250 OK'
+
+ controller = Mock()
+ controller.msg.return_value = mocking.get_message(response)
+
+ interpreter = ControlInterpretor(controller)
+
+ self.assertEqual('\x1b[34m%s\x1b[0m\n' % response, interpreter.run_command('GETINFO version'))
+ controller.msg.assert_called_with('GETINFO version')
+
+ controller.msg.side_effect = stem.ControllerError('kaboom!')
+ self.assertEqual('\x1b[1;31mkaboom!\x1b[0m\n', interpreter.run_command('getinfo process/user'))
+
+ def test_getconf(self):
+ response = '250-Log=notice stdout\r\n250 Address'
+
+ controller = Mock()
+ controller.msg.return_value = mocking.get_message(response)
+
+ interpreter = ControlInterpretor(controller)
+
+ self.assertEqual('\x1b[34m%s\x1b[0m\n' % response, interpreter.run_command('GETCONF log address'))
+ controller.msg.assert_called_with('GETCONF log address')
+
+ def test_setevents(self):
+ controller = Mock()
+ controller.msg.return_value = mocking.get_message('250 OK')
+
+ interpreter = ControlInterpretor(controller)
+
+ self.assertEqual('\x1b[34m250 OK\x1b[0m\n', interpreter.run_command('SETEVENTS BW'))
diff --git a/test/unit/interpreter/help.py b/test/unit/interpreter/help.py
new file mode 100644
index 0000000..49df746
--- /dev/null
+++ b/test/unit/interpreter/help.py
@@ -0,0 +1,54 @@
+import unittest
+
+from stem.interpreter.help import response, _normalize
+
+from test.unit.interpreter import CONTROLLER
+
+
+class TestHelpResponses(unittest.TestCase):
+ def test_normalization(self):
+ self.assertEqual('', _normalize(''))
+ self.assertEqual('', _normalize(' '))
+
+ self.assertEqual('GETINFO', _normalize('GETINFO'))
+ self.assertEqual('GETINFO', _normalize('GetInfo'))
+ self.assertEqual('GETINFO', _normalize('getinfo'))
+ self.assertEqual('GETINFO', _normalize('GETINFO version'))
+ self.assertEqual('GETINFO', _normalize('GETINFO '))
+
+ self.assertEqual('INFO', _normalize('/info'))
+ self.assertEqual('INFO', _normalize('/info caerSidi'))
+
+ def test_unrecognized_option(self):
+ result = response(CONTROLLER, 'FOOBAR')
+ self.assertEqual("\x1b[1;31mNo help information available for 'FOOBAR'...\x1b[0m", result)
+
+ def test_general_help(self):
+ result = response(CONTROLLER, '')
+ self.assertTrue('Interpretor commands include:' in result)
+ self.assertTrue('\x1b[34;1m GETINFO\x1b[0m\x1b[34m - queries information from tor\x1b[0m\n' in result)
+
+ def test_getinfo_help(self):
+ result = response(CONTROLLER, 'GETINFO')
+ self.assertTrue('Queries the tor process for information. Options are...' in result)
+ self.assertTrue('\x1b[34;1minfo/names \x1b[0m\x1b[34m - List of GETINFO options, types, and documentation.' in result)
+
+ def test_getconf_help(self):
+ result = response(CONTROLLER, 'GETCONF')
+ self.assertTrue('Provides the current value for a given configuration value. Options include...' in result)
+ self.assertTrue('\x1b[34mExitNodes ExitPolicy' in result)
+
+ def test_signal_help(self):
+ result = response(CONTROLLER, 'SIGNAL')
+ self.assertTrue('Issues a signal that tells the tor process to' in result)
+ self.assertTrue('\x1b[34;1mRELOAD / HUP \x1b[0m\x1b[34m - reload our torrc' in result)
+
+ def test_setevents_help(self):
+ result = response(CONTROLLER, 'SETEVENTS')
+ self.assertTrue('Sets the events that we will receive.' in result)
+ self.assertTrue('\x1b[34mBW DEBUG INFO NOTICE\x1b[0m' in result)
+
+ def test_usefeature_help(self):
+ result = response(CONTROLLER, 'USEFEATURE')
+ self.assertTrue('Customizes the behavior of the control port.' in result)
+ self.assertTrue('\x1b[34mVERBOSE_NAMES EXTENDED_EVENTS\x1b[0m' in result)
diff --git a/test/unit/interpretor/__init__.py b/test/unit/interpretor/__init__.py
deleted file mode 100644
index 705734d..0000000
--- a/test/unit/interpretor/__init__.py
+++ /dev/null
@@ -1,39 +0,0 @@
-"""
-Unit tests for the stem's interpretor prompt.
-"""
-
-__all__ = [
- 'arguments',
- 'autocomplete',
- 'commands',
- 'help',
-]
-
-try:
- # added in python 3.3
- from unittest.mock import Mock
-except ImportError:
- from mock import Mock
-
-GETINFO_NAMES = """
-info/names -- List of GETINFO options, types, and documentation.
-ip-to-country/* -- Perform a GEOIP lookup
-md/id/* -- Microdescriptors by ID
-""".strip()
-
-GETCONF_NAMES = """
-ExitNodes RouterList
-ExitPolicy LineList
-ExitPolicyRejectPrivate Boolean
-""".strip()
-
-
-CONTROLLER = Mock()
-
-CONTROLLER.get_info.side_effect = lambda arg, _: {
- 'info/names': GETINFO_NAMES,
- 'config/names': GETCONF_NAMES,
- 'events/names': 'BW DEBUG INFO NOTICE',
- 'features/names': 'VERBOSE_NAMES EXTENDED_EVENTS',
- 'signal/names': 'RELOAD HUP SHUTDOWN',
-}[arg]
diff --git a/test/unit/interpretor/arguments.py b/test/unit/interpretor/arguments.py
deleted file mode 100644
index ab835c4..0000000
--- a/test/unit/interpretor/arguments.py
+++ /dev/null
@@ -1,57 +0,0 @@
-import unittest
-
-from stem.interpretor.arguments import DEFAULT_ARGS, parse, get_help
-
-
-class TestArgumentParsing(unittest.TestCase):
- def test_that_we_get_default_values(self):
- args = parse([])
-
- for attr in DEFAULT_ARGS:
- self.assertEqual(DEFAULT_ARGS[attr], getattr(args, attr))
-
- def test_that_we_load_arguments(self):
- args = parse(['--interface', '10.0.0.25:80'])
- self.assertEqual('10.0.0.25', args.control_address)
- self.assertEqual(80, args.control_port)
-
- args = parse(['--interface', '80'])
- self.assertEqual(DEFAULT_ARGS['control_address'], args.control_address)
- self.assertEqual(80, args.control_port)
-
- args = parse(['--socket', '/tmp/my_socket'])
- self.assertEqual('/tmp/my_socket', args.control_socket)
-
- args = parse(['--help'])
- self.assertEqual(True, args.print_help)
-
- def test_examples(self):
- args = parse(['-i', '1643'])
- self.assertEqual(1643, args.control_port)
-
- args = parse(['-s', '~/.tor/socket'])
- self.assertEqual('~/.tor/socket', args.control_socket)
-
- def test_that_we_reject_unrecognized_arguments(self):
- self.assertRaises(ValueError, parse, ['--blarg', 'stuff'])
-
- def test_that_we_reject_invalid_interfaces(self):
- invalid_inputs = (
- '',
- ' ',
- 'blarg',
- '127.0.0.1',
- '127.0.0.1:',
- ':80',
- '400.0.0.1:80',
- '127.0.0.1:-5',
- '127.0.0.1:500000',
- )
-
- for invalid_input in invalid_inputs:
- self.assertRaises(ValueError, parse, ['--interface', invalid_input])
-
- def test_get_help(self):
- help_text = get_help()
- self.assertTrue('Interactive interpretor for Tor.' in help_text)
- self.assertTrue('change control interface from 127.0.0.1:9051' in help_text)
diff --git a/test/unit/interpretor/autocomplete.py b/test/unit/interpretor/autocomplete.py
deleted file mode 100644
index 6541da3..0000000
--- a/test/unit/interpretor/autocomplete.py
+++ /dev/null
@@ -1,112 +0,0 @@
-import unittest
-
-from stem.interpretor.autocomplete import _get_commands, Autocompleter
-
-from test.unit.interpretor import CONTROLLER
-
-try:
- # added in python 3.3
- from unittest.mock import Mock
-except ImportError:
- from mock import Mock
-
-
-class TestAutocompletion(unittest.TestCase):
- def test_autocomplete_results_from_config(self):
- """
- Check that we load autocompletion results from our configuration.
- """
-
- commands = _get_commands(None)
- self.assertTrue('PROTOCOLINFO' in commands)
- self.assertTrue('/quit' in commands)
-
- def test_autocomplete_results_from_tor(self):
- """
- Check our ability to determine autocompletion results based on our tor
- instance's capabilities.
- """
-
- # Check that when GETINFO requests fail we have base commands, but nothing
- # with arguments.
-
- controller = Mock()
- controller.get_info.return_value = None
- commands = _get_commands(controller)
-
- self.assertTrue('GETINFO ' in commands)
- self.assertTrue('GETCONF ' in commands)
- self.assertTrue('SIGNAL ' in commands)
-
- self.assertFalse('GETINFO info/names' in commands)
- self.assertFalse('GETCONF ExitPolicy' in commands)
- self.assertFalse('SIGNAL SHUTDOWN' in commands)
-
- # Now check where we should be able to determine tor's capabilities.
-
- commands = _get_commands(CONTROLLER)
-
- expected = (
- 'GETINFO info/names',
- 'GETINFO ip-to-country/',
- 'GETINFO md/id/',
-
- 'GETCONF ExitNodes',
- 'GETCONF ExitPolicy',
- 'SETCONF ExitPolicy',
- 'RESETCONF ExitPolicy',
-
- 'SETEVENTS BW',
- 'SETEVENTS INFO',
- 'USEFEATURE VERBOSE_NAMES',
- 'USEFEATURE EXTENDED_EVENTS',
- 'SIGNAL RELOAD',
- 'SIGNAL SHUTDOWN',
- )
-
- for result in expected:
- self.assertTrue(result in commands)
-
- # We shouldn't include the base commands since we have results with
- # their arguments.
-
- self.assertFalse('GETINFO ' in commands)
- self.assertFalse('GETCONF ' in commands)
- self.assertFalse('SIGNAL ' in commands)
-
- def test_autocompleter_match(self):
- """
- Exercise our Autocompleter's match method.
- """
-
- autocompleter = Autocompleter(None)
-
- self.assertEqual(['/help'], autocompleter.matches('/help'))
- self.assertEqual(['/help'], autocompleter.matches('/hel'))
- self.assertEqual(['/help'], autocompleter.matches('/he'))
- self.assertEqual(['/help'], autocompleter.matches('/h'))
- self.assertEqual(['/help', '/events', '/info', '/quit'], autocompleter.matches('/'))
-
- # check case sensitivity
-
- self.assertEqual(['/help'], autocompleter.matches('/HELP'))
- self.assertEqual(['/help'], autocompleter.matches('/HeLp'))
-
- # check when we shouldn't have any matches
-
- self.assertEqual([], autocompleter.matches('blarg'))
-
- def test_autocompleter_complete(self):
- """
- Exercise our Autocompleter's complete method.
- """
-
- autocompleter = Autocompleter(None)
-
- self.assertEqual('/help', autocompleter.complete('/', 0))
- self.assertEqual('/events', autocompleter.complete('/', 1))
- self.assertEqual('/info', autocompleter.complete('/', 2))
- self.assertEqual('/quit', autocompleter.complete('/', 3))
- self.assertEqual(None, autocompleter.complete('/', 4))
-
- self.assertEqual(None, autocompleter.complete('blarg', 0))
diff --git a/test/unit/interpretor/commands.py b/test/unit/interpretor/commands.py
deleted file mode 100644
index dfdbddb..0000000
--- a/test/unit/interpretor/commands.py
+++ /dev/null
@@ -1,198 +0,0 @@
-import datetime
-import unittest
-
-import stem
-import stem.response
-import stem.version
-
-from stem.interpretor.commands import ControlInterpretor, _get_fingerprint
-
-from test import mocking
-from test.unit.interpretor import CONTROLLER
-
-try:
- # added in python 3.3
- from unittest.mock import Mock
-except ImportError:
- from mock import Mock
-
-EXPECTED_EVENTS_RESPONSE = """\
-\x1b[34mBW 15 25\x1b[0m
-\x1b[34mBW 758 570\x1b[0m
-\x1b[34mDEBUG connection_edge_process_relay_cell(): Got an extended cell! Yay.\x1b[0m
-"""
-
-EXPECTED_INFO_RESPONSE = """\
-moria1 (9695DFC35FFEB861329B9F1AB04C46397020CE31)
-\x1b[34;1maddress: \x1b[0m128.31.0.34:9101 (us)
-\x1b[34;1mpublished: \x1b[0m05:52:05 05/05/2014
-\x1b[34;1mos: \x1b[0mLinux
-\x1b[34;1mversion: \x1b[0m0.2.5.3-alpha-dev
-\x1b[34;1mflags: \x1b[0mAuthority, Fast, Guard, HSDir, Named, Running, Stable, V2Dir, Valid
-\x1b[34;1mexit policy: \x1b[0mreject 1-65535
-\x1b[34;1mcontact: \x1b[0m1024D/28988BF5 arma mit edu
-"""
-
-EXPECTED_GETCONF_RESPONSE = """\
-\x1b[34;1mlog\x1b[0m\x1b[34m => notice stdout\x1b[0m
-\x1b[34;1maddress\x1b[0m\x1b[34m => \x1b[0m
-
-"""
-
-FINGERPRINT = '9695DFC35FFEB861329B9F1AB04C46397020CE31'
-
-
-class TestInterpretorCommands(unittest.TestCase):
- def test_get_fingerprint_for_ourselves(self):
- controller = Mock()
-
- controller.get_info.side_effect = lambda arg: {
- 'fingerprint': FINGERPRINT,
- }[arg]
-
- self.assertEqual(FINGERPRINT, _get_fingerprint('', controller))
-
- controller.get_info.side_effect = stem.ControllerError
- self.assertRaises(ValueError, _get_fingerprint, '', controller)
-
- def test_get_fingerprint_for_fingerprint(self):
- self.assertEqual(FINGERPRINT, _get_fingerprint(FINGERPRINT, Mock()))
-
- def test_get_fingerprint_for_nickname(self):
- controller, descriptor = Mock(), Mock()
- descriptor.fingerprint = FINGERPRINT
-
- controller.get_network_status.side_effect = lambda arg: {
- 'moria1': descriptor,
- }[arg]
-
- self.assertEqual(FINGERPRINT, _get_fingerprint('moria1', controller))
-
- controller.get_network_status.side_effect = stem.ControllerError
- self.assertRaises(ValueError, _get_fingerprint, 'moria1', controller)
-
- def test_get_fingerprint_for_address(self):
- controller = Mock()
-
- self.assertRaises(ValueError, _get_fingerprint, '127.0.0.1:-1', controller)
- self.assertRaises(ValueError, _get_fingerprint, '127.0.0.901:80', controller)
-
- descriptor = Mock()
- descriptor.address = '127.0.0.1'
- descriptor.or_port = 80
- descriptor.fingerprint = FINGERPRINT
-
- controller.get_network_statuses.return_value = [descriptor]
-
- self.assertEqual(FINGERPRINT, _get_fingerprint('127.0.0.1', controller))
- self.assertEqual(FINGERPRINT, _get_fingerprint('127.0.0.1:80', controller))
- self.assertRaises(ValueError, _get_fingerprint, '127.0.0.1:81', controller)
- self.assertRaises(ValueError, _get_fingerprint, '127.0.0.2', controller)
-
- def test_get_fingerprint_for_unrecognized_inputs(self):
- self.assertRaises(ValueError, _get_fingerprint, 'blarg!', Mock())
-
- def test_when_disconnected(self):
- controller = Mock()
- controller.is_alive.return_value = False
-
- interpretor = ControlInterpretor(controller)
- self.assertRaises(stem.SocketClosed, interpretor.run_command, '/help')
-
- def test_quit(self):
- interpretor = ControlInterpretor(CONTROLLER)
- self.assertRaises(stem.SocketClosed, interpretor.run_command, '/quit')
- self.assertRaises(stem.SocketClosed, interpretor.run_command, 'QUIT')
-
- def test_help(self):
- interpretor = ControlInterpretor(CONTROLLER)
-
- self.assertTrue('Interpretor commands include:' in interpretor.run_command('/help'))
- self.assertTrue('Queries the tor process for information.' in interpretor.run_command('/help GETINFO'))
- self.assertTrue('Queries the tor process for information.' in interpretor.run_command('/help GETINFO version'))
-
- def test_events(self):
- interpretor = ControlInterpretor(CONTROLLER)
-
- # no received events
-
- self.assertEqual('\n', interpretor.run_command('/events'))
-
- # with enqueued events
-
- event_contents = (
- '650 BW 15 25',
- '650 BW 758 570',
- '650 DEBUG connection_edge_process_relay_cell(): Got an extended cell! Yay.',
- )
-
- for content in event_contents:
- event = mocking.get_message(content)
- stem.response.convert('EVENT', event)
- interpretor._received_events.append(event)
-
- self.assertEqual(EXPECTED_EVENTS_RESPONSE, interpretor.run_command('/events'))
-
- def test_info(self):
- controller, server_desc, ns_desc = Mock(), Mock(), Mock()
-
- controller.get_microdescriptor.return_value = None
- controller.get_server_descriptor.return_value = server_desc
- controller.get_network_status.return_value = ns_desc
-
- controller.get_info.side_effect = lambda arg, _: {
- 'ip-to-country/128.31.0.34': 'us',
- }[arg]
-
- ns_desc.address = '128.31.0.34'
- ns_desc.or_port = 9101
- ns_desc.published = datetime.datetime(2014, 5, 5, 5, 52, 5)
- ns_desc.nickname = 'moria1'
- ns_desc.flags = ['Authority', 'Fast', 'Guard', 'HSDir', 'Named', 'Running', 'Stable', 'V2Dir', 'Valid']
-
- server_desc.exit_policy.summary.return_value = 'reject 1-65535'
- server_desc.platform = 'Linux'
- server_desc.tor_version = stem.version.Version('0.2.5.3-alpha-dev')
- server_desc.contact = '1024D/28988BF5 arma mit edu'
-
- interpretor = ControlInterpretor(controller)
- self.assertEqual(EXPECTED_INFO_RESPONSE, interpretor.run_command('/info ' + FINGERPRINT))
-
- def test_unrecognized_interpretor_command(self):
- interpretor = ControlInterpretor(CONTROLLER)
-
- expected = "\x1b[1;31m'/unrecognized' isn't a recognized command\x1b[0m\n"
- self.assertEqual(expected, interpretor.run_command('/unrecognized'))
-
- def test_getinfo(self):
- response = '250-version=0.2.5.1-alpha-dev (git-245ecfff36c0cecc)\r\n250 OK'
-
- controller = Mock()
- controller.msg.return_value = mocking.get_message(response)
-
- interpretor = ControlInterpretor(controller)
-
- self.assertEqual('\x1b[34m%s\x1b[0m\n' % response, interpretor.run_command('GETINFO version'))
- controller.msg.assert_called_with('GETINFO version')
-
- controller.msg.side_effect = stem.ControllerError('kaboom!')
- self.assertEqual('\x1b[1;31mkaboom!\x1b[0m\n', interpretor.run_command('getinfo process/user'))
-
- def test_getconf(self):
- response = '250-Log=notice stdout\r\n250 Address'
-
- controller = Mock()
- controller.msg.return_value = mocking.get_message(response)
-
- interpretor = ControlInterpretor(controller)
-
- self.assertEqual('\x1b[34m%s\x1b[0m\n' % response, interpretor.run_command('GETCONF log address'))
- controller.msg.assert_called_with('GETCONF log address')
-
- def test_setevents(self):
- controller = Mock()
- controller.msg.return_value = mocking.get_message('250 OK')
-
- interpretor = ControlInterpretor(controller)
-
- self.assertEqual('\x1b[34m250 OK\x1b[0m\n', interpretor.run_command('SETEVENTS BW'))
diff --git a/test/unit/interpretor/help.py b/test/unit/interpretor/help.py
deleted file mode 100644
index e656060..0000000
--- a/test/unit/interpretor/help.py
+++ /dev/null
@@ -1,54 +0,0 @@
-import unittest
-
-from stem.interpretor.help import response, _normalize
-
-from test.unit.interpretor import CONTROLLER
-
-
-class TestHelpResponses(unittest.TestCase):
- def test_normalization(self):
- self.assertEqual('', _normalize(''))
- self.assertEqual('', _normalize(' '))
-
- self.assertEqual('GETINFO', _normalize('GETINFO'))
- self.assertEqual('GETINFO', _normalize('GetInfo'))
- self.assertEqual('GETINFO', _normalize('getinfo'))
- self.assertEqual('GETINFO', _normalize('GETINFO version'))
- self.assertEqual('GETINFO', _normalize('GETINFO '))
-
- self.assertEqual('INFO', _normalize('/info'))
- self.assertEqual('INFO', _normalize('/info caerSidi'))
-
- def test_unrecognized_option(self):
- result = response(CONTROLLER, 'FOOBAR')
- self.assertEqual("\x1b[1;31mNo help information available for 'FOOBAR'...\x1b[0m", result)
-
- def test_general_help(self):
- result = response(CONTROLLER, '')
- self.assertTrue('Interpretor commands include:' in result)
- self.assertTrue('\x1b[34;1m GETINFO\x1b[0m\x1b[34m - queries information from tor\x1b[0m\n' in result)
-
- def test_getinfo_help(self):
- result = response(CONTROLLER, 'GETINFO')
- self.assertTrue('Queries the tor process for information. Options are...' in result)
- self.assertTrue('\x1b[34;1minfo/names \x1b[0m\x1b[34m - List of GETINFO options, types, and documentation.' in result)
-
- def test_getconf_help(self):
- result = response(CONTROLLER, 'GETCONF')
- self.assertTrue('Provides the current value for a given configuration value. Options include...' in result)
- self.assertTrue('\x1b[34mExitNodes ExitPolicy' in result)
-
- def test_signal_help(self):
- result = response(CONTROLLER, 'SIGNAL')
- self.assertTrue('Issues a signal that tells the tor process to' in result)
- self.assertTrue('\x1b[34;1mRELOAD / HUP \x1b[0m\x1b[34m - reload our torrc' in result)
-
- def test_setevents_help(self):
- result = response(CONTROLLER, 'SETEVENTS')
- self.assertTrue('Sets the events that we will receive.' in result)
- self.assertTrue('\x1b[34mBW DEBUG INFO NOTICE\x1b[0m' in result)
-
- def test_usefeature_help(self):
- result = response(CONTROLLER, 'USEFEATURE')
- self.assertTrue('Customizes the behavior of the control port.' in result)
- self.assertTrue('\x1b[34mVERBOSE_NAMES EXTENDED_EVENTS\x1b[0m' in result)
diff --git a/tor-prompt b/tor-prompt
index fc115a3..0f3755b 100755
--- a/tor-prompt
+++ b/tor-prompt
@@ -2,7 +2,7 @@
# Copyright 2014, Damian Johnson and The Tor Project
# See LICENSE for licensing information
-import stem.interpretor
+import stem.interpreter
if __name__ == '__main__':
- stem.interpretor.main()
+ stem.interpreter.main()
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits