home *** CD-ROM | disk | FTP | other *** search
-
- package Apache::ASP;
-
- # quickly decomped out of Apache::ASP so we could load the routines only
- # when we are managing State objects
-
- use Apache::ASP::State;
-
- use strict;
- use vars qw(
- $CleanupGroups
- $SessionIDLength $SessionTimeout $StateManager
- $DefaultStateDB $DefaultStateSerializer
- );
-
- $SessionTimeout = 20;
- $StateManager = 10;
-
- # Some OS's have hashed directory lookups up to 16 bytes, so we leave room
- # for .lock extension ... nevermind, security is more important, back to 32
- # $SessionIDLength = 11;
- $SessionIDLength = 32;
- $DefaultStateDB = 'SDBM_File';
- $DefaultStateSerializer = 'Data::Dumper';
-
- sub InitState {
- my $self = shift;
- my $r = $self->{r};
- my $global_asa = $self->{GlobalASA};
-
- ## STATE INITS
- # what percent of the session_timeout's time do we garbage collect
- # state files and run programs like Session_OnEnd and Application_OnEnd
- $self->{state_manager} = &config($self, 'StateManager', undef, $Apache::ASP::StateManager);
-
- # state is the path where state files are stored, like $Session, $Application, etc.
- $self->{state_dir} = &config($self, 'StateDir', undef, $self->{global}.'/.state');
- $self->{state_dir} =~ tr///; # untaint
- $self->{session_state} = &config($self, 'AllowSessionState', undef, 1);
- $self->{state_serialize} = &config($self, 'ApplicationSerialize');
-
- if($self->{state_db} = &config($self, 'StateDB')) {
- # StateDB - Check StateDB module support
- $Apache::ASP::State::DB{$self->{state_db}} ||
- $self->Error("$self->{state_db} is not supported for StateDB, try: " .
- join(", ", keys %Apache::ASP::State::DB));
- $self->{state_db} =~ /^(.*)$/; # untaint
- $self->{state_db} = $1; # untaint
- # load the state database module && serializer
- $self->LoadModule('StateDB', $self->{state_db});
- }
- if($self->{state_serializer} = &config($self, 'StateSerializer')) {
- $self->{state_serializer} =~ tr///; # untaint
- $self->LoadModule('StateSerializer', $self->{state_serializer});
- }
-
- # INTERNAL tie to the application internal info
- my %Internal;
- tie(%Internal, 'Apache::ASP::State', $self, 'internal', 'server')
- || $self->Error("can't tie to internal state");
- my $internal = $self->{Internal} = bless \%Internal, 'Apache::ASP::State';
- $self->{state_serialize} && $internal->LOCK;
-
- # APPLICATION create application object
- $self->{app_state} = &config($self, 'AllowApplicationState', undef, 1);
- if($self->{app_state}) {
- # load at runtime for CGI environments, preloaded for mod_perl
- require Apache::ASP::Application;
-
- ($self->{Application} = &Apache::ASP::Application::new($self))
- || $self->Error("can't get application state");
- $self->{state_serialize} && $self->{Application}->Lock;
-
- } else {
- $self->{dbg} && $self->Debug("no application allowed config");
- }
-
- # SESSION if we are tracking state, set up the appropriate objects
- my $session;
- if($self->{session_state}) {
- ## SESSION INITS
- $self->{cookie_path} = &config($self, 'CookiePath', undef, '/');
- $self->{cookie_domain} = &config($self, 'CookieDomain');
- $self->{paranoid_session} = &config($self, 'ParanoidSession');
- $self->{remote_ip} = $r->connection()->remote_ip();
- $self->{session_count} = &config($self, 'SessionCount');
-
- # cookieless session support, cascading values
- $self->{session_url_parse_match} = &config($self, 'SessionQueryParseMatch');
- $self->{session_url_parse} = $self->{session_url_parse_match} || &config($self, 'SessionQueryParse');
- $self->{session_url_match} = $self->{session_url_parse_match} || &config($self, 'SessionQueryMatch');
- $self->{session_url} = $self->{session_url_parse} || $self->{session_url_match} || &config($self, 'SessionQuery');
- $self->{session_url_force} = &config($self, 'SessionQueryForce');
-
- $self->{session_serialize} = &config($self, 'SessionSerialize');
- $self->{secure_session} = &config($self, 'SecureSession');
- # session timeout in seconds since that is what we work with internally
- $self->{session_timeout} = &config($self, 'SessionTimeout', undef, $SessionTimeout) * 60;
- $self->{'ua'} = $self->{headers_in}->get('User-Agent') || 'UNKNOWN UA';
- # refresh group by some increment smaller than session timeout
- # to withstand DoS, bruteforce guessing attacks
- # defaults to checking the group once every 2 minutes
- $self->{group_refresh} = int($self->{session_timeout} / $self->{state_manager});
-
- # Session state is dependent on internal state
-
- # load at runtime for CGI environments, preloaded for mod_perl
- require Apache::ASP::Session;
-
- $session = $self->{Session} = &Apache::ASP::Session::new($self)
- || $self->Die("can't create session");
- $self->{state_serialize} && $session->Lock();
-
- } else {
- $self->{dbg} && $self->Debug("no sessions allowed config");
- }
-
- # update after long state init, possible with SessionSerialize config
- $self->{Response}->IsClientConnected();
-
- # POSTPOSE STATE EVENTS, so we can delay the Response object creation
- # until after the state objects are created
- if($session) {
- my $last_session_timeout;
- if($session->Started()) {
- # we only want one process purging at a time
- if($self->{app_state}) {
- $internal->LOCK();
- if(($last_session_timeout = $internal->{LastSessionTimeout} || 0) < time()) {
- $internal->{'LastSessionTimeout'} = $self->{session_timeout} + time;
- $internal->UNLOCK();
- $self->{Application}->Lock;
- my $obj = tied(%{$self->{Application}});
- if($self->CleanupGroups('PURGE')) {
- $last_session_timeout && $global_asa->ApplicationOnEnd();
- $global_asa->ApplicationOnStart();
- }
- $self->{Application}->UnLock;
- }
- $internal->UNLOCK();
- }
- $global_asa->SessionOnStart();
- }
-
- if($self->{app_state}) {
- # The last session timeout should only be updated every group_refresh period
- # another optimization, rand() so not all at once either
- $internal->LOCK();
- $last_session_timeout ||= $internal->{'LastSessionTimeout'};
- if($last_session_timeout < $self->{session_timeout} + time +
- (rand() * $self->{group_refresh} / 2))
- {
- $self->{dbg} && $self->Debug("updating LastSessionTimeout from $last_session_timeout");
- $internal->{'LastSessionTimeout'} =
- $self->{session_timeout} + time() + $self->{group_refresh};
- }
- $internal->UNLOCK();
- }
- }
-
- $self;
- }
-
- # Cleanup a state group, by default the group of the current session
- # We do this currently in DESTROY, which happens after the current
- # script has been executed, so that cleanup doesn't happen until
- # after output to user
- #
- # We always exit unless there is a $Session defined, since we only
- # cleanup groups of sessions if sessions are allowed for this script
- sub CleanupGroup {
- my($self, $group_id, $force) = @_;
- return unless $self->{Session};
-
- my $asp = $self; # bad hack for some moved around code
- $force ||= 0;
-
- # GET GROUP_ID
- my $state;
- unless($group_id) {
- $state = $self->{Session}{_STATE};
- $group_id = $state->GroupId();
- }
-
- # we must have a group id to work with
- $asp->Error("no group id") unless $group_id;
- my $group_key = "GroupId" . $group_id;
-
- # cleanup timed out sessions, from current group
- my $internal = $asp->{Internal};
- $internal->LOCK();
- my $group_check = $internal->{$group_key} || 0;
- unless($force || ($group_check < time())) {
- $internal->UNLOCK();
- return;
- }
-
- # set the next group_check, randomize a bit to unclump the group checks,
- # for 20 minute session timeout, had rand() / 2 + .5, but it was still
- # too clumpy, going with pure rand() now, even if a bit less efficient
-
- my $next_check = int($asp->{group_refresh} * rand()) + 1;
- $internal->{$group_key} = time() + $next_check;
- $internal->UNLOCK();
-
- ## GET STATE for group
- $state ||= &Apache::ASP::State::new($asp, $group_id);
- my $ids = $state->GroupMembers() || [];
-
- # don't return so we can't delete the empty group later
- # return unless scalar(@$ids);
-
- $asp->{dbg} && $asp->Debug("group check $group_id, next in $next_check sec");
- my $id = $self->{Session}->SessionID();
- my $deleted = 0;
- $internal->LOCK();
- $asp->{dbg} && $asp->Debug("checking group ids", $ids);
- for my $id (@$ids) {
- eval {
-
- # if($id eq $_) {
- # $asp->{dbg} && $asp->Debug("skipping delete self", {id => $id});
- # next;
- # }
-
- # we lock the internal, so a session isn't being initialized
- # while we are garbage collecting it... we release it every
- # time so we don't starve session creation if this is a large
- # directory that we are garbage collecting
- my $idata = $internal->{$id};
-
- # do this check in case this data is corrupt, and not deserialized, correctly
- unless(ref($idata) && (ref($idata) eq 'HASH')) {
- $idata = {};
- }
-
- my $timeout = $idata->{timeout} || 0;
-
- unless($timeout) {
- # we don't have the timeout always, since this session
- # may just have been created, just in case this is
- # a corrupted session (does this happen still ??), we give it
- # a timeout now, so we will be sure to clean it up
- # eventualy
- $idata->{timeout} = time() + $asp->{session_timeout};
- $internal->{$id} = $idata;
- $asp->Debug("resetting timeout for $id to $idata->{timeout}");
- return; # no next in eval {}
- }
- # only delete sessions that have timed out
- unless($timeout < time()) {
- $asp->{dbg} && $asp->Debug("$id not timed out with $timeout");
- return; # no next in eval {}
- }
-
- # UPDATE & UNLOCK, as soon as we update internal, we may free it
- # definately don't lock around SessionOnEnd, as it might take
- # a while to process
-
- # set the timeout for this session forward so it won't
- # get garbage collected by another process
- $asp->{dbg} && $asp->Debug("resetting timeout for deletion lock on $id");
- $internal->{$id} = {
- %{$internal->{$id}},
- 'timeout' => time() + $asp->{session_timeout},
- 'end' => 1,
- };
-
-
- # unlock many times in case we are locked above this loop
- for (1..3) { $internal->UNLOCK() }
- $asp->{GlobalASA}->SessionOnEnd($id);
- $internal->LOCK;
-
- # set up state
- my($member_state) = Apache::ASP::State::new($asp, $id);
- if(my $count = $member_state->Delete()) {
- $asp->{dbg} &&
- $asp->Debug("deleting session", {
- session_id => $id,
- files_deleted => $count,
- });
- $deleted++;
- delete $internal->{$id};
- } else {
- $asp->Error("can't delete session id: $id");
- return; # no next in eval {}
- }
- };
- if($@) {
- $asp->Error("error for cleanup of session id $id: $@");
- }
- }
- $internal->UNLOCK();
-
- #### LEAVE DIRECTORIES, NASTY RACE CONDITION POTENTIAL
- ## NOW PRUNE ONLY DIRECTORIES THAT WE DON'T NEED TO KEEP
- ## FOR PERFORMANCE
- # REMOVE DIRECTORY, LOCK
- # if the directory is still empty, remove it, lock it
- # down so no new sessions will be created in it while we
- # are testing
- if($deleted == @$ids) {
- if ($state->GroupId !~ /^[0]/) {
- $asp->{Internal}->LOCK();
- my $ids = $state->GroupMembers();
- if(@{$ids} == 0) {
- $self->Log("purging stale group ".$state->GroupId.", which should only happen ".
- "after Apache::ASP upgrade to beyond 2.09");
- $state->DeleteGroupId();
- }
- $asp->{Internal}->UNLOCK();
- }
- }
-
- $deleted;
- }
-
- sub CleanupGroups {
- my($self, $force) = @_;
- return unless $self->{Session};
-
- my $cleanup = 0;
- my $state_dir = $self->{state_dir};
- my $internal = $self->{Internal};
- $force ||= 0;
-
- $self->Debug("forcing groups cleanup") if ($self->{dbg} && $force);
-
- # each apache process has an internal time in which it
- # did its last check, once we have passed that, we check
- # $Internal for the last time the check was done. We
- # break it up in this way so that locking on $Internal
- # does not become another bottleneck for scripts
- if($force || ($Apache::ASP::CleanupGroups{$state_dir} || 0) < time()) {
- # /8 to keep it less bursty... since we check groups every group_refresh/2
- # we'll average 1/4 of the groups everytime we check them on a busy server
- $Apache::ASP::CleanupGroups{$state_dir} = time() + $self->{group_refresh}/8;
- $self->{dbg} && $self->Debug("testing internal time for cleanup groups");
- if($self->CleanupMaster) {
- $internal->LOCK();
- if($force || ($internal->{CleanupGroups} < (time - $self->{group_refresh}/8))) {
- $internal->{CleanupGroups} = time;
- $cleanup = 1;
- }
- $internal->UNLOCK;
- }
- }
- return unless $cleanup;
-
- # clean cache, so caching won't affect CleanupGroups() being called multiple times
- $self->{internal_cached_keys} = undef;
-
- # only one process doing CleanupGroup at a time now, so OK
- # lock around, necessary when keeping empty group directories
- my $groups = $self->{Session}{_SELF}{'state'}->DefaultGroups();
- $self->{dbg} && $self->Debug("groups ", $groups);
- my($sum_active, $sum_deleted);
- $internal->LOCK();
- my $start_cleanup = time;
- for(@{$groups}) {
- $sum_deleted = $self->CleanupGroup($_, $force);
- if ($start_cleanup > time) {
- # every second, take a breather in the lock management
- # so that sessions can be created, and the like, so for
- # long purges, the application will get sticky in 1 second
- # bursts
- $start_cleanup = time;
- $internal->UNLOCK;
- $internal->LOCK;
- last unless $self->CleanupMaster;
- }
- }
- $internal->UNLOCK();
- $self->{dbg} && $self->Debug("cleanup groups", { deleted => $sum_deleted }) if $self->{dbg};
-
- # boolean true at least for master
- $sum_deleted || 1;
- }
-
- sub CleanupMaster {
- my $self = shift;
- my $internal = $self->{Internal};
-
- $internal->LOCK;
- my $master = $internal->{CleanupMaster} ||
- {
- ServerID => '',
- PID => 0,
- Checked => 0,
- };
-
- my $is_master = (($master->{ServerID} eq $ServerID) and ($master->{PID} eq $$)) ? 1 : 0;
- $self->{dbg} && $self->Debug(current_master => $master, is_master => $is_master );
- my $stale_time = $is_master ? $self->{group_refresh} / 4 :
- $self->{group_refresh} / 2 + int($self->{group_refresh} * rand() / 2) + 1;
- $stale_time += $master->{Checked};
-
- if($stale_time < time()) {
- $internal->{CleanupMaster} =
- {
- ServerID => $ServerID,
- PID => $$,
- Checked => time()
- };
- $internal->UNLOCK; # flush write
- $self->{dbg} && $self->Debug("$stale_time time is stale, is_master $is_master", $master);
-
- # we are only worried about multiprocess NFS here ... if running not
- # in mod_perl mode, probably just CGI mounted on local disk
- # Only do this while in DESTROY() mode too, so we avoid Application_OnStart
- # hang behavior.
- if($^O !~ /Win/ && $ENV{MOD_PERL} && $self->{DESTROY}) {
- $self->Debug("sleep for acquire master check in case of shared state");
- sleep(1);
- }
-
- my $master = $internal->{CleanupMaster}; # recheck after flush
- my $is_master = (($master->{ServerID} eq $ServerID) and ($master->{PID} eq $$)) ? 1 : 0;
- $self->{dbg} && $self->Debug("is_master $is_master after update $ServerID - $$");
- $is_master;
- } elsif($is_master) {
- $master->{Checked} = time();
- $internal->{CleanupMaster} = $master;
- $internal->UNLOCK;
- $self->{dbg} && $self->Debug("$stale_time time is fresh, is_master $is_master", $master);
- 1; # is master
- } else {
- $internal->UNLOCK;
- $self->{dbg} && $self->Debug("$stale_time time is fresh, is_master $is_master", $master);
- 0; # not master
- }
- }
-
- # combo get / set
- sub SessionId {
- my($self, $id) = @_;
-
- if(defined $id) {
- unless($self->{session_url_force}) {
- # don't set the cookie when we are just using SessionQuery* configs
- my $secure = $self->{secure_session} ? '; secure' : '';
- my $domain = $self->{cookie_domain} ? '; domain='.$self->{cookie_domain} : '';
- $self->{r}->err_headers_out->add('Set-Cookie', "$SessionCookieName=$id; path=$self->{cookie_path}".$domain.$secure);
- }
- $self->{session_id} = $id;
- } else {
- # if we have already parsed it out, return now
- # quick session_id caching, mostly for use with
- # cookie less url building
- $self->{session_id} && return $self->{session_id};
-
- my $session_cookie = 0;
-
- unless($self->{session_url_force}) {
- # don't read the cookie when we are just using SessionQuery* configs
- my $cookie = $self->{r}->headers_in->{"Cookie"} || '';
- my(@parts) = split(/\;\s*/, $cookie);
- for(@parts) {
- my($name, $value) = split(/\=/, $_, 2);
- if($name eq $SessionCookieName) {
- $id = $value;
- $session_cookie = 1;
- $self->{dbg} && $self->Debug("session id from cookie: $id");
- last;
- }
- }
- }
-
- my $session_from_url;
- if(! defined($id) && $self->{session_url}) {
- $id = delete $self->{Request}{QueryString}{$SessionCookieName};
- # if there was more than one session id in the query string, then just
- # take the first one
- ref($id) =~ /ARRAY/ and ($id) = @$id;
- $id && $self->{dbg} && $self->Debug("session id from query string: $id");
- $session_from_url = 1;
- }
-
- # SANTIZE the id against hacking
- if(defined $id) {
- if($id =~ /^[0-9a-z]{8,32}$/s) {
- # at least 8 bytes, but less than 32 bytes
- $self->{session_id} = $id;
- } else {
- $self->Log("passed in session id $id failed checks sanity checks");
- $id = undef;
- }
- }
-
- if ($session_from_url && defined $id) {
- $self->SessionId($id);
- }
-
- if(defined $id) {
- $self->{session_id} = $id;
- $self->{session_cookie} = $session_cookie;
- }
- }
-
- $id;
- }
-
- sub Secret {
- my $self = shift;
- # have enough data in here that even if srand() is seeded for the purpose
- # of debugging an external program, should have decent behavior.
- my $data = $self . $self->{remote_ip} . rand() . time() .
- $self->{global} . $self->{'r'} . $self->{'filename'}.
- $$ . $ServerID;
- my $secret = substr(md5_hex($data), 0, $SessionIDLength);
- # by having [0-1][0-f] as the first 2 chars, only 32 groups now, which remains
- # efficient for inactive sites, even with empty groups
- $secret =~ s/^(.)/0/;
- $secret;
- }
-
- sub RefreshSessionId {
- my($self, $id, $reset) = @_;
- $id || $self->Error("no id for refreshing");
- my $internal = $self->{Internal};
-
- $internal->LOCK;
- my $idata = $internal->{$id};
- my $refresh_timeout = $reset ?
- $self->{session_timeout} : $idata->{refresh_timeout} || $self->{session_timeout};
- $idata->{'timeout'} = time() + $refresh_timeout;
- $internal->{$id} = $idata;
- $internal->UNLOCK;
- $self->{dbg} && $self->Debug("refreshing $id with timeout $idata->{timeout}");
-
- 1;
- }
-
- 1;
-