This page contains an annotated program in PHP
that gets data from ldap.rutgers.edu, with ldap2.rutgers.edu as backup.
Most of the complexity is in using the backup, because PHP doesn't
support timeouts for connection attempts.
Note that the proper way to use LDAP is as follows:
1. Start by opening a TLS connection, and binding it to
the special service DN and password that you are issued by
ldap-support@rutgers.edu. If you don't do this, you're limited to
data that is visible to the public.
2. Take the user's NetID and look it up, using a query
like (uid=XXX) where XXX is the NetID. The lookup will return
the actual DN for the user. This is typically
uid=XXX,ou=people,dc=rutgers,dc=edu. However your code should
not assume that it knows the format of the DN. You always want
to lookup uid=XXX and let the directory return the DN.
3. The search will also return attributes of the user's directory entry. You may need to check them to make sure that the user is allowed to use your service, or to get information that will help you serve the user.
4. Given the DN returned by the search, you can now check the user's password. To do this, attempt to bind with the DN and the password that the user supplied.
The following example is designed to be called from a login form. The form is assumed to set $uname and $pwd. Of course you can change this to be any variables you want.
This assumes that PHP was built with openssl and LDAP.
NOTE: Your web application must use SSL or TLS, because it's processing University passwords. The example code uses TLS on the default port 389. This is now considered a better approach than SSL on the special port, 636.
Note that most of this example consists of functions to deal with backups. Because PHP doesn't support timeouts for connection attempts, the best we can do is remember when a system has been down, and try the other one first. You'll take a 3 minute hit when the primary first goes down, but after that, programs will try the backup first. A file is kept in /tmp that has the current order of servers to use. When servers at the beginning of the list fail, the first one that works is moved to the start of the list.
You'll need to specify a file name to start the current order in. I recommend putting it on /tmp. The code will recreate it if it's missing or invalid. The same file should be used by all applications that use the same set of LDAP servers.
<?
// Put your password in a file that's outside the area apache is allowed to see
$passfile = '....';
// This is the dn you are assigned by the ldap.rutgers.edu administrator
$privdn = "....";
$dirhosts = array('ldap.rutgers.edu', 'ldap2.rutgers.edu');
$dirofile = "/tmp/demodirorder";
$base = "ou=people,dc=rutgers,dc=edu";
// Get arguments from login form
$uname = $_POST['uname'];
$pwd = $_POST['pwd'];
// Connect to ldap server, TLS
$ds = connect_ldap($dirhosts, $dirofile);
if (!$ds)
fatal ("Can't connect to $dirhost");
// Bind as your privileged DN
$f = file($passfile);
$rr=ldap_bind($ds,$privdn, trim($f[0]));
if (!$rr)
fatal ("Can't bind to privileged dn");
// Look up the user
$sr=ldap_search($ds,$base,"uid=$uname");
if (!$sr)
fatal ("Can't find user $uname");
// Search had better return exactly 1 item; there should be 1 person per netid
$nentries = ldap_count_entries($ds,$sr);
if ($nentries < 1)
fatal ("Can't find user $uname");
if ($nentries > 1)
fatal ("Ambiguous user $uname");
// $info is an array, because searches can return more than one entry
// in this case you'll always want $info[0], since we've verified that
// the search returned exactly one item
$info = ldap_get_entries($ds, $sr);
// get the DN of the user we found
$userdn = $info[0]["dn"];
// check the password by trying to bind to the user's dn
$r=ldap_bind($ds, $userdn, $pwd);
if (!$r)
fatal ("Unable to login as user $uname");
// Now print some stuff from the user's entry. The form of ldap_search
// used above returns all entries. $info[0] is this entry
// $info[0]["cn"] is an array with all the values of the attribute "cn".
// (Note that the attribute name must be given in lowercase here.)
// $info[0]["cn"][0] is the first value of "cn". In this example I print
// the first "cn" and all values of "employeetype"
print "Full name: ". $info[0]["cn"][0] . "<br>";
$c = $info[0]["employeetype"]["count"];
for ($i = 0; $i < $c; $i++)
print "Employee type: " . $info[0]["employeetype"][$i] . "<br>";
// You'll probably have your own function for printing errors.
function fatal($msg) {
print $msg;
exit;
}
// Code for opening ldap and moving to a backup server when needed.
// Note that a down server will cause a pause for 180 sec before the
// change occurs. During that time, apache is likely to build up
// lots of processes that are hung waiting for ldap.
// This code uses TLS on port 389. This is the current approach,
// rather than using SSL on port 636. Disable user aborts. We
// have to run to completion or we won't swap servers. However this
// may be unnecessary. User aborts apparently aren't detected until
// you try to output on a connection that's been closed. Might as
// well leave the code in though.
function connect_ldap($dirhosts, $dirofile) {
$sdirhosts = sort_dir_hosts($dirhosts, $dirofile);
$prevabort = ignore_user_abort(true);
$c = count($sdirhosts);
for ($i = 0; $i < $c; $i++) {
$ds = connect_ldap1($sdirhosts[$i]);
if ($ds) {
if ($i > 0)
promote_dir_host($dirhosts, $dirofile, $sdirhosts[$i]);
break;
}
}
// put aborts back
ignore_user_abort($prevabort);
// and see if he aborted in the meantime
$a = connection_aborted();
// if so, and we had allowed aborts, exit
if ($a && ! $prevabort) {
fclose($handle);
exit();
}
if ($i < $c)
return $ds;
return false;
}
function connect_ldap1($dirhost) {
$ds=ldap_connect($dirhost);
// with openldap at least this will always succeed
if (!$ds)
return false;
if (!ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3))
return false;
// this will actually do something on the wire
if (!ldap_start_tls($ds))
return false;
return $ds;
}
// this function rewrites the order file. It's not quite as
// paranoid as sort_dir_hosts
function promote_dir_host($dirhosts, $dirofile, $host) {
$f = file($dirofile);
$c = count($f);
// if the file is actually a permutation, this should always
// find the host to be promoted
for ($i = 0; $i < $c; $i++) {
if ($dirhosts[(int)$f[$i]] == $host)
break;
}
// $i < $c: found the thing to be promoted
// $i > 0: not already first
if ($i < $c && $i > 0) {
$save = $f[$i];
for ($j = $i; $j > 0; $j--)
$f[$j] = $f[$j-1];
$f[0] = $save;
// now write the new file
$u = uniqid("");
$handle = fopen("$dirofile.$u", "w");
if (!$handle) {
exit();
}
for ($i = 0; $i < $c; $i++) {
fwrite($handle, $f[$i]);
}
fclose($handle);
if (!rename("$dirofile.$u", $dirofile)) {
exit();
}
}
}
// this function is sort of paranoid. The file had better be
// a permutation of 0 .. count(dirhosts)-1, or we ignore it and
// reinit it.
function sort_dir_hosts($dirhosts, $dirofile) {
$f = file($dirofile);
$oldhosts = $dirhosts;
$newhosts = array();
$c = count($f);
if ($c == count($dirhosts)) {
for ($i = 0; $i < $c; $i++) {
$newhosts[$i] = $oldhosts[(int)$f[$i]];
unset($oldhosts[(int)$f[$i]]); // make sure we dont use anything twice
}
for ($i = 0; $i < $c; $i++) {
if ($oldhosts[$i]) {
break; /* some member of list not used */
}
if ($newhosts[$i] === NULL) {
break; /* some member of new list not set */
}
}
if ($i == $c) /* OK */
return $newhosts;
}
/* problem, reinit the lists */
/* write to a new file and rename, so this is atomic */
/* there could still be several scripts doing this whole function at
once. I think that's OK. */
$u = uniqid("");
$handle = fopen("$dirofile.$u", "w");
if (!$handle) {
exit();
}
$c = count ($dirhosts);
for ($i = 0; $i < $c; $i++) {
fwrite($handle, "$i\n");
}
fclose($handle);
if (!rename("$dirofile.$u", $dirofile)) {
exit();
}
return $dirhosts;
}
?>