# -*- coding: utf-8 -*-
import os
import sys
import netaddr

if '/usr/scripts' not in sys.path:
    sys.path.append('/usr/scripts')

import syslog
import subprocess

from gestion.affich_tools import anim, OK, cprint
from gestion.iptools import AddrInNet, NetSubnets, IpSubnet, NetInNets

import lc_ldap.shortcuts
import lc_ldap.objets
import lc_ldap.attributs

squeeze = os.uname()[2] < '3'

#: Chaines par défaut d'iptables
default_chains = ['INPUT', 'OUTPUT', 'FORWARD', 'PREROUTING', 'POSTROUTING']
#: Tables d'iptables
tables = ['raw', 'mangle', 'filter', 'nat']


def pretty_print(table, chain):
    """Affiche quelle chaine est en train d'être construite dans quelle table de NetFilter"""
    anim('\t%s dans %s' % (chain, table))


class TcError(Exception):
    """ Gestion des erreurs de tc """
    def __init__(self,cmd,err_code,output):
        self.cmd=cmd
        self.err_code=err_code
        self.output=output
        syslog.syslog(syslog.LOG_ERR,"%s : status %s,%s" % (cmd,err_code,output))
    def __str__(self):
        return "%s\n  status : %s\n  %s" % (self.cmd,self.err_code,self.output)

def tc(cmd, block=True):
    """ Interface de tc """
    params = ['/sbin/tc']
    params.extend(cmd.split())
    p = subprocess.Popen(params , stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    if block:
        status = p.wait()
        stdoutdata, stderrdata = p.communicate()
        if status:
            raise TcError(' '.join(params), status, stdoutdata + stderrdata)
        return stdoutdata + stderrdata

class firewall_tools(object) :
    """Classe de base du pare-feu implémentant l'association mac-ip (pour les machines filaires) et les blacklists hard"""

    def machines(self):
        """Renvois la liste de toutes les machines"""
        if self._machines:
            return self._machines
        # On utilise allMachinesAdherents car on a besoin que
        # les machine.proprio() soit déjà peuplés. En effet, on regarde
        # les blacklistes d'un proprio lorsque l'on regarde les blacklistes
        # d'une machine
        anim('\tChargement des machines')
        self._machines, self._adherents = self.conn.allMachinesAdherents()
        self._adherents = [ adh for adh in self._adherents if adh.paiement_ok() ]
        print OK
        return self._machines

    def adherents(self):
        """
        Renvois la liste de tous les adhérents à jour de paiement
        (car on suppose que la blackliste paiement est hard)
        """
        if self._adherents:
            return self._adherents
        self._machines, self._adherents = self.conn.allMachinesAdherents()
        self._adherents = [ adh for adh in self._adherents if adh.paiement_ok() ]
        return self._adherents

    def blacklisted_machines(self):
        """Renvois la liste de toutes les machines ayant une blackliste actives"""
        if self._blacklisted_machines:
            return self._blacklisted_machines
        self._blacklisted_machines = [ machine for machine in self.machines() if machine.blacklist_actif() ]
        return self._blacklisted_machines

    def blacklisted_ips(self, blacklist_sanctions=None, nets=None):
        """Renvois l'ensemble des ips des machines ayant une blacklist dans blacklist_sanctions et étant dans nets si spécifié"""
        bl_ips = set()
        for machine in self.blacklisted_machines():
            if blacklist_sanctions is None or set(bl['type'] for bl in machine.blacklist_actif()).intersection(blacklist_sanctions):
                for ip in machine['ipHostNumber']:
                    if nets is None:
                        bl_ips.add(str(ip))
                    else:
                        for net in nets:
                            if ip.value in netaddr.IPNetwork(net):
                                bl_ips.add(str(ip))
        return bl_ips

    def blacklisted_adherents(self, excepts=[]):
        """Renvois la liste de tous les adhérents ayant une blackliste active en ignorant les blacklist de excepts"""
        if self._blacklisted_adherents and self._blacklisted_adherents_type == set(excepts):
            return self._blacklisted_adherents
        self._blacklisted_adherents = filter(lambda adh: adh.blacklist_actif(excepts), self.adherents())
        self._blacklisted_adherents_type = set(excepts)
        return self._blacklisted_adherents

    def add(self, table, chain, rule):
        """Ajoute la règle ``rule`` à la chaine ``chain`` dans la table ``table``"""
        if not chain in self.chain_list[table]:
            self.chain_list[table].append(chain)
            self.rules_list[table][chain]=[]
        self.rules_list[table][chain].append(rule)

    def delete(self, table=None, chain=None):
        """Supprime ``chain`` de ``table`` si fournis, sinon supprime ``table``, sinon toutes les tables"""
        if not table:
            for table in tables:
                self.delete(table, chain)
        if not chain:
            for chain in self.chain_list[table]:
                self.delete(table, chain)

        if not chain in self.chain_list[table]:
            return
        if not chain in default_chains:
            self.chain_list[table].remove(chain)
            del(self.rules_list[table][chain])
        else:
            self.rules_list[table][chain]=[]

    def flush(self, table=None, chain=None):
        """Vide ``chain`` dans ``table`` si fournis, sinon vide toutes
           les chaines de  ``table``, sinon vide toutes les chaines de toutes les tables"""
        if not table:
            for table in tables:
                self.flush(table, chain)
        if not chain:
            for chain in self.chain_list[table]:
                self.flush(table, chain)
        if not chain in self.chain_list[table]:
            self.chain_list[table].append(chain)
        self.rules_list[table][chain]=[]

    def restore(self, table=None, chains=[], noflush=False):
        """Restores les règles du pare-feu dans le noyau en appelant ``iptables-restore``.
           Si ``table`` est fournis, on ne restore que table.
           Si ``noflush`` n'est pas à ``True`` tout le contenu précédent est supprimé"""
        str=self.format(chains)
        f=open('/tmp/ipt_rules', 'w')
        f.write(str)
        f.close()
        params = ['/sbin/iptables-restore']
        if noflush:
            params.append('--noflush')
        if table and table in ['raw', 'mangle', 'filter', 'nat']:
            params.append('--table')
            params.append(table)
        p = subprocess.Popen(params , stdin=subprocess.PIPE)
        p.communicate(input=str)

    def apply(self, table, chain):
        """Applique les règles de ``chain`` dans ``table``"""
        if not chain in self.chain_list[table]:
            return
        self.restore(table, [chain], noflush=True)
        self.delete(table, chain)

    def format(self, chains=[]):
        """Transforme la structure interne des règles du pare-feu en celle comprise par ``iptables-restore``"""
        str = ''
        for table in self.chain_list.keys():
            str += '*%s\n' % table
            for chain in self.chain_list[table]:
                if not chains or chain in chains or chain in default_chains:
                    str += ':%s %s [0:0]\n' % (chain, chain in default_chains and 'ACCEPT' or '-')
            for chain in self.chain_list[table]:
                if not chains or chain in chains :
                    for rule in self.rules_list[table][chain]:
                        str += '-A %s %s\n' % (chain, rule)
            str += 'COMMIT\n'
        return str

    def reload(self, func_name):
        if squeeze and self.reloadable[func_name] in self.use_ipset:
            anim('\tVidage de %s' % self.reloadable[func_name]())
            for table in ['raw', 'mangle', 'filter', 'nat']:
                self.flush(table, self.reloadable[func_name]())
            self.restore(noflush=True)
            print OK

        for table in ['raw', 'mangle', 'filter', 'nat']:
            self.reloadable[func_name](table)
        if self.reloadable[func_name] in self.use_ipset:
            self.reloadable[func_name](fill_ipset=True)
        if self.reloadable[func_name] in self.use_tc:
            self.reloadable[func_name](run_tc=True)

        anim('\tRestauration d\'iptables')
        self.restore(noflush=True)
        print OK

    def __init__(self):
        #initialisation des structures communes : récupération des ipset
        if os.getuid() != 0:
            from affich_tools import coul
            sys.stderr.write(coul("Il faut être root pour utiliser le firewall\n", 'gras'))
            sys.exit(1)

        # Connection à la base ldap
        self.conn = lc_ldap.shortcuts.lc_ldap_admin(user=u'firewall')

        self.reloadable = {}

        self.use_ipset = []
        self.use_tc = []

        self._machines = None
        self._adherents = None
        self._blacklisted_machines = None
        self._blacklisted_adherents = None

        self.chain_list={
            'raw':['OUTPUT', 'PREROUTING'],
            'mangle':['INPUT', 'OUTPUT', 'FORWARD', 'PREROUTING', 'POSTROUTING'],
            'filter':['INPUT', 'OUTPUT', 'FORWARD'],
            'nat':['OUTPUT', 'PREROUTING', 'POSTROUTING']
        }
        self.rules_list = {
            'raw': { 'OUTPUT':[], 'PREROUTING':[] },
            'mangle':{'INPUT':[], 'OUTPUT':[], 'FORWARD':[], 'PREROUTING':[], 'POSTROUTING':[]},
            'filter':{'INPUT':[], 'OUTPUT':[], 'FORWARD':[]},
            'nat':{'OUTPUT':[], 'PREROUTING':[], 'POSTROUTING':[]}
        }

        self.ipset={}



    def start(self):
        """Démarre le pare-feu : génère les règles, puis les restore"""
        self.machines()

        if squeeze:
            anim('\tVidage du pare-feu')
            self.restore()
            print OK

        self.raw_table()
        self.mangle_table()
        self.filter_table()
        self.nat_table()

        anim('\tRestauration d\'iptables')
        self.restore()
        print OK
        return

    def stop(self):
        """Vide les règles du pare-feu"""
        self.delete()
        self.restore()
        return

    def restart(self):
        """Alias de :py:func:`start`"""
        self.start()
        return

