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