#!/usr/local/sendmail/perl/bin/perl -w
#
# Copyright (c) 2004,2010 Sendmail, Inc. and its suppliers.
#	All rights reserved
#
# ldapfiler - 	used to store or retrieve a file stored in the Directory Server
#
# Reproduction and/or modification of this code by any person or agency 
# other than Sendmail, Inc. employees is not permitted under any 
# circumstances, except under the specific direction of a member of
# the Sendmail Technical Support & Consulting organization.

use strict;
use Getopt::Std;
use Net::LDAP qw(:all);
use Net::LDAP::Message qw(:all);
use Net::LDAP::Util qw(ldap_error_name ldap_error_text) ;

use vars qw ( $LDAP $LDAPdn $LDAPpw $LDAPuri $LDAPbase $LDAPfilter %opt );

# Use these values as defaults, overridden by options if desired

$LDAPdn='cn=ldapfiler,ou=sysaccounts,dc=admin,dc=local';
$LDAPpw='<your_password_here>';
$LDAPuri="ldaps://127.0.0.1/";
$LDAPbase="ou=ldapFiles,ou=smiConfig";

sub Usage
{
	print STDERR <<EOB;

Usage: $0 -f<[path/]filename> -v|-r|-w|-X|-L 
		[-i|-I[<description>]] [-V<version>] [-d]
		[-b<baseDN>] [-H<URI>] [-D<bindDN>] [-P<passwd_file>]

Supported parameters:
 -v <filename>	Report the existing version information, and exit.
		This option may be used with -I (see below).
 -r <filename>	Read file from the directory, store to local file.
 -w <filename>	Write file to the directory, as read from local file.
 -X <filename>	Remove the file specified from the directory
 -L		Lists files currently stored within the directory
 -f <filename>	Specifies the path to the local file.  (Full path is optional.) 
 -i		Provide complete set of information about the file, in 
		user-friendly format.  Used in -v and -r modes.
 -I <new desc>	Description / information, used to supply new value
		when used with -w to update the directory.
 -O		'Overwrite' (-w option) Regardless of the current on-disk 
		version information, overwrite the file.
 -V <version>	Version information for the file being sent to the directory.
		(-V is only used with -w.)
		Note: On update when a value for -V is not specified, the 
		current system time will be used, as returned by the 
		time() function.
 -d		Display debugging information
 -H		Optional LDAP URI of the directory server 
	   	(Default is '$LDAPuri')
 -D		Optional bind DN used to query server.  
	   	(Default is '$LDAPdn')
 -b		Base DN for storage of and query of dirctory entries
	   	(Default is '$LDAPbase')
 -P		Password path/file from which to read the bind password
 -h		Display this help information

EOB
}

getopts('f:v:r:w:iI:OV:H:b:D:P:hdX:L', \%opt);

if ($opt{'h'} )
{
	&Usage();
	exit(0);
}

CheckOpts(%opt);  # Check minumum requirements of invocation 

if ($opt{'H'})
{
	$LDAPuri=$opt{'H'};
	print "Using URI: $LDAPuri\n" if ($opt{'d'});
}
if (defined $opt{'D'})
{
	# Note: value may be defined as '', which is anonymous bind.

	if ($opt{'D'})
	{
		$LDAPdn=$opt{'D'};
		print "Binding as: $LDAPdn\n" if ($opt{'d'});
	}
	else
	{
		$LDAPdn="";
		print "Binding anonymously\n" if ($opt{'d'});
	}
}

if (defined $opt{'P'})
{
	($LDAPpw)=`cat $opt{'P'}`;
	chomp $LDAPpw;
	if ($LDAPpw)
	{
		print "Using password from file $opt{'P'}\n" if ($opt{'d'});
	}
	else
	{
		print "\nError:\nCould not read the password from ";
		print "'$opt{'P'}'\n\n";
		print "Use '$0 -h' for usage information\n\n";
		exit(1);
	}
}

if (defined $opt{'b'})
{
	($LDAPbase)=$opt{'b'};
}

$LDAP = NewLDAPconnection($LDAPuri, $LDAPdn, $LDAPpw);

print "Searching from base DN: '$LDAPbase'\n" if ($opt{'d'});

if ( $opt{'v'} )
{
	fileInfo($LDAP,%opt);
}
elsif ( $opt{'L'} )
{
	listFiles($LDAP,%opt);
}
elsif ( $opt{'r'} )
{
	readFile($LDAP,%opt);
}
elsif ( $opt{'w'} )
{
	writeFile($LDAP,%opt);
}
elsif ( $opt{'X'} )
{
	deleteFile($LDAP,%opt);
}
else
{
	die ("The -L, -r, -v, or -w flag is required!");
}

$LDAP->unbind();
print "LDAP Connection closed\n" if ($opt{'d'});
exit(0);


sub NewLDAPconnection 
{
	my ($uri, $dn, $password) = @_;
	my ($LDAP,$result,$error);
	$LDAP = Net::LDAP->new($uri);
	if (! $LDAP) 
	{
		# the LDAP server is down.
		print "The LDAP server $uri is currently unavailable. \n";
		exit(2);
	}

	if ($dn) 
	{
		$result = $LDAP->bind(dn => $dn, 
			password => $password ,
			version => 3 );
	} 
	else
	{
		$result = $LDAP->bind(anonymous => "", version => 3);
	}

	ResultCheck($result,
		"Connection to '$LDAPuri' established\n" .
		"Error binding as '$dn' to server: \n\t");

	if ($opt{'d'})
	{
		print "LDAP Connection established to $uri\n";
		if ($dn)
		{
			print "\tbinding as '$dn'\n";
		}
		else
		{
			print "\tbinding anonymously\n";
		}
	}
	return $LDAP;
}

sub ResultCheck
{
	my ($result, $string) = @_;
	$string || ($string = "Error while accessing the directory: ");

	if ( $result->code )
	{
		print $string . $result->error . " \n";
		exit (30);
	}
}

sub CheckOpts
{
	return(0) if ($opt{'L'});
	return(0) if ($opt{'v'});
	
	if ($opt{'r'})
	{
		if ($opt{'f'})
		{
			print "Operating in 'Read' mode\n" if ($opt{'d'});
			print "local filename: $opt{'f'}\n" if ($opt{'d'});
		}
		else
		{
			print "Operating in 'Read' mode\n";
			print "Error: You must specify the -f value!\n";
			print "Use '$0 -h' for usage information\n\n";
			exit(5);
		}
		return();
	}
	elsif ($opt{'w'})
	{
		if ($opt{'f'})
		{
			print "Operating in 'Write' mode\n" if ($opt{'d'});
			print "local filename: $opt{'f'}\n" if ($opt{'d'});
		}
		else
		{
			print "Operating in 'Write' mode\n";
			print "Error: You must specify the -f value!\n";
			print "Use '$0 -h' for usage information\n\n";
			exit(6);
		}
		return();
	}
	elsif ($opt{'X'})
	{
		print "Operating in 'Delete' mode\n" if ($opt{'d'});
		print "Directory filename: $opt{'X'}\n" if ($opt{'d'});
	}
	else
	{
		print "\nError:\n\t";
		print "You must specify one or more options for this tool\n\n";
		print "Use '$0 -h' for usage information\n\n";
		exit(7);
	}
}


sub fileInfo
{
	my ($LDAP, %opt) = @_;
	my ($result, $ver, $desc, $entry);

	$result = $LDAP->search(
		base => $LDAPbase,
		scope => 'sub',
		attrs => ['description','ldapFileVersion'],
		filter => "ldapFileName=$opt{'v'}" );

	ResultCheck($result,"Error retrieving entries from directory server:");
	
	if ($result->entries ==0)
	{
		print "Error: unable to retrieve the file '$opt{'v'}'\n";
		return();
	}
	if ($result->entries >1)
	{
		print "Error: more than one file retrieved with file name";
		print "'$opt{'v'}'\n";
		return();
	}

	$entry =$result->first_entry();
	($ver) = $entry->get_value("ldapFileVersion");
	($desc) = $entry->get_value("description");
	$desc || ($desc = '<none>');

	if ($opt{'i'})
	{
		print "LDAP File Name: $opt{'v'}\n";
		print "LDAP File Version: $ver\n";
		print "Description: $desc\n" if $opt{'i'};
	}
	else
	{
		print "$ver\n";
	}

	if ($opt{'i'} && $opt{'f'} && versionDiffers($LDAP,%opt))
	{
		print "Disk file version is out of sync.\n";
	}
}

sub readFile
{
	my ($LDAP, %opt) = @_;
	my ($result, $ver, $verfile, $desc, $entry);

	if ( (!$opt{'O'}) && (!versionDiffers($LDAP,%opt)))
	{
		# print "The file is current and will not be overwritten.\n"
		#	if ($opt{'i'} || $opt{'d'});
		return();
	}

	$result = $LDAP->search(
		base => $LDAPbase,
		scope => 'sub',
		filter => "ldapFileName=$opt{'r'}" );

	ResultCheck($result,"Error retrieving entries from directory server:");
	
	if ($result->entries ==0)
	{
		print "Error: unable to retrieve the file '$opt{'r'}'\n";
		return();
	}
	if ($result->entries >1)
	{
		print "Error: more than one file retrieved with file name";
		print "'$opt{'r'}'\n";
		return();
	}

	$entry =$result->first_entry();
	($ver) = $entry->get_value("ldapFileVersion");
	($desc) = $entry->get_value("description");

	if ($opt{'i'})
	{
		print "LDAP File name: $opt{'r'}\n";
		print "LDAP Version: $ver\n";
		print "Description: $desc\n" if ($desc);
	}

	open OUT, ">$opt{'f'}" ||
		 die ("Unable to open file $opt{'f'} for writing");
	binmode(OUT);
	print OUT $entry->get_value('ldapFileData');
	close OUT;

	# Finally, update the local version file.
	if ($opt{'f'} =~ /\//)
	{
		$opt{'f'} =~ /(^.*\/)(.*$)/;
		$verfile = "$1.$2.ver";
	}
	else
	{
		$verfile= '.' . $opt{'f'} . '.ver';
	}
	open OUTVER, ">$verfile" ||
		 die ("Unable to open version file for writing");
	print OUTVER $ver;
	close OUTVER;

	print "File '$opt{'f'}', version $ver written to disk\n";
}

sub versionDiffers
{
	# Returns 1 if the LDAP version is different,
	# Returns 2 if the local .ver file cannot be read,
	# Returns 0 when the files are identical

	my ($LDAP, %opt) = @_;
	my ($filever,$ldapver,$ldapfile,$verfile,$result,$entry);

	if ($opt{'v'})
	{
		$ldapfile = $opt{'v'};
	}
	elsif ($opt{'r'})
        {
                $ldapfile = $opt{'r'};
        }

	if ($opt{'f'} =~ /\//)
	{
		$opt{'f'} =~ /(^.*\/)(.*$)/;
		$verfile = "$1.$2.ver";
	}
	else
	{
		$verfile= '.' . $opt{'f'} . '.ver';
	}

	if ($opt{'i'})
	{
		print "Disk File name: $opt{'f'}\n" if ($opt{'d'});
		print "Disk Version file: $verfile\n" if ($opt{'d'});
	}

	if (! -r $verfile)
	{
		print "$verfile could not be read\n" if ($opt{'d'});
		print "Disk version information could not be read\n" 
			if ($opt{'i'});
		return(2);
	}
	$filever = `cat $verfile`;
	chomp $filever;

	$result = $LDAP->search(
		base => $LDAPbase,
		scope => 'sub',
		attrs => ['ldapFileVersion'],
		filter => "ldapFileName=$ldapfile" );

	ResultCheck($result,"Error retrieving entries from directory server:");
	
	if ($result->entries ==0)
	{
		print "Error: unable to retrieve the file '$ldapfile'\n";
		return();
	}
	if ($result->entries >1)
	{
		print "Error: more than one file retrieved with file name";
		print "'$ldapfile'\n";
		return();
	}

	$entry =$result->first_entry();
	($ldapver) = $entry->get_value("ldapFileVersion");
	
	if ($opt{'i'})
	{
		print "Disk Version: $filever\n";
		print "Disk Version is out of sync\n" 
			if ($ldapver ne $filever);
	}

	return (1) if ($ldapver ne $filever);

	if ($opt{'i'} || $opt{'d'})
	{
		print "Disk file version is current.\n";
	}
	return(0);
}

sub writeFile
{
	my ($LDAP, %opt) = @_;
	my ($result);

	if (! -r $opt{'f'})
	{
		print "Disk file $opt{'f'} could not be read\n";
		print "Please check the -f <filename> value you supplied\n";
		exit(3);
	}

	$result = $LDAP->search(
		base => $LDAPbase,
		scope => 'sub',
		filter => "ldapFilename=$opt{'w'}" );

	ResultCheck($result,"Error retrieving entries from directory server:");

	if ($result->entries >1)
	{
		print "Error: more than one file retrieved with file name";
		print "'$opt{'w'}'\n";
		return();
	}
	elsif ($result->entries ==0)
	{
		# create the new entry, write local .ver file
		CreateEntry($LDAP, %opt);
	}
	else
	{
		UpdateEntry($LDAP,$result->first_entry,%opt);
	}
}

sub UpdateEntry
{
	my ($LDAP, $entry,%opt) = @_;

	my ($ver,$verfile,$data,$result,$buffer);

	# Update the description
	if (defined($opt{'I'}))
	{
		if ($opt{'I'})
		{
			$result = $LDAP->modify( $entry, replace => { 
			    'description' => [$opt{'I'}] });
			ResultCheck($result, 
			    "Error updating entry in the directory server:");
			print "description set to: $opt{'I'}\n" if $opt{'d'};
		}
		else
		{
			$result = $LDAP->modify( $entry, delete => { 
			    'description' => [] });
			ResultCheck($result, 
			    "Error updating entry in the directory server:");
			print "description removed\n" if $opt{'d'};
		}
	}

	# Update the version information
	if ($opt{'V'})
	{
		$ver = $opt{'V'};
	}
	else
	{
		$ver=time();
	}

       	$result = $LDAP->modify( $entry, replace => { 
	    'ldapFileVersion' => [$ver] });
	ResultCheck($result, 
	    "Error updating entry in the directory server:");
	print "version set to: $ver\n" if $opt{'d'};

	# Update the file contents
	open(IN, $opt{'f'});
	binmode(IN);
	while (read(IN,$buffer,1024,0))
	{
		$data .= $buffer;
	}
	close(IN);
       	$result = $LDAP->modify( $entry, replace => { 
	    'ldapFileData' => [$data] });
	ResultCheck($result,"Error updating entry in the directory server:");

	# Finally, update the local version file.
	if ($opt{'f'} =~ /\//)
	{
		$opt{'f'} =~ /(^.*\/)(.*$)/;
		$verfile = "$1.$2.ver";
	}
	else
	{
		$verfile= '.' . $opt{'f'} . '.ver';
	}
	open OUTVER, ">$verfile" ||
		 die ("Unable to open version file for writing");
	print OUTVER $ver;
	close OUTVER;

	print "File '$opt{'w'}', version $ver has been updated in the".
		" directory\n";
	# Updates complete
	return ();
}


sub CreateEntry 
{
	my ($LDAP, %opt) =@_;
	my ($data, $ver, $verfile, $dn, $result, $entry, $buffer);
	die ("No filename was specified!") unless defined ($opt{'w'});

	if (! -r $opt{'f'})
	{
		print "Disk file $opt{'f'} could not be read\n";
		print "Please check the -f <filename> value you supplied\n";
		exit(3);
	}

	print "Creating new entry in the directory\n" if ($opt{'d'});
	$dn = "ldapFileName=$opt{'w'},$LDAPbase";
	print "DN: $dn\n" if ($opt{'d'});

	open(IN, $opt{'f'});
	binmode(IN);
	while (read(IN,$buffer,1024,0))
	{
		$data .= $buffer;
	}
	close(IN);

	if ($opt{'V'})
	{
		$ver = $opt{'V'};
	}
	else
	{
		$ver=time();
	}
	print "ldapFileName: $opt{'w'}\n" if $opt{'d'};
	print "ldapFileData: <Binary>\n" if $opt{'d'};
	print "ldapFileVersion: $ver\n" if $opt{'d'};

	$entry = [
		objectClass => [ 'ldapFile' ],
		ldapFileName => [ $opt{'w'} ],
		ldapFileData => [ $data ],
		ldapFileVersion => [ $ver ],
	];

	push (@$entry, "description", $opt{'I'} ) if ($opt{'I'});
	print "description: $opt{'I'}\n" if ($opt{'I'} && $opt{'d'});

	$result = $LDAP->add( "$dn", attrs => [ @$entry ] );
	ResultCheck($result,"Error creating new entry $dn in the directory: ");

	# Finally, update the local version file.
	if ($opt{'f'} =~ /\//)
	{
		$opt{'f'} =~ /(^.*\/)(.*$)/;
		$verfile = "$1.$2.ver";
	}
	else
	{
		$verfile= '.' . $opt{'f'} . '.ver';
	}
	open OUTVER, ">$verfile" ||
		 die ("Unable to open version file for writing");
	print OUTVER $ver;
	close OUTVER;

	print "File '$opt{'w'}', version $ver has been added to the".
		" directory\n";
}

sub deleteFile
{
	my ($LDAP, %opt) = @_;
	my ($result, $ver, $desc, $entry);

	$result = $LDAP->search(
		base => $LDAPbase,
		scope => 'sub',
		attrs => ['-1'],
		filter => "ldapFileName=$opt{'X'}" );

	ResultCheck($result,"Error retrieving entries from directory server:");
	
	if ($result->entries ==0)
	{
		print "Unable to locate the file '$opt{'X'}' in the ".
			"directory\n";
		return();
	}
	if ($result->entries >1)
	{
		print "Error: more than one file retrieved with file name";
		print "'$opt{'X'}'\n";
		return();
	}

	$entry =$result->first_entry();

	$result = $LDAP->delete( $entry->dn );
	ResultCheck($result,
		"Error deleting entry " . $entry->dn ."from the directory:");
	
	print "File '$opt{'X'}' has been removed from the directory\n";
}

sub listFiles
{
	my ($LDAP,%opt) = @_;
	my ($result, $ver, $desc, $entry);

	$result = $LDAP->search(
		base => $LDAPbase,
		scope => 'sub',
		attrs => ['ldapfileName','ldapFileVersion','description'],
		filter => "ldapFileName=*" );

	ResultCheck($result,"Error retrieving entries from directory server:");
	
	if ($result->entries ==0)
	{
		print "Unable to locate any files in the directory\n";
		return();
	}
	foreach $entry ($result->sorted('ldapFileName'))
	{
		if ($opt{'i'})
		{
			my ($name) = $entry->get_value('ldapFileName');
			my ($ver) = $entry->get_value('ldapFileVersion');
			my ($desc) = $entry->get_value('description');
			$ver || ($ver="");
			$desc || ($desc="");
			printf("%20s %15s %s\n", $name,$ver,$desc);
		}
		else
		{
			print $entry->get_value('ldapFileName') . "\n";
		}
	}
}
