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