File Coverage

blib/lib/Apache/ASP/StateManager.pm
Criterion Covered Total %
statement 194 262 74.0
branch 73 158 46.2
condition 39 82 47.6
subroutine 10 10 100.0
pod 0 7 0.0
total 316 519 60.9


line stmt bran cond sub pod time code
1              
2             package Apache::ASP;
3              
4             # quickly decomped out of Apache::ASP so we could load the routines only
5             # when we are managing State objects
6              
7 6     6   141 use Apache::ASP::State;
  6         82  
  6         157  
8              
9 6     6   119 use strict;
  6         57  
  6         148  
10 6         104 use vars qw(
11             $CleanupGroups
12             $SessionIDLength $SessionTimeout $StateManager
13             $DefaultStateDB $DefaultStateSerializer
14 6     6   93 );
  6         55  
15              
16             $SessionTimeout = 20;
17             $StateManager   = 10;
18              
19             # Some OS's have hashed directory lookups up to 16 bytes, so we leave room
20             # for .lock extension ... nevermind, security is more important, back to 32
21             # $SessionIDLength = 11;
22             $SessionIDLength = 32;
23             $DefaultStateDB = 'SDBM_File';
24             $DefaultStateSerializer = 'Data::Dumper';
25              
26             sub InitState {
27 7     7 0 78     my $self = shift;
28 7         84     my $r = $self->{r};
29 7         78     my $global_asa = $self->{GlobalASA};
30              
31             ## STATE INITS
32             # what percent of the session_timeout's time do we garbage collect
33             # state files and run programs like Session_OnEnd and Application_OnEnd
34 7         99     $self->{state_manager} = &config($self, 'StateManager', undef, $Apache::ASP::StateManager);
35              
36             # state is the path where state files are stored, like $Session, $Application, etc.
37 7         106     $self->{state_dir} = &config($self, 'StateDir', undef, $self->{global}.'/.state');
38 7         81     $self->{state_dir} =~ tr///; # untaint
39 7         92     $self->{session_state} = &config($self, 'AllowSessionState', undef, 1);
40 7         91     $self->{state_serialize} = &config($self, 'ApplicationSerialize');
41              
42 7 50       90     if($self->{state_db} = &config($self, 'StateDB')) {
43             # StateDB - Check StateDB module support
44 0 0       0 $Apache::ASP::State::DB{$self->{state_db}} ||
45             $self->Error("$self->{state_db} is not supported for StateDB, try: " .
46             join(", ", keys %Apache::ASP::State::DB));
47 0         0 $self->{state_db} =~ /^(.*)$/; # untaint
48 0         0 $self->{state_db} = $1; # untaint
49             # load the state database module && serializer
50 0         0 $self->LoadModule('StateDB', $self->{state_db});
51                 }
52 7 50       89     if($self->{state_serializer} = &config($self, 'StateSerializer')) {
53 0         0 $self->{state_serializer} =~ tr///; # untaint
54 0         0 $self->LoadModule('StateSerializer', $self->{state_serializer});
55                 }
56              
57             # INTERNAL tie to the application internal info
58 7         367     my %Internal;
59 7 50       97     tie(%Internal, 'Apache::ASP::State', $self, 'internal', 'server')
60                   || $self->Error("can't tie to internal state");
61 7         171     my $internal = $self->{Internal} = bless \%Internal, 'Apache::ASP::State';
62 7 50       153     $self->{state_serialize} && $internal->LOCK;
63              
64             # APPLICATION create application object
65 7         96     $self->{app_state} = &config($self, 'AllowApplicationState', undef, 1);
66 7 50       88     if($self->{app_state}) {
67             # load at runtime for CGI environments, preloaded for mod_perl
68 7         210 require Apache::ASP::Application;
69              
70 7 50       99 ($self->{Application} = &Apache::ASP::Application::new($self))
71             || $self->Error("can't get application state");
72 7 50       147 $self->{state_serialize} && $self->{Application}->Lock;
73              
74                 } else {
75 0 0       0 $self->{dbg} && $self->Debug("no application allowed config");
76                 }
77              
78             # SESSION if we are tracking state, set up the appropriate objects
79 7         64     my $session;
80 7 50       89     if($self->{session_state}) {
81             ## SESSION INITS
82 7         111 $self->{cookie_path}       = &config($self, 'CookiePath', undef, '/');
83 7         137 $self->{cookie_domain}     = &config($self, 'CookieDomain');
84 7         90 $self->{paranoid_session}  = &config($self, 'ParanoidSession');
85 7         161 $self->{remote_ip}         = $r->connection()->remote_ip();
86 7         122 $self->{session_count}     = &config($self, 'SessionCount');
87            
88             # cookieless session support, cascading values
89 7         119 $self->{session_url_parse_match} = &config($self, 'SessionQueryParseMatch');
90 7   33     147 $self->{session_url_parse} = $self->{session_url_parse_match} || &config($self, 'SessionQueryParse');
91 7   33     110 $self->{session_url_match} = $self->{session_url_parse_match} || &config($self, 'SessionQueryMatch');
92 7   33     147 $self->{session_url} = $self->{session_url_parse} || $self->{session_url_match} || &config($self, 'SessionQuery');
      33        
93 7         85 $self->{session_url_force} = &config($self, 'SessionQueryForce');
94            
95 7         86 $self->{session_serialize} = &config($self, 'SessionSerialize');
96 7         87 $self->{secure_session}    = &config($self, 'SecureSession');
97             # session timeout in seconds since that is what we work with internally
98 7         91 $self->{session_timeout}   = &config($self, 'SessionTimeout', undef, $SessionTimeout) * 60;
99 7   50     108 $self->{'ua'}              = $self->{headers_in}->get('User-Agent') || 'UNKNOWN UA';
100             # refresh group by some increment smaller than session timeout
101             # to withstand DoS, bruteforce guessing attacks
102             # defaults to checking the group once every 2 minutes
103 7         139 $self->{group_refresh}     = int($self->{session_timeout} / $self->{state_manager});
104            
105             # Session state is dependent on internal state
106              
107             # load at runtime for CGI environments, preloaded for mod_perl
108 7         289 require Apache::ASP::Session;
109              
110 7   33     108 $session = $self->{Session} = &Apache::ASP::Session::new($self)
111             || $self->Die("can't create session");
112 7 50       94 $self->{state_serialize} && $session->Lock();
113            
114                 } else {
115 0 0       0 $self->{dbg} && $self->Debug("no sessions allowed config");
116                 }
117              
118             # update after long state init, possible with SessionSerialize config
119 7         124     $self->{Response}->IsClientConnected();
120              
121             # POSTPOSE STATE EVENTS, so we can delay the Response object creation
122             # until after the state objects are created
123 7 50       77     if($session) {
124 7         68 my $last_session_timeout;
125 7 50       331 if($session->Started()) {
126             # we only want one process purging at a time
127 7 50       86 if($self->{app_state}) {
128 7         96 $internal->LOCK();
129 7 100 100     378 if(($last_session_timeout = $internal->{LastSessionTimeout} || 0) < time()) {
130 1         15 $internal->{'LastSessionTimeout'} = $self->{session_timeout} + time;
131 1         48 $internal->UNLOCK();
132 1         14 $self->{Application}->Lock;
133 1         10 my $obj = tied(%{$self->{Application}});
  1         12  
134 1 50       15 if($self->CleanupGroups('PURGE')) {
135 1 50       13 $last_session_timeout && $global_asa->ApplicationOnEnd();
136 1         17 $global_asa->ApplicationOnStart();
137             }
138 1         16 $self->{Application}->UnLock;
139             } 
140 7         458 $internal->UNLOCK();
141             }
142 7         107 $global_asa->SessionOnStart();
143             }
144              
145 7 50       102 if($self->{app_state}) {
146             # The last session timeout should only be updated every group_refresh period
147             # another optimization, rand() so not all at once either
148 7         99 $internal->LOCK();
149 7   100     90 $last_session_timeout ||= $internal->{'LastSessionTimeout'};
150 7 100       327 if($last_session_timeout < $self->{session_timeout} + time +
151             (rand() * $self->{group_refresh} / 2))
152             {
153 2 50       26 $self->{dbg} && $self->Debug("updating LastSessionTimeout from $last_session_timeout");
154 2         27 $internal->{'LastSessionTimeout'} =
155             $self->{session_timeout} + time() + $self->{group_refresh};
156             }
157 7         108 $internal->UNLOCK();
158             }
159                 }
160              
161 7         98     $self;
162             }
163              
164             # Cleanup a state group, by default the group of the current session
165             # We do this currently in DESTROY, which happens after the current
166             # script has been executed, so that cleanup doesn't happen until
167             # after output to user
168             #
169             # We always exit unless there is a $Session defined, since we only
170             # cleanup groups of sessions if sessions are allowed for this script
171             sub CleanupGroup {
172 6     6 0 62     my($self, $group_id, $force) = @_;
173 6 50       67     return unless $self->{Session};
174              
175 6         52     my $asp = $self; # bad hack for some moved around code
176 6   100     61     $force ||= 0;
177              
178             # GET GROUP_ID
179 6         50     my $state;
180 6 50       59     unless($group_id) {
181 0         0 $state = $self->{Session}{_STATE};
182 0         0 $group_id = $state->GroupId();
183                 }
184              
185             # we must have a group id to work with
186 6 50       59     $asp->Error("no group id") unless $group_id;
187 6         57     my $group_key = "GroupId" . $group_id;
188              
189             # cleanup timed out sessions, from current group
190 6         55     my $internal = $asp->{Internal};
191 6         68     $internal->LOCK();
192 6   100     76     my $group_check = $internal->{$group_key} || 0;
193 6 50 66     237     unless($force || ($group_check < time())) {
194 0         0 $internal->UNLOCK();
195 0         0 return;
196                 }
197                 
198             # set the next group_check, randomize a bit to unclump the group checks,
199             # for 20 minute session timeout, had rand() / 2 + .5, but it was still
200             # too clumpy, going with pure rand() now, even if a bit less efficient
201              
202 6         82     my $next_check = int($asp->{group_refresh} * rand()) + 1;
203 6         73     $internal->{$group_key} = time() + $next_check;
204 6         99     $internal->UNLOCK();
205              
206             ## GET STATE for group
207 6   33     84     $state ||= &Apache::ASP::State::new($asp, $group_id);
208 6   50     74     my $ids = $state->GroupMembers() || [];
209              
210             # don't return so we can't delete the empty group later
211             # return unless scalar(@$ids);
212              
213 6 50       3889     $asp->{dbg} && $asp->Debug("group check $group_id, next in $next_check sec");
214 6         88     my $id = $self->{Session}->SessionID();
215 6         119     my $deleted = 0;
216 6         73     $internal->LOCK();
217 6 50       65     $asp->{dbg} && $asp->Debug("checking group ids", $ids);
218 6         59     for my $id (@$ids) {
219 7         97 eval {
220              
221             # if($id eq $_) {
222             # $asp->{dbg} && $asp->Debug("skipping delete self", {id => $id});
223             # next;
224             # }
225            
226             # we lock the internal, so a session isn't being initialized
227             # while we are garbage collecting it... we release it every
228             # time so we don't starve session creation if this is a large
229             # directory that we are garbage collecting
230 7         100 my $idata = $internal->{$id};
231            
232             # do this check in case this data is corrupt, and not deserialized, correctly
233 7 50 33     140 unless(ref($idata) && (ref($idata) eq 'HASH')) {
234 0         0 $idata = {};
235             }
236              
237 7   50     76 my $timeout = $idata->{timeout} || 0;
238            
239 7 50       70 unless($timeout) {
240             # we don't have the timeout always, since this session
241             # may just have been created, just in case this is
242             # a corrupted session (does this happen still ??), we give it
243             # a timeout now, so we will be sure to clean it up
244             # eventualy
245 0         0 $idata->{timeout} = time() + $asp->{session_timeout};
246 0         0 $internal->{$id} = $idata;
247 0         0 $asp->Debug("resetting timeout for $id to $idata->{timeout}");
248 0         0 return; # no next in eval {}
249             }
250             # only delete sessions that have timed out
251 7 50       129 unless($timeout < time()) {
252 7 50       73 $asp->{dbg} && $asp->Debug("$id not timed out with $timeout");
253 7         77 return; # no next in eval {}
254             }
255            
256             # UPDATE & UNLOCK, as soon as we update internal, we may free it
257             # definately don't lock around SessionOnEnd, as it might take
258             # a while to process
259            
260             # set the timeout for this session forward so it won't
261             # get garbage collected by another process
262 0 0       0 $asp->{dbg} && $asp->Debug("resetting timeout for deletion lock on $id");
263 0         0 $internal->{$id} = {
264 0         0 %{$internal->{$id}},
265             'timeout' => time() + $asp->{session_timeout},
266             'end' => 1,
267             };
268            
269            
270             # unlock many times in case we are locked above this loop
271 0         0 for (1..3) { $internal->UNLOCK() }
  0         0  
272 0         0 $asp->{GlobalASA}->SessionOnEnd($id);
273 0         0 $internal->LOCK;
274            
275             # set up state
276 0         0 my($member_state) = Apache::ASP::State::new($asp, $id);
277 0 0       0 if(my $count = $member_state->Delete()) {
278 0 0       0 $asp->{dbg} &&
279             $asp->Debug("deleting session", {
280             session_id => $id,
281             files_deleted => $count,
282             });
283 0         0 $deleted++;
284 0         0 delete $internal->{$id};
285             } else {
286 0         0 $asp->Error("can't delete session id: $id");
287 0         0 return; # no next in eval {}
288             }
289             };
290 7 50       78 if($@) {
291 0         0 $asp->Error("error for cleanup of session id $id: $@");
292             }
293                 }
294 6         76     $internal->UNLOCK();
295              
296             #### LEAVE DIRECTORIES, NASTY RACE CONDITION POTENTIAL
297             ## NOW PRUNE ONLY DIRECTORIES THAT WE DON'T NEED TO KEEP
298             ## FOR PERFORMANCE
299             # REMOVE DIRECTORY, LOCK
300             # if the directory is still empty, remove it, lock it
301             # down so no new sessions will be created in it while we
302             # are testing
303 6 50       65     if($deleted == @$ids) {
304 0 0       0 if ($state->GroupId !~ /^[0]/) {
305 0         0 $asp->{Internal}->LOCK();
306 0         0 my $ids = $state->GroupMembers();
307 0 0       0 if(@{$ids} == 0) {
  0         0  
308 0         0 $self->