3 package Catalyst::Plugin::Session;
4 use base qw/Class::Accessor::Fast/;
10 use Catalyst::Exception ();
13 use Object::Signature ();
15 our $VERSION = "0.05";
17 my @session_data_accessors; # used in delete_session
19 __PACKAGE__->mk_accessors(
20 "_session_delete_reason",
21 @session_data_accessors = qw/
37 $c->check_session_plugin_requirements;
43 sub check_session_plugin_requirements {
46 unless ( $c->isa("Catalyst::Plugin::Session::State")
47 && $c->isa("Catalyst::Plugin::Session::Store") )
50 ( "The Session plugin requires both Session::State "
51 . "and Session::Store plugins to be used as well." );
54 Catalyst::Exception->throw($err);
61 my $cfg = ( $c->config->{session} ||= {} );
69 $c->NEXT::setup_session();
75 if ( $c->config->{session}{flash_to_stash}
77 and my $flash_data = $c->flash )
79 @{ $c->stash }{ keys %$flash_data } = values %$flash_data;
82 $c->NEXT::prepare_action(@_);
91 $c->NEXT::finalize(@_);
97 if ( my $sid = $c->_sessionid ) {
99 # all sessions are extended at the end of the request
102 if ( my $expires = $c->session_expires ) {
103 $c->store_session_data( "expires:$sid" => $expires );
106 if ( my $session_data = $c->_session ) {
108 no warnings 'uninitialized';
109 if ( Object::Signature::signature($session_data) ne
110 $c->_session_data_sig )
112 $session_data->{__updated} = $now;
113 $c->store_session_data( "session:$sid" => $session_data );
122 if ( my $sid = $c->_sessionid ) {
123 if ( my $flash_data = $c->_flash ) {
124 delete @{$flash_data}{ @{ $c->_flash_stale_keys || [] } };
127 $c->store_session_data( "flash:$sid", $flash_data );
130 $c->delete_session_data("flash:$sid");
139 if ( my $sid = $c->_sessionid ) {
140 if ( $c->session_expires ) { # > 0
142 my $session_data = $c->get_session_data("session:$sid");
143 $c->_session($session_data);
145 no warnings 'uninitialized'; # ne __address
146 if ( $c->config->{session}{verify_address}
147 && $session_data->{__address} ne $c->request->address )
150 "Deleting session $sid due to address mismatch ("
151 . $session_data->{__address} . " != "
152 . $c->request->address . ")",
154 $c->delete_session("address mismatch");
158 $c->log->debug(qq/Restored session "$sid"/) if $c->debug;
159 $c->_session_data_sig( Object::Signature::signature($session_data) ) if $session_data;
160 $c->_expire_session_keys;
162 return $session_data;
172 if ( my $sid = $c->_sessionid ) {
173 if ( my $flash_data = $c->_flash
174 || $c->_flash( $c->get_session_data("flash:$sid") ) )
176 $c->_flash_stale_keys( [ keys %$flash_data ] );
184 sub _expire_session_keys {
185 my ( $c, $data ) = @_;
189 my $expiry = ( $data || $c->_session || {} )->{__expire_keys} || {};
190 foreach my $key ( grep { $expiry->{$_} < $now } keys %$expiry ) {
191 delete $c->_session->{$key};
192 delete $expiry->{$key};
197 my ( $c, $msg ) = @_;
199 # delete the session data
200 my $sid = $c->_sessionid || return;
201 $c->delete_session_data("${_}:${sid}") for qw/session expires flash/;
203 # reset the values in the context object
204 # see the BEGIN block
205 $c->$_(undef) for @session_data_accessors;
207 $c->_session_delete_reason($msg);
210 sub session_delete_reason {
214 if ( $c->_sessionid && !$c->_session ); # must verify session data
216 $c->_session_delete_reason(@_);
219 sub session_expires {
220 my ( $c, $should_create ) = @_;
222 $c->_session_expires || do {
223 if ( my $sid = $c->_sessionid ) {
226 if ( !$should_create ) {
227 if ( ( $c->get_session_data("expires:$sid") || 0 ) < $now ) {
230 $c->log->debug("Deleting session $sid (expired)")
232 $c->delete_session("session expired");
237 return $c->_session_expires(
238 $now + $c->config->{session}{expires} );
247 if($c->_sessionid()) {
248 $c->log->warn('Session ID already set, ignoring.');
249 return $c->_sessionid();
251 if ( $c->validate_session_id( my $sid = shift ) ) {
252 $c->_sessionid($sid);
253 return unless defined wantarray;
256 my $err = "Tried to set invalid session ID '$sid'";
257 $c->log->error($err);
258 Catalyst::Exception->throw($err);
263 if ( $c->_sessionid && !$c->_session ); # must verify session data
265 return $c->_sessionid;
268 sub validate_session_id {
269 my ( $c, $sid ) = @_;
271 $sid and $sid =~ /^[a-f\d]+$/i;
277 $c->_session || $c->_load_session || do {
278 $c->create_session_id;
280 $c->initialize_session_data;
286 $c->_flash || $c->_load_flash || do {
287 $c->create_session_id;
292 sub session_expire_key {
293 my ( $c, %keys ) = @_;
296 @{ $c->session->{__expire_keys} }{ keys %keys } =
297 map { $now + $_ } values %keys;
300 sub initialize_session_data {
311 $c->config->{session}{verify_address}
312 ? ( __address => $c->request->address )
319 sub generate_session_id {
322 my $digest = $c->_find_digest();
323 $digest->add( $c->session_hash_seed() );
324 return $digest->hexdigest;
327 sub create_session_id {
330 if ( !$c->_sessionid ) {
331 my $sid = $c->generate_session_id;
333 $c->log->debug(qq/Created session "$sid"/) if $c->debug;
336 $c->session_expires(1);
342 sub session_hash_seed {
345 return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
350 sub _find_digest () {
352 foreach my $alg (qw/SHA-1 SHA-256 MD5/) {
353 if ( eval { Digest->new($alg) } ) {
358 Catalyst::Exception->throw(
359 "Could not find a suitable Digest module. Please install "
360 . "Digest::SHA1, Digest::SHA, or Digest::MD5" )
364 return Digest->new($usable);
371 $c->NEXT::dump_these(),
374 ? ( [ "Session ID" => $c->sessionid ], [ Session => $c->session ], )
387 Catalyst::Plugin::Session - Generic Session plugin - ties together server side
388 storage and client side state required to maintain session data.
392 # To get sessions to "just work", all you need to do is use these plugins:
396 Session::Store::FastMmap
397 Session::State::Cookie
400 # you can replace Store::FastMmap with Store::File - both have sensible
401 # default configurations (see their docs for details)
403 # more complicated backends are available for other scenarios (DBI storage,
407 # after you've loaded the plugins you can save session data
408 # For example, if you are writing a shopping cart, it could be implemented
411 sub add_item : Local {
412 my ( $self, $c ) = @_;
414 my $item_id = $c->req->param("item");
416 # $c->session is a hash ref, a bit like $c->stash
417 # the difference is that it' preserved across requests
419 push @{ $c->session->{items} }, $item_id;
421 $c->forward("MyView");
424 sub display_items : Local {
425 my ( $self, $c ) = @_;
427 # values in $c->session are restored
428 $c->stash->{items_to_display} =
429 [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
431 $c->forward("MyView");
436 The Session plugin is the base of two related parts of functionality required
437 for session management in web applications.
439 The first part, the State, is getting the browser to repeat back a session key,
440 so that the web application can identify the client and logically string
441 several requests together into a session.
443 The second part, the Store, deals with the actual storage of information about
444 the client. This data is stored so that the it may be revived for every request
445 made by the same client.
447 This plugin links the two pieces together.
449 =head1 RECCOMENDED BACKENDS
453 =item Session::State::Cookie
455 The only really sane way to do state is using cookies.
457 =item Session::Store::File
459 A portable backend, based on Cache::File.
461 =item Session::Store::FastMmap
463 A fast and flexible backend, based on Cache::FastMmap.
473 An accessor for the session ID value.
477 Returns a hash reference that might contain unserialized values from previous
478 requests in the same session, and whose modified value will be saved for future
481 This method will automatically create a new session and session ID if none
484 =item session_expires
486 =item session_expires $reset
488 This method returns the time when the current session will expire, or 0 if
489 there is no current session. If there is a session and it already expired, it
490 will delete the session and return 0 as well.
492 If the C<$reset> parameter is true, and there is a session ID the expiry time
493 will be reset to the current time plus the time to live (see
494 L</CONFIGURATION>). This is used when creating a new session.
498 This is like Ruby on Rails' flash data structure. Think of it as a stash that
499 lasts for longer than one request, letting you redirect instead of forward.
501 The flash data will be cleaned up only on requests on which actually use
502 $c->flash (thus allowing multiple redirections), and the policy is to delete
503 all the keys which were present at the time the data was loaded just before the
507 my ( $self, $c ) = @_;
509 $c->flash->{beans} = 10;
510 $c->response->redirect( $c->uri_for("foo") );
514 my ( $self, $c ) = @_;
516 my $value = $c->flash->{beans};
520 $c->response->redirect( $c->uri_for("bar") );
524 my ( $self, $c ) = @_;
526 if ( exists $c->flash->{beans} ) { # false
531 =item session_delete_reason
533 This accessor contains a string with the reason a session was deleted. Possible
548 =item session_expire_key $key, $ttl
550 Mark a key to expire at a certain time (only useful when shorter than the
551 expiry time for the whole session).
555 __PACKAGE__->config->{session}{expires} = 1000000000000; # forever
559 $c->session_expire_key( __user => 3600 );
561 Will make the session data survive, but the user will still be logged out after
564 Note that these values are not auto extended.
568 =head1 INTERNAL METHODS
574 This method is extended to also make calls to
575 C<check_session_plugin_requirements> and C<setup_session>.
577 =item check_session_plugin_requirements
579 This method ensures that a State and a Store plugin are also in use by the
584 This method populates C<< $c->config->{session} >> with the default values
585 listed in L</CONFIGURATION>.
589 This methoid is extended.
591 It's only effect is if the (off by default) C<flash_to_stash> configuration
592 parameter is on - then it will copy the contents of the flash to the stash at
597 This method is extended and will extend the expiry time, as well as persist the
598 session data if a session exists.
600 =item delete_session REASON
602 This method is used to invalidate a session. It takes an optional parameter
603 which will be saved in C<session_delete_reason> if provided.
605 =item initialize_session_data
607 This method will initialize the internal structure of the session, and is
608 called by the C<session> method if appropriate.
610 =item create_session_id
612 Creates a new session id using C<generate_session_id> if there is no session ID
615 =item validate_session_id SID
617 Make sure a session ID is of the right format.
619 This currently ensures that the session ID string is any amount of case
620 insensitive hexadecimal characters.
622 =item generate_session_id
624 This method will return a string that can be used as a session ID. It is
625 supposed to be a reasonably random string with enough bits to prevent
626 collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
627 MD5 or SHA-256, depending on the availibility of these modules.
629 =item session_hash_seed
631 This method is actually rather internal to generate_session_id, but should be
632 overridable in case you want to provide more random data.
634 Currently it returns a concatenated string which contains:
648 One value from C<rand>.
652 The stringified value of a newly allocated hash reference
656 The stringified value of the Catalyst context object
660 In the hopes that those combined values are entropic enough for most uses. If
661 this is not the case you can replace C<session_hash_seed> with e.g.
663 sub session_hash_seed {
664 open my $fh, "<", "/dev/random";
665 read $fh, my $bytes, 20;
670 Or even more directly, replace C<generate_session_id>:
672 sub generate_session_id {
673 open my $fh, "<", "/dev/random";
674 read $fh, my $bytes, 20;
676 return unpack("H*", $bytes);
679 Also have a look at L<Crypt::Random> and the various openssl bindings - these
680 modules provide APIs for cryptographically secure random data.
684 See L<Catalyst/dump_these> - ammends the session data structure to the list of
685 dumped objects if session ID is defined.
689 =head1 USING SESSIONS DURING PREPARE
691 The earliest point in time at which you may use the session data is after
692 L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
694 State plugins must set $c->session ID before C<prepare_action>, and during
695 C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
701 # don't touch $c->session yet!
703 $c->NEXT::prepare_action( @_ );
705 $c->session; # this is OK
706 $c->sessionid; # this is also OK
711 $c->config->{session} = {
715 All configuation parameters are provided in a hash reference under the
716 C<session> key in the configuration hash.
722 The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
727 When true, C<<$c->request->address>> will be checked at prepare time. If it is
728 not the same as the address that initiated the session, the session is deleted.
732 This option makes it easier to have actions behave the same whether they were
733 forwarded to or redirected to. On prepare time it copies the contents of
734 C<flash> (if any) to the stash.
740 The hash reference returned by C<< $c->session >> contains several keys which
741 are automatically set:
747 This key no longer exists. Use C<session_expires> instead.
751 The last time a session was saved to the store.
755 The time when the session was first created.
759 The value of C<< $c->request->address >> at the time the session was created.
760 This value is only populated if C<verify_address> is true in the configuration.
766 =head2 Round the Robin Proxies
768 C<verify_address> could make your site inaccessible to users who are behind
769 load balanced proxies. Some ISPs may give a different IP to each request by the
770 same client due to this type of proxying. If addresses are verified these
771 users' sessions cannot persist.
773 To let these users access your site you can either disable address verification
774 as a whole, or provide a checkbox in the login dialog that tells the server
775 that it's OK for the address of the client to change. When the server sees that
776 this box is checked it should delete the C<__address> sepcial key from the
777 session hash when the hash is first created.
779 =head2 Race Conditions
781 In this day and age where cleaning detergents and dutch football (not the
782 american kind) teams roam the plains in great numbers, requests may happen
783 simultaneously. This means that there is some risk of session data being
784 overwritten, like this:
790 request a starts, request b starts, with the same session id
794 session data is loaded in request a
798 session data is loaded in request b
802 session data is changed in request a
806 request a finishes, session data is updated and written to store
810 request b finishes, session data is updated and written to store, overwriting
815 If this is a concern in your application, a soon to be developed locking
816 solution is the only safe way to go. This will have a bigger overhead.
818 For applications where any given user is only making one request at a time this
819 plugin should be safe enough.
827 =item Christian Hansen
829 =item Yuval Kogman, C<nothingmuch@woobling.org> (current maintainer)
831 =item Sebastian Riedel
835 And countless other contributers from #catalyst. Thanks guys!
837 =head1 COPYRIGHT & LICENSE
839 Copyright (c) 2005 the aforementioned authors. All rights
840 reserved. This program is free software; you can redistribute
841 it and/or modify it under the same terms as Perl itself.