/usr/local/CPAN/Apache-ASP/Apache/ASP/Session.pm
package Apache::ASP::Session;
use Apache::ASP::State;
use strict;
no strict qw(refs);
use vars qw(@ISA);
@ISA = qw(Apache::ASP::Collection);
# allow to pass in id so we can cleanup other sessions with
# the session manager
sub new {
my($asp, $id, $perms, $no_error) = @_;
my($state, %self, $started);
my $internal = $asp->{Internal};
# if we are passing in the id, then we are doing a
# quick session lookup and can bypass the normal checks
# this is useful for the session manager and such
if($id) {
$internal->LOCK;
$state = Apache::ASP::State::new($asp, $id, undef, $perms, $no_error);
# $state->Set() || $asp->Error("session state get failed");
if($state) {
tie %self, 'Apache::ASP::Session',
{
state=>$state,
asp=>$asp,
id=>$id,
};
$internal->UNLOCK;
return bless \%self;
} else {
$internal->UNLOCK;
return;
}
}
# lock down so no conflict with garbage collection
$internal->LOCK();
if($id = $asp->SessionId()) {
my $idata = $internal->{$id};
# $asp->Debug("internal data for session $id", $idata);
if($idata && ! $idata->{'end'} ) {
# user is authentic, since the id is in our internal hash
if($idata->{timeout} > time()) {
# refresh and unlock as early as possible to not conflict
# with garbage collection
$asp->RefreshSessionId($id);
$state = Apache::ASP::State::new($asp, $id);
$internal->UNLOCK();
# session not expired
$asp->{dbg} &&
$asp->Debug("session not expired",{'time'=>time(), timeout=>$idata->{timeout}});
if($asp->{paranoid_session}) {
local $^W = 0;
# by testing for whether UA was set to begin with, we
# allow a smooth upgrade to ParanoidSessions
$state->WriteLock() if $asp->{session_serialize};
my $state_ua = $state->FETCH('_UA');
if(defined($state_ua) and $state_ua ne $asp->{'ua'}) {
$asp->Log("[security] hacker guessed id $id; ".
"user-agent ($asp->{'ua'}) does not match ($state_ua); ".
"destroying session & establishing new session id"
);
$state->Init();
undef $state;
goto NEW_SESSION_ID;
}
}
$started = 0;
} else {
# expired, get & reset
$internal->{$id} = { %{$internal->{$id}}, 'end' => 1 };
$internal->UNLOCK();
# remove this section, allow lazy cleanup, this caused a bug
# in which sessions cleared in this way, but didn't have their files cleaned up
# would have their timeout restored later
#
# $asp->Debug("session $id timed out, clearing");
# $asp->{GlobalASA}->SessionOnEnd($id);
# $internal->LOCK();
# delete $internal->{$id};
# $internal->UNLOCK();
# we need to create a new state now after the clobbering
# with SessionOnEnd
goto NEW_SESSION_ID;
}
} else {
# never seen before, maybe session garbage collected already
# or coming in from querystringed search engine
# wish we could do more
# but proxying + nat prevents us from securing via ip address
goto NEW_SESSION_ID;
}
} else {
# give user new session id, we must lock this portion to avoid
# concurrent identical session key creation, this is the
# only critical part of the session manager
NEW_SESSION_ID:
my($trys);
for(1..10) {
$trys++;
$id = $asp->Secret();
if($internal->{$id}) {
$id = '';
} else {
last;
}
}
$id && $asp->RefreshSessionId($id, {});
$asp->{Internal}->UNLOCK();
$asp->Log("[security] secret algorithm is no good with $trys trys")
if ($trys > 3);
$asp->Error("no unique secret generated")
unless $id;
$asp->{dbg} && $asp->Debug("new session id $id");
$asp->SessionId($id);
$state = &Apache::ASP::State::new($asp, $id);
# $state->Set() || $asp->Error("session state set failed");
if($asp->{paranoid_session}) {
$asp->Debug("storing user-agent $asp->{'ua'}");
$state->STORE('_UA', $asp->{'ua'});
}
$started = 1;
}
if(! $state) {
$asp->Error("can't get state for id $id");
return;
}
$state->WriteLock() if $asp->{session_serialize};
$asp->Debug("tieing session $id");
tie %self, 'Apache::ASP::Session',
{
state=>$state,
asp=>$asp,
id=>$id,
started=>$started,
};
if($started) {
$asp->{dbg} && $asp->Debug("clearing starting session");
if($state->Size > 0) {
$asp->{dbg} && $asp->Debug("clearing data in old session $id");
%self = ();
}
}
bless \%self;
}
sub TIEHASH {
my($package, $self) = @_;
bless $self;
}
# stub so we don't have to test for it in autoload
sub DESTROY {
my $self = shift;
# wrapped in eval to suppress odd global destruction error messages
# in perl 5.6.0, --jc 5/28/2001
return unless eval { $self->{state} };
$self->{state}->DESTROY;
undef $self->{state};
%$self = ();
}
# don't need to skip DESTROY since we have it here
# return if ($AUTOLOAD =~ /DESTROY/);
sub AUTOLOAD {
my $self = shift;
my $AUTOLOAD = $Apache::ASP::Session::AUTOLOAD;
$AUTOLOAD =~ s/^(.*)::(.*?)$/$2/o;
$self->{state}->$AUTOLOAD(@_);
}
sub FETCH {
my($self, $index) = @_;
# putting these comparisons in a regexp was a little
# slower than keeping them in these 'eq' statements
if($index eq '_SELF') {
$self;
} elsif($index eq '_STATE') {
$self->{state};
} elsif($index eq 'SessionID') {
$self->{id};
} elsif($index eq 'Timeout') {
$self->Timeout();
} else {
$self->{state}->FETCH($index);
}
}
sub STORE {
my($self, $index, $value) = @_;
if($index eq 'Timeout') {
$self->Timeout($value);
} else {
$self->{state}->STORE($index, $value);
}
}
# firstkey and nextkey skip the _UA key so the user
# we need to keep the ua info in the session db itself,
# so we are not dependent on writes going through to Internal
# for this very critical informatioh. _UA is used for security
# validation / the user's user agent.
sub FIRSTKEY {
my $self = shift;
my $value = $self->{state}->FIRSTKEY();
if(defined $value and $value eq '_UA') {
$self->{state}->NEXTKEY($value);
} else {
$value;
}
}
sub NEXTKEY {
my($self, $key) = @_;
my $value = $self->{state}->NEXTKEY($key);
if(defined($value) && ($value eq '_UA')) {
$self->{state}->NEXTKEY($value);
} else {
$value;
}
}
sub CLEAR {
my $state = shift->{state};
my $ua = $state->FETCH('_UA');
my $rv = $state->CLEAR();
$ua && $state->STORE('_UA', $ua);
$rv;
}
sub SessionID {
my $self = shift;
tied(%$self)->{id};
}
sub Timeout {
my($self, $minutes) = @_;
if(tied(%$self)) {
$self = tied(%$self);
}
if($minutes) {
$self->{asp}{Internal}->LOCK;
my($internal_session) = $self->{asp}{Internal}{$self->{id}};
$internal_session->{refresh_timeout} = $minutes * 60;
$internal_session->{timeout} = time() + $minutes * 60;
$self->{asp}{Internal}{$self->{id}} = $internal_session;
$self->{asp}{Internal}->UNLOCK;
} else {
my($refresh) = $self->{asp}{Internal}{$self->{id}}{refresh_timeout};
$refresh ||= $self->{asp}{session_timeout};
$refresh / 60;
}
}
sub Abandon {
shift->Timeout(-1);
}
sub TTL {
my $self = shift;
$self = tied(%$self);
# time to live is current timeout - time... positive means
# session is still active, returns ttl in seconds
my $timeout = $self->{asp}{Internal}{$self->{id}}{timeout};
my $ttl = $timeout - time();
}
sub Started {
my $self = shift;
tied(%$self)->{started};
}
# we provide these, since session serialize is not
# the default... locking around writes will also be faster,
# since there will be only one tie to the database and
# one flush per lock set
sub Lock { tied(%{$_[0]})->{state}->WriteLock(); }
sub UnLock { tied(%{$_[0]})->{state}->UnLock(); }
1;