2016-06-03 22:17:47 +00:00
|
|
|
from __future__ import print_function
|
|
|
|
|
|
|
|
import os
|
|
|
|
import time
|
|
|
|
start = time.time()
|
2017-04-23 19:56:19 +00:00
|
|
|
import six
|
2016-06-03 22:17:47 +00:00
|
|
|
from textwrap import fill, dedent
|
|
|
|
from sys import stdout, stderr
|
|
|
|
from . import public_relay
|
|
|
|
from .. import __version__
|
|
|
|
from ..timing import DebugTiming
|
2016-12-09 00:49:44 +00:00
|
|
|
from ..errors import (WrongPasswordError, WelcomeError, KeyFormatError,
|
2017-04-16 16:25:37 +00:00
|
|
|
TransferError, NoTorError, UnsendableFileError,
|
|
|
|
ServerConnectionError)
|
2016-06-03 22:17:47 +00:00
|
|
|
from twisted.internet.defer import inlineCallbacks, maybeDeferred
|
2016-08-14 22:50:29 +00:00
|
|
|
from twisted.python.failure import Failure
|
2016-06-03 22:17:47 +00:00
|
|
|
from twisted.internet.task import react
|
|
|
|
|
|
|
|
import click
|
|
|
|
top_import_finish = time.time()
|
|
|
|
|
|
|
|
|
|
|
|
class Config(object):
|
|
|
|
"""
|
|
|
|
Union of config options that we pass down to (sub) commands.
|
|
|
|
"""
|
|
|
|
def __init__(self):
|
2016-07-15 04:37:35 +00:00
|
|
|
# This only holds attributes which are *not* set by CLI arguments.
|
|
|
|
# Everything else comes from Click decorators, so we can be sure
|
|
|
|
# we're exercising the defaults.
|
2016-06-03 22:17:47 +00:00
|
|
|
self.timing = DebugTiming()
|
|
|
|
self.cwd = os.getcwd()
|
|
|
|
self.stdout = stdout
|
|
|
|
self.stderr = stderr
|
2016-08-14 23:14:29 +00:00
|
|
|
self.tor = False # XXX?
|
2016-06-03 22:17:47 +00:00
|
|
|
|
2016-07-24 00:54:15 +00:00
|
|
|
def _compose(*decorators):
|
|
|
|
def decorate(f):
|
|
|
|
for d in reversed(decorators):
|
|
|
|
f = d(f)
|
|
|
|
return f
|
|
|
|
return decorate
|
|
|
|
|
2016-06-03 22:17:47 +00:00
|
|
|
|
|
|
|
ALIASES = {
|
|
|
|
"tx": "send",
|
|
|
|
"rx": "receive",
|
2016-07-31 22:55:42 +00:00
|
|
|
"recieve": "receive",
|
|
|
|
"recv": "receive",
|
2016-06-03 22:17:47 +00:00
|
|
|
}
|
|
|
|
class AliasedGroup(click.Group):
|
|
|
|
def get_command(self, ctx, cmd_name):
|
|
|
|
cmd_name = ALIASES.get(cmd_name, cmd_name)
|
|
|
|
return click.Group.get_command(self, ctx, cmd_name)
|
|
|
|
|
|
|
|
|
|
|
|
# top-level command ("wormhole ...")
|
|
|
|
@click.group(cls=AliasedGroup)
|
2016-12-22 20:44:13 +00:00
|
|
|
@click.option(
|
|
|
|
"--appid", default=None, metavar="APPID", help="appid to use")
|
2016-06-03 22:17:47 +00:00
|
|
|
@click.option(
|
|
|
|
"--relay-url", default=public_relay.RENDEZVOUS_RELAY,
|
|
|
|
metavar="URL",
|
|
|
|
help="rendezvous relay to use",
|
|
|
|
)
|
|
|
|
@click.option(
|
|
|
|
"--transit-helper", default=public_relay.TRANSIT_RELAY,
|
|
|
|
metavar="tcp:HOST:PORT",
|
|
|
|
help="transit relay to use",
|
|
|
|
)
|
|
|
|
@click.option(
|
|
|
|
"--dump-timing", type=type(u""), # TODO: hide from --help output
|
|
|
|
default=None,
|
|
|
|
metavar="FILE.json",
|
|
|
|
help="(debug) write timing data to file",
|
|
|
|
)
|
|
|
|
@click.version_option(
|
|
|
|
message="magic-wormhole %(version)s",
|
|
|
|
version=__version__,
|
|
|
|
)
|
|
|
|
@click.pass_context
|
2016-12-22 20:44:13 +00:00
|
|
|
def wormhole(context, dump_timing, transit_helper, relay_url, appid):
|
2016-06-03 22:17:47 +00:00
|
|
|
"""
|
|
|
|
Create a Magic Wormhole and communicate through it.
|
|
|
|
|
|
|
|
Wormholes are created by speaking the same magic CODE in two
|
|
|
|
different places at the same time. Wormholes are secure against
|
|
|
|
anyone who doesn't use the same code.
|
|
|
|
"""
|
2016-06-27 01:14:07 +00:00
|
|
|
context.obj = cfg = Config()
|
2016-12-22 20:44:13 +00:00
|
|
|
cfg.appid = appid
|
2016-06-03 22:17:47 +00:00
|
|
|
cfg.relay_url = relay_url
|
|
|
|
cfg.transit_helper = transit_helper
|
|
|
|
cfg.dump_timing = dump_timing
|
|
|
|
|
|
|
|
|
|
|
|
@inlineCallbacks
|
|
|
|
def _dispatch_command(reactor, cfg, command):
|
|
|
|
"""
|
2016-06-22 07:29:13 +00:00
|
|
|
Internal helper. This calls the given command (a no-argument
|
2016-06-03 22:17:47 +00:00
|
|
|
callable) with the Config instance in cfg and interprets any
|
|
|
|
errors for the user.
|
|
|
|
"""
|
|
|
|
cfg.timing.add("command dispatch")
|
|
|
|
cfg.timing.add("import", when=start, which="top").finish(when=top_import_finish)
|
|
|
|
|
|
|
|
try:
|
|
|
|
yield maybeDeferred(command)
|
2017-01-15 22:35:46 +00:00
|
|
|
except (WrongPasswordError, KeyFormatError, NoTorError) as e:
|
2016-06-03 22:17:47 +00:00
|
|
|
msg = fill("ERROR: " + dedent(e.__doc__))
|
2017-04-23 19:56:19 +00:00
|
|
|
print(msg, file=cfg.stderr)
|
2016-12-09 00:48:12 +00:00
|
|
|
raise SystemExit(1)
|
2017-05-24 00:36:34 +00:00
|
|
|
except (WelcomeError, UnsendableFileError) as e:
|
2016-06-03 22:17:47 +00:00
|
|
|
msg = fill("ERROR: " + dedent(e.__doc__))
|
2017-04-23 19:56:19 +00:00
|
|
|
print(msg, file=cfg.stderr)
|
|
|
|
print(six.u(""), file=cfg.stderr)
|
|
|
|
print(six.text_type(e), file=cfg.stderr)
|
2016-12-09 00:48:12 +00:00
|
|
|
raise SystemExit(1)
|
2016-12-09 00:49:44 +00:00
|
|
|
except TransferError as e:
|
2017-04-23 19:56:19 +00:00
|
|
|
print(u"TransferError: %s" % six.text_type(e), file=cfg.stderr)
|
2016-12-09 00:49:44 +00:00
|
|
|
raise SystemExit(1)
|
2017-04-16 16:25:37 +00:00
|
|
|
except ServerConnectionError as e:
|
|
|
|
msg = fill("ERROR: " + dedent(e.__doc__))
|
|
|
|
msg += "\n" + six.text_type(e)
|
|
|
|
print(msg, file=cfg.stderr)
|
|
|
|
raise SystemExit(1)
|
2016-06-03 22:17:47 +00:00
|
|
|
except Exception as e:
|
2016-08-14 22:50:29 +00:00
|
|
|
# this prints a proper traceback, whereas
|
|
|
|
# traceback.print_exc() just prints a TB to the "yield"
|
|
|
|
# line above ...
|
2017-04-23 19:56:19 +00:00
|
|
|
Failure().printTraceback(file=cfg.stderr)
|
|
|
|
print(u"ERROR:", six.text_type(e), file=cfg.stderr)
|
2016-06-03 22:17:47 +00:00
|
|
|
raise SystemExit(1)
|
|
|
|
|
|
|
|
cfg.timing.add("exit")
|
|
|
|
if cfg.dump_timing:
|
2017-04-23 19:56:19 +00:00
|
|
|
cfg.timing.write(cfg.dump_timing, cfg.stderr)
|
2016-06-20 23:06:15 +00:00
|
|
|
|
2016-06-03 22:17:47 +00:00
|
|
|
|
2016-07-24 00:54:15 +00:00
|
|
|
CommonArgs = _compose(
|
|
|
|
click.option("-0", "zeromode", default=False, is_flag=True,
|
|
|
|
help="enable no-code anything-goes mode",
|
|
|
|
),
|
2016-07-28 00:52:21 +00:00
|
|
|
click.option("-c", "--code-length", default=2, metavar="NUMWORDS",
|
|
|
|
help="length of code (in bytes/words)",
|
|
|
|
),
|
|
|
|
click.option("-v", "--verify", is_flag=True, default=False,
|
|
|
|
help="display verification string (and wait for approval)",
|
|
|
|
),
|
|
|
|
click.option("--hide-progress", is_flag=True, default=False,
|
|
|
|
help="supress progress-bar display",
|
|
|
|
),
|
|
|
|
click.option("--listen/--no-listen", default=True,
|
|
|
|
help="(debug) don't open a listening socket for Transit",
|
|
|
|
),
|
2017-01-16 03:13:10 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
TorArgs = _compose(
|
2016-07-28 00:52:21 +00:00
|
|
|
click.option("--tor", is_flag=True, default=False,
|
|
|
|
help="use Tor when connecting",
|
|
|
|
),
|
rewrite Tor support (py2 only)
The new TorManager adds --launch-tor and --tor-control-port= arguments
(requiring the user to explicitly request a new Tor process, if that's what
they want). The default (when --tor is enabled) looks for a control port in
the usual places (/var/run/tor/control, localhost:9051, localhost:9151), then
falls back to hoping there's a SOCKS port in the usual
place (localhost:9050). (closes #64)
The ssh utilities should now accept the same tor arguments as ordinary
send/receive commands. There are now full tests for TorManager, and basic
tests for how send/receive use it. (closes #97)
Note that Tor is only supported on python2.7 for now, since txsocksx (and
therefore txtorcon) doesn't work on py3. You need to do "pip install
magic-wormhole[tor]" to get Tor support, and that will get you an inscrutable
error on py3 (referencing vcversioner, "install_requires must be a string or
list of strings", and "int object not iterable").
To run tests, you must install with the [dev] extra (to get "mock" and other
libraries). Our setup.py only includes "txtorcon" in the [dev] extra when on
py2, not on py3. Unit tests tolerate the lack of txtorcon (they mock out
everything txtorcon would provide), so they should provide the same coverage
on both py2 and py3.
2017-01-16 03:24:23 +00:00
|
|
|
click.option("--launch-tor", is_flag=True, default=False,
|
|
|
|
help="launch Tor, rather than use existing control/socks port",
|
|
|
|
),
|
|
|
|
click.option("--tor-control-port", default=None, metavar="ENDPOINT",
|
|
|
|
help="endpoint descriptor for Tor control port",
|
|
|
|
),
|
2016-07-24 00:54:15 +00:00
|
|
|
)
|
|
|
|
|
2016-06-03 22:17:47 +00:00
|
|
|
# wormhole send (or "wormhole tx")
|
|
|
|
@wormhole.command()
|
2016-07-24 00:54:15 +00:00
|
|
|
@CommonArgs
|
2017-01-16 03:13:10 +00:00
|
|
|
@TorArgs
|
2016-06-03 22:17:47 +00:00
|
|
|
@click.option(
|
|
|
|
"--code", metavar="CODE",
|
|
|
|
help="human-generated code phrase",
|
|
|
|
)
|
|
|
|
@click.option(
|
|
|
|
"--text", default=None, metavar="MESSAGE",
|
|
|
|
help="text message to send, instead of a file. Use '-' to read from stdin.",
|
|
|
|
)
|
2017-05-24 00:36:34 +00:00
|
|
|
@click.option(
|
|
|
|
"--ignore-unsendable-files", default=False, is_flag=True,
|
|
|
|
help="Don't raise an error if a file can't be read."
|
|
|
|
)
|
2017-06-15 13:49:46 +00:00
|
|
|
@click.argument("what", required=False, type=click.Path(path_type=type(u"")))
|
2016-06-03 22:17:47 +00:00
|
|
|
@click.pass_obj
|
2016-07-23 02:13:59 +00:00
|
|
|
def send(cfg, **kwargs):
|
2016-06-03 22:17:47 +00:00
|
|
|
"""Send a text message, file, or directory"""
|
2016-07-23 02:13:59 +00:00
|
|
|
for name, value in kwargs.items():
|
|
|
|
setattr(cfg, name, value)
|
2016-06-03 22:17:47 +00:00
|
|
|
with cfg.timing.add("import", which="cmd_send"):
|
|
|
|
from . import cmd_send
|
|
|
|
|
2016-07-15 04:22:01 +00:00
|
|
|
return go(cmd_send.send, cfg)
|
|
|
|
|
|
|
|
# this intermediate function can be mocked by tests that need to build a
|
|
|
|
# Config object
|
|
|
|
def go(f, cfg):
|
2016-06-22 20:15:07 +00:00
|
|
|
# note: react() does not return
|
2016-07-15 04:22:01 +00:00
|
|
|
return react(_dispatch_command, (cfg, lambda: f(cfg)))
|
2016-06-03 22:17:47 +00:00
|
|
|
|
|
|
|
|
|
|
|
# wormhole receive (or "wormhole rx")
|
|
|
|
@wormhole.command()
|
2016-07-24 00:54:15 +00:00
|
|
|
@CommonArgs
|
2017-01-16 03:13:10 +00:00
|
|
|
@TorArgs
|
2016-06-03 22:17:47 +00:00
|
|
|
@click.option(
|
|
|
|
"--only-text", "-t", is_flag=True,
|
|
|
|
help="refuse file transfers, only accept text transfers",
|
|
|
|
)
|
|
|
|
@click.option(
|
|
|
|
"--accept-file", is_flag=True,
|
|
|
|
help="accept file transfer without asking for confirmation",
|
|
|
|
)
|
|
|
|
@click.option(
|
|
|
|
"--output-file", "-o",
|
|
|
|
metavar="FILENAME|DIRNAME",
|
|
|
|
help=("The file or directory to create, overriding the name suggested"
|
|
|
|
" by the sender."),
|
|
|
|
)
|
|
|
|
@click.argument(
|
|
|
|
"code", nargs=-1, default=None,
|
|
|
|
# help=("The magic-wormhole code, from the sender. If omitted, the"
|
|
|
|
# " program will ask for it, using tab-completion."),
|
|
|
|
)
|
|
|
|
@click.pass_obj
|
2016-07-23 02:13:59 +00:00
|
|
|
def receive(cfg, code, **kwargs):
|
2016-06-03 22:17:47 +00:00
|
|
|
"""
|
|
|
|
Receive a text message, file, or directory (from 'wormhole send')
|
|
|
|
"""
|
2016-07-23 02:13:59 +00:00
|
|
|
for name, value in kwargs.items():
|
|
|
|
setattr(cfg, name, value)
|
2016-06-03 22:17:47 +00:00
|
|
|
with cfg.timing.add("import", which="cmd_receive"):
|
|
|
|
from . import cmd_receive
|
|
|
|
if len(code) == 1:
|
|
|
|
cfg.code = code[0]
|
|
|
|
elif len(code) > 1:
|
|
|
|
print(
|
|
|
|
"Pass either no code or just one code; you passed"
|
|
|
|
" {}: {}".format(len(code), ', '.join(code))
|
|
|
|
)
|
|
|
|
raise SystemExit(1)
|
|
|
|
else:
|
|
|
|
cfg.code = None
|
|
|
|
|
2016-07-15 04:22:01 +00:00
|
|
|
return go(cmd_receive.receive, cfg)
|
2016-08-14 23:14:29 +00:00
|
|
|
|
|
|
|
|
2016-08-15 01:57:00 +00:00
|
|
|
@wormhole.group()
|
|
|
|
def ssh():
|
|
|
|
"""
|
|
|
|
Facilitate sending/receiving SSH public keys
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
@ssh.command(name="invite")
|
2016-08-14 23:14:29 +00:00
|
|
|
@click.option(
|
|
|
|
"-c", "--code-length", default=2,
|
|
|
|
metavar="NUMWORDS",
|
|
|
|
help="length of code (in bytes/words)",
|
|
|
|
)
|
|
|
|
@click.option(
|
2016-08-15 01:57:00 +00:00
|
|
|
"--user", "-u",
|
|
|
|
default=None,
|
|
|
|
metavar="USER",
|
|
|
|
help="Add to USER's ~/.ssh/authorized_keys",
|
2016-08-14 23:14:29 +00:00
|
|
|
)
|
2017-01-16 03:13:10 +00:00
|
|
|
@TorArgs
|
2016-08-14 23:14:29 +00:00
|
|
|
@click.pass_context
|
2017-01-16 16:29:20 +00:00
|
|
|
def ssh_invite(ctx, code_length, user, **kwargs):
|
2016-08-15 01:57:00 +00:00
|
|
|
"""
|
|
|
|
Add a public-key to a ~/.ssh/authorized_keys file
|
|
|
|
"""
|
2017-01-16 16:29:20 +00:00
|
|
|
for name, value in kwargs.items():
|
|
|
|
setattr(ctx.obj, name, value)
|
2016-08-14 23:14:29 +00:00
|
|
|
from . import cmd_ssh
|
|
|
|
ctx.obj.code_length = code_length
|
2016-08-15 01:57:00 +00:00
|
|
|
ctx.obj.ssh_user = user
|
|
|
|
return go(cmd_ssh.invite, ctx.obj)
|
2016-08-14 23:14:29 +00:00
|
|
|
|
|
|
|
|
2016-08-15 01:57:00 +00:00
|
|
|
@ssh.command(name="accept")
|
2016-08-14 23:14:29 +00:00
|
|
|
@click.argument(
|
|
|
|
"code", nargs=1, required=True,
|
|
|
|
)
|
2016-08-15 01:57:00 +00:00
|
|
|
@click.option(
|
|
|
|
"--key-file", "-F",
|
|
|
|
default=None,
|
|
|
|
type=click.Path(exists=True),
|
|
|
|
)
|
2016-08-14 23:14:29 +00:00
|
|
|
@click.option(
|
|
|
|
"--yes", "-y", is_flag=True,
|
|
|
|
help="Skip confirmation prompt to send key",
|
|
|
|
)
|
2017-01-16 03:13:10 +00:00
|
|
|
@TorArgs
|
2016-08-14 23:14:29 +00:00
|
|
|
@click.pass_obj
|
2017-01-16 16:29:20 +00:00
|
|
|
def ssh_accept(cfg, code, key_file, yes, **kwargs):
|
2016-08-15 01:57:00 +00:00
|
|
|
"""
|
|
|
|
Send your SSH public-key
|
|
|
|
|
|
|
|
In response to a 'wormhole ssh invite' this will send public-key
|
|
|
|
you specify (if there's only one in ~/.ssh/* that will be sent).
|
|
|
|
"""
|
|
|
|
|
2017-01-16 16:29:20 +00:00
|
|
|
for name, value in kwargs.items():
|
2017-01-16 16:34:32 +00:00
|
|
|
setattr(cfg, name, value)
|
2016-08-14 23:14:29 +00:00
|
|
|
from . import cmd_ssh
|
2016-08-15 01:57:00 +00:00
|
|
|
kind, keyid, pubkey = cmd_ssh.find_public_key(key_file)
|
2016-08-14 23:14:29 +00:00
|
|
|
print("Sending public key type='{}' keyid='{}'".format(kind, keyid))
|
|
|
|
if yes is not True:
|
|
|
|
click.confirm("Really send public key '{}' ?".format(keyid), abort=True)
|
|
|
|
cfg.public_key = (kind, keyid, pubkey)
|
|
|
|
cfg.code = code
|
|
|
|
|
2016-08-15 01:57:00 +00:00
|
|
|
return go(cmd_ssh.accept, cfg)
|