3 package Catalyst::Plugin::Session;
4 use base qw/Class::Accessor::Fast/;
10 use Catalyst::Exception ();
14 our $VERSION = "0.02";
17 __PACKAGE__->mk_accessors(qw/_sessionid _session _session_delete_reason/);
25 $c->check_session_plugin_requirements;
31 sub check_session_plugin_requirements {
34 unless ( $c->isa("Catalyst::Plugin::Session::State")
35 && $c->isa("Catalyst::Plugin::Session::Store") )
38 ( "The Session plugin requires both Session::State "
39 . "and Session::Store plugins to be used as well." );
42 Catalyst::Exception->throw($err);
49 my $cfg = ( $c->config->{session} ||= {} );
57 $c->NEXT::setup_session();
63 if ( my $session_data = $c->_session ) {
65 # all sessions are extended at the end of the request
67 @{ $session_data }{qw/__updated __expires/} =
68 ( $now, $c->config->{session}{expires} + $now );
69 delete @{ $session_data->{__flash} }{ @{ delete $session_data->{__flash_stale_keys} || [] } };
70 $c->store_session_data( $c->sessionid, $session_data );
73 $c->NEXT::finalize(@_);
79 if ( my $sid = $c->_sessionid ) {
80 no warnings 'uninitialized'; # ne __address
82 my $session_data = $c->_session || $c->_session( $c->get_session_data($sid) );
83 if ( !$session_data or $session_data->{__expires} < time ) {
86 $c->log->debug("Deleting session $sid (expired)") if $c->debug;
87 $c->delete_session("session expired");
89 elsif ($c->config->{session}{verify_address}
90 && $session_data->{__address} ne $c->request->address )
93 "Deleting session $sid due to address mismatch ("
94 . $session_data->{__address} . " != "
95 . $c->request->address . ")",
97 $c->delete_session("address mismatch");
100 $c->log->debug(qq/Restored session "$sid"/) if $c->debug;
103 $c->_expire_ession_keys;
104 $session_data->{__flash_stale_keys} = [ keys %{ $session_data->{__flash} } ];
106 return $session_data;
112 sub _expire_ession_keys {
113 my ( $c, $data ) = @_;
117 my $expiry = ($data || $c->_session || {})->{__expire_keys} || {};
118 foreach my $key (grep { $expiry->{$_} < $now } keys %$expiry ) {
119 delete $c->_session->{$key};
120 delete $expiry->{$key};
125 my ( $c, $msg ) = @_;
127 # delete the session data
128 my $sid = $c->_sessionid || return;
129 $c->delete_session_data($sid);
131 # reset the values in the context object
133 $c->_sessionid(undef);
134 $c->_session_delete_reason($msg);
137 sub session_delete_reason {
140 $c->_load_session if ( $c->_sessionid && !$c->_session ); # must verify session data
142 $c->_session_delete_reason( @_ );
149 if ( $c->validate_session_id( my $sid = shift ) ) {
150 $c->_sessionid( $sid );
151 return unless defined wantarray;
153 my $err = "Tried to set invalid session ID '$sid'";
154 $c->log->error( $err );
155 Catalyst::Exception->throw( $err );
159 $c->_load_session if ( $c->_sessionid && !$c->_session ); # must verify session data
161 return $c->_sessionid;
164 sub validate_session_id {
165 my ( $c, $sid ) = @_;
167 $sid =~ /^[a-f\d]+$/i;
173 $c->_session || $c->_load_session || do {
174 my $sid = $c->generate_session_id;
177 $c->log->debug(qq/Created session "$sid"/) if $c->debug;
179 $c->initialize_session_data;
185 return $c->session->{__flash} ||= {};
188 sub session_expire_key {
189 my ( $c, %keys ) = @_;
192 @{ $c->session->{__expire_keys} }{keys %keys} = map { $now + $_ } values %keys;
195 sub initialize_session_data {
200 return $c->_session({
203 __expires => $now + $c->config->{session}{expires},
206 $c->config->{session}{verify_address}
207 ? ( __address => $c->request->address )
213 sub generate_session_id {
216 my $digest = $c->_find_digest();
217 $digest->add( $c->session_hash_seed() );
218 return $digest->hexdigest;
223 sub session_hash_seed {
226 return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
231 sub _find_digest () {
233 foreach my $alg (qw/SHA-1 MD5 SHA-256/) {
235 my $obj = Digest->new($alg);
241 or Catalyst::Exception->throw(
242 "Could not find a suitable Digest module. Please install "
243 . "Digest::SHA1, Digest::SHA, or Digest::MD5" );
246 return Digest->new($usable);
253 $c->NEXT::dump_these(),
256 ? ( [ "Session ID" => $c->sessionid ], [ Session => $c->session ], )
269 Catalyst::Plugin::Session - Generic Session plugin - ties together server side
270 storage and client side state required to maintain session data.
274 # To get sessions to "just work", all you need to do is use these plugins:
278 Session::Store::FastMmap
279 Session::State::Cookie
282 # you can replace Store::FastMmap with Store::File - both have sensible
283 # default configurations (see their docs for details)
285 # more complicated backends are available for other scenarios (DBI storage,
289 # after you've loaded the plugins you can save session data
290 # For example, if you are writing a shopping cart, it could be implemented
293 sub add_item : Local {
294 my ( $self, $c ) = @_;
296 my $item_id = $c->req->param("item");
298 # $c->session is a hash ref, a bit like $c->stash
299 # the difference is that it' preserved across requests
301 push @{ $c->session->{items} }, $item_id;
303 $c->forward("MyView");
306 sub display_items : Local {
307 my ( $self, $c ) = @_;
309 # values in $c->session are restored
310 $c->stash->{items_to_display} =
311 [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
313 $c->forward("MyView");
318 The Session plugin is the base of two related parts of functionality required
319 for session management in web applications.
321 The first part, the State, is getting the browser to repeat back a session key,
322 so that the web application can identify the client and logically string
323 several requests together into a session.
325 The second part, the Store, deals with the actual storage of information about
326 the client. This data is stored so that the it may be revived for every request
327 made by the same client.
329 This plugin links the two pieces together.
331 =head1 RECCOMENDED BACKENDS
335 =item Session::State::Cookie
337 The only really sane way to do state is using cookies.
339 =item Session::Store::File
341 A portable backend, based on Cache::File.
343 =item Session::Store::FastMmap
345 A fast and flexible backend, based on Cache::FastMmap.
355 An accessor for the session ID value.
359 Returns a hash reference that might contain unserialized values from previous
360 requests in the same session, and whose modified value will be saved for future
363 This method will automatically create a new session and session ID if none
368 This is like Ruby on Rails' flash data structure. Think of it as a stash that
369 lasts a single redirect, not only a forward.
372 my ( $self, $c ) = @_;
374 $c->flash->{beans} = 10;
375 $c->response->redirect( $c->uri_for("foo") );
379 my ( $self, $c ) = @_;
381 my $value = $c->flash->{beans};
385 $c->response->redirect( $c->uri_for("bar") );
389 my ( $self, $c ) = @_;
391 if ( exists $c->flash->{beans} ) { # false
396 =item session_delete_reason
398 This accessor contains a string with the reason a session was deleted. Possible
413 =item session_expire_key $key, $ttl
415 Mark a key to expire at a certain time (only useful when shorter than the
416 expiry time for the whole session).
420 __PACKAGE__->config->{session}{expires} = 1000000000000; # forever
424 $c->session_expire_key( __user => 3600 );
426 Will make the session data survive, but the user will still be logged out after
429 Note that these values are not auto extended.
433 =item INTERNAL METHODS
439 This method is extended to also make calls to
440 C<check_session_plugin_requirements> and C<setup_session>.
442 =item check_session_plugin_requirements
444 This method ensures that a State and a Store plugin are also in use by the
449 This method populates C<< $c->config->{session} >> with the default values
450 listed in L</CONFIGURATION>.
454 This methoid is extended, and will restore session data and check it for
455 validity if a session id is defined. It assumes that the State plugin will
456 populate the C<sessionid> key beforehand.
460 This method is extended and will extend the expiry time, as well as persist the
461 session data if a session exists.
463 =item delete_session REASON
465 This method is used to invalidate a session. It takes an optional parameter
466 which will be saved in C<session_delete_reason> if provided.
468 =item initialize_session_data
470 This method will initialize the internal structure of the session, and is
471 called by the C<session> method if appropriate.
473 =item generate_session_id
475 This method will return a string that can be used as a session ID. It is
476 supposed to be a reasonably random string with enough bits to prevent
477 collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
478 MD5 or SHA-256, depending on the availibility of these modules.
480 =item session_hash_seed
482 This method is actually rather internal to generate_session_id, but should be
483 overridable in case you want to provide more random data.
485 Currently it returns a concatenated string which contains:
487 =item validate_session_id SID
489 Make sure a session ID is of the right format.
491 This currently ensures that the session ID string is any amount of case
492 insensitive hexadecimal characters.
506 One value from C<rand>.
510 The stringified value of a newly allocated hash reference
514 The stringified value of the Catalyst context object
518 In the hopes that those combined values are entropic enough for most uses. If
519 this is not the case you can replace C<session_hash_seed> with e.g.
521 sub session_hash_seed {
522 open my $fh, "<", "/dev/random";
523 read $fh, my $bytes, 20;
528 Or even more directly, replace C<generate_session_id>:
530 sub generate_session_id {
531 open my $fh, "<", "/dev/random";
532 read $fh, my $bytes, 20;
534 return unpack("H*", $bytes);
537 Also have a look at L<Crypt::Random> and the various openssl bindings - these
538 modules provide APIs for cryptographically secure random data.
542 See L<Catalyst/dump_these> - ammends the session data structure to the list of
543 dumped objects if session ID is defined.
547 =head1 USING SESSIONS DURING PREPARE
549 The earliest point in time at which you may use the session data is after
550 L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
552 State plugins must set $c->session ID before C<prepare_action>, and during
553 C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
559 # don't touch $c->session yet!
561 $c->NEXT::prepare_action( @_ );
563 $c->session; # this is OK
564 $c->sessionid; # this is also OK
569 $c->config->{session} = {
573 All configuation parameters are provided in a hash reference under the
574 C<session> key in the configuration hash.
580 The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
585 When true, C<<$c->request->address>> will be checked at prepare time. If it is
586 not the same as the address that initiated the session, the session is deleted.
592 The hash reference returned by C<< $c->session >> contains several keys which
593 are automatically set:
599 A timestamp whose value is the last second when the session is still valid. If
600 a session is restored, and __expires is less than the current time, the session
605 The last time a session was saved. This is the value of
606 C<< $c->session->{__expires} - $c->config->session->{expires} >>.
610 The time when the session was first created.
614 The value of C<< $c->request->address >> at the time the session was created.
615 This value is only populated if C<verify_address> is true in the configuration.
621 =head2 Round the Robin Proxies
623 C<verify_address> could make your site inaccessible to users who are behind
624 load balanced proxies. Some ISPs may give a different IP to each request by the
625 same client due to this type of proxying. If addresses are verified these
626 users' sessions cannot persist.
628 To let these users access your site you can either disable address verification
629 as a whole, or provide a checkbox in the login dialog that tells the server
630 that it's OK for the address of the client to change. When the server sees that
631 this box is checked it should delete the C<__address> sepcial key from the
632 session hash when the hash is first created.
634 =head2 Race Conditions
636 In this day and age where cleaning detergents and dutch football (not the
637 american kind) teams roam the plains in great numbers, requests may happen
638 simultaneously. This means that there is some risk of session data being
639 overwritten, like this:
645 request a starts, request b starts, with the same session id
649 session data is loaded in request a
653 session data is loaded in request b
657 session data is changed in request a
661 request a finishes, session data is updated and written to store
665 request b finishes, session data is updated and written to store, overwriting
670 If this is a concern in your application, a soon to be developed locking
671 solution is the only safe way to go. This will have a bigger overhead.
673 For applications where any given user is only making one request at a time this
674 plugin should be safe enough.
682 =item Christian Hansen
684 =item Yuval Kogman, C<nothingmuch@woobling.org> (current maintainer)
686 =item Sebastian Riedel
690 And countless other contributers from #catalyst. Thanks guys!
692 =head1 COPYRIGHT & LICENSE
694 Copyright (c) 2005 the aforementioned authors. All rights
695 reserved. This program is free software; you can redistribute
696 it and/or modify it under the same terms as Perl itself.