diff --git a/sbws/util/stem.py b/sbws/util/stem.py index c3fffd1f..d8e5cf80 100644 --- a/sbws/util/stem.py +++ b/sbws/util/stem.py @@ -117,6 +117,52 @@ def _init_controller_socket(socket): return c +def parse_user_torrc_config(torrc, torrc_text): + """Parse the user configuration torrc text call `extra_lines` + to a dictionary suitable to use with stem and return a new torrc + dictionary that merges that dictionary with the existing torrc. + Example: + [tor] + extra_lines = + Log debug file /tmp/tor-debug.log + NumCPUs 1 + """ + torrc_dict = torrc.copy() + for line in torrc_text.split('\n'): + # Remove leading and trailing whitespace, if any + line = line.strip() + # Ignore blank lines + if len(line) < 1: + continue + # Some torrc options are only a key, some are a key value pair. + kv = line.split(None, 1) + if len(kv) > 1: + key, value = kv + else: + key = kv[0] + value = None + # It's really easy to add to the torrc if the key doesn't exist + if key not in torrc: + torrc_dict.update({key: value}) + # But if it does, we have to make a list of values. For example, say + # the user wants to add a SocksPort and we already have + # 'SocksPort auto' in the torrc. We'll go from + # torrc['SocksPort'] == 'auto' + # to + # torrc['SocksPort'] == ['auto', '9050'] + else: + existing_val = torrc[key] + if isinstance(existing_val, str): + torrc_dict.update({key: [existing_val, value]}) + else: + assert isinstance(existing_val, list) + existing_val.append(value) + torrc_dict.update({key: existing_val}) + log.debug('Adding "%s %s" to torrc with which we are launching Tor', + key, value) + return torrc_dict + + def launch_tor(conf): assert isinstance(conf, ConfigParser) os.makedirs(conf.getpath('tor', 'datadir'), mode=0o700, exist_ok=True) @@ -139,53 +185,8 @@ def launch_tor(conf): 'LearnCircuitBuildTimeout': '0', 'CircuitBuildTimeout': conf['general']['circuit_timeout'], }) - # This block of code reads additional torrc lines from the user's - # config.ini so they can add arbitrary additional options. - # - # The user can't replace our options, only add to them. For example, - # there's no way to remove 'SocksPort auto' (if it is still in - # TORRC_STARTING_POINT). If you add a SocksPort in your config.ini, you'll - # open two socks ports. - # - # As an example, maybe the user hates their HDD and wants to fill it with - # debug logs, and wants to tell Tor to use only 1 CPU core. - # - # [tor] - # extra_lines = - # Log debug file /tmp/tor-debug.log - # NumCPUs 1 - for line in conf['tor']['extra_lines'].split('\n'): - # Remove leading and trailing whitespace, if any - line = line.strip() - # Ignore blank lines - if len(line) < 1: - continue - # The way stem handles configuring Tor with a dictionary is the first - # word is a key and the remaining words are the value. - kv = line.split(None, 1) - if len(kv) < 2: - fail_hard('All torrc lines must have 2 or more words. "%s" has ' - 'fewer', line) - key, value = kv - log.info('Adding "%s %s" to torrc with which we are launching Tor', - key, value) - # It's really easy to add to the torrc if the key doesn't exist - if key not in torrc: - torrc.update({key: value}) - # But if it does, we have to make a list of values. For example, say - # the user wants to add a SocksPort and we already have - # 'SocksPort auto' in the torrc. We'll go from - # torrc['SocksPort'] == 'auto' - # to - # torrc['SocksPort'] == ['auto', '9050'] - else: - existing_val = torrc[key] - if isinstance(existing_val, str): - torrc.update({key: [existing_val, value]}) - else: - assert isinstance(existing_val, list) - existing_val.append(value) - torrc.update({key: existing_val}) + + torrc = parse_user_torrc_config(torrc, conf['tor']['extra_lines']) # Finally launch Tor try: stem.process.launch_tor_with_config( diff --git a/tests/unit/util/test_stem.py b/tests/unit/util/test_stem.py new file mode 100644 index 00000000..c2aafe98 --- /dev/null +++ b/tests/unit/util/test_stem.py @@ -0,0 +1,34 @@ +"""Unit tests for stem.py""" + +from sbws.util.stem import parse_user_torrc_config + + +def test_parse_user_torrc_config_new_keyvalue_options_success(): + config_torrc_extra_lines = """ + Log debug file /tmp/tor-debug.log + NumCPUs 1 + """ + torrc_dict = parse_user_torrc_config({}, config_torrc_extra_lines) + assert torrc_dict == \ + {'Log': 'debug file /tmp/tor-debug.log', 'NumCPUs': '1'} + + +def test_parse_user_torrc_config_existing_keyvalue_options_fail(caplog): + torrc_dict = {'SocksPort': 'auto'} + config_torrc_extra_lines = """ + SocksPort 9050 + """ + torrc_dict_new = parse_user_torrc_config( + torrc_dict, config_torrc_extra_lines) + # the new dictionary contains the existing key option and a list with both + # the existing value and the new value + assert torrc_dict_new != torrc_dict + assert torrc_dict_new == {'SocksPort': ['auto', '9050']} + + +def test_parse_user_torrc_config_new_key_option_success(): + config_torrc_extra_lines = """ + LongLivedPorts + """ + torrc_dict = parse_user_torrc_config({}, config_torrc_extra_lines) + assert torrc_dict == {'LongLivedPorts': None}