'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 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)

View File

@ -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))