Advertisement
Guest User

services.pm

a guest
Dec 2nd, 2010
1,533
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Bash 40.99 KB | None | 0 0
  1. package pf::services;
  2.  
  3. =head1 NAME
  4.  
  5. pf::services - module to manage the PacketFence services and daemons.
  6.  
  7. =head1 DESCRIPTION
  8.  
  9. pf::services contains the functions necessary to control the different
  10. PacketFence services and daemons. It also contains the functions used
  11. to generate or validate some configuration files.
  12.  
  13. =head1 CONFIGURATION AND ENVIRONMENT
  14.  
  15. Read the following configuration files: F<dhcpd_vlan.conf>,
  16. F<named-vlan.conf>, F<named-isolation.ca>, F<named-registration.ca>,
  17. F<networks.conf>, F<violations.conf> and F<switches.conf>.
  18.  
  19. Generate the following configuration files: F<dhcpd.conf>, F<named.conf>,
  20. F<snort.conf>, F<httpd.conf>, F<snmptrapd.conf>.
  21.  
  22. =cut
  23.  
  24. use strict;
  25. use warnings;
  26. use File::Basename;
  27. use Config::IniFiles;
  28. use Log::Log4perl;
  29. use UNIVERSAL::require;
  30.  
  31. use pf::config;
  32. use pf::util;
  33. use pf::violation qw(violation_view_open_uniq);
  34. use pf::node qw(nodes_registered_not_violators);
  35. use pf::trigger qw(trigger_delete_all);
  36. use pf::class qw(class_view_all class_merge);
  37. use pf::SwitchFactory;
  38.  
  39. my %flags;
  40. $flags{'apache2'}          = "-f $conf_dir/httpd.conf";
  41. $flags{'pfdetect'}       = "-d -p $install_dir/var/alert &";
  42. $flags{'pfmon'}          = "-d &";
  43. $flags{'pfdhcplistener'} = "-d &";
  44. $flags{'pfredirect'}     = "-d &";
  45. $flags{'pfsetvlan'}      = "-d &";
  46. $flags{'dhcpd'}
  47.     = " -lf $conf_dir/dhcpd/dhcpd.leases -cf $conf_dir/dhcpd.conf "
  48.     . join( " ", get_dhcp_devs() );
  49. $flags{'named'} = "-u pf -c $install_dir/conf/named.conf";
  50. $flags{'snmptrapd'}
  51.     = "-n -c $conf_dir/snmptrapd.conf -C -A -Lf $install_dir/logs/snmptrapd.log -p $install_dir/var/snmptrapd.pid -On";
  52.  
  53. if ( isenabled( $Config{'trapping'}{'detection'} ) && $monitor_int ) {
  54.     $flags{'snort'}
  55.         = "-u pf -c $conf_dir/snort.conf -i "
  56.         . $monitor_int
  57.         . " -o -N -D -l $install_dir/var";
  58. }
  59.  
  60. =head1 SUBROUTINES
  61.  
  62. =over
  63.  
  64. =item * service_ctl
  65.  
  66. =cut
  67.  
  68. sub service_ctl {
  69.     my ( $daemon, $action, $quick ) = @_;
  70.     my $logger = Log::Log4perl::get_logger('pf::services');
  71.     my $service
  72.         = ( $Config{'services'}{$daemon} || "$install_dir/sbin/$daemon" );
  73.     my $exe = basename($service);
  74.     $logger->info("$service $action");
  75.     if ( $exe
  76.         =~ /^(named|dhcpd|pfdhcplistener|pfmon|pfdetect|pfredirect|snort|apache2|snmptrapd|pfsetvlan)$/
  77.         )
  78.     {
  79.         $exe = $1;
  80.     CASE: {
  81.             $action eq "start" && do {
  82.                 return (0)
  83.                     if (
  84.                     $exe =~ /dhcpd/
  85.                     && (( $Config{'network'}{'mode'} =~ /^arp$/ )
  86.                         || (   ( $Config{'network'}{'mode'} =~ /^vlan$/i )
  87.                             && ( !isenabled( $Config{'vlan'}{'dhcpd'} ) ) )
  88.                     )
  89.                     );
  90.                 return (0)
  91.                     if ( $exe =~ /snort/
  92.                     && !isenabled( $Config{'trapping'}{'detection'} ) );
  93.                 return (0)
  94.                     if ( $exe =~ /pfdhcplistener/
  95.                     && !isenabled( $Config{'network'}{'dhcpdetector'} ) );
  96.                 return (0)
  97.                     if ( $exe =~ /snmptrapd/
  98.                     && !( $Config{'network'}{'mode'} =~ /vlan/i ) );
  99.                 return (0)
  100.                     if ( $exe =~ /pfsetvlan/
  101.                     && !( $Config{'network'}{'mode'} =~ /vlan/i ) );
  102.                 return (0)
  103.                     if (
  104.                     $exe =~ /named/
  105.                     && !(
  106.                            ( $Config{'network'}{'mode'} =~ /vlan/i )
  107.                         && ( isenabled( $Config{'vlan'}{'named'} ) )
  108.                     )
  109.                     );
  110.                 if ( $daemon =~ /(named|dhcpd|snort|apache2|snmptrapd)/
  111.                     && !$quick )
  112.                 {
  113.                     my $confname = "generate_" . $daemon . "_conf";
  114.                     $logger->info(
  115.                         "Generating configuration file for $exe ($confname)");
  116.                     my %serviceHash = (
  117.                         'named' => \&generate_named_conf,
  118.                         'dhcpd' => \&generate_dhcpd_conf,
  119.                         'snort' => \&generate_snort_conf,
  120.                         'apache2' => \&generate_httpd_conf,
  121.                         'snmptrapd' => \&generate_snmptrapd_conf
  122.                     );
  123.                     if ( $serviceHash{$daemon} ) {
  124.                         $serviceHash{$daemon}->();
  125.                     } else {
  126.                         print "No such sub: $confname\n";
  127.                     }
  128.                 }
  129.                 if (  ( $service =~ /named|dhcpd|pfdhcplistener|pfmon|pfdetect|pfredirect|snort|apache2|snmptrapd|pfsetvlan/ )
  130.                       && ( $daemon =~ /named|dhcpd|pfdhcplistener|pfmon|pfdetect|pfredirect|snort|apache2|snmptrapd|pfsetvlan/ )
  131.                       && ( defined( $flags{$daemon} ) ) ) {
  132.                     if ( $daemon ne 'pfdhcplistener' ) {
  133.                         if (   ( $daemon eq 'pfsetvlan' )
  134.                             && ( !switches_conf_is_valid() ) )
  135.                         {
  136.                             $logger->error(
  137.                                 "errors in switches.conf. pfsetvlan will NOT be started"
  138.                             );
  139.                             return 0;
  140.                         }
  141.                         $logger->info(
  142.                             "Starting $exe with '$service $flags{$daemon}'");
  143.                         my $cmd_line = "$service $flags{$daemon}";
  144.                         if ($cmd_line =~ /(.+)/) {
  145.                             $cmd_line = $1;
  146.                             return ( system($cmd_line) );
  147.                         }
  148.                     } else {
  149.                         if ( isenabled( $Config{'network'}{'dhcpdetector'} ) )
  150.                         {
  151.                             my @devices = @listen_ints;
  152.                             push @devices, @dhcplistener_ints;
  153.                             @devices = get_dhcp_devs()
  154.                                 if (
  155.                                 $Config{'network'}{'mode'} =~ /^dhcp$/i );
  156.                             foreach my $dev (@devices) {
  157.                                 my $cmd_line = "$service -i $dev $flags{$daemon}";
  158.                                 if ($cmd_line =~ /^(.+)$/) {
  159.                                     $cmd_line = $1;
  160.                                     $logger->info(
  161.                                         "Starting $exe with '$cmd_line'"
  162.                                     );
  163.                                     system($cmd_line);
  164.                                 }
  165.                             }
  166.                             return 1;
  167.                         }
  168.                     }
  169.                 }
  170.                 last CASE;
  171.             };
  172.             $action eq "stop" && do {
  173.                 #my @debug= system('pkill','-f',$exe);
  174.                 $logger->info("Stopping $exe with 'pkill $exe'");
  175.                 eval { `pkill $exe`; };
  176.                 if ($@) {
  177.                     $logger->logcroak("Can't stop $exe with 'pkill $exe': $@");
  178.                     return;
  179.                 }
  180.  
  181.                 #$logger->info("pkill shows " . join(@debug));
  182.                 my $maxWait = 10;
  183.                 my $curWait = 0;
  184.                 while (( $curWait < $maxWait )
  185.                     && ( service_ctl( $exe, "status" ) ne "0" ) )
  186.                 {
  187.                     $logger->info("Waiting for $exe to stop");
  188.                     sleep(2);
  189.                     $curWait++;
  190.                 }
  191.                 if ( -e $install_dir . "/var/$exe.pid" ) {
  192.                     $logger->info("Removing $install_dir/var/$exe.pid");
  193.                     unlink( $install_dir . "/var/$exe.pid" );
  194.                 }
  195.                 last CASE;
  196.             };
  197.             $action eq "restart" && do {
  198.                 service_ctl( "pfdetect", "stop" ) if ( $daemon eq "snort" );
  199.                 service_ctl( $daemon, "stop" );
  200.  
  201.                 service_ctl( "pfdetect", "start" ) if ( $daemon eq "snort" );
  202.                 service_ctl( $daemon, "start" );
  203.                 last CASE;
  204.             };
  205.             $action eq "status" && do {
  206.                 my $pid;
  207.                 chop( $pid = `pidof -x $exe` );
  208.                 $pid = 0 if ( !$pid );
  209.                 $logger->info("pidof -x $exe returned $pid");
  210.                 return ($pid);
  211.             }
  212.         }
  213.     } else {
  214.         $logger->logcroak("unknown service $exe!");
  215.         return 0;
  216.     }
  217.     return 1;
  218. }
  219.  
  220. =item * service_list
  221.  
  222. return an array of enabled services
  223.  
  224. =cut
  225.  
  226. sub service_list {
  227.     my @services         = @_;
  228.     my @finalServiceList = ();
  229.     my $snortflag        = 0;
  230.     foreach my $service (@services) {
  231.         if ( $service eq "snort" ) {
  232.             $snortflag = 1
  233.                 if ( isenabled( $Config{'trapping'}{'detection'} ) );
  234.         } elsif ( $service eq "pfdetect" ) {
  235.             push @finalServiceList, $service
  236.                 if ( isenabled( $Config{'trapping'}{'detection'} ) );
  237.         } elsif ( $service eq "pfredirect" ) {
  238.             push @finalServiceList, $service
  239.                 if ( $Config{'ports'}{'listeners'} );
  240.         } elsif ( $service eq "dhcpd" ) {
  241.             push @finalServiceList, $service
  242.                 if (
  243.                 ( $Config{'network'}{'mode'} =~ /^dhcp$/i )
  244.                 || (   ( $Config{'network'}{'mode'} =~ /^vlan$/i )
  245.                     && ( isenabled( $Config{'vlan'}{'dhcpd'} ) ) )
  246.                 );
  247.         } elsif ( $service eq "snmptrapd" ) {
  248.             push @finalServiceList, $service
  249.                 if ( $Config{'network'}{'mode'} =~ /vlan/i );
  250.         } elsif ( $service eq "named" ) {
  251.             push @finalServiceList, $service
  252.                 if ( ( $Config{'network'}{'mode'} =~ /vlan/i )
  253.                 && ( isenabled( $Config{'vlan'}{'named'} ) ) );
  254.         } elsif ( $service eq "pfsetvlan" ) {
  255.             push @finalServiceList, $service
  256.                 if ( $Config{'network'}{'mode'} =~ /vlan/i );
  257.         } else {
  258.             push @finalServiceList, $service;
  259.         }
  260.     }
  261.  
  262.     #add snort last
  263.     push @finalServiceList, "snort" if ($snortflag);
  264.     return @finalServiceList;
  265. }
  266.  
  267. =item * generate_named_conf
  268.  
  269. =cut
  270.  
  271. sub generate_named_conf {
  272.     my $logger = Log::Log4perl::get_logger('pf::services');
  273.     require Net::Netmask;
  274.     import Net::Netmask;
  275.     my %tags;
  276.     $tags{'template'}    = "$conf_dir/templates/named_vlan.conf";
  277.     $tags{'install_dir'} = $install_dir;
  278.  
  279.     my %network_conf;
  280.     tie %network_conf, 'Config::IniFiles',
  281.         ( -file => "$conf_dir/networks.conf", -allowempty => 1 );
  282.     my @errors = @Config::IniFiles::errors;
  283.     if ( scalar(@errors) ) {
  284.         $logger->error(
  285.             "Error reading networks.conf: " . join( "\n", @errors ) . "\n" );
  286.         return 0;
  287.     }
  288.  
  289.     my @routed_isolation_nets_named;
  290.     my @routed_registration_nets_named;
  291.     foreach my $section ( tied(%network_conf)->Sections ) {
  292.         foreach my $key ( keys %{ $network_conf{$section} } ) {
  293.             $network_conf{$section}{$key} =~ s/\s+$//;
  294.         }
  295.         if ( ( $network_conf{$section}{'named'} eq 'enabled' )
  296.           && ( exists( $network_conf{$section}{'type'} ) ) ) {
  297.             if ( lc($network_conf{$section}{'type'}) eq 'isolation' ) {
  298.                 my $isolation_obj = new Net::Netmask( $section,
  299.                     $network_conf{$section}{'netmask'} );
  300.                 push @routed_isolation_nets_named, $isolation_obj;
  301.             } elsif ( lc($network_conf{$section}{'type'}) eq 'registration' ) {
  302.                 my $registration_obj = new Net::Netmask( $section,
  303.                     $network_conf{$section}{'netmask'} );
  304.                 push @routed_registration_nets_named, $registration_obj;
  305.             }
  306.         }
  307.     }
  308.  
  309.     $tags{'registration_clients'} = "";
  310.     foreach my $net ( @routed_registration_nets_named ) {
  311.         $tags{'registration_clients'} .= $net . "; ";
  312.     }
  313.     $tags{'isolation_clients'} = "";
  314.     foreach my $net ( @routed_isolation_nets_named ) {
  315.         $tags{'isolation_clients'} .= $net . "; ";
  316.     }
  317.     parse_template(
  318.         \%tags,
  319.         "$conf_dir/templates/named_vlan.conf",
  320.         "$install_dir/conf/named.conf"
  321.     );
  322.  
  323.     my %tags_isolation;
  324.     $tags_isolation{'template'} = "$conf_dir/templates/named-isolation.ca";
  325.     $tags_isolation{'hostname'} = $Config{'general'}{'hostname'};
  326.     $tags_isolation{'incharge'}
  327.         = "pf."
  328.         . $Config{'general'}{'hostname'} . "."
  329.         . $Config{'general'}{'domain'};
  330.     parse_template(
  331.         \%tags_isolation,
  332.         "$conf_dir/templates/named-isolation.ca",
  333.         "$install_dir/conf/named/named-isolation.ca"
  334.     );
  335.  
  336.     my %tags_registration;
  337.     $tags_registration{'template'}
  338.         = "$conf_dir/templates/named-registration.ca";
  339.     $tags_registration{'hostname'} = $Config{'general'}{'hostname'};
  340.     $tags_registration{'incharge'}
  341.         = "pf."
  342.         . $Config{'general'}{'hostname'} . "."
  343.         . $Config{'general'}{'domain'};
  344.     parse_template(
  345.         \%tags_registration,
  346.         "$conf_dir/templates/named-registration.ca",
  347.         "$install_dir/conf/named/named-registration.ca"
  348.     );
  349.  
  350.     return 1;
  351. }
  352.  
  353. =item * generate_dhcpd_vlan_conf
  354.  
  355. =cut
  356.  
  357. sub generate_dhcpd_vlan_conf {
  358.     my $logger = Log::Log4perl::get_logger('pf::services');
  359.     my %tags;
  360.     $tags{'template'} = "$conf_dir/templates/dhcpd_vlan.conf";
  361.     $tags{'networks'} = '';
  362.  
  363.     my %network_conf;
  364.     tie %network_conf, 'Config::IniFiles',
  365.         ( -file => "$conf_dir/networks.conf", -allowempty => 1 );
  366.     my @errors = @Config::IniFiles::errors;
  367.     if ( scalar(@errors) ) {
  368.         $logger->error(
  369.             "Error reading networks.conf: " . join( "\n", @errors ) . "\n" );
  370.         return 0;
  371.     }
  372.     foreach my $section ( tied(%network_conf)->Sections ) {
  373.         foreach my $key ( keys %{ $network_conf{$section} } ) {
  374.             $network_conf{$section}{$key} =~ s/\s+$//;
  375.         }
  376.         if ( $network_conf{$section}{'dhcpd'} eq 'enabled' ) {
  377.             $tags{'networks'} .= <<"EOT";
  378. subnet $section netmask $network_conf{$section}{'netmask'} {
  379.   option routers $network_conf{$section}{'gateway'};
  380.   option subnet-mask $network_conf{$section}{'netmask'};
  381.   option domain-name "$network_conf{$section}{'domain-name'}";
  382.   option domain-name-servers $network_conf{$section}{'dns'};
  383.   range $network_conf{$section}{'dhcp_start'} $network_conf{$section}{'dhcp_end'};
  384.   default-lease-time $network_conf{$section}{'dhcp_default_lease_time'};
  385.   max-lease-time $network_conf{$section}{'dhcp_max_lease_time'};
  386. }
  387.  
  388. EOT
  389.         }
  390.     }
  391.  
  392.     parse_template( \%tags, "$conf_dir/templates/dhcpd_vlan.conf",
  393.         "$conf_dir/dhcpd.conf" );
  394.  
  395.     return 1;
  396. }
  397.  
  398. =item * generate_dhcpd_conf
  399.  
  400. =cut
  401.  
  402. sub generate_dhcpd_conf {
  403.     if ( $Config{'network'}{'mode'} =~ /vlan/i ) {
  404.         generate_dhcpd_vlan_conf();
  405.         return;
  406.     }
  407.     my %tags;
  408.     my $logger = Log::Log4perl::get_logger('pf::services');
  409.     $tags{'template'}   = "$conf_dir/templates/dhcpd.conf";
  410.     $tags{'domain'}     = $Config{'general'}{'domain'};
  411.     $tags{'hostname'}   = $Config{'general'}{'hostname'};
  412.     $tags{'dnsservers'} = $Config{'general'}{'dnsservers'};
  413.  
  414.     parse_template( \%tags, "$conf_dir/templates/dhcpd.conf",
  415.         "$conf_dir/dhcpd.conf" );
  416.  
  417.     my %shared_nets;
  418.     $logger->info("generating $conf_dir/dhcpd.conf");
  419.     foreach my $dhcp ( tied(%Config)->GroupMembers("dhcp") ) {
  420.         my @registered_scopes;
  421.         my @unregistered_scopes;
  422.         my @isolation_scopes;
  423.  
  424.         if ( defined( $Config{$dhcp}{'registered_scopes'} ) ) {
  425.             @registered_scopes
  426.                 = split( /\s*,\s*/, $Config{$dhcp}{'registered_scopes'} );
  427.         }
  428.         if ( defined( $Config{$dhcp}{'unregistered_scopes'} ) ) {
  429.             @unregistered_scopes
  430.                 = split( /\s+/, $Config{$dhcp}{'unregistered_scopes'} );
  431.         }
  432.         if ( defined( $Config{$dhcp}{'isolation_scopes'} ) ) {
  433.             @isolation_scopes
  434.                 = split( /\s+/, $Config{$dhcp}{'isolation_scopes'} );
  435.         }
  436.  
  437.         foreach my $registered_scope (@registered_scopes) {
  438.             my $reg_obj = new Net::Netmask(
  439.                 $Config{ 'scope ' . $registered_scope }{'network'} );
  440.             $reg_obj->tag( "scope", $registered_scope );
  441.             foreach my $shared_net ( keys(%shared_nets) ) {
  442.                 if ( $shared_net ne $dhcp
  443.                     && defined(
  444.                         $shared_nets{$shared_net}{ $reg_obj->desc() } ) )
  445.                 {
  446.                     $logger->logcroak( "Network "
  447.                             . $reg_obj->desc()
  448.                             . " is defined in another shared-network!\n" );
  449.                 }
  450.             }
  451.             push(
  452.                 @{ $shared_nets{$dhcp}{ $reg_obj->desc() }{'registered'} },
  453.                 $reg_obj
  454.             );
  455.         }
  456.         foreach my $isolation_scope (@isolation_scopes) {
  457.             my $iso_obj = new Net::Netmask(
  458.                 $Config{ 'scope ' . $isolation_scope }{'network'} );
  459.             $iso_obj->tag( "scope", $isolation_scope );
  460.             foreach my $shared_net ( keys(%shared_nets) ) {
  461.                 if ( $shared_net ne $dhcp
  462.                     && defined(
  463.                         $shared_nets{$shared_net}{ $iso_obj->desc() } ) )
  464.                 {
  465.                     $logger->logcroak( "Network "
  466.                             . $iso_obj->desc()
  467.                             . " is defined in another shared-network!\n" );
  468.                 }
  469.             }
  470.             push(
  471.                 @{ $shared_nets{$dhcp}{ $iso_obj->desc() }{'isolation'} },
  472.                 $iso_obj
  473.             );
  474.         }
  475.         foreach my $unregistered_scope (@unregistered_scopes) {
  476.             my $unreg_obj = new Net::Netmask(
  477.                 $Config{ 'scope ' . $unregistered_scope }{'network'} );
  478.             $unreg_obj->tag( "scope", $unregistered_scope );
  479.             foreach my $shared_net ( keys(%shared_nets) ) {
  480.                 if ($shared_net ne $dhcp
  481.                     && defined(
  482.                         $shared_nets{$shared_net}{ $unreg_obj->desc() }
  483.                     )
  484.                     )
  485.                 {
  486.                     $logger->logcroak( "Network "
  487.                             . $unreg_obj->desc()
  488.                             . " is defined in another shared-network!\n" );
  489.                 }
  490.             }
  491.             push(
  492.                 @{  $shared_nets{$dhcp}{ $unreg_obj->desc() }{'unregistered'}
  493.                     },
  494.                 $unreg_obj
  495.             );
  496.         }
  497.     }
  498.  
  499.     #open dhcpd.conf file
  500.     my $dhcpdconf_fh;
  501.     open( $dhcpdconf_fh, '>>', "$conf_dir/dhcpd.conf" )
  502.         || $logger->logcroak("Unable to append to $conf_dir/dhcpd.conf: $!");
  503.     foreach my $internal_interface ( get_internal_devs_phy() ) {
  504.         my $dhcp_interface = get_internal_info($internal_interface);
  505.         print {$dhcpdconf_fh} "subnet "
  506.             . $dhcp_interface->base()
  507.             . " netmask "
  508.             . $dhcp_interface->mask()
  509.             . " {\n  not authoritative;\n}\n";
  510.     }
  511.     foreach my $shared_net ( keys(%shared_nets) ) {
  512.         my $printable_shared = $shared_net;
  513.         $printable_shared =~ s/dhcp //;
  514.         print {$dhcpdconf_fh} "shared-network $printable_shared {\n";
  515.         foreach my $key ( keys( %{ $shared_nets{$shared_net} } ) ) {
  516.             my $tmp_obj = new Net::Netmask($key);
  517.             print {$dhcpdconf_fh} "  subnet "
  518.                 . $tmp_obj->base()
  519.                 . " netmask "
  520.                 . $tmp_obj->mask() . " {\n";
  521.  
  522.             if (defined( @{ $shared_nets{$shared_net}{$key}{'registered'} } )
  523.                 )
  524.             {
  525.                 foreach my $reg (
  526.                     @{ $shared_nets{$shared_net}{$key}{'registered'} } )
  527.                 {
  528.  
  529.                     my $range = normalize_dhcpd_range(
  530.                         $Config{ 'scope ' . $reg->tag("scope") }{'range'} );
  531.                     if ( !$range ) {
  532.                         $logger->logcroak( "Invalid scope range: "
  533.                                 . $Config{ 'scope ' . $reg->tag("scope") }
  534.                                 {'range'} );
  535.                     }
  536.                     print {$dhcpdconf_fh} "    pool {\n";
  537.                     print {$dhcpdconf_fh} "      # I AM A REGISTERED SCOPE\n";
  538.                     print {$dhcpdconf_fh} "      deny unknown clients;\n";
  539.                     print {$dhcpdconf_fh}
  540.                         "      allow members of \"registered\";\n";
  541.                     print {$dhcpdconf_fh} "      option routers "
  542.                         . $Config{ 'scope ' . $reg->tag("scope") }{'gateway'}
  543.                         . ";\n";
  544.  
  545.                     my $lease_time;
  546.                     if ( defined( $Config{$shared_net}{'registered_lease'} ) )
  547.                     {
  548.                         $lease_time
  549.                             = $Config{$shared_net}{'registered_lease'};
  550.                     } else {
  551.                         $lease_time = 7200;
  552.                     }
  553.  
  554.                     print {$dhcpdconf_fh}
  555.                         "      max-lease-time $lease_time;\n";
  556.                     print {$dhcpdconf_fh}
  557.                         "      default-lease-time $lease_time;\n";
  558.                     print {$dhcpdconf_fh} "      range $range;\n";
  559.                     print {$dhcpdconf_fh} "    }\n";
  560.                 }
  561.             }
  562.  
  563.             if (defined( @{ $shared_nets{$shared_net}{$key}{'isolation'} } ) )
  564.             {
  565.                 foreach my $iso (
  566.                     @{ $shared_nets{$shared_net}{$key}{'isolation'} } )
  567.                 {
  568.  
  569.                     my $range = normalize_dhcpd_range(
  570.                         $Config{ 'scope ' . $iso->tag("scope") }{'range'} );
  571.                     if ( !$range ) {
  572.                         $logger->logcroak( "Invalid scope range: "
  573.                                 . $Config{ 'scope ' . $iso->tag("scope") }
  574.                                 {'range'} );
  575.                     }
  576.  
  577.                     print {$dhcpdconf_fh} "    pool {\n";
  578.                     print {$dhcpdconf_fh} "      # I AM AN ISOLATION SCOPE\n";
  579.                     print {$dhcpdconf_fh} "      deny unknown clients;\n";
  580.                     print {$dhcpdconf_fh}
  581.                         "      allow members of \"isolated\";\n";
  582.                     print {$dhcpdconf_fh} "      option routers "
  583.                         . $Config{ 'scope ' . $iso->tag("scope") }{'gateway'}
  584.                         . ";\n";
  585.  
  586.                     my $lease_time;
  587.                     if ( defined( $Config{$shared_net}{'isolation_lease'} ) )
  588.                     {
  589.                         $lease_time = $Config{$shared_net}{'isolation_lease'};
  590.                     } else {
  591.                         $lease_time = 120;
  592.                     }
  593.  
  594.                     print {$dhcpdconf_fh}
  595.                         "      max-lease-time $lease_time;\n";
  596.                     print {$dhcpdconf_fh}
  597.                         "      default-lease-time $lease_time;\n";
  598.                     print {$dhcpdconf_fh} "      range $range;\n";
  599.                     print {$dhcpdconf_fh} "    }\n";
  600.                 }
  601.             }
  602.  
  603.             if (defined(
  604.                     @{ $shared_nets{$shared_net}{$key}{'unregistered'} }
  605.                 )
  606.                 )
  607.             {
  608.                 foreach my $unreg (
  609.                     @{ $shared_nets{$shared_net}{$key}{'unregistered'} } )
  610.                 {
  611.  
  612.                     my $range = normalize_dhcpd_range(
  613.                         $Config{ 'scope ' . $unreg->tag("scope") }{'range'} );
  614.                     if ( !$range ) {
  615.                         $logger->logcroak( "Invalid scope range: "
  616.                                 . $Config{ 'scope ' . $unreg->tag("scope") }
  617.                                 {'range'} );
  618.                     }
  619.  
  620.                     print {$dhcpdconf_fh} "    pool {\n";
  621.                     print {$dhcpdconf_fh}
  622.                         "      # I AM AN UNREGISTERED SCOPE\n";
  623.                     print {$dhcpdconf_fh} "      allow unknown clients;\n";
  624.                     print {$dhcpdconf_fh} "      option routers "
  625.                         . $Config{ 'scope ' . $unreg->tag("scope") }
  626.                         {'gateway'} . ";\n";
  627.  
  628.                     my $lease_time;
  629.                     if (defined( $Config{$shared_net}{'unregistered_lease'} )
  630.                         )
  631.                     {
  632.                         $lease_time
  633.                             = $Config{$shared_net}{'unregistered_lease'};
  634.                     } else {
  635.                         $lease_time = 120;
  636.                     }
  637.  
  638.                     print {$dhcpdconf_fh}
  639.                         "      max-lease-time $lease_time;\n";
  640.                     print {$dhcpdconf_fh}
  641.                         "      default-lease-time $lease_time;\n";
  642.                     print {$dhcpdconf_fh} "      range $range;\n";
  643.                     print {$dhcpdconf_fh} "    }\n";
  644.                 }
  645.             }
  646.  
  647.             print {$dhcpdconf_fh} "  }\n";
  648.         }
  649.         print {$dhcpdconf_fh} "}\n";
  650.     }
  651.     print {$dhcpdconf_fh} "include \"$conf_dir/isolated.mac\";\n";
  652.     print {$dhcpdconf_fh} "include \"$conf_dir/registered.mac\";\n";
  653.     close $dhcpdconf_fh;
  654.  
  655.     #close(DHCPDCONF);
  656.  
  657.     generate_dhcpd_iso();
  658.     generate_dhcpd_reg();
  659.  
  660.     return 1;
  661. }
  662.  
  663. =item * generate_dhcpd_iso
  664.  
  665. open isolated.mac file
  666.  
  667. =cut
  668.  
  669. sub generate_dhcpd_iso {
  670.     my $logger = Log::Log4perl::get_logger('pf::services');
  671.     my $isomac_fh;
  672.     open( $isomac_fh, '>', "$conf_dir/isolated.mac" )
  673.         || $logger->logcroak("Unable to open $conf_dir/isolated.mac : $!");
  674.     my @isolated = violation_view_open_uniq();
  675.     my @isolatednodes;
  676.     foreach my $row (@isolated) {
  677.         my $mac      = $row->{'mac'};
  678.         my $hostname = $mac;
  679.         $hostname =~ s/://g;
  680.         print {$isomac_fh}
  681.             "host $hostname { hardware ethernet $mac; } subclass \"isolated\" 01:$mac;";
  682.     }
  683.  
  684.     close( $isomac_fh );
  685.     return 1;
  686. }
  687.  
  688. =item * generate_dhcpd_reg
  689.  
  690. open registered.mac file
  691.  
  692. =cut
  693.  
  694. sub generate_dhcpd_reg {
  695.     my $logger = Log::Log4perl::get_logger('pf::services');
  696.     if ( isenabled( $Config{'trapping'}{'registration'} ) ) {
  697.         my $regmac_fh;
  698.         open( $regmac_fh, '>', "$conf_dir/registered.mac" )
  699.             || $logger->logcroak(
  700.             "Unable to open $conf_dir/registered.mac : $!");
  701.         my @registered = nodes_registered_not_violators();
  702.         my @registerednodes;
  703.         foreach my $row (@registered) {
  704.             my $mac      = $row->{'mac'};
  705.             my $hostname = $mac;
  706.             $hostname =~ s/://g;
  707.             print {$regmac_fh}
  708.                 "host $hostname { hardware ethernet $mac; } subclass \"registered\" 01:$mac;";
  709.         }
  710.  
  711.         close( $regmac_fh );
  712.     }
  713.     return 1;
  714. }
  715.  
  716. =item * generate_snort_conf
  717.  
  718. =cut
  719.  
  720. sub generate_snort_conf {
  721.     my $logger = Log::Log4perl::get_logger('pf::services');
  722.     my %tags;
  723.     $tags{'template'}      = "$conf_dir/templates/snort.conf";
  724.     $tags{'internal-ips'}  = join( ",", get_internal_ips() );
  725.     $tags{'internal-nets'} = join( ",", get_internal_nets() );
  726.     $tags{'gateways'}      = join( ",", get_gateways() );
  727.     $tags{'dhcp_servers'}  = $Config{'general'}{'dhcpservers'};
  728.     $tags{'dns_servers'}   = $Config{'general'}{'dnsservers'};
  729.     $tags{'install_dir'}   = $install_dir;
  730.     my %violations_conf;
  731.     tie %violations_conf, 'Config::IniFiles',
  732.         ( -file => "$conf_dir/violations.conf" );
  733.     my @errors = @Config::IniFiles::errors;
  734.     if ( scalar(@errors) ) {
  735.         $logger->error( "Error reading violations.conf: "
  736.                         .  join( "\n", @errors ) . "\n" );
  737.         return 0;
  738.     }
  739.  
  740.     my @rules;
  741.  
  742.     foreach my $rule (
  743.         split( /\s*,\s*/, $violations_conf{'defaults'}{'snort_rules'} ) )
  744.     {
  745.  
  746.         #append install_dir if the path doesn't start with /
  747.         $rule = "\$RULE_PATH/$rule" if ( $rule !~ /^\// );
  748.         push @rules, "include $rule";
  749.     }
  750.     $tags{'snort_rules'} = join( "\n", @rules );
  751.     $logger->info("generating $conf_dir/snort.conf");
  752.     parse_template( \%tags, "$conf_dir/templates/snort.conf",
  753.         "$conf_dir/snort.conf" );
  754.     return 1;
  755. }
  756.  
  757. =item * generate_snmptrapd_conf
  758.  
  759. =cut
  760.  
  761. sub generate_snmptrapd_conf {
  762.     my $logger = Log::Log4perl::get_logger('pf::services');
  763.     my %tags;
  764.     $tags{'authLines'} = '';
  765.     $tags{'userLines'} = '';
  766.     my %SNMPv3Users;
  767.     my %SNMPCommunities;
  768.     my $switchFactory
  769.         = new pf::SwitchFactory( -configFile => "$conf_dir/switches.conf" );
  770.     my %switchConfig = %{ $switchFactory->{_config} };
  771.  
  772.     foreach my $key ( sort keys %switchConfig ) {
  773.         if ( $key ne 'default' ) {
  774.             my $switch = $switchFactory->instantiate($key);
  775.             if ( $switch->{_SNMPVersionTrap} eq '3' ) {
  776.                 $SNMPv3Users{ $switch->{_SNMPUserNameTrap} }
  777.                     = '-e ' . $switch->{_SNMPEngineID} . ' '
  778.                     . $switch->{_SNMPUserNameTrap} . ' '
  779.                     . $switch->{_SNMPAuthProtocolTrap} . ' '
  780.                     . $switch->{_SNMPAuthPasswordTrap} . ' '
  781.                     . $switch->{_SNMPPrivProtocolTrap} . ' '
  782.                     . $switch->{_SNMPPrivPasswordTrap};
  783.             } else {
  784.                 $SNMPCommunities{ $switch->{_SNMPCommunityTrap} } = 1;
  785.             }
  786.         }
  787.     }
  788.     foreach my $userName ( sort keys %SNMPv3Users ) {
  789.         $tags{'userLines'}
  790.             .= "createUser " . $SNMPv3Users{$userName} . "\n";
  791.         $tags{'authLines'} .= "authUser log $userName priv\n";
  792.     }
  793.     foreach my $community ( sort keys %SNMPCommunities ) {
  794.         $tags{'authLines'} .= "authCommunity log $community\n";
  795.     }
  796.     $tags{'template'} = "$conf_dir/templates/snmptrapd.conf";
  797.     $logger->info("generating $conf_dir/snmptrapd.conf");
  798.     parse_template( \%tags, "$conf_dir/templates/snmptrapd.conf",
  799.         "$conf_dir/snmptrapd.conf" );
  800.     return 1;
  801. }
  802.  
  803. =item * generate_httpd_conf
  804.  
  805. =cut
  806.  
  807. sub generate_httpd_conf {
  808.     my ( %tags, $httpdconf_fh, $authconf_fh );
  809.     my $logger = Log::Log4perl::get_logger('pf::services');
  810.     $tags{'template'}      = "$conf_dir/templates/httpd.conf";
  811.     $tags{'internal-nets'} = join( " ", get_internal_nets() );
  812.     $tags{'routed-nets'}   = join( " ", get_routed_isolation_nets() ) . " "
  813.         . join( " ", get_routed_registration_nets() );
  814.     $tags{'hostname'}    = $Config{'general'}{'hostname'};
  815.     $tags{'domain'}      = $Config{'general'}{'domain'};
  816.     $tags{'admin_port'}  = $Config{'ports'}{'admin'};
  817.     $tags{'install_dir'} = $install_dir;
  818.  
  819.     my @proxies;
  820.     my %proxy_configs = %{ $Config{'proxies'} };
  821.     foreach my $proxy ( keys %proxy_configs ) {
  822.         if ( $proxy =~ /^\// ) {
  823.             if ( $proxy !~ /^\/(content|admin|redirect|cgi-bin)/ ) {
  824.                 push @proxies,
  825.                     "ProxyPassReverse $proxy $proxy_configs{$proxy}";
  826.                 push @proxies, "ProxyPass $proxy $proxy_configs{$proxy}";
  827.                 $logger->warn(
  828.                     "proxy $proxy is not relative - add path to apache rewrite exclude list!"
  829.                 );
  830.             } else {
  831.                 $logger->warn("proxy $proxy conflicts with PF paths!");
  832.                 next;
  833.             }
  834.         } else {
  835.             push @proxies,
  836.                   "ProxyPassReverse /proxies/"
  837.                 . $proxy . " "
  838.                 . $proxy_configs{$proxy};
  839.             push @proxies,
  840.                 "ProxyPass /proxies/" . $proxy . " " . $proxy_configs{$proxy};
  841.         }
  842.     }
  843.     $tags{'proxies'} = join( "\n", @proxies );
  844.  
  845.     my @contentproxies;
  846.     if ( $Config{'trapping'}{'passthrough'} eq "proxy" ) {
  847.         my @proxies = class_view_all();
  848.         foreach my $row (@proxies) {
  849.             my $url = $row->{'url'};
  850.             my $vid = $row->{'vid'};
  851.             next if ( ( !defined($url) ) || ( $url =~ /^\// ) );
  852.             if ( $url !~ /^(http|https):\/\// ) {
  853.                 $logger->warn(
  854.                     "vid " . $vid . ": unrecognized content URL: " . $url );
  855.                 next;
  856.             }
  857.             if ( $url =~ /^((http|https):\/\/.+)\/$/ ) {
  858.                 push @contentproxies, "ProxyPass        /content/$vid/ $url";
  859.                 push @contentproxies, "ProxyPassReverse /content/$vid/ $url";
  860.                 push @contentproxies, "ProxyHTMLURLMap      $1 /content/$vid";
  861.             } else {
  862.                 $url =~ /^((http|https):\/\/.+)\//;
  863.                 push @contentproxies, "ProxyPass        /content/$vid/ $1/";
  864.                 push @contentproxies, "ProxyPassReverse /content/$vid/ $1/";
  865.                 push @contentproxies, "ProxyHTMLURLMap      $url /content/$vid";
  866.             }
  867.             push @contentproxies, "ProxyPass        /content/$vid $url";
  868.             push @contentproxies, "<Location /content/$vid>";
  869.             push @contentproxies, "  SetOutputFilter    proxy-html";
  870.             push @contentproxies, "  ProxyHTMLDoctype   HTML";
  871.             push @contentproxies, "  ProxyHTMLURLMap    / /content/$vid/";
  872.             push @contentproxies,
  873.                 "  ProxyHTMLURLMap  /content/$vid /content/$vid";
  874.             push @contentproxies, "  RequestHeader  unset   Accept-Encoding";
  875.             push @contentproxies, "</Location>";
  876.         }
  877.     }
  878.     $tags{'content-proxies'} = join( "\n", @contentproxies );
  879.  
  880.     $logger->info("generating $conf_dir/httpd.conf");
  881.     parse_template( \%tags, "$conf_dir/templates/httpd.conf",
  882.         "$conf_dir/httpd.conf" );
  883.     return 1;
  884. }
  885.  
  886. =item * switches_conf_is_valid
  887.  
  888. =cut
  889.  
  890. sub switches_conf_is_valid {
  891.     my $logger = Log::Log4perl::get_logger('pf::services');
  892.     my %switches_conf;
  893.     tie %switches_conf, 'Config::IniFiles',
  894.         ( -file => "$conf_dir/switches.conf" );
  895.     my @errors = @Config::IniFiles::errors;
  896.     if ( scalar(@errors) ) {
  897.         $logger->error(
  898.             "Error reading switches.conf: " . join( "\n", @errors ) . "\n" );
  899.         return 0;
  900.     }
  901.     foreach my $section ( tied(%switches_conf)->Sections ) {
  902.         foreach my $key ( keys %{ $switches_conf{$section} } ) {
  903.             $switches_conf{$section}{$key} =~ s/\s+$//;
  904.         }
  905.     }
  906.     foreach my $section ( keys %switches_conf ) {
  907.         if ( ( $section ne 'default' )
  908.             && ( $section ne '127.0.0.1' ) ) {
  909.  
  910.             # check type
  911.             my $type
  912.                 = "pf::SNMP::"
  913.                 . (    $switches_conf{$section}{'type'}
  914.                     || $switches_conf{'default'}{'type'} );
  915.             if ( ! $type->require() ) {
  916.                 $logger->error(
  917.                     "Unknown switch type: $type for switch $section: $@");
  918.                 return 0;
  919.             }
  920.  
  921.             if ( !valid_ip($section) ) {
  922.                 $logger->error("switch IP is invalid for $section");
  923.                 return 0;
  924.             }
  925.  
  926.             # check SNMP version
  927.             my $SNMPVersion
  928.                 = (    $switches_conf{$section}{'SNMPVersion'}
  929.                     || $switches_conf{$section}{'version'}
  930.                     || $switches_conf{'default'}{'SNMPVersion'}
  931.                     || $switches_conf{'default'}{'version'} );
  932.             if ( !( $SNMPVersion =~ /^1|2c|3$/ ) ) {
  933.                 $logger->error("switch SNMP version is invalid for $section");
  934.                 return 0;
  935.             }
  936.             my $SNMPVersionTrap
  937.                 = (    $switches_conf{$section}{'SNMPVersionTrap'}
  938.                     || $switches_conf{'default'}{'SNMPVersionTrap'} );
  939.             if ( !( $SNMPVersionTrap =~ /^1|2c|3$/ ) ) {
  940.                 $logger->error(
  941.                     "switch SNMP trap version is invalid for $section");
  942.                 return 0;
  943.             }
  944.  
  945.             # check uplink
  946.             my $uplink = $switches_conf{$section}{'uplink'}
  947.                 || $switches_conf{'default'}{'uplink'};
  948.             if (( !defined($uplink) )
  949.                 || (   ( $uplink ne 'dynamic' )
  950.                     && ( !( $uplink =~ /(\d+,)*\d+/ ) ) )
  951.                 )
  952.             {
  953.                 $logger->error( "switch uplink ("
  954.                         . ( defined($uplink) ? $uplink : 'undefined' )
  955.                         . ") is invalid for $section" );
  956.                 return 0;
  957.             }
  958.  
  959.             # check mode
  960.             my @valid_switch_modes = (
  961.                 'testing', 'ignore', 'production', 'registration',
  962.                 'discovery'
  963.             );
  964.             my $mode = $switches_conf{$section}{'mode'}
  965.                 || $switches_conf{'default'}{'mode'};
  966.             if ( !grep( { lc($_) eq lc($mode) } @valid_switch_modes ) ) {
  967.                 $logger->error("switch mode ($mode) is invalid for $section");
  968.                 return 0;
  969.             }
  970.         }
  971.     }
  972.     return 1;
  973. }
  974.  
  975. =item * read_violations_conf
  976.  
  977. =cut
  978.  
  979. sub read_violations_conf {
  980.     my $logger = Log::Log4perl::get_logger('pf::services');
  981.     my %violations_conf;
  982.     tie %violations_conf, 'Config::IniFiles',
  983.         ( -file => "$conf_dir/violations.conf" );
  984.     my @errors = @Config::IniFiles::errors;
  985.     if ( scalar(@errors) ) {
  986.         $logger->error( "Error reading violations.conf: "
  987.                         .  join( "\n", @errors ) . "\n" );
  988.         return 0;
  989.     }
  990.     my %violations = class_set_defaults(%violations_conf);
  991.  
  992.     #clear all triggers at startup
  993.     trigger_delete_all();
  994.     foreach my $violation ( keys %violations ) {
  995.  
  996.         # parse triggers if they exist
  997.         my @triggers;
  998.         if ( defined $violations{$violation}{'trigger'} ) {
  999.             foreach my $trigger (
  1000.                 split( /\s*,\s*/, $violations{$violation}{'trigger'} ) )
  1001.             {
  1002.                 my ( $type, $tid ) = split( /::/, $trigger );
  1003.                 $type = lc($type);
  1004.                 if ( !grep( { lc($_) eq lc($type) } @valid_trigger_types ) ) {
  1005.                     $logger->warn(
  1006.                         "invalid trigger '$type' found at $violation");
  1007.                     next;
  1008.                 }
  1009.                 if ( $tid =~ /(\d+)-(\d+)/ ) {
  1010.                     push @triggers, [ $1, $2, $type ];
  1011.                 } else {
  1012.                     push @triggers, [ $tid, $tid, $type ];
  1013.                 }
  1014.             }
  1015.         }
  1016.  
  1017.         #print Dumper(@triggers);
  1018.         class_merge(
  1019.             $violation,
  1020.             $violations{$violation}{'desc'},
  1021.             $violations{$violation}{'auto_enable'},
  1022.             $violations{$violation}{'max_enable'},
  1023.             $violations{$violation}{'grace'},
  1024.             $violations{$violation}{'priority'},
  1025.             $violations{$violation}{'url'},
  1026.             $violations{$violation}{'max_enable_url'},
  1027.             $violations{$violation}{'redirect_url'},
  1028.             $violations{$violation}{'button_text'},
  1029.             $violations{$violation}{'disable'},
  1030.             $violations{$violation}{'vlan'},
  1031.             # actions are expected to be in this position (handled in a special way)
  1032.             $violations{$violation}{'actions'},
  1033.             \@triggers
  1034.         );
  1035.     }
  1036.     return 1;
  1037. }
  1038.  
  1039. =item * class_set_defaults
  1040.  
  1041. =cut
  1042.  
  1043. sub class_set_defaults {
  1044.     my %violations_conf = @_;
  1045.     my %violations      = %violations_conf;
  1046.  
  1047.     foreach my $violation ( keys %violations_conf ) {
  1048.         foreach my $default ( keys %{ $violations_conf{'defaults'} } ) {
  1049.             if ( !defined( $violations{$violation}{$default} ) ) {
  1050.                 $violations{$violation}{$default}
  1051.                     = $violations{'defaults'}{$default};
  1052.             }
  1053.         }
  1054.     }
  1055.     delete( $violations{'defaults'} );
  1056.     return (%violations);
  1057. }
  1058.  
  1059. =item * normalize_dhcpd_range
  1060.  
  1061. =cut
  1062.  
  1063. sub normalize_dhcpd_range {
  1064.     my ($range) = @_;
  1065.     if ( $range
  1066.         =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s*-\s*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
  1067.         )
  1068.     {
  1069.         $range =~ s/\s*\-\s*/ /;
  1070.         return ($range);
  1071.     } elsif (
  1072.         $range =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3})\.(\d{1,3})\s*-\s*(\d{1,3})$/ )
  1073.     {
  1074.         my $net   = $1;
  1075.         my $start = $2;
  1076.         my $end   = $3;
  1077.         return ("$net.$start $net.$end");
  1078.     } elsif ( $range =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ ) {
  1079.         return ("$range $range");
  1080.     } else {
  1081.         return;
  1082.     }
  1083. }
  1084.  
  1085. =back
  1086.  
  1087. =head1 AUTHOR
  1088.  
  1089. David LaPorte <david@davidlaporte.org>
  1090.  
  1091. Kevin Amorin <kev@amorin.org>
  1092.  
  1093. Olivier Bilodeau <obilodeau@inverse.ca>
  1094.  
  1095. =head1 COPYRIGHT
  1096.  
  1097. Copyright (C) 2005 David LaPorte
  1098.  
  1099. Copyright (C) 2005 Kevin Amorin
  1100.  
  1101. Copyright (C) 2009 Inverse inc.
  1102.  
  1103. This program is free software; you can redistribute it and/or
  1104. modify it under the terms of the GNU General Public License
  1105. as published by the Free Software Foundation; either version 2
  1106. of the License, or (at your option) any later version.
  1107.  
  1108. This program is distributed in the hope that it will be useful,
  1109. but WITHOUT ANY WARRANTY; without even the implied warranty of
  1110. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  1111. GNU General Public License for more details.
  1112.  
  1113. You should have received a copy of the GNU General Public License
  1114. along with this program; if not, write to the Free Software
  1115. Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
  1116. USA.
  1117.  
  1118. =cut
  1119.  
  1120. 1;
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement