7e2ac415024e16be56488839254333114c8e0d7c
[catagits/Catalyst-Plugin-Session.git] / lib / Catalyst / Plugin / Session.pm
1 #!/usr/bin/perl
2
3 package Catalyst::Plugin::Session;
4 use base qw/Class::Accessor::Fast/;
5
6 use strict;
7 use warnings;
8
9 use NEXT;
10 use Catalyst::Exception ();
11 use Digest              ();
12 use overload            ();
13
14 our $VERSION = "0.02";
15
16 BEGIN {
17     __PACKAGE__->mk_accessors(qw/_sessionid _session _session_delete_reason/);
18 }
19
20 sub setup {
21     my $c = shift;
22
23     $c->NEXT::setup(@_);
24
25     $c->check_session_plugin_requirements;
26     $c->setup_session;
27
28     return $c;
29 }
30
31 sub check_session_plugin_requirements {
32     my $c = shift;
33
34     unless ( $c->isa("Catalyst::Plugin::Session::State")
35         && $c->isa("Catalyst::Plugin::Session::Store") )
36     {
37         my $err =
38           (     "The Session plugin requires both Session::State "
39               . "and Session::Store plugins to be used as well." );
40
41         $c->log->fatal($err);
42         Catalyst::Exception->throw($err);
43     }
44 }
45
46 sub setup_session {
47     my $c = shift;
48
49     my $cfg = ( $c->config->{session} ||= {} );
50
51     %$cfg = (
52         expires        => 7200,
53         verify_address => 1,
54         %$cfg,
55     );
56
57     $c->NEXT::setup_session();
58 }
59
60 sub finalize {
61     my $c = shift;
62
63     if ( my $session_data = $c->_session ) {
64
65         # all sessions are extended at the end of the request
66         my $now = time;
67         @{ $session_data }{qw/__updated __expires/} =
68           ( $now, $c->config->{session}{expires} + $now );
69         delete @{ $session_data->{__flash} }{ @{ delete $session_data->{__flash_stale_keys} || [] } };
70         $c->store_session_data( $c->sessionid, $session_data );
71     }
72
73     $c->NEXT::finalize(@_);
74 }
75
76 sub _load_session {
77     my $c = shift;
78
79     if ( my $sid = $c->_sessionid ) {
80                 no warnings 'uninitialized'; # ne __address
81
82         my $session_data = $c->_session || $c->_session( $c->get_session_data($sid) );
83         if ( !$session_data or $session_data->{__expires} < time ) {
84
85             # session expired
86             $c->log->debug("Deleting session $sid (expired)") if $c->debug;
87             $c->delete_session("session expired");
88         }
89         elsif ($c->config->{session}{verify_address}
90             && $session_data->{__address} ne $c->request->address )
91         {
92             $c->log->warn(
93                     "Deleting session $sid due to address mismatch ("
94                   . $session_data->{__address} . " != "
95                   . $c->request->address . ")",
96             );
97             $c->delete_session("address mismatch");
98         }
99         else {
100             $c->log->debug(qq/Restored session "$sid"/) if $c->debug;
101         }
102        
103         $c->_expire_ession_keys;
104         $session_data->{__flash_stale_keys} = [ keys %{ $session_data->{__flash} } ];
105
106         return $session_data;
107     }
108
109     return undef;
110 }
111
112 sub _expire_ession_keys {
113     my ( $c, $data ) = @_;
114
115     my $now = time;
116
117     my $expiry = ($data || $c->_session || {})->{__expire_keys} || {};
118     foreach my $key (grep { $expiry->{$_} < $now } keys %$expiry ) {
119         delete $c->_session->{$key};
120         delete $expiry->{$key};
121     }
122 }
123
124 sub delete_session {
125     my ( $c, $msg ) = @_;
126
127     # delete the session data
128     my $sid = $c->_sessionid || return;
129     $c->delete_session_data($sid);
130
131     # reset the values in the context object
132     $c->_session(undef);
133     $c->_sessionid(undef);
134     $c->_session_delete_reason($msg);
135 }
136
137 sub session_delete_reason {
138     my $c = shift;
139
140     $c->_load_session if ( $c->_sessionid && !$c->_session ); # must verify session data
141
142     $c->_session_delete_reason( @_ );
143 }
144
145 sub sessionid {
146         my $c = shift;
147     
148         if ( @_ ) {
149                 if ( $c->validate_session_id( my $sid = shift ) ) {
150                         $c->_sessionid( $sid );
151             return unless defined wantarray;
152                 } else {
153                         my $err = "Tried to set invalid session ID '$sid'";
154                         $c->log->error( $err );
155                         Catalyst::Exception->throw( $err );
156                 }
157         }
158     
159     $c->_load_session if ( $c->_sessionid && !$c->_session ); # must verify session data
160
161         return $c->_sessionid;
162 }
163
164 sub validate_session_id {
165         my ( $c, $sid ) = @_;
166
167         $sid =~ /^[a-f\d]+$/i;
168 }
169
170 sub session {
171     my $c = shift;
172
173     $c->_session || $c->_load_session || do {
174         my $sid = $c->generate_session_id;
175         $c->sessionid($sid);
176
177         $c->log->debug(qq/Created session "$sid"/) if $c->debug;
178
179         $c->initialize_session_data;
180         };
181 }
182
183 sub flash {
184     my $c = shift;
185     return $c->session->{__flash} ||= {};
186 }
187
188 sub session_expire_key {
189     my ( $c, %keys ) = @_;
190
191     my $now = time;
192     @{ $c->session->{__expire_keys} }{keys %keys} = map { $now + $_ } values %keys;
193 }
194
195 sub initialize_session_data {
196     my $c = shift;
197
198     my $now = time;
199
200     return $c->_session({
201         __created => $now,
202         __updated => $now,
203         __expires => $now + $c->config->{session}{expires},
204
205         (
206             $c->config->{session}{verify_address}
207             ? ( __address => $c->request->address )
208             : ()
209         ),
210     });
211 }
212
213 sub generate_session_id {
214     my $c = shift;
215
216     my $digest = $c->_find_digest();
217     $digest->add( $c->session_hash_seed() );
218     return $digest->hexdigest;
219 }
220
221 my $counter;
222
223 sub session_hash_seed {
224     my $c = shift;
225
226     return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
227 }
228
229 my $usable;
230
231 sub _find_digest () {
232     unless ($usable) {
233         foreach my $alg (qw/SHA-1 MD5 SHA-256/) {
234             eval {
235                 my $obj = Digest->new($alg);
236                 $usable = $alg;
237                 return $obj;
238             };
239         }
240         $usable
241           or Catalyst::Exception->throw(
242                 "Could not find a suitable Digest module. Please install "
243               . "Digest::SHA1, Digest::SHA, or Digest::MD5" );
244     }
245
246     return Digest->new($usable);
247 }
248
249 sub dump_these {
250     my $c = shift;
251
252     (
253         $c->NEXT::dump_these(),
254
255         $c->sessionid
256         ? ( [ "Session ID" => $c->sessionid ], [ Session => $c->session ], )
257         : ()
258     );
259 }
260
261 __PACKAGE__;
262
263 __END__
264
265 =pod
266
267 =head1 NAME
268
269 Catalyst::Plugin::Session - Generic Session plugin - ties together server side
270 storage and client side state required to maintain session data.
271
272 =head1 SYNOPSIS
273
274     # To get sessions to "just work", all you need to do is use these plugins:
275
276     use Catalyst qw/
277       Session
278       Session::Store::FastMmap
279       Session::State::Cookie
280       /;
281
282         # you can replace Store::FastMmap with Store::File - both have sensible
283         # default configurations (see their docs for details)
284
285         # more complicated backends are available for other scenarios (DBI storage,
286         # etc)
287
288
289     # after you've loaded the plugins you can save session data
290     # For example, if you are writing a shopping cart, it could be implemented
291     # like this:
292
293     sub add_item : Local {
294         my ( $self, $c ) = @_;
295
296         my $item_id = $c->req->param("item");
297
298         # $c->session is a hash ref, a bit like $c->stash
299         # the difference is that it' preserved across requests
300
301         push @{ $c->session->{items} }, $item_id;
302
303         $c->forward("MyView");
304     }
305
306     sub display_items : Local {
307         my ( $self, $c ) = @_;
308
309         # values in $c->session are restored
310         $c->stash->{items_to_display} =
311           [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
312
313         $c->forward("MyView");
314     }
315
316 =head1 DESCRIPTION
317
318 The Session plugin is the base of two related parts of functionality required
319 for session management in web applications.
320
321 The first part, the State, is getting the browser to repeat back a session key,
322 so that the web application can identify the client and logically string
323 several requests together into a session.
324
325 The second part, the Store, deals with the actual storage of information about
326 the client. This data is stored so that the it may be revived for every request
327 made by the same client.
328
329 This plugin links the two pieces together.
330
331 =head1 RECCOMENDED BACKENDS
332
333 =over 4
334
335 =item Session::State::Cookie
336
337 The only really sane way to do state is using cookies.
338
339 =item Session::Store::File
340
341 A portable backend, based on Cache::File.
342
343 =item Session::Store::FastMmap
344
345 A fast and flexible backend, based on Cache::FastMmap.
346
347 =back
348
349 =head1 METHODS
350
351 =over 4
352
353 =item sessionid
354
355 An accessor for the session ID value.
356
357 =item session
358
359 Returns a hash reference that might contain unserialized values from previous
360 requests in the same session, and whose modified value will be saved for future
361 requests.
362
363 This method will automatically create a new session and session ID if none
364 exists.
365
366 =item flash
367
368 This is like Ruby on Rails' flash data structure. Think of it as a stash that
369 lasts a single redirect, not only a forward.
370
371     sub moose : Local {
372         my ( $self, $c ) = @_;
373
374         $c->flash->{beans} = 10;
375         $c->response->redirect( $c->uri_for("foo") );
376     }
377
378     sub foo : Local {
379         my ( $self, $c ) = @_;
380
381         my $value = $c->flash->{beans};
382
383         # ...
384
385         $c->response->redirect( $c->uri_for("bar") );
386     }
387
388     sub bar : Local {
389         my ( $self, $c ) = @_;
390
391         if ( exists $c->flash->{beans} ) { # false
392         
393         }
394     }
395
396 =item session_delete_reason
397
398 This accessor contains a string with the reason a session was deleted. Possible
399 values include:
400
401 =over 4
402
403 =item *
404
405 C<address mismatch>
406
407 =item *
408
409 C<session expired>
410
411 =back
412
413 =item session_expire_key $key, $ttl
414
415 Mark a key to expire at a certain time (only useful when shorter than the
416 expiry time for the whole session).
417
418 For example:
419
420     __PACKAGE__->config->{session}{expires} = 1000000000000; # forever
421
422     # later
423
424     $c->session_expire_key( __user => 3600 );
425
426 Will make the session data survive, but the user will still be logged out after
427 an hour.
428
429 Note that these values are not auto extended.
430
431 =back
432
433 =item INTERNAL METHODS
434
435 =over 4
436
437 =item setup
438
439 This method is extended to also make calls to
440 C<check_session_plugin_requirements> and C<setup_session>.
441
442 =item check_session_plugin_requirements
443
444 This method ensures that a State and a Store plugin are also in use by the
445 application.
446
447 =item setup_session
448
449 This method populates C<< $c->config->{session} >> with the default values
450 listed in L</CONFIGURATION>.
451
452 =item prepare_action
453
454 This methoid is extended, and will restore session data and check it for
455 validity if a session id is defined. It assumes that the State plugin will
456 populate the C<sessionid> key beforehand.
457
458 =item finalize
459
460 This method is extended and will extend the expiry time, as well as persist the
461 session data if a session exists.
462
463 =item delete_session REASON
464
465 This method is used to invalidate a session. It takes an optional parameter
466 which will be saved in C<session_delete_reason> if provided.
467
468 =item initialize_session_data
469
470 This method will initialize the internal structure of the session, and is
471 called by the C<session> method if appropriate.
472
473 =item generate_session_id
474
475 This method will return a string that can be used as a session ID. It is
476 supposed to be a reasonably random string with enough bits to prevent
477 collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
478 MD5 or SHA-256, depending on the availibility of these modules.
479
480 =item session_hash_seed
481
482 This method is actually rather internal to generate_session_id, but should be
483 overridable in case you want to provide more random data.
484
485 Currently it returns a concatenated string which contains:
486
487 =item validate_session_id SID
488
489 Make sure a session ID is of the right format.
490
491 This currently ensures that the session ID string is any amount of case
492 insensitive hexadecimal characters.
493
494 =over 4
495
496 =item *
497
498 A counter
499
500 =item *
501
502 The current time
503
504 =item *
505
506 One value from C<rand>.
507
508 =item *
509
510 The stringified value of a newly allocated hash reference
511
512 =item *
513
514 The stringified value of the Catalyst context object
515
516 =back
517
518 In the hopes that those combined values are entropic enough for most uses. If
519 this is not the case you can replace C<session_hash_seed> with e.g.
520
521     sub session_hash_seed {
522         open my $fh, "<", "/dev/random";
523         read $fh, my $bytes, 20;
524         close $fh;
525         return $bytes;
526     }
527
528 Or even more directly, replace C<generate_session_id>:
529
530     sub generate_session_id {
531         open my $fh, "<", "/dev/random";
532         read $fh, my $bytes, 20;
533         close $fh;
534         return unpack("H*", $bytes);
535     }
536
537 Also have a look at L<Crypt::Random> and the various openssl bindings - these
538 modules provide APIs for cryptographically secure random data.
539
540 =item dump_these
541
542 See L<Catalyst/dump_these> - ammends the session data structure to the list of
543 dumped objects if session ID is defined.
544
545 =back
546
547 =head1 USING SESSIONS DURING PREPARE
548
549 The earliest point in time at which you may use the session data is after
550 L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
551
552 State plugins must set $c->session ID before C<prepare_action>, and during
553 C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
554 the store.
555
556         sub prepare_action {
557                 my $c = shift;
558
559                 # don't touch $c->session yet!
560
561                 $c->NEXT::prepare_action( @_ );
562
563                 $c->session;  # this is OK
564                 $c->sessionid; # this is also OK
565         }
566
567 =head1 CONFIGURATION
568
569     $c->config->{session} = {
570         expires => 1234,
571     };
572
573 All configuation parameters are provided in a hash reference under the
574 C<session> key in the configuration hash.
575
576 =over 4
577
578 =item expires
579
580 The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
581 hours).
582
583 =item verify_address
584
585 When true, C<<$c->request->address>> will be checked at prepare time. If it is
586 not the same as the address that initiated the session, the session is deleted.
587
588 =back
589
590 =head1 SPECIAL KEYS
591
592 The hash reference returned by C<< $c->session >> contains several keys which
593 are automatically set:
594
595 =over 4
596
597 =item __expires
598
599 A timestamp whose value is the last second when the session is still valid. If
600 a session is restored, and __expires is less than the current time, the session
601 is deleted.
602
603 =item __updated
604
605 The last time a session was saved. This is the value of
606 C<< $c->session->{__expires} - $c->config->session->{expires} >>.
607
608 =item __created
609
610 The time when the session was first created.
611
612 =item __address
613
614 The value of C<< $c->request->address >> at the time the session was created.
615 This value is only populated if C<verify_address> is true in the configuration.
616
617 =back
618
619 =head1 CAVEATS
620
621 =head2 Round the Robin Proxies
622
623 C<verify_address> could make your site inaccessible to users who are behind
624 load balanced proxies. Some ISPs may give a different IP to each request by the
625 same client due to this type of proxying. If addresses are verified these
626 users' sessions cannot persist.
627
628 To let these users access your site you can either disable address verification
629 as a whole, or provide a checkbox in the login dialog that tells the server
630 that it's OK for the address of the client to change. When the server sees that
631 this box is checked it should delete the C<__address> sepcial key from the
632 session hash when the hash is first created.
633
634 =head2 Race Conditions
635
636 In this day and age where cleaning detergents and dutch football (not the
637 american kind) teams roam the plains in great numbers, requests may happen
638 simultaneously. This means that there is some risk of session data being
639 overwritten, like this:
640
641 =over 4
642
643 =item 1.
644
645 request a starts, request b starts, with the same session id
646
647 =item 2.
648
649 session data is loaded in request a
650
651 =item 3.
652
653 session data is loaded in request b
654
655 =item 4.
656
657 session data is changed in request a
658
659 =item 5.
660
661 request a finishes, session data is updated and written to store
662
663 =item 6.
664
665 request b finishes, session data is updated and written to store, overwriting
666 changes by request a
667
668 =back
669
670 If this is a concern in your application, a soon to be developed locking
671 solution is the only safe way to go. This will have a bigger overhead.
672
673 For applications where any given user is only making one request at a time this
674 plugin should be safe enough.
675
676 =head1 AUTHORS
677
678 =over 4
679
680 =item Andy Grundman
681
682 =item Christian Hansen
683
684 =item Yuval Kogman, C<nothingmuch@woobling.org> (current maintainer)
685
686 =item Sebastian Riedel
687
688 =back
689
690 And countless other contributers from #catalyst. Thanks guys!
691
692 =head1 COPYRIGHT & LICENSE
693
694         Copyright (c) 2005 the aforementioned authors. All rights
695         reserved. This program is free software; you can redistribute
696         it and/or modify it under the same terms as Perl itself.
697
698 =cut
699
700