3 package Catalyst::Plugin::Session;
4 use base qw/Class::Accessor::Fast/;
10 use Catalyst::Exception ();
13 use Object::Signature ();
15 our $VERSION = "0.02";
18 __PACKAGE__->mk_accessors(qw/
23 _session_delete_reason
34 $c->check_session_plugin_requirements;
40 sub check_session_plugin_requirements {
43 unless ( $c->isa("Catalyst::Plugin::Session::State")
44 && $c->isa("Catalyst::Plugin::Session::Store") )
47 ( "The Session plugin requires both Session::State "
48 . "and Session::Store plugins to be used as well." );
51 Catalyst::Exception->throw($err);
58 my $cfg = ( $c->config->{session} ||= {} );
66 $c->NEXT::setup_session();
72 if ( $c->config->{session}{flash_to_stash} and $c->_sessionid and my $flash_data = $c->flash ) {
73 @{ $c->stash }{ keys %$flash_data } = values %$flash_data;
76 $c->NEXT::prepare_action(@_);
85 $c->NEXT::finalize(@_);
91 if ( my $sid = $c->_sessionid ) {
92 if ( my $session_data = $c->_session ) {
94 # all sessions are extended at the end of the request
96 $c->store_session_data( "expires:$sid" => ( $c->config->{session}{expires} + $now ) );
98 my $new_sig = Object::Signature::signature( $session_data );
100 no warnings 'uninitialized';
101 if ( $new_sig ne $c->_session_data_sig ) {
102 $session_data->{__updated} = $now;
103 $c->store_session_data( "session:$sid" => $session_data );
112 if ( my $sid = $c->_sessionid ) {
113 if ( my $flash_data = $c->_flash ) {
114 if ( %$flash_data ) { # damn 'my' declarations
115 delete @{ $flash_data }{ @{ $c->_flash_stale_keys || [] } };
116 $c->store_session_data( "flash:$sid", $flash_data );
119 $c->delete_session_data( "flash:$sid" );
127 if ( my $sid = $c->_sessionid ) {
128 no warnings 'uninitialized'; # ne __address
130 my ( $session_data, $session_expires ) = $c->get_session_data( "session:$sid", "expires:$sid" );
131 $c->_session( $session_data );
133 if ( !$session_data or $session_expires < time ) {
136 $c->log->debug("Deleting session $sid (expired)") if $c->debug;
137 $c->delete_session("session expired");
139 elsif ($c->config->{session}{verify_address}
140 && $session_data->{__address} ne $c->request->address )
143 "Deleting session $sid due to address mismatch ("
144 . $session_data->{__address} . " != "
145 . $c->request->address . ")",
147 $c->delete_session("address mismatch");
150 $c->log->debug(qq/Restored session "$sid"/) if $c->debug;
151 $c->_session_data_sig( Object::Signature::signature( $session_data ) );
152 $c->_expire_session_keys;
155 return $session_data;
164 if ( my $sid = $c->_sessionid ) {
165 if ( my $flash_data = $c->_flash || $c->_flash( $c->get_session_data( "flash:$sid" ) ) ) {
166 $c->_flash_stale_keys([ keys %$flash_data ]);
174 sub _expire_session_keys {
175 my ( $c, $data ) = @_;
179 my $expiry = ($data || $c->_session || {})->{__expire_keys} || {};
180 foreach my $key (grep { $expiry->{$_} < $now } keys %$expiry ) {
181 delete $c->_session->{$key};
182 delete $expiry->{$key};
187 my ( $c, $msg ) = @_;
189 # delete the session data
190 my $sid = $c->_sessionid || return;
191 $c->delete_session_data( "session:$sid" );
193 # reset the values in the context object
195 $c->_sessionid(undef);
196 $c->_session_delete_reason($msg);
199 sub session_delete_reason {
202 $c->_load_session if ( $c->_sessionid && !$c->_session ); # must verify session data
204 $c->_session_delete_reason( @_ );
211 if ( $c->validate_session_id( my $sid = shift ) ) {
212 $c->_sessionid( $sid );
213 return unless defined wantarray;
215 my $err = "Tried to set invalid session ID '$sid'";
216 $c->log->error( $err );
217 Catalyst::Exception->throw( $err );
221 $c->_load_session if ( $c->_sessionid && !$c->_session ); # must verify session data
223 return $c->_sessionid;
226 sub validate_session_id {
227 my ( $c, $sid ) = @_;
229 $sid and $sid =~ /^[a-f\d]+$/i;
235 $c->_session || $c->_load_session || do {
236 $c->create_session_id;
238 $c->initialize_session_data;
244 $c->_flash || $c->_load_flash || do {
245 $c->create_session_id;
250 sub session_expire_key {
251 my ( $c, %keys ) = @_;
254 @{ $c->session->{__expire_keys} }{keys %keys} = map { $now + $_ } values %keys;
257 sub initialize_session_data {
262 return $c->_session({
267 $c->config->{session}{verify_address}
268 ? ( __address => $c->request->address )
274 sub generate_session_id {
277 my $digest = $c->_find_digest();
278 $digest->add( $c->session_hash_seed() );
279 return $digest->hexdigest;
282 sub create_session_id {
285 if ( !$c->_sessionid ) {
286 my $sid = $c->generate_session_id;
288 $c->log->debug(qq/Created session "$sid"/) if $c->debug;
296 sub session_hash_seed {
299 return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
304 sub _find_digest () {
306 foreach my $alg (qw/SHA-1 SHA-256 MD5/) {
316 or Catalyst::Exception->throw(
317 "Could not find a suitable Digest module. Please install "
318 . "Digest::SHA1, Digest::SHA, or Digest::MD5" );
321 return Digest->new($usable);
328 $c->NEXT::dump_these(),
331 ? ( [ "Session ID" => $c->sessionid ], [ Session => $c->session ], )
344 Catalyst::Plugin::Session - Generic Session plugin - ties together server side
345 storage and client side state required to maintain session data.
349 # To get sessions to "just work", all you need to do is use these plugins:
353 Session::Store::FastMmap
354 Session::State::Cookie
357 # you can replace Store::FastMmap with Store::File - both have sensible
358 # default configurations (see their docs for details)
360 # more complicated backends are available for other scenarios (DBI storage,
364 # after you've loaded the plugins you can save session data
365 # For example, if you are writing a shopping cart, it could be implemented
368 sub add_item : Local {
369 my ( $self, $c ) = @_;
371 my $item_id = $c->req->param("item");
373 # $c->session is a hash ref, a bit like $c->stash
374 # the difference is that it' preserved across requests
376 push @{ $c->session->{items} }, $item_id;
378 $c->forward("MyView");
381 sub display_items : Local {
382 my ( $self, $c ) = @_;
384 # values in $c->session are restored
385 $c->stash->{items_to_display} =
386 [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
388 $c->forward("MyView");
393 The Session plugin is the base of two related parts of functionality required
394 for session management in web applications.
396 The first part, the State, is getting the browser to repeat back a session key,
397 so that the web application can identify the client and logically string
398 several requests together into a session.
400 The second part, the Store, deals with the actual storage of information about
401 the client. This data is stored so that the it may be revived for every request
402 made by the same client.
404 This plugin links the two pieces together.
406 =head1 RECCOMENDED BACKENDS
410 =item Session::State::Cookie
412 The only really sane way to do state is using cookies.
414 =item Session::Store::File
416 A portable backend, based on Cache::File.
418 =item Session::Store::FastMmap
420 A fast and flexible backend, based on Cache::FastMmap.
430 An accessor for the session ID value.
434 Returns a hash reference that might contain unserialized values from previous
435 requests in the same session, and whose modified value will be saved for future
438 This method will automatically create a new session and session ID if none
443 This is like Ruby on Rails' flash data structure. Think of it as a stash that
444 lasts a single redirect, not only a forward.
447 my ( $self, $c ) = @_;
449 $c->flash->{beans} = 10;
450 $c->response->redirect( $c->uri_for("foo") );
454 my ( $self, $c ) = @_;
456 my $value = $c->flash->{beans};
460 $c->response->redirect( $c->uri_for("bar") );
464 my ( $self, $c ) = @_;
466 if ( exists $c->flash->{beans} ) { # false
471 =item session_delete_reason
473 This accessor contains a string with the reason a session was deleted. Possible
488 =item session_expire_key $key, $ttl
490 Mark a key to expire at a certain time (only useful when shorter than the
491 expiry time for the whole session).
495 __PACKAGE__->config->{session}{expires} = 1000000000000; # forever
499 $c->session_expire_key( __user => 3600 );
501 Will make the session data survive, but the user will still be logged out after
504 Note that these values are not auto extended.
508 =item INTERNAL METHODS
514 This method is extended to also make calls to
515 C<check_session_plugin_requirements> and C<setup_session>.
517 =item check_session_plugin_requirements
519 This method ensures that a State and a Store plugin are also in use by the
524 This method populates C<< $c->config->{session} >> with the default values
525 listed in L</CONFIGURATION>.
529 This methoid is extended.
531 It's only effect is if the (off by default) C<flash_to_stash> configuration
532 parameter is on - then it will copy the contents of the flash to the stash at
537 This method is extended and will extend the expiry time, as well as persist the
538 session data if a session exists.
540 =item delete_session REASON
542 This method is used to invalidate a session. It takes an optional parameter
543 which will be saved in C<session_delete_reason> if provided.
545 =item initialize_session_data
547 This method will initialize the internal structure of the session, and is
548 called by the C<session> method if appropriate.
550 =item create_session_id
552 Creates a new session id using C<generate_session_id> if there is no session ID
555 =item generate_session_id
557 This method will return a string that can be used as a session ID. It is
558 supposed to be a reasonably random string with enough bits to prevent
559 collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
560 MD5 or SHA-256, depending on the availibility of these modules.
562 =item session_hash_seed
564 This method is actually rather internal to generate_session_id, but should be
565 overridable in case you want to provide more random data.
567 Currently it returns a concatenated string which contains:
569 =item validate_session_id SID
571 Make sure a session ID is of the right format.
573 This currently ensures that the session ID string is any amount of case
574 insensitive hexadecimal characters.
588 One value from C<rand>.
592 The stringified value of a newly allocated hash reference
596 The stringified value of the Catalyst context object
600 In the hopes that those combined values are entropic enough for most uses. If
601 this is not the case you can replace C<session_hash_seed> with e.g.
603 sub session_hash_seed {
604 open my $fh, "<", "/dev/random";
605 read $fh, my $bytes, 20;
610 Or even more directly, replace C<generate_session_id>:
612 sub generate_session_id {
613 open my $fh, "<", "/dev/random";
614 read $fh, my $bytes, 20;
616 return unpack("H*", $bytes);
619 Also have a look at L<Crypt::Random> and the various openssl bindings - these
620 modules provide APIs for cryptographically secure random data.
624 See L<Catalyst/dump_these> - ammends the session data structure to the list of
625 dumped objects if session ID is defined.
629 =head1 USING SESSIONS DURING PREPARE
631 The earliest point in time at which you may use the session data is after
632 L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
634 State plugins must set $c->session ID before C<prepare_action>, and during
635 C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
641 # don't touch $c->session yet!
643 $c->NEXT::prepare_action( @_ );
645 $c->session; # this is OK
646 $c->sessionid; # this is also OK
651 $c->config->{session} = {
655 All configuation parameters are provided in a hash reference under the
656 C<session> key in the configuration hash.
662 The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
667 When true, C<<$c->request->address>> will be checked at prepare time. If it is
668 not the same as the address that initiated the session, the session is deleted.
672 This option makes it easier to have actions behave the same whether they were
673 forwarded to or redirected to. On prepare time it copies the contents of
674 C<flash> (if any) to the stash.
680 The hash reference returned by C<< $c->session >> contains several keys which
681 are automatically set:
687 This key no longer exists. This data is now saved elsewhere.
691 The last time a session was saved to the store.
695 The time when the session was first created.
699 The value of C<< $c->request->address >> at the time the session was created.
700 This value is only populated if C<verify_address> is true in the configuration.
706 =head2 Round the Robin Proxies
708 C<verify_address> could make your site inaccessible to users who are behind
709 load balanced proxies. Some ISPs may give a different IP to each request by the
710 same client due to this type of proxying. If addresses are verified these
711 users' sessions cannot persist.
713 To let these users access your site you can either disable address verification
714 as a whole, or provide a checkbox in the login dialog that tells the server
715 that it's OK for the address of the client to change. When the server sees that
716 this box is checked it should delete the C<__address> sepcial key from the
717 session hash when the hash is first created.
719 =head2 Race Conditions
721 In this day and age where cleaning detergents and dutch football (not the
722 american kind) teams roam the plains in great numbers, requests may happen
723 simultaneously. This means that there is some risk of session data being
724 overwritten, like this:
730 request a starts, request b starts, with the same session id
734 session data is loaded in request a
738 session data is loaded in request b
742 session data is changed in request a
746 request a finishes, session data is updated and written to store
750 request b finishes, session data is updated and written to store, overwriting
755 If this is a concern in your application, a soon to be developed locking
756 solution is the only safe way to go. This will have a bigger overhead.
758 For applications where any given user is only making one request at a time this
759 plugin should be safe enough.
767 =item Christian Hansen
769 =item Yuval Kogman, C<nothingmuch@woobling.org> (current maintainer)
771 =item Sebastian Riedel
775 And countless other contributers from #catalyst. Thanks guys!
777 =head1 COPYRIGHT & LICENSE
779 Copyright (c) 2005 the aforementioned authors. All rights
780 reserved. This program is free software; you can redistribute
781 it and/or modify it under the same terms as Perl itself.