"""

Listener handling functionality for Empire.

Handles listener startup from the database, listener
shutdowns, and maintains the current listener 
configuration.

"""

import http
import helpers

from pydispatch import dispatcher
import hashlib
import sqlite3

class Listeners:

    def __init__(self, MainMenu, args=None):
        
        # pull out the controller objects
        self.mainMenu = MainMenu
        self.conn = MainMenu.conn
        self.agents = MainMenu.agents
        self.modules = None
        self.stager = None
        self.installPath = self.mainMenu.installPath

        # {listenerId : EmpireServer object}
        self.listeners = {}

        self.args = args

        # used to get a dict back from the query
        def dict_factory(cursor, row):
            d = {}
            for idx, col in enumerate(cursor.description):
                d[col[0]] = row[idx]
            return d

        # set the initial listener config to be the config defaults
        self.conn.row_factory = dict_factory
        cur = self.conn.cursor()
        cur.execute("SELECT staging_key,default_delay,default_jitter,default_profile,default_cert_path,default_port,default_lost_limit FROM config")
        defaults = cur.fetchone()
        cur.close()
        self.conn.row_factory = None

        # the current listener config options
        self.options = {
            'Name' : {
                'Description'   :   'Listener name.',
                'Required'      :   True,
                'Value'         :   'test'
            },
            'Host' : {
                'Description'   :   'Hostname/IP for staging.',
                'Required'      :   True,
                'Value'         :   "http://" + helpers.lhost() + ":" + defaults['default_port']
            },
            'Type' : {
                'Description'   :   'Listener type (native, pivot, hop, foreign, meter).',
                'Required'      :   True,
                'Value'         :   "native"
            },
            'RedirectTarget' : {
                'Description'   :   'Listener target to redirect to for pivot/hop.',
                'Required'      :   False,
                'Value'         :   ""
            },
            'StagingKey' : {
                'Description'   :   'Staging key for initial agent negotiation.',
                'Required'      :   True,
                'Value'         :   defaults['staging_key']
            },
            'DefaultDelay' : {
                'Description'   :   'Agent delay/reach back interval (in seconds).',
                'Required'      :   True,
                'Value'         :   defaults['default_delay']
            },
            'DefaultJitter' : {
                'Description'   :   'Jitter in agent reachback interval (0.0-1.0).',
                'Required'      :   True,
                'Value'         :   defaults['default_jitter']
            },
            'DefaultLostLimit' : {
                'Description'   :   'Number of missed checkins before exiting',
                'Required'      :   True,
                'Value'         :   defaults['default_lost_limit']
            },
            'DefaultProfile' : {
                'Description'   :   'Default communication profile for the agent.',
                'Required'      :   True,
                'Value'         :   defaults['default_profile']
            },
            'CertPath' : {
                'Description'   :   'Certificate path for https listeners.',
                'Required'      :   False,
                'Value'         :   defaults['default_cert_path']
            },
            'Port' : {
                'Description'   :   'Port for the listener.',
                'Required'      :   True,
                'Value'         :   defaults['default_port']
            },
            'KillDate' : {
                'Description'   :   'Date for the listener to exit (MM/dd/yyyy).',
                'Required'      :   False,
                'Value'         :   ''
            },
            'WorkingHours' : {
                'Description'   :   'Hours for the agent to operate (09:00-17:00).',
                'Required'      :   False,
                'Value'         :   ''
            }
        }


    def start_existing_listeners(self):
        """
        Startup any listeners that are current in the database.
        """

        cur = self.conn.cursor()
        cur.execute("SELECT id,name,host,port,cert_path,staging_key,default_delay,default_jitter,default_profile,kill_date,working_hours,listener_type,redirect_target,default_lost_limit FROM listeners")
        results = cur.fetchall()
        cur.close()

        # for each listener in the database, add it to the cache
        for result in results:
            
            # don't start the listener unless it's a native one
            if result[11] != "native":
                self.listeners[result[0]] = None

            else:
                lhost = http.host2lhost(result[2])
		port = result[3]
		
                # if cert_path is empty, no ssl is used
                cert_path = result[4]

                # build the handler server and kick if off
                server = http.EmpireServer(self.agents, lhost=lhost, port=port, cert=cert_path)

                # check if the listener started correctly
                if server.success:
                    server.start()

                    if (server.base_server()):
                        # store off this servers in the "[id] : server" object array
                        # only if the server starts up correctly
                        self.listeners[result[0]] = server


    def set_listener_option(self, option, value):
        """
        Set a listener option in the listener dictionary.
        """

        # parse and auto-set some host parameters
        if option == "Host":

            if not value.startswith("http"):
                # if there's a current ssl cert path set, assume this is https                
                if self.options['CertPath']['Value'] != "":
                    self.options['Host']['Value'] = "https://"+str(value)
                else:
                    # otherwise assume it's http
                    self.options['Host']['Value'] = "http://"+str(value)

                # if there's a port specified, set that as well
                parts = value.split(":")
                if len(parts) > 1:
                    self.options['Host']['Value'] = self.options['Host']['Value'] + ":" + str(parts[1])
                    self.options['Port']['Value'] = parts[1]

            elif value.startswith("https"):
                self.options['Host']['Value'] = value
                if self.options['CertPath']['Value'] == "":
                    print helpers.color("[!] Error: Please specify a SSL cert path first")
                    return False
                else:
                    parts = value.split(":")
                    # check if we have a port to extract
                    if len(parts) == 3:
                        # in case there's a resource uri at the end
                        parts = parts[2].split("/")
                        self.options['Port']['Value'] = parts[0]
                    else:
                        self.options['Port']['Value'] = "443"

            elif value.startswith("http"):
                self.options['Host']['Value'] = value
                parts = value.split(":")
                # check if we have a port to extract
                if len(parts) == 3:
                    # in case there's a resource uri at the end
                    parts = parts[2].split("/")
                    self.options['Port']['Value'] = parts[0]
                else:
                    self.options['Port']['Value'] = "80"

            return True

        elif option == "CertPath":
            self.options[option]['Value'] = value
            host = self.options["Host"]['Value']
            # if we're setting a SSL cert path, but the host is specific at http
            if host.startswith("http:"):
                self.options["Host"]['Value'] = self.options["Host"]['Value'].replace("http:", "https:")
            return True

        elif option == "Port":
            self.options[option]['Value'] = value
            # set the port in the Host configuration as well
            host = self.options["Host"]['Value']
            parts = host.split(":")
            if len(parts) == 2 or len(parts) == 3:
                self.options["Host"]['Value'] = parts[0] + ":" + parts[1] + ":" + str(value)
            return True

        elif option == "StagingKey":
            # if the staging key isn't 32 characters, assume we're md5 hashing it
            if len(value) != 32:
                self.options[option]['Value'] = hashlib.md5(value).hexdigest()
            return True

        elif option in self.options:

            self.options[option]['Value'] = value
            if option.lower() == "type":
                if value.lower() == "hop":
                    # set the profile for hop.php for hop
                    parts = self.options['DefaultProfile']['Value'].split("|")
                    self.options['DefaultProfile']['Value'] = "/hop.php|" + "|".join(parts[1:])
            return True

        else:
            print helpers.color("[!] Error: invalid option name")
            return False


    def get_listener_options(self):
        """
        Return all currently set listener options.
        """
        return self.options.keys()


    def kill_listener(self, listenerId):
        """
        Shut a listener down and remove it from the database.
        """
        self.shutdown_listener(listenerId)
        self.delete_listener(listenerId)


    def delete_listener(self, listenerId):
        """
        Shut down the server associated with a listenerId and delete the
        listener from the database.
        """

        # see if we were passed a name instead of an ID
        nameid = self.get_listener_id(listenerId)
        if nameid : listenerId = nameid

        # shut the listener down and remove it from the cache
        self.shutdown_listener(listenerId)

        # remove the listener from the database
        cur = self.conn.cursor()
        cur.execute("DELETE FROM listeners WHERE id=?", [listenerId])
        cur.close()


    def shutdown_listener(self, listenerId):
        """
        Shut down the server associated with a listenerId/name, but DON'T 
        delete it from the database.

        If the listener is a pivot, task the associated agent to kill the redirector.
        """
        
        try:
            # get the listener information
            [ID,name,host,port,cert_path,staging_key,default_delay,default_jitter,default_profile,kill_date,working_hours,listener_type,redirect_target,default_lost_limit] = self.get_listener(listenerId)

            listenerId = int(ID)

            if listenerId in self.listeners:
                # can't shut down hop, foreign, or meter listeners
                if listener_type == "hop" or listener_type == "foreign" or listener_type == "meter":
                    pass
                # if this listener is a pivot, task the associated agent to shut it down
                elif listener_type == "pivot":
                    print helpers.color("[*] Tasking pivot listener to shut down on agent " + name)
                    killCmd = "netsh interface portproxy reset"
                    self.agents.add_agent_task(name, "TASK_SHELL", killCmd)
                else:
                    # otherwise get the server object associated with this listener and shut it down
                    self.listeners[listenerId].shutdown()

                # remove the listener object from the internal cache
                del self.listeners[listenerId]

        except Exception as e:
            dispatcher.send("[!] Error shutting down listener " + str(listenerId), sender="Listeners")


    def get_listener(self, listenerId):
        """
        Get the a specific listener from the database.
        """
        
        # see if we were passed a name instead of an ID
        nameid = self.get_listener_id(listenerId)
        if nameid : listenerId = nameid

        cur = self.conn.cursor()
        cur.execute("SELECT id,name,host,port,cert_path,staging_key,default_delay,default_jitter,default_profile,kill_date,working_hours,listener_type,redirect_target,default_lost_limit FROM listeners WHERE id=?", [listenerId])
        listener = cur.fetchone()

        cur.close()
        return listener


    def get_listeners(self):
        """
        Return all listeners in the database.
        """
        cur = self.conn.cursor()
        cur.execute("SELECT * FROM listeners")
        results = cur.fetchall()
        cur.close()
        return results


    def get_listener_names(self):
        """
        Return all listener names in the database.
        """
        cur = self.conn.cursor()
        cur.execute("SELECT name FROM listeners")
        results = cur.fetchall()
        cur.close()
        results = [str(n[0]) for n in results]
        return results


    def get_listener_ids(self):
        """
        Return all listener IDs in the database.
        """
        cur = self.conn.cursor()