diff --git a/basicswap/bin/prepare.py b/basicswap/bin/prepare.py index 662f4f0..c6715f9 100755 --- a/basicswap/bin/prepare.py +++ b/basicswap/bin/prepare.py @@ -283,18 +283,23 @@ TOR_PROXY_HOST = os.getenv("TOR_PROXY_HOST", "127.0.0.1") TOR_PROXY_PORT = int(os.getenv("TOR_PROXY_PORT", 9050)) TOR_CONTROL_PORT = int(os.getenv("TOR_CONTROL_PORT", 9051)) TOR_DNS_PORT = int(os.getenv("TOR_DNS_PORT", 5353)) -TOR_CONTROL_LISTEN_INTERFACE = os.getenv( - "TOR_CONTROL_LISTEN_INTERFACE", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0" -) -TORRC_PROXY_HOST = os.getenv( - "TORRC_PROXY_HOST", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0" -) -TORRC_CONTROL_HOST = os.getenv( - "TORRC_CONTROL_HOST", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0" -) -TORRC_DNS_HOST = os.getenv( - "TORRC_DNS_HOST", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0" -) + + +def setTorrcVars(): + global TOR_CONTROL_LISTEN_INTERFACE, TORRC_PROXY_HOST, TORRC_CONTROL_HOST, TORRC_DNS_HOST + TOR_CONTROL_LISTEN_INTERFACE = os.getenv( + "TOR_CONTROL_LISTEN_INTERFACE", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0" + ) + TORRC_PROXY_HOST = os.getenv( + "TORRC_PROXY_HOST", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0" + ) + TORRC_CONTROL_HOST = os.getenv( + "TORRC_CONTROL_HOST", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0" + ) + TORRC_DNS_HOST = os.getenv( + "TORRC_DNS_HOST", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0" + ) + TEST_TOR_PROXY = toBool( os.getenv("TEST_TOR_PROXY", "true") @@ -2073,7 +2078,69 @@ def load_config(config_path): if not os.path.exists(config_path): exitWithError("{} does not exist".format(config_path)) with open(config_path) as fs: - return json.load(fs) + settings = json.load(fs) + + BSX_ALLOW_ENV_OVERRIDE = toBool(os.getenv("BSX_ALLOW_ENV_OVERRIDE", "false")) + + saved_env_var_settings = [ + ("setup_docker_mode", "BSX_DOCKER_MODE"), + ("setup_local_tor", "BSX_LOCAL_TOR"), + ("setup_tor_control_listen_interface", "TOR_CONTROL_LISTEN_INTERFACE"), + ("setup_torrc_proxy_host", "TORRC_PROXY_HOST"), + ("setup_torrc_control_host", "TORRC_CONTROL_HOST"), + ("setup_torrc_dns_host", "TORRC_DNS_HOST"), + ("tor_proxy_host", "TOR_PROXY_HOST"), + ("tor_proxy_port", "TOR_PROXY_PORT"), + ("tor_control_port", "TOR_CONTROL_PORT"), + ] + for setting in saved_env_var_settings: + config_name, env_name = setting + env_value = globals()[env_name] + saved_config_value = settings.get(config_name, env_value) + if saved_config_value != env_value: + if os.getenv(env_name): + # If the env var was manually set override the saved config if allowed else fail. + if BSX_ALLOW_ENV_OVERRIDE: + logger.warning( + f"Env var {env_name} differs from saved config '{config_name}', overriding." + ) + else: + print( + f"Env var {env_name} differs from saved config '{config_name}', set 'BSX_ALLOW_ENV_OVERRIDE' to override.", + file=sys.stderr, + ) + sys.exit(1) + else: + logger.info(f"Setting {env_name} from saved config '{config_name}'.") + globals()[env_name] = saved_config_value + # Recalculate env vars that depend on the changed var + if env_name == "BSX_LOCAL_TOR": + setTorrcVars() + return settings + + +def save_config(config_path, settings, add_options: bool = True) -> None: + + if add_options is True: + if os.getenv("BSX_DOCKER_MODE") or "docker_mode" not in settings: + settings["setup_docker_mode"] = BSX_DOCKER_MODE + if os.getenv("BSX_LOCAL_TOR") or "local_tor" not in settings: + settings["setup_local_tor"] = BSX_LOCAL_TOR + + # Add to settings only if manually set + if os.getenv("TOR_CONTROL_LISTEN_INTERFACE"): + settings["setup_tor_control_listen_interface"] = ( + TOR_CONTROL_LISTEN_INTERFACE + ) + if os.getenv("TORRC_PROXY_HOST"): + settings["setup_torrc_proxy_host"] = TORRC_PROXY_HOST + if os.getenv("TORRC_CONTROL_HOST"): + settings["setup_torrc_control_host"] = TORRC_CONTROL_HOST + if os.getenv("TORRC_DNS_HOST"): + settings["setup_torrc_dns_host"] = TORRC_DNS_HOST + + with open(config_path, "w") as fp: + json.dump(settings, fp, indent=4) def signal_handler(sig, frame): @@ -2143,6 +2210,7 @@ def ensure_coin_valid(coin: str, test_disabled: bool = True) -> None: def main(): global use_tor_proxy, with_coins_changed + setTorrcVars() data_dir = None bin_dir = None port_offset = None @@ -2735,9 +2803,7 @@ def main(): settings, coin, tor_control_password, enable=True, extra_opts=extra_opts ) - with open(config_path, "w") as fp: - json.dump(settings, fp, indent=4) - + save_config(config_path, settings) logger.info("Done.") return 0 @@ -2754,9 +2820,7 @@ def main(): extra_opts=extra_opts, ) - with open(config_path, "w") as fp: - json.dump(settings, fp, indent=4) - + save_config(config_path, settings) logger.info("Done.") return 0 @@ -2781,9 +2845,7 @@ def main(): if "manage_wallet_daemon" in coin_settings: coin_settings["manage_wallet_daemon"] = False - with open(config_path, "w") as fp: - json.dump(settings, fp, indent=4) - + save_config(config_path, settings) logger.info("Done.") return 0 @@ -2805,8 +2867,7 @@ def main(): coin_settings["manage_daemon"] = True if "manage_wallet_daemon" in coin_settings: coin_settings["manage_wallet_daemon"] = True - with open(config_path, "w") as fp: - json.dump(settings, fp, indent=4) + save_config(config_path, settings) logger.info("Done.") return 0 exitWithError("{} is already in the settings file".format(add_coin)) @@ -2821,7 +2882,6 @@ def main(): test_particl_encryption(data_dir, settings, chain, use_tor_proxy) settings["chainclients"][add_coin] = chainclients[add_coin] - settings["use_tor_proxy"] = use_tor_proxy if not no_cores: prepareCore(add_coin, known_coins[add_coin], settings, data_dir, extra_opts) @@ -2843,8 +2903,7 @@ def main(): use_tor_proxy, ) - with open(config_path, "w") as fp: - json.dump(settings, fp, indent=4) + save_config(config_path, settings) logger.info(f"Done. Coin {add_coin} successfully added.") return 0 @@ -2863,8 +2922,7 @@ def main(): if c not in settings["chainclients"]: settings["chainclients"][c] = chainclients[c] elif upgrade_cores: - with open(config_path) as fs: - settings = json.load(fs) + settings = load_config(config_path) with_coins_start = with_coins if not with_coins_changed: @@ -2913,8 +2971,8 @@ def main(): # Run second loop to update, so all versions are logged together. # Backup settings old_config_path = config_path[:-5] + "_" + str(int(time.time())) + ".json" - with open(old_config_path, "w") as fp: - json.dump(settings, fp, indent=4) + save_config(old_config_path, settings, add_options=False) + for c in with_coins: prepareCore(c, known_coins[c], settings, data_dir, extra_opts) current_coin_settings = chainclients[c] @@ -2927,8 +2985,7 @@ def main(): settings["chainclients"][c][ "core_version_group" ] = current_version_group - with open(config_path, "w") as fp: - json.dump(settings, fp, indent=4) + save_config(config_path, settings) logger.info("Done.") return 0 @@ -2978,8 +3035,7 @@ def main(): for c in with_coins: prepareDataDir(c, settings, chain, particl_wallet_mnemonic, extra_opts) - with open(config_path, "w") as fp: - json.dump(settings, fp, indent=4) + save_config(config_path, settings) if particl_wallet_mnemonic == "none": logger.info("Done.") diff --git a/tests/basicswap/extended/test_network.py b/tests/basicswap/extended/test_network.py index 872ed2d..15df05d 100644 --- a/tests/basicswap/extended/test_network.py +++ b/tests/basicswap/extended/test_network.py @@ -197,7 +197,7 @@ class Test(unittest.TestCase): logger.handlers = [] logger.setLevel(logging.INFO) # DEBUG shows many messages from requests.post formatter = logging.Formatter("%(asctime)s %(levelname)s : %(message)s") - stream_stdout = logging.StreamHandler() + stream_stdout = logging.StreamHandler(sys.stdout) stream_stdout.setFormatter(formatter) logger.addHandler(stream_stdout) diff --git a/tests/basicswap/extended/test_prepare.py b/tests/basicswap/extended/test_prepare.py index d65990c..7cb1fd2 100644 --- a/tests/basicswap/extended/test_prepare.py +++ b/tests/basicswap/extended/test_prepare.py @@ -6,30 +6,34 @@ # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. -import os -import sys +import importlib import json -import shutil import logging -import unittest -import threading import multiprocessing +import os +import shutil +import sys +import threading +import unittest from io import StringIO from unittest.mock import patch import basicswap.config as cfg from tests.basicswap.util import ( + make_boolean, read_json_api, waitForServer, ) +TEST_DELETE_DIRS = make_boolean(os.getenv("TEST_DELETE_DIRS", True)) bin_path = os.path.expanduser(os.getenv("TEST_BIN_PATH", "")) test_base_path = os.path.expanduser(os.getenv("TEST_PREPARE_PATH", "~/test_basicswap")) test_path_plain = os.path.join(test_base_path, "plain") test_path_encrypted = os.path.join(test_base_path, "encrypted") test_path_encrypt = os.path.join(test_base_path, "encrypt") + delay_event = threading.Event() logger = logging.getLogger() logger.level = logging.DEBUG @@ -59,14 +63,28 @@ def start_run(args, env_pairs=[]): class Test(unittest.TestCase): + @classmethod + def setUpClass(cls): + super(Test, cls).setUpClass() + # Reset env vars + reset_vars = ["BSX_ALLOW_ENV_OVERRIDE", "BSX_DOCKER_MODE", "BSX_LOCAL_TOR"] + for var_name in reset_vars: + if var_name in os.environ: + del os.environ[var_name] + @classmethod def tearDownClass(self): try: - for test_dir in (test_path_plain, test_path_encrypted, test_path_encrypt): - if os.path.exists(test_dir): - shutil.rmtree(test_dir) + if TEST_DELETE_DIRS: + for test_dir in ( + test_path_plain, + test_path_encrypted, + test_path_encrypt, + ): + if os.path.exists(test_dir): + shutil.rmtree(test_dir) except Exception as ex: - logger.warning("tearDownClass %s", str(ex)) + logger.warning(f"tearDownClass {ex}") super(Test, self).tearDownClass() def test_plain(self): @@ -146,6 +164,7 @@ class Test(unittest.TestCase): self.assertTrue( settings["chainclients"]["bitcoin"]["connection_type"] == "rpc" ) + assert settings.get("use_tor", False) is False logging.info("notorproxy") testargs = [ @@ -165,8 +184,89 @@ class Test(unittest.TestCase): "--usetorproxy and --notorproxy together" in fake_stderr.getvalue() ) + logger.info("Test persistent setup config.") + + with open(config_path) as fs: + settings = json.load(fs) + assert settings.get("use_tor", False) is False + assert settings["setup_docker_mode"] is False + assert settings["setup_local_tor"] is False + assert "setup_tor_control_listen_interface" not in settings + assert "setup_torrc_dns_host" not in settings + + os.environ["BSX_LOCAL_TOR"] = "true" + # Reimport to reset globals and set env + importlib.reload(prepareSystem) + + testargs = [ + "basicswap-prepare", + "-datadir=" + test_path_plain, + "--enabletor", + ] + with patch("sys.stderr", new=StringIO()) as fake_stderr: + with patch.object(sys, "argv", testargs): + with self.assertRaises(SystemExit) as cm: + prepareSystem.main() + + self.assertEqual(cm.exception.code, 1) + self.assertTrue( + "Env var BSX_LOCAL_TOR differs from saved config" + in fake_stderr.getvalue() + ) + + os.environ["BSX_ALLOW_ENV_OVERRIDE"] = "true" + os.environ["TOR_CONTROL_LISTEN_INTERFACE"] = "127.1.1.1" + importlib.reload(prepareSystem) + + with patch.object(sys, "argv", testargs): + prepareSystem.main() + + with open(config_path) as fs: + settings = json.load(fs) + assert settings.get("use_tor", False) is True + assert settings["setup_docker_mode"] is False + assert settings["setup_local_tor"] is True + assert settings["setup_tor_control_listen_interface"] == "127.1.1.1" + assert "setup_torrc_dns_host" not in settings + + particl_config_path = os.path.join( + test_path_plain, "particl", "particl.conf" + ) + with open(particl_config_path) as fs: + particl_conf = fs.read() + assert "bind=127.1.1.1:" in particl_conf + + testargs = [ + "basicswap-prepare", + "-datadir=" + test_path_plain, + "-disablecoin=bitcoin", + ] + with patch.object(sys, "argv", testargs): + prepareSystem.main() + + os.environ["TOR_PROXY_HOST"] = "127.2.2.2" + del os.environ["BSX_ALLOW_ENV_OVERRIDE"] + importlib.reload(prepareSystem) + + testargs = [ + "basicswap-prepare", + "-datadir=" + test_path_plain, + "-addcoin=bitcoin", + "-notorproxy", # Disable TOR connection check + ] + with patch("sys.stderr", new=StringIO()) as fake_stderr: + with patch.object(sys, "argv", testargs): + with self.assertRaises(SystemExit) as cm: + prepareSystem.main() + + self.assertEqual(cm.exception.code, 1) + self.assertTrue( + "Env var TOR_PROXY_HOST differs from saved config" + in fake_stderr.getvalue() + ) + finally: - del prepareSystem + del prepareSystem # Does not cause next import to refresh globals and env (tested v3.13) def test_encrypted(self): if os.path.exists(test_path_encrypted): diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index a85c7db..63b94a1 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -12,6 +12,7 @@ import os import random import shutil import signal +import sys import threading import time import traceback @@ -350,7 +351,7 @@ class BaseTest(unittest.TestCase): logger.handlers = [] logger.setLevel(logging.INFO) # DEBUG shows many messages from requests.post formatter = logging.Formatter("%(asctime)s %(levelname)s : %(message)s") - stream_stdout = logging.StreamHandler() + stream_stdout = logging.StreamHandler(sys.stdout) stream_stdout.setFormatter(formatter) logger.addHandler(stream_stdout) diff --git a/tests/basicswap/util.py b/tests/basicswap/util.py index 81a11c3..3b55a2e 100644 --- a/tests/basicswap/util.py +++ b/tests/basicswap/util.py @@ -19,7 +19,11 @@ REQUIRED_SETTINGS = { } -def make_boolean(s): +def make_boolean(s) -> bool: + if isinstance(s, bool): + return s + if isinstance(s, int): + return False if s == 0 else True return s.lower() in ["1", "true"]