Ïðèãëàøàåì ïîñåòèòü
Õîäàñåâè÷ (hodasevich.lit-info.ru)

11.7 Making It Mature, Part 1

Previous Table of Contents Next

11.7 Making It Mature, Part 1

Are we satisfied yet? No! This program is still fragile because of duplication of information: The template contains the list of attributes, twice, and so does our program, and they all have to be in the same order. If the list of attributes changes, we'll have to update three places. Furthermore, our CGI parameter-parsing loop blindly accepts the first parameter it sees and assumes it is a search term. A future change to the input might pass in some other kind of parameter and then we'd pick up the wrong one. That change could happen by accident: Someone might be using an HTML editor that gives a NAME attribute to the submit button and then we'd receive an input parameter for the button click.

Let's get the template to use the list of attributes in the program, and at the same time, specify which attributes are search parameters. The results template now needs a loop wherever it had a list of attributes and becomes:

Example 11.12. output.tmpl, Version 2

<HTML><HEAD><TITLE>Search Results</TITLE></HEAD>

 <BODY>

  <H1>Results of search for <TMPL_VAR NAME=filter></H1>

  <TABLE BORDER="1">

   <TR>

    <TMPL_LOOP NAME="attributes">

     <TH><TMPL_VAR NAME="attribute"></TH>

    </TMPL_LOOP>

   </TR>

   <TMPL_LOOP NAME="results">

    <TR>

     <TMPL_LOOP NAME="attributes">

     <TD><TMPL_VAR NAME="attribute"></TD>

     </TMPL_LOOP>

    </TR>

   </TMPL_LOOP>

  </TABLE>

 </BODY>

</HTML>

While we're at it, this program has gone too long without taint checking, so we add the -T command-line flag to the shebang line. That requires that we untaint inputs that might be used in potentially dangerous operations like opening files. These inputs will only be used in LDAP searches, however, so they don't need untainting for the program to run; but this line of inquiry has put us in a cautious state of mind; could there be any kind of malicious or accidental input that could cause an LDAP search to fail?

It's apparent from looking at the way a filter (LDAP terminology for "search query") is constructed that a value that contained a right parenthesis would cause problems because it would appear that the filter ended at that point and contained extraneous characters. That is enough to make us wonder what other characters might cause a problem, and we browse the Net::LDAP documentation to find out. Finding the module Net::LDAP::Filter in a list, we see its documentation referring to the authoritative reference on the subject, a Request for Comments (RFC): http://www.ietf.org/rfc/rfc2254.txt. That tells us that these characters must be escaped in filter values via hexadecimal encoding: (, ), *, \, NUL.

Before we reach for the substitution operator, however, we should be lazy and see if someone else has done the work for us . . . and indeed they ("they" being Graham Barr) have, in Net::LDAP::Filter, the documentation for which states ". . . lets you directly manipulate LDAP filters without worrying about the string representation and all the associated escaping mechanisms." And the documentation for the search() method of Net::LDAP—which we read assiduously before using it, right?—says that the filter parameter can be text, or a Net::LDAP::Filter object. So all we have to do is wrap the filter text inside a Net::LDAP::Filter constructor, and the resulting object can be used in lieu of the filter text, without having to worry about escaping naughty characters.

All this documentation reading has revealed something else to us: The Net::LDAP class supports an option called onerror, which affects what happens when a Net::LDAP function has a problem. The code as it stands laboriously checks to see whether the code() method of the Net::LDAP object returns a nonzero value, indicating error, after every method call . Or does it? Isn't the entries() method a potential point of failure given that it involves a round trip to the server?

Well, why bother checking when we can just use onerror to ensure that any problem will be reported to the police; in this case, the "police" will be an exception handler:

Example 11.13. dirsearch.cgi, Version 8

1  #!/usr/bin/perl -T

2  use strict;

3  use warnings;

4

5  use CGI qw(param header);

6  use Net::LDAP;

7  use Net::LDAP::Filter;

8  use HTML::Template;

9

10 my %SEARCH_OPTS =

11    (base  => 'ou=People, dc=wp, dc=emerald, dc=city, dc=oz',

12     scope => 'sub');

13 my $LDAP_SERVER = "whitepages";

14 my @LDAP_OPTS = ($LDAP_SERVER, timeout => 10, onerror => 'die');

15 my @OUTPUT_ATTRS = qw(username location haircolor

16                       telephone email fax name);

17 my %SEARCH_ATTR = map { ($_ => 1) }

18                       qw(username location haircolor);

19

20 my $filter;

21 foreach my $input (param)

22 {

23   next unless $SEARCH_ATTR{$input};

24   my ($value) = param($input);

25   $filter and do_error("Cannot lookup by >1 attribute... pick one only");

26   $filter = "($input=$value)";

27 }

28 $filter or do_error("Need an attribute to search on");

29

30 my $ldap = Net::LDAP->new(@LDAP_OPTS)

31   or do_email_error("Can't connect to $LDAP_SERVER: $@");

32 eval

33 {

34   $ldap->bind;

35   my $res = $ldap->search(%SEARCH_OPTS,

36              filter => Net::LDAP::Filter->new($filter));

37

38   $res->count or do_error("No match for $filter");

39

40   my $tem = HTML::Template->new(filename => "output.tmpl");

41   my @results;

42   foreach my $ent ($res->entries)

43   {

44     push @results, { attributes =>

45              [ map +{ attribute => $ent->get_value($_) } =>

46                    @OUTPUT_ATTRS ] };

47   }

48   $tem->param(filter    => $filter,

49               attributes => [ map +{ (attribute => $_) } => @OUTPUT_ATTRS ]

50               results    => \@results);

51   print header, $tem->output;

52 };           # Remember the semicolon here!

53

54 do_email_error($@) if $@;

Once again, the remaining subroutines have not changed. We removed the unbind() method call because it is superfluous at the end of the program—the connection will be unbound automatically on program exit. Note that we still handle the case of no matches differently; that is one of the normal possible outcomes of the program, we don't need e-mail sent to us about it. Unfortunately, Net::LDAP::new() does not throw an exception if it fails, it returns undef and sets $@ instead.

    Previous Table of Contents Next