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