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