#!/usr/bin/env python3

import argparse, os

from meissner.logger import *
import meissner

try:
    from smartbytes import *
except ModuleNotFoundError:
    logging.error('Please install the smartbytes module:\n\n', colored_command('pip3 install --user smartbytes'), '\n')
    exit(1)

if __name__ == '__main__':
    parser_fuzz = argparse.ArgumentParser(description = 'Meissner Lop - XSS Filter Bypass Exploit Fuzzer')
    parser_fuzz.add_argument('--no-ansi', '-c', default = False, action = 'store_true', help = 'disable ANSI coloring on all output')
    parser_fuzz.add_argument('--log-level', '-v', default = 'debug', type = str, help = 'set logging level')

    parser_fuzz.add_argument('--url', '--uri', '-u', default = False, type = str, help = 'use a URL harness, where {xss} is the injection point')
    parser_fuzz.add_argument('--dictionary', '--dict', '-d', default = '%s/dictionary/small.txt' % (os.path.dirname(os.path.abspath(__file__)) + '/../../meissner'), type = str, help = 'the Meissner mutation dictionary to use')
    parser_fuzz.add_argument('--threads', '--threads-count', '-t', default = 4, type = int, help = 'the number of threads allocated to use for engines')
    parser_fuzz.add_argument('--filter', '-f', default = [], action='append', help = 'pass the input through a filter before the program')
    parser_fuzz.add_argument('--engine', '--browser', '-e', default = 'chrome', type = str, help = 'the browser rendering engine to use')
    parser_fuzz.add_argument('cmd', nargs = '*', default = None, help = 'the command to execute, where {xss} is the injection point')

    args = parser_fuzz.parse_args()

    # setup logging

    log_level = args.log_level
    log_level = log_levels.get(log_level, log_level)

    try:
        log_level = int(log_level)
    except ValueError:
        # convert to int if possible
        logging.warn('Unable to convert logging level ', colored_command(log_level), ' to an integer.')

    logging.setLevel(log_level)

    # setup harness

    url = args.url
    command = args.cmd

    if url and command:
        logging.warning('Both a URL and a command were specified. The command will take precedence.')

    if command:
        logging.debug('Using harness ', colored_command('CommandHarness'))
        harness = meissner.harnesses.CommandHarness(command)
    elif url:
        logging.debug('Using harness ', colored_command('URLHarness'))
        harness = meissner.harnesses.URLHarness(url)
    else:
        logging.error('No harness specified. Please use ', colored_command('--url'), ' to specify a URL harness or ', colored_command('-- [command...]'), ' to specify a command harness.')
        exit(1)

    # setup filters

    filters = list()
    _filters = list()
    _invalid_filters = set()
    all_filters = {
        'urlencode' : meissner.filters.urlencode.URLEncoder
    }

    for filter in args.filter:
        if filter.lower() not in all_filters:
            _invalid_filters.add(filter)
        else:
            _filters.append(filter.lower())
            filters.append(all_filters[filter.lower()]())

    if _invalid_filters:
        logging.error('The filters %s are invalid. Please choose a valid filter from the list %s.' % (
            repr(_invalid_filters),
            repr(all_filters.keys())
        ))
        exit(1)

    if filters:
        logging.debug('Using filters: ', colored_command(', '.join(_filters)))

    # setup engines

    # TODO: support more engines
    all_engines = {
        'chrome' : meissner.engines.Chrome
    }

    engine = args.engine.strip().lower()
    if engine in all_engines:
        engine = all_engines[engine]
    else:
        logging.error('Engine ', colored_command(engine), ' does not exist. Please choose a valid engine from the list %s.' % repr(all_engines.keys()))
        exit(1)

    # parse dictionary file

    dictionary = set()

    try:
        with open(args.dictionary, 'rb') as f:
            for line in f.read().split(b'\n'):
                # skip empty lines
                if not line:
                    continue

                dictionary.add(line)
    except (FileNotFoundError, NotADirectoryError) as e:
        logging.exception('Dictionary file ', colored_command(args.dictionary), ' was not found. Please specify an existing file using the ', colored_command('--dictionary'), ' argument.')
        exit(1)

    # convert to list of smartbytes
    dictionary = [smartbytes(x) for x in dictionary]

    # begin fuzzing

    logging.info('Initializing Meissner...')

    fuzzer = meissner.Meissner(harness, dictionary, filters = filters, engine = engine)
    try:
        fuzzer.start(threads = args.threads)
    except KeyboardInterrupt:
        logging.warning('Stopped Meissner... but it wasn\'t even running yet?')
    except:
        logging.exception('An unknown error occurred. Please report this.')

    logging.debug('Goodbye!')
