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