home | O'Reilly's CD bookshelfs | FreeBSD | Linux | Cisco | Cisco Exam  


Writing Apache Modules with Perl and C
By:   Lincoln Stein and Doug MacEachern
Published:   O'Reilly & Associates, Inc.  - March 1999

Copyright © 1999 by O'Reilly & Associates, Inc.


 


   Show Contents   Previous Page   Next Page

Chapter 4 - Content Handlers
Redirection

In this section...

Introduction
Internal Redirection

Introduction

   Show Contents   Go to Top   Previous Page   Next Page

Instead of synthesizing a document, a content handler has the option of redirecting the browser to fetch a different URI using the HTTP redirect mechanism. You can use this facility to randomly select a page or picture to display in response to a URI request (many banner ad generators work this way) or to implement a custom navigation system.

Redirection is extremely simple with the Apache API. You need only add a Location field to the HTTP header containing the full or partial URI of the desired destination, and return a REDIRECT result code. A complete functional example using mod_perl is only a few lines (Example 4-8). This module, named Apache::GoHome, redirects users to the hardcoded URI http://www.ora.com/. When the user selects a document or a portion of the document tree that this content handler has been attached to, the browser will immediately jump to that URI.

The module begins by importing the REDIRECT error code from Apache::Constants (REDIRECT isn't among the standard set of result codes imported with :common). The handler() method then adds the desired location to the outgoing headers by calling Apache::header_out(). header_out() can take one or two arguments. Called with one argument, it returns the current value of the indicated HTTP header field. Called with two arguments, it sets the field indicated by the first argument to the value indicated by the second argument. In this case, we use the two-argument form to set the HTTP Location field to the desired URI.

The final step is to return the REDIRECT result code. There's no need to generate an HTML body, since most HTTP-compliant browsers will take you directly to the Location URI. However, Apache adds an appropriate body automatically in order to be HTTP-compliant. You can see the header and body message using telnet:

% telnet localhost 80
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET /gohome HTTP/1.0
HTTP/1.1 302 Moved Temporarily
Date: Mon, 05 Oct 1998 22:15:17 GMT
Server: Apache/1.3.3-dev (Unix) mod_perl/1.16
Location: http://www.ora.com/
Connection: close
Content-Type: text/html
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<HTML><HEAD>
<TITLE>302 Moved Temporarily</TITLE>
</HEAD><BODY>
<H1>Moved Temporarily</H1>
The document has moved <A HREF="http://www.ora.com/">here</A>.<P>
</BODY></HTML>
Connection closed by foreign host.

You'll notice from this example that the REDIRECT status causes a "Moved Temporarily" message to be issued. This is appropriate in most cases because it makes no warrants to the browser that it will be redirected to the same location the next time it tries to fetch the desired URI. If you wish to redirect permanently, you should use the MOVED status code instead, which results in a "301 Moved Permanently" message. A smart browser might remember the redirected URI and fetch it directly from its new location the next time it's needed.

Example 4-8. Generating a Redirect from a Content Handler

package Apache::GoHome;
# file: Apache/GoHome.pm
use strict;
use Apache::Constants qw(REDIRECT);
sub handler {
 my $r = shift;
 $r->content_type('text/html');
 $r->header_out(Location => 'http://www.ora.com/');
 return REDIRECT;
}
1;
__END__

As a more substantial example of redirection in action, consider Apache::RandPicture (Example 4-9) which randomly selects a different image file to display each time it's called. It works by selecting an image file from among the contents of a designated directory, then redirecting the browser to that file's URI. In addition to demonstrating a useful application of redirection, it again shows off the idiom for interconverting physical file names and URIs.

The handler begins by fetching the name of a directory to fetch the images from, which is specified in the server configuration file by the Perl variable PictureDir. Because the selected image has to be directly fetchable by the browser, the image directory must be given as a URI rather than as a physical path.

The next task is to convert the directory URI into a physical directory path. The subroutine adds a / to the end of the URI if there isn't one there already (ensuring that Apache treats the URI as a directory), then calls the request object's lookup_uri() and filename() methods in order to perform the URI translation steps. The code looks like this:

    my $subr = $r->lookup_uri($dir_uri);
   my $dir = $subr->filename;

Now we need to obtain a listing of image files in the directory. The simple way to do this would be to use the Perl glob operator, for instance:

chdir $dir;
@files = <*.{jpg,gif}>;

However, this technique is flawed. First off, on many systems the glob operation launches a C subshell, which sends performance plummeting and won't even work on systems without the C shell (like Win32 platforms). Second, it makes assumptions about the extension types of image files. Your site may have defined an alternate extension for image files (or may be using a completely different system for keeping track of image types, such as the Apache MIME magic module), in which case this operation will miss some images.

Instead, we create a DirHandle object using Perl's directory handle object wrapper. We call the directory handle's read() method repeatedly to iterate through the contents of the directory. For each item we ask Apache what it thinks the file's MIME type should be, by calling the lookup_uri() method to turn the filename into a subrequest and content_type() to fetch the MIME type information from the subrequest. We perform a pattern match on the returned type and, if the file is one of the MIME image types, add it to a growing list of image URIs. The subrequest object's uri() method is called to return the absolute URI for the image. The whole process looks like this:

    my @files;
   for my $entry ($dh->read) {
      # get the file's MIME type
      my $rr = $subr->lookup_uri($entry);
      my $type = $rr->content_type;
      next unless $type =~ m:^image/:;
      push @files, $rr->uri;
   }

Note that we look up the directory entry's filename by calling the subrequest object's lookup_uri() method rather than using the main request object stored in $r. This takes advantage of the fact that subrequests will look up relative paths relative to their own URI.

The next step is to select a member of this list randomly, which we do using this time-tested Perl idiom:

 my $lucky_one = $files[rand @files];

The last step is to set the Location header to point at this file (being sure to express the location as a URI) and to return a REDIRECT result code. If you install the module using the sample configuration file and <IMG> tag shown at the bottom of the listing, a different picture will be displayed every time you load the page.

Example 4-9. Redirecting the Browser to a Randomly Chosen Picture

package Apache::RandPicture;
# file: Apache/RandPicture.pm
use strict;
use Apache::Constants qw(:common REDIRECT);
use DirHandle ();
sub handler {
   my $r = shift;
   my $dir_uri = $r->dir_config('PictureDir');
   unless ($dir_uri) {
      $r->log_reason("No PictureDir configured");
      return SERVER_ERROR;
   }
   $dir_uri .= "/" unless $dir_uri =~ m:/$:;
    my $subr = $r->lookup_uri($dir_uri);
   my $dir = $subr->filename;
   # Get list of images in the directory.
   my $dh = DirHandle->new($dir);
   unless ($dh) {
      $r->log_error("Can't read directory $dir: $!");
return SERVER_ERROR; }
    my @files;
   for my $entry ($dh->read) {
      # get the file's MIME type
      my $rr = $subr->lookup_uri($entry);
      my $type = $rr->content_type;
      next unless $type =~ m:^image/:;
      push @files, $rr->uri;
   }
   $dh->close;
   unless (@files) {
      $r->log_error("No image files in directory");
      return SERVER_ERROR;
   }
    my $lucky_one = $files[rand @files];
   $r->header_out(Location => $lucky_one);
   return REDIRECT;
}
1;
__END__

A configuration section to go with Apache::RandPicture might be:

<Location /random/picture>
  SetHandler  perl-script
  PerlHandler Apache::RandPicture
  PerlSetVar  PictureDir   /banners
</Location>

And you'd use it in an HTML document like this:

<image src="/random/picture" alt="[Our Sponsor]">

Although elegant, this technique for selecting a random image file suffers from a bad performance bottleneck. Instead of requiring only a single network operation to get the picture from the server to the browser, it needs two round-trips across the network: one for the browser's initial request and redirect and one to fetch the image itself.

You can eliminate this overhead in several different ways. The more obvious technique is to get rid of the redirection entirely and simply send the image file directly. After selecting the random image and placing it in the variable $lucky_one, we replace the last two lines of the handler() subroutine with code like this:

    $subr = $r->lookup_uri($lucky_one);
   $r->content_type($subr->content_type);
   $r->send_http_header;
   return OK unless $r->header_only;
   my $fh = Apache::File->new($subr->filename) || return FORBIDDEN;
   $r->send_fd($fh);

We create yet another subrequest, this one for the selected image file, then use information from the subrequest to set the outgoing content type. We then open up the file and send it with the send_fd() method.

However, this is still a little wasteful because it requires you to open up the file yourself. A more subtle solution would be to let Apache do the work of sending the file by invoking the subrequest's run() method. run() invokes the subrequest's content handler to send the body of the document, just as if the browser had made the request itself. The code now looks like this:

   my $subr = $r->lookup_uri($lucky_one);
  unless ($subr->status == DOCUMENT_FOLLOWS) {
      $r->log_error("Can't lookup file $lucky_one}: $!");
      return SERVER_ERROR;
  }
  $r->content_type($subr->content_type);
  $r->send_http_header;
  return OK if $r->header_only;
  $subr->run;
  return OK;

We call lookup_uri() and check the value returned by its status() method in order to make sure that it is DOCUMENT_FOLLOWS (status code 200, the same as HTTP_OK). This constant is not exported by Apache::Constants by default but has to be imported explicitly. We then set the main request's content type to the same as that of the subrequest, and send off the appropriate HTTP header. Finally, we call the subrequest's run() method to invoke its content handler and send the contents of the image to the browser.

   Show Contents   Go to Top   Previous Page   Next Page
Copyright © 1999 by O'Reilly & Associates, Inc.