File Coverage

blib/lib/Apache/Session/Counted.pm
Criterion Covered Total %
statement 24 159 15.0
branch 0 54 0.0
condition 0 27 0.0
subroutine 8 19 42.1
pod 0 2 0.0
total 32 261 12.2


line stmt bran cond sub pod time code
1             package Apache::Session::Counted;
2 1     1   797 use Apache::Session::Serialize::Storable;
  1         3392  
  1         24  
3              
4 1     1   6 use strict;
  1         2  
  1         20  
5 1     1   4 use vars qw(@ISA);
  1         1  
  1         39  
6             @ISA = qw(Apache::Session);
7 1     1   4 use vars qw($VERSION $RELEASE_DATE);
  1         2  
  1         42  
8             $VERSION = '1.119';
9             $RELEASE_DATE = q$Date: 2002/04/15 12:39:07 $;
10              
11 1     1   454 use Apache::Session 1.50;
  1         1509  
  1         28  
12 1     1   384 use File::CounterFile;
  1         4142  
  1         47  
13              
14             {
15             package Apache::Session::CountedStore;
16 1     1   7 use Symbol qw(gensym);
  1         2  
  1         36  
17              
18 1     1   5 use strict;
  1         1  
  1         1487  
19              
20 0     0     sub new { bless {}, shift }
21              
22             # write. Note that we alias insert and update
23             sub update {
24 0     0     my $self = shift;
25 0           my $session = shift;
26 0           my $storefile = $self->storefilename($session);
27 0           my $fh = gensym;
28 0 0         unless ( open $fh, ">$storefile\0" ) {
29 0           warn qq{A:S:Counted: Could not open file $storefile for writing: $!
30             Maybe you haven't initialized the storage directory with
31             use Apache::Session::Counted;
32             Apache::Session::CountedStore->tree_init("$session->{args}{Directory}","$session->{args}{DirLevels}");
33             I'm trying to band-aid by creating this directory};
34 0           require File::Basename;
35 0           my $dir = File::Basename::dirname($storefile);
36 0           require File::Path;
37 0           File::Path::mkpath($dir);
38 0           warn "A:S:Counted: mkdir on directory $dir successfully done.";
39             }
40 0 0         if ( open $fh, ">$storefile\0" ) {
41 0           print $fh $session->{serialized}; # $fh->print might fail in some perls
42 0           close $fh;
43             } else {
44 0           die "Giving up. Could not open file $storefile for writing: $!";
45             }
46             }
47             *insert = \&update;
48              
49             # retrieve
50             sub materialize {
51 0     0     my $self = shift;
52 0           my $session = shift;
53 0 0         my $sessionID = $session->{data}{_session_id} or die "Got no session ID";
54 0           my($host) = $sessionID =~ /(?:([^:]+)(?::))/;
55 0           my($content);
56              
57 0 0 0       if ($host &&
      0        
58             $session->{args}{HostID} &&
59             $session->{args}{HostID} ne $host
60             ) {
61             # warn sprintf("configured hostID[%s]host from argument[%s]",
62             # $session->{args}{HostID},
63             # $host);
64 0           my $surl;
65 0 0         if (exists $session->{args}{HostURL}) {
66 0           $surl = $session->{args}{HostURL}->($host,$sessionID);
67             } else {
68 0           $surl = sprintf "http://%s/?SESSIONID=%s", $host, $sessionID;
69             }
70             # warn "surl[$surl]";
71 0 0         if ($surl) {
72 0           require LWP::UserAgent;
73 0           require HTTP::Request::Common;
74 0           my $ua = LWP::UserAgent->new;
75 0   0       $ua->timeout($session->{args}{Timeout} || 10);
76 0           my $req = HTTP::Request::Common::GET $surl;
77 0           my $result = $ua->request($req);
78 0 0         if ($result->is_success) {
79 0           $content = $result->content;
80             } else {
81 0           $content = Storable::nfreeze {};
82             }
83             } else {
84 0           $content = Storable::nfreeze {};
85             }
86 0           $session->{serialized} = $content;
87 0           return;
88             }
89              
90 0           my $storefile = $self->storefilename($session);
91 0           my $fh = gensym;
92 0 0         if ( open $fh, "<$storefile\0" ) {
93 0           local $/;
94 0           $session->{serialized} = <$fh>;
95 0 0         close $fh or die $!;
96 0 0 0       if ($content && $content ne $session->{serialized}) {
97 0           warn "A:S:Counted: content and serialized are NOT equal";
98 0           require Dumpvalue;
99 0           my $dumper = Dumpvalue->new;
100 0           $dumper->set(unctrl => "quote");
101             warn sprintf "A:S:Counted: content[%s]serialized[%s]",
102             $dumper->stringify($content),
103 0           $dumper->stringify($session->{serialized});
104             }
105             } else {
106 0           warn "A:S:Counted: Could not open file $storefile for reading: $!";
107 0           $session->{data} = {};
108 0           $session->{serialized} = $session->{serialize}->($session);
109             }
110             }
111              
112             sub remove {
113 0     0     warn "A:S:Counted: remove not implemented"; # doesn't make sense
114             # for our concept of a
115             # session
116 0           return;
117              
118 0           my $self = shift;
119 0           my $session = shift;
120 0           my $storefile = $self->storefilename($session);
121 0 0         unlink $storefile or
122             warn "A:S:Counted: Object $storefile does not exist in the data store";
123             }
124              
125             sub tree_init {
126 0     0     my $self = shift;
127 0           my $dir = shift;
128 0           my $levels = shift;
129 0           my $n = 0x100 ** $levels;
130             # warn "A:S:Counted: Creating directory $dir
131             # and $n subdirectories in $levels level(s)\n";
132             # warn "A:S:Counted: This may take a while\n" if $levels>1;
133 0           require File::Path;
134 0           $|=1;
135             my $feedback =
136             sub {
137 0     0     $n--;
138 0 0         printf "\r$n directories left " unless $n % 256;
139 0 0         print "\n" unless $n;
140 0           };
141 0           File::Path::mkpath($dir);
142 0           make_dirs($dir,$levels,$feedback); # function for speed
143             }
144              
145             sub make_dirs {
146 0     0     my($dir, $levels, $feedback) = @_;
147 0           $levels--;
148 0           for (my $i=0; $i<256; $i++) {
149 0           my $subdir = sprintf "%s/%02x", $dir, $i;
150 0 0 0       -d $subdir or mkdir $subdir, 0755 or die "Couldn't mkdir $subdir: $!";
151 0           $feedback->();
152 0 0         make_dirs($subdir, $levels, $feedback) if $levels;
153             }
154             }
155              
156             sub storefilename {
157 0     0     my $self = shift;
158 0           my $session = shift;
159             die "The argument 'Directory' for object storage must be passed as an argument"
160 0 0         unless defined $session->{args}{Directory};
161 0           my $dir = $session->{args}{Directory};
162 0   0       my $levels = $session->{args}{DirLevels} || 0;
163             # here we depart from TreeStore:
164 0 0         my $sessionID = $session->{data}{_session_id} or die "Got no session ID";
165 0           my($host,$file) = $sessionID =~ /(?:([^:]+)(?::))?([\da-f]+)/;
166 0 0         die "Too short ID part '$file' in session ID'" if length($file)<8;
167 0           while ($levels) {
168 0           $file =~ s|((..){$levels})|$1/|;
169 0           $levels--;
170             }
171 0           "$dir/$file";
172             }
173             }
174              
175             # Counted is locked by definition
176             sub release_all_locks {
177 0     0 0   return;
178             }
179              
180             *get_lock_manager = \&release_all_locks;
181             *release_read_lock = \&release_all_locks;
182             *release_write_lock = \&release_all_locks;
183             *acquire_read_lock = \&release_all_locks;
184             *acquire_write_lock = \&release_all_locks;
185              
186             sub TIEHASH {
187 0     0     my $class = shift;
188              
189 0           my $session_id = shift;
190 0   0       my $args = shift || {};
191              
192 0           my $self = {
193             args => $args,
194              
195             data => { _session_id => $session_id },
196             # we always *have* read and write lock and need not care
197             lock => Apache::Session::READ_LOCK|Apache::Session::WRITE_LOCK,
198             status => 0,
199             lock_manager => undef,
200             generate => undef,
201             serialize => \&Apache::Session::Serialize::Storable::serialize,
202             unserialize => \&Apache::Session::Serialize::Storable::unserialize,
203             };
204              
205 0           bless $self, $class;
206 0           $self->{object_store} = Apache::Session::CountedStore->new($self);
207              
208             #If a session ID was passed in, this is an old hash.
209             #If not, it is a fresh one.
210              
211 0 0         if (defined $session_id) {
212 0           $self->make_old;
213 0           $self->restore; # calls materialize and unserialize via Apache::Session
214 0 0 0       if (
      0        
      0        
215             exists $self->{data} &&
216             exists $self->{data}{_session_id} &&
217             defined $self->{data}{_session_id} && # protect agains unini warning
218             $session_id eq $self->{data}{_session_id}
219             ) {
220             # Fine. Validated. Kind of authenticated.
221             # ready for a new session ID, keeping state otherwise.
222 0 0         $self->make_modified if $self->{args}{AlwaysSave};
223             } else {
224             # oops, somebody else tried this ID, don't show him data.
225 0           delete $self->{data};
226 0           $self->make_new;
227             }
228             }
229             # if we have no counterfile, we cannot generate an ID, that's OK:
230             # this session will not need to be written.
231             $self->{data}->{_session_id} = $self->generate_id() if
232 0 0         $self->{args}{CounterFile};
233             # no make_new here, session-ID doesn't count as data
234              
235 0           return $self;
236             }
237              
238             sub generate_id {
239 0     0 0   my $self = shift;
240             # wants counterfile
241             my $cf = $self->{args}{CounterFile} or
242 0 0         die "Argument CounterFile needed in the attribute hash to the tie";
243 0           my $c;
244 0           eval { $c = File::CounterFile->new($cf,"0"); };
  0            
245 0 0         if ($@) {
246 0           warn "A:S:Counted: Counterfile problem, trying to repair...";
247 0 0         if (-e $cf) {
248 0           warn "A:S:Counted: Retrying after removing $cf.";
249 0           unlink $cf; # May fail. stupid enough that we are here.
250 0           $c = File::CounterFile->new($cf,"0");
251             } else {
252 0           require File::Basename;
253 0           my $dirname = File::Basename::dirname($cf);
254 0           my @mkdir;
255 0           while (! -d $dirname) {
256 0           push @mkdir, $dirname;
257 0           $dirname = File::Basename::dirname($dirname);
258             }
259 0           while (@mkdir) {
260 0           my $dirname = pop @mkdir;
261 0 0         mkdir $dirname, 0755 or die "Couldn't mkdir $dirname. Please create it with appropriate permissions";
262             }
263 0           $c = File::CounterFile->new($cf,"0");
264             }
265 0           warn "A:S:Counted: Counterfile problem successfully reapired.";
266             }
267 0           my $rhexid = sprintf "%08x", $c->inc;
268 0           my $hexid = scalar reverse $rhexid; # optimized for treestore. Not
269             # everything in one directory
270              
271             # we have entropy as bad as rand(). Typically not very good.
272 0           my $password = sprintf "%08x%08x", rand(0xffffffff), rand(0xffffffff);
273              
274 0 0         if (exists $self->{args}{HostID}) {
275 0           return sprintf "%s:%s_%s", $self->{args}{HostID}, $hexid, $password;
276             } else {
277 0           return $hexid . "_" . $password;
278             }
279             }
280              
281             1;
282              
283             =head1 NAME
284              
285             Apache::Session::Counted - Session management via a File::CounterFile
286              
287             =head1 SYNOPSIS
288              
289             tie %s, 'Apache::Session::Counted', $sessionid, {
290             Directory => ,
291             DirLevels => ,
292             CounterFile => ,
293             AlwaysSave => ,
294             HostID => ,
295             HostURL => ,
296             Timeout => ,
297             }
298              
299             =head1 DESCRIPTION
300              
301             This session module is based on Apache::Session, but it persues a
302             different notion of a session, so you probably have to adjust your
303             expectations a little.
304              
305             The dialog that is implemented within an HTTP based application is a
306             nonlinear chain of events. The user can decide to use the back button
307             at any time without informing the application about it. A proper
308             session management must be prepared for this and must maintain the
309             state of every single event. For handling the notion of a session and
310             the notion of a registered user, the application has to differentiate
311             carefully between global state of user data and a user's session
312             related state. Some data may expire after a day, others may be
313             regarded as unexpirable. This module is solely responsible for
314             handling session related data. Saving unexpirable user related data
315             must be handled by the calling application.
316              
317             In Apache::Session::Counted, a session-ID only lasts from one request
318             to the next at which point a new session-ID is computed by the
319             File::CounterFile module. Thus what you have to treat differently than
320             in Apache::Session are those parts that rely on the session-ID as a
321             fixed token per user. Accordingly, there is no option to delete a
322             session. The remove method is simply disabled as old session data will
323             be overwritten as soon as the counter is reset to zero.
324              
325             The usage of the module is via a tie as described in the synopsis. The
326             arguments have the following meaning:
327              
328             =over
329              
330             =item Directory, DirLevels
331              
332             Works similar to filestore but as most file systems are slow on large
333             directories, works in a tree of subdirectories.
334              
335             =item CounterFile
336              
337             A filename to be used by the File::CounterFile module. By changing
338             that file or the filename periodically, you can achieve arbitrary
339             patterns of key generation. If you do not specify a CounterFile, you
340             promise that in this session there is no need to generate a new ID and
341             that the whole purpose of this object is to retrieve previously stored
342             session data. Thus no new session file will be written. If you break
343             your promise and write something to the session hash, the retrieved
344             session file will be overwritten.
345              
346             =item AlwaysSave
347              
348             A boolean which, if true, forces storing of session data in any case.
349             If false, only a STORE, DELETE or CLEAR trigger that the session file
350             will be written when the tied hash goes out of scope. This has the
351             advantage that you can retrieve an old session without storing its
352             state again.
353              
354             =item HostID
355              
356             A string that serves as an identifier for the host we are running on.
357             This string will become part of the session-ID and must not contain a
358             colon. This can be used in a cluster environment so that a load
359             balancer or other interested parties can retrieve the session data
360             again.
361              
362             =item HostURL
363              
364             A callback that returns the service URL that can be called to get at
365             the session data from another host. This is needed in a cluster
366             environment. Two arguments are passed to this callback: HostID and
367             Session-ID. The URL must return the serialized data in Storable's
368             nfreeze format. The Apache::Session::Counted module can be used to set
369             such an URL up. If HostURL is not defined, the default is
370              
371             sprintf "http://%s/?SESSIONID=%s", , ;
372              
373             The callback can return false to signal that there is no session to
374             retrieve (e.g. when the host or id argument is illegal).
375              
376             =item Timeout
377              
378             Sets the timeout for LWP::UserAgent for retrieving a session from a
379             different host. Default is 10 seconds.
380              
381             =back
382              
383             =head2 What this model buys you
384              
385             =over
386              
387             =item storing state selectively
388              
389             You need not store session data for each and every request of a
390             particular user. There are so many CGI requests that can easily be
391             handled with two hidden fields and do not need any session support on
392             the server side, and there are others where you definitely need
393             session support. Both can appear within the same application.
394             Apache::Session::Counted allows you to switch session writing on and
395             off during your application without effort. (In fact, this advantage
396             is shared with the clean persistence model of Apache::Session)
397              
398             =item keeping track of transactions
399              
400             As each request of a single user remains stored until you restart the
401             counter, there are all previous states of a single session close at
402             hand. The user presses the back button 5 times and changes a decision
403             and simply opens a new branch of the same session. This can be an
404             advantage and a disadvantage. I tend to see it as a very strong
405             feature. Your milage may vary.
406              
407             =item counter
408              
409             You get a counter for free which you can control just like
410             File::CounterFile (because it B File::CounterFile).
411              
412             =item cleanup
413              
414             Your data storage area cleans up itself automatically. Whenever you
415             reset your counter via File::CounterFile, the storage area in use is
416             being reused. Old files are being overwritten in the same order they
417             were written, giving you a lot of flexibility to control session
418             storage time and session storage disk space.
419              
420             =item performance
421              
422             The notion of daisy-chained sessions simplifies the code of the
423             session handler itself quite a bit and it is likely that this
424             simplification results in an improved performance (not tested yet due
425             to lack of benchmarking apps for sessions). There are less file stats
426             and less sections that need locking, but without real world figures,
427             it's hard to tell.
428              
429             =back
430              
431             As with other modules in the Apache::Session collection, the tied hash
432             contains a key C<_session_id>. You must be aware that the value of this
433             hash entry is not the same as the one you passed in when you retrieved
434             the session (if you retrieved a session at all). So you have to make
435             sure that you send your users a new session-id in each response, and
436             that this is never the old one.
437              
438             As an implemenation detail it may be of interest to you, that the
439             session ID in Apache::Session::Counted consists of two or three parts:
440             an optional host alias given by the HostID paramter, followed by a
441             colon. Then an ordinary number which is a simple counter which is
442             followed by an underscore. And finally a session-ID like the one in
443             Apache::Session. The number part is used as an identifier of the
444             session and the ID part is used as a password. The number part is
445             easily predictable, but the second part is reasonable unpredictable.
446             We use the first part for implementation details like storage on the
447             disk and the second part to verify the ownership of that token.
448              
449             =head1 PREREQUISITES
450              
451             Apache::Session::Counted needs Apache::Session and File::CounterFile,
452             all available from the CPAN. The HostID and HostURL parameters for a
453             cluster solution need LWP installed.
454              
455             =head1 EXAMPLES
456              
457             The following example resets the counter every 24 hours and keeps the
458             totals of every day as a side effect:
459              
460             my(@t) = localtime;
461             tie %session, 'Apache::Session::Counted', $sid,
462             {
463             Directory => ...,
464             DirLevels => ...,
465             CounterFile => sprintf("/some/dir/%04d-%02d-%02d", $t[5]+1900,$t[4]+1,$t[3])
466             };
467              
468              
469             The same effect can be accomplished with a fixed filename and an
470             external cronjob that resets the counter like so:
471              
472             use File::CounterFile;
473             $c=File::CounterFile->new("/usr/local/apache/data/perl/sessiondemo/counter");
474             $c->lock;
475             $c-- while $c>0;
476             $c->unlock;
477              
478              
479             =head1 AUTHOR
480              
481             Andreas Koenig
482              
483             =head1 COPYRIGHT
484              
485             This software is copyright(c) 1999-2002 Andreas Koenig. It is free
486             software and can be used under the same terms as perl, i.e. either the
487             GNU Public Licence or the Artistic License.
488              
489             =cut
490