File Coverage

blib/lib/Apache/AppSamurai.pm
Criterion Covered Total %
statement 15 38 39.4
branch 1 2 50.0
condition n/a
subroutine 5 5 100.0
pod n/a
total 21 45 46.6


line stmt bran cond sub pod time code
1             # Apache::AppSamurai - Protect your master, even if he is without honour.
2              
3             # $Id: AppSamurai.pm,v 1.66 2008/05/03 06:43:25 pauldoom Exp $
4              
5             ##
6             # Copyright (c) 2008 Paul M. Hirsch (paul@voltagenoir.org).
7             # All rights reserved.
8             #
9             # This program is free software; you can redistribute it and/or modify it under
10             # the same terms as Perl itself.
11             ##
12              
13             # AppSamurai is a set of scripts and a module that tie into Apache via
14             # mod_perl to provide an authenticating reverse proxy front end for
15             # web applications. It allows the use of outside authentication not
16             # supported by the backend app, and also adds session tracking.
17              
18             package Apache::AppSamurai;
19 1     1   24378 use strict;
  1         4  
  1         64  
20 1     1   7 use warnings;
  1         2  
  1         40  
21              
22             # Keep VERSION (set manually) and REVISION (set by CVS)
23 1     1   7 use vars qw($VERSION $REVISION $MP);
  1         6  
  1         104  
24             $VERSION = '1.01';
25             $REVISION = substr(q$Revision: 1.66 $, 10, -1);
26              
27 1     1   8 use Carp;
  1         2  
  1         1823  
28              
29             # mod_perl Includes
30             BEGIN {
31 1 50   1   4 if (eval{require mod_perl2;}) {
  1         1227  
32 0         0 mod_perl2->import(qw(1.9922 StackedHandlers MethodHandlers Authen
33             Authz));
34 0         0 require Apache2::Connection;
35 0         0 require Apache2::RequestRec;
36 0         0 require Apache2::RequestUtil;
37 0         0 require Apache2::Log;
38 0         0 require Apache2::Access;
39 0         0 require Apache2::Response;
40 0         0 require Apache2::Util;
41 0         0 require Apache2::URI;
42 0         0 require APR::Table;
43 0         0 require APR::Pool;
44 0         0 require Apache2::Const;
45 0         0 Apache2::Const->import(qw(OK DECLINED REDIRECT HTTP_FORBIDDEN
46             HTTP_INTERNAL_SERVER_ERROR
47             HTTP_MOVED_TEMPORARILY HTTP_UNAUTHORIZED
48             M_GET));
49 0         0 require Apache2::Request;
50 0         0 $MP = 2;
51             } else {
52 1         19553 require mod_perl;
53 0           mod_perl->import(qw(1.07 StackedHandlers MethodHandlers Authen Authz));
54 0           require Apache;
55 0           require Apache::Log;
56 0           require Apache::Util;
57 0           require Apache::Constants;
58 0           Apache::Constants->import(qw(OK DECLINED REDIRECT HTTP_FORBIDDEN
59             HTTP_INTERNAL_SERVER_ERROR
60             HTTP_MOVED_TEMPORARILY HTTP_UNAUTHORIZED
61             M_GET));
62 0           require Apache::Request;
63 0           $MP = 1;
64             }
65             }
66              
67             # Non-mod_perl includes
68             use CGI::Cookie;
69             use URI;
70             use Time::HiRes qw(usleep);
71              
72             use Apache::AppSamurai::Util qw(CreateSessionAuthKey CheckSidFormat
73             HashPass HashAny ComputeSessionId
74             CheckUrlFormat CheckHostName
75             CheckHostIP XHalf);
76              
77             # Apache::AppSamurai::Session is a replacement for Apache::Session::Flex
78             # It provides normal Apache::Session::Flex features, plus optional extras
79             # like alternate session key generators/sizes and record level encryption
80             use Apache::AppSamurai::Session;
81              
82             # Apache::AppSamurai::Tracker is a special instance of Session meant to
83             # be shared between all processes serving an auth_name
84             use Apache::AppSamurai::Tracker;
85              
86             ### START Apache::AuthSession based methods
87              
88             # The following lower case methods are directly based on Apache::AuthCookie, or
89             # are required AuthCookie methods (like authen_cred() and authen_ses_key())
90              
91             # Note - ($$) syntax, used in mod_perl 1 to induce calling the handler as
92             # an object, has been eliminated in mod_perl 2. Each handler method called
93             # directly from Apache must be wrapped to support mod_perl 1 and mod_perl 2
94             # calls. (Just explaining the mess before you have to read it.)
95              
96             # Identify the username for the session and set for the request
97             sub recognize_user_mp1 ($$) { &recognize_user_real }
98             sub recognize_user_mp2 : method { &recognize_user_real }
99             *recognize_user = ($MP eq 1) ? \&recognize_user_mp1 : \&recognize_user_mp2;
100              
101             sub recognize_user_real {
102             my ($self, $r) = @_;
103             my ($auth_type, $auth_name) = ($r->auth_type, $r->auth_name);
104            
105             return DECLINED unless $auth_type and $auth_name;
106              
107             my $cookie_name = $self->cookie_name($r);
108            
109             my ($cookie) = $r->headers_in->{'Cookie'} =~ /$cookie_name=([^;]+)/;
110             if (!$cookie && $r->dir_config("${auth_name}Keysource")) {
111             # Try to get key text using alternate method then compute the key.
112             # FetchKeysource returns '' if no custom source is configured, in
113             # which case the cookie should have been previously set, so non-zero
114             # output is required.
115             $cookie = $self->FetchKeysource($r);
116             if ($cookie) {
117             $cookie = CreateSessionAuthKey($cookie);
118             }
119             }
120            
121             return DECLINED unless $cookie;
122            
123             $self->Log($r, ('debug', "recognize_user(): cookie $cookie_name is " . XHalf($cookie)));
124            
125             my ($user,@args) = $auth_type->authen_ses_key($r, $cookie);
126             if ($user and scalar @args == 0) {
127             $self->Log($r, ('debug', "recognize_user(): user is $user"));
128             ($MP eq 1) ? ($r->connection->user($user)) : ($r->user($user));
129             } elsif (scalar @args > 0 and $auth_type->can('custom_errors')) {
130             return $auth_type->custom_errors($r, $user, @args);
131             } else {
132             # Shrug
133             $self->Log($r, ('warn', "recognize_user(): Unexpected result"));
134             return DECLINED;
135             }
136            
137             return OK;
138             }
139              
140             # Get the cookie name for this protected area
141             sub cookie_name {
142             my ($self, $r) = @_;
143              
144             my $auth_type = $r->auth_type;
145             my $auth_name = $r->auth_name;
146             my $cookie_name = $r->dir_config("${auth_name}CookieName") ||
147             "${auth_type}_${auth_name}";
148             return $cookie_name;
149             }
150              
151             # Set request cache options (no-cache unless specifically told to cache)
152             sub handle_cache {
153             my ($self, $r) = @_;
154            
155             my $auth_name = $r->auth_name;
156             return unless $auth_name;
157              
158             unless ($r->dir_config("${auth_name}Cache")) {
159             $r->no_cache(1);
160             if (!$r->headers_out->{'Pragma'}) {
161             $r->err_headers_out->{'Pragma'} = 'no-cache';
162             }
163             }
164             }
165              
166             # Backdate cookie to attempt to clear from web browser cookie store
167             sub remove_cookie {
168             my ($self, $r) = @_;
169            
170             my $cookie_name = $self->cookie_name($r);
171             my $str = $self->cookie_string( request => $r,
172             key => $cookie_name,
173             value => '',
174             expires => 'Mon, 21-May-1971 00:00:00 GMT' );
175            
176             $r->err_headers_out->add("Set-Cookie" => "$str");
177            
178             $self->Log($r, ('debug', "remove_cookie(): removed_cookie \"$cookie_name\""));
179             }
180              
181             # Convert current POST request to GET
182             # Note - The use of this is questionable now that Apache::Request is being
183             # used. May go away in the future.
184             sub _convert_to_get {
185             my ($self, $r) = @_;
186             return unless $r->method eq 'POST';
187              
188             $self->Log($r, ('debug', "Converting POST -> GET"));
189              
190             # Use Apache::Request for immediate access to all arguments.
191             my $ar = ($MP eq 1) ?
192             Apache::Request->instance($r) :
193             Apache2::Request->new($r);
194            
195             # Pull list if GET and POST args
196             my @params = $ar->param;
197             my ($name, @values, $value);
198             my @pairs = ();
199              
200             foreach $name (@params) {
201             # we don't want to copy login data, only extra data.
202             $name =~ /^(destination|credential_\d+)$/ and next;
203            
204             # Pull list of values for this key
205             @values = $ar->param($name);
206            
207             # Make sure there is at least one value, which can be empty
208             (scalar(@values)) or ($values[0] = '');
209              
210             foreach $value (@values) {
211             if ($MP eq 1) {
212             push(@pairs, Apache::Util::escape_uri($name) . '=' .
213             Apache::Util::escape_uri($value));
214             } else {
215             # Assume mod_perl 2 behaviour
216             push(@pairs, Apache2::Util::escape_path($name, $r->pool) .
217             '=' . Apache2::Util::escape_path($value, $r->pool));
218             }
219             }
220             }
221            
222             $r->args(join '&', @pairs) if scalar(@pairs) > 0;
223            
224             $r->method('GET');
225             $r->method_number(M_GET);
226             $r->headers_in->unset('Content-Length');
227             }
228              
229              
230             # Handle regular (form based) login
231             sub login_mp1 ($$) { &login_real }
232             sub login_mp2 : method { &login_real }
233             *login = ($MP eq 1) ? \&login_mp1 : \&login_mp2;
234             sub login_real {
235             my ($self, $r) = @_;
236             my ($auth_type, $auth_name) = ($r->auth_type, $r->auth_name);
237            
238             # Use the magic of Apache::Request to ditch POST handling code
239             # and cut to the args.
240             my $ar = ($MP eq 1) ?
241             Apache::Request->instance($r) :
242             Apache2::Request->new($r);
243              
244             my ($ses_key, $tc, $destination, $nonce, $sig, $serverkey);
245             my @credentials = ();
246              
247             # Get the hard set destination, or setup to just reload
248             if ($r->dir_config("${auth_name}LoginDestination")) {
249             $destination = $r->dir_config("${auth_name}LoginDestination");
250             } elsif ($ar->param("destination")) {
251             $destination = $ar->param("destination");
252             } else {
253             # Someday something slick could hold the URL, then cut through
254             # to it. Someday. Today we die.
255             $self->Log($r, ('warn', "No key 'destination' found in form data"));
256             $r->subprocess_env('AuthCookieReason', 'no_cookie');
257             return $auth_type->login_form($r);
258             }
259              
260             # Check form nonce and signature
261             if (defined($ar->param("nonce")) and defined($ar->param("sig"))) {
262             unless (($nonce = CheckSidFormat($ar->param("nonce"))) and
263             ($sig = CheckSidFormat($ar->param("sig")))) {
264            
265             $self->Log($r, ('warn', "Missing/invalid form nonce or sig"));
266             $r->subprocess_env('AuthCookieReason', 'no_cookie');
267             $r->err_headers_out->{'Location'} = $self->URLErrorCode($destination, 'bad_credentials');
268             $r->status(REDIRECT);
269             return REDIRECT;
270             }
271             $serverkey = $self->GetServerKey($r) or die("FATAL: Could not fetch valid server key\n");
272              
273             # Now check!
274             unless ($sig eq ComputeSessionId($nonce, $serverkey)) {
275             # Failed!
276             $self->Log($r, ('warn', "Bad signature on posted form (Possible scripted attack)"));
277             $r->subprocess_env('AuthCookieReason', 'no_cookie');
278             $r->err_headers_out->{'Location'} = $self->URLErrorCode($destination, 'bad_credentials');
279             $r->status(REDIRECT);
280             return REDIRECT;
281             }
282             } else {
283             # Failed!
284             $self->Log($r, ('warn', "Missing NONCE and/or SIG in posted form (Possible scripted attack)"));
285             $r->subprocess_env('AuthCookieReason', 'no_cookie');
286             $r->err_headers_out->{'Location'} = $self->URLErrorCode($destination, 'bad_credentials');
287             $r->status(REDIRECT);
288             return REDIRECT;
289             }
290              
291             # Get the credentials from the data posted by the client
292             while ($tc = $ar->param("credential_" . scalar(@credentials))) {
293             push(@credentials, $tc);
294            
295             ($tc) ? ($tc =~ s/^(.).*$/$1/s) : ($tc = ''); # Only pull first char
296             # for logging
297             $self->Log($r, ('debug', "login(); Received credential_" . (scalar(@credentials) - 1) . ": $tc (hint)"));
298             }
299              
300             # Convert all args into a GET and clear the credential_X args
301             $self->_convert_to_get($r) if $r->method eq 'POST';
302            
303             # Check against credential cache if UniqueCredentials is set
304             if ($r->dir_config("${auth_name}AuthUnique")) {
305             unless ($self->CheckTracker($r, 'AuthUnique', @credentials)) {
306             # Tried to send the same credentials twice (or tracker system
307             # failure. Delete the credentials to fall through
308             @credentials = ();
309             $self->Log($r, ('warn', "login(): AuthUnique check failed: Tracker failure, or same credentials have been sent before"));
310             }
311             }
312              
313             if (@credentials) {
314             # Exchange the credentials for a session key.
315             $ses_key = $self->authen_cred($r, @credentials);
316             if ($ses_key) {
317             # Set session cookie with expiration included if SessionExpire
318             # is set. (Extended +8 hours so we see logout events and cleanup)
319             if ($r->dir_config("${auth_name}SessionExpire")) {
320             $self->send_cookie($r, $ses_key, {expires => $r->dir_config("${auth_name}SessionExpire") + 28800});
321             } else {
322             $self->send_cookie($r, $ses_key);
323             }
324             $self->handle_cache($r);
325            
326             # Log 1/2 of session key to debug
327             $self->Log($r, ('debug', "login(): session key (browser cookie value): " . XHalf($ses_key)));
328            
329             # Godspeed You Black Emperor!
330             $r->headers_out->{"Location"} = $destination;
331             return HTTP_MOVED_TEMPORARILY;
332             }
333             }
334              
335             # Add their IP to the failure tracker
336             # Ignores return (refusing a login page to an attacker doesn't stop them
337             # from blindly reposting... can add a fail here if an embedded form
338             # verification key is added to the mix in the future)
339             if ($r->dir_config("${auth_name}IPFailures")) {
340             if ($MP eq 1) {
341             $self->CheckTracker($r, 'IPFailures', $r->dir_config("${auth_name}IPFailures"), $r->get_remote_host);
342             } else {
343             $self->CheckTracker($r, 'IPFailures', $r->dir_config("${auth_name}IPFailures"), $r->connection->get_remote_host);
344             }
345             }
346              
347             # Append special error message code and try to redirect to the entry
348             # point. (Avoids having the LOGIN URL show up in the browser window)
349             $r->err_headers_out->{'Location'} = $self->URLErrorCode($destination, 'bad_credentials');
350             $r->status(REDIRECT);
351             return REDIRECT;
352             # Handle this ol' style - XXX remove?
353             #$r->subprocess_env('AuthCookieReason', 'bad_credentials');
354             #$r->uri($destination);
355             #return $auth_type->login_form($r);
356             }
357              
358             # Special version of login that handles Basic Auth login instead of form
359             # Can be called by authenticate() if there is no valid session but a
360             # Authorization: Basic header is detected. Can also be called directly,
361             # just like login() for targeted triggering
362             sub loginBasic_mp1 ($$) { &loginBasic_real }
363             sub loginBasic_mp2 : method { &loginBasic_real }
364             *loginBasic = ($MP eq 1) ? \&loginBasic_mp1 : \&loginBasic_mp2;
365             sub loginBasic_real {
366             my ($self, $r) = @_;
367             my ($auth_type, $auth_name) = ($r->auth_type, $r->auth_name);
368            
369             my ($ses_key, $t, @at, $tc);
370             my @credentials = ();
371              
372             return DECLINED unless $r->is_initial_req; # Authenticate first req only
373            
374             # Count input credentials to figure how to split input
375             my @authmethods = $self->GetAuthMethods($r);
376             (@authmethods) || (die("loginBasic(): Missing authentication methods\n"));
377             my $amc = scalar(@authmethods);
378              
379             # Extract basic auth info and fill out @credentials array
380             my ($stat, $pass) = $r->get_basic_auth_pw;
381              
382             if ($r->user && $pass) {
383             # Strip "domain\" portion of user if present.
384             # (Thanks Windows Mobile ActiveSync for forcing domain\username syntax)
385             $t = $r->user;
386             $t =~ s/^.*\\+//;
387             $r->user($t);
388             push(@credentials, $t);
389              
390             # Use custom map pattern if set; else just a generic split on semicolon
391             if (defined($r->dir_config("${auth_name}BasicAuthMap"))) {
392             push(@credentials, $self->ApplyAuthMap($r,$pass,$amc));
393             } else {
394             # Boring old in-order split
395             foreach (split(';', $pass, $amc)) {
396             push(@credentials, $_);
397             }
398             }
399              
400             # Log partial first char of each credential
401             if ($r->dir_config("${auth_name}Debug")) {
402             for (my $i = 0; $i < scalar(@credentials); $i++) {
403             $credentials[$i] =~ /^(.)/;
404             $self->Log($r, ('debug', "loginBasic(): Received credential_$i: $1 (hint)"));
405             }
406             }
407              
408             # Check against credential cache if AuthUnique is set
409             if ($r->dir_config("${auth_name}AuthUnique")) {
410             unless ($self->CheckTracker($r, 'AuthUnique', @credentials)) {
411             # Tried to send the same credentials twice (or tracker system
412             # failure. Delete the credentials to fall through
413             @credentials = ();
414             $self->Log($r, ('warn', "loginBasic(): AuthUnique check failed: Same credentials have been sent before"));
415             }
416             }
417            
418             if (@credentials) {
419             # Exchange the credentials for a session key.
420             $ses_key = $self->authen_cred($r, @credentials);
421             if ($ses_key) {
422             # Set session cookie with expiration included if SessionExpire
423             # is set. (Extended +8 hours for logouts/cleanup)
424             if ($r->dir_config("${auth_name}SessionExpire")) {
425             $self->send_cookie($r, $ses_key, {expires => $r->dir_config("${auth_name}SessionExpire") + 28800});
426             } else {
427             $self->send_cookie($r, $ses_key);
428             }
429             $self->handle_cache($r);
430              
431             # Log 1/2 of session key to debug
432             $self->Log($r, ('debug', "loginBasic(): session key (browser cookie value): " . XHalf($ses_key)));
433            
434             # Godspeed You Black Emperor!
435             $t = $r->uri;
436             ($r->args) && ($t .= '?' . $r->args);
437             $self->Log($r, ('debug', "loginBasic(): REDIRECTING TO: $t"));
438             $r->err_headers_out->{'Location'} = $t;
439             return REDIRECT;
440             }
441             }
442             }
443              
444             # Unset the username if set
445             $r->user() and $r->user(undef);
446              
447             # Add their IP to the failure tracker and just return HTTP_FORBIDDEN
448             # if they exceed the limit
449             if ($r->dir_config("${auth_name}IPFailures")) {
450             if ($MP eq 1) {
451             unless ($self->CheckTracker($r, 'IPFailures', $r->dir_config("${auth_name}IPFailures"), $r->get_remote_host)) {
452             $self->Log($r, ('warn', "loginBasic(): Returning HTTP_FORBIDDEN to IPFailires banned IP"));
453             return HTTP_FORBIDDEN;
454             }
455             } else {
456             unless ($self->CheckTracker($r, 'IPFailures', $r->dir_config("${auth_name}IPFailures"), $r->connection->get_remote_host)) {
457             $self->Log($r, ('warn', "loginBasic(): Returning HTTP_FORBIDDEN to IPFailires banned IP"));
458             return HTTP_FORBIDDEN;
459             }
460             }
461             }
462              
463             # Set the basic auth header and send back to the client
464             $r->note_basic_auth_failure;
465             return HTTP_UNAUTHORIZED;
466             }
467              
468              
469             # Logout, kill session, kill, kill, kill
470             sub logout_mp1 ($$) { &logout_real }
471             sub logout_mp2 : method { &logout_real }
472             *logout = ($MP eq 1) ? \&logout_mp1 : \&logout_mp2;
473             sub logout_real {
474             my $self = shift;
475             my $r = shift;
476             my $auth_name = $r->auth_name;
477             my $redirect = shift || "";
478             my ($sid, %sess, $sessconfig, $username, $alterlist);
479            
480             # Get the Cookie header. If there is a session key for this realm, strip
481             # off everything but the value of the cookie.
482             my $cookie_name = $self->cookie_name($r);
483             my ($key) = $r->headers_in->{'Cookie'} =~ /$cookie_name=([^;]+)/;
484            
485             # Try custom keysource if no cookie is present and Keysource is configured
486             if (!$key && $auth_name && $r->dir_config("${auth_name}Keysource")) {
487             # Pull in key text
488             $key = $self->FetchKeysource($r);
489             # Non-empty, so use to generate the real session auth key
490             if ($key) {
491             $key = CreateSessionAuthKey($key);
492             }
493             }
494              
495             # If set, check key format, else check for custom keysource
496             if ($key) {
497             ($key = CheckSidFormat($key)) || (($self->Log($r, 'error', 'logout(): Invalid Session Key Format')) && (return undef));
498             }
499              
500             # Get session config from Apache
501             ($sessconfig = $self->GetSessionConfig($r)) || (die("logout: Unable to get session configuration while checking authentication\n"));
502              
503             if ($key) {
504             # Enter the authentication key into the session config (NEVER STORE IT
505             # IN THE ACTUAL SESSION DATA!)
506             $sessconfig->{key} = $key;
507              
508             # Compute real session ID
509             ($sessconfig->{ServerKey}) ||
510             (($self->Log($r, ('error', 'logout(): ${auth_name}SessionServerPass or ${auth_name}SessionServerKey not set (required for HMAC sessions)'))) &&
511             (return undef));
512             ($sid = ComputeSessionId($key, $sessconfig->{ServerKey})) || (($self->Log($r, ('error', 'logout(): Error computing session ID'))) && (return undef));
513             } else {
514             $sid = '';
515             }
516              
517             # Try to delete the session. Note that session handling errors do not
518             # return but fall through to return OK or REDIRECT depending
519             # on how we were called.
520             if ($sid) {
521             # Check the SID
522             if ($sid = CheckSidFormat($sid)) {
523             # Open the session (this should die on a non-existant session)!!!
524             eval { tie(%sess, 'Apache::AppSamurai::Session', $sid, $sessconfig); };
525             if ($@) {
526             $self->Log($r, ('debug', "logout(): Unable to open session \"$sid\": $@"));
527             } else {
528             $username = $sess{'username'};
529            
530             # Load alterlist
531             $alterlist = $self->AlterlistLoad(\%sess);
532             # Re-apply passback cookies to which were cleared and backdated
533             # after session creation. (This clears the passback cookies)
534             if (defined($alterlist->{cookie})) {
535             $self->AlterlistPassBackCookie($alterlist, $r);
536             }
537              
538             $self->DestroySession($r, \%sess);
539             untie(%sess);
540             $self->Log($r, ('notice', "LOGOUT: username=\"$username\", session=\"$sid\", reason=logout"));
541             }
542             } else {
543             $self->Log($r, ('error', 'logout(): Invalid Session ID Format'));
544             }
545             } else {
546             # No cookie set
547             $self->Log($r, ('error', 'logout(): Missing session ID'));
548             }
549            
550             # Clear cookie and set no-cache for client
551             $self->remove_cookie($r);
552             $self->handle_cache($r);
553              
554             # Check for hard-coded redirect for logout, or failing that, our
555             # landing page
556             if ($r->dir_config("${auth_name}LogoutDestination")) {
557             $redirect = $r->dir_config("${auth_name}LogoutDestination");
558             } elsif ($r->dir_config("${auth_name}LoginDestination")) {
559             $redirect = $r->dir_config("${auth_name}LoginDestination");
560             }
561              
562             if ($redirect ne '') {
563             $r->err_headers_out->{'Location'} = $redirect;
564             $r->status(REDIRECT);
565             return REDIRECT;
566             } else {
567             # Strip path and reload - THIS ONLY WORKS IF / IS REDIRECTED TO THE
568             # LANDING PAGE
569             $r->err_headers_out->{'Location'} = '/';
570             $r->status(REDIRECT);
571             return REDIRECT;
572             }
573            
574             # Returning the login form without redirecting on logout is probably not
575             # right for any circumstance. (Leaving this here for reference.)
576             # else {
577             # return $self->login_form($r);
578             # }
579             }
580              
581              
582             # Check for unauthenticated session and force login if not authenticated
583             sub authenticate_mp1 ($$) { &authenticate_real }
584             sub authenticate_mp2 : method { &authenticate_real }
585             *authenticate = ($MP eq 1) ? \&authenticate_mp1 : \&authenticate_mp2;
586             sub authenticate_real {
587             my ($self, $r) = @_;
588             my $auth_user;
589             my ($t, $foundcookie);
590            
591             unless ($r->is_initial_req) {
592             if (defined $r->prev) {
593             # we are in a sub-request. Just copy user from previous request.
594             ($MP eq 1) ? ($r->connection->user($r->prev->connection->user)) :
595             ($r->user($r->prev->user));
596             }
597             return OK;
598             }
599            
600             # Type must either be our own, or Basic
601             unless (($r->auth_type eq $self) || ($r->auth_type =~ /^basic$/i)) {
602             # Location requires authentication but we don't handle this AuthType.
603             $self->Log($r, ('debug', "authenticate(): AuthType mismatch: $self =/= ".$r->auth_type));
604             return DECLINED;
605             }
606              
607             # AuthType is $auth_type which we handle, Check the authentication realm
608             my $auth_name = $r->auth_name;
609             $self->Log($r, ('debug', "authenticate(): auth_name " . $auth_name));
610             unless ($auth_name) {
611             $r->log_reason("AuthName not set, AuthType=$self", $r->uri);
612             return HTTP_INTERNAL_SERVER_ERROR;
613             }
614            
615             # Get the Cookie header. If there is a session key for this realm, strip
616             # off everything but the value of the cookie.
617             my $cookie_name = $self->cookie_name($r);
618             my ($ses_key_cookie) = ($r->headers_in->{"Cookie"} || "") =~ /$cookie_name=([^;]+)/;
619            
620             $foundcookie = 0;
621             if ($ses_key_cookie) {
622             # If cookie found and not "", set $foundcookie to note auth key source
623             $foundcookie = 1;
624             } elsif ($r->dir_config("${auth_name}Keysource")) {
625             # Try custom keysource if no cookie is present and Keysource is configured
626             # Pull in key text
627             $ses_key_cookie = $self->FetchKeysource($r);
628              
629             if ($ses_key_cookie) {
630             # Non-empty, so use to generate the real session auth key
631             $ses_key_cookie = CreateSessionAuthKey($ses_key_cookie);
632             } else {
633             $ses_key_cookie = "";
634             }
635             } else {
636             $ses_key_cookie = "";
637             }
638              
639             # Report half of session key
640             $self->Log($r, ('debug', "authenticate(): Current ses_key_cookie: \"" . XHalf($ses_key_cookie) . "\""));
641            
642             if ($ses_key_cookie) {
643             my ($auth_user, @args) = $self->authen_ses_key($r, $ses_key_cookie);
644            
645             if ($auth_user and scalar @args == 0) {
646             # We have a valid session key, so we return with an OK value.
647             # Tell the rest of Apache what the authentication method and
648             # user is.
649             if ($MP eq 1) {
650             $r->connection->auth_type($self);
651             $r->connection->user($auth_user);
652             } else {
653             # Assume MP2 behaviour
654             $r->ap_auth_type($self);
655             $r->user($auth_user);
656             }
657             $self->Log($r, ('debug', "authenticate(): user authenticated as $auth_user"));
658            
659             return OK;
660              
661             } elsif (scalar @args > 0 and $self->can('custom_errors')) {
662             return $self->custom_errors($r, $auth_user, @args);
663             } else {
664             # There was a session key set, but it's invalid.
665             if ($foundcookie) {
666             # Remove cookie from the client now so it does not come back.
667             $self->remove_cookie($r);
668             }
669             $self->handle_cache($r);
670             $r->subprocess_env('AppSamuraiReason', 'bad_cookie');
671              
672             # Add to our the session tracker (so we can short cut if resent)
673             # Ignores return (we are already on the way out...)
674             if ($r->dir_config("${auth_name}SessionUnique")) {
675             $self->CheckTracker($r, 'SessionUnique', $ses_key_cookie);
676             }
677             }
678             } else {
679             # They have no cookie or Keysource generated auth key
680             $r->subprocess_env('AppSamuraiReason', 'no_cookie');
681             }
682              
683             # If serving Basic, hand control over the the basic login handler
684             if ($r->auth_type =~ /^basic$/i) {
685             # (Returns an OK if the login was good or return a 401 if not.)
686             $self->Log($r, ('debug', "authenticate(): Basic auth protected area: Attempting loginBasic()"));
687             return $self->loginBasic($r);
688             } else {
689             # They aren't authenticated, and they tried to get a protected
690             # document. Send them the authen form.
691             return $self->login_form($r);
692             }
693             }
694              
695             # Generate login form
696             sub login_form {
697             my ($self, $r) = @_;
698             my $auth_name = $r->auth_name;
699            
700             # Pull POST args into the GET args and set type as GET
701             $self->_convert_to_get($r) if $r->method eq 'POST';
702            
703             my $authen_script;
704             unless ($authen_script = $r->dir_config($auth_name . "LoginScript")) {
705             $self->Log($r, ('error', "login_form(): PerlSetVar '${auth_name}LoginScript' not set", $r->uri));
706             return HTTP_INTERNAL_SERVER_ERROR;
707             }
708             $self->Log($r, ('debug', "login_form(): Displaying $authen_script"));
709             $r->custom_response(HTTP_FORBIDDEN, $authen_script);
710            
711             return HTTP_FORBIDDEN;
712             }
713              
714             # Check for sane "satisfy" setting
715             sub satisfy_is_valid {
716             my ($self, $r, $satisfy) = @_;
717             $satisfy = lc $satisfy;
718            
719             if ($satisfy eq 'any' or $satisfy eq 'all') {
720             return 1;
721             } else {
722             my $auth_name = $r->auth_name;
723             $self->Log($r, ('error', "satisfy_is_valid(): PerlSetVar ${auth_name}Satisfy $satisfy invalid",$r->uri));
724             return 0;
725             }
726             }
727              
728             # Get satisfy setting
729             sub get_satisfy {
730             my ($self, $r) = @_;
731             my $auth_name = $r->auth_name;
732             return lc $r->dir_config("${auth_name}Satisfy") || 'all';
733             }
734              
735              
736             # Check for proper authorization for the area
737             sub authorize_mp1 ($$) { &authorize_real }
738             sub authorize_mp2 : method { &authorize_real }
739             *authorize = ($MP eq 1) ? \&authorize_mp1 : \&authorize_mp2;
740             sub authorize_real {
741             my ($self, $r) = @_;
742              
743             $self->Log($r, ('debug', 'authorize(): URI '.$r->uri()));
744             return OK unless $r->is_initial_req; #only the first internal request
745            
746             unless (($r->auth_type eq $self) || ($r->auth_type =~ /^basic$/i)) {
747             $self->Log($r, ('debug', $self . "authorize(): Wrong auth type: " . $r->auth_type));
748             return DECLINED;
749             }
750            
751             my $reqs_arr = $r->requires or return DECLINED;
752            
753             my $user = ($MP eq 1) ? ($r->connection->user) : ($r->user);
754             unless ($user) {
755             # user is either undef or =0 which means the authentication failed
756             $r->log_reason("No user authenticated", $r->uri);
757             return HTTP_FORBIDDEN;
758             }
759            
760             my $satisfy = $self->get_satisfy($r);
761             return HTTP_INTERNAL_SERVER_ERROR unless $self->satisfy_is_valid($r,$satisfy);
762             my $satisfy_all = $satisfy eq 'all';
763            
764             my ($forbidden);
765             foreach my $req (@$reqs_arr) {
766             my ($requirement, $args) = split /\s+/, $req->{requirement}, 2;
767             $args = '' unless defined $args;
768             $self->Log($r, ('debug', "authorize(): requirement := $requirement, $args"));
769            
770             if ( lc($requirement) eq 'valid-user' ) {
771             if ($satisfy_all) {
772             next;
773             } else {
774             return OK;
775             }
776             }
777            
778             if($requirement eq 'user') {
779             if ($args =~ m/\b$user\b/) {
780             next if $satisfy_all;
781             return OK; # satisfy any
782             }
783            
784             $forbidden = 1;
785             next;
786             }
787            
788             # Call a custom method
789             my $ret_val = $self->$requirement($r, $args);
790             $self->Log($r, ('debug', "authorize(): $self->$requirement returned $ret_val"));
791             if ($ret_val == OK) {
792             next if $satisfy_all;
793             return OK; # satisfy any
794             }
795            
796             # Nothing succeeded, deny access to this user.
797             $forbidden = 1;
798             }
799              
800             return $forbidden ? HTTP_FORBIDDEN : OK;
801             }
802              
803             # Have a session cookie Mr. Browser
804             sub send_cookie {
805             my ($self, $r, $ses_key, $cookie_args) = @_;
806            
807             $cookie_args = {} unless defined $cookie_args;
808            
809             my $cookie_name = $self->cookie_name($r);
810            
811             my $cookie = $self->cookie_string( request => $r,
812             key => $cookie_name,
813             value => $ses_key,
814             %$cookie_args );
815            
816             # add P3P header if user has configured it.
817             my $auth_name = $r->auth_name;
818             if (my $p3p = $r->dir_config("${auth_name}P3P")) {
819             $r->err_headers_out->{'P3P'} = $p3p;
820             }
821            
822             $r->err_headers_out->add("Set-Cookie" => $cookie);
823             }
824              
825             # Convert cookie store to header ready string
826             sub cookie_string {
827             my $self = shift;
828            
829             # if passed 3 args, we have old-style call.
830             if (scalar(@_) == 3) {
831             carp "cookie_string(): deprecated old style call to ".__PACKAGE__."::cookie_string()";
832             my ($r, $key, $value) = @_;
833             return $self->cookie_string(request=>$r, key=>$key, value=>$value);
834             }
835             # otherwise assume named parameters.
836             my %p = @_;
837             for (qw/request key/) {
838             die("cookie_string(): missing required parameter $_\n") unless defined $p{$_};
839             }
840             # its okay if value is undef here.
841            
842             my $r = $p{request};
843            
844             $p{value} = '' unless defined $p{value};
845            
846             my $string = sprintf '%s=%s', @p{'key','value'};
847            
848             my $auth_name = $r->auth_name;
849            
850             if (my $expires = $p{expires} || $r->dir_config("${auth_name}Expires")) {
851             $expires = Apache::AppSamurai::Util::expires($expires);
852             $string .= "; expires=$expires";
853             }
854            
855             $string .= '; path=' . ( $self->get_cookie_path($r) || '/' );
856            
857             if (my $domain = $r->dir_config("${auth_name}Domain")) {
858             $string .= "; domain=$domain";
859             }
860            
861             if (!$r->dir_config("${auth_name}Secure") || ($r->dir_config("${auth_name}Secure") == 1)) {
862             $string .= '; secure';
863             }
864            
865             # HttpOnly is an MS extension. See
866             # http://msdn.microsoft.com/workshop/author/dhtml/httponly_cookies.asp
867             if ($r->dir_config("${auth_name}HttpOnly")) {
868             $string .= '; HttpOnly';
869             }
870            
871             return $string;
872             }
873              
874             # Retrieve session cookie value
875             sub key {
876             my ($self, $r) = @_;
877              
878             my $auth_name = $r->auth_name;
879             my $key = "";
880              
881             my $allcook = ($r->headers_in->{"Cookie"} || "");
882             my $cookie_name = $self->cookie_name($r);
883             ($key) = $allcook =~ /(?:^|\s)$cookie_name=([^;]*)/;
884              
885             # Try custom keysource if no cookie is present and Keysource is configured
886             if (!$key && $auth_name && $r->dir_config("${auth_name}Keysource")) {
887             # Pull in key text
888             $key = $self->FetchKeysource($r);
889             # Non-empty, so use to generate the real session auth key
890             if ($key) {
891             $key = CreateSessionAuthKey($key);
892             }
893             }
894              
895             return $key;
896             }
897              
898             # Retrieve session cookie path
899             sub get_cookie_path {
900             my ($self, $r) = @_;
901            
902             my $auth_name = $r->auth_name;
903            
904             return $r->dir_config("${auth_name}Path");
905             }
906              
907             # Check authentication credentials and return a new session key
908             sub authen_cred {
909             my $self = shift;
910             my $r = shift;
911             my $username = shift;
912             my @creds = @_;
913             my $alterlist = {};
914              
915             # Check for matching credentials and configured authentication methods
916             unless (@creds) {
917             $self->Log($r, ('error', "LOGIN FAILURE: Missing credentials"));
918             return undef;
919             }
920              
921             my @authmethods = $self->GetAuthMethods($r);
922             unless (@authmethods) {
923             $self->Log($r, ('error', "LOGIN FAILURE: No authentication methods defined"));
924             return undef;
925             }
926             unless (scalar(@creds) == scalar(@authmethods)) {
927             $self->Log($r, ('error', "LOGIN FAILURE: Wrong number of credentials supplied"));
928             return undef;
929             }
930            
931             my $authenticated = 0;
932            
933             my ($ret, $errors);
934            
935             # Require and get new instance of each authentication module
936             my $authenticators = $self->InitAuthenticators($r, @authmethods);
937            
938             $self->Log($r, ('debug', "authen_cred(): About to cycle authenticators"));
939             for (my $i = 0; $i < scalar(@authmethods); $i++) {
940             $self->Log($r, ('debug', "authen_cred(): Checking $authmethods[$i]"));
941            
942             # Perform auth check
943             $ret = $authenticators->{$authmethods[$i]}->Authenticate($username, $_[$i]);
944             # Log any errors, warnings, etc.
945             ($errors = $authenticators->{$authmethods[$i]}->Errors) && ($self->Log($r, $errors));
946             $self->Log($r, ('debug', "authen_cred(): Done checking $authmethods[$i]"));
947            
948             if ($ret) {
949             # Success!
950             $authenticated++;
951              
952             # Modify header (add/delete/filter) and cookie
953             # (add/delete/filter/pass) rules
954             $self->AlterlistMod($alterlist, $authenticators->{$authmethods[$i]}->{alterlist});
955             $self->Log($r, ('debug', "authen_cred(): Added alterlist groups for" . join(",", keys %{$alterlist})));
956              
957             } else {
958             # Failure! Stop checking auth.
959             last;
960             }
961             }
962              
963             $self->Log($r, ('debug', "authen_cred(): Done cycling authenticators"));
964              
965             # If the number of successful authentications equals the number of
966             # authentication methods, you may pass.
967             if (($authenticated == scalar(@authmethods)) && ($ret = $self->CreateSession($r, $username, $alterlist))) {
968             return $ret;
969             } else {
970             # Log username (Log handles cleanup of the username and all log lines)
971             if ($username) {
972             $self->Log($r, ('error', "LOGIN FAILURE: Authentication failed for \"$username\""));
973             } else {
974             $self->Log($r, ('error', "LOGIN FAILURE: Authentication failed for missing or malformed username"));
975             }
976              
977             # Lame excuse for brute force protection! Sleep from 0.0 to 1.0 secs
978             # on failure to ensure someone can DoS us. :) IPFailures tracker needs
979             # work to have a pre-check, or to have a call out to a script to
980             # do something (like add the IP to a firewall block table)
981             usleep(rand(1000000));
982             }
983              
984             return undef;
985             }
986              
987             # Check session key and return user ID
988             sub authen_ses_key {
989             my ($self, $r, $key) = @_;
990             my ($sid, %sess, $username, $sessconfig, $tk, $tv, $reason);
991             my $alterlist = {};
992              
993             # Is it well formed?
994             ($key = CheckSidFormat($key)) || (($self->Log($r,('error', 'Invalid Session Key Format'))) && (return undef));
995              
996             # Get session config from Apache
997             ($sessconfig = $self->GetSessionConfig($r)) || (die("authen_ses_key(): Unable to get session configuration while checking authentication\n"));
998              
999             # Enter the authentication key into the session config
1000             # (NOTE: MUST NOT BE STORED INSIDE SESSION)
1001             $sessconfig->{key} = $key;
1002              
1003             # Compute real session ID
1004             ($sessconfig->{ServerKey}) || (($self->Log($r, ('error', 'authen_ses_key(): ServerPass or ServerKey not set (required for HMAC sessions)'))) && (return undef));
1005            
1006             ($sid = ComputeSessionId($sessconfig->{key}, $sessconfig->{ServerKey})) || (($self->Log($r, ('error', 'authen_ses_key(): Error computing session ID'))) && (return undef));
1007            
1008             # Open the session (Eval will die on a non-existent session)
1009             eval { tie(%sess, 'Apache::AppSamurai::Session', $sid, $sessconfig); };
1010             if ($@) {
1011             $self->Log($r, ('debug', "authen_ses_key(): Unable to open session \"$sid\": $@"));
1012             return undef;
1013             }
1014              
1015             # Dump session contents to log (with some attempted cleanup for security)
1016             if ($self->_debug($r)) {
1017             my @tsl = ();
1018             push(@tsl, "authen_ses_key(): Dump of session \"$sid\": ");
1019             foreach $tk (sort keys %sess) {
1020             $tv = $sess{$tk};
1021             if ($tk eq 'al-header') {
1022             # Sanitize headers (Leaving 8 chars of context for each)
1023             $tv =~ s/^(\w+:authorization:.{1,8})(.*)$/$1 . "X" x length($2)/gmie;
1024             } elsif ($tk eq 'al-cookie') {
1025             # Sanitize cookies (Leaving 8 characters of context)
1026             $tv =~ s/^(\w+:[^\:\=]+:.{1,8})([^;]*)(;.*)$/$1 . ("X" x length($2)) . $3/gmie;
1027             } elsif ($tk =~ /auth/i) {
1028             # Probably something we want hidden
1029             $tv =~ s/^(.{1,8})(.*)$/$1 . "X" x length($2)/gmie;
1030             }
1031             push(@tsl, "$tk=>\"$tv\"");
1032             }
1033             $self->Log($r, ('debug', join(",", @tsl)));
1034             }
1035              
1036             # Pull header and cookie mod lists
1037             $alterlist = $self->AlterlistLoad(\%sess);
1038              
1039             # No reason... yet
1040             $reason = '';
1041              
1042             # Give me a reason... anything... any little excuse to kill your session...
1043             $username = $sess{'username'};
1044             if (!$username) {
1045             # Session must have a username
1046             $reason = 'no_username';
1047             # Extra-bad!
1048             $self->Log($r, ('error', "authen_ses_key(): No username for session \"$sid\""));
1049             } elsif (!$self->CheckTime(\%sess)) {
1050             # Expiration check failed
1051             $reason = 'timeout';
1052             } elsif (($sess{"Authorization"}) && ($r->headers_in->{"Authorization"}) && ($r->headers_in->{"Authorization"} ne $sess{"Authorization"})) {
1053             # Client sent a Authorization header that does not match the one sent
1054             # when logging in. This indicates one of two potential issues:
1055             # 1) For areas configured to use basic auth, the auth has changed on
1056             # the browser side, so kill the session.
1057             # 2) For areas we front with a form, this indicates that the backend
1058             # server sent a 401 to the client. We need to kill the session to
1059             # get things in line again.
1060             $reason = "basic_auth_change";
1061             }
1062              
1063             if ($reason) {
1064             # Oh no! They gave us a reason... It's ON! (well, off)
1065            
1066             # Remove passback and session cookies first
1067             if (defined($alterlist->{cookie})) {
1068             $self->AlterlistPassBackCookie($alterlist, $r);
1069             }
1070            
1071             $self->remove_cookie($r);
1072             $self->handle_cache($r);
1073            
1074             # Wake up. Time to die.
1075             $self->DestroySession($r, \%sess);
1076             untie(%sess);
1077             $self->Log($r, ('notice', "LOGOUT: username=\"$username\", session=\"$sid\", reason=$reason"));
1078            
1079             # If serving basic auth, return undef instead of triggering login form
1080             if ($r->auth_type =~ /^basic$/i) {
1081             return undef;
1082             } else {
1083             # Use Apache::AuthCookie based custom_errors feature, which will
1084             # call back into our custom_errors() method. (expired_cookie
1085             # applies as an acceptable error for all of these cases.)
1086             return('login', 'expired_cookie');
1087             }
1088             }
1089              
1090             # Apply header and cookie alterations to request headed for backend server
1091             $self->AlterlistApply($alterlist, $r);
1092             $self->Log($r, ('debug', "authen_ses_key(): Loaded and applied alterlist groups " . join(",", keys %{$alterlist})));
1093              
1094             # Release session file
1095             untie(%sess);
1096            
1097             return $username;
1098             }
1099              
1100              
1101             # custom_errors are a nice way to get flexible actions based on certain events
1102             # without having to rewrite authentication() and other methods. Takes
1103             # the request, a "code", and a message. The original intent of this was to
1104             # allow for custom server return messages, but I muck it up to do things like
1105             # redirecting on certain errors, too.
1106             sub custom_errors {
1107             my ($self, $r, $code, $message) = @_;
1108             my $t;
1109              
1110             # Handle request based on the format of the $code argument
1111             if ($code =~ /^login$/) {
1112             # Append the passed error code using ASERRCODE and bring up the login
1113             # form. (Adds error code query to the current URI, which the login
1114             # form will pull back in)
1115             $t = $r->uri;
1116             ($r->args) && ($t .= "?" . $r->args);
1117             $r->uri($self->URLErrorCode($t, 'message'));
1118             return $self->login_form($r);
1119             } elsif ($code =~ /^([A-Z0-9_]+)$/) {
1120             # Codes in all caps with an underscore are assumed to be Apache
1121             # response codes
1122             ($message) && ($r->custom_response($code, $message));
1123             return $code;
1124             } else {
1125             # What was that? Die out.
1126             die "custom_errors(): Invalid code passed to custom_errors: \"$code\"";
1127             }
1128             }
1129            
1130             ## END Apache::AuthCookie based methods
1131              
1132             # Everything past this point is not an overridden/modified Apache::AuthCookie
1133             # function.
1134              
1135             # Taking a request, try to get the AuthMethods list for the resource
1136             sub GetAuthMethods {
1137             my ($self, $r) = @_;
1138             my ($authname, $authmethlist);
1139             my @authmethods = ();
1140              
1141             # Get the auth name
1142             ($authname = $r->auth_name()) || (die("GetAuthMethods(): No auth name set for this request!\n"));
1143             ($authmethlist = $r->dir_config($authname . "AuthMethods")) || (die("GetAuthMethods(): No authentication methods found for $authname!\n"));
1144              
1145             # AuthMethods should be a comma deliminated list of methods. Let
1146             # us see, shall we?
1147             foreach (split(',', $authmethlist)) {
1148             (/^\s*(Auth[\w\d]+)\s*$/) || (die("GetAuthMethods(): Invalid ${authname}AuthMethods definition!\n"));
1149             push(@authmethods, $1);
1150             }
1151              
1152             return @authmethods;
1153             }
1154              
1155             # This just loads the appropriate Apache::AppSamurai::AuthXXX modules
1156             # so they are ready to authenticate against. Note that this function
1157             # needs only be called by authen_cred() most of the time. Returns a ref
1158             # to a hash with AuthName->AuthNameInstance mappings
1159             sub InitAuthenticators {
1160             my $self = shift;
1161             my $r = shift;
1162             my @authmethods = @_;
1163             my ($am, $amn, $lkn, $skn, $ch, $authname, $dirconfig);
1164            
1165             (scalar(@authmethods)) || (die("InitAuthenticators(): You must specify at least one authentication method!\n"));
1166              
1167             # Clear authenticator handle hash
1168             my $authenticators = {};
1169              
1170             # Get directory authentication name and a hash of its config
1171             ($authname = $r->auth_name()) || (die("InitAuthenticators(): No auth name set for this request!\n"));
1172             $dirconfig = $r->dir_config();
1173              
1174             # Init each auth method
1175             foreach $am (@authmethods) {
1176             ($am =~ /^Auth[A-Z0-9][a-zA-Z0-9:]+$/) || (die("InitAuthenticators(): Illegal authentication method name! (Check case)\n"));
1177            
1178             # Extract any config variables set for the configure auth methods
1179             # and store in a temp hash before creating auth module instance
1180             $ch = {};
1181             $lkn = '';
1182             $skn = '';
1183             foreach $lkn (keys %{$dirconfig}) {
1184             ($lkn =~ /^${authname}${am}([\w\d]+)\s*$/) || (next);
1185             $skn = $1;
1186             $ch->{$skn} = $dirconfig->{$lkn};
1187              
1188             # If a "header:" is requested, replace with the named
1189             # header's value from the client request, or an empty string
1190             if ($ch->{$skn} =~ /^header:([\w\d\-]+)$/i) {
1191             $ch->{$skn} = $r->headers_in->{$1};
1192             }
1193             }
1194              
1195             if ($am =~ /^(AuthSimple)(.+)$/) {
1196             # Framework auth modules (like AuthSimple) - These need
1197             # a master AppSamurai::Auth*** module that expects the name
1198             # of a submodule and its arguments
1199             # Set submodule name, assuming it is under the master's tree
1200             $ch->{SubModule} = $2;
1201             # Use the master auth module from AppSamurai itself
1202             $amn = 'Apache::AppSamurai::' . $1;
1203             } else {
1204             $amn = 'Apache::AppSamurai::' . $am;
1205             }
1206            
1207             (eval "require $amn;") ||
1208             (die("InitAuthenticators(): Could not load $amn\n"));
1209            
1210             {
1211             # Disable strict within block so we can call ::new
1212             no strict 'refs';
1213             ($authenticators->{$am} = $amn->new(%{$ch})) ||
1214             (die("InitAuthenticators(): Could not create new $amn instance: " . $! . "\n"));
1215             }
1216            
1217             # A little sanity check on the returned authenticator
1218             $authenticators->{$am}->can("Authenticate") or die("InitAuthenticators(): Newly created $amn instance (for $am) does not have Authenticate() method");
1219             }
1220            
1221             return $authenticators;
1222             }
1223            
1224             # Retrieve session configuration from Apache config and return as a hash ref
1225             sub GetSessionConfig {
1226             my ($self, $r) = @_;
1227             my $auth_name = ($r->auth_name()) || (die("GetSessionConfig(): No auth name defined!\n"));
1228             my $dirconfig = $r->dir_config;
1229             # Set some defaults that shouldn't be too horrible
1230             my $sessconfig = {};
1231            
1232             # Pull in session configuration
1233             foreach (keys %{$dirconfig}) {
1234             (/^${auth_name}Session([\w\d]+)\s*$/) || (next);
1235             $sessconfig->{$1} = $dirconfig->{$_};
1236             }
1237            
1238             unless (scalar(keys %{$sessconfig})) {
1239             $self->Log($r, ('error', "GetSessionConfig(): No Session configuration found for $auth_name!"));
1240             return undef;
1241             }
1242              
1243             ## TODO - This section of session autoconfig/defaults is pretty
1244             ## inflexilbe and too tightly tied to HMAC_SHA and CryptBase64.
1245             ## It should be abolished or moved out and into a generalized
1246             ## pre-config module that can be called ONCE (at startup)
1247              
1248             # Use files for storage and locking by default
1249             (exists($sessconfig->{Store})) || ($sessconfig->{Store} = "File");
1250             (exists($sessconfig->{Lock})) || ($sessconfig->{Lock} = "File");
1251              
1252             # If files are being used, paths must be set
1253             if ($sessconfig->{Store} eq 'File' && !exists($sessconfig->{Directory})) {
1254             $self->Log($r, ('error', "GetSessionConfig(): ${auth_name}SessionDirectoy must be defined for the File session store"));
1255             return undef;
1256             }
1257             if ($sessconfig->{Lock} eq 'File' && !exists($sessconfig->{LockDirectory})) {
1258             $self->Log($r, ('error', "GetSessionConfig(): ${auth_name}SessionLockDirectoy must be defined for the File session lock"));
1259             return undef;
1260             }
1261              
1262             # Use HMAC_SHA and CryptBase64 for session creation and serialization
1263             # by default.
1264             (exists($sessconfig->{Generate})) || ($sessconfig->{Generate} = "AppSamurai/HMAC_SHA");
1265             (exists($sessconfig->{Serialize})) || ($sessconfig->{Serialize} = "AppSamurai/CryptBase64");
1266            
1267             # Check/clean ServerPass if present (else assume ServerKey set)
1268             if (exists($sessconfig->{ServerPass})) {
1269             # Set the key (note - GetServerKey logs the error, if any)
1270             ($sessconfig->{ServerKey} = $self->GetServerKey($r)) || (return undef);
1271             }
1272              
1273             # We have to have a ServerKey at this point, in hex form.
1274             if (($sessconfig->{Generate} =~ /HMAC/i) || ($sessconfig->{Serialize} =~ /Crypt/i)) {
1275             unless (CheckSidFormat($sessconfig->{ServerKey})) {
1276             # Bad server key format
1277             $self->Log($r, ('error', "GetSessionConfig(): You must a valid ${auth_name}ServerPass or ${auth_name}ServerKey!"));
1278             return undef;
1279             }
1280            
1281             # For speed, SerializeCipher should be set in the config
1282             if (!$sessconfig->{SerializeCipher}) {
1283             # Attempt to load CryptBase64 module
1284             unless (eval "require Apache::AppSamurai::Session::Serialize::CryptBase64") {
1285             $self->Log($r, ('error', "GetSessionConfig(): Could not load CryptBase64 while attempting to auto-select ${auth_name}SerializeCipher value: $!"));
1286             return undef;
1287             }
1288             # Use CryptBase64 cipher detection utility (Slower)
1289             unless ($sessconfig->{SerializeCipher} = &Apache::AppSamurai::Session::Serialize::CryptBase64::find_cipher()) {
1290             # None found. (Note - Check @allowedciphers in CryptBase64.pm
1291             # for supported ciphers)
1292             $self->Log($r, ('error', "GetSessionConfig(): Could not auto-detect a suitable ${auth_name}SerializeCipher value (Please configure manualy): $!"));
1293             return undef;
1294             }
1295             }
1296             }
1297            
1298             # Set a 1hr Timeout if neither Timeout or Expire are set
1299             unless ($sessconfig->{Timeout} || $sessconfig->{Expire}) {
1300             $sessconfig->{Timeout} = 3600;
1301             }
1302            
1303             return $sessconfig;
1304             }
1305              
1306              
1307             # Compute/check server key from server pass, returning key.
1308             sub GetServerKey {
1309             my ($self, $r) = @_;
1310             my $auth_name = ($r->auth_name()) || (die("GetServerKey(): No auth name defined!\n"));
1311             my $dirconfig = $r->dir_config;
1312             my $serverkey = '';
1313            
1314             if (exists($dirconfig->{$auth_name . "SessionServerPass"})) {
1315             my $serverpass = $dirconfig->{$auth_name . "SessionServerPass"};
1316            
1317             unless ($serverpass =~ s/^\s*([[:print:]]{8,}?)\s*$/$1/s) {
1318             $self->Log($r, ('error', "GetServerKey(): Invalid ${auth_name}SessionServerPass (must be use at least 8 printable characters"));
1319             return undef;
1320             }
1321            
1322             if ($serverpass =~ /^(password|serverkey|serverpass|12345678)$/i) {
1323             $self->Log($r, ('error', "GetServerKey(): ${auth_name}SessionServerPass is $1... That is too lousy"));
1324             return undef;
1325             }
1326            
1327             unless ($serverkey = HashPass($serverpass)) {
1328             $self->Log($r, ('error', "GetServerKey(): Problem computing server key hash for ${auth_name}SessionServerPass"));
1329             return undef;
1330             }
1331              
1332             } elsif (exists($dirconfig->{$auth_name . "SessionServerKey"})) {
1333             $serverkey = $dirconfig->{$auth_name . "SessionServerKey"};
1334              
1335             } else {
1336             $self->Log($r, ('error', "GetServerKey(): You must define either ${auth_name}SessionServerPass or ${auth_name}SessionServerKey in your Apache configuration"));
1337             return undef;
1338             }
1339            
1340             # Check for valid key format
1341             unless (CheckSidFormat($serverkey)) {
1342             # Not good, dude. This should not happen
1343             $self->Log($r, ('error', "GetServerKey(): Invalid server session key (CheckSidFormat() failure) for $auth_name"));
1344             return undef;
1345             }
1346              
1347             return $serverkey;
1348             }
1349              
1350              
1351             # Apply the configured BasicAuthMap to the passed in credentials
1352             # BasicAuthMap allows for flexibly parsing a single line of authentication
1353             # data into multiple credentials in any order. (Keep those users happy...)
1354             # Returns an array with the parsed credentials in order, or an empty set on
1355             # failure.
1356             sub ApplyAuthMap {
1357             my ($self, $r, $pass, $amc) = @_;
1358             my $auth_name = ($r->auth_name) || ('');
1359             my ($o, $m, $i, @ct);
1360             my @creds = ();
1361              
1362             # Check basic map format
1363             ($r->dir_config("${auth_name}BasicAuthMap") =~ /^\s*([\d\,]+)\s*\=\s*(.+?)\s*$/) || (die("ApplyAuthMap(): Bad format in ${auth_name}BasicAuthMap\n"));
1364             $o = $1;
1365             $m = $2;
1366            
1367             # Try to map values from pass string
1368             (@ct) = $pass =~ /^$m$/;
1369             unless (scalar(@ct) eq $amc) {
1370             $self->Log($r, ('warn', "ApplyAuthMap: Unable to match credentials with ${auth_name}BasicAuthMap"));
1371             return ();
1372             }
1373            
1374             # Check credential numbers for sanity and assign values
1375             foreach $i (split(',', $o)) {
1376             ($i =~ s/^\s*(\d+)\s*$/$1/) || (die("ApplyAuthMap(): Bad mapping format in ${auth_name}BasicAuthMap\n"));
1377             push(@creds, $ct[$i - 1]);
1378             }
1379            
1380             return @creds;
1381             }
1382              
1383              
1384             # Gather header and argument items from request to build custom session
1385             # authentication key. Not nearly as secure as random generation, but
1386             # for cookie losing clients (generally automated), it is the only choice.
1387             #
1388             # Synatax:
1389             #
1390             # TYPE:NAME
1391             #
1392             # TYPE - Type of item (header or arg) to pull in
1393             # NAME - Name of header or argument to pull in
1394             #
1395             # The name match is case insensitive, but strict: Only the exact names
1396             # will be used to ensure a consistent key text source. MAKE SURE TO USE
1397             # PER-CLIENT UNIQUE VALUES! The less random the key text source is, the
1398             # easier it can be guessed/hacked. (Once again: Do not use the custom
1399             # key text source feature if you can avoid it!)
1400             sub FetchKeysource {
1401             my ($self, $r) = @_;
1402             my $auth_name = ($r->auth_name()) || (die("FetchKeysource(): No auth name defined!\n"));
1403             my @srcs = $r->dir_config->get("${auth_name}Keysource");
1404            
1405             # Return empty, which session key creators MUST interpret as a request
1406             # for a fully randomized key
1407             return '' unless (scalar @srcs);
1408              
1409             # Use Apache::Request for immediate access to all arguments.
1410             my $ar = ($MP eq 1) ? Apache::Request->instance($r) : Apache2::Request->new($r);
1411              
1412             my ($s, $t);
1413             my $keytext = '';
1414            
1415             # Pull values in with very moderate checking
1416             foreach $s (@srcs) {
1417             if ($s =~ /^\s*header:([\w\d\-\_]+)\s*$/i) {
1418             if ($r->headers_in->{$1} and
1419             ($t) = $r->headers_in->{$1} =~ /^\s*([\x20-\x7e]+?)\s*$/s) {
1420             $keytext .= $t;
1421             $self->Log($r, ('debug', "FetchKeysource(): Collected $s: " . XHalf($t)));
1422             } else {
1423             $self->Log($r, ('warn', "FetchKeysource(): Missing header field: \"$1\": Can not calculate session key"));
1424             return undef;
1425             }
1426             } elsif ($s =~ /^\s*arg:([\w\d\.\-\_]+)\s*$/i) {
1427             if (($t = $ar->param($1)) && ($t =~ s/^\s*([^\r\n]+?)\s*$/$1/)) {
1428             $keytext .= $t;
1429             $self->Log($r, ('debug', "FetchKeysource(): Collected $s: " . XHalf($t)));
1430             } else {
1431             $self->Log($r, ('warn', "FetchKeysource(): Missing argument: \"$1\": Can not calculate session key"));
1432             return undef;
1433             }
1434             } else {
1435             $self->Log($r, ('error', "FetchKeysource(): Invalid Keysource definition for $auth_name"));
1436             return undef;
1437             }
1438             }
1439            
1440             return $keytext;
1441             }
1442              
1443             # Initiate a new session and return a session key. Takes the $r request (for
1444             # record keeping), the username, and an optional "alter list" to be used
1445             # to change cookies and/or headers sent from the proxy to the backend server.
1446             sub CreateSession {
1447             my ($self, $r, $username, $alterlist) = @_;
1448             (defined($alterlist)) || ($alterlist = {});
1449             my (%sess, $sid, $sessconfig, $kt);
1450            
1451             # Extract the session config
1452             ($sessconfig = $self->GetSessionConfig($r)) || (die "CreateSession(): Unable to get session configuration while creating new session");
1453            
1454             # Create a session auth key to send back to send back as the cookie
1455             # value, and to use the HMAC-SHA and optional session file encryptor.
1456             # FetchKeysource returns "" by default, resulting in a fully randomized
1457             # key.
1458             $kt = $self->FetchKeysource($r);
1459             if (defined($kt)) {
1460             $sessconfig->{key} = CreateSessionAuthKey($kt);
1461             } else {
1462             $self->Log($r, ('warn', "CreateSession(): Failed to generate session authentication key: Session creation denied"));
1463             return undef;
1464             }
1465            
1466             # Check for valid looking key
1467             unless (CheckSidFormat($sessconfig->{key})) {
1468             $self->Log($r, ('warn', "CreateSession(): Bad session authentication key returned! Session creation denied"));
1469             return undef;
1470             }
1471              
1472             # Run against the unique session tracker if configured. (*Don't make
1473             # the same session twice)
1474             if ($sessconfig->{Unique}) {
1475             unless ($self->CheckTracker($r, 'SessionUnique', $sessconfig->{key})) {
1476             $self->Log($r, ('warn', "CreateSession(): SessionUnique detected duplicate session authentication key! Session creation denied"));
1477             return undef;
1478             }
1479             }
1480              
1481             # Wrapped this in an eval, since Apache:Session dies on failures
1482             eval { tie(%sess, 'Apache::AppSamurai::Session', undef, $sessconfig); };
1483             if ($@) {
1484             $self->Log($r, ('error', "CreateSession(): Unable to create new session: $@"));
1485             return undef;
1486             }
1487             $sid = $sess{_session_id};
1488              
1489             # Make sure we received a good session ID.
1490             (CheckSidFormat($sid)) || (($self->Log($r, ('error', 'CreateSession(): Invalid Session ID Format on new Session'))) && (return undef));
1491             $self->Log($r, ('notice', "LOGIN: username=\"$username\", session=\"$sid\""));
1492            
1493             # Store some basics
1494             $sess{'username'} = $username;
1495             $sess{'ctime'} = time();
1496            
1497             # Track last access time if Timeout is set
1498             if ($sessconfig->{Timeout}) {
1499             $sess{'atime'} = $sess{'ctime'};
1500             $sess{'Timeout'} = $sessconfig->{Timeout};
1501             }
1502              
1503             # Set hard expiration time if Expire is set
1504             if ($sessconfig->{Expire}) {
1505             $sess{'etime'} = $sess{'ctime'} + $sessconfig->{Expire};
1506             $sess{'Expire'} = $sessconfig->{Expire};
1507             }
1508              
1509             # Apply passback cookies to response, and pull in updated alterlist
1510             if (defined($alterlist->{cookie})) {
1511             $alterlist = $self->AlterlistPassBackCookie($alterlist, $r);
1512             }
1513              
1514             # If present, save Authorization header to detect future changes,
1515             # then prepend an alterlist rule to delete the header to prevent
1516             # pass though to the backend server. (If needed, a separate
1517             # alterlist rule to add an Authorization header should be set
1518             # by a auth module.)
1519             if ($r->headers_in->{"Authorization"}) {
1520             $sess{'Authorization'} = $r->headers_in->{"Authorization"};
1521             # Stick it in front in case we have an existing add
1522             # header from an auth module
1523             unshift(@{$alterlist->{header}}, 'delete:Authorization:');
1524             }
1525              
1526             # Save current alterlist to session
1527             $self->AlterlistSave($alterlist, \%sess);
1528            
1529             # Release session
1530             untie(%sess);
1531            
1532             # Return the session auth key
1533             return $sessconfig->{key};
1534             }
1535              
1536             # Destroy a session, rendering it forever useless. Takes a request hash ref
1537             # and a session hash ref as args. (Session must be tied when DestroySession
1538             # is called.)
1539             sub DestroySession {
1540             my ($self, $r, $sess) = @_;
1541              
1542             # Call the delete method for the the tied hash. Wrapped in eval goodness
1543             # since Apache::Session will die on error.
1544             eval { tied(%{$sess})->delete; };
1545             if ($@) {
1546             $self->Log($r, ('warn', "DestroySession(): Unable to destroy session: $@"));
1547             return undef;
1548             }
1549              
1550             return 1;
1551             }
1552              
1553              
1554             ## TRACKER - A system to store persistant and shared data for various
1555             ## uses. This is yet more code that could be refactored and busted into
1556             ## external modules to allow for adding arbitrary stateful checks of
1557             ## all sorts of things, (like the authentication handlers).
1558             ## For now, only a small set of tracker types are provided,
1559             ## and all are defined in this module.
1560              
1561             # Get Tracker config (Tracker being a special case Session type targetted
1562             # at IPC tasks) The tracker should never hold sensitive data since encryption
1563             # support is not provided! Make sure to hash sensitive info if you need to
1564             # track old session authentication keys or other items.
1565             sub GetTrackerConfig {
1566             my ($self, $r) = @_;
1567             my $auth_name = ($r->auth_name()) || (die("GetTrackerConfig(): No auth name defined!\n"));
1568             my $dirconfig = $r->dir_config;
1569              
1570             my $trakconfig = {};
1571              
1572             # Pull in the session configurations then write the tracker config
1573             # over top.
1574             foreach (keys %{$dirconfig}) {
1575             (/^${auth_name}Session([\w\d]+)\s*$/) || (next);
1576             $trakconfig->{$1} = $dirconfig->{$_};
1577             }
1578            
1579             foreach (keys %{$dirconfig}) {
1580             (/^${auth_name}Tracker([\w\d]+)\s*$/) || (next);
1581             $trakconfig->{$1} = $dirconfig->{$_};
1582             }
1583              
1584             # Use files for storage and locking by default
1585             (exists($trakconfig->{Store})) || ($trakconfig->{Store} = "File");
1586             (exists($trakconfig->{Lock})) || ($trakconfig->{Lock} = "File");
1587              
1588             # Always use the basic Base64 serializer: it is portable, and avoids
1589             # having to special case of override when Crypt is used on sessions
1590             $trakconfig->{Serialize} = "Base64";
1591              
1592             # If files are being used, use the Session paths (if set), else die
1593             if ($trakconfig->{Store} eq 'File' && !exists($trakconfig->{Directory})) {
1594             $self->Log($r, ('error', "GetTrackerConfig(): ${auth_name}TrackerDirectoy must be defined for the File store"));
1595             return undef;
1596             }
1597             if ($trakconfig->{Lock} eq 'File' && !exists($trakconfig->{LockDirectory})) {
1598             $self->Log($r, ('error', "GetTrackerConfig(): ${auth_name}TrackerLockDirectoy must be defined for the File lock"));
1599             return undef;
1600             }
1601              
1602             return $trakconfig;
1603             }
1604              
1605             # Initiate the tracker. Takes the $r request, a name (usually just the name of
1606             # the module using the tracker), and returns a tied tracker hash reference.
1607             # Currently does not support different settings for various tracker modules.
1608             # All use the same cleanup, etc.
1609             sub InitTracker {
1610             my ($self, $r, $name) = @_;
1611             my (%trak, $trakconfig);
1612            
1613             # Extract the tracker config
1614             ($trakconfig = $self->GetTrackerConfig($r)) || (die("InitTracker(): Unable to get tracker configuration: Please properly configure the tracker system or dissable features that use it\n"));
1615            
1616             # Basic sanity check on name, then set value in tracker config so the
1617             # Tracker module can pick it up if needed
1618             (($name) && ($name =~ /^[\w\d:\.\_\-]+$/)) || (die("InitTracker(): No tracker name or bad name specified for tracker\n"));
1619             $trakconfig->{Name} = $name;
1620              
1621             # Wrapped this in an eval, since Apache:Session type modules die on failure
1622             eval { tie(%trak, 'Apache::AppSamurai::Tracker', $name, $trakconfig); };
1623             if ($@) {
1624             $self->Log($r, ('error', "InitTracker(): Unable to setup tracker for \"$name\", retrying..."));
1625             # Try making a new one
1626             eval { tie(%trak, 'Apache::AppSamurai::Tracker', undef, $trakconfig); };
1627             if ($@) {
1628             $self->Log($r, ('error', "InitTracker(): Unable to create new tracker for \"$name\": $@"));
1629             return undef;
1630             }
1631             # Save its name inside
1632             $trak{Name} = $name;
1633             $self->Log($r, ('error', "InitTracker(): Created new tracker instance for \"$name\""));
1634             }
1635              
1636             # If cleanup is set, check if we need to run it
1637             if ($trakconfig->{Cleanup}) {
1638             # Fake last cleanup time as now, if not already set
1639             unless($trak{LastClean}) {
1640             $trak{LastClean} = time();
1641             $self->Log($r, ('debug', "InitTracker(): Set last cleanup for \"$name\" to " . $trak{LastClean}));
1642             }
1643            
1644             if ((time() - $trak{LastClean}) >= $trakconfig->{Cleanup}) {
1645             $self->CleanupTracker($r, \%trak, $trakconfig->{Cleanup});
1646             }
1647             }
1648              
1649             # Return the open tracker hash ref
1650             return \%trak;
1651             }
1652              
1653             # Cleanup stale tracker items older than the given cleanup interval.
1654             # Each item to clean MUST have a timestamp record in the first slot
1655             # prefixed by "ts".
1656             sub CleanupTracker {
1657             my ($self,$r,$trak,$to) = @_;
1658             my ($tk, $tts);
1659             my $time = time();
1660              
1661             (tied(%{$trak})) || die("CleanupTracker(): Called without valid tracker handle\n");
1662             ($trak->{LastClean}) || die("CleanupTracker(): Called without LastClean set for " . $trak->{Name} . "\n");
1663              
1664             # Default to 24 hour cleanup
1665             ($to) || die("CleanupTracker(): Called without Cleanup time specified for " . $trak->{Name} . "\n");
1666              
1667             $self->Log($r, ('debug', "CleanupTracker(): Cleaning up items in \"" . $trak->{Name} . "\" older than $to seconds"));
1668              
1669             foreach $tk (keys %{$trak}) {
1670             # Skip items with no time stamp at start of value
1671             next unless ($trak->{$tk} =~ /^ts(\d+)/);
1672             $tts = $1;
1673             if (($tts + $to) < $time) {
1674             # Older than timeout: kill!
1675             $self->Log($r, ('debug', "CleanupTracker(): Deleting stale item in \"" . $trak->{Name} . "\": $tk," . $trak->{$tk}));
1676             delete($trak->{$tk});
1677             }
1678             }
1679              
1680             # Update the LastClean time
1681             $trak->{LastClean} = $time;
1682             }
1683              
1684             # Get ready to use tracker handle, then pass the specified tracker module
1685             # (second arg), and pass it the rest of the arguments. Returns 1 on sucessful
1686             # setup and a good return from the tracker module, else 0;
1687             sub CheckTracker {
1688             my $self = shift;
1689             my $r = shift;
1690             my $tmod = shift;
1691             my @args = @_;
1692             my ($tconf,$trak);
1693             my $ret = 0;
1694              
1695             # This should all be extended to use external modules instead (like the
1696             # authentication modules.) I am pressed for time, so only a small set
1697             # of local checks are currently supported.
1698             if ($tmod =~ /^(IPFailures|AuthUnique|SessionUnique)$/) {
1699             $tmod = "CheckTracker" . $tmod;
1700              
1701             $self->Log($r, ('debug', "CheckTracker(): Calling $tmod"));
1702              
1703             unless ($trak = $self->InitTracker($r,$tmod)) {
1704             $self->Log($r, ('error', "CheckTracker(): Failed to get initialized tracker handle for $tmod"));
1705             return 0;
1706             }
1707              
1708             # Tracker methods currently die on major failure. (A move to
1709             # object based setup, like auth system, would allow removing this
1710             # while maintining proper logging.)
1711             eval { { no strict "refs"; $ret = &$tmod($trak, @args) } };
1712              
1713             if ($@) {
1714             $self->Log($r, ('error', "CheckTracker(): Tracker processing error: $@"));
1715             untie(%{$trak});
1716             return undef;
1717             }
1718              
1719             # Uncomment to get a dump of the tracker to the log
1720             #foreach (sort keys %{$trak}) {
1721             # $self->Log($r, ('debug', "$tmod: $_ = " . $trak->{$_}));
1722             #}
1723            
1724             untie(%{$trak});
1725             } else {
1726             $self->Log($r, ('error', "CheckTracker(): Unknown tracker type $tmod"));
1727             $ret = 0;
1728             }
1729              
1730             return $ret;
1731             }
1732              
1733             # TODO - GET ALL TRACKER CHECKS AND MANAGEMENT REFACTORED TO OUTSIDE MODULES
1734             # Check given tracker hash ($_[0]) for IP ($_[1]) hitting more than max ($_[2])
1735             # times with no less than in interval ($_[3]) seconds between.
1736             # Updates tracker item.
1737             sub CheckTrackerIPFailures {
1738             my ($trak, $setting, $ip) = @_;
1739             my ($max, $interval, $tc, $tts);
1740             my $time = time();
1741              
1742             ($max,$interval) = split(':', $setting);
1743             unless (($max) && ($max =~ /^\d+$/) && ($interval) && ($interval =~ /^\d+$/)) {
1744             die("CheckTrackerIPFailures(): FATAL: Bad arguments to IPFailures: \"$setting\"\n");
1745             }
1746              
1747             ($ip = CheckHostIP($ip)) || (die("CheckTrackerIPFailures(): FATAL: Bad IP address\n"));
1748              
1749             # Force defaults of 10 failures in 1 minute or less intervals.
1750             ($max) || ($max = 10);
1751             ($interval) || ($interval = 60);
1752              
1753             # If defined and not timed out: add. Else starts fresh
1754             if ($trak->{$ip}) {
1755             ($tts, $tc) = split(':', $trak->{$ip}, 2);
1756            
1757             # Sanity check, and pull actual numbers
1758             (($tts =~ s/^ts(\d+)$/$1/) && ($tc =~ s/^cnt(\d+)$/$1/)) || (die("CheckTrackerIPFailures(): FATAL: Corrupt entry for $ip detected\n"));
1759              
1760             # Not yet timed out
1761             if (($tts + $interval) > $time) {
1762             $tc++;
1763             $tts = $time;
1764              
1765             $trak->{$ip} = join(':', "ts$tts", "cnt$tc");
1766              
1767             if ($tc >= $max) {
1768             die("CheckTrackerIPFailures(): RULE VIOLATION: ip=$ip, count=$tc\n");
1769             }
1770              
1771             return 1;
1772             }
1773             }
1774              
1775             # Expired or New entry: Set timestamp to now and count to 1
1776             $trak->{$ip} = join(':', "ts$time", "cnt1");
1777            
1778             return 1;
1779             }
1780              
1781             # Check given tracker hash ($_[0]), make sure we have not seen the same
1782             # set of credentials ($_[1] - $_[n-1]) before. Stores a hash of credential
1783             # string to minimize security risk.
1784             sub CheckTrackerAuthUnique {
1785             my $trak = shift;
1786             my $ch = HashAny(@_);
1787             my $time = time();
1788              
1789             # If defined, the jig is up!
1790             if ($trak->{$ch}) {
1791             die("CheckTrackerAuthUnique(): RULE VIOLATION: credkey=$ch\n");
1792             } else {
1793             # Set value to
1794             $trak->{$ch} = 'ts' . $time . ":cnt1";
1795             }
1796            
1797             return 1;
1798             }
1799              
1800             # Check given tracker hash ($_[0]), make sure we have not seen the same
1801             # session authentication key (cookie) before. Stores a hash of session key
1802             # string to minimize security risk.
1803             sub CheckTrackerSessionUnique {
1804             my $trak = shift;
1805             my $ch = HashAny(@_);
1806             my $time = time();
1807              
1808             # If defined, the jig is up!
1809             if ($trak->{$ch}) {
1810             die("CheckTrackerSessionUnique(): RULE VIOLATION: sesskey=$ch\n");
1811             } else {
1812             # Set value to
1813             $trak->{$ch} = 'ts' . $time . ":cnt1";
1814             }
1815            
1816             return 1;
1817             }
1818              
1819             # Check the last access time stamp, and update if needed, for a given session.
1820             # Does NOT update the time if a fixed timeout has been set.
1821             # Returns undef if the atime is more than the session's timeout age
1822             # or if etime is set and is over the session's expire age.
1823             sub CheckTime {
1824             my ($self, $sess) = @_;
1825             my $time = time();
1826             my $tdiff;
1827             my $ret = undef;
1828              
1829             # All sessions require at least a floating or fixed timeout!
1830             ($sess->{atime} || $sess->{etime}) or return undef;
1831              
1832             # Check the hard timeout first, if it exists.
1833             # This short circuits further checking since the hard timeout is king!
1834             if ($sess->{etime}) {
1835             if ($time >= $sess->{etime}) {
1836             return undef;
1837             } else {
1838             $ret = $sess->{etime};
1839             }
1840             }
1841              
1842             if ($sess->{atime}) {
1843             $tdiff = $time - $sess->{atime};
1844             if ($tdiff < $sess->{Timeout}) {
1845             # We are still valid. Update the time if we are over 60 seconds
1846             # stale.
1847             if ($tdiff >= 60) {
1848             $sess->{atime} = $time;
1849             }
1850             $ret = $sess->{atime};
1851             } else {
1852             return undef;
1853             }
1854             }
1855              
1856             return $ret;
1857             }
1858              
1859              
1860             # The Alterlist functions manipulate and apply a list of transforms to apply to
1861             # the headers and cookies of the client request before sending the request
1862             # through to the backend server. $self->{alterlist} is a hash containing
1863             # one or more of the following container arrays:
1864             #
1865             # header
1866             # ------
1867             # @{$self->{alterlist}->{header}} - One or more header transforms, with the
1868             # syntax:
1869             # ACTION:NAME:VALUE
1870             # ACTION - add, replace, or delete
1871             # NAME - Header name (or regex match for delete)
1872             # VALUE - New value of header for add or replace, else optional regex filter
1873             # for delete (Prefix pattern with ! for negation)
1874             #
1875             # cookie
1876             # ------
1877             # @{$self->{alterlist}->{cookie}} - One or more cookie transforms, with the
1878             # syntax:
1879             # ACTION:NAME:VALUE
1880             # ACTION - add, replace, delete, or passback
1881             # NAME - Cookie name (or regex match for delete)
1882             # VALUE - New value of cookie, or regex filter for delete action (Prefix
1883             # pattern with ! for negation)
1884             #
1885             # Note - delete rules with optional value match pattern will delete only values
1886             # of a multi-value cookie that match the value pattern
1887             #
1888             # The special "passback" action passes cookies back to the web browser on
1889             # login, This allows us to gather cookies from backend servers on login, but
1890             # have the web browser maintain them.
1891             #
1892             # More containers can be added without modifying the generic functions.
1893              
1894             # Load Alterlist rules from session and return a ref to the loaded alterlist
1895             sub AlterlistLoad {
1896             my ($self, $sess) = @_;
1897             my ($sk,$rk);
1898             my $alterlist = {};
1899              
1900             # All alterlist save value start with al-
1901             foreach $sk (keys %{$sess}) {
1902             ($sk =~ /^al\-([\w]+)$/) || (next);
1903             $rk = $1;
1904             @{$alterlist->{$rk}} = split("\n", $sess->{$sk});
1905             }
1906              
1907             return $alterlist;
1908             }
1909              
1910             # Update current alterlist with given alterlist hash ref
1911             sub AlterlistMod {
1912             my ($self, $alterlist, $alm) = @_;
1913             my $rk;
1914              
1915             (defined($alterlist)) || ($alterlist = {});
1916              
1917             # Update alterlist from $alm hash ref
1918             foreach $rk (keys %{$alm}) {
1919             foreach (@{$alm->{$rk}}) {
1920             push(@{$alterlist->{$rk}}, $_);
1921             }
1922             }
1923              
1924             # Modifications made directly, but return the ref in case
1925             return $alterlist;
1926             }
1927              
1928             # Save existing alterlist to given session
1929             sub AlterlistSave {
1930             my ($self, $alterlist, $sess) = @_;
1931             my ($sk,$rk);
1932              
1933             # Save alterlist to session in \n deliminated form.
1934             if (defined($alterlist) && scalar(keys %{$alterlist})) {
1935             foreach $rk (keys %{$alterlist}) {
1936             $sk = "al-" . $rk;
1937             $sess->{$sk} = join("\n", @{$alterlist->{$rk}});
1938             }
1939             }
1940              
1941             return $alterlist;
1942             }
1943              
1944             # Apply current alterlist rules to request (just calls sub methods in order)
1945             sub AlterlistApply {
1946             my ($self, $alterlist, $r) = @_;
1947             my $status = 1;
1948             (defined($alterlist)) || (return 0);
1949              
1950             if (defined($alterlist->{header})) {
1951             # Run through headers (saving off alter count)
1952             $self->AlterlistApplyHeader($alterlist, $r);
1953             $self->Log($r, ('debug', "AlterlistApply(): Applied alterlist for header"));
1954             }
1955              
1956             if (defined($alterlist->{cookie})) {
1957             # Run through cookies (saving off alter count)
1958             $self->AlterlistApplyCookie($alterlist, $r);
1959             $self->Log($r, ('debug', "AlterlistApply(): Applied alterlist for cookie"));
1960             }
1961              
1962             return $alterlist;
1963             }
1964              
1965             # Apply alterlist rules to request headers.
1966             sub AlterlistApplyHeader {
1967             my ($self, $alterlist, $r) = @_;
1968             (defined($alterlist->{header})) || (return 0);
1969             my ($t, $h, $hl, $act, $key, $val, $tk, $tv);
1970              
1971             # Extract current header hash and build \n deliminated lookup string
1972             # to fast match against
1973             $h = $r->headers_in;
1974             $hl = "\n" . join("\n", keys(%{$h})) . "\n";
1975            
1976             # Cycle through all header transforms
1977             foreach $t (@{$alterlist->{header}}) {
1978             ($t =~ /^(add|replace|rep|delete|del):([\w\d\-]+):(.*?)$/i) || (($self->Log($r, ('debug', "AlterlistApplyHeader(): Skipping illegal header transform \"$t\""))) && (next));
1979             $act = $1;
1980             $key = $2;
1981             $val = $3;
1982            
1983             if ($act =~ /^add$/) {
1984             # Blindly clear then add the header
1985             $r->headers_in->unset($key);
1986             $r->headers_in->add($key => $val);
1987              
1988             # Log obscured value
1989             $self->Log($r, ('debug', "HEADER ADD: $key: " . XHalf($val)));
1990             } else {
1991             # Replace and delete allow for regex header name matches
1992             while ($hl =~ /($key)/igm) {
1993             # Update
1994             $tk = $1;
1995             # Make sure header was not deleted
1996             ($r->headers_in->{$tk}) || (next);
1997             if ($act =~ /^replace|rep$/) {
1998             # Blindly delete then add the header
1999             # Save old value for log
2000             $tv = $r->headers_in->{$tk};
2001             $r->headers_in->unset($tk);
2002             $r->headers_in->add($tk => $val);
2003              
2004             # Log obscured values
2005             $self->Log($r, 'debug', ("AlterlistApplyHeader(): HEADER REPLACE: $tk: " . XHalf($tv) . " -> " . XHalf($val)));
2006              
2007             } elsif ($act =~ /^delete|del$/) {
2008             # Check for extra content match
2009             if ($val) {
2010             $tv = $r->headers_in->{$tk};
2011             # Handle negation
2012             if ($val =~ s/^\!//) {
2013             ($tv =~ /($val)/is) && (next);
2014             } else {
2015             ($tv =~ /($val)/is) || (next);
2016             }
2017             }
2018              
2019             # Kill!
2020             $r->headers_in->unset($tk);
2021              
2022             # Log obscured value
2023             $self->Log($r, ('debug', "AlterlistApplyHeader(): HEADER DELETE: $tk: " . XHalf($tv)));
2024             }
2025             }
2026             }
2027             }
2028            
2029             return $alterlist;
2030             }
2031              
2032              
2033             # Apply alterlist rules to request cookies.
2034             # Note - Does not handle "passback" cookie. Use AlterlistPassBackCookie() to
2035             # retrieve and clear passback cookies)
2036             sub AlterlistApplyCookie {
2037             my ($self, $alterlist, $r) = @_;
2038              
2039             (defined($alterlist->{cookie})) || (return 0);
2040             my ($t, %c, $cl, $act, $key, $val, $tk, $tv, @ta, @td);
2041             my $alterred = 0;
2042              
2043             # Grab any cookies any put into a hash of CGI::Cookies, or just make an
2044             # empty cookie hash for now.
2045             %c = CGI::Cookie->fetch($r);
2046             (%c) || (%c = ());
2047             # Build \n deliminated lookup string to fast match against
2048             $cl = "\n" . join("\n", keys(%c)) . "\n";
2049            
2050             foreach $t (@{$alterlist->{cookie}}) {
2051             # Note - : or = allowed between NAME and VALUE to make life easier
2052             ($t =~ /^(add|replace|rep|delete|del|passback|set):([\w\d\-]+)(?:\:|\=)(.*?)$/i) || (($self->Log($r, ('debug', "AlterlistApplyCookie(): Skipping illegal cookie transform \"$t\""))) && (next));
2053             $act = $1;
2054             $key = $2;
2055             $val = $3;
2056            
2057             if ($act =~ /^passback|set$/) {
2058             # passback not handled in this method
2059             next;
2060             } elsif ($act =~ /^add$/) {
2061             # Blindly add the cookie
2062             @ta = split('&', $val);
2063             # Add a new CGI::Cookie to the hash
2064             $c{$key} = new CGI::Cookie(-name => $key, -value => \@ta);
2065              
2066             # Log obscured value
2067             $self->Log($r, ('debug', "AlterlistApplyCookie(): COOKIE ADD: $key=" . XHalf($val)));
2068             $alterred++;
2069             } else {
2070             # Replace and delete allow for regex cookie name matches
2071             while ($cl =~ /($key)/igm) {
2072             # Update
2073             $tk = $1;
2074             if ($act =~ /^replace|rep$/) {
2075             # Blindly delete then add the cookie back with new value
2076             # Save old value for log
2077             $tv = join('&', $c{$tk}->value);;
2078             delete($c{$tk});
2079             @ta = split('&', $val);
2080             $c{$tk} = new CGI::Cookie(-name => $tk, -value => \@ta);
2081            
2082             # Log obscured values
2083             $self->Log($r, ('debug', "AlterlistApplyCookie(): COOKIE REPLACE: $tk: " . XHalf($tv) . " -> " . XHalf($val)));
2084             $alterred++;
2085             } elsif ($act =~ /^delete|del$/) {
2086             # Check for extra content match
2087             if ($val) {
2088             @ta = ();
2089             @td = ();
2090             # Cycle through multi-values
2091             foreach $tv ($c{$tk}->value) {
2092             # Handle negation
2093             if ($val =~ s/^\!//) {
2094             # Save value and continue
2095             if ($tv =~ /($val)/is) {
2096             push(@ta, $tv);
2097             next;
2098             }
2099             } else {
2100             # Save value and continue
2101             unless ($tv =~ /($val)/is) {
2102             push(@ta, $tv);
2103             next;
2104             }
2105             }
2106             # Fell through, so this value is history/unsaved
2107             push(@td, $tv);
2108             $alterred++;
2109             }
2110             # Kill!
2111             if (scalar @ta) {
2112             # Some values left not deleted, so set those back
2113             $c{$tk}->value(\@ta);
2114             $tv = join('&', @td);
2115              
2116             # Log obscured value
2117             $self->Log($r, ('debug', "AlterlistApplyCookie(): COOKIE DELETE PARTIAL: $tk=" . XHalf($tv)));
2118             } else {
2119             # Nothing left inside. KILL!
2120             delete($c{$tk});
2121             $tv = join('&', @td);
2122              
2123             # Obscure values for logging
2124             $tv =~ s/([^X])[\w\d]/${1}X/gs;
2125             $self->Log($r, ('debug', "AlterlistApplyCookie(): COOKIE DELETE FULL: $tk=$tv"));
2126             }
2127             } else {
2128             # Kill Em All
2129             $tv = $c{$key}->value;
2130             delete($c{$key});
2131            
2132             # Obscure values for logging
2133             $tv =~ s/([^X])[\w\d]/${1}X/gs;
2134             $self->Log($r, ('debug', "AlterlistApplyCookie(): COOKIE DELETE FULL: $key=$tv"));
2135              
2136             $alterred++;
2137             }
2138             }
2139             }
2140             }
2141             }
2142            
2143             # Unset, then add cookies to header if changes were made
2144             if ($alterred) {
2145             $r->headers_in->unset('Cookie');
2146             $t = '';
2147             foreach $tk (keys %c) {
2148             # Cookie to list in string form.
2149             $t .= $c{$tk}->name . "=" . join('&', $c{$tk}->value) . "; ";
2150             }
2151             # Kill trailing '; '
2152             $t =~ s/\; $//s;
2153             # Ship it
2154             $r->headers_in->add('Cookie' => $t);
2155             }
2156              
2157             return $alterlist;
2158             }
2159              
2160             # Add a Set-cookie: header to r for all alterlist "passback" cookies and return
2161             # a modified alterlist with the passback cookie values cleared and expired.
2162             #
2163             # Unlike normal alterlist rules, passback cookies are sent BACK to the client.
2164             # The only time this can occur is upon login/redirect. The purpose of passback
2165             # cookies is to set the same cookies in the browser as they would have set
2166             # if they were connecting directly to the backend server(s).
2167             #
2168             # The return should be used to update the alterlist. When
2169             # AlterlistPassBackCookie is applied again, it will UNSET the passback cookies.
2170             # This should be done on logout.
2171             sub AlterlistPassBackCookie() {
2172             my ($self, $alterlist, $r) = @_;
2173              
2174             (defined($alterlist->{cookie})) || (return 0);
2175             my ($t, $key, $val, $opt, $tdomain, $tpath, $texpire);
2176             my @ct = ();
2177             my %c = ();
2178              
2179             foreach $t (@{$alterlist->{cookie}}) {
2180             # Note - : or = allowed between NAME and VALUE to make life easier
2181             ($t =~ /^(passback|set):([\w\d\-]+)(?:\:|\=)([^;]*)(;.*)?$/i) || ((push(@ct, $t)) && (next));
2182             $key = $2;
2183             $val = $3;
2184             $opt = $4;
2185             $tdomain = $tpath = $texpire = '';
2186              
2187             # Unlike AlterlistApplyCookie which just needs to parse name and
2188             # value, the PassBack cookies are Set-Cookie items which may
2189             # have options. Also, only process the last cookie value if
2190             # a multi-value cookie is passed
2191              
2192             # Add a new CGI::Cookie to the hash
2193             $c{$key} = new CGI::Cookie(-name => $key,
2194             -value => $val,
2195             );
2196             # Set further options (only Expires and Path currently passed through)
2197             foreach $t (split(';', $opt)) {
2198             if ($t =~ /^\s*expires=([\w\d \:\;\-,]+)\s*$/) {
2199             $c{$key}->expires($1);
2200             } elsif ($t =~ /^\s*path=(\/.*?)\s*$/) {
2201             $c{$key}->path($1);
2202             }
2203             }
2204              
2205             # Set other options to match session cookie values (could be made a
2206             # configurable, and allow for maintaining the original options from the
2207             # cookie. I don't see a need.)
2208             my $auth_name = $r->auth_name;
2209            
2210             if ($r->dir_config("${auth_name}Domain")) {
2211             $c{$key}->domain($r->dir_config("${auth_name}Domain"));
2212             }
2213             if (!$r->dir_config("${auth_name}Secure") || ($r->dir_config("${auth_name}Secure") == 1)) {
2214             $c{$key}->secure(1);
2215             }
2216            
2217             $r->err_headers_out->add('Set-Cookie' => $c{$key});
2218              
2219             # Clean up and log
2220             $t = $c{$key};
2221             $t =~ /($key\s*\=\s*)(.*?)(;|$)/;
2222             $self->Log($r, ('debug', "AlterlistPassBackCookie(): COOKIE PASSBACK: " . $1 . XHalf($2) . $3));
2223              
2224             # Save an empty/expired cookie so next call to AlterlistPassBackCookie
2225             # with this alterlist will unset the cookie
2226             $c{$key}->value('');
2227             $c{$key}->expires('Thu, 1-Jan-1970 00:00:00 GMT');
2228             push(@ct, "passback:" . $c{$key});
2229             }
2230              
2231             # Save updated cookie array
2232             @{$alterlist->{cookie}} = @ct;
2233              
2234             return $alterlist;
2235             }
2236              
2237              
2238             # Append an error code to the list of query args in a given URL. (Used to
2239             # pass friendly error messages to users in external redirects. (Note that
2240             # AuthCookie used subprocess_env() to pass that info, but since that will only
2241             # work in the same main request, it won't pass into an external redirect.)
2242             sub URLErrorCode {
2243             my $self = shift;
2244             my $uri = (shift) || (return undef);
2245             my $ecode = (shift) || ('');
2246            
2247             ($uri = new URI($uri)) || (return undef);
2248            
2249             # Error codes must contain only letters, numbers, and/or _ chars.
2250             # Your login.pl script should read them in CAREFULLY and make sure
2251             # they follow this format.
2252             ($ecode =~ /^([\w\d_]+)$/) || (return undef);
2253            
2254             # Add the error code and return the URI in string form
2255             $uri->query_form($uri->query_form, 'ASERRCODE' => $ecode);
2256             return $uri->as_string;
2257             }
2258              
2259             # Log to configured log. Always takes the request as the 1st arg. Can
2260             # take either a loglevel and a message as args 2 and 3, or an array
2261             # of loglevel and message arrays as the 2nd arg.
2262             sub Log {
2263             my $self = shift;
2264             my $r = shift;
2265             my $la = [];
2266             my $debug = $self->_debug($r);
2267              
2268             # Check if being called with a level and message, or with a log array
2269             if (ref($_[0]) eq "ARRAY") {
2270             $la = $_[0];
2271             (defined(@{$la}) && (scalar @{$la})) || (return 0);
2272             } else {
2273             (defined($_[0]) && defined($_[1])) || (return 0);
2274             # Set to a single child array of arrays
2275             $la = [[$_[0], $_[1]]];
2276             }
2277              
2278             # Collect a few tidbits (package name, client IP and URI?args)
2279             my $auth_name = ($r->auth_name || "");
2280             $auth_name .= ': ';
2281             my $info = '
2282             if ($MP eq 1) {
2283             $info .= ($r->get_remote_host || "");
2284             } else {
2285             $info .= ($r->connection->get_remote_host || "");
2286             }
2287             $info .= ', uri="';
2288             $info .= ($r->uri() || "");
2289             (defined($r->args())) && ($info .= '?' . $r->args());
2290             $info .= '">';
2291              
2292             # Get the log handle for the server
2293             my $log = $r->server->log;
2294             my $defaultlevel = 'error';
2295             my ($li, $level, $line);
2296              
2297             # Cycle through the log entries
2298             foreach $li (@{$la}) {
2299             if (scalar(@{$li}) == 2) { # 2 argument form with level and line
2300             $level = $li->[0];
2301             ($level) || ($level = $defaultlevel);
2302             ($level =~ s/^(emerg|alert|crit|error|warn|notice|info|debug)$/$1/i) || (return 0);
2303             $level = lc($level);
2304             # Skip debug unless debug is enabled
2305             next if (!$debug && ($level eq 'debug'));
2306             $line = $auth_name . $li->[1] . $info;
2307             } elsif (scalar(@{$li}) == 1) { # 1 argument form: must add a level
2308             $level = $defaultlevel;
2309             $line = $auth_name . $li->[0] . $info;
2310             } else { # Who knows form: must hit someone's fingers with a hammer
2311             return 0;
2312             }
2313              
2314             # Check log line, then ship it.
2315             $line = $self->FilterLogLine($line);
2316             $log->$level($line);
2317             }
2318              
2319             return 1;
2320             }
2321              
2322             # Check debug setting
2323             sub _debug {
2324             my $self = shift;
2325             my $r = shift;
2326             my $debug = 0;
2327             if ($r->auth_name) {
2328             my $auth_name = $r->auth_name;
2329             if ($r->dir_config("${auth_name}Debug")) {
2330             ($r->dir_config("${auth_name}Debug") =~ /^(\d+)$/) && ($debug = $1);
2331             }
2332             }
2333            
2334             return $debug;
2335             }
2336              
2337             # Filter the output line before logging. Restricts to no more than CharMax
2338             # characters and converts everything matching BlankChars to a space to
2339             # try and protect logging systems and log monitors from attack.
2340             sub FilterLogLine {
2341             my $self = shift;
2342             my $line = (shift || return undef);
2343             my $LogCharMax = 1024;
2344              
2345             # Strip surrounding whitespace
2346             $line =~ s/^\s*(.+?)\s*$/$1/s;
2347             # Convert newlines to ', '
2348             $line =~ s/\r?\n/, /sg;
2349             # Check length and truncate if needed
2350             $line = substr($line, 0, $LogCharMax);
2351             # Convert BlankChars matches to blanks
2352             $line =~ s/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f\'\\]+/ /g;
2353              
2354             return $line;
2355             }
2356              
2357             1; # End of Apache::AppSamurai
2358              
2359             __END__