File Coverage

blib/lib/Apache2/Controller/Session.pm
Criterion Covered Total %
statement 15 15 100.0
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 20 20 100.0


line stmt bran cond sub pod time code
1             package Apache2::Controller::Session;
2              
3             =head1 NAME
4              
5             Apache2::Controller::Session - Apache2::Controller with Apache::Session
6              
7             =head1 VERSION
8              
9             Version 1.001.001
10              
11             =cut
12              
13 1     1   7882 use version;
  1         3  
  1         8  
14             our $VERSION = version->new('1.001.001');
15              
16             =head1 SYNOPSIS
17              
18             Set your A2C session subclass as a C.
19              
20             This example assumes use of L.
21              
22             # get configuration directives:
23             PerlLoadModule Apache2::Controller::Directives
24              
25             # cookies will get path => /somewhere
26            
27             SetHandler modperl
28              
29             # see Apache2::Controller::Dispatch for dispatch subclass info
30             PerlInitHandler MyApp::Dispatch
31              
32             # see Apache2::Controller::DBI::Connector for database directives
33              
34             A2C_Session_Cookie_Opts name myapp_sessid
35             A2C_Session_Class Apache::Session::MySQL
36             A2C_Session_Secret jfa803m8cma083ak803kjf9-32
37              
38             PerlHeaderParserHandler Apache2::Controller::DBI::Connector MyApp::Session
39            
40              
41             In controllers, tied session hash is C<< $r->pnotes->{a2c}{session} >>.
42              
43             In this example above, you implement C
44             in your session subclass to return the options hashref to
45             C for L.
46              
47             If you do not implement get_options(), it will try to create
48             directories to use Apache::Session::File
49             using C<< /tmp/a2c_sessions// >>
50             and C<< /var/lock/a2c_sessions/ >>
51              
52             =head1 DESCRIPTION
53              
54             This is a module to make an L store available
55             to methods in your controllers. It is not just a session id -
56             if you just need a tracking mechanism or a way to store data
57             in cookies, you should roll your own handler with L.
58              
59             Your session module uses an Apache2::Controller::Session tracker module
60             as a base and you specify your L options either as
61             config variables or by implementing a method C<>.
62              
63             Instead of having a bunch of different options for all the different
64             L types, it's easier for me to make you provide
65             a method C in your subclass that will return a
66             has of the appropriate options for your chosen session store.
67              
68             =head2 CONFIG ALTERNATIVE 1: directives or PerlSetVar variables
69              
70             If you do not implement a special C method
71             or use settings other than these, these are the default:
72            
73            
74             PerlHeaderParserHandler MyApp::ApacheSessionFile
75              
76             A2C_Session_Class Apache::Session::File
77             A2C_Session_Opts Directory /tmp/sessions
78             A2C_Session_Opts LockDirectory /var/lock/sessions
79            
80              
81             Until directives work and the kludgey PerlSetVar syntax goes away,
82             spaces are not allowed in the argument values. Warning!
83             The kludgey PerlSetVar syntax will go away when
84             directives work properly.
85              
86             =head2 CONFIG ALTERNATIVE 2: C<< YourApp::YourSessionClass->get_options() >>
87              
88             Implement C in your subclass to return the final options
89             hashref for your L session type.
90              
91             For example, if your app uses DBIx::Class, maybe you want to
92             go ahead and init your schema so you can get the database
93             handle directly and pass that to your session class.
94              
95             See
96             L
97             for directives to set database connection in pnotes->{a2c}{dbh}.
98              
99             Here's a code example for Location /somewhere above:
100              
101             package MyApp::Session;
102             use strict;
103             use warnings FATAL => 'all';
104              
105             use base qw( Apache2::Controller::Session::Cookie );
106              
107             use English '-no_match_vars';
108             use Apache2::Controller::X;
109              
110             sub get_options {
111             my ($self) = @_;
112              
113             my $r = $self->{r};
114             eval {
115             $r->pnotes->{a2c}{dbh} ||= DBI->connect(
116             'dbi:mysql:database=myapp;host=mydbhost';
117             'myuser', 'mypassword'
118             );
119             };
120             a2cx "cannot connect to DB: $EVAL_ERROR" if $EVAL_ERROR;
121            
122             my $dbh = $r->pnotes->{a2c}{dbh}; # save handle for later use
123             # in controllers, etc.
124              
125             return {
126             Handle => $dbh,
127             LockHandle => $dbh,
128             };
129             }
130              
131             If you do it this way or use Apache::DBI,
132             be careful about transactions. See L below.
133              
134             # ...
135              
136             In your controller module, access the session in C<< pnotes->{a2c}{session} >>.
137            
138             package MyApp::Controller::SomeWhere::Overtherainbow;
139             use base qw( Apache2::Controller Apache2::Request );
140             # ...
141             sub default {
142             my ($self) = @_;
143              
144             my $session = $self->pnotes->{a2c}{session};
145             $session->{foo} = 'bar';
146              
147             # session will be saved by a PerlLogHandler
148             # that was automatically pushed by Apache2::Controller::Session
149              
150             # and in my example
151              
152             return Apache2::Const::HTTP_OK;
153             }
154              
155             =head1 DATABASE TRANSACTION SAFETY
156              
157             When this handler runs, it ties the session into a special
158             hash that it keeps internally, and loads a copy into
159             C<< $r->pnotes->{a2c}{session} >>. So, modifying the session hash
160             is fine, as long as you do not dereference it, or as long
161             as you save your changes back to C<< $r->pnotes->{a2c}{session} >>.
162              
163             No changes are auto-committed. The one in pnotes is
164             copied back into the tied session hash in a C,
165             after the server finishes output but I it closes
166             the connection to the client. If the connection is detected
167             to be aborted in the C phase, changes are NOT
168             saved into the session object.
169              
170             If you implemented C as per above and decided
171             to save your $dbh for later use in your controllers, feel free
172             to start transactions and use them normally. Just make sure you
173             use L correctly and roll back or commit your
174             transactions.
175              
176             If you decide to push a C
177             to roll back transactions for broken connections or something,
178             or C to do something else (don't use
179             post-connection phases for database transactions or you'll get out of sync),
180             be aware
181             that this handler 'unshifts' a log handler closure that
182             saves the copy in pnotes back into the tied hash.
183             It does this by re-ordering the C stack with
184             L and C.
185             So if you push another post-response handler that wants to
186             choose whether to save the session or not, be aware that
187             it may not work as you expect unless you re-order that
188             phase's handler stack again.
189              
190             =head1 TO SAVE OR NOT TO SAVE
191              
192             Generally in your code, it's complicated to decide whether everything
193             has worked before you save anything to the session. It's easier just
194             to save stuff, and then if something goes wrong, it is as if this
195             rolls back.
196              
197             A C subroutine is 'unshifted' to the request stack
198             which decides whether to save changes to the session. By default,
199             it saves changes only if A) the connection is not aborted,
200             and B) your controller set HTTP status < 300,
201             i.e. it returned C (0), one of the C family (100+)
202             or one of the C family (200+).
203              
204             So for an C, or throwing an exception, redirecting,
205             forbidding access, etc (>= 300), it normally would not save changes.
206             If your L controller module returns one of these
207             non-OK statuses, but you want to force the saving of the session contents,
208             set C<< $self->pnotes->{a2c}{session_force_save} = 1 >> before
209             your response phase controller returns a status to L.
210              
211             If the connection is aborted mid-way (i.e. the pipe was broken
212             due to a network failure or the user clicked 'stop'
213             in the browser), then the session will not be saved,
214             whether you set the force save flag or not.
215             (If this is not useful and correct behavior contact me and I
216             will add another switch, but it seems right to me.)
217              
218             It actually re-orders the C stack so that
219             its handlers run first, before the handler pushed by
220             L commits the database
221             transaction, for example.
222              
223             This used to push a C to save the session,
224             which made sense at the time, but the OpenID auth tests revealed
225             that the Cleanup handler is apparently assigned a thread to
226             process it independently, even under prefork with C.
227             So, the test script was firing off a new request
228             before the old request Cleanup handler ran to save the session,
229             which resulted in sporadic and inconsistent failures...
230             yeah, THOSE kind, you know the type, the most maddening ones.
231              
232             Apache::Session does not always save automatically, for
233             example if you change something in the bottom tier of
234             a multi-level hash. If you want to, set the directive flag
235             C and this will set a top-level
236             timestamp C<< $r->pnotes->{a2c}{session}{a2c_timestamp} >>
237             on the way out to trigger L to save everything.
238             But if you are potentially accessing the session contents without
239             setting it every time, you should just set a top-level timestamp
240             manually to indicate to L that you want
241             things saved at the end of every request, but this may
242             slow you down on a busy site, so it is not the default.
243             See L
244             and L.
245              
246             =head1 IMPLEMENTING TRACKER SUBCLASSES
247              
248             See L for how to implement
249             a custom tracker subclass. This implements C<$sid = get_session_id()>
250             which gets a session id from a cookie, and C
251             which sets the session id in the cookie.
252              
253             Perhaps some custom tracker subclass would implement
254             C to get the session_id out of the request
255             query params, and C would push a C
256             to post-process all other handler output and append the session id param
257             onto any url links that refer to our site. That would be cool...
258             release your own plug-in.
259             If you wanted to do it with combined cookies and url params in
260             this way you could
261             overload C and C, etc. etc.
262              
263             =head1 ERRORS
264              
265             C<> will throw an error exception if the
266             session setup encounters an error.
267              
268             =head1 METHODS
269              
270             =cut
271              
272 1     1   163 use strict;
  1         2  
  1         41  
273 1     1   6 use warnings FATAL => 'all';
  1         2  
  1         57  
274 1     1   6 use English '-no_match_vars';
  1         3  
  1         9  
275              
276 1         667 use base qw(
277             Apache2::Controller::NonResponseBase
278             Apache2::Controller::Methods
279 1     1   552 );
  1         2  
280              
281             use YAML::Syck;
282             use Log::Log4perl qw(:easy);
283             use File::Spec;
284             use Digest::SHA qw( sha224_base64 );
285              
286             use Apache2::Const -compile => qw( OK );
287             use Apache2::RequestUtil ();
288             use Apache2::Controller::X;
289             use Apache2::Controller::Const qw( $DEFAULT_SESSION_SECRET );
290              
291             =head2 process
292              
293             The C method
294             attaches or creates a session, and pushes a PerlLogHandler
295             closure to save the session after the end of the request.
296              
297             It sets the session id cookie
298             with an expiration that you set in your subclass as C
299             in a format that is passed to Apache2::Cookie. (i.e. '3M', '2D', etc.)
300             Don't set that if you want them to expire at the end of the
301             browser session.
302              
303             =cut
304              
305             my %used; # i feel used!
306              
307             sub process {
308             my ($self) = @_;
309             my $r = $self->{r};
310              
311             my $session_id = $self->get_session_id();
312             DEBUG "processing session: ".($session_id ? $session_id : '[new session]');
313              
314             my $directives = $self->get_directives();
315             my $class = $directives->{A2C_Session_Class} || 'Apache::Session::File';
316             DEBUG "using session class $class";
317              
318             do {
319             eval "use $class;";
320             a2cx $EVAL_ERROR if $EVAL_ERROR;
321             $used{$class} = 1;
322             } if !exists $used{$class};
323              
324             my $options = $self->get_options();
325             DEBUG sub{"Creating session with options:\n".Dump($options)};
326              
327             my %tied_session = ();
328             my $tieobj = undef;
329             ($session_id, $tieobj) = $self->tie_session(
330             \%tied_session,
331             $class,
332             $session_id,
333             $options,
334             );
335              
336             # set the session id in the tracker, however that works
337             $session_id ||= $tied_session{_session_id};
338             DEBUG "session_id is '$session_id'";
339              
340             # put the session id value in pnotes
341             $r->pnotes->{a2c}{session_id} = $session_id;
342              
343             $self->set_session_id($session_id);
344              
345             my %session_copy = (%tied_session);
346             $r->pnotes->{a2c}{session} = \%session_copy;
347             $r->pnotes->{a2c}{_tied_session} = \%tied_session;
348              
349             DEBUG "ref of real tied_session is '".\%tied_session."'";
350              
351             # set state detection handler as the first handler in
352             # the last phase that connection is open
353            
354             my @log_handlers = qw(
355             Apache2::Controller::Log::DetectAbortedConnection
356             Apache2::Controller::Log::SessionSave
357             );
358              
359             # we reset the whole PerlLogHandler stack to make sure session
360             # gets saved before the database commit happens... lame!
361             push @log_handlers,
362             grep defined,
363             @{ $r->get_handlers('PerlLogHandler') || [] };
364              
365             DEBUG sub {"reordering the PerlLogHandler stack:\n".Dump(\@log_handlers)};
366             $r->set_handlers(PerlLogHandler => \@log_handlers);
367              
368             DEBUG "returning OK";
369             return Apache2::Const::OK;
370             }
371              
372             =head2 tie_session
373              
374             Separate tying the session so it can be called again to set a new cookie
375             if the existing cookie is not found in the data store. Is this a good
376             idea? Not sure. Does it expose to being able to create infinite sessions?
377             Somehow a non-existent session has to be able to be cleared. This issue
378             cropped up when I put a `find -atime` in cron to clear out old session
379             files when using Apache::Session::File in /dev/shm. (Or when rebooting.)
380             We can't ask the user to clear their cookies every time this happens.
381             So, if tying fails saying "Object does not exist in the data store"
382             then it tries again with an undefined session id. Returns session id.
383              
384             =cut
385              
386             sub tie_session {
387             my ($self, $tied_session, $class, $session_id, $options, $recursion) = @_;
388             $recursion ||= 0;
389             $recursion++;
390             a2cx "Recursion limit exceeded" if $recursion > 5;
391              
392             my $tieobj = undef;
393              
394             eval {
395             tie %{$tied_session}, $class, $session_id, $options;
396             DEBUG 'Finished tie.';
397             $tieobj = tied(%{$tied_session});
398             DEBUG sub {
399             'Session is '.($tieobj ? 'tied' : 'not tied').", contents:"
400             .Dump($tied_session);
401             };
402             };
403             if (my $err = $EVAL_ERROR) {
404             if ($err =~ /Object does not exist in the data store/) {
405             ($session_id, $tieobj) = $self->tie_session(
406             $tied_session,
407             $class,
408             undef,
409             $options,
410             );
411             }
412             else {
413             a2cx $err;
414             }
415             }
416              
417             a2cx "no session_id" if !$tied_session->{_session_id};
418             a2cx "no tied obj" if !defined $tieobj;
419             a2cx "session_id mismatch"
420             if defined $session_id && $session_id ne $tied_session->{_session_id};
421              
422             return ($session_id, $tieobj);
423             }
424              
425             =head2 signature
426              
427             my $signature_string = $self->signature($session_id);
428              
429             Return the string which is the signature of the session id
430             plus the secret.
431              
432             Override this in a subclass if you want to use something other
433             than SHA224. See L.
434              
435             The secret is the value associated with the directive A2C_Session_Secret,
436             or the default if that directive was not used.
437              
438             See L,
439             L,
440             L.
441              
442             =cut
443              
444             sub signature {
445             my ($self, $sid) = @_;
446             a2cx "no sid param" if !defined $sid;
447              
448             my $secret = $self->{secret}
449             ||= $self->get_directive('A2C_Session_Secret')
450             || $DEFAULT_SESSION_SECRET;
451              
452             my $sig = sha224_base64( $sid . $secret );
453             DEBUG sub { Dump({
454             sid => $sid,
455             secret => $secret,
456             sig => $sig,
457             })};
458             return sha224_base64( $sid . $secret );
459             }
460              
461             =head2 get_options
462              
463             If you do not configure C<> or override the subroutine,
464             the default C method assumes default Apache2::Session::File.
465              
466             Default settings try to create C<>
467             and C<>. (uses C<tmpdir>>,
468             so it should work on Windoze?).
469              
470             If you want to do something differently, use your
471             own settings or overload C.
472              
473             =cut
474              
475             my %created_temp_dirs;
476              
477             sub get_options {
478             my ($self) = @_;
479              
480             my $opts = $self->get_directive('A2C_Session_Opts');
481            
482             if (!$opts) {
483             my $hostname = $self->{r}->hostname();
484             my $tmp = File::Spec->tmpdir();
485             my $dir = File::Spec->catfile($tmp, 'A2C', $hostname);
486             my $sess = File::Spec->catfile($dir, 'sess');
487             my $lock = File::Spec->catfile($dir, 'lock');
488              
489             if (!exists $created_temp_dirs{$hostname}) {
490             do { mkdir $_ || a2cx "Cannot create $_: $OS_ERROR" }
491             for grep !-d, $dir, $sess, $lock;
492             $created_temp_dirs{$hostname} = 1;
493             }
494              
495             $opts = {
496             Directory => $sess,
497             LockDirectory => $lock,
498             };
499             }
500              
501             DEBUG "returning session opts:\n".Dump($opts);
502             return $opts;
503             }
504              
505             =head1 DIRECTIVES
506              
507             Apache2 configuration directives. L
508              
509             =over 4
510              
511             =item A2C_Session_Class
512              
513             =item A2C_Session_Opts
514              
515             =back
516              
517             =head1 SEE ALSO
518              
519             L
520              
521             L
522              
523             L
524              
525             L
526              
527             =head1 THANKS
528              
529             Thanks to David Ihern for edumacating me about the
530             proper session cookie signature algorithm.
531              
532             =head1 AUTHOR
533              
534             Mark Hedges, C<< >>
535              
536             =head1 COPYRIGHT & LICENSE
537              
538             Copyright 2008-2010 Mark Hedges, all rights reserved.
539              
540             This program is free software; you can redistribute it and/or modify it
541             under the same terms as Perl itself.
542              
543             This software is provided as-is, with no warranty
544             and no guarantee of fitness
545             for any particular purpose.
546              
547             =cut
548              
549              
550             1;