Apache-AuthCAS
view release on metacpan or search on metacpan
lib/Apache/AuthCAS.pm view on Meta::CPAN
# Apache::AuthCAS
# David Castro, April 2004
# $Revision: 1.7 $
#
# Apache auth module to protect underlying resources using Yale's Central
# Authentication service
package Apache::AuthCAS;
$^W = 1;
use diagnostics;
use warnings;
use strict;
use mod_perl qw(StackedHandlers MethodHandlers Authen Authz);
use constant MP2 => $mod_perl::VERSION >= 1.99;
use vars qw($INITIALIZED $SESSION_CLEANUP_COUNTER);
BEGIN {
if (MP2) {
require Apache::Const;
require APR::URI;
Apache::Const->import(-compile => qw(FORBIDDEN HTTP_MOVED_TEMPORARILY OK DECLINED HTTP_OK));
} else {
require Apache::Constants;
Apache::Constants->import(qw(FORBIDDEN HTTP_MOVED_TEMPORARILY OK DECLINED HTTP_OK));
}
}
use Apache::URI;
use Net::SSLeay;
use MIME::Base64;
use DBI;
# logging flags
my $LOG_ERROR = "0";
my $LOG_WARN = "1";
my $LOG_INFO = "2";
my $LOG_DEBUG = "3";
my $LOG_INSANE = "4";
my $DEFAULT_LOG_LEVEL = $LOG_ERROR;
my $LOG_LEVEL = $DEFAULT_LOG_LEVEL;
# the URL the client is redirected to when an error occurs
my $DEFAULT_ERROR_URL="http://localhost/cas/error/";
my $ERROR_URL=$DEFAULT_ERROR_URL;
# error codes
my $DB_ERROR_CODE = "Database Service Error";
my $PGT_ERROR_CODE = "CAS Proxy Service Error";
my $INVALID_ST_ERROR_CODE = "Invalid Service Ticket";
my $INVALID_PGT_ERROR_CODE = "Invalid Proxy Granting Ticket";
my $MISSING_NETID_ERROR_CODE = "CAS failed to return NetID";
my $CAS_CONNECT_ERROR_CODE = "CAS couldn't validate service ticket";
# the URL a client is redirected to after logging in
my $SERVICE="";
# the service proxy tickets will be granted for
my $PROXY_SERVICE="";
# the host name of the CAS server
my $CAS_HOST="";
my $DEVEL_CAS_HOST="devel.localhost";
my $PROD_CAS_HOST="localhost";
# the port number for the CAS server
my $CAS_PORT="";
my $DEVEL_CAS_PORT="443";
my $PROD_CAS_PORT="443";
# CAS login URI
my $DEFAULT_CAS_LOGIN_URI="/cas/login";
my $CAS_LOGIN_URI=$DEFAULT_CAS_LOGIN_URI;
# CAS logout URI
my $DEFAULT_CAS_LOGOUT_URI="/cas/logout";
my $CAS_LOGOUT_URI=$DEFAULT_CAS_LOGOUT_URI;
# CAS proxy URI
my $DEFAULT_CAS_PROXY_URI="/cas/proxy";
my $CAS_PROXY_URI=$DEFAULT_CAS_PROXY_URI;
# CAS proxy validate URI
my $DEFAULT_CAS_PROXY_VALIDATE_URI="/cas/proxyValidate";
my $CAS_PROXY_VALIDATE_URI=$DEFAULT_CAS_PROXY_VALIDATE_URI;
# CAS service validate URI
my $DEFAULT_CAS_SERVICE_VALIDATE_URI="/cas/serviceValidate";
my $CAS_SERVICE_VALIDATE_URI=$DEFAULT_CAS_SERVICE_VALIDATE_URI;
# parameter used to pass in PGTIOU
my $PGT_IOU_PARAM = "pgtIou";
# parameter used to pass in PGT
my $PGT_ID_PARAM = "pgtId";
# number of proxy tickets to give the underlying application
my $DEFAULT_NUM_PROXY_TICKETS = 1;
my $NUM_PROXY_TICKETS = $DEFAULT_NUM_PROXY_TICKETS;
# the name of the cookie that will be used for sessions
my $DEFAULT_SESSION_COOKIE_NAME = "APACHECAS";
my $SESSION_COOKIE_NAME = $DEFAULT_SESSION_COOKIE_NAME;
# the domain the session cookies will be sent for
my $DEFAULT_SESSION_COOKIE_DOMAIN = "";
my $SESSION_COOKIE_DOMAIN = "";
# the max time before a session expires (in seconds)
my $DEFAULT_SESSION_TIMEOUT = 1800;
my $SESSION_TIMEOUT = $DEFAULT_SESSION_TIMEOUT;
# the name of the DBI database driver
my $DB_DRIVER = "";
my $DEVEL_DB_DRIVER = "Pg";
my $PROD_DB_DRIVER = "Pg";
# the host name of the database server
my $DB_HOST = "";
my $DEVEL_DB_HOST = "devel.localhost";
my $PROD_DB_HOST = "localhost";
# the port number of the database server
my $DB_PORT = "";
my $DEVEL_DB_PORT = "5432";
my $PROD_DB_PORT = "5432";
# the name of the database for sessions/pgtiou mapping
my $DB_NAME = "";
my $DEVEL_DB_NAME = "apache_cas";
my $PROD_DB_NAME = "apache_cas";
# the name of the session table
my $DB_SESSION_TABLE = "";
my $DEVEL_DB_SESSION_TABLE = "cas_sessions";
my $PROD_DB_SESSION_TABLE = "cas_sessions";
# the name of the pgtiou to pgt mapping table
my $DB_PGTIOU_TABLE = "";
my $DEVEL_DB_PGTIOU_TABLE = "cas_pgtiou_to_pgt";
my $PROD_DB_PGTIOU_TABLE = "cas_pgtiou_to_pgt";
# the user to connnect to the database with
my $DB_USER = "";
my $DEVEL_DB_USER = "develuser";
my $PROD_DB_USER = "produser";
# the password to connect to the databse with
my $DB_PASS = "";
my $DEVEL_DB_PASS = "develpass";
my $PROD_DB_PASS = "prodpass";
# whether or not we want redirect magic to remove service ticket from URL
my $DEFAULT_REMOVE_TICKET = "0";
my $REMOVE_TICKET = $DEFAULT_REMOVE_TICKET;
# are we running with production config, or other?
my $PRODUCTION = "0";
# session cleanup threshold (1 in N requests, session cleanup will occur for
# each Apache thread or process - i.e. for 10 processes, it may take as many as
# 100 requests before session cleanup is performed for a threshold of 10)
my $SESSION_CLEANUP_THRESHOLD = "10";
# when set to true, this module will attempt to make the underlying authz
lib/Apache/AuthCAS.pm view on Meta::CPAN
# make sure the session is still valid
Apache->warn("$$: CAS: authenticate(): session last_accessed=$last_accessed") unless ($LOG_LEVEL < $LOG_DEBUG);
if ($last_accessed + $SESSION_TIMEOUT >= time()) {
# session is still valid
Apache->warn("$$: CAS: authenticate(): session '$sid' is still valid") unless ($LOG_LEVEL < $LOG_DEBUG);
# record the last time the session was accessed
$session_data[1] = time();
Apache->warn("$$: CAS: authenticate(): setting last accessed time to '".time()."'") unless ($LOG_LEVEL < $LOG_DEBUG);
# if something bad happened, like database unavailability
if (!$self->set_session_data(@session_data)) {
Apache->warn("$$: CAS: authenticate(): problem saving session data, redirecting to the error page") unless ($LOG_LEVEL < $LOG_ERROR);
return $self->redirect($r, $ERROR_URL, $DB_ERROR_CODE);
} else {
Apache->warn("$$: CAS: authenticate(): saved session data: ".join(",",@session_data)) unless ($LOG_LEVEL < $LOG_DEBUG);
}
# set the pgtiou
$user = $session_data[2];
$pgtiou = $session_data[3];
if ($PROXY_SERVICE) {
return $self->do_proxy($r, $sid, $pgtiou, $user, 0);
} else {
# no proxy stuff, so we are done
Apache->warn("$$: CAS: authenticate(): no proxy stuff, we are done") unless ($LOG_LEVEL < $LOG_DEBUG);
Apache->warn("$$: CAS: authenticate(): setting header CAS_FILTER_USER=$user") unless ($LOG_LEVEL < $LOG_DEBUG);
$r->header_in('CAS_FILTER_USER', $user);
if ($PRETEND_BASIC_AUTH) {
# setup this up for underlying authz modules that rely on Basic auth having been performed
$r->header_in('Authorization', "Basic " . encode_base64($user . ":DUMMYPASS"));
$r->user($user);
$r->connection->user($user);
$r->connection->auth_type("Basic");
}
return (MP2 ? Apache::OK : Apache::Constants::OK);
}
} else {
Apache->warn("$$: CAS: authenticate(): session '$sid' has expired") unless ($LOG_LEVEL < $LOG_DEBUG);
if (!$self->delete_session_data($sid)) {
Apache->warn("$$: CAS: authenticate(): couldn't delete expired session id='$sid'") unless ($LOG_LEVEL < $LOG_WARN);
}
Apache->warn("$$: CAS: authenticate(): deleted expired session '$sid'") unless ($LOG_LEVEL < $LOG_DEBUG);
$sid = "";
}
} else {
Apache->warn("$$: CAS: authenticate(): session '$sid' is invalid") unless ($LOG_LEVEL < $LOG_DEBUG);
$sid = "";
}
}
# note: not an else if, because we may find an invalid session id and
# fallback to ticket
# if we have a service ticket
if (($sid eq "") and ($ticket ne "")) {
# validate service ticket through CAS, since no valid cookie was found
my %properties = $self->validate_service_ticket($r, $ticket, $PROXY_SERVICE ?"1":"0");
if ($properties{'error'}) {
# error occurred validating service ticket
return $self->redirect($r, $ERROR_URL, $properties{'error'});
} else {
Apache->warn("$$: CAS: authenticate(): valid service ticket '$ticket'") unless ($LOG_LEVEL < $LOG_DEBUG);
}
$pgtiou = $properties{'pgtiou'} || "";
$user = $properties{'user'} || "";
# we should get back a netid when validating a service ticket
if ($user eq "") {
return $self->redirect($r, $ERROR_URL, $MISSING_NETID_ERROR_CODE);
}
$sid = &create_session_id();
Apache->warn("$$: CAS: authenticate(): setting sid='$sid' for netid='$user'") unless ($LOG_LEVEL < $LOG_DEBUG);
# map a new session id to this pgtiou and give the client a cookie
my $time = time();
Apache->warn("$$: CAS: authenticate(): trying to save session data: ".join(",",$sid, $time, $user, $pgtiou)) unless ($LOG_LEVEL < $LOG_DEBUG);
if (!$self->set_session_data($sid, $time, $user, $pgtiou)) {
# if something bad happened, like database unavailability
Apache->warn("$$: CAS: authenticate(): problem saving session data, redirecting to the error page") unless ($LOG_LEVEL < $LOG_ERROR);
return $self->redirect($r, $ERROR_URL, $DB_ERROR_CODE);
} else {
Apache->warn("$$: CAS: authenticate(): saved session data: ".join(",",$sid, $time, $user, $pgtiou)) unless ($LOG_LEVEL < $LOG_DEBUG);
}
Apache->warn("$$: CAS: authenticate(): sending session cookie") unless ($LOG_LEVEL < $LOG_DEBUG);
my $cookie = "$SESSION_COOKIE_NAME=$sid;path=/";
if ($SESSION_COOKIE_DOMAIN ne "") {
$cookie .= ";domain=.$SESSION_COOKIE_DOMAIN";
}
# send the cookie to the browser
$r->header_out("Set-Cookie" => $cookie);
# in case we redirect (considered an "error")
$r->err_header_out("Set-Cookie" => $cookie);
} else {
Apache->warn("$$: CAS: authenticate(): no valid session id or ticket") unless ($LOG_LEVEL < $LOG_DEBUG);
return $self->redirect_login($r);
}
Apache->warn("$$: CAS: authenticate(): got user: '$user'") unless ($LOG_LEVEL < $LOG_DEBUG);
Apache->warn("$$: CAS: authenticate(): got PGTIOU: '$pgtiou'") unless ($LOG_LEVEL < $LOG_DEBUG);
if ($PROXY_SERVICE) {
return $self->do_proxy($r, $sid, $pgtiou, $user, 1);
} else {
# no proxy stuff, so we are done
Apache->warn("$$: CAS: authenticate(): no proxy stuff, so we are done") unless ($LOG_LEVEL < $LOG_DEBUG);
# redirect to this same page minus the ticket
if (($REMOVE_TICKET eq "true") || ($REMOVE_TICKET eq "1")) {
Apache->warn("$$: CAS: authenticate(): setting header CAS_FILTER_USER=$user") unless ($LOG_LEVEL < $LOG_DEBUG);
$r->header_in('CAS_FILTER_USER', $user);
lib/Apache/AuthCAS.pm view on Meta::CPAN
return (MP2 ? Apache::HTTP_MOVED_TEMPORARILY : Apache::Constants::HTTP_MOVED_TEMPORARILY);
}
sub redirect_login($$) {
my $self = shift;
my $r = shift;
Apache->warn("$$: CAS: redirect_login()") unless ($LOG_LEVEL < $LOG_DEBUG);
my $service;
if ($SERVICE eq "") {
# use the current URL as the service
$service = $self->this_url_encoded($r);
} else {
# use the static entry point into this service
$service = $self->urlEncode($SERVICE);
}
Apache->warn("$$: CAS: redirect_login(): redirecting to CAS for service: '$service'") unless ($LOG_LEVEL < $LOG_INFO);
my $redirect_url = "https://$CAS_HOST:$CAS_PORT$CAS_LOGIN_URI?service=$service";
$r->header_out("Location" => $redirect_url);
return (MP2 ? Apache::HTTP_MOVED_TEMPORARILY : Apache::Constants::HTTP_MOVED_TEMPORARILY);
}
sub redirect($$) {
my $self = shift;
my $r = shift;
my $url = shift || "";
my $errcode = shift || "";
Apache->warn("$$: CAS: redirect()") unless ($LOG_LEVEL < $LOG_DEBUG);
if ($url) {
my $service;
if ($SERVICE eq "") {
# use the current URL as the service
$service = $self->this_url_encoded($r);
Apache->warn("$$: CAS: redirect(): using self as service") unless ($LOG_LEVEL < $LOG_DEBUG);
} else {
# use the static entry point into this service
$service = $self->urlEncode($SERVICE);
Apache->warn("$$: CAS: redirect(): using configured service") unless ($LOG_LEVEL < $LOG_DEBUG);
}
$r->header_out("CAS_FILTER_CAS_HOST", $CAS_HOST);
$r->header_out("CAS_FILTER_CAS_PORT", $CAS_PORT);
$r->header_out("CAS_FILTER_CAS_LOGIN_URI", $CAS_LOGIN_URI);
$r->header_out("CAS_FILTER_SERVICE", $service);
Apache->warn("$$: CAS: redirect(): redirecting to url: '$url' service: '$service'") unless ($LOG_LEVEL < $LOG_INFO);
$r->header_out("Location" => "$url?login_url=https://$CAS_HOST:$CAS_PORT$CAS_LOGIN_URI&service=$service&errcode=$errcode");
return (MP2 ? Apache::HTTP_MOVED_TEMPORARILY : Apache::Constants::HTTP_MOVED_TEMPORARILY);
} else {
Apache->warn("$$: CAS: redirect(): no redirect URL, displaying message") unless ($LOG_LEVEL < $LOG_INFO);
$r->content_type ('text/html');
$r->print("<html><body>service misconfigured</body></html>");
$r->rflush;
return (MP2 ? Apache::HTTP_OK : Apache::Constants::HTTP_OK);
}
}
# params
# apache request object
# ticket to be validated
# 1 or 0, whether we need proxy tickets
# returns a hash with keys on success
# 'user', 'pgtiou'
# NULL on failure
sub validate_service_ticket($$) {
my $self = shift;
my $r = shift;
my $ticket = shift;
my $proxy = shift;
Apache->warn("$$: CAS: validate_service_ticket(): validating service ticket '$ticket' through CAS") unless ($LOG_LEVEL < $LOG_DEBUG);
my %properties;
my $service;
if ($SERVICE eq "") {
# use the current URL as the service
$service = $self->this_url_encoded($r);
} else {
# use the static entry point into this service
$service = $self->urlEncode($SERVICE);
}
Apache->warn("$$: CAS: validate_service_ticket(): requesting validation for service: '$service'") unless ($LOG_LEVEL < $LOG_DEBUG);
my $tmp;
# FIXME - diff urls for proxy vs. none?
if ($proxy) {
$tmp = $CAS_PROXY_VALIDATE_URI . "?service=$service&ticket=$ticket&pgtUrl=$service";
} else {
$tmp = $CAS_SERVICE_VALIDATE_URI . "?service=$service&ticket=$ticket";
}
Apache->warn("$$: CAS: validate_service_ticket(): request URL: '$tmp'") unless ($LOG_LEVEL < $LOG_DEBUG);
if ($LOG_LEVEL >= $LOG_INSANE) {
$Net::SSLeay::trace = 3; # 0=no debugging, 1=ciphers, 2=trace, 3=dump data
} else {
$Net::SSLeay::trace = 0; # 0=no debugging, 1=ciphers, 2=trace, 3=dump data
}
#$Net::SSLeay::linux_debug = 1;
my ($page, $response, %reply_headers) = Net::SSLeay::get_https($CAS_HOST, $CAS_PORT, $tmp);
# if we had some type of connection problem
if (!defined($page)) {
Apache->warn("$$: CAS: validate_service_ticket(): error validating service");
$properties{'error'} = $CAS_CONNECT_ERROR_CODE;
return %properties;
}
Apache->warn("$$: CAS: validate_service_ticket(): page: $page") unless ($LOG_LEVEL < $LOG_INSANE);
Apache->warn("$$: CAS: validate_service_ticket(): response: $response") unless ($LOG_LEVEL < $LOG_INSANE);
# FIXME - add a check for a 404 error/other errors
if ($page =~ /<cas:user>([^<]+)<\/cas:user>/) {
my $user = $1;
chomp $user;
Apache->warn("$$: CAS: validate_service_ticket(): valid service ticket, user '$user' authenticated") unless ($LOG_LEVEL < $LOG_DEBUG);
$properties{'user'} = $user;
# only try to get PGTIOU if we are doing proxy stuff
if ($proxy) {
if ($page =~ /<cas:proxyGrantingTicket>([^<]+)<\/cas:proxyGrantingTicket>/) {
Apache->warn("$$: CAS: validate_service_ticket(): got pgt='$1' for user='$user'") unless ($LOG_LEVEL < $LOG_DEBUG);
if ($1 ne "") {
$properties{'pgtiou'} = $1;
} else {
Apache->warn("$$: CAS: validate_service_ticket(): empty PGT in response from CAS") unless ($LOG_LEVEL < $LOG_ERROR);
}
} else {
Apache->warn("$$: CAS: validate_service_ticket(): no PGT in response from CAS") unless ($LOG_LEVEL < $LOG_ERROR);
$properties{'error'} = $PGT_ERROR_CODE;
return %properties;
}
}
} else {
Apache->warn("$$: CAS: validate_service_ticket(): invalid service ticket, user denied access") unless ($LOG_LEVEL < $LOG_DEBUG);
$properties{'error'} = $INVALID_ST_ERROR_CODE;
return %properties;
}
return %properties;
}
sub send_proxysuccess($$) {
my $self = shift;
my $r = shift;
Apache->warn("$$: CAS: send_proxysuccess(): sending proxy success for CAS callback") unless ($LOG_LEVEL < $LOG_DEBUG);
$r->content_type("text/html");
$r->print("<casClient:proxySuccess xmlns:casClient=\"http://www.yale.edu/tp/casClient\"/>\n");
$r->rflush();
return (MP2 ? Apache::OK : Apache::Constants::OK);
}
sub get_proxy_tickets($$) {
my $self = shift;
my $pgt = shift;
my $target = shift;
my $num_tickets = shift;
Apache->warn("$$: CAS: get_proxy_tickets()") unless ($LOG_LEVEL < $LOG_DEBUG);
my @tickets;
for (my $i=0; $i < $num_tickets; $i++) {
my $uri = "$CAS_PROXY_URI?pgt=$pgt&targetService=$target";
Apache->warn("$$: CAS: get_proxy_tickets(): using PGT to obtain PT: calling URL '$uri'") unless ($LOG_LEVEL < $LOG_DEBUG);
if ($LOG_LEVEL >= $LOG_INSANE) {
$Net::SSLeay::trace = 3; # 0=no debugging, 1=ciphers, 2=trace, 3=dump data
} else {
$Net::SSLeay::trace = 0; # 0=no debugging, 1=ciphers, 2=trace, 3=dump data
}
my ($page, $response, %reply_headers) = Net::SSLeay::get_https($CAS_HOST, $CAS_PORT, $uri);
if ($page =~ /<cas:proxySuccess>/) {
Apache->warn("$$: CAS: get_proxy_tickets(): successful proxy request") unless ($LOG_LEVEL < $LOG_DEBUG);
if ($page =~ /<cas:proxyTicket>([^<]+)<\/cas:proxyTicket>/) {
Apache->warn("$$: CAS: get_proxy_tickets(): successfully retrieved proxy ticket") unless ($LOG_LEVEL < $LOG_DEBUG);
push(@tickets, $1);
} else {
Apache->warn("$$: CAS: get_proxy_tickets(): no proxy ticket in response") unless ($LOG_LEVEL < $LOG_DEBUG);
return qw();
}
} else {
Apache->warn("$$: CAS: get_proxy_tickets(): unsuccessful proxy request") unless ($LOG_LEVEL < $LOG_DEBUG);
return qw();
}
}
if (@tickets) {
return @tickets;
} else {
return qw();
lib/Apache/AuthCAS.pm view on Meta::CPAN
# the port number for the CAS server
PerlSetVar CASPort "443"
# are we running with production config or dev config
PerlSetVar CASProduction "1"
# the URL a client is redirected to after logging in
PerlSetVar CASService "https://somedomain.com/email/"
# the service proxy tickets will be granted for
PerlSetVar CASProxyService "mail.somedomain.com"
# number of proxy tickets to give the underlying application
PerlSetVar CASNumProxyTickets "2"
# the URL the client is redirected to when an error occurs
PerlSetVar CASErrorURL "https://somedomain.com/error/"
# the name of the DBI database driver
PerlSetVar CASDatabaseDriver "Pg"
# the host name of the database server
PerlSetVar CASDatabaseHost "db.somedomain.com"
# the port number of the database server
PerlSetVar CASDatabasePort "5433"
# the name of the database for sessions/pgtiou mapping
PerlSetVar CASDatabaseName "cas"
# the user to connnect to the database with
PerlSetVar CASDatabaseUser "dbuser"
# the password to connect to the databse with
PerlSetVar CASDatabasePass "dbpass"
# the name of the session table
PerlSetVar CASDatabaseSessionTable "cas_sessions"
# the name of the pgtiou to pgt mapping table
PerlSetVar CASDatabasePGTIOUTable "cas_pgtiou_to_pgt"
# the level of logging
PerlSetVar CASLogLevel "4"
# whether we should perform a redirect, stripping the service ticket
# once we have already created a session for the client
PerlSetVar CASRemoveTicket "true"
# the name of the cookie that will be used for sessions
PerlSetVar CASSessionCookieName "APACHECAS"
# the max time before a session expires (in seconds)
PerlSetVar CASSessionTimeout "1800"
# not currently able to override through Apache configuration:
# CAS login URI
# CAS logout URI
# CAS proxy URI
# CAS proxy validate URI
# CAS service validate URI
# parameter used to pass in PGTIOU
# parameter used to pass in PGT
# session cleanup threshold
# basic authentication emulation
=head1 NOTES
Any options that are not set in the Apache configuration will default to the
values preconfigured in the Apache::AuthCAS module. Either explicitly override
those options that do not match your environment or set them in the module
itself.
=head1 COMPATIBILITY
This module should work in both mod_perl 1 and 2. For Apache 2/mod_perl 2, the
Apache::compat may need to be loaded in your mod_perl startup script. This can
be done by adding:
use Apache::compat;
into the script included by the PerlRequire directive in your Apache
configuration. For instance, if your Apache configuration includes the line:
PerlRequire /usr/local/sbin/modperl_startup.pl
then the "use" line mentioned above should be added to this file. Consult the
mod_perl documentation for more information regarding mod_perl startup scripts.
=head1 SEE ALSO
=head2 Official Yale CAS Website
http://www.yale.edu/tp/auth/
=head2 mod_perl Documentation
http://perl.apache.org/
=head1 AUTHOR
David Castro <dcastro@apu.edu>
=head1 COPYRIGHT
Copyright (C) 2004 David Castro <dcastro@apu.edu>
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation; either version 2 of the License, or (at your option) any later
version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
this program; if not, write to the Free Software Foundation, Inc.,
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
( run in 2.034 seconds using v1.01-cache-2.11-cpan-140bd7fdf52 )