3 package Catalyst::Plugin::Session;
4 use base qw/Class::Accessor::Fast/;
10 use Catalyst::Exception ();
13 use Object::Signature ();
15 our $VERSION = "0.08";
17 my @session_data_accessors; # used in delete_session
19 __PACKAGE__->mk_accessors(
20 "_session_delete_reason",
21 @session_data_accessors = qw/
29 _tried_loading_session_id
30 _tried_loading_session_data
31 _tried_loading_session_expires
32 _tried_loading_flash_data
42 $c->check_session_plugin_requirements;
48 sub check_session_plugin_requirements {
51 unless ( $c->isa("Catalyst::Plugin::Session::State")
52 && $c->isa("Catalyst::Plugin::Session::Store") )
55 ( "The Session plugin requires both Session::State "
56 . "and Session::Store plugins to be used as well." );
59 Catalyst::Exception->throw($err);
66 my $cfg = ( $c->config->{session} ||= {} );
74 $c->NEXT::setup_session();
80 if ( $c->config->{session}{flash_to_stash}
82 and my $flash_data = $c->flash )
84 @{ $c->stash }{ keys %$flash_data } = values %$flash_data;
87 $c->NEXT::prepare_action(@_);
93 $c->_save_session_expires;
98 $c->NEXT::finalize(@_);
101 sub _save_session_id {
105 sub _save_session_expires {
108 if ( defined(my $expires = $c->_session_expires) ) {
109 my $sid = $c->sessionid;
110 $c->store_session_data( "expires:$sid" => $expires );
112 $c->_session_expires(undef);
113 $c->_tried_loading_session_expires(undef);
120 if ( my $session_data = $c->_session ) {
122 no warnings 'uninitialized';
123 if ( Object::Signature::signature($session_data) ne
124 $c->_session_data_sig )
126 $session_data->{__updated} = time();
127 my $sid = $c->sessionid;
128 $c->store_session_data( "session:$sid" => $session_data );
132 $c->_tried_loading_session_data(undef);
139 if ( my $flash_data = $c->_flash ) {
141 my $hashes = $c->_flash_key_hashes || {};
142 my $keep = $c->_flash_keep_keys || {};
143 foreach my $key ( keys %$hashes ) {
144 if ( !exists $keep->{$key} and Object::Signature::signature( \$flash_data->{$key} ) eq $hashes->{$key} ) {
145 delete $flash_data->{$key};
149 my $sid = $c->sessionid;
152 $c->store_session_data( "flash:$sid", $flash_data );
155 $c->delete_session_data("flash:$sid");
159 $c->_tried_loading_flash_data(undef);
163 sub _load_session_expires {
165 return $c->_session_expires if $c->_tried_loading_session_expires;
166 $c->_tried_loading_session_expires(1);
168 if ( my $sid = $c->sessionid ) {
169 my $expires = $c->get_session_data("expires:$sid") || 0;
171 if ( $expires >= time() ) {
172 return $c->extend_session_expires( $expires );
174 $c->delete_session( "session expired" );
184 return $c->_session if $c->_tried_loading_session_data;
185 $c->_tried_loading_session_data(1);
187 if ( my $sid = $c->sessionid ) {
188 if ( $c->session_expires ) { # > 0
190 my $session_data = $c->get_session_data("session:$sid") || return;
191 $c->_session($session_data);
193 no warnings 'uninitialized'; # ne __address
194 if ( $c->config->{session}{verify_address}
195 && $session_data->{__address} ne $c->request->address )
198 "Deleting session $sid due to address mismatch ("
199 . $session_data->{__address} . " != "
200 . $c->request->address . ")"
202 $c->delete_session("address mismatch");
206 $c->log->debug(qq/Restored session "$sid"/) if $c->debug;
207 $c->_session_data_sig( Object::Signature::signature($session_data) ) if $session_data;
208 $c->_expire_session_keys;
210 return $session_data;
219 return $c->_flash if $c->_tried_loading_flash_data;
220 $c->_tried_loading_flash_data(1);
222 if ( my $sid = $c->sessionid ) {
223 if ( my $flash_data = $c->_flash
224 || $c->_flash( $c->get_session_data("flash:$sid") ) )
226 $c->_flash_key_hashes({ map { $_ => Object::Signature::signature( \$flash_data->{$_} ) } keys %$flash_data });
235 sub _expire_session_keys {
236 my ( $c, $data ) = @_;
240 my $expire_times = ( $data || $c->_session || {} )->{__expire_keys} || {};
241 foreach my $key ( grep { $expire_times->{$_} < $now } keys %$expire_times ) {
242 delete $c->_session->{$key};
243 delete $expire_times->{$key};
248 my ( $c, $msg ) = @_;
250 $c->log->debug("Deleting session") if $c->debug;
252 # delete the session data
253 if ( my $sid = $c->sessionid ) {
254 $c->delete_session_data("${_}:${sid}") for qw/session expires flash/;
255 $c->delete_session_id($sid);
258 # reset the values in the context object
259 # see the BEGIN block
260 $c->$_(undef) for @session_data_accessors;
262 $c->_session_delete_reason($msg);
265 sub session_delete_reason {
268 $c->session_is_valid; # check that it was loaded
270 $c->_session_delete_reason(@_);
273 sub session_expires {
276 if ( defined( my $expires = $c->_session_expires ) ) {
278 } elsif ( defined( $expires = $c->_load_session_expires ) ) {
279 $c->_session_expires($expires);
286 sub extend_session_expires {
287 my ( $c, $expires ) = @_;
288 $c->_session_expires( my $updated = $c->calculate_extended_session_expires( $expires ) );
289 $c->extend_session_id( $c->sessionid, $updated );
293 sub calculate_initial_session_expires {
295 return ( time() + $c->config->{session}{expires} );
298 sub calculate_extended_session_expires {
299 my ( $c, $prev ) = @_;
300 $c->calculate_initial_session_expires;
303 sub reset_session_expires {
304 my ( $c, $sid ) = @_;
305 $c->_session_expires( my $exp = $c->calculate_initial_session_expires );
312 return $c->_sessionid || $c->_load_sessionid;
315 sub _load_sessionid {
317 return if $c->_tried_loading_session_id;
318 $c->_tried_loading_session_id(1);
320 if ( defined( my $sid = $c->get_session_id ) ) {
321 if ( $c->validate_session_id($sid) ) {
322 $c->_sessionid( $sid );
325 my $err = "Tried to set invalid session ID '$sid'";
326 $c->log->error($err);
327 Catalyst::Exception->throw($err);
334 sub session_is_valid {
337 # force a check for expiry, but also __address, etc
338 if ( $c->_load_session ) {
345 sub validate_session_id {
346 my ( $c, $sid ) = @_;
348 $sid and $sid =~ /^[a-f\d]+$/i;
354 $c->_session || $c->_load_session || do {
355 $c->create_session_id_if_needed;
356 $c->initialize_session_data;
361 my ( $c, @keys ) = @_;
362 my $href = $c->_flash_keep_keys || $c->_flash_keep_keys({});
363 (@{$href}{@keys}) = ((undef) x @keys);
368 $c->_flash || $c->_load_flash || do {
369 $c->create_session_id_if_needed;
374 sub session_expire_key {
375 my ( $c, %keys ) = @_;
378 @{ $c->session->{__expire_keys} }{ keys %keys } =
379 map { $now + $_ } values %keys;
382 sub initialize_session_data {
393 $c->config->{session}{verify_address}
394 ? ( __address => $c->request->address )
401 sub generate_session_id {
404 my $digest = $c->_find_digest();
405 $digest->add( $c->session_hash_seed() );
406 return $digest->hexdigest;
409 sub create_session_id_if_needed {
411 $c->create_session_id unless $c->sessionid;
414 sub create_session_id {
417 my $sid = $c->generate_session_id;
419 $c->log->debug(qq/Created session "$sid"/) if $c->debug;
421 $c->_sessionid($sid);
422 $c->reset_session_expires;
423 $c->set_session_id($sid);
430 sub session_hash_seed {
433 return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
438 sub _find_digest () {
440 foreach my $alg (qw/SHA-1 SHA-256 MD5/) {
441 if ( eval { Digest->new($alg) } ) {
446 Catalyst::Exception->throw(
447 "Could not find a suitable Digest module. Please install "
448 . "Digest::SHA1, Digest::SHA, or Digest::MD5" )
452 return Digest->new($usable);
459 $c->NEXT::dump_these(),
462 ? ( [ "Session ID" => $c->sessionid ], [ Session => $c->session ], )
468 sub get_session_id { shift->NEXT::get_session_id(@_) }
469 sub set_session_id { shift->NEXT::set_session_id(@_) }
470 sub delete_session_id { shift->NEXT::delete_session_id(@_) }
471 sub extend_session_id { shift->NEXT::extend_session_id(@_) }
481 Catalyst::Plugin::Session - Generic Session plugin - ties together server side
482 storage and client side state required to maintain session data.
486 # To get sessions to "just work", all you need to do is use these plugins:
490 Session::Store::FastMmap
491 Session::State::Cookie
494 # you can replace Store::FastMmap with Store::File - both have sensible
495 # default configurations (see their docs for details)
497 # more complicated backends are available for other scenarios (DBI storage,
501 # after you've loaded the plugins you can save session data
502 # For example, if you are writing a shopping cart, it could be implemented
505 sub add_item : Local {
506 my ( $self, $c ) = @_;
508 my $item_id = $c->req->param("item");
510 # $c->session is a hash ref, a bit like $c->stash
511 # the difference is that it' preserved across requests
513 push @{ $c->session->{items} }, $item_id;
515 $c->forward("MyView");
518 sub display_items : Local {
519 my ( $self, $c ) = @_;
521 # values in $c->session are restored
522 $c->stash->{items_to_display} =
523 [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
525 $c->forward("MyView");
530 The Session plugin is the base of two related parts of functionality required
531 for session management in web applications.
533 The first part, the State, is getting the browser to repeat back a session key,
534 so that the web application can identify the client and logically string
535 several requests together into a session.
537 The second part, the Store, deals with the actual storage of information about
538 the client. This data is stored so that the it may be revived for every request
539 made by the same client.
541 This plugin links the two pieces together.
543 =head1 RECCOMENDED BACKENDS
547 =item Session::State::Cookie
549 The only really sane way to do state is using cookies.
551 =item Session::Store::File
553 A portable backend, based on Cache::File.
555 =item Session::Store::FastMmap
557 A fast and flexible backend, based on Cache::FastMmap.
567 An accessor for the session ID value.
571 Returns a hash reference that might contain unserialized values from previous
572 requests in the same session, and whose modified value will be saved for future
575 This method will automatically create a new session and session ID if none
578 =item session_expires
580 =item session_expires $reset
582 This method returns the time when the current session will expire, or 0 if
583 there is no current session. If there is a session and it already expired, it
584 will delete the session and return 0 as well.
586 If the C<$reset> parameter is true, and there is a session ID the expiry time
587 will be reset to the current time plus the time to live (see
588 L</CONFIGURATION>). This is used when creating a new session.
592 This is like Ruby on Rails' flash data structure. Think of it as a stash that
593 lasts for longer than one request, letting you redirect instead of forward.
595 The flash data will be cleaned up only on requests on which actually use
596 $c->flash (thus allowing multiple redirections), and the policy is to delete
597 all the keys which haven't changed since the flash data was loaded at the end
601 my ( $self, $c ) = @_;
603 $c->flash->{beans} = 10;
604 $c->response->redirect( $c->uri_for("foo") );
608 my ( $self, $c ) = @_;
610 my $value = $c->flash->{beans};
614 $c->response->redirect( $c->uri_for("bar") );
618 my ( $self, $c ) = @_;
620 if ( exists $c->flash->{beans} ) { # false
625 =item keep_flash @keys
627 If you wawnt to keep a flash key for the next request too, even if it hasn't
628 changed, call C<keep_flash> and pass in the keys as arguments.
630 =item delete_session REASON
632 This method is used to invalidate a session. It takes an optional parameter
633 which will be saved in C<session_delete_reason> if provided.
635 =item session_delete_reason
637 This accessor contains a string with the reason a session was deleted. Possible
652 =item session_expire_key $key, $ttl
654 Mark a key to expire at a certain time (only useful when shorter than the
655 expiry time for the whole session).
659 __PACKAGE__->config->{session}{expires} = 1000000000000; # forever
663 $c->session_expire_key( __user => 3600 );
665 Will make the session data survive, but the user will still be logged out after
668 Note that these values are not auto extended.
672 =head1 INTERNAL METHODS
678 This method is extended to also make calls to
679 C<check_session_plugin_requirements> and C<setup_session>.
681 =item check_session_plugin_requirements
683 This method ensures that a State and a Store plugin are also in use by the
688 This method populates C<< $c->config->{session} >> with the default values
689 listed in L</CONFIGURATION>.
693 This methoid is extended.
695 It's only effect is if the (off by default) C<flash_to_stash> configuration
696 parameter is on - then it will copy the contents of the flash to the stash at
701 This method is extended and will extend the expiry time, as well as persist the
702 session data if a session exists.
704 =item initialize_session_data
706 This method will initialize the internal structure of the session, and is
707 called by the C<session> method if appropriate.
709 =item create_session_id
711 Creates a new session id using C<generate_session_id> if there is no session ID
714 =item validate_session_id SID
716 Make sure a session ID is of the right format.
718 This currently ensures that the session ID string is any amount of case
719 insensitive hexadecimal characters.
721 =item generate_session_id
723 This method will return a string that can be used as a session ID. It is
724 supposed to be a reasonably random string with enough bits to prevent
725 collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
726 MD5 or SHA-256, depending on the availibility of these modules.
728 =item session_hash_seed
730 This method is actually rather internal to generate_session_id, but should be
731 overridable in case you want to provide more random data.
733 Currently it returns a concatenated string which contains:
747 One value from C<rand>.
751 The stringified value of a newly allocated hash reference
755 The stringified value of the Catalyst context object
759 In the hopes that those combined values are entropic enough for most uses. If
760 this is not the case you can replace C<session_hash_seed> with e.g.
762 sub session_hash_seed {
763 open my $fh, "<", "/dev/random";
764 read $fh, my $bytes, 20;
769 Or even more directly, replace C<generate_session_id>:
771 sub generate_session_id {
772 open my $fh, "<", "/dev/random";
773 read $fh, my $bytes, 20;
775 return unpack("H*", $bytes);
778 Also have a look at L<Crypt::Random> and the various openssl bindings - these
779 modules provide APIs for cryptographically secure random data.
783 See L<Catalyst/dump_these> - ammends the session data structure to the list of
784 dumped objects if session ID is defined.
788 =head1 USING SESSIONS DURING PREPARE
790 The earliest point in time at which you may use the session data is after
791 L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
793 State plugins must set $c->session ID before C<prepare_action>, and during
794 C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
800 # don't touch $c->session yet!
802 $c->NEXT::prepare_action( @_ );
804 $c->session; # this is OK
805 $c->sessionid; # this is also OK
810 $c->config->{session} = {
814 All configuation parameters are provided in a hash reference under the
815 C<session> key in the configuration hash.
821 The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
826 When true, C<<$c->request->address>> will be checked at prepare time. If it is
827 not the same as the address that initiated the session, the session is deleted.
831 This option makes it easier to have actions behave the same whether they were
832 forwarded to or redirected to. On prepare time it copies the contents of
833 C<flash> (if any) to the stash.
839 The hash reference returned by C<< $c->session >> contains several keys which
840 are automatically set:
846 This key no longer exists. Use C<session_expires> instead.
850 The last time a session was saved to the store.
854 The time when the session was first created.
858 The value of C<< $c->request->address >> at the time the session was created.
859 This value is only populated if C<verify_address> is true in the configuration.
865 =head2 Round the Robin Proxies
867 C<verify_address> could make your site inaccessible to users who are behind
868 load balanced proxies. Some ISPs may give a different IP to each request by the
869 same client due to this type of proxying. If addresses are verified these
870 users' sessions cannot persist.
872 To let these users access your site you can either disable address verification
873 as a whole, or provide a checkbox in the login dialog that tells the server
874 that it's OK for the address of the client to change. When the server sees that
875 this box is checked it should delete the C<__address> sepcial key from the
876 session hash when the hash is first created.
878 =head2 Race Conditions
880 In this day and age where cleaning detergents and dutch football (not the
881 american kind) teams roam the plains in great numbers, requests may happen
882 simultaneously. This means that there is some risk of session data being
883 overwritten, like this:
889 request a starts, request b starts, with the same session id
893 session data is loaded in request a
897 session data is loaded in request b
901 session data is changed in request a
905 request a finishes, session data is updated and written to store
909 request b finishes, session data is updated and written to store, overwriting
914 If this is a concern in your application, a soon to be developed locking
915 solution is the only safe way to go. This will have a bigger overhead.
917 For applications where any given user is only making one request at a time this
918 plugin should be safe enough.
926 =item Christian Hansen
928 =item Yuval Kogman, C<nothingmuch@woobling.org> (current maintainer)
930 =item Sebastian Riedel
934 And countless other contributers from #catalyst. Thanks guys!
936 =head1 COPYRIGHT & LICENSE
938 Copyright (c) 2005 the aforementioned authors. All rights
939 reserved. This program is free software; you can redistribute
940 it and/or modify it under the same terms as Perl itself.