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