/* 

                         Firewall Builder

                 Copyright (C) 2002 NetCitadel, LLC

  Author:  Vadim Kurland     vadim@vk.crocodile.org

  $Id: pix.cpp 726 2009-01-01 20:55:43Z vadim $

  This program is free software which we release under the GNU General Public
  License. You may redistribute and/or modify this program under the terms
  of that license as published by the Free Software Foundation; either
  version 2 of the License, or (at your option) any later version.

  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.
 
  To get a copy of the GNU General Public License, write to the Free Software
  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

*/

#include "../../config.h"

#include <fstream>
#include <iostream>
#include <sstream>
#include <map>
#include <algorithm>
#include <functional>

#ifdef _WIN32
#  include <direct.h>
#else
#  include <unistd.h>
#endif

#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <ctype.h>
#include <assert.h>
#include <cstring>

#include "PolicyCompiler_pix.h"
#include "RoutingCompiler_pix.h"
#include "NATCompiler_pix.h"
#include "OSConfigurator_pix_os.h"

#include "fwcompiler/Preprocessor.h"

#include "fwbuilder/Resources.h"
#include "fwbuilder/FWObjectDatabase.h"
#include "fwbuilder/Firewall.h"
#include "fwbuilder/Interface.h"
#include "fwbuilder/IPv4.h"
#include "fwbuilder/XMLTools.h"
#include "fwbuilder/FWException.h"
#include "fwbuilder/Tools.h"

#ifdef HAVE_GETOPT_H
#include <getopt.h>
#else
#ifdef _WIN32
#include <getopt.h>
#else
#include <stdlib.h>
#endif
#endif

#include "../common/init.cpp"

using namespace std;
using namespace libfwbuilder;
using namespace fwcompiler;


static const char      *filename       = NULL;
static const char      *wdir           = NULL;
static const char      *fwobjectname   = NULL;
static string           ofname         = "";
static int              dl             = 0;
static int              drp            = -1;
static int              drn            = -1;
static int              verbose        = 0;
static int              test_mode      = 0;
static int              only_print_inspection_code = 0;
static bool             fw_by_id       = false;

FWObjectDatabase       *objdb = NULL;


class UpgradePredicate: public XMLTools::UpgradePredicate
{
    public:
    virtual bool operator()(const string &msg) const 
    { 
        msg.size();  // to make compiler happy about unused parameter
	cout << "Data file has been created in the old version of Firewall Builder.\nLoad it in the GUI to convert it to the new version." << endl;
	return false;
    }
};

    

class sort_by_net_zone {
    string any_address_id;
    public:
    explicit sort_by_net_zone()
    {
        any_address_id = FWObjectDatabase::getStringId(
            FWObjectDatabase::ANY_ADDRESS_ID);
    }
    bool operator()(const FWObject *a, const FWObject *b)
    {
	if (Interface::constcast(a) && Interface::constcast(b))
        {
	    string netzone_a=a->getStr("network_zone");
	    string netzone_b=b->getStr("network_zone");
	    if ( netzone_a==any_address_id) return false;
	    if ( netzone_b==any_address_id) return true;
	}
	return false;
    }
};


void usage(const char *name)
{
    cout << "Firewall Builder:  policy compiler for Cisco PIX firewall (with support for FWSM)" << endl;
    cout << "Copyright 2002-2004 NetCitadel, LLC" << endl;
    cout << "Version " << VERSION << "-" << RELEASE_NUM << endl;
    cout << "Usage: " << name << " [-tvV] [-f filename.xml] [-d destdir] [-o output.fw] firewall_object_name" << endl;
}


int main(int argc, char * const * argv)
{   
    if (argc<=1)
    {
        usage(argv0.c_str());
        exit(1);
    }

    int   opt;

    while( (opt=getopt(argc,argv,"x:ivVf:d:r:tLo:I")) != EOF )
    {
        switch(opt)
        {
        case 'i':
            fw_by_id = true;
            break;
        case 'I':
            only_print_inspection_code++;
            break;
        case 'd':
            wdir = strdup(optarg);
            break;
        case 'r':
            respath = string(optarg);
            break;
        case 'f':
            filename = strdup(optarg);
            break;
        case 'o':
            ofname = string(optarg);
            break;
        case 'x':
            if (*optarg=='p') {
                ++optarg;
                drp  = atoi(optarg);
            } else {
                if (*optarg=='n') {
                    ++optarg;
                    drn  = atoi(optarg);
                } else {
                    if (isdigit(*optarg))  dl=atoi(optarg);  // increase debug level
                    else {
                        usage(argv[0]);
                        exit(1);
                    }
                }
            }
            break;
        case 't':
            test_mode++;
            break;
        case 'v':
            verbose++;
            break;
        case 'V':
            usage(argv[0]);
            exit(1);
	case 'h':
            usage(argv[0]);
            exit(1);
        }
    }

/* can use init and init2 only after command line option "-r" has been read */

    init(argv);

    if((argc-1) != optind)
    {
        usage(argv[0]);
        exit(1);
    }

    fwobjectname = strdup( argv[optind++] );

    if (filename==NULL || fwobjectname==NULL)
    {
        usage(argv[0]);
        exit(1);
    }

    if (wdir==0) 	wdir="./";

    if (
#ifdef _WIN32
        _chdir(wdir)
#else
        chdir(wdir)
#endif
    ) {
        cerr << "Can't change working directory to: " << wdir << endl;
        exit(1);
    }



    if (test_mode)
        cout << "*** Running in test mode, all errors are ignored"
             << endl << endl;

    try
    {
        new Resources(respath+FS_SEPARATOR+"resources.xml");

/* create database */
        objdb = new FWObjectDatabase();

/* load the data file */
        UpgradePredicate upgrade_predicate; 

        if (verbose) cout << " *** Loading data ...";

#if 0
        objdb->setReadOnly( false );
        objdb->load( sysfname, &upgrade_predicate, librespath);
        objdb->setFileName("");
        FWObjectDatabase *ndb = new FWObjectDatabase();
        ndb->load(filename, &upgrade_predicate,  librespath);
        objdb->merge(ndb, NULL);
        delete ndb;
        objdb->setFileName(filename);
#endif


        objdb->setReadOnly( false );
        objdb->load( filename, &upgrade_predicate, librespath);
        objdb->setFileName(filename);
        objdb->reIndex();

        if (verbose) cout << " done\n";

        /*
         *  some general sanity checks first
         */
	Firewall* fw;
        if (fw_by_id)
        {
            // fwobjectname is actually object id
            fw = Firewall::cast(
                objdb->findInIndex(objdb->getIntId(fwobjectname)));
            fwobjectname = fw->getName().c_str();
        }
        else
            fw = objdb->findFirewallByName(fwobjectname);

        if (ofname.empty())
            ofname=string(fwobjectname)+".fw";

        FWOptions* options=fw->getOptionsObject();

        bool pix_acl_basic=options->getBool("pix_acl_basic");
        bool pix_acl_no_clear=options->getBool("pix_acl_no_clear");
        bool pix_acl_substitution=options->getBool("pix_acl_substitution");
        bool pix_add_clear_statements=options->getBool("pix_add_clear_statements");
        
        if ( !pix_acl_basic &&
             !pix_acl_no_clear &&
             !pix_acl_substitution )
        {
            if ( pix_add_clear_statements ) options->setBool("pix_acl_basic",true);
            else options->setBool("pix_acl_no_clear",true);
        }

        if (only_print_inspection_code)
        {
            OSConfigurator_pix_os *oscnf=NULL;
            oscnf = new OSConfigurator_pix_os(objdb , fwobjectname, false);
            oscnf->prolog();

            cout << oscnf->getProtocolInspectionCommands();
            return 0;
        }

        Helper helper(NULL);

        multimap<string, FWObject*> netzone_objects;

        std::list<FWObject*> l2=fw->getByType(Interface::TYPENAME);
        for (std::list<FWObject*>::iterator i=l2.begin(); i!=l2.end(); ++i)
        {
            Interface *iface=dynamic_cast<Interface*>(*i);
            assert(iface);
/*
 * missing labels on interfaces
 */
            if (iface->getLabel()=="")
            {
                string lbl;
                if (iface->getSecurityLevel()==0)   lbl="outside";
                else
                {
                    if (iface->getSecurityLevel()==100) lbl="inside";
                    else
                    {
                        char s[64];
                        sprintf(s,"dmz%d",iface->getSecurityLevel());
                        lbl=s;
                    }
                }
                iface->setLabel(lbl);
            }

            if ( iface->isDyn())  
            {
                list<FWObject*> l3=iface->getByType(IPv4::TYPENAME);
                if (l3.size()>0)
                {
                    char errstr[256];
                    for (list<FWObject*>::iterator j=l3.begin(); j!=l3.end(); ++j) 
                        if ( objdb->findAllReferences(*j).size()!=0 )
                        {
                            sprintf(errstr,
                                    _("Dynamic interface %s has an IP address that is used in the firewall policy rule.\n"),
                                    iface->getName().c_str() );
                            throw FWException(errstr);
                        }

                    sprintf(errstr,
                            _("Dynamic interface %s should not have an IP address object attached to it. This IP address object will be ignored.\n"),
                            iface->getName().c_str() );
                    cerr << errstr;
                    for (list<FWObject*>::iterator j=l3.begin(); j!=l3.end(); ++j) 
                        iface->remove(*j);
                }
            }

/*
 * no address
 */
            if (iface->isRegular())
            {
                FWObject *ipv4=iface->getFirstByType(IPv4::TYPENAME);
                if (ipv4==NULL)
                    throw FWException(
                        "Interface "+
                        iface->getName()+" ("+iface->getLabel()+") has no address." );
            }
/*
 * there shouldn't be two interfaces with the same security level
 */
            for (std::list<FWObject*>::iterator j=l2.begin(); j!=l2.end(); ++j)
            {
                Interface *iface2=dynamic_cast<Interface*>(*j);
                assert(iface2);
                if (iface->getId()==iface2->getId()) continue;
                
                if (iface->getSecurityLevel()==iface2->getSecurityLevel())
                    throw FWException(
                        "Security level of each interface should be unique, however interfaces "+
                        iface->getName()+" ("+iface->getLabel()+")"+
                        " and "+
                        iface2->getName()+" ("+iface2->getLabel()+")"+
                        " have the same security level."
                    );

            }
/*
 * in PIX, we need network zones to be defined for all interfaces
 */
            string netzone_id=iface->getStr("network_zone");
            if (netzone_id=="") {
                throw FWException(
                    "Network zone definition is missing for interface "
                    +iface->getName()+" ("+iface->getLabel()+")");
            }
            FWObject *netzone=objdb->findInIndex(
                FWObjectDatabase::getIntId(netzone_id));
            if (netzone==NULL) 
            {
                throw FWException(
                    "Network zone points at nonexisting object for interface "
                    +iface->getName()+" ("+iface->getLabel()+")");
            }
/*
 * netzone may be a group, in which case we need to expand it
 * (recursively). 
 * 
 * 1. We create new temporary object (type Group).
 *
 * 2. put it in the database somewhere
 *
 * 3. add all objects that belong to the network zone to this
 * group. We add objects directly, not as a reference.
 *
 * 4. finally replace reference to the old network zone object in the
 * interface with reference to this new group.
 *
 * 5. we store ID of the original network zone object 
 *    using iface->setStr("orig_netzone_id")
 *
 * This ensures netzones do not contain other groups and do not
 * require any recursive expanding anymore. Since objects were added
 * to netzones directly, we do not need to bother with dereferencing,
 * too.
 */
            list<FWObject*> ol;
            helper.expand_group_recursive_no_cache(netzone,ol);

            FWObject *nz = objdb->createObjectGroup();
            assert(nz!=NULL);
            nz->setName("netzone_"+iface->getLabel());
            objdb->add(nz);

            for (list<FWObject*>::iterator j=ol.begin(); j!=ol.end(); ++j)
            {
                netzone_objects.insert( pair<string,FWObject*>(iface->getLabel(),*j));
                nz->add(*j);
            }
            iface->setStr("orig_netzone_id", netzone_id );
            iface->setStr("network_zone",
                          FWObjectDatabase::getStringId(nz->getId()) );
        }

/*
 * the same object (network or host) can not belong to network zones
 * of two different interfaces. Map netzone_objects holds pairs
 * interface_id/object. We just make sure the same object does not
 * appear in two pairs with different interfaces.
 */
        multimap<string,FWObject*>::iterator k;
        for (k=netzone_objects.begin(); k!=netzone_objects.end(); ++k)
        {
            multimap<string,FWObject*>::iterator l;
            l=k;
            ++l;
            for ( ; l!=netzone_objects.end(); ++l)
            {
                if ( l->second->getId() == k->second->getId() )
                {
                    if (k->first==l->first)
                        throw FWException(
                            "Object "+l->second->getName()
                            +" is used more than once in network zone of interface '"
                            +k->first+"'");
                    else
                        throw FWException(
                            "Object "+l->second->getName()
                            +" is used more than once in network zones of the following interfaces: '"
                            +k->first+"' and '"+l->first+"'");
                }
            }
            
        }

/*
 *  now sort interfaces by their network zone "width" (that is, more narrow 
 *  network zone should go first, interface with network zone "any" should be
 *  the last)
 *
 std::sort(fw->begin(), fw->end(), sort_by_net_zone() );
*/

        char           timestr[256];
        time_t         tm;

        tm=time(NULL);
        strcpy(timestr,ctime(&tm));
        timestr[ strlen(timestr)-1 ]='\0';
    
#ifdef _WIN32
        char* user_name=getenv("USERNAME");
#else
        char* user_name=getenv("USER");
#endif
        if (user_name==NULL) 
            throw FWException("Can't figure out your user name, aborting");

        Preprocessor* prep=new Preprocessor(objdb , fwobjectname, false);
        prep->compile();

/*
 * Process firewall options, build OS network configuration script
 */
        OSConfigurator *oscnf=NULL;
        oscnf = new OSConfigurator_pix_os(objdb , fwobjectname, false);

        oscnf->prolog();
        oscnf->processFirewallOptions();


/* create compilers and run the whole thing */

        NATCompiler_pix *n = new NATCompiler_pix( objdb ,
                                                  fwobjectname, false, oscnf );

        if (test_mode) n->setTestMode();
        n->setDebugLevel( dl );
        n->setDebugRule(  drn );
        n->setVerbose( verbose );

        if ( n->prolog() > 0 ) {
            n->compile();
            n->epilog();
        } else
            cout << " Nothing to compile in NAT \n" << flush;


        PolicyCompiler_pix *c = new PolicyCompiler_pix( objdb ,
                                                        fwobjectname ,
                                                        false,
                                                        oscnf , n);

        if (test_mode) c->setTestMode();
        c->setDebugLevel( dl );
        c->setDebugRule(  drp );
        c->setVerbose( verbose );

        if ( c->prolog() > 0 ) {
            c->compile();
            c->epilog();
        } else
            cout << " Nothing to compile in Policy \n" << flush;

        RoutingCompiler_pix *r = new RoutingCompiler_pix( objdb ,
                                                        fwobjectname ,
                                                        false,
                                                        oscnf);
//                                                        oscnf , n);

        if (test_mode) r->setTestMode();
        r->setDebugLevel( dl );
        r->setDebugRule(  drp );
        r->setVerbose( verbose );

        if ( r->prolog() > 0 ) {
            r->compile();
            r->epilog();
        } else
            cout << " Nothing to compile in Routing \n" << flush;

#ifdef _WIN32
        ofstream ofile(ofname.c_str(), ios::out|ios::binary);
#else
        ofstream ofile(ofname.c_str());
#endif

        ofile << "!\n\
!  This is automatically generated file. DO NOT MODIFY !\n\
!\n\
!  Firewall Builder  fwb_pix v" << VERSION << "-" << BUILD_NUM << " \n\
!\n\
!  Generated " << timestr
              << " "
              << tzname[0]
              << " by " 
              << user_name;

        ofile << endl;

        string vers = fw->getStr("version");
        string platform = fw->getStr("platform");

        bool outbound_acl_supported = Resources::platform_res[platform]->getResourceBool(
            string("/FWBuilderResources/Target/options/")+
            "version_"+vers+
            "/pix_outbound_acl_supported");
        bool afpa = options->getBool("pix_assume_fw_part_of_any");
        bool emulate_outb_acls = options->getBool("pix_emulate_out_acl");
        bool generate_outb_acls = options->getBool("pix_generate_out_acl");

        ofile << "!" << endl;
        ofile << "!" 
              << " Compiled for " << platform << " " << vers << endl;
        ofile << "!"
              << " Outbound ACLs " << string((outbound_acl_supported)?"supported":"not supported")
              << endl;
        if (!outbound_acl_supported)
        {
            ofile << "!"
                  << " Emulate outbound ACLs: " << string((emulate_outb_acls)?"yes":"no")
                  << endl;
        }
            ofile << "!"
                  << " Generating outbound ACLs: " << string((generate_outb_acls)?"yes":"no")
                  << endl;

        ofile << "!"
              << " Assume firewall is part of 'any': " << string((afpa)?"yes":"no")
              << endl;

        ofile << "!" << endl;
        ofile << "!" << MANIFEST_MARKER << "* " << ofname << endl;
        ofile << "!" << endl;

        ofile << endl;
        ofile << "!" << endl;
        ofile << "! Prolog script:" << endl;
        ofile << "!" << endl;

        string pre_hook= fw->getOptionsObject()->getStr("pix_prolog_script");
        ofile << pre_hook << endl;

        ofile << "!" << endl;
        ofile << "! End of prolog script:" << endl;
        ofile << "!" << endl;


        ofile << oscnf->getCompiledScript();
        ofile << endl;

        if (c->haveErrorsAndWarnings())
        {
            ofile << "! Policy compiler errors and warnings:"
                    << endl;
            ofile << c->getErrors("! ");
        }

        ofile << c->getCompiledScript();
        ofile << endl;

        if (n->haveErrorsAndWarnings())
        {
            ofile << "! NAT compiler errors and warnings:"
                    << endl;
            ofile << n->getErrors("! ");
        }

        ofile << n->getCompiledScript();
        
        ofile << endl;

        if (r->haveErrorsAndWarnings())
        {
            ofile << "! Routing compiler errors and warnings:"
                    << endl;
            ofile << r->getErrors("! ");
        }

        ofile << r->getCompiledScript();

        ofile << "!" << endl;
        ofile << "! Epilog script:" << endl;
        ofile << "!" << endl;

        string post_hook= fw->getOptionsObject()->getStr("pix_epilog_script");
        ofile << post_hook << endl;

        ofile << endl;
        ofile << "! End of epilog script:" << endl;
        ofile << "!" << endl;

        ofile.close();

        cout << _(" Compiled successfully") << endl << flush;
        
    } catch(libfwbuilder::FWException &ex)  {
        cerr << ex.toString() << endl;
        return 1;
    } catch (std::string s) {
        cerr << s << endl;
        return 1;
//     } catch (std::exception ex) {
//         cerr << "exception: " << ex.what() << endl;
//         return 1;
//     } catch (...) {
//         cerr << "Unsupported exception";
//         return 1;
    }

    return 0;
}
