Sign In/My Account | View Cart  
advertisement


Listen Print

Programming with Mason
by Dave Rolsky | Pages: 1, 2

Doing It My Way (Thanks Frank)

But what if you don't want to use Apache::AuthCookie? For example, your site may need to work without using cookies. No doubt this was exactly what Frank Sinatra was thinking about when he sang "My Way," so let's do it our way.

First, we will show an example authentication system that only uses Mason and passes the authentication token around via the URL (actually, via a session).

This example assumes that we already have some sort of session system that passes the session id around as part of the URL, as discussed previously.

We start with a quick login form. We will call this component login_form.html:


  <%args>
   $username => ''
   $password => ''
   $redirect_to => ''
   @errors => ()
  </%args>
  <html>
  <head>
  <title>Mason Book Login</title>
  </head>

  <body>

  % if (@errors) {
  <h2>Errors</h2>
  %   foreach (@errors) {
  <b><% $_ | h %></b><br>
  %   }
  % }

  <form action="login_submit.html">
  <input type="hidden" name="redirect_to" value="<% $redirect_to %>">
  <table align="left">
   <tr>
    <td align="right"><b>Login:</b></td>
    <td><input type="text" name="username" value="<% $username %>"></td>
   </tr>
   <tr>
    <td align="right"><b>Password:</b></td>
    <td><input type="password" name="password" value="<% $password %>"></td>
   </tr>
   <tr>
    <td colspan="2" align="center"><input type="submit" value="Login"></td>
   </tr>
  </table>
  </form>

  </body>
  </html>

This form uses some of the same techniques we show in Chapter 8 ("Building a Mason Site") to pre-populate the form and to handle errors.

Now let's make the component that handles the form submission. This component, called login_submit.html, will check the username and password and, if they are valid, place an authentication token into the user's session:


  <%args>
   $username
   $password
   $redirect_to
  </%args>
  <%init>
   if (my @errors = check_login($username, $password) {
       $m->comp( 'redirect.mas',
                  path => 'login_form.html',
                  query => { errors => \@errors,
                             username => $username,
                             password => $password,
                             redirect_to => $redirect_to } );
   }
 
   $MasonBook::Session{username} = $username;
   $MasonBook::Session{token} =
       Digest::SHA1::sha1_hex( 'My secret phrase', $username );
 
   $m->comp( 'redirect.mas',
             path => $redirect_to );
  </%init>

This component simply checks (via magic hand waving) that the username and password are valid and if they are, it generates an authentication token, which is added to the user's session. To generate this token, we take the username, which is also in the session, and combine it with a secret phrase. We then generate a MAC from those two things.

The authentication and authorization check looks like this:


  if ( $MasonBook::Session{token} ) {
      if ( $MasonBook::Session{token} eq
           Digest::SHA1::sha1_hex( 'My secret phrase',
                                   $MasonBook::Session{username} ) {

          # R<... valid login, do something here>
      } else {
          # R<... someone is trying to be sneaky!>
      }
  } else { # no token
       my $wanted_page = $r->uri;
       
       # Append query string if we have one.
       $wanted_page .= '?' . $r->args if $r->args;

       $m->comp( 'redirect.mas',
                  path => '/login/login_form.html',
                  query => { redirect_to => $wanted_page } );
  }

We could put all the pages that require authorization in a single directory tree and have a top-level autohandler in that tree do the check. If there is no token to check, then we redirect the browser to the login page, and after a successful login they'll return, assuming that they submit valid login credentials.

Access Controls With Attributes

The components we saw previously assumed that there are only two access levels, unauthenticated and authenticated. A more complicated version of this code might involve checking that the user has a certain access level or role.

In that case, we'd first check that we had a valid authentication token and then go on to check that the user actually had the appropriate access rights. This is simply an extra step in the authorization process.

Using attributes, we can easily define access controls for different portions of our site. Let's assume that we have four access levels, "Guest," "User," "Editor" and "Admin." Most of the site is public, and viewable by anyone. Some parts of the site require a valid login, while some require a higher level of privilege.

We implement our access check in our top-level autohandler, , from which all other components must inherit in order for the access control code to be effective.



  <%init>
   my $user = get_user();  # again, hand waving
 
   my $required_access = $m->base_comp->attr('required_access');
 
   unless ( $user->has_access_level($required_access) ) {
      # R<... do something like send them to another page>
   }
 
   $m->call_next;
  </%init>
  <%attr>
   required_access => 'Guest'
  </%attr>

It is crucial that we set a default access level in this autohandler. By doing this, we are saying that by default, all components are accessible by all people, since every visitor will have at least "Guest" access.

We can override this default elsewhere. For example, in a component called /admin/autohandler, we might have:


  <%attr>
   required_access => 'Admin'
  </%attr>

As long as all the components in the directory inherit from the component and don't override the required_access attribute, we have effectively limited that directory (and its subdirectories) to administration users only. If we, for some reason, had an individual component in the directory that we wanted editors to be able to see, we could simply set the "required_access" attribute for that component to "Editor."

Managing DBI Connections

Not infrequently, we see people on the Mason users list asking questions about how to handle caching DBI connections.

Our recipe for this is really simple:


  use Apache::DBI

Rather than reinventing the wheel, use Apache::DBI, which provides the following features:

  • It is completely transparent to use. Once you've used it, you simply call DBI->connect() as always and Apache::DBI gives you an existing handle if one is available.

  • It makes sure that the handle is live, so that if your RDBMS goes down and then back up, your connections still work just fine.

  • It does not cache handles made before Apache forks, as many DBI drivers do not support using a handle after a fork.

Generating Config Files

Config files are a good candidate for generation by Mason. For example, your production and staging Web server config files might differ in only a few areas. Changes to one usually will need to be propagated to another. This is especially true with mod_perl, where Web server configuration can basically be part of a Web-based application.

On top of this, you may decide to set up a per-developer environment, either by having each developer run the necessary software on their own machine, or by starting Web servers on many different ports on a single development server. In this scenario, a template-driven config file generator becomes even more appealing.

Here's a simple script to drive this generation. This script assumes that all the processes are running on one shared development machine.


  #!/usr/bin/perl -w

  use strict;

  use Cwd;
  use File::Spec;
  use HTML::Mason;
  use User::pwent;

  my $comp_root =
      File::Spec->rel2abs( File::Spec->catfile( cwd(), 'config' ) );

  my $output;
  my $interp =
      HTML::Mason::Interp->new( comp_root  => $comp_root,
				out_method => \$output,
			      );

  my $user = getpwuid($<);

  $interp->exec( '/httpd.conf.mas', user => $user );

  my $file =  File::Spec->catfile( $user->dir, 'etc', 'httpd.conf' );
  open FILE, ">$file" or die "Cannot open $file: $!";
  print FILE $output;
  close FILE;

A httpd.conf.mas component might look like this:


  ServerRoot <% $user->dir %>

  PidFile <% File::Spec->catfile( $user->dir, 'logs', 'httpd.pid' ) %>

  LockFile <% File::Spec->catfile( $user->dir, 'logs', 'httpd.lock' ) %>

  Port <% $user->uid + 5000 %>

  # loads Apache modules, defines content type handling, etc.
  <& standard_apache_config.mas &>

  <perl>
   use lib <% File::Spec->catfile( $user->dir, 'project', 'lib' ) %>;
  </perl>

  DocumentRoot <% File::Spec->catfile( $user->dir, 'project', 'htdocs' ) %>

  PerlSetVar MasonCompRoot <% File::Spec->catfile( $user->dir, 'project', 'htdocs' ) %>
  PerlSetVar MasonDataDir <% File::Spec->catfile( $user->dir, 'mason' ) %>

  PerlModule HTML::Mason::ApacheHandler

  <filesmatch "\.html$">
   SetHandler perl-script
   PerlHandler HTML::Mason::ApacheHandler
  </filesmatch>

  <%args>
  $user
  </%args>

This points the server's document root to the developer's working directory. Similarly, it adds the project/lib directory to Perl's @INC via use lib so that the user's working copy of the project's modules are seen first. The server will listen on a port equal to the user's user ID, plus 5,000.

Obviously, this is an incomplete example. It doesn't specify where logs will go, or other necessary config items. It also doesn't handle generating the config file for a server intended to be run by the root user on a standard port.

If You Want More ...

These recipes were adapted from Chapter 11, "Recipes," of Embedding Perl in HTML With Mason. And, of course, the book contains a lot more than just recipes. If you're interested in learning more about Mason, the book is a great place to start.

Also, don't forget to check out the Mason HQ site at www.masonhq.com/, which contains online documentation, user-contributed code and docs, and links to the Mason users mailing list, which is another great resource for developers using Mason.


O'Reilly & Associates recently released (October 2002) Embedding Perl in HTML with Mason.