package Lemonldap::Handlers::Generic4a2; use strict; use warnings; ##### use ###### use Apache(); use Apache::URI(); use Apache::Constants qw(:common :response); use Apache::Session::Memorycached; #use Apache::ModuleConfig; use MIME::Base64; use LWP::UserAgent; use Lemonldap::Config::Parameters; #print STDERR "je passe phase 0\n"; #if(DEBUG) { use Data::Dumper; #} #### common declaration ####### our (@ISA, $VERSION, @EXPORTS); $VERSION = '0.07'; our $VERSION_LEMONLDAP="1.1" ; our $VERSION_INTERNAL="0.03-4" ; #### #### #### my declaration ######### our $UA; our $DEBUG; our $ID_HANDLER; our $CONF; our $PROXY; our $KEYIPC; our $IPCNB; our $LDAPCONTROL; our $DISABLEDCONTROL; our $ATTRLDAP; our $COOKIE; our $PORTAL; our $BASEPUB; our $GLUE; our $BASEPRIV; our $SERVERS; our $CACHE; our $CLIENT; our %STACK; our $PROXYEXT; our $DOMAIN; our $FILE; our $STOPCOOKIE; our $RECURSIF; our $CACHE1_ENTETE; our $GENERAL; our $ID_HANDLER_IN_PROCESS; our $NOM= __PACKAGE__ ; ############################# @ISA = qw(LWP::UserAgent ); #if($ENV{MOD_PERL}) { # push @ISA, 'Dynaloader'; # __PACKAGE__->bootstrap($VERSION); # } #Apache->push_handlers( PerlChildInitHandler=>\&childInit ); ############################### ## ## #our ($VERSION, @EXPORTS); sub handler { my $r=shift; #### retrieve directive and build variables #### but I 'll try it one time # I must know if the handler is call in the same virtualhost ou contener # # ### I will try retieve ID_HANDLER from httpd conf $ID_HANDLER = $r->dir_config('LemonldapHandlerId'); if ($ID_HANDLER) { print STDERR "$NOM: Phase : handler initialization LOAD ID_HANDLER httpd.conf:$ID_HANDLER : succeded\n" if $DEBUG ; } else { # I don't find anything for this handler in order to make link with XLM conf section print STDERR "$NOM: Phase : handler initialization LOAD ID_HANDLER httpd.conf:failed\n" if $DEBUG ; } ############################################ unless ($ID_HANDLER eq $ID_HANDLER_IN_PROCESS ) { ## It doesn t be in the same context ; undef $LDAPCONTROL; undef $CLIENT; } ## now I save the context of handler $ID_HANDLER_IN_PROCESS =$ID_HANDLER; if ($LDAPCONTROL) { print STDERR "$ID_HANDLER: Phase : handler initialization one step beyond\n" if $DEBUG ; } unless ($LDAPCONTROL) { $DEBUG = $r->dir_config('LemonldapDEBUG') ; print STDERR "$ID_HANDLER: Phase : handler initialization DEBUG => $DEBUG\n" if $DEBUG ; print STDERR "$ID_HANDLER: Phase : handler initialization LOAD XML file from httpd.conf\n" if $DEBUG ; ### I will try retieve XML from httpd conf $FILE = $r->dir_config('LemonldapConfig'); $GLUE = $r->dir_config('LemonldapConfigIpcKey'); print STDERR "$ID_HANDLER: Phase : handler initialization LOAD XML file $FILE and $GLUE from httpd.conf\n" if $DEBUG ; ### retrieve domain xml print STDERR "$ID_HANDLER: Phase : handler initialization LOAD DOMAIN httpd.conf\n" if $DEBUG ; ### I will try retieve domain from httpd conf $DOMAIN = $r->dir_config('LemonldapDomain'); #### print STDERR "$ID_HANDLER: Phase : handler initialization LOAD ID_HANDLER httpd.conf\n" if $DEBUG ; ### I will try retieve ID_HANDLER from httpd conf #$ID_HANDLER = $r->dir_config('LemonldapHandlerId'); # if ($ID_HANDLER) { #print STDERR "$ID_HANDLER: Phase : handler initialization LOAD ID_HANDLER httpd.conf:$ID_HANDLER : succeded\n" if $DEBUG ; # # } else { # I don't find anything for this handler in order to make link with XLM conf section #print STDERR "$ID_HANDLER: Phase : handler initialization LOAD ID_HANDLER httpd.conf:failed\n" if $DEBUG ; #} ##### now I'll load conf from XML file #### sereval cases #### I have domain but nothing for id_handler in httpd.conf #### I have not domain and id_handler so #### I have both #### Normaly the cookie is got from domain #### I 'll try to load domain ONLY if httpd.conf don't have domain print STDERR "$ID_HANDLER: Phase : handler initialization try to load XML conf\n" if $DEBUG ; $CONF= Lemonldap::Config::Parameters->new ( file => $FILE , cache => $GLUE ); if ($CONF) { print STDERR "$ID_HANDLER: Phase : handler initialization LOAD XML conf :succeded \n" if $DEBUG ; } else { print STDERR "$ID_HANDLER: Phase : handler initialization LOAD XML conf : failed \n" if $DEBUG ; } ### here conf from XML is ready print STDERR "$ID_HANDLER: Phase : handler initialization LOAD XML conf\n" if $DEBUG ; ### I'll search XML section from ID_HANDLER if ($ID_HANDLER) { my $tmpconf; print STDERR "domain $DOMAIN\n"; if ($DOMAIN) { $GENERAL = $CONF->getDomain($DOMAIN) ; my $tmpconf = $GENERAL->{handler}->{$ID_HANDLER}; $COOKIE = $GENERAL->{Cookie}; $PORTAL=$GENERAL->{Portal}; $CACHE =$GENERAL->{Session} ; parseConfig($tmpconf); } else { $tmpconf= $CONF->{$ID_HANDLER} ; parseConfig($tmpconf); } } #now I read httpd.conf in order to overlay the XML config # # # # } ### I 'll do this only once unless ($LDAPCONTROL) { #################################################### my $_proxy = $r->dir_config('LemonldapEnabledproxy'); $PROXY= $_proxy if $_proxy; #################################################### my $_keyIPC= $r->dir_config('LemonldapIpcKey'); $KEYIPC= $_keyIPC if $_keyIPC; #################################################### my $_IPCNB= $r->dir_config('LemonldapIpcNb'); $IPCNB= $_IPCNB if $_IPCNB; #################################################### my $_attrldap= $r->dir_config('LemonldapAttrLdap'); $ATTRLDAP= $_attrldap if $_attrldap; #################################################### my $_ldapcontrol= $r->dir_config('LemonldapCodeAppli'); $LDAPCONTROL=$_ldapcontrol if $_ldapcontrol; #################################################### my $_disabledcontrol= $r->dir_config('LemonldapDisabled'); $DISABLEDCONTROL=$_disabledcontrol if $_disabledcontrol; #################################################### my $_cache= $r->dir_config('LemonldapSession'); $CACHE=$_cache if $_cache; #################################################### my $_stop= $r->dir_config('LemonldapStopCookie'); $STOPCOOKIE=$_stop if $_stop; #################################################### my $_mode= $r->dir_config('LemonldapRecursive'); $RECURSIF=$_mode if $_mode; #################################################### my $_proxyext= $r->dir_config('LemonldapProxyExt'); $PROXYEXT=$_proxyext if $_proxyext; #################################################### # # # # Result print STDERR "$ID_HANDLER: Phase : handler initialization VARIABLES PROXY => $PROXY KEYIPC => $KEYIPC IPCNB => $IPCNB ATTRLDAP => $ATTRLDAP LDAPCONTROL => $LDAPCONTROL DISABLEDCONTROL => $DISABLEDCONTROL RECURSIF => $RECURSIF PROXYEXT => $PROXYEXT STOPCOOKIE => $STOPCOOKIE\n" if $DEBUG ; } ##### end of initialization ## deleted those line my $uri =$r->uri; print STDERR "$ID_HANDLER :uri requested: $uri\n"; ##### end deleted lines if($PROXY){ $UA = __PACKAGE__->new; $UA->agent(join "/", __PACKAGE__, $VERSION); print STDERR "$ID_HANDLER: Build-in proxy actived\n" ; $r->handler("perl-script"); $r->push_handlers( PerlHandler => \&proxy_handler ); } # Stop process if protection is disabled return DECLINED if ($DISABLEDCONTROL); # return DECLINED unless ($PROXY); # is this area protected # configuration check #unless ($COOKIE) { #################################################### my $_cookie = $r->dir_config('LemonldapCookie'); $COOKIE= $_cookie if $_cookie; #################################################### my $_basepub= $r->dir_config('LemonldapBasePub'); $BASEPUB=$_basepub if $_basepub; #################################################### my $_basepriv= $r->dir_config('LemonldapBasePriv'); $BASEPRIV=$_basepriv if $_basepriv; #################################################### my $_portal= $r->dir_config('LemonldapPortal'); $PORTAL=$_portal if $_portal; #################################################### print STDERR "$ID_HANDLER: Phase : handler AUTHORIZATION VARIABLES COOKIE => $COOKIE BASEPUB => $BASEPUB BASEPRIV => $BASEPRIV PORTAL => $PORTAL\n" if $DEBUG ; #} #### Read cache info from XML config # # unless ($SERVERS) { my $xmlsession= $CONF->findParagraph('session',$CACHE); $SERVERS = $CONF->formateLineHash ($xmlsession->{SessionParams}); print STDERR "$ID_HANDLER: Phase : handler AUTHORIZATION CACHE CONFIG: $SERVERS \n" if $DEBUG ; } # # # AUTHENTICATION # cookie search my $entete2 =$r->headers_in(); my %entete =$r->headers_in(); my $idx =$entete2->{'Cookie'} ; # Load id value from cookie # $idx =~ /$COOKIE=([^; ]+)(;?)/o; # I remove the o option : o parse one time the regexp. $idx =~ /$COOKIE=([^; ]+)(;?)/; my $id =$1; # $id='675314908c539c2c775072227c7b5d69'; # $id='c167b67d628deb1dcfe09de7aa7f927e'; unless ($id) { # No cookie found: redirect to portal print STDERR "$ID_HANDLER : No cookie found for ".$r->uri."\n" if $DEBUG; return goPortal($r,'c'); } print STDERR "$ID_HANDLER: id session : $id<--->$idx\n" if $DEBUG; # SESSIONS CACHE #cache level 1 test my $ligne_h; unless ($id eq $CLIENT) { # Level 2 test by IPC print STDERR "$ID_HANDLER: No match in cache level 1 for $id\n" if $DEBUG; if ($IPCNB) { #### We want use IPC print STDERR "$ID_HANDLER : search in cache level 2 for $id\n" if $DEBUG; tie %STACK ,'IPC::Shareable' , $KEYIPC, {create => 1 , mode => 0666}; $ligne_h = $STACK{$id} ; if ($ligne_h) { ## match in ipc print STDERR "$ID_HANDLER : match in cache level 2 for $id\n" if $DEBUG; expire_session($id) ;# put on the top of stack } else { print STDERR "$ID_HANDLER: No match in cache level 2 for $id\n" if $DEBUG; } ## end no match in ipc } #### We want use IPC unless ($ligne_h) { # no match in cache level 1 and 2 print STDERR "$ID_HANDLER : Search in cache level 3 for $id\n" if $DEBUG; ###### ###### ###### search in backend cache ###### my %session ; tie %session, 'Apache::Session::Memorycached', $id,$SERVERS; unless ($session{dn}) { ## the cookie is present but i can't retrieve session ## tree causes : Too many connection are served. ## the server of session was restarted ## It's time out print STDERR "$ID_HANDLER: ERROR OF LOCKING ON :$id\n" if $DEBUG; # I say it's time out return goPortal($r,'t'); } #here we are retrieve session print STDERR "$ID_HANDLER: SESSION FIND FOR:$id\n" if $DEBUG; #now we will look at authorization and build an header and stock it for the next access #FIRST : authorization my $uid = $session{uid}; $uid=~ s/ //g; my $cn = $session{cn}; my $mail = $session{mail}; my $fonction = $session{fonction}; my $codique =$session{codique}; my $dn = $session{dn}; $dn=~ s/ //g; my $etat; ### $etat =0 access denied $etat <> 0 access granted my $complement; ### $complement stores the string add after the dn in header send to application ################# control section ############# # # # # if ($session{$ATTRLDAP}{$LDAPCONTROL}) { #the user have the good authorization # in order to access at application $etat =1 ;# We open tge gate $complement =$session{$ATTRLDAP}{$LDAPCONTROL}; #### begin: here for he compatibility with older lemonldap $complement=~ s/#.*//; ### end : here for he compatibility with older lemonldap } ### end of control if ($etat) { print STDERR "$ID_HANDLER: controle: $dn $uri :ACCEPTED \n" if $DEBUG; } else { # save_session ($id,'BIDON') ; #print STDERR "$ID_HANDLER: $id saving in cache level 2\n" if $DEBUG; print STDERR "$ID_HANDLER: controle: $dn $uri :DENIED \n" if $DEBUG; } untie %session; unless ($etat) { ### c est meme pas la peine essayer ! return FORBIDDEN;; exit; } ######### ici tout est ok pour moi #recuperation de l attribut multivalue en fonction de l url ############################################ $ligne_h = $dn; if ($complement) { $ligne_h.=":$complement"; } } ### end of search in cache 1 ,2 and 3 } ## end of cache level 1 if ($id eq $CLIENT){ ## corresponding at : #cache level 1 test # unless($id eq $CLIENT) $ligne_h=$CACHE1_ENTETE; print STDERR "$ID_HANDLER: match in cache level 1 for $id\n" if $DEBUG; } # all is done for this phase we can cache the header . # now we must up date the cache level i1 and 2 (IPC) ##### I must to resume here (the three caches ) #### ### ## # $CLIENT=$id; $CACHE1_ENTETE=$ligne_h; print STDERR "$ID_HANDLER: $id saving in cache level 1\n" if $DEBUG; if (($IPCNB) && (defined (%STACK))) { #we want cache IPC level 2 save_session ($id,$ligne_h) ; print STDERR "$ID_HANDLER: $id saving in cache level 2\n" if $DEBUG; untie %STACK ; } my $hcode =encode_base64($ligne_h,''); print STDERR "$ID_HANDLER: header before encoding: $ligne_h\n" if ($DEBUG); print STDERR "$ID_HANDLER: header after encoding: $hcode\n" if $DEBUG; ############### We can insert the header ##################### my $entete_spec = "Basic "; $entete_spec.=$hcode."\n"; $r->header_in('Authorization'=> $entete_spec); ############################################################### # STOP_COOKIE is used to hide cookie value to the remote application # (to avoid programmers to usurp client identities) if($STOPCOOKIE) { $r->headers_in->do(sub { (my $cle ,my $valeur) = @_; if ($valeur=~ /$COOKIE/) { my $tmp =~ /$COOKIE=.+b/; $_[1]=~ s/$tmp//; print STDERR "$ID_HANDLER: STOPCOOKIE done\n" if ($DEBUG); } 1; }); } ############################ return OK if $PROXY ; return DECLINED unless $PROXY; ############################ ################# # end of handler# ################# } sub proxy_handler { my $r = shift; # Transformation: GET /index.html becomes http://servername/index.html # $url contains the real value (hided server) # $url_init contains the asked value my $url =$r->uri; $url .="?".$r->args if ($r->args); my %entete = $r->headers_in(); my $url_init= $BASEPUB.$url; my $uuu = $url; ### only for test################ # $uuu=~ s/handler/ressource/; ################################# $url = $BASEPRIV.$uuu; print STDERR "$ID_HANDLER: URLPRIV ACTIVED: $url URLPUB REQUESTED : $url_init\n" if ($DEBUG); my $request = HTTP::Request->new($r->method, $url); $r->headers_in->do(sub { $request->header(@_); 1; }); # copy POST data, if any if($r->method eq 'POST') { my $len = $r->header_in('Content-length'); my $buf; $r->read($buf, $len); $request->content($buf); $request->content_type($r->content_type); } ###begin: some modification like mod_proxy does if ($request->header('Host')){ my $host =$request->header('Host') ; $host=~ s/$BASEPUB/$BASEPRIV/ ; $request->header('Host' => $host); } ### here I modify keep alive by close if ($request->header('Connection')){ $request->header('Connection' => 'close'); } print STDERR "$ID_HANDLER: request ".$request->as_string()."\n" if($DEBUG); if ($RECURSIF) { print STDERR "$ID_HANDLER: RECURSIF LWP DESACTIVED\n" if ($DEBUG); my @tt= ('HEAD'); $UA->requests_redirectable(\@tt); } # LWP proxy # I 'll forward on an external proxy if ($PROXYEXT) { print STDERR "$ID_HANDLER:OUTPUT PROXY:$PROXYEXT\n" if ($DEBUG); $UA->proxy(http => $PROXYEXT); } my $response = $UA->request($request); my $type =$response->header('Content-type'); ### begin: somes bad requests have bad header . my $content = $response->header('Content-type'); $content=~ s/,/;/g ; ### end: somes bad requests have bad header . $r->content_type($content); ### begin: I correct on the fly some incomming header like mod_proxy does if ($response->header('Location')) { my $h =$response->header('Location'); $h=~ s/$BASEPRIV/$BASEPUB/ ; $response->header('Location' => $h); } ### end: I correct on the fly some incomming header like mod_proxy does $r->status($response->code); $r->status_line(join " ", $response->code, $response->message); $response->scan(sub { $r->headers_out->add(@_); }); if ($r->header_out('Location')) { my $h =$r->header_out('Location'); $h=~ s/$BASEPRIV/$BASEPUB/ ; $r->header_out('Location' => $h); } if ($r->header_only) { $r->send_http_header(); return OK; } my $content = \$response->content; $r->content_type('text/html') unless $$content; $r->send_http_header; $r->print($$content || $response->error_as_HTML); print STDERR "$ID_HANDLER: response sent\n" if ($DEBUG); return OK; } sub goPortal { my $r = shift; my $op= shift; my $urlc_init = $BASEPUB.$r->uri; $urlc_init.="?".$r->args if $r->args; my $urlc_initenc = encode_base64($urlc_init,""); print STDERR "GERMANEX $urlc_init\n" ; $r->header_out(location =>$PORTAL."?op=$op&url=$urlc_initenc"); print STDERR "$ID_HANDLER : Redirect to portal (url was ".$urlc_init.")\n" if($DEBUG); return REDIRECT; exit; } sub expire_session { my $id = shift; tied(%STACK)->shlock ; my $tmpvar = $STACK{'QUEUE'}; #my $ligne= Dumper ($tmpvar); #print STDERR "eZZZ : $ligne \n"; my @tmp ; if ($tmpvar) { @tmp= split /#/,$tmpvar ; } #$ligne= Dumper (@tmp); #print STDERR "eXXX : $ligne \n"; my @stack; @stack= grep ($id ne $_ ,@tmp); unshift @stack, $id; my $config = \@stack; #my $configsx=Dumper ($config); #print STDERR "final = $configsx\n"; if ($#stack > $IPCNB ) { my $to_delete = pop @stack ; print STDERR "sup $to_delete\n"; delete $STACK{$to_delete}; } #$Data::Dumper::Purity=1; #$Data::Dumper::Terse=1; my $buffer; foreach (@stack){ $buffer.="$_"."#"; } $buffer=~ s/#$//; $config = \@stack; #$configsx=Dumper ($config); #print STDERR "essss : $configsx \n"; #my $configs=Dumper ($buffer); #print STDERR "errr : $configs \n"; $STACK{'QUEUE'} = $buffer; tied(%STACK)->shunlock ; } sub save_session { my $id = shift; my $trace = shift; tied(%STACK)->shlock ; $STACK{$id} = $trace; tied(%STACK)->shunlock ; } sub parseConfig { my $tmp =shift; $PROXY= $tmp->{Enabledproxy}; $KEYIPC=$tmp->{IpcKey}; $IPCNB= $tmp->{IpcNb}; $ATTRLDAP= $tmp->{AttrLdap}; $LDAPCONTROL=$tmp->{CodeAppli}; $DISABLEDCONTROL=$tmp->{Disabled}; $BASEPUB=$tmp->{BasePub}; $BASEPRIV=$tmp->{BasePriv}; $STOPCOOKIE= $tmp->{StopCookie}; $PROXYEXT = $tmp->{ProxyExt}; $RECURSIF= $tmp->{Recursive}; } 1; __END__ # Below is stub documentation for your module. You'd better edit it! =head1 NAME Lemonldap::Handlers::Generic4a2 - Handler Apache2 for Lemonldap sso system =head1 SYNOPSIS In httpd.conf PerlSetVar LemonldapEnabledproxy 1 ServerName serverpub.foo.bar:80 DocumentRoot /usr/local/apache2/htdocs PerlInitHandler Lemonldap::Handlers::Generic4a2 #ProxyPass / http://serverpriv.foo.bar/ #ProxyPassReverse / http://serverpriv.foo.bar/ PerlSetVar LemonldapConfig /usr/local/apache2/conf/application_new.xml PerlSetVar LemonldapConfigIpcKey CONF PerlSetVar LemonldapDEBUG 1 PerlSetVar LemonldapDomain foo.bar PerlSetVar LemonldapHandlerID handler2 PerlSetVar LemonldapBasePub http://serverpub.foo.bar PerlSetVar LemonldapBasePriv http://serverpriv.foo.bar PerlSetVar LemonldapCodeAppli APPLI PerlSetVar LemonldapAttrLdap profilapplicatif =head1 DESCRIPTION =head2 Parameters =head4 LemonldapConfig "/foo/bar/file_config.xml" The filename of the mean XML Config :It's REQUIRED =head4 LemonldapConfigIpcKey GLUE The identifier of config segment IPC :It's REQUIRED =head4 LemonldapDomain foo.bar If present , it fixes the value of domain for the application protected by this handler (see below) =head4 LemonldapHandlerId If present the configuration of handler is read from XML config backend. You can overlay XML config backend with httpd.conf =head4 LemonldapEnabledproxy 0|1 0 : don't use built-in proxy (configuration must use with mod_proxy or mod_rewrite ) 1 : use built-in proxy default : 0 =head4 LemonldapDEBUG 0|1 0 : mode debug disabled 1 : mode debug enabled default : 0 =head4 LemonldapIpcNb 0..nn IPNB is the number of session which you want to keep in cache evel 2 (IPC) min value : 0 (don't use cache IPC level2) max value : ??? : It depends of your server recommended : 100 The youngest value replace the oldest . =head4 LemonldapIpcKey '4 carac' A string of 4 caracteres (see IPC::Shareable doc) It must be 'unique' . =head4 LemonldapAttrLdap 'string' The first level of hash session , whi can to be the name of LDAP attribute see below =head4 LemonldapCodeAppli 'string' The second level of hash session , whi can to be the code of application The access of %session if $session{LemonldapAttrLDAP}{lemonldapCodeAppli} with the value of key = profil . =head4 LemonldapDisabled 0|1 0 : Control the request (default) 1 : Don't control the request (useful for jpeg ) =head4 LemonldapStopCookie 0|1 0 : Let pass the lemonldap cookie to application (default). 1 : Block the lemonldap cookie. =head4 LemonldapRECURSIVE 0|1 0 : Let LWP chases redirection (default). 1 : Let Client chases redirection instead LWP. =head4 LemonldapProxyExt 0|1 0 : Let LWP resquets on ressource (default). 1 : force LWP to request via an external proxy. =head4 LemonldapSession 'cachelevel 3' It is the name of XML section which describes the backend used in order to store the session . =head4 LemonldapCookie 'name_of_cookie' eg: lemontest =head4 LemonldapBasePub The public host name avaiable by user =head4 LemonldapBasePriv The private host name not avaiable by user =head4 LemonldapPortal The url of login page =head1 SEE ALSO Lemonldap(3), Lemonldap::Portal::Standard http://lemonldap.sourceforge.net/ "Writing Apache Modules with Perl and C" by Lincoln Stein E Doug MacEachern - O'REILLY =over 1 =item Eric German, Egermanlinux@yahoo.frE =item Xavier Guimard, Ex.guimard@free.frE =item Portage under Apache2 is made with help of : Ali Pouya and Shervin Ahmadi (MINEFI/DGI) =back =head1 COPYRIGHT AND LICENSE Copyright (C) 2004 by Eric German E Xavier Guimard Lemonldap originaly written by Eric german who decided to publish him in 2003 under the terms of the GNU General Public License version 2. =over 1 =item This package is under the GNU General Public License, Version 2. =item The primary copyright holder is Eric German. =item Portions are copyrighted under the same license as Perl itself. =item Portions are copyrighted by Doug MacEachern and Lincoln Stein. This library is under the GNU General Public License, Version 2. =back 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; version 2 dated June, 1991. 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. A copy of the GNU General Public License is available in the source tree; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. =cut