#!/usr/bin/env python3
"""
Add keys to SSH agent

Usage:
    addsshkeys [options] [<config>]

Options:
    -v, --verbose    list the keys as they are being added to ssh agent
    -l, --list       list the keys without adding them to ssh agent

Configurations can be found in: {settings_dir}.
They are Python files.  The default configuration is {config_filename}.

Currently there is only one setting available: ssh_keys.  It contains 
a dictionary of dictionaries that contains information about each key.  The 
primary dictionary contains a name and the values for each key. The values are 
held in a dictionary that may contain three fields: paths, account, and 
passphrase.  The first, paths, is required and contains the paths to one or more 
SSH private key files.  It may be a list of strings, or a single string that is 
split.  If a relative path is given, it is relative to ~/.ssh.  The second, 
account, gives the name of the avendesora account that holds passphrase for the 
keys. If present, Avendesora will be queried for the passphrase. The third, 
passphrase is required if account is not given, otherwise it is optional.  If 
account is given, it is the name of the passphrase field in Avendesora, which 
defaults to 'passcode'. If account is not given, it is the passphrase itself. In 
this case, the settings file should only be readable by the user.

A description of how to configure and use this program can be found at 
`<https://avendesora.readthedocs.io/en/latest/api.html#example-add-ssh-keys>_.
"""

from appdirs import user_config_dir
from avendesora import PasswordGenerator, PasswordError
from avendesora.gpg import PythonFile
from inform import Inform, Error, codicil, comment, display, error, warn
from docopt import docopt
from pathlib import Path
import pexpect

__version__ = "0.0.2"
__released__ = "2019-03-23"

# Settings
# These cannot be overridden in ~/.config/addsshkeys/config
prog_name = 'addsshkeys'
config_filename = 'config'
settings_dir = user_config_dir(prog_name)

# These can be overridden in ~/.config/addsshkeys/config
ssh_add = 'ssh-add'
ssh_keys = {}
config_file_mask = 0o077

# read command line
cmdline = docopt(__doc__.format(**locals()))
verbose = cmdline['--verbose']
list_keys = cmdline['--list']
config = cmdline['<config>']
if not config:
    config = config_filename

try:
    # initialization
    Inform(verbose=verbose)
    settings_dir = Path(settings_dir)
    pw = PasswordGenerator()

    # read settings file
    config_filepath = Path(settings_dir, config)
    if config_filepath.exists():
        settings = PythonFile(config_filepath)
        settings.initialize()
        locals().update(settings.run())
    else:
        raise Error('config file not found.', culprit=config_filepath)

    if not ssh_keys:
        raise Error('no keys found.', culprit=config_filepath)

    # load keys
    for key, values in ssh_keys.items():
        try:
            paths = values['paths'].split()
        except AttributeError:
            paths = values['paths']
        except KeyError:
            raise Error('missing paths.', culprit=key)
        account_name = values.get('account')
        passphrase = values.get('passphrase')
        if list_keys:
            display(f'{key}:')
            for path in paths:
                display(f'    {path}')
            continue
        try:
            if account_name:
                account = pw.get_account(account_name)
                if passphrase:
                    passphrase = str(account.get_value(passphrase).value)
                else:
                    passphrase = str(account.get_passcode().value)
            elif not passphrase:
                raise Error('missing passphrase.', culprit=key)
            else:
                # config file contains the passphrase, check its permissions
                permissions = config_filepath.stat().st_mode & 0o777
                violation = permissions & config_file_mask
                if violation:
                    recommended = permissions & ~config_file_mask
                    warn("file permissions are too loose.", culprit=config_filepath)
                    codicil("Recommend running: chmod {:o} {}".format(recommended, config_filepath))

            for path in paths:
                comment(f'{key:>15}: adding {path}')
                path = Path(path).expanduser()
                path = Path('~/.ssh', path).expanduser()
                try:
                    sshadd = pexpect.spawn(ssh_add, [str(path)])
                    sshadd.expect('Enter passphrase for %s: ' % (path), timeout=4)
                    sshadd.sendline(passphrase)
                    sshadd.expect(pexpect.EOF)
                    sshadd.close()
                    response = sshadd.before.decode('utf-8')
                    if 'identity added' in response.lower():
                        continue
                except (pexpect.EOF, pexpect.TIMEOUT):
                    pass
                error('failed.', culprit=key)
                codicil('response:', sshadd.before.decode('utf8'), culprit=ssh_add)
                codicil('exit status:', sshadd.exitstatus , culprit=ssh_add)
        except PasswordError as e:
            e.report(culprit=key)

except (Error, PasswordError) as e:
    e.report()
except KeyboardInterrupt as e:
    comment('Killed by user.')
