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 _flash _flash_stale_keys/);
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();
67 $c->NEXT::finalize(@_);
73 if ( my $sid = $c->_sessionid ) {
74 if ( my $session_data = $c->_session ) {
76 # all sessions are extended at the end of the request
78 @{ $session_data }{qw/__updated __expires/} =
79 ( $now, $c->config->{session}{expires} + $now );
81 $c->store_session_data( "session:$sid", $session_data );
89 if ( my $sid = $c->_sessionid ) {
90 if ( my $flash_data = $c->_flash ) {
91 if ( %$flash_data ) { # damn 'my' declarations
92 delete @{ $flash_data }{ @{ $c->_flash_stale_keys || [] } };
93 $c->store_session_data( "flash:$sid", $flash_data );
96 $c->delete_session_data( "flash:$sid" );
104 if ( my $sid = $c->_sessionid ) {
105 no warnings 'uninitialized'; # ne __address
107 my $session_data = $c->_session || $c->_session( $c->get_session_data( "session:$sid" ) );
108 if ( !$session_data or $session_data->{__expires} < time ) {
111 $c->log->debug("Deleting session $sid (expired)") if $c->debug;
112 $c->delete_session("session expired");
114 elsif ($c->config->{session}{verify_address}
115 && $session_data->{__address} ne $c->request->address )
118 "Deleting session $sid due to address mismatch ("
119 . $session_data->{__address} . " != "
120 . $c->request->address . ")",
122 $c->delete_session("address mismatch");
125 $c->log->debug(qq/Restored session "$sid"/) if $c->debug;
128 $c->_expire_ession_keys;
130 return $session_data;
139 if ( my $sid = $c->_sessionid ) {
140 if ( my $flash_data = $c->_flash || $c->_flash( $c->get_session_data( "flash:$sid" ) ) ) {
141 $c->_flash_stale_keys([ keys %$flash_data ]);
149 sub _expire_ession_keys {
150 my ( $c, $data ) = @_;
154 my $expiry = ($data || $c->_session || {})->{__expire_keys} || {};
155 foreach my $key (grep { $expiry->{$_} < $now } keys %$expiry ) {
156 delete $c->_session->{$key};
157 delete $expiry->{$key};
162 my ( $c, $msg ) = @_;
164 # delete the session data
165 my $sid = $c->_sessionid || return;
166 $c->delete_session_data( "session:$sid" );
168 # reset the values in the context object
170 $c->_sessionid(undef);
171 $c->_session_delete_reason($msg);
174 sub session_delete_reason {
177 $c->_load_session if ( $c->_sessionid && !$c->_session ); # must verify session data
179 $c->_session_delete_reason( @_ );
186 if ( $c->validate_session_id( my $sid = shift ) ) {
187 $c->_sessionid( $sid );
188 return unless defined wantarray;
190 my $err = "Tried to set invalid session ID '$sid'";
191 $c->log->error( $err );
192 Catalyst::Exception->throw( $err );
196 $c->_load_session if ( $c->_sessionid && !$c->_session ); # must verify session data
198 return $c->_sessionid;
201 sub validate_session_id {
202 my ( $c, $sid ) = @_;
204 $sid and $sid =~ /^[a-f\d]+$/i;
210 $c->_session || $c->_load_session || do {
211 my $sid = $c->generate_session_id;
214 $c->log->debug(qq/Created session "$sid"/) if $c->debug;
216 $c->initialize_session_data;
222 $c->_flash || $c->_load_flash || $c->_flash( {} );
225 sub session_expire_key {
226 my ( $c, %keys ) = @_;
229 @{ $c->session->{__expire_keys} }{keys %keys} = map { $now + $_ } values %keys;
232 sub initialize_session_data {
237 return $c->_session({
240 __expires => $now + $c->config->{session}{expires},
243 $c->config->{session}{verify_address}
244 ? ( __address => $c->request->address )
250 sub generate_session_id {
253 my $digest = $c->_find_digest();
254 $digest->add( $c->session_hash_seed() );
255 return $digest->hexdigest;
260 sub session_hash_seed {
263 return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
268 sub _find_digest () {
270 foreach my $alg (qw/SHA-1 MD5 SHA-256/) {
272 my $obj = Digest->new($alg);
278 or Catalyst::Exception->throw(
279 "Could not find a suitable Digest module. Please install "
280 . "Digest::SHA1, Digest::SHA, or Digest::MD5" );
283 return Digest->new($usable);
290 $c->NEXT::dump_these(),
293 ? ( [ "Session ID" => $c->sessionid ], [ Session => $c->session ], )
306 Catalyst::Plugin::Session - Generic Session plugin - ties together server side
307 storage and client side state required to maintain session data.
311 # To get sessions to "just work", all you need to do is use these plugins:
315 Session::Store::FastMmap
316 Session::State::Cookie
319 # you can replace Store::FastMmap with Store::File - both have sensible
320 # default configurations (see their docs for details)
322 # more complicated backends are available for other scenarios (DBI storage,
326 # after you've loaded the plugins you can save session data
327 # For example, if you are writing a shopping cart, it could be implemented
330 sub add_item : Local {
331 my ( $self, $c ) = @_;
333 my $item_id = $c->req->param("item");
335 # $c->session is a hash ref, a bit like $c->stash
336 # the difference is that it' preserved across requests
338 push @{ $c->session->{items} }, $item_id;
340 $c->forward("MyView");
343 sub display_items : Local {
344 my ( $self, $c ) = @_;
346 # values in $c->session are restored
347 $c->stash->{items_to_display} =
348 [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
350 $c->forward("MyView");
355 The Session plugin is the base of two related parts of functionality required
356 for session management in web applications.
358 The first part, the State, is getting the browser to repeat back a session key,
359 so that the web application can identify the client and logically string
360 several requests together into a session.
362 The second part, the Store, deals with the actual storage of information about
363 the client. This data is stored so that the it may be revived for every request
364 made by the same client.
366 This plugin links the two pieces together.
368 =head1 RECCOMENDED BACKENDS
372 =item Session::State::Cookie
374 The only really sane way to do state is using cookies.
376 =item Session::Store::File
378 A portable backend, based on Cache::File.
380 =item Session::Store::FastMmap
382 A fast and flexible backend, based on Cache::FastMmap.
392 An accessor for the session ID value.
396 Returns a hash reference that might contain unserialized values from previous
397 requests in the same session, and whose modified value will be saved for future
400 This method will automatically create a new session and session ID if none
405 This is like Ruby on Rails' flash data structure. Think of it as a stash that
406 lasts a single redirect, not only a forward.
409 my ( $self, $c ) = @_;
411 $c->flash->{beans} = 10;
412 $c->response->redirect( $c->uri_for("foo") );
416 my ( $self, $c ) = @_;
418 my $value = $c->flash->{beans};
422 $c->response->redirect( $c->uri_for("bar") );
426 my ( $self, $c ) = @_;
428 if ( exists $c->flash->{beans} ) { # false
433 =item session_delete_reason
435 This accessor contains a string with the reason a session was deleted. Possible
450 =item session_expire_key $key, $ttl
452 Mark a key to expire at a certain time (only useful when shorter than the
453 expiry time for the whole session).
457 __PACKAGE__->config->{session}{expires} = 1000000000000; # forever
461 $c->session_expire_key( __user => 3600 );
463 Will make the session data survive, but the user will still be logged out after
466 Note that these values are not auto extended.
470 =item INTERNAL METHODS
476 This method is extended to also make calls to
477 C<check_session_plugin_requirements> and C<setup_session>.
479 =item check_session_plugin_requirements
481 This method ensures that a State and a Store plugin are also in use by the
486 This method populates C<< $c->config->{session} >> with the default values
487 listed in L</CONFIGURATION>.
491 This methoid is extended, and will restore session data and check it for
492 validity if a session id is defined. It assumes that the State plugin will
493 populate the C<sessionid> key beforehand.
497 This method is extended and will extend the expiry time, as well as persist the
498 session data if a session exists.
500 =item delete_session REASON
502 This method is used to invalidate a session. It takes an optional parameter
503 which will be saved in C<session_delete_reason> if provided.
505 =item initialize_session_data
507 This method will initialize the internal structure of the session, and is
508 called by the C<session> method if appropriate.
510 =item generate_session_id
512 This method will return a string that can be used as a session ID. It is
513 supposed to be a reasonably random string with enough bits to prevent
514 collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
515 MD5 or SHA-256, depending on the availibility of these modules.
517 =item session_hash_seed
519 This method is actually rather internal to generate_session_id, but should be
520 overridable in case you want to provide more random data.
522 Currently it returns a concatenated string which contains:
524 =item validate_session_id SID
526 Make sure a session ID is of the right format.
528 This currently ensures that the session ID string is any amount of case
529 insensitive hexadecimal characters.
543 One value from C<rand>.
547 The stringified value of a newly allocated hash reference
551 The stringified value of the Catalyst context object
555 In the hopes that those combined values are entropic enough for most uses. If
556 this is not the case you can replace C<session_hash_seed> with e.g.
558 sub session_hash_seed {
559 open my $fh, "<", "/dev/random";
560 read $fh, my $bytes, 20;
565 Or even more directly, replace C<generate_session_id>:
567 sub generate_session_id {
568 open my $fh, "<", "/dev/random";
569 read $fh, my $bytes, 20;
571 return unpack("H*", $bytes);
574 Also have a look at L<Crypt::Random> and the various openssl bindings - these
575 modules provide APIs for cryptographically secure random data.
579 See L<Catalyst/dump_these> - ammends the session data structure to the list of
580 dumped objects if session ID is defined.
584 =head1 USING SESSIONS DURING PREPARE
586 The earliest point in time at which you may use the session data is after
587 L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
589 State plugins must set $c->session ID before C<prepare_action>, and during
590 C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
596 # don't touch $c->session yet!
598 $c->NEXT::prepare_action( @_ );
600 $c->session; # this is OK
601 $c->sessionid; # this is also OK
606 $c->config->{session} = {
610 All configuation parameters are provided in a hash reference under the
611 C<session> key in the configuration hash.
617 The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
622 When true, C<<$c->request->address>> will be checked at prepare time. If it is
623 not the same as the address that initiated the session, the session is deleted.
629 The hash reference returned by C<< $c->session >> contains several keys which
630 are automatically set:
636 A timestamp whose value is the last second when the session is still valid. If
637 a session is restored, and __expires is less than the current time, the session
642 The last time a session was saved. This is the value of
643 C<< $c->session->{__expires} - $c->config->session->{expires} >>.
647 The time when the session was first created.
651 The value of C<< $c->request->address >> at the time the session was created.
652 This value is only populated if C<verify_address> is true in the configuration.
658 =head2 Round the Robin Proxies
660 C<verify_address> could make your site inaccessible to users who are behind
661 load balanced proxies. Some ISPs may give a different IP to each request by the
662 same client due to this type of proxying. If addresses are verified these
663 users' sessions cannot persist.
665 To let these users access your site you can either disable address verification
666 as a whole, or provide a checkbox in the login dialog that tells the server
667 that it's OK for the address of the client to change. When the server sees that
668 this box is checked it should delete the C<__address> sepcial key from the
669 session hash when the hash is first created.
671 =head2 Race Conditions
673 In this day and age where cleaning detergents and dutch football (not the
674 american kind) teams roam the plains in great numbers, requests may happen
675 simultaneously. This means that there is some risk of session data being
676 overwritten, like this:
682 request a starts, request b starts, with the same session id
686 session data is loaded in request a
690 session data is loaded in request b
694 session data is changed in request a
698 request a finishes, session data is updated and written to store
702 request b finishes, session data is updated and written to store, overwriting
707 If this is a concern in your application, a soon to be developed locking
708 solution is the only safe way to go. This will have a bigger overhead.
710 For applications where any given user is only making one request at a time this
711 plugin should be safe enough.
719 =item Christian Hansen
721 =item Yuval Kogman, C<nothingmuch@woobling.org> (current maintainer)
723 =item Sebastian Riedel
727 And countless other contributers from #catalyst. Thanks guys!
729 =head1 COPYRIGHT & LICENSE
731 Copyright (c) 2005 the aforementioned authors. All rights
732 reserved. This program is free software; you can redistribute
733 it and/or modify it under the same terms as Perl itself.