## @file # Base file for Lemonldap::NG handlers ## @class # Base class for Lemonldap::NG handlers. # All methods in handler are class methods: in ModPerl environment, handlers # are always launched without object created. # # The main method is run() who is called by Apache for each requests (using # handler() wrapper). # # The main initialization subroutine is init() who launch localInit() and # globalInit(). package Lemonldap::NG::Handler::Simple; use strict; use MIME::Base64; use Exporter 'import'; use AutoLoader 'AUTOLOAD'; use Safe; use Lemonldap::NG::Common::Safelib; #link protected safe Safe object use Lemonldap::NG::Common::Crypto; require POSIX; use CGI::Util 'expires'; use constant SAFEWRAP => ( Safe->can("wrap_code_ref") ? 1 : 0 ); use constant UNPROTECT => 1; use constant SKIP => 2; use constant MAINTENANCE_CODE => 503; #inherits Cache::Cache #inherits Apache::Session #link Lemonldap::NG::Common::Apache::Session::SOAP protected globalStorage our $VERSION = '1.2.2'; our %EXPORT_TAGS; our @EXPORT_OK; our @EXPORT; # Shared variables our ( $locationRegexp, $locationCondition, $defaultCondition, $locationProtection, $defaultProtection, $forgeHeaders, $apacheRequest, $locationCount, $cookieName, $datas, $globalStorage, $globalStorageOptions, $localStorage, $localStorageOptions, $whatToTrace, $https, $refLocalStorage, $safe, $port, $statusPipe, $statusOut, $customFunctions, $transform, $cda, $childInitDone, $httpOnly, $cookieExpiration, $timeoutActivity, $datasUpdate, $useRedirectOnForbidden, $useRedirectOnError, $useSafeJail, $securedCookie, $key, $cipher, $headerList, $maintenance, ); ########################################## # COMPATIBILITY WITH APACHE AND APACHE 2 # ########################################## BEGIN { %EXPORT_TAGS = ( localStorage => [qw( $localStorage $localStorageOptions $refLocalStorage )], globalStorage => [qw( $globalStorage $globalStorageOptions )], locationRules => [ qw( $locationCondition $defaultCondition $locationCount $locationProtection $defaultProtection $datasUpdate $locationRegexp $apacheRequest $datas safe $customFunctions $useSafeJail ) ], import => [qw( import @EXPORT_OK @EXPORT %EXPORT_TAGS )], headers => [ qw( $forgeHeaders lmHeaderIn lmSetHeaderIn lmHeaderOut lmSetHeaderOut lmSetErrHeaderOut $cookieName $https $port $securedCookie $key $cipher $headerList ) ], traces => [qw( $whatToTrace $statusPipe $statusOut)], apache => [ qw( MP OK REDIRECT FORBIDDEN DONE DECLINED SERVER_ERROR $useRedirectOnForbidden $useRedirectOnError $maintenance ) ], post => [qw($transform postFilter)], cda => ['$cda'], cookie => [ qw( $cookieName $https $httpOnly $cookieExpiration $securedCookie $key $cipher ) ], session => ['$timeoutActivity'], ); push( @EXPORT_OK, @{ $EXPORT_TAGS{$_} } ) foreach ( keys %EXPORT_TAGS ); $EXPORT_TAGS{all} = \@EXPORT_OK; if ( exists $ENV{MOD_PERL} ) { if ( $ENV{MOD_PERL_API_VERSION} and $ENV{MOD_PERL_API_VERSION} >= 2 ) { eval 'use constant MP => 2;'; } else { eval 'use constant MP => 1;'; } } else { eval 'use constant MP => 0;'; } if ( MP() == 2 ) { require Apache2::Log; require Apache2::RequestUtil; Apache2::RequestUtil->import(); require Apache2::RequestRec; Apache2::RequestRec->import(); require Apache2::ServerUtil; Apache2::ServerUtil->import(); require Apache2::Connection; Apache2::Connection->import(); require Apache2::RequestIO; Apache2::RequestIO->import(); require APR::Table; APR::Table->import(); require Apache2::URI; Apache2::URI->import(); require Apache2::Const; Apache2::Const->import( '-compile', qw(:common :log) ); eval ' use constant FORBIDDEN => Apache2::Const::FORBIDDEN; use constant REDIRECT => Apache2::Const::REDIRECT; use constant OK => Apache2::Const::OK; use constant DECLINED => Apache2::Const::DECLINED; use constant DONE => Apache2::Const::DONE; use constant SERVER_ERROR => Apache2::Const::SERVER_ERROR; '; eval { require threads::shared; threads::shared::share($locationRegexp); threads::shared::share($locationCondition); threads::shared::share($defaultCondition); threads::shared::share($locationProtection); threads::shared::share($defaultProtection); threads::shared::share($forgeHeaders); threads::shared::share($locationCount); threads::shared::share($cookieName); threads::shared::share($globalStorage); threads::shared::share($globalStorageOptions); threads::shared::share($localStorage); threads::shared::share($localStorageOptions); threads::shared::share($whatToTrace); threads::shared::share($https); threads::shared::share($port); threads::shared::share($refLocalStorage); threads::shared::share($statusPipe); threads::shared::share($statusOut); threads::shared::share($timeoutActivity); threads::shared::share($useRedirectOnForbidden); threads::shared::share($useRedirectOnError); threads::shared::share($useSafeJail); threads::shared::share($customFunctions); threads::shared::share($securedCookie); threads::shared::share($key); threads::shared::share($headerList); threads::shared::share($maintenance); }; print "eval error: $@" if ($@); } elsif ( MP() == 1 ) { require Apache; require Apache::Log; require Apache::Constants; Apache::Constants->import(':common'); Apache::Constants->import(':response'); } else { # For Test or CGI eval ' use constant FORBIDDEN => 1; use constant REDIRECT => 1; use constant OK => 1; use constant DECLINED => 1; use constant DONE => 1; use constant SERVER_ERROR => 1; '; } *handler = ( MP() == 2 ) ? \&handler_mp2 : \&handler_mp1; *logout = ( MP() == 2 ) ? \&logout_mp2 : \&logout_mp1; } ## @rmethod protected int handler_mp2() # Launch run() when used under mod_perl version 2 # @return Apache constant sub handler_mp2 : method { shift->run(@_); } ## @rmethod protected int logout_mp2() # Launch unlog() when used under mod_perl version 2 # @return Apache constant sub logout_mp2 : method { shift->unlog(@_); } ## @rmethod void lmLog(string mess, string level) # Wrapper for Apache log system # @param $mess message to log # @param $level string (debug, info, warning or error) sub lmLog { my ( $class, $mess, $level ) = splice @_; die("Level is required") unless ($level); my $call; unless ( $level eq 'debug' ) { my @tmp = caller(); $call = "$tmp[1] $tmp[2]:"; } if ( MP() == 2 ) { Apache2::ServerRec->log->debug($call) if ($call); Apache2::ServerRec->log->$level($mess); } elsif ( MP() == 1 ) { Apache->server->log->debug($call) if ($call); Apache->server->log->$level($mess); } else { print STDERR "[$level] $mess\n"; } } ## @rmethod protected void lmSetApacheUser(Apache2::RequestRec r,string s) # Inform Apache for the data to use as user for logs # @param $r current request # @param $s string to use sub lmSetApacheUser { my ( $class, $r, $s ) = splice @_; return unless ($s); if ( MP() == 2 ) { $r->user($s); } else { $r->connection->user($s); } } ## @ifn protected string protected regRemoteIp(string str) # Replaces $ip by the client IP address in the string # @param $str string # @return string sub regRemoteIp { my ( $class, $str ) = splice @_; if ( MP() == 2 ) { $str =~ s/\$datas->\{ip\}/\$apacheRequest->connection->remote_ip/g; } else { $str =~ s/\$datas->\{ip\}/\$apacheRequest->remote_ip/g; } return $str; } ## @rmethod void lmSetHeaderIn(Apache2::RequestRec r, hash headers) # Set HTTP headers in the HTTP request. # @param $r Current request # @param %headers Hash of header names and values sub lmSetHeaderIn { my ( $class, $r, %headers ) = splice @_; while ( my ( $h, $v ) = each %headers ) { if ( MP() == 2 ) { $r->headers_in->set( $h => $v ); } elsif ( MP() == 1 ) { $r->header_in( $h => $v ); } $class->lmLog( "Send header $h with value $v", 'debug' ); } } ## @rmethod void lmUnsetHeaderIn(Apache2::RequestRec r, array headers) # Unset HTTP headers in the HTTP request. # @param $r Current request # @param @headers Name of the headers sub lmUnsetHeaderIn { my ( $class, $r, @headers ) = splice @_; foreach my $h (@headers) { if ( MP() == 2 ) { $r->headers_in->unset($h); } elsif ( MP() == 1 ) { $r->header_in( $h => "" ) if ( $r->header_in($h) ); } $class->lmLog( "Unset header $h", 'debug' ); } } ## @rfn string lmHeaderIn(Apache2::RequestRec r, string h) # Return an HTTP header value from the HTTP request. # @param $r Current request # @param $h Name of the header # @return Value of the header sub lmHeaderIn { my ( $r, $h ) = splice @_; if ( MP() == 2 ) { return $r->headers_in->{$h}; } elsif ( MP() == 1 ) { return $r->header_in($h); } } ## @rfn void lmSetErrHeaderOut(Apache2::RequestRec r, string h, string v) # Set an HTTP header in the HTTP response in error context # @param $r Current request # @param $h Name of the header # @param $v Value of the header sub lmSetErrHeaderOut { my ( $r, $h, $v ) = splice @_; if ( MP() == 2 ) { return $r->err_headers_out->set( $h => $v ); } elsif ( MP() == 1 ) { return $r->err_header_out( $h => $v ); } } ## @rfn void lmSetHeaderOut(Apache2::RequestRec r, string h, string v) # Set an HTTP header in the HTTP response in normal context # @param $r Current request # @param $h Name of the header # @param $v Value of the header sub lmSetHeaderOut { my ( $r, $h, $v ) = splice @_; if ( MP() == 2 ) { return $r->headers_out->set( $h => $v ); } elsif ( MP() == 1 ) { return $r->header_out( $h => $v ); } } ## @rfn string lmHeaderOut(Apache2::RequestRec r, string h) # Return an HTTP header value from the HTTP response. # @param $r Current request # @param $h Name of the header # @return Value of the header sub lmHeaderOut { my ( $r, $h, $v ) = splice @_; if ( MP() == 2 ) { return $r->headers_out->{$h}; } elsif ( MP() == 1 ) { return $r->header_out($h); } } ############################## # Fake Safe jail subroutines # ############################## ## @method reval # Fake reval method if useSafeJail desactivated sub reval { my ( $class, $e ) = splice @_; return eval $e; } ## @method wrap_code_ref # Fake wrap_code_ref method if useSafeJail desactivated sub wrap_code_ref { my ( $class, $e ) = splice @_; return $e; } ## @method share # Fake share method if useSafeJail desactivated sub share { my ( $class, @vars ) = splice @_; $class->share_from( scalar(caller), \@vars ); } ## @method share_form # Fake share_from method if useSafeJail desactivated sub share_from { my ( $class, $pkg, $vars ) = splice @_; no strict 'refs'; foreach my $arg (@$vars) { my ( $var, $type ); $type = $1 if ( $var = $arg ) =~ s/^(\W)//; for ( 1 .. 2 ) { # assign twice to avoid any 'used once' warnings *{$var} = ( !$type ) ? \&{ $pkg . "::$var" } : ( $type eq '&' ) ? \&{ $pkg . "::$var" } : ( $type eq '$' ) ? \${ $pkg . "::$var" } : ( $type eq '@' ) ? \@{ $pkg . "::$var" } : ( $type eq '%' ) ? \%{ $pkg . "::$var" } : ( $type eq '*' ) ? *{ $pkg . "::$var" } : undef; } } } ############################## # Initialization subroutines # ############################## ## @imethod protected Safe safe() # Build and return the security jail used to compile rules and headers. # @return Safe object sub safe { my $class = shift; return $safe if ($safe); $useSafeJail = 1 unless defined $useSafeJail; my @t = $customFunctions ? split( /\s+/, $customFunctions ) : (); foreach (@t) { $class->lmLog( "Custom function : $_", 'debug' ); my $sub = $_; unless (/::/) { $sub = "$class\::$_"; } else { s/^.*:://; } next if ( $class->can($_) ); eval "sub $_ { return $sub(\$apacheRequest->uri . ( \$apacheRequest->args ? '?' . \$apacheRequest->args : '' ) , \@_) }"; $class->lmLog( $@, 'error' ) if ($@); } if ($useSafeJail) { $safe = new Safe; $safe->share_from( 'main', ['%ENV'] ); } else { $safe = $class; } # Share objets with Safe jail $safe->share_from( 'Lemonldap::NG::Common::Safelib', $Lemonldap::NG::Common::Safelib::functions ); $safe->share( '&encode_base64', '$datas', '&portal', '$apacheRequest', @t ); return $safe; } ## @imethod void localInit(hashRef args) # Call purgeCache() to purge the local cache, launch the status process # (statusProcess()) in wanted and launch childInit(). # @param $args reference to the initialization hash sub localInit($$) { my ( $class, $args ) = splice @_; if ( $localStorage = $args->{localStorage} ) { $localStorageOptions = $args->{localStorageOptions}; $localStorageOptions->{default_expires_in} ||= 600; $class->purgeCache(); } if ( $args->{status} ) { if ( defined $localStorage ) { statusProcess(); } else { # localStorage is mandatory for status module $class->lmLog( "Status module can not be loaded without localStorage parameter", 'warn' ); } } $class->childInit($args); } ## @imethod protected boolean childInit() # Indicates to Apache that it has to launch: # - initLocalStorage() for each child process (after fork and uid change) # - cleanLocalStorage() after each requests # @return True sub childInit { my ( $class, $args ) = splice @_; return 1 if ($childInitDone); # We don't initialise local storage in the "init" subroutine because it can # be used at the starting of Apache and so with the "root" privileges. Local # Storage is also initialized just after Apache's fork and privilege lost. # Local storage is cleaned after giving the content of the page to increase # performances. no strict; if ( MP() == 2 ) { $s = Apache2::ServerUtil->server; $s->push_handlers( PerlChildInitHandler => sub { return $class->initLocalStorage( $_[1], $_[0] ); } ); $s->push_handlers( PerlPostConfigHandler => sub { my ( $c, $l, $t, $s ) = splice @_; $s->add_version_component( 'Lemonldap::NG::Handler/' . $VERSION ); } ) unless ( $args->{hideSignature} ); } elsif ( MP() == 1 ) { Apache->push_handlers( PerlChildInitHandler => sub { return $class->initLocalStorage(@_); } ); } $childInitDone++; 1; } ## @imethod protected void purgeCache() # Purge the local cache. # Launched at Apache startup. sub purgeCache { my $class = shift; eval "use $localStorage;"; die("Unable to load $localStorage: $@") if ($@); # At each Apache (re)start, we've to clear the cache to avoid living # with old datas eval '$refLocalStorage = new ' . $localStorage . '($localStorageOptions);'; if ( defined $refLocalStorage ) { $refLocalStorage->clear(); } else { $class->lmLog( "Unable to clear local cache: $@", 'error' ); } } ## @imethod void globalInit(hashRef args) # Global initialization process. Launch : # - defaultValuesInit() # - portalInit() # - locationRulesInit() # - globalStorageInit() # - forgeHeadersInit() # - postUrlInit() # @param $args reference to the configuration hash sub globalInit { my $class = shift; $class->defaultValuesInit(@_); $class->portalInit(@_); $class->locationRulesInit(@_); $class->globalStorageInit(@_); $class->headerListInit(@_); $class->forgeHeadersInit(@_); $class->postUrlInit(@_); } ## @imethod protected codeRef conditionSub(string cond) # Returns a compiled function used to grant users (used by # locationRulesInit(). The second value returned is a non null # constant if URL is not protected (by "unprotect" or "skip"), 0 else. # @param $cond The boolean expression to use # @return array (ref(sub), int) sub conditionSub { my ( $class, $cond ) = splice @_; my ( $OK, $NOK ) = ( sub { 1 }, sub { 0 } ); # Simple cases : accept and deny return ( $OK, 0 ) if ( $cond =~ /^accept$/i ); return ( $NOK, 0 ) if ( $cond =~ /^deny$/i ); # Cases unprotect and skip : 2nd value is 1 or 2 return ( $OK, UNPROTECT ) if ( $cond =~ /^unprotect$/i ); return ( $OK, SKIP ) if ( $cond =~ /^skip$/i ); # Case logout if ( $cond =~ /^logout(?:_sso)?(?:\s+(.*))?$/i ) { my $url = $1; return ( $url ? ( sub { $datas->{_logout} = $url; return 0 }, 0 ) : ( sub { $datas->{_logout} = portal(); return 0 }, 0 ) ); } # Since filter exists only with Apache>=2, logout_app and logout_app_sso # targets are available only for it. # This error can also appear with Manager configured as CGI script if ( $cond =~ /^logout_app/i and MP() < 2 ) { $class->lmLog( "Rules logout_app and logout_app_sso require Apache>=2", 'warn' ); return ( sub { 1 }, 0 ); } # logout_app if ( $cond =~ /^logout_app(?:\s+(.*))?$/i ) { my $u = $1 || $class->portal(); eval 'use Apache2::Filter' unless ( $INC{"Apache2/Filter.pm"} ); return ( sub { $apacheRequest->add_output_filter( sub { return $class->redirectFilter( $u, @_ ); } ); 1; }, 0 ); } elsif ( $cond =~ /^logout_app_sso(?:\s+(.*))?$/i ) { eval 'use Apache2::Filter' unless ( $INC{"Apache2/Filter.pm"} ); my $u = $1 || $class->portal(); return ( sub { $class->localUnlog; $apacheRequest->add_output_filter( sub { return $class->redirectFilter( $class->portal() . "?url=" . $class->encodeUrl($u) . "&logout=1", @_ ); } ); 1; }, 0 ); } # Replace some strings in condition $cond =~ s/\$date/&POSIX::strftime("%Y%m%d%H%M%S",localtime())/e; $cond =~ s/\$(\w+)/\$datas->{$1}/g; $cond =~ s/\$datas->{vhost}/\$apacheRequest->hostname/g; # Eval sub my $sub = ( SAFEWRAP ? $class->safe->wrap_code_ref( $class->safe->reval("sub{return($cond)}") ) : $class->safe->reval("sub{return($cond)}") ); # Return sub and protected flag return ( $sub, 0 ); } ## @imethod protected void defaultValuesInit(hashRef args) # Set default values for non-customized variables # @param $args reference to the configuration hash sub defaultValuesInit { my ( $class, $args ) = splice @_; # Warning: first start of handler load values from MyHanlder.pm # and lemonldap-ng.ini # These values should be erased by global configuration! $cookieName = $args->{cookieName} || $cookieName || 'lemonldap'; $securedCookie = defined( $args->{securedCookie} ) ? $args->{securedCookie} : defined($securedCookie) ? $securedCookie : 1; $whatToTrace = $args->{whatToTrace} || $whatToTrace || 'uid'; $whatToTrace =~ s/\$//g; $https = defined($https) ? $https : $args->{https}; $port ||= $args->{port}; $customFunctions = $args->{customFunctions}; $cda = defined($cda) ? $cda : $args->{cda}; $httpOnly = defined($httpOnly) ? $httpOnly : $args->{httpOnly}; $cookieExpiration = $args->{cookieExpiration} || $cookieExpiration; $timeoutActivity = $args->{timeoutActivity} || $timeoutActivity || 0; $useRedirectOnError = defined($useRedirectOnError) ? $useRedirectOnError : $args->{useRedirectOnError}; $useRedirectOnForbidden = defined($useRedirectOnForbidden) ? $useRedirectOnForbidden : $args->{useRedirectOnForbidden}; $useSafeJail = defined($useSafeJail) ? $useSafeJail : $args->{useSafeJail}; $key ||= 'lemonldap-ng-key'; $cipher ||= Lemonldap::NG::Common::Crypto->new($key); if ( $args->{key} && ( $args->{key} ne $key ) ) { $key = $args->{key}; $cipher = Lemonldap::NG::Common::Crypto->new($key); } $maintenance = defined($maintenance) ? $maintenance : $args->{maintenance}; 1; } ## @imethod protected void portalInit(hashRef args) # Verify that portal variable exists. Die unless # @param $args reference to the configuration hash sub portalInit { my ( $class, $args ) = splice @_; die("portal parameter required") unless ( $args->{portal} ); if ( $args->{portal} =~ /[\$\(&\|"']/ ) { my ($portal) = $class->conditionSub( $args->{portal} ); eval "sub portal {return &\$portal}"; } else { eval "sub portal {return '$args->{portal}'}"; } die("Unable to read portal parameter ($@)") if ($@); 1; } ## @imethod protected void globalStorageInit(hashRef args) # Initialize the Apache::Session::* module choosed to share user's variables. # @param $args reference to the configuration hash sub globalStorageInit { my ( $class, $args ) = splice @_; $globalStorage = $args->{globalStorage} or die("globalStorage required"); eval "use $globalStorage;"; die($@) if ($@); $globalStorageOptions = $args->{globalStorageOptions}; } ## @imethod protected int initLocalStorage() # Prepare local cache (if not done before by Lemonldap::NG::Common::Conf) # @return Apache2::Const::DECLINED sub initLocalStorage { my ( $class, $r ) = splice @_; if ( $localStorage and not $refLocalStorage ) { eval "use $localStorage;\$refLocalStorage = new $localStorage(\$localStorageOptions);"; $class->lmLog( "Local cache initialization failed: $@", 'error' ) unless ( defined $refLocalStorage ); } return DECLINED; } ################### # RUNNING METHODS # ################### ## @rmethod protected void updateStatus(string user,string url,string action) # Inform the status process of the result of the request if it is available. sub updateStatus { my ( $class, $user, $url, $action ) = splice @_; eval { print $statusPipe "$user => " . $apacheRequest->hostname . "$url $action\n" if ($statusPipe); }; } ## @rmethod protected int forbidden(string uri) # Used to reject non authorized requests. # Inform the status processus and call logForbidden(). # @param uri URI requested # @return Apache2::Const::REDIRECT or Apache2::Const::FORBIDDEN sub forbidden { my ( $class, $uri ) = splice @_; if ( $datas->{_logout} ) { $class->updateStatus( $datas->{$whatToTrace}, $_[0], 'LOGOUT' ); my $u = $datas->{_logout}; $class->localUnlog; return $class->goToPortal( $u, 'logout=1' ); } $class->updateStatus( $datas->{$whatToTrace}, $_[0], 'REJECT' ); $apacheRequest->push_handlers( PerlLogHandler => sub { $_[0]->status(FORBIDDEN); $class->logForbidden( $uri, $datas ); DECLINED; } ); # Redirect or Forbidden? if ($useRedirectOnForbidden) { $class->lmLog( "Use redirect for forbidden access", 'debug' ); return $class->goToPortal( $uri, 'lmError=403' ); } else { $class->lmLog( "Return forbidden access", 'debug' ); return FORBIDDEN; } } ## @rmethod protected void logForbidden(string uri,hashref datas) # Insert a log in Apache errors log system to inform that the user was rejected. # This method has to be overloaded to use different logs systems # @param $uri uri asked # @param $datas hash re to user's datas sub logForbidden { my ( $class, $uri, $datas ) = splice @_; $class->lmLog( 'User "' . $datas->{$whatToTrace} . '" was reject when he tried to access to ' . $uri, 'notice' ); } ## @rmethod protected void logGranted(string uri) # Insert a log in Apache errors log system to inform that the user was # authorizated. This method has to be overloaded to use different logs systems # @param $uri uri asked sub logGranted { my ( $class, $uri, $datas ) = splice @_; $class->lmLog( 'User "' . $datas->{$whatToTrace} . '" was granted to access to ' . $uri, 'debug' ); } ## @rmethod protected void hideCookie() # Hide Lemonldap::NG cookie to the protected application. sub hideCookie { my $class = shift; $class->lmLog( "removing cookie", 'debug' ); my $tmp = lmHeaderIn( $apacheRequest, 'Cookie' ); $tmp =~ s/$cookieName(?:http)?[^,;]*[,;]?//og; $class->lmSetHeaderIn( $apacheRequest, 'Cookie' => $tmp ); } ## @rmethod protected string encodeUrl(string url) # Encode URl in the format used by Lemonldap::NG::Portal for redirections. # @return Base64 encoded string sub encodeUrl { my ( $class, $url ) = splice @_; $url = $class->_buildUrl($url) if ( $url !~ m#^https?://# ); return encode_base64( $url, '' ); } ## @rmethod protected int goToPortal(string url, string arg) # Redirect non-authenticated users to the portal by setting "Location:" header. # @param $url Url requested # @param $arg optionnal GET parameters # @return Apache2::Const::REDIRECT sub goToPortal { my ( $class, $url, $arg ) = splice @_; $class->lmLog( "Redirect " . $apacheRequest->connection->remote_ip . " to portal (url was $url)", 'debug' ); my $urlc_init = $class->encodeUrl($url); lmSetHeaderOut( $apacheRequest, 'Location' => $class->portal() . "?url=$urlc_init" . ( $arg ? "&$arg" : "" ) ); return REDIRECT; } ## @rmethod protected $ fetchId() # Get user cookies and search for Lemonldap::NG cookie. # @return Value of the cookie if found, 0 else sub fetchId { my $t = lmHeaderIn( $apacheRequest, 'Cookie' ); my $lookForHttpCookie = $securedCookie =~ /^(2|3)$/ && $https->{_} == 0; my $value = $lookForHttpCookie ? ( $t =~ /${cookieName}http=([^,; ]+)/o ? $1 : 0 ) : ( $t =~ /$cookieName=([^,; ]+)/o ? $1 : 0 ); $value = $cipher->decryptHex( $value, "http" ) if ( $value && $lookForHttpCookie && $securedCookie == 3 ); return $value; } ## @rmethod protected boolean retrieveSession(id) # Tries to retrieve the session whose index is id # @return true if the session was found, false else sub retrieveSession { my ( $class, $id ) = @_; # 1. search if the user was the same as previous (very efficient in # persistent connection). return 1 if ( $id eq $datas->{_session_id} and ( time() - $datasUpdate < 60 ) ); # 2. search in the local cache if exists return 1 if ( $refLocalStorage and $datas = $refLocalStorage->get($id) ); # 3. search in the central cache my %h; eval { tie %h, $globalStorage, $id, $globalStorageOptions; }; if ($@) { $class->lmLog( "Session $id can't be retrieved: $@", 'info' ); return 0; } # Update the session to notify activity, if necessary $h{_lastSeen} = time() if ($timeoutActivity); # Store data in current shared variables $datas->{$_} = $h{$_} foreach ( keys %h ); # Store the session in local storage $refLocalStorage->set( $id, $datas, "10 minutes" ) if ($refLocalStorage); untie %h; $datasUpdate = time(); return 1; } # MAIN SUBROUTINE called by Apache (using PerlHeaderParserHandler option) ## @rmethod int run(Apache2::RequestRec apacheRequest) # Main method used to control access. # Calls : # - fetchId() # - retrieveSession() # - lmSetApacheUser() # - grant() # - forbidden() if user is rejected # - sendHeaders() if user is granted # - hideCookie() # - updateStatus() # @param $apacheRequest Current request # @return Apache2::Const value (OK, FORBIDDEN, REDIRECT or SERVER_ERROR) sub run ($$) { my $class; ( $class, $apacheRequest ) = splice @_; return DECLINED unless ( $apacheRequest->is_initial_req ); my $args = $apacheRequest->args; # Direct return if maintenance mode is active if ( $class->checkMaintenanceMode() ) { if ($useRedirectOnError) { $class->lmLog( "Got to portal with maintenance error code", 'debug' ); return $class->goToPortal( '/', 'lmError=' . MAINTENANCE_CODE ); } else { $class->lmLog( "Return maintenance error code", 'debug' ); return MAINTENANCE_CODE; } } # Cross domain authentication if ( $cda and $args =~ s/[\?&]?($cookieName(http)?=\w+)$//oi ) { my $str = $1; $class->lmLog( 'CDA request', 'debug' ); $apacheRequest->args($args); my $host = $apacheRequest->get_server_name(); my $redirectUrl = $class->_buildUrl( $apacheRequest->uri ); my $redirectHttps = ( $redirectUrl =~ m/^https/ ); lmSetErrHeaderOut( $apacheRequest, 'Location' => $redirectUrl . ( $args ? "?" . $args : "" ) ); $host =~ s/^[^\.]+\.(.*\..*$)/$1/; lmSetErrHeaderOut( $apacheRequest, 'Set-Cookie' => "$str; domain=$host; path=/" . ( $redirectHttps ? "; secure" : "" ) . ( $httpOnly ? "; HttpOnly" : "" ) . ( $cookieExpiration ? "; expires=" . expires( $cookieExpiration, 'cookie' ) : "" ) ); return REDIRECT; } my $uri = $apacheRequest->uri . ( $args ? "?$args" : "" ); Apache2::URI::unescape_url($uri); my $protection = $class->isUnprotected($uri); if ( $protection == SKIP ) { $class->lmLog( "Access control skipped", "debug" ); $class->updateStatus( $apacheRequest->connection->remote_ip, $apacheRequest->uri, 'SKIP' ); $class->hideCookie; $class->cleanHeaders; return OK; } my $id; # Try to recover cookie and user session if ( $id = $class->fetchId and $class->retrieveSession($id) ) { # AUTHENTICATION done my $kc = keys %$datas; # in order to detect new local macro # ACCOUNTING (1. Inform Apache) $class->lmSetApacheUser( $apacheRequest, $datas->{$whatToTrace} ); # AUTHORIZATION return $class->forbidden($uri) unless ( $class->grant($uri) ); $class->updateStatus( $datas->{$whatToTrace}, $apacheRequest->uri, 'OK' ); # ACCOUNTING (2. Inform remote application) $class->sendHeaders; # Store local macros if ( keys %$datas > $kc and $refLocalStorage ) { $class->lmLog( "Update local cache", "debug" ); $refLocalStorage->set( $id, $datas, "10 minutes" ); } # Hide Lemonldap::NG cookie $class->hideCookie; # Log $apacheRequest->push_handlers( PerlLogHandler => sub { $class->logGranted( $uri, $datas ); DECLINED }, ); # Catch POST rules $class->transformUri($uri); return OK; } elsif ( $protection == UNPROTECT ) { # Ignore unprotected URIs $class->lmLog( "No valid session but unprotected access", "debug" ); $class->updateStatus( $apacheRequest->connection->remote_ip, $apacheRequest->uri, 'UNPROTECT' ); $class->hideCookie; $class->cleanHeaders; return OK; } else { # Redirect user to the portal $class->lmLog( "$class: No cookie found", 'info' ) unless ($id); # if the cookie was fetched, a log is sent by retrieveSession() $class->updateStatus( $apacheRequest->connection->remote_ip, $apacheRequest->uri, $id ? 'EXPIRED' : 'REDIRECT' ); return $class->goToPortal($uri); } } ## @rmethod protected boolean checkMaintenanceMode # Check if we are in maintenance mode # @return true if maintenance mode sub checkMaintenanceMode { my ($class) = splice @_; if ($maintenance) { $class->lmLog( "Maintenance mode activated", 'debug' ); return 1; } return 0; } 1; __END__ =head1 NAME =encoding utf8 Lemonldap::NG::Handler::Simple - Perl base extension for building Lemonldap::NG compatible handler. =head1 SYNOPSIS Create your own package: package My::Package; use Lemonldap::NG::Handler::Simple; our @ISA = qw(Lemonldap::NG::Handler::Simple); __PACKAGE__->init ({ locationRules => { default => '$ou =~ /brh/' }, globalStorage => 'Apache::Session::MySQL', globalStorageOptions => { DataSource => 'dbi:mysql:database=dbname;host=127.0.0.1', UserName => 'db_user', Password => 'db_password', TableName => 'sessions', LockDataSource => 'dbi:mysql:database=dbname;host=127.0.0.1', LockUserName => 'db_user', LockPassword => 'db_password', }, localStorage => 'Cache::DBFile', localStorageOptions => {}, portal => 'https://portal/', }); More complete example package My::Package; use Lemonldap::NG::Handler::Simple; our @ISA = qw(Lemonldap::NG::Handler::Simple); __PACKAGE__->init ( { locationRules => { '^/pj/.*$' => '$qualif="opj"', '^/rh/.*$' => '$ou=~/brh/', '^/rh_or_opj.*$' => '$qualif="opj" or $ou=~/brh/', default => 'accept', # means that all authenticated users are greanted }, globalStorage => 'Apache::Session::MySQL', globalStorageOptions => { DataSource => 'dbi:mysql:database=dbname;host=127.0.0.1', UserName => 'db_user', Password => 'db_password', TableName => 'sessions', LockDataSource => 'dbi:mysql:database=dbname;host=127.0.0.1', LockUserName => 'db_user', LockPassword => 'db_password', }, localStorage => 'Cache::DBFile', localStorageOptions => {}, cookieName => 'lemon', portal => 'https://portal/', whatToTrace => '$uid', exportedHeaders => { 'Auth-User' => '$uid', 'Unit' => '$ou', https => 1, } ); Call your package in /conf/httpd.conf PerlRequire MyFile # TOTAL PROTECTION PerlHeaderParserHandler My::Package # OR SELECTED AREA PerlHeaderParserHandler My::Package You can also unprotect an URI PerlHeaderParserHandler My::Package->unprotect =head1 DESCRIPTION Lemonldap::NG::Handler::Simple is designed to be overloaded. See L for more. =head2 INITIALISATION PARAMETERS This section presents the C method parameters. =over =item B (required) Reference to a hash that contains "url-regexp => perl-expression" entries to manage authorizations. =over =item * "url-regexp" can be a perl regexp or the keyword 'default' which corresponds to the default police (accept by default). =item * "perl-expression" can be a perl condition or the keyword "accept" or the keyword "deny". All the variables announced by $ are replaced by the values resulting from the global session store. =back =item B E B (required) Name and parameters of the Apache::Session::* module used by the portal to store user's datas. See L for more explanations. =item B E B Name and parameters of the optional but recommended Cache::* module used to share user's datas between Apache processes. There is no need to set expires options since L call the Cache::*::purge method itself. =item B (default: lemon) Name of the cookie used by the Lemonldap::NG infrastructure. =item B (required) Url of the portal used to authenticate users. =item B (default: uid) Stored user variable to use in Apache logs. =item B Reference to a hash that contains "Name => value" entries. Those headers are calculated for each user by replacing the variables announced by "$" by their values resulting from the global session store. =item B (default: 1) Indicates if the protected server is protected by SSL. It is used to build redirections, so you have to set it to avoid bad redirections after authentication. =item B (default: undef) If port is not well defined in redirection, you can fix listen port here. =back =head2 EXPORT None by default. You can import the following tags for inheritance: =over =item * B<:localStorage> : variables used to manage local storage =item * B<:globalStorage> : variables used to manage global storage =item * B<:locationRules> : variables used to manage area protection =item * B<:import> : import function inherited from L and related variables =item * B<:headers> : functions and variables used to manage custom HTTP headers exported to the applications =item * B : functions and variables used to dialog with mod_perl. This is done to be compatible both with Apache 1 and 2. =back =head1 SEE ALSO L, L, L =head1 AUTHOR Xavier Guimard, Ex.guimard@free.frE =head1 BUG REPORT Use OW2 system to report bug or ask for features: L =head1 DOWNLOAD Lemonldap::NG is available at L =head1 COPYRIGHT AND LICENSE Copyright (C) 2005, 2007, 2010 by Xavier Guimard Ex.guimard@free.frE This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.10.0 or, at your option, any later version of Perl 5 you may have available. =cut ## @rmethod int abort(string mess) # Logs message and exit or redirect to the portal if "useRedirectOnError" is # set to true. # @param $mess Message to log # @return Apache2::Const::REDIRECT or Apache2::Const::SERVER_ERROR sub abort { my ( $class, $mess ) = splice @_; # If abort is called without a valid request, fall to die eval { my $args = $apacheRequest->args; my $uri = $apacheRequest->uri . ( $args ? "?$args" : "" ); # Set error 500 in logs even if "useRedirectOnError" is set $apacheRequest->push_handlers( PerlLogHandler => sub { $_[0]->status(SERVER_ERROR); DECLINED; } ); $class->lmLog( $mess, 'error' ); # Redirect or die if ($useRedirectOnError) { $class->lmLog( "Use redirect for error", 'debug' ); return $class->goToPortal( $uri, 'lmError=500' ); } else { return SERVER_ERROR; } }; die $mess if ($@); } ## @rmethod protected int handler_mp1() # Launch run() when used under mod_perl version 1 # @return Apache constant sub handler_mp1 ($$) { shift->run(@_); } ## @rmethod protected int logout_mp1() # Launch unlog() when used under mod_perl version 1 # @return Apache constant sub logout_mp1 ($$) { shift->unlog(@_); } ## @imethod void init(hashRef args) # Calls localInit() and globalInit(). # @param $args reference to the initialization hash sub init($$) { my $class = shift; $class->localInit(@_); $class->globalInit(@_); } ## @imethod protected void locationRulesInit(hashRef args) # Compile rules. # Rules are stored in $args->{locationRules} that contains regexp=>test # expressions where : # - regexp is used to test URIs # - test contains an expression used to grant the user # # This function creates 2 arrays containing : # - the list of the compiled regular expressions # - the list of the compiled functions (compiled with conditionSub()) # @param $args reference to the configuration hash sub locationRulesInit { my ( $class, $args ) = splice @_; $locationCount = 0; # Pre compilation : both regexp and conditions foreach ( sort keys %{ $args->{locationRules} } ) { if ( $_ eq 'default' ) { ( $defaultCondition, $defaultProtection ) = $class->conditionSub( $args->{locationRules}->{$_} ); } else { ( $locationCondition->[$locationCount], $locationProtection->[$locationCount] ) = $class->conditionSub( $args->{locationRules}->{$_} ); $locationRegexp->[$locationCount] = qr/$_/; $locationCount++; } } # Default police: all authenticated users are accepted ( $defaultCondition, $defaultProtection ) = $class->conditionSub('accept') unless ($defaultCondition); 1; } ## @imethod protected void forgeHeadersInit(hashRef args) # Create the &$forgeHeaders subroutine used to insert # headers into the HTTP request. # @param $args reference to the configuration hash sub forgeHeadersInit { my ( $class, $args ) = splice @_; # Creation of the subroutine who will generate headers my %tmp; if ( $args->{exportedHeaders} ) { %tmp = %{ $args->{exportedHeaders} }; } else { %tmp = ( 'User-Auth' => '$uid' ); } foreach ( keys %tmp ) { $tmp{$_} =~ s/\$(\w+)/\$datas->{$1}/g; $tmp{$_} = $class->regRemoteIp( $tmp{$_} ); } my $sub; foreach ( keys %tmp ) { $sub .= "'$_' => join('',split(/[\\r\\n]+/,$tmp{$_})),"; } $forgeHeaders = ( SAFEWRAP ? $class->safe->wrap_code_ref( $class->safe->reval("sub{$sub}") ) : $class->safe->reval("sub{return($sub)}") ); $class->lmLog( "$class: Unable to forge headers: $@: sub {$sub}", 'error' ) if ($@); 1; } ## @imethod protected void headerListInit(hashRef args) # Lists the exported HTTP headers into $headerList # @param $args reference to the configuration hash sub headerListInit { my ( $class, $args ) = splice @_; if ( $args->{exportedHeaders} ) { my @tmp = keys %{ $args->{exportedHeaders} }; $headerList = \@tmp; } 1; } ## @imethod protected buildPostForm(string url, int count) # Build form that will be posted by client # Fill an input hidden with fake value to # reach the size of initial request # @param url Target of POST # @param count Fake input size # @return Apache2::Const::OK sub buildPostForm { my $class = shift; my $url = shift; my $count = shift || 1000; $apacheRequest->handler("perl-script"); $apacheRequest->set_handlers( 'PerlResponseHandler' => sub { my $r = shift; $r->content_type('text/html; charset=UTF-8'); $r->print( qq{
} ); OK; } ); OK; } ## @rmethod protected void sendHeaders() # Launch function compiled by forgeHeadersInit() sub sendHeaders { my ($class) = splice @_; $class->lmSetHeaderIn( $apacheRequest, &$forgeHeaders ); } ## @rmethod protected void cleanHeaders() # Clean HTTP headers to prevent user to send custom headers # that would not be caught if access rule is unprotect or skip sub cleanHeaders { my ($class) = splice @_; $class->lmUnsetHeaderIn( $apacheRequest, @{$headerList} ); } ## @rmethod protected int isUnprotected() # @return 0 if URI is protected, # UNPROTECT if it is unprotected by "unprotect", # SKIP if it is unprotected by "skip" sub isUnprotected { my ( $class, $uri ) = splice @_; for ( my $i = 0 ; $i < $locationCount ; $i++ ) { return $locationProtection->[$i] if ( $uri =~ $locationRegexp->[$i] ); } return $defaultProtection; } ## @rmethod protected boolean grant(string uri) # Grant or refuse client using compiled regexp and functions # @param uri URI requested # @return True if the user is granted to access to the current URL sub grant { my ( $class, $uri ) = splice @_; for ( my $i = 0 ; $i < $locationCount ; $i++ ) { return &{ $locationCondition->[$i] }($datas) if ( $uri =~ $locationRegexp->[$i] ); } return &$defaultCondition($datas); } ## @imethod protected void postUrlInit() # Prepare methods to post form attributes sub postUrlInit { my ( $class, $args ) = splice @_; # Do nothing if no POST configured return unless ( $args->{post} ); # Load required modules eval 'use Apache2::Filter;use URI'; # Prepare transform sub $transform = {}; # Browse all POST URI while ( my ( $url, $d ) = each( %{ $args->{post} } ) ) { # Where to POST $d->{postUrl} ||= $url; # Register POST form for POST URL $transform->{ $d->{postUrl} } = sub { $class->buildPostForm( $d->{postUrl} ) } if ( $url ne $d->{postUrl} ); # Get datas to POST my $expr = $d->{expr}; my %postdata; # Manage old and new configuration format # OLD: expr => 'param1 => value1, param2 => value2', # NEW : expr => { param1 => value1, param2 => value2 }, if ( ref $expr eq 'HASH' ) { %postdata = %$expr; } else { %postdata = split /(?:\s*=>\s*|\s*,\s*)/, $expr; } # Build string for URI::query_form my $tmp; foreach ( keys %postdata ) { $postdata{$_} =~ s/\$(\w+)/\$datas->{$1}/g; $postdata{$_} = "'$postdata{$_}'" if ( $postdata{$_} =~ /^\w+$/ ); $tmp .= "'$_'=>$postdata{$_},"; } $class->lmLog( "Compiling POST request for $url", 'debug' ); $transform->{$url} = sub { return $class->buildPostForm($url) if ( $apacheRequest->method ne 'POST' ); $apacheRequest->add_input_filter( sub { $class->postFilter( $tmp, @_ ); } ); OK; } } } ## @rmethod protected int postFilter(hashref data, Apache2::Filter f) # POST data # @param $data Data to POST # @param $f Current Apache2::Filter object # @return Apache2::Const::OK sub postFilter { my $class = shift; my $data = shift; my $f = shift; my $l; unless ( $f->ctx ) { $f->ctx(1); my $u = URI->new('http:'); $u->query_form( { $class->safe->reval($data) } ); my $s = $u->query(); $l = $f->r->headers_in->{'Content-Length'}; $f->r->headers_in->set( 'Content-Length' => length($s) ); $f->r->headers_in->set( 'Content-Type' => 'application/x-www-form-urlencoded' ); $f->print($s); while ( $f->read( my $b, $l ) ) { } $f->seen_eos(1); } return OK; } ## @rmethod protected transformUri(string uri) # Transform URI to replay POST forms # @param uri URI to catch # @return Apache2::Const sub transformUri { my ( $class, $uri ) = splice @_; if ( defined( $transform->{$uri} ) ) { return &{ $transform->{$uri} }; } OK; } ## @method private string _buildUrl(string s) # Transform / into http(s?)://:/s # @param $s path # @return URL sub _buildUrl { my ( $class, $s ) = splice @_; my $portString = $port || $apacheRequest->get_server_port(); $portString = ( $https && $portString == 443 ) ? '' : ( !$https && $portString == 80 ) ? '' : ':' . $portString; return "http" . ( $https ? "s" : "" ) . "://" . $apacheRequest->get_server_name() . $portString . $s; } # Status daemon creation ## @ifn protected void statusProcess() # Launch the status processus. sub statusProcess { require IO::Pipe; $statusPipe = IO::Pipe->new; $statusOut = IO::Pipe->new; if ( my $pid = fork() ) { $statusPipe->writer(); $statusOut->reader(); $statusPipe->autoflush(1); } else { require Data::Dumper; $statusPipe->reader(); $statusOut->writer(); my $fdin = $statusPipe->fileno; my $fdout = $statusOut->fileno; open STDIN, "<&$fdin"; open STDOUT, ">&$fdout"; my @tmp = (); push @tmp, "-I$_" foreach (@INC); exec 'perl', '-MLemonldap::NG::Handler::Status', @tmp, '-e', '&Lemonldap::NG::Handler::Status::run(' . $localStorage . ',' . Data::Dumper->new( [$localStorageOptions] )->Terse(1)->Dump . ');'; } } ## @rmethod int unprotect() # Used to unprotect an area. # To use it, set "PerlHeaderParserHandler My::Package->unprotect" Apache # configuration file. # It replace run() by doing nothing. # @return Apache2::Const::OK sub unprotect { OK; } ## @rmethod protected void localUnlog() # Delete current user from local cache entry. sub localUnlog { my $class = shift; if ( my $id = $class->fetchId ) { # Delete Apache thread datas if ( $id eq $datas->{_session_id} ) { $datas = {}; } # Delete Apache local cache if ( $refLocalStorage and $refLocalStorage->get($id) ) { $refLocalStorage->remove($id); } } } ## @rmethod protected int unlog(Apache::RequestRec apacheRequest) # Call localUnlog() then goToPortal() to unlog the current user. # @return Apache2::Const value returned by goToPortal() sub unlog ($$) { my $class; ( $class, $apacheRequest ) = splice @_; $class->localUnlog; $class->updateStatus( $apacheRequest->connection->remote_ip, $apacheRequest->uri, 'LOGOUT' ); return $class->goToPortal( '/', 'logout=1' ); } ## @rmethod protected int redirectFilter(string url, Apache2::Filter f) # Launch the current HTTP request then redirects the user to $url. # Used by logout_app and logout_app_sso targets # @param $url URL to redirect the user # @param $f Current Apache2::Filter object # @return Apache2::Const::OK sub redirectFilter { my $class = shift; my $url = shift; my $f = shift; unless ( $f->ctx ) { # Here, we can use Apache2 functions instead of lmSetHeaderOut because # this function is used only with Apache2. $f->r->status(REDIRECT); $f->r->status_line("303 See Other"); $f->r->headers_out->unset('Location'); $f->r->err_headers_out->set( 'Location' => $url ); $f->ctx(1); } while ( $f->read( my $buffer, 1024 ) ) { } $class->updateStatus( ( $datas->{$whatToTrace} ? $datas->{$whatToTrace} : $f->r->connection->remote_ip ), 'filter', 'REDIRECT' ); return OK; } ## @rmethod int status(Apache2::RequestRec $r) # Get the result from the status process and launch a PerlResponseHandler to # display it. # @param $r Current request # @return Apache2::Const::OK sub status($$) { my ( $class, $r ) = splice @_; $class->lmLog( "$class: request for status", 'debug' ); return $class->abort("$class: status page can not be displayed") unless ( $statusPipe and $statusOut ); $r->handler("perl-script"); print $statusPipe "STATUS" . ( $r->args ? " " . $r->args : '' ) . "\n"; my $buf; while (<$statusOut>) { last if (/^END$/); $buf .= $_; } if ( MP() == 2 ) { $r->push_handlers( 'PerlResponseHandler' => sub { my $r = shift; $r->content_type('text/html; charset=UTF-8'); $r->print($buf); OK; } ); } else { $r->push_handlers( 'PerlHandler' => sub { my $r = shift; $r->content_type('text/html; charset=UTF-8'); $r->send_http_header; $r->print($buf); OK; } ); } return OK; }