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