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