diff --git a/src/allmydata/scripts/create_node.py b/src/allmydata/scripts/create_node.py index 7d15b95ec..85d1c46cd 100644 --- a/src/allmydata/scripts/create_node.py +++ b/src/allmydata/scripts/create_node.py @@ -1,3 +1,8 @@ + +from __future__ import annotations + +from typing import Optional + import io import os @@ -21,7 +26,37 @@ from allmydata.scripts.common import ( from allmydata.scripts.default_nodedir import _default_nodedir from allmydata.util.assertutil import precondition from allmydata.util.encodingutil import listdir_unicode, argv_to_unicode, quote_local_unicode_path, get_io_encoding -from allmydata.util import fileutil, i2p_provider, iputil, tor_provider, jsonbytes as json + +i2p_provider: Listener +tor_provider: Listener + +from allmydata.util import fileutil, i2p_provider, tor_provider, jsonbytes as json + +from ..listeners import ListenerConfig, Listener, TCPProvider, StaticProvider + +def _get_listeners() -> dict[str, Listener]: + """ + Get all of the kinds of listeners we might be able to use. + """ + return { + "tor": tor_provider, + "i2p": i2p_provider, + "tcp": TCPProvider(), + "none": StaticProvider( + available=True, + hide_ip=False, + config=defer.succeed(None), + # This is supposed to be an IAddressFamily but we have none for + # this kind of provider. We could implement new client and server + # endpoint types that always fail and pass an IAddressFamily here + # that uses those. Nothing would ever even ask for them (at + # least, yet), let alone try to use them, so that's a lot of extra + # work for no practical result so I'm not doing it now. + address=None, # type: ignore[arg-type] + ), + } + +_LISTENERS = _get_listeners() dummy_tac = """ import sys @@ -98,8 +133,11 @@ def validate_where_options(o): if o['listen'] != "none" and o.get('join', None) is None: listeners = o['listen'].split(",") for l in listeners: - if l not in ["tcp", "tor", "i2p"]: - raise UsageError("--listen= must be none, or one/some of: tcp, tor, i2p") + if l not in _LISTENERS: + raise UsageError( + "--listen= must be one/some of: " + f"{', '.join(sorted(_LISTENERS))}", + ) if 'tcp' in listeners and not o['hostname']: raise UsageError("--listen=tcp requires --hostname=") if 'tcp' not in listeners and o['hostname']: @@ -108,7 +146,7 @@ def validate_where_options(o): def validate_tor_options(o): use_tor = "tor" in o["listen"].split(",") if use_tor or any((o["tor-launch"], o["tor-control-port"])): - if tor_provider._import_txtorcon() is None: + if not _LISTENERS["tor"].is_available(): raise UsageError( "Specifying any Tor options requires the 'txtorcon' module" ) @@ -123,7 +161,7 @@ def validate_tor_options(o): def validate_i2p_options(o): use_i2p = "i2p" in o["listen"].split(",") if use_i2p or any((o["i2p-launch"], o["i2p-sam-port"])): - if i2p_provider._import_txi2p() is None: + if not _LISTENERS["i2p"].is_available(): raise UsageError( "Specifying any I2P options requires the 'txi2p' module" ) @@ -145,7 +183,7 @@ class _CreateBaseOptions(BasedirOptions): def postOptions(self): super(_CreateBaseOptions, self).postOptions() if self['hide-ip']: - if tor_provider._import_txtorcon() is None and i2p_provider._import_txi2p() is None: + if not (_LISTENERS["tor"].is_available() or _LISTENERS["i2p"].is_available()): raise UsageError( "--hide-ip was specified but neither 'txtorcon' nor 'txi2p' " "are installed.\nTo do so:\n pip install tahoe-lafs[tor]\nor\n" @@ -218,8 +256,20 @@ class CreateIntroducerOptions(NoDefaultBasedirOptions): validate_i2p_options(self) -@defer.inlineCallbacks -def write_node_config(c, config): +def merge_config( + left: Optional[ListenerConfig], + right: Optional[ListenerConfig], +) -> Optional[ListenerConfig]: + if left is None or right is None: + return None + return ListenerConfig( + list(left.tub_ports) + list(right.tub_ports), + list(left.tub_locations) + list(right.tub_locations), + dict(list(left.node_config.items()) + list(right.node_config.items())), + ) + + +async def write_node_config(c, config): # this is shared between clients and introducers c.write("# -*- mode: conf; coding: {c.encoding} -*-\n".format(c=c)) c.write("\n") @@ -232,9 +282,10 @@ def write_node_config(c, config): if config["hide-ip"]: c.write("[connections]\n") - if tor_provider._import_txtorcon(): + if _LISTENERS["tor"].is_available(): c.write("tcp = tor\n") else: + # XXX What about i2p? c.write("tcp = disabled\n") c.write("\n") @@ -253,38 +304,23 @@ def write_node_config(c, config): c.write("web.port = %s\n" % (webport,)) c.write("web.static = public_html\n") - listeners = config['listen'].split(",") + listener_config = ListenerConfig([], [], {}) + for listener_name in config['listen'].split(","): + listener = _LISTENERS[listener_name] + listener_config = merge_config( + (await listener.create_config(reactor, config)), + listener_config, + ) - tor_config = {} - i2p_config = {} - tub_ports = [] - tub_locations = [] - if listeners == ["none"]: - c.write("tub.port = disabled\n") - c.write("tub.location = disabled\n") + if listener_config is None: + tub_ports = ["disabled"] + tub_locations = ["disabled"] else: - if "tor" in listeners: - (tor_config, tor_port, tor_location) = \ - yield tor_provider.create_config(reactor, config) - tub_ports.append(tor_port) - tub_locations.append(tor_location) - if "i2p" in listeners: - (i2p_config, i2p_port, i2p_location) = \ - yield i2p_provider.create_config(reactor, config) - tub_ports.append(i2p_port) - tub_locations.append(i2p_location) - if "tcp" in listeners: - if config["port"]: # --port/--location are a pair - tub_ports.append(config["port"]) - tub_locations.append(config["location"]) - else: - assert "hostname" in config - hostname = config["hostname"] - new_port = iputil.allocate_tcp_port() - tub_ports.append("tcp:%s" % new_port) - tub_locations.append("tcp:%s:%s" % (hostname, new_port)) - c.write("tub.port = %s\n" % ",".join(tub_ports)) - c.write("tub.location = %s\n" % ",".join(tub_locations)) + tub_ports = listener_config.tub_ports + tub_locations = listener_config.tub_locations + + c.write("tub.port = %s\n" % ",".join(tub_ports)) + c.write("tub.location = %s\n" % ",".join(tub_locations)) c.write("\n") c.write("#log_gatherer.furl =\n") @@ -294,17 +330,12 @@ def write_node_config(c, config): c.write("#ssh.authorized_keys_file = ~/.ssh/authorized_keys\n") c.write("\n") - if tor_config: - c.write("[tor]\n") - for key, value in list(tor_config.items()): - c.write("%s = %s\n" % (key, value)) - c.write("\n") - - if i2p_config: - c.write("[i2p]\n") - for key, value in list(i2p_config.items()): - c.write("%s = %s\n" % (key, value)) - c.write("\n") + if listener_config is not None: + for section, items in listener_config.node_config.items(): + c.write(f"[{section}]\n") + for k, v in items: + c.write(f"{k} = {v}\n") + c.write("\n") def write_client_config(c, config): @@ -445,7 +476,7 @@ def create_node(config): fileutil.make_dirs(os.path.join(basedir, "private"), 0o700) cfg_name = os.path.join(basedir, "tahoe.cfg") with io.open(cfg_name, "w", encoding='utf-8') as c: - yield write_node_config(c, config) + yield defer.Deferred.fromCoroutine(write_node_config(c, config)) write_client_config(c, config) print("Node created in %s" % quote_local_unicode_path(basedir), file=out) @@ -488,7 +519,7 @@ def create_introducer(config): fileutil.make_dirs(os.path.join(basedir, "private"), 0o700) cfg_name = os.path.join(basedir, "tahoe.cfg") with io.open(cfg_name, "w", encoding='utf-8') as c: - yield write_node_config(c, config) + yield defer.Deferred.fromCoroutine(write_node_config(c, config)) print("Introducer created in %s" % quote_local_unicode_path(basedir), file=out) defer.returnValue(0) diff --git a/src/allmydata/test/cli/test_create.py b/src/allmydata/test/cli/test_create.py index 1d1576082..d100f481f 100644 --- a/src/allmydata/test/cli/test_create.py +++ b/src/allmydata/test/cli/test_create.py @@ -17,6 +17,7 @@ from ..common import ( disable_modules, ) from ...scripts import create_node +from ...listeners import ListenerConfig, StaticProvider from ... import client def read_config(basedir): @@ -45,7 +46,7 @@ class Config(unittest.TestCase): e = self.assertRaises(usage.UsageError, parse_cli, verb, *args) self.assertIn("option %s not recognized" % (option,), str(e)) - def test_create_client_config(self): + async def test_create_client_config(self): d = self.mktemp() os.mkdir(d) fname = os.path.join(d, 'tahoe.cfg') @@ -59,7 +60,7 @@ class Config(unittest.TestCase): "shares-happy": "1", "shares-total": "1", } - create_node.write_node_config(f, opts) + await create_node.write_node_config(f, opts) create_node.write_client_config(f, opts) # should succeed, no exceptions @@ -245,7 +246,7 @@ class Config(unittest.TestCase): parse_cli, "create-node", "--listen=tcp,none", basedir) - self.assertEqual(str(e), "--listen= must be none, or one/some of: tcp, tor, i2p") + self.assertEqual(str(e), "--listen=tcp requires --hostname=") def test_node_listen_bad(self): basedir = self.mktemp() @@ -253,7 +254,7 @@ class Config(unittest.TestCase): parse_cli, "create-node", "--listen=XYZZY,tcp", basedir) - self.assertEqual(str(e), "--listen= must be none, or one/some of: tcp, tor, i2p") + self.assertEqual(str(e), "--listen= must be one/some of: i2p, none, tcp, tor") def test_node_listen_tor_hostname(self): e = self.assertRaises(usage.UsageError, @@ -287,24 +288,15 @@ class Config(unittest.TestCase): self.assertIn("To avoid clobbering anything, I am going to quit now", err) @defer.inlineCallbacks - def test_node_slow_tor(self): - basedir = self.mktemp() + def test_node_slow(self): d = defer.Deferred() - self.patch(tor_provider, "create_config", lambda *a, **kw: d) - d2 = run_cli("create-node", "--listen=tor", basedir) - d.callback(({}, "port", "location")) - rc, out, err = yield d2 - self.assertEqual(rc, 0) - self.assertIn("Node created", out) - self.assertEqual(err, "") + slow = StaticProvider(True, False, d, None) + create_node._LISTENERS["xxyzy"] = slow + self.addCleanup(lambda: create_node._LISTENERS.pop("xxyzy")) - @defer.inlineCallbacks - def test_node_slow_i2p(self): basedir = self.mktemp() - d = defer.Deferred() - self.patch(i2p_provider, "create_config", lambda *a, **kw: d) - d2 = run_cli("create-node", "--listen=i2p", basedir) - d.callback(({}, "port", "location")) + d2 = run_cli("create-node", "--listen=xxyzy", basedir) + d.callback(None) rc, out, err = yield d2 self.assertEqual(rc, 0) self.assertIn("Node created", out) @@ -369,10 +361,12 @@ def fake_config(testcase: unittest.TestCase, module: Any, result: Any) -> list[t class Tor(unittest.TestCase): def test_default(self): basedir = self.mktemp() - tor_config = {"abc": "def"} + tor_config = {"tor": [("abc", "def")]} tor_port = "ghi" tor_location = "jkl" - config_d = defer.succeed( (tor_config, tor_port, tor_location) ) + config_d = defer.succeed( + ListenerConfig([tor_port], [tor_location], tor_config) + ) calls = fake_config(self, tor_provider, config_d) rc, out, err = self.successResultOf( @@ -391,10 +385,7 @@ class Tor(unittest.TestCase): def test_launch(self): basedir = self.mktemp() - tor_config = {"abc": "def"} - tor_port = "ghi" - tor_location = "jkl" - config_d = defer.succeed( (tor_config, tor_port, tor_location) ) + config_d = defer.succeed(None) calls = fake_config(self, tor_provider, config_d) rc, out, err = self.successResultOf( @@ -410,10 +401,7 @@ class Tor(unittest.TestCase): def test_control_port(self): basedir = self.mktemp() - tor_config = {"abc": "def"} - tor_port = "ghi" - tor_location = "jkl" - config_d = defer.succeed( (tor_config, tor_port, tor_location) ) + config_d = defer.succeed(None) calls = fake_config(self, tor_provider, config_d) rc, out, err = self.successResultOf( @@ -451,10 +439,10 @@ class Tor(unittest.TestCase): class I2P(unittest.TestCase): def test_default(self): basedir = self.mktemp() - i2p_config = {"abc": "def"} + i2p_config = {"i2p": [("abc", "def")]} i2p_port = "ghi" i2p_location = "jkl" - dest_d = defer.succeed( (i2p_config, i2p_port, i2p_location) ) + dest_d = defer.succeed(ListenerConfig([i2p_port], [i2p_location], i2p_config)) calls = fake_config(self, i2p_provider, dest_d) rc, out, err = self.successResultOf( @@ -479,10 +467,7 @@ class I2P(unittest.TestCase): def test_sam_port(self): basedir = self.mktemp() - i2p_config = {"abc": "def"} - i2p_port = "ghi" - i2p_location = "jkl" - dest_d = defer.succeed( (i2p_config, i2p_port, i2p_location) ) + dest_d = defer.succeed(None) calls = fake_config(self, i2p_provider, dest_d) rc, out, err = self.successResultOf(