Merge PR418: add 'tahoe invite' and 'tahoe create-client --join='

refs ticket:126 , although this is just the first step
This commit is contained in:
Brian Warner 2017-08-08 23:12:29 -07:00
commit 3eaf18eba4
11 changed files with 688 additions and 5 deletions

View File

@ -13,6 +13,7 @@ Contents:
about about
INSTALL INSTALL
running running
magic-wormhole-invites
configuration configuration
architecture architecture

View File

@ -1,3 +1,5 @@
.. _magic-folder-howto:
========================= =========================
Magic Folder Set-up Howto Magic Folder Set-up Howto
========================= =========================

View File

@ -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 <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 <secret code>`` 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.

View File

@ -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 own grid you'll need to create the introducer and several initial storage
nodes (see the note about small grids below). 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 <one-time code>`, 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 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 it will do is connect to the introducer and get itself connected to all other
nodes on the grid. 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
<one-time-code>`` 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 By default, “``tahoe create-client``” creates a client-only node, that
does not offer its disk space to other nodes. To configure other behavior, does not offer its disk space to other nodes. To configure other behavior,
use “``tahoe create-node``” or see :doc:`configuration`. 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 start``” command. To stop a node started in this way, use
``tahoe stop``”. ``tahoe --help`` gives a summary of all commands. ``tahoe stop``”. ``tahoe --help`` gives a summary of all commands.
Running a Server or Introducer 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 (Note that “``tahoe run .``” doesn't work for introducers, this is a
known issue: `#937`_.) 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 <one-time-code>``.
Storage servers are created the same way: ``tahoe create-node Storage servers are created the same way: ``tahoe create-node
--hostname=HOSTNAME .`` from a new directory. You'll need to provide the --hostname=HOSTNAME .`` from a new directory. You'll need to provide the
introducer FURL (either as a ``--introducer=`` argument, or by editing 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 .. _public test grid: https://tahoe-lafs.org/trac/tahoe-lafs/wiki/TestGrid
.. _TestGrid page: 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 .. _#937: https://tahoe-lafs.org/trac/tahoe-lafs/ticket/937
.. _magic wormhole: https://magic-wormhole.io/
A note about small grids A note about small grids

View File

@ -24,8 +24,8 @@ def test_upload_immutable(reactor, temp_dir, introducer_furl, flog_gatherer, sto
node_dir = join(temp_dir, 'edna') node_dir = join(temp_dir, 'edna')
print("waiting 5 seconds unil we're maybe ready") print("waiting 10 seconds unil we're maybe ready")
yield task.deferLater(reactor, 5, lambda: None) yield task.deferLater(reactor, 10, lambda: None)
# upload a file, which should fail because we have don't have 7 # upload a file, which should fail because we have don't have 7
# storage servers (but happiness is set to 7) # storage servers (but happiness is set to 7)

View File

@ -91,6 +91,9 @@ install_requires = [
"PyYAML >= 3.11", "PyYAML >= 3.11",
"six >= 1.10.0", "six >= 1.10.0",
# for 'tahoe invite' and 'tahoe join'
"magic-wormhole >= 0.10.2",
] ]
# Includes some indirect dependencies, but does not include allmydata. # Includes some indirect dependencies, but does not include allmydata.

View File

@ -1,6 +1,8 @@
import os, sys, urllib, textwrap import os, sys, urllib, textwrap
import codecs import codecs
from ConfigParser import NoSectionError
from os.path import join
from twisted.python import usage from twisted.python import usage
from allmydata.util.assertutil import precondition from allmydata.util.assertutil import precondition
from allmydata.util.encodingutil import unicode_to_url, quote_output, \ from allmydata.util.encodingutil import unicode_to_url, quote_output, \
@ -103,6 +105,26 @@ class NoDefaultBasedirOptions(BasedirOptions):
DEFAULT_ALIAS = u"tahoe" 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): def get_aliases(nodedir):
aliases = {} aliases = {}
aliasfile = os.path.join(nodedir, "private", "aliases") aliasfile = os.path.join(nodedir, "private", "aliases")

View File

@ -1,11 +1,14 @@
import os import os
import json
from twisted.internet import reactor, defer from twisted.internet import reactor, defer
from twisted.python.usage import UsageError from twisted.python.usage import UsageError
from allmydata.scripts.common import BasedirOptions, NoDefaultBasedirOptions from allmydata.scripts.common import BasedirOptions, NoDefaultBasedirOptions
from allmydata.scripts.default_nodedir import _default_nodedir from allmydata.scripts.default_nodedir import _default_nodedir
from allmydata.util.assertutil import precondition 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 allmydata.util import fileutil, i2p_provider, iputil, tor_provider
from wormhole import wormhole
dummy_tac = """ dummy_tac = """
@ -80,7 +83,7 @@ def validate_where_options(o):
else: else:
# no --location and --port? expect --listen= (maybe the default), and # no --location and --port? expect --listen= (maybe the default), and
# --listen=tcp requires --hostname. But --listen=none is special. # --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(",") listeners = o['listen'].split(",")
for l in listeners: for l in listeners:
if l not in ["tcp", "tor", "i2p"]: 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-needed", None, 3, "Needed shares required for uploaded files."),
("shares-happy", None, 7, "How many servers new files must be placed on."), ("shares-happy", None, 7, "How many servers new files must be placed on."),
("shares-total", None, 10, "Total shares required for uploaded files."), ("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 # 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("enabled = false\n")
c.write("\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 @defer.inlineCallbacks
def create_node(config): def create_node(config):
out = config.stdout out = config.stdout
@ -344,6 +387,37 @@ def create_node(config):
os.mkdir(basedir) os.mkdir(basedir)
write_tac(basedir, "client") 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)
fileutil.make_dirs(os.path.join(basedir, "private"), 0700) fileutil.make_dirs(os.path.join(basedir, "private"), 0700)
with open(os.path.join(basedir, "tahoe.cfg"), "w") as c: with open(os.path.join(basedir, "tahoe.cfg"), "w") as c:
yield write_node_config(c, config) yield write_node_config(c, config)

View File

@ -7,7 +7,7 @@ from twisted.internet import defer, task, threads
from allmydata.scripts.common import get_default_nodedir from allmydata.scripts.common import get_default_nodedir
from allmydata.scripts import debug, create_node, startstop_node, cli, \ 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 from allmydata.util.encodingutil import quote_output, quote_local_unicode_path, get_io_encoding
def GROUP(s): def GROUP(s):
@ -47,6 +47,8 @@ class Options(usage.Options):
+ GROUP("Using the file store") + GROUP("Using the file store")
+ cli.subCommands + cli.subCommands
+ magic_folder_cli.subCommands + magic_folder_cli.subCommands
+ GROUP("Grid Management")
+ tahoe_invite.subCommands
) )
optFlags = [ optFlags = [
@ -56,6 +58,8 @@ class Options(usage.Options):
] ]
optParameters = [ optParameters = [
["node-directory", "d", None, NODEDIR_HELP], ["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): def opt_version(self):
@ -139,6 +143,8 @@ def dispatch(config,
# same # same
f0 = magic_folder_cli.dispatch[command] f0 = magic_folder_cli.dispatch[command]
f = lambda so: threads.deferToThread(f0, so) f = lambda so: threads.deferToThread(f0, so)
elif command in tahoe_invite.dispatch:
f = tahoe_invite.dispatch[command]
else: else:
raise usage.UsageError() raise usage.UsageError()

View File

@ -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] <nickname>"
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,
}

View File

@ -0,0 +1,346 @@
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,
)
# should still succeed -- just ignores the not-whitelisted
# "something-else" option
self.assertEqual(0, rc)
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)