#!/usr/bin/perl5

# Manipulate ndbm database for DHCP

use Getopt::Std;
use Fcntl;
use NDBM_File;
use Socket;
use POSIX;

# globals
$dhcpDBFilename = "/var/dhcp/etherToIP";

sub lockDHCPdb {
    if (flock LOCKDB, $LOCK_EX != 0) {
	print "Can't lock file\n";
    }
}

sub unlockDHCPdb {
    if (flock LOCKDB, $LOCK_UN != 0) {
	print "Can't unlock file\n";
    }
}


sub usage {
    print "Entry mode usage: $0 -a | -d | -u | -p [-r] [-X]\n";
    print "\t-C cid -M mac_address -I ip_address -H qualified_hostname \n";
    print "\t-T lease_secs|INF|STATIC [-f dbm_database]\n";
    print "File mode usage: $0 -D | -L [-f dbm database]\n";
    print "Use in entry mode with the following options:-\n";
    print  "\t-a add an entry - specify the -C -M -I -H and -T options\n";
    print  "\t-d delete an entry - specify -C|-M|-I|-H\n";
    print  "\t-u update lease time - specify -C|-M|-I|-H\n";
    print  "\t-p print entry - specify -C|-M|-I|-H\n";
    print  "\t-r replace mode for add (replace existing entry or create new)\n";
    print  "\t-X print and accept client identifiers as hex_string\n";
    print  "\t-T lease time can be specified with print option to print\n";
    print  "\t\t leases expiring in less than (<value) or greater than(>value)\n";
    print  "\t\t seconds. For example, dhcpdb -p -T <1000\n";
    print  "\t All fields should be provided to add entries. To update or delete\n";
    print  "\t a single key (cid, mac address, ip address, or hostname) is required.\n";
    print  "\nUse in file mode with the following options:-\n";
    print  "\t-D dump to text file (stdout is output)\n";
    print  "\t-L load from text file to database (stdin is input)\n";
    exit();
}

# get a cid external form from a string
sub cid_stoa {
    my $val = $_[0];
    my $optX = $_[1];
    my ($len, $cid, $cidout, @cid);
    $len = length($val);
    @cid = unpack("C$len", $val);
    $cidout = "";
    if ($optX eq "") {
	$cidout = pack("C$len", @cid);
    } 
    else {
	foreach $cid (@cid) {
	    $cidout .= sprintf "%x:", $cid;
	}
    }
    $cidout =~ s/:$//;
    $cidout;
}

# convert cid to internally stored form (opaque string)
sub cid_atos {
    my (@cid, $len, $cid);
    my $optX = $_[1];
    if ($optX eq "") {
	return $_[0];
    }
    else {
	@cid = split(/[;:]/, $_[0]);
  	$len = $#cid + 1;
	for ($i = 0; $i < $len; $i++) {
	    $cids[$i] = hex($cid[$i]);
	}
	$cid = pack("C$len", @cids);
	return $cid;
    }
}

# prints an ethernet address as a string of hex bytes

sub ether_ntoa {
    my $ether, $byte;
    foreach $byte (@_) {
	$ether .= sprintf "%x:", $byte;
    }
    $ether =~ s/:$//;
    $ether;
}

sub ether_aton {
    my @ether, $ether, @eth;
    @ether = split(":", $_[0]);
    for ($i = 0; $i <= $#ether; $i++) {
	$eth[$i] = hex($ether[$i]);
    }
    @eth;
}

# create Keys

sub crEtherKey {
    my $opt = $_[0];
    my @ether, $len, $keyM;
    @ether = ether_aton($opt);
    $len = $#ether + 1;
    $keyM = pack("a C$len", 0, @ether);
}

sub crHostNameKey {
    my $opt = $_[0];
    my $len, $keyH;
    $len = length($opt);
    $keyH = pack("a A$len", (1, $opt));
}

sub crIpaddrKey {
    my $opt = $_[0];
    my $ipaddr, $len, @ipaddr, $keyI;
    $ipaddr = inet_aton("$opt");
    $len = length($ipaddr);
    @ipaddr = unpack("C4", $ipaddr);
    $keyI = pack("a C4", (2, @ipaddr));
}

# always takes input in internal form

sub crCidKey {
    my $opt = $_[0];
    my $len, $keyC;
    $len = length($opt);
    $keyC = pack("a A$len", (3, $opt));
}

sub getKeyCfromM {
    my $opt = $_[0];
    my $keyM, $cid, $keyC;
    $keyM = crEtherKey($opt);
    if ($DHCPDB{$keyM} eq undef) {
	print "Entry with key $opt_M not found.\n";
	return undef;
    }
    else {
	$cid = $DHCPDB{$keyM};
	$keyC = crCidKey($cid);
    }
    return $keyC;
}

sub getKeyCfromH {
    my $opt = $_[0];
    my $keyH, $cid, $keyC;
    $keyH = crHostNameKey($opt);
    if ($DHCPDB{$keyH} eq undef) {
	print "Entry with key $opt_H not found.\n";
	return undef;
    }
    else {
	$cid = $DHCPDB{$keyH};
	$keyC = crCidKey($cid);
    }
    return $keyC;
}

sub getKeyCfromI {
    my $opt = $_[0];
    my $keyI, $cid, $keyC;
    $keyI = crIpaddrKey($opt);
    if ($DHCPDB{$keyI} eq undef) {
	print "Entry with key $opt_I not found.\n";
	return undef;
    }
    else {
	$cid = $DHCPDB{$keyI};
	$keyC = crCidKey($cid);
    }
    return $keyC;
}

# add an entry to the DHCP database
sub addDHCPdb {
    my $dh_obj;
    my $keyM, $keyH, $keyI, $keyC, $data, $err, $valC, @optT, $optT;
    if ( ($opt_M eq "") || ($opt_C eq "") || ($opt_I eq "") || 
	 ($opt_H eq "") || ($opt_T eq "") ) {
	print  "Must specify the -C, -M, -I, -H, and -T options to add\n";
	usage();
    }
    
    @optT = split / /, $opt_T;
    if (@optT > 1) {
	$optT = mktime(@optT);
	if ($optT ne undef) {
	    $opt_T = $optT;
	}
	else {
	    print "Time should be entered in time() or mktime() format\n";
	    usage();
	}
    }

    if ($opt_T eq "INF") {
        $opt_T = -1;
    }
    if ($opt_T eq "STATIC") {
	$opt_T = -3;
    }
    if (($opt_T == 0) || ($opt_T < -3)) {
	print "Lease time should be positive.\n";
	untie %DHCPDB;
	return -1;
    }

    $keyM = crEtherKey($opt_M);

    $keyH = crHostNameKey($opt_H);

    $keyI = crIpaddrKey($opt_I);

    $valC = cid_atos($opt_C, $opt_X);
    $keyC = crCidKey($valC);

    $data = sprintf "%s\t%s\t%s\t%s\0", $opt_M, $opt_I, $opt_H, $opt_T;

    $db_obj = tie(%DHCPDB, "NDBM_File", $dhcpDBFilename, 
		  O_RDWR|O_CREAT, 0604);

    if ($db_obj eq undef) {
	die "Could not open $dhcpDBFilename.\n";
	exit();
    }

    # before adding this record make sure that its not already
    # in there.
    $err = 0;
    if ($opt_r == 0) {
	if  ($DHCPDB{$keyM} ne undef) {
	    $keyC = getKeyCfromM($opt_M);
	    print "Duplicate for Mac $opt_M: $DHCPDB{$keyC} exists.\n";
	    $err = 1;
	}
	if ($DHCPDB{$keyH} ne undef) {
	    $keyC = getKeyCfromH($opt_H);
	    print "Duplicate for Hostname $opt_H: $DHCPDB{$keyC} exists.\n";
	    $err = 1;
	}
	if ($DHCPDB{$keyI} ne undef) {
	    $keyC = getKeyCfromI($opt_I);
	    print "Duplicate for IP address $opt_I: $DHCPDB{$keyC} exists.\n";
	    $err = 1;
	}
	if ($DHCPDB{$keyC} ne undef) {
	    print "Duplicate for Cid $opt_C: $DHCPDB{$keyC} exists.\n";
	    $err = 1;
	}
	if ($err == 1) {
	    untie %DHCPDB;
	    return -1;
	}
    }
    

    lockDHCPdb();
    $DHCPDB{$keyM} = $valC;
    $DHCPDB{$keyH} = $valC;
    $DHCPDB{$keyI} = $valC;
    $DHCPDB{$keyC} = $data;
    unlockDHCPdb();

    untie %DHCPDB;
    return 0;
}

# delete an entry from the DHCP database
sub deleteDHCPdb {
    my $dh_obj, $selections, $delete, $keyC, $keyI, $data, $keyH, $keyM;
    my $M, $I, $H, $lease, $cid;

    if ( ($opt_M eq "") && ($opt_C eq "") && ($opt_I eq "") && ($opt_H eq "") ) {
	print  "Must specify the -C, -M, -I, or -H options to delete\n";
	usage();
    }

    $selections = ($opt_M ne undef) + ($opt_C ne undef) + 
      ($opt_I ne undef) + ($opt_H ne undef);
    if ($selections > 1) {
	print "Deletion can be done on one key only.\n";
	return -1;
    }

    $delete = 0;
    $db_obj = tie(%DHCPDB, "NDBM_File", $dhcpDBFilename, 
		  O_RDWR|O_CREAT, 0604);

    if ($db_obj eq undef) {
	die "Could not open $dhcpDBFilename.\n";
	exit();
    }
    
    if ($opt_C ne "") {
        $cid = cid_atos($opt_C, $opt_X);
	$keyC = crCidKey($cid);
	if ($DHCPDB{$keyC} eq undef) {
	    print "Entry with key $opt_C not found.\n";
	}
	else {
	    $data = $DHCPDB{$keyC};
	    ($M, $I, $H, $lease) = split(/[\t ]/, $data);
	    $keyH = crHostNameKey($H);
	    $keyI = crIpaddrKey($I);
	    $keyM = crEtherKey($M);
	    $delete = 1;
	}
    }
    elsif ($opt_M ne "") {
        $opt_M =~ tr/A-Z/a-z/;
	$keyC = getKeyCfromM($opt_M);
	if ($keyC) {
	    $data = $DHCPDB{$keyC};
	    ($M, $I, $H, $lease) = split(/[\t ]/, $data);
 	    $M =~ tr/A-Z/a-z/;
	    if ($opt_M ne $M) {
		print "Inconsistent data $opt_M != $M .\n";
		untie %DHCPDB;
		return -1;
	    }
	    $keyH = crHostNameKey($H);
	    $keyI = crIpaddrKey($I);
	    $delete = 1;
	}
    }
    elsif ($opt_H ne "") {
	$keyC = getKeyCfromH($opt_H);
	if ($keyC) {
	    $data = $DHCPDB{$keyC};
	    ($M, $I, $H, $lease) = split(/[\t ]/, $data);
	    if ($opt_H ne $H) {
		print "Inconsistent data $opt_H != $H .\n";
		untie %DHCPDB;
		return -1;
	    }
	    $keyI = crIpaddrKey($I);
	    $keyM = crEtherKey($M);
	    $delete = 1;
	}
    }
    elsif ($opt_I ne "") {
	$keyC = getKeyCfromI($opt_I);
	if ($keyC ne undef) {
	    $data = $DHCPDB{$keyC};
	    ($M, $I, $H, $lease) = split(/[\t ]/, $data);
	    if ($opt_I ne $I) {
		print "Inconsistent data $opt_I != $I .\n";
		untie %DHCPDB;
		return -1;
	    }
	    $keyM = crEtherKey($M);
	    $keyH = crHostNameKey($H);
	    $delete = 1;
	}
    }
    else {
	print "What option is this ?\n";
	untie %DHCPDB;
	return -1;
    }
    
    if ($delete == 1) {
	lockDHCPdb();
	
	delete $DHCPDB{$keyC};
	delete $DHCPDB{$keyM};
	delete $DHCPDB{$keyH};
	delete $DHCPDB{$keyI};
	
	unlockDHCPdb();
    }
    untie %DHCPDB;
}

# update a lease

sub updateDHCPdb {
    my $dh_obj, $selections, $update, $keyC, $keyI, $data, $keyH, $keyM;
    my $M, $I, $H, $lease, $cid, @optT, $optT;

    if ( ($opt_M eq "") && ($opt_C eq "") && ($opt_I eq "") && ($opt_H eq "") ) {
	print  "Must specify the -C, -M, -I, or -H options to update\n";
	usage();
    }
    elsif ($opt_T eq "") {
	print  "Must specify the lease value to update\n";
	usage();
    }

    if ($opt_T eq "INF") {
        $opt_T = -1;
    }
    if ($opt_T eq "STATIC") {
	$opt_T = -3;
    }
    if (($opt_T == 0) || ($opt_T < -3)) {
	print "Lease must be positive or -1(static allocation).\n";
	return -1;
    }

    @optT = split / /, $opt_T;
    if (@optT > 1) {
	$optT = mktime(@optT);
	if ($optT ne undef) {
	    $opt_T = $optT;
	}
	else {
	    print "Time should be entered in time() or mktime() format\n";
	    usage();
	}
    }

    $selections = ($opt_M ne undef) + ($opt_C ne undef) + 
      ($opt_I ne undef) + ($opt_H ne undef);
    if ($selections > 1) {
	print "Update can be done on one key only.\n";
	return -1;
    }

    $update = 0;

    $db_obj = tie(%DHCPDB, "NDBM_File", $dhcpDBFilename, 
		  O_RDWR|O_CREAT, 0604);
    if ($db_obj eq undef) {
	die "Could not open $dhcpDBFilename.\n";
	exit();
    }
    if ($opt_M ne "") {
        $opt_M =~ tr/A-Z/a-z/;
	$keyC = getKeyCfromM($opt_M);
	if ($keyC) {
	    $data = $DHCPDB{$keyC};
	    ($M, $I, $H, $lease) = split(/[\t ]/, $data);
 	    $M =~ tr/A-Z/a-z/;
	    if ($opt_M ne $M) {
		print "Inconsistent data $opt_M != $M .\n";
		untie %DHCPDB;
		return -1;
	    }
	    $update = 1;
	}
    }
    elsif ($opt_C ne "") {
        $cid = cid_atos($opt_C, $opt_X);
	$keyC = crCidKey($opt_C);
	if ($DHCPDB{$keyC} eq undef) {
	    print "Entry with key $opt_C not found.\n";
	}
	else {
	    $data = $DHCPDB{$keyC};
	    ($M, $I, $H, $lease) = split(/[\t ]/, $data);
	    $update = 1;
	}
    }
    elsif ($opt_H ne "") {
	$keyC = getKeyCfromH($opt_H);
	if ($keyC ne undef) {
	    $data = $DHCPDB{$keyC};
	    ($M, $I, $H, $lease) = split(/[\t ]/, $data);
	    if ($opt_H ne $H) {
		print "Inconsistent data $opt_H != $H .\n";
		untie %DHCPDB;
		return -1;
	    }
	    $update = 1;
	}
    }
    elsif ($opt_I ne "") {
	$keyI = getKeyCfromI($opt_I);
	if ($keyC ne undef) {
	    $data = $DHCPDB{$keyC};
	    ($M, $I, $H, $lease) = split(/[\t ]/, $data);
	    if ($opt_I ne $I) {
		print "Inconsistent data $opt_I != $I .\n";
		untie %DHCPDB;
		return -1;
	    }
	    $update = 1;
	}
    }
    else {
	print "What option is this ?\n";
	untie %DHCPDB;
	return -1;
    }

    if ($update == 1) {
	lockDHCPdb();
	
	$data = sprintf "%s\t%s\t%s\t%s\0", $M, $I, $H, $opt_T;
	$DHCPDB{$keyC} = $data;
	
	unlockDHCPdb();
    }
    untie %DHCPDB;
    
}


# print database entries

sub printDHCPdb {

    my $val, $print, $keyC, $keyM, $keyH, $keyI, $M, $H, $I, $lease;
    my $op, $secs, $time, $key, $val, $type, @keybuf, $cid, $please;
    if ( ($opt_M eq "") && ($opt_C eq "") && ($opt_I eq "") && 
	 ($opt_H eq "") && ($opt_T eq "") ) {
	print  "Must specify the -C, -M, -I, -H, or -T options to print\n";
	usage();
    }

    dbmopen %DHCPDB, $dhcpDBFilename, undef
      or die "Can't open $dhcpDBFilename: $!\n";

    $print = 0;
    if ($opt_M ne "") {
        $opt_M =~ tr/A-Z/a-z/;
	$keyC = getKeyCfromM($opt_M);
	if ($keyC ne undef) {
	    $data = $DHCPDB{$keyC};
	    ($M, $I, $H, $lease) = split(/[\t ]/, $data);
 	    $M =~ tr/A-Z/a-z/;
	    if ($opt_M ne $M) {
		print "Inconsistent data $opt_M != $M .\n";
		dbmclose %DHCPDB;
		return -1;
	    }
	    $print = 1;
	}
    }
    elsif ($opt_C ne "") {
	$cid = cid_atos($opt_C, $opt_X);
	$keyC = crCidKey($cid);
	if ($DHCPDB{$keyC} eq undef) {
	    print "Entry with key $opt_C not found.\n";
	}
	else {
	    $data = $DHCPDB{$keyC};
	    ($M, $I, $H, $lease) = split(/[\t ]/, $data);
	    $print = 1;
	}
    }
    elsif ($opt_H ne "") {
	$keyC = getKeyCfromH($opt_H);
	if ($keyC ne undef) {
	    $data = $DHCPDB{$keyC};
	    ($M, $I, $H, $lease) = split(/[\t ]/, $data);
	    if ($opt_H ne $H) {
		print "Inconsistent data $opt_H != $H .\n";
		dbmclose %DHCPDB;
		return -1;
	    }
	    $print = 1;
	}
    }
    elsif ($opt_I ne "") {
	$keyC = getKeyCfromI($opt_I);
	if ($keyC ne undef) {
	    $data = $DHCPDB{$keyC};
	    ($M, $I, $H, $lease) = split(/[\t ]/, $data);
	    if ($opt_I ne $I) {
		print "Inconsistent data $opt_I != $I .\n";
		dbmclose %DHCPDB;
		return -1;
	    }
	    $print = 1;
	}
    }
    elsif ($opt_T ne "") {
	$op = substr($opt_T, 0, 1);
	if (($op eq "<") || ($op eq ">")) {
	    $secs = substr($opt_T, 1);
	}
	else {
	    $op = ">";
	    $secs = $opt_T;
	}
	$time = time();
	while (($key,$val) = each %DHCPDB) {
	    ($type, @keybuf) = unpack("a C*", $key);
	    if ($type == 3) {	# cid is the key
		($M, $I, $H, $lease) = split(/[\t ]/, $val);
		if ($lease <= 0) {
		    $please = $lease . "\n";
		} else {
		    $please = ctime($lease);
		}
		if ($op eq ">") {
		    if ( ($lease - $time)  >= $secs ) {
			$cid = pack("C*", @keybuf);
			printf "%s :- %s\t%s\t%s\t%s", cid_stoa($cid, $opt_X), $M, $I, $H, 
			$please;
		    }
		}
		else {
		    if ( ($lease - $time)  < $secs ) {
			$cid = pack("C*", @keybuf);
			
			printf "%s :- %s\t%s\t%s\t%s", cid_stoa($cid, $opt_X), $M, $I, $H, 
			$please;
		    }
		}
	    }
	}
    }
    else {
	print "What option is this ?\n";
	dbmclose %DHCPDB;
	return -1;
    }

    if ($print == 1) {
	printf "%s :- %s\n", cid_stoa($cid, $opt_X), $data;
    }
    dbmclose %DHCPDB;
}


# dump the database entries

sub dumpDHCPdb {
    my $key, $val, $type, @keybuf, $cid;

    dbmopen %DHCPDB, $dhcpDBFilename, undef
      or die "Can't open $dhcpDBFilename: $!\n";

    while (($key,$val) = each %DHCPDB) {
	($type, @keybuf) = unpack("a C*", $key);
	
	if ($type == 3) {	# cid is the key
	    $cid = pack("C*", @keybuf);
            chop $val if ($val =~ /\0$/);
	    printf "%s\t%s\n", cid_stoa($cid, $opt_X), $val;
	}
    }
    dbmclose %DHCPDB;
}

#load entries into the database
    
sub loadDHCPdb {
    my $dh_obj;
    my $key, $M, $H, $I, $lease, $keyM, $keyC, $keyH, $keyI, $data, $valC;

    $db_obj = tie(%DHCPDB, "NDBM_File", $dhcpDBFilename, 
		  O_RDWR|O_CREAT, 0604);

    while (<>) {
	chop $_ if ($_ =~ /\n$/);
	($key, $M, $I, $H, $lease) = split(/[ \t]/, $_);
	$valC = cid_atos($key,$opt_X);
	$keyC = crCidKey($valC);
	if ($DHCPDB{$keyC} ne "") {
	    print "Updating entry for $_\n";
	}
	$keyM = crEtherKey($M);

	$keyH = crHostNameKey($H);

	$keyI = crIpaddrKey($I);
	$data = sprintf "%s\t%s\t%s\t%s\0", $M, $I, $H, $lease;

	lockDHCPdb();
	$DHCPDB{$keyM} = $valC;
	$DHCPDB{$keyH} = $valC;
	$DHCPDB{$keyI} = $valC;
	$DHCPDB{$keyC} = $data;
	unlockDHCPdb();
    }
    untie %DHCPDB;
}
	
sub main {
    if (getopts('haduprXLDC:M:H:I:T:f:') == 0) {
	usage();
    }
    if ($opt_h) {
	usage();
    }
    if ($opt_f) {
	$dhcpDBFilename = $opt_f;
    }

    sysopen(LOCKDB, "$dhcpDBFilename" . ".lock", O_RDONLY|O_CREAT, 0604)
      or die "Can't open lock file: $!";

    if (($opt_a == 1) || ($opt_r == 1)) {
	$opt_a = 1;
	addDHCPdb();
    }
    elsif ($opt_d == 1) {
	deleteDHCPdb();
    }
    elsif ($opt_u == 1) {
	updateDHCPdb();
    }
    elsif ($opt_p == 1) {
	printDHCPdb();
    }
    elsif ($opt_D == 1) {
	dumpDHCPdb();
    }
    elsif ($opt_L == 1) {
	loadDHCPdb();
    }
    else {
	print  "Must specify add/delete/update/print/replace\n\n";
	usage();
    }
    close(LOCKDB);
}


main();


