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 8 - Customizing the Apache Configuration Process / The Apache Configuration Directive API
Introduction

Apache provides an API for defining configuration directives. You provide the directive's name, syntax, and a string briefly summarizing the directive's intended usage. You may also limit the applicability of the directive to certain parts of the configuration files. Apache parses the directive and passes the parsed structure to your module for processing. Your module will then use this information to set up global variables or initialize whatever it needs.

The process of defining new configuration directives is not as simple as other parts of the Perl API. This is because configuration directives are defined in a compiled C structure that cannot be built dynamically at runtime. In order to work with this restriction, mod_perl requires you to take the following roundabout route:

  1. Create an empty module directory with h2xs.
  2. Modify the newly created Makefile.PL file to declare an array containing the definitions for the new configuration directives and to invoke the command_table() function from a helper module named Apache::ExtUtils.
  3. Write a .pm file containing Perl handlers for each of the configuration directives you define.
  4. Run perl Makefile.PL to autogenerate a .xs file.
  5. Run make and make install to create the loadable module and move it into place.
  6. Add a PerlModule directive to the server configuration file to load the module at server startup time.

We'll take you through a short example first to show you the whole process and then get into the details later. Our candidate for adding configuration directives is the Apache::PassThru module (Example 7-10), which transparently maps portions of the server's document tree onto remote servers using Apache's proxy mechanism.

As you may recall, Apache::PassThru used a single PerlSetVar variable named Perl-PassThru, which contained a series of URI=>proxy pairs stored in one long string. Although this strategy is adequate, it's not particularly elegant. Our goal here is to create a new first-class configuration directive named Perl-PassThru. Perl-Pass-Thru will take two arguments: a local URI and a remote URI to map it to. You'll be able to repeat the directive to map several local URIs to remote servers. Because it makes no sense for the directory to appear in directory sections or .htaccess files, PerlPassThru will be limited to the main parts of the httpd.conf, srm.conf, and access.conf files, as well as to <VirtualHost> sections.

First we'll need something to start with, so we use h2xs to create a skeletal module directory:

% h2xs -Af -n Apache::PassThru
Writing Apache/PassThru/PassThru.pm
Writing Apache/PassThru/PassThru.xs
Writing Apache/PassThru/Makefile.PL
Writing Apache/PassThru/test.pl
Writing Apache/PassThru/Changes
Writing Apache/PassThru/MANIFEST

The -A and -f command-line switches turn off the generation of autoloader stubs and the C header file conversion steps, respectively. -n gives the module a name. We'll be editing the files Makefile.PL and PassThru.pm. PassThru.xs will be overwritten when we go to make the module, so there's no need to worry about it.

The next step is to edit the Makefile.PL script to add the declaration of the Perl-Pass-Thru directive and to arrange for Apache::ExtUtils' command_table() function to be executed at the appropriate moment. Example 8-1 shows a suitable version of the file.

Example 8-1. Makefile.PL for the Improved Apache::PassThru

package Apache::PassThru;
# File: Apache/PassThru/Makefile.PL
use ExtUtils::MakeMaker;
use Apache::ExtUtils qw(command_table);
use Apache::src ();
my @directives = (
               { name     => 'PerlPassThru',
                 errmsg   => 'a local path and a remote URI to pass through to', 
                  args_how => 'TAKE2',
                 req_override => 'RSRC_CONF'
               }
              );
command_table(\@directives);
WriteMakefile(
   'NAME'     => __PACKAGE__,
   'VERSION_FROM' => 'PassThru.pm',
   'INC'      => Apache::src->new->inc,
   'INSTALLSITEARCH' => '/usr/local/apache/lib/perl',
   'INSTALLSITELIB'  => '/usr/local/apache/lib/perl',
);
__END__

We've made multiple modifications to the Makefile.PL originally produced by h2xs. First, we've placed a package declaration at the top, putting the whole script in the Apache::PassThru namespace. Then, after the original use Ext-Utils::Make-Maker line, we load two mod_perl-specific modules: Apache::Ext-Utils, which defines the command_table() function, and Apache::src, a small utility class that can be used to find the location of the Apache header files. These will be needed during the make.

Next, we define the new configuration directives themselves. We create a list named @directives, each element of which corresponds to a different directive. In this case, we only have one directive to declare, so @directives is one element long.

Each element of the list is an anonymous hash containing one or more of the keys name, errmsg, args_how, and req_override (we'll see later how to implement the most common type of directive using a succinct anonymous array form). name corresponds to the name of the directive, PerlPassThru in this case, and errmsg corresponds to a short message that will be displayed in the event of a configuration syntax error. args_how tells Apache how to parse the directive's arguments. In this case we specify TAKE2, which tells Apache that the directive takes two (and only two) arguments. We'll go over the complete list of parsing options later and also show you a shortcut for specifying parsing options using Perl prototypes.

The last key, req_override, tells Apache what configuration file contexts the directive is allowed in. In this case we specify the most restrictive context, RSRC_CONF, which limits the directive to appearing in the main part of the configuration files or in virtual host sections. Notice that RSRC_CONF is an ordinary string, not a bareword function call!

Having defined our configuration directive array, we pass a reference to it to the command_table() function. When run, this routine writes out a file named PassThru.xs to the current directory. command_table() uses the package information returned by the Perl caller() function to figure out the name of the file to write. This is why it was important to include a package declaration at the top of the script.

The last part of Makefile.PL is the call WriteMakefile(), a routine provided by ExtUtils::MakeMaker and automatically placed in Makefile.PL by h2xs. However, we've modified the autogenerated call in three small but important ways. The INC key, which MakeMaker uses to generate include file switches, has been modified to use the value returned by Apache::src->new->inc (a shorthand way of creating a new Apache::src object and immediately calling its inc() method). This call will return a list of directories that contain various header files needed to build Apache C-language modules. We've also added the keys INSTALLSITEARCH and INSTALLSITELIB to the parameters passed to WriteMakeFile(), in each case specifying the path we use for Apache Perl API modules on our system (you'll have to modify this for your setup). This ensures that when we run make install the module file and its loadable object will be moved to the location of Apache-specific modules rather than the default Perl library directory.

The next step is to modify PassThru.pm to accommodate the new configuration directive. We start with the file from Example 7-10 and add the following lines to the top:

use Apache::ModuleConfig ();
use DynaLoader ();
use vars qw($VERSION);
$VERSION = '1.00';
if($ENV{MOD_PERL}) {
   no strict;
   @ISA = qw(DynaLoader);
   __PACKAGE__->bootstrap($VERSION);
}

This brings in code for fetching and modifying the current configuration settings and loads the DynaLoader module, which provides the bootstrap() routine for loading shared library code. We test the MOD_PERL environment variable to find out if we are running inside httpd and, if so, invoke bootstrap() to load the object file that contains the compiled directory configuration record.

Next, we add the following configuration processing callback routine to the file:

sub PerlPassThru ($$$$) {
 my($cfg, $parms, $local, $remote) = @_;
 $cfg->{PassThru}{$local} = $remote;
}

The callback (also known as the "directive handler") is a subroutine that will be called each time Apache processes a PerlPassThru directive. It is responsible for stashing the information into a configuration record where it can be retrieved later by the handler() subroutine. The name of the subroutine must exactly match the name of the configuration directive, capitalization included. It should also have a prototype that correctly matches the syntax of the configuration directive. All configuration callbacks are called with at least two scalar arguments, indicated by the function prototype ($$). The first argument, $cfg, is the per-directory or per-server object where the configuration data will be stashed. As we will explain shortly, your handlers will recover this object and retrieve the values of the configuration directives it needs. The second argument, $parms, is an Apache::CmdParms object from which you can retrieve various other information about the configuration.

Callbacks may be passed other parameters as well, corresponding to the arguments of the configuration directive that the callback is responsible for. Because PerlPassThru is a TAKE2 directive, we expect two additional arguments, so the complete function prototype is ($$$$).

The body of the subroutine is trivial. For all intents and purposes, the configuration object is a hash reference in which you can store arbitrary key/value pairs. The convention is to choose a key with the same name as the configuration directive. In this case, we use an anonymous hash to store the current local and remote URIs into the configuration object at a key named PassThru. This allows us to have multiple mappings while guaranteeing that each local URI is unique.

The Apache::PassThru handler() subroutine needs a slight modification as well. We remove this line:

my %mappings = split /\s*(?:,|=>)\s*/, $r->dir_config('PerlPassThru');

and substitute the following:

my %mappings = ();
if(my $cfg = Apache::ModuleConfig->get($r)) {
   %mappings = %{ $cfg->{PassThru} } if $cfg->{PassThru};
}

We call the Apache::ModuleConfig class method get(), passing it the request object. This retrieves the same configuration object that was previously processed by PerlPassThru(). We then fetch the value of the configuration object's PassThru key. If the key is present, we dereference it and store it into %mappings. We then proceed as before. Example 8-2 gives the complete code for the modified module.

The last step is to arrange for Apache::PassThru to be loaded at server startup time. The easiest way to do this is to load the module with a PerlModule directive:

PerlModule Apache::PassThru

The only trick to this is that you must be careful that the PerlModule directive is called before any PerlPassThru directives appear. Otherwise, Apache won't recognize the new directive and will abort with a configuration file syntax error. The other caveat is that PerlModule only works to bootstrap configuration directives in mod_perl Versions 1.17 and higher. If you are using an earlier version, use this configuration section instead:

<Perl>
 use Apache::PassThru ();
</Perl>

<Perl> sections are described in detail toward the end of this chapter.

Now change the old Apache::PassThru configuration to use the first-class Perl-PassThru directive:

PerlModule Apache::PassThru
PerlTransHandler Apache::PassThru
PerlPassThru /CPAN   http://www.perl.com/CPAN
PerlPassThru /search http://www.altavista.com

After restarting the server, you should now be able to test the Apache::PassThru handler to confirm that it correctly proxies the /CPAN and /search URIs.

If your server has the mod_info module configured, you should be able to view the entry for the Apache::PassThru module. It will look something like this:

Module Name: Apache::PassThru
Content handlers: none
Configuration Phase Participation: Create Directory Config, Create
      Server Config
Request Phase Participation: none
Module Directives:
      PerlPassThru - a local path and a remote URI to pass through
      to
Current Configuration:
httpd.conf
      PerlPassThru /CPAN http://www.perl.com/CPAN
      PerlPassThru /search http://www.altavista.com

Now try changing the syntax of the PerlPassThru directive. Create a directive that has too many arguments or one that has too few. Try putting the directive inside a <Directory> section or .htaccess file. Any attempt to violate the syntax restrictions we specified in Makefile.PL with the args_how and req_override keys should cause a syntax error at server startup time.

Example 8-2. Apache::PassThru with a Custom Configuration Directive

package Apache::PassThru;
# file: Apache/PassThru.pm;
use strict;
use vars qw($VERSION);
use Apache::Constants qw(:common);
use Apache::ModuleConfig ();
use DynaLoader ();
$VERSION = '1.00';
if($ENV{MOD_PERL}) {
   no strict;
   @ISA = qw(DynaLoader);
   __PACKAGE__->bootstrap($VERSION);
}
sub handler {
   my $r = shift;
   return DECLINED if $r->proxyreq;
   my $uri = $r->uri;
   my %mappings = ();
    if(my $cfg = Apache::ModuleConfig->get($r)) {
      %mappings = %{ $cfg->{PassThru} } if $cfg->{PassThru}; 
}
    foreach my $src (keys %mappings) {
      next unless $uri =~ s/^$src/$mappings{$src}/;
      $r->proxyreq(1);
      $r->uri($uri);
      $r->filename("proxy:$uri");
      $r->handler('proxy-server');
      return OK;
   }
   return DECLINED;
}
sub PerlPassThru ($$$$) {
   my($cfg, $parms, $local, $remote) = @_;
   unless ($remote =~ /^http:/) {
      die "Argument '$remote' is not a URL\n";
   }
   $cfg->{PassThru}{$local} = $remote;
}
1;
__END__

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

HIVE: All information for read only. Please respect copyright!
Hosted by hive КГБ: Киевская городская библиотека