#!/usr/bin/python

#
# Copyright(C) 2005 INL
# Written by Jean Gillaux <jean@inl.fr>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 2 of the License.
#
#  This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#


"""Generates iptables commands and ldap nufw acls from xml file.

Nupyf takes an nufw acls xml file and an nufw firewall xml file and can
    * output iptables commands for both nufw and rescue (standard) modes
    * inserts nufw acls into an ldap tree

usage: nupyf [options] acls.xml firewall.xml

"""


__version__ = '1.2'
__author__ = 'Jean Gillaux'
__copyright__ = 'Copyright 2005, INL'

import xml.dom.minidom
import sys
from nupyf.nuacl import LoopError, LinkError, EltGrpList, xml_new_eltgrp, Acl, parse_groups
from nupyf import fw, ipt, nuldap, nubackend, nunat, nuxml
from os import linesep
from optparse import OptionParser
from time import strftime
from getpass import getpass
from ldap import LDAPError, NO_SUCH_OBJECT
import cPickle

import IPy
# Do not check networks netmasks
IPy.check_addr_prefixlen = False

def check_ldap(options):
    if not options.ldap_server and not options.ldap_basedn:
        # LDAP is not used: exit
        return
    if not (options.ldap_server and options.ldap_basedn):
        print >> sys.stderr, 'Error: --server and --basedn options must be provided together'
        sys.exit(1)

def parse_config_file(config_file, options):
    """Parse config_file and put directives into options.
    """
    try:
        config = eval(open(config_file).read())
    except IOError, ioe:
        print ioe
        sys.exit(1)
    for key, value in config.iteritems():
        if key == 'ldap_password': options.ldap_pwd = value
        if key == 'ldap_user': options.ldap_user = value
        if key == 'ldap_server': options.ldap_server = value
        if key == 'ldap_basedn': options.ldap_basedn = value

def try_write_file(filename, *args):
    """write args in file with name = filename
- value for filename make function print to stdout
    """
    if filename == '-':
        fd = sys.stdout
    else:
        fd = open(filename, 'w')
    for arg in args:
        if type(arg) == list:
            fd.writelines(arg)
        if type(arg) in [str, unicode]:
            fd.write(arg)

def mainLDAP(filename, server, user, password, basedn):
    try:
        dapfw = cPickle.load(open(filename))
        ldapconn = nuldap.LDAPConn(server, user, password, basedn)
        dapfw.set_conn(ldapconn)
        dapfw.load_ldap()
        dapfw.process()
        sys.exit(0)
    except NO_SUCH_OBJECT, err:
        print "LDAP error: no such object!"
        print "Check that the top domain does exist."
    except LDAPError, err:
        print "LDAP error: %s" % err
    print "(user: %s, server: %s, basedn: %s)" % (user, server, basedn)
    print
    print "Please check LDAP parameters of your nupyf configuration"
    sys.exit(1)

def parseOptions():
    usage = "usage: %prog [options] firewall_file.xml acls_file.xml"
    parser = OptionParser(usage, version = '%prog '+ __version__)
    # command line options
    parser.add_option('-d', '--dispatch', dest = 'dispatch', help = 'file to write dispatch commands in',
                        metavar = 'FILE', default = '')
    parser.add_option('-f', '--forward', dest = 'forward', help = 'file to write forward iptables rules in', metavar = 'FILE')
    parser.add_option('-i', '--input', dest = 'input', help = 'file to ouput input rules in', metavar = 'FILE', default = '')
    parser.add_option('-o', '--output', dest = 'output', help = 'file to write output rules in', metavar = 'FILE', default = '')
    parser.add_option('-m', '--mangle', dest = 'mangle', help = 'file to write mangle rules in', metavar = 'FILE', default = '')
    parser.add_option('-v', '--vpn', dest = 'vpn_rules', help = 'file to output vpn rules for packet marks, special value - means stdout',
                        metavar = 'FILE', default = '')
    parser.add_option('-n', '--nat', dest = 'nat_rules', help = 'file to output nat rules in, special value - means stdout',
                        metavar = 'FILE')
    parser.add_option('-r', '--rescue', dest = 'rescue', action = 'store_true', help = 'activate rescue mode for firewall rules generation')
    parser.add_option('', '--ulog', dest = 'ulog', action = "store_true", help = 'use ULOG system in log commands, default is to use LOG.')
    parser.add_option('', '--iptables', dest = 'ipt', help = 'path to iptables, can be a variable.', metavar = 'VALUE', default = '')
    parser.add_option('-s', '--server', dest = 'ldap_server', help = 'ldap server address.', metavar = 'ADDR', default = '')
    parser.add_option('-u', '--user', dest = 'ldap_user', help = 'ldap user used to connect.', metavar = 'USER', default = '')
    parser.add_option('-p', '--pwd', dest = 'ldap_pwd', help = 'password used to connect on ldap server (WARNING: visible on comand line).',
                        metavar = 'PWD', default = '')
    parser.add_option('-b', '--basedn', dest = 'ldap_basedn', help = 'basedn used to insert acls in (ex: ou = acls, dc = domain, dc = com).',
                        metavar = 'DN', default = '')
    parser.add_option('-a', '--askpwd', dest = 'ask_ldappwd', help = 'ask user for ldap passwd.', action = 'store_true', default = False)
    parser.add_option('-c', '--config', dest = 'config_file', help = 'configuration file', default = '')
    parser.add_option('', '--dumpldap', dest = 'dump_ldap', help = 'file to save ldap informations', default = '')
    parser.add_option('', '--loadldap', dest = 'load_ldap', help = 'file to load ldap informations from', default = None)
    parser.add_option('', '--auth_ext', dest = 'auth_ext', action = 'store_true', help = 'Generate netfilter rules to autenticate internet')
    parser.add_option('', '--sortid', dest = 'sortid', help = 'Use this ID for acl order')
    parser.add_option('', '--no-same-iface', dest = 'no_same_iface', action = 'store_true', help = 'Do not generate netfilter rules with same input and output interfaces', default = False)
    parser.add_option('', '--no-check-net', dest = 'no_check_net', action = 'store_true', help = 'Do not check inclusion and overlapping on enterprise networks', default = False)
    parser.add_option('', '--ipv6', action = 'store_true', help = 'Enable mode IPv6 (for NuFW 2.2)', default=False)
    parser.add_option('', '--nulayer7', dest = 'enable_nulayer7', action = 'store_true', help = 'Enable nulayer7 feature (generate rules which set connmarks)', default = False)
    parser.add_option('', '--short-description', dest = 'disable_logprefix', action = 'store_true', help = 'Only save ACL id in the description field of ACL objects in LDAP.', default = False)

    parser.set_defaults(rescue = False, forward = '', nat_rules = '', auth_ext = False)
    parser.set_defaults(sortid=0)
    return parser.parse_args(sys.argv)

class Nupyf:
    def __init__(self):
        self.layer7_mask = None
        self.acls = []
        self.ressources = EltGrpList()
        self.subjects = EltGrpList()
        self.protocols = EltGrpList()
        self.applications = EltGrpList()

        (self.options, args) = parseOptions()
        if self.options.config_file:
            parse_config_file(self.options.config_file, self.options)
        if self.options.ipv6:
            nuldap.USE_IPV6 = True
        if self.options.ask_ldappwd:
            self.options.ldap_pwd = getpass('LDAP Password: ')
        check_ldap(self.options)
        if self.options.load_ldap:
            mainLDAP(self.options.load_ldap, self.options.ldap_server, self.options.ldap_user, self.options.ldap_pwd, self.options.ldap_basedn)

        nubackend.NURule.desable_logprefix = self.options.disable_logprefix

        # set iptables path
        if self.options.ipt:
            ipt.IPTABLES = self.options.ipt

        self.desc_filename = args[1]
        self.acl_filename = args[2]
        self.doc = xml.dom.minidom.parse(self.acl_filename)

    def run(self):
        self.parseAll()
        self.process()
        self.createIptablesCommands()
        if self.options.ldap_server and self.options.ldap_basedn:
            self.ldapStuff()

    def parseAll(self):
        # Parse network description file, and take first firewall
        fwlist = fw.parse(self.desc_filename)
        try:
            (fwid, self.myfw) = fwlist.popitem()
        except KeyError:
            print "unable to find a firewall"
            sys.exit(1)

        # Parse groups
        self.groups_list = parse_groups(self.doc)
        if 1 not in self.groups_list:
            # group 1 is enabled by default
            self.groups_list[1] = 1

        # Parse NAT
        self.lsnat, self.ldnat, self.lpnat = nunat.nats_from_xml(self.doc, self.groups_list)

        # Parse ressources
        for myress in self.doc.getElementsByTagName("ressource"):
            elts = myress.getElementsByTagName("elt")
            mygrp = xml_new_eltgrp(myress.getAttribute('ID'), myress.getAttribute('name'), 'or', elts)
            self.ressources.add(mygrp)

        # Parse subjects
        for mysubject in self.doc.getElementsByTagName("subject"):
            elts2 = mysubject.getElementsByTagName("elt")
            mygrp2 = xml_new_eltgrp(mysubject.getAttribute('ID'), mysubject.getAttribute('name'), mysubject.getAttribute('op'), elts2)
            self.subjects.add(mygrp2)

        # Layer7 support
        l7connmark = {}
        if self.options.enable_nulayer7:
            from nulayer7 import l7xml
            l7rulelist = l7xml.load(open(self.acl_filename))
            l7connmark['mask'] = l7rulelist.mask
            self.layer7_mask = l7rulelist.mask  
            for l7rule in l7rulelist:
                l7connmark[str(l7rule.ID)] = l7rule.connmark

        # Parse protocols
        for myproto in self.doc.getElementsByTagName("protocol"):
            elts3 = myproto.getElementsByTagName("elt")
            mygrp3 = xml_new_eltgrp(myproto.getAttribute('ID'), myproto.getAttribute('name'), 'or', elts3, l7connmark=l7connmark)
            self.protocols.add(mygrp3)

        # Parse applications
        for myproto in self.doc.getElementsByTagName("application"):
            elts3 = myproto.getElementsByTagName("elt")
            mygrp4 = xml_new_eltgrp(myproto.getAttribute('ID'), myproto.getAttribute('name'), myproto.getAttribute('op'), elts3)
            self.applications.add(mygrp4)

        # Parse periods
        self.listperiodsobj = nuxml.parse_periods(self.doc)

    def processAcl(self, acl):
        # Check that ACL group is enabled
        try:
            acl_group = int(acl.getAttribute("group"))
            if acl_group \
            and (acl_group not in self.groups_list or self.groups_list[acl_group]!=1):
                # Group disabled: exit
                return
        except ValueError:
            pass
        src = acl.getAttribute("from")
        dst = acl.getAttribute("to")
        proto = acl.getAttribute("proto")
        idappli = acl.getAttribute("with")
        idperiod = acl.getAttribute("period")
        proto = self.protocols.find(proto)
        descsort = nuxml.parse_descsort(acl)
        order = descsort[self.options.sortid]

        if idappli:
            appli = self.applications.find(idappli)
        else:
            appli = None
        if idperiod:
            period = self.listperiodsobj.find(idperiod).name
        else:
            period = ''

        #get acl only if its group is enabled
        subject = self.subjects.find(src)
        ressource = self.ressources.find(dst)
        aclobj = Acl(subject, ressource, self.myfw,
            id=acl.getAttribute('ID'),
            proto=proto,
            name=acl.getAttribute("name"),
            decision=acl.getAttribute('decision'),
            ulog_prefix=acl.getAttribute('prefix'),
            appli=appli,
            period_name=period,
            sort_order=order)
        self.acls.append(aclobj)

    def process(self):
        #self.subjects.make_graph()
        #self.protocols.make_graph()
        #self.ressources.make_graph()
        #do the operator and link operations
        #Shazam
        self.subjects.normall()
        self.ressources.normall()
        self.protocols.normall()
        self.applications.normall()
        self.listperiodsobj.normall()

        # do we have to authenticate packets from internet?
        self.myfw.auth_ext = self.options.auth_ext
        # don't generate rules for same output and input intefaces ?
        self.myfw.no_same_iface = self.options.no_same_iface
        # don't check inclusion and overlaping on enterprise networks ?
        self.myfw.no_check_net = self.options.no_check_net

        # to check descsort unicity by (descsort.ID, elt.from, elt.to, elt.order)
        for acl in self.doc.getElementsByTagName("acl"):
            self.processAcl(acl)

        #net_ent = self.myfw.entreprise_nets()
        self.myfw.chains(all = 1, inout = 1)

        for x in self.acls:
            for myress in x.rules():
                self.myfw.add_rule(myress)
        self.myfw.set_nat_rules(self.lsnat, self.ldnat, self.lpnat)
        if not self.options.input:
            self.myfw.manage_input = False
        if not self.options.output:
            self.myfw.manage_output = False

    def createIptablesCommands(self):
        if self.options.ulog:
            log_type = 'ulog'
        else:
            log_type = 'log'
        fwp = ipt.FWipt(self.myfw, logtype = log_type)
        rules_create = fwp.create_ipt_chains()
        rules_connect = fwp.connect_chains(rescue = self.options.rescue)
        rules_mangle = fwp.mangle_rules()

        input_rules = output_rules = srules = []
        input_rules, output_rules, srules = fwp.gen_rules(rescue = self.options.rescue)
        intro = "#Generated by nupyf on %s from %s\n\n" % (
            strftime('%d %m %Y at %H:%M'), self.acl_filename)
        if self.options.dispatch:
            try_write_file(self.options.dispatch, intro, '#DISPATCH and DEFAULT Rules%s'%(linesep), rules_create, rules_connect)
        if self.options.forward:
            try_write_file(self.options.forward, intro, '#Rules for FORWARD%s'%linesep, srules)
        if self.options.input:
            try_write_file(self.options.input, intro, '#Rules for INPUT%s'%linesep, input_rules)
        if self.options.output:
            try_write_file(self.options.output, intro, '#Rules for OUTPUT%s'%linesep, output_rules)
        if self.options.vpn_rules:
            try_write_file(self.options.vpn_rules, intro, '#VPN Rules%s'%linesep, fwp.gen_vpn_rules())
        if self.options.nat_rules:
            #try_write_file(self.options.nat_rules, intro, '*nat%s'%linesep)
            try_write_file(self.options.nat_rules, intro, '#NAT Rules%s'%linesep, fwp.gen_nat_rules())
            #try_write_file(self.options.nat_rules, intro, 'COMMIT%s'%linesep)
        if self.options.mangle:
            try_write_file(self.options.mangle, intro, '#MANGLE Rules%s'%linesep, fwp.mangle_rules())

    def ldapStuff(self):
        ldapfw = nuldap.LDAPfw(None, fw = self.myfw)
        if self.options.dump_ldap:
            cPickle.dump(ldapfw, open(self.options.dump_ldap, 'w+'))
        else:
            conn = nuldap.LDAPConn(self.options.ldap_server, self.options.ldap_user, self.options.ldap_pwd, self.options.ldap_basedn)
            ldapfw.set_conn(conn)
            ldapfw.load_ldap()
            ldapfw.process()


def main():
    #use psyco if available
    try:
        import psyco
        psyco.full()
    except ImportError:
        pass

    # Call main code
    try:
        Nupyf().run()
        sys.exit(0)
    except LinkError, linke:
        print >> sys.stderr, str(linke)
    except LoopError, loope:
        print >> sys.stderr, str(loope)
    except fw.UnknownVpn, evpn:
        print >> sys.stderr, 'ERROR: unknown vpn with mark %s detected'%(evpn.mark)
    except LDAPError, ldaperr:
        raise
        print >> sys.stderr, 'ERROR ldap:', ldaperr
    sys.exit(1)

if __name__ == "__main__":
    main()

