#!/usr/bin/perl -T

# This is finger-ldap, a wrapper around finger for machines using LDAP
# Copyright (C) 2004  Simon Law
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public 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.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

use strict;
use warnings;
use English;

use Net::LDAP;

# Untaint the PATH environment variable
$ENV{PATH} = '/bin:/usr/bin:/usr/X11R6/bin';

# Global variables
my $program = $PROGRAM_NAME;	# This program's name
my $version = '1.1';		# This program's version
my $finger = '/usr/bin/finger.real';	# The finger binary
my $nss_ldap = '/etc/libnss-ldap.conf';	# The libnss-ldap configuration file

# ($base, $base_passwd, @servers) parse_conf ($filename)
#
# This function parses the libnss-ldap configuration file stored in
# $filename to extract the LDAP $base, the LDAP $base_passwd and the
# LDAP @servers available to query.
sub parse_conf ($)
{
    my $filename = shift;

    # Open the configuration file
    open (my $fh, "<$filename") or die ("$program: Can't read $filename\n");

    my $base;
    my $base_passwd;
    my @servers;
    # Go through each line and try to parse out what we want
    while (<$fh>) {
	# Strip leading and tailing whitespace
	s/^\s*(.*?)\s*$/$1/;

	# Strip out any comments
	s/^\#.*//;
	s/([^\\])\#.*/$1/;

	# Get the base, stripping whitespace
	if (m/^base\s+(.*?)$/) {
	    $base = "$1";
	}
	# Get the servers, either using URI or host (deprecated)
	elsif (m/^uri\s+(.*)/ or m/^host\s+(.*)/) {
	    @servers = split (/\s+/, $1);
	}
	# Get the base passwd, so we can resolve user entries
	elsif (m/^nss_base_passwd\s+([^?])*/) {
	    $base_passwd = "$1";
	}
    }

    return ($base, $base_passwd, @servers);
}

# \%usernames = get_usernames ($ldap, $arg)
#
# This function queries the $ldap server for userids, using $base as
# the base domain name.
#
# It first queries for an exact userid match.  If that fails, it will
# try to find the user's name.  When it's done, it will return a
# reference to a hash-table whose keys are the usernames that were
# found.
sub get_usernames ($$$$)
{
    my $ldap = shift;
    my $base = shift;
    my $base_passwd = shift;
    my $arg = shift;

    # Usernames that should be passed to finger -m
    my %usernames;

    # Net::LDAP::Message is returned for all queries
    my $mesg;

    # Search for the userid first
    $mesg = $ldap->search (base => "$base_passwd,$base",
			   filter => "(uid=$arg)");
    unless ($mesg->code ()) {
	for my $entry ($mesg->all_entries ()) {
	    if ($entry->get_value ('uid')) {
		$usernames{$arg} = 1;
	    }
	}
    }

    unless (scalar (%usernames)) {
	# Search for the user's name then
	$mesg = $ldap->search (base => $base,
			       filter => "(cn=*$arg*)");
	unless ($mesg->code ()) {
	    for my $entry ($mesg->all_entries ()) {
		if (my $result = $entry->get_value ('uid')) {
		    $usernames{$result} = 1;
		}
	    }
	}
    }

    return \%usernames;
}

# int main ()
#
# First we parse the libnss-ldap config file to get our settings.
# Then we connect to the LDAP database, and query against each of the
# full names, if necessary.  After collecting all the usernames
# that match these full names, we pass this information to `finger -m`
# which does the final resolution.
sub main
{
    # Get some information from the libnss-ldap configuration file.
    (my $base, my $base_passwd, my @servers) = parse_conf ($nss_ldap);

    # Construct a new Net::LDAP query object
    my $ldap;
    my $error;
    for my $server (@servers) {
	$ldap = Net::LDAP->new ($server);
	unless ($ldap) {
	    $error = $EVAL_ERROR;
	    $error =~ s/^IO.*?: //; # Strip out IO::Foo errors
	    next;
	}
	else {
	    undef ($error);
	    last;
	}
    }
    die ("$program: $error\n") if ($error);

    # Net::LDAP::Message is returned for all queries
    my $mesg;

    # Connect to the database
    $mesg = $ldap->bind ();
    $mesg->code () and
	die ("$program: Could not bind to LDAP servers: " . $mesg->error ()
	     . "\n");

    # Array to store arguments for finger
    my @fingerargs;

    # Query for the usernames
    my %usernames;		# Keys are the usernames
    my $preventmatching = 0;	# Has -m been passed?
    for my $arg (@ARGV) {
	if ($arg =~ m/^-.*m/) {
	    # -m has been passed, we'll let finger do everything
	    $preventmatching = 1;
	    last;
	}
	elsif ($arg =~ m/@/) {
	    # If the userid is on a remote host, let finger deal with it
	    $usernames{$arg} = 1;
	}
	elsif ($arg =~ m/^-/) {
	    # This is an argument passed to finger.
	    push (@fingerargs, $arg);
	}
	else {
	    # Query the database for usernames
	    my $ref = get_usernames ($ldap, $base, $base_passwd, $arg);
	    # Merge into the primary hash
	    if (scalar (%$ref)) {
		for my $username (keys %$ref) {
		    $usernames{$username} = 1;
		}
	    }
	    else {
		# No results?  Let finger deal with it
		$usernames{$arg} = 1;
	    }
	}
    }

    # Call finger -m
    my @command;
    if ($preventmatching) {
	@command = ($finger, @ARGV);
    }
    else {
	@command = ($finger, '-m', @fingerargs, keys %usernames);
    }
    my $retval = system (@command);

    # Disconnect from the database
    $mesg = $ldap->unbind ();

    return $retval;
}
main ();
