From f79c5003acc7c72bc0d7551bf8467d457df3bcda Mon Sep 17 00:00:00 2001 From: meejah Date: Fri, 7 Oct 2016 12:15:05 -0600 Subject: [PATCH] Add 'tahoe invite' and 'tahoe create-node --join' commands This opens a wormhole and sends appropriate JSON down it to a tahoe-gui using a wormhole server running on tahoe-lafs.org The other end uses the 'tahoe create-node' command (with new --join option) to read the configuration JSON from a 'tahoe invite' command --- docs/index.rst | 1 + docs/magic-folder-howto.rst | 2 + docs/magic-wormhole-invites.rst | 73 +++++ docs/running.rst | 47 +++ integration/test_servers_of_happiness.py | 4 +- src/allmydata/_auto_deps.py | 3 + src/allmydata/scripts/common.py | 22 ++ src/allmydata/scripts/create_node.py | 80 +++++- src/allmydata/scripts/runner.py | 8 +- src/allmydata/scripts/tahoe_invite.py | 109 +++++++ src/allmydata/test/cli/test_invite.py | 347 +++++++++++++++++++++++ src/allmydata/test/test_node.py | 1 + 12 files changed, 692 insertions(+), 5 deletions(-) create mode 100644 docs/magic-wormhole-invites.rst create mode 100644 src/allmydata/scripts/tahoe_invite.py create mode 100644 src/allmydata/test/cli/test_invite.py diff --git a/docs/index.rst b/docs/index.rst index aafd7cba98..57f17acb99 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,6 +13,7 @@ Contents: about INSTALL running + magic-wormhole-invites configuration architecture diff --git a/docs/magic-folder-howto.rst b/docs/magic-folder-howto.rst index 655cdb1908..b368972b80 100644 --- a/docs/magic-folder-howto.rst +++ b/docs/magic-folder-howto.rst @@ -1,3 +1,5 @@ +.. _magic-folder-howto: + ========================= Magic Folder Set-up Howto ========================= diff --git a/docs/magic-wormhole-invites.rst b/docs/magic-wormhole-invites.rst new file mode 100644 index 0000000000..0c9e2206d5 --- /dev/null +++ b/docs/magic-wormhole-invites.rst @@ -0,0 +1,73 @@ +********************** +Magic Wormhole Invites +********************** + +Magic Wormhole +============== + +`magic wormhole`_ is a server and a client which together use Password +Authenticated Key Exchange (PAKE) to use a short code to establish a +secure channel between two computers. These codes are one-time use and +an attacker gets at most one "guess", thus allowing low-entropy codes +to be used. + +.. _magic wormhole: https://github.com/warner/magic-wormhole#design + + +Invites and Joins +================= + +Inside Tahoe-LAFS we are using a channel created using `magic +wormhole`_ to exchange configuration and the secret fURL of the +Introducer with new clients. In the future, we would like to make the +Magic Folder (:ref:`Magic Folder HOWTO `) invites and joins work this way +as well. + +This is a two-part process. Alice runs a grid and wishes to have her +friend Bob use it as a client. She runs ``tahoe invite bob`` which +will print out a short "wormhole code" like ``2-unicorn-quiver``. You +may also include some options for total, happy and needed shares if +you like. + +Alice then transmits this one-time secret code to Bob. Alice must keep +her command running until Bob has done his step as it is waiting until +a secure channel is established before sending the data. + +Bob then runs ``tahoe create-client --join `` with any +other options he likes. This will "use up" the code establishing a +secure session with Alice's computer. If an attacker tries to guess +the code, they get only once chance to do so (and then Bob's side will +fail). Once Bob's computer has connected to Alice's computer, the two +computers performs the protocol described below, resulting in some +JSON with the Introducer fURL, nickname and any other options being +sent to Bob's computer. The ``tahoe create-client`` command then uses +these options to set up Bob's client. + + + +Tahoe-LAFS Secret Exchange +========================== + +The protocol that the Alice (the one doing the invite) and Bob (the +one being invited) sides perform once a magic wormhole secure channel +has been established goes as follows: + +Alice and Bob both immediately send an "abilities" message as +JSON. For Alice this is ``{"abilities": {"server-v1": {}}}``. For Bob, +this is ``{"abilities": {"client-v1": {}}}``. + +After receiving the message from the other side and confirming the +expected protocol, Alice transmits the configuration JSON:: + + { + "needed": 3, + "total": 10, + "happy": 7, + "nickname": "bob", + "introducer": "pb://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@example.com:41505/yyyyyyyyyyyyyyyyyyyyyyy" + } + +Both sides then disconnect. + +As you can see, there is room for future revisions of the protocol but +as of yet none have been sketched out. diff --git a/docs/running.rst b/docs/running.rst index 3df1a99b72..825897dab4 100644 --- a/docs/running.rst +++ b/docs/running.rst @@ -26,6 +26,39 @@ grid`_ as you only need to create a client node. When you want to create your own grid you'll need to create the introducer and several initial storage nodes (see the note about small grids below). + +Being Introduced to a Grid +-------------------------- + +A collection of Tahoe servers is called a Grid and usually has 1 +Introducer (but sometimes more, and it's possible to run with zero). The +Introducer announces which storage servers constitute the Grid and how to +contact them. There is a secret "fURL" you need to know to talk to the +Introducer. + +One way to get this secret is using traditional tools such as encrypted +email, encrypted instant-messaging, etcetera. It is important to transmit +this fURL secretly as knowing it gives you access to the Grid. + +An additional way to share the fURL securely is via `magic +wormhole`_. This uses a weak one-time password and a server on the +internet (at `wormhole.tahoe-lafs.org`) to open a secure channel between +two computers. In Tahoe-LAFS this functions via the commands `tahoe +invite` and `tahoe create-client --join`. A person who already has access +to a Grid can use `tahoe invite` to create one end of the `magic +wormhole`_ and then transmits some JSON (including the Introducer's +secret fURL) to the other end. `tahoe invite` will print a one-time +secret code; you must then communicate this code to the person who will +join the Grid. + +The other end of the `magic wormhole`_ in this case is `tahoe +create-client --join `, where the person being invited +types in the code they were given. Ideally, this code would be +transmitted securely. It is, however, only useful exactly once. Also, it +is much easier to transcribe by a human. Codes look like +`7-surrender-tunnel` (a short number and two words). + + Running a Client ---------------- @@ -38,6 +71,11 @@ To construct a client node, run “``tahoe create-client``”, which will create it will do is connect to the introducer and get itself connected to all other nodes on the grid. +Some Grids use "magic wormhole" one-time codes to configure the basic +options. In such a case you use ``tahoe create-client --join +`` and do not have to do any of the ``tahoe.cfg`` editing +mentioned above. + By default, “``tahoe create-client``” creates a client-only node, that does not offer its disk space to other nodes. To configure other behavior, use “``tahoe create-node``” or see :doc:`configuration`. @@ -47,6 +85,7 @@ On Unix, you can run it in the background instead by using the “``tahoe start``” command. To stop a node started in this way, use “``tahoe stop``”. ``tahoe --help`` gives a summary of all commands. + Running a Server or Introducer ------------------------------ @@ -67,6 +106,13 @@ URL the other nodes must use in order to connect to this introducer. (Note that “``tahoe run .``” doesn't work for introducers, this is a known issue: `#937`_.) +You can distribute your Introducer fURL securely to new clients by using +the ``tahoe invite`` command. This will prepare some JSON to send to the +other side, request a `magic wormhole`_ code from +``wormhole.tahoe-lafs.org`` and print it out to the terminal. This +one-time code should be transmitted to the user of the client, who can +then run ``tahoe create-client --join ``. + Storage servers are created the same way: ``tahoe create-node --hostname=HOSTNAME .`` from a new directory. You'll need to provide the introducer FURL (either as a ``--introducer=`` argument, or by editing @@ -79,6 +125,7 @@ Tahoe-LAFS. .. _public test grid: https://tahoe-lafs.org/trac/tahoe-lafs/wiki/TestGrid .. _TestGrid page: https://tahoe-lafs.org/trac/tahoe-lafs/wiki/TestGrid .. _#937: https://tahoe-lafs.org/trac/tahoe-lafs/ticket/937 +.. _magic wormhole: https://magic-wormhole.io/ A note about small grids diff --git a/integration/test_servers_of_happiness.py b/integration/test_servers_of_happiness.py index 04ed8880a2..1688b056ad 100644 --- a/integration/test_servers_of_happiness.py +++ b/integration/test_servers_of_happiness.py @@ -24,8 +24,8 @@ def test_upload_immutable(reactor, temp_dir, introducer_furl, flog_gatherer, sto node_dir = join(temp_dir, 'edna') - print("waiting 5 seconds unil we're maybe ready") - yield task.deferLater(reactor, 5, lambda: None) + print("waiting 10 seconds unil we're maybe ready") + yield task.deferLater(reactor, 10, lambda: None) # upload a file, which should fail because we have don't have 7 # storage servers (but happiness is set to 7) diff --git a/src/allmydata/_auto_deps.py b/src/allmydata/_auto_deps.py index bec052fa01..20740acb86 100644 --- a/src/allmydata/_auto_deps.py +++ b/src/allmydata/_auto_deps.py @@ -91,6 +91,9 @@ "PyYAML >= 3.11", "six >= 1.10.0", + + # for 'tahoe invite' and 'tahoe join' + "magic-wormhole >= 0.10.2", ] # Includes some indirect dependencies, but does not include allmydata. diff --git a/src/allmydata/scripts/common.py b/src/allmydata/scripts/common.py index e2f4cc601f..941564acbf 100644 --- a/src/allmydata/scripts/common.py +++ b/src/allmydata/scripts/common.py @@ -1,6 +1,8 @@ import os, sys, urllib, textwrap import codecs +from ConfigParser import NoSectionError +from os.path import join from twisted.python import usage from allmydata.util.assertutil import precondition from allmydata.util.encodingutil import unicode_to_url, quote_output, \ @@ -103,6 +105,26 @@ def getSynopsis(self): DEFAULT_ALIAS = u"tahoe" +def get_introducer_furl(nodedir, config): + """ + :return: the introducer FURL for the given node (no matter if it's + a client-type node or an introducer itself) + """ + try: + introducer_furl = config.get('client', 'introducer.furl') + except NoSectionError: + # we're not a client; maybe this is running *on* the introducer? + try: + with open(join(nodedir, "private", "introducer.furl"), "r") as f: + introducer_furl = f.read().strip() + except IOError: + raise Exception( + "Can't find introducer FURL in tahoe.cfg nor " + "{}/private/introducer.furl".format(nodedir) + ) + return introducer_furl + + def get_aliases(nodedir): aliases = {} aliasfile = os.path.join(nodedir, "private", "aliases") diff --git a/src/allmydata/scripts/create_node.py b/src/allmydata/scripts/create_node.py index bd148e18f6..8a042d6e0c 100644 --- a/src/allmydata/scripts/create_node.py +++ b/src/allmydata/scripts/create_node.py @@ -1,11 +1,14 @@ import os +import json + from twisted.internet import reactor, defer from twisted.python.usage import UsageError from allmydata.scripts.common import BasedirOptions, NoDefaultBasedirOptions 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 +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 +from wormhole import wormhole dummy_tac = """ @@ -80,7 +83,7 @@ def validate_where_options(o): else: # no --location and --port? expect --listen= (maybe the default), and # --listen=tcp requires --hostname. But --listen=none is special. - if o['listen'] != "none": + 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"]: @@ -155,6 +158,7 @@ class CreateClientOptions(_CreateBaseOptions): ("shares-needed", None, 3, "Needed shares required for uploaded files."), ("shares-happy", None, 7, "How many servers new files must be placed on."), ("shares-total", None, 10, "Total shares required for uploaded files."), + ("join", None, None, "Join a grid with the given Invite Code."), ] # This is overridden in order to ensure we get a "Wrong number of @@ -325,6 +329,45 @@ def write_client_config(c, config): c.write("enabled = false\n") c.write("\n") + +@defer.inlineCallbacks +def _get_config_via_wormhole(config): + out = config.stdout + print >>out, "Opening wormhole with code '{}'".format(config['join']) + relay_url = config.parent['wormhole-server'] + print >>out, "Connecting to '{}'".format(relay_url) + + wh = wormhole.create( + appid=config.parent['wormhole-invite-appid'], + relay_url=relay_url, + reactor=reactor, + ) + code = unicode(config['join']) + wh.set_code(code) + yield wh.get_welcome() + print >>out, "Connected to wormhole server" + + intro = { + u"abilities": { + "client-v1": {}, + } + } + wh.send_message(json.dumps(intro)) + + server_intro = yield wh.get_message() + server_intro = json.loads(server_intro) + + print >>out, " received server introduction" + if u'abilities' not in server_intro: + raise RuntimeError(" Expected 'abilities' in server introduction") + if u'server-v1' not in server_intro['abilities']: + raise RuntimeError(" Expected 'server-v1' in server abilities") + + remote_data = yield wh.get_message() + print >>out, " received configuration" + defer.returnValue(json.loads(remote_data)) + + @defer.inlineCallbacks def create_node(config): out = config.stdout @@ -344,6 +387,39 @@ def create_node(config): os.mkdir(basedir) write_tac(basedir, "client") + # if we're doing magic-wormhole stuff, do it now + if config['join'] is not None: + try: + remote_config = yield _get_config_via_wormhole(config) + except RuntimeError as e: + print >>err, str(e) + defer.returnValue(1) + + # configuration we'll allow the inviter to set + whitelist = [ + 'shares-happy', 'shares-needed', 'shares-total', + 'introducer', 'nickname', + ] + sensitive_keys = ['introducer'] + + print >>out, "Encoding: {shares-needed} of {shares-total} shares, on at least {shares-happy} servers".format(**remote_config) + print >>out, "Overriding the following config:" + + for k in whitelist: + v = remote_config.get(k, None) + if v is not None: + # we're faking usually argv-supplied options :/ + if isinstance(v, unicode): + v = v.encode(get_io_encoding()) + config[k] = v + if k not in sensitive_keys: + if k not in ['shares-happy', 'shares-total', 'shares-needed']: + print >>out, " {}: {}".format(k, v) + else: + print >>out, " {}: [sensitive data; see tahoe.cfg]".format(k) + else: + print >>out, "option '{}' not whitelisted; removing".format(k) + fileutil.make_dirs(os.path.join(basedir, "private"), 0700) with open(os.path.join(basedir, "tahoe.cfg"), "w") as c: yield write_node_config(c, config) diff --git a/src/allmydata/scripts/runner.py b/src/allmydata/scripts/runner.py index 26e748bfc7..ffe6f9fc30 100644 --- a/src/allmydata/scripts/runner.py +++ b/src/allmydata/scripts/runner.py @@ -7,7 +7,7 @@ from allmydata.scripts.common import get_default_nodedir from allmydata.scripts import debug, create_node, startstop_node, cli, \ - stats_gatherer, admin, magic_folder_cli + stats_gatherer, admin, magic_folder_cli, tahoe_invite from allmydata.util.encodingutil import quote_output, quote_local_unicode_path, get_io_encoding def GROUP(s): @@ -47,6 +47,8 @@ class Options(usage.Options): + GROUP("Using the file store") + cli.subCommands + magic_folder_cli.subCommands + + GROUP("Grid Management") + + tahoe_invite.subCommands ) optFlags = [ @@ -56,6 +58,8 @@ class Options(usage.Options): ] optParameters = [ ["node-directory", "d", None, NODEDIR_HELP], + ["wormhole-server", None, u"ws://wormhole.tahoe-lafs.org:4000/v1", "The magic wormhole server to use.", unicode], + ["wormhole-invite-appid", None, u"tahoe-lafs.org/invite", "The appid to use on the wormhole server.", unicode], ] def opt_version(self): @@ -139,6 +143,8 @@ def dispatch(config, # same f0 = magic_folder_cli.dispatch[command] f = lambda so: threads.deferToThread(f0, so) + elif command in tahoe_invite.dispatch: + f = tahoe_invite.dispatch[command] else: raise usage.UsageError() diff --git a/src/allmydata/scripts/tahoe_invite.py b/src/allmydata/scripts/tahoe_invite.py new file mode 100644 index 0000000000..2f3e29fa0d --- /dev/null +++ b/src/allmydata/scripts/tahoe_invite.py @@ -0,0 +1,109 @@ +import json +from os.path import join + +from twisted.python import usage +from twisted.internet import defer, reactor + +from wormhole import wormhole + +from allmydata.util import configutil +from allmydata.util.encodingutil import argv_to_abspath +from allmydata.scripts.common import get_default_nodedir, get_introducer_furl + + +class InviteOptions(usage.Options): + synopsis = "[options] " + description = "Create a client-only Tahoe-LAFS node (no storage server)." + + optParameters = [ + ("shares-needed", None, None, "How many shares are needed to reconstruct files from this node"), + ("shares-happy", None, None, "Distinct storage servers new node will upload shares to"), + ("shares-total", None, None, "Total number of shares new node will upload"), + ] + + def parseArgs(self, *args): + if len(args) != 1: + raise usage.UsageError( + "Provide a single argument: the new node's nickname" + ) + self['nick'] = args[0].strip() + + +@defer.inlineCallbacks +def _send_config_via_wormhole(options, config): + out = options.stdout + err = options.stderr + relay_url = options.parent['wormhole-server'] + print >>out, "Connecting to '{}'...".format(relay_url) + wh = wormhole.create( + appid=options.parent['wormhole-invite-appid'], + relay_url=relay_url, + reactor=reactor, + ) + yield wh.get_welcome() + print >>out, "Connected to wormhole server" + + # must call allocate_code before get_code will ever succeed + wh.allocate_code() + code = yield wh.get_code() + print >>out, "Invite Code for client: {}".format(code) + + wh.send_message(json.dumps({ + u"abilities": { + u"server-v1": {}, + } + })) + + client_intro = yield wh.get_message() + print >>out, " received client introduction" + client_intro = json.loads(client_intro) + if not u'abilities' in client_intro: + print >>err, "No 'abilities' from client" + defer.returnValue(1) + if not u'client-v1' in client_intro[u'abilities']: + print >>err, "No 'client-v1' in abilities from client" + defer.returnValue(1) + + print >>out, " transmitting configuration" + wh.send_message(json.dumps(config)) + yield wh.close() + + +@defer.inlineCallbacks +def invite(options): + if options.parent['node-directory']: + basedir = argv_to_abspath(options.parent['node-directory']) + else: + basedir = get_default_nodedir() + config = configutil.get_config(join(basedir, 'tahoe.cfg')) + out = options.stdout + err = options.stderr + + try: + introducer_furl = get_introducer_furl(basedir, config) + except Exception as e: + print >>err, "Can't find introducer FURL for node '{}': {}".format(basedir, str(e)) + raise SystemExit(1) + + nick = options['nick'] + + remote_config = { + "shares-needed": options["shares-needed"] or config.get('client', 'shares.needed'), + "shares-total": options["shares-total"] or config.get('client', 'shares.total'), + "shares-happy": options["shares-happy"] or config.get('client', 'shares.happy'), + "nickname": nick, + "introducer": introducer_furl, + } + + yield _send_config_via_wormhole(options, remote_config) + print >>out, "Completed successfully" + + +subCommands = [ + ("invite", None, InviteOptions, + "Invite a new node to this grid"), +] + +dispatch = { + "invite": invite, +} diff --git a/src/allmydata/test/cli/test_invite.py b/src/allmydata/test/cli/test_invite.py new file mode 100644 index 0000000000..0f3fcc675b --- /dev/null +++ b/src/allmydata/test/cli/test_invite.py @@ -0,0 +1,347 @@ +import os +import mock +import json +from os.path import join + +from twisted.trial import unittest +from twisted.internet import defer +from ..common_util import run_cli +from ..no_network import GridTestMixin +from .common import CLITestMixin + + +class _FakeWormhole(object): + + def __init__(self, outgoing_messages): + self.messages = [] + self._outgoing = outgoing_messages + + def get_code(self): + return defer.succeed(u"6-alarmist-tuba") + + def set_code(self, code): + self._code = code + + def get_welcome(self): + return defer.succeed( + json.dumps({ + u"welcome": {}, + }) + ) + + def allocate_code(self): + return None + + def send_message(self, msg): + self.messages.append(msg) + + def get_message(self): + return defer.succeed(self._outgoing.pop(0)) + + def close(self): + return defer.succeed(None) + + +def _create_fake_wormhole(outgoing_messages): + return _FakeWormhole(outgoing_messages) + + +class Join(GridTestMixin, CLITestMixin, unittest.TestCase): + + @defer.inlineCallbacks + def setUp(self): + self.basedir = self.mktemp() + yield super(Join, self).setUp() + yield self.set_up_grid(oneshare=True) + + @defer.inlineCallbacks + def test_create_node_join(self): + """ + successfully join after an invite + """ + node_dir = self.mktemp() + + with mock.patch('allmydata.scripts.create_node.wormhole') as w: + fake_wh = _create_fake_wormhole([ + json.dumps({u"abilities": {u"server-v1": {}}}), + json.dumps({ + u"shares-needed": 1, + u"shares-happy": 1, + u"shares-total": 1, + u"nickname": u"somethinghopefullyunique", + u"introducer": u"pb://foo", + }), + ]) + w.create = mock.Mock(return_value=fake_wh) + + rc, out, err = yield run_cli( + "create-client", + "--join", "1-abysmal-ant", + node_dir, + ) + + self.assertEqual(0, rc) + with open(join(node_dir, 'tahoe.cfg'), 'r') as f: + config = f.read() + self.assertIn("pb://foo", config) + self.assertIn(u"somethinghopefullyunique", config) + + @defer.inlineCallbacks + def test_create_node_illegal_option(self): + """ + Server sends JSON with unknown/illegal key + """ + node_dir = self.mktemp() + + with mock.patch('allmydata.scripts.create_node.wormhole') as w: + fake_wh = _create_fake_wormhole([ + json.dumps({u"abilities": {u"server-v1": {}}}), + json.dumps({ + u"shares-needed": 1, + u"shares-happy": 1, + u"shares-total": 1, + u"nickname": u"somethinghopefullyunique", + u"introducer": u"pb://foo", + u"something-else": u"not allowed", + }), + ]) + w.create = mock.Mock(return_value=fake_wh) + + rc, out, err = yield run_cli( + "create-client", + "--join", "1-abysmal-ant", + node_dir, + ) + + # will still succeed, but we should have seen a message + # about ignoring the "something-else" option + self.assertEqual(0, rc) + self.assertIn(u"option 'something-else' not whitelisted", out + err) + + +class Invite(GridTestMixin, CLITestMixin, unittest.TestCase): + + @defer.inlineCallbacks + def setUp(self): + self.basedir = self.mktemp() + yield super(Invite, self).setUp() + yield self.set_up_grid(oneshare=True) + intro_dir = os.path.join(self.basedir, "introducer") + yield run_cli( + "create-introducer", + "--listen", "none", + intro_dir, + ) + + @defer.inlineCallbacks + def test_invite_success(self): + """ + successfully send an invite + """ + intro_dir = os.path.join(self.basedir, "introducer") + # we've never run the introducer, so it hasn't created + # introducer.furl yet + priv_dir = join(intro_dir, "private") + with open(join(priv_dir, "introducer.furl"), "w") as f: + f.write("pb://fooblam\n") + + with mock.patch('allmydata.scripts.tahoe_invite.wormhole') as w: + fake_wh = _create_fake_wormhole([ + json.dumps({u"abilities": {u"client-v1": {}}}), + ]) + w.create = mock.Mock(return_value=fake_wh) + + rc, out, err = yield run_cli( + "-d", intro_dir, + "invite", + "--shares-needed", "1", + "--shares-happy", "1", + "--shares-total", "1", + "foo", + ) + self.assertEqual(2, len(fake_wh.messages)) + self.assertEqual( + json.loads(fake_wh.messages[0]), + { + "abilities": + { + "server-v1": {} + }, + }, + ) + self.assertEqual( + json.loads(fake_wh.messages[1]), + { + "shares-needed": "1", + "shares-total": "1", + "nickname": "foo", + "introducer": "pb://fooblam", + "shares-happy": "1", + }, + ) + + @defer.inlineCallbacks + def test_invite_no_furl(self): + """ + Invites must include the Introducer FURL + """ + intro_dir = os.path.join(self.basedir, "introducer") + + with mock.patch('allmydata.scripts.tahoe_invite.wormhole') as w: + fake_wh = _create_fake_wormhole([ + json.dumps({u"abilities": {u"client-v1": {}}}), + ]) + w.create = mock.Mock(return_value=fake_wh) + + rc, out, err = yield run_cli( + "-d", intro_dir, + "invite", + "--shares-needed", "1", + "--shares-happy", "1", + "--shares-total", "1", + "foo", + ) + self.assertNotEqual(rc, 0) + self.assertIn(u"Can't find introducer FURL", out + err) + + @defer.inlineCallbacks + def test_invite_wrong_client_abilities(self): + """ + Send unknown client version + """ + intro_dir = os.path.join(self.basedir, "introducer") + # we've never run the introducer, so it hasn't created + # introducer.furl yet + priv_dir = join(intro_dir, "private") + with open(join(priv_dir, "introducer.furl"), "w") as f: + f.write("pb://fooblam\n") + + with mock.patch('allmydata.scripts.tahoe_invite.wormhole') as w: + fake_wh = _create_fake_wormhole([ + json.dumps({u"abilities": {u"client-v9000": {}}}), + ]) + w.create = mock.Mock(return_value=fake_wh) + + rc, out, err = yield run_cli( + "-d", intro_dir, + "invite", + "--shares-needed", "1", + "--shares-happy", "1", + "--shares-total", "1", + "foo", + ) + self.assertNotEqual(rc, 0) + self.assertIn(u"No 'client-v1' in abilities", out + err) + + @defer.inlineCallbacks + def test_invite_no_client_abilities(self): + """ + Client doesn't send any client abilities at all + """ + intro_dir = os.path.join(self.basedir, "introducer") + # we've never run the introducer, so it hasn't created + # introducer.furl yet + priv_dir = join(intro_dir, "private") + with open(join(priv_dir, "introducer.furl"), "w") as f: + f.write("pb://fooblam\n") + + with mock.patch('allmydata.scripts.tahoe_invite.wormhole') as w: + fake_wh = _create_fake_wormhole([ + json.dumps({}), + ]) + w.create = mock.Mock(return_value=fake_wh) + + rc, out, err = yield run_cli( + "-d", intro_dir, + "invite", + "--shares-needed", "1", + "--shares-happy", "1", + "--shares-total", "1", + "foo", + ) + self.assertNotEqual(rc, 0) + self.assertIn(u"No 'abilities' from client", out + err) + + @defer.inlineCallbacks + def test_invite_wrong_server_abilities(self): + """ + Server sends unknown version + """ + intro_dir = os.path.join(self.basedir, "introducer") + # we've never run the introducer, so it hasn't created + # introducer.furl yet + priv_dir = join(intro_dir, "private") + with open(join(priv_dir, "introducer.furl"), "w") as f: + f.write("pb://fooblam\n") + + with mock.patch('allmydata.scripts.create_node.wormhole') as w: + fake_wh = _create_fake_wormhole([ + json.dumps({u"abilities": {u"server-v9000": {}}}), + json.dumps({ + "shares-needed": "1", + "shares-total": "1", + "shares-happy": "1", + "nickname": "foo", + "introducer": "pb://fooblam", + }), + ]) + w.create = mock.Mock(return_value=fake_wh) + + rc, out, err = yield run_cli( + "create-client", + "--join", "1-alarmist-tuba", + "foo", + ) + self.assertNotEqual(rc, 0) + self.assertIn("Expected 'server-v1' in server abilities", out + err) + + @defer.inlineCallbacks + def test_invite_no_server_abilities(self): + """ + Server sends unknown version + """ + intro_dir = os.path.join(self.basedir, "introducer") + # we've never run the introducer, so it hasn't created + # introducer.furl yet + priv_dir = join(intro_dir, "private") + with open(join(priv_dir, "introducer.furl"), "w") as f: + f.write("pb://fooblam\n") + + with mock.patch('allmydata.scripts.create_node.wormhole') as w: + fake_wh = _create_fake_wormhole([ + json.dumps({}), + json.dumps({ + "shares-needed": "1", + "shares-total": "1", + "shares-happy": "1", + "nickname": "bar", + "introducer": "pb://fooblam", + }), + ]) + w.create = mock.Mock(return_value=fake_wh) + + rc, out, err = yield run_cli( + "create-client", + "--join", "1-alarmist-tuba", + "bar", + ) + self.assertNotEqual(rc, 0) + self.assertIn("Expected 'abilities' in server introduction", out + err) + + @defer.inlineCallbacks + def test_invite_no_nick(self): + """ + Should still work if server sends no nickname + """ + intro_dir = os.path.join(self.basedir, "introducer") + + with mock.patch('allmydata.scripts.tahoe_invite.wormhole'): + rc, out, err = yield run_cli( + "-d", intro_dir, + "invite", + "--shares-needed", "1", + "--shares-happy", "1", + "--shares-total", "1", + ) + self.assertTrue(rc) + self.assertIn(u"Provide a single argument", out + err) diff --git a/src/allmydata/test/test_node.py b/src/allmydata/test/test_node.py index cd19eea45c..01971b265f 100644 --- a/src/allmydata/test/test_node.py +++ b/src/allmydata/test/test_node.py @@ -30,6 +30,7 @@ def setUp(self): testutil.SignalMixin.setUp(self) self.parent = LoggingMultiService() self.parent.startService() + def tearDown(self): log.msg("%s.tearDown" % self.__class__.__name__) testutil.SignalMixin.tearDown(self)