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