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