3 package Catalyst::Plugin::Session;
6 with 'MooseX::Emulate::Class::Accessor::Fast';
8 use Catalyst::Exception ();
11 use Object::Signature ();
14 use namespace::clean -except => 'meta';
16 our $VERSION = '0.21';
18 my @session_data_accessors; # used in delete_session
20 __PACKAGE__->mk_accessors(
21 "_session_delete_reason",
22 @session_data_accessors = qw/
26 _extended_session_expires
31 _tried_loading_session_id
32 _tried_loading_session_data
33 _tried_loading_session_expires
34 _tried_loading_flash_data
42 $c->maybe::next::method(@_);
44 $c->check_session_plugin_requirements;
50 sub check_session_plugin_requirements {
53 unless ( $c->isa("Catalyst::Plugin::Session::State")
54 && $c->isa("Catalyst::Plugin::Session::Store") )
57 ( "The Session plugin requires both Session::State "
58 . "and Session::Store plugins to be used as well." );
61 Catalyst::Exception->throw($err);
68 my $cfg = ( $c->config->{session} ||= {} );
76 $c->maybe::next::method();
82 if ( $c->config->{session}{flash_to_stash}
84 and my $flash_data = $c->flash )
86 @{ $c->stash }{ keys %$flash_data } = values %$flash_data;
89 $c->maybe::next::method(@_);
92 sub finalize_headers {
95 # fix cookie before we send headers
96 $c->_save_session_expires;
98 return $c->maybe::next::method(@_);
104 # We have to finalize our session *before* $c->engine->finalize_xxx is called,
105 # because we do not want to send the HTTP response before the session is stored/committed to
106 # the session database (or whatever Session::Store you use).
107 $c->finalize_session;
109 return $c->maybe::next::method(@_);
112 sub finalize_session {
115 $c->maybe::next::method(@_);
117 $c->_save_session_id;
121 $c->_clear_session_instance_data;
124 sub _save_session_id {
127 # we already called set when allocating
128 # no need to tell the state plugins anything new
131 sub _save_session_expires {
134 if ( defined($c->_session_expires) ) {
135 my $expires = $c->session_expires; # force extension
137 my $sid = $c->sessionid;
138 $c->store_session_data( "expires:$sid" => $expires );
145 if ( my $session_data = $c->_session ) {
147 no warnings 'uninitialized';
148 if ( Object::Signature::signature($session_data) ne
149 $c->_session_data_sig )
151 $session_data->{__updated} = time();
152 my $sid = $c->sessionid;
153 $c->store_session_data( "session:$sid" => $session_data );
161 if ( my $flash_data = $c->_flash ) {
163 my $hashes = $c->_flash_key_hashes || {};
164 my $keep = $c->_flash_keep_keys || {};
165 foreach my $key ( keys %$hashes ) {
166 if ( !exists $keep->{$key} and Object::Signature::signature( \$flash_data->{$key} ) eq $hashes->{$key} ) {
167 delete $flash_data->{$key};
171 my $sid = $c->sessionid;
173 my $session_data = $c->_session;
175 $session_data->{__flash} = $flash_data;
178 delete $session_data->{__flash};
180 $c->_session($session_data);
185 sub _load_session_expires {
187 return $c->_session_expires if $c->_tried_loading_session_expires;
188 $c->_tried_loading_session_expires(1);
190 if ( my $sid = $c->sessionid ) {
191 my $expires = $c->get_session_data("expires:$sid") || 0;
193 if ( $expires >= time() ) {
194 $c->_session_expires( $expires );
197 $c->delete_session( "session expired" );
207 return $c->_session if $c->_tried_loading_session_data;
208 $c->_tried_loading_session_data(1);
210 if ( my $sid = $c->sessionid ) {
211 if ( $c->_load_session_expires ) { # > 0
213 my $session_data = $c->get_session_data("session:$sid") || return;
214 $c->_session($session_data);
216 no warnings 'uninitialized'; # ne __address
217 if ( $c->config->{session}{verify_address}
218 && $session_data->{__address} ne $c->request->address )
221 "Deleting session $sid due to address mismatch ("
222 . $session_data->{__address} . " != "
223 . $c->request->address . ")"
225 $c->delete_session("address mismatch");
229 $c->log->debug(qq/Restored session "$sid"/) if $c->debug;
230 $c->_session_data_sig( Object::Signature::signature($session_data) ) if $session_data;
231 $c->_expire_session_keys;
233 return $session_data;
242 return $c->_flash if $c->_tried_loading_flash_data;
243 $c->_tried_loading_flash_data(1);
245 if ( my $sid = $c->sessionid ) {
247 my $session_data = $c->session;
248 $c->_flash($session_data->{__flash});
250 if ( my $flash_data = $c->_flash )
252 $c->_flash_key_hashes({ map { $_ => Object::Signature::signature( \$flash_data->{$_} ) } keys %$flash_data });
261 sub _expire_session_keys {
262 my ( $c, $data ) = @_;
266 my $expire_times = ( $data || $c->_session || {} )->{__expire_keys} || {};
267 foreach my $key ( grep { $expire_times->{$_} < $now } keys %$expire_times ) {
268 delete $c->_session->{$key};
269 delete $expire_times->{$key};
273 sub _clear_session_instance_data {
275 $c->$_(undef) for @session_data_accessors;
276 $c->maybe::next::method(@_); # allow other plugins to hook in on this
280 my ( $c, $msg ) = @_;
282 $c->log->debug("Deleting session" . ( defined($msg) ? "($msg)" : '(no reason given)') ) if $c->debug;
284 # delete the session data
285 if ( my $sid = $c->sessionid ) {
286 $c->delete_session_data("${_}:${sid}") for qw/session expires flash/;
287 $c->delete_session_id($sid);
290 # reset the values in the context object
291 # see the BEGIN block
292 $c->_clear_session_instance_data;
294 $c->_session_delete_reason($msg);
297 sub session_delete_reason {
300 $c->session_is_valid; # check that it was loaded
302 $c->_session_delete_reason(@_);
305 sub session_expires {
308 if ( defined( my $expires = $c->_extended_session_expires ) ) {
310 } elsif ( defined( $expires = $c->_load_session_expires ) ) {
311 return $c->extend_session_expires( $expires );
317 sub extend_session_expires {
318 my ( $c, $expires ) = @_;
319 $c->_extended_session_expires( my $updated = $c->calculate_extended_session_expires( $expires ) );
320 $c->extend_session_id( $c->sessionid, $updated );
324 sub calculate_initial_session_expires {
326 return ( time() + $c->config->{session}{expires} );
329 sub calculate_extended_session_expires {
330 my ( $c, $prev ) = @_;
331 $c->calculate_initial_session_expires;
334 sub reset_session_expires {
335 my ( $c, $sid ) = @_;
337 my $exp = $c->calculate_initial_session_expires;
338 $c->_session_expires( $exp );
339 $c->_extended_session_expires( $exp );
346 return $c->_sessionid || $c->_load_sessionid;
349 sub _load_sessionid {
351 return if $c->_tried_loading_session_id;
352 $c->_tried_loading_session_id(1);
354 if ( defined( my $sid = $c->get_session_id ) ) {
355 if ( $c->validate_session_id($sid) ) {
356 # temporarily set the inner key, so that validation will work
357 $c->_sessionid($sid);
360 my $err = "Tried to set invalid session ID '$sid'";
361 $c->log->error($err);
362 Catalyst::Exception->throw($err);
369 sub session_is_valid {
372 # force a check for expiry, but also __address, etc
373 if ( $c->_load_session ) {
380 sub validate_session_id {
381 my ( $c, $sid ) = @_;
383 $sid and $sid =~ /^[a-f\d]+$/i;
389 $c->_session || $c->_load_session || do {
390 $c->create_session_id_if_needed;
391 $c->initialize_session_data;
396 my ( $c, @keys ) = @_;
397 my $href = $c->_flash_keep_keys || $c->_flash_keep_keys({});
398 (@{$href}{@keys}) = ((undef) x @keys);
403 $c->_flash || $c->_load_flash || do {
404 $c->create_session_id_if_needed;
412 my $items = @_ > 1 ? {@_} : $_[0];
413 croak('flash takes a hash or hashref') unless ref $items;
414 @{ $c->_flash }{ keys %$items } = values %$items;
428 #$c->delete_session_data("flash:" . $c->sessionid); # should this be in here? or delayed till finalization?
429 $c->_flash_key_hashes({});
430 $c->_flash_keep_keys({});
434 sub session_expire_key {
435 my ( $c, %keys ) = @_;
438 @{ $c->session->{__expire_keys} }{ keys %keys } =
439 map { $now + $_ } values %keys;
442 sub initialize_session_data {
453 $c->config->{session}{verify_address}
454 ? ( __address => $c->request->address )
461 sub generate_session_id {
464 my $digest = $c->_find_digest();
465 $digest->add( $c->session_hash_seed() );
466 return $digest->hexdigest;
469 sub create_session_id_if_needed {
471 $c->create_session_id unless $c->sessionid;
474 sub create_session_id {
477 my $sid = $c->generate_session_id;
479 $c->log->debug(qq/Created session "$sid"/) if $c->debug;
481 $c->_sessionid($sid);
482 $c->reset_session_expires;
483 $c->set_session_id($sid);
490 sub session_hash_seed {
493 return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
498 sub _find_digest () {
500 foreach my $alg (qw/SHA-1 SHA-256 MD5/) {
501 if ( eval { Digest->new($alg) } ) {
506 Catalyst::Exception->throw(
507 "Could not find a suitable Digest module. Please install "
508 . "Digest::SHA1, Digest::SHA, or Digest::MD5" )
512 return Digest->new($usable);
519 $c->maybe::next::method(),
522 ? ( [ "Session ID" => $c->sessionid ], [ Session => $c->session ], )
528 sub get_session_id { shift->maybe::next::method(@_) }
529 sub set_session_id { shift->maybe::next::method(@_) }
530 sub delete_session_id { shift->maybe::next::method(@_) }
531 sub extend_session_id { shift->maybe::next::method(@_) }
541 Catalyst::Plugin::Session - Generic Session plugin - ties together server side storage and client side state required to maintain session data.
545 # To get sessions to "just work", all you need to do is use these plugins:
549 Session::Store::FastMmap
550 Session::State::Cookie
553 # you can replace Store::FastMmap with Store::File - both have sensible
554 # default configurations (see their docs for details)
556 # more complicated backends are available for other scenarios (DBI storage,
560 # after you've loaded the plugins you can save session data
561 # For example, if you are writing a shopping cart, it could be implemented
564 sub add_item : Local {
565 my ( $self, $c ) = @_;
567 my $item_id = $c->req->param("item");
569 # $c->session is a hash ref, a bit like $c->stash
570 # the difference is that it' preserved across requests
572 push @{ $c->session->{items} }, $item_id;
574 $c->forward("MyView");
577 sub display_items : Local {
578 my ( $self, $c ) = @_;
580 # values in $c->session are restored
581 $c->stash->{items_to_display} =
582 [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
584 $c->forward("MyView");
589 The Session plugin is the base of two related parts of functionality required
590 for session management in web applications.
592 The first part, the State, is getting the browser to repeat back a session key,
593 so that the web application can identify the client and logically string
594 several requests together into a session.
596 The second part, the Store, deals with the actual storage of information about
597 the client. This data is stored so that the it may be revived for every request
598 made by the same client.
600 This plugin links the two pieces together.
602 =head1 RECOMENDED BACKENDS
606 =item Session::State::Cookie
608 The only really sane way to do state is using cookies.
610 =item Session::Store::File
612 A portable backend, based on Cache::File.
614 =item Session::Store::FastMmap
616 A fast and flexible backend, based on Cache::FastMmap.
626 An accessor for the session ID value.
630 Returns a hash reference that might contain unserialized values from previous
631 requests in the same session, and whose modified value will be saved for future
634 This method will automatically create a new session and session ID if none
637 =item session_expires
639 =item session_expires $reset
641 This method returns the time when the current session will expire, or 0 if
642 there is no current session. If there is a session and it already expired, it
643 will delete the session and return 0 as well.
645 If the C<$reset> parameter is true, and there is a session ID the expiry time
646 will be reset to the current time plus the time to live (see
647 L</CONFIGURATION>). This is used when creating a new session.
651 This is like Ruby on Rails' flash data structure. Think of it as a stash that
652 lasts for longer than one request, letting you redirect instead of forward.
654 The flash data will be cleaned up only on requests on which actually use
655 $c->flash (thus allowing multiple redirections), and the policy is to delete
656 all the keys which haven't changed since the flash data was loaded at the end
660 my ( $self, $c ) = @_;
662 $c->flash->{beans} = 10;
663 $c->response->redirect( $c->uri_for("foo") );
667 my ( $self, $c ) = @_;
669 my $value = $c->flash->{beans};
673 $c->response->redirect( $c->uri_for("bar") );
677 my ( $self, $c ) = @_;
679 if ( exists $c->flash->{beans} ) { # false
686 Zap all the keys in the flash regardless of their current state.
688 =item keep_flash @keys
690 If you want to keep a flash key for the next request too, even if it hasn't
691 changed, call C<keep_flash> and pass in the keys as arguments.
693 =item delete_session REASON
695 This method is used to invalidate a session. It takes an optional parameter
696 which will be saved in C<session_delete_reason> if provided.
698 NOTE: This method will B<also> delete your flash data.
700 =item session_delete_reason
702 This accessor contains a string with the reason a session was deleted. Possible
717 =item session_expire_key $key, $ttl
719 Mark a key to expire at a certain time (only useful when shorter than the
720 expiry time for the whole session).
724 __PACKAGE__->config->{session}{expires} = 1000000000000; # forever
728 $c->session_expire_key( __user => 3600 );
730 Will make the session data survive, but the user will still be logged out after
733 Note that these values are not auto extended.
737 =head1 INTERNAL METHODS
743 This method is extended to also make calls to
744 C<check_session_plugin_requirements> and C<setup_session>.
746 =item check_session_plugin_requirements
748 This method ensures that a State and a Store plugin are also in use by the
753 This method populates C<< $c->config->{session} >> with the default values
754 listed in L</CONFIGURATION>.
758 This method is extended.
760 Its only effect is if the (off by default) C<flash_to_stash> configuration
761 parameter is on - then it will copy the contents of the flash to the stash at
764 =item finalize_headers
766 This method is extended and will extend the expiry time before sending
771 This method is extended and will call finalize_session before the other
772 finalize_body methods run. Here we persist the session data if a session exists.
774 =item initialize_session_data
776 This method will initialize the internal structure of the session, and is
777 called by the C<session> method if appropriate.
779 =item create_session_id
781 Creates a new session ID using C<generate_session_id> if there is no session ID
784 =item validate_session_id SID
786 Make sure a session ID is of the right format.
788 This currently ensures that the session ID string is any amount of case
789 insensitive hexadecimal characters.
791 =item generate_session_id
793 This method will return a string that can be used as a session ID. It is
794 supposed to be a reasonably random string with enough bits to prevent
795 collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
796 MD5 or SHA-256, depending on the availability of these modules.
798 =item session_hash_seed
800 This method is actually rather internal to generate_session_id, but should be
801 overridable in case you want to provide more random data.
803 Currently it returns a concatenated string which contains:
809 =item * The current time
811 =item * One value from C<rand>.
813 =item * The stringified value of a newly allocated hash reference
815 =item * The stringified value of the Catalyst context object
819 in the hopes that those combined values are entropic enough for most uses. If
820 this is not the case you can replace C<session_hash_seed> with e.g.
822 sub session_hash_seed {
823 open my $fh, "<", "/dev/random";
824 read $fh, my $bytes, 20;
829 Or even more directly, replace C<generate_session_id>:
831 sub generate_session_id {
832 open my $fh, "<", "/dev/random";
833 read $fh, my $bytes, 20;
835 return unpack("H*", $bytes);
838 Also have a look at L<Crypt::Random> and the various openssl bindings - these
839 modules provide APIs for cryptographically secure random data.
841 =item finalize_session
843 Clean up the session during C<finalize>.
845 This clears the various accessors after saving to the store.
849 See L<Catalyst/dump_these> - ammends the session data structure to the list of
850 dumped objects if session ID is defined.
853 =item calculate_extended_session_expires
855 =item calculate_initial_session_expires
857 =item create_session_id_if_needed
859 =item delete_session_id
861 =item extend_session_expires
863 =item extend_session_id
867 =item reset_session_expires
869 =item session_is_valid
875 =head1 USING SESSIONS DURING PREPARE
877 The earliest point in time at which you may use the session data is after
878 L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
880 State plugins must set $c->session ID before C<prepare_action>, and during
881 C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
887 # don't touch $c->session yet!
889 $c->NEXT::prepare_action( @_ );
891 $c->session; # this is OK
892 $c->sessionid; # this is also OK
897 $c->config->{session} = {
901 All configuation parameters are provided in a hash reference under the
902 C<session> key in the configuration hash.
908 The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
913 When true, C<<$c->request->address>> will be checked at prepare time. If it is
914 not the same as the address that initiated the session, the session is deleted.
920 This option makes it easier to have actions behave the same whether they were
921 forwarded to or redirected to. On prepare time it copies the contents of
922 C<flash> (if any) to the stash.
928 The hash reference returned by C<< $c->session >> contains several keys which
929 are automatically set:
935 This key no longer exists. Use C<session_expires> instead.
939 The last time a session was saved to the store.
943 The time when the session was first created.
947 The value of C<< $c->request->address >> at the time the session was created.
948 This value is only populated if C<verify_address> is true in the configuration.
954 =head2 Round the Robin Proxies
956 C<verify_address> could make your site inaccessible to users who are behind
957 load balanced proxies. Some ISPs may give a different IP to each request by the
958 same client due to this type of proxying. If addresses are verified these
959 users' sessions cannot persist.
961 To let these users access your site you can either disable address verification
962 as a whole, or provide a checkbox in the login dialog that tells the server
963 that it's OK for the address of the client to change. When the server sees that
964 this box is checked it should delete the C<__address> special key from the
965 session hash when the hash is first created.
967 =head2 Race Conditions
969 In this day and age where cleaning detergents and Dutch football (not the
970 American kind) teams roam the plains in great numbers, requests may happen
971 simultaneously. This means that there is some risk of session data being
972 overwritten, like this:
978 request a starts, request b starts, with the same session ID
982 session data is loaded in request a
986 session data is loaded in request b
990 session data is changed in request a
994 request a finishes, session data is updated and written to store
998 request b finishes, session data is updated and written to store, overwriting
1003 If this is a concern in your application, a soon-to-be-developed locking
1004 solution is the only safe way to go. This will have a bigger overhead.
1006 For applications where any given user is only making one request at a time this
1007 plugin should be safe enough.
1015 Yuval Kogman, C<nothingmuch@woobling.org>
1019 Tomas Doran (t0m) C<bobtfish@bobtfish.net> (current maintainer)
1023 And countless other contributers from #catalyst. Thanks guys!
1025 =head1 COPYRIGHT & LICENSE
1027 Copyright (c) 2005 the aforementioned authors. All rights
1028 reserved. This program is free software; you can redistribute
1029 it and/or modify it under the same terms as Perl itself.