commit c5a7bbe46555f9a922639942561786c7155ca4f0 Author: Damian Johnson atagar@torproject.org 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@bar.com +| +| * 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@bar.com -| -| * 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@lists.torproject.org