| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | package Apache::Session::Counted; | 
| 2 | 1 |  |  | 1 |  | 4727 | use Apache::Session::Serialize::Storable; | 
|  | 1 |  |  |  |  | 6608 |  | 
|  | 1 |  |  |  |  | 32 |  | 
| 3 |  |  |  |  |  |  |  | 
| 4 | 1 |  |  | 1 |  | 81 | use strict; | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 48 |  | 
| 5 | 1 |  |  | 1 |  | 7 | use vars qw(@ISA); | 
|  | 1 |  |  |  |  | 9 |  | 
|  | 1 |  |  |  |  | 221 |  | 
| 6 |  |  |  |  |  |  | @ISA = qw(Apache::Session); | 
| 7 | 1 |  |  | 1 |  | 9 | use vars qw($VERSION $RELEASE_DATE); | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 104 |  | 
| 8 |  |  |  |  |  |  | $VERSION = sprintf "%d.%03d", q$Revision: 1.118 $ =~ /(\d+)\.(\d+)/; | 
| 9 |  |  |  |  |  |  | $RELEASE_DATE = q$Date: 2002/04/15 12:39:07 $; | 
| 10 |  |  |  |  |  |  |  | 
| 11 | 1 |  |  | 1 |  | 1331 | use Apache::Session 1.50; | 
|  | 1 |  |  |  |  | 2235 |  | 
|  | 1 |  |  |  |  | 29 |  | 
| 12 | 1 |  |  | 1 |  | 1093 | use File::CounterFile; | 
|  | 1 |  |  |  |  | 14315 |  | 
|  | 1 |  |  |  |  | 47 |  | 
| 13 |  |  |  |  |  |  |  | 
| 14 |  |  |  |  |  |  | { | 
| 15 |  |  |  |  |  |  | package Apache::Session::CountedStore; | 
| 16 | 1 |  |  | 1 |  | 9 | use Symbol qw(gensym); | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 108 |  | 
| 17 |  |  |  |  |  |  |  | 
| 18 | 1 |  |  | 1 |  | 7 | use strict; | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 4718 |  | 
| 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 | 0 |  |  |  |  |  | warn sprintf "A:S:Counted: content[%s]serialized[%s]", | 
| 102 |  |  |  |  |  |  | $dumper->stringify($content), | 
| 103 |  |  |  |  |  |  | $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 | 0 |  |  |  |  |  | warn "A:S:Counted: Creating directory $dir | 
| 131 |  |  |  |  |  |  | and $n subdirectories in $levels level(s)\n"; | 
| 132 | 0 | 0 |  |  |  |  | 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 | 0 | 0 |  |  |  |  | die "The argument 'Directory' for object storage must be passed as an argument" | 
| 160 |  |  |  |  |  |  | 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 | 0 | 0 |  |  |  |  | $self->{data}->{_session_id} = $self->generate_id() if | 
| 232 |  |  |  |  |  |  | $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 | 0 | 0 |  |  |  |  | my $cf = $self->{args}{CounterFile} or | 
| 242 |  |  |  |  |  |  | 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 |  |  |  |  |  |  |  |