'wormhole ssh' cleanups

- move to 'wormhole ssh' group with accept/invite subcommands
- change names of methods
- check for permissions
- use --user option (instead of --auth-file)
- move implementation to cmd_ssh.py
- if multiple public-keys, ask user
This commit is contained in:
meejah 2016-08-14 19:57:00 -06:00
parent 069b76485b
commit fe29b3130b
2 changed files with 104 additions and 31 deletions

View File

@ -3,7 +3,6 @@ from __future__ import print_function
import os import os
import time import time
start = time.time() start = time.time()
from os.path import expanduser, exists
from textwrap import fill, dedent from textwrap import fill, dedent
from sys import stdout, stderr from sys import stdout, stderr
from . import public_relay from . import public_relay
@ -220,41 +219,65 @@ def receive(cfg, code, **kwargs):
return go(cmd_receive.receive, cfg) 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( @click.option(
"-c", "--code-length", default=2, "-c", "--code-length", default=2,
metavar="NUMWORDS", metavar="NUMWORDS",
help="length of code (in bytes/words)", help="length of code (in bytes/words)",
) )
@click.option( @click.option(
"--auth-file", "-f", "--user", "-u",
default=expanduser('~/.ssh/authorized_keys'), default=None,
type=click.Path(exists=False), metavar="USER",
help="Add to USER's ~/.ssh/authorized_keys",
) )
@click.pass_context @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 from . import cmd_ssh
ctx.obj.code_length = code_length ctx.obj.code_length = code_length
ctx.obj.auth_file = auth_file ctx.obj.ssh_user = user
return go(cmd_ssh.add, ctx.obj) return go(cmd_ssh.invite, ctx.obj)
@wormhole.command(name="ssh-send") @ssh.command(name="accept")
@click.argument( @click.argument(
"code", nargs=1, required=True, "code", nargs=1, required=True,
) )
@click.option(
"--key-file", "-F",
default=None,
type=click.Path(exists=True),
)
@click.option( @click.option(
"--yes", "-y", is_flag=True, "--yes", "-y", is_flag=True,
help="Skip confirmation prompt to send key", help="Skip confirmation prompt to send key",
) )
@click.pass_obj @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 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)) print("Sending public key type='{}' keyid='{}'".format(kind, keyid))
if yes is not True: if yes is not True:
click.confirm("Really send public key '{}' ?".format(keyid), abort=True) click.confirm("Really send public key '{}' ?".format(keyid), abort=True)
cfg.public_key = (kind, keyid, pubkey) cfg.public_key = (kind, keyid, pubkey)
cfg.code = code cfg.code = code
return go(cmd_ssh.send, cfg) return go(cmd_ssh.accept, cfg)

View File

@ -1,23 +1,55 @@
from __future__ import print_function 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.defer import inlineCallbacks
from twisted.internet import reactor from twisted.internet import reactor
import click
from .. import xfer_util 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 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 Returns a 3-tuple: kind, keyid, pubkey_data
""" """
# XXX FIXME don't blindly just send this one... if hint is None:
with open(expanduser('~/.ssh/id_rsa.pub'), 'r') as f: hint = expanduser('~/.ssh/')
pubkey = f.read() 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() parts = pubkey.strip().split()
kind = parts[0] kind = parts[0]
keyid = 'unknown' if len(parts) <= 2 else parts[2] keyid = 'unknown' if len(parts) <= 2 else parts[2]
@ -26,7 +58,7 @@ def find_public_key():
@inlineCallbacks @inlineCallbacks
def send(cfg, reactor=reactor): def accept(cfg, reactor=reactor):
yield xfer_util.send( yield xfer_util.send(
reactor, reactor,
u"lothar.com/wormhole/ssh-add", u"lothar.com/wormhole/ssh-add",
@ -39,14 +71,35 @@ def send(cfg, reactor=reactor):
@inlineCallbacks @inlineCallbacks
def add(cfg, reactor=reactor): def invite(cfg, reactor=reactor):
def on_code_created(code): def on_code_created(code):
print("Now tell the other user to run:") print("Now tell the other user to run:")
print() print()
print("wormhole ssh-send {}".format(code)) print("wormhole ssh accept {}".format(code))
print() 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( pubkey = yield xfer_util.receive(
reactor, reactor,
u"lothar.com/wormhole/ssh-add", u"lothar.com/wormhole/ssh-add",
@ -60,13 +113,10 @@ def add(cfg, reactor=reactor):
kind = parts[0] kind = parts[0]
keyid = 'unknown' if len(parts) <= 2 else parts[2] keyid = 'unknown' if len(parts) <= 2 else parts[2]
path = cfg.auth_file if not exists(auth_key_path):
if path == '-': if not exists(ssh_path):
print(pubkey.strip()) os.mkdir(ssh_path, mode=0o700)
else: with open(auth_key_path, 'a', 0o600) as f:
if not exists(path): f.write('{}\n'.format(pubkey.strip()))
print("Note: '{}' not found; will be created".format(path)) print("Appended key type='{kind}' id='{key_id}' to '{auth_file}'".format(
with open(path, 'a') as f: kind=kind, key_id=keyid, auth_file=auth_key_path))
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))