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