diff --git a/src/wormhole/cli/cli.py b/src/wormhole/cli/cli.py index ff706a0..4f0a341 100644 --- a/src/wormhole/cli/cli.py +++ b/src/wormhole/cli/cli.py @@ -3,7 +3,6 @@ from __future__ import print_function import os import time start = time.time() -from os.path import expanduser, exists from textwrap import fill, dedent from sys import stdout, stderr from . import public_relay @@ -220,41 +219,65 @@ def receive(cfg, code, **kwargs): return go(cmd_receive.receive, cfg) -@wormhole.command(name="ssh-add") +@wormhole.group() +def ssh(): + """ + Facilitate sending/receiving SSH public keys + """ + pass + + +@ssh.command(name="invite") @click.option( "-c", "--code-length", default=2, metavar="NUMWORDS", help="length of code (in bytes/words)", ) @click.option( - "--auth-file", "-f", - default=expanduser('~/.ssh/authorized_keys'), - type=click.Path(exists=False), + "--user", "-u", + default=None, + metavar="USER", + help="Add to USER's ~/.ssh/authorized_keys", ) @click.pass_context -def ssh_add(ctx, code_length, auth_file): +def ssh_invite(ctx, code_length, user): + """ + Add a public-key to a ~/.ssh/authorized_keys file + """ from . import cmd_ssh ctx.obj.code_length = code_length - ctx.obj.auth_file = auth_file - return go(cmd_ssh.add, ctx.obj) + ctx.obj.ssh_user = user + return go(cmd_ssh.invite, ctx.obj) -@wormhole.command(name="ssh-send") +@ssh.command(name="accept") @click.argument( "code", nargs=1, required=True, ) +@click.option( + "--key-file", "-F", + default=None, + type=click.Path(exists=True), +) @click.option( "--yes", "-y", is_flag=True, help="Skip confirmation prompt to send key", ) @click.pass_obj -def ssh_send(cfg, code, yes): +def ssh_accept(cfg, code, key_file, yes): + """ + 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). + """ + from . import cmd_ssh - kind, keyid, pubkey = cmd_ssh.find_public_key() + kind, keyid, pubkey = cmd_ssh.find_public_key(key_file) 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 - return go(cmd_ssh.send, cfg) + return go(cmd_ssh.accept, cfg) diff --git a/src/wormhole/cli/cmd_ssh.py b/src/wormhole/cli/cmd_ssh.py index 8b2faf3..6b5c4ee 100644 --- a/src/wormhole/cli/cmd_ssh.py +++ b/src/wormhole/cli/cmd_ssh.py @@ -1,23 +1,55 @@ from __future__ import print_function -from os.path import expanduser, exists +import os +from os.path import expanduser, exists, join from twisted.internet.defer import inlineCallbacks from twisted.internet import reactor +import click from .. import xfer_util -def find_public_key(): +def find_public_key(hint=None): """ This looks for an appropriate SSH key to send, possibly querying - the user in the meantime. + the user in the meantime. DO NOT CALL after reactor.run as this + (possibly) does blocking stuff like asking the user questions (via + click.prompt()) Returns a 3-tuple: kind, keyid, pubkey_data """ - # XXX FIXME don't blindly just send this one... - with open(expanduser('~/.ssh/id_rsa.pub'), 'r') as f: - pubkey = f.read() + if hint is None: + hint = expanduser('~/.ssh/') + else: + if not exists(hint): + raise RuntimeError("Can't find '{}'".format(hint)) + + pubkeys = [f for f in os.listdir(hint) if f.endswith('.pub')] + if len(pubkeys) == 0: + raise RuntimeError("No public keys in '{}'".format(hint)) + elif len(pubkeys) > 1: + got_key = False + while not got_key: + ans = click.prompt( + "Multiple public-keys found:\n" + \ + "\n".join([" {}: {}".format(a, b) for a, b in enumerate(pubkeys)]) + \ + "\nSend which one?" + ) + try: + ans = int(ans) + if ans < 0 or ans >= len(pubkeys): + ans = None + else: + got_key = True + with open(join(hint, pubkeys[ans]), 'r') as f: + pubkey = f.read() + + except Exception: + got_key = False + else: + with open(join(hint, pubkeys[0]), 'r') as f: + pubkey = f.read() parts = pubkey.strip().split() kind = parts[0] keyid = 'unknown' if len(parts) <= 2 else parts[2] @@ -26,7 +58,7 @@ def find_public_key(): @inlineCallbacks -def send(cfg, reactor=reactor): +def accept(cfg, reactor=reactor): yield xfer_util.send( reactor, u"lothar.com/wormhole/ssh-add", @@ -39,14 +71,35 @@ def send(cfg, reactor=reactor): @inlineCallbacks -def add(cfg, reactor=reactor): +def invite(cfg, reactor=reactor): def on_code_created(code): print("Now tell the other user to run:") print() - print("wormhole ssh-send {}".format(code)) + print("wormhole ssh accept {}".format(code)) print() + if cfg.ssh_user is None: + ssh_path = expanduser('~/.ssh/'.format(cfg.ssh_user)) + else: + ssh_path = '/home/{}/.ssh/'.format(cfg.ssh_user) + auth_key_path = join(ssh_path, 'authorized_keys') + if not exists(auth_key_path): + print("Note: '{}' not found; will be created".format(auth_key_path)) + if not exists(ssh_path): + print(" '{}' doesn't exist either".format(ssh_path)) + else: + try: + open(auth_key_path, 'a').close() + except OSError: + print("No write permission on '{}'".format(auth_key_path)) + return + try: + os.listdir(ssh_path) + except OSError: + print("Can't read '{}'".format(ssh_path)) + return + pubkey = yield xfer_util.receive( reactor, u"lothar.com/wormhole/ssh-add", @@ -60,13 +113,10 @@ def add(cfg, reactor=reactor): kind = parts[0] keyid = 'unknown' if len(parts) <= 2 else parts[2] - path = cfg.auth_file - if path == '-': - print(pubkey.strip()) - else: - if not exists(path): - print("Note: '{}' not found; will be created".format(path)) - with open(path, 'a') as f: - f.write('{}\n'.format(pubkey.strip())) - print("Appended key type='{kind}' id='{key_id}' to '{auth_file}'".format( - kind=kind, key_id=keyid, auth_file=path)) + if not exists(auth_key_path): + if not exists(ssh_path): + os.mkdir(ssh_path, mode=0o700) + with open(auth_key_path, 'a', 0o600) as f: + f.write('{}\n'.format(pubkey.strip())) + print("Appended key type='{kind}' id='{key_id}' to '{auth_file}'".format( + kind=kind, key_id=keyid, auth_file=auth_key_path))