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