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();
66 $c->NEXT::finalize(@_);
72 if ( my $sid = $c->_sessionid ) {
73 if ( my $session_data = $c->_session ) {
75 # all sessions are extended at the end of the request
77 @{ $session_data }{qw/__updated __expires/} =
78 ( $now, $c->config->{session}{expires} + $now );
80 $c->store_session_data( "session:$sid", $session_data );
88 if ( my $sid = $c->_sessionid ) {
89 if ( my $flash_data = $c->_flash ) {
90 if ( %$flash_data ) { # damn 'my' declarations
91 delete @{ $flash_data }{ @{ $c->_flash_stale_keys || [] } };
92 $c->store_session_data( "flash:$sid", $flash_data );
95 $c->delete_session_data( "flash:$sid" );
103 if ( my $sid = $c->_sessionid ) {
104 no warnings 'uninitialized'; # ne __address
106 my $session_data = $c->_session || $c->_session( $c->get_session_data( "session:$sid" ) );
107 if ( !$session_data or $session_data->{__expires} < time ) {
110 $c->log->debug("Deleting session $sid (expired)") if $c->debug;
111 $c->delete_session("session expired");
113 elsif ($c->config->{session}{verify_address}
114 && $session_data->{__address} ne $c->request->address )
117 "Deleting session $sid due to address mismatch ("
118 . $session_data->{__address} . " != "
119 . $c->request->address . ")",
121 $c->delete_session("address mismatch");
124 $c->log->debug(qq/Restored session "$sid"/) if $c->debug;
127 $c->_expire_ession_keys;
129 return $session_data;
138 if ( my $sid = $c->_sessionid ) {
139 if ( my $flash_data = $c->_flash || $c->_flash( $c->get_session_data( "flash:$sid" ) ) ) {
140 $c->_flash_stale_keys([ keys %$flash_data ]);
148 sub _expire_ession_keys {
149 my ( $c, $data ) = @_;
153 my $expiry = ($data || $c->_session || {})->{__expire_keys} || {};
154 foreach my $key (grep { $expiry->{$_} < $now } keys %$expiry ) {
155 delete $c->_session->{$key};
156 delete $expiry->{$key};
161 my ( $c, $msg ) = @_;
163 # delete the session data
164 my $sid = $c->_sessionid || return;
165 $c->delete_session_data( "session:$sid" );
167 # reset the values in the context object
169 $c->_sessionid(undef);
170 $c->_session_delete_reason($msg);
173 sub session_delete_reason {
176 $c->_load_session if ( $c->_sessionid && !$c->_session ); # must verify session data
178 $c->_session_delete_reason( @_ );
185 if ( $c->validate_session_id( my $sid = shift ) ) {
186 $c->_sessionid( $sid );
187 return unless defined wantarray;
189 my $err = "Tried to set invalid session ID '$sid'";
190 $c->log->error( $err );
191 Catalyst::Exception->throw( $err );
195 $c->_load_session if ( $c->_sessionid && !$c->_session ); # must verify session data
197 return $c->_sessionid;
200 sub validate_session_id {
201 my ( $c, $sid ) = @_;
203 $sid and $sid =~ /^[a-f\d]+$/i;
209 $c->_session || $c->_load_session || do {
210 $c->create_session_id;
212 $c->initialize_session_data;
218 $c->_flash || $c->_load_flash || do {
219 $c->create_session_id;
224 sub session_expire_key {
225 my ( $c, %keys ) = @_;
228 @{ $c->session->{__expire_keys} }{keys %keys} = map { $now + $_ } values %keys;
231 sub initialize_session_data {
236 return $c->_session({
239 __expires => $now + $c->config->{session}{expires},
242 $c->config->{session}{verify_address}
243 ? ( __address => $c->request->address )
249 sub generate_session_id {
252 my $digest = $c->_find_digest();
253 $digest->add( $c->session_hash_seed() );
254 return $digest->hexdigest;
257 sub create_session_id {
260 if ( !$c->_sessionid ) {
261 my $sid = $c->generate_session_id;
263 $c->log->debug(qq/Created session "$sid"/) if $c->debug;
271 sub session_hash_seed {
274 return join( "", ++$counter, time, rand, $$, {}, overload::StrVal($c), );
279 sub _find_digest () {
281 foreach my $alg (qw/SHA-1 MD5 SHA-256/) {
283 my $obj = Digest->new($alg);
289 or Catalyst::Exception->throw(
290 "Could not find a suitable Digest module. Please install "
291 . "Digest::SHA1, Digest::SHA, or Digest::MD5" );
294 return Digest->new($usable);
301 $c->NEXT::dump_these(),
304 ? ( [ "Session ID" => $c->sessionid ], [ Session => $c->session ], )
317 Catalyst::Plugin::Session - Generic Session plugin - ties together server side
318 storage and client side state required to maintain session data.
322 # To get sessions to "just work", all you need to do is use these plugins:
326 Session::Store::FastMmap
327 Session::State::Cookie
330 # you can replace Store::FastMmap with Store::File - both have sensible
331 # default configurations (see their docs for details)
333 # more complicated backends are available for other scenarios (DBI storage,
337 # after you've loaded the plugins you can save session data
338 # For example, if you are writing a shopping cart, it could be implemented
341 sub add_item : Local {
342 my ( $self, $c ) = @_;
344 my $item_id = $c->req->param("item");
346 # $c->session is a hash ref, a bit like $c->stash
347 # the difference is that it' preserved across requests
349 push @{ $c->session->{items} }, $item_id;
351 $c->forward("MyView");
354 sub display_items : Local {
355 my ( $self, $c ) = @_;
357 # values in $c->session are restored
358 $c->stash->{items_to_display} =
359 [ map { MyModel->retrieve($_) } @{ $c->session->{items} } ];
361 $c->forward("MyView");
366 The Session plugin is the base of two related parts of functionality required
367 for session management in web applications.
369 The first part, the State, is getting the browser to repeat back a session key,
370 so that the web application can identify the client and logically string
371 several requests together into a session.
373 The second part, the Store, deals with the actual storage of information about
374 the client. This data is stored so that the it may be revived for every request
375 made by the same client.
377 This plugin links the two pieces together.
379 =head1 RECCOMENDED BACKENDS
383 =item Session::State::Cookie
385 The only really sane way to do state is using cookies.
387 =item Session::Store::File
389 A portable backend, based on Cache::File.
391 =item Session::Store::FastMmap
393 A fast and flexible backend, based on Cache::FastMmap.
403 An accessor for the session ID value.
407 Returns a hash reference that might contain unserialized values from previous
408 requests in the same session, and whose modified value will be saved for future
411 This method will automatically create a new session and session ID if none
416 This is like Ruby on Rails' flash data structure. Think of it as a stash that
417 lasts a single redirect, not only a forward.
420 my ( $self, $c ) = @_;
422 $c->flash->{beans} = 10;
423 $c->response->redirect( $c->uri_for("foo") );
427 my ( $self, $c ) = @_;
429 my $value = $c->flash->{beans};
433 $c->response->redirect( $c->uri_for("bar") );
437 my ( $self, $c ) = @_;
439 if ( exists $c->flash->{beans} ) { # false
444 =item session_delete_reason
446 This accessor contains a string with the reason a session was deleted. Possible
461 =item session_expire_key $key, $ttl
463 Mark a key to expire at a certain time (only useful when shorter than the
464 expiry time for the whole session).
468 __PACKAGE__->config->{session}{expires} = 1000000000000; # forever
472 $c->session_expire_key( __user => 3600 );
474 Will make the session data survive, but the user will still be logged out after
477 Note that these values are not auto extended.
481 =item INTERNAL METHODS
487 This method is extended to also make calls to
488 C<check_session_plugin_requirements> and C<setup_session>.
490 =item check_session_plugin_requirements
492 This method ensures that a State and a Store plugin are also in use by the
497 This method populates C<< $c->config->{session} >> with the default values
498 listed in L</CONFIGURATION>.
502 This methoid is extended, and will restore session data and check it for
503 validity if a session id is defined. It assumes that the State plugin will
504 populate the C<sessionid> key beforehand.
508 This method is extended and will extend the expiry time, as well as persist the
509 session data if a session exists.
511 =item delete_session REASON
513 This method is used to invalidate a session. It takes an optional parameter
514 which will be saved in C<session_delete_reason> if provided.
516 =item initialize_session_data
518 This method will initialize the internal structure of the session, and is
519 called by the C<session> method if appropriate.
521 =item generate_session_id
523 This method will return a string that can be used as a session ID. It is
524 supposed to be a reasonably random string with enough bits to prevent
525 collision. It basically takes C<session_hash_seed> and hashes it using SHA-1,
526 MD5 or SHA-256, depending on the availibility of these modules.
528 =item session_hash_seed
530 This method is actually rather internal to generate_session_id, but should be
531 overridable in case you want to provide more random data.
533 Currently it returns a concatenated string which contains:
535 =item validate_session_id SID
537 Make sure a session ID is of the right format.
539 This currently ensures that the session ID string is any amount of case
540 insensitive hexadecimal characters.
554 One value from C<rand>.
558 The stringified value of a newly allocated hash reference
562 The stringified value of the Catalyst context object
566 In the hopes that those combined values are entropic enough for most uses. If
567 this is not the case you can replace C<session_hash_seed> with e.g.
569 sub session_hash_seed {
570 open my $fh, "<", "/dev/random";
571 read $fh, my $bytes, 20;
576 Or even more directly, replace C<generate_session_id>:
578 sub generate_session_id {
579 open my $fh, "<", "/dev/random";
580 read $fh, my $bytes, 20;
582 return unpack("H*", $bytes);
585 Also have a look at L<Crypt::Random> and the various openssl bindings - these
586 modules provide APIs for cryptographically secure random data.
590 See L<Catalyst/dump_these> - ammends the session data structure to the list of
591 dumped objects if session ID is defined.
595 =head1 USING SESSIONS DURING PREPARE
597 The earliest point in time at which you may use the session data is after
598 L<Catalyst::Plugin::Session>'s C<prepare_action> has finished.
600 State plugins must set $c->session ID before C<prepare_action>, and during
601 C<prepare_action> L<Catalyst::Plugin::Session> will actually load the data from
607 # don't touch $c->session yet!
609 $c->NEXT::prepare_action( @_ );
611 $c->session; # this is OK
612 $c->sessionid; # this is also OK
617 $c->config->{session} = {
621 All configuation parameters are provided in a hash reference under the
622 C<session> key in the configuration hash.
628 The time-to-live of each session, expressed in seconds. Defaults to 7200 (two
633 When true, C<<$c->request->address>> will be checked at prepare time. If it is
634 not the same as the address that initiated the session, the session is deleted.
640 The hash reference returned by C<< $c->session >> contains several keys which
641 are automatically set:
647 A timestamp whose value is the last second when the session is still valid. If
648 a session is restored, and __expires is less than the current time, the session
653 The last time a session was saved. This is the value of
654 C<< $c->session->{__expires} - $c->config->session->{expires} >>.
658 The time when the session was first created.
662 The value of C<< $c->request->address >> at the time the session was created.
663 This value is only populated if C<verify_address> is true in the configuration.
669 =head2 Round the Robin Proxies
671 C<verify_address> could make your site inaccessible to users who are behind
672 load balanced proxies. Some ISPs may give a different IP to each request by the
673 same client due to this type of proxying. If addresses are verified these
674 users' sessions cannot persist.
676 To let these users access your site you can either disable address verification
677 as a whole, or provide a checkbox in the login dialog that tells the server
678 that it's OK for the address of the client to change. When the server sees that
679 this box is checked it should delete the C<__address> sepcial key from the
680 session hash when the hash is first created.
682 =head2 Race Conditions
684 In this day and age where cleaning detergents and dutch football (not the
685 american kind) teams roam the plains in great numbers, requests may happen
686 simultaneously. This means that there is some risk of session data being
687 overwritten, like this:
693 request a starts, request b starts, with the same session id
697 session data is loaded in request a
701 session data is loaded in request b
705 session data is changed in request a
709 request a finishes, session data is updated and written to store
713 request b finishes, session data is updated and written to store, overwriting
718 If this is a concern in your application, a soon to be developed locking
719 solution is the only safe way to go. This will have a bigger overhead.
721 For applications where any given user is only making one request at a time this
722 plugin should be safe enough.
730 =item Christian Hansen
732 =item Yuval Kogman, C<nothingmuch@woobling.org> (current maintainer)
734 =item Sebastian Riedel
738 And countless other contributers from #catalyst. Thanks guys!
740 =head1 COPYRIGHT & LICENSE
742 Copyright (c) 2005 the aforementioned authors. All rights
743 reserved. This program is free software; you can redistribute
744 it and/or modify it under the same terms as Perl itself.