#! /usr/bin/perl
# IBM(c) 2013 EPL license http://www.eclipse.org/legal/epl-v10.html
# This program will start a proxydhcp daemon to handle 4011 request

use Sys::Syslog;
use IO::Socket::INET;
use IO::Select;
use Getopt::Long;

my $quit = 0;
my $doreload = 0;
my %nodecfg;
my $bootpmagic = pack("C*", 0x63, 0x82, 0x53, 0x63);

# set signal handler to set flag to reload configuration file
$SIG{USR1} = sub {
    $doreload = 1;
};
$SIG{TERM} = $SIG{INT} = sub {
    $quit = 1;
};

my $verbose;
my $tobedaemon;
Getopt::Long::Configure("bundling");
Getopt::Long::Configure("pass_through");
GetOptions(
    'V' => \$verbose,
    'd' => \$tobedaemon,
);

if ($tobedaemon) {
    daemonize();
}

# open syslog 
openlog("proxydhcp", "nofatal", "local4");

my $socket;
my $retry = 5;
while ($retry > 0) { 
    $socket = IO::Socket::INET->new(LocalPort => 4011,
        Proto     => 'udp',
        Domain => AF_INET);
    if ($socket) {
        last;
    } else {
        sleep 1;
    }
    $retry--;
}
unless ($socket) {
    syslog ("info", "Unable to open socket on port 4011.");
    closelog(); 
    exit;
}

# regist my pid to /var/run/xcat/proxydhcp.pid
if (open (PIDF, ">/var/run/xcat/proxydhcp-xcat.pid")) {
    print PIDF $$;
    close(PIDF);
} else {
    syslog ("info", "Cannot open /var/run/xcat/proxydhcp.pid.");
    closelog(); 
    exit;
}


my $select = new IO::Select;
$select->add($socket);
load_cfg();
until ($quit) {	
    until ($select->can_read(5)) {  #Wait for data
        if ($doreload) {
            load_cfg();
            syslog ("info", "Reload configuration file in select.");
        }
        if ($quit) { last; }; 
        yield; 
    }

    if ($doreload) {
        load_cfg();
        syslog ("info", "Reload configuration file before recv.");
    }

    my $data;
    my $caddr = $socket->recv($data,1500);

    my ($cport, $cnip) = sockaddr_in($caddr);
    my $snip = my_ip_facing($cnip);
    unless ($snip) {
        syslog ("info", "Cannot find the server ip of proxydhcp daemon");
        next;
    }

    if (length ($data) < 320) {
        next;
    }

    my @package = unpack("C*", $data);
    my @replypkg;

    if (pack("C*", $package[0xec], $package[0xed], $package[0xee], $package[0xef]) != $bootpmagic) {
        next;
    } 

    # get the node name of client
    my $nodename = gethostbyaddr($cnip, AF_INET);
    
    # get the winpepath
    my $winpepath = "";
    if ($nodename) {
        if (defined $nodecfg{$nodename}) {
            $winpepath = $nodecfg{$nodename};
            if ($verbose) {syslog ("info", "Find configure for $nodename= $nodecfg{$nodename} in configuration file")};
        } else {
            $nodename =~ s/\..*//;
            if (defined $nodecfg{$nodename}) {
                $winpepath = $nodecfg{$nodename};
                if ($verbose) {syslog ("info", "Find configure for $nodename= $nodecfg{$nodename} in configuration file")};
            }
        }
    }

    # get the Vendor class identifier
    my $strp = 0xf0;
    my $archp = 0;
    while ($strp < $#package) {
        if ($package[$strp] eq 60) {
            $archp = $strp + 0x11;
            last;
        } else {
            $strp += $package[$strp+1] + 2;
        }
    }

    # get the winpe boot loader path 
    my $winboot = $winpepath."Boot/pxeboot.0";
    if ($archp) {
        my $clienttype = substr($data, $archp, 5);
        if ($clienttype == "00000") {
        #if ("$package[$archp]$package[$archp+1]$package[$archp+2]$package[$archp+3]$package[$archp+4]" == "00000") {
            $winboot = $winpepath."Boot/pxeboot.0";
        } elsif ($clienttype == "00007") {
            $winboot = $winpepath."Boot/bootmgfw.efi";
        }
    }

    syslog ("info", "The boot loader path for node $nodename is $winboot");

    # set message type
    $replypkg[0] = 2;

    # set the hardware type
    $replypkg[1] = $package[1];

    # set the hardware address length
    $replypkg[2] = $package[2];

    # set the hops
    $replypkg[3] = $package[3];

    # set the transaction ID
    @replypkg = (@replypkg, @package[4 .. 7]);

    # set elapsed time
    $replypkg[8] = 0;
    $replypkg[9] = 0;

    # set bootp flag
    $replypkg[0xa] = 0;
    $replypkg[0xb] = 0;

    # set client ip
    @replypkg = (@replypkg, @package[0xc .. 0xf]);

    # set Your (client IP)
    @replypkg = (@replypkg, 0, 0, 0, 0);

    #set Next server IP (set my IP)
    @replypkg = (@replypkg, unpack("C*", $snip));

    # set dhcp relay agent ip
    @replypkg = (@replypkg, 0, 0, 0, 0);

    # set client hardware address
    @replypkg = (@replypkg, @package[0x1c .. 0x2b]);

    # set server host name
    foreach (0x2c .. 0x6b) {
        $replypkg[$_] = 0;
    }

    # set the boot file name
    @replypkg = (@replypkg, unpack("C*", $winboot));
    my $lenth = length ($winboot);
    foreach (0x6c + $lenth .. 0xeb) {
        $replypkg[$_] = 0;
    }

    # add magic cookie
    #my @xx = unpack("C*", $bootpmagic);
    #@replypkg = (@replypkg, @xx);
    @replypkg = (@replypkg, unpack("C*", $bootpmagic));

    # set dhcp msg type
    $replypkg[0xf0] = 0x35; # option number
    $replypkg[0xf1] = 0x1; # msg length
    $replypkg[0xf2] = 0x5; # dhcp ack

    # set dhcp server identifer
    $replypkg[0xf3] = 0x36; # option number
    $replypkg[0xf4] = 0x4; # msg length
    @replypkg = (@replypkg, unpack("C*", $snip));

    # set the bcd path
    my $winbcd = $winpepath."Boot/BCD";
    $replypkg[0xf9] = 0xfc; # option number
    $replypkg[0xfa] = length($winbcd) + 1; # msg length
    @replypkg = (@replypkg, unpack("C*", $winbcd));
    $replypkg[0xfa + length($winbcd) + 1] = 0;
    $replypkg[0xfa + length($winbcd) + 2] = 0xff;
    
    $socket->send(pack("C*", @replypkg), 0, $caddr);

    syslog ("info", "The BCD path for node $nodename is $winbcd");

    # debug package detail
    if (0) {
    my $msg; 
    my $firstline = 1;
    my $num = 0;
    foreach (@replypkg) {
        my $v = sprintf("%2x ", $_);
        $msg .= $v;
        if (($num - 5)%8 eq 0) {
            $msg .= "   ";
        }
        if (($num - 5)%16 eq 0) {
            syslog ("info", $msg);
            print $msg."\n";
            $msg = "";
        }
        $num++;
    }
    print $msg."\n";
    }  
}

closelog(); 

# daemonize the service
sub daemonize
{
    chdir('/');
    umask 0022;
    my $pid = fork;

    if ($pid) {
        exit;
    }
    open STDOUT, '>/dev/null';
    open STDERR, '>/dev/null';
    $0='proxydhcp-xcat';
    $progname = \$0;
}

#  load configuration from /var/lib/xcat/proxydhcp.cfg to %nodecfg
sub load_cfg
{
    $doreload = 0;
    if (! -r "/var/lib/xcat/proxydhcp.cfg") {
        return;
    }

    if (! open (CFG, "</var/lib/xcat/proxydhcp.cfg")) {
        syslog ("info", "Cannot open /var/lib/xcat/proxydhcp.cfg");
        return;
    }

    my $mycfg;
    while (<CFG>) {
        $mycfg .= $_;
    }

    my $p = 0;
    while (1) {
    
        my $name = substr($mycfg, $p, 50); 
        $p += 50;
        my $value = substr($mycfg, $p, 150); 
        $p += 150;

        $name =~ s/\0//g;
        $value =~ s/\0//g;
        if ($name) {
            $nodecfg{$name} = $value;
        } else {
            return;
        }
    }


    close(CFG);
}

# get the ip in xCAT MN/SN which facing target ip
sub my_ip_facing
{
    my $peer = shift;
    unless ($peer) { return undef; }
    my $noden = unpack("N", $peer);
    my @nets = split /\n/, `/sbin/ip addr`;
    foreach (@nets)
    {
        my @elems = split /\s+/;
        unless (/^\s*inet\s/)
        {
            next;
        }
        (my $curnet, my $maskbits) = split /\//, $elems[2];
        my $curmask = 2**$maskbits - 1 << (32 - $maskbits);
        my $curn = unpack("N", inet_aton($curnet));
        if (($noden & $curmask) == ($curn & $curmask))
        {
            return inet_aton($curnet);
        }
    }
    return undef;
}