1""" 2Tests the stem.process functions with various use cases. 3""" 4 5from __future__ import absolute_import 6 7import binascii 8import hashlib 9import os 10import random 11import re 12import shutil 13import subprocess 14import tempfile 15import threading 16import time 17import unittest 18 19import stem.prereq 20import stem.process 21import stem.socket 22import stem.util.str_tools 23import stem.util.system 24import stem.util.test_tools 25import stem.util.tor_tools 26import stem.version 27import test 28import test.require 29 30from contextlib import contextmanager 31from stem.util.test_tools import asynchronous, assert_equal, assert_in, skip 32 33try: 34 # added in python 3.3 35 from unittest.mock import patch, Mock 36except ImportError: 37 from mock import patch, Mock 38 39BASIC_RELAY_TORRC = """\ 40SocksPort 9089 41ExtORPort 6001 42Nickname stemIntegTest 43ExitPolicy reject *:* 44PublishServerDescriptor 0 45DataDirectory %s 46""" 47 48TOR_CMD = 'tor' 49 50 51def random_port(): 52 while True: 53 port = random.randint(1024, 65535) 54 55 if stem.util.system.pid_by_port(port) is None: 56 return str(port) 57 58 59@contextmanager 60def tmp_directory(): 61 tmp_dir = tempfile.mkdtemp() 62 63 try: 64 yield tmp_dir 65 finally: 66 shutil.rmtree(tmp_dir) 67 68 69@contextmanager 70def torrc(): 71 with tmp_directory() as data_directory: 72 torrc_path = os.path.join(data_directory, 'torrc') 73 74 with open(torrc_path, 'w') as torrc_file: 75 torrc_file.write(BASIC_RELAY_TORRC % data_directory) 76 77 yield torrc_path 78 79 80def run_tor(tor_cmd, *args, **kwargs): 81 # python doesn't allow us to have individual keyword arguments when there's 82 # an arbitrary number of positional arguments, so explicitly checking 83 84 expect_failure = kwargs.pop('expect_failure', False) 85 with_torrc = kwargs.pop('with_torrc', False) 86 stdin = kwargs.pop('stdin', None) 87 88 if kwargs: 89 raise ValueError('Got unexpected keyword arguments: %s' % kwargs) 90 91 with torrc() as torrc_path: 92 if with_torrc: 93 args = ['-f', torrc_path] + list(args) 94 95 args = [tor_cmd] + list(args) 96 tor_process = subprocess.Popen(args, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE) 97 98 if stdin: 99 tor_process.stdin.write(stem.util.str_tools._to_bytes(stdin)) 100 101 stdout = tor_process.communicate()[0] 102 exit_status = tor_process.poll() 103 104 if exit_status and not expect_failure: 105 raise AssertionError("Tor failed to start when we ran: %s\n%s" % (' '.join(args), stdout)) 106 elif not exit_status and expect_failure: 107 raise AssertionError("Didn't expect tor to be able to start when we run: %s\n%s" % (' '.join(args), stdout)) 108 109 return stem.util.str_tools._to_unicode(stdout) if stem.prereq.is_python_3() else stdout 110 111 112class TestProcess(unittest.TestCase): 113 @staticmethod 114 def run_tests(args): 115 global TOR_CMD 116 TOR_CMD = args.tor_cmd 117 118 for func, async_test in stem.util.test_tools.ASYNC_TESTS.items(): 119 if func.startswith('test.integ.process.'): 120 async_test.run(TOR_CMD) 121 122 @asynchronous 123 def test_version_argument(tor_cmd): 124 """ 125 Check that 'tor --version' matches 'GETINFO version'. 126 """ 127 128 assert_equal('Tor version %s.\n' % test.tor_version(), run_tor(tor_cmd, '--version')) 129 130 @asynchronous 131 def test_help_argument(tor_cmd): 132 """ 133 Check that 'tor --help' provides the expected output. 134 """ 135 136 help_output = run_tor(tor_cmd, '--help') 137 138 if not help_output.startswith('Copyright (c) 2001') or not help_output.endswith('tor -f <torrc> [args]\nSee man page for options, or https://www.torproject.org/ for documentation.\n'): 139 raise AssertionError("Help output didn't have the expected strings: %s" % help_output) 140 141 assert_equal(help_output, run_tor(tor_cmd, '-h'), "'tor -h' should simply be an alias for 'tor --help'") 142 143 @asynchronous 144 def test_quiet_argument(tor_cmd): 145 """ 146 Check that we don't provide anything on stdout when running 'tor --quiet'. 147 """ 148 149 quiet_output = run_tor(tor_cmd, '--quiet', '--invalid_argument', 'true', expect_failure = True) 150 assert_equal('', quiet_output, 'No output should be provided with the --quiet argument') 151 152 @asynchronous 153 def test_hush_argument(tor_cmd): 154 """ 155 Check that we only get warnings and errors when running 'tor --hush'. 156 """ 157 158 output = run_tor(tor_cmd, '--hush', '--invalid_argument', with_torrc = True, expect_failure = True) 159 assert_in("[warn] Command-line option '--invalid_argument' with no value. Failing.", output) 160 161 output = run_tor(tor_cmd, '--hush', '--invalid_argument', 'true', with_torrc = True, expect_failure = True) 162 assert_in("[warn] Failed to parse/validate config: Unknown option 'invalid_argument'. Failing.", output) 163 164 @asynchronous 165 def test_hash_password(tor_cmd): 166 """ 167 Hash a controller password. It's salted so can't assert that we get a 168 particular value. Also, tor's output is unnecessarily verbose so including 169 hush to cut it down. 170 """ 171 172 output = run_tor(tor_cmd, '--hush', '--hash-password', 'my_password').splitlines()[-1] 173 174 if not re.match('^16:[0-9A-F]{58}$', output): 175 raise AssertionError("Unexpected response from 'tor --hash-password my_password': %s" % output) 176 177 # I'm not gonna even pretend to understand the following. Ported directly 178 # from tor's test_cmdline_args.py. 179 180 if stem.prereq.is_python_3(): 181 output_hex = binascii.a2b_hex(stem.util.str_tools._to_bytes(output).strip()[3:]) 182 salt, how, hashed = output_hex[:8], output_hex[8], output_hex[9:] 183 else: 184 output_hex = binascii.a2b_hex(output.strip()[3:]) 185 salt, how, hashed = output_hex[:8], ord(output_hex[8]), output_hex[9:] 186 187 count = (16 + (how & 15)) << ((how >> 4) + 6) 188 stuff = salt + b'my_password' 189 repetitions = count // len(stuff) + 1 190 inp = (stuff * repetitions)[:count] 191 assert_equal(hashlib.sha1(inp).digest(), hashed) 192 193 @asynchronous 194 def test_hash_password_requires_argument(tor_cmd): 195 """ 196 Check that 'tor --hash-password' balks if not provided with something to 197 hash. 198 """ 199 200 output = run_tor(tor_cmd, '--hash-password', expect_failure = True) 201 assert_in("[warn] Command-line option '--hash-password' with no value. Failing.", output) 202 203 @asynchronous 204 def test_dump_config_argument(tor_cmd): 205 """ 206 Exercises our 'tor --dump-config' arugments. 207 """ 208 209 short_output = run_tor(tor_cmd, '--dump-config', 'short', with_torrc = True) 210 non_builtin_output = run_tor(tor_cmd, '--dump-config', 'non-builtin', with_torrc = True) 211 full_output = run_tor(tor_cmd, '--dump-config', 'full', with_torrc = True) 212 run_tor(tor_cmd, '--dump-config', 'invalid_option', with_torrc = True, expect_failure = True) 213 214 assert_in('Nickname stemIntegTest', short_output) 215 assert_in('Nickname stemIntegTest', non_builtin_output) 216 assert_in('Nickname stemIntegTest', full_output) 217 218 @asynchronous 219 def test_validate_config_argument(tor_cmd): 220 """ 221 Exercises our 'tor --validate-config' argument. 222 """ 223 224 valid_output = run_tor(tor_cmd, '--verify-config', with_torrc = True) 225 assert_in('Configuration was valid\n', valid_output, 'Expected configuration to be valid') 226 run_tor(tor_cmd, '--verify-config', '-f', __file__, expect_failure = True) 227 228 @asynchronous 229 def test_list_fingerprint_argument(tor_cmd): 230 """ 231 Exercise our 'tor --list-fingerprint' argument. 232 """ 233 234 # This command should only work with a relay (which our test instance isn't). 235 236 output = run_tor(tor_cmd, '--list-fingerprint', with_torrc = True, expect_failure = True) 237 assert_in("Clients don't have long-term identity keys. Exiting.", output, 'Should fail to start due to lacking an ORPort') 238 239 with tmp_directory() as data_directory: 240 torrc_path = os.path.join(data_directory, 'torrc') 241 242 with open(torrc_path, 'w') as torrc_file: 243 torrc_file.write(BASIC_RELAY_TORRC % data_directory + '\nORPort 6954') 244 245 output = run_tor(tor_cmd, '--list-fingerprint', '-f', torrc_path) 246 nickname, fingerprint_with_spaces = output.splitlines()[-1].split(' ', 1) 247 fingerprint = fingerprint_with_spaces.replace(' ', '') 248 249 assert_equal('stemIntegTest', nickname) 250 assert_equal(49, len(fingerprint_with_spaces)) 251 252 if not stem.util.tor_tools.is_valid_fingerprint(fingerprint): 253 raise AssertionError('We should have a valid fingerprint: %s' % fingerprint) 254 255 with open(os.path.join(data_directory, 'fingerprint')) as fingerprint_file: 256 expected = 'stemIntegTest %s\n' % fingerprint 257 assert_equal(expected, fingerprint_file.read()) 258 259 @asynchronous 260 def test_list_torrc_options_argument(tor_cmd): 261 """ 262 Exercise our 'tor --list-torrc-options' argument. 263 """ 264 265 output = run_tor(tor_cmd, '--list-torrc-options') 266 267 if len(output.splitlines()) < 50: 268 raise AssertionError("'tor --list-torrc-options' should have numerous entries, but only had %i" % len(output.splitlines())) 269 elif 'UseBridges' not in output or 'SocksPort' not in output: 270 raise AssertionError("'tor --list-torrc-options' didn't have options we expect") 271 272 @asynchronous 273 def test_no_orphaned_process(tor_cmd): 274 """ 275 Check that when an exception arises in the middle of spawning tor that we 276 don't leave a lingering process. 277 """ 278 279 if not stem.util.system.is_available('sleep'): 280 skip('(sleep unavailable)') 281 282 with patch('re.compile', Mock(side_effect = KeyboardInterrupt('nope'))): 283 # We don't need to actually run tor for this test. Rather, any process will 284 # do the trick. Picking sleep so this'll clean itself up if our test fails. 285 286 mock_tor_process = subprocess.Popen(['sleep', '60']) 287 288 with patch('subprocess.Popen', Mock(return_value = mock_tor_process)): 289 try: 290 stem.process.launch_tor(tor_cmd) 291 raise AssertionError("tor shoudn't have started") 292 except KeyboardInterrupt as exc: 293 if os.path.exists('/proc/%s' % mock_tor_process.pid): 294 raise AssertionError('launch_tor() left a lingering tor process') 295 296 assert_equal('nope', str(exc)) 297 298 @asynchronous 299 def test_torrc_arguments(tor_cmd): 300 """ 301 Pass configuration options on the commandline. 302 """ 303 304 with torrc() as torrc_path: 305 config_args = [ 306 '+SocksPort', '9090', # append an extra SocksPort 307 '/ExtORPort', # drops our ExtORPort 308 '/TransPort', # drops a port we didn't originally have 309 '+ControlPort', '9005', # appends a ControlPort where we didn't have any before 310 ] 311 312 output = run_tor(tor_cmd, '-f', torrc_path, '--dump-config', 'short', *config_args) 313 result = [line for line in output.splitlines() if not line.startswith('DataDirectory')] 314 315 expected = [ 316 'ControlPort 9005', 317 'ExitPolicy reject *:*', 318 'Nickname stemIntegTest', 319 'PublishServerDescriptor 0', 320 'SocksPort 9089', 321 'SocksPort 9090', 322 ] 323 324 assert_equal(expected, result) 325 326 @asynchronous 327 def test_torrc_arguments_via_stdin(tor_cmd): 328 """ 329 Pass configuration options via stdin. 330 """ 331 332 if test.tor_version() < stem.version.Requirement.TORRC_VIA_STDIN: 333 skip('(requires %s)' % stem.version.Requirement.TORRC_VIA_STDIN) 334 335 with tmp_directory() as data_directory: 336 torrc = BASIC_RELAY_TORRC % data_directory 337 output = run_tor(tor_cmd, '-f', '-', '--dump-config', 'short', stdin = torrc) 338 assert_equal(sorted(torrc.splitlines()), sorted(output.splitlines())) 339 340 @asynchronous 341 def test_with_missing_torrc(tor_cmd): 342 """ 343 Provide a torrc path that doesn't exist. 344 """ 345 346 output = run_tor(tor_cmd, '-f', '/path/that/really/shouldnt/exist', '--verify-config', expect_failure = True) 347 assert_in('[warn] Unable to open configuration file "/path/that/really/shouldnt/exist".', output, 'Tor should refuse to read a non-existant torrc file') 348 349 output = run_tor(tor_cmd, '-f', '/path/that/really/shouldnt/exist', '--verify-config', '--ignore-missing-torrc') 350 assert_in('[notice] Configuration file "/path/that/really/shouldnt/exist" not present, using reasonable defaults.', output, 'Missing torrc should be allowed with --ignore-missing-torrc') 351 352 @asynchronous 353 def test_unanonymous_hidden_service_config_must_match(tor_cmd): 354 """ 355 Checking that startup fails if HiddenServiceNonAnonymousMode and 356 HiddenServiceSingleHopMode don't match. 357 """ 358 359 try: 360 stem.process.launch_tor_with_config( 361 tor_cmd = tor_cmd, 362 config = {'HiddenServiceNonAnonymousMode': '1'}, 363 ) 364 365 raise AssertionError("Tor shouldn't start with 'HiddenServiceNonAnonymousMode' set but not 'HiddenServiceSingleHopMode'") 366 except OSError as exc: 367 if test.tor_version() >= stem.version.Requirement.ADD_ONION_NON_ANONYMOUS: 368 assert_equal('Process terminated: HiddenServiceNonAnonymousMode does not provide any server anonymity. It must be used with HiddenServiceSingleHopMode set to 1.', str(exc)) 369 else: 370 assert_equal("Process terminated: Unknown option 'HiddenServiceNonAnonymousMode'. Failing.", str(exc)) 371 372 try: 373 stem.process.launch_tor_with_config( 374 tor_cmd = tor_cmd, 375 config = {'HiddenServiceSingleHopMode': '1'}, 376 ) 377 378 raise AssertionError("Tor shouldn't start with 'HiddenServiceSingleHopMode' set but not 'HiddenServiceNonAnonymousMode'") 379 except OSError as exc: 380 if test.tor_version() >= stem.version.Requirement.ADD_ONION_NON_ANONYMOUS: 381 assert_equal('Process terminated: HiddenServiceSingleHopMode does not provide any server anonymity. It must be used with HiddenServiceNonAnonymousMode set to 1.', str(exc)) 382 else: 383 assert_equal("Process terminated: Unknown option 'HiddenServiceSingleHopMode'. Failing.", str(exc)) 384 385 @asynchronous 386 def test_can_run_multithreaded(tor_cmd): 387 """ 388 Our launch_tor() function uses signal to support its timeout argument. 389 This only works in the main thread so ensure we give a useful message when 390 it isn't. 391 """ 392 393 with tmp_directory() as data_directory: 394 # Tries running tor in another thread with the given timeout argument. This 395 # issues an invalid torrc so we terminate right away if we get to the point 396 # of actually invoking tor. 397 # 398 # Returns None if launching tor is successful, and otherwise returns the 399 # exception we raised. 400 401 def launch_async_with_timeout(timeout_arg): 402 raised_exc = [None] 403 404 def short_launch(): 405 try: 406 stem.process.launch_tor_with_config( 407 tor_cmd = tor_cmd, 408 config = { 409 'SocksPort': 'invalid', 410 'DataDirectory': data_directory, 411 }, 412 completion_percent = 100, 413 timeout = timeout_arg, 414 ) 415 except Exception as exc: 416 raised_exc[0] = exc 417 418 t = threading.Thread(target = short_launch) 419 t.start() 420 t.join() 421 422 if 'Invalid SocksPort' in str(raised_exc[0]): 423 return None # got to the point of invoking tor 424 else: 425 return raised_exc[0] 426 427 exc = launch_async_with_timeout(0.5) 428 assert_equal(OSError, type(exc)) 429 assert_equal('Launching tor with a timeout can only be done in the main thread', str(exc)) 430 431 # We should launch successfully if no timeout is specified or we specify it 432 # to be 'None'. 433 434 if launch_async_with_timeout(None) is not None: 435 raise AssertionError('Launching tor without a timeout should be successful') 436 437 if launch_async_with_timeout(stem.process.DEFAULT_INIT_TIMEOUT) is not None: 438 raise AssertionError('Launching tor with the default timeout should be successful') 439 440 @asynchronous 441 def test_launch_tor_with_config_via_file(tor_cmd): 442 """ 443 Exercises launch_tor_with_config when we write a torrc to disk. 444 """ 445 446 with tmp_directory() as data_directory: 447 control_port = random_port() 448 control_socket, tor_process = None, None 449 450 try: 451 # Launch tor without a torrc, but with a control port. Confirms that this 452 # works by checking that we're still able to access the new instance. 453 454 with patch('stem.version.get_system_tor_version', Mock(return_value = stem.version.Version('0.0.0.1'))): 455 tor_process = stem.process.launch_tor_with_config( 456 tor_cmd = tor_cmd, 457 config = { 458 'SocksPort': random_port(), 459 'ControlPort': control_port, 460 'DataDirectory': data_directory, 461 }, 462 completion_percent = 0 463 ) 464 465 control_socket = stem.socket.ControlPort(port = int(control_port)) 466 stem.connection.authenticate(control_socket) 467 468 # exercises the socket 469 control_socket.send('GETCONF ControlPort') 470 getconf_response = control_socket.recv() 471 472 assert_equal('ControlPort=%s' % control_port, str(getconf_response)) 473 finally: 474 if control_socket: 475 control_socket.close() 476 477 if tor_process: 478 tor_process.kill() 479 tor_process.wait() 480 481 @asynchronous 482 def test_launch_tor_with_config_via_stdin(tor_cmd): 483 """ 484 Exercises launch_tor_with_config when we provide our torrc via stdin. 485 """ 486 487 if test.tor_version() < stem.version.Requirement.TORRC_VIA_STDIN: 488 skip('(requires %s)' % stem.version.Requirement.TORRC_VIA_STDIN) 489 490 with tmp_directory() as data_directory: 491 control_port = random_port() 492 control_socket, tor_process = None, None 493 494 try: 495 tor_process = stem.process.launch_tor_with_config( 496 tor_cmd = tor_cmd, 497 config = { 498 'SocksPort': random_port(), 499 'ControlPort': control_port, 500 'DataDirectory': data_directory, 501 }, 502 completion_percent = 0 503 ) 504 505 control_socket = stem.socket.ControlPort(port = int(control_port)) 506 stem.connection.authenticate(control_socket) 507 508 # exercises the socket 509 control_socket.send('GETCONF ControlPort') 510 getconf_response = control_socket.recv() 511 512 assert_equal('ControlPort=%s' % control_port, str(getconf_response)) 513 finally: 514 if control_socket: 515 control_socket.close() 516 517 if tor_process: 518 tor_process.kill() 519 tor_process.wait() 520 521 @asynchronous 522 def test_with_invalid_config(tor_cmd): 523 """ 524 Spawn a tor process with a configuration that should make it dead on arrival. 525 """ 526 527 # Set the same SocksPort and ControlPort, this should fail with... 528 # 529 # [warn] Failed to parse/validate config: Failed to bind one of the listener ports. 530 # [err] Reading config failed--see warnings above. 531 532 with tmp_directory() as data_directory: 533 both_ports = random_port() 534 535 try: 536 stem.process.launch_tor_with_config( 537 tor_cmd = tor_cmd, 538 config = { 539 'SocksPort': both_ports, 540 'ControlPort': both_ports, 541 'DataDirectory': data_directory, 542 }, 543 ) 544 545 raise AssertionError('Tor should fail to launch') 546 except OSError as exc: 547 assert_equal('Process terminated: Failed to bind one of the listener ports.', str(exc)) 548 549 def test_launch_tor_with_timeout(self): 550 """ 551 Runs launch_tor where it times out before completing. 552 """ 553 554 with tmp_directory() as data_directory: 555 start_time = time.time() 556 557 try: 558 stem.process.launch_tor_with_config( 559 tor_cmd = TOR_CMD, 560 timeout = 0.05, 561 config = { 562 'SocksPort': random_port(), 563 'DataDirectory': data_directory, 564 }, 565 ) 566 567 raise AssertionError('Tor should fail to launch') 568 except OSError: 569 runtime = time.time() - start_time 570 571 if not (runtime > 0.05 and runtime < 3): 572 raise AssertionError('Test should have taken 0.05-3 seconds, took %0.1f instead' % runtime) 573 574 @asynchronous 575 def test_take_ownership_via_pid(tor_cmd): 576 """ 577 Checks that the tor process quits after we do if we set take_ownership. To 578 test this we spawn a process and trick tor into thinking that it is us. 579 """ 580 581 if not stem.util.system.is_available('sleep'): 582 skip('(sleep unavailable)') 583 elif test.tor_version() < stem.version.Requirement.TAKEOWNERSHIP: 584 skip('(requires %s)' % stem.version.Requirement.TAKEOWNERSHIP) 585 586 with tmp_directory() as data_directory: 587 sleep_process = subprocess.Popen(['sleep', '60']) 588 589 tor_process = stem.process.launch_tor_with_config( 590 tor_cmd = tor_cmd, 591 config = { 592 'SocksPort': random_port(), 593 'ControlPort': random_port(), 594 'DataDirectory': data_directory, 595 '__OwningControllerProcess': str(sleep_process.pid), 596 }, 597 completion_percent = 0, 598 ) 599 600 # Kill the sleep command. Tor should quit shortly after. 601 602 sleep_process.kill() 603 sleep_process.communicate() 604 605 # tor polls for the process every fifteen seconds so this may take a 606 # while... 607 # 608 # https://trac.torproject.org/projects/tor/ticket/21281 609 610 start_time = time.time() 611 612 while time.time() - start_time < 30: 613 if tor_process.poll() == 0: 614 return # tor exited 615 616 time.sleep(0.01) 617 618 raise AssertionError("tor didn't quit after the process that owned it terminated") 619 620 @asynchronous 621 def test_take_ownership_via_controller(tor_cmd): 622 """ 623 Checks that the tor process quits after the controller that owns it 624 connects, then disconnects.. 625 """ 626 627 if test.tor_version() < stem.version.Requirement.TAKEOWNERSHIP: 628 skip('(requires %s)' % stem.version.Requirement.TAKEOWNERSHIP) 629 630 with tmp_directory() as data_directory: 631 control_port = random_port() 632 633 tor_process = stem.process.launch_tor_with_config( 634 tor_cmd = tor_cmd, 635 config = { 636 'SocksPort': random_port(), 637 'ControlPort': control_port, 638 'DataDirectory': data_directory, 639 }, 640 completion_percent = 0, 641 take_ownership = True, 642 ) 643 644 # We're the controlling process. Just need to connect then disconnect. 645 646 controller = stem.control.Controller.from_port(port = int(control_port)) 647 controller.authenticate() 648 controller.close() 649 650 # give tor a few seconds to quit 651 start_time = time.time() 652 653 while time.time() - start_time < 5: 654 if tor_process.poll() == 0: 655 return # tor exited 656 657 time.sleep(0.01) 658 659 raise AssertionError("tor didn't quit after the controller that owned it disconnected") 660