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