1# Copyright 2015-2019, Damian Johnson and The Tor Project
2# See LICENSE for licensing information
3
4"""
5Interactive interpreter for interacting with Tor directly. This adds usability
6features such as tab completion, history, and IRC-style functions (like /help).
7"""
8
9import os
10import sys
11
12import stem
13import stem.connection
14import stem.prereq
15import stem.process
16import stem.util.conf
17import stem.util.system
18import stem.util.term
19
20from stem.util.term import Attr, Color, format
21
22__all__ = [
23  'arguments',
24  'autocomplete',
25  'commands',
26  'help',
27]
28
29PROMPT = format('>>> ', Color.GREEN, Attr.BOLD, Attr.READLINE_ESCAPE)
30
31STANDARD_OUTPUT = (Color.BLUE, Attr.LINES)
32BOLD_OUTPUT = (Color.BLUE, Attr.BOLD, Attr.LINES)
33HEADER_OUTPUT = (Color.GREEN, Attr.LINES)
34HEADER_BOLD_OUTPUT = (Color.GREEN, Attr.BOLD, Attr.LINES)
35ERROR_OUTPUT = (Attr.BOLD, Color.RED, Attr.LINES)
36
37settings_path = os.path.join(os.path.dirname(__file__), 'settings.cfg')
38uses_settings = stem.util.conf.uses_settings('stem_interpreter', settings_path)
39
40
41@uses_settings
42def msg(message, config, **attr):
43  return config.get(message).format(**attr)
44
45
46def main():
47  import readline
48
49  import stem.interpreter.arguments
50  import stem.interpreter.autocomplete
51  import stem.interpreter.commands
52
53  try:
54    args = stem.interpreter.arguments.parse(sys.argv[1:])
55  except ValueError as exc:
56    print(exc)
57    sys.exit(1)
58
59  if args.print_help:
60    print(stem.interpreter.arguments.get_help())
61    sys.exit()
62
63  if args.disable_color or not sys.stdout.isatty():
64    global PROMPT
65    stem.util.term.DISABLE_COLOR_SUPPORT = True
66    PROMPT = '>>> '
67
68  # If the user isn't connecting to something in particular then offer to start
69  # tor if it isn't running.
70
71  if not (args.user_provided_port or args.user_provided_socket):
72    is_tor_running = stem.util.system.is_running('tor') or stem.util.system.is_running('tor.real')
73
74    if not is_tor_running:
75      if args.tor_path == 'tor' and not stem.util.system.is_available('tor'):
76        print(format(msg('msg.tor_unavailable'), *ERROR_OUTPUT))
77        sys.exit(1)
78      else:
79        if not args.run_cmd and not args.run_path:
80          print(format(msg('msg.starting_tor'), *HEADER_OUTPUT))
81
82        control_port = '9051' if args.control_port == 'default' else str(args.control_port)
83
84        try:
85          stem.process.launch_tor_with_config(
86            config = {
87              'SocksPort': '0',
88              'ControlPort': control_port,
89              'CookieAuthentication': '1',
90              'ExitPolicy': 'reject *:*',
91            },
92            tor_cmd = args.tor_path,
93            completion_percent = 5,
94            take_ownership = True,
95          )
96        except OSError as exc:
97          print(format(msg('msg.unable_to_start_tor', error = exc), *ERROR_OUTPUT))
98          sys.exit(1)
99
100  control_port = (args.control_address, args.control_port)
101  control_socket = args.control_socket
102
103  # If the user explicitely specified an endpoint then just try to connect to
104  # that.
105
106  if args.user_provided_socket and not args.user_provided_port:
107    control_port = None
108  elif args.user_provided_port and not args.user_provided_socket:
109    control_socket = None
110
111  controller = stem.connection.connect(
112    control_port = control_port,
113    control_socket = control_socket,
114    password_prompt = True,
115  )
116
117  if controller is None:
118    sys.exit(1)
119
120  with controller:
121    interpreter = stem.interpreter.commands.ControlInterpreter(controller)
122    showed_close_confirmation = False
123
124    if args.run_cmd:
125      if args.run_cmd.upper().startswith('SETEVENTS '):
126        # TODO: we can use a lambda here when dropping python 2.x support, but
127        # until then print's status as a keyword prevents it from being used in
128        # lambdas
129
130        def handle_event(event_message):
131          print(format(str(event_message), *STANDARD_OUTPUT))
132
133        controller._handle_event = handle_event
134
135        if sys.stdout.isatty():
136          events = args.run_cmd.upper().split(' ', 1)[1]
137          print(format('Listening to %s events. Press any key to quit.\n' % events, *HEADER_BOLD_OUTPUT))
138
139        controller.msg(args.run_cmd)
140
141        try:
142          raw_input()
143        except (KeyboardInterrupt, stem.SocketClosed):
144          pass
145      else:
146        interpreter.run_command(args.run_cmd, print_response = True)
147    elif args.run_path:
148      try:
149        for line in open(args.run_path).readlines():
150          interpreter.run_command(line.strip(), print_response = True)
151      except IOError as exc:
152        print(format(msg('msg.unable_to_read_file', path = args.run_path, error = exc), *ERROR_OUTPUT))
153        sys.exit(1)
154
155    else:
156      autocompleter = stem.interpreter.autocomplete.Autocompleter(controller)
157      readline.parse_and_bind('tab: complete')
158      readline.set_completer(autocompleter.complete)
159      readline.set_completer_delims('\n')
160
161      for line in msg('msg.startup_banner').splitlines():
162        line_format = HEADER_BOLD_OUTPUT if line.startswith('  ') else HEADER_OUTPUT
163        print(format(line, *line_format))
164
165      print('')
166
167      while True:
168        try:
169          prompt = '... ' if interpreter.is_multiline_context else PROMPT
170          user_input = input(prompt) if stem.prereq.is_python_3() else raw_input(prompt)
171          interpreter.run_command(user_input, print_response = True)
172        except stem.SocketClosed:
173          if showed_close_confirmation:
174            print(format('Unable to run tor commands. The control connection has been closed.', *ERROR_OUTPUT))
175          else:
176            prompt = format("Tor's control port has closed. Do you want to continue this interpreter? (y/n) ", *HEADER_BOLD_OUTPUT)
177            user_input = input(prompt) if stem.prereq.is_python_3() else raw_input(prompt)
178            print('')  # blank line
179
180            if user_input.lower() in ('y', 'yes'):
181              showed_close_confirmation = True
182            else:
183              break
184        except (KeyboardInterrupt, EOFError, stem.SocketClosed):
185          print('')  # move cursor to the following line
186          break
187